Class: Plushie::Animation::Tween

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

Overview

SDK-side animation interpolation.

Pure functions operating on structs for manual, host-side animations. The host computes interpolated values on each animation frame tick.

For most use cases, prefer renderer-side descriptors (Transition, Spring, Sequence) which animate with zero wire traffic. Use Tween when you need host-side control over the interpolated value (e.g., driving non-visual state from an animation).

== 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

== Animation struct

The Tween 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::Tween.new(0.0, 1.0, 300, easing: :ease_out)
anim = Plushie::Animation::Tween.start(anim, timestamp)
value, anim = Plushie::Animation::Tween.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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Attribute Details

#auto_reverse [Boolean] swap from/to on each cycle([Boolean]) ⇒ Object (readonly)

Immutable animation state.



45
46
47
48
49
50
51
52
53
54
# File 'lib/plushie/animation/tween.rb', line 45

State = ::Data.define(:from, :to, :duration, :started_at, :easing, :value,
  :repeat, :auto_reverse) do
  include Plushie::Model::Extensions

  def initialize(from:, to:, duration:, started_at: nil, easing: :linear,
    value: nil, repeat: nil, auto_reverse: false)
    super(from: from, to: to, duration: duration, started_at: started_at,
          easing: easing, value: value.nil? ? from : value, repeat: repeat, auto_reverse: auto_reverse)
  end
end

#repeat [Integer, :forever, nil] remaining repeat count([Integer,: forever, nil]) ⇒ Object (readonly)

Immutable animation state.



45
46
47
48
49
50
51
52
53
54
# File 'lib/plushie/animation/tween.rb', line 45

State = ::Data.define(:from, :to, :duration, :started_at, :easing, :value,
  :repeat, :auto_reverse) do
  include Plushie::Model::Extensions

  def initialize(from:, to:, duration:, started_at: nil, easing: :linear,
    value: nil, repeat: nil, auto_reverse: false)
    super(from: from, to: to, duration: duration, started_at: started_at,
          easing: easing, value: value.nil? ? from : value, repeat: repeat, auto_reverse: auto_reverse)
  end
end

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.

Parameters:

  • anim (State)
  • timestamp (Integer)

Returns:

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


179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/plushie/animation/tween.rb', line 179

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
    handle_cycle_end(anim)
  else
    [current, anim.with(value: current)]
  end
end

.finished?(anim) ⇒ Boolean

Returns true if the animation has run to completion.

Parameters:

Returns:

  • (Boolean)


197
198
199
200
# File 'lib/plushie/animation/tween.rb', line 197

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.

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)


116
117
118
119
120
121
# File 'lib/plushie/animation/tween.rb', line 116

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

.looping(from, to, duration_ms, **opts) ⇒ State

Create a looping tween that repeats forever with auto-reverse.

Convenience for common back-and-forth animations (pulsing, breathing, oscillating).

Examples:

anim = Tween.looping(0.0, 1.0, 500, easing: :ease_in_out)

Parameters:

  • from (Numeric)
  • to (Numeric)
  • duration_ms (Integer)
  • opts (Hash)

    additional options (e.g. easing:)

Returns:



158
159
160
# File 'lib/plushie/animation/tween.rb', line 158

def self.looping(from, to, duration_ms, **opts)
  new(from, to, duration_ms, repeat: :forever, auto_reverse: true, **opts)
end

.new(from, to, duration_ms, easing: :linear, repeat: nil, auto_reverse: false) ⇒ 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)


132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/plushie/animation/tween.rb', line 132

def self.new(from, to, duration_ms, easing: :linear, repeat: nil, auto_reverse: false)
  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,
    easing: easing,
    repeat: repeat,
    auto_reverse: auto_reverse
  )
end

.start(anim, timestamp) ⇒ State

Start (or restart) the animation at the given frame timestamp.

Parameters:

  • anim (State)
  • timestamp (Integer)

    frame timestamp in milliseconds

Returns:



167
168
169
# File 'lib/plushie/animation/tween.rb', line 167

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)


205
# File 'lib/plushie/animation/tween.rb', line 205

def self.value(anim) = anim.value