Interact

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.

trigger
    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);
    StartTrigger
    "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.

    scrubProgress, the marker0.00player.time, the line0.00 s
    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 height px tall whose top is top px below the top of a view view px tall.
    SCRUB_STARTnumber
    0.8: the start line, as a share of the view from its top.
    SCRUB_ENDnumber
    0.2: the end line.
    SCRUB_SECONDSnumber
    1: 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.

    on
    onEnd
    toggle
      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"));
      RuleParams
      onRuleTriggerrequired
      What plays it: see below.
      keysreadonly string[]required
      Key rules: KeyboardEvent.key values. 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 repeat to -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.
      RuleTrigger
      "click"
      Replays it from the start. With toggle, plays it open, then back.
      "key"
      The same, for a key in keys pressed 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:grab to stew:release.
      "throw"
      Replays it when a drag on them lets go faster than 300 px a second (stew:throw). With toggle, plays it open, then back.
      RuleEnd: when a hold ends
      "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.

      Calls
      startOn(player, trigger, host, scroller, allowed?)() => void
      Arms player with 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 player on an event. Returns what stops it and puts back the cursor and tabindex it 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.
      Arguments
      hostElementrequired
      Hears the click or hover, and is what scrolls into view.
      scrollerElement | nullrequired
      The box that scrolls the host. null is 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.
      Constants
      IN_VIEWnumber
      0.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.
      NextSprings and throws