Paths, shapes and fields
matchPaths() and shape() return SVG path data for a track or a computed property, and distribute() and falloff() place many points and give each one a weight from a field that moves with the scene's time.
A path is a value, bound with data-bind-attr="{ d: name }" or written inside path("…") on a CSS property.
A count such as sides does not interpolate, so to morph between counts give both shapes to matchPaths(), whose paths need the same number of subpaths.
A transform track on an SVG shape replaces its transform attribute, so put the placement on a wrapping <g>.
Paths and shapes
These functions return SVG path data, so a path is a value a track or a computed property gives, bound with data-bind-attr="{ d: name }" or written inside path("…") on a CSS property.
matchPaths(paths, options)
matchPaths(paths, options) converts paths with different commands into paths a text track can move between.
paths is an array of two or more SVG paths, each a d string or a path("…") value.
const { scene, matchPaths } = wildflower.kinetic;
const SQUARE = 'M -100 -100 H 100 V 100 H -100 Z';
const CIRCLE = 'M 0 -120 A 120 120 0 1 1 0 120 A 120 120 0 1 1 0 -120 Z';
scene('logo', {
keyframes: {
shape: { at: [0, 1.2], to: matchPaths([SQUARE, CIRCLE]), ease: 'power2.inOut' }
}
});
// <path data-bind-attr="{ d: shape }"></path>
The result is an array of the same length, each path in the form it came in, made of absolute cubic Béziers with the same commands and number of segments.
It is keyframe values, so the morph is a track like any other, and interpolate() takes it too.
| Option | Default | Meaning |
|---|---|---|
segments | 8 | The fewest segments each subpath gets, a whole number, 1 or more. 8 is the fewest that keeps a square to a circle from twisting. |
align | true | Each closed subpath after the first path's is reversed when it runs the other way, and then started at its point nearest the first path's start. false keeps every path as given. |
- Any SVG path data is accepted, with
M,L,H,V,C,S,Q,T,AandZ, absolute or relative, repeated commands left implicit, and numbers and flags written together. - Each segment becomes a cubic.
A line gets its control points at its thirds, a quadratic is raised to a cubic, and an arc is converted by the SVG 2 implementation notes and split into pieces of 90 degrees at most.
A
Zwhose last point is not the subpath's first closes it with a line. - Subpaths are matched in order, so the paths need the same number of them.
Each subpath gets the most segments any of the paths has there, and at least
segments. - Each further cut goes to the segment whose pieces are longest, and a segment cut into k pieces is cut into k equal spans of its curve's parameter. Polygons morph evenly at the least common multiple of their corner counts, such as 24 for a triangle and an octagon.
- A
Zis written where every path closes the subpath, and an open subpath is never reversed or restarted. - Numbers have at most four decimals.
The browser's
getTotalLength()of each result is within 0.05 px of its input's. - Fewer than two paths, an empty path, path data the grammar rejects, a different number of subpaths, or a
segmentsthat is not a whole number of 1 or more throws (KN-100). For path data the grammar rejects, the message gives the path, and the development build's message gives the index of the first character it rejects.
shape(kind, options)
shape(kind, options) returns the path data of a polygon, a star, a sector or an arrow, with every option a number, or a point for an arrow, so a track or a computed property can drive any of them.
| Kind | Options (defaults) | Shape |
|---|---|---|
'polygon' | sides (a whole number, 3 or more), radius, cx (0), cy (0), rotate (0) | A regular polygon around (cx, cy), its first vertex pointing up at rotate 0. |
'star' | points (a whole number, 2 or more), outer, inner, cx, cy, rotate | points vertices at outer from the center, with one at inner between each two, the first pointing up at rotate 0. |
'sector' | radius, inner (0), from (0), to, cx, cy | A pie slice from from to to, or a ring segment when inner is above 0. |
'arrow' | from [x, y], to [x, y], width, head | A block arrow from from to its point at to, with a shaft width wide and a head head long and twice that wide. |
const { scene, shape } = wildflower.kinetic;
scene('gauge', {
keyframes: { sweep: { at: [0, 2], to: [0, 270], ease: 'power2.out' } },
computed: {
pie() { return shape('sector', { radius: 120, to: this.sweep, cx: 160, cy: 160 }); }
}
});
// <path data-bind-attr="{ d: pie }"></path>
- Angles are in degrees, clockwise from up, and numbers have at most four decimals.
- A sector's
tobelow itsfromruns anticlockwise. A sector whosetoequals itsfromreturns an empty string, so nothing is drawn, with no line from the center. A sweep of 360 degrees or more is drawn as two arcs so the circle closes, and a whole ring keeps its hole. - An arrow shorter than its head is drawn as the head alone, as long as the arrow, and an arrow whose
fromequals itstoreturns an empty string. - A star's
innerabove itsouteris not checked, and draws the star turned over. - A count does not interpolate, so a track from 3 to 8 sides passes values such as 3.5, which throw, and with
round: truethe shape jumps from one count to the next. To morph between counts, give both shapes to matchPaths() and put its result in a track.
An unknown kind, a missing option, a value that is not a finite number, a sides or points that is not a whole number, a radius, outer, inner, width or head below 0, or a sector's inner not below its radius throws (KN-100).
trimPath(d, from, to, options)
trimPath() returns the stretch of a path between two fractions of it, from 0 at its start to 1 at its end, as path data.
The dash recipe draws a stroke partway and gives no shape, while this is the shape itself, so it can fill, feed a canvas or three.js, or be measured.
const { trimPath, ramp } = wildflower.kinetic;
trace() { return trimPath(FLIGHT_D, 0, ramp(this.local, 1, 3)); } // the line drawn from its start
tail() { return trimPath(ROUTE_D, this.u - 0.08, this.u); } // the stretch just behind a mover
flow() { return trimPath([TOP_D, BOTTOM_D], 0, this.p); } // a band between two edges, closed
// <path data-bind-attr="{ d: trace }"></path>
| Option | Default | Sets |
|---|---|---|
by | 'length' | What a fraction measures: of the whole length, or with 'segment' each segment an equal share, with the curve's own parameter inside it. |
from to to. The dots are the joins between the curve's three segments: a long arch, 80% of the length, and two short lines. By length, to at 0.6 stops partway along the arch; by segment each segment is a third, so it runs past the arch onto the first line. [TOP, BOTTOM] fills the band between two edges.fromandtoare clamped to 0 and 1, and afromat or pasttoreturns an empty string, so nothing is drawn.- The stretch is written as absolute cubics, with a new move wherever it crosses into another subpath.
A closed subpath taken whole keeps its
Z, and one cut partway leaves it off. - By segment, a path written as one segment for each even step of time is cut at a time, so a trace can end at a mover whose speed changes, where a fraction of the length would run ahead of it or behind it.
- For a band,
[top, bottom]are two edges of one subpath each, running from the band's start to its end, and the result is the top's stretch, the bottom's stretch run back, and aZ. A bottom edge written the other way, which starts nearer the top's end, is turned round first, so it draws the same band. - A
path("…")value comes back in the same form, forclipPathoroffsetPath. Trimmed to nothing, it is a lone move such aspath("M 0 0"), which draws nothing as adand hides everything as aclipPath, where''would take the style off. pathLength(d)gives the path's length in its own units, from the same measure, so a distance along it becomes a fraction:trimPath(d, 0, 150 / pathLength(d))is the first 150 units.
d that is not path data, a from or to that is not a finite number, an unknown by, or band edges that are not two paths of one subpath each throws (KN-100).
Transform tracks on SVG elements
A rotate or scale track on an SVG element turns or sizes it about the origin of the SVG's viewBox.
transform-box: fill-box; transform-origin: center on the element moves the origin to the center of its own box.
transform-box is CSS's own property for the box that transform-origin is measured in: fill-box is the box around the shape's geometry, the area its fill covers with the stroke left out, and view-box, the default for SVG, is the SVG's own viewport.
A transform track writes the CSS transform property, which replaces the element's transform attribute, so a shape placed with transform="translate(…)" loses its place.
Put the placement on a wrapping <g> and the track on the shape inside it.
<g transform="translate(400 360)">
<path style="transform-box: fill-box; transform-origin: center"
data-bind-attr="{ d: shape }" data-bind-style="turn"></path>
</g>
Distributions and falloffs
Two more pure functions move many things as one.
distribute() places n points, and falloff() gives each point a weight from a field the scene moves with its time.
A computed then turns the weight into a scale, an opacity, a color mix or a delay, such as styleAt(this.local - (1 - w) * 0.6, tracks).
distribute(kind, n, options)
distribute(kind, n, options) returns n points, each { i, x, y, heading, u }, placed on the line, circle, grid, spiral, scatter or path its kind names.
| Kind | Options | Points |
|---|---|---|
'line' | from and to, both [x, y] | Evenly from from to to, both ends included. |
'circle' | cx, cy and radius, all three required; from (0) and to (from + 360), in degrees clockwise from up as shape() takes them | A full turn spaced 360 / n, or an arc with a point at each end. |
'grid' | columns, cell as px or [width, height], cx, cy | Centered on cx and cy, numbered row by row as stagger(i, n, { grid }) numbers its cells, with col and row on each point. |
'spiral' | radius, cx, cy | A sunflower by the golden angle, with the same area for each point, the points facing outward. |
'scatter' | width, height, cx, cy, seed (1) | Spread evenly by best candidate over a width by height area centered on cx and cy, the same for the same seed. |
'path' | d, SVG path data | Evenly by length along the path, with both ends of an open path, and n even steps round a closed one. |
headingis the direction of travel in degrees forrotate(), 0 pointing right and 90 down, asoffset-rotate: autoturns an element.uruns from 0 at the first point to 1 at the last.cxandcyare 0 when left out, except on a circle.- A grid's short last row starts at the grid's left edge, so a grid with fewer points than
columnssits left ofcx. - A spiral's point i is at
radius * Math.sqrt((i + 0.5) / n), so a lone point sits off the center. - A scatter's search and a path's measure are kept for their arguments, so a call on every seek costs the arithmetic alone, and every call returns new objects.
An unknown kind, an n that is not a whole number of 0 or more, or a bad option throws (KN-100), with the call and the option in its where.
falloff(kind, point, options)
falloff(kind, point, options) gives a point, { x, y } or [x, y], a weight from 0 to 1.
| Kind | Options | Weight |
|---|---|---|
'radial' | x, y, radius, inner (0) | 1 inside inner, falling to 0 at radius. |
'linear' | x, y, angle, width | An edge through x, y heading toward angle: 1 behind it, falling to 0 over width ahead. |
'band' | x, y, angle, width | 1 on the line through x, y across angle, 0 at width either side. |
'ring' | x, y, radius, width | 1 at radius from x, y, 0 at width either side. |
'shape' | d, x (0), y (0), scale (1), width (0), rule ('nonzero' or 'evenodd') | 1 inside the path data d, moved by x and y and sized by scale, falling to 0 at width px outside its edge. |
Every kind takes ease, which shapes the weight, and invert.
A moving linear edge or band reveals or passes through a field of points, a growing ring is a ripple, and a shape is a logo made of them.
const { distribute, falloff, interpolate } = wildflower.kinetic;
dots() {
const edge = interpolate(this.local, [0, 3], [-200, 1480]);
return distribute('spiral', 400, { cx: 640, cy: 360, radius: 320 }).map((p) => {
const w = falloff('linear', p, { x: edge, y: 360, angle: 0, width: 240, ease: 'sine.inOut' });
return { id: p.i, style: { transform: `translate(${p.x}px, ${p.y}px) scale(${0.4 + 0.8 * w})` } };
});
}
At width 0 a point exactly on a shape's edge reads 1 on some edges and 0 on others, so a point grid along the outline needs a width above 0.
An unknown kind, a point without x and y, or a bad option throws (KN-100), with the call and the option in its where.