@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
@@ -1,5 +1,5 @@
1
1
  // Tile paints.
2
- import type { TilePaint } from './paint.js';
2
+ import type { TilePaint } from './index.js';
3
3
 
4
4
  /** Tile kinds. */
5
5
  export const TILES: readonly ('checker' | 'stripes' | 'dots' | 'grid' | 'custom')[];
@@ -2,12 +2,12 @@
2
2
  * Tile paints: a pattern repeated over a shape (checker, stripes, dots,
3
3
  * grid), or a custom tile of markup.
4
4
  *
5
- * @module effects/tile
5
+ * @module paint/tile
6
6
  */
7
7
 
8
- import { attrs, num } from '../svg.js';
9
- import { len } from '../geometry.js';
10
- import { toHex } from '../color.js';
8
+ import { attrs, num } from '@logomarks/image-effects/svg';
9
+ import { len } from '@logomarks/image-effects/length';
10
+ import { toHex } from '@logomarks/image-effects/color';
11
11
  import { sanitize } from '../layers/svg.js';
12
12
 
13
13
  /** Tile kinds. */
package/src/palette.js CHANGED
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { relativeLuminance, contrastRatio, readableOn, readableText } from '@arraypress/color-utils';
23
- import { toHex, toOklch, fromOklch, mixOklab } from './color.js';
23
+ import { toHex, toOklch, fromOklch, mixOklab } from '@logomarks/image-effects/color';
24
24
 
25
25
  /**
26
26
  * Curated presets. Each is a full palette; the light ones pair with the
@@ -0,0 +1,6 @@
1
+ // Files read from disk in Node.
2
+
3
+ /** Read a text file synchronously (Node). */
4
+ export function readTextSync(path: string | URL): string;
5
+ /** A file shipped with the package, from disk (Node), or undefined. */
6
+ export function packageFileSync(rel: string): Uint8Array | undefined;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Files read from disk in Node: a recipe's JSON, and the fonts that ship
3
+ * with this package. Node's built-ins come from the image-effects platform
4
+ * adapter (`process.getBuiltinModule`, or what `@arraypress/covers/node`
5
+ * installs).
6
+ *
7
+ * @module platform/files
8
+ */
9
+
10
+ import { node } from '@logomarks/image-effects/platform';
11
+
12
+ /** This module was loaded from disk (Node, Deno, Bun), not a bundle or a page. */
13
+ const FROM_DISK = String(import.meta.url).startsWith('file:');
14
+
15
+ /**
16
+ * Read a text file synchronously (Node only; `loadRecipe`).
17
+ *
18
+ * @param {string | URL} path
19
+ * @returns {string}
20
+ */
21
+ export function readTextSync(path) {
22
+ const fs = node('fs');
23
+ if (!fs) throw new Error('Reading a file synchronously needs Node. In a browser or Worker, fetch the JSON and pass it to defineRecipe().');
24
+ return fs.readFileSync(path, 'utf8');
25
+ }
26
+
27
+ /**
28
+ * Read a file that ships with this package (Node, from disk), or undefined.
29
+ *
30
+ * @param {string} rel - Relative to the package root (`'fonts/inter-latin-400.woff'`).
31
+ * @returns {Uint8Array | undefined}
32
+ */
33
+ export function packageFileSync(rel) {
34
+ const fs = node('fs');
35
+ if (!fs || !FROM_DISK) return undefined;
36
+ try {
37
+ return fs.readFileSync(new URL(`../../${rel}`, import.meta.url));
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
@@ -7,7 +7,7 @@
7
7
  * @module platform/harfbuzz
8
8
  */
9
9
 
10
- import { isBytes } from './bytes.js';
10
+ import { isBytes } from '@logomarks/image-effects/platform';
11
11
  import { settings } from './settings.js';
12
12
  import { loadBytes, asBytes } from './load.js';
13
13
 
@@ -1,8 +1,8 @@
1
1
  // The platform adapter.
2
- export { node, installNode, isNode, FROM_DISK, readTextSync, packageFileSync } from './node.js';
3
- export type { NodeModules } from './node.js';
2
+ export { node, installNode, isNode, deflateSync, deflate, inflate, unbrotli, unbrotliSync, u8, isBytes, toBase64, fromBase64, latin1, utf8, encodeUtf8, concat, fromDataUri, hexDigest } from '@logomarks/image-effects/platform';
3
+ export type { NodeModules } from '@logomarks/image-effects/platform';
4
+ export { readTextSync, packageFileSync } from './files.js';
4
5
  export { settings, configure, registry } from './settings.js';
5
6
  export type { Settings } from './settings.js';
6
7
  export { loadBytes } from './load.js';
7
- export { deflateSync, deflate, inflate, unbrotli, unbrotliSync } from './compress.js';
8
8
  export { harfbuzz, shaperModule } from './harfbuzz.js';
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * The platform: everything that differs between Node, a browser and a
3
- * Cloudflare Worker, behind one small adapter. Nothing here imports a
4
- * `node:` module.
3
+ * Cloudflare Worker, behind one small adapter. Node's built-ins, zlib and
4
+ * the byte helpers are image-effects' (one registry, so
5
+ * `@arraypress/covers/node` installs for both); settings, loading, files
6
+ * and HarfBuzz are covers' own. Nothing here imports a `node:` module.
5
7
  *
6
8
  * @module platform
7
9
  */
8
10
 
9
- export { node, installNode, isNode, FROM_DISK, readTextSync, packageFileSync } from './node.js';
11
+ export { node, installNode, isNode, deflateSync, deflate, inflate, unbrotli, unbrotliSync, u8, isBytes, toBase64, fromBase64, latin1, utf8, encodeUtf8, concat, fromDataUri, hexDigest } from '@logomarks/image-effects/platform';
12
+ export { readTextSync, packageFileSync } from './files.js';
10
13
  export { settings, configure, registry } from './settings.js';
11
14
  export { loadBytes } from './load.js';
12
- export { deflateSync, deflate, inflate, unbrotli, unbrotliSync } from './compress.js';
13
15
  export { harfbuzz, shaperModule } from './harfbuzz.js';
@@ -6,8 +6,7 @@
6
6
  * @module platform/load
7
7
  */
8
8
 
9
- import { isBytes, u8, fromDataUri } from './bytes.js';
10
- import { node, isNode } from './node.js';
9
+ import { isBytes, u8, fromDataUri, node, isNode } from '@logomarks/image-effects/platform';
11
10
  import { settings } from './settings.js';
12
11
 
13
12
  /** @returns {boolean} */
package/src/print.js CHANGED
@@ -5,11 +5,13 @@
5
5
  * @module print
6
6
  */
7
7
 
8
- import { attrs, num, layerAttrs, esc } from './svg.js';
9
- import { len, regionPx } from './geometry.js';
10
- import { toHex } from './color.js';
11
- import { createRng } from './rng.js';
12
- import { encodePngAsync, pngUri } from './png.js';
8
+ import { attrs, num, esc } from '@logomarks/image-effects/svg';
9
+ import { layerAttrs } from './layers/util.js';
10
+ import { len } from '@logomarks/image-effects/length';
11
+ import { regionPx } from './geometry.js';
12
+ import { toHex } from '@logomarks/image-effects/color';
13
+ import { createRng } from '@logomarks/image-effects/rng';
14
+ import { encodePngAsync, pngUri } from '@logomarks/image-effects/png';
13
15
  import { text as textLayer } from './layers.js';
14
16
 
15
17
  /** Classic risograph inks, by name (`'riso:pink'`). Approximations, not trademarks. */
@@ -181,7 +183,7 @@ export async function ink(layer, ctx) {
181
183
  /**
182
184
  * Seeded value noise in 2D, smooth, in [0, 1], with octaves.
183
185
  *
184
- * @param {import('./rng.js').Rng} rng
186
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
185
187
  * @param {number} octaves
186
188
  * @returns {(x: number, y: number) => number}
187
189
  */
@@ -457,7 +459,7 @@ function smoothPath(pts) {
457
459
  * Seeded demo peaks: a smooth loudness envelope with transients. Not audio:
458
460
  * pass `peaks` or `audio` for a real episode.
459
461
  *
460
- * @param {import('./rng.js').Rng} rng
462
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
461
463
  * @param {number} n
462
464
  * @returns {number[]}
463
465
  */
package/src/recipes.js CHANGED
@@ -33,7 +33,7 @@ import { BUILTIN_JSON } from './builtins.js';
33
33
  * @property {number} width - Canvas width.
34
34
  * @property {number} height - Canvas height.
35
35
  * @property {import('./index.js').Palette} palette - Resolved palette.
36
- * @property {import('./rng.js').Rng} rng - The cover's random stream (for recipe-level choices).
36
+ * @property {import('@logomarks/image-effects/rng').Rng} rng - The cover's random stream (for recipe-level choices).
37
37
  * @property {number} u - The design unit: `sqrt(width × height)`.
38
38
  * @property {string} seed - The cover's seed (JSON recipes seed each layer's draws from it).
39
39
  */
@@ -5,8 +5,8 @@
5
5
  * @module render/backdrop
6
6
  */
7
7
 
8
- import { toHex } from '../color.js';
9
- import { pixelsOf } from '../raster/index.js';
8
+ import { toHex } from '@logomarks/image-effects/color';
9
+ import { pixelsOf } from '@logomarks/image-effects/raster';
10
10
 
11
11
  /**
12
12
  * The average colour under a box, from the layers drawn so far: rendered
@@ -16,7 +16,7 @@ import { VARY_FLOOR, VARY_PEAK } from '../layers.js';
16
16
  * @param {any} sharp - The sharp module.
17
17
  * @param {number} width @param {number} height
18
18
  * @param {import('../index.js').GrainLayer} layer
19
- * @param {import('../rng.js').Rng} rng - The layer's stream.
19
+ * @param {import('@logomarks/image-effects/rng').Rng} rng - The layer's stream.
20
20
  * @returns {Promise<{ data: Buffer, channels: 1 | 3 }>} Full-size noise.
21
21
  */
22
22
  export async function noise(sharp, width, height, layer, rng) {
@@ -4,10 +4,10 @@
4
4
  * @module render/group
5
5
  */
6
6
 
7
- import { attrs, esc, num } from '../svg.js';
7
+ import { attrs, esc, num } from '@logomarks/image-effects/svg';
8
8
  import { pos } from '../layers/util.js';
9
9
  import { scaledReport } from './report.js';
10
- import { IDENTITY, compose } from '../effects/transform.js';
10
+ import { IDENTITY, compose } from './transform.js';
11
11
 
12
12
  /**
13
13
  * `group` — a stack of layers drawn as one: in a box (`x`, `y`, `width`,
@@ -40,7 +40,7 @@ export async function renderGroup(layer, ctx, decorate) {
40
40
  const tx = bx + (bw - vw * sx) * ax;
41
41
  const ty = by + (bh - vh * sy) * ay;
42
42
  const k = Math.min(sx, sy);
43
- /** @type {import('../effects/transform.js').Affine} Grid units to the box. */
43
+ /** @type {import('./transform.js').Affine} Grid units to the box. */
44
44
  const grid = [sx, 0, 0, sy, tx - vx * sx, ty - vy * sy];
45
45
  const parts = await ctx.stack(layer.layers ?? [], { tag: 'g', width: vw, height: vh, scale: k, res: Math.max(sx, sy), matrix: grid });
46
46
  let defs = parts.map((/** @type {any} */ p) => p.defs).join('');
package/src/render/key.js CHANGED
@@ -4,8 +4,7 @@
4
4
  * @module render/key
5
5
  */
6
6
 
7
- import { loadBytes } from '../platform/index.js';
8
- import { hexDigest, u8, isBytes } from '../platform/bytes.js';
7
+ import { loadBytes, hexDigest, u8, isBytes } from '../platform/index.js';
9
8
  import { fontBytes } from '../fonts.js';
10
9
  import { cover } from '../render.js';
11
10
  import { VERSION } from '../version.js';
@@ -15,7 +15,8 @@ export interface LayerTransform {
15
15
  origin?: [Length, Length] | { x?: Length; y?: Length } | 'center';
16
16
  }
17
17
 
18
-
18
+ /** A transform's `origin` in px (`'center'`, the default, is the canvas centre). */
19
+ export function originOf(origin: LayerTransform['origin'], canvas: { width: number; height: number }): [number, number];
19
20
  /** A `transform` option as an SVG transform string (a string is used as written). */
20
21
  export function transformAttr(t: LayerTransform | string | undefined, canvas: { width: number; height: number }): string | undefined;
21
22
 
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * A layer's `transform` option as an SVG transform.
3
3
  *
4
- * @module effects/transform
4
+ * @module render/transform
5
5
  */
6
6
 
7
- import { num } from '../svg.js';
7
+ import { num } from '@logomarks/image-effects/svg';
8
8
  import { pos } from '../layers/util.js';
9
9
 
10
10
  /**
@@ -25,9 +25,7 @@ export function transformAttr(t, canvas) {
25
25
  const parts = [];
26
26
  const tr = Array.isArray(t.translate) ? { x: t.translate[0], y: t.translate[1] } : t.translate;
27
27
  if (tr) parts.push(`translate(${num(pos(tr.x ?? 0, 'x', canvas))} ${num(pos(tr.y ?? 0, 'y', canvas))})`);
28
- const o = t.origin === undefined || t.origin === 'center'
29
- ? [canvas.width / 2, canvas.height / 2]
30
- : Array.isArray(t.origin) ? [pos(t.origin[0], 'x', canvas), pos(t.origin[1], 'y', canvas)] : [pos(t.origin.x, 'x', canvas), pos(t.origin.y, 'y', canvas)];
28
+ const o = originOf(t.origin, canvas);
31
29
  const inner = [];
32
30
  if (t.rotate) inner.push(`rotate(${num(t.rotate, 3)})`);
33
31
  let [sx, sy] = t.scale == null ? [1, 1] : Array.isArray(t.scale) ? t.scale : [t.scale, t.scale];
@@ -40,6 +38,20 @@ export function transformAttr(t, canvas) {
40
38
  return parts.length ? parts.join(' ') : undefined;
41
39
  }
42
40
 
41
+ /**
42
+ * A transform's `origin` in px: `[x, y]` or `{ x, y }` (px, `'%'` or
43
+ * lengths), or `'center'` (the default).
44
+ *
45
+ * @param {unknown} origin
46
+ * @param {{ width: number, height: number }} canvas
47
+ * @returns {[number, number]}
48
+ */
49
+ export function originOf(origin, canvas) {
50
+ if (origin === undefined || origin === 'center') return [canvas.width / 2, canvas.height / 2];
51
+ const o = /** @type {any} */ (origin);
52
+ return Array.isArray(o) ? [pos(o[0], 'x', canvas), pos(o[1], 'y', canvas)] : [pos(o.x, 'x', canvas), pos(o.y, 'y', canvas)];
53
+ }
54
+
43
55
  /** @typedef {[number, number, number, number, number, number]} Affine - `[a, b, c, d, e, f]`, as SVG's `matrix()`. */
44
56
 
45
57
  /** @param {Affine} m @param {Affine} n @returns {Affine} `m × n`. */
@@ -1,6 +1,6 @@
1
1
  // Reusable stacks: `defs` and `use`.
2
2
  import type { Layer, BaseLayer, Length } from '../index.js';
3
- import type { LayerTransform } from '../effects/transform.js';
3
+ import type { LayerTransform } from './transform.js';
4
4
  import type { RenderContext, Markup } from './context.js';
5
5
 
6
6
  /** `defs`: named layers (each with an `id`) that `use` draws; nothing is drawn here. */
package/src/render/use.js CHANGED
@@ -8,9 +8,9 @@
8
8
  * @module render/use
9
9
  */
10
10
 
11
- import { attrs, num } from '../svg.js';
11
+ import { attrs, num } from '@logomarks/image-effects/svg';
12
12
  import { pos, wrap } from '../layers/util.js';
13
- import { transformAttr } from '../effects/transform.js';
13
+ import { transformAttr } from './transform.js';
14
14
 
15
15
  /**
16
16
  * `defs`: register named layers for `use`.
@@ -5,12 +5,11 @@
5
5
  * @module render/wrap
6
6
  */
7
7
 
8
- import { attrs, esc, num } from '../svg.js';
9
- import { applyEffects, transformAttr, rasterEffect, isRaster, RESOLVED, maskImage } from '../effects/index.js';
10
- import { transformMatrix, stretchOf, compose, IDENTITY } from '../effects/transform.js';
8
+ import { attrs, esc, num } from '@logomarks/image-effects/svg';
9
+ import { drawEffects, contentBox } from '@logomarks/image-effects';
10
+ import { transformAttr, transformMatrix, stretchOf, compose, IDENTITY } from './transform.js';
11
11
  import { renderGroup } from './group.js';
12
12
  import { scaledReport } from './report.js';
13
- import { contentBox } from './content.js';
14
13
 
15
14
  /** Layer options handled here, for every layer type. */
16
15
  export const WRAP_KEYS = ['effects', 'mask', 'knockout', 'clip', 'area'];
@@ -64,7 +63,7 @@ export async function renderWrapped(layer, ctx, scope, renderOne) {
64
63
 
65
64
  let { defs, body } = grid
66
65
  ? await renderGroup(inner, lctx, (/** @type {any} */ part, /** @type {any} */ gctx) => decorate(part, layer, gctx))
67
- : await decorateEffects(await renderOne(inner, lctx), layer.effects, lctx);
66
+ : await drawEffects(await renderOne(inner, lctx), layer.effects, lctx);
68
67
  if (geometric) {
69
68
  const t = transformAttr(layer.transform, lctx.canvas);
70
69
  if (t) body = `<g transform="${t}">${body}</g>`;
@@ -104,61 +103,7 @@ function areaBox(a, ctx, scope) {
104
103
  * @returns {Promise<{ defs: string, body: string }>}
105
104
  */
106
105
  export async function decorate(m, layer, ctx) {
107
- return decorateMasks(await decorateEffects(m, layer.effects, ctx), layer, ctx);
108
- }
109
-
110
- /**
111
- * Effects, in order. Pixel effects are computed on pixels where something
112
- * can draw them (sharp, a canvas) and copy effects drawn as copies; the
113
- * rest are SVG filters. An effect's layer `mask` is drawn first (as
114
- * markup, and as an image for a filter to mix through).
115
- *
116
- * @param {{ defs: string, body: string }} m
117
- * @param {any} list
118
- * @param {any} ctx
119
- * @returns {Promise<{ defs: string, body: string }>}
120
- */
121
- async function decorateEffects(m, list, ctx) {
122
- if (!list) return m;
123
- const effects = await Promise.all((Array.isArray(list) ? list : [list]).map((e, i) => withMask(typeof e === 'string' ? { kind: e } : e, ctx, i)));
124
- let { defs, body } = m;
125
- const filters = async (/** @type {number} */ from, /** @type {number} */ to) => {
126
- if (to <= from) return;
127
- // In renderImage, filters work over what the layer paints, not the whole canvas.
128
- const out = applyEffects(body, effects.slice(from, to), ctx, from, await contentBox({ defs, body }, ctx), defs);
129
- defs += out.defs;
130
- body = out.body;
131
- };
132
- let from = 0;
133
- for (let i = 0; i < effects.length; i++) {
134
- const fx = effects[i];
135
- if (!isRaster(fx, ctx)) continue;
136
- await filters(from, i);
137
- from = i + 1;
138
- const raster = await rasterEffect(fx, { defs, body }, ctx, i);
139
- if (raster) ({ defs, body } = raster);
140
- else await filters(i, i + 1);
141
- }
142
- await filters(from, effects.length);
143
- return { defs, body };
144
- }
145
-
146
- /**
147
- * An effect with a layer `mask`: the mask's layers drawn (kept on the
148
- * mask), and for a filter effect drawn as an image too, where something
149
- * can draw pixels. Other effects come back as they are.
150
- *
151
- * @param {any} fx
152
- * @param {any} ctx
153
- * @param {number} i
154
- * @returns {Promise<any>}
155
- */
156
- async function withMask(fx, ctx, i) {
157
- if (!fx?.mask?.layers || !ctx.stack) return fx;
158
- const parts = await ctx.stack(fx.mask.layers, { tag: `fm${i}` });
159
- const mask = { ...fx.mask, [RESOLVED]: { defs: parts.map((/** @type {any} */ p) => p.defs).join(''), body: parts.map((/** @type {any} */ p) => p.body).join('') } };
160
- if (!isRaster(fx, ctx)) mask[RESOLVED].image = await maskImage(mask, ctx, fx.pad ?? 0.3, fx.kind === 'motion' ? fx.angle ?? 0 : 0);
161
- return { ...fx, mask };
106
+ return decorateMasks(await drawEffects(m, layer.effects, ctx), layer, ctx);
162
107
  }
163
108
 
164
109
  /**
package/src/render.js CHANGED
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import { node } from './platform/index.js';
8
- import { createRng } from './rng.js';
9
- import { resolveSize, unit, len } from './geometry.js';
8
+ import { createRng } from '@logomarks/image-effects/rng';
9
+ import { resolveSize, unit } from './geometry.js';
10
+ import { len } from '@logomarks/image-effects/length';
10
11
  import { resolveFormat, printMarks, zonesOverlay, safeBox } from './formats/index.js';
11
- import { animate, isAnimated } from './animate/index.js';
12
+ import { animate, isAnimated, renderMotion, createSheet, nameSheet } from './animate/index.js';
12
13
  import { preloadIcons } from './icons.js';
13
- import { rasterize } from './raster/index.js';
14
14
  import { resolvePalette } from './palette.js';
15
15
  import { fontPool } from './fonts.js';
16
16
  import { LAYERS } from './layers.js';
@@ -20,15 +20,15 @@ import { registerDefs, renderUse } from './render/use.js';
20
20
  import { expandRepeat } from './layers/repeat.js';
21
21
  import { scaledReport } from './render/report.js';
22
22
  import { hasLayoutRef, resolveLayoutRefs } from './type/layout-ref.js';
23
- import { svgPixels } from './render/tiles.js';
24
- import { IDENTITY, compose } from './effects/transform.js';
25
- import { needsPixels } from './effects/index.js';
23
+ import { rasterize, svgPixels, webpImages } from '@logomarks/image-effects/raster';
24
+ import { IDENTITY, compose } from './render/transform.js';
25
+ import { needsPixels } from '@logomarks/image-effects';
26
26
  import { backdropSampler } from './render/backdrop.js';
27
27
  import { noise, applyGrain } from './render/grain.js';
28
28
  import { extractPalette } from './extract.js';
29
29
  import { recipes, applyOverrides } from './recipes.js';
30
30
  import { defineRecipe } from './json-recipe.js';
31
- import { attrs, esc, num } from './svg.js';
31
+ import { attrs, esc, num } from '@logomarks/image-effects/svg';
32
32
 
33
33
  export { VERSION } from './version.js';
34
34
  export { coverKey } from './render/key.js';
@@ -39,7 +39,7 @@ export { safeBox } from './formats/index.js';
39
39
  const built = new WeakSet();
40
40
 
41
41
  /** Options that belong to the cover itself, never to a recipe layer. */
42
- const COVER_KEYS = new Set(['width', 'height', 'aspect', 'seed', 'palette', 'recipe', 'layers', 'fonts', 'alt', 'radius', 'output', 'format', 'dpi', 'bleed', 'safe', 'cropMarks', 'cutLine', 'cut', 'textRender', 'overlay', 'loop']);
42
+ const COVER_KEYS = new Set(['width', 'height', 'aspect', 'seed', 'palette', 'recipe', 'layers', 'fonts', 'alt', 'radius', 'output', 'format', 'dpi', 'bleed', 'safe', 'cropMarks', 'cutLine', 'cut', 'textRender', 'overlay', 'loop', 'animate', 'poster']);
43
43
 
44
44
  /**
45
45
  * Build a cover: resolve its size and palette and expand its recipe into a
@@ -120,6 +120,8 @@ export function cover(options) {
120
120
  if (fmt?.zones) /** @type {any} */ (result).zones = fmt.zones;
121
121
  if (o.overlay) /** @type {any} */ (result).overlay = o.overlay;
122
122
  if (o.loop != null) /** @type {any} */ (result).loop = o.loop;
123
+ if (o.animate === false) /** @type {any} */ (result).animate = false;
124
+ if (o.poster != null) /** @type {any} */ (result).poster = o.poster;
123
125
  built.add(result);
124
126
  return result;
125
127
  }
@@ -136,7 +138,9 @@ export function cover(options) {
136
138
  * @property {((box: { x: number, y: number, width: number, height: number }) => Promise<string | undefined>) | undefined} backdrop
137
139
  * @property {((entry: any) => void) | undefined} report
138
140
  * @property {object} doc - Identity of the document being rendered.
139
- * @property {number} [t] - The time being drawn (seconds), for animated layers.
141
+ * @property {number} [t] - The time being drawn (seconds), for animated layers: the frame stills show.
142
+ * @property {boolean} [live] - Motion plays in the SVG (CSS keyframes in `sheet`).
143
+ * @property {import('./animate/sheet.js').Sheet} [sheet]
140
144
  */
141
145
 
142
146
  /**
@@ -172,12 +176,13 @@ async function renderStack(layers, scope) {
172
176
  if (!layer || !Object.hasOwn(LAYERS, layer.type)) throw new TypeError(`Unknown layer type "${layer?.type}". Types: ${Object.keys(LAYERS).join(', ')}`);
173
177
  const n = (seen[layer.type] = (seen[layer.type] ?? -1) + 1);
174
178
  const key = layer.seed ?? layer.id ?? `${layer.type}#${n}`;
179
+ const stream = scope.seedPrefix ? `${c.seed}:${scope.seedPrefix}/${key}` : `${c.seed}:${key}`;
175
180
  const ctx = /** @type {any} */ ({
176
181
  width: scope.width,
177
182
  height: scope.height,
178
183
  canvas: { width: scope.width, height: scope.height },
179
184
  palette: c.palette,
180
- rng: createRng(scope.seedPrefix ? `${c.seed}:${scope.seedPrefix}/${key}` : `${c.seed}:${key}`),
185
+ rng: createRng(stream),
181
186
  id: top ? `l${i}` : `${scope.prefix}_${i}`,
182
187
  fonts: scope.fonts,
183
188
  sharp: scope.sharp,
@@ -214,7 +219,16 @@ async function renderStack(layers, scope) {
214
219
  height: sub.height ?? self.height,
215
220
  });
216
221
  };
217
- const markup = wrapped(layer) ? await renderWrapped(/** @type {any} */ (layer), ctx, scope, renderOne) : await renderOne(/** @type {any} */ (layer), ctx);
222
+ // The layer drawn with its stream from the start (motion draws it more than once, and its frames with `extra` context).
223
+ const draw = (/** @type {any} */ l, /** @type {any} */ extra) => {
224
+ ctx.rng = createRng(stream);
225
+ const x = extra ? { ...ctx, ...extra } : ctx;
226
+ return wrapped(l) ? renderWrapped(l, x, scope, renderOne) : renderOne(l, x);
227
+ };
228
+ // Motion is drawn when it plays, or at a time a still asks for; otherwise the layer is at rest.
229
+ const markup = layer.motion != null && (scope.live || scope.t != null)
230
+ ? await renderMotion(layer, ctx, { live: Boolean(scope.live), t: scope.t, sheet: /** @type {any} */ (scope.sheet), draw })
231
+ : await draw(layer);
218
232
  out.push({ layer, ...markup, ctx });
219
233
  }
220
234
  return out;
@@ -244,7 +258,7 @@ async function renderOne(layer, ctx) {
244
258
  *
245
259
  * @param {import('./index.js').Cover} c
246
260
  * @param {any} sharp
247
- * @param {{ report?: (entry: any) => void, t?: number, raster?: boolean, reach?: { px: number } }} [extra] - `raster`: drawn for `renderImage` (librsvg), so effects may take its faster forms; `reach` collects how far filters reach, output px.
261
+ * @param {{ report?: (entry: any) => void, t?: number, live?: boolean, sheet?: import('./animate/sheet.js').Sheet, raster?: boolean, reach?: { px: number } }} [extra] - `t`: the time to draw (default the cover's `poster`); `live`: motion plays (into `sheet`); `raster`: drawn for `renderImage` (librsvg), so effects may take its faster forms; `reach` collects how far filters reach, output px.
248
262
  */
249
263
  async function renderLayers(c, sharp, extra = {}) {
250
264
  // Icon sets named anywhere in the stack load first (from node_modules, or configure({ icons }) elsewhere).
@@ -255,7 +269,7 @@ async function renderLayers(c, sharp, extra = {}) {
255
269
  scan(c.layers);
256
270
  if (names.size) await preloadIcons(names);
257
271
  const auto = c.layers.some(usesAuto);
258
- const t = /** @type {any} */ (extra).t;
272
+ const t = extra.t ?? /** @type {any} */ (c).poster;
259
273
  return renderStack(c.layers, {
260
274
  c,
261
275
  sharp,
@@ -270,6 +284,8 @@ async function renderLayers(c, sharp, extra = {}) {
270
284
  layout: new Map(),
271
285
  defs: new Map(),
272
286
  t,
287
+ live: extra.live,
288
+ sheet: extra.sheet,
273
289
  raster: extra.raster,
274
290
  reach: extra.reach,
275
291
  });
@@ -316,8 +332,12 @@ function documentFor(c, parts) {
316
332
  * needs no fonts; images are embedded as data URIs; nothing is fetched.
317
333
  * The same cover always gives byte-identical output.
318
334
  *
335
+ * Layers with `motion` play in the SVG (CSS keyframes, stopped by
336
+ * `prefers-reduced-motion`), unless the cover has `animate: false` or a
337
+ * time is asked for: then it's a still.
338
+ *
319
339
  * @param {import('./index.js').CoverOptions | import('./index.js').Cover} input - The cover.
320
- * @param {{ t?: number }} [options] - `t`: the time to draw, in seconds, for layers with keyframes (static covers ignore it).
340
+ * @param {{ t?: number, images?: 'webp', quality?: number }} [options] - `t`: a still at this time, in seconds, for layers with keyframes or motion (static covers ignore it). `images: 'webp'`: embedded photos and pixel effects re-encoded as WebP where smaller (`quality` for photos, default 82; needs sharp), for SVGs shown in browsers (librsvg can't draw WebP).
321
341
  * @returns {Promise<string>} The SVG document.
322
342
  *
323
343
  * @example
@@ -325,8 +345,24 @@ function documentFor(c, parts) {
325
345
  */
326
346
  export async function renderSvg(input, options = {}) {
327
347
  const c = cover(input);
348
+ return svgDocument(c, { ...options, live: options.t == null && /** @type {any} */ (c).animate !== false });
349
+ }
350
+
351
+ /**
352
+ * A cover's SVG document, playing its motion or still.
353
+ *
354
+ * @param {import('./index.js').Cover} c
355
+ * @param {{ t?: number, live?: boolean, images?: 'webp', quality?: number }} options
356
+ * @returns {Promise<string>}
357
+ */
358
+ async function svgDocument(c, options) {
328
359
  const sharp = c.layers.some(hasImage) ? await loadSharp(false) : undefined;
329
- return documentFor(c, await renderLayers(c, sharp, { t: options.t }));
360
+ const sheet = options.live ? createSheet() : undefined;
361
+ const parts = await renderLayers(c, sharp, { t: options.t, live: options.live, sheet });
362
+ const svg = sheet && !sheet.empty ? nameSheet(documentFor(c, [{ defs: `<style>${sheet.css()}</style>`, body: '' }, ...parts])) : documentFor(c, parts);
363
+ if (!options.images) return svg;
364
+ if (options.images !== 'webp') throw new TypeError(`Unknown images "${options.images}" (webp)`);
365
+ return webpImages(svg, sharp ?? await loadSharp(false), { quality: options.quality });
330
366
  }
331
367
 
332
368
  /**
@@ -383,14 +419,15 @@ export async function renderReport(input) {
383
419
  }
384
420
 
385
421
  /**
386
- * Render a cover to an image in the browser (a canvas; no sharp).
422
+ * Render a cover to an image in the browser (a canvas; no sharp): a still,
423
+ * the cover's `poster` frame or its layers at rest.
387
424
  *
388
425
  * @param {import('./index.js').CoverOptions | import('./index.js').Cover} input
389
426
  * @param {{ format?: 'png' | 'jpeg' | 'webp', quality?: number, scale?: number, width?: number }} [options]
390
427
  * @returns {Promise<Blob>}
391
428
  */
392
429
  export async function renderBlob(input, options = {}) {
393
- return rasterize(await renderSvg(input), options);
430
+ return rasterize(await svgDocument(cover(input), {}), options);
394
431
  }
395
432
 
396
433
  /** @type {Promise<any> | undefined} */
package/src/text.js CHANGED
@@ -21,7 +21,7 @@
21
21
  * @module text
22
22
  */
23
23
 
24
- import { num } from './svg.js';
24
+ import { num } from '@logomarks/image-effects/svg';
25
25
 
26
26
  /**
27
27
  * A shaped run of text.
@@ -11,7 +11,7 @@
11
11
  * @module type/auto-color
12
12
  */
13
13
 
14
- import { toHex } from '../color.js';
14
+ import { toHex } from '@logomarks/image-effects/color';
15
15
 
16
16
  /** Backdrop luma (0–1) above which `color: 'auto'` picks the dark colour. */
17
17
  export const AUTO_THRESHOLD = 0.72;
@@ -12,8 +12,8 @@
12
12
  * @module type/glyphs
13
13
  */
14
14
 
15
- import { len } from '../geometry.js';
16
- import { toHex } from '../color.js';
15
+ import { len } from '@logomarks/image-effects/length';
16
+ import { toHex } from '@logomarks/image-effects/color';
17
17
 
18
18
  /** @param {any} list @param {number} i @param {any} fallback @returns {any} The i-th of a cycled list (or a single value). */
19
19
  const at = (list, i, fallback) => (list == null ? fallback : Array.isArray(list) ? (list.length ? list[i % list.length] : fallback) : list);
package/src/type/line.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * @module type/line
6
6
  */
7
7
 
8
- import { num } from '../svg.js';
8
+ import { num } from '@logomarks/image-effects/svg';
9
9
  import { loadFont, pickFont, variationsFor } from '../fonts.js';
10
10
  import { shape, outline } from '../text.js';
11
11
  import { pixelFace } from './pixel.js';