Class: WhirledPeas::Animator::Easing

Inherits:
Object
  • Object
show all
Defined in:
lib/whirled_peas/animator/easing.rb

Constant Summary collapse

EASING =

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

{
  bezier: proc do |value|
    value /= 2
    2 * value * value * (3 - 2 * value)
  end,
  linear: proc { |value| value },
  parametric: proc do |value|
    value /= 2
    squared = value * value
    2 * squared / (2 * (squared - value) + 1)
  end,
  quadratic: proc do |value|
    value * value
  end
}
EFFECTS =
%i[in out in_out]
INVERSE_MAX_ITERATIONS =
10
INVERSE_DELTA =
0.0001
INVERSE_EPSILON =
0.00001

Instance Method Summary collapse

Constructor Details

#initialize(easing = :linear, effect = :in_out) ⇒ Easing

Returns a new instance of Easing.



28
29
30
31
32
33
34
35
36
37
38
39
# 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

Instance Method Details

#ease(value) ⇒ Object



41
42
43
44
45
46
47
48
49
50
# 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) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# 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