Animation Blending

Smooth transitions between states

Play Walk. Trigger Jump. The character's legs smoothly transition from walking to jumping — no pop, no glitch. Mock-based blending makes it work.


Mock objects

Animations don't write directly to nodes. They evaluate into mocks — preallocated property buffers.

Mock = {
  x, y, rotation, width, height, opacity
  // only properties that animations target
}

Two mocks per bone. Animation A fills mock A. Animation B fills mock B. The system blends between them based on weight, then applies the result to the real node.

Why mocks matter

  • No corruption — animations never touch real node data until the final apply step
  • Blending — two animations can run simultaneously, blended by weight
  • State capture — mocks capture the pose without modifying the scene
  • Zero allocation — mocks are pooled and reused every frame

Blend weight

A single number (0 to 1) controls the mix:

WeightResult
0.0100% animation A
0.370% A, 30% B
0.5Equal mix
1.0100% animation B

During a transition, weight ramps from 0 to 1 over the blend duration. The ramp is smooth — no visible seam.


Two mixing modes

Freeze-blend

Source animation freezes at its current pose. Over the blend duration, lerp from frozen snapshot to target animation's live output.

Walk freezes mid-stride → Jump advances
Legs stop, morph into jump pose over 0.3 seconds

Use for: abrupt transitions, action triggers (attack, jump), state changes where the source should stop.

Live-blend

Source animation keeps playing during the blend. Both evaluate every frame, weight shifts gradually.

Walk keeps playing → Jump advances
Legs keep walking but stride shortens as Jump weight increases

Use for: smooth locomotion transitions (walk → run), looping animations that shouldn't freeze.

Source speed control

Fine-tune live blending with sourceSpeed:

playAnimation('jump', {
duration: 0.3,
mode: 'live',
sourceSpeed: 0.5 // Walk decelerates during blend
})

sourceSpeed: 1 = source plays at full speed. sourceSpeed: 0.5 = half speed. sourceSpeed: 0 = effectively freeze mode.


Per-frame evaluation flow

for each animated node:
1. Initialize mockA and mockB from real node state
2. Evaluate clipA into mockA (if active)
3. Evaluate clipB into mockB (if active)
4. Blend: result = mockA * (1-w) + mockB * w
5. Apply result to real node
6. Recompute transform

Initializing mocks from the real node each frame ensures unanimated properties pass through. If Walk animates legs but not opacity, and game logic set opacity to 0, the mock inherits opacity=0.


State capture

When an animation starts, the current node state is captured. This snapshot becomes the starting point for the transition.

1. Character is at position (100, 200) with rotation 15
2. playAnimation('Jump') called
3. Snapshot: { x: 100, y: 200, rotation: 15 }
4. Blend from snapshot → Jump's first keyframe

If the character was mid-Walk when Jump triggers, the snapshot captures the mid-stride pose. The transition blends from that exact pose — not from Walk's start.

Chained transitions

Walk → Jump → Land. When Land starts, it snapshots the mid-blend pose between Walk and Jump. Three-way resolution happens naturally:

t=0: Walk playing
t=1: Jump triggered → snapshot Walk's current pose → blend Walk → Jump
t=1.5: Land triggered → snapshot current blend pose → blend blend → Land

No special three-animation code. Each transition only knows about its source (snapshot) and target (new clip).


title: "Blend between oscillations"
description: "Two motion patterns that blend smoothly"
nodes:
  - type: ELLIPSE, name: "Ball", x: 200, y: 200, w: 40, h: 40, fill: "#E74C3C"
formulas:
  - node: "Ball", prop: "x", value: "200 + Math.sin(time * 2) * 80"
  - node: "Ball", prop: "y", value: "200 + Math.cos(time * 3) * 60"
tools_visible: [select, play]
ui_visible: [properties, formulas]

Angle interpolation

Rotation blending uses lerpAngle — the shortest path around the circle. Blending from 350 to 10 goes through 0, not through 180.

lerpAngle(350, 10, 0.5) = 0 // correct: shortest path
lerp(350, 10, 0.5) = 180 // wrong: long way around


Back to animationIK solversTimeline

← НазадInverse KinematicsДальше →Animation