lib/whirled_peas/animator/easing.rb in whirled_peas-0.11.1 vs lib/whirled_peas/animator/easing.rb in whirled_peas-0.12.0

- old
+ new

@@ -19,10 +19,14 @@ end } EFFECTS = %i[in out in_out] + INVERSE_MAX_ITERATIONS = 10 + INVERSE_DELTA = 0.0001 + INVERSE_EPSILON = 0.00001 + def initialize(easing=:linear, effect=:in_out) unless EASING.key?(easing) raise ArgumentError, "Invalid easing function: #{easing}, expecting one of #{EASING.keys.join(', ')}" end @@ -43,10 +47,60 @@ else ease_in_out(value) end end + def invert(target) + ease_fn = case effect + when :in + proc { |v| ease_in(v) } + when :out + proc { |v| ease_out(v) } + else + proc { |v| ease_in_out(v) } + end + + # Use Newton's method(!!) to find the inverse values of the easing function for the + # specified target. Make an initial guess that is equal to the target and see how + # far off the eased value is from the target. If we are close enough (as dictated by + # INVERSE_EPSILON constant), then we return our guess. If we aren't close enough, then + # find the slope of the eased line (approximated with a small step of INVERSE_DELTA + # along the x-axis). The intersection of the slope and target will give us the value + # of our next guess. + # + # Since most easing functions only vary slightly from the identity line (y = x), we + # can typically get the eased guess within epsilon of the target in a few iterations, + # however only iterate at most INVERSE_MAX_ITERATIONS times. + # + # ┃ ...... + # ┃ ... + # target -┃------------+ ... + # ┃ /.|.. + # ┃ ../. | + # ┃ ... | | + # ┃... | | + # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # | | + # guess next guess + # + # IMPORTANT: This method only works well for monotonic easing functions + + # Pick the target as the first guess. For targets of 0 and 1 (and 0.5 for ease_in_out), + # this guess will be the exact value that yields the target. For other values, the + # eased guess will generally be close to the target. + guess = target + INVERSE_MAX_ITERATIONS.times do |i| + eased_guess = ease_fn.call(guess) + error = eased_guess - target + break if error.abs < INVERSE_EPSILON + next_eased_guess = ease_fn.call(guess + INVERSE_DELTA) + delta_eased = next_eased_guess - eased_guess + guess -= INVERSE_DELTA * error / delta_eased + end + guess + end + private attr_reader :easing, :effect # The procs in EASING define the ease-in functions, so we simply need @@ -58,9 +112,12 @@ # The ease-out function will be the ease-in function rotated 180 degrees. def ease_out(value) 1 - EASING[easing].call(1 - value) end + # Ease in/ease out + # + # @see https://www.youtube.com/watch?v=5WPbqYoz9HA def ease_in_out(value) if value < 0.5 ease_in(value * 2) / 2 else 0.5 + ease_out(2 * value - 1) / 2