Class: Plushie::Animation

Inherits:
Object
  • Object
show all
Defined in:
lib/plushie/animation.rb

Overview

Server-side animation interpolation and easing functions.

Pure functions operating on structs -- no threads, no state management beyond what lives in your app model. The host computes interpolated values on each animation frame tick.

== Easing functions

All easing functions take a +t+ value in 0.0..1.0 and return a curved +t+ value. Available easings:

  • +linear+ -- identity
  • +ease_in+ -- cubic ease in
  • +ease_out+ -- cubic ease out
  • +ease_in_out+ -- cubic ease in-out
  • +ease_in_quad+ -- quadratic ease in
  • +ease_out_quad+ -- quadratic ease out
  • +ease_in_out_quad+ -- quadratic ease in-out
  • +spring+ -- spring with overshoot

== Interpolation

+interpolate+ lerps between two numbers with an optional easing function applied to +t+.

== Animation struct

The Animation tracks a single animated value over time. Create one with +new+, start it with +start+, and advance it on each frame with +advance+.

Examples:

anim = Plushie::Animation.new(0.0, 1.0, 300, easing: :ease_out)
anim = Plushie::Animation.start(anim, timestamp)
value, anim = Plushie::Animation.advance(anim, next_timestamp)

Defined Under Namespace

Classes: State

Constant Summary collapse

EASINGS =

-- Easing functions ---------------------------------------------------

{
  linear: ->(t) { t },

  ease_in: ->(t) { t * t * t },

  ease_out: ->(t) {
    inv = 1.0 - t
    1.0 - inv * inv * inv
  },

  ease_in_out: ->(t) {
    if t < 0.5
      4.0 * t * t * t
    else
      inv = -2.0 * t + 2.0
      1.0 - inv * inv * inv / 2.0
    end
  },

  ease_in_quad: ->(t) { t * t },

  ease_out_quad: ->(t) { 1.0 - (1.0 - t) * (1.0 - t) },

  ease_in_out_quad: ->(t) {
    if t < 0.5
      2.0 * t * t
    else
      1.0 - (-2.0 * t + 2.0)**2 / 2.0
    end
  },

  spring: ->(t) {
    if t <= 0.0
      0.0
    elsif t >= 1.0
      1.0
    else
      c4 = 2.0 * Math::PI / 3.0
      2.0**(-10.0 * t) * Math.sin((t * 10.0 - 0.75) * c4) + 1.0
    end
  }
}.freeze

Class Method Summary collapse

Class Method Details

.advance(anim, timestamp) ⇒ Array(Numeric, State), Array(Numeric, Symbol)

Advance the animation to the given frame timestamp.

Returns +[current_value, updated_animation]+ while the animation is in progress, or +[final_value, :finished]+ when it completes.

If the animation has not been started yet, returns +[from, animation]+ unchanged.

Parameters:

  • anim (State)
  • timestamp (Integer)

Returns:

  • (Array(Numeric, State), Array(Numeric, Symbol))


194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/plushie/animation.rb', line 194

def self.advance(anim, timestamp)
  return [anim.value, anim] if anim.started_at.nil?

  elapsed = timestamp - anim.started_at
  t = clamp(elapsed.to_f / anim.duration)
  current = interpolate(anim.from, anim.to, t, anim.easing)

  if t >= 1.0
    [anim.to, :finished]
  else
    [current, anim.with(value: current)]
  end
end

.ease_in(t) ⇒ Float

Cubic ease in. Starts slow, accelerates.

Parameters:

  • t (Float)

Returns:

  • (Float)


99
# File 'lib/plushie/animation.rb', line 99

def self.ease_in(t) = EASINGS[:ease_in].call(t)

.ease_in_out(t) ⇒ Float

Cubic ease in-out. Slow start, fast middle, slow end.

Parameters:

  • t (Float)

Returns:

  • (Float)


109
# File 'lib/plushie/animation.rb', line 109

def self.ease_in_out(t) = EASINGS[:ease_in_out].call(t)

.ease_in_out_quad(t) ⇒ Float

Quadratic ease in-out. Slow start and end, fast middle.

Parameters:

  • t (Float)

Returns:

  • (Float)


124
# File 'lib/plushie/animation.rb', line 124

def self.ease_in_out_quad(t) = EASINGS[:ease_in_out_quad].call(t)

.ease_in_quad(t) ⇒ Float

Quadratic ease in. Starts slow, accelerates.

Parameters:

  • t (Float)

Returns:

  • (Float)


114
# File 'lib/plushie/animation.rb', line 114

def self.ease_in_quad(t) = EASINGS[:ease_in_quad].call(t)

.ease_out(t) ⇒ Float

Cubic ease out. Starts fast, decelerates.

Parameters:

  • t (Float)

Returns:

  • (Float)


104
# File 'lib/plushie/animation.rb', line 104

def self.ease_out(t) = EASINGS[:ease_out].call(t)

.ease_out_quad(t) ⇒ Float

Quadratic ease out. Starts fast, decelerates.

Parameters:

  • t (Float)

Returns:

  • (Float)


119
# File 'lib/plushie/animation.rb', line 119

def self.ease_out_quad(t) = EASINGS[:ease_out_quad].call(t)

.finished?(anim) ⇒ Boolean

Returns true if the animation has run to completion.

Note: once +advance+ returns +[value, :finished]+, the animation struct is no longer updated. Use the +:finished+ return value from +advance+ as the primary completion signal.

Parameters:

Returns:

  • (Boolean)


216
217
218
219
# File 'lib/plushie/animation.rb', line 216

def self.finished?(anim)
  return false if anim.started_at.nil?
  anim.value == anim.to
end

.interpolate(from, to, t, easing = :linear) ⇒ Float

Linearly interpolate between +from+ and +to+ at progress +t+, with an optional easing function applied to +t+ first.

+t+ is clamped to 0.0..1.0 before easing is applied.

Parameters:

  • from (Numeric)

    start value

  • to (Numeric)

    end value

  • t (Numeric)

    progress (0.0 to 1.0)

  • easing (Proc, Symbol) (defaults to: :linear)

    easing function or symbol name

Returns:

  • (Float)


144
145
146
147
148
149
# File 'lib/plushie/animation.rb', line 144

def self.interpolate(from, to, t, easing = :linear)
  easing_fn = resolve_easing(easing)
  clamped = clamp(t)
  eased = easing_fn.call(clamped)
  from + (to - from) * eased
end

.linear(t) ⇒ Float

Linear easing (identity). Returns +t+ unchanged.

Parameters:

  • t (Float)

Returns:

  • (Float)


94
# File 'lib/plushie/animation.rb', line 94

def self.linear(t) = EASINGS[:linear].call(t)

.new(from, to, duration_ms, easing: :linear) ⇒ State

Create a new animation.

Parameters:

  • from (Numeric)

    start value

  • to (Numeric)

    end value

  • duration_ms (Integer)

    duration in milliseconds (must be > 0)

  • easing (Proc, Symbol) (defaults to: :linear)

    easing function or name (default: :linear)

Returns:

Raises:

  • (ArgumentError)


160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/plushie/animation.rb', line 160

def self.new(from, to, duration_ms, easing: :linear)
  raise ArgumentError, "duration_ms must be positive" unless duration_ms.is_a?(Integer) && duration_ms > 0

  State.new(
    from: from,
    to: to,
    duration: duration_ms,
    started_at: nil,
    easing: easing,
    value: from
  )
end

.spring(t) ⇒ Float

Spring easing with overshoot. Overshoots the target slightly before settling. Uses a single-period damped sine approximation.

Parameters:

  • t (Float)

Returns:

  • (Float)


130
# File 'lib/plushie/animation.rb', line 130

def self.spring(t) = EASINGS[:spring].call(t)

.start(anim, timestamp) ⇒ State

Start (or restart) the animation at the given frame timestamp. Resets the current value to +from+.

Parameters:

  • anim (State)
  • timestamp (Integer)

    frame timestamp in milliseconds

Returns:



179
180
181
# File 'lib/plushie/animation.rb', line 179

def self.start(anim, timestamp)
  anim.with(started_at: timestamp, value: anim.from)
end

.value(anim) ⇒ Numeric

Return the current interpolated value.

Parameters:

Returns:

  • (Numeric)


224
# File 'lib/plushie/animation.rb', line 224

def self.value(anim) = anim.value