@arraypress/covers 0.0.0-stage → 1.1.0

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 (245) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +29 -0
  3. package/README.md +846 -2
  4. package/fonts/OFL.txt +93 -0
  5. package/fonts/inter-latin-400.woff +0 -0
  6. package/fonts/inter-latin-500.woff +0 -0
  7. package/fonts/inter-latin-600.woff +0 -0
  8. package/fonts/inter-latin-700.woff +0 -0
  9. package/package.json +124 -4
  10. package/recipes/aurora.json +132 -0
  11. package/recipes/bauhaus.json +113 -0
  12. package/recipes/bigType.json +103 -0
  13. package/recipes/brand.json +1143 -0
  14. package/recipes/contour.json +109 -0
  15. package/recipes/cutout.json +191 -0
  16. package/recipes/diagram.json +92 -0
  17. package/recipes/dither.json +170 -0
  18. package/recipes/duotone.json +75 -0
  19. package/recipes/gridTitle.json +136 -0
  20. package/recipes/iconTile.json +214 -0
  21. package/recipes/issue.json +221 -0
  22. package/recipes/mark.json +170 -0
  23. package/recipes/photo.json +156 -0
  24. package/recipes/riso.json +387 -0
  25. package/recipes/stripes.json +206 -0
  26. package/recipes/waveform.json +154 -0
  27. package/schema/recipe.schema.json +8822 -0
  28. package/src/animate/index.d.ts +19 -0
  29. package/src/animate/index.js +116 -0
  30. package/src/astro.d.ts +31 -0
  31. package/src/astro.js +86 -0
  32. package/src/builtins.js +4 -0
  33. package/src/color.js +246 -0
  34. package/src/diagram.js +248 -0
  35. package/src/effects/ascii.d.ts +7 -0
  36. package/src/effects/ascii.js +93 -0
  37. package/src/effects/buffer.d.ts +60 -0
  38. package/src/effects/buffer.js +472 -0
  39. package/src/effects/detail.d.ts +12 -0
  40. package/src/effects/detail.js +124 -0
  41. package/src/effects/digital.d.ts +14 -0
  42. package/src/effects/digital.js +216 -0
  43. package/src/effects/distort.d.ts +20 -0
  44. package/src/effects/distort.js +314 -0
  45. package/src/effects/dither.d.ts +8 -0
  46. package/src/effects/dither.js +147 -0
  47. package/src/effects/drop-shadow.d.ts +4 -0
  48. package/src/effects/drop-shadow.js +45 -0
  49. package/src/effects/extrude.d.ts +5 -0
  50. package/src/effects/extrude.js +55 -0
  51. package/src/effects/film.d.ts +11 -0
  52. package/src/effects/film.js +204 -0
  53. package/src/effects/glow.d.ts +8 -0
  54. package/src/effects/glow.js +56 -0
  55. package/src/effects/grade.d.ts +30 -0
  56. package/src/effects/grade.js +274 -0
  57. package/src/effects/halftone.d.ts +8 -0
  58. package/src/effects/halftone.js +95 -0
  59. package/src/effects/index.d.ts +125 -0
  60. package/src/effects/index.js +277 -0
  61. package/src/effects/ink.d.ts +12 -0
  62. package/src/effects/ink.js +70 -0
  63. package/src/effects/lens.d.ts +14 -0
  64. package/src/effects/lens.js +277 -0
  65. package/src/effects/light.d.ts +10 -0
  66. package/src/effects/light.js +59 -0
  67. package/src/effects/looks.d.ts +9 -0
  68. package/src/effects/looks.js +74 -0
  69. package/src/effects/mask.d.ts +52 -0
  70. package/src/effects/mask.js +314 -0
  71. package/src/effects/motion.d.ts +6 -0
  72. package/src/effects/motion.js +60 -0
  73. package/src/effects/newsprint.d.ts +6 -0
  74. package/src/effects/newsprint.js +89 -0
  75. package/src/effects/optics.d.ts +10 -0
  76. package/src/effects/optics.js +213 -0
  77. package/src/effects/options.d.ts +6 -0
  78. package/src/effects/options.js +106 -0
  79. package/src/effects/paint-presets.d.ts +10 -0
  80. package/src/effects/paint-presets.js +47 -0
  81. package/src/effects/paint.d.ts +49 -0
  82. package/src/effects/paint.js +66 -0
  83. package/src/effects/pixels.d.ts +24 -0
  84. package/src/effects/pixels.js +138 -0
  85. package/src/effects/polar.d.ts +10 -0
  86. package/src/effects/polar.js +265 -0
  87. package/src/effects/region.d.ts +8 -0
  88. package/src/effects/region.js +136 -0
  89. package/src/effects/round-border.d.ts +5 -0
  90. package/src/effects/round-border.js +62 -0
  91. package/src/effects/screen.d.ts +14 -0
  92. package/src/effects/screen.js +212 -0
  93. package/src/effects/sort.d.ts +8 -0
  94. package/src/effects/sort.js +125 -0
  95. package/src/effects/symmetry.d.ts +6 -0
  96. package/src/effects/symmetry.js +74 -0
  97. package/src/effects/tile.d.ts +7 -0
  98. package/src/effects/tile.js +55 -0
  99. package/src/effects/tone.d.ts +16 -0
  100. package/src/effects/tone.js +117 -0
  101. package/src/effects/transform.d.ts +35 -0
  102. package/src/effects/transform.js +127 -0
  103. package/src/effects/util.d.ts +16 -0
  104. package/src/effects/util.js +85 -0
  105. package/src/extract.js +91 -0
  106. package/src/flatten.js +234 -0
  107. package/src/fonts.d.ts +55 -0
  108. package/src/fonts.js +393 -0
  109. package/src/formats/cut.d.ts +10 -0
  110. package/src/formats/cut.js +77 -0
  111. package/src/formats/index.d.ts +6 -0
  112. package/src/formats/index.js +9 -0
  113. package/src/formats/presets.d.ts +41 -0
  114. package/src/formats/presets.js +89 -0
  115. package/src/formats/print.d.ts +25 -0
  116. package/src/formats/print.js +117 -0
  117. package/src/formats/zones.d.ts +10 -0
  118. package/src/formats/zones.js +64 -0
  119. package/src/geometry.js +187 -0
  120. package/src/grid.js +41 -0
  121. package/src/handmade.js +253 -0
  122. package/src/icons.js +191 -0
  123. package/src/index.d.ts +1083 -0
  124. package/src/index.js +27 -0
  125. package/src/inter-data.js +27 -0
  126. package/src/json-recipe.js +616 -0
  127. package/src/kit/index.d.ts +20 -0
  128. package/src/kit/index.js +58 -0
  129. package/src/layer-options.js +56 -0
  130. package/src/layers/barcode.d.ts +19 -0
  131. package/src/layers/barcode.js +63 -0
  132. package/src/layers/code128.d.ts +4 -0
  133. package/src/layers/code128.js +28 -0
  134. package/src/layers/expr.d.ts +6 -0
  135. package/src/layers/expr.js +121 -0
  136. package/src/layers/geometry.d.ts +6 -0
  137. package/src/layers/geometry.js +238 -0
  138. package/src/layers/index.d.ts +18 -0
  139. package/src/layers/index.js +15 -0
  140. package/src/layers/perforation.d.ts +14 -0
  141. package/src/layers/perforation.js +47 -0
  142. package/src/layers/repeat.d.ts +29 -0
  143. package/src/layers/repeat.js +90 -0
  144. package/src/layers/shape.d.ts +46 -0
  145. package/src/layers/shape.js +115 -0
  146. package/src/layers/soft.d.ts +5 -0
  147. package/src/layers/soft.js +43 -0
  148. package/src/layers/svg.d.ts +23 -0
  149. package/src/layers/svg.js +109 -0
  150. package/src/layers/table.d.ts +25 -0
  151. package/src/layers/table.js +80 -0
  152. package/src/layers/tear.d.ts +21 -0
  153. package/src/layers/tear.js +74 -0
  154. package/src/layers/texture.d.ts +17 -0
  155. package/src/layers/texture.js +106 -0
  156. package/src/layers/util.d.ts +8 -0
  157. package/src/layers/util.js +48 -0
  158. package/src/layers.js +1056 -0
  159. package/src/lint/image-size.d.ts +4 -0
  160. package/src/lint/image-size.js +34 -0
  161. package/src/lint/index.d.ts +16 -0
  162. package/src/lint/index.js +127 -0
  163. package/src/lint/rules.d.ts +5 -0
  164. package/src/lint/rules.js +19 -0
  165. package/src/node.d.ts +2 -0
  166. package/src/node.js +20 -0
  167. package/src/palette.js +185 -0
  168. package/src/platform/bytes.d.ts +22 -0
  169. package/src/platform/bytes.js +103 -0
  170. package/src/platform/compress.d.ts +12 -0
  171. package/src/platform/compress.js +81 -0
  172. package/src/platform/harfbuzz.d.ts +9 -0
  173. package/src/platform/harfbuzz.js +80 -0
  174. package/src/platform/index.d.ts +8 -0
  175. package/src/platform/index.js +13 -0
  176. package/src/platform/load.d.ts +6 -0
  177. package/src/platform/load.js +72 -0
  178. package/src/platform/no-harfbuzz.d.ts +2 -0
  179. package/src/platform/no-harfbuzz.js +11 -0
  180. package/src/platform/node.d.ts +26 -0
  181. package/src/platform/node.js +86 -0
  182. package/src/platform/settings.d.ts +28 -0
  183. package/src/platform/settings.js +51 -0
  184. package/src/png.js +126 -0
  185. package/src/print.js +659 -0
  186. package/src/raster/index.d.ts +24 -0
  187. package/src/raster/index.js +171 -0
  188. package/src/recipes.js +79 -0
  189. package/src/render/backdrop.d.ts +6 -0
  190. package/src/render/backdrop.js +55 -0
  191. package/src/render/content.d.ts +5 -0
  192. package/src/render/content.js +58 -0
  193. package/src/render/context.d.ts +82 -0
  194. package/src/render/grain.d.ts +7 -0
  195. package/src/render/grain.js +113 -0
  196. package/src/render/group.d.ts +23 -0
  197. package/src/render/group.js +67 -0
  198. package/src/render/key.d.ts +5 -0
  199. package/src/render/key.js +91 -0
  200. package/src/render/report.d.ts +5 -0
  201. package/src/render/report.js +19 -0
  202. package/src/render/tiles.d.ts +6 -0
  203. package/src/render/tiles.js +77 -0
  204. package/src/render/use.d.ts +27 -0
  205. package/src/render/use.js +73 -0
  206. package/src/render/wrap.d.ts +14 -0
  207. package/src/render/wrap.js +199 -0
  208. package/src/render.js +553 -0
  209. package/src/rng.js +95 -0
  210. package/src/svg.js +69 -0
  211. package/src/text.d.ts +29 -0
  212. package/src/text.js +237 -0
  213. package/src/type/auto-color.d.ts +9 -0
  214. package/src/type/auto-color.js +44 -0
  215. package/src/type/fallback.d.ts +11 -0
  216. package/src/type/fallback.js +92 -0
  217. package/src/type/glyphs.d.ts +24 -0
  218. package/src/type/glyphs.js +87 -0
  219. package/src/type/index.d.ts +5 -0
  220. package/src/type/index.js +10 -0
  221. package/src/type/kern.d.ts +4 -0
  222. package/src/type/kern.js +144 -0
  223. package/src/type/layout-ref.d.ts +10 -0
  224. package/src/type/layout-ref.js +74 -0
  225. package/src/type/line.d.ts +11 -0
  226. package/src/type/line.js +105 -0
  227. package/src/type/live.d.ts +6 -0
  228. package/src/type/live.js +39 -0
  229. package/src/type/name.d.ts +4 -0
  230. package/src/type/name.js +32 -0
  231. package/src/type/path.d.ts +18 -0
  232. package/src/type/path.js +89 -0
  233. package/src/type/pixel.d.ts +9 -0
  234. package/src/type/pixel.js +115 -0
  235. package/src/type/sfnt.d.ts +20 -0
  236. package/src/type/sfnt.js +258 -0
  237. package/src/type/spans.d.ts +33 -0
  238. package/src/type/spans.js +138 -0
  239. package/src/type/symbols.d.ts +7 -0
  240. package/src/type/symbols.js +89 -0
  241. package/src/type/typeset.d.ts +10 -0
  242. package/src/type/typeset.js +352 -0
  243. package/src/type/woff2.d.ts +6 -0
  244. package/src/type/woff2.js +301 -0
  245. package/src/version.js +4 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,126 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@arraypress/covers` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] — 2026-11-05
9
+
10
+ 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).
11
+
12
+ ### Added
13
+
14
+ - **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`.
15
+ - **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.
16
+ - **`renderBlob()` / `rasterize()`**: PNG, JPEG or WebP in the browser through a canvas.
17
+ - **Fonts by `src`** (path, URL, loader) as well as bytes; `registerFont()`; variable fonts (`weight: [100, 900]`, per-layer `axes`).
18
+ - **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`).
19
+ - **Every layer** takes `transform`, `effects`, `knockout`, `clip`, `mask` and `area`.
20
+ - **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.
21
+ - **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'`.
22
+ - **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**.
23
+ - **Two-sided pieces**: `pages` in a recipe, `when: { page: 'back' }`, `renderPages()`.
24
+ - **`kit(brand, { formats, recipe })`**: a brand kit in one call, and a built-in **`brand`** recipe that lays a brand out on every format.
25
+ - **`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.
26
+ - **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.
27
+ - **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.
28
+ - 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.
29
+
30
+ - **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.
31
+ - **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.
32
+ - **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`.
33
+ - **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`).
34
+ - **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.
35
+ - **Print cuts**: `cut` takes `{ radius }` (mm) or `{ d, viewBox }`; tickets and lanyard passes have rounded corners; the die line follows it.
36
+ - **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.
37
+ - **`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.
38
+
39
+ - **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`).
40
+ - **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).
41
+ - **`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.
42
+ - **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).
43
+ - **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`.
44
+ - **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).
45
+ - **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`.
46
+ - **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).
47
+ - `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.
48
+
49
+ ### Fixed
50
+
51
+ - **`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.
52
+ - **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.
53
+ - **`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.
54
+ - **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.)
55
+ - Text on the poster typesetter dropped its `opacity` and `blend`.
56
+ - The `filter` sanitiser rejected `specularExponent`, `diffuseConstant` and `exponent` (it matched `on…=` inside names); it now matches attribute names that start with `on`.
57
+ - `feDropShadow` ignored its `in` in librsvg (a felt texture vanished under its own shadow): it's spelled out as primitives.
58
+ - A dither with a transform on the same layer rendered as a solid colour in `renderImage`.
59
+ - 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.
60
+ - `shape` `path`s with a `viewBox` rotated about the wrong point.
61
+ - Recipe validation now checks anchor names (`'bottom-center'` failed only at render).
62
+ - `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`).
63
+ - `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).
64
+
65
+ ### Performance
66
+
67
+ `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.
68
+
69
+ - 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.
70
+ - `feMorphology` (the die-cut `border`) is split into an x and a y pass: the same result, O(r) instead of O(r²).
71
+ - Filter regions shrink to the box the layer paints, when the chain provably can't paint outside its source.
72
+ - `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).
73
+
74
+ ### Known limits
75
+
76
+ - `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).
77
+ - WOFF2 needs Brotli: Node has it; most Workers don't (convert to TTF with `scripts/woff2-to-ttf.mjs`).
78
+
79
+ ### Changed
80
+
81
+ - `engines` stays Node 20; on Node before 20.16 import `@arraypress/covers/node`.
82
+ - Cache keys include the version, so 1.0 caches render again once.
83
+
84
+ ## [1.0.0] — Unreleased
85
+
86
+ ### Added
87
+
88
+ - **`cover()`, `renderSvg()`, `renderImage()`**: build-time cover images from a
89
+ seed. The same input gives byte-identical SVG; each layer has its own seeded
90
+ stream, so adding one never reshuffles the others.
91
+ - **Gradient fields keep to neighbouring hues** (`mesh.hueSpan`, 60° in `aurora`, `gridTitle` and `diagram`): no olive-with-teal or red-with-orange clashes. **`issue`** sets its numeral from the left, sized by digit count and cropped at the bottom only, so it reads on every aspect and never covers the "No." (new `aspect` condition and `length` operator). **Per-recipe encoding:** a recipe's `output: { lossless: true }` (dither: ~1 KB lossless against 150–240 KB lossy) and `renderImage({ lossless })`. The WebP default stays at 90.
92
+ - **Seven more recipes: `riso`, `dither`, `contour`, `waveform`, `bauhaus`, `issue`, `photo`**, on six new layers: `ink` (risograph inks with multiply, misregistration, speckle or halftone, uneven density; `RISO_INKS`), `dither` (Bayer 4×4/8×8 or noise, crisp cells), `contour` (marching-squares isolines, index and accent lines), `waveform` (peaks, or audio decoded with `@arraypress/waveform-gen` as an optional peer; demo peaks otherwise), `tiles` (a Bauhaus grid with a shared `series` rhythm) and `rule`. `mesh.from` and `extractPalette()` take colours from a photo. Titles are held to 4.5:1 and marks to 3:1 on every preset (tested).
93
+ - **`diagram`, `paper` and `scribble` layers; `diagram` and `cutout` recipes.** White line-art nodes and arrows with a dotted grid that breaks around them (layouts single / row / triangle / square / stack / hub); cut-paper shapes with scissor-cut edges; icons re-drawn by hand (SVG shapes flattened, wobbled, stroked at a drifting width) and hand-drawn node graphs. White line-art is held to 3:1 and both cutout inks to 3:1 on every preset.
94
+ - **`mesh` gains `fold` (a silk-edged second field) and `streaks`; `grain` gains `vary: 'edges'`; `pattern` gains `divisions` and `gaps`; JSON gains `tone`.** `aurora`/`gridTitle` carry no title unless given (the reference sets it under the card), vary folds/streaks per seed, and use a dotted 4×4 grid. `renderImage` renders mesh layers padded (librsvg clips filters to the canvas, which made the fields fade at the edges).
95
+ - **`mesh` is full-bleed**: long fields along a seeded direction (`arrangement: 'sweep'`, `angle`, `streak`), a gradient base between its colours (`base: 'auto'`) so the page colour never shows round the edges, and `tone` to pull colours into an OKLCH band. `aurora` and `gridTitle` retuned against openai.com/news (measured: no vignette, mid-tone fields, dashed grid); a test holds their titles to 4.5:1 on every preset. `pattern` gains `dash`.
96
+ - **Twenty-one layer types** (the newer ones above): `solid`, `linear`, `radial`, `mesh`, `stripes`,
97
+ `image` (cover/contain, smart crop, duotone, tint), `grain`, `pattern`
98
+ (grid, dots, lines, rings, waves, with fades), `shapes`, `icon` (Iconify
99
+ names, raw SVG or a path, optional tile), `text` (HarfBuzz-shaped outlines,
100
+ balanced wrapping, `maxLines` with ellipsis, `minSize` fit, label, pill) and
101
+ `frame`.
102
+ - **Sixteen recipes** (the newer ones above and below): `aurora`, `gridTitle`, `iconTile`, `bigType`, `stripes`,
103
+ `duotone` and `mark` (one icon on the accent or an accent gradient; white
104
+ whenever it clears WCAG 1.4.11's 3:1 against every colour under the icon,
105
+ else near-black; `iconColor` to force one). Any layer is customised by id: a string sets its
106
+ content, an object merges in, `false` removes it.
107
+ - **JSON recipes**: `defineRecipe(json)`, `loadRecipe(path)`, or the document
108
+ passed as `recipe`. Palette and canvas tokens (`@accent`, `@colors.1`,
109
+ `@u`), vars (`$name`), option templates (`{title}`, `{opt.x}`), operators
110
+ (`mix` in OKLab or OKLCH, `readable`, seeded `pick`/`range`/`int`,
111
+ arithmetic, `ramp`, `taper`, `reverse`, `if`), `when` conditions, `params`
112
+ and `requires`. Validated on definition with the JSON path in every error;
113
+ a JSON Schema ships as `@arraypress/covers/recipe.schema.json`. All seven
114
+ built-ins are JSON (`recipes/*.json`), tested byte-identical to the
115
+ original JavaScript recipes.
116
+ - `palette.onAccent`, `mixOklch()`, `onBackground()` and `gradientAt()`; the
117
+ `readable` operator's `[backgrounds, ratio, prefer]` form and
118
+ `gradientUnder`.
119
+ - **Palettes**: nine presets, `[bg, ...colors]` arrays, partial objects, and
120
+ `derivePalette()` from a theme's tokens (OKLCH, text at 7:1).
121
+ - **Inter** (Latin, 400/500/600/700, WOFF) bundled; any TTF/OTF/WOFF can be
122
+ added.
123
+ - **`@arraypress/covers/astro`**: `coverEndpoint()` (`getStaticPaths` + `GET`
124
+ for `src/pages/covers/[slug].webp.ts`) and `coverUrl()`.
125
+ - **Content-hash cache** (`cache: true` → `node_modules/.cache/covers`) and
126
+ `coverKey()`.
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArrayPress Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Third-party files:
26
+
27
+ - fonts/inter-latin-*.woff: Inter (https://rsms.me/inter/), Copyright 2016
28
+ The Inter Project Authors, under the SIL Open Font License 1.1
29
+ (fonts/OFL.txt).