Skip to content
pixi-silk
GitHub repository
Interface

Interaction and motion

Pointer events test the painted shape, so rings are hollow and lines react only on their stroke. damp, Spring and ease move things the same at any refresh rate.

Updated pixi-silk 1.0.1View as Markdown

A SilkGraphics answers pointer events with the shape it paints, not its bounding box. The motion helpers in the root import take the frame time, so an animation runs at the same speed on a 60 Hz and a 144 Hz screen.

React to the pointer#

Hover the shapes. Try the ring's hole and the space around the line.Open in Studio

Set eventMode and add listeners, as with any Pixi display object:

const button = new SilkGraphics();

button.eventMode = 'static';
button.cursor = 'pointer';
button.on('pointertap', () => console.log('tap'));
button.roundRect(0, 0, 160, 48, 24).fill(0x0a84ff);

The tap fires inside the rounded rect only, not in the corners its radius cuts away.

containsPoint() checks the object’s bounds first. Then it runs the shader’s distance functions on the CPU for the primitives under the pointer, last drawn first. A point is inside when its distance to the painted region is negative. So a stroked circle is a ring with no centre, a line counts only within its stroke width, and a blurred paint counts only up to its geometric edge.

You can call it yourself:

if (g.containsPoint(g.toLocal(pointerGlobal))) {
    // hovered
}

In a large interactive scene, split independent targets into separate SilkGraphics objects. Each one then tests only its own primitives. For drags, taps and long presses on any container, see trackGesture in pixi-silk/ui/gesture.

Move things at any refresh rate#

Drag the knob and let go. Change stiffness and damping.

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 in seconds instead:

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

x = damp(x, target, 10, dt);             // exponential approach, lambda is about 1 / seconds
angle = dampAngle(angle, target, 8, dt); // the 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); // also inOutCubic, inOutSine, outExpo

damp closes a fixed share of the gap per second, whatever the frame rate. Spring.step() integrates in fixed sub-steps, so it stays stable when a frame takes long. spring.settled tells you when to stop redrawing.

HelperArgumentsUse it for
dampcurrent, target, lambda, dtA value that follows a target without overshoot
dampAnglecurrent, target, lambda, dtAn angle, the short way round
Springvalue (0), stiffness (170), damping (26)Motion with overshoot and a settle
easet from 0 to 1Timed tweens

Roll a number with an odometer#

Tap to add an amount. Tap again while it counts to skip to the end.Open in Studio
import { Odometer } from 'pixi-silk/ui/odometer';

const total = new Odometer({ value: 1250, decimals: 2, prefix: '$', fontSize: 44 });

total.set(9800); // counts up, longer for bigger jumps
total.skip(); // straight to the end
total.state.counting.subscribe((on) => console.log(on ? 'counting' : 'done'));

Every digit column rolls on its own. A column moves only while the one below it passes from 9 to 0, so carries ripple like a mechanical counter. Glyphs come from a bitmap font made from the text style, so a frame only moves them, and one mask clips them all. format(value) returns the same text without rolling, for labels and screen readers.

OptionDefaultWhat it does
value0Start value
decimals0Digits after the point
prefix, suffixnoneText around the number, such as a currency
thousands, point, and .Separators. '' turns thousands off
minDigits1Smallest number of whole digits
fontSize, color, textStyle32, whiteThe look of the digits
duration0.9Count-up time in seconds for a small change
speedUptrueBigger jumps count for longer, up to three times
easingoutCubicEasing of the count
onDonenoneCalled with the value when the count ends

Redraw every frame#

The simplest animation clears and redraws in the ticker. clear() keeps the GPU buffers, so a redraw only uploads the new primitive data.

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 the parts that do not move in a second SilkGraphics and draw it once. Performance has the numbers for large scenes.

Slow shapes glide between pixels instead of snapping, because silk renders coordinates as given and createSilkApp turns pixel rounding off. The sub-pixel experiment shows it.

API reference: SilkGraphics, damp, dampAngle, Spring, ease, Odometer