Text and data
Kinetic's text and data helpers split text into rows, reveal it over time, format numbers, build axis ticks and rank rows for a bar race. They also group a transcript into caption lines, read SRT and WebVTT subtitles, and turn a beat file into a pulse.
These are pure functions, and a bad argument throws (KN-100).
split() keeps whitespace as rows flagged space, so a stagger can skip them, and the row needs white-space: pre to keep the gaps.
format() uses 'en-US' unless options.locale says otherwise, so a number reads the same on every render machine.
Text and data
These are pure functions too.
split(text, by)returns rows{ i, key, text, space }for adata-list, one per grapheme ('letter', the default) or per word. Whitespace keeps its own rows, flaggedspace, so a stagger can skip them and the layout keeps its gaps (give the rowwhite-space: pre).typewriter(t, text, cps)returns the firstMath.floor(t * cps)graphemes of the text, andcpsdefaults to 20. Before 0 it returns an empty string, and past the end all of the text. An empty string gives the element no height, so give it amin-height, such as1.3em, to keep the layout still before the first character.
typewriter(this.local - 0.5, 'Every frame is a function of time.', 28) keeps the line empty until the start, types cps characters a second, and holds the whole text, and a click on the stage pauses it.scramble(t, text, options)returns the text settling from scrambled characters, left to right, for a decode reveal or a split-flap board (below).textMetrics(text, font, options)measures a line with the canvas, each letter's place in it, and the font size that fits a width (below).format(n, options)isIntl.NumberFormatwith'en-US'unlessoptions.localesays otherwise, so a number reads the same on every render machine. The other options areIntl.NumberFormat's, and a locale or options that make it throw are KN-100.ticks(min, max, count)returns aboutcount(5 by default, and 2 or more) round values from at or belowminto at or abovemax, for an axis, soticks(0, 100, 5)is[0, 20, 40, 60, 80, 100]. A range many orders of magnitude smaller than its ends, such as 1 to1 + 1e-15, gives values that are not round.
ticks() returns the round values for an axis from 0 to max, about count of them and ending at or above max, and format() writes each label in the notation chosen; the small number under each label is the value itself.rank(t, rows, by, options)returns one{ row, index, value, position }per input row, in input order, at a fractional keyframet, for a bar race (below).
rank(t, rows, by, options)
rank takes t as a fractional keyframe.
const { rank } = wildflower.kinetic;
const ROWS = [{ id: 'a', values: [10, 30] }, { id: 'b', values: [20, 15] }];
// t = 0.5 blends keyframes 0 and 1: values and positions are halfway between
const bars = rank(0.5, ROWS, (row, k) => row.values[k]);
t runs from keyframe 0 to keyframe 4 over eight seconds and the last keyframe repeats the first, so the bars trade places on a loop, and a click on the stage pauses.- Whole numbers of
tare the keyframes passed toby(row, keyframe, index), and a fraction blends the two neighboring values and orderings. To pause the race on each keyframe, map the clock tok + interpolate(raw - k, [0.45, 1], [0, 1], { ease }), whererawis the fractional keyframe andkisMath.floor(raw), which holds each keyframe for the first 45% of its span and then eases to the next. byis called for the whole keyframes either side oft, whatevertis, so it answers for any whole number. A value that is not a finite number, as from a keyframe past the data, counts as 0.position0 is first. The order is descending unlessoptions.ascendingis set, and ties keep input order.
scramble(t, text, options)
scramble returns text settling from scrambled characters, left to right, over duration seconds from t = 0.
| Option | Default | Sets |
|---|---|---|
duration | 1 | The seconds until the last character has settled, above 0. |
chars | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*+=?' | What an unsettled character shows, by grapheme, one character or more. |
fps | 20 | The changes a second of an unsettled character, above 0. |
seed | 1 | The characters, a number or a string: the same seed gives the same ones. |
A t that is not a number, a bad option, or an option scramble() does not have throws (KN-100).
A text that is null or undefined is the empty string.
const { scramble } = wildflower.kinetic;
wildflower.kinetic.scene('board', {
computed: {
gate() { return scramble(this.local, 'GATE 12', { duration: 1.5, seed: 'gate' }); }
}
});
// the characters settle in turn, and the text is whole from 1.5 s
- Character
kof a text of lengthL, counted in graphemes assplit()counts them, settles att = duration * (k + 1) / L. - Until then it shows one of
charspicked by a hash of the seed,kandMath.floor(t * fps), so it comes out the same on every machine. - Whitespace is never scrambled.
- Before 0 every character but whitespace is scrambled, and from
durationon the result istext.
textMetrics(text, font, options)
textMetrics() measures a line of text with the canvas's own measureText(), for centring on a letter, laying out around a line, or fitting a title to a width.
font is the CSS font shorthand, such as '700 54px "Work Sans"'.
const { textMetrics } = wildflower.kinetic;
textMetrics('is a function of', '700 54px "Work Sans"'); // { width, left, right, ascent, descent }
textMetrics(this.title, '900 100px "Work Sans"', { width: 1600 }).size; // the px at which the line is 1600 wide
textMetrics(line, font, { letters: true }).letters; // split()'s rows, each with x and width
| Option | Default | Sets |
|---|---|---|
tracking | 0 | Space between letters in em, as text() takes it, added between graphemes and never after the last. |
letters | false | Adds letters: split()'s rows, each with x, its left edge from the line's start, and width, its advance, kerning included. |
width | none | Adds size: the font size in px at which the line is that wide, tracking included. |
textMetrics() returns for the width, and the bracket under it is that width. The boxes are each letter's left edge and advance from a second call at that size, with letters: true.widthis the advance of the whole line, andleft,right,ascentanddescentare the ink's extent left and right of the line's start and above and below its baseline, in px.- The fit is exact for one line, since a line's width grows in step with its size. A line that wraps, or a fit to a box's height, is left to your own layout.
- The numbers come from the font files, so a web font from
@font-facemeasures the same on every machine, and a system font may not. - The element has to draw with the font that was measured.
Give that
@font-facefont-display: blockorswap: withoptional, text the browser first drew in the fallback can stay in it after the font loads, and then the word on the page is not the word measured. - Readiness waits for each font
textMetrics()has measured with, soready(),check()andrender()never measure the fallback. A computed that reads the scene's time measures again on every seek; one that reads no time, on a static page, measures again once it reads something that changes afterready(). - It needs a document, so it does not run in node.
Text that is not a string, a font the canvas refuses, a tracking that is not finite, a width that is not above 0, or an empty line to fit throws (KN-100).
captions(t, words, options)
captions returns the caption line current at t from a word-timed transcript, an array of { id?, text, start, end } in seconds, which transcription tools write and srt() reads from an SRT or WebVTT file.
Words group into lines at the end of a sentence, at a gap longer than maxGap, and at maxWords or maxChars.
| Option | Default | Meaning |
|---|---|---|
maxWords | 5 | Most words in a line. |
maxChars | 40 | Most characters in a line. |
maxGap | 1 | Seconds of silence that start a new line. |
sentences | true | A word ending in ., ! or ? ends its line. |
hold | 0.4 | Seconds a line stays after its last word, until the next line starts. |
maxWords and maxChars are whole numbers from 1, maxGap and hold are seconds from 0, and any of the four may be Infinity for no limit.
sentences is a boolean.
Another value throws (KN-100), with captions(t, words, { maxGap }) or the option's own name in its where.
The returned line is { index, start, end, text, words, empty }, and each entry of words is { key, text, start, end, state, p }.
lineWords() {
return this.line.words.map((w) => ({
key: w.key,
text: w.text,
style: { opacity: String(w.state === 'future' ? 0.3 : 1) }
}));
}
// a computed beside line() for <p data-list="lineWords" data-key="key">
srt(), the active word is in the accent and the past words are dimmed, and a click on the stage pauses.stateis'past','active'or'future', andpis the word's own progress, 0 to 1. The active word can swell as it is spoken withscale: 1 + 0.08 * (1 - w.p), which is largest as the word starts and settles by its end.keyis the word'sid, or its index in the input.- Between lines, and before the first,
emptyis true,indexis -1 andwordsis[], so adata-listover it renders nothing. - Lines are cached per input array, so a computed that calls
captionson every seek groups the words once.
srt(text)
srt reads SRT or WebVTT subtitles into the transcript captions takes, [{ id, text, start, end }], with one entry per word and ids 'w0', 'w1' and so on.
Each cue's words share its time in proportion to their lengths in characters, and the last word ends at the cue's end.
- Times are read as
hh:mm:ss,mmm(SRT) orhh:mm:ss.mmmandmm:ss.mmm(WebVTT). - Each timing line starts a cue, which runs to a blank line or to the next timing line. With no blank line between two cues, a number on the line above the second timing line is read as that cue's number and left out of the text.
- A cue's line breaks become spaces.
- The cue numbers and identifiers, VTT cue settings, the
WEBVTTheader,NOTE,STYLEandREGIONblocks, and a byte order mark are dropped. - Tags such as
<i>,<v Speaker>and<00:00:01.000>, and SRT's override tags such as{\an8}, are dropped. A<that opens no tag, as ina < b, stays in the text. - After the tags are dropped, the WebVTT character references
&,<,>, ,‎and‏are decoded, and a non-breaking space keeps the two words on either side of it together. - A cue's order is not checked, so a cue that ends before it starts gives words that run backwards.
- A value that is not a string, or a text with no cue, throws (KN-100).
Call srt() once, when the page loads, and pass its array to captions on every seek.
captions groups the words once per array, so a new array from srt() on every seek would be grouped again each time.
const { captions, srt } = wildflower.kinetic;
const words = srt(`1
00:00:00,500 --> 00:00:02,000
Every frame is a function of time.`);
wildflower.kinetic.scene('subtitles', {
computed: { line() { return captions(this.local, words, { maxWords: 6 }); } }
});
The caption examples read this.local, the seconds since the scene's own clip started, so the words follow the clip wherever it is placed.
A subtitles scene that starts with its voice, data-start="voice.start" (Starting with another clip), reads the transcript's times from the voice's start.
captions() compares the times only with t, so with data-media-start="12" on the voice, a transcript timed from the start of the file reads captions(this.local + 12, words).
Subtitles from a file
A URL from in query() reads the response as JSON, so an .srt or .vtt file needs a function from that fetches the text.
Here it returns the words srt() reads from the text, once, and waitFor holds frame 0 until they are in.
const { captions, srt } = wildflower.kinetic;
wildflower.query('subs', {
from: () => fetch('subs.srt').then((r) => r.text()).then(srt)
});
wildflower.kinetic.scene('subtitles', {
waitFor: ['subs'],
computed: {
line() { return captions(this.local, wildflower.getQuery('subs').rows, { maxWords: 6 }); }
}
});
A file that fails to load, or whose text has no cue, leaves the query in error, which fails readiness (KN-131). Queries exist on the full tier only.
beat(t, beats, options)
beat returns a pulse from a beat file, an object { version: 1, audio?, beats: [{ time, strength? }] }, or from its beats array.
Each beat's strength, 0 to 1 and 0.5 when absent, decays as e^(-elapsed / decay) from the moment of the beat, and the result is the largest decayed strength among the beats before t.
const { beat, styleAt } = wildflower.kinetic;
wildflower.query('beats', { from: 'data/beats.json', select: (file) => file.beats });
wildflower.kinetic.scene('disc', {
waitFor: ['beats'],
computed: {
pulse() { return beat(this.local, wildflower.getQuery('beats').rows, { decay: 0.3 }); },
disc() { return styleAt(this.local, { scale: 1 + 0.14 * this.pulse }); }
}
});
options.decay is in seconds, above 0, and defaults to 0.2.
The result is 0 before the first beat.
See Which to use for when a beat file fits and when the clip's tempo does, which is the default for steady music.