@arraypress/covers 1.0.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +18 -524
- package/package.json +34 -5
- package/recipes/brand.json +1143 -0
- package/recipes/flyers/afterglow-motion.json +540 -0
- package/recipes/flyers/afterglow.json +70 -0
- package/recipes/flyers/arcfield-mirror-motion.json +397 -0
- package/recipes/flyers/arcfield-mirror.json +364 -0
- package/recipes/flyers/bauhaus-oktav.json +319 -0
- package/recipes/flyers/bauhaus-ritmo.json +735 -0
- package/recipes/flyers/bauhaus-volta.json +394 -0
- package/recipes/flyers/bloomfall.json +99 -0
- package/recipes/flyers/drag-solenne-motion.json +444 -0
- package/recipes/flyers/drag-solenne.json +426 -0
- package/recipes/flyers/fennick-patches.json +874 -0
- package/recipes/flyers/ferro-pass.json +398 -0
- package/recipes/flyers/ferrova.json +812 -0
- package/recipes/flyers/grid-rest-rhythm.json +125 -0
- package/recipes/flyers/groovy-honeycomb.json +816 -0
- package/recipes/flyers/groovy-marigold.json +479 -0
- package/recipes/flyers/groovy-peachfuzz.json +974 -0
- package/recipes/flyers/halden-orbit-motion.json +381 -0
- package/recipes/flyers/halden-orbit.json +356 -0
- package/recipes/flyers/halvard.json +120 -0
- package/recipes/flyers/kairo-weekender.json +696 -0
- package/recipes/flyers/linger-motion.json +518 -0
- package/recipes/flyers/linger.json +67 -0
- package/recipes/flyers/little-orchard.json +105 -0
- package/recipes/flyers/lofi-hexmoth.json +912 -0
- package/recipes/flyers/lofi-lowbit-motion.json +888 -0
- package/recipes/flyers/lofi-lowbit.json +880 -0
- package/recipes/flyers/lofi-nullpoint.json +3013 -0
- package/recipes/flyers/lumio-stack-motion.json +648 -0
- package/recipes/flyers/lumio-stack.json +438 -0
- package/recipes/flyers/magazine-cover.json +168 -0
- package/recipes/flyers/magazine-extended.json +269 -0
- package/recipes/flyers/magazine-knockout.json +40 -0
- package/recipes/flyers/northfold-varsity.json +618 -0
- package/recipes/flyers/open-studio.json +69 -0
- package/recipes/flyers/orrin-merch.json +715 -0
- package/recipes/flyers/oxyde.json +113 -0
- package/recipes/flyers/quiet-fields.json +610 -0
- package/recipes/flyers/scrapbook-halcyon.json +948 -0
- package/recipes/flyers/solenne-ticket.json +730 -0
- package/recipes/flyers/solvane-motion.json +576 -0
- package/recipes/flyers/solvane.json +98 -0
- package/recipes/flyers/subduct.json +99 -0
- package/recipes/flyers/sunvale-lineup.json +584 -0
- package/recipes/flyers/tidewell.json +91 -0
- package/recipes/flyers/tour-kairo.json +535 -0
- package/recipes/flyers/velora-stickers.json +885 -0
- package/recipes/flyers/wheatpaste-black.json +313 -0
- package/recipes/flyers/wheatpaste-mark.json +198 -0
- package/recipes/flyers/wheatpaste-torn.json +114 -0
- package/recipes/flyers/wheatpaste-wall.json +984 -0
- package/recipes/flyers/wheatpaste-words.json +233 -0
- package/recipes/release/aura.json +137 -0
- package/recipes/release/chrome.json +433 -0
- package/recipes/release/collage.json +372 -0
- package/recipes/release/duotone.json +113 -0
- package/recipes/release/film-still.json +102 -0
- package/recipes/release/headline.json +70 -0
- package/recipes/release/instant.json +170 -0
- package/recipes/release/label.json +364 -0
- package/recipes/release/liquid-glass.json +301 -0
- package/recipes/release/odd-tile-dark.json +688 -0
- package/recipes/release/odd-tile-dense.json +1056 -0
- package/recipes/release/odd-tile-shift.json +541 -0
- package/recipes/release/odd-tile-split.json +571 -0
- package/recipes/release/odd-tile.json +633 -0
- package/recipes/release/off-edge.json +96 -0
- package/recipes/release/plinth.json +794 -0
- package/recipes/release/riso.json +108 -0
- package/recipes/release/series/fluid-label.json +561 -0
- package/recipes/release/series/fluid-pixel.json +334 -0
- package/recipes/release/series/hairline.json +305 -0
- package/recipes/release/series/halo.json +282 -0
- package/recipes/release/series/mark-crop.json +135 -0
- package/recipes/release/series/mark-window.json +150 -0
- package/recipes/release/series/photo-label.json +475 -0
- package/recipes/release/series/smear-field.json +345 -0
- package/recipes/release/series/smear.json +202 -0
- package/recipes/release/series/spin.json +172 -0
- package/recipes/release/series/split-ghost.json +230 -0
- package/recipes/release/series/zoom.json +168 -0
- package/recipes/release/small-print.json +114 -0
- package/recipes/release/stack.json +156 -0
- package/recipes/release/swiss.json +117 -0
- package/schema/recipe.schema.json +7624 -827
- package/src/animate/index.d.ts +9 -0
- package/src/animate/index.js +11 -0
- package/src/animate/keyframes.d.ts +21 -0
- package/src/animate/keyframes.js +119 -0
- package/src/animate/marquee.d.ts +30 -0
- package/src/animate/marquee.js +93 -0
- package/src/animate/morph.d.ts +8 -0
- package/src/animate/morph.js +99 -0
- package/src/animate/motion.d.ts +59 -0
- package/src/animate/motion.js +63 -0
- package/src/animate/sheet.d.ts +19 -0
- package/src/animate/sheet.js +61 -0
- package/src/animate/timing.d.ts +28 -0
- package/src/animate/timing.js +146 -0
- package/src/animate/tracks.d.ts +18 -0
- package/src/animate/tracks.js +139 -0
- package/src/builtins.js +4 -0
- package/src/diagram.js +4 -3
- package/src/extract.js +6 -5
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +245 -53
- package/src/formats/cut.d.ts +10 -0
- package/src/formats/cut.js +77 -0
- package/src/formats/index.d.ts +6 -0
- package/src/formats/index.js +9 -0
- package/src/formats/presets.d.ts +41 -0
- package/src/formats/presets.js +89 -0
- package/src/formats/print.d.ts +25 -0
- package/src/formats/print.js +117 -0
- package/src/formats/zones.d.ts +10 -0
- package/src/formats/zones.js +64 -0
- package/src/geometry.js +5 -35
- package/src/grid.js +1 -1
- package/src/handmade.js +9 -7
- package/src/icons.js +42 -9
- package/src/index.d.ts +232 -55
- package/src/index.js +14 -6
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +201 -27
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +43 -4
- package/src/layers/barcode.d.ts +19 -0
- package/src/layers/barcode.js +63 -0
- package/src/layers/code128.d.ts +4 -0
- package/src/layers/code128.js +28 -0
- package/src/layers/expr.d.ts +6 -0
- package/src/layers/expr.js +121 -0
- package/src/layers/geometry.d.ts +6 -0
- package/src/layers/geometry.js +238 -0
- package/src/layers/index.d.ts +18 -0
- package/src/layers/index.js +15 -0
- package/src/layers/perforation.d.ts +14 -0
- package/src/layers/perforation.js +47 -0
- package/src/layers/repeat.d.ts +29 -0
- package/src/layers/repeat.js +90 -0
- package/src/layers/shape.d.ts +46 -0
- package/src/layers/shape.js +115 -0
- package/src/layers/soft.d.ts +5 -0
- package/src/layers/soft.js +43 -0
- package/src/layers/svg.d.ts +23 -0
- package/src/layers/svg.js +108 -0
- package/src/layers/table.d.ts +25 -0
- package/src/layers/table.js +80 -0
- package/src/layers/tear.d.ts +21 -0
- package/src/layers/tear.js +74 -0
- package/src/layers/texture.d.ts +17 -0
- package/src/layers/texture.js +106 -0
- package/src/layers/util.d.ts +8 -0
- package/src/layers/util.js +61 -0
- package/src/layers.js +129 -61
- package/src/lint/image-size.d.ts +4 -0
- package/src/lint/image-size.js +34 -0
- package/src/lint/index.d.ts +16 -0
- package/src/lint/index.js +127 -0
- package/src/lint/rules.d.ts +5 -0
- package/src/lint/rules.js +19 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +20 -0
- package/src/paint/index.d.ts +49 -0
- package/src/paint/index.js +66 -0
- package/src/paint/presets.d.ts +10 -0
- package/src/paint/presets.js +47 -0
- package/src/paint/tile.d.ts +7 -0
- package/src/paint/tile.js +55 -0
- package/src/palette.js +1 -1
- package/src/platform/files.d.ts +6 -0
- package/src/platform/files.js +41 -0
- package/src/platform/harfbuzz.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +15 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +71 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/print.js +16 -13
- package/src/recipes.js +5 -4
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -0
- package/src/render/context.d.ts +82 -0
- package/src/render/grain.d.ts +7 -0
- package/src/render/grain.js +113 -0
- package/src/render/group.d.ts +23 -0
- package/src/render/group.js +67 -0
- package/src/render/key.d.ts +5 -0
- package/src/render/key.js +90 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/transform.d.ts +36 -0
- package/src/render/transform.js +139 -0
- package/src/render/use.d.ts +27 -0
- package/src/render/use.js +73 -0
- package/src/render/wrap.d.ts +14 -0
- package/src/render/wrap.js +144 -0
- package/src/render.js +343 -215
- package/src/text.d.ts +29 -0
- package/src/text.js +23 -16
- package/src/type/auto-color.d.ts +9 -0
- package/src/type/auto-color.js +44 -0
- package/src/type/fallback.d.ts +11 -0
- package/src/type/fallback.js +92 -0
- package/src/type/glyphs.d.ts +24 -0
- package/src/type/glyphs.js +87 -0
- package/src/type/index.d.ts +5 -0
- package/src/type/index.js +10 -0
- package/src/type/kern.d.ts +4 -0
- package/src/type/kern.js +144 -0
- package/src/type/layout-ref.d.ts +10 -0
- package/src/type/layout-ref.js +74 -0
- package/src/type/line.d.ts +11 -0
- package/src/type/line.js +105 -0
- package/src/type/live.d.ts +6 -0
- package/src/type/live.js +39 -0
- package/src/type/name.d.ts +4 -0
- package/src/type/name.js +32 -0
- package/src/type/path.d.ts +18 -0
- package/src/type/path.js +89 -0
- package/src/type/pixel.d.ts +7 -0
- package/src/type/pixel.js +85 -0
- package/src/type/sfnt.d.ts +20 -0
- package/src/type/sfnt.js +258 -0
- package/src/type/spans.d.ts +33 -0
- package/src/type/spans.js +138 -0
- package/src/type/symbols.d.ts +7 -0
- package/src/type/symbols.js +89 -0
- package/src/type/typeset.d.ts +10 -0
- package/src/type/typeset.js +363 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
- package/src/color.js +0 -246
- package/src/png.js +0 -77
- package/src/rng.js +0 -95
- package/src/svg.js +0 -69
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Animation: motion that plays in the SVG, and keyframes drawn a frame at a time.
|
|
2
|
+
export { renderMotion } from './motion.js';
|
|
3
|
+
export type { Motion, MotionPreset, MotionScope } from './motion.js';
|
|
4
|
+
export { motionList, progress, easing, MOTION_PRESETS } from './timing.js';
|
|
5
|
+
export type { Easing, ResolvedMotion } from './timing.js';
|
|
6
|
+
export { createSheet, nameSheet } from './sheet.js';
|
|
7
|
+
export type { Sheet } from './sheet.js';
|
|
8
|
+
export { animate, isAnimated, valueAt, interpolate } from './keyframes.js';
|
|
9
|
+
export type { Keyframes, Ease } from './keyframes.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation: `motion` plays in the SVG itself (CSS keyframes, no script);
|
|
3
|
+
* keyframes draw one frame at a time (`renderSvg(cover, { t })`).
|
|
4
|
+
*
|
|
5
|
+
* @module animate
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { renderMotion } from './motion.js';
|
|
9
|
+
export { motionList, progress, easing, MOTION_PRESETS } from './timing.js';
|
|
10
|
+
export { createSheet, nameSheet } from './sheet.js';
|
|
11
|
+
export { animate, isAnimated, valueAt, interpolate } from './keyframes.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Keyframes against time, for frames drawn one at a time.
|
|
2
|
+
|
|
3
|
+
/** A keyframed value. */
|
|
4
|
+
export interface Keyframes<T = unknown> {
|
|
5
|
+
/** `[t, value]` pairs (seconds), or `{ t, value, ease }`. */
|
|
6
|
+
keyframes: ([number, T] | [number, T, Ease] | { t: number; value: T; ease?: Ease })[];
|
|
7
|
+
ease?: Ease;
|
|
8
|
+
/** Repeat over the cover's `loop` (or the last key); `'pingpong'` goes back and forth. */
|
|
9
|
+
loop?: boolean | 'pingpong';
|
|
10
|
+
}
|
|
11
|
+
/** Easing curves. */
|
|
12
|
+
export type Ease = 'linear' | 'in' | 'out' | 'inOut' | 'step';
|
|
13
|
+
|
|
14
|
+
/** Whether a value has keyframes anywhere in it. */
|
|
15
|
+
export function isAnimated(v: unknown): boolean;
|
|
16
|
+
/** A keyframed value at time `t` (seconds). */
|
|
17
|
+
export function valueAt<T>(k: Keyframes<T>, t: number, duration?: number): T;
|
|
18
|
+
/** A value with every keyframed part resolved at `t`. */
|
|
19
|
+
export function animate<T>(v: T, t: number, duration?: number): T;
|
|
20
|
+
/** Two values `f` (0–1) of the way between them: numbers, colours (OKLab), same-unit lengths, arrays and objects of them. */
|
|
21
|
+
export function interpolate<T>(a: T, b: T, f: number): T;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyframes, for frames drawn one at a time: any layer option can be
|
|
3
|
+
* keyframed against `t` (seconds), and `renderSvg(cover, { t })` draws the
|
|
4
|
+
* frame at that time. Static covers have no keyframes and ignore `t`, so
|
|
5
|
+
* nothing changes for them. (Motion that plays in the SVG itself is
|
|
6
|
+
* `motion`; see ./motion.js.)
|
|
7
|
+
*
|
|
8
|
+
* ```json
|
|
9
|
+
* { "type": "text", "content": "{name}",
|
|
10
|
+
* "opacity": { "keyframes": [[0, 0], [0.6, 1]], "ease": "out" },
|
|
11
|
+
* "transform": { "rotate": { "keyframes": [[0, -4], [2, 4]], "loop": "pingpong" } } }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Numbers interpolate; colours mix in OKLab; lengths with the same unit
|
|
15
|
+
* (`"4vmin"` → `"9vmin"`) interpolate; anything else holds until the next
|
|
16
|
+
* key. `loop: true` repeats over the cover's `loop` (seconds; or the last key),
|
|
17
|
+
* `'pingpong'` goes back and forth.
|
|
18
|
+
*
|
|
19
|
+
* @module animate/keyframes
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { mixOklab, toHex } from '@logomarks/image-effects/color';
|
|
23
|
+
|
|
24
|
+
/** @param {unknown} v @returns {v is { keyframes: any[] }} */
|
|
25
|
+
const isKeyed = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v) && Array.isArray(/** @type {any} */ (v).keyframes);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a layer has keyframes anywhere in its options.
|
|
29
|
+
*
|
|
30
|
+
* @param {unknown} v
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isAnimated(v) {
|
|
34
|
+
if (isKeyed(v)) return true;
|
|
35
|
+
if (Array.isArray(v)) return v.some(isAnimated);
|
|
36
|
+
if (v && typeof v === 'object' && !(v instanceof Uint8Array)) return Object.values(v).some(isAnimated);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Easing curves on 0–1. */
|
|
41
|
+
const EASE = /** @type {Record<string, (x: number) => number>} */ ({
|
|
42
|
+
linear: (x) => x,
|
|
43
|
+
in: (x) => x * x * x,
|
|
44
|
+
out: (x) => 1 - (1 - x) ** 3,
|
|
45
|
+
inOut: (x) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2),
|
|
46
|
+
step: () => 0,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Two values `f` of the way between them: numbers, colours (OKLab),
|
|
51
|
+
* same-unit lengths, and arrays and objects of them; anything else holds
|
|
52
|
+
* until `f` reaches 1.
|
|
53
|
+
*
|
|
54
|
+
* @param {any} a @param {any} b @param {number} f
|
|
55
|
+
* @returns {any}
|
|
56
|
+
*/
|
|
57
|
+
export function interpolate(a, b, f) {
|
|
58
|
+
if (typeof a === 'number' && typeof b === 'number') return a + (b - a) * f;
|
|
59
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
60
|
+
const la = /^(-?[\d.]+)(vmin|vmax|vw|vh|px|%)$/.exec(a);
|
|
61
|
+
const lb = /^(-?[\d.]+)(vmin|vmax|vw|vh|px|%)$/.exec(b);
|
|
62
|
+
if (la && lb && la[2] === lb[2]) return `${+la[1] + (+lb[1] - +la[1]) * f}${la[2]}`;
|
|
63
|
+
try {
|
|
64
|
+
return mixOklab(toHex(a), toHex(b), f);
|
|
65
|
+
} catch {
|
|
66
|
+
return f < 1 ? a : b;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) return a.map((x, i) => interpolate(x, b[i], f));
|
|
70
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') return Object.fromEntries(Object.keys(a).map((k) => [k, k in b ? interpolate(a[k], b[k], f) : a[k]]));
|
|
71
|
+
return f < 1 ? a : b;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A keyframed value at time `t`.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ keyframes: any[], ease?: string, loop?: boolean | 'pingpong' }} k
|
|
78
|
+
* @param {number} t
|
|
79
|
+
* @param {number} [duration]
|
|
80
|
+
* @returns {any}
|
|
81
|
+
*/
|
|
82
|
+
export function valueAt(k, t, duration) {
|
|
83
|
+
const keys = k.keyframes.map((x) => (Array.isArray(x) ? { t: x[0], value: x[1], ease: x[2] } : x)).sort((a, b) => a.t - b.t);
|
|
84
|
+
if (!keys.length) return undefined;
|
|
85
|
+
const end = duration ?? keys[keys.length - 1].t;
|
|
86
|
+
let time = t;
|
|
87
|
+
if (k.loop && end > 0) {
|
|
88
|
+
time = ((t % end) + end) % end;
|
|
89
|
+
if (k.loop === 'pingpong' && Math.floor(t / end) % 2 === 1) time = end - time;
|
|
90
|
+
}
|
|
91
|
+
if (time <= keys[0].t) return keys[0].value;
|
|
92
|
+
for (let i = 1; i < keys.length; i++) {
|
|
93
|
+
if (time <= keys[i].t) {
|
|
94
|
+
const a = keys[i - 1];
|
|
95
|
+
const b = keys[i];
|
|
96
|
+
const ease = EASE[b.ease ?? k.ease ?? 'inOut'] ?? EASE.inOut;
|
|
97
|
+
return interpolate(a.value, b.value, ease((time - a.t) / Math.max(1e-9, b.t - a.t)));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return keys[keys.length - 1].value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A layer with every keyframed option resolved at `t`.
|
|
105
|
+
*
|
|
106
|
+
* @template T
|
|
107
|
+
* @param {T} v
|
|
108
|
+
* @param {number} t - Seconds.
|
|
109
|
+
* @param {number} [duration] - The cover's `loop` length in seconds (loops wrap at it).
|
|
110
|
+
* @returns {T}
|
|
111
|
+
*/
|
|
112
|
+
export function animate(v, t, duration) {
|
|
113
|
+
if (isKeyed(v)) return valueAt(v, t, duration);
|
|
114
|
+
if (Array.isArray(v)) return /** @type {any} */ (v.map((x) => animate(x, t, duration)));
|
|
115
|
+
if (v && typeof v === 'object' && !(v instanceof Uint8Array) && isAnimated(v)) {
|
|
116
|
+
return /** @type {any} */ (Object.fromEntries(Object.entries(v).map(([k, x]) => [k, animate(x, t, duration)])));
|
|
117
|
+
}
|
|
118
|
+
return v;
|
|
119
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// `marquee`: text on a path running along it.
|
|
2
|
+
import type { ResolvedMotion } from './timing.js';
|
|
3
|
+
import type { Sheet } from './sheet.js';
|
|
4
|
+
|
|
5
|
+
/** The motion, as the text layer gets it. */
|
|
6
|
+
export interface MarqueeMotion extends ResolvedMotion {
|
|
7
|
+
/** Playing (CSS), or a still. */
|
|
8
|
+
live: boolean;
|
|
9
|
+
/** The still's time, seconds (none: at rest). */
|
|
10
|
+
t?: number;
|
|
11
|
+
sheet?: Sheet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Text laid out on its path. */
|
|
15
|
+
export interface MarqueeText {
|
|
16
|
+
/** The shaped line (`glyphs[i].x` from its start). */
|
|
17
|
+
run: { glyphs: { gid: number; x: number; face?: unknown; paint?: string }[] };
|
|
18
|
+
/** Each glyph's middle, from the line's start. */
|
|
19
|
+
mids: number[];
|
|
20
|
+
/** Where the line starts along the path, px. */
|
|
21
|
+
start: number;
|
|
22
|
+
sampler: { length: number; at(s: number): { x: number; y: number; tx: number; ty: number } };
|
|
23
|
+
/** The layer's id in the document (names the glyphs' defs). */
|
|
24
|
+
id: string;
|
|
25
|
+
/** Glyph `i` drawn in its own frame: its middle on the baseline at 0, 0. */
|
|
26
|
+
glyph(i: number): string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The marquee's part: `shift` (px) to move a still's start by, or (playing) the glyphs' defs and copies. */
|
|
30
|
+
export function marquee(m: MarqueeMotion, o: MarqueeText): { shift: number; defs: string; body?: string };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `marquee`: text on a path running along it, one repeat of its words per
|
|
3
|
+
* `duration`, looping seamlessly for as long as it shows (back toward the
|
|
4
|
+
* path's start; `direction: 'reverse'` runs it forward).
|
|
5
|
+
*
|
|
6
|
+
* The text is outlines, so the moving offset can't be a `textPath`
|
|
7
|
+
* attribute. Each glyph is drawn once (in its own frame, centred on the
|
|
8
|
+
* baseline) and placed by copies (`<use>`). Every copy runs the same
|
|
9
|
+
* keyframes, the path sampled as `translate() rotate()` stops, over a track
|
|
10
|
+
* as long as the text; the copies differ only in their delay. So a copy
|
|
11
|
+
* that leaves one end comes back in at the other, a whole number of repeats
|
|
12
|
+
* away, where the same glyph was: the loop never shows a seam. The still
|
|
13
|
+
* (and reduced motion) is each copy's own `transform`.
|
|
14
|
+
*
|
|
15
|
+
* @module animate/marquee
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { num } from '@logomarks/image-effects/svg';
|
|
19
|
+
import { NS } from './sheet.js';
|
|
20
|
+
|
|
21
|
+
/** Most px between two keyframes along the track. */
|
|
22
|
+
const STEP = 8;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The marquee's part in drawing text on a path.
|
|
26
|
+
*
|
|
27
|
+
* @param {import('./marquee.js').MarqueeMotion} m - The motion (and whether it plays, and the still's time).
|
|
28
|
+
* @param {import('./marquee.js').MarqueeText} o - The laid-out text.
|
|
29
|
+
* @returns {{ shift: number, defs: string, body?: string }} `shift`: px to move the start by for a still; `body`: the playing glyphs.
|
|
30
|
+
* @throws {TypeError} When the text doesn't repeat, or the direction alternates.
|
|
31
|
+
*/
|
|
32
|
+
export function marquee(m, o) {
|
|
33
|
+
if (m.direction.startsWith('alternate')) throw new TypeError('motion: a marquee runs one way (direction normal or reverse)');
|
|
34
|
+
const glyphs = o.run.glyphs;
|
|
35
|
+
const period = periodOf(glyphs);
|
|
36
|
+
const unit = glyphs[period].x - glyphs[0].x;
|
|
37
|
+
const dir = m.direction === 'reverse' ? 1 : -1;
|
|
38
|
+
const phase = m.t == null ? 0 : (((m.t - m.delay) / m.duration) % 1 + 1) % 1;
|
|
39
|
+
if (!m.live) return { shift: dir * unit * phase, defs: '' };
|
|
40
|
+
|
|
41
|
+
const units = Math.ceil(glyphs.length / period);
|
|
42
|
+
const span = units * unit;
|
|
43
|
+
// The track: half a repeat before the first glyph, so no copy starts exactly at its end.
|
|
44
|
+
const from = o.start + o.mids[0] - unit / 2;
|
|
45
|
+
const steps = Math.max(8, Math.min(400, Math.ceil(span / STEP)));
|
|
46
|
+
const place = (/** @type {number} */ s) => {
|
|
47
|
+
const at = o.sampler.at(s);
|
|
48
|
+
return { x: at.x, y: at.y, a: (Math.atan2(at.ty, at.tx) * 180) / Math.PI };
|
|
49
|
+
};
|
|
50
|
+
const frames = Array.from({ length: steps + 1 }, (_, i) => {
|
|
51
|
+
const p = place(from + (span * i) / steps);
|
|
52
|
+
return /** @type {[number, string]} */ ([i / steps, `transform:translate(${num(p.x)}px,${num(p.y)}px) rotate(${num(p.a, 2)}deg)`]);
|
|
53
|
+
});
|
|
54
|
+
const loop = units * m.duration;
|
|
55
|
+
const cls = m.sheet.animation(frames, `${num(loop, 3)}s linear 0s infinite ${dir > 0 ? 'normal' : 'reverse'} both`);
|
|
56
|
+
|
|
57
|
+
let defs = '';
|
|
58
|
+
for (let j = 0; j < period; j++) defs += `<g id="${NS}${o.id}q${j}">${o.glyph(j)}</g>`;
|
|
59
|
+
let body = '';
|
|
60
|
+
for (let k = 0; k < units; k++) {
|
|
61
|
+
for (let j = 0; j < period; j++) {
|
|
62
|
+
const s0 = o.start + o.mids[j] + k * unit;
|
|
63
|
+
const f0 = (s0 - from) / span;
|
|
64
|
+
// CSS progress f0 at t = 0, shifted by the motion's own delay; written in (−loop, 0].
|
|
65
|
+
const lead = dir > 0 ? m.delay - f0 * loop : m.delay + f0 * loop;
|
|
66
|
+
const delay = lead - loop * Math.ceil(lead / loop);
|
|
67
|
+
const still = from + ((((s0 - from + dir * unit * phase) % span) + span) % span);
|
|
68
|
+
const p = place(still);
|
|
69
|
+
body += `<use href="#${NS}${o.id}q${j}" class="${cls}" transform="translate(${num(p.x)} ${num(p.y)}) rotate(${num(p.a, 2)})" style="animation-delay:${num(delay, 3)}s"/>`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { shift: 0, defs, body };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The text's repeat, in glyphs: the shortest run the rest repeats, glyph
|
|
77
|
+
* for glyph and at the same spacing.
|
|
78
|
+
*
|
|
79
|
+
* @param {any[]} glyphs
|
|
80
|
+
* @returns {number}
|
|
81
|
+
* @throws {TypeError} When it doesn't repeat at least twice.
|
|
82
|
+
*/
|
|
83
|
+
function periodOf(glyphs) {
|
|
84
|
+
const same = (/** @type {any} */ a, /** @type {any} */ b) => a.gid === b.gid && a.face === b.face && a.paint === b.paint;
|
|
85
|
+
// Twice over, give or take a trailing space.
|
|
86
|
+
for (let p = 1; p * 2 <= glyphs.length + 1; p++) {
|
|
87
|
+
const step = glyphs[p].x - glyphs[0].x;
|
|
88
|
+
let ok = true;
|
|
89
|
+
for (let i = 0; ok && i + p < glyphs.length; i++) ok = same(glyphs[i], glyphs[i + p]) && Math.abs(glyphs[i + p].x - glyphs[i].x - step) < 0.01;
|
|
90
|
+
if (ok) return p;
|
|
91
|
+
}
|
|
92
|
+
throw new TypeError('motion: a marquee needs its words repeated along the path (at least twice), so it can loop');
|
|
93
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// `vary`: layer options drawn frame by frame and played as outline morphs.
|
|
2
|
+
import type { ResolvedMotion } from './timing.js';
|
|
3
|
+
import type { MotionScope } from './motion.js';
|
|
4
|
+
|
|
5
|
+
/** `vary`'s values at progress `p` (0–1), merged into the layer. */
|
|
6
|
+
export function varied<L>(layer: L, vary: Record<string, unknown>, p: number, ease: (x: number) => number): L;
|
|
7
|
+
/** A layer drawn with `vary`: the still, and when live each changing outline keyframed (`d: path()`). */
|
|
8
|
+
export function morph(layer: any, m: ResolvedMotion, o: MotionScope): Promise<{ defs: string; body: string }>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vary`: layer options that change over the motion, drawn frame by frame
|
|
3
|
+
* and played as outline morphs.
|
|
4
|
+
*
|
|
5
|
+
* ```json
|
|
6
|
+
* "motion": { "vary": { "axes": { "wdth": [62, 150] } }, "duration": 2.8, "easing": "inOut", "direction": "alternate" }
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Every leaf of `vary` is a list of stops (numbers, colours, same-unit
|
|
10
|
+
* lengths). The layer is drawn at `frames` points per stop (default 12),
|
|
11
|
+
* with the easing applied; the frames must differ only in their outlines
|
|
12
|
+
* (`d`), which become CSS `d: path(…)` keyframes. Outlines interpolate
|
|
13
|
+
* point by point, which is how a variable font interpolates between its
|
|
14
|
+
* masters, so a width axis breathes as the font itself would draw it
|
|
15
|
+
* (exact at every frame, linear between frames a few percent apart).
|
|
16
|
+
*
|
|
17
|
+
* @module animate/morph
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { atStops } from './tracks.js';
|
|
21
|
+
import { easing, progress, cssTiming } from './timing.js';
|
|
22
|
+
|
|
23
|
+
/** A path's `d` attribute, captured. */
|
|
24
|
+
const D = / d="([^"]*)"/g;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `vary`'s values at progress `p`, merged into the layer.
|
|
28
|
+
*
|
|
29
|
+
* @param {any} layer
|
|
30
|
+
* @param {any} vary
|
|
31
|
+
* @param {number} p
|
|
32
|
+
* @param {(x: number) => number} ease
|
|
33
|
+
* @returns {any}
|
|
34
|
+
*/
|
|
35
|
+
export function varied(layer, vary, p, ease) {
|
|
36
|
+
const merge = (/** @type {any} */ base, /** @type {any} */ v) => {
|
|
37
|
+
if (Array.isArray(v)) return atStops(v, p, ease);
|
|
38
|
+
if (!v || typeof v !== 'object') throw new TypeError('motion: every value in vary is a list of stops ([from, to, …])');
|
|
39
|
+
const out = { ...(base && typeof base === 'object' ? base : {}) };
|
|
40
|
+
for (const [k, x] of Object.entries(v)) out[k] = merge(out[k], x);
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
return merge(layer, vary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A layer drawn with `vary`: a still at `t`, or (live) the still with each
|
|
48
|
+
* outline that changes keyframed.
|
|
49
|
+
*
|
|
50
|
+
* @param {any} layer - Without its motion.
|
|
51
|
+
* @param {import('./timing.js').ResolvedMotion} m
|
|
52
|
+
* @param {import('./motion.js').MotionScope} o
|
|
53
|
+
* @returns {Promise<{ defs: string, body: string }>}
|
|
54
|
+
* @throws {TypeError} When the frames differ in more than their outlines.
|
|
55
|
+
*/
|
|
56
|
+
export async function morph(layer, m, o) {
|
|
57
|
+
const ease = easing(m.easing);
|
|
58
|
+
const still = o.t == null ? layer : varied(layer, m.vary, progress(m, o.t), ease);
|
|
59
|
+
if (!o.live) return o.draw(still);
|
|
60
|
+
const stops = Math.max(...leaves(m.vary).map((l) => l.length));
|
|
61
|
+
const count = Math.max(1, Math.round(m.frames ?? 12)) * Math.max(1, stops - 1);
|
|
62
|
+
/** @type {{ p: number, ds: string[] }[]} */
|
|
63
|
+
const frames = [];
|
|
64
|
+
const name = layer.id ?? layer.type;
|
|
65
|
+
/** @type {string | undefined} */
|
|
66
|
+
let shape;
|
|
67
|
+
const check = (/** @type {{ defs: string, body: string }} */ f) => {
|
|
68
|
+
shape ??= strip(f);
|
|
69
|
+
if (strip(f) !== shape) throw new TypeError(`motion: vary on "${name}" changes more than its outlines, so it can't play as a morph`);
|
|
70
|
+
};
|
|
71
|
+
for (let i = 0; i <= count; i++) {
|
|
72
|
+
const p = i / count;
|
|
73
|
+
const f = await o.draw(varied(layer, m.vary, p, ease));
|
|
74
|
+
check(f);
|
|
75
|
+
frames.push({ p, ds: [...f.body.matchAll(D)].map((x) => x[1]) });
|
|
76
|
+
}
|
|
77
|
+
// The still last, so what the layer leaves behind (its layout box, its stream) is the still's.
|
|
78
|
+
const rest = await o.draw(still);
|
|
79
|
+
check(rest);
|
|
80
|
+
let slot = -1;
|
|
81
|
+
const body = rest.body.replace(D, (whole) => {
|
|
82
|
+
slot += 1;
|
|
83
|
+
const ds = frames.map((f) => f.ds[slot]);
|
|
84
|
+
if (ds.every((d) => d === ds[0])) return whole;
|
|
85
|
+
if (ds.some((d) => commands(d) !== commands(ds[0]))) throw new TypeError(`motion: vary on "${name}" changes an outline's shape (not just its points), so it can't morph`);
|
|
86
|
+
const cls = o.sheet.animation(frames.map((f) => [f.p, `d:path("${f.ds[slot]}")`]), cssTiming(m, { linear: true }));
|
|
87
|
+
return ` class="${cls}"${whole}`;
|
|
88
|
+
});
|
|
89
|
+
return { ...rest, body };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {{ defs: string, body: string }} m @returns {string} Markup with every outline blanked. */
|
|
93
|
+
const strip = (m) => `${m.defs}\n${m.body}`.replace(D, ' d=""');
|
|
94
|
+
|
|
95
|
+
/** @param {string} d @returns {string} A path's commands, without their numbers. */
|
|
96
|
+
const commands = (d) => d.replace(/[^A-Za-z]/g, '');
|
|
97
|
+
|
|
98
|
+
/** @param {any} v @returns {any[][]} Every stop list in `vary`. */
|
|
99
|
+
const leaves = (v) => (Array.isArray(v) ? [v] : Object.values(v ?? {}).flatMap(leaves));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// `motion`: a layer moving in the SVG itself, as CSS keyframes.
|
|
2
|
+
import type { Length, TextPath } from '../index.js';
|
|
3
|
+
import type { Easing } from './timing.js';
|
|
4
|
+
import type { Sheet } from './sheet.js';
|
|
5
|
+
|
|
6
|
+
/** A coordinate: px, `'%'` of the layer's grid, or a length. */
|
|
7
|
+
type Coordinate = Length | `${number}%`;
|
|
8
|
+
|
|
9
|
+
/** A named motion. */
|
|
10
|
+
export type MotionPreset = 'drift' | 'spin' | 'breathe' | 'blink' | 'marquee';
|
|
11
|
+
|
|
12
|
+
/** One motion on a layer. A single value moves from the layer at rest to it; a list is stops, evenly spaced in time. */
|
|
13
|
+
export interface Motion {
|
|
14
|
+
/** Start from a preset's defaults. */
|
|
15
|
+
preset?: MotionPreset;
|
|
16
|
+
/** `[x, y]` to move by (px in the layer's grid, `'%'` of it, or lengths), or points to visit. */
|
|
17
|
+
translate?: [Coordinate, Coordinate] | [Coordinate, Coordinate][];
|
|
18
|
+
/** Travel a path from where the layer is (the easing spans the whole path). */
|
|
19
|
+
path?: string | TextPath;
|
|
20
|
+
/** Degrees to turn to, or a list of angles. */
|
|
21
|
+
rotate?: number | number[];
|
|
22
|
+
/** A factor to grow to, or a list (about `origin`). */
|
|
23
|
+
scale?: number | number[];
|
|
24
|
+
scaleX?: number | number[];
|
|
25
|
+
scaleY?: number | number[];
|
|
26
|
+
/** 0–1 to fade to (times the layer's own opacity), or a list. */
|
|
27
|
+
opacity?: number | number[];
|
|
28
|
+
/** What turns and scales are about: `[x, y]` (px, `'%'` or lengths). Default the centre. */
|
|
29
|
+
origin?: [Coordinate, Coordinate] | { x?: Coordinate; y?: Coordinate } | 'center';
|
|
30
|
+
/** Layer options as lists of stops (`{ axes: { wdth: [62, 150] } }`), drawn frame by frame and played as outline morphs. */
|
|
31
|
+
vary?: Record<string, unknown>;
|
|
32
|
+
/** `vary`: frames drawn per stop. Default 12. */
|
|
33
|
+
frames?: number;
|
|
34
|
+
/** Text on a path runs along it, one repeat of its words per `duration` (the words must repeat). */
|
|
35
|
+
marquee?: boolean;
|
|
36
|
+
/** Seconds per iteration. Default 2 (or the preset's). */
|
|
37
|
+
duration?: number;
|
|
38
|
+
/** Seconds before it starts; negative starts it part-way. */
|
|
39
|
+
delay?: number;
|
|
40
|
+
/** Between each pair of stops. Default `'linear'`. */
|
|
41
|
+
easing?: Easing;
|
|
42
|
+
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
|
43
|
+
/** Default `'infinite'`. */
|
|
44
|
+
iterations?: number | 'infinite';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** How a layer with motion is being drawn. */
|
|
48
|
+
export interface MotionScope {
|
|
49
|
+
/** Playing: CSS keyframes into `sheet`. */
|
|
50
|
+
live: boolean;
|
|
51
|
+
/** The still's time, seconds (none: at rest). */
|
|
52
|
+
t?: number;
|
|
53
|
+
sheet?: Sheet;
|
|
54
|
+
/** The layer drawn with its stream from the start (`extra` joins its context). */
|
|
55
|
+
draw(layer: any, extra?: Record<string, unknown>): Promise<{ defs: string; body: string }>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A layer with its motion: the still (at `t`, or at rest) and, live, the keyframes that play it. */
|
|
59
|
+
export function renderMotion(layer: any, ctx: { canvas: { width: number; height: number } }, o: MotionScope): Promise<{ defs: string; body: string }>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `motion`: a layer moving in the SVG itself, as CSS keyframes, so a plain
|
|
3
|
+
* `<img>` or inline SVG plays it with no script.
|
|
4
|
+
*
|
|
5
|
+
* ```json
|
|
6
|
+
* { "type": "mesh", "motion": { "translate": ["5vw", "-4vw"], "scale": 1.08, "origin": ["22%", "66%"],
|
|
7
|
+
* "duration": 26, "easing": "inOut", "direction": "alternate" } }
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* A layer takes one motion or a list (or a preset name: `drift`, `spin`,
|
|
11
|
+
* `breathe`, `blink`, `marquee`). Each moves the whole layer (its effects
|
|
12
|
+
* and masks with it): `translate` or `path`, `rotate`, `scale`, `opacity`
|
|
13
|
+
* (see ./tracks.js), or changes it: `vary` (outlines morph; ./morph.js) and
|
|
14
|
+
* `marquee` (text running along its path; ./marquee.js). Timing is CSS's:
|
|
15
|
+
* `duration`, `delay` (seconds), `easing`, `direction`, `iterations`.
|
|
16
|
+
*
|
|
17
|
+
* Every element keeps its still as plain attributes, which is what shows
|
|
18
|
+
* under `prefers-reduced-motion` and in renderers without CSS animation:
|
|
19
|
+
* the layer at rest, or the cover's `poster` frame.
|
|
20
|
+
*
|
|
21
|
+
* @module animate/motion
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { attrs, esc } from '@logomarks/image-effects/svg';
|
|
25
|
+
import { motionList, progress, cssTiming, easing } from './timing.js';
|
|
26
|
+
import { tracks, atStops } from './tracks.js';
|
|
27
|
+
import { morph } from './morph.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A layer with its motion: the still at `o.t` (or at rest), and when live,
|
|
31
|
+
* the keyframes that play it.
|
|
32
|
+
*
|
|
33
|
+
* @param {any} layer
|
|
34
|
+
* @param {any} ctx - The layer's render context (its canvas is the layer's grid).
|
|
35
|
+
* @param {import('./motion.js').MotionScope} o
|
|
36
|
+
* @returns {Promise<{ defs: string, body: string }>}
|
|
37
|
+
* @throws {TypeError} On a motion that can't be drawn (see the modules it uses).
|
|
38
|
+
*/
|
|
39
|
+
export async function renderMotion(layer, ctx, o) {
|
|
40
|
+
const list = motionList(layer.motion);
|
|
41
|
+
const blend = layer.blend && layer.blend !== 'normal' ? layer.blend : undefined;
|
|
42
|
+
// Blend goes on the outermost group (a moving group would otherwise blend only with itself).
|
|
43
|
+
const base = { ...layer, motion: undefined, blend: undefined };
|
|
44
|
+
const varies = list.filter((m) => m.vary != null);
|
|
45
|
+
const runs = list.filter((m) => m.marquee);
|
|
46
|
+
if (varies.length + runs.length > 1) throw new TypeError(`motion: "${layer.id ?? layer.type}" takes one vary or marquee`);
|
|
47
|
+
const markup = varies.length
|
|
48
|
+
? await morph(base, varies[0], o)
|
|
49
|
+
: runs.length
|
|
50
|
+
? await o.draw(base, { marquee: { ...runs[0], live: o.live, t: o.t, sheet: o.sheet } })
|
|
51
|
+
: await o.draw(base);
|
|
52
|
+
let body = markup.body;
|
|
53
|
+
for (const m of [...list].reverse()) {
|
|
54
|
+
for (const track of tracks(m, ctx.canvas).reverse()) {
|
|
55
|
+
const still = o.t == null ? null : track.attr(atStops(track.stops, progress(m, o.t), track.sampled ? (x) => x : easing(m.easing)));
|
|
56
|
+
const cls = o.live ? o.sheet.animation(track.stops.map((v, i, all) => [all.length > 1 ? i / (all.length - 1) : 0, track.css(v)]), cssTiming(m, { linear: track.sampled })) : undefined;
|
|
57
|
+
const a = attrs({ class: cls, ...still });
|
|
58
|
+
if (a) body = `<g${a}>${body}</g>`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (blend) body = `<g style="mix-blend-mode:${esc(blend)}">${body}</g>`;
|
|
62
|
+
return { ...markup, body };
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The stylesheet an animated SVG carries.
|
|
2
|
+
|
|
3
|
+
/** The placeholder prefix animation names carry until the document is named. */
|
|
4
|
+
export const NS: string;
|
|
5
|
+
|
|
6
|
+
/** Keyframes and animation rules, each written once. */
|
|
7
|
+
export interface Sheet {
|
|
8
|
+
/** Add an animation: `[progress 0–1, CSS declarations]` frames and its timing. Returns the class attribute for the element. */
|
|
9
|
+
animation(frames: [number, string][], timing: string): string;
|
|
10
|
+
/** Nothing added yet. */
|
|
11
|
+
readonly empty: boolean;
|
|
12
|
+
/** The stylesheet, with the `prefers-reduced-motion` rule that stops it. */
|
|
13
|
+
css(): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A stylesheet to collect animations into. */
|
|
17
|
+
export function createSheet(): Sheet;
|
|
18
|
+
/** Give a finished document's animation names their prefix (`cv` and a hash of the document). */
|
|
19
|
+
export function nameSheet(svg: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stylesheet an animated SVG carries: its keyframes and animation
|
|
3
|
+
* rules (each written once, however many elements share it), and a
|
|
4
|
+
* `prefers-reduced-motion` rule that stops them all, so what shows is the
|
|
5
|
+
* SVG's own attributes: the still.
|
|
6
|
+
*
|
|
7
|
+
* Names are written with a placeholder prefix while the cover is drawn and
|
|
8
|
+
* given the document's hash at the end ({@link nameSheet}), so two covers
|
|
9
|
+
* inlined in one page never share a name unless they're the same cover.
|
|
10
|
+
*
|
|
11
|
+
* @module animate/sheet
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { num } from '@logomarks/image-effects/svg';
|
|
15
|
+
import { hashString } from '@logomarks/image-effects/rng';
|
|
16
|
+
|
|
17
|
+
/** The placeholder for the document's prefix (outside base64's alphabet). */
|
|
18
|
+
export const NS = '__cv__';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A stylesheet to collect animations into.
|
|
22
|
+
*
|
|
23
|
+
* @returns {import('./sheet.js').Sheet}
|
|
24
|
+
*/
|
|
25
|
+
export function createSheet() {
|
|
26
|
+
/** @type {Map<string, string>} keyframes body → name */
|
|
27
|
+
const keyframes = new Map();
|
|
28
|
+
/** @type {Map<string, string>} animation → class */
|
|
29
|
+
const rules = new Map();
|
|
30
|
+
return {
|
|
31
|
+
animation(frames, timing) {
|
|
32
|
+
const body = frames.map(([p, decl]) => `${num(p * 100, 3)}%{${decl}}`).join('');
|
|
33
|
+
let name = keyframes.get(body);
|
|
34
|
+
if (!name) keyframes.set(body, (name = `${NS}k${keyframes.size}`));
|
|
35
|
+
const anim = `${name} ${timing}`;
|
|
36
|
+
let cls = rules.get(anim);
|
|
37
|
+
if (!cls) rules.set(anim, (cls = `${NS}a${rules.size}`));
|
|
38
|
+
return `${NS} ${cls}`;
|
|
39
|
+
},
|
|
40
|
+
get empty() {
|
|
41
|
+
return rules.size === 0;
|
|
42
|
+
},
|
|
43
|
+
css() {
|
|
44
|
+
let css = `.${NS}{transform-box:view-box;transform-origin:0 0}`;
|
|
45
|
+
for (const [body, name] of keyframes) css += `@keyframes ${name}{${body}}`;
|
|
46
|
+
for (const [anim, cls] of rules) css += `.${cls}{animation:${anim}}`;
|
|
47
|
+
return `${css}@media (prefers-reduced-motion:reduce){.${NS}{animation:none!important}}`;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Give a finished document's animation names their prefix: `cv` and a hash
|
|
54
|
+
* of the document.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} svg
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function nameSheet(svg) {
|
|
60
|
+
return svg.includes(NS) ? svg.replaceAll(NS, `cv${hashString(svg).toString(36)}`) : svg;
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Motion timing: presets, defaults, easing, and where a motion is at any time (CSS semantics).
|
|
2
|
+
import type { Motion, MotionPreset } from './motion.js';
|
|
3
|
+
|
|
4
|
+
/** An easing: CSS's named curves (`in`, `out`, `inOut` are ease-in, ease-out, ease-in-out), a hold (`step`), or a cubic Bézier. */
|
|
5
|
+
export type Easing = 'linear' | 'ease' | 'in' | 'out' | 'inOut' | 'step' | [number, number, number, number];
|
|
6
|
+
|
|
7
|
+
/** A motion with its preset expanded and every timing option filled in. */
|
|
8
|
+
export interface ResolvedMotion extends Omit<Motion, 'iterations'> {
|
|
9
|
+
duration: number;
|
|
10
|
+
delay: number;
|
|
11
|
+
easing: Easing;
|
|
12
|
+
direction: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
|
13
|
+
/** `Infinity` for `'infinite'`. */
|
|
14
|
+
iterations: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Named motions: each is a motion's defaults. */
|
|
18
|
+
export const MOTION_PRESETS: Readonly<Record<MotionPreset, Readonly<Motion>>>;
|
|
19
|
+
/** A layer's `motion` as resolved motions (presets expanded, defaults filled in); throws on an unknown preset, direction or easing. */
|
|
20
|
+
export function motionList(v: unknown): ResolvedMotion[];
|
|
21
|
+
/** An easing as a function on 0–1. */
|
|
22
|
+
export function easing(e: Easing): (x: number) => number;
|
|
23
|
+
/** An easing as CSS writes it. */
|
|
24
|
+
export function cssEasing(e: Easing): string;
|
|
25
|
+
/** How far through its stops a motion is at `t` seconds (0–1, direction applied; `animation-fill-mode: both`). */
|
|
26
|
+
export function progress(m: ResolvedMotion, t: number): number;
|
|
27
|
+
/** A motion's CSS `animation` timing (everything but the name); `linear` when its stops were sampled eased. */
|
|
28
|
+
export function cssTiming(m: ResolvedMotion, o?: { linear?: boolean; delay?: number }): string;
|