Interact

Pointer actions

Shapes can react to the pointer: lean toward it, run from it, or be picked up and thrown. The same actions also work on single nodes of a path.

Follow mouse

cursorAction with kind: "follow" leans a layer toward the pointer: strength percent of the way, never more than maxDistance pixels. mirror leans the other way. Here the body leans, and the pupils follow all the way.

mirror
follow.ts
import { createStage, cursorAction, frameClock } from "@stew/engine";

const stage = createStage(), clock = frameClock();

// the body leans toward the pointer...
const lean = {
  kind: "follow",
  strength: 22,
  maxDistance: 48,
  radius: 0,
  smooth: 0.4,
  lockX: false,
  lockY: false,
  mirror: false,
};
cursorAction({ el: body, layer: bodyLayer, anchor: null }, lean, stage, clock);

// ...and the pupils look at it
const look = {
  kind: "follow",
  strength: 100,
  maxDistance: 4,
  radius: 0,
  smooth: 0.12,
  lockX: false,
  lockY: false,
  mirror: false,
};
cursorAction({ el: eyes, layer: pupils, anchor: null }, look, stage, clock);

Run away

With kind: "runAway", a layer flees once the pointer comes within radius pixels: up to strength pixels, further the closer it gets. Outside the radius it comes home.

runAway.ts
import { createStage, cursorAction, frameClock } from "@stew/engine";

const stage = createStage(), clock = frameClock();
const flee = {
  kind: "runAway",
  strength: 70,
  maxDistance: 0,
  radius: 150,
  smooth: 0.35,
  lockX: false,
  lockY: false,
  mirror: false,
};

// each circle measures its body and moves the layer round it
for (const { body, layer } of circles) {
  cursorAction({ el: body, layer, anchor: null }, flee, stage, clock);
}

Drag

dragAction lets the pointer pick a shape up and move it. Let go fast and it glides on (throw), then springs home (spring). jelly tilts it with its speed, and lift makes it a little bigger while held.

It tells the page as it goes: stew:grab, stew:release and, let go fast, stew:throw, on the shape, bubbling. Rules listen for these.

Springs and throws, in Physics, has the exact rules: how fast, how far, how long.

    drag.ts
    import { createStage, dragAction, frameClock, GRAB, RELEASE, THROW } from "@stew/engine";
    
    const stage = createStage(), clock = frameClock();
    const feel = {
      spring: 0.5,
      throw: 0.6,
      jelly: 0.6,
      lift: 0.5,
      lockX: false,
      lockY: false,
    };
    
    dragAction({ el: star, layer: starLayer }, feel, stage, clock);
    dragAction({ el: box, layer: boxLayer }, feel, stage, clock);
    
    // what it tells the page bubbles up from the shape
    for (const name of [GRAB, RELEASE, THROW]) {
      svg.addEventListener(name, () => log(name));
    }

    On single nodes

    nodeBend pulls single nodes of a path, each by its own action, and they all bend the outline together. Here every node is pulled toward the pointer and pushed away from it at once.

    Nodes
    bend.ts
    import { createStage, frameClock, nodeBend } from "@stew/engine";
    
    const stage = createStage(), clock = frameClock();
    const toward = {
      kind: "follow",
      strength: 14,
      maxDistance: 24,
      radius: 0,
      smooth: 0.45,
      lockX: false,
      lockY: false,
      mirror: false,
    };
    const away = {
      kind: "runAway",
      strength: 34,
      maxDistance: 0,
      radius: 120,
      smooth: 0.22,
      lockX: false,
      lockY: false,
      mirror: false,
    };
    
    // the template says which coordinate pairs each action moves:
    // here two actions per node, 12 nodes
    nodeBend(
      { path: blob, outline: [blob], carryable: null, template },
      nodes.flatMap(() => [toward, away]),
      stage, clock,
    );

    Targets and layers

    An action measures one element and moves another: the shape (el) and a layer around it (layer), usually a <g>. Measuring the shape and moving its layer keeps the push from feeding back into itself.

    Pushes are worked out in screen pixels, then turned into the layer's own frame, so a flipped, turned or scaled group around it can't reverse or stretch the push. Actions write through a stage, so several on one layer add up.

    Calls (each returns what stops it and puts everything back)
    cursorAction(target, params, stage, clock)
    Follow mouse or Run away on a shape.
    dragAction(target, params, stage, clock)
    Drag on a shape.
    nodeBend(target, actions, stage, clock)
    Follow mouse, Run away or Drag on single nodes of a path, one action per node in the template.
    CursorParams
    kind"follow" | "runAway"required
    Lean toward the pointer, or flee it.
    strengthnumberrequired
    Follow: percent of the way. Run away: pixels of push.
    maxDistancenumberrequired
    Follow: the most it leans, in pixels.
    radiusnumberrequired
    Run away: pixels within which it flees.
    smoothnumberrequired
    Seconds it takes to get there.
    lockX, lockYbooleanrequired
    A locked axis never moves.
    mirrorbooleanrequired
    Follow: lean away instead.
    DragParams
    springnumberrequired
    Springs home after it's let go: 0.6 + 1.1 x spring seconds. 0 stays put.
    thrownumberrequired
    Glides on with its speed when let go. 0 stops dead.
    jellynumberrequired
    Tilts with its speed, up to 16 degrees x jelly.
    liftnumberrequired
    Bigger while held: 8% x lift.
    lockX, lockYbooleanrequired
    A locked axis stays.
    Targets
    CursorTarget{ el, layer, anchor }
    The shape it measures, the layer it moves, and the point it measures from (null = the shape's middle).
    DragTarget{ el, layer }
    The shape that tells the page what happens, and the layer it moves.
    NodeBendTarget{ path, outline, carryable, template }
    The path, every element drawn from its outline, and which coordinate pairs each action moves.
    NextStarts and rules