Starts and rules
A start trigger says when a clip first plays. A rule plays a clip each time something happens on its elements, or a key is pressed.
Start triggers
startOn(player, trigger, host, scroller) arms a player with a start trigger. Until the trigger starts it, the clip rests on its first frame (with "scrollscrub", where the scroll has it). host hears the click or hover and is what scrolls into view. scroller is the box that scrolls it, or null for the page. It returns what stops it listening.
Pick a trigger, or press the startOn button, to arm the jump again. The log shows what the player tells its listeners.
import { compile, createPlayer, createStage, startOn, svgDrawer } from "@stew/engine";
import clip from "./clip.json";
// the clip, read once, drawn on the elements carrying each target's uid
const stage = createStage();
const program = compile(clip);
const drawer = svgDrawer(program, uid =>
[...svg.querySelectorAll(`[data-stew-uid="${uid}"]`)], stage);
const player = createPlayer(program, drawer);
// the log
for (const name of ["start", "complete", "reverseComplete"] as const) {
player.on(name, () => log(name));
}
// on its first frame until the trigger starts it:
// plays while the pointer is over svg; leaving winds it back
const stop = startOn(player, "hover", svg, null);"load"- Plays at once.
"click"- Every click on the host replays it from the start.
"hover"- Plays while the pointer is over the host. Leaving winds it back to the start from where it is, through the lap it is in only. A host already under the pointer when it is armed starts at once.
"inview"- Plays once, as soon as 30% of the host is in view (
IN_VIEW). It carries on when it is scrolled away. "scrollscrub"- The playhead follows the scroll. See below.
Scroll scrub
With "scrollscrub", the scroll sets the playhead: at the start while the host's top is 80% or more down the view, at the end once its bottom is 20% down, in a straight line between (scrubProgress times the clip's duration). It is measured from where the host is on screen, on every scroll and resize.
The playhead doesn't jump there. It glides from wherever it is over 1 second along expo.out, and each scroll or resize starts a new glide. When it is armed, it starts right where the scroll has it.
Scroll the page. The white marker is where the scroll puts the playhead, and the ink line is where the playhead is.
import { compile, createPlayer, createStage, scrubProgress, startOn, svgDrawer } from "@stew/engine";
import clip from "./clip.json";
// the clip, read once, drawn on the elements carrying each target's uid
const stage = createStage();
const program = compile(clip);
const drawer = svgDrawer(program, uid =>
[...svg.querySelectorAll(`[data-stew-uid="${uid}"]`)], stage);
const player = createPlayer(program, drawer);
// the playhead follows the scroll (null: the page scrolls svg)
const stop = startOn(player, "scrollscrub", svg, null);
// the marker: where the scroll puts the playhead, measured as startOn does
const aim = () => {
const r = svg.getBoundingClientRect();
moveMarker(scrubProgress(r.top, r.height, window.innerHeight));
};
aim();
window.addEventListener("scroll", aim, { passive: true });
window.addEventListener("resize", aim);
// where the playhead is: now (startOn's first seek tells no listener),
// then after every frame it draws
showTime(player.time);
player.on("update", () => showTime(player.time));scrubProgress(top, height, view)number- The progress, 0 to 1, of a host
heightpx tall whose top istoppx below the top of a viewviewpx tall. SCRUB_STARTnumber0.8: the start line, as a share of the view from its top.SCRUB_ENDnumber0.2: the end line.SCRUB_SECONDSnumber1: how long the playhead takes to catch up.SCRUB_EASEstring"expo.out": how it catches up.
Rules
ruleOn(player, params, targets, saved) plays a clip each time something happens on targets, or a key is pressed on the page. saved is where it claims what it lends (a hand cursor, a tabindex): an owner on the stage the clip draws on, stage.saved.owner(). It returns what stops it and puts those back.
Pick what the button listens for, what the end of a hold does, and whether a click or a key toggles. The printed params change with them.
import { compile, createPlayer, createStage, ruleOn, svgDrawer, type RuleParams } from "@stew/engine";
import clip from "./clip.json";
// the clip, read once, drawn on the elements carrying each target's uid
const stage = createStage();
const program = compile(clip);
const drawer = svgDrawer(program, uid =>
[...svg.querySelectorAll(`[data-stew-uid="${uid}"]`)], stage);
const player = createPlayer(program, drawer);
// its first frame, until the rule plays it
player.seek(0);
// the log
for (const name of ["start", "complete", "reverseComplete"] as const) {
player.on(name, () => log(name));
}
const rule: RuleParams = {
on: "hover",
keys: [],
toggle: false,
loop: false,
onEnd: "reverse",
whole: false,
};
// the button listens
const stop = ruleOn(player, rule, [button], stage.saved.owner(),
window, () => log("onLeave"));onRuleTriggerrequired- What plays it: see below.
keysreadonly string[]required- Key rules:
KeyboardEvent.keyvalues. Any of them plays it. togglebooleanrequired- Click, key and throw rules: play it open, then back from wherever it is.
loopbooleanrequired- Repeat while triggered. Set the player's
repeatto -1 as well, as Stew Factory does: winding back then goes through the lap it is in only. onEndRuleEndrequired- Hover, press, focus and grab rules: what the end of the hold does.
wholebooleanrequired- The whole component listens, so its clicks are not marked as a shape's own.
"click"- Replays it from the start. With
toggle, plays it open, then back. "key"- The same, for a key in
keyspressed on the page. "hover"- Plays while the pointer is on the targets.
"press"- Plays while a pointer is pressed on them, until that press ends or that pointer leaves them all.
"focus"- Plays while they have focus.
"grab"- Plays while a drag holds them: from
stew:grabtostew:release. "throw"- Replays it when a drag on them lets go faster than 300 px a second (
stew:throw). Withtoggle, plays it open, then back.
"reverse"- Plays back home from where it is.
"finish"- Runs on to the end. With
loop, it finishes the lap it is in, then stops. "hold"- Stops where it is.
What counts
A hold ends only when it is really left: its press ends, its drag lets go, or the pointer or focus moves to something outside all the rule's targets. Moving from one target to another is not leaving. Coming back in carries on, or starts over once the clip has finished.
A key plays once per press. A held key's repeats don't count, nor does typing into a form field or editable text, and a letter counts in either case.
Click and press rules put the hand cursor on their targets, and focus rules lend a tabindex to what can't take focus. Both are put back exactly when the rule stops. A button, link, label or form field keeps its own.
A click a shape's own rule answers is marked with CLIP_CLICK, never stopped: the page and other click rules still hear it, but a clip whose start trigger is "click" doesn't restart from it. Inside a <label>, one click is heard once, not again as the click the label hands to its control. Nothing is prevented either, so a drag on the same shape still hears the press.
Reference
Stew Factory's previews and its exports (@stew-engine/player) start scenes and play their rule clips with these same calls.
startOn(player, trigger, host, scroller, allowed?)() => void- Arms
playerwith a start trigger, on its first frame until it starts (a scroll scrub, where the scroll has it). Returns what stops it listening. ruleOn(player, params, targets, saved, keysOn?, onLeave?, allowed?)() => void- Plays
playeron an event. Returns what stops it and puts back the cursor andtabindexit lent. ruleVerbs(player, params)RuleVerbs- What a rule does to its player, with no listening:
fire()for a click, key or throw,enter()when a hold begins,leave()when it ends. keyFires(keys, event)boolean- Whether a key press plays a clip listening for
keys.
hostElementrequired- Hears the click or hover, and is what scrolls into view.
scrollerElement | nullrequired- The box that scrolls the host.
nullis the page. targetsreadonly Element[]required- The elements a rule listens on.
savedSavedrequired- Where a rule claims what it lends:
stage.saved.owner(). keysOnEventTargetdefault window- Where a key rule listens.
onLeave(() => void) | nulldefault null- Runs whenever a hold ends.
allowed() => booleandefault () => true- While it returns false, no click, hover or press starts anything. Grab, throw, focus and key rules still play. Stew Factory's exports use it for a disabled control.
IN_VIEWnumber0.3: how much of the host must be in view to start an"inview"clip.CLIP_CLICKstring"stewClipClick": the mark on a click a shape's own rule answered.