@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.
Files changed (246) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +18 -524
  3. package/package.json +34 -5
  4. package/recipes/brand.json +1143 -0
  5. package/recipes/flyers/afterglow-motion.json +540 -0
  6. package/recipes/flyers/afterglow.json +70 -0
  7. package/recipes/flyers/arcfield-mirror-motion.json +397 -0
  8. package/recipes/flyers/arcfield-mirror.json +364 -0
  9. package/recipes/flyers/bauhaus-oktav.json +319 -0
  10. package/recipes/flyers/bauhaus-ritmo.json +735 -0
  11. package/recipes/flyers/bauhaus-volta.json +394 -0
  12. package/recipes/flyers/bloomfall.json +99 -0
  13. package/recipes/flyers/drag-solenne-motion.json +444 -0
  14. package/recipes/flyers/drag-solenne.json +426 -0
  15. package/recipes/flyers/fennick-patches.json +874 -0
  16. package/recipes/flyers/ferro-pass.json +398 -0
  17. package/recipes/flyers/ferrova.json +812 -0
  18. package/recipes/flyers/grid-rest-rhythm.json +125 -0
  19. package/recipes/flyers/groovy-honeycomb.json +816 -0
  20. package/recipes/flyers/groovy-marigold.json +479 -0
  21. package/recipes/flyers/groovy-peachfuzz.json +974 -0
  22. package/recipes/flyers/halden-orbit-motion.json +381 -0
  23. package/recipes/flyers/halden-orbit.json +356 -0
  24. package/recipes/flyers/halvard.json +120 -0
  25. package/recipes/flyers/kairo-weekender.json +696 -0
  26. package/recipes/flyers/linger-motion.json +518 -0
  27. package/recipes/flyers/linger.json +67 -0
  28. package/recipes/flyers/little-orchard.json +105 -0
  29. package/recipes/flyers/lofi-hexmoth.json +912 -0
  30. package/recipes/flyers/lofi-lowbit-motion.json +888 -0
  31. package/recipes/flyers/lofi-lowbit.json +880 -0
  32. package/recipes/flyers/lofi-nullpoint.json +3013 -0
  33. package/recipes/flyers/lumio-stack-motion.json +648 -0
  34. package/recipes/flyers/lumio-stack.json +438 -0
  35. package/recipes/flyers/magazine-cover.json +168 -0
  36. package/recipes/flyers/magazine-extended.json +269 -0
  37. package/recipes/flyers/magazine-knockout.json +40 -0
  38. package/recipes/flyers/northfold-varsity.json +618 -0
  39. package/recipes/flyers/open-studio.json +69 -0
  40. package/recipes/flyers/orrin-merch.json +715 -0
  41. package/recipes/flyers/oxyde.json +113 -0
  42. package/recipes/flyers/quiet-fields.json +610 -0
  43. package/recipes/flyers/scrapbook-halcyon.json +948 -0
  44. package/recipes/flyers/solenne-ticket.json +730 -0
  45. package/recipes/flyers/solvane-motion.json +576 -0
  46. package/recipes/flyers/solvane.json +98 -0
  47. package/recipes/flyers/subduct.json +99 -0
  48. package/recipes/flyers/sunvale-lineup.json +584 -0
  49. package/recipes/flyers/tidewell.json +91 -0
  50. package/recipes/flyers/tour-kairo.json +535 -0
  51. package/recipes/flyers/velora-stickers.json +885 -0
  52. package/recipes/flyers/wheatpaste-black.json +313 -0
  53. package/recipes/flyers/wheatpaste-mark.json +198 -0
  54. package/recipes/flyers/wheatpaste-torn.json +114 -0
  55. package/recipes/flyers/wheatpaste-wall.json +984 -0
  56. package/recipes/flyers/wheatpaste-words.json +233 -0
  57. package/recipes/release/aura.json +137 -0
  58. package/recipes/release/chrome.json +433 -0
  59. package/recipes/release/collage.json +372 -0
  60. package/recipes/release/duotone.json +113 -0
  61. package/recipes/release/film-still.json +102 -0
  62. package/recipes/release/headline.json +70 -0
  63. package/recipes/release/instant.json +170 -0
  64. package/recipes/release/label.json +364 -0
  65. package/recipes/release/liquid-glass.json +301 -0
  66. package/recipes/release/odd-tile-dark.json +688 -0
  67. package/recipes/release/odd-tile-dense.json +1056 -0
  68. package/recipes/release/odd-tile-shift.json +541 -0
  69. package/recipes/release/odd-tile-split.json +571 -0
  70. package/recipes/release/odd-tile.json +633 -0
  71. package/recipes/release/off-edge.json +96 -0
  72. package/recipes/release/plinth.json +794 -0
  73. package/recipes/release/riso.json +108 -0
  74. package/recipes/release/series/fluid-label.json +561 -0
  75. package/recipes/release/series/fluid-pixel.json +334 -0
  76. package/recipes/release/series/hairline.json +305 -0
  77. package/recipes/release/series/halo.json +282 -0
  78. package/recipes/release/series/mark-crop.json +135 -0
  79. package/recipes/release/series/mark-window.json +150 -0
  80. package/recipes/release/series/photo-label.json +475 -0
  81. package/recipes/release/series/smear-field.json +345 -0
  82. package/recipes/release/series/smear.json +202 -0
  83. package/recipes/release/series/spin.json +172 -0
  84. package/recipes/release/series/split-ghost.json +230 -0
  85. package/recipes/release/series/zoom.json +168 -0
  86. package/recipes/release/small-print.json +114 -0
  87. package/recipes/release/stack.json +156 -0
  88. package/recipes/release/swiss.json +117 -0
  89. package/schema/recipe.schema.json +7624 -827
  90. package/src/animate/index.d.ts +9 -0
  91. package/src/animate/index.js +11 -0
  92. package/src/animate/keyframes.d.ts +21 -0
  93. package/src/animate/keyframes.js +119 -0
  94. package/src/animate/marquee.d.ts +30 -0
  95. package/src/animate/marquee.js +93 -0
  96. package/src/animate/morph.d.ts +8 -0
  97. package/src/animate/morph.js +99 -0
  98. package/src/animate/motion.d.ts +59 -0
  99. package/src/animate/motion.js +63 -0
  100. package/src/animate/sheet.d.ts +19 -0
  101. package/src/animate/sheet.js +61 -0
  102. package/src/animate/timing.d.ts +28 -0
  103. package/src/animate/timing.js +146 -0
  104. package/src/animate/tracks.d.ts +18 -0
  105. package/src/animate/tracks.js +139 -0
  106. package/src/builtins.js +4 -0
  107. package/src/diagram.js +4 -3
  108. package/src/extract.js +6 -5
  109. package/src/fonts.d.ts +55 -0
  110. package/src/fonts.js +245 -53
  111. package/src/formats/cut.d.ts +10 -0
  112. package/src/formats/cut.js +77 -0
  113. package/src/formats/index.d.ts +6 -0
  114. package/src/formats/index.js +9 -0
  115. package/src/formats/presets.d.ts +41 -0
  116. package/src/formats/presets.js +89 -0
  117. package/src/formats/print.d.ts +25 -0
  118. package/src/formats/print.js +117 -0
  119. package/src/formats/zones.d.ts +10 -0
  120. package/src/formats/zones.js +64 -0
  121. package/src/geometry.js +5 -35
  122. package/src/grid.js +1 -1
  123. package/src/handmade.js +9 -7
  124. package/src/icons.js +42 -9
  125. package/src/index.d.ts +232 -55
  126. package/src/index.js +14 -6
  127. package/src/inter-data.js +27 -0
  128. package/src/json-recipe.js +201 -27
  129. package/src/kit/index.d.ts +20 -0
  130. package/src/kit/index.js +58 -0
  131. package/src/layer-options.js +43 -4
  132. package/src/layers/barcode.d.ts +19 -0
  133. package/src/layers/barcode.js +63 -0
  134. package/src/layers/code128.d.ts +4 -0
  135. package/src/layers/code128.js +28 -0
  136. package/src/layers/expr.d.ts +6 -0
  137. package/src/layers/expr.js +121 -0
  138. package/src/layers/geometry.d.ts +6 -0
  139. package/src/layers/geometry.js +238 -0
  140. package/src/layers/index.d.ts +18 -0
  141. package/src/layers/index.js +15 -0
  142. package/src/layers/perforation.d.ts +14 -0
  143. package/src/layers/perforation.js +47 -0
  144. package/src/layers/repeat.d.ts +29 -0
  145. package/src/layers/repeat.js +90 -0
  146. package/src/layers/shape.d.ts +46 -0
  147. package/src/layers/shape.js +115 -0
  148. package/src/layers/soft.d.ts +5 -0
  149. package/src/layers/soft.js +43 -0
  150. package/src/layers/svg.d.ts +23 -0
  151. package/src/layers/svg.js +108 -0
  152. package/src/layers/table.d.ts +25 -0
  153. package/src/layers/table.js +80 -0
  154. package/src/layers/tear.d.ts +21 -0
  155. package/src/layers/tear.js +74 -0
  156. package/src/layers/texture.d.ts +17 -0
  157. package/src/layers/texture.js +106 -0
  158. package/src/layers/util.d.ts +8 -0
  159. package/src/layers/util.js +61 -0
  160. package/src/layers.js +129 -61
  161. package/src/lint/image-size.d.ts +4 -0
  162. package/src/lint/image-size.js +34 -0
  163. package/src/lint/index.d.ts +16 -0
  164. package/src/lint/index.js +127 -0
  165. package/src/lint/rules.d.ts +5 -0
  166. package/src/lint/rules.js +19 -0
  167. package/src/node.d.ts +2 -0
  168. package/src/node.js +20 -0
  169. package/src/paint/index.d.ts +49 -0
  170. package/src/paint/index.js +66 -0
  171. package/src/paint/presets.d.ts +10 -0
  172. package/src/paint/presets.js +47 -0
  173. package/src/paint/tile.d.ts +7 -0
  174. package/src/paint/tile.js +55 -0
  175. package/src/palette.js +1 -1
  176. package/src/platform/files.d.ts +6 -0
  177. package/src/platform/files.js +41 -0
  178. package/src/platform/harfbuzz.d.ts +9 -0
  179. package/src/platform/harfbuzz.js +80 -0
  180. package/src/platform/index.d.ts +8 -0
  181. package/src/platform/index.js +15 -0
  182. package/src/platform/load.d.ts +6 -0
  183. package/src/platform/load.js +71 -0
  184. package/src/platform/no-harfbuzz.d.ts +2 -0
  185. package/src/platform/no-harfbuzz.js +11 -0
  186. package/src/platform/settings.d.ts +28 -0
  187. package/src/platform/settings.js +51 -0
  188. package/src/print.js +16 -13
  189. package/src/recipes.js +5 -4
  190. package/src/render/backdrop.d.ts +6 -0
  191. package/src/render/backdrop.js +55 -0
  192. package/src/render/context.d.ts +82 -0
  193. package/src/render/grain.d.ts +7 -0
  194. package/src/render/grain.js +113 -0
  195. package/src/render/group.d.ts +23 -0
  196. package/src/render/group.js +67 -0
  197. package/src/render/key.d.ts +5 -0
  198. package/src/render/key.js +90 -0
  199. package/src/render/report.d.ts +5 -0
  200. package/src/render/report.js +19 -0
  201. package/src/render/transform.d.ts +36 -0
  202. package/src/render/transform.js +139 -0
  203. package/src/render/use.d.ts +27 -0
  204. package/src/render/use.js +73 -0
  205. package/src/render/wrap.d.ts +14 -0
  206. package/src/render/wrap.js +144 -0
  207. package/src/render.js +343 -215
  208. package/src/text.d.ts +29 -0
  209. package/src/text.js +23 -16
  210. package/src/type/auto-color.d.ts +9 -0
  211. package/src/type/auto-color.js +44 -0
  212. package/src/type/fallback.d.ts +11 -0
  213. package/src/type/fallback.js +92 -0
  214. package/src/type/glyphs.d.ts +24 -0
  215. package/src/type/glyphs.js +87 -0
  216. package/src/type/index.d.ts +5 -0
  217. package/src/type/index.js +10 -0
  218. package/src/type/kern.d.ts +4 -0
  219. package/src/type/kern.js +144 -0
  220. package/src/type/layout-ref.d.ts +10 -0
  221. package/src/type/layout-ref.js +74 -0
  222. package/src/type/line.d.ts +11 -0
  223. package/src/type/line.js +105 -0
  224. package/src/type/live.d.ts +6 -0
  225. package/src/type/live.js +39 -0
  226. package/src/type/name.d.ts +4 -0
  227. package/src/type/name.js +32 -0
  228. package/src/type/path.d.ts +18 -0
  229. package/src/type/path.js +89 -0
  230. package/src/type/pixel.d.ts +7 -0
  231. package/src/type/pixel.js +85 -0
  232. package/src/type/sfnt.d.ts +20 -0
  233. package/src/type/sfnt.js +258 -0
  234. package/src/type/spans.d.ts +33 -0
  235. package/src/type/spans.js +138 -0
  236. package/src/type/symbols.d.ts +7 -0
  237. package/src/type/symbols.js +89 -0
  238. package/src/type/typeset.d.ts +10 -0
  239. package/src/type/typeset.js +363 -0
  240. package/src/type/woff2.d.ts +6 -0
  241. package/src/type/woff2.js +301 -0
  242. package/src/version.js +4 -0
  243. package/src/color.js +0 -246
  244. package/src/png.js +0 -77
  245. package/src/rng.js +0 -95
  246. package/src/svg.js +0 -69
package/CHANGELOG.md CHANGED
@@ -5,6 +5,124 @@ All notable changes to `@arraypress/covers` are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.1] — 2026-11-06
9
+
10
+ ### Changed
11
+
12
+ - Depends on `@logomarks/image-effects` `^0.1.1` (renamed from `@arraypress/image-effects`); the imports, `.d.ts` files and docs use the new name. No code changes.
13
+
14
+ ## [1.3.0] — 2026-11-05
15
+
16
+ Animation, built to the 1.1 design: covers that move in the SVG itself. Every published recipe renders as before (all 76, SVG at 4:5 and as release covers, and `renderImage` PNGs: 228 renders byte-identical to 1.2.0).
17
+
18
+ ### Added
19
+
20
+ - **`motion` on every layer**: CSS keyframes in the SVG, so a plain `<img>` or inline SVG plays with no script. `translate` (by an offset, or through points), `path` (travel a path from where the layer is), `rotate`, `scale` / `scaleX` / `scaleY` about an `origin`, `opacity`; timing as CSS (`duration`, `delay`, `easing`, `direction`, `iterations`); a list of motions per layer; presets `drift`, `spin`, `breathe`, `blink`, `marquee` (`motion: 'blink'`). The whole layer moves, its effects and masks with it; `blend` stays on the outermost group.
21
+ - **`marquee`**: text on a path runs along it, one repeat of its words per `duration`, looping without a seam. The text is outlines, so each glyph is drawn once and placed by `<use>` copies that share one set of keyframes (the path sampled as `translate() rotate()`), each copy with its own delay.
22
+ - **`vary`**: layer options as lists of stops (`{ axes: { wdth: [62, 150] } }`), drawn frame by frame and played as outline morphs (CSS `d: path()`). A variable font interpolates its outlines point by point between masters, so morphing the outlines it draws breathes a width axis as the font itself would; frames that differ in more than their outlines are refused.
23
+ - **Stills**: `poster` (seconds) is the frame `renderImage`, `renderBlob` and `animate: false` draw, and what an animated SVG shows under `prefers-reduced-motion` (its own attributes); without it, the layers at rest. `renderSvg(cover, { t })` draws a still at `t` for motion too, following CSS timing (so it matches what a browser shows at `t`).
24
+ - **`prefers-reduced-motion`**: one rule in the SVG's stylesheet stops every animation. Animation names carry a hash of the document, so covers inlined together never collide.
25
+ - **Eight recipes that move**, `recipes/flyers/*-motion.json`: `afterglow`, `linger` and `solvane` (aura orbs drifting), `halden-orbit` (rings turning opposite ways), `lumio-stack` (widths breathing), `arcfield-mirror` (a turning ring and a marquee on three waves), `drag-solenne` (the smear drifting) and `lofi-lowbit` (a blinking cursor): the Logomarks scenes lab's animated flyers. Each is its at-rest recipe with motion added, and renders as it when still.
26
+ - `MOTION_PRESETS`; the `Motion`, `MotionPreset` and `Easing` types; `motion` in the recipe schema (options and presets checked in JSON recipes).
27
+
28
+ ### Changed
29
+
30
+ - `renderSvg(cover, { t })` now also applies `motion` at `t` (covers without motion are unchanged).
31
+ - The keyframes module moved to `src/animate/keyframes.js` (`src/animate/` holds the motion modules too); the exports are the same.
32
+
33
+ ## [1.2.0] — 2026-11-05
34
+
35
+ The image filters are now their own library, [`@arraypress/image-effects`](https://github.com/arraypress/image-effects), and covers depends on it. Nothing a covers user sees changes: the same exports, the same effect options, and the same output (every built-in and example recipe, 224 renders, checked before and after the split: SVG byte-identical, rasters pixel-identical).
36
+
37
+ ### Changed
38
+
39
+ - **Effects come from `@arraypress/image-effects` ^0.1.0**: the 71 effects, masks, the pixel-effect runner, and the utilities they share (SVG writing, lengths, seeded randomness, colour, the PNG encoder, rasterising, the platform adapter). covers re-exports what it exported before (`EFFECTS`, `EFFECT_OPTIONS`, `rasterize`, the colour helpers, `createRng`, `hashString`, and the `Effect`, `EffectKind`, `EffectMask`, `Color`, `Length` and `Rng` types). Layer paints (`src/paint/`) and the layer `transform` (`src/render/transform.js`) stay in covers.
40
+
41
+ ### Added
42
+
43
+ - **The example recipes are published**: `recipes/flyers/` (44), `recipes/release/` (20) and `recipes/release/series/` (12), importable as `@arraypress/covers/recipes/flyers/bloomfall.json` (the existing `./recipes/*.json` export), with `$schema` pointing at the bundled schema. `wheatpaste-torn` joined `flyers/`; the earlier `bloomfall` and `grid-rest-rhythm` ports were dropped (the `flyers/` versions are them plus print grain and a release layout).
44
+ - **`renderSvg(cover, { images: 'webp', quality })`**: embedded photos and pixel effects re-encoded as WebP where smaller (about a third off a photo-heavy flyer; needs sharp). For SVGs shown in browsers: librsvg can't draw WebP, so `renderImage` never uses it.
45
+
46
+ ### Notes
47
+
48
+ - **Astro and other Vite SSR builds must keep covers external**: `vite: { ssr: { external: ['@arraypress/covers'] } }`. Bundled, Vite resolves covers' `#harfbuzz` import to the project's own harfbuzzjs (satori's is older) and text fails with "hb.Blob is not a constructor". `@arraypress/image-effects` needs nothing.
49
+
50
+ ## [1.1.0] — 2026-11-05
51
+
52
+ Flyers, posters, release art, social formats and print stationery, in Node, browsers and Cloudflare Workers. Every 1.0 cover renders byte-identical SVG (694 covers checked against hashes taken with the 1.0.0 code).
53
+
54
+ ### Added
55
+
56
+ - **Runs anywhere.** No module imports `node:` any more: Node's built-ins are found with `process.getBuiltinModule` (or installed by the new `@arraypress/covers/node` entry), Inter and the built-in recipes are plain modules, PNG and WOFF use `CompressionStream` / `DecompressionStream` outside Node, and cache keys use Web Crypto (same keys). `configure({ load, network, fetch, harfbuzz, shaper, icons, fonts })`: a loader hook for images, fonts, audio and icon sets; URLs fetched in browsers and Workers (in Node only when allowed, as before); HarfBuzz's wasm from a URL, bytes, a `WebAssembly.Module` or a `Response`.
57
+ - **A JavaScript TrueType outliner** (`cmap`, `hmtx`, `glyf` with composites, GPOS and `kern` pair kerning) for Cloudflare Workers, where harfbuzzjs can't run (it compiles wasm at runtime). `#harfbuzz` resolves to a stub under the `workerd` / `worker` conditions so wrangler never bundles it. Byte-identical to HarfBuzz on all 694 test covers.
58
+ - **`renderBlob()` / `rasterize()`**: PNG, JPEG or WebP in the browser through a canvas.
59
+ - **Fonts by `src`** (path, URL, loader) as well as bytes; `registerFont()`; variable fonts (`weight: [100, 900]`, per-layer `axes`).
60
+ - **Poster type** on the `text` layer: baseline placement (`x`, `y`), justification to a width (`width`, `lengthAdjust`), `fit: 'width' | 'lines' | 'shrink'`, `stretch`, `axes`, outline type and stacked keylines (`fill`, `stroke`, `strokes`), text on a path (circle, arc, ellipse, wave, any `d`; `fitPath`), `color: 'auto'` (white or near-black against what's drawn underneath), and `render: 'live'` (`<text>` with `@font-face`).
61
+ - **Every layer** takes `transform`, `effects`, `knockout`, `clip`, `mask` and `area`.
62
+ - **Effects**: `blur`, `motion` (directional), `shadow`, `glow`, `border` (die-cut), `wear` (riso), `rough`, `warp`, `xerox`, `photocopy`, `duotone`, `grayscale`, `levels`, `saturate`, `matrix`, `grit`, `halftone` (any screen angle), `dither` (Bayer), `threshold`, and a raw `filter` escape hatch.
63
+ - **Eight layers**: `group` (a nested stack on its own design grid), `shape` (fifteen exact shapes, deckled and torn edges, gradient paints including reflected foil), `svg` (raw markup with `{{slot}}` and palette tokens, sanitised), `table` (meta grids, lineups, tracklists), `barcode` (real Code 128), `perforation`, `tear` (torn paper with fibres) and `texture` (paper, creases, concrete, speck, wood, folds, dust). `icon` takes a mark's `markup` and `x`/`y`; `image` takes an explicit box and `fit: 'stretch'`.
64
+ - **Formats** (`format`, `FORMATS`): release cover 3000×3000, Spotify Canvas, Bandcamp header, Instagram post/square/story/reel, TikTok cover, YouTube thumbnail and channel art, X, LinkedIn, Facebook, Twitch, Discord and Bluesky banners, Pinterest, avatars, Open Graph, a 4:5 flyer, A6–A2, business cards (85×55 mm and 3.5×2 in), letterhead (A4, US Letter), compliment slips, stickers, tickets, badges and lanyard passes. Screen formats carry their **safe zones** (sourced; anchored text and icons are laid out inside them; `@safeX`… tokens; `overlay: true` previews them). Print formats render at a `dpi` with **bleed**, a **safe** margin, optional **crop marks** and a **die line**.
65
+ - **Two-sided pieces**: `pages` in a recipe, `when: { page: 'back' }`, `renderPages()`.
66
+ - **`kit(brand, { formats, recipe })`**: a brand kit in one call, and a built-in **`brand`** recipe that lays a brand out on every format.
67
+ - **`lint(cover, { target: 'spotify' })`**: Spotify's artwork requirements and distributors' content rules (URLs, handles, contact details, prices, promo wording, dates, platform names, barcodes, metadata), text too small for a 64 px thumbnail, upscaled images.
68
+ - **Animation, designed in**: any option can be keyframed against `t`, and `renderSvg(cover, { t })` draws that frame (static covers ignore it); `loop`, `valueAt()`. Video encoding is a later phase.
69
+ - **JSON recipes**: brand and event slots (`{name}`, `{mark}`, `{photo}`, `{cutout}`, `{lineup.0}`…), `join` / `upper` / `lower` / `slice`, `format` and `page` conditions, print and safe-zone tokens, nested layers validated.
70
+ - Tests: byte identity with 1.0, a headless-Chrome smoke test, and an opt-in Workers test (`npm run test:worker`). `examples/recipes/` re-expresses ~45 flyers and 16 release-cover styles as JSON.
71
+
72
+ - **Repeat and reuse.** `repeat` draws a layer on a grid, round a ring or along a line, with `{#…}` expressions per instance (`i`, `row`, `col`, `odd`, `angle`, …; a small parser, no `eval`), `odd` and `cycle` overrides and `skip`. `defs` names layers and `use` draws them again, placed (one drawing, referenced), or with `with` overrides.
73
+ - **Type.** `spans` (mixed runs on one line: font, weight, size, colour, baseline shift), `glyphs` (per-glyph scale, shift, turn and colour arrays: bulging 70s type is one layer, and lint counts it as one text), `font: 'pixel'` (a built-in 5×7 face as merged cells), glyph fallback (`fallback`, fonts registered with `fallback: true`, then built-in ✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪), `fit: 'words'` and `'wrap'`, `split: 'words'`, `maxHeight`, `dash` on strokes, `extrude`, and `{layout.<id>.bottom + 12}` so a later layer sits under a wrapped block.
74
+ - **Paints** for shapes, text and icons: elliptical radial gradients (`rx`, `ry`) with per-stop opacities, `tile` patterns (checker, stripes, dots, grid, custom), and presets `chrome`, `steel` and `gold` (a reflected horizon with its keyline, how good Y2K chrome is painted). `icon` takes `fill`.
75
+ - **Shapes**: `rotate` about an `origin`, open arches (`open`), `soft` (a blur drawn padded: no edge fade, no square edge), `dashOffset`, and `kind: 'cut'` (the print format's die-cut, `outset`).
76
+ - **Effects**: `extrude` (an n-step offset in a darker colour, replacing stacks of copies), `border` with `shape: 'circle'` (a round sticker backing), and `colorSpace: 'sRGB' | 'linearRGB'` on every filter effect.
77
+ - **Print cuts**: `cut` takes `{ radius }` (mm) or `{ d, viewBox }`; tickets and lanyard passes have rounded corners; the die line follows it.
78
+ - **Fonts**: WOFF2 is outlined (Brotli from Node's zlib or a browser's `DecompressionStream('brotli')`; glyf, loca and hmtx rebuilt in JavaScript, outlines identical to the TTF builds); recipes declare their fonts (`"fonts": [{ name, src, weight }]`, paths beside the recipe); `scripts/woff2-to-ttf.mjs` for runtimes without Brotli.
79
+ - **`svg` slots** take markup: `{{mark}}` between elements goes in as sanitised shapes (paths and basic shapes, geometry and paint attributes); anything else is still escaped.
80
+
81
+ - **Image filters: 50 new effects**, on any layer or group, each data-first (validated by `defineRecipe`, seedable where random). Filters stay SVG (vector, sized in the layer's units); what filters can't do is computed on pixels: the layer is drawn once at the pixels it's shown at (sharp in Node, tiled past a few megapixels; a canvas in the browser, so `renderBlob` and `rasterize` get them too), processed on a typed array and embedded as one image. One module per family in `src/effects/`, with shared pixel helpers (`buffer.js`) and the runner (`pixels.js`).
82
+ - **Blurs** (pixels): `zoom` (radial), `spin` (rotational: both resampled into polar coordinates and blurred with running sums, so a long blur costs no more than a short one; the centre pixel never moves), `tilt` (tilt-shift: a sharp band, the blur growing beyond it, `saturate` for the miniature look), `lens` (lens blur in linear light with highlights boosted into bokeh discs or `blades`-sided polygons, the aperture convolved a row at a time from running sums).
83
+ - **`mask` on every effect**: `{ gradient: 'linear' | 'radial' | 'band', from, to, center, angle, aspect, invert, ease }` or `{ layers }` (their luminance). An exact mix of before and after: inside the filter (a small greyscale mask image), or per pixel. "The left half motion-blurred, the face sharp" is one effect.
84
+ - **Distortions** (pixels): `wave`, `ripple`, `twirl`, `bulge`, `pinch`, `fisheye`, `glass` (`frosted`, `reeded` (fluted), `pebbled`), `displace` (seeded noise, or the luminance of any layers: a texture, a photo, type), `kaleidoscope` (the frame filled by reflection), `stretch` (pixel stretch), `sort` (pixel sorting by luminance, hue or saturation). `mirror` (vector copies).
85
+ - **Colour** (filters): `curves` (monotone cubic, per channel), `gradientMap` (any stops, sRGB or OKLab), `posterize`, `invert`, `hue` (hue, saturation, lightness), `colorBalance` (shadows, midtones, highlights), `channelMixer`, `sepia`, `look` presets (`fade`, `crossProcess`, `bleachBypass`, `tealOrange`, `warmFilm`, `coolFilm`, `noir`, with `amount`); `vibrance` on pixels. `levels` takes Photoshop's `black`, `white`, `gamma` and `output`.
86
+ - **Light**: `bloom`, `halation`, `vignette` (`ellipse`, `circle`, `rect`; size, softness, colour), `lightLeak`, `lensFlare` (glow, halo, ghosts, optional anamorphic streak: drawn from small greyscale sprites the filter scales and tints), `rays` (god rays on pixels: occluders cast shafts).
87
+ - **Texture and print**: `grain` (film grain overlaid, monochrome or colour), `paper` (`paper`, `canvas`), `scanlines`; on pixels `dust` (specks, hairs, scratches), `crt` (aperture or shadow mask, scanlines, curvature), `vhs` (chroma smear and shift, line jitter, tracking noise), `jpeg` (a real JPEG encoder, blocks at any size, re-saves), `newsprint` (CMYK process halftone with rosettes). `halftone` takes a `shape`: `dot`, `line`, `square`, `diamond`, `cross`.
88
+ - **Digital**: `chromatic` / `rgbSplit` (directional as a filter; `mode: 'radial'` on pixels), `glitch` (channel shift, torn slices, displaced blocks), `pixelate` / `mosaic` (`square` cells embedded at one pixel a cell; `hex`), `ascii` (the built-in pixel face's glyphs by ink, in the source's colours or one ink), `edges` (`ink` or `glow`), `sketch` (pencil), `emboss`, `sharpen` (unsharp mask).
89
+ - `EFFECT_OPTIONS` lists every kind's options (validation, the JSON Schema's autocomplete, the README table). Where nothing can draw pixels (a Worker) `lens` falls back to a Gaussian blur, `tilt` to one through its band, and the other pixel effects are left out.
90
+
91
+ ### Fixed
92
+
93
+ - **`motion` blurred in every direction at any angle but 0 in `renderImage`**: librsvg can't apply a one-axis `feGaussianBlur` under a rotation. `renderImage` now draws motion blur on pixels (in a frame turned to the angle, with the edge held, so a full-bleed photo no longer fades at its edges); `renderSvg` keeps the filter, which browsers draw correctly.
94
+ - **WOFF2 fonts with a large `glyf` lost glyphs past 128 KB** (Doto drew no digits and no "/"): the decoder rebuilds `glyf` plainly, a word a coordinate, so it can outgrow the font's short `loca`, whose offsets then wrapped. It now writes a long `loca` (and says so in `head`) when the table needs one; decoded glyphs, metrics and cmap match fontTools on both Doto builds. A generated WOFF2 with composite digits past the limit is in the tests.
95
+ - **`fit: 'words'` (and `'width'`, `'wrap'`) with a `width` stretched every line to the full width** (a lone "&" became a bar). Under those fits only the widest line fills the width, as documented; the others keep their natural width. `width` without a fit still justifies every line.
96
+ - **Images in a `viewBox` group were resampled at design-unit size** (a 400-wide grid embedded a photo 400 px across, however large the output): images now resample at the pixels they're drawn at, through every group and transform scale. They're embedded at no more than the source's pixels (the SVG scales up), and as JPEG when an alpha channel is unused, so two full-bleed photos no longer overflow librsvg's parser at 3000 px. (A top-level image under 4 MP keeps 1.0's box-sized embed, so 1.0 covers stay byte-identical.)
97
+ - Text on the poster typesetter dropped its `opacity` and `blend`.
98
+ - The `filter` sanitiser rejected `specularExponent`, `diffuseConstant` and `exponent` (it matched `on…=` inside names); it now matches attribute names that start with `on`.
99
+ - `feDropShadow` ignored its `in` in librsvg (a felt texture vanished under its own shadow): it's spelled out as primitives.
100
+ - A dither with a transform on the same layer rendered as a solid colour in `renderImage`.
101
+ - A group's own effects, clips, masks and knockouts used the parent's coordinates: with a `viewBox` they now work in the group's grid, so their numbers (noise frequency, wobble) look the same at every output size.
102
+ - `shape` `path`s with a `viewBox` rotated about the wrong point.
103
+ - Recipe validation now checks anchor names (`'bottom-center'` failed only at render).
104
+ - `color: 'auto'` picked black on mid-tones (a mid-pink photo) where designers set white: it now uses the site's rule, luma above 0.72 takes the dark colour (`autoThreshold`).
105
+ - `lint`: no more false positives for store names that are words ("Tidal Archive": only with a cue or on their own), day names in titles ("Second Sun": short names only beside a number), or text spelled from the metadata a letter or a knocked-out word at a time. Legibility measures the whole block as drawn (every line, stretch, justification, group and transform scale), and `upscaled` measures images through groups and transforms (and flags embeds smaller than drawn).
106
+
107
+ ### Performance
108
+
109
+ `renderImage` at release size (3000×3000) takes seconds where it took minutes. Rendering every ported flyer and style (104 covers) went from 2662 s to 156 s; the slowest cover is now 11 s. Some before → after: `lofi-hexmoth` 609 → 0.9 s, the three `chrome` styles ~235 → 1.5 s, `ferrova` 220 → 1.1 s, `velora-stickers` 182 → 6.3 s, `lofi-lowbit` 145 → 1.6 s, the riso flyers ~67 → 9 s, the `riso` styles ~46 → 6 s.
110
+
111
+ - SVGs with filters past ~2.4 MP render as parallel 1024 px tiles, each with a margin as wide as the furthest any filter reaches (effects report it at their own resolution); one render cost 5–6× its pixel count.
112
+ - `feMorphology` (the die-cut `border`) is split into an x and a y pass: the same result, O(r) instead of O(r²).
113
+ - Filter regions shrink to the box the layer paints, when the chain provably can't paint outside its source.
114
+ - `dither` is computed on pixels: the layer drawn once at one pixel per cell, thresholded in JavaScript, embedded nearest-neighbour (the filter's pixelation ran at output size).
115
+
116
+ ### Known limits
117
+
118
+ - `wear`, `xerox`, lighting and displacement filters are still librsvg's own primitives: a full-bleed two-ink riso cover is ~6–9 s at 3000×3000 (from about a minute). Browsers draw the same SVG far faster. `halftone` stays a filter (it wasn't the bottleneck once tiled).
119
+ - WOFF2 needs Brotli: Node has it; most Workers don't (convert to TTF with `scripts/woff2-to-ttf.mjs`).
120
+
121
+ ### Changed
122
+
123
+ - `engines` stays Node 20; on Node before 20.16 import `@arraypress/covers/node`.
124
+ - Cache keys include the version, so 1.0 caches render again once.
125
+
8
126
  ## [1.0.0] — Unreleased
9
127
 
10
128
  ### Added