@arraypress/covers 1.1.0 → 1.3.1

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 (244) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +18 -809
  3. package/package.json +2 -1
  4. package/recipes/flyers/afterglow-motion.json +540 -0
  5. package/recipes/flyers/afterglow.json +70 -0
  6. package/recipes/flyers/arcfield-mirror-motion.json +397 -0
  7. package/recipes/flyers/arcfield-mirror.json +364 -0
  8. package/recipes/flyers/bauhaus-oktav.json +319 -0
  9. package/recipes/flyers/bauhaus-ritmo.json +735 -0
  10. package/recipes/flyers/bauhaus-volta.json +394 -0
  11. package/recipes/flyers/bloomfall.json +99 -0
  12. package/recipes/flyers/drag-solenne-motion.json +444 -0
  13. package/recipes/flyers/drag-solenne.json +426 -0
  14. package/recipes/flyers/fennick-patches.json +874 -0
  15. package/recipes/flyers/ferro-pass.json +398 -0
  16. package/recipes/flyers/ferrova.json +812 -0
  17. package/recipes/flyers/grid-rest-rhythm.json +125 -0
  18. package/recipes/flyers/groovy-honeycomb.json +816 -0
  19. package/recipes/flyers/groovy-marigold.json +479 -0
  20. package/recipes/flyers/groovy-peachfuzz.json +974 -0
  21. package/recipes/flyers/halden-orbit-motion.json +381 -0
  22. package/recipes/flyers/halden-orbit.json +356 -0
  23. package/recipes/flyers/halvard.json +120 -0
  24. package/recipes/flyers/kairo-weekender.json +696 -0
  25. package/recipes/flyers/linger-motion.json +518 -0
  26. package/recipes/flyers/linger.json +67 -0
  27. package/recipes/flyers/little-orchard.json +105 -0
  28. package/recipes/flyers/lofi-hexmoth.json +912 -0
  29. package/recipes/flyers/lofi-lowbit-motion.json +888 -0
  30. package/recipes/flyers/lofi-lowbit.json +880 -0
  31. package/recipes/flyers/lofi-nullpoint.json +3013 -0
  32. package/recipes/flyers/lumio-stack-motion.json +648 -0
  33. package/recipes/flyers/lumio-stack.json +438 -0
  34. package/recipes/flyers/magazine-cover.json +168 -0
  35. package/recipes/flyers/magazine-extended.json +269 -0
  36. package/recipes/flyers/magazine-knockout.json +40 -0
  37. package/recipes/flyers/northfold-varsity.json +618 -0
  38. package/recipes/flyers/open-studio.json +69 -0
  39. package/recipes/flyers/orrin-merch.json +715 -0
  40. package/recipes/flyers/oxyde.json +113 -0
  41. package/recipes/flyers/quiet-fields.json +610 -0
  42. package/recipes/flyers/scrapbook-halcyon.json +948 -0
  43. package/recipes/flyers/solenne-ticket.json +730 -0
  44. package/recipes/flyers/solvane-motion.json +576 -0
  45. package/recipes/flyers/solvane.json +98 -0
  46. package/recipes/flyers/subduct.json +99 -0
  47. package/recipes/flyers/sunvale-lineup.json +584 -0
  48. package/recipes/flyers/tidewell.json +91 -0
  49. package/recipes/flyers/tour-kairo.json +535 -0
  50. package/recipes/flyers/velora-stickers.json +885 -0
  51. package/recipes/flyers/wheatpaste-black.json +313 -0
  52. package/recipes/flyers/wheatpaste-mark.json +198 -0
  53. package/recipes/flyers/wheatpaste-torn.json +114 -0
  54. package/recipes/flyers/wheatpaste-wall.json +984 -0
  55. package/recipes/flyers/wheatpaste-words.json +233 -0
  56. package/recipes/release/aura.json +137 -0
  57. package/recipes/release/chrome.json +433 -0
  58. package/recipes/release/collage.json +372 -0
  59. package/recipes/release/duotone.json +113 -0
  60. package/recipes/release/film-still.json +102 -0
  61. package/recipes/release/headline.json +70 -0
  62. package/recipes/release/instant.json +170 -0
  63. package/recipes/release/label.json +364 -0
  64. package/recipes/release/liquid-glass.json +301 -0
  65. package/recipes/release/odd-tile-dark.json +688 -0
  66. package/recipes/release/odd-tile-dense.json +1056 -0
  67. package/recipes/release/odd-tile-shift.json +541 -0
  68. package/recipes/release/odd-tile-split.json +571 -0
  69. package/recipes/release/odd-tile.json +633 -0
  70. package/recipes/release/off-edge.json +96 -0
  71. package/recipes/release/plinth.json +794 -0
  72. package/recipes/release/riso.json +108 -0
  73. package/recipes/release/series/fluid-label.json +561 -0
  74. package/recipes/release/series/fluid-pixel.json +334 -0
  75. package/recipes/release/series/hairline.json +305 -0
  76. package/recipes/release/series/halo.json +282 -0
  77. package/recipes/release/series/mark-crop.json +135 -0
  78. package/recipes/release/series/mark-window.json +150 -0
  79. package/recipes/release/series/photo-label.json +475 -0
  80. package/recipes/release/series/smear-field.json +345 -0
  81. package/recipes/release/series/smear.json +202 -0
  82. package/recipes/release/series/spin.json +172 -0
  83. package/recipes/release/series/split-ghost.json +230 -0
  84. package/recipes/release/series/zoom.json +168 -0
  85. package/recipes/release/small-print.json +114 -0
  86. package/recipes/release/stack.json +156 -0
  87. package/recipes/release/swiss.json +117 -0
  88. package/schema/recipe.schema.json +332 -65
  89. package/src/animate/index.d.ts +9 -19
  90. package/src/animate/index.js +6 -111
  91. package/src/animate/keyframes.d.ts +21 -0
  92. package/src/animate/keyframes.js +119 -0
  93. package/src/animate/marquee.d.ts +30 -0
  94. package/src/animate/marquee.js +93 -0
  95. package/src/animate/morph.d.ts +8 -0
  96. package/src/animate/morph.js +99 -0
  97. package/src/animate/motion.d.ts +59 -0
  98. package/src/animate/motion.js +63 -0
  99. package/src/animate/sheet.d.ts +19 -0
  100. package/src/animate/sheet.js +61 -0
  101. package/src/animate/timing.d.ts +28 -0
  102. package/src/animate/timing.js +146 -0
  103. package/src/animate/tracks.d.ts +18 -0
  104. package/src/animate/tracks.js +139 -0
  105. package/src/diagram.js +4 -3
  106. package/src/extract.js +2 -2
  107. package/src/fonts.js +1 -2
  108. package/src/geometry.js +4 -34
  109. package/src/grid.js +1 -1
  110. package/src/handmade.js +9 -7
  111. package/src/icons.js +1 -2
  112. package/src/index.d.ts +39 -48
  113. package/src/index.js +5 -5
  114. package/src/inter-data.js +1 -1
  115. package/src/json-recipe.js +23 -7
  116. package/src/layer-options.js +21 -1
  117. package/src/layers/barcode.js +3 -3
  118. package/src/layers/geometry.js +5 -5
  119. package/src/layers/perforation.js +3 -3
  120. package/src/layers/repeat.js +2 -2
  121. package/src/layers/shape.d.ts +2 -2
  122. package/src/layers/shape.js +5 -5
  123. package/src/layers/soft.js +2 -2
  124. package/src/layers/svg.js +2 -3
  125. package/src/layers/table.js +3 -3
  126. package/src/layers/tear.d.ts +1 -1
  127. package/src/layers/tear.js +4 -4
  128. package/src/layers/texture.js +3 -3
  129. package/src/layers/util.js +15 -2
  130. package/src/layers.js +10 -26
  131. package/src/{effects/paint.js → paint/index.js} +5 -5
  132. package/src/{effects/paint-presets.d.ts → paint/presets.d.ts} +1 -1
  133. package/src/{effects/paint-presets.js → paint/presets.js} +1 -1
  134. package/src/{effects → paint}/tile.d.ts +1 -1
  135. package/src/{effects → paint}/tile.js +4 -4
  136. package/src/palette.js +1 -1
  137. package/src/platform/files.d.ts +6 -0
  138. package/src/platform/files.js +41 -0
  139. package/src/platform/harfbuzz.js +1 -1
  140. package/src/platform/index.d.ts +3 -3
  141. package/src/platform/index.js +6 -4
  142. package/src/platform/load.js +1 -2
  143. package/src/print.js +9 -7
  144. package/src/recipes.js +1 -1
  145. package/src/render/backdrop.js +2 -2
  146. package/src/render/grain.js +1 -1
  147. package/src/render/group.js +3 -3
  148. package/src/render/key.js +1 -2
  149. package/src/{effects → render}/transform.d.ts +2 -1
  150. package/src/{effects → render}/transform.js +17 -5
  151. package/src/render/use.d.ts +1 -1
  152. package/src/render/use.js +2 -2
  153. package/src/render/wrap.js +5 -60
  154. package/src/render.js +55 -18
  155. package/src/text.js +1 -1
  156. package/src/type/auto-color.js +1 -1
  157. package/src/type/glyphs.js +2 -2
  158. package/src/type/line.js +1 -1
  159. package/src/type/live.js +1 -1
  160. package/src/type/path.js +2 -2
  161. package/src/type/pixel.d.ts +0 -2
  162. package/src/type/pixel.js +3 -33
  163. package/src/type/spans.js +2 -2
  164. package/src/type/typeset.js +19 -8
  165. package/src/version.js +1 -1
  166. package/src/color.js +0 -246
  167. package/src/effects/ascii.d.ts +0 -7
  168. package/src/effects/ascii.js +0 -93
  169. package/src/effects/buffer.d.ts +0 -60
  170. package/src/effects/buffer.js +0 -472
  171. package/src/effects/detail.d.ts +0 -12
  172. package/src/effects/detail.js +0 -124
  173. package/src/effects/digital.d.ts +0 -14
  174. package/src/effects/digital.js +0 -216
  175. package/src/effects/distort.d.ts +0 -20
  176. package/src/effects/distort.js +0 -314
  177. package/src/effects/dither.d.ts +0 -8
  178. package/src/effects/dither.js +0 -147
  179. package/src/effects/drop-shadow.d.ts +0 -4
  180. package/src/effects/drop-shadow.js +0 -45
  181. package/src/effects/extrude.d.ts +0 -5
  182. package/src/effects/extrude.js +0 -55
  183. package/src/effects/film.d.ts +0 -11
  184. package/src/effects/film.js +0 -204
  185. package/src/effects/glow.d.ts +0 -8
  186. package/src/effects/glow.js +0 -56
  187. package/src/effects/grade.d.ts +0 -30
  188. package/src/effects/grade.js +0 -274
  189. package/src/effects/halftone.d.ts +0 -8
  190. package/src/effects/halftone.js +0 -95
  191. package/src/effects/index.d.ts +0 -125
  192. package/src/effects/index.js +0 -277
  193. package/src/effects/ink.d.ts +0 -12
  194. package/src/effects/ink.js +0 -70
  195. package/src/effects/lens.d.ts +0 -14
  196. package/src/effects/lens.js +0 -277
  197. package/src/effects/light.d.ts +0 -10
  198. package/src/effects/light.js +0 -59
  199. package/src/effects/looks.d.ts +0 -9
  200. package/src/effects/looks.js +0 -74
  201. package/src/effects/mask.d.ts +0 -52
  202. package/src/effects/mask.js +0 -314
  203. package/src/effects/motion.d.ts +0 -6
  204. package/src/effects/motion.js +0 -60
  205. package/src/effects/newsprint.d.ts +0 -6
  206. package/src/effects/newsprint.js +0 -89
  207. package/src/effects/optics.d.ts +0 -10
  208. package/src/effects/optics.js +0 -213
  209. package/src/effects/options.d.ts +0 -6
  210. package/src/effects/options.js +0 -106
  211. package/src/effects/pixels.d.ts +0 -24
  212. package/src/effects/pixels.js +0 -138
  213. package/src/effects/polar.d.ts +0 -10
  214. package/src/effects/polar.js +0 -265
  215. package/src/effects/region.d.ts +0 -8
  216. package/src/effects/region.js +0 -136
  217. package/src/effects/round-border.d.ts +0 -5
  218. package/src/effects/round-border.js +0 -62
  219. package/src/effects/screen.d.ts +0 -14
  220. package/src/effects/screen.js +0 -212
  221. package/src/effects/sort.d.ts +0 -8
  222. package/src/effects/sort.js +0 -125
  223. package/src/effects/symmetry.d.ts +0 -6
  224. package/src/effects/symmetry.js +0 -74
  225. package/src/effects/tone.d.ts +0 -16
  226. package/src/effects/tone.js +0 -117
  227. package/src/effects/util.d.ts +0 -16
  228. package/src/effects/util.js +0 -85
  229. package/src/platform/bytes.d.ts +0 -22
  230. package/src/platform/bytes.js +0 -103
  231. package/src/platform/compress.d.ts +0 -12
  232. package/src/platform/compress.js +0 -81
  233. package/src/platform/node.d.ts +0 -26
  234. package/src/platform/node.js +0 -86
  235. package/src/png.js +0 -126
  236. package/src/raster/index.d.ts +0 -24
  237. package/src/raster/index.js +0 -171
  238. package/src/render/content.d.ts +0 -5
  239. package/src/render/content.js +0 -58
  240. package/src/render/tiles.d.ts +0 -6
  241. package/src/render/tiles.js +0 -77
  242. package/src/rng.js +0 -95
  243. package/src/svg.js +0 -69
  244. /package/src/{effects/paint.d.ts → paint/index.d.ts} +0 -0
@@ -1,212 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
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;
@@ -1,125 +0,0 @@
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
- }
@@ -1,6 +0,0 @@
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;
@@ -1,74 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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;
@@ -1,117 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
1
- // Helpers for writing filter primitives.
2
-
3
- /** Rec. 709 luma weights. */
4
- export const LUMA: [number, number, number];
5
- /** A colour as 0–1 channels. */
6
- export function rgb(c: string): [number, number, number];
7
- /** A colour-matrix value list from 20 numbers. */
8
- export function mat(m: number[]): string;
9
- /** A matrix flooding RGB with a colour, alpha from a 5-number row (default the source alpha). */
10
- export function flood(color: string, alpha?: number[]): string;
11
- /** The effect's `seed`, else a seeded one. */
12
- export function seedOf(ctx: { rng: { int: (a: number, b: number) => number } }, fx: { seed?: number }): number;
13
- /** A `<filter>` over the canvas, padded by `pad` (a fraction) on each side, in user space. */
14
- export function filterElement(id: string, ctx: { width: number; height: number }, pad: number, body: string, colorSpace?: 'sRGB' | 'linearRGB', region?: { x: number; y: number; width: number; height: number }): string;
15
- /** Turbulence fed to a displacement map. */
16
- export function displace(frequency: number | number[], octaves: number, seed: number, scale: number, result?: string): string;
@@ -1,85 +0,0 @@
1
- /**
2
- * Helpers for writing filter primitives.
3
- *
4
- * @module effects/util
5
- */
6
-
7
- import { attrs, num } from '../svg.js';
8
- import { toHex } from '../color.js';
9
-
10
- /** Rec. 709 luma weights. */
11
- export const LUMA = [0.2126, 0.7152, 0.0722];
12
-
13
- /**
14
- * A colour as 0–1 channels.
15
- *
16
- * @param {string} c
17
- * @returns {[number, number, number]}
18
- */
19
- export function rgb(c) {
20
- const h = toHex(c);
21
- return /** @type {[number, number, number]} */ ([1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255));
22
- }
23
-
24
- /**
25
- * A colour-matrix value list.
26
- *
27
- * @param {number[]} m - 20 numbers.
28
- * @returns {string}
29
- */
30
- export const mat = (m) => m.map((x) => num(x, 4)).join(' ');
31
-
32
- /**
33
- * A matrix that floods RGB with a colour and takes alpha from `alpha` (a
34
- * 5-number row; default the source alpha).
35
- *
36
- * @param {string} color
37
- * @param {number[]} [alpha]
38
- * @returns {string}
39
- */
40
- export function flood(color, alpha = [0, 0, 0, 1, 0]) {
41
- const [r, g, b] = rgb(color);
42
- return mat([0, 0, 0, 0, r, 0, 0, 0, 0, g, 0, 0, 0, 0, b, ...alpha]);
43
- }
44
-
45
- /**
46
- * A layer's seeded turbulence seed (or the effect's own `seed`).
47
- *
48
- * @param {{ rng: { int: (a: number, b: number) => number } }} ctx
49
- * @param {{ seed?: number }} fx
50
- * @returns {number}
51
- */
52
- export const seedOf = (ctx, fx) => fx.seed ?? ctx.rng.int(1, 9999);
53
-
54
- /**
55
- * A `<filter>` over the canvas, padded, in user space.
56
- *
57
- * @param {string} id
58
- * @param {{ width: number, height: number }} ctx
59
- * @param {number} pad - Fraction of the canvas on each side.
60
- * @param {string} body - Primitives.
61
- * @param {'sRGB' | 'linearRGB'} [colorSpace='sRGB'] - Where the primitives do their arithmetic.
62
- * @param {{ x: number, y: number, width: number, height: number }} [region] - A smaller region (the content's box plus padding).
63
- * @returns {string}
64
- */
65
- export function filterElement(id, ctx, pad, body, colorSpace = 'sRGB', region) {
66
- const px = ctx.width * pad;
67
- const py = ctx.height * pad;
68
- if (colorSpace !== 'sRGB' && colorSpace !== 'linearRGB') throw new TypeError(`Unknown colorSpace "${colorSpace}" (sRGB, linearRGB)`);
69
- const r = region ?? { x: -px, y: -py, width: ctx.width + 2 * px, height: ctx.height + 2 * py };
70
- return `<filter${attrs({ id, filterUnits: 'userSpaceOnUse', primitiveUnits: 'userSpaceOnUse', ...r, 'color-interpolation-filters': colorSpace })}>${body}</filter>`;
71
- }
72
-
73
- /**
74
- * Noise turned into displacement: the shared start of rough, warp and wear.
75
- *
76
- * @param {number | number[]} frequency
77
- * @param {number} octaves
78
- * @param {number} seed
79
- * @param {number} scale - Displacement, px.
80
- * @param {string} [result]
81
- * @returns {string}
82
- */
83
- export function displace(frequency, octaves, seed, scale, result) {
84
- return `<feTurbulence type="fractalNoise" baseFrequency="${[].concat(/** @type {any} */ (frequency)).map((f) => num(f, 4)).join(' ')}" numOctaves="${octaves}" seed="${seed}" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="${num(scale)}" xChannelSelector="R" yChannelSelector="G"${result ? ` result="${result}"` : ''}/>`;
85
- }