Skip to content

Animations

Ripl provides two approaches to animation: manual transitions using the standalone transition function, and renderer-based transitions using renderer.transition(). Both are promise-based and support easing, keyframes, and custom interpolators.

Manual Transitions

The transition function runs a timed animation loop using requestAnimationFrame. It's useful when you don't have a scene/renderer setup — for example, animating a single element rendered directly to a context.

ts
import {
    easeOutCubic,
    transition,
} from '@ripl/core';

await transition({
    duration: 1000,
    ease: easeOutCubic,
}, (t) => {
    // t goes from 0 to 1 over the duration
    circle.radius = 50 + t * 50;
    context.clear();
    circle.render(context);
});

Transition Options

OptionTypeDefaultDescription
durationnumber0Duration in milliseconds
easeEaseeaseLinearEasing function
loopbooleanfalseLoop the transition indefinitely

Cancelling a Transition

The transition function returns a Transition object (which extends Task). You can cancel it:

ts
const t = transition({ duration: 2000 }, (t) => {
    circle.radius = 50 + t * 50;
});

// Cancel after 500ms
setTimeout(() => t.cancel(), 500);

Renderer Transitions

When working with a scene and renderer, use renderer.transition() for a higher-level API that handles interpolation, re-rendering, and multi-element animations automatically.

ts
import {
    createRenderer,
    createScene,
    easeOutCubic,
} from '@ripl/core';

const scene = createScene('.container', { children: [circle] });
const renderer = createRenderer(scene);

await renderer.transition(circle, {
    duration: 1000,
    ease: easeOutCubic,
    state: {
        radius: 100,
        fillStyle: '#ff006e',
    },
});

The renderer automatically:

  • Interpolates each property using the appropriate interpolator
  • Re-renders the scene each frame
  • Starts/stops the render loop as needed (with autoStart/autoStop)

Easing Functions

Easing functions control the rate of change over time. Ripl provides 13 built-in easing functions:

FunctionDescription
easeLinearConstant speed (no easing)
easeInQuadAccelerate from zero
easeOutQuadDecelerate to zero
easeInOutQuadAccelerate then decelerate
easeInCubicStronger acceleration
easeOutCubicStronger deceleration
easeInOutCubicStronger ease in/out
easeInQuartEven stronger acceleration
easeOutQuartEven stronger deceleration
easeInOutQuartEven stronger ease in/out
easeInQuintStrongest acceleration
easeOutQuintStrongest deceleration
easeInOutQuintStrongest ease in/out
ts
import {
    easeOutCubic,
} from '@ripl/core';

await renderer.transition(circle, {
    duration: 800,
    ease: easeOutCubic,
    state: { radius: 100 },
});

Custom Easing

An easing function takes a value from 0–1 and returns a transformed value:

ts
// Bounce easing
const easeBounce = (t: number) => {
    const n1 = 7.5625;
    const d1 = 2.75;
    if (t < 1 / d1) return n1 * t * t;
    if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
    if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
    return n1 * (t -= 2.625 / d1) * t + 0.984375;
};

await renderer.transition(circle, {
    duration: 1000,
    ease: easeBounce,
    state: { cy: 250 },
});

Chaining Animations

Transitions are awaitable, so you can chain them sequentially:

ts
async function animate() {
    await renderer.transition(circle, {
        duration: 500,
        ease: easeOutCubic,
        state: { radius: 100,
            fillStyle: '#ff006e' },
    });

    await renderer.transition(circle, {
        duration: 500,
        ease: easeInOutQuad,
        state: { radius: 50,
            fillStyle: '#3a86ff' },
    });
}

Parallel Animations

Run multiple transitions simultaneously with Promise.all:

ts
await Promise.all([
    renderer.transition(circle, {
        duration: 800,
        ease: easeOutCubic,
        state: { cx: 300 },
    }),
    renderer.transition(rect, {
        duration: 800,
        ease: easeOutCubic,
        state: { x: 100 },
    }),
]);

Keyframe Animations

Transitions support CSS-like keyframe arrays for multi-step animations within a single transition.

Implicit Offsets

Values are evenly distributed across the duration:

ts
await renderer.transition(circle, {
    duration: 2000,
    state: {
        fillStyle: [
            '#3a86ff', // offset 0.33
            '#ff006e', // offset 0.66
            '#8338ec', // offset 1.0
        ],
    },
});

Explicit Offsets

Specify exact positions for each keyframe:

ts
await renderer.transition(circle, {
    duration: 2000,
    state: {
        fillStyle: [
            { value: '#ff006e',
                offset: 0.25 },
            { value: '#8338ec',
                offset: 0.5 },
            { value: '#3a86ff',
                offset: 1.0 },
        ],
    },
});

Custom Interpolator Functions

Pass a function instead of a target value for full control over the interpolation:

ts
await renderer.transition(circle, {
    duration: 1000,
    state: {
        // t goes from 0 to 1 (after easing)
        radius: t => 50 + Math.sin(t * Math.PI * 4) * 20,
    },
});

Demo