@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,277 @@
1
+ /**
2
+ * Effects: what any layer takes as `effects: [{ kind, … }]`, applied in
3
+ * order (innermost first) in the layer's own coordinates. Three sorts:
4
+ *
5
+ * - **Filters**: one SVG `<filter>` each, over the canvas padded so blurs
6
+ * and glows don't clip (colour, light, detail, grain…).
7
+ * - **Pixels**: what filters can't do (spin and zoom blur, bokeh,
8
+ * distortions, pixel sorting, ASCII…), drawn once at the layer's
9
+ * resolution, processed on a typed array and embedded (`pixels.js`).
10
+ * - **Copies**: the layer's own markup drawn again (`extrude`, `mirror`),
11
+ * so vectors stay vectors.
12
+ *
13
+ * Any of them takes a `mask` (`mask.js`): a gradient or layers saying
14
+ * where, and how much, it applies.
15
+ *
16
+ * @module effects
17
+ */
18
+
19
+ import { num } from '../svg.js';
20
+ import { len } from '../geometry.js';
21
+ import { filterElement } from './util.js';
22
+ import { blur, shadow, border } from './light.js';
23
+ import { rough, wear, xerox, grit } from './ink.js';
24
+ import { duotone, levels, saturate, matrix, threshold, photocopy } from './tone.js';
25
+ import { halftone } from './halftone.js';
26
+ import { dither, ditherRaster } from './dither.js';
27
+ import { expandDropShadows } from './drop-shadow.js';
28
+ import { extrude } from './extrude.js';
29
+ import { roundBorder } from './round-border.js';
30
+ import { separableMorphology, bounded, filterReach } from './region.js';
31
+ import { curves, gradientMap, posterize, invert, hue, colorBalance, channelMixer, sepia, vibrance } from './grade.js';
32
+ import { look } from './looks.js';
33
+ import { bloom, halation } from './glow.js';
34
+ import { vignette, lightLeak, lensFlare } from './optics.js';
35
+ import { grain, paper, dust } from './film.js';
36
+ import { scanlines, crt, vhs, jpeg } from './screen.js';
37
+ import { sketch, edges, emboss, sharpen } from './detail.js';
38
+ import { chromatic, chromaticRadial, glitch, pixelate } from './digital.js';
39
+ import { spin, zoom, rays } from './polar.js';
40
+ import { lens, tilt } from './lens.js';
41
+ import { motion as motionPixels } from './motion.js';
42
+ import { wave, ripple, twirl, bulge, fisheye, kaleidoscope, glass, displace } from './distort.js';
43
+ import { sort, stretch } from './sort.js';
44
+ import { newsprint } from './newsprint.js';
45
+ import { ascii } from './ascii.js';
46
+ import { mirror } from './symmetry.js';
47
+ import { pixelEffect } from './pixels.js';
48
+ import { gradientImage, lerpPrimitives, maskOverlay, RESOLVED } from './mask.js';
49
+ import { EFFECT_OPTIONS } from './options.js';
50
+
51
+ export { transformAttr } from './transform.js';
52
+ export { paint } from './paint.js';
53
+ export { expandDropShadows } from './drop-shadow.js';
54
+ export { EFFECT_OPTIONS, EFFECT_COMMON } from './options.js';
55
+ export { checkMask, RESOLVED, maskImage } from './mask.js';
56
+
57
+ /** Scripts, event-handler attributes (a name starting `on`) and `javascript:` URLs. */
58
+ const UNSAFE = /<script|(?:^|[\s"'/])on[a-z]+\s*=|javascript:/i;
59
+
60
+ /**
61
+ * `filter`: raw primitives, the escape hatch. `{{seed}}` is a seeded
62
+ * number, `{{id}}` the layer's id prefix. `feDropShadow` is spelled out
63
+ * as primitives, so its `in` is honoured everywhere (librsvg ignores it).
64
+ *
65
+ * @param {{ markup?: string }} fx
66
+ * @param {import('../layers.js').RenderContext} ctx
67
+ * @returns {string}
68
+ */
69
+ function raw(fx, ctx) {
70
+ const markup = String(fx.markup ?? '');
71
+ if (UNSAFE.test(markup)) throw new TypeError('Filter markup may not contain scripts or event handlers');
72
+ return expandDropShadows(markup.replace(/\{\{seed\}\}/g, () => String(ctx.rng.int(1, 9999))).replace(/\{\{id\}\}/g, ctx.id));
73
+ }
74
+
75
+ /** Effects drawn as one filter (every kind but `motion`), by name. @type {Record<string, (fx: any, ctx: import('../layers.js').RenderContext) => string>} */
76
+ const KINDS = {
77
+ blur, shadow, glow: shadow, border, wear, rough, warp: rough, xerox, photocopy, duotone, grayscale: levels, levels, saturate, matrix, grit, halftone, dither, threshold, filter: raw,
78
+ curves, gradientMap, posterize, invert, hue, colorBalance, channelMixer, sepia, look,
79
+ bloom, halation, vignette, lightLeak, lensFlare,
80
+ grain, paper, scanlines,
81
+ sketch, edges, emboss, sharpen,
82
+ chromatic, rgbSplit: chromatic,
83
+ };
84
+
85
+ /** Effects computed on pixels, by name. @type {Record<string, (img: import('./buffer.js').Pixels, fx: any, ctx: any) => any>} */
86
+ const PIXEL = {
87
+ zoom, spin, rays, lens, tilt, motion: motionPixels,
88
+ wave, ripple, twirl, bulge, pinch: bulge, fisheye, kaleidoscope, glass, displace,
89
+ sort, stretch, vibrance, dust, crt, vhs, jpeg, newsprint,
90
+ chromatic: chromaticRadial, glitch, pixelate, mosaic: pixelate, ascii,
91
+ };
92
+
93
+ /** Effects drawn as copies of the layer's markup. @type {Record<string, (fx: any, m: { defs: string, body: string }, ctx: any, id: string) => { defs: string, body: string }>} */
94
+ const COPIES = { extrude, mirror };
95
+
96
+ /**
97
+ * What a pixel effect falls back to where nothing can draw pixels (a
98
+ * Worker): a filter close to it (`tilt`: a Gaussian through its band), or
99
+ * nothing (the layer is drawn without it).
100
+ *
101
+ * @type {Record<string, (fx: any, ctx: any) => string>}
102
+ */
103
+ const FALLBACK = {
104
+ lens: (fx, ctx) => blur({ kind: 'blur', radius: len(fx.radius, ctx.canvas, '1.2vmin') * 0.5 }, ctx),
105
+ // A Gaussian through the band (an effect's own mask is mixed on top).
106
+ tilt: (fx, ctx) => blur({ kind: 'blur', radius: len(fx.radius, ctx.canvas, '1.4vmin') * 0.7 }, ctx)
107
+ + (fx.mask ? '' : lerpPrimitives(gradientImage({ gradient: 'band', center: fx.center, angle: fx.angle, from: fx.width ?? '8vmin', to: len(fx.width ?? '8vmin', ctx.canvas) + len(fx.falloff ?? '22vmin', ctx.canvas) }, ctx, 0.25))),
108
+ };
109
+
110
+ /** Every effect kind, for validation and docs. */
111
+ export const EFFECTS = Object.freeze(Object.keys(EFFECT_OPTIONS));
112
+
113
+ /** Effects computed outside SVG filters (on pixels, or as copies of the markup), always. */
114
+ export const RASTER_EFFECTS = Object.freeze(['dither', 'extrude', ...Object.keys(COPIES).filter((k) => k !== 'extrude'), ...Object.keys(PIXEL).filter((k) => k !== 'chromatic' && k !== 'motion')]);
115
+
116
+ /** @param {any} fx @returns {any} An effect given as a string, as an object. */
117
+ const asEffect = (fx) => (typeof fx === 'string' ? { kind: fx } : fx);
118
+
119
+ /**
120
+ * Whether an effect is computed on pixels: the pixel kinds, `chromatic`
121
+ * with `mode: 'radial'`, and `motion` when rasterising (librsvg can't
122
+ * blur along a turned axis, and the pixel blur holds the edge).
123
+ *
124
+ * @param {any} fx
125
+ * @param {{ raster?: boolean }} [ctx]
126
+ * @returns {boolean}
127
+ */
128
+ export function isPixel(fx, ctx) {
129
+ const e = asEffect(fx);
130
+ if (!e || !Object.hasOwn(PIXEL, e.kind)) return false;
131
+ if (e.kind === 'chromatic') return e.mode === 'radial';
132
+ if (e.kind === 'motion') return Boolean(ctx?.raster);
133
+ return true;
134
+ }
135
+
136
+ /**
137
+ * Whether an effect needs something that draws pixels (so `renderSvg`
138
+ * loads sharp in Node): pixel effects, `dither`, a round `border`.
139
+ *
140
+ * @param {any} fx
141
+ * @returns {boolean}
142
+ */
143
+ export function needsPixels(fx) {
144
+ const e = asEffect(fx);
145
+ return Boolean(e) && (isPixel(e) || e.kind === 'dither' || (e.kind === 'border' && e.shape === 'circle'));
146
+ }
147
+
148
+ /**
149
+ * Whether an effect is drawn without an SVG filter (pixels, copies, a
150
+ * round `border`).
151
+ *
152
+ * @param {any} fx
153
+ * @param {{ raster?: boolean }} [ctx]
154
+ * @returns {boolean}
155
+ */
156
+ export const isRaster = (fx, ctx) => {
157
+ const e = asEffect(fx);
158
+ return Boolean(e) && (e.kind === 'dither' || Object.hasOwn(COPIES, e.kind) || isPixel(e, ctx) || (e.kind === 'border' && e.shape === 'circle'));
159
+ };
160
+
161
+ /**
162
+ * An effect drawn without an SVG filter: on pixels, as copies, or a round
163
+ * `border`; with its `mask` (a pixel mix, or for copies an SVG mask).
164
+ * Undefined where nothing can draw pixels (its filter is used instead).
165
+ *
166
+ * @param {any} fx
167
+ * @param {{ defs: string, body: string }} m
168
+ * @param {import('../layers.js').RenderContext} ctx
169
+ * @param {number} i - The effect's index (for unique ids).
170
+ * @returns {Promise<{ defs: string, body: string } | undefined>}
171
+ */
172
+ export async function rasterEffect(fx, m, ctx, i) {
173
+ const id = `${ctx.id}x${i}`;
174
+ if (isPixel(fx, ctx)) return pixelEffect(fx, m, ctx, (img) => PIXEL[fx.kind](img, fx, ctx));
175
+ let out;
176
+ if (Object.hasOwn(COPIES, fx.kind)) out = COPIES[fx.kind](fx, m, ctx, id);
177
+ else if (fx.kind === 'border') out = await roundBorder(fx, m, ctx);
178
+ else out = await ditherRaster(fx, m, ctx, id);
179
+ if (!out || !fx.mask) return out;
180
+ return maskOverlay(m.body, out, fx.mask, ctx, `${id}m`);
181
+ }
182
+
183
+ /** Region padding by kind, as a fraction of the canvas. */
184
+ const PAD = /** @type {Record<string, number>} */ ({ blur: 0.25, shadow: 0.25, glow: 0.25, warp: 0.25, bloom: 0.25, lens: 0.25, tilt: 0.25 });
185
+
186
+ /**
187
+ * Wrap markup in a layer's filter effects.
188
+ *
189
+ * `motion` (a directional blur at any `angle`) blurs along x in a frame
190
+ * turned to the angle; every other kind is one filter. A pixel effect
191
+ * here (nothing could draw pixels) takes its fallback filter, or is left
192
+ * out.
193
+ *
194
+ * Each takes `colorSpace` (`'sRGB'`, the default, or `'linearRGB'`, the
195
+ * SVG default a browser's own filters use), `pad` and `mask`: a gradient
196
+ * mask is mixed inside the filter; a layer mask too when it has been
197
+ * drawn as an image, else the effect is drawn over the layer through an
198
+ * SVG mask.
199
+ *
200
+ * @param {string} body
201
+ * @param {import('./index.js').Effect | import('./index.js').Effect[]} list
202
+ * @param {import('../layers.js').RenderContext} ctx
203
+ * @param {number} [start=0] - Index of the first effect (ids stay unique when a list is applied in runs).
204
+ * @param {{ x: number, y: number, width: number, height: number }} [content] - The box the layer paints, when known: filters that stay inside their source get a region that size (plus their padding) instead of the canvas.
205
+ * @param {string} [prior=''] - The defs drawn so far (an SVG mask fallback repeats the layer).
206
+ * @returns {{ defs: string, body: string }}
207
+ * @throws {TypeError} On an unknown kind.
208
+ */
209
+ export function applyEffects(body, list, ctx, start = 0, content, prior = '') {
210
+ const effects = Array.isArray(list) ? list : [list];
211
+ let defs = '';
212
+ let out = body;
213
+ /** What the layer paints so far (grows with each effect's padding), when known. */
214
+ let painted = content;
215
+ effects.forEach((e, i) => {
216
+ if (!e) return;
217
+ const fx = /** @type {any} */ (asEffect(e));
218
+ const id = `${ctx.id}x${start + i}`;
219
+ const before = out;
220
+ const mask = fx.mask;
221
+ // A layer mask not drawn as an image: the effect goes over the layer through an SVG mask.
222
+ const overlay = mask && mask.layers && !mask[RESOLVED]?.image;
223
+ /** @param {number} pad @param {number} [angle] @returns {string} The mix with the source, for a mask. */
224
+ const mix = (pad, angle = 0) => (!mask || overlay ? '' : lerpPrimitives(mask.layers ? mask[RESOLVED].image : gradientImage(mask, ctx, pad, angle)));
225
+ if (fx.kind === 'motion') {
226
+ const a = fx.angle ?? 0;
227
+ const c = `${num(ctx.width / 2)} ${num(ctx.height / 2)}`;
228
+ const pad = fx.pad ?? 0.3;
229
+ defs += filterElement(id, ctx, pad, `<feGaussianBlur stdDeviation="${num(len(fx.length ?? '3vmin', ctx.canvas))} ${num(len(fx.spread ?? 0, ctx.canvas))}"/>${mix(pad, a)}`, fx.colorSpace);
230
+ out = `<g transform="rotate(${num(a)} ${c})"><g filter="url(#${id})"><g transform="rotate(${num(-a)} ${c})">${out}</g></g></g>`;
231
+ painted = undefined;
232
+ } else {
233
+ const make = Object.hasOwn(KINDS, fx.kind) ? KINDS[fx.kind] : FALLBACK[fx.kind];
234
+ if (!make) {
235
+ if (Object.hasOwn(PIXEL, fx.kind) || Object.hasOwn(COPIES, fx.kind)) return;
236
+ throw new TypeError(`Unknown effect "${fx.kind}" (${EFFECTS.join(', ')})`);
237
+ }
238
+ const pad = fx.pad ?? PAD[fx.kind] ?? 0.05;
239
+ let primitives = make(fx, ctx);
240
+ if (!primitives) return;
241
+ primitives += mix(pad);
242
+ // For librsvg: separable morphology, and a region the size of what's painted.
243
+ if (/** @type {any} */ (ctx).raster) primitives = separableMorphology(primitives);
244
+ const region = painted && bounded(primitives) ? grow(painted, pad, ctx) : undefined;
245
+ // How far it reaches at the output's resolution (renderImage's tiles overlap by that).
246
+ /** @type {any} */ (ctx).noteReach?.(filterReach(primitives) * (/** @type {any} */ (ctx).res ?? 1));
247
+ painted = region;
248
+ defs += filterElement(id, ctx, pad, primitives, fx.colorSpace, region);
249
+ out = `<g filter="url(#${id})">${out}</g>`;
250
+ }
251
+ if (overlay) {
252
+ const got = maskOverlay(before, { defs: prior + defs, body: out }, mask, ctx, `${id}m`);
253
+ defs = got.defs.slice(prior.length);
254
+ out = got.body;
255
+ painted = undefined;
256
+ }
257
+ });
258
+ return { defs, body: out };
259
+ }
260
+
261
+ /**
262
+ * A painted box grown by an effect's padding, inside the canvas's padded region.
263
+ *
264
+ * @param {{ x: number, y: number, width: number, height: number }} box
265
+ * @param {number} pad - Fraction of the canvas.
266
+ * @param {{ width: number, height: number }} ctx
267
+ * @returns {{ x: number, y: number, width: number, height: number }}
268
+ */
269
+ function grow(box, pad, ctx) {
270
+ const px = ctx.width * pad;
271
+ const py = ctx.height * pad;
272
+ const x0 = Math.max(-px, box.x - px);
273
+ const y0 = Math.max(-py, box.y - py);
274
+ const x1 = Math.min(ctx.width + px, box.x + box.width + px);
275
+ const y1 = Math.min(ctx.height + py, box.y + box.height + py);
276
+ return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
277
+ }
@@ -0,0 +1,12 @@
1
+ // Ink and print effects.
2
+ import type { Effect } from './index.js';
3
+ import type { RenderContext } from '../render/context.js';
4
+
5
+ /** The `rough | warp` effect's filter primitives. */
6
+ export function rough(fx: Extract<Effect, { kind: 'rough' | 'warp' }>, ctx: RenderContext): string;
7
+ /** The `wear` effect's filter primitives. */
8
+ export function wear(fx: Extract<Effect, { kind: 'wear' }>, ctx: RenderContext): string;
9
+ /** The `xerox` effect's filter primitives. */
10
+ export function xerox(fx: Extract<Effect, { kind: 'xerox' }>, ctx: RenderContext): string;
11
+ /** The `grit` effect's filter primitives. */
12
+ export function grit(fx: Extract<Effect, { kind: 'grit' }>, ctx: RenderContext): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Ink and print effects: displacement (rough, warp), riso drum wear,
3
+ * photocopied vector ink, and grit.
4
+ *
5
+ * @module effects/ink
6
+ */
7
+
8
+ import { num } from '../svg.js';
9
+ import { len } from '../geometry.js';
10
+ import { mat, seedOf, displace } from './util.js';
11
+
12
+ /**
13
+ * `rough` (a fine wobble) and `warp` (a melting distortion).
14
+ *
15
+ * @param {import('./index.js').Effect & { kind: 'rough' | 'warp' }} fx
16
+ * @param {import('../layers.js').RenderContext} ctx
17
+ * @returns {string}
18
+ */
19
+ export function rough(fx, ctx) {
20
+ const warp = fx.kind === 'warp';
21
+ return displace(fx.frequency ?? (warp ? [0.005, 0.011] : 0.9), fx.octaves ?? 2, seedOf(ctx, fx), len(fx.scale, ctx.canvas, warp ? '8vmin' : '0.26vmin'));
22
+ }
23
+
24
+ /**
25
+ * `wear`: riso drum wear. Edges a touch rough (`rough`), coverage uneven
26
+ * (`uneven`), specks where the drum missed (`specks`).
27
+ *
28
+ * @param {import('./index.js').Effect & { kind: 'wear' }} fx
29
+ * @param {import('../layers.js').RenderContext} ctx
30
+ * @returns {string}
31
+ */
32
+ export function wear(fx, ctx) {
33
+ const uneven = fx.uneven ?? 0.4;
34
+ const specks = fx.specks ?? 1;
35
+ const s1 = seedOf(ctx, fx);
36
+ const s2 = ctx.rng.int(1, 9999);
37
+ const s3 = ctx.rng.int(1, 9999);
38
+ return displace(0.9, 2, s1, len(fx.rough, ctx.canvas, '0.26vmin'), 'rough')
39
+ + `<feTurbulence type="fractalNoise" baseFrequency="0.02 0.05" numOctaves="3" seed="${s2}" result="lo"/>`
40
+ + `<feColorMatrix in="lo" type="matrix" values="${mat([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, uneven, 0, 0, 0, 1.16 - uneven])}" result="even"/>`
41
+ + `<feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="1" seed="${s3}" result="sp"/>`
42
+ + `<feColorMatrix in="sp" type="matrix" values="${mat([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16 * specks, 13.2 + (1 - specks) * 3])}" result="holes"/>`
43
+ + '<feComposite in="even" in2="holes" operator="arithmetic" k1="1" result="cover"/><feComposite in="rough" in2="cover" operator="in"/>';
44
+ }
45
+
46
+ /**
47
+ * `xerox`: photocopied vector ink, a wobble and toner dropout.
48
+ *
49
+ * @param {import('./index.js').Effect & { kind: 'xerox' }} fx
50
+ * @param {import('../layers.js').RenderContext} ctx
51
+ * @returns {string}
52
+ */
53
+ export function xerox(fx, ctx) {
54
+ const s1 = seedOf(ctx, fx);
55
+ const s2 = ctx.rng.int(1, 9999);
56
+ return displace(1.1, 2, s1, len(fx.rough, ctx.canvas, '0.32vmin'), 'w')
57
+ + `<feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="1" seed="${s2}" result="d"/><feColorMatrix in="d" type="matrix" values="${mat([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 10.6 + (fx.dropout != null ? (0.5 - fx.dropout) * 4 : 0)])}" result="h"/><feComposite in="w" in2="h" operator="in"/>`;
58
+ }
59
+
60
+ /**
61
+ * `grit`: noise added to the colour (`amount`), kept inside the source.
62
+ *
63
+ * @param {import('./index.js').Effect & { kind: 'grit' }} fx
64
+ * @param {import('../layers.js').RenderContext} ctx
65
+ * @returns {string}
66
+ */
67
+ export function grit(fx, ctx) {
68
+ const a = fx.amount ?? 0.14;
69
+ return `<feTurbulence type="fractalNoise" baseFrequency="${num(fx.frequency ?? 0.9, 3)}" numOctaves="1" seed="${seedOf(ctx, fx)}" result="g"/><feComposite in="SourceGraphic" in2="g" operator="arithmetic" k2="1" k3="${num(a, 3)}" k4="${num(-a / 2, 3)}"/><feComposite in2="SourceAlpha" operator="in"/>`;
70
+ }
@@ -0,0 +1,14 @@
1
+ // Optical blurs on pixels: lens bokeh and tilt-shift.
2
+ import type { Effect } from './index.js';
3
+ import type { Pixels } from './buffer.js';
4
+
5
+ /** One row of an aperture: its span (pixel offsets) and how much of the row it fills. */
6
+ export interface Span { dy: number; l: number; r: number; wgt: number }
7
+ /** An aperture (a disc, or a polygon of `blades` sides) as anti-aliased spans. */
8
+ export function aperture(R: number, blades: number, rotation: number): Span[];
9
+ /** Convolve premultiplied float RGBA with an aperture. */
10
+ export function convolveSpans(src: Float32Array, w: number, h: number, spans: Span[]): Float32Array;
11
+ /** `lens`: a lens blur with bokeh. */
12
+ export function lens(img: Pixels, fx: Extract<Effect, { kind: 'lens' }>, ctx: object): Uint8ClampedArray;
13
+ /** `tilt`: tilt-shift. */
14
+ export function tilt(img: Pixels, fx: Extract<Effect, { kind: 'tilt' }>, ctx: object): Uint8ClampedArray;
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Optical blurs on pixels: `lens` (out of focus through an aperture, with
3
+ * bright highlights blooming into bokeh discs or polygons) and `tilt`
4
+ * (tilt-shift: a sharp band, the blur growing away from it).
5
+ *
6
+ * The lens blur works in linear light with highlights boosted (so a
7
+ * small light spreads into a bright disc, as it does through glass) and
8
+ * convolves with the aperture's exact shape a row at a time: each row of
9
+ * the shape is a span, summed from running totals, so the cost is one
10
+ * lookup per row of the kernel, not per pixel of it. Large radii work on
11
+ * a smaller copy (bokeh has no fine detail) and scale back.
12
+ *
13
+ * @module effects/lens
14
+ */
15
+
16
+ import { TO_LINEAR, toSrgb, px, pointPx, smoothstep, gaussian, shrink, bilinear, premultiplied, unpremultiply, enlarge } from './buffer.js';
17
+
18
+ /** The kernel radius a lens blur works at, px (larger radii work on a smaller copy). */
19
+ const WORK_RADIUS = 10;
20
+
21
+ /**
22
+ * One row of the aperture: the span it covers (pixel offsets, fractional)
23
+ * and how much of the row's height it fills.
24
+ *
25
+ * @typedef {{ dy: number, l: number, r: number, wgt: number }} Span
26
+ */
27
+
28
+ /**
29
+ * The aperture as spans: a disc (`blades` 0) or a polygon of `blades`
30
+ * sides turned `rotation` degrees, radius `R`. Each row is the average
31
+ * of four sub-rows, so its top and bottom edges are anti-aliased too.
32
+ *
33
+ * @param {number} R
34
+ * @param {number} blades
35
+ * @param {number} rotation
36
+ * @returns {Span[]}
37
+ */
38
+ export function aperture(R, blades, rotation) {
39
+ const n = Math.ceil(R + 0.5);
40
+ /** @type {[number, number][]} */
41
+ const poly = [];
42
+ if (blades >= 3) {
43
+ for (let k = 0; k < blades; k++) {
44
+ const a = ((rotation - 90) * Math.PI) / 180 + (2 * Math.PI * k) / blades;
45
+ poly.push([R * Math.cos(a), R * Math.sin(a)]);
46
+ }
47
+ }
48
+ /** @param {number} y @returns {[number, number] | null} */
49
+ const at = (y) => {
50
+ if (!poly.length) {
51
+ if (Math.abs(y) >= R) return null;
52
+ const s = Math.sqrt(R * R - y * y);
53
+ return [-s, s];
54
+ }
55
+ let lo = Infinity;
56
+ let hi = -Infinity;
57
+ for (let k = 0; k < poly.length; k++) {
58
+ const [x0, y0] = poly[k];
59
+ const [x1, y1] = poly[(k + 1) % poly.length];
60
+ if ((y0 <= y && y1 >= y) || (y1 <= y && y0 >= y)) {
61
+ const x = y1 === y0 ? x0 : x0 + ((y - y0) / (y1 - y0)) * (x1 - x0);
62
+ lo = Math.min(lo, x);
63
+ hi = Math.max(hi, x);
64
+ }
65
+ }
66
+ return lo <= hi ? [lo, hi] : null;
67
+ };
68
+ /** @type {Span[]} */
69
+ const spans = [];
70
+ for (let dy = -n; dy <= n; dy++) {
71
+ let l = 0;
72
+ let r = 0;
73
+ let hits = 0;
74
+ for (let s = 0; s < 4; s++) {
75
+ const got = at(dy - 0.5 + (s + 0.5) / 4);
76
+ if (!got) continue;
77
+ l += got[0];
78
+ r += got[1];
79
+ hits++;
80
+ }
81
+ if (hits) spans.push({ dy, l: l / hits, r: r / hits, wgt: hits / 4 });
82
+ }
83
+ return spans;
84
+ }
85
+
86
+ /**
87
+ * Convolve premultiplied float RGBA with an aperture, edges held.
88
+ *
89
+ * @param {Float32Array} src - Interleaved RGBA.
90
+ * @param {number} w
91
+ * @param {number} h
92
+ * @param {Span[]} spans
93
+ * @returns {Float32Array}
94
+ */
95
+ export function convolveSpans(src, w, h, spans) {
96
+ const pad = Math.ceil(Math.max(...spans.map((s) => Math.max(-s.l, s.r)))) + 2;
97
+ const pw = w + 2 * pad;
98
+ // Running totals along each row (edge held), one more than the row.
99
+ const sums = new Float32Array(h * (pw + 1) * 4);
100
+ for (let y = 0; y < h; y++) {
101
+ const base = y * (pw + 1) * 4;
102
+ for (let x = 0; x < pw; x++) {
103
+ const sx = Math.min(w - 1, Math.max(0, x - pad));
104
+ const i = (y * w + sx) * 4;
105
+ const o = base + (x + 1) * 4;
106
+ sums[o] = sums[o - 4] + src[i];
107
+ sums[o + 1] = sums[o - 3] + src[i + 1];
108
+ sums[o + 2] = sums[o - 2] + src[i + 2];
109
+ sums[o + 3] = sums[o - 1] + src[i + 3];
110
+ }
111
+ }
112
+ // Each span: whole pixels [L, R], and part of the pixel either side.
113
+ const rows = spans.map((s) => {
114
+ const L = Math.ceil(s.l + 0.5);
115
+ const R = Math.floor(s.r - 0.5);
116
+ const cover = (/** @type {number} */ dx) => Math.max(0, Math.min(dx + 0.5, s.r) - Math.max(dx - 0.5, s.l));
117
+ return { dy: s.dy, L, R, wgt: s.wgt, pl: L - 1 === R + 1 ? cover(L - 1) : cover(L - 1), pr: L - 1 === R + 1 ? 0 : cover(R + 1), full: R >= L };
118
+ });
119
+ let total = 0;
120
+ for (const r of rows) total += r.wgt * ((r.full ? r.R - r.L + 1 : 0) + r.pl + r.pr);
121
+ const norm = 1 / total;
122
+ const out = new Float32Array(w * h * 4);
123
+ const acc = new Float64Array(4);
124
+ for (let y = 0; y < h; y++) {
125
+ for (let x = 0; x < w; x++) {
126
+ acc[0] = acc[1] = acc[2] = acc[3] = 0;
127
+ for (let k = 0; k < rows.length; k++) {
128
+ const r = rows[k];
129
+ let yy = y + r.dy;
130
+ if (yy < 0) yy = 0; else if (yy >= h) yy = h - 1;
131
+ const base = yy * (pw + 1) * 4;
132
+ const wg = r.wgt;
133
+ if (r.full) {
134
+ const a = base + (x + r.L + pad) * 4;
135
+ const b = base + (x + r.R + pad + 1) * 4;
136
+ acc[0] += (sums[b] - sums[a]) * wg;
137
+ acc[1] += (sums[b + 1] - sums[a + 1]) * wg;
138
+ acc[2] += (sums[b + 2] - sums[a + 2]) * wg;
139
+ acc[3] += (sums[b + 3] - sums[a + 3]) * wg;
140
+ }
141
+ if (r.pl) {
142
+ const sx = x + r.L - 1;
143
+ const i = (yy * w + (sx < 0 ? 0 : sx >= w ? w - 1 : sx)) * 4;
144
+ const f = r.pl * wg;
145
+ acc[0] += src[i] * f; acc[1] += src[i + 1] * f; acc[2] += src[i + 2] * f; acc[3] += src[i + 3] * f;
146
+ }
147
+ if (r.pr) {
148
+ const sx = x + r.R + 1;
149
+ const i = (yy * w + (sx < 0 ? 0 : sx >= w ? w - 1 : sx)) * 4;
150
+ const f = r.pr * wg;
151
+ acc[0] += src[i] * f; acc[1] += src[i + 1] * f; acc[2] += src[i + 2] * f; acc[3] += src[i + 3] * f;
152
+ }
153
+ }
154
+ const o = (y * w + x) * 4;
155
+ out[o] = acc[0] * norm; out[o + 1] = acc[1] * norm; out[o + 2] = acc[2] * norm; out[o + 3] = acc[3] * norm;
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+
161
+ /**
162
+ * `lens`: a lens blur. `radius` (the aperture), `blades` (0 round, else
163
+ * that many sides), `rotation`, and the highlights: everything brighter
164
+ * than `threshold` is boosted by `highlights` before blurring, so lights
165
+ * become bright bokeh shapes.
166
+ *
167
+ * @param {import('./buffer.js').Pixels} img
168
+ * @param {import('./index.js').Effect & { kind: 'lens' }} fx
169
+ * @param {any} ctx
170
+ * @returns {Uint8ClampedArray}
171
+ */
172
+ export function lens(img, fx, ctx) {
173
+ const { data, width: w, height: h } = img;
174
+ const R = px(fx.radius, ctx, img, '1.2vmin');
175
+ if (R < 0.5) return data;
176
+ const f = Math.max(1, Math.floor(R / WORK_RADIUS));
177
+ const sw = Math.ceil(w / f);
178
+ const sh = Math.ceil(h / f);
179
+ const t = fx.threshold ?? 0.75;
180
+ const gain = 8 * (fx.highlights ?? 1);
181
+ // Linear, premultiplied, highlights boosted, averaged into f×f cells.
182
+ const work = new Float32Array(sw * sh * 4);
183
+ const count = new Float32Array(sw * sh);
184
+ for (let y = 0; y < h; y++) {
185
+ const row = ((y / f) | 0) * sw;
186
+ for (let x = 0; x < w; x++) {
187
+ const i = (y * w + x) * 4;
188
+ const a = data[i + 3] / 255;
189
+ const c = row + ((x / f) | 0);
190
+ count[c]++;
191
+ if (!a) continue;
192
+ const lum = (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255;
193
+ const s = smoothstep(t, 1, lum);
194
+ const k = a * (1 + gain * s * s);
195
+ const o = c * 4;
196
+ work[o] += TO_LINEAR[data[i]] * k;
197
+ work[o + 1] += TO_LINEAR[data[i + 1]] * k;
198
+ work[o + 2] += TO_LINEAR[data[i + 2]] * k;
199
+ work[o + 3] += a;
200
+ }
201
+ }
202
+ for (let c = 0; c < count.length; c++) { const k = 1 / count[c]; work[c * 4] *= k; work[c * 4 + 1] *= k; work[c * 4 + 2] *= k; work[c * 4 + 3] *= k; }
203
+ const blurred = convolveSpans(work, sw, sh, aperture(R / f, fx.blades ?? 0, fx.rotation ?? 0));
204
+ const small = new Uint8ClampedArray(sw * sh * 4);
205
+ for (let i = 0; i < blurred.length; i += 4) {
206
+ const a = blurred[i + 3];
207
+ if (a <= 1e-4) continue;
208
+ small[i] = toSrgb(blurred[i] / a);
209
+ small[i + 1] = toSrgb(blurred[i + 1] / a);
210
+ small[i + 2] = toSrgb(blurred[i + 2] / a);
211
+ small[i + 3] = a * 255 + 0.5;
212
+ }
213
+ return f === 1 ? small : enlarge(small, sw, sh, w, h);
214
+ }
215
+
216
+ /**
217
+ * `tilt`: tilt-shift. Sharp within `width` of a line through `center` at
218
+ * `angle`, the blur growing over `falloff` beyond it to `radius`; with
219
+ * `saturate` for the toy-town look.
220
+ *
221
+ * @param {import('./buffer.js').Pixels} img
222
+ * @param {import('./index.js').Effect & { kind: 'tilt' }} fx
223
+ * @param {any} ctx
224
+ * @returns {Uint8ClampedArray}
225
+ */
226
+ export function tilt(img, fx, ctx) {
227
+ const { width: w, height: h } = img;
228
+ const sigma = px(fx.radius, ctx, img, '1.4vmin');
229
+ const band = px(fx.width, ctx, img, '8vmin');
230
+ const falloff = Math.max(1, px(fx.falloff, ctx, img, '22vmin'));
231
+ const [cx, cy] = pointPx(fx.center, img);
232
+ const a = ((fx.angle ?? 0) * Math.PI) / 180;
233
+ const nx = -Math.sin(a);
234
+ const ny = Math.cos(a);
235
+ const src = premultiplied(img);
236
+ // A sharp level and three blurred ones (worked small: a wide blur has no
237
+ // detail to lose); each pixel mixes the two either side of its blur,
238
+ // read straight from the small copies.
239
+ const steps = [0, 0.25, 0.5, 1];
240
+ const levels = steps.slice(1).map((k) => {
241
+ const f = Math.max(1, Math.min(8, Math.floor((sigma * k) / 6)));
242
+ const small = f > 1 ? shrink(src, w, h, f) : { data: new Uint8ClampedArray(src), width: w, height: h };
243
+ gaussian(small.data, small.width, small.height, 4, (sigma * k) / f);
244
+ return { ...small, f };
245
+ });
246
+ const out = new Uint8ClampedArray(src.length);
247
+ const lo = new Float32Array(4);
248
+ const hi = new Float32Array(4);
249
+ /** @param {number} k @param {number} x @param {number} y @param {Float32Array} to */
250
+ const read = (k, x, y, to) => {
251
+ if (k === 0) { const i = (y * w + x) * 4; to[0] = src[i]; to[1] = src[i + 1]; to[2] = src[i + 2]; to[3] = src[i + 3]; return; }
252
+ const l = levels[k - 1];
253
+ bilinear(l.data, l.width, l.height, (x + 0.5) / l.f - 0.5, (y + 0.5) / l.f - 0.5, to, 0);
254
+ };
255
+ const sat = fx.saturate ?? 1;
256
+ for (let y = 0, i = 0; y < h; y++) {
257
+ for (let x = 0; x < w; x++, i += 4) {
258
+ const d = Math.abs((x + 0.5 - cx) * nx + (y + 0.5 - cy) * ny);
259
+ const s = smoothstep(band, band + falloff, d);
260
+ if (s <= 0) { out[i] = src[i]; out[i + 1] = src[i + 1]; out[i + 2] = src[i + 2]; out[i + 3] = src[i + 3]; } else {
261
+ let k = 1;
262
+ while (k < steps.length - 1 && steps[k] < s) k++;
263
+ const t = (s - steps[k - 1]) / (steps[k] - steps[k - 1]);
264
+ read(k - 1, x, y, lo);
265
+ read(k, x, y, hi);
266
+ for (let c = 0; c < 4; c++) out[i + c] = lo[c] + (hi[c] - lo[c]) * t;
267
+ }
268
+ if (sat !== 1) {
269
+ const l = 0.2126 * out[i] + 0.7152 * out[i + 1] + 0.0722 * out[i + 2];
270
+ out[i] = l + (out[i] - l) * sat;
271
+ out[i + 1] = l + (out[i + 1] - l) * sat;
272
+ out[i + 2] = l + (out[i + 2] - l) * sat;
273
+ }
274
+ }
275
+ }
276
+ return img.opaque ? out : unpremultiply(out);
277
+ }
@@ -0,0 +1,10 @@
1
+ // Blur, shadow, glow and die-cut borders.
2
+ import type { Effect } from './index.js';
3
+ import type { RenderContext } from '../render/context.js';
4
+
5
+ /** The `blur` effect's filter primitives. */
6
+ export function blur(fx: Extract<Effect, { kind: 'blur' }>, ctx: RenderContext): string;
7
+ /** The `shadow | glow` effect's filter primitives. */
8
+ export function shadow(fx: Extract<Effect, { kind: 'shadow' | 'glow' }>, ctx: RenderContext): string;
9
+ /** The `border` effect's filter primitives. */
10
+ export function border(fx: Extract<Effect, { kind: 'border' }>, ctx: RenderContext): string;