Motion paths
A motion track moves a target along a path. Its value is how far along it is: 0 at the start, 1 at the end, measured by distance.
Follow a path
Give the target a track with property: "motion", and put the path it follows in the clip's paths, under the same uid. A keyframe's value of 0 is the path's start and 1 its end.
The dot on the car is its anchor: the point that rides the road. Pick another anchor, turn rotate off or slide rotateOffset, and the car and the JSON below change together.
{
"duration": 5,
"paths": {
"car": {
"d": "M -80 160 C -20 160 40 104 100 104 S 220 192 300 192 S 420 104 500 104 S 640 160 720 160",
"rotate": true,
"rotateOffset": 0
}
},
"tracks": [
{
"targetUid": "car",
"property": "motion",
"keyframes": [
{ "time": 0, "value": 0, "easing": "none" },
{ "time": 5, "value": 1 }
]
}
]
}Anchor and facing
The anchor is a point in the traveller's own units. Without one it is the origin, 0, 0, so art drawn around its own 0, 0 rides the path by that point. The car above is drawn with 0, 0 between its wheels, on the ground.
With rotate, the traveller turns about its anchor to the way the path heads, where 0 degrees is right and 90 is down. rotateOffset adds degrees for the way the art itself faces: none for art drawn facing right, 90 facing up, -90 facing down, 180 facing left.
Without rotate, the traveller doesn't turn: it keeps the angle it is drawn at, and rotateOffset does nothing.
Progress is distance
A motion track's value is measured along the path's length, so 0.5 is halfway along it, however its commands are spread out. The ticks mark every tenth of the length. With the ease none, the beetle passes them at an even beat.
Several subpaths are walked one after another, and the jump between them doesn't count as distance. Past either end the traveller carries straight on along the end's heading, so an ease that overshoots, like back.out, takes it a little past the end and back.
- progress
- 0.60
- distance
- 389.38 of 648.96
- x, y
- 339.8, 72.3
- heading
- -28°
import { parsePath, pathTable, pointAtDistance, printPath } from "@stew/engine";
// every command read as cubics (null: it draws nothing)
const curves = parsePath("M 60 150 H 200 A 50 50 0 0 0 300 150 Q 300 60 400 60 T 580 130") ?? [];
// and printed back: a move, then one C per cubic
printPath(curves);
// M60 150
// C106.667 150 153.333 150 200 150
// C200 177.614 222.386 200 250 200
// C277.614 200 300 177.614 300 150
// C300 90 333.333 60 400 60
// C466.667 60 526.667 83.333 580 130
// measured once
const table = pathTable(curves);
table.total; // 648.96
// a tick at every tenth of the way, by distance
const ticks: [number, number, number][] = [];
const out = new Float64Array(3);
for (let i = 0; i <= 10; i++) {
pointAtDistance(table, (table.total * i) / 10, out);
ticks.push([out[0], out[1], out[2]]); // x, y, heading in degrees
}Paths in another space
A path's d is read in the space it was drawn in. When the traveller moves in another one, say inside a group that is moved, scaled or turned, give frame: the matrix (a, b, c, d, e, f) from the space the traveller moves in into the path's. The engine brings each point on the path into the traveller's space with the frame's inverse, and takes the frame's own turn off the facing. So the traveller sits on the path for any frame, and faces the way it goes as seen when the frame only moves, turns and scales evenly. A stretch, skew or mirror leaves the facing off.
svgDrawer writes the traveller's place as a matrix(...) in the transform of the elements bind gives for the "motion" channel, in place of any transform they had. So give a traveller a wrapper of its own for the motion channel, and bind its other moves to an element inside it, so they travel with it. Stew Factory does this for you, and measures frame too: from the wrapper's parent to the artboard.
Path helpers
The engine's path tools are exported too. The ticks and the readout above use them on the very trail the beetle walks.
A path is read into cubic curves, lines, quadratics and arcs included, and each cubic is measured once, in 64 steps. Finding a point after that creates nothing, so it can run every frame.
parsePath(d)Curves | null- Path data into cubics. Every command, absolute or relative:
M L H V C S Q T A Z. Like a browser, it stops at the first thing it can't read and keeps what came before.nullwhen it doesn't start with a move or draws nothing. printPath(curves)string- Back into path data: a move, one
Cper cubic andZwhere a subpath closes, numbers to three places. pathTable(curves)PathTable- Measures the curves once. Its
totalis the whole length. pointAtDistance(table, distance, out)void- Writes the point
distancealong intoout[0]andout[1], and its heading in degrees intoout[2].outis aFloat64Array. Past either end it carries on straight. CurvesSubpath[]- One
Subpathper subpath:points, aFloat64Array(the start, then six numbers per cubic: two controls and its end), andclosed. PathTable{ cubics, lengths, total }- Every cubic's 8 numbers, how far along each measuring step ends, and the whole length.
Reference
A motion track the engine can't play is left out and noted in program.skipped with the reason, like car motion: no path to follow. The rest of the clip still plays.
dstringrequired- SVG path data, in the space it was drawn in.
rotatebooleandefault false- Turn to face the way it travels.
rotateOffsetnumberdefault 0- Degrees added to that turn: which way the art itself faces.
anchorStewPointdefault 0, 0- The point that rides the path, in the traveller's own units. (An
anchortrack is something else: it keys where a target turns about.) frame[number, number, number, number, number, number]default 1, 0, 0, 1, 0, 0- The matrix (a, b, c, d, e, f) from the space the traveller moves in into the path's. Its turn is taken off the facing (exact for a frame that moves, turns and scales evenly).
pathsRecord<string, StewPath>optional- The path each target's
motiontrack follows, under the target's uid. "motion"number- A track's property: how far along its path, 0 its start, 1 its end, by distance. Two on one target: the later one in
tracksplays.
no path to follow- The clip's
pathshas nothing under the track's target. its path draws nothing- Its
ddoesn't start with a move, or draws nothing at all. its frame flattens everything- Its
framehas no inverse: it squashes everything flat, or one of its first four numbers (a, b, c, d) isn't finite. left out N keyframe(s) it can't read- A keyframe whose time or value doesn't read as a finite number is left out. With none left, so is the track.