@arraypress/covers 1.1.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.
Files changed (244) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +18 -809
  3. package/package.json +2 -1
  4. package/recipes/flyers/afterglow-motion.json +540 -0
  5. package/recipes/flyers/afterglow.json +70 -0
  6. package/recipes/flyers/arcfield-mirror-motion.json +397 -0
  7. package/recipes/flyers/arcfield-mirror.json +364 -0
  8. package/recipes/flyers/bauhaus-oktav.json +319 -0
  9. package/recipes/flyers/bauhaus-ritmo.json +735 -0
  10. package/recipes/flyers/bauhaus-volta.json +394 -0
  11. package/recipes/flyers/bloomfall.json +99 -0
  12. package/recipes/flyers/drag-solenne-motion.json +444 -0
  13. package/recipes/flyers/drag-solenne.json +426 -0
  14. package/recipes/flyers/fennick-patches.json +874 -0
  15. package/recipes/flyers/ferro-pass.json +398 -0
  16. package/recipes/flyers/ferrova.json +812 -0
  17. package/recipes/flyers/grid-rest-rhythm.json +125 -0
  18. package/recipes/flyers/groovy-honeycomb.json +816 -0
  19. package/recipes/flyers/groovy-marigold.json +479 -0
  20. package/recipes/flyers/groovy-peachfuzz.json +974 -0
  21. package/recipes/flyers/halden-orbit-motion.json +381 -0
  22. package/recipes/flyers/halden-orbit.json +356 -0
  23. package/recipes/flyers/halvard.json +120 -0
  24. package/recipes/flyers/kairo-weekender.json +696 -0
  25. package/recipes/flyers/linger-motion.json +518 -0
  26. package/recipes/flyers/linger.json +67 -0
  27. package/recipes/flyers/little-orchard.json +105 -0
  28. package/recipes/flyers/lofi-hexmoth.json +912 -0
  29. package/recipes/flyers/lofi-lowbit-motion.json +888 -0
  30. package/recipes/flyers/lofi-lowbit.json +880 -0
  31. package/recipes/flyers/lofi-nullpoint.json +3013 -0
  32. package/recipes/flyers/lumio-stack-motion.json +648 -0
  33. package/recipes/flyers/lumio-stack.json +438 -0
  34. package/recipes/flyers/magazine-cover.json +168 -0
  35. package/recipes/flyers/magazine-extended.json +269 -0
  36. package/recipes/flyers/magazine-knockout.json +40 -0
  37. package/recipes/flyers/northfold-varsity.json +618 -0
  38. package/recipes/flyers/open-studio.json +69 -0
  39. package/recipes/flyers/orrin-merch.json +715 -0
  40. package/recipes/flyers/oxyde.json +113 -0
  41. package/recipes/flyers/quiet-fields.json +610 -0
  42. package/recipes/flyers/scrapbook-halcyon.json +948 -0
  43. package/recipes/flyers/solenne-ticket.json +730 -0
  44. package/recipes/flyers/solvane-motion.json +576 -0
  45. package/recipes/flyers/solvane.json +98 -0
  46. package/recipes/flyers/subduct.json +99 -0
  47. package/recipes/flyers/sunvale-lineup.json +584 -0
  48. package/recipes/flyers/tidewell.json +91 -0
  49. package/recipes/flyers/tour-kairo.json +535 -0
  50. package/recipes/flyers/velora-stickers.json +885 -0
  51. package/recipes/flyers/wheatpaste-black.json +313 -0
  52. package/recipes/flyers/wheatpaste-mark.json +198 -0
  53. package/recipes/flyers/wheatpaste-torn.json +114 -0
  54. package/recipes/flyers/wheatpaste-wall.json +984 -0
  55. package/recipes/flyers/wheatpaste-words.json +233 -0
  56. package/recipes/release/aura.json +137 -0
  57. package/recipes/release/chrome.json +433 -0
  58. package/recipes/release/collage.json +372 -0
  59. package/recipes/release/duotone.json +113 -0
  60. package/recipes/release/film-still.json +102 -0
  61. package/recipes/release/headline.json +70 -0
  62. package/recipes/release/instant.json +170 -0
  63. package/recipes/release/label.json +364 -0
  64. package/recipes/release/liquid-glass.json +301 -0
  65. package/recipes/release/odd-tile-dark.json +688 -0
  66. package/recipes/release/odd-tile-dense.json +1056 -0
  67. package/recipes/release/odd-tile-shift.json +541 -0
  68. package/recipes/release/odd-tile-split.json +571 -0
  69. package/recipes/release/odd-tile.json +633 -0
  70. package/recipes/release/off-edge.json +96 -0
  71. package/recipes/release/plinth.json +794 -0
  72. package/recipes/release/riso.json +108 -0
  73. package/recipes/release/series/fluid-label.json +561 -0
  74. package/recipes/release/series/fluid-pixel.json +334 -0
  75. package/recipes/release/series/hairline.json +305 -0
  76. package/recipes/release/series/halo.json +282 -0
  77. package/recipes/release/series/mark-crop.json +135 -0
  78. package/recipes/release/series/mark-window.json +150 -0
  79. package/recipes/release/series/photo-label.json +475 -0
  80. package/recipes/release/series/smear-field.json +345 -0
  81. package/recipes/release/series/smear.json +202 -0
  82. package/recipes/release/series/spin.json +172 -0
  83. package/recipes/release/series/split-ghost.json +230 -0
  84. package/recipes/release/series/zoom.json +168 -0
  85. package/recipes/release/small-print.json +114 -0
  86. package/recipes/release/stack.json +156 -0
  87. package/recipes/release/swiss.json +117 -0
  88. package/schema/recipe.schema.json +332 -65
  89. package/src/animate/index.d.ts +9 -19
  90. package/src/animate/index.js +6 -111
  91. package/src/animate/keyframes.d.ts +21 -0
  92. package/src/animate/keyframes.js +119 -0
  93. package/src/animate/marquee.d.ts +30 -0
  94. package/src/animate/marquee.js +93 -0
  95. package/src/animate/morph.d.ts +8 -0
  96. package/src/animate/morph.js +99 -0
  97. package/src/animate/motion.d.ts +59 -0
  98. package/src/animate/motion.js +63 -0
  99. package/src/animate/sheet.d.ts +19 -0
  100. package/src/animate/sheet.js +61 -0
  101. package/src/animate/timing.d.ts +28 -0
  102. package/src/animate/timing.js +146 -0
  103. package/src/animate/tracks.d.ts +18 -0
  104. package/src/animate/tracks.js +139 -0
  105. package/src/diagram.js +4 -3
  106. package/src/extract.js +2 -2
  107. package/src/fonts.js +1 -2
  108. package/src/geometry.js +4 -34
  109. package/src/grid.js +1 -1
  110. package/src/handmade.js +9 -7
  111. package/src/icons.js +1 -2
  112. package/src/index.d.ts +39 -48
  113. package/src/index.js +5 -5
  114. package/src/inter-data.js +1 -1
  115. package/src/json-recipe.js +23 -7
  116. package/src/layer-options.js +21 -1
  117. package/src/layers/barcode.js +3 -3
  118. package/src/layers/geometry.js +5 -5
  119. package/src/layers/perforation.js +3 -3
  120. package/src/layers/repeat.js +2 -2
  121. package/src/layers/shape.d.ts +2 -2
  122. package/src/layers/shape.js +5 -5
  123. package/src/layers/soft.js +2 -2
  124. package/src/layers/svg.js +2 -3
  125. package/src/layers/table.js +3 -3
  126. package/src/layers/tear.d.ts +1 -1
  127. package/src/layers/tear.js +4 -4
  128. package/src/layers/texture.js +3 -3
  129. package/src/layers/util.js +15 -2
  130. package/src/layers.js +10 -26
  131. package/src/{effects/paint.js → paint/index.js} +5 -5
  132. package/src/{effects/paint-presets.d.ts → paint/presets.d.ts} +1 -1
  133. package/src/{effects/paint-presets.js → paint/presets.js} +1 -1
  134. package/src/{effects → paint}/tile.d.ts +1 -1
  135. package/src/{effects → paint}/tile.js +4 -4
  136. package/src/palette.js +1 -1
  137. package/src/platform/files.d.ts +6 -0
  138. package/src/platform/files.js +41 -0
  139. package/src/platform/harfbuzz.js +1 -1
  140. package/src/platform/index.d.ts +3 -3
  141. package/src/platform/index.js +6 -4
  142. package/src/platform/load.js +1 -2
  143. package/src/print.js +9 -7
  144. package/src/recipes.js +1 -1
  145. package/src/render/backdrop.js +2 -2
  146. package/src/render/grain.js +1 -1
  147. package/src/render/group.js +3 -3
  148. package/src/render/key.js +1 -2
  149. package/src/{effects → render}/transform.d.ts +2 -1
  150. package/src/{effects → render}/transform.js +17 -5
  151. package/src/render/use.d.ts +1 -1
  152. package/src/render/use.js +2 -2
  153. package/src/render/wrap.js +5 -60
  154. package/src/render.js +55 -18
  155. package/src/text.js +1 -1
  156. package/src/type/auto-color.js +1 -1
  157. package/src/type/glyphs.js +2 -2
  158. package/src/type/line.js +1 -1
  159. package/src/type/live.js +1 -1
  160. package/src/type/path.js +2 -2
  161. package/src/type/pixel.d.ts +0 -2
  162. package/src/type/pixel.js +3 -33
  163. package/src/type/spans.js +2 -2
  164. package/src/type/typeset.js +19 -8
  165. package/src/version.js +1 -1
  166. package/src/color.js +0 -246
  167. package/src/effects/ascii.d.ts +0 -7
  168. package/src/effects/ascii.js +0 -93
  169. package/src/effects/buffer.d.ts +0 -60
  170. package/src/effects/buffer.js +0 -472
  171. package/src/effects/detail.d.ts +0 -12
  172. package/src/effects/detail.js +0 -124
  173. package/src/effects/digital.d.ts +0 -14
  174. package/src/effects/digital.js +0 -216
  175. package/src/effects/distort.d.ts +0 -20
  176. package/src/effects/distort.js +0 -314
  177. package/src/effects/dither.d.ts +0 -8
  178. package/src/effects/dither.js +0 -147
  179. package/src/effects/drop-shadow.d.ts +0 -4
  180. package/src/effects/drop-shadow.js +0 -45
  181. package/src/effects/extrude.d.ts +0 -5
  182. package/src/effects/extrude.js +0 -55
  183. package/src/effects/film.d.ts +0 -11
  184. package/src/effects/film.js +0 -204
  185. package/src/effects/glow.d.ts +0 -8
  186. package/src/effects/glow.js +0 -56
  187. package/src/effects/grade.d.ts +0 -30
  188. package/src/effects/grade.js +0 -274
  189. package/src/effects/halftone.d.ts +0 -8
  190. package/src/effects/halftone.js +0 -95
  191. package/src/effects/index.d.ts +0 -125
  192. package/src/effects/index.js +0 -277
  193. package/src/effects/ink.d.ts +0 -12
  194. package/src/effects/ink.js +0 -70
  195. package/src/effects/lens.d.ts +0 -14
  196. package/src/effects/lens.js +0 -277
  197. package/src/effects/light.d.ts +0 -10
  198. package/src/effects/light.js +0 -59
  199. package/src/effects/looks.d.ts +0 -9
  200. package/src/effects/looks.js +0 -74
  201. package/src/effects/mask.d.ts +0 -52
  202. package/src/effects/mask.js +0 -314
  203. package/src/effects/motion.d.ts +0 -6
  204. package/src/effects/motion.js +0 -60
  205. package/src/effects/newsprint.d.ts +0 -6
  206. package/src/effects/newsprint.js +0 -89
  207. package/src/effects/optics.d.ts +0 -10
  208. package/src/effects/optics.js +0 -213
  209. package/src/effects/options.d.ts +0 -6
  210. package/src/effects/options.js +0 -106
  211. package/src/effects/pixels.d.ts +0 -24
  212. package/src/effects/pixels.js +0 -138
  213. package/src/effects/polar.d.ts +0 -10
  214. package/src/effects/polar.js +0 -265
  215. package/src/effects/region.d.ts +0 -8
  216. package/src/effects/region.js +0 -136
  217. package/src/effects/round-border.d.ts +0 -5
  218. package/src/effects/round-border.js +0 -62
  219. package/src/effects/screen.d.ts +0 -14
  220. package/src/effects/screen.js +0 -212
  221. package/src/effects/sort.d.ts +0 -8
  222. package/src/effects/sort.js +0 -125
  223. package/src/effects/symmetry.d.ts +0 -6
  224. package/src/effects/symmetry.js +0 -74
  225. package/src/effects/tone.d.ts +0 -16
  226. package/src/effects/tone.js +0 -117
  227. package/src/effects/util.d.ts +0 -16
  228. package/src/effects/util.js +0 -85
  229. package/src/platform/bytes.d.ts +0 -22
  230. package/src/platform/bytes.js +0 -103
  231. package/src/platform/compress.d.ts +0 -12
  232. package/src/platform/compress.js +0 -81
  233. package/src/platform/node.d.ts +0 -26
  234. package/src/platform/node.js +0 -86
  235. package/src/png.js +0 -126
  236. package/src/raster/index.d.ts +0 -24
  237. package/src/raster/index.js +0 -171
  238. package/src/render/content.d.ts +0 -5
  239. package/src/render/content.js +0 -58
  240. package/src/render/tiles.d.ts +0 -6
  241. package/src/render/tiles.js +0 -77
  242. package/src/rng.js +0 -95
  243. package/src/svg.js +0 -69
  244. /package/src/{effects/paint.d.ts → paint/index.d.ts} +0 -0
@@ -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;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Motion timing: presets, defaults, easing curves, and where a motion is at
3
+ * any time, with CSS animation semantics (so a still drawn at `t` matches
4
+ * what a browser shows at `t`).
5
+ *
6
+ * @module animate/timing
7
+ */
8
+
9
+ import { num } from '@logomarks/image-effects/svg';
10
+
11
+ /** Named motions: each is a motion's defaults, under whatever else you set. */
12
+ export const MOTION_PRESETS = Object.freeze({
13
+ drift: Object.freeze({ translate: ['3vmin', '-2vmin'], duration: 20, easing: 'inOut', direction: 'alternate' }),
14
+ spin: Object.freeze({ rotate: 360, duration: 30, easing: 'linear' }),
15
+ breathe: Object.freeze({ scale: 1.06, duration: 3, easing: 'inOut', direction: 'alternate' }),
16
+ blink: Object.freeze({ opacity: [1, 0, 1], duration: 1, easing: 'step' }),
17
+ marquee: Object.freeze({ marquee: true, duration: 10, easing: 'linear' }),
18
+ });
19
+
20
+ /** Timing defaults. */
21
+ const DEFAULTS = { duration: 2, delay: 0, easing: 'linear', direction: 'normal', iterations: Infinity };
22
+
23
+ /** Directions, as CSS names them. */
24
+ const DIRECTIONS = ['normal', 'reverse', 'alternate', 'alternate-reverse'];
25
+
26
+ /** Named easing curves as cubic Béziers (CSS's), and the step. */
27
+ const CURVES = /** @type {Record<string, [number, number, number, number]>} */ ({
28
+ linear: [0, 0, 1, 1],
29
+ ease: [0.25, 0.1, 0.25, 1],
30
+ in: [0.42, 0, 1, 1],
31
+ out: [0, 0, 0.58, 1],
32
+ inOut: [0.42, 0, 0.58, 1],
33
+ });
34
+
35
+ /**
36
+ * A layer's `motion` as a list of resolved motions: presets expanded,
37
+ * timing defaults filled in, `iterations: 'infinite'` as `Infinity`.
38
+ *
39
+ * @param {unknown} v - A motion, a preset name, or a list of them.
40
+ * @returns {import('./timing.js').ResolvedMotion[]}
41
+ * @throws {TypeError} On an unknown preset, direction or easing, or a duration that isn't positive.
42
+ */
43
+ export function motionList(v) {
44
+ return (Array.isArray(v) ? v : [v]).filter((m) => m != null && m !== false).map((m) => {
45
+ const own = typeof m === 'string' ? { preset: m } : /** @type {any} */ (m);
46
+ const preset = own.preset == null ? {} : MOTION_PRESETS[/** @type {keyof typeof MOTION_PRESETS} */ (own.preset)];
47
+ if (!preset) throw new TypeError(`Unknown motion preset "${own.preset}" (${Object.keys(MOTION_PRESETS).join(', ')})`);
48
+ const r = { ...DEFAULTS, ...preset, ...own };
49
+ if (r.iterations === 'infinite') r.iterations = Infinity;
50
+ if (!(r.duration > 0)) throw new TypeError(`motion: duration must be a positive number of seconds, got ${r.duration}`);
51
+ if (!(r.iterations > 0)) throw new TypeError(`motion: iterations must be positive or "infinite", got ${r.iterations}`);
52
+ if (!DIRECTIONS.includes(r.direction)) throw new TypeError(`motion: unknown direction "${r.direction}" (${DIRECTIONS.join(', ')})`);
53
+ easing(r.easing);
54
+ return r;
55
+ });
56
+ }
57
+
58
+ /**
59
+ * An easing as a function on 0–1.
60
+ *
61
+ * @param {import('./timing.js').Easing} e
62
+ * @returns {(x: number) => number}
63
+ * @throws {TypeError} On an unknown name.
64
+ */
65
+ export function easing(e) {
66
+ if (e === 'step') return (x) => (x >= 1 ? 1 : 0);
67
+ const c = Array.isArray(e) ? e : CURVES[/** @type {string} */ (e)];
68
+ if (!c || c.length !== 4) throw new TypeError(`motion: unknown easing ${JSON.stringify(e)} (${[...Object.keys(CURVES), 'step'].join(', ')}, or [x1, y1, x2, y2])`);
69
+ return bezier(c[0], c[1], c[2], c[3]);
70
+ }
71
+
72
+ /**
73
+ * An easing as CSS writes it.
74
+ *
75
+ * @param {import('./timing.js').Easing} e
76
+ * @returns {string}
77
+ */
78
+ export function cssEasing(e) {
79
+ if (e === 'step') return 'steps(1,end)';
80
+ if (e === 'linear') return 'linear';
81
+ const [a, b, c, d] = Array.isArray(e) ? e : CURVES[/** @type {string} */ (e)];
82
+ return `cubic-bezier(${[a, b, c, d].map((v) => num(v, 4)).join(',')})`;
83
+ }
84
+
85
+ /**
86
+ * A CSS cubic-bezier timing function, solved for y at x.
87
+ *
88
+ * @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2
89
+ * @returns {(x: number) => number}
90
+ */
91
+ function bezier(x1, y1, x2, y2) {
92
+ const at = (/** @type {number} */ t, /** @type {number} */ a, /** @type {number} */ b) => 3 * a * t * (1 - t) ** 2 + 3 * b * t * t * (1 - t) + t ** 3;
93
+ return (x) => {
94
+ if (x <= 0) return 0;
95
+ if (x >= 1) return 1;
96
+ let lo = 0;
97
+ let hi = 1;
98
+ let t = x;
99
+ for (let i = 0; i < 40; i++) {
100
+ const v = at(t, x1, x2);
101
+ if (Math.abs(v - x) < 1e-7) break;
102
+ if (v < x) lo = t; else hi = t;
103
+ t = (lo + hi) / 2;
104
+ }
105
+ return at(t, y1, y2);
106
+ };
107
+ }
108
+
109
+ /**
110
+ * How far through its stops a motion is at `t` seconds (0–1, direction
111
+ * applied), as CSS plays it with `animation-fill-mode: both`: before its
112
+ * delay it holds the start, after its last iteration the end.
113
+ *
114
+ * @param {import('./timing.js').ResolvedMotion} m
115
+ * @param {number} t
116
+ * @returns {number}
117
+ */
118
+ export function progress(m, t) {
119
+ const local = t - m.delay;
120
+ let iteration = 0;
121
+ let p = 0;
122
+ if (local >= m.duration * m.iterations) {
123
+ iteration = Math.ceil(m.iterations) - 1;
124
+ p = m.iterations - iteration;
125
+ } else if (local > 0) {
126
+ const whole = local / m.duration;
127
+ iteration = Math.floor(whole);
128
+ p = whole - iteration;
129
+ }
130
+ const back = m.direction === 'reverse'
131
+ || (m.direction === 'alternate' && iteration % 2 === 1)
132
+ || (m.direction === 'alternate-reverse' && iteration % 2 === 0);
133
+ return back ? 1 - p : p;
134
+ }
135
+
136
+ /**
137
+ * A motion's CSS `animation` timing (everything but the name).
138
+ *
139
+ * @param {import('./timing.js').ResolvedMotion} m
140
+ * @param {{ linear?: boolean, delay?: number }} [o] - `linear`: the stops were sampled with the easing already applied.
141
+ * @returns {string}
142
+ */
143
+ export function cssTiming(m, o = {}) {
144
+ const iterations = m.iterations === Infinity ? 'infinite' : num(m.iterations, 3);
145
+ return `${num(m.duration, 3)}s ${o.linear ? 'linear' : cssEasing(m.easing)} ${num(o.delay ?? m.delay, 3)}s ${iterations} ${m.direction} both`;
146
+ }
@@ -0,0 +1,18 @@
1
+ // A motion's transform and opacity as tracks of stops.
2
+ import type { ResolvedMotion } from './timing.js';
3
+
4
+ /** One animated property: its stops (evenly spaced in time) and how a value is written. */
5
+ export interface Track<T = any> {
6
+ stops: T[];
7
+ /** Stops sampled along a path with the easing applied (CSS plays them linearly). */
8
+ sampled?: boolean;
9
+ /** A value as a CSS declaration, for keyframes. */
10
+ css(v: T): string;
11
+ /** A value as SVG attributes, for the still (`null` when it's the layer at rest). */
12
+ attr(v: T): Record<string, string> | null;
13
+ }
14
+
15
+ /** A value at progress `p` (0–1) through evenly spaced stops, eased between each pair. */
16
+ export function atStops<T>(stops: T[], p: number, ease: (x: number) => number): T;
17
+ /** A motion's tracks, outermost first: translate (or path), rotate, scale, opacity. */
18
+ export function tracks(m: ResolvedMotion, canvas: { width: number; height: number }): Track[];
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A motion's transform and opacity as tracks: each is a list of stops (a
3
+ * value per stop, evenly spaced in time) and how one value is written, as a
4
+ * CSS declaration for keyframes and as SVG attributes for a still.
5
+ *
6
+ * - `translate`: `[x, y]` to move by (from where the layer is), or a list
7
+ * of points to visit (`[[0, 0], [x, y], …]`); px in the layer's grid, `'%'`
8
+ * of it, or lengths (`'4vw'`).
9
+ * - `path`: travel a path from where the layer is (`{ d }` or a text-path
10
+ * kind: `circle`, `ellipse`, `arc`, `wave`); the easing spans the whole path.
11
+ * - `rotate`: degrees to turn to, or a list of angles.
12
+ * - `scale`, `scaleX`, `scaleY`: a factor to grow to, or a list of factors.
13
+ * - `opacity`: 0–1 to fade to (multiplying the layer's own), or a list.
14
+ *
15
+ * Turns and scales are about `origin` (`[x, y]`, default the centre).
16
+ *
17
+ * @module animate/tracks
18
+ */
19
+
20
+ import { num } from '@logomarks/image-effects/svg';
21
+ import { textPathD, pathSampler } from '../type/path.js';
22
+ import { pos } from '../layers/util.js';
23
+ import { originOf } from '../render/transform.js';
24
+ import { interpolate } from './keyframes.js';
25
+ import { easing } from './timing.js';
26
+
27
+ /**
28
+ * A value at progress `p` (0–1) through evenly spaced stops, eased between
29
+ * each pair (as CSS eases between keyframes).
30
+ *
31
+ * @param {any[]} stops
32
+ * @param {number} p
33
+ * @param {(x: number) => number} ease
34
+ * @returns {any}
35
+ */
36
+ export function atStops(stops, p, ease) {
37
+ if (stops.length === 1) return stops[0];
38
+ const x = Math.max(0, Math.min(1, p)) * (stops.length - 1);
39
+ const k = Math.min(Math.floor(x), stops.length - 2);
40
+ return interpolate(stops[k], stops[k + 1], ease(x - k));
41
+ }
42
+
43
+ /**
44
+ * A scalar option as stops: a list as given, one value as rest → value.
45
+ *
46
+ * @param {unknown} v @param {number} rest
47
+ * @returns {number[] | undefined}
48
+ */
49
+ const scalar = (v, rest) => (v == null ? undefined : Array.isArray(v) ? v.map(Number) : [rest, Number(v)]);
50
+
51
+ /**
52
+ * A motion's tracks, outermost first: translate (or path), rotate, scale,
53
+ * opacity.
54
+ *
55
+ * @param {import('./timing.js').ResolvedMotion} m
56
+ * @param {{ width: number, height: number }} canvas - The layer's grid.
57
+ * @returns {import('./tracks.js').Track[]}
58
+ * @throws {TypeError} When `translate` and `path` are both set, or scale lists differ in length.
59
+ */
60
+ export function tracks(m, canvas) {
61
+ /** @type {import('./tracks.js').Track[]} */
62
+ const out = [];
63
+ const px = (/** @type {any} */ p) => [pos(p[0] ?? 0, 'x', canvas), pos(p[1] ?? 0, 'y', canvas)];
64
+ if (m.translate != null && m.path != null) throw new TypeError('motion: give translate or path, not both');
65
+ if (m.translate != null) {
66
+ const list = Array.isArray(m.translate[0]) ? m.translate : [[0, 0], m.translate];
67
+ out.push(translateTrack(list.map(px), false));
68
+ } else if (m.path != null) {
69
+ out.push(translateTrack(pathStops(m.path, canvas, easing(m.easing)), true));
70
+ }
71
+ const [ox, oy] = originOf(m.origin, canvas);
72
+ const cssAbout = (/** @type {string} */ fn) => `translate(${num(ox)}px,${num(oy)}px) ${fn} translate(${num(-ox)}px,${num(-oy)}px)`;
73
+ const svgAbout = (/** @type {string} */ fn) => `translate(${num(ox)} ${num(oy)}) ${fn} translate(${num(-ox)} ${num(-oy)})`;
74
+ const rotate = scalar(m.rotate, 0);
75
+ if (rotate) {
76
+ out.push({
77
+ stops: rotate,
78
+ css: (a) => `transform:${cssAbout(`rotate(${num(a, 3)}deg)`)}`,
79
+ attr: (a) => (a ? { transform: `rotate(${num(a, 3)} ${num(ox)} ${num(oy)})` } : null),
80
+ });
81
+ }
82
+ const s = scalar(m.scale, 1);
83
+ const sx = scalar(m.scaleX, 1);
84
+ const sy = scalar(m.scaleY, 1);
85
+ if (s || sx || sy) {
86
+ const n = Math.max(s?.length ?? 0, sx?.length ?? 0, sy?.length ?? 0);
87
+ if ([s, sx, sy].some((l) => l && l.length !== n)) throw new TypeError('motion: scale, scaleX and scaleY need the same number of stops');
88
+ const stops = Array.from({ length: n }, (_, i) => [(s?.[i] ?? 1) * (sx?.[i] ?? 1), (s?.[i] ?? 1) * (sy?.[i] ?? 1)]);
89
+ out.push({
90
+ stops,
91
+ css: ([a, b]) => `transform:${cssAbout(`scale(${num(a, 4)},${num(b, 4)})`)}`,
92
+ attr: ([a, b]) => (a === 1 && b === 1 ? null : { transform: svgAbout(`scale(${num(a, 4)} ${num(b, 4)})`) }),
93
+ });
94
+ }
95
+ const opacity = scalar(m.opacity, 1);
96
+ if (opacity) {
97
+ out.push({
98
+ stops: opacity,
99
+ css: (o) => `opacity:${num(o, 3)}`,
100
+ attr: (o) => (o === 1 ? null : { opacity: num(Math.max(0, o), 3) }),
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * A translate track.
108
+ *
109
+ * @param {[number, number][]} stops - px.
110
+ * @param {boolean} sampled - Stops sampled along a path (eased already).
111
+ * @returns {import('./tracks.js').Track}
112
+ */
113
+ function translateTrack(stops, sampled) {
114
+ return {
115
+ stops,
116
+ sampled,
117
+ css: ([x, y]) => `transform:translate(${num(x)}px,${num(y)}px)`,
118
+ attr: ([x, y]) => (x || y ? { transform: `translate(${num(x)} ${num(y)})` } : null),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Points along a path, relative to its start, sampled evenly in time with
124
+ * the easing applied over the whole path.
125
+ *
126
+ * @param {any} path - `{ d }`, a text-path kind, or path data.
127
+ * @param {{ width: number, height: number }} canvas
128
+ * @param {(x: number) => number} ease
129
+ * @returns {[number, number][]}
130
+ */
131
+ function pathStops(path, canvas, ease) {
132
+ const sampler = pathSampler(textPathD(path, canvas));
133
+ const n = Math.max(16, Math.min(240, Math.ceil(sampler.length / 4)));
134
+ const o = sampler.at(0);
135
+ return Array.from({ length: n + 1 }, (_, i) => {
136
+ const at = sampler.at(ease(i / n) * sampler.length);
137
+ return /** @type {[number, number]} */ ([at.x - o.x, at.y - o.y]);
138
+ });
139
+ }
package/src/diagram.js CHANGED
@@ -19,9 +19,10 @@
19
19
  * @module diagram
20
20
  */
21
21
 
22
- import { attrs, num, layerAttrs } from './svg.js';
23
- import { len } from './geometry.js';
24
- import { toHex } from './color.js';
22
+ import { attrs, num } from '@logomarks/image-effects/svg';
23
+ import { layerAttrs } from './layers/util.js';
24
+ import { len } from '@logomarks/image-effects/length';
25
+ import { toHex } from '@logomarks/image-effects/color';
25
26
  import { resolveIcon, parseSvgIcon } from './icons.js';
26
27
  import { gridPath } from './grid.js';
27
28
 
package/src/extract.js CHANGED
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import { loadBytes } from './platform/index.js';
8
- import { toOklch, fromOklch, toHex } from './color.js';
9
- import { createRng } from './rng.js';
8
+ import { toOklch, fromOklch, toHex } from '@logomarks/image-effects/color';
9
+ import { createRng } from '@logomarks/image-effects/rng';
10
10
 
11
11
  /** @type {Map<string, Promise<string[]>>} */
12
12
  const cache = new Map();
package/src/fonts.js CHANGED
@@ -23,8 +23,7 @@
23
23
  * @module fonts
24
24
  */
25
25
 
26
- import { shaperModule, loadBytes, inflate, unbrotli, unbrotliSync, packageFileSync, registry, node } from './platform/index.js';
27
- import { u8 } from './platform/bytes.js';
26
+ import { shaperModule, loadBytes, inflate, unbrotli, unbrotliSync, packageFileSync, registry, node, u8 } from './platform/index.js';
28
27
  import { parseSfnt } from './type/sfnt.js';
29
28
  import { woff2Directory, woff2ToSfnt } from './type/woff2.js';
30
29
 
package/src/geometry.js CHANGED
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Canvas size, lengths and anchoring.
3
- *
4
- * Lengths are pixels when given as numbers, or CSS-style viewport units as
5
- * strings: `'6vmin'` (6% of the shorter side), `'4vw'`, `'4vh'`, `'5vmax'`.
6
- * Defaults are written in these units, so a cover keeps its proportions at
7
- * any output size.
2
+ * Canvas size and anchoring. Lengths (`'6vmin'`, `'4vw'`…) resolve with
3
+ * image-effects' `len`, so layers and effects read them the same way.
8
4
  *
9
5
  * @module geometry
10
6
  */
11
7
 
8
+ import { len } from '@logomarks/image-effects/length';
9
+
12
10
  /** Named aspect ratios, as `width / height`. */
13
11
  export const ASPECTS = Object.freeze({
14
12
  '1:1': 1,
@@ -77,34 +75,6 @@ function checkSize(w, h) {
77
75
  return { width, height };
78
76
  }
79
77
 
80
- /**
81
- * Resolve a length to pixels.
82
- *
83
- * @param {number | string | undefined | null} value - Pixels, or `'Nvmin'` / `'Nvw'` / `'Nvh'` / `'Nvmax'`.
84
- * @param {{ width: number, height: number }} canvas - The canvas size.
85
- * @param {number | string} [fallback=0] - Used when `value` is undefined or null.
86
- * @returns {number} Pixels.
87
- * @throws {TypeError} On a string with an unknown unit.
88
- *
89
- * @example
90
- * len('5vmin', { width: 1200, height: 630 }); // 31.5
91
- * len(undefined, canvas, '6vmin'); // the default
92
- */
93
- export function len(value, canvas, fallback = 0) {
94
- const v = value ?? fallback;
95
- if (typeof v === 'number') return v;
96
- const m = String(v).match(/^\s*(-?\d*\.?\d+)\s*(vmin|vmax|vw|vh|px)?\s*$/);
97
- if (!m) throw new TypeError(`Invalid length "${v}" (use px numbers or vmin/vmax/vw/vh)`);
98
- const n = parseFloat(m[1]);
99
- switch (m[2]) {
100
- case 'vmin': return (n / 100) * Math.min(canvas.width, canvas.height);
101
- case 'vmax': return (n / 100) * Math.max(canvas.width, canvas.height);
102
- case 'vw': return (n / 100) * canvas.width;
103
- case 'vh': return (n / 100) * canvas.height;
104
- default: return n;
105
- }
106
- }
107
-
108
78
  /** The nine anchors, as fractions of the free space along x and y. */
109
79
  export const ANCHORS = Object.freeze({
110
80
  'top-left': [0, 0], top: [0.5, 0], 'top-right': [1, 0],
package/src/grid.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @module grid
5
5
  */
6
6
 
7
- import { num } from './svg.js';
7
+ import { num } from '@logomarks/image-effects/svg';
8
8
 
9
9
  /**
10
10
  * Path data for full-length grid lines at `xs` (columns) and `ys` (rows),
package/src/handmade.js CHANGED
@@ -11,9 +11,11 @@
11
11
  * @module handmade
12
12
  */
13
13
 
14
- import { attrs, num, layerAttrs } from './svg.js';
15
- import { len, regionPx } from './geometry.js';
16
- import { toHex } from './color.js';
14
+ import { attrs, num } from '@logomarks/image-effects/svg';
15
+ import { layerAttrs } from './layers/util.js';
16
+ import { len } from '@logomarks/image-effects/length';
17
+ import { regionPx } from './geometry.js';
18
+ import { toHex } from '@logomarks/image-effects/color';
17
19
  import { resolveIcon, parseSvgIcon } from './icons.js';
18
20
  import { flattenBody, densify } from './flatten.js';
19
21
 
@@ -25,7 +27,7 @@ export const INK = '#141413';
25
27
  /**
26
28
  * Smooth 1D value noise from a seeded stream: `noise(t)` in [-1, 1].
27
29
  *
28
- * @param {import('./rng.js').Rng} rng
30
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
29
31
  * @returns {(t: number) => number}
30
32
  */
31
33
  function noise1(rng) {
@@ -47,7 +49,7 @@ function noise1(rng) {
47
49
  * @param {[number, number][]} outline - A closed polyline (first point not repeated).
48
50
  * @param {number} cut - Cut length in px.
49
51
  * @param {number} wobble - Max nudge in px.
50
- * @param {import('./rng.js').Rng} rng
52
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
51
53
  * @returns {string} Path data.
52
54
  */
53
55
  function scissor(outline, cut, wobble, rng) {
@@ -74,7 +76,7 @@ function scissor(outline, cut, wobble, rng) {
74
76
  *
75
77
  * @param {string} kind - rect | square | circle | blob | arch | pill
76
78
  * @param {number} w @param {number} h
77
- * @param {import('./rng.js').Rng} rng
79
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
78
80
  * @returns {[number, number][]}
79
81
  */
80
82
  function outlineOf(kind, w, h, rng) {
@@ -176,7 +178,7 @@ export function paper(layer, ctx) {
176
178
  * @param {number} o.amp - Wobble amplitude in px.
177
179
  * @param {number} o.period - Wobble period in px.
178
180
  * @param {string} o.color
179
- * @param {import('./rng.js').Rng} o.rng
181
+ * @param {import('@logomarks/image-effects/rng').Rng} o.rng
180
182
  * @returns {string} Markup.
181
183
  */
182
184
  function handLine(pts, { width, amp, period, color, rng }) {
package/src/icons.js CHANGED
@@ -14,8 +14,7 @@
14
14
  * @module icons
15
15
  */
16
16
 
17
- import { node, settings, loadBytes } from './platform/index.js';
18
- import { utf8 } from './platform/bytes.js';
17
+ import { node, settings, loadBytes, utf8 } from './platform/index.js';
19
18
 
20
19
  /** @type {Map<string, IconSet>} */
21
20
  const sets = new Map();