@arraypress/covers 1.0.0 → 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 (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +296 -11
  3. package/package.json +33 -5
  4. package/recipes/brand.json +1143 -0
  5. package/schema/recipe.schema.json +7300 -770
  6. package/src/animate/index.d.ts +19 -0
  7. package/src/animate/index.js +116 -0
  8. package/src/builtins.js +4 -0
  9. package/src/effects/ascii.d.ts +7 -0
  10. package/src/effects/ascii.js +93 -0
  11. package/src/effects/buffer.d.ts +60 -0
  12. package/src/effects/buffer.js +472 -0
  13. package/src/effects/detail.d.ts +12 -0
  14. package/src/effects/detail.js +124 -0
  15. package/src/effects/digital.d.ts +14 -0
  16. package/src/effects/digital.js +216 -0
  17. package/src/effects/distort.d.ts +20 -0
  18. package/src/effects/distort.js +314 -0
  19. package/src/effects/dither.d.ts +8 -0
  20. package/src/effects/dither.js +147 -0
  21. package/src/effects/drop-shadow.d.ts +4 -0
  22. package/src/effects/drop-shadow.js +45 -0
  23. package/src/effects/extrude.d.ts +5 -0
  24. package/src/effects/extrude.js +55 -0
  25. package/src/effects/film.d.ts +11 -0
  26. package/src/effects/film.js +204 -0
  27. package/src/effects/glow.d.ts +8 -0
  28. package/src/effects/glow.js +56 -0
  29. package/src/effects/grade.d.ts +30 -0
  30. package/src/effects/grade.js +274 -0
  31. package/src/effects/halftone.d.ts +8 -0
  32. package/src/effects/halftone.js +95 -0
  33. package/src/effects/index.d.ts +125 -0
  34. package/src/effects/index.js +277 -0
  35. package/src/effects/ink.d.ts +12 -0
  36. package/src/effects/ink.js +70 -0
  37. package/src/effects/lens.d.ts +14 -0
  38. package/src/effects/lens.js +277 -0
  39. package/src/effects/light.d.ts +10 -0
  40. package/src/effects/light.js +59 -0
  41. package/src/effects/looks.d.ts +9 -0
  42. package/src/effects/looks.js +74 -0
  43. package/src/effects/mask.d.ts +52 -0
  44. package/src/effects/mask.js +314 -0
  45. package/src/effects/motion.d.ts +6 -0
  46. package/src/effects/motion.js +60 -0
  47. package/src/effects/newsprint.d.ts +6 -0
  48. package/src/effects/newsprint.js +89 -0
  49. package/src/effects/optics.d.ts +10 -0
  50. package/src/effects/optics.js +213 -0
  51. package/src/effects/options.d.ts +6 -0
  52. package/src/effects/options.js +106 -0
  53. package/src/effects/paint-presets.d.ts +10 -0
  54. package/src/effects/paint-presets.js +47 -0
  55. package/src/effects/paint.d.ts +49 -0
  56. package/src/effects/paint.js +66 -0
  57. package/src/effects/pixels.d.ts +24 -0
  58. package/src/effects/pixels.js +138 -0
  59. package/src/effects/polar.d.ts +10 -0
  60. package/src/effects/polar.js +265 -0
  61. package/src/effects/region.d.ts +8 -0
  62. package/src/effects/region.js +136 -0
  63. package/src/effects/round-border.d.ts +5 -0
  64. package/src/effects/round-border.js +62 -0
  65. package/src/effects/screen.d.ts +14 -0
  66. package/src/effects/screen.js +212 -0
  67. package/src/effects/sort.d.ts +8 -0
  68. package/src/effects/sort.js +125 -0
  69. package/src/effects/symmetry.d.ts +6 -0
  70. package/src/effects/symmetry.js +74 -0
  71. package/src/effects/tile.d.ts +7 -0
  72. package/src/effects/tile.js +55 -0
  73. package/src/effects/tone.d.ts +16 -0
  74. package/src/effects/tone.js +117 -0
  75. package/src/effects/transform.d.ts +35 -0
  76. package/src/effects/transform.js +127 -0
  77. package/src/effects/util.d.ts +16 -0
  78. package/src/effects/util.js +85 -0
  79. package/src/extract.js +4 -3
  80. package/src/fonts.d.ts +55 -0
  81. package/src/fonts.js +246 -53
  82. package/src/formats/cut.d.ts +10 -0
  83. package/src/formats/cut.js +77 -0
  84. package/src/formats/index.d.ts +6 -0
  85. package/src/formats/index.js +9 -0
  86. package/src/formats/presets.d.ts +41 -0
  87. package/src/formats/presets.js +89 -0
  88. package/src/formats/print.d.ts +25 -0
  89. package/src/formats/print.js +117 -0
  90. package/src/formats/zones.d.ts +10 -0
  91. package/src/formats/zones.js +64 -0
  92. package/src/geometry.js +1 -1
  93. package/src/icons.js +43 -9
  94. package/src/index.d.ts +205 -19
  95. package/src/index.js +12 -4
  96. package/src/inter-data.js +27 -0
  97. package/src/json-recipe.js +181 -23
  98. package/src/kit/index.d.ts +20 -0
  99. package/src/kit/index.js +58 -0
  100. package/src/layer-options.js +23 -4
  101. package/src/layers/barcode.d.ts +19 -0
  102. package/src/layers/barcode.js +63 -0
  103. package/src/layers/code128.d.ts +4 -0
  104. package/src/layers/code128.js +28 -0
  105. package/src/layers/expr.d.ts +6 -0
  106. package/src/layers/expr.js +121 -0
  107. package/src/layers/geometry.d.ts +6 -0
  108. package/src/layers/geometry.js +238 -0
  109. package/src/layers/index.d.ts +18 -0
  110. package/src/layers/index.js +15 -0
  111. package/src/layers/perforation.d.ts +14 -0
  112. package/src/layers/perforation.js +47 -0
  113. package/src/layers/repeat.d.ts +29 -0
  114. package/src/layers/repeat.js +90 -0
  115. package/src/layers/shape.d.ts +46 -0
  116. package/src/layers/shape.js +115 -0
  117. package/src/layers/soft.d.ts +5 -0
  118. package/src/layers/soft.js +43 -0
  119. package/src/layers/svg.d.ts +23 -0
  120. package/src/layers/svg.js +109 -0
  121. package/src/layers/table.d.ts +25 -0
  122. package/src/layers/table.js +80 -0
  123. package/src/layers/tear.d.ts +21 -0
  124. package/src/layers/tear.js +74 -0
  125. package/src/layers/texture.d.ts +17 -0
  126. package/src/layers/texture.js +106 -0
  127. package/src/layers/util.d.ts +8 -0
  128. package/src/layers/util.js +48 -0
  129. package/src/layers.js +125 -41
  130. package/src/lint/image-size.d.ts +4 -0
  131. package/src/lint/image-size.js +34 -0
  132. package/src/lint/index.d.ts +16 -0
  133. package/src/lint/index.js +127 -0
  134. package/src/lint/rules.d.ts +5 -0
  135. package/src/lint/rules.js +19 -0
  136. package/src/node.d.ts +2 -0
  137. package/src/node.js +20 -0
  138. package/src/platform/bytes.d.ts +22 -0
  139. package/src/platform/bytes.js +103 -0
  140. package/src/platform/compress.d.ts +12 -0
  141. package/src/platform/compress.js +81 -0
  142. package/src/platform/harfbuzz.d.ts +9 -0
  143. package/src/platform/harfbuzz.js +80 -0
  144. package/src/platform/index.d.ts +8 -0
  145. package/src/platform/index.js +13 -0
  146. package/src/platform/load.d.ts +6 -0
  147. package/src/platform/load.js +72 -0
  148. package/src/platform/no-harfbuzz.d.ts +2 -0
  149. package/src/platform/no-harfbuzz.js +11 -0
  150. package/src/platform/node.d.ts +26 -0
  151. package/src/platform/node.js +86 -0
  152. package/src/platform/settings.d.ts +28 -0
  153. package/src/platform/settings.js +51 -0
  154. package/src/png.js +77 -28
  155. package/src/print.js +8 -7
  156. package/src/raster/index.d.ts +24 -0
  157. package/src/raster/index.js +171 -0
  158. package/src/recipes.js +4 -3
  159. package/src/render/backdrop.d.ts +6 -0
  160. package/src/render/backdrop.js +55 -0
  161. package/src/render/content.d.ts +5 -0
  162. package/src/render/content.js +58 -0
  163. package/src/render/context.d.ts +82 -0
  164. package/src/render/grain.d.ts +7 -0
  165. package/src/render/grain.js +113 -0
  166. package/src/render/group.d.ts +23 -0
  167. package/src/render/group.js +67 -0
  168. package/src/render/key.d.ts +5 -0
  169. package/src/render/key.js +91 -0
  170. package/src/render/report.d.ts +5 -0
  171. package/src/render/report.js +19 -0
  172. package/src/render/tiles.d.ts +6 -0
  173. package/src/render/tiles.js +77 -0
  174. package/src/render/use.d.ts +27 -0
  175. package/src/render/use.js +73 -0
  176. package/src/render/wrap.d.ts +14 -0
  177. package/src/render/wrap.js +199 -0
  178. package/src/render.js +303 -212
  179. package/src/text.d.ts +29 -0
  180. package/src/text.js +22 -15
  181. package/src/type/auto-color.d.ts +9 -0
  182. package/src/type/auto-color.js +44 -0
  183. package/src/type/fallback.d.ts +11 -0
  184. package/src/type/fallback.js +92 -0
  185. package/src/type/glyphs.d.ts +24 -0
  186. package/src/type/glyphs.js +87 -0
  187. package/src/type/index.d.ts +5 -0
  188. package/src/type/index.js +10 -0
  189. package/src/type/kern.d.ts +4 -0
  190. package/src/type/kern.js +144 -0
  191. package/src/type/layout-ref.d.ts +10 -0
  192. package/src/type/layout-ref.js +74 -0
  193. package/src/type/line.d.ts +11 -0
  194. package/src/type/line.js +105 -0
  195. package/src/type/live.d.ts +6 -0
  196. package/src/type/live.js +39 -0
  197. package/src/type/name.d.ts +4 -0
  198. package/src/type/name.js +32 -0
  199. package/src/type/path.d.ts +18 -0
  200. package/src/type/path.js +89 -0
  201. package/src/type/pixel.d.ts +9 -0
  202. package/src/type/pixel.js +115 -0
  203. package/src/type/sfnt.d.ts +20 -0
  204. package/src/type/sfnt.js +258 -0
  205. package/src/type/spans.d.ts +33 -0
  206. package/src/type/spans.js +138 -0
  207. package/src/type/symbols.d.ts +7 -0
  208. package/src/type/symbols.js +89 -0
  209. package/src/type/typeset.d.ts +10 -0
  210. package/src/type/typeset.js +352 -0
  211. package/src/type/woff2.d.ts +6 -0
  212. package/src/type/woff2.js +301 -0
  213. package/src/version.js +4 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The `shape` layer.
3
+ *
4
+ * @module layers/shape
5
+ */
6
+
7
+ import { attrs, num } from '../svg.js';
8
+ import { len } from '../geometry.js';
9
+ import { toHex } from '../color.js';
10
+ import { paint } from '../effects/paint.js';
11
+ import { presetStrokes } from '../effects/paint-presets.js';
12
+ import { wrap, pos } from './util.js';
13
+ import { geometry } from './geometry.js';
14
+ import { soften } from './soft.js';
15
+
16
+ /** Kinds drawn around a centre, whose `rotate` turns their points (about the centre). */
17
+ const POLAR = ['star', 'burst', 'scallop', 'rays', 'sector'];
18
+
19
+ /** Kinds that are lines unless closed: no fill by default. */
20
+ const isOpen = (/** @type {any} */ s) => s.kind === 'line' || s.kind === 'polyline' || (s.kind === 'wave' && !s.band) || (s.kind === 'arch' && s.open);
21
+
22
+ /**
23
+ * `shape` — one exact shape, or `items` of them: rectangles (with deckled,
24
+ * torn, zigzag or wavy `edge`s), circles, ellipses, lines, polygons, stars,
25
+ * bursts, scallops, rays, rings, sectors, arches (open: an arc), waves, raw
26
+ * paths and the print format's `cut`. Each takes `fill` and `stroke` (a
27
+ * colour, a gradient, a tile or a preset), `strokeWidth`, `dash`,
28
+ * `dashOffset`, `linecap`, `linejoin`, `fillRule`, `opacity`, `rotate`
29
+ * (about its centre, or `origin`), `soft` (a blur, drawn padded so it never
30
+ * shows a square edge) and `strokes` (stacked keylines, widest first).
31
+ *
32
+ * @param {any} layer
33
+ * @param {import('../layers.js').RenderContext} ctx
34
+ * @returns {Promise<import('../layers.js').Markup>}
35
+ */
36
+ export async function shape(layer, ctx) {
37
+ const items = layer.items ?? [layer];
38
+ let defs = '';
39
+ let body = '';
40
+ for (let i = 0; i < items.length; i++) {
41
+ const raw = items[i];
42
+ const s = layer.items ? { fill: layer.fill, stroke: layer.stroke, strokeWidth: layer.strokeWidth, dash: layer.dash, dashOffset: layer.dashOffset, linecap: layer.linecap, linejoin: layer.linejoin, strokes: layer.strokes, soft: layer.soft, ...raw } : raw;
43
+ const polar = POLAR.includes(s.kind) && s.origin == null;
44
+ const g = /** @type {any} */ (geometry(polar ? s : { ...s, rotate: undefined }, ctx));
45
+ const open = g.el ?? `<path d="${g.d}"`;
46
+ const fp = paint(s.fill, isOpen(s) ? 'none' : ctx.palette.text, { id: `${ctx.id}s${i}f`, box: g.box, canvas: ctx.canvas });
47
+ const sp = s.stroke ? paint(s.stroke, 'none', { id: `${ctx.id}s${i}k`, box: g.box, canvas: ctx.canvas }) : null;
48
+ let own = fp.defs + (sp?.defs ?? '');
49
+ const sw = s.strokeWidth != null ? len(s.strokeWidth, ctx.canvas) : sp ? len('0.2vmin', ctx.canvas) : undefined;
50
+ const tr = [rotation(s, g, ctx, polar), g.transform].filter(Boolean).join(' ') || undefined;
51
+ const common = { 'fill-rule': s.fillRule, opacity: s.opacity != null && s.opacity < 1 ? s.opacity : undefined, transform: tr };
52
+ let markup = '';
53
+ const strokes = s.strokes ?? presetStrokes(s.fill);
54
+ if (strokes) {
55
+ const sorted = [...strokes].sort((a, b) => len(b.width, ctx.canvas) - len(a.width, ctx.canvas));
56
+ for (const k of sorted) markup += `${open}${attrs({ fill: 'none', stroke: toHex(k.color), 'stroke-width': len(k.width, ctx.canvas), 'stroke-linejoin': 'round', 'stroke-dasharray': k.dash ? dashArray(k.dash, len(k.width, ctx.canvas), ctx) : undefined, ...common })}/>`;
57
+ }
58
+ markup += `${open}${attrs({ fill: fp.value, stroke: sp?.value, 'stroke-width': sp ? sw : undefined, 'stroke-dasharray': s.dash ? dashArray(s.dash, sw ?? 1, ctx) : undefined, 'stroke-dashoffset': s.dash && s.dashOffset != null ? len(s.dashOffset, ctx.canvas) : undefined, 'stroke-linecap': s.linecap ?? (s.dash === 'dotted' ? 'round' : undefined), 'stroke-linejoin': s.linejoin, ...common })}/>`;
59
+ if (s.soft) {
60
+ // The blur's reach, round the shape's bounds (turned, when it's turned).
61
+ const soft = await soften({ defs: own, body: markup }, reach(g.box, s, tr ? s.rotate ?? 0 : 0, sw), len(s.soft, ctx.canvas), ctx, `${ctx.id}s${i}b`);
62
+ own = soft.defs;
63
+ markup = soft.body;
64
+ }
65
+ defs += own;
66
+ body += markup;
67
+ }
68
+ return { defs, body: wrap(layer, body) };
69
+ }
70
+
71
+ /**
72
+ * A dash pattern: `'dashed'`, `'dotted'`, or lengths `[on, off, …]`.
73
+ *
74
+ * @param {any} dash @param {number} width - The stroke width (dashed and dotted scale with it).
75
+ * @param {import('../layers.js').RenderContext} ctx
76
+ * @returns {string}
77
+ */
78
+ export function dashArray(dash, width, ctx) {
79
+ if (Array.isArray(dash)) return dash.map((/** @type {any} */ d) => num(len(d, ctx.canvas))).join(' ');
80
+ return dash === 'dotted' ? `0 ${num(width * 2.4)}` : `${num(width * 3)} ${num(width * 2.4)}`;
81
+ }
82
+
83
+ /**
84
+ * The rotation of a shape: about its centre, or `origin` (`[x, y]`, px or
85
+ * `'%'`). Polar kinds turn their points about the centre instead.
86
+ *
87
+ * @param {any} s @param {{ box: { x: number, y: number, width: number, height: number } }} g
88
+ * @param {import('../layers.js').RenderContext} ctx @param {boolean} polar
89
+ * @returns {string | undefined}
90
+ */
91
+ function rotation(s, g, ctx, polar) {
92
+ if (!s.rotate || polar) return undefined;
93
+ const [ox, oy] = s.origin != null
94
+ ? (Array.isArray(s.origin) ? [pos(s.origin[0], 'x', ctx.canvas), pos(s.origin[1], 'y', ctx.canvas)] : [pos(s.origin.x, 'x', ctx.canvas), pos(s.origin.y, 'y', ctx.canvas)])
95
+ : [g.box.x + g.box.width / 2, g.box.y + g.box.height / 2];
96
+ return `rotate(${num(s.rotate, 3)} ${num(ox)} ${num(oy)})`;
97
+ }
98
+
99
+ /**
100
+ * The box a shape can paint in: its bounds (turned, when turned about its
101
+ * centre) plus its stroke.
102
+ *
103
+ * @param {{ x: number, y: number, width: number, height: number }} box
104
+ * @param {any} s @param {number} rotate @param {number | undefined} sw
105
+ * @returns {{ x: number, y: number, width: number, height: number }}
106
+ */
107
+ function reach(box, s, rotate, sw) {
108
+ const a = (rotate * Math.PI) / 180;
109
+ const w = s.origin == null ? Math.abs(box.width * Math.cos(a)) + Math.abs(box.height * Math.sin(a)) : Math.hypot(box.width, box.height) * 2;
110
+ const h = s.origin == null ? Math.abs(box.width * Math.sin(a)) + Math.abs(box.height * Math.cos(a)) : w;
111
+ const stroke = (sw ?? 0) + Math.max(0, ...(s.strokes ?? []).map((/** @type {any} */ k) => (typeof k.width === 'number' ? k.width : 0)));
112
+ const cx = box.x + box.width / 2;
113
+ const cy = box.y + box.height / 2;
114
+ return { x: cx - w / 2 - stroke, y: cy - h / 2 - stroke, width: w + 2 * stroke, height: h + 2 * stroke };
115
+ }
@@ -0,0 +1,5 @@
1
+ // Soft (blurred) shapes, drawn padded.
2
+ import type { RenderContext, Markup } from '../render/context.js';
3
+
4
+ /** Blur markup by `sigma`, padded 3σ round `box` (raster with sharp, else an SVG blur). */
5
+ export function soften(m: Markup, box: { x: number; y: number; width: number; height: number }, sigma: number, ctx: RenderContext, id: string): Promise<Markup>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Soft shapes: a blur drawn padded, so a blurred orb never fades where it
3
+ * meets the canvas edge (librsvg clips a filter's input to the canvas) or
4
+ * shows the square edge of a filter region.
5
+ *
6
+ * @module layers/soft
7
+ */
8
+
9
+ import { attrs, num } from '../svg.js';
10
+ import { toBase64 } from '../platform/bytes.js';
11
+
12
+ /**
13
+ * Blur markup by `sigma` (px in the layer's units). In `renderImage`, the markup
14
+ * is drawn on its own padded canvas (3σ round `box`) at the resolution the
15
+ * blur needs (about four pixels per σ, never more than the output's), blurred
16
+ * there, and embedded as an image; elsewhere it's an SVG blur over a filter
17
+ * region padded the same way.
18
+ *
19
+ * @param {{ defs: string, body: string }} m
20
+ * @param {{ x: number, y: number, width: number, height: number }} box - What the markup paints.
21
+ * @param {number} sigma
22
+ * @param {import('../layers.js').RenderContext} ctx
23
+ * @param {string} id
24
+ * @returns {Promise<{ defs: string, body: string }>}
25
+ */
26
+ export async function soften(m, box, sigma, ctx, id) {
27
+ const pad = 3 * sigma;
28
+ const r = { x: box.x - pad, y: box.y - pad, width: box.width + 2 * pad, height: box.height + 2 * pad };
29
+ // Pixels only for renderImage (an SVG keeps its blur as a filter).
30
+ const sharp = /** @type {any} */ (ctx).raster ? /** @type {any} */ (ctx.sharp) : undefined;
31
+ if (!sharp || sigma <= 0) {
32
+ if (sigma <= 0) return m;
33
+ const filter = `<filter${attrs({ id, filterUnits: 'userSpaceOnUse', ...r, 'color-interpolation-filters': 'sRGB' })}><feGaussianBlur stdDeviation="${num(sigma, 3)}"/></filter>`;
34
+ return { defs: m.defs + filter, body: `<g filter="url(#${id})">${m.body}</g>` };
35
+ }
36
+ const res = /** @type {any} */ (ctx).res ?? 1;
37
+ const ppu = Math.min(res, Math.max(4 / sigma, 64 / Math.max(r.width, r.height)));
38
+ const w = Math.max(1, Math.ceil(r.width * ppu));
39
+ const h = Math.max(1, Math.ceil(r.height * ppu));
40
+ const doc = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${num(r.x, 4)} ${num(r.y, 4)} ${num(w / ppu, 4)} ${num(h / ppu, 4)}" preserveAspectRatio="none">${m.defs ? `<defs>${m.defs}</defs>` : ''}${m.body}</svg>`;
41
+ const png = await sharp(new TextEncoder().encode(doc)).blur(Math.max(0.3, sigma * ppu)).png().toBuffer();
42
+ return { defs: '', body: `<image${attrs({ x: r.x, y: r.y, width: num(w / ppu, 4), height: num(h / ppu, 4), preserveAspectRatio: 'none', href: `data:image/png;base64,${toBase64(png)}` })}/>` };
43
+ }
@@ -0,0 +1,23 @@
1
+ // The `svg` layer.
2
+ import type { Color, Length, BaseLayer, Anchor } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+
5
+ /** `svg`: raw markup, the escape hatch. */
6
+ export interface SvgLayer extends BaseLayer {
7
+ type: 'svg';
8
+ /** Tokens: `{{@accent}}`, `{{@colors.1}}`, `{{@width}}`, `{{id}}`, `{{seed}}`; in JSON recipes `{{title}}` or any option. */
9
+ markup: string;
10
+ defs?: string;
11
+ viewBox?: [number, number, number, number];
12
+ x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
13
+ fit?: 'contain' | 'cover' | 'stretch';
14
+ align?: string;
15
+ }
16
+
17
+
18
+ /** Strip scripts, foreignObject, event handlers and links out of SVG markup. */
19
+ export function sanitize(s: string): string;
20
+ /** Shapes from a slot's markup (paths and basic shapes with geometry and paint attributes); anything else is escaped. */
21
+ export function shapesOnly(markup: string): string;
22
+ /** Draw raw markup with its tokens filled. */
23
+ export function svg(layer: SvgLayer, ctx: RenderContext): Markup;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The `svg` layer: raw markup, sanitised, with render-time tokens.
3
+ *
4
+ * @module layers/svg
5
+ */
6
+
7
+ import { attrs, esc, num } from '../svg.js';
8
+ import { unit } from '../geometry.js';
9
+ import { pos, wrap } from './util.js';
10
+ import { expandDropShadows } from '../effects/drop-shadow.js';
11
+ import { filterReach } from '../effects/region.js';
12
+
13
+
14
+ /**
15
+ * Remove what an SVG fragment must never carry: scripts, foreign objects,
16
+ * event handlers and links out (`href`s other than `#…` and `data:`).
17
+ *
18
+ * @param {string} s
19
+ * @returns {string}
20
+ */
21
+ export function sanitize(s) {
22
+ return String(s)
23
+ .replace(/<script\b[\s\S]*?<\/script\s*>/gi, '')
24
+ .replace(/<script\b[^>]*\/>/gi, '')
25
+ .replace(/<foreignObject\b[\s\S]*?<\/foreignObject\s*>/gi, '')
26
+ .replace(/\son[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
27
+ .replace(/\s(xlink:)?href\s*=\s*("|')(?!#|data:)[^"']*\2/gi, '');
28
+ }
29
+
30
+ /** Elements a markup slot may carry (shapes and groups). */
31
+ const SHAPES = new Set(['g', 'path', 'circle', 'ellipse', 'rect', 'polygon', 'polyline', 'line']);
32
+
33
+ /** Attributes a markup slot may carry. */
34
+ const SHAPE_ATTRS = new Set(['d', 'cx', 'cy', 'r', 'rx', 'ry', 'x', 'y', 'width', 'height', 'points', 'x1', 'y1', 'x2', 'y2', 'transform', 'fill', 'stroke', 'stroke-width', 'opacity', 'fill-opacity', 'stroke-opacity', 'fill-rule', 'clip-rule', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset']);
35
+
36
+ /**
37
+ * Shapes from a slot's markup (a mark's paths): `g`, `path`, `circle`,
38
+ * `ellipse`, `rect`, `polygon`, `polyline` and `line` with their geometry
39
+ * and paint attributes are kept; any other element is escaped (inert
40
+ * text), and other attributes dropped. Paint can't reference anything
41
+ * outside (`url(…)` is dropped).
42
+ *
43
+ * @param {string} markup
44
+ * @returns {string}
45
+ */
46
+ export function shapesOnly(markup) {
47
+ return String(markup).replace(/<\/?([a-zA-Z][\w:-]*)([^>]*)>/g, (tag, name, rest) => {
48
+ if (!SHAPES.has(name)) return tag.replace(/[&<>"']/g, (ch) => /** @type {Record<string, string>} */ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch]);
49
+ if (tag.startsWith('</')) return `</${name}>`;
50
+ const self = /\/\s*$/.test(rest);
51
+ let kept = '';
52
+ for (const m of rest.matchAll(/([\w:-]+)\s*=\s*("([^"]*)"|'([^']*)')/g)) {
53
+ const value = m[3] ?? m[4];
54
+ if (SHAPE_ATTRS.has(m[1]) && !/url\s*\(|javascript:/i.test(value)) kept += ` ${m[1]}="${esc(value)}"`;
55
+ }
56
+ return `<${name}${kept}${self ? '/' : ''}>`;
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Replace render-time tokens in markup: `{{@accent}}` and the other palette
62
+ * and canvas tokens, `{{id}}` (a prefix unique to this layer, for ids in
63
+ * `defs`), and `{{seed}}` (a seeded number).
64
+ *
65
+ * @param {string} s @param {import('../layers.js').RenderContext} ctx
66
+ * @returns {string}
67
+ */
68
+ function tokens(s, ctx) {
69
+ const p = /** @type {any} */ (ctx.palette);
70
+ return s.replace(/\{\{\s*([@\w.]+)\s*\}\}/g, (m, k) => {
71
+ if (k === 'id') return ctx.id;
72
+ if (k === 'seed') return String(ctx.rng.int(1, 9999));
73
+ if (k.startsWith('@')) {
74
+ const [head, idx] = k.slice(1).split('.');
75
+ if (head === 'width' || head === 'height') return num(ctx[head]);
76
+ if (head === 'u') return num(unit(ctx.canvas));
77
+ if (head === 'colors') return esc(p.colors[Number(idx ?? 0) % p.colors.length]);
78
+ if (Object.hasOwn(p, head)) return esc(String(p[head]));
79
+ }
80
+ return m;
81
+ });
82
+ }
83
+
84
+ /**
85
+ * `svg` — raw SVG markup, the escape hatch for anything too bespoke for a
86
+ * layer. `markup` is drawn in the layer's canvas, or from a `viewBox` into
87
+ * a box (`x`, `y`, `width`, `height`, `fit`); `defs` go in the document's
88
+ * defs. Palette and canvas tokens are written `{{@accent}}`, `{{@colors.1}}`,
89
+ * `{{@width}}`; `{{id}}` prefixes ids; in a JSON recipe `{{title}}` and any
90
+ * other option is filled in (escaped) too. Scripts, event handlers and
91
+ * external links are stripped.
92
+ *
93
+ * @param {any} layer
94
+ * @param {import('../layers.js').RenderContext} ctx
95
+ * @returns {import('../layers.js').Markup}
96
+ */
97
+ export function svg(layer, ctx) {
98
+ const markup = expandDropShadows(sanitize(tokens(String(layer.markup ?? ''), ctx)));
99
+ const defs = layer.defs ? expandDropShadows(sanitize(tokens(String(layer.defs), ctx))) : '';
100
+ /** @type {any} */ (ctx).noteReach?.(filterReach(defs + markup) * (/** @type {any} */ (ctx).res ?? 1));
101
+ let body = markup;
102
+ if (layer.viewBox) {
103
+ const vb = Array.isArray(layer.viewBox) ? layer.viewBox : String(layer.viewBox).split(/[\s,]+/).map(Number);
104
+ const fit = layer.fit ?? 'contain';
105
+ const par = fit === 'stretch' ? 'none' : `${layer.align ?? 'xMidYMid'} ${fit === 'cover' ? 'slice' : 'meet'}`;
106
+ body = `<svg${attrs({ x: pos(layer.x ?? 0, 'x', ctx.canvas), y: pos(layer.y ?? 0, 'y', ctx.canvas), width: layer.width != null ? pos(layer.width, 'x', ctx.canvas) : ctx.width, height: layer.height != null ? pos(layer.height, 'y', ctx.canvas) : ctx.height, viewBox: vb.map((/** @type {number} */ v) => num(v, 3)).join(' '), preserveAspectRatio: par, overflow: 'visible' })}>${markup}</svg>`;
107
+ }
108
+ return { defs, body: wrap(layer, body) };
109
+ }
@@ -0,0 +1,25 @@
1
+ // The `table` layer.
2
+ import type { Color, Length, BaseLayer, Anchor } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+
5
+ /** A table cell: text, or text with its own style. */
6
+ export type TableCell = string | number | { content: string | number; weight?: number; size?: Length; color?: Color; align?: 'left' | 'center' | 'right'; font?: string; opacity?: number; width?: Length };
7
+
8
+ export interface TableLayer extends BaseLayer {
9
+ type: 'table';
10
+ rows: (TableCell[] | string | Record<string, TableCell>)[];
11
+ columns?: { x?: Length | `${number}%`; align?: 'left' | 'center' | 'right'; width?: Length; key?: string; weight?: number; size?: Length; color?: Color; font?: string; letterSpacing?: number; transform?: 'uppercase'; opacity?: number; lengthAdjust?: 'spacingAndGlyphs' | 'spacing' }[];
12
+ x?: Length | `${number}%`;
13
+ /** First baseline. */
14
+ y?: Length | `${number}%`;
15
+ size?: Length; lineHeight?: Length; rowHeight?: Length; rowGap?: Length;
16
+ font?: string; weight?: number; style?: 'normal' | 'italic'; axes?: Record<string, number>;
17
+ color?: Color; letterSpacing?: number;
18
+ header?: Record<string, unknown>;
19
+ rules?: boolean | { at?: 'between' | 'each' | 'top'; color?: Color; width?: Length; opacity?: number; x1?: Length; x2?: Length; above?: number };
20
+ columnWidth?: Length;
21
+ }
22
+
23
+
24
+ /** Draw text on a grid. */
25
+ export function table(layer: TableLayer, ctx: RenderContext): Promise<Markup>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The `table` layer: meta grids, lineups, tracklists.
3
+ *
4
+ * @module layers/table
5
+ */
6
+
7
+ import { attrs, num } from '../svg.js';
8
+ import { len } from '../geometry.js';
9
+ import { toHex } from '../color.js';
10
+ import { faceFor, setLine } from '../type/line.js';
11
+ import { pos, wrap, contentOf } from './util.js';
12
+
13
+
14
+ /**
15
+ * `table` — text on a grid: meta blocks, lineups, tracklists, programmes.
16
+ * `rows` is an array of rows (each an array of cells, or an object read
17
+ * through each column's `key`); a cell is text (`\n` for more lines) or
18
+ * `{ content, weight, size, color, align, font, opacity }`. `columns` give
19
+ * each column's `x` (px or `'%'`), `align`, `width` (justify), `key` and
20
+ * text style. Rows start at `y` (the first baseline) and step by
21
+ * `rowHeight`, or by their line count × `lineHeight` plus `rowGap`.
22
+ * `rules` draws hairlines between or above rows.
23
+ *
24
+ * @param {any} layer
25
+ * @param {import('../layers.js').RenderContext} ctx
26
+ * @returns {Promise<import('../layers.js').Markup>}
27
+ */
28
+ export async function table(layer, ctx) {
29
+ const rows = /** @type {any[]} */ (layer.rows ?? []);
30
+ const cols = /** @type {any[]} */ (layer.columns ?? []);
31
+ const size = len(layer.size, ctx.canvas, '2.2vmin');
32
+ const lineH = len(layer.lineHeight, ctx.canvas, size * 1.22);
33
+ const x0 = pos(layer.x ?? 0, 'x', ctx.canvas);
34
+ let y = pos(layer.y ?? 0, 'y', ctx.canvas);
35
+ const color = layer.color ? toHex(layer.color) : ctx.palette.text;
36
+ /** @type {Map<string, string>} */
37
+ const byColor = new Map();
38
+ const add = (/** @type {string} */ c, /** @type {string} */ d) => byColor.set(c, (byColor.get(c) ?? '') + d);
39
+ let rules = '';
40
+ const r = layer.rules ? (layer.rules === true ? {} : layer.rules) : null;
41
+ const rx1 = r ? pos(r.x1 ?? layer.x ?? 0, 'x', ctx.canvas) : 0;
42
+ const rx2 = r ? pos(r.x2 ?? '100%', 'x', ctx.canvas) : 0;
43
+ const ruleAt = (/** @type {number} */ ry) => { rules += `M${num(rx1)} ${num(ry)}H${num(rx2)}`; };
44
+
45
+ for (let ri = 0; ri < rows.length; ri++) {
46
+ const row = rows[ri];
47
+ const cells = Array.isArray(row) ? row : typeof row === 'string' || typeof row === 'number' ? [row] : cols.map((c) => row?.[c.key]);
48
+ if (r && (r.at === 'top' || r.at === 'each') && ri === 0) ruleAt(y - size * (r.above ?? 1.1));
49
+ let lines = 1;
50
+ for (let ci = 0; ci < cells.length; ci++) {
51
+ const cell = cells[ci];
52
+ if (cell == null || cell === '') continue;
53
+ const col = { ...(layer.header && ri === 0 ? layer.header : {}), ...(cols[ci] ?? {}) };
54
+ const o = typeof cell === 'object' && !Array.isArray(cell) ? { ...col, ...cell } : { ...col, content: cell };
55
+ const text = String(Array.isArray(o.content) ? o.content.join('\n') : contentOf(o.content) ?? o.content);
56
+ const face = await faceFor(ctx, { font: o.font ?? layer.font, weight: o.weight ?? layer.weight ?? 400, style: o.style ?? layer.style, axes: o.axes ?? layer.axes });
57
+ const s = len(o.size, ctx.canvas, size);
58
+ const ls = (o.letterSpacing ?? layer.letterSpacing ?? 0) * s;
59
+ const cx = o.x != null ? pos(o.x, 'x', ctx.canvas) : x0 + ci * len(layer.columnWidth, ctx.canvas, '20vmin');
60
+ const parts = (o.transform === 'uppercase' ? text.toUpperCase() : text).split('\n');
61
+ parts.forEach((t, li) => {
62
+ const line = setLine({ face, text: t, size: s, tracking: ls, x: cx, y: y + li * lineH, align: o.align ?? 'left', width: o.width != null ? len(o.width, ctx.canvas) : undefined, adjust: o.lengthAdjust });
63
+ const fill = o.color ? toHex(o.color) : color;
64
+ add(o.opacity != null ? `${fill}|${o.opacity}` : fill, line.d);
65
+ });
66
+ lines = Math.max(lines, parts.length);
67
+ }
68
+ const step = layer.rowHeight != null ? len(layer.rowHeight, ctx.canvas) : lines * lineH + len(layer.rowGap, ctx.canvas, size * 0.6);
69
+ if (r && (r.at === 'between' || r.at == null || r.at === 'each') && ri < rows.length - 1) ruleAt(y + step - size * (r.above ?? 1.1));
70
+ y += step;
71
+ }
72
+ let body = '';
73
+ if (rules && r) body += `<path${attrs({ d: rules, fill: 'none', stroke: r.color ? toHex(r.color) : color, 'stroke-width': len(r.width, ctx.canvas, '0.12vmin'), opacity: r.opacity })}/>`;
74
+ for (const [key, d] of byColor) {
75
+ const [fill, op] = key.split('|');
76
+ body += `<path${attrs({ d, fill, opacity: op != null ? Number(op) : undefined })}/>`;
77
+ }
78
+ /** @type {any} */ (ctx).report?.({ kind: 'text', id: layer.id, content: rows.map((row) => (Array.isArray(row) ? row : Object.values(row ?? {})).map((c) => (typeof c === 'object' && c ? c.content : c)).join(' ')).join('\n'), size, capHeight: size * 0.72, box: null });
79
+ return { defs: '', body: wrap(layer, body) };
80
+ }
@@ -0,0 +1,21 @@
1
+ // The `tear` layer.
2
+ import type { Color, Length, BaseLayer, Anchor } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+ import type { Paint } from '../effects/paint.js';
5
+
6
+ export interface TearLayer extends BaseLayer {
7
+ type: 'tear';
8
+ from?: [Length | `${number}%`, Length | `${number}%`];
9
+ to?: [Length | `${number}%`, Length | `${number}%`];
10
+ side?: 'above' | 'below' | 'left' | 'right';
11
+ color?: Paint;
12
+ sheetOpacity?: number;
13
+ step?: Length; jitter?: Length;
14
+ rip?: false | { color?: Color; width?: Length; opacity?: number };
15
+ fibres?: false | { color?: Color; width?: Length; opacity?: number; length?: [Length, Length]; spread?: Length };
16
+ ripColor?: Color;
17
+ }
18
+
19
+
20
+ /** Draw torn paper: the sheet, its rip and fibres. */
21
+ export function tear(layer: TearLayer, ctx: RenderContext): Markup;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The `tear` layer: torn paper with fibres at the rip.
3
+ *
4
+ * @module layers/tear
5
+ */
6
+
7
+ import { attrs, num } from '../svg.js';
8
+ import { len } from '../geometry.js';
9
+ import { toHex } from '../color.js';
10
+ import { paint } from '../effects/paint.js';
11
+ import { pos, wrap } from './util.js';
12
+
13
+
14
+ /**
15
+ * `tear` — paper torn along a jagged line from `from` to `to` (edge to
16
+ * edge): the sheet on one `side` (`'above'`, `'below'`, `'left'`, `'right'`)
17
+ * in `color`, the white core of the rip along the edge (`rip`), and fibres
18
+ * hanging off it (`fibres`). `step` and `jitter` set the jaggedness. Use
19
+ * the same `seed` on two tear layers to draw the sheet and its edge
20
+ * separately (say, the sheet under a knockout and the edge over it).
21
+ *
22
+ * @param {any} layer
23
+ * @param {import('../layers.js').RenderContext} ctx
24
+ * @returns {import('../layers.js').Markup}
25
+ */
26
+ export function tear(layer, ctx) {
27
+ const { width: w, height: h, rng } = ctx;
28
+ const P = (/** @type {any} */ q) => /** @type {[number, number]} */ ([pos(q?.[0] ?? q?.x ?? 0, 'x', ctx.canvas), pos(q?.[1] ?? q?.y ?? 0, 'y', ctx.canvas)]);
29
+ const from = P(layer.from ?? [0, '62%']);
30
+ const to = P(layer.to ?? ['100%', '36%']);
31
+ const step = len(layer.step, ctx.canvas, '2vmin');
32
+ const jitter = len(layer.jitter, ctx.canvas, '4.4vmin');
33
+ const L = Math.hypot(to[0] - from[0], to[1] - from[1]);
34
+ const n = Math.max(2, Math.round(L / step));
35
+ const nx = -(to[1] - from[1]) / L;
36
+ const ny = (to[0] - from[0]) / L;
37
+ const pts = [];
38
+ for (let i = 0; i <= n; i++) {
39
+ const t = i / n;
40
+ const j = (rng.next() - 0.5) * jitter;
41
+ pts.push([from[0] + (to[0] - from[0]) * t + nx * j, from[1] + (to[1] - from[1]) * t + ny * j]);
42
+ }
43
+ const line = pts.map((q, i) => `${i ? 'L' : 'M'}${num(q[0], 1)} ${num(q[1], 1)}`).join('');
44
+ let body = '';
45
+ const side = layer.side ?? 'above';
46
+ if (layer.color !== 'none' && layer.color !== false) {
47
+ const pad = Math.max(w, h);
48
+ const ext = { above: [0, -pad], below: [0, pad], left: [-pad, 0], right: [pad, 0] }[/** @type {'above'} */ (side)] ?? [0, -pad];
49
+ const first = pts[0];
50
+ const last = pts[pts.length - 1];
51
+ // Extend the ends past the canvas so a line that stops at the edge still closes off the corner.
52
+ const sx = Math.sign(last[0] - first[0]) || 1;
53
+ const sy = Math.sign(last[1] - first[1]) || 1;
54
+ const horizontal = side === 'above' || side === 'below';
55
+ const d = horizontal
56
+ ? `M${num(first[0] - sx * pad, 1)} ${num(first[1], 1)}${line.replace(/^M/, 'L')}L${num(last[0] + sx * pad, 1)} ${num(last[1], 1)}L${num(last[0] + sx * pad, 1)} ${num(last[1] + ext[1], 1)}L${num(first[0] - sx * pad, 1)} ${num(first[1] + ext[1], 1)}Z`
57
+ : `M${num(first[0], 1)} ${num(first[1] - sy * pad, 1)}${line.replace(/^M/, 'L')}L${num(last[0], 1)} ${num(last[1] + sy * pad, 1)}L${num(last[0] + ext[0], 1)} ${num(last[1] + sy * pad, 1)}L${num(first[0] + ext[0], 1)} ${num(first[1] - sy * pad, 1)}Z`;
58
+ const fp = paint(layer.color, ctx.palette.accent, { id: `${ctx.id}t`, box: { x: 0, y: 0, width: w, height: h }, canvas: ctx.canvas });
59
+ body += `<path${attrs({ d, fill: fp.value, opacity: layer.sheetOpacity })}/>`;
60
+ }
61
+ if (layer.fibres !== false) {
62
+ const f = layer.fibres && typeof layer.fibres === 'object' ? layer.fibres : {};
63
+ const [lo, hi] = (f.length ?? ['0.8vmin', '2vmin']).map((/** @type {any} */ v) => len(v, ctx.canvas));
64
+ const spread = len(f.spread, ctx.canvas, '0.6vmin');
65
+ const dir = side === 'above' || side === 'left' ? 1 : -1;
66
+ const d = pts.map((q) => `M${num(q[0], 1)} ${num(q[1], 1)}l${num((rng.next() * 2 - 1) * spread, 1)} ${num(dir * (lo + rng.next() * (hi - lo)), 1)}`).join('');
67
+ body += `<path${attrs({ d, stroke: toHex(f.color ?? layer.ripColor ?? '#fff6fb'), 'stroke-width': len(f.width, ctx.canvas, '0.44vmin'), 'stroke-linecap': 'round', fill: 'none', opacity: f.opacity ?? 0.85 })}/>`;
68
+ }
69
+ if (layer.rip !== false) {
70
+ const r = layer.rip && typeof layer.rip === 'object' ? layer.rip : {};
71
+ body += `<path${attrs({ d: line, stroke: toHex(r.color ?? layer.ripColor ?? '#fff6fb'), 'stroke-width': len(r.width, ctx.canvas, '0.7vmin'), fill: 'none', opacity: r.opacity ?? 0.9 })}/>`;
72
+ }
73
+ return { defs: '', body: wrap(layer, body) };
74
+ }
@@ -0,0 +1,17 @@
1
+ // The `texture` layer.
2
+ import type { Color, Length, BaseLayer, Anchor } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+
5
+ export interface TextureLayer extends BaseLayer {
6
+ type: 'texture';
7
+ kind?: 'paper' | 'creases' | 'concrete' | 'speck' | 'wood' | 'folds' | 'dust';
8
+ color?: Color; amount?: number; frequency?: number | [number, number]; octaves?: number;
9
+ depth?: number; azimuth?: number; elevation?: number;
10
+ x?: number[]; y?: number[]; width?: Length; strength?: number;
11
+ count?: number; size?: [Length, Length];
12
+ region?: { x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%` };
13
+ }
14
+
15
+
16
+ /** Draw a surface texture. */
17
+ export function texture(layer: TextureLayer, ctx: RenderContext): Markup;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The `texture` layer: paper, creases, concrete, speck, wood, folds, dust.
3
+ *
4
+ * @module layers/texture
5
+ */
6
+
7
+ import { attrs, num } from '../svg.js';
8
+ import { len } from '../geometry.js';
9
+ import { toHex } from '../color.js';
10
+ import { pos, wrap } from './util.js';
11
+
12
+
13
+ /**
14
+ * `texture` — a surface laid over (or under) the artwork: `paper` (fibres),
15
+ * `creases` (pasted paper lit from the side; soft-light), `concrete`,
16
+ * `speck` (white specks), `wood`, `folds` (straight fold lines with a lit
17
+ * and a shaded side), `dust` (seeded specks of dirt). `color`, `opacity`,
18
+ * `blend`, `frequency`, `octaves` and `region` adjust it.
19
+ *
20
+ * @param {any} layer
21
+ * @param {import('../layers.js').RenderContext} ctx
22
+ * @returns {import('../layers.js').Markup}
23
+ */
24
+ export function texture(layer, ctx) {
25
+ const { width: w, height: h, rng } = ctx;
26
+ const kind = layer.kind ?? 'paper';
27
+ const id = `${ctx.id}t`;
28
+ const seed = layer.seed ?? rng.int(1, 9999);
29
+ const rgb = (/** @type {string} */ c) => [1, 3, 5].map((i) => num(parseInt(toHex(c).slice(i, i + 2), 16) / 255, 3));
30
+ const f = (/** @type {any} */ v, /** @type {string} */ d) => (v == null ? d : [].concat(v).map((x) => num(x, 4)).join(' '));
31
+ const rect = (/** @type {string} */ extra = '') => `<rect${attrs({ width: w, height: h, filter: `url(#${id})` })}${extra}/>`;
32
+ const region = `${attrs({ id, x: 0, y: 0, width: w, height: h, filterUnits: 'userSpaceOnUse', 'color-interpolation-filters': 'sRGB' })}`;
33
+ let defs = '';
34
+ let body = '';
35
+ let blend = layer.blend;
36
+ let opacity = layer.opacity;
37
+ // Feature sizes are tuned on a 400 px wide sheet; scale with the canvas.
38
+ const k = 400 / Math.min(w, h * 0.8);
39
+ const fq = (/** @type {number[]} */ ...v) => v.map((x) => x * k);
40
+ switch (kind) {
41
+ case 'paper': {
42
+ const [r, g, b] = rgb(layer.color ?? '#73664d');
43
+ defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, fq(0.55, 0.3).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 3}" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 -0.9 ${num(0.5 * (layer.amount ?? 1), 3)}"/></filter>`;
44
+ body = rect();
45
+ break;
46
+ }
47
+ case 'creases':
48
+ defs = `<filter${region}><feTurbulence type="turbulence" baseFrequency="${f(layer.frequency, fq(0.006, 0.022).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 3}" seed="${seed}" result="t"/><feDiffuseLighting in="t" surfaceScale="${num(layer.depth ?? 3.4, 3)}" lighting-color="#fff" result="lit"><feDistantLight azimuth="${layer.azimuth ?? 225}" elevation="${layer.elevation ?? 38}"/></feDiffuseLighting><feColorMatrix in="lit" type="saturate" values="0"/></filter>`;
49
+ body = rect();
50
+ blend ??= 'soft-light';
51
+ opacity ??= 0.95;
52
+ break;
53
+ case 'concrete': {
54
+ const [r, g, b] = rgb(layer.color ?? '#857f7a');
55
+ defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, fq(0.035, 0.05).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 4}" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0.6 0.6 0.6 0 -0.4"/></filter>`;
56
+ body = rect();
57
+ break;
58
+ }
59
+ case 'speck': {
60
+ const [r, g, b] = rgb(layer.color ?? '#ffffff');
61
+ defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, num(0.7 * k, 4))}" numOctaves="1" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 -12 7.3"/></filter>`;
62
+ body = rect();
63
+ opacity ??= 0.25;
64
+ break;
65
+ }
66
+ case 'wood': {
67
+ const [r, g, b] = rgb(layer.color ?? '#6b4224');
68
+ defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, fq(0.006, 0.16).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 4}" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 1.4 0 0 0 -0.45"/></filter>`;
69
+ body = rect();
70
+ break;
71
+ }
72
+ case 'folds': {
73
+ // Straight folds: a lit side and a shaded side either side of each line.
74
+ const xs = layer.x ?? [];
75
+ const ys = layer.y ?? (layer.x ? [] : [0.5]);
76
+ const bw = len(layer.width, ctx.canvas, '1.6vmin');
77
+ const strength = layer.strength ?? 0.35;
78
+ const band = (/** @type {boolean} */ vertical, /** @type {number} */ at, /** @type {number} */ i) => {
79
+ const gid = `${id}${vertical ? 'v' : 'h'}${i}`;
80
+ const p = vertical ? at * w : at * h;
81
+ defs += `<linearGradient${attrs({ id: gid, gradientUnits: 'userSpaceOnUse', x1: vertical ? p - bw : 0, y1: vertical ? 0 : p - bw, x2: vertical ? p + bw : 0, y2: vertical ? 0 : p + bw })}><stop offset="0" stop-color="#000" stop-opacity="0"/><stop offset="0.48" stop-color="#000" stop-opacity="${num(strength, 3)}"/><stop offset="0.52" stop-color="#fff" stop-opacity="${num(strength * 1.4, 3)}"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>`;
82
+ body += vertical ? `<rect${attrs({ x: p - bw, y: 0, width: bw * 2, height: h, fill: `url(#${gid})` })}/>` : `<rect${attrs({ x: 0, y: p - bw, width: w, height: bw * 2, fill: `url(#${gid})` })}/>`;
83
+ };
84
+ xs.forEach((/** @type {number} */ x, /** @type {number} */ i) => band(true, x, i));
85
+ ys.forEach((/** @type {number} */ y, /** @type {number} */ i) => band(false, y, i));
86
+ blend ??= 'soft-light';
87
+ break;
88
+ }
89
+ case 'dust': {
90
+ const n = layer.count ?? 70;
91
+ const c = toHex(layer.color ?? ctx.palette.text);
92
+ const [lo, hi] = (layer.size ?? ['0.06vmin', '0.26vmin']).map((/** @type {any} */ v) => len(v, ctx.canvas));
93
+ for (let i = 0; i < n; i++) body += `<circle${attrs({ cx: rng.next() * w, cy: rng.next() * h, r: lo + rng.next() * (hi - lo) })}/>`;
94
+ body = `<g fill="${c}">${body}</g>`;
95
+ break;
96
+ }
97
+ default:
98
+ throw new TypeError(`Unknown texture "${kind}" (paper, creases, concrete, speck, wood, folds, dust)`);
99
+ }
100
+ if (layer.region) {
101
+ const rg = layer.region;
102
+ defs += `<clipPath id="${id}c"><rect${attrs({ x: pos(rg.x ?? 0, 'x', ctx.canvas), y: pos(rg.y ?? 0, 'y', ctx.canvas), width: pos(rg.width ?? '100%', 'x', ctx.canvas), height: pos(rg.height ?? '100%', 'y', ctx.canvas) })}/></clipPath>`;
103
+ body = `<g clip-path="url(#${id}c)">${body}</g>`;
104
+ }
105
+ return { defs, body: wrap({ blend, opacity }, body) };
106
+ }
@@ -0,0 +1,8 @@
1
+ // Helpers the poster layers share.
2
+
3
+ /** A coordinate: px, `'50%'` of the canvas along `axis`, or a length. */
4
+ export function pos(v: unknown, axis: 'x' | 'y', canvas: { width: number; height: number }): number;
5
+ /** Markup wrapped in the layer's opacity and blend (when it has them). */
6
+ export function wrap(layer: { opacity?: number; blend?: string }, body: string): string;
7
+ /** The text of a string-or-object option. */
8
+ export function contentOf(v: unknown): string | undefined;