@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,138 @@
1
+ /**
2
+ * Effects computed on pixels, where SVG filters can't do them (spin and
3
+ * zoom blur, bokeh, distortions, pixel sorting, ASCII…): the layer is
4
+ * drawn once at the pixels it's shown at (sharp in Node, tiled past a few
5
+ * megapixels; a canvas in a page), processed here on a typed array, mixed
6
+ * with the original through the effect's `mask`, and embedded back as one
7
+ * image (JPEG where it's opaque, PNG where it isn't or where flat colour
8
+ * matters), cropped to what it paints. Where nothing can draw pixels (a
9
+ * Worker) the caller falls back to the effect's filter, if it has one.
10
+ *
11
+ * @module effects/pixels
12
+ */
13
+
14
+ import { attrs, num } from '../svg.js';
15
+ import { pixelsOf, encodePixels } from '../raster/index.js';
16
+ import { svgPixels } from '../render/tiles.js';
17
+ import { toBase64 } from '../platform/bytes.js';
18
+ import { nearest } from './buffer.js';
19
+ import { maskGrid, lerpPixels } from './mask.js';
20
+
21
+ /** The most pixels an effect works on (about 3550²); past that it works smaller and the image scales up. */
22
+ export const MAX_PIXELS = 12.6e6;
23
+
24
+ /**
25
+ * What a pixel effect returns: the processed pixels, at the layer's
26
+ * raster size or (with `pixelated`) a grid of cells scaled up crisp.
27
+ *
28
+ * @typedef {object} PixelResult
29
+ * @property {Uint8ClampedArray} data
30
+ * @property {number} width
31
+ * @property {number} height
32
+ * @property {boolean} [lossless] - Embed as PNG even when opaque (flat colour).
33
+ * @property {boolean} [pixelated] - Smaller than the layer: scaled up nearest-neighbour.
34
+ */
35
+
36
+ /**
37
+ * A layer's markup as pixels, `w`×`h` over its canvas.
38
+ *
39
+ * @param {{ defs: string, body: string }} m
40
+ * @param {any} ctx
41
+ * @param {number} w
42
+ * @param {number} h
43
+ * @returns {Promise<Uint8ClampedArray | undefined>} RGBA, or undefined where nothing can draw.
44
+ */
45
+ export async function drawLayer(m, ctx, w, h) {
46
+ const sx = w / ctx.width;
47
+ const sy = h / ctx.height;
48
+ const doc = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${m.defs ? `<defs>${m.defs}</defs>` : ''}<g transform="scale(${num(sx, 6)} ${num(sy, 6)})">${m.body}</g></svg>`;
49
+ try {
50
+ const out = ctx.sharp ? await svgPixels(ctx.sharp, doc, w, h) : await pixelsOf(doc, w, h);
51
+ return out ? new Uint8ClampedArray(out.buffer, out.byteOffset, out.byteLength) : undefined;
52
+ } catch {
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The raster size for a layer: its pixels as shown (`ctx.res` per unit),
59
+ * at most {@link MAX_PIXELS} (or the effect's own cap).
60
+ *
61
+ * @param {any} ctx
62
+ * @param {number} [cap=MAX_PIXELS]
63
+ * @returns {{ w: number, h: number, scale: number }}
64
+ */
65
+ export function rasterSize(ctx, cap = MAX_PIXELS) {
66
+ const res = Math.min(ctx.res ?? 1, Math.sqrt(cap / (ctx.width * ctx.height)));
67
+ const w = Math.max(1, Math.round(ctx.width * res));
68
+ const h = Math.max(1, Math.round(ctx.height * res));
69
+ return { w, h, scale: Math.sqrt((w / ctx.width) * (h / ctx.height)) };
70
+ }
71
+
72
+ /**
73
+ * Run a pixel effect on a layer's markup.
74
+ *
75
+ * @param {any} fx - The effect (its `mask` is applied here).
76
+ * @param {{ defs: string, body: string }} m - The layer so far.
77
+ * @param {any} ctx
78
+ * @param {(img: import('./buffer.js').Pixels) => PixelResult | Uint8ClampedArray | Promise<PixelResult | Uint8ClampedArray>} run
79
+ * @param {{ cap?: number }} [o]
80
+ * @returns {Promise<{ defs: string, body: string } | undefined>} Undefined where nothing can draw pixels.
81
+ */
82
+ export async function pixelEffect(fx, m, ctx, run, o = {}) {
83
+ const { w, h, scale } = rasterSize(ctx, o.cap);
84
+ const data = await drawLayer(m, ctx, w, h);
85
+ if (!data) return undefined;
86
+ let opaque = true;
87
+ for (let i = 3; i < data.length; i += 4) if (data[i] !== 255) { opaque = false; break; }
88
+ const img = { data, width: w, height: h, scale, opaque };
89
+ const got = await run(img);
90
+ /** @type {PixelResult} */
91
+ let res = got instanceof Uint8ClampedArray ? { data: got, width: w, height: h } : got;
92
+ if (fx.mask) {
93
+ const grid = await maskGrid(fx.mask, ctx, { x: 0, y: 0, width: ctx.width, height: ctx.height }, w, h);
94
+ if (grid) {
95
+ const full = res.width === w && res.height === h ? res.data : nearest(res.data, res.width, res.height, w, h);
96
+ res = { data: lerpPixels(data, full, grid), width: w, height: h, lossless: res.lossless };
97
+ }
98
+ }
99
+ return embed(res, ctx);
100
+ }
101
+
102
+ /**
103
+ * Pixels as an `<image>` over the layer's canvas, cropped to what's painted.
104
+ *
105
+ * @param {PixelResult} res
106
+ * @param {any} ctx
107
+ * @returns {Promise<{ defs: string, body: string }>}
108
+ */
109
+ async function embed(res, ctx) {
110
+ const { data, width: w, height: h } = res;
111
+ let x0 = w; let y0 = h; let x1 = -1; let y1 = -1;
112
+ let opaque = true;
113
+ for (let y = 0; y < h; y++) {
114
+ for (let x = 0; x < w; x++) {
115
+ const a = data[(y * w + x) * 4 + 3];
116
+ if (a !== 255) opaque = false;
117
+ if (!a) continue;
118
+ if (x < x0) x0 = x;
119
+ if (x > x1) x1 = x;
120
+ if (y < y0) y0 = y;
121
+ if (y > y1) y1 = y;
122
+ }
123
+ }
124
+ if (x1 < 0) return { defs: '', body: '' };
125
+ let crop = data;
126
+ const cw = x1 - x0 + 1;
127
+ const ch = y1 - y0 + 1;
128
+ if (cw !== w || ch !== h) {
129
+ crop = new Uint8ClampedArray(cw * ch * 4);
130
+ for (let y = 0; y < ch; y++) crop.set(data.subarray(((y0 + y) * w + x0) * 4, ((y0 + y) * w + x1 + 1) * 4), y * cw * 4);
131
+ }
132
+ const { mime, bytes } = await encodePixels(crop, cw, ch, { opaque, lossless: res.lossless || res.pixelated }, ctx.sharp);
133
+ const kx = ctx.width / w;
134
+ const ky = ctx.height / h;
135
+ const crisp = res.pixelated ? { 'image-rendering': 'pixelated', style: 'image-rendering:pixelated' } : {};
136
+ const image = `<image${attrs({ x: num(x0 * kx, 4), y: num(y0 * ky, 4), width: num(cw * kx, 4), height: num(ch * ky, 4), preserveAspectRatio: 'none', ...crisp, href: `data:${mime};base64,${toBase64(bytes)}` })}/>`;
137
+ return { defs: '', body: image };
138
+ }
@@ -0,0 +1,10 @@
1
+ // Blurs about a centre, on pixels.
2
+ import type { Effect } from './index.js';
3
+ import type { Pixels } from './buffer.js';
4
+
5
+ /** `spin`: a rotational blur. */
6
+ export function spin(img: Pixels, fx: Extract<Effect, { kind: 'spin' }>): Uint8ClampedArray;
7
+ /** `zoom`: a radial blur toward the centre. */
8
+ export function zoom(img: Pixels, fx: Extract<Effect, { kind: 'zoom' }>): Uint8ClampedArray;
9
+ /** `rays`: god rays from a light. */
10
+ export function rays(img: Pixels, fx: Extract<Effect, { kind: 'rays' }>): Uint8ClampedArray;
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Blurs about a centre, on pixels: `zoom` (radial, every point smeared
3
+ * toward the centre), `spin` (rotational, along circles round it) and
4
+ * `rays` (god rays: the highlights streaked away from a light).
5
+ *
6
+ * The layer is resampled into polar coordinates (one row per radius or
7
+ * per angle, a pixel apart at the rim), blurred along one axis with
8
+ * running sums (O(1) a sample, whatever the length), and resampled back.
9
+ * The centre pixel never moves.
10
+ *
11
+ * @module effects/polar
12
+ */
13
+
14
+ import { premultiplied, unpremultiply, bilinear, pointPx, luma, smoothstep, toSrgb, TO_LINEAR, shrink, enlarge } from './buffer.js';
15
+ import { rgb } from './util.js';
16
+
17
+ /** The most polar samples (angles × radii) a blur works on. */
18
+ const BUDGET = 30e6;
19
+
20
+ /**
21
+ * A polar grid round a centre, sized to the farthest corner: a sample a
22
+ * pixel apart along each ray, and round the rim a pixel apart (two, when
23
+ * `blur` px of smear there hides it).
24
+ *
25
+ * @param {number} w @param {number} h @param {number} cx @param {number} cy
26
+ * @param {(rmax: number) => number} [blur] - How long the blur is at the rim, px.
27
+ * @returns {{ nr: number, nt: number }}
28
+ */
29
+ function gridFor(w, h, cx, cy, blur) {
30
+ const rmax = Math.max(Math.hypot(cx, cy), Math.hypot(w - 1 - cx, cy), Math.hypot(cx, h - 1 - cy), Math.hypot(w - 1 - cx, h - 1 - cy)) + 2;
31
+ const nr = Math.ceil(rmax) + 1;
32
+ const step = blur && blur(rmax) >= 32 ? 2 : 1;
33
+ const nt = Math.max(16, Math.min(Math.ceil((2 * Math.PI * rmax) / step), Math.floor(BUDGET / nr)));
34
+ return { nr, nt };
35
+ }
36
+
37
+ /**
38
+ * Resample into polar coordinates: row-major by angle (`byAngle`, each row
39
+ * a ray from the centre) or by radius (each row a circle).
40
+ *
41
+ * @param {Uint8ClampedArray} src - Premultiplied RGBA.
42
+ * @param {number} w @param {number} h @param {number} cx @param {number} cy
43
+ * @param {number} nt @param {number} nr
44
+ * @param {boolean} byAngle
45
+ * @returns {Uint8ClampedArray}
46
+ */
47
+ function toPolar(src, w, h, cx, cy, nt, nr, byAngle) {
48
+ const out = new Uint8ClampedArray(nt * nr * 4);
49
+ for (let j = 0; j < nt; j++) {
50
+ const t = (2 * Math.PI * j) / nt;
51
+ const c = Math.cos(t);
52
+ const s = Math.sin(t);
53
+ for (let i = 0; i < nr; i++) bilinear(src, w, h, cx + i * c, cy + i * s, out, (byAngle ? j * nr + i : i * nt + j) * 4);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * Resample polar samples back onto the layer's pixels.
60
+ *
61
+ * @param {Uint8ClampedArray} p
62
+ * @param {number} w @param {number} h @param {number} cx @param {number} cy
63
+ * @param {number} nt @param {number} nr
64
+ * @param {boolean} byAngle
65
+ * @returns {Uint8ClampedArray}
66
+ */
67
+ function fromPolar(p, w, h, cx, cy, nt, nr, byAngle) {
68
+ const out = new Uint8ClampedArray(w * h * 4);
69
+ const kt = nt / (2 * Math.PI);
70
+ for (let y = 0, o = 0; y < h; y++) {
71
+ const dy = y - cy;
72
+ for (let x = 0; x < w; x++, o += 4) {
73
+ const dx = x - cx;
74
+ let r = Math.sqrt(dx * dx + dy * dy);
75
+ let t = Math.atan2(dy, dx);
76
+ if (t < 0) t += 2 * Math.PI;
77
+ if (r > nr - 1) r = nr - 1;
78
+ const ft = t * kt;
79
+ let j0 = ft | 0;
80
+ const wt = ft - j0;
81
+ if (j0 >= nt) j0 -= nt;
82
+ const j1 = j0 + 1 === nt ? 0 : j0 + 1;
83
+ const i0 = r | 0;
84
+ const wr = r - i0;
85
+ const i1 = i0 + 1 < nr ? i0 + 1 : i0;
86
+ const a = (byAngle ? j0 * nr + i0 : i0 * nt + j0) * 4;
87
+ const b = (byAngle ? j0 * nr + i1 : i1 * nt + j0) * 4;
88
+ const c = (byAngle ? j1 * nr + i0 : i0 * nt + j1) * 4;
89
+ const d = (byAngle ? j1 * nr + i1 : i1 * nt + j1) * 4;
90
+ const w00 = (1 - wr) * (1 - wt);
91
+ const w10 = wr * (1 - wt);
92
+ const w01 = (1 - wr) * wt;
93
+ const w11 = wr * wt;
94
+ for (let k = 0; k < 4; k++) out[o + k] = p[a + k] * w00 + p[b + k] * w10 + p[c + k] * w01 + p[d + k] * w11;
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /** The centre in pixel coordinates (pixel x's centre is x). @param {any} fx @param {import('./buffer.js').Pixels} img @returns {[number, number]} */
101
+ function centreOf(fx, img) {
102
+ const [x, y] = pointPx(fx.center, img);
103
+ return [x - 0.5, y - 0.5];
104
+ }
105
+
106
+ /**
107
+ * `spin`: a rotational blur, `angle` degrees along circles round `center`
108
+ * (a camera turned during the exposure).
109
+ *
110
+ * @param {import('./buffer.js').Pixels} img
111
+ * @param {import('./index.js').Effect & { kind: 'spin' }} fx
112
+ * @returns {Uint8ClampedArray}
113
+ */
114
+ export function spin(img, fx) {
115
+ const { width: w, height: h } = img;
116
+ const [cx, cy] = centreOf(fx, img);
117
+ const turn = Math.min(360, Math.abs(fx.angle ?? 12));
118
+ const { nr, nt } = gridFor(w, h, cx, cy, (r) => (turn / 180) * Math.PI * r);
119
+ const span = Math.round((turn / 360) * nt);
120
+ if (span < 2) return img.data;
121
+ const p = toPolar(premultiplied(img), w, h, cx, cy, nt, nr, false);
122
+ // Each row is a circle: a circular box of `span` samples, centred.
123
+ const row = new Uint8ClampedArray(nt * 4);
124
+ const acc = new Float64Array(4);
125
+ const half = span >> 1;
126
+ const inv = 1 / (span + 1);
127
+ for (let i = 0; i < nr; i++) {
128
+ const base = i * nt * 4;
129
+ row.set(p.subarray(base, base + nt * 4));
130
+ acc.fill(0);
131
+ for (let k = -half; k <= span - half; k++) {
132
+ const j = ((k % nt) + nt) % nt;
133
+ for (let c = 0; c < 4; c++) acc[c] += row[j * 4 + c];
134
+ }
135
+ for (let j = 0; j < nt; j++) {
136
+ const o = base + j * 4;
137
+ for (let c = 0; c < 4; c++) p[o + c] = acc[c] * inv;
138
+ let add = j + span - half + 1;
139
+ if (add >= nt) add -= nt;
140
+ let sub = j - half;
141
+ if (sub < 0) sub += nt;
142
+ for (let c = 0; c < 4; c++) acc[c] += row[add * 4 + c] - row[sub * 4 + c];
143
+ }
144
+ }
145
+ const out = fromPolar(p, w, h, cx, cy, nt, nr, false);
146
+ return img.opaque ? out : unpremultiply(out);
147
+ }
148
+
149
+ /**
150
+ * `zoom`: a radial blur, each point averaged with what lies between it and
151
+ * `center` over `amount` of the way (a zoom during the exposure).
152
+ *
153
+ * @param {import('./buffer.js').Pixels} img
154
+ * @param {import('./index.js').Effect & { kind: 'zoom' }} fx
155
+ * @returns {Uint8ClampedArray}
156
+ */
157
+ export function zoom(img, fx) {
158
+ const { width: w, height: h } = img;
159
+ const [cx, cy] = centreOf(fx, img);
160
+ const amount = Math.min(0.95, Math.max(0, fx.amount ?? 0.2));
161
+ const { nr, nt } = gridFor(w, h, cx, cy, (r) => amount * r);
162
+ if (!amount) return img.data;
163
+ const p = toPolar(premultiplied(img), w, h, cx, cy, nt, nr, true);
164
+ const sum = new Float64Array((nr + 1) * 4);
165
+ for (let j = 0; j < nt; j++) {
166
+ const base = j * nr * 4;
167
+ for (let i = 0; i < nr; i++) for (let c = 0; c < 4; c++) sum[(i + 1) * 4 + c] = sum[i * 4 + c] + p[base + i * 4 + c];
168
+ for (let i = 1; i < nr; i++) {
169
+ const from = Math.floor(i * (1 - amount));
170
+ const k = 1 / (i + 1 - from);
171
+ for (let c = 0; c < 4; c++) p[base + i * 4 + c] = (sum[(i + 1) * 4 + c] - sum[from * 4 + c]) * k;
172
+ }
173
+ }
174
+ const out = fromPolar(p, w, h, cx, cy, nt, nr, true);
175
+ return img.opaque ? out : unpremultiply(out);
176
+ }
177
+
178
+ /**
179
+ * Where the light is, when `rays` isn't given a `center`: the brightest
180
+ * patch, from a coarse grid.
181
+ *
182
+ * @param {import('./buffer.js').Pixels} img
183
+ * @returns {[number, number]} Pixel coordinates.
184
+ */
185
+ function brightest(img) {
186
+ const { data, width: w, height: h } = img;
187
+ const g = 48;
188
+ const cw = w / g;
189
+ const ch = h / g;
190
+ let best = -1;
191
+ let at = [w / 2, h / 2];
192
+ for (let gy = 0; gy < g; gy++) {
193
+ for (let gx = 0; gx < g; gx++) {
194
+ let s = 0;
195
+ let n = 0;
196
+ for (let y = Math.floor(gy * ch); y < Math.floor((gy + 1) * ch); y += 2) {
197
+ for (let x = Math.floor(gx * cw); x < Math.floor((gx + 1) * cw); x += 2) { const i = (y * w + x) * 4; s += luma(data, i) * data[i + 3]; n++; }
198
+ }
199
+ if (n && s / n > best) { best = s / n; at = [(gx + 0.5) * cw, (gy + 0.5) * ch]; }
200
+ }
201
+ }
202
+ return /** @type {[number, number]} */ (at);
203
+ }
204
+
205
+ /**
206
+ * `rays`: god rays. The highlights above `threshold` are streaked away
207
+ * from the light (`center`, default the brightest patch): each point gets
208
+ * the light between it and the source, so anything in the way casts a
209
+ * shaft, fading over `length` (a fraction of the frame), tinted with
210
+ * `color` and added on top (`intensity`).
211
+ *
212
+ * @param {import('./buffer.js').Pixels} img
213
+ * @param {import('./index.js').Effect & { kind: 'rays' }} fx
214
+ * @returns {Uint8ClampedArray}
215
+ */
216
+ export function rays(img, fx) {
217
+ const { data, width: w, height: h } = img;
218
+ const [cx0, cy0] = fx.center ? centreOf(fx, img) : brightest(img);
219
+ const t = fx.threshold ?? 0.8;
220
+ const tint = rgb(fx.color ?? '#fff3d6');
221
+ // Bright pass, premultiplied.
222
+ const bright = new Uint8ClampedArray(w * h * 4);
223
+ for (let i = 0; i < data.length; i += 4) {
224
+ const k = smoothstep(t, 1, luma(data, i) / 255) * (data[i + 3] / 255);
225
+ if (!k) continue;
226
+ bright[i] = data[i] * k * tint[0];
227
+ bright[i + 1] = data[i + 1] * k * tint[1];
228
+ bright[i + 2] = data[i + 2] * k * tint[2];
229
+ bright[i + 3] = 255 * k;
230
+ }
231
+ // Rays are soft: past a couple of megapixels they're worked at half size.
232
+ const f = w * h > 2.5e6 ? 2 : 1;
233
+ const small = f > 1 ? shrink(bright, w, h, f) : { data: bright, width: w, height: h };
234
+ const sw = small.width;
235
+ const sh = small.height;
236
+ const cx = (cx0 + 0.5) / f - 0.5;
237
+ const cy = (cy0 + 0.5) / f - 0.5;
238
+ const { nr, nt } = gridFor(sw, sh, cx, cy);
239
+ const p = toPolar(small.data, sw, sh, cx, cy, nt, nr, true);
240
+ // Each point gets the mean of the light between it and the source (an
241
+ // occluder on the way leaves a shadow shaft), fading with distance.
242
+ const reach = Math.max(1, (fx.length ?? 0.5) * nr);
243
+ const acc = new Float64Array(4);
244
+ for (let j = 0; j < nt; j++) {
245
+ const base = j * nr * 4;
246
+ acc.fill(0);
247
+ for (let i = 0; i < nr; i++) {
248
+ const fade = Math.exp(-i / reach) / (i + 1);
249
+ for (let c = 0; c < 4; c++) {
250
+ acc[c] += p[base + i * 4 + c];
251
+ p[base + i * 4 + c] = Math.min(255, acc[c] * fade * 1.6);
252
+ }
253
+ }
254
+ }
255
+ const back = fromPolar(p, sw, sh, cx, cy, nt, nr, true);
256
+ const glow = f > 1 ? enlarge(back, sw, sh, w, h) : back;
257
+ // Added in linear light, so the rays brighten like light does.
258
+ const k = fx.intensity ?? 1;
259
+ const out = new Uint8ClampedArray(data.length);
260
+ for (let i = 0; i < data.length; i += 4) {
261
+ out[i + 3] = Math.max(data[i + 3], glow[i + 3]);
262
+ for (let c = 0; c < 3; c++) out[i + c] = toSrgb(TO_LINEAR[data[i + c]] * (data[i + 3] / Math.max(1, out[i + 3])) + TO_LINEAR[glow[i + c]] * k);
263
+ }
264
+ return out;
265
+ }
@@ -0,0 +1,8 @@
1
+ // Making filters cheap for librsvg without changing what they draw.
2
+
3
+ /** Split each `feMorphology` into an x pass and a y pass (identical output, O(r) not O(r²)). */
4
+ export function separableMorphology(markup: string): string;
5
+ /** Whether a chain of primitives only paints where its source does. */
6
+ export function bounded(markup: string): boolean;
7
+ /** How far filter primitives reach from what they read, in their units (an overestimate). */
8
+ export function filterReach(markup: string): number;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Making filters cheap for librsvg (`renderImage`), without changing what
3
+ * they draw:
4
+ *
5
+ * - **Separable morphology.** `feMorphology` is a max/min over a
6
+ * rectangle, which is the same as a pass along x then one along y;
7
+ * librsvg does the full rectangle per pixel (O(r²)), the two passes are
8
+ * O(r). Identical output.
9
+ * - **Content-sized regions.** A filter's region defaults to the canvas
10
+ * (padded); librsvg works every primitive over all of it. When the
11
+ * chain's result can only paint where its source does (worked out from
12
+ * the primitives: floods, turbulence, images and lighting paint
13
+ * everywhere; `in` composites, blurs and offsets stay with their input),
14
+ * the region shrinks to the layer's painted box plus the same padding.
15
+ *
16
+ * @module effects/region
17
+ */
18
+
19
+ import { attrs } from '../svg.js';
20
+
21
+ /**
22
+ * Split each `feMorphology` into an x pass and a y pass.
23
+ *
24
+ * @param {string} markup
25
+ * @returns {string}
26
+ */
27
+ export function separableMorphology(markup) {
28
+ return markup.replace(/<feMorphology\b([^>]*?)\/>/g, (whole, rest) => {
29
+ const r = /\sradius\s*=\s*"([^"]*)"/.exec(rest);
30
+ if (!r) return whole;
31
+ const [rx, ry = rx] = r[1].trim().split(/[\s,]+/).map(Number);
32
+ if (!(rx > 0 && ry > 0)) return whole;
33
+ const result = /\sresult\s*=\s*"([^"]*)"/.exec(rest)?.[1];
34
+ const base = rest.replace(/\s(radius|result)\s*=\s*"[^"]*"/g, '');
35
+ const mid = `${result ?? 'm'}${Math.round(rx * 1000)}x`;
36
+ const second = base.replace(/\sin\s*=\s*"[^"]*"/, '');
37
+ return `<feMorphology${base}${attrs({ radius: `${rx} 0`, result: mid })}/><feMorphology${second}${attrs({ in: mid, radius: `0 ${ry}`, result })}/>`;
38
+ });
39
+ }
40
+
41
+ /** @param {string} svg @param {RegExp} re @returns {number} The largest number a pattern's first group holds. */
42
+ function most(svg, re) {
43
+ let m = 0;
44
+ for (const x of svg.matchAll(re)) for (const v of x[1].trim().split(/[\s,]+/)) m = Math.max(m, Math.abs(Number(v)) || 0);
45
+ return m;
46
+ }
47
+
48
+ /**
49
+ * How far filter primitives reach from what they read, in their units:
50
+ * blur (3σ), displacement (half its scale), morphology radius and offsets,
51
+ * added up (an overestimate).
52
+ *
53
+ * @param {string} markup
54
+ * @returns {number}
55
+ */
56
+ export function filterReach(markup) {
57
+ return 3 * most(markup, /stdDeviation="([^"]*)"/g)
58
+ + most(markup, /<feDisplacementMap\b[^>]*\sscale="([^"]*)"/g) / 2
59
+ + most(markup, /<feMorphology\b[^>]*\sradius="([^"]*)"/g)
60
+ + most(markup, /<(?:feOffset|feDropShadow)\b[^>]*\sd[xy]="([^"]*)"/g);
61
+ }
62
+
63
+ /** @param {string} s @returns {Record<string, string>} */
64
+ const attributesOf = (s) => Object.fromEntries([...s.matchAll(/([\w:-]+)\s*=\s*"([^"]*)"/g)].map((m) => [m[1], m[2]]));
65
+
66
+ /**
67
+ * Whether a chain of primitives only paints where its source does (so its
68
+ * region can shrink to the source's box plus padding).
69
+ *
70
+ * @param {string} markup - Filter primitives.
71
+ * @returns {boolean}
72
+ */
73
+ export function bounded(markup) {
74
+ /** @type {Map<string, boolean>} */
75
+ const results = new Map([['SourceGraphic', true], ['SourceAlpha', true]]);
76
+ let last = true;
77
+ const input = (/** @type {string | undefined} */ name) => (name == null ? last : results.get(name) ?? false);
78
+ for (const m of markup.matchAll(/<(fe\w+)\b([^>]*?)(\/>|>([\s\S]*?)<\/\1>)/g)) {
79
+ const [, tag, rest, , inner = ''] = m;
80
+ const a = attributesOf(rest);
81
+ let b;
82
+ switch (tag) {
83
+ case 'feFlood': case 'feTurbulence': case 'feImage': case 'feDiffuseLighting': case 'feSpecularLighting': case 'feTile':
84
+ b = false; break;
85
+ case 'feComposite': {
86
+ const i1 = input(a.in);
87
+ const i2 = input(a.in2);
88
+ const op = a.operator ?? 'over';
89
+ if (op === 'in') b = i1 || i2;
90
+ else if (op === 'atop') b = i2;
91
+ else if (op === 'out') b = i1;
92
+ else if (op === 'arithmetic') {
93
+ const k = (/** @type {string} */ n) => Number(a[n] ?? 0);
94
+ b = k('k4') === 0 && (k('k2') === 0 || i1) && (k('k3') === 0 || i2) && (k('k1') === 0 || i1 || i2);
95
+ } else b = i1 && i2;
96
+ break;
97
+ }
98
+ case 'feMerge':
99
+ b = [...inner.matchAll(/<feMergeNode\b([^>]*)\/?>/g)].every((n) => input(attributesOf(n[1]).in));
100
+ break;
101
+ case 'feBlend':
102
+ b = input(a.in) && input(a.in2);
103
+ break;
104
+ case 'feColorMatrix': {
105
+ const v = (a.values ?? '').trim().split(/[\s,]+/).map(Number);
106
+ // A matrix whose alpha row adds a constant can paint where the source is clear.
107
+ b = input(a.in) && !((a.type ?? 'matrix') === 'matrix' && v.length === 20 && v[19] > 0);
108
+ break;
109
+ }
110
+ case 'feComponentTransfer': {
111
+ const fa = /<feFuncA\b([^>]*)\/?>/.exec(inner);
112
+ b = input(a.in) && (!fa || zeroStays(attributesOf(fa[1])));
113
+ break;
114
+ }
115
+ case 'feDisplacementMap':
116
+ b = input(a.in);
117
+ break;
118
+ default:
119
+ // Blur, offset, morphology, convolve: they spread their input (the padding covers it).
120
+ b = input(a.in);
121
+ }
122
+ last = b;
123
+ if (a.result) results.set(a.result, b);
124
+ }
125
+ return last;
126
+ }
127
+
128
+ /** @param {Record<string, string>} f - A feFuncA's attributes. @returns {boolean} Whether alpha 0 stays 0. */
129
+ function zeroStays(f) {
130
+ const type = f.type ?? 'identity';
131
+ if (type === 'identity') return true;
132
+ if (type === 'linear') return Number(f.intercept ?? 0) <= 0;
133
+ if (type === 'gamma') return Number(f.offset ?? 0) <= 0;
134
+ const first = Number((f.tableValues ?? '0').trim().split(/[\s,]+/)[0]);
135
+ return first <= 0;
136
+ }
@@ -0,0 +1,5 @@
1
+ // A round sticker border.
2
+ import type { RenderContext, Markup } from '../render/context.js';
3
+
4
+ /** A disc under the layer, round what it paints (undefined where nothing can rasterise). */
5
+ export function roundBorder(fx: { kind: 'border'; shape: 'circle'; width?: import('../index.js').Length; color?: string; line?: boolean | string; lineWidth?: import('../index.js').Length; cx?: import('../index.js').Length; cy?: import('../index.js').Length }, m: Markup, ctx: RenderContext): Promise<Markup | undefined>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `border` with `shape: 'circle'`: a round sticker backing. The layer is
3
+ * drawn small to find the circle that holds it (centred on what it paints,
4
+ * out to its farthest pixel), then a disc `width` beyond that goes under it
5
+ * (and a cut line outside, with `line`).
6
+ *
7
+ * @module effects/round-border
8
+ */
9
+
10
+ import { attrs, num } from '../svg.js';
11
+ import { len } from '../geometry.js';
12
+ import { toHex } from '../color.js';
13
+ import { pixelsOf } from '../raster/index.js';
14
+
15
+ /** The probe's longer side, px. */
16
+ const PROBE = 320;
17
+
18
+ /**
19
+ * The round border, or undefined where nothing can rasterise (the
20
+ * die-cut `border` filter is used instead).
21
+ *
22
+ * @param {any} fx - `{ kind: 'border', shape: 'circle', width, color, line, lineWidth, cx, cy }`.
23
+ * @param {{ defs: string, body: string }} m
24
+ * @param {import('../layers.js').RenderContext} ctx
25
+ * @returns {Promise<{ defs: string, body: string } | undefined>}
26
+ */
27
+ export async function roundBorder(fx, m, ctx) {
28
+ const ox = -ctx.width / 2;
29
+ const oy = -ctx.height / 2;
30
+ const uw = ctx.width * 2;
31
+ const uh = ctx.height * 2;
32
+ const s = PROBE / Math.max(uw, uh);
33
+ const w = Math.max(1, Math.round(uw * s));
34
+ const h = Math.max(1, Math.round(uh * s));
35
+ const doc = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="${num(ox, 4)} ${num(oy, 4)} ${num(uw, 4)} ${num(uh, 4)}" preserveAspectRatio="none">${m.defs ? `<defs>${m.defs}</defs>` : ''}${m.body}</svg>`;
36
+ let px;
37
+ try {
38
+ px = await pixelsOf(doc, w, h, /** @type {any} */ (ctx).sharp);
39
+ } catch {
40
+ px = undefined;
41
+ }
42
+ if (!px) return undefined;
43
+ /** @type {[number, number][]} */
44
+ const painted = [];
45
+ let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity;
46
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
47
+ if (px[(y * w + x) * 4 + 3] < 8) continue;
48
+ painted.push([x, y]);
49
+ x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y);
50
+ }
51
+ if (!painted.length) return m;
52
+ const cx = fx.cx != null ? len(fx.cx, ctx.canvas) : ox + ((x0 + x1 + 1) / 2) / s;
53
+ const cy = fx.cy != null ? len(fx.cy, ctx.canvas) : oy + ((y0 + y1 + 1) / 2) / s;
54
+ // The farthest painted pixel's far corner.
55
+ let reach = 0;
56
+ for (const [x, y] of painted) reach = Math.max(reach, Math.hypot(ox + (x + 0.5) / s - cx, oy + (y + 0.5) / s - cy) + 0.71 / s);
57
+ const r = reach + len(fx.width, ctx.canvas, '1.2vmin');
58
+ let under = '';
59
+ if (fx.line) under += `<circle${attrs({ cx, cy, r: r + len(fx.lineWidth, ctx.canvas, '0.25vmin'), fill: fx.line === true ? '#c9c6c0' : toHex(fx.line) })}/>`;
60
+ under += `<circle${attrs({ cx, cy, r, fill: toHex(fx.color ?? '#ffffff') })}/>`;
61
+ return { defs: m.defs, body: `${under}${m.body}` };
62
+ }
@@ -0,0 +1,14 @@
1
+ // Screens and signals.
2
+ import type { Effect } from './index.js';
3
+ import type { RenderContext } from '../render/context.js';
4
+ import type { Pixels } from './buffer.js';
5
+ import type { PixelResult } from './pixels.js';
6
+
7
+ /** The `scanlines` effect's filter primitives. */
8
+ export function scanlines(fx: Extract<Effect, { kind: 'scanlines' }>, ctx: RenderContext): string;
9
+ /** `crt`, on pixels. */
10
+ export function crt(img: Pixels, fx: Extract<Effect, { kind: 'crt' }>, ctx: object): Uint8ClampedArray;
11
+ /** `vhs`, on pixels. */
12
+ export function vhs(img: Pixels, fx: Extract<Effect, { kind: 'vhs' }>, ctx: object): Uint8ClampedArray;
13
+ /** `jpeg`: through a real JPEG encoder, on pixels. */
14
+ export function jpeg(img: Pixels, fx: Extract<Effect, { kind: 'jpeg' }>, ctx: object): Promise<PixelResult | Uint8ClampedArray>;