class WhirledPeas::Animator::Easing

Constants

EASING

Implementations of available ease-in functions. Ease-out and ease-in-out can all be derived from ease-in.

EFFECTS
INVERSE_DELTA
INVERSE_EPSILON
INVERSE_MAX_ITERATIONS

Attributes

easing[R]
effect[R]

Public Class Methods

new(easing=:linear, effect=:in_out) click to toggle source
# File lib/whirled_peas/animator/easing.rb, line 28
def initialize(easing=:linear, effect=:in_out)
  unless EASING.key?(easing)
    raise ArgumentError,
          "Invalid easing function: #{easing}, expecting one of #{EASING.keys.join(', ')}"
  end
  unless EFFECTS.include?(effect)
    raise ArgumentError,
          "Invalid effect: #{effect}, expecting one of #{EFFECTS.join(', ')}"
  end
  @easing = easing
  @effect = effect
end

Public Instance Methods

ease(value) click to toggle source
# File lib/whirled_peas/animator/easing.rb, line 41
def ease(value)
  case effect
  when :in
    ease_in(value)
  when :out
    ease_out(value)
  else
    ease_in_out(value)
  end
end
invert(target) click to toggle source
# File lib/whirled_peas/animator/easing.rb, line 52
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 Instance Methods

ease_in(value) click to toggle source

The procs in EASING define the ease-in functions, so we simply need to invoke the function with the given normalized value

# File lib/whirled_peas/animator/easing.rb, line 108
def ease_in(value)
  EASING[easing].call(value)
end
ease_in_out(value) click to toggle source

Ease in/ease out

@see www.youtube.com/watch?v=5WPbqYoz9HA

# File lib/whirled_peas/animator/easing.rb, line 120
def ease_in_out(value)
  if value < 0.5
    ease_in(value * 2) / 2
  else
    0.5 + ease_out(2 * value - 1) / 2
  end
end
ease_out(value) click to toggle source

The ease-out function will be the ease-in function rotated 180 degrees.

# File lib/whirled_peas/animator/easing.rb, line 113
def ease_out(value)
  1 - EASING[easing].call(1 - value)
end