Skip to content
GitHub repository
Building UI

Animation

Redraw SilkGraphics cheaply every frame. Animate with damp, dampAngle, Spring and ease so motion feels the same at 60, 120 or 144 Hz.

Updated pixi-silk 0.1.0

Source src/demos/spring.demo.ts
import { Container } from 'pixi.js';
import { damp, SilkGraphics, Spring } from 'pixi-silk';
import { defineDemo } from './runtime';

// Frame-rate independent motion: the knob glides the same at 60, 120 or 144 Hz.
export default defineDemo({
    size: [480, 200],
    touchAction: 'none',
    controls: {
        stiffness: { type: 'range', label: 'stiffness', min: 40, max: 400, step: 1, value: 170 },
        damping: { type: 'range', label: 'damping', min: 4, max: 40, step: 1, value: 14 },
    },
    setup({ stage, params, tick }) {
        const g = new SilkGraphics();
        const hit = new Container();
        const x = new Spring(120);
        let glow = 0;
        let dragging = false;

        hit.eventMode = 'static';
        hit.hitArea = { contains: (px: number, py: number) => px >= 0 && px <= 480 && py >= 0 && py <= 200 };
        hit.on('pointerdown', (e) => {
            dragging = true;
            x.target = Math.min(440, Math.max(40, e.getLocalPosition(stage).x));
        });
        hit.on('globalpointermove', (e) => {
            if (dragging) x.target = Math.min(440, Math.max(40, e.getLocalPosition(stage).x));
        });
        hit.on('pointerup', () => (dragging = false));
        hit.on('pointerupoutside', () => (dragging = false));
        stage.addChild(g, hit);
        tick((_t, dt) => {
            x.stiffness = params.stiffness;
            x.damping = params.damping;
            x.step(dt);
            glow = damp(glow, dragging ? 1 : 0, 10, dt);
            g.clear();
            g.pill(40, 94, 400, 12).fill(0x2c2c2e);
            g.pill(40, 94, Math.max(12, x.value - 40), 12).fill(0x0a84ff);
            g.circle(x.value, 100, 22 + glow * 6).fill({ color: 0x0a84ff, alpha: 0.35 * glow, blur: 10 });
            g.circle(x.value, 100, 20).fill(0xffffff);
        });
    },
});
Drag the knob. The spring settles identically at any refresh rate.

Immediate mode is cheap#

The simplest way to animate is to clear and redraw every frame. clear() keeps the GPU buffers. Each primitive is 40 floats, and one instanced draw call renders them all:

app.ticker.add((ticker) => {
    const t = ticker.lastTime / 1000;

    g.clear();
    g.arcSweep(80, 80, 44, -Math.PI / 2, Math.PI * 2 * (0.5 + 0.4 * Math.sin(t))).stroke({ width: 12, cap: 'round', color: 0x30d158 });
});

Put static parts in a second SilkGraphics and draw it once.

Frame-rate independent motion#

Easing by a fixed fraction per frame (x += (target - x) * 0.1) runs twice as fast on a 120 Hz display. The helpers take the frame time instead:

import { damp, dampAngle, Spring, ease } from 'pixi-silk';

x = damp(x, target, 10, dt);              // exponential approach, lambda is about 1 / seconds
angle = dampAngle(angle, target, 8, dt);  // shortest way around the circle

const spring = new Spring(0, 170, 26);    // value, stiffness, damping
spring.target = 1;
app.ticker.add((t) => draw(spring.step(t.deltaMS / 1000)));

ease.outCubic(0.5); // inOutCubic, outCubic, inOutSine, outExpo

Spring.step() integrates with fixed sub-steps, so it stays stable with large frame times. spring.settled tells you when to stop redrawing.

Sub-pixel motion#

Slow-moving shapes glide instead of snapping from pixel to pixel. Silk renders coordinates exactly as given, and createSilkApp turns off pixel rounding. The sub-pixel lab page compares Silk with tessellated graphics.

API reference: damp, dampAngle, Spring, ease