@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,212 @@
1
+ /**
2
+ * Screens and signals: `scanlines` (a filter: dark lines across the
3
+ * layer), and on pixels `crt` (a tube's RGB phosphor mask, scanlines and
4
+ * curvature), `vhs` (tape: smeared, shifted chroma, line jitter, tracking
5
+ * noise, snow) and `jpeg` (real JPEG compression, as crunchy as you like,
6
+ * blocks at any size).
7
+ *
8
+ * @module effects/screen
9
+ */
10
+
11
+ import { attrs, num } from '../svg.js';
12
+ import { len } from '../geometry.js';
13
+ import { encodePng, pngUri } from '../png.js';
14
+ import { createRng } from '../rng.js';
15
+ import { jpegRoundTrip } from '../raster/index.js';
16
+ import { px, seedFor, bilinear, shrink, gaussian, smoothstep, hash2 } from './buffer.js';
17
+
18
+ /**
19
+ * `scanlines`: dark lines every `spacing` (a length), `thickness` of each
20
+ * period (0–1), `amount` dark (0–1), across the layer.
21
+ *
22
+ * @param {import('./index.js').Effect & { kind: 'scanlines' }} fx
23
+ * @param {import('../render/context.js').RenderContext} ctx
24
+ * @returns {string}
25
+ */
26
+ export function scanlines(fx, ctx) {
27
+ const { width: W, height: H } = ctx;
28
+ const spacing = Math.max(0.5, len(fx.spacing, ctx.canvas, '0.5vmin'));
29
+ const t = Math.min(0.95, Math.max(0.05, fx.thickness ?? 0.45));
30
+ const a = fx.amount ?? 0.45;
31
+ // One column of the pattern, eight samples a line, stretched across.
32
+ const rows = Math.min(8192, Math.max(2, Math.ceil((H / spacing) * 8)));
33
+ const col = new Uint8Array(rows);
34
+ for (let y = 0; y < rows; y++) {
35
+ const p = (((y + 0.5) * H) / rows / spacing) % 1;
36
+ const d = Math.abs(p - 0.5) * 2;
37
+ col[y] = Math.round(255 * (1 - a * (1 - smoothstep(t - 0.12, t + 0.12, d))));
38
+ }
39
+ return `<feImage${attrs({ href: pngUri(encodePng(col, 1, rows, 1)), x: 0, y: 0, width: W, height: H, preserveAspectRatio: 'none', result: 'sl' })}/>`
40
+ + '<feBlend in="sl" in2="SourceGraphic" mode="multiply"/><feComposite in2="SourceAlpha" operator="in"/>';
41
+ }
42
+
43
+ /**
44
+ * `crt`: a cathode-ray tube. `mask` (`'aperture'`: vertical RGB stripes,
45
+ * `'shadow'`: staggered triads), its triad `size` (a length), `amount`
46
+ * (0–1), `scanlines` (0–1) and barrel `curvature` (0–1, with the corners
47
+ * going dark).
48
+ *
49
+ * @param {import('./buffer.js').Pixels} img
50
+ * @param {import('./index.js').Effect & { kind: 'crt' }} fx
51
+ * @param {any} ctx
52
+ * @returns {Uint8ClampedArray}
53
+ */
54
+ export function crt(img, fx, ctx) {
55
+ const { data, width: w, height: h } = img;
56
+ const size = Math.max(3, px(fx.size, ctx, img, '0.45vmin'));
57
+ const amount = fx.amount ?? 0.75;
58
+ const lines = fx.scanlines ?? 0.45;
59
+ const curve = fx.curvature ?? 0;
60
+ const shadow = (fx.mask ?? 'aperture') === 'shadow';
61
+ if (!['aperture', 'shadow'].includes(fx.mask ?? 'aperture')) throw new TypeError(`Unknown crt mask "${fx.mask}" (aperture, shadow)`);
62
+ const out = new Uint8ClampedArray(data.length);
63
+ const tmp = new Float32Array(4);
64
+ const cx = (w - 1) / 2;
65
+ const cy = (h - 1) / 2;
66
+ const k = curve * 0.25;
67
+ // Mask brightness lost, given back so the mean holds.
68
+ const gain = 1 / (1 - amount * 0.55) / (1 - lines * 0.3);
69
+ const lineH = size;
70
+ for (let y = 0, o = 0; y < h; y++) {
71
+ for (let x = 0; x < w; x++, o += 4) {
72
+ let sx = x;
73
+ let sy = y;
74
+ let edge = 1;
75
+ if (k) {
76
+ const u = (x - cx) / cx;
77
+ const v = (y - cy) / cy;
78
+ const r2 = u * u + v * v;
79
+ sx = cx + u * (1 + k * r2) * cx;
80
+ sy = cy + v * (1 + k * r2) * cy;
81
+ if (sx < 0 || sy < 0 || sx > w - 1 || sy > h - 1) { out[o + 3] = 255; continue; }
82
+ edge = smoothstep(0, 0.04, Math.min(sx / w, sy / h, 1 - sx / w, 1 - sy / h) * 4);
83
+ }
84
+ bilinear(data, w, h, sx, sy, tmp, 0);
85
+ // Which phosphor this pixel is: a third of a triad each.
86
+ const col = (x / size) * 3;
87
+ const row = shadow ? y / lineH + (Math.floor(x / size) % 2) * 0.5 : y / lineH;
88
+ const ph = col - Math.floor(col);
89
+ const sub = Math.floor(col) % 3;
90
+ const soft = 1 - Math.abs(ph - 0.5) * 2 * 0.5;
91
+ const scan = 1 - lines * (1 - Math.sin(Math.PI * (row - Math.floor(row))) ** 2);
92
+ for (let c = 0; c < 3; c++) {
93
+ const m = c === sub ? soft : 1 - amount;
94
+ out[o + c] = tmp[c] * (1 - amount + amount * m) * scan * gain * edge;
95
+ }
96
+ out[o + 3] = tmp[3];
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * `vhs`: a tape played back. Chroma smeared by `bleed` and shifted by
104
+ * `shift` (lengths), lines jittered (`jitter`, a length), a tracking band
105
+ * of torn lines (`tracking`, 0–1), `noise` (0–1) and a little softness;
106
+ * seeded.
107
+ *
108
+ * @param {import('./buffer.js').Pixels} img
109
+ * @param {import('./index.js').Effect & { kind: 'vhs' }} fx
110
+ * @param {any} ctx
111
+ * @returns {Uint8ClampedArray}
112
+ */
113
+ export function vhs(img, fx, ctx) {
114
+ const { data, width: w, height: h } = img;
115
+ const seed = seedFor(fx, ctx);
116
+ const rng = createRng(`vhs:${seed}`);
117
+ const bleed = px(fx.bleed, ctx, img, '0.9vmin');
118
+ const shift = px(fx.shift, ctx, img, '0.35vmin');
119
+ const jitter = px(fx.jitter, ctx, img, '0.12vmin');
120
+ const tracking = fx.tracking ?? 0.5;
121
+ const noise = fx.noise ?? 0.35;
122
+ // YIQ: luma kept sharp-ish, chroma smeared along the line.
123
+ const Y = new Float32Array(w * h);
124
+ const IQ = new Float32Array(w * h * 2);
125
+ for (let p = 0, i = 0; p < Y.length; p++, i += 4) {
126
+ const r = data[i];
127
+ const g = data[i + 1];
128
+ const b = data[i + 2];
129
+ Y[p] = 0.299 * r + 0.587 * g + 0.114 * b;
130
+ IQ[p * 2] = 0.596 * r - 0.274 * g - 0.322 * b;
131
+ IQ[p * 2 + 1] = 0.211 * r - 0.523 * g + 0.312 * b;
132
+ }
133
+ gaussian(Y, w, h, 1, px('0.06vmin', ctx, img, 0), 0);
134
+ gaussian(IQ, w, h, 2, Math.max(1, bleed), 0);
135
+ // Per-line offsets: a gentle wobble, and a tracking band that tears.
136
+ const offs = new Float32Array(h);
137
+ const band = rng.range(0.62, 0.9) * h;
138
+ const bandH = h * (0.04 + 0.08 * tracking);
139
+ for (let y = 0; y < h; y++) {
140
+ let o = (hash2(y >> 1, 0, seed) - 0.5) * 2 * jitter + Math.sin(y / (h / 7) + rng.next() * 0.02) * jitter * 0.5;
141
+ const t = Math.abs(y - band) / bandH;
142
+ if (tracking && t < 1) o += (1 - t) ** 2 * tracking * px('4vmin', ctx, img, 0) * (hash2(y >> 2, 1, seed) - 0.3);
143
+ offs[y] = o;
144
+ }
145
+ const out = new Uint8ClampedArray(data.length);
146
+ for (let y = 0; y < h; y++) {
147
+ const o = offs[y];
148
+ const t = Math.abs(y - band) / bandH;
149
+ const torn = tracking && t < 1 ? (1 - t) * tracking : 0;
150
+ for (let x = 0; x < w; x++) {
151
+ const sx = Math.min(w - 1, Math.max(0, x - o));
152
+ const x0 = sx | 0;
153
+ const f = sx - x0;
154
+ const x1 = Math.min(w - 1, x0 + 1);
155
+ const yv = Y[y * w + x0] * (1 - f) + Y[y * w + x1] * f;
156
+ const cxp = Math.min(w - 1, Math.max(0, sx - shift));
157
+ const c0 = cxp | 0;
158
+ const cf = cxp - c0;
159
+ const c1 = Math.min(w - 1, c0 + 1);
160
+ const iv = IQ[(y * w + c0) * 2] * (1 - cf) + IQ[(y * w + c1) * 2] * cf;
161
+ const qv = IQ[(y * w + c0) * 2 + 1] * (1 - cf) + IQ[(y * w + c1) * 2 + 1] * cf;
162
+ // Snow: fine noise everywhere, white streaks in the torn band.
163
+ let n = (hash2(x, y, seed) - 0.5) * 60 * noise;
164
+ if (torn && hash2(x >> 3, y, seed + 1) > 1 - torn * 0.35) n += 160 * torn * hash2(x, y, seed + 2);
165
+ const yy = yv + n;
166
+ const i = (y * w + x) * 4;
167
+ out[i] = yy + 0.956 * iv + 0.621 * qv;
168
+ out[i + 1] = yy - 0.272 * iv - 0.647 * qv;
169
+ out[i + 2] = yy - 1.106 * iv + 1.703 * qv;
170
+ out[i + 3] = data[i + 3];
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+
176
+ /**
177
+ * `jpeg`: through a real JPEG encoder at `quality` (1–100) and back,
178
+ * `passes` times (each re-save slightly shifted, as images degrade when
179
+ * passed round), its 8-pixel blocks drawn `block` big (a length: bigger
180
+ * is crunchier), colour subsampled unless `subsample: false`.
181
+ *
182
+ * @param {import('./buffer.js').Pixels} img
183
+ * @param {import('./index.js').Effect & { kind: 'jpeg' }} fx
184
+ * @param {any} ctx
185
+ * @returns {Promise<import('./pixels.js').PixelResult | Uint8ClampedArray>}
186
+ */
187
+ export async function jpeg(img, fx, ctx) {
188
+ const { data, width: w, height: h } = img;
189
+ const b = Math.max(1, Math.round(px(fx.block, ctx, img, '0.4vmin')));
190
+ const q = Math.max(1, Math.min(100, Math.round(fx.quality ?? 12)));
191
+ const passes = Math.max(1, Math.min(8, Math.round(fx.passes ?? 1)));
192
+ const small = b > 1 ? shrink(data, w, h, b) : { data: new Uint8ClampedArray(data), width: w, height: h };
193
+ let cur = small.data;
194
+ for (let p = 0; p < passes; p++) {
195
+ const got = await jpegRoundTrip(cur, small.width, small.height, Math.max(1, q - p * 2), fx.subsample !== false, ctx.sharp);
196
+ if (!got) return data;
197
+ // Later passes see the image a pixel over, so their blocks fall differently.
198
+ if (p < passes - 1) {
199
+ const shifted = new Uint8ClampedArray(got.length);
200
+ const sw = small.width;
201
+ for (let y = 0; y < small.height; y++) for (let x = 0; x < sw; x++) {
202
+ const s = (Math.min(small.height - 1, y + 1) * sw + Math.min(sw - 1, x + 1)) * 4;
203
+ shifted.set(got.subarray(s, s + 4), (y * sw + x) * 4);
204
+ }
205
+ cur = shifted;
206
+ } else cur = got;
207
+ }
208
+ for (let i = 3; i < cur.length; i += 4) cur[i] = 255;
209
+ if (b === 1) return cur;
210
+ // Embedded at one pixel per JPEG pixel, scaled up crisp.
211
+ return { data: cur, width: small.width, height: small.height, pixelated: true };
212
+ }
@@ -0,0 +1,8 @@
1
+ // Pixel sorting and pixel stretch.
2
+ import type { Effect } from './index.js';
3
+ import type { Pixels } from './buffer.js';
4
+
5
+ /** `sort`, on pixels. */
6
+ export function sort(img: Pixels, fx: Extract<Effect, { kind: 'sort' }>, ctx: object): Uint8ClampedArray;
7
+ /** `stretch`, on pixels. */
8
+ export function stretch(img: Pixels, fx: Extract<Effect, { kind: 'stretch' }>): Uint8ClampedArray;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Glitch-art pixel moves: `sort` (runs of pixels within a brightness
3
+ * range sorted along rows or columns, Kim Asendorf's pixel sorting) and
4
+ * `stretch` (one row or column smeared to the edge).
5
+ *
6
+ * @module effects/sort
7
+ */
8
+
9
+ import { seedFor, luma } from './buffer.js';
10
+ import { createRng } from '../rng.js';
11
+
12
+ /**
13
+ * A sort key, 0–255: luminance, hue or saturation.
14
+ *
15
+ * @param {Uint8ClampedArray} d
16
+ * @param {number} i
17
+ * @param {'luma' | 'hue' | 'saturation'} by
18
+ * @returns {number}
19
+ */
20
+ function keyOf(d, i, by) {
21
+ if (by === 'luma') return luma(d, i) | 0;
22
+ const r = d[i];
23
+ const g = d[i + 1];
24
+ const b = d[i + 2];
25
+ const max = Math.max(r, g, b);
26
+ const min = Math.min(r, g, b);
27
+ if (by === 'saturation') return max ? ((255 * (max - min)) / max) | 0 : 0;
28
+ if (max === min) return 0;
29
+ const c = max - min;
30
+ const hh = max === r ? ((g - b) / c + 6) % 6 : max === g ? (b - r) / c + 2 : (r - g) / c + 4;
31
+ return ((hh / 6) * 255) | 0;
32
+ }
33
+
34
+ /**
35
+ * `sort`: along `direction` (`'horizontal'` rows, `'vertical'` columns),
36
+ * every run of pixels whose luminance lies within `threshold` (`[lo, hi]`,
37
+ * 0–1) is sorted by `by` (`'luma'`, `'hue'`, `'saturation'`), dark to
38
+ * light unless `reverse`. `length` (0–1 of the line) breaks long runs at
39
+ * seeded points.
40
+ *
41
+ * @param {import('./buffer.js').Pixels} img
42
+ * @param {import('./index.js').Effect & { kind: 'sort' }} fx
43
+ * @param {any} ctx
44
+ * @returns {Uint8ClampedArray}
45
+ */
46
+ export function sort(img, fx, ctx) {
47
+ const { data, width: w, height: h } = img;
48
+ const vertical = (fx.direction ?? 'horizontal') === 'vertical';
49
+ const by = fx.by ?? 'luma';
50
+ if (!['luma', 'hue', 'saturation'].includes(by)) throw new TypeError(`Unknown sort key "${by}" (luma, hue, saturation)`);
51
+ const [lo, hi] = (fx.threshold ?? [0.25, 0.85]).map((v) => v * 255);
52
+ const rng = createRng(`sort:${seedFor(fx, ctx)}`);
53
+ const n = vertical ? h : w;
54
+ const lines = vertical ? w : h;
55
+ const cap = fx.length ? Math.max(2, fx.length * n) : Infinity;
56
+ const out = new Uint8ClampedArray(data);
57
+ const u32 = new Uint32Array(out.buffer);
58
+ const keys = new Uint8Array(n);
59
+ const inside = new Uint8Array(n);
60
+ const px = new Uint32Array(n);
61
+ const counts = new Uint32Array(256);
62
+ const buf = new Uint32Array(n);
63
+ for (let l = 0; l < lines; l++) {
64
+ const at = (/** @type {number} */ k) => (vertical ? k * w + l : l * w + k);
65
+ for (let k = 0; k < n; k++) {
66
+ const p = at(k);
67
+ px[k] = u32[p];
68
+ const lu = luma(out, p * 4);
69
+ inside[k] = lu >= lo && lu <= hi ? 1 : 0;
70
+ keys[k] = inside[k] ? keyOf(out, p * 4, by) : 0;
71
+ }
72
+ let k = 0;
73
+ while (k < n) {
74
+ if (!inside[k]) { k++; continue; }
75
+ // A run: as far as the range holds, or the seeded cap.
76
+ const limit = cap === Infinity ? n : k + Math.max(2, Math.round(cap * rng.range(0.4, 1)));
77
+ let e = k;
78
+ while (e < n && e < limit && inside[e]) e++;
79
+ // Counting sort of [k, e).
80
+ counts.fill(0);
81
+ for (let j = k; j < e; j++) counts[keys[j]]++;
82
+ let acc = 0;
83
+ for (let c = 0; c < 256; c++) { const t = counts[c]; counts[c] = acc; acc += t; }
84
+ for (let j = k; j < e; j++) buf[counts[keys[j]]++] = px[j];
85
+ const len = e - k;
86
+ for (let j = 0; j < len; j++) u32[at(k + j)] = buf[fx.reverse ? len - 1 - j : j];
87
+ k = e;
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * `stretch`: the row or column at `at` (0–1) smeared toward `direction`
95
+ * (`'down'`, `'up'`, `'left'`, `'right'`) for `length` of the way to the
96
+ * edge (default all of it), fading out by `fade` (0–1).
97
+ *
98
+ * @param {import('./buffer.js').Pixels} img
99
+ * @param {import('./index.js').Effect & { kind: 'stretch' }} fx
100
+ * @returns {Uint8ClampedArray}
101
+ */
102
+ export function stretch(img, fx) {
103
+ const { data, width: w, height: h } = img;
104
+ const dir = fx.direction ?? 'down';
105
+ if (!['down', 'up', 'left', 'right'].includes(dir)) throw new TypeError(`Unknown stretch direction "${dir}" (down, up, left, right)`);
106
+ const vertical = dir === 'down' || dir === 'up';
107
+ const n = vertical ? h : w;
108
+ const lines = vertical ? w : h;
109
+ const from = Math.min(n - 1, Math.max(0, Math.round((fx.at ?? 0.5) * (n - 1))));
110
+ const sign = dir === 'down' || dir === 'right' ? 1 : -1;
111
+ const room = sign > 0 ? n - 1 - from : from;
112
+ const reach = Math.round(room * Math.min(1, Math.max(0, fx.length ?? 1)));
113
+ const fade = Math.min(1, Math.max(0, fx.fade ?? 0));
114
+ const out = new Uint8ClampedArray(data);
115
+ for (let l = 0; l < lines; l++) {
116
+ const s = (vertical ? from * w + l : l * w + from) * 4;
117
+ for (let k = 1; k <= reach; k++) {
118
+ const pos = from + sign * k;
119
+ const o = (vertical ? pos * w + l : l * w + pos) * 4;
120
+ const t = fade * (k / Math.max(1, reach));
121
+ for (let c = 0; c < 4; c++) out[o + c] = data[s + c] + (data[o + c] - data[s + c]) * t;
122
+ }
123
+ }
124
+ return out;
125
+ }
@@ -0,0 +1,6 @@
1
+ // Mirror, as copies of the markup.
2
+ import type { Effect } from './index.js';
3
+ import type { Markup } from '../render/context.js';
4
+
5
+ /** `mirror`: one side reflected onto the other. */
6
+ export function mirror(fx: Extract<Effect, { kind: 'mirror' }>, m: Markup, ctx: { width: number; height: number; res?: number }, id: string): Markup;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `mirror`, drawn as copies of the layer's own markup (so vector art
3
+ * stays vector): one half reflected onto the other, or one quarter onto
4
+ * all four. (`kaleidoscope` is on pixels, in `distort.js`: its wedges
5
+ * need the frame filled past the layer's edge.)
6
+ *
7
+ * @module effects/symmetry
8
+ */
9
+
10
+ import { attrs, num } from '../svg.js';
11
+ import { fraction } from './buffer.js';
12
+
13
+ /**
14
+ * A copy of the source drawn through a transform, clipped to a polygon.
15
+ *
16
+ * @param {string} src - The source's id.
17
+ * @param {string} clip - The clip path's id.
18
+ * @param {string} [transform]
19
+ * @returns {string}
20
+ */
21
+ const copy = (src, clip, transform) => `<g clip-path="url(#${clip})"><use${attrs({ href: `#${src}`, transform })}/></g>`;
22
+
23
+ /** @param {string} id @param {[number, number][]} pts @returns {string} */
24
+ const clipPath = (id, pts) => `<clipPath${attrs({ id, clipPathUnits: 'userSpaceOnUse' })}><polygon points="${pts.map(([x, y]) => `${num(x, 3)},${num(y, 3)}`).join(' ')}"/></clipPath>`;
25
+
26
+ /**
27
+ * `mirror`: `keep` one side (`'left'`, `'right'`, `'top'`, `'bottom'`) and
28
+ * reflect it onto the other across a line at `at` (0–1), or a quarter
29
+ * (`'top-left'`, …) onto all four.
30
+ *
31
+ * @param {import('./index.js').Effect & { kind: 'mirror' }} fx
32
+ * @param {{ defs: string, body: string }} m
33
+ * @param {any} ctx
34
+ * @param {string} id
35
+ * @returns {{ defs: string, body: string }}
36
+ */
37
+ export function mirror(fx, m, ctx, id) {
38
+ const keep = fx.keep ?? 'left';
39
+ const KEEP = ['left', 'right', 'top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
40
+ if (!KEEP.includes(keep)) throw new TypeError(`Unknown mirror side "${keep}" (${KEEP.join(', ')})`);
41
+ const { width: W, height: H } = ctx;
42
+ const [fx0, fy0] = fraction(/** @type {any} */ (fx.at), [0.5, 0.5]);
43
+ const ax = (typeof fx.at === 'number' ? fx.at : fx0) * W;
44
+ const ay = (typeof fx.at === 'number' ? fx.at : fy0) * H;
45
+ const P = Math.max(W, H);
46
+ // A hair of overlap past the line hides the anti-aliased seam.
47
+ const e = 0.5 / ((ctx.res ?? 1) || 1);
48
+ const sx = keep.includes('left') ? -1 : keep.includes('right') ? 1 : 0;
49
+ const sy = keep.includes('top') ? -1 : keep.includes('bottom') ? 1 : 0;
50
+ /** @param {number} qx @param {number} qy @returns {[number, number][]} The quadrant (or half) on that side. */
51
+ const region = (qx, qy) => {
52
+ const x0 = qx < 0 ? -P : qx > 0 ? ax - e : -P;
53
+ const x1 = qx < 0 ? ax + e : qx > 0 ? W + P : W + P;
54
+ const y0 = qy < 0 ? -P : qy > 0 ? ay - e : -P;
55
+ const y1 = qy < 0 ? ay + e : qy > 0 ? H + P : H + P;
56
+ return [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
57
+ };
58
+ const src = `${id}s`;
59
+ let defs = `${m.defs}<g id="${src}">${m.body}</g>`;
60
+ let body = '';
61
+ /** @type {[number, number][]} */
62
+ const flips = [[1, 1]];
63
+ if (sx) flips.push([-1, 1]);
64
+ if (sy) flips.push([1, -1]);
65
+ if (sx && sy) flips.push([-1, -1]);
66
+ // Mirrored copies first, the kept side on top.
67
+ flips.reverse().forEach(([fxs, fys], i) => {
68
+ const clip = `${id}c${i}`;
69
+ defs += clipPath(clip, region(sx * fxs, sy * fys));
70
+ const t = fxs === 1 && fys === 1 ? undefined : `translate(${num(fxs === -1 ? 2 * ax : 0, 3)} ${num(fys === -1 ? 2 * ay : 0, 3)}) scale(${fxs} ${fys})`;
71
+ body += copy(src, clip, t);
72
+ });
73
+ return { defs, body };
74
+ }
@@ -0,0 +1,7 @@
1
+ // Tile paints.
2
+ import type { TilePaint } from './paint.js';
3
+
4
+ /** Tile kinds. */
5
+ export const TILES: readonly ('checker' | 'stripes' | 'dots' | 'grid' | 'custom')[];
6
+ /** A tile paint as a `<pattern>` and its fill value. */
7
+ export function tile(v: TilePaint, o: { id: string; box: { x: number; y: number; width: number; height: number }; canvas: { width: number; height: number } }): { defs: string; value: string };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tile paints: a pattern repeated over a shape (checker, stripes, dots,
3
+ * grid), or a custom tile of markup.
4
+ *
5
+ * @module effects/tile
6
+ */
7
+
8
+ import { attrs, num } from '../svg.js';
9
+ import { len } from '../geometry.js';
10
+ import { toHex } from '../color.js';
11
+ import { sanitize } from '../layers/svg.js';
12
+
13
+ /** Tile kinds. */
14
+ export const TILES = Object.freeze(['checker', 'stripes', 'dots', 'grid', 'custom']);
15
+
16
+ /**
17
+ * A tile paint as a `<pattern>`: `{ type: 'tile', kind, size, colors: [ground, ink], ratio, angle, markup }`.
18
+ * `size` is one cell (a length, default `'4vmin'`); `ratio` the stripe,
19
+ * dot or line share of it; `angle` turns the pattern; a `custom` tile draws
20
+ * `markup` (sanitised) in a `size` square (or `[width, height]`). The
21
+ * pattern starts at the shape's top-left corner. `colors[0]` may be
22
+ * `'none'` for a transparent ground.
23
+ *
24
+ * @param {any} v
25
+ * @param {{ id: string, box: { x: number, y: number, width: number, height: number }, canvas: { width: number, height: number } }} o
26
+ * @returns {{ defs: string, value: string }}
27
+ */
28
+ export function tile(v, o) {
29
+ const kind = v.kind ?? 'checker';
30
+ if (!TILES.includes(kind)) throw new TypeError(`Unknown tile kind "${kind}" (${TILES.join(', ')})`);
31
+ const [sw, sh] = Array.isArray(v.size) ? v.size.map((/** @type {any} */ s) => len(s, o.canvas)) : [len(v.size, o.canvas, '4vmin'), len(v.size, o.canvas, '4vmin')];
32
+ const [ground, ink] = [v.colors?.[0] ?? '#ffffff', v.colors?.[1] ?? '#111111'].map((c) => (c === 'none' ? 'none' : toHex(c)));
33
+ const ratio = v.ratio ?? (kind === 'dots' ? 0.6 : kind === 'grid' ? 0.08 : 0.5);
34
+ let w = sw;
35
+ let h = sh;
36
+ let body = ground === 'none' ? '' : `<rect${attrs({ width: num(kind === 'checker' ? 2 * sw : sw, 4), height: num(kind === 'checker' ? 2 * sh : sh, 4), fill: ground })}/>`;
37
+ if (kind === 'checker') {
38
+ w = 2 * sw;
39
+ h = 2 * sh;
40
+ body += `<path${attrs({ d: `M0 0h${num(sw, 4)}v${num(sh, 4)}h${num(-sw, 4)}ZM${num(sw, 4)} ${num(sh, 4)}h${num(sw, 4)}v${num(sh, 4)}h${num(-sw, 4)}Z`, fill: ink })}/>`;
41
+ } else if (kind === 'stripes') {
42
+ body += `<rect${attrs({ width: num(sw * ratio, 4), height: num(sh, 4), fill: ink })}/>`;
43
+ } else if (kind === 'dots') {
44
+ body += `<ellipse${attrs({ cx: num(sw / 2, 4), cy: num(sh / 2, 4), rx: num((sw * ratio) / 2, 4), ry: num((sh * ratio) / 2, 4), fill: ink })}/>`;
45
+ } else if (kind === 'grid') {
46
+ body += `<path${attrs({ d: `M0 0H${num(sw, 4)}M0 0V${num(sh, 4)}`, stroke: ink, 'stroke-width': num(Math.min(sw, sh) * ratio * 2, 4), fill: 'none' })}/>`;
47
+ } else {
48
+ body += sanitize(String(v.markup ?? ''));
49
+ }
50
+ const turn = v.angle ? ` rotate(${num(v.angle, 3)})` : '';
51
+ return {
52
+ defs: `<pattern${attrs({ id: o.id, patternUnits: 'userSpaceOnUse', width: num(w, 4), height: num(h, 4), patternTransform: `translate(${num(o.box.x)} ${num(o.box.y)})${turn}` })}>${body}</pattern>`,
53
+ value: `url(#${o.id})`,
54
+ };
55
+ }
@@ -0,0 +1,16 @@
1
+ // Tone and colour.
2
+ import type { Effect } from './index.js';
3
+ import type { RenderContext } from '../render/context.js';
4
+
5
+ /** The `duotone` effect's filter primitives. */
6
+ export function duotone(fx: Extract<Effect, { kind: 'duotone' }>, ctx: RenderContext): string;
7
+ /** The `grayscale | levels` effect's filter primitives. */
8
+ export function levels(fx: Extract<Effect, { kind: 'grayscale' | 'levels' }>, ctx: RenderContext): string;
9
+ /** The `saturate` effect's filter primitives. */
10
+ export function saturate(fx: Extract<Effect, { kind: 'saturate' }>, ctx: RenderContext): string;
11
+ /** The `matrix` effect's filter primitives. */
12
+ export function matrix(fx: Extract<Effect, { kind: 'matrix' }>, ctx: RenderContext): string;
13
+ /** The `threshold` effect's filter primitives. */
14
+ export function threshold(fx: Extract<Effect, { kind: 'threshold' }>, ctx: RenderContext): string;
15
+ /** The `photocopy` effect's filter primitives. */
16
+ export function photocopy(fx: Extract<Effect, { kind: 'photocopy' }>, ctx: RenderContext): string;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Tone and colour: duotone and tritone ramps, greyscale and levels,
3
+ * saturation, a raw colour matrix, a one-ink threshold, and a photo
4
+ * through a copier.
5
+ *
6
+ * @module effects/tone
7
+ */
8
+
9
+ import { esc, num } from '../svg.js';
10
+ import { LUMA, rgb, mat, flood, seedOf } from './util.js';
11
+
12
+ /**
13
+ * A linear transfer on R, G and B.
14
+ *
15
+ * @param {number} slope @param {number} intercept
16
+ * @returns {string}
17
+ */
18
+ const linear = (slope, intercept) => `<feComponentTransfer>${['R', 'G', 'B'].map((ch) => `<feFunc${ch} type="linear" slope="${num(slope, 3)}" intercept="${num(intercept, 3)}"/>`).join('')}</feComponentTransfer>`;
19
+
20
+ /**
21
+ * `duotone`: greyscale, optional `contrast`, then a tone ramp through two or
22
+ * more `colors` (shadows first).
23
+ *
24
+ * @param {import('./index.js').Effect & { kind: 'duotone' }} fx
25
+ * @returns {string}
26
+ */
27
+ export function duotone(fx) {
28
+ const cols = (fx.colors ?? ['#1b0d06', '#f6e6c4']).map(rgb);
29
+ const pre = fx.contrast != null ? linear(fx.contrast, (1 - fx.contrast) / 2) : '';
30
+ return `<feColorMatrix type="saturate" values="0"/>${pre}<feComponentTransfer>${['R', 'G', 'B'].map((ch, i) => `<feFunc${ch} type="table" tableValues="${cols.map((c) => num(c[i], 3)).join(' ')}"/>`).join('')}</feComponentTransfer>`;
31
+ }
32
+
33
+ /**
34
+ * `grayscale` and `levels`: a linear tone curve (`slope`, `intercept`, or
35
+ * `contrast` and `brightness`), greyscale first for `grayscale`.
36
+ *
37
+ * @param {import('./index.js').Effect & { kind: 'grayscale' | 'levels' }} fx
38
+ * @returns {string}
39
+ */
40
+ export function levels(fx) {
41
+ const gray = fx.kind === 'grayscale';
42
+ if (fx.black != null || fx.white != null || fx.gamma != null || fx.output != null) return inputLevels(fx, gray);
43
+ const slope = fx.slope ?? fx.contrast ?? (gray ? 1 : 1.25);
44
+ const intercept = fx.intercept ?? (fx.brightness != null ? fx.brightness : fx.contrast != null ? (1 - fx.contrast) / 2 : gray ? 0 : -0.1);
45
+ return `${gray ? '<feColorMatrix type="saturate" values="0"/>' : ''}${slope !== 1 || intercept !== 0 ? linear(slope, intercept) : ''}`;
46
+ }
47
+
48
+ /**
49
+ * Photoshop's Levels: input `black` and `white` points (0–1), `gamma`
50
+ * for the midtones (above 1 lightens), and an `output` range.
51
+ *
52
+ * @param {any} fx
53
+ * @param {boolean} gray
54
+ * @returns {string}
55
+ */
56
+ function inputLevels(fx, gray) {
57
+ const b = fx.black ?? 0;
58
+ const w = Math.max(b + 1e-3, fx.white ?? 1);
59
+ const [lo, hi] = fx.output ?? [0, 1];
60
+ const g = Math.max(0.01, fx.gamma ?? 1);
61
+ const funcs = (/** @type {string} */ f) => ['R', 'G', 'B'].map((ch) => `<feFunc${ch} ${f}/>`).join('');
62
+ return `${gray ? '<feColorMatrix type="saturate" values="0"/>' : ''}<feComponentTransfer>${funcs(`type="linear" slope="${num(1 / (w - b), 4)}" intercept="${num(-b / (w - b), 4)}"`)}</feComponentTransfer>`
63
+ + `<feComponentTransfer>${funcs(`type="gamma" amplitude="${num(hi - lo, 4)}" exponent="${num(1 / g, 4)}" offset="${num(lo, 4)}"`)}</feComponentTransfer>`;
64
+ }
65
+
66
+ /**
67
+ * `saturate`.
68
+ *
69
+ * @param {import('./index.js').Effect & { kind: 'saturate' }} fx
70
+ * @returns {string}
71
+ */
72
+ export const saturate = (fx) => `<feColorMatrix type="saturate" values="${num(fx.amount ?? 1.4, 3)}"/>`;
73
+
74
+ /**
75
+ * `matrix`: a raw 4×5 colour matrix.
76
+ *
77
+ * @param {import('./index.js').Effect & { kind: 'matrix' }} fx
78
+ * @returns {string}
79
+ */
80
+ export const matrix = (fx) => `<feColorMatrix type="matrix" values="${esc(Array.isArray(fx.values) ? fx.values.join(' ') : String(fx.values))}"/>`;
81
+
82
+ /**
83
+ * `threshold`: one ink where the image is darker than `level`, on `paper`
84
+ * (or transparent).
85
+ *
86
+ * @param {import('./index.js').Effect & { kind: 'threshold' }} fx
87
+ * @returns {string}
88
+ */
89
+ export function threshold(fx) {
90
+ const ink = rgb(fx.ink ?? '#000000');
91
+ const steps = 64;
92
+ const cut = Math.round((fx.level ?? 0.5) * steps);
93
+ const table = (/** @type {number} */ a, /** @type {number} */ b) => Array.from({ length: steps }, (_, k) => num(k < cut ? a : b, 3)).join(' ');
94
+ if (fx.paper && fx.paper !== 'none') {
95
+ const paper = rgb(fx.paper);
96
+ const lum = `<feColorMatrix type="matrix" values="${mat([...LUMA, 0, 0, ...LUMA, 0, 0, ...LUMA, 0, 0, 0, 0, 0, 1, 0])}"/>`;
97
+ return `${lum}<feComponentTransfer>${['R', 'G', 'B'].map((ch, i) => `<feFunc${ch} type="discrete" tableValues="${table(ink[i], paper[i])}"/>`).join('')}</feComponentTransfer><feComposite in2="SourceAlpha" operator="in"/>`;
98
+ }
99
+ return `<feColorMatrix type="matrix" values="${flood(fx.ink ?? '#000000', [...LUMA, 0, 0])}"/><feComponentTransfer><feFuncA type="discrete" tableValues="${table(1, 0)}"/></feComponentTransfer><feComposite in2="SourceAlpha" operator="in"/>`;
100
+ }
101
+
102
+ /**
103
+ * `photocopy`: a photo through a copier, dark becoming solid `ink` with a
104
+ * noisy hard threshold.
105
+ *
106
+ * @param {import('./index.js').Effect & { kind: 'photocopy' }} fx
107
+ * @param {import('../layers.js').RenderContext} ctx
108
+ * @returns {string}
109
+ */
110
+ export function photocopy(fx, ctx) {
111
+ const gain = fx.contrast ?? 0.5;
112
+ const noise = fx.noise ?? 0.9;
113
+ return `<feColorMatrix in="SourceGraphic" type="matrix" values="${mat([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -gain, -gain, -gain, 0, 0.75 + gain + (fx.lift ?? 0)])}" result="ink"/>`
114
+ + `<feTurbulence type="fractalNoise" baseFrequency="${num(fx.frequency ?? 0.85, 3)}" seed="${seedOf(ctx, fx)}" result="n"/><feColorMatrix in="n" type="matrix" values="${mat([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, noise, -noise / 2])}" result="nn"/>`
115
+ + '<feComposite in="ink" in2="nn" operator="arithmetic" k2="1" k3="1"/><feComponentTransfer><feFuncA type="discrete" tableValues="0 0 1 1"/></feComponentTransfer>'
116
+ + `<feColorMatrix type="matrix" values="${flood(fx.ink ?? '#16040d')}"/><feComposite in2="SourceAlpha" operator="in"/>`;
117
+ }
@@ -0,0 +1,35 @@
1
+ // A layer's `transform` option as an SVG transform.
2
+ import type { Length } from '../index.js';
3
+
4
+ /** A transform any layer takes (about `origin`, default the canvas centre). */
5
+ export interface LayerTransform {
6
+ translate?: [Length, Length] | { x?: Length; y?: Length };
7
+ /** Degrees. */
8
+ rotate?: number;
9
+ scale?: number | [number, number];
10
+ skewX?: number;
11
+ skewY?: number;
12
+ /** `'x'` flips left–right, `'y'` top–bottom. */
13
+ mirror?: 'x' | 'y' | 'both';
14
+ /** `[x, y]` px, or `'center'`. */
15
+ origin?: [Length, Length] | { x?: Length; y?: Length } | 'center';
16
+ }
17
+
18
+
19
+ /** A `transform` option as an SVG transform string (a string is used as written). */
20
+ export function transformAttr(t: LayerTransform | string | undefined, canvas: { width: number; height: number }): string | undefined;
21
+
22
+ /** An affine matrix `[a, b, c, d, e, f]`, as SVG's `matrix()`. */
23
+ export type Affine = [number, number, number, number, number, number];
24
+ /** A transform (string or object form) as one matrix. */
25
+ export function transformMatrix(t: LayerTransform | string | undefined, canvas: { width: number; height: number }): Affine;
26
+ /** How much a matrix enlarges what it draws: smallest and largest stretch, and their geometric mean. */
27
+ export function stretchOf(m: Affine): { min: number; max: number; mean: number };
28
+ /** The inverse of an affine matrix. */
29
+ export function invert(m: Affine): Affine;
30
+ /** A point through an affine matrix. */
31
+ export function apply(m: Affine, x: number, y: number): [number, number];
32
+ /** The identity matrix. */
33
+ export const IDENTITY: Readonly<Affine>;
34
+ /** `m × n`: `n` applied first, in `m`'s space. */
35
+ export function compose(m: Affine, n: Affine): Affine;