@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,19 @@
1
+ // Keyframes against time.
2
+
3
+ /** A keyframed value. */
4
+ export interface Keyframes<T = unknown> {
5
+ /** `[t, value]` pairs (seconds), or `{ t, value, ease }`. */
6
+ keyframes: ([number, T] | [number, T, Ease] | { t: number; value: T; ease?: Ease })[];
7
+ ease?: Ease;
8
+ /** Repeat over the cover's `loop` (or the last key); `'pingpong'` goes back and forth. */
9
+ loop?: boolean | 'pingpong';
10
+ }
11
+ /** Easing curves. */
12
+ export type Ease = 'linear' | 'in' | 'out' | 'inOut' | 'step';
13
+
14
+ /** Whether a value has keyframes anywhere in it. */
15
+ export function isAnimated(v: unknown): boolean;
16
+ /** A keyframed value at time `t` (seconds). */
17
+ export function valueAt<T>(k: Keyframes<T>, t: number, duration?: number): T;
18
+ /** A value with every keyframed part resolved at `t`. */
19
+ export function animate<T>(v: T, t: number, duration?: number): T;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Animation, the data side: any layer option can be keyframed against `t`
3
+ * (seconds), and `renderSvg(cover, { t })` draws the frame at that time.
4
+ * Static covers have no keyframes and ignore `t`, so nothing changes for
5
+ * them. Encoding frames to video is a later phase (see the README).
6
+ *
7
+ * ```json
8
+ * { "type": "text", "content": "{name}",
9
+ * "opacity": { "keyframes": [[0, 0], [0.6, 1]], "ease": "out" },
10
+ * "transform": { "rotate": { "keyframes": [[0, -4], [2, 4]], "loop": "pingpong" } } }
11
+ * ```
12
+ *
13
+ * Numbers interpolate; colours mix in OKLab; lengths with the same unit
14
+ * (`"4vmin"` → `"9vmin"`) interpolate; anything else holds until the next
15
+ * key. `loop: true` repeats over the cover's `loop` (seconds; or the last key),
16
+ * `'pingpong'` goes back and forth.
17
+ *
18
+ * @module animate
19
+ */
20
+
21
+ import { mixOklab, toHex } from '../color.js';
22
+
23
+ /** @param {unknown} v @returns {v is { keyframes: any[] }} */
24
+ const isKeyed = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v) && Array.isArray(/** @type {any} */ (v).keyframes);
25
+
26
+ /**
27
+ * Whether a layer has keyframes anywhere in its options.
28
+ *
29
+ * @param {unknown} v
30
+ * @returns {boolean}
31
+ */
32
+ export function isAnimated(v) {
33
+ if (isKeyed(v)) return true;
34
+ if (Array.isArray(v)) return v.some(isAnimated);
35
+ if (v && typeof v === 'object' && !(v instanceof Uint8Array)) return Object.values(v).some(isAnimated);
36
+ return false;
37
+ }
38
+
39
+ /** Easing curves on 0–1. */
40
+ const EASE = /** @type {Record<string, (x: number) => number>} */ ({
41
+ linear: (x) => x,
42
+ in: (x) => x * x * x,
43
+ out: (x) => 1 - (1 - x) ** 3,
44
+ inOut: (x) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2),
45
+ step: () => 0,
46
+ });
47
+
48
+ /**
49
+ * Two values `f` of the way between them.
50
+ *
51
+ * @param {any} a @param {any} b @param {number} f
52
+ * @returns {any}
53
+ */
54
+ function mix(a, b, f) {
55
+ if (typeof a === 'number' && typeof b === 'number') return a + (b - a) * f;
56
+ if (typeof a === 'string' && typeof b === 'string') {
57
+ const la = /^(-?[\d.]+)(vmin|vmax|vw|vh|px|%)$/.exec(a);
58
+ const lb = /^(-?[\d.]+)(vmin|vmax|vw|vh|px|%)$/.exec(b);
59
+ if (la && lb && la[2] === lb[2]) return `${+la[1] + (+lb[1] - +la[1]) * f}${la[2]}`;
60
+ try {
61
+ return mixOklab(toHex(a), toHex(b), f);
62
+ } catch {
63
+ return f < 1 ? a : b;
64
+ }
65
+ }
66
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) return a.map((x, i) => mix(x, b[i], f));
67
+ if (a && b && typeof a === 'object' && typeof b === 'object') return Object.fromEntries(Object.keys(a).map((k) => [k, k in b ? mix(a[k], b[k], f) : a[k]]));
68
+ return f < 1 ? a : b;
69
+ }
70
+
71
+ /**
72
+ * A keyframed value at time `t`.
73
+ *
74
+ * @param {{ keyframes: any[], ease?: string, loop?: boolean | 'pingpong' }} k
75
+ * @param {number} t
76
+ * @param {number} [duration]
77
+ * @returns {any}
78
+ */
79
+ export function valueAt(k, t, duration) {
80
+ const keys = k.keyframes.map((x) => (Array.isArray(x) ? { t: x[0], value: x[1], ease: x[2] } : x)).sort((a, b) => a.t - b.t);
81
+ if (!keys.length) return undefined;
82
+ const end = duration ?? keys[keys.length - 1].t;
83
+ let time = t;
84
+ if (k.loop && end > 0) {
85
+ time = ((t % end) + end) % end;
86
+ if (k.loop === 'pingpong' && Math.floor(t / end) % 2 === 1) time = end - time;
87
+ }
88
+ if (time <= keys[0].t) return keys[0].value;
89
+ for (let i = 1; i < keys.length; i++) {
90
+ if (time <= keys[i].t) {
91
+ const a = keys[i - 1];
92
+ const b = keys[i];
93
+ const ease = EASE[b.ease ?? k.ease ?? 'inOut'] ?? EASE.inOut;
94
+ return mix(a.value, b.value, ease((time - a.t) / Math.max(1e-9, b.t - a.t)));
95
+ }
96
+ }
97
+ return keys[keys.length - 1].value;
98
+ }
99
+
100
+ /**
101
+ * A layer with every keyframed option resolved at `t`.
102
+ *
103
+ * @template T
104
+ * @param {T} v
105
+ * @param {number} t - Seconds.
106
+ * @param {number} [duration] - The cover's `loop` length in seconds (loops wrap at it).
107
+ * @returns {T}
108
+ */
109
+ export function animate(v, t, duration) {
110
+ if (isKeyed(v)) return valueAt(v, t, duration);
111
+ if (Array.isArray(v)) return /** @type {any} */ (v.map((x) => animate(x, t, duration)));
112
+ if (v && typeof v === 'object' && !(v instanceof Uint8Array) && isAnimated(v)) {
113
+ return /** @type {any} */ (Object.fromEntries(Object.entries(v).map(([k, x]) => [k, animate(x, t, duration)])));
114
+ }
115
+ return v;
116
+ }
@@ -0,0 +1,4 @@
1
+ // Generated by scripts/generate.mjs from recipes/*.json. Do not edit.
2
+
3
+ /** The built-in recipe documents, by name. */
4
+ export const BUILTIN_JSON = {"aurora":{"$schema":"../schema/recipe.schema.json","name":"aurora","description":"Full-bleed sweeps of the palette's colours, sometimes folded or streaked, with grain (the openai.com/news look). A title sets in the corner only when given.","layers":[{"id":"bg","type":"solid"},{"id":"mesh","type":"mesh","count":4,"blur":"11vmax","size":[0.45,0.8],"streak":1.8,"tone":{"if":"title","then":{"if":{"mode":"dark"},"then":{"l":[0.34,0.5],"c":[0.12,0.22]},"else":{"l":[0.66,0.82],"c":[0.13,0.2]}},"else":{"l":[0.55,0.85],"c":[0.12,0.22]}},"fold":{"if":"title","then":false,"else":{"pick":[false,false,true]}},"streaks":{"if":"title","then":0,"else":{"pick":[0,0,0,6]}},"hues":{"pick":[2,2,3]},"hueSpan":60},{"id":"grain","type":"grain","amount":0.09,"vary":{"pick":["edges",null]}},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"@text","content":"{label}"}}}]},"bauhaus":{"$schema":"../schema/recipe.schema.json","name":"bauhaus","description":"A Bauhaus grid of quarter circles, half circles, circles, squares and triangles in the palette's colours and the page colour. A shared `series` keeps a family's grid rhythm while each cover's seed fills the cells.","params":{"series":null},"layers":[{"id":"bg","type":"solid"},{"id":"tiles","type":"tiles","series":"{series}","colors":[{"tone":["@accent",{"l":[0.5,0.75],"c":[0.12,0.22]}]},{"tone":["@colors.1",{"l":[0.5,0.75],"c":[0.12,0.22]}]},{"tone":["@colors.2",{"l":[0.5,0.75],"c":[0.12,0.22]}]},"@text"],"ground":"@bg","region":{"if":"title","then":{"x":0,"y":0,"width":1,"height":0.72},"else":{"x":0,"y":0,"width":1,"height":1}}},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.056]},"minSize":{"mul":["@u",0.042]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":2,"label":{"if":"label","then":{"color":"@text","content":"{label}"}}}]},"bigType":{"$schema":"../schema/recipe.schema.json","name":"bigType","description":"The headline is the cover: large, tight, shrinking to fit four lines, with the label in the accent at the top.","vars":{"corner":{"pick":[[1,0],[0,0],[1,1],[0.5,0]]}},"layers":[{"id":"bg","type":"solid"},{"id":"wash","type":"radial","colors":[{"mix":["@bg","@accent",{"if":{"mode":"dark"},"then":0.16,"else":0.12}]},"@bg"],"x":"$corner.0","y":"$corner.1","radius":"110vmax"},{"id":"grain","type":"grain","amount":0.05},{"id":"label","type":"text","when":"label","content":"{label}","anchor":"top-left","size":{"mul":["@u",0.024]},"weight":600,"letterSpacing":0.09,"transform":"uppercase","color":"@accent","maxLines":1,"balance":false},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":600,"size":{"mul":["@u",0.13]},"minSize":{"mul":["@u",0.078]},"lineHeight":0.98,"letterSpacing":-0.045,"maxLines":4}]},"brand":{"$schema":"../schema/recipe.schema.json","name":"brand","description":"A brand on every format: avatars, headers and banners, posts and stories, business cards (front and back), letterhead, compliment slips, tickets, passes and stickers. kit() uses it by default. Options: name, tagline, mark (path markup on a 512 grid), photo, lines (contact or info lines), person and role (cards and passes), ticket (a ticket number), compliments.","pages":["front","back"],"params":{"tagline":""},"layers":[{"id":"bg","type":"solid","color":"@accent","when":{"not":{"any":[{"format":["letterhead","letterhead-us","compliment-slip","badge","lanyard-pass"]},{"all":[{"format":["business-card","business-card-us"]},{"page":"back"}]}]}}},{"id":"bg","type":"solid","color":"@bg","when":{"any":[{"format":["letterhead","letterhead-us","compliment-slip","badge","lanyard-pass"]},{"all":[{"format":["business-card","business-card-us"]},{"page":"back"}]}]}},{"id":"photo","type":"image","src":"{photo}","when":{"all":["photo",{"not":{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square","business-card","business-card-us","letterhead","letterhead-us","compliment-slip","badge","lanyard-pass","ticket"]}}]},"effects":[{"kind":"duotone","colors":[{"mix":["@accent","#000000",0.6]},"@accent",{"mix":["@accent","#ffffff",0.65]}]}],"opacity":0.9},{"id":"scrim","type":"linear","angle":180,"colors":["@accent","@accent"],"opacities":[0,0.75],"when":{"all":["photo",{"not":{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square","business-card","business-card-us","letterhead","letterhead-us","compliment-slip","badge","lanyard-pass","ticket"]}}]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"center","size":{"mul":[{"min":["@safeWidth","@safeHeight"]},0.86]},"color":"@onAccent","when":{"all":["mark",{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square"]}]}},{"id":"initial","type":"text","content":{"upper":{"slice":["{name}",0,1]}},"anchor":"center","align":"center","size":{"mul":[{"min":["@safeWidth","@safeHeight"]},0.62]},"weight":700,"letterSpacing":0,"color":"@onAccent","when":{"all":[{"not":"mark"},"name",{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square"]}]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"left","size":{"mul":["@safeHeight",0.6]},"color":"@onAccent","when":{"all":["mark",{"all":[{"not":{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square","business-card","business-card-us","letterhead","letterhead-us","compliment-slip","badge","lanyard-pass","ticket"]}},{"aspect":"wide"}]}]}},{"id":"name","type":"text","content":"{name}","label":{"content":"{tagline}","color":{"mix":["@onAccent","@accent",0.3]}},"anchor":"left","weight":700,"letterSpacing":-0.03,"offset":{"if":"mark","then":{"x":{"mul":["@safeHeight",0.74]}},"else":{"x":0}},"maxWidth":{"if":"mark","then":{"sub":["@safeWidth",{"mul":["@safeHeight",0.9]}]},"else":{"mul":["@safeWidth",0.9]}},"size":{"min":[{"mul":["@safeHeight",0.3]},{"mul":["@safeWidth",0.09]}]},"minSize":{"mul":["@safeHeight",0.14]},"maxLines":2,"color":"@onAccent","when":{"all":["name",{"all":[{"not":{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square","business-card","business-card-us","letterhead","letterhead-us","compliment-slip","badge","lanyard-pass","ticket"]}},{"aspect":"wide"}]}]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"center","offset":{"y":{"mul":["@safeHeight",-0.1]}},"size":{"mul":[{"min":["@safeWidth","@safeHeight"]},0.42]},"color":"@onAccent","when":{"all":["mark",{"all":[{"not":{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square","business-card","business-card-us","letterhead","letterhead-us","compliment-slip","badge","lanyard-pass","ticket"]}},{"not":{"aspect":"wide"}}]}]}},{"id":"name","type":"text","content":"{name}","label":{"content":"{tagline}","color":{"mix":["@onAccent","@accent",0.3]}},"anchor":{"if":"mark","then":"bottom","else":"center"},"align":"center","weight":700,"letterSpacing":-0.03,"size":{"mul":["@safeWidth",0.1]},"minSize":{"mul":["@safeWidth",0.06]},"maxLines":2,"color":"@onAccent","when":{"all":["name",{"all":[{"not":{"format":["avatar","x-avatar","youtube-avatar","sticker","sticker-square","business-card","business-card-us","letterhead","letterhead-us","compliment-slip","badge","lanyard-pass","ticket"]}},{"not":{"aspect":"wide"}}]}]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"center","offset":{"y":{"mul":["@trimHeight",-0.08]}},"size":{"mul":["@trimHeight",0.4]},"color":"@onAccent","when":{"all":["mark",{"all":[{"format":["business-card","business-card-us"]},{"not":{"page":"back"}}]}]}},{"id":"name","type":"text","content":"{name}","anchor":"bottom","align":"center","weight":700,"letterSpacing":-0.02,"size":{"mul":["@trimHeight",0.1]},"maxLines":1,"color":"@onAccent","when":{"all":["name",{"all":[{"format":["business-card","business-card-us"]},{"not":{"page":"back"}}]}]}},{"id":"person","type":"text","content":"{opt.person}","label":"{opt.role}","anchor":"top-left","weight":700,"letterSpacing":-0.02,"size":{"mul":["@trimHeight",0.095]},"maxLines":1,"color":"@text","when":{"all":["opt.person",{"all":[{"format":["business-card","business-card-us"]},{"page":"back"}]}]}},{"id":"lines","type":"table","rows":"{lines}","x":"@safeX","y":{"mul":["@height",0.6]},"columns":[{"x":"@safeX"}],"size":{"mul":["@trimHeight",0.058]},"color":"@text","when":{"all":["lines",{"all":[{"format":["business-card","business-card-us"]},{"page":"back"}]}]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"bottom-right","size":{"mul":["@trimHeight",0.22]},"color":"@accent","when":{"all":["mark",{"all":[{"format":["business-card","business-card-us"]},{"page":"back"}]}]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"top-left","size":{"mul":["@trimWidth",0.085]},"color":"@accent","when":{"all":["mark",{"format":["letterhead","letterhead-us","compliment-slip"]}]}},{"id":"name","type":"text","content":"{name}","label":{"content":"{tagline}","color":"@muted"},"anchor":"top-right","align":"right","weight":700,"letterSpacing":-0.02,"size":{"mul":["@trimWidth",0.03]},"maxLines":1,"color":"@text","when":{"all":["name",{"format":["letterhead","letterhead-us","compliment-slip"]}]}},{"id":"compliments","type":"text","content":"{opt.compliments}","anchor":"left","weight":400,"size":{"mul":["@trimHeight",0.09]},"color":"@text","when":{"all":["opt.compliments",{"format":"compliment-slip"}]}},{"id":"rule","type":"shape","kind":"line","x1":"@safeX","x2":{"add":["@safeX","@safeWidth"]},"y1":{"sub":[{"add":["@safeY","@safeHeight"]},{"mul":["@trimHeight",{"if":{"format":"compliment-slip"},"then":0.16,"else":0.05}]}]},"stroke":"@accent","strokeWidth":{"mul":["@mm",0.3]},"when":{"format":["letterhead","letterhead-us","compliment-slip"]}},{"id":"lines","type":"table","rows":["{lines}"],"y":{"sub":[{"add":["@safeY","@safeHeight"]},{"mul":["@trimHeight",{"if":{"format":"compliment-slip"},"then":0.08,"else":0.025}]}]},"columns":[{"x":"@safeX"},{"x":"37%"},{"x":"68%"}],"size":{"mul":["@mm",2.6]},"color":"@muted","when":{"all":["lines",{"format":["letterhead","letterhead-us","compliment-slip"]}]}},{"id":"band","type":"shape","kind":"rect","x":0,"y":0,"width":"@width","height":{"mul":["@height",0.36]},"fill":"@accent","when":{"format":["badge","lanyard-pass"]}},{"id":"mark","type":"icon","markup":"{mark}","anchor":"top","size":{"mul":["@trimHeight",0.2]},"color":"@onAccent","when":{"all":["mark",{"format":["badge","lanyard-pass"]}]}},{"id":"name","type":"text","content":"{opt.person}","label":"{opt.role}","anchor":"center","align":"center","offset":{"y":{"mul":["@height",0.1]}},"weight":700,"letterSpacing":-0.03,"size":{"mul":["@trimWidth",0.12]},"maxLines":2,"color":"@text","when":{"all":["opt.person",{"format":["badge","lanyard-pass"]}]}},{"id":"footer","type":"text","content":"{name}","anchor":"bottom","align":"center","weight":600,"size":{"mul":["@trimWidth",0.055]},"letterSpacing":0.08,"transform":"uppercase","color":"@accent","when":{"all":["name",{"format":["badge","lanyard-pass"]}]}},{"id":"name","type":"text","content":"{name}","label":{"content":"{tagline}","color":{"mix":["@onAccent","@accent",0.3]}},"anchor":"left","weight":700,"letterSpacing":-0.03,"size":{"mul":["@trimHeight",0.24]},"maxWidth":{"mul":["@trimWidth",0.6]},"maxLines":2,"color":"@onAccent","when":{"all":["name",{"format":["ticket"]}]}},{"id":"perforation","type":"perforation","x":"74%","y1":"@bleed","y2":{"sub":["@height","@bleed"]},"color":"@onAccent","size":{"mul":["@mm",0.5]},"gap":{"mul":["@mm",2.2]},"when":{"format":["ticket"]}},{"id":"barcode","type":"barcode","value":{"upper":"{opt.ticket}"},"x":"79%","y":"28%","width":"16%","height":"34%","text":true,"color":"@onAccent","when":{"all":["opt.ticket",{"format":["ticket"]}]}},{"id":"lines","type":"table","rows":["{lines.0}"],"y":{"mul":["@height",0.82]},"columns":[{"x":{"add":["@safeX",0]}}],"size":{"mul":["@trimHeight",0.065]},"color":"@onAccent","when":{"all":["lines",{"format":["ticket"]}]}}]},"contour":{"$schema":"../schema/recipe.schema.json","name":"contour","description":"Topographic isolines of seeded noise on a flat field: thin even lines, an index contour every fifth line, one line in the accent.","params":{"levels":null},"vars":{"field":{"mix":["@bg","@accent",{"if":{"mode":"dark"},"then":0.08,"else":0.05}]}},"layers":[{"id":"bg","type":"solid","color":"$field"},{"id":"contour","type":"contour","levels":{"if":"levels","then":"{levels}","else":{"pick":[14,18,22,26,30]}},"color":"@text","opacity":0.42,"index":5,"scale":{"range":[1.8,3.2]}},{"id":"grain","type":"grain","amount":0.035},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.062]},"minSize":{"mul":["@u",0.046]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"@text","content":"{label}"}},"background":{"color":"$field","radius":0,"padding":[{"mul":["@u",0.03]},{"mul":["@u",0.035]}]}}]},"cutout":{"$schema":"../schema/recipe.schema.json","name":"cutout","description":"A flat matte field (or a hand-cut panel on the page colour) with cut-paper shapes and a hand-drawn icon or node graph.","params":{"panel":false,"shapes":null,"graph":false,"stroke":null},"vars":{"field":{"tone":["@accent",{"l":[0.52,0.64],"c":[0.05,0.11]}]},"count":{"if":{"shapes":null},"then":{"if":"panel","then":0,"else":3},"else":"{shapes}"},"ink":{"if":{"stroke":"light"},"then":"#faf9f5","else":{"if":{"stroke":"dark"},"then":"#141413","else":{"if":"panel","then":"#faf9f5","else":{"if":"graph","then":"#141413","else":{"if":{"shapes":0},"then":"#faf9f5","else":"#141413"}}}}},"type":{"readable":[{"if":"panel","then":"@bg","else":"$field"},4.5,"white"]}},"layers":[{"id":"bg","type":"solid","color":{"if":"panel","then":"@bg","else":"$field"}},{"id":"panel","type":"paper","when":"panel","items":[{"kind":"rect","x":0.5,"y":0.5,"width":{"mul":["@width",0.64]},"height":{"mul":["@height",0.6]}}],"region":{"x":0,"y":0,"width":1,"height":1},"color":"$field","wobble":0.3},{"id":"shapes","type":"paper","count":"$count","region":{"x":0.24,"y":0.22,"width":0.52,"height":0.56}},{"id":"icon","type":"scribble","when":{"not":"graph"},"icon":"{icon}","color":"$ink","size":{"if":"panel","then":"34vmin","else":"44vmin"},"stroke":"1.6vmin"},{"id":"graph","type":"scribble","when":"graph","graph":true,"color":"$ink","size":"52vmin","stroke":"1.7vmin"},{"id":"grain","type":"grain","amount":0.035},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"$type","content":"{label}"}},"color":"$type"}]},"diagram":{"$schema":"../schema/recipe.schema.json","name":"diagram","description":"White line-art over the gradient field: one icon, or nodes in outlined tiles joined by arrows, with a dotted grid that breaks around them (the engineering-blog look).","params":{"nodes":null,"layout":null,"arrows":null},"layers":[{"id":"bg","type":"solid"},{"id":"mesh","type":"mesh","count":4,"blur":"11vmax","size":[0.45,0.8],"streak":1.8,"tone":{"l":[0.4,0.6],"c":[0.14,0.24]},"fold":{"pick":[false,false,{"sheen":0}]},"hueSpan":60},{"id":"diagram","type":"diagram","icon":"{icon}","nodes":"{nodes}","layout":"{layout}","arrows":"{arrows}","grid":true},{"id":"grain","type":"grain","amount":0.07},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"#ffffff","content":"{label}"}},"color":"#ffffff"}]},"dither":{"$schema":"../schema/recipe.schema.json","name":"dither","description":"A soft field (noise, a gradient, or a lit sphere / arch / circle) rendered 1-bit at a visible pixel size: Bayer 4×4 / 8×8 or blue-ish noise, in two colours or three.","params":{"matrix":"bayer4","cell":"0.9vmin","source":null,"shape":null,"three":false},"vars":{"ink":{"tone":["@accent",{"if":{"mode":"dark"},"then":{"l":[0.62,0.85],"c":[0.1,0.22]},"else":{"l":[0.3,0.55],"c":[0.1,0.24]}}]},"colors":{"if":"three","then":["@bg",{"tone":["@colors.1",{"l":[0.55,0.75],"c":[0.1,0.2]}]},"$ink"],"else":["@bg","$ink"]}},"output":{"lossless":true},"layers":[{"id":"dither","type":"dither","colors":"$colors","source":{"if":"source","then":"{source}","else":{"pick":["field","linear","radial"]}},"shape":{"if":"shape","then":"{shape}","else":{"pick":["sphere","none","circle","arch","sphere"]}},"matrix":"{matrix}","cell":"{cell}","center":{"x":{"range":[0.4,0.6]},"y":{"range":[0.38,0.55]}},"size":"28vmin"},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.062]},"minSize":{"mul":["@u",0.046]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"@text","content":"{label}"}},"color":"@text","background":{"color":"@bg","radius":0,"padding":[{"mul":["@u",0.03]},{"mul":["@u",0.035]}]}}]},"duotone":{"$schema":"../schema/recipe.schema.json","name":"duotone","description":"A photo mapped to the palette's shadow and highlight colours, with grain, and a title over a scrim when given.","requires":["image"],"vars":{"shadow":{"if":{"mode":"dark"},"then":"@bg","else":"@text"}},"layers":[{"id":"image","type":"image","src":"{image}","duotone":true},{"id":"scrim","type":"linear","when":"title","angle":180,"colors":["$shadow","$shadow"],"opacities":[0,0.62]},{"id":"grain","type":"grain","amount":0.07},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"#e9e9e9","content":"{label}"}},"color":"#ffffff"}]},"gridTitle":{"$schema":"../schema/recipe.schema.json","name":"gridTitle","description":"The aurora field under a dotted grid (four cells each way), grain, and an optional headline lower left.","layers":[{"id":"bg","type":"solid"},{"id":"mesh","type":"mesh","count":4,"blur":"11vmax","size":[0.45,0.8],"streak":1.8,"tone":{"if":"title","then":{"if":{"mode":"dark"},"then":{"l":[0.34,0.5],"c":[0.12,0.22]},"else":{"l":[0.66,0.82],"c":[0.13,0.2]}},"else":{"l":[0.55,0.85],"c":[0.12,0.22]}},"fold":{"if":"title","then":false,"else":{"pick":[false,false,true]}},"streaks":{"if":"title","then":0,"else":{"pick":[0,0,0,6]}},"hues":{"pick":[2,2,3]},"hueSpan":60},{"id":"grid","type":"pattern","kind":"grid","divisions":4,"stroke":"0.3vmin","dash":"dotted","color":"#ffffff","opacity":0.6},{"id":"grain","type":"grain","amount":0.08},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"@text","content":"{label}"}}}]},"iconTile":{"$schema":"../schema/recipe.schema.json","name":"iconTile","description":"An icon on a raised tile over a soft tint of the accent, a glow and faint rings. With a title, the tile moves to the upper left.","vars":{"tint":{"mix":["@bg","@accent",{"if":{"mode":"dark"},"then":0.1,"else":0.07}]},"glow":{"if":"title","then":{"x":{"add":[0.22,{"range":[-0.12,0.12]}]},"y":{"add":[0.26,{"range":[-0.12,0.12]}]}},"else":{"x":{"add":[0.5,{"range":[-0.12,0.12]}]},"y":{"add":[0.5,{"range":[-0.12,0.12]}]}}}},"layers":[{"id":"bg","type":"solid","color":"$tint"},{"id":"glow","type":"radial","colors":[{"mix":["$tint","@accent",{"if":{"mode":"dark"},"then":0.32,"else":0.26}]},"$tint"],"x":"$glow.x","y":"$glow.y","radius":"52vmax"},{"id":"rings","type":"pattern","kind":"rings","size":{"if":"title","then":"6vmin","else":"7vmin"},"center":"$glow","opacity":{"if":{"mode":"dark"},"then":0.14,"else":0.16},"fade":"radial","color":{"if":{"mode":"dark"},"then":"@text","else":"@accent"}},{"id":"icon","type":"icon","name":"{icon}","path":{"if":{"not":"icon"},"then":"M12 2c.4 4.9 2.1 7.6 7 8.6l3 .4-3 .4c-4.9 1-6.6 3.7-7 8.6-.4-4.9-2.1-7.6-7-8.6L2 11l3-.4c4.9-1 6.6-3.7 7-8.6Z"},"viewBox":{"if":{"not":"icon"},"then":[0,0,24,24]},"size":{"mul":["@u",{"if":"title","then":0.075,"else":0.12}]},"anchor":{"if":"title","then":"top-left","else":"center"},"tile":{"color":{"if":{"mode":"dark"},"then":{"mix":["$tint","@text",0.09]},"else":{"mix":["$tint","#ffffff",0.85]}}}},{"id":"grain","type":"grain","amount":0.05},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":"{label}"}]},"issue":{"$schema":"../schema/recipe.schema.json","name":"issue","description":"Type-led: a huge issue number in the accent, sized to the canvas and cropped at the bottom only, an optional No., a masthead line with the date, a hairline.","params":{"number":null,"masthead":null,"date":null,"prefix":true},"vars":{"number":{"if":"number","then":"{number}","else":{"int":[1,64]}},"numeral":{"readable":["@bg",3,"@accent"]},"size":{"min":[{"mul":["@height",1.04]},{"div":[{"mul":["@width",1.62]},{"length":"$number"}]}]},"bleed":{"add":[1,{"div":[{"mul":["$size",0.1]},"@height"]}]}},"layers":[{"id":"bg","type":"solid"},{"id":"masthead","type":"text","when":"masthead","content":"{masthead}","anchor":"top-left","size":{"mul":["@u",0.024]},"weight":600,"letterSpacing":0.08,"transform":"uppercase","maxLines":1,"balance":false,"color":"@text"},{"id":"date","type":"text","when":"date","content":"{date}","anchor":"top-right","size":{"mul":["@u",0.024]},"weight":500,"letterSpacing":0.04,"maxLines":1,"balance":false,"color":"@text"},{"id":"rule","type":"rule","y":{"if":{"any":["masthead","date"]},"then":0.13,"else":0.1}},{"id":"no","type":"text","when":{"not":{"prefix":false}},"content":"No.","anchor":"left","position":{"x":0.07,"y":{"if":"title","then":0.31,"else":0.27}},"size":{"mul":["@u",0.05]},"weight":500,"letterSpacing":-0.02,"color":"$numeral","maxLines":1,"balance":false},{"id":"number","type":"text","content":"$number","anchor":"bottom-left","position":{"x":0.055,"y":"$bleed"},"size":"$size","weight":600,"letterSpacing":-0.06,"lineHeight":1,"maxLines":1,"maxWidth":100000,"balance":false,"color":"$numeral"},{"id":"title","type":"text","when":"title","content":"{title}","anchor":{"if":{"aspect":"wide"},"then":"top-right","else":"top-left"},"weight":500,"size":{"mul":["@u",0.05]},"minSize":{"mul":["@u",0.04]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":2,"label":{"if":"label","then":{"color":"@text","content":"{label}"}},"offset":{"y":"9vh"},"maxWidth":{"if":{"aspect":"wide"},"then":"40vw","else":"52vw"},"align":"left"}]},"mark":{"$schema":"../schema/recipe.schema.json","name":"mark","description":"One icon, centred, on the accent or an accent gradient: white whenever white clears 3:1 (WCAG 1.4.11, non-text) against everything under it, else near-black.","params":{"background":"solid","angle":135,"colors":null,"size":"24vmin","iconColor":"auto"},"vars":{"gradient":{"if":"colors","then":"{colors}","else":["@accent",{"mix":["@accent","@colors.1",0.5,"oklch"]}]},"ink":{"if":{"iconColor":"auto"},"then":{"if":{"background":"gradient"},"then":{"readable":[{"gradientUnder":["$gradient","{angle}","{size}"]},3,"white"]},"else":{"readable":["@accent",3,"white"]}},"else":{"if":{"iconColor":"white"},"then":"#ffffff","else":{"if":{"iconColor":"black"},"then":"#111111","else":"{iconColor}"}}},"type":{"if":{"background":"gradient"},"then":{"readable":["$gradient",4.5,"$ink"]},"else":{"readable":["@accent",4.5,"$ink"]}}},"layers":[{"id":"bg","type":"solid","when":{"background":"solid"},"color":"@accent"},{"id":"bg","type":"linear","when":{"background":"gradient"},"angle":"{angle}","colors":"$gradient"},{"id":"icon","type":"icon","name":"{icon}","path":{"if":{"not":"icon"},"then":"M12 2c.4 4.9 2.1 7.6 7 8.6l3 .4-3 .4c-4.9 1-6.6 3.7-7 8.6-.4-4.9-2.1-7.6-7-8.6L2 11l3-.4c4.9-1 6.6-3.7 7-8.6Z"},"viewBox":{"if":{"not":"icon"},"then":[0,0,24,24]},"size":"{size}","color":"$ink"},{"id":"grain","type":"grain","amount":0.03},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.066]},"minSize":{"mul":["@u",0.048]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"$type","content":"{label}"}},"color":"$type"}]},"photo":{"$schema":"../schema/recipe.schema.json","name":"photo","description":"A palette taken from a photo, laid out as full-bleed sweeps like aurora (the photo itself not shown), or `show: 'blur'` for the photo heavily blurred.","params":{"show":null},"requires":["image"],"vars":{"src":{"if":"image","then":"{image}","else":null}},"layers":[{"id":"bg","type":"solid"},{"id":"mesh","type":"mesh","when":{"not":{"show":"blur"}},"from":"$src","count":4,"blur":"11vmax","size":[0.45,0.8],"streak":1.8,"hues":3,"tone":{"if":"title","then":{"if":{"mode":"dark"},"then":{"l":[0.34,0.5],"c":[0.08,0.2]},"else":{"l":[0.66,0.82],"c":[0.08,0.18]}},"else":{"l":[0.5,0.85],"c":[0.08,0.2]}},"fold":{"if":"title","then":false,"else":{"pick":[false,false,true]}}},{"id":"photo","type":"image","when":{"show":"blur"},"src":"$src","blur":"7vmax"},{"id":"scrim","type":"solid","when":{"all":[{"show":"blur"},"title"]},"color":{"if":{"mode":"dark"},"then":"#000000","else":"#ffffff"},"opacity":0.4},{"id":"grain","type":"grain","amount":0.08},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.062]},"minSize":{"mul":["@u",0.046]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"@text","content":"{label}"}},"color":"@text"}]},"riso":{"$schema":"../schema/recipe.schema.json","name":"riso","description":"Two (or three) flat risograph inks overprinted on paper: big simple forms, slight misregistration, speckled or halftone texture, uneven density. Inks from the palette, or classic riso inks.","params":{"inks":null,"texture":"grain","third":false},"vars":{"ground":{"if":{"mode":"dark"},"then":"@bg","else":"#f6f1e7"},"blend":{"if":{"mode":"dark"},"then":"screen","else":"multiply"},"inks":{"if":"inks","then":"{inks}","else":[{"tone":["@accent",{"l":[0.5,0.72],"c":[0.14,0.26]}]},{"tone":["@colors.1",{"l":[0.5,0.72],"c":[0.14,0.26]}]},{"tone":["@colors.2",{"l":[0.5,0.72],"c":[0.14,0.26]}]}]},"comp":{"if":"title","then":{"pick":[[[{"kind":"circle","x":{"range":[0.4,0.6]},"y":0.32,"r":"24vmin"}],[{"kind":"band","y":0.42,"height":"16vmin","rotate":{"range":[-8,8]}}]],[[{"kind":"arch","x":{"range":[0.34,0.42]},"y":0.34,"width":"34vmin","height":"44vmin"}],[{"kind":"circle","x":{"range":[0.58,0.66]},"y":0.3,"r":"16vmin"}]],[[{"kind":"half","x":0.5,"y":0.5,"r":"32vmin"}],[{"kind":"circle","x":0.5,"y":0.3,"r":"14vmin"}]]]},"else":{"pick":[[[{"kind":"circle","x":{"range":[0.36,0.5]},"y":{"range":[0.38,0.5]},"r":"34vmin"}],[{"kind":"band","y":{"range":[0.56,0.68]},"height":"26vmin","rotate":{"range":[-14,14]}}]],[[{"kind":"arch","x":{"range":[0.32,0.42]},"y":0.54,"width":"46vmin","height":"66vmin"}],[{"kind":"circle","x":{"range":[0.6,0.68]},"y":{"range":[0.34,0.44]},"r":"22vmin"}]],[[{"kind":"half","x":0.5,"y":{"range":[0.6,0.7]},"r":"42vmin"}],[{"kind":"circle","x":{"range":[0.42,0.58]},"y":{"range":[0.34,0.42]},"r":"19vmin"}]],[[{"kind":"ring","x":{"range":[0.4,0.48]},"y":0.48,"r":"30vmin","thickness":"12vmin"}],[{"kind":"circle","x":{"range":[0.56,0.64]},"y":{"range":[0.5,0.58]},"r":"24vmin"}]]]}}},"layers":[{"id":"bg","type":"solid","color":"$ground"},{"id":"inkA","type":"ink","color":"$inks.0","shapes":"$comp.0","texture":"{texture}","blend":"$blend"},{"id":"inkB","type":"ink","color":"$inks.1","shapes":"$comp.1","texture":"{texture}","blend":"$blend"},{"id":"inkC","type":"ink","when":"third","color":"$inks.2","shapes":[{"kind":"circle","x":{"range":[0.2,0.8]},"y":{"range":[0.2,0.4]},"r":"7vmin"}],"texture":"{texture}","blend":"$blend"},{"id":"grain","type":"grain","amount":0.05},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"bottom-left","weight":500,"size":{"mul":["@u",0.062]},"minSize":{"mul":["@u",0.046]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"@text","content":"{label}"}},"color":"@text"}]},"stripes":{"$schema":"../schema/recipe.schema.json","name":"stripes","description":"A tonal band of tapering stripes down one side, a disc riding its edge (with an optional icon), and the title on the open side (the claude.com/resources look).","params":{"side":"right"},"vars":{"spotY":{"add":[{"if":"title","then":0.36,"else":0.5},{"range":[-0.06,0.06]}]},"steps":{"int":[5,8]},"taper":{"range":[0.72,0.86]},"ramp":{"ramp":["@accent","@bg","$steps",0.88]},"weights":{"taper":["$taper","$steps"]},"edge":{"if":{"side":"right"},"then":{"sub":[1,0.46]},"else":0.46},"spot":{"mul":[{"min":["@width","@height"]},0.34]}},"layers":[{"id":"bg","type":"solid"},{"id":"stripes","type":"stripes","direction":"vertical","region":{"if":{"side":"right"},"then":{"x":"$edge","y":0,"width":0.46,"height":1},"else":{"x":0,"y":0,"width":0.46,"height":1}},"widths":{"if":{"side":"right"},"then":{"reverse":"$weights"},"else":"$weights"},"colors":{"if":{"side":"right"},"then":{"reverse":"$ramp"},"else":"$ramp"}},{"id":"spot","type":"shapes","items":[{"kind":"circle","x":"$edge","y":"$spotY","size":"$spot","color":"@text"}]},{"id":"icon","type":"icon","when":"icon","name":"{icon}","size":{"mul":["$spot",0.4]},"position":{"x":"$edge","y":"$spotY"},"color":"@bg"},{"id":"grain","type":"grain","amount":0.06},{"id":"title","type":"text","when":"title","content":"{title}","anchor":{"if":{"side":"right"},"then":"bottom-left","else":"bottom-right"},"weight":500,"size":{"mul":["@u",0.058]},"minSize":{"mul":["@u",0.044]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":4,"label":"{label}","maxWidth":{"sub":[{"mul":["@width",{"sub":[1,0.46]}]},{"mul":["@u",0.12]}]}}]},"waveform":{"$schema":"../schema/recipe.schema.json","name":"waveform","description":"An episode's waveform (peaks you pass, or audio decoded at build) as bars, mirrored bars, a line or dots, on a flat or toned field, with an optional duration and label. Without peaks or audio it draws seeded demo peaks.","params":{"peaks":null,"audio":null,"duration":null,"style":null,"field":"flat"},"vars":{"field":{"if":{"field":"toned"},"then":{"tone":["@accent",{"if":{"mode":"dark"},"then":{"l":[0.3,0.4],"c":[0.06,0.12]},"else":{"l":[0.88,0.94],"c":[0.03,0.06]}}]},"else":"@bg"},"wave":{"readable":["$field",3,"@accent"]},"type":{"readable":["$field",4.5,"@text"]}},"layers":[{"id":"bg","type":"solid","color":"$field"},{"id":"wave","type":"waveform","peaks":"{peaks}","audio":"{audio}","style":{"if":"style","then":"{style}","else":{"pick":["mirrored","bars","line","dots","mirrored"]}},"color":"$wave","duration":"{duration}","caption":{"if":{"any":["duration","audio"]},"then":true,"else":null},"captionColor":"$type","region":{"if":"title","then":{"x":0.07,"y":0.42,"width":0.86,"height":0.3},"else":{"x":0.07,"y":0.3,"width":0.86,"height":0.4}}},{"id":"grain","type":"grain","amount":0.04},{"id":"title","type":"text","when":"title","content":"{title}","anchor":"top-left","weight":500,"size":{"mul":["@u",0.062]},"minSize":{"mul":["@u",0.046]},"lineHeight":1.08,"letterSpacing":-0.025,"maxLines":3,"label":{"if":"label","then":{"color":"$type","content":"{label}"}},"color":"$type"}]}};
@@ -0,0 +1,7 @@
1
+ // The layer as text art.
2
+ import type { Effect } from './index.js';
3
+ import type { Pixels } from './buffer.js';
4
+ import type { PixelResult } from './pixels.js';
5
+
6
+ /** `ascii`, on pixels (one pixel per font pixel, scaled up crisp). */
7
+ export function ascii(img: Pixels, fx: Extract<Effect, { kind: 'ascii' }>, ctx: object): PixelResult;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `ascii`: the layer as text art. Each cell's brightness picks a
3
+ * character from a ramp (ordered by how much ink each glyph of the
4
+ * built-in 5×7 pixel face carries), drawn in the cell's own colour or one
5
+ * ink, on a background. Rendered at one pixel per font pixel and embedded
6
+ * scaled up crisp, so it's tiny and sharp at any size.
7
+ *
8
+ * @module effects/ascii
9
+ */
10
+
11
+ import { px, premultiplied } from './buffer.js';
12
+ import { rgb } from './util.js';
13
+ import { pixelBitmap } from '../type/pixel.js';
14
+
15
+ /** The default ramp, light to dense. */
16
+ const RAMP = ' .:-=+*%#@';
17
+
18
+ /**
19
+ * `ascii`: `cell` (a length: a character's width; height is 8/6 of it),
20
+ * `ramp` (characters, sparse to dense, re-ordered by their ink), `color`
21
+ * (`'source'`, each character the colour under it, or one colour),
22
+ * `background` (a colour, or `'none'`), `invert` (dense where it's dark,
23
+ * for dark ink on paper), `contrast` (after the tones are stretched to the
24
+ * layer's own range).
25
+ *
26
+ * @param {import('./buffer.js').Pixels} img
27
+ * @param {import('./index.js').Effect & { kind: 'ascii' }} fx
28
+ * @param {any} ctx
29
+ * @returns {import('./pixels.js').PixelResult}
30
+ */
31
+ export function ascii(img, fx, ctx) {
32
+ const { width: w, height: h } = img;
33
+ const cw = Math.max(3, px(fx.cell, ctx, img, '1.4vmin'));
34
+ const ch = (cw * 8) / 6;
35
+ const cols = Math.max(1, Math.round(w / cw));
36
+ const rows = Math.max(1, Math.round(h / ch));
37
+ // Glyphs that the face draws, by ink.
38
+ const glyphs = [...new Set([...(fx.ramp ?? RAMP)])].map((c) => ({ c, bits: c === ' ' ? '.'.repeat(35) : pixelBitmap(c) })).filter((g) => g.bits);
39
+ if (!glyphs.length) throw new TypeError('ascii: the ramp has no characters the pixel face draws (A–Z, 0–9, punctuation)');
40
+ glyphs.sort((a, b) => (a.bits?.split('#').length ?? 0) - (b.bits?.split('#').length ?? 0));
41
+ const src = premultiplied(img);
42
+ const acc = new Float64Array(cols * rows * 5);
43
+ for (let y = 0; y < h; y++) {
44
+ const cy = Math.min(rows - 1, ((y * rows) / h) | 0);
45
+ for (let x = 0; x < w; x++) {
46
+ const c = (cy * cols + Math.min(cols - 1, ((x * cols) / w) | 0)) * 5;
47
+ const i = (y * w + x) * 4;
48
+ acc[c] += src[i]; acc[c + 1] += src[i + 1]; acc[c + 2] += src[i + 2]; acc[c + 3] += src[i + 3]; acc[c + 4]++;
49
+ }
50
+ }
51
+ const ink = fx.color && fx.color !== 'source' ? rgb(fx.color).map((v) => v * 255) : null;
52
+ const bg = fx.background === 'none' ? null : rgb(fx.background ?? '#0b0b0b').map((v) => v * 255);
53
+ const contrast = fx.contrast ?? 1.2;
54
+ // Tones stretched to the cells' own range, so a dim layer still reads.
55
+ const lums = new Float32Array(cols * rows);
56
+ let lo = 1;
57
+ let hi = 0;
58
+ for (let c = 0; c < lums.length; c++) {
59
+ const a = acc[c * 5 + 3];
60
+ if (!a) continue;
61
+ lums[c] = (0.2126 * acc[c * 5] + 0.7152 * acc[c * 5 + 1] + 0.0722 * acc[c * 5 + 2]) / a;
62
+ lo = Math.min(lo, lums[c]);
63
+ hi = Math.max(hi, lums[c]);
64
+ }
65
+ const span = hi - lo > 0.05 ? hi - lo : 1;
66
+ const W = cols * 6;
67
+ const H = rows * 8;
68
+ const out = new Uint8ClampedArray(W * H * 4);
69
+ for (let cy = 0; cy < rows; cy++) {
70
+ for (let cx = 0; cx < cols; cx++) {
71
+ const c = (cy * cols + cx) * 5;
72
+ const n = acc[c + 4] || 1;
73
+ const a = acc[c + 3] / n;
74
+ const k = a ? 255 / a : 0;
75
+ const r = (acc[c] / n) * k;
76
+ const g = (acc[c + 1] / n) * k;
77
+ const b = (acc[c + 2] / n) * k;
78
+ let l = (lums[cy * cols + cx] - (span === 1 ? 0 : lo)) / span;
79
+ l = Math.min(1, Math.max(0, (l - 0.5) * contrast + 0.5));
80
+ if (fx.invert) l = 1 - l;
81
+ const glyph = a < 8 ? glyphs[0] : glyphs[Math.min(glyphs.length - 1, Math.floor(l * glyphs.length))];
82
+ const col = ink ?? [r, g, b];
83
+ for (let gy = 0; gy < 8; gy++) {
84
+ for (let gx = 0; gx < 6; gx++) {
85
+ const on = gy < 7 && gx < 5 && glyph.bits?.[gy * 5 + gx] === '#';
86
+ const o = ((cy * 8 + gy) * W + cx * 6 + gx) * 4;
87
+ if (on) { out[o] = col[0]; out[o + 1] = col[1]; out[o + 2] = col[2]; out[o + 3] = 255; } else if (bg) { out[o] = bg[0]; out[o + 1] = bg[1]; out[o + 2] = bg[2]; out[o + 3] = 255; }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return { data: out, width: W, height: H, pixelated: true };
93
+ }
@@ -0,0 +1,60 @@
1
+ // Pixel helpers shared by the pixel effects.
2
+ import type { Length } from '../index.js';
3
+
4
+ /** A layer drawn as pixels: RGBA, 8-bit, not premultiplied. */
5
+ export interface Pixels {
6
+ data: Uint8ClampedArray;
7
+ width: number;
8
+ height: number;
9
+ /** Pixels per layer unit. */
10
+ scale: number;
11
+ /** Every pixel has alpha 255. */
12
+ opaque: boolean;
13
+ }
14
+
15
+ /** sRGB byte → linear light, 0–1. */
16
+ export const TO_LINEAR: Float32Array;
17
+ /** Linear light (clipped) to an sRGB byte. */
18
+ export function toSrgb(v: number): number;
19
+ /** A length in pixels. */
20
+ export function px(v: Length | undefined, ctx: { canvas: { width: number; height: number } }, img: Pixels, fallback: Length): number;
21
+ /** A point given as fractions of the canvas, in pixels. */
22
+ export function pointPx(p: [number, number] | { x: number; y: number } | undefined, img: { width: number; height: number }, fallback?: [number, number]): [number, number];
23
+ /** A point as fractions. */
24
+ export function fraction(p: [number, number] | { x: number; y: number } | undefined, fallback: [number, number]): [number, number];
25
+ /** 0–1. */
26
+ export function clamp01(v: number): number;
27
+ /** Hermite step from a to b. */
28
+ export function smoothstep(a: number, b: number, v: number): number;
29
+ /** A premultiplied copy (the same buffer when opaque). */
30
+ export function premultiplied(img: Pixels): Uint8ClampedArray;
31
+ /** Undo premultiplying, in place. */
32
+ export function unpremultiply(d: Uint8ClampedArray): Uint8ClampedArray;
33
+ /** Bilinear RGBA sample, the edge held, written to `out` at `o`. */
34
+ export function bilinear(src: Uint8ClampedArray, w: number, h: number, x: number, y: number, out: Uint8ClampedArray | Float32Array, o: number): void;
35
+ /** Resample every pixel from where `map` says it came from. */
36
+ export function remap(img: Pixels, map: (x: number, y: number, at: Float64Array) => void): Uint8ClampedArray;
37
+ /** Box radii for three passes approximating a Gaussian. */
38
+ export function boxesFor(sigma: number): number[];
39
+ /** A box blur along rows (0) or columns (1), the edge held. */
40
+ export function boxPass(src: Uint8ClampedArray | Float32Array, dst: Uint8ClampedArray | Float32Array, w: number, h: number, ch: number, r: number, dir: 0 | 1): void;
41
+ /** A Gaussian blur in place (three box passes each way). */
42
+ export function gaussian<T extends Uint8ClampedArray | Float32Array>(data: T, w: number, h: number, ch: number, sigma: number, sigmaY?: number): T;
43
+ /** Downsample by a whole factor, averaging blocks. */
44
+ export function shrink(src: Uint8ClampedArray, w: number, h: number, f: number): { data: Uint8ClampedArray; width: number; height: number };
45
+ /** Upscale, bilinear. */
46
+ export function enlarge(src: Uint8ClampedArray, sw: number, sh: number, w: number, h: number): Uint8ClampedArray;
47
+ /** Upscale, nearest neighbour. */
48
+ export function nearest(src: Uint8ClampedArray, sw: number, sh: number, w: number, h: number): Uint8ClampedArray;
49
+ /** Rec. 709 luma of a pixel, 0–255. */
50
+ export function luma(d: ArrayLike<number>, i: number): number;
51
+ /** The effect's own seed (hashed), else one from the layer's stream. */
52
+ export function seedFor(fx: { seed?: number | string }, ctx: { rng: { int: (a: number, b: number) => number } }): number;
53
+ /** An integer hash of a lattice point and a seed, 0–1. */
54
+ export function hash2(x: number, y: number, seed: number): number;
55
+ /** Smooth value noise, -1–1. */
56
+ export function valueNoise(x: number, y: number, seed: number): number;
57
+ /** Fractal value noise, about -1–1. */
58
+ export function fractalNoise(x: number, y: number, seed: number, octaves?: number): number;
59
+ /** A fractal noise field for every pixel, -1–1. */
60
+ export function noiseField(w: number, h: number, period: number, seed: number, octaves?: number): Float32Array;