@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
@@ -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;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Small helpers the poster layers share: coordinates in px or `'%'`, the
3
+ * opacity/blend wrapper, and the text of a string-or-object option.
4
+ *
5
+ * @module layers/util
6
+ */
7
+
8
+ import { attrs, esc } from '../svg.js';
9
+ import { len } from '../geometry.js';
10
+
11
+ /**
12
+ * A coordinate: px as a number, `'50%'` of the canvas along `axis`, or a
13
+ * length (`'4vmin'`).
14
+ *
15
+ * @param {unknown} v
16
+ * @param {'x' | 'y'} axis
17
+ * @param {{ width: number, height: number }} canvas
18
+ * @returns {number} px
19
+ */
20
+ export function pos(v, axis, canvas) {
21
+ if (typeof v === 'string' && v.trim().endsWith('%')) return (parseFloat(v) / 100) * (axis === 'x' ? canvas.width : canvas.height);
22
+ return len(/** @type {any} */ (v), canvas, 0);
23
+ }
24
+
25
+ /**
26
+ * Wrap markup in a group with the layer's `opacity` and `blend` (only when
27
+ * it has them).
28
+ *
29
+ * @param {{ opacity?: number, blend?: string }} layer
30
+ * @param {string} body
31
+ * @returns {string}
32
+ */
33
+ export function wrap(layer, body) {
34
+ const a = attrs({ opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined, style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${esc(layer.blend)}` : undefined });
35
+ return a ? `<g${a}>${body}</g>` : body;
36
+ }
37
+
38
+ /**
39
+ * The text of a string-or-object option (`'Hello'` or `{ content: 'Hello' }`).
40
+ *
41
+ * @param {unknown} v
42
+ * @returns {string | undefined}
43
+ */
44
+ export function contentOf(v) {
45
+ if (typeof v === 'string' || typeof v === 'number') return String(v);
46
+ if (v && typeof v === 'object' && 'content' in v) return /** @type {any} */ (v).content;
47
+ return undefined;
48
+ }