Play

Clocks and the stage

A clock tells players what time it is: the page's frames, or a clock you step yourself. A stage lets several players draw on the same artwork, so what they move on one shape adds up.

Where time comes from

A player has no clock of its own. It asks its clock for the time now, and to be called on every frame while it plays. Hand it one as createPlayer(program, drawer, { clock }). Without one, it plays on the page's frame clock.

frameClock() is that clock: one requestAnimationFrame loop, shared by every player playing on it. The first call makes it, and every later call returns the same one. It sleeps while nothing ticks on it.

Its now() is performance.now() in seconds, read when asked rather than at the last frame. So a player started between two frames starts at that very moment.

Clock
now()number
Seconds, from whatever zero the clock keeps.
onFrame(tick)() => void
Calls tick(now) on every frame, with that frame's time in seconds. Returns what stops it.
tickingnumber
How many are ticking on it right now (0: it sleeps). A player never asks for it. Read only.

A clock you step yourself

manualClock(start?) makes a clock at start seconds that moves only when told. advance(seconds) moves it on and ticks every player on it once, at the new time. Use it in tests, or to render video one frame at a time.

Each button below makes one call. Pause the player and clock.ticking drops to 0. Advance then: the clock moves on, and the player stays where it is. The clock keeps counting, while player.time starts over every lap. Each frame drawn leaves a dot, so the eases show in the spacing.

clock.now()
player.time
clock.ticking
import { compile, createPlayer, manualClock, svgDrawer } from "@stew/engine";
import clip from "./clip.json";

const program = compile(clip);
const drawer = svgDrawer(program, uid =>
  [...svg.querySelectorAll(`[data-stew-uid="${uid}"]`)]);

// a clock that moves only when told, from 0 seconds
const clock = manualClock();
const player = createPlayer(program, drawer, { clock, repeat: -1 });
player.play(); // ticking on the clock, still at 0

// the buttons
clock.advance(1 / 30);
clock.advance(0.25);
player.pause();
player.play();

// the readouts
console.log(clock.now(), player.time, clock.ticking);
The clocks
frameClock()Clock
The page's shared frame clock, made on first use.
manualClock(start?)ManualClockdefault start 0
A clock at start seconds that moves only when told.
clock.advance(seconds)
A manual clock's own call: moves it on seconds and ticks everyone on it once, at the new time.

One shape, several players

Hand the same stage to each player's svgDrawer, and the players that draw one element keep one transform on it. Their moves add up, their turns and skews add up, and their stretches multiply. An element is written only when its matrix changed.

Each turn, skew and stretch happens about the pivot of the last player turning, skewing or stretching it. Below, the hop lifts the friend and squashes it on its bottom edge, while the flip turns it about its middle. A move is never turned by the turn: it runs along the axes the element sits in.

Turn a player off and it is destroyed: it lets go, and the friend shows what the other still writes. With both off, the friend has no transform at all, just as it was drawn.

0.0 / 1.2 s
transform
hopper
flipper
import { compile, createPlayer, createStage, svgDrawer, type StewDoc } from "@stew/engine";
import hop from "./hop.json";
import flip from "./flip.json";

// one stage: what its players move on one shape adds up
const stage = createStage();
const nodes = (uid: string) =>
  [...svg.querySelectorAll(`[data-stew-uid="${uid}"]`)];

function play(clip: StewDoc) {
  const program = compile(clip);
  const drawer = svgDrawer(program, nodes, stage);
  const player = createPlayer(program, drawer, { repeat: -1 });
  player.play();
  return player;
}

// both draw the element with data-stew-uid="friend"
const hopper = play(hop);
const flipper = play(flip);

Clips and ranks

By default, everything on a stage adds up. A drawer made with { exclusive: true } draws a clip: of the clips on one element, each part (move x, move y, turn, skew, stretch) comes from the clip that drew it last. Anything that isn't a clip still adds to it.

For a pivot, the last player is the one that joined the stage last, and one still at rest has no say. Of the clips, only the one that drew a part last counts for that part's pivot. ranked(stage, rank) changes the order: players drawn through it come after every player ranked lower, however late those joined, so their pivot wins.

cursorAction and dragAction write through a stage too. They aren't clips, so a pointer action and a clip that move one layer add up.

svgDrawer(program, bind, stage?, options?)Drawerdefault stage createStage()
Draws a program on the SVG through stage. Without one, it makes a stage of its own.
options.exclusivebooleandefault false
A clip's drawer: on each element, the parts it drew last win over other clips'.
ranked(stage, rank)Stage
The same stage for players of rank: each one comes after every player ranked lower.

Letting go

The stage remembers how every attribute looked before its players first wrote it. When a player is destroyed, its drawer's restore() runs: what it wrote gives way to what the others still write, and once the last one lets go, the element looks exactly as it did before the first frame.

Only what the engine wrote goes back. A value someone else wrote since, like an edit made while it played, is theirs and stays.

A transform is written as the element's transform attribute. The artwork's own <svg> is moved through its inline style instead, since WebKit ignores the attribute there. Its pivot is a share of its box (0 to 1), written as its transform-origin, and one pivot serves its turn, skew and stretch: the turn's pivot while it turns, else the stretch's, else the skew's.

Stage
createStage()Stage
A stage for players to share.
savedSaved
The first look of every attribute written on the stage. Read only.
join(node, moves, exclusive?, rank?)numberdefault exclusive false, rank 0
Each drawer joins one source per element it moves. A source writes node's transform, moving the parts moves names (the MOVES_* flags). Returns its handle, the s below.
set(s, parts, at)
Sets source s's parts: SOURCE_PARTS numbers of parts from at (move x, y, turn, skew x, y, stretch x, y, pivot x, y, and a keyed pivot's shift x, y).
leave(s)
Source s stops writing: its node shows the others, or looks as it was once none is left.
flush()
Writes every node a source changed since the last flush.
NextPointer actions