@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.
- package/CHANGELOG.md +76 -0
- package/README.md +296 -11
- package/package.json +33 -5
- package/recipes/brand.json +1143 -0
- package/schema/recipe.schema.json +7300 -770
- package/src/animate/index.d.ts +19 -0
- package/src/animate/index.js +116 -0
- package/src/builtins.js +4 -0
- package/src/effects/ascii.d.ts +7 -0
- package/src/effects/ascii.js +93 -0
- package/src/effects/buffer.d.ts +60 -0
- package/src/effects/buffer.js +472 -0
- package/src/effects/detail.d.ts +12 -0
- package/src/effects/detail.js +124 -0
- package/src/effects/digital.d.ts +14 -0
- package/src/effects/digital.js +216 -0
- package/src/effects/distort.d.ts +20 -0
- package/src/effects/distort.js +314 -0
- package/src/effects/dither.d.ts +8 -0
- package/src/effects/dither.js +147 -0
- package/src/effects/drop-shadow.d.ts +4 -0
- package/src/effects/drop-shadow.js +45 -0
- package/src/effects/extrude.d.ts +5 -0
- package/src/effects/extrude.js +55 -0
- package/src/effects/film.d.ts +11 -0
- package/src/effects/film.js +204 -0
- package/src/effects/glow.d.ts +8 -0
- package/src/effects/glow.js +56 -0
- package/src/effects/grade.d.ts +30 -0
- package/src/effects/grade.js +274 -0
- package/src/effects/halftone.d.ts +8 -0
- package/src/effects/halftone.js +95 -0
- package/src/effects/index.d.ts +125 -0
- package/src/effects/index.js +277 -0
- package/src/effects/ink.d.ts +12 -0
- package/src/effects/ink.js +70 -0
- package/src/effects/lens.d.ts +14 -0
- package/src/effects/lens.js +277 -0
- package/src/effects/light.d.ts +10 -0
- package/src/effects/light.js +59 -0
- package/src/effects/looks.d.ts +9 -0
- package/src/effects/looks.js +74 -0
- package/src/effects/mask.d.ts +52 -0
- package/src/effects/mask.js +314 -0
- package/src/effects/motion.d.ts +6 -0
- package/src/effects/motion.js +60 -0
- package/src/effects/newsprint.d.ts +6 -0
- package/src/effects/newsprint.js +89 -0
- package/src/effects/optics.d.ts +10 -0
- package/src/effects/optics.js +213 -0
- package/src/effects/options.d.ts +6 -0
- package/src/effects/options.js +106 -0
- package/src/effects/paint-presets.d.ts +10 -0
- package/src/effects/paint-presets.js +47 -0
- package/src/effects/paint.d.ts +49 -0
- package/src/effects/paint.js +66 -0
- package/src/effects/pixels.d.ts +24 -0
- package/src/effects/pixels.js +138 -0
- package/src/effects/polar.d.ts +10 -0
- package/src/effects/polar.js +265 -0
- package/src/effects/region.d.ts +8 -0
- package/src/effects/region.js +136 -0
- package/src/effects/round-border.d.ts +5 -0
- package/src/effects/round-border.js +62 -0
- package/src/effects/screen.d.ts +14 -0
- package/src/effects/screen.js +212 -0
- package/src/effects/sort.d.ts +8 -0
- package/src/effects/sort.js +125 -0
- package/src/effects/symmetry.d.ts +6 -0
- package/src/effects/symmetry.js +74 -0
- package/src/effects/tile.d.ts +7 -0
- package/src/effects/tile.js +55 -0
- package/src/effects/tone.d.ts +16 -0
- package/src/effects/tone.js +117 -0
- package/src/effects/transform.d.ts +35 -0
- package/src/effects/transform.js +127 -0
- package/src/effects/util.d.ts +16 -0
- package/src/effects/util.js +85 -0
- package/src/extract.js +4 -3
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +246 -53
- package/src/formats/cut.d.ts +10 -0
- package/src/formats/cut.js +77 -0
- package/src/formats/index.d.ts +6 -0
- package/src/formats/index.js +9 -0
- package/src/formats/presets.d.ts +41 -0
- package/src/formats/presets.js +89 -0
- package/src/formats/print.d.ts +25 -0
- package/src/formats/print.js +117 -0
- package/src/formats/zones.d.ts +10 -0
- package/src/formats/zones.js +64 -0
- package/src/geometry.js +1 -1
- package/src/icons.js +43 -9
- package/src/index.d.ts +205 -19
- package/src/index.js +12 -4
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +181 -23
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +23 -4
- package/src/layers/barcode.d.ts +19 -0
- package/src/layers/barcode.js +63 -0
- package/src/layers/code128.d.ts +4 -0
- package/src/layers/code128.js +28 -0
- package/src/layers/expr.d.ts +6 -0
- package/src/layers/expr.js +121 -0
- package/src/layers/geometry.d.ts +6 -0
- package/src/layers/geometry.js +238 -0
- package/src/layers/index.d.ts +18 -0
- package/src/layers/index.js +15 -0
- package/src/layers/perforation.d.ts +14 -0
- package/src/layers/perforation.js +47 -0
- package/src/layers/repeat.d.ts +29 -0
- package/src/layers/repeat.js +90 -0
- package/src/layers/shape.d.ts +46 -0
- package/src/layers/shape.js +115 -0
- package/src/layers/soft.d.ts +5 -0
- package/src/layers/soft.js +43 -0
- package/src/layers/svg.d.ts +23 -0
- package/src/layers/svg.js +109 -0
- package/src/layers/table.d.ts +25 -0
- package/src/layers/table.js +80 -0
- package/src/layers/tear.d.ts +21 -0
- package/src/layers/tear.js +74 -0
- package/src/layers/texture.d.ts +17 -0
- package/src/layers/texture.js +106 -0
- package/src/layers/util.d.ts +8 -0
- package/src/layers/util.js +48 -0
- package/src/layers.js +125 -41
- package/src/lint/image-size.d.ts +4 -0
- package/src/lint/image-size.js +34 -0
- package/src/lint/index.d.ts +16 -0
- package/src/lint/index.js +127 -0
- package/src/lint/rules.d.ts +5 -0
- package/src/lint/rules.js +19 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +20 -0
- package/src/platform/bytes.d.ts +22 -0
- package/src/platform/bytes.js +103 -0
- package/src/platform/compress.d.ts +12 -0
- package/src/platform/compress.js +81 -0
- package/src/platform/harfbuzz.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +13 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +72 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/node.d.ts +26 -0
- package/src/platform/node.js +86 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/png.js +77 -28
- package/src/print.js +8 -7
- package/src/raster/index.d.ts +24 -0
- package/src/raster/index.js +171 -0
- package/src/recipes.js +4 -3
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -0
- package/src/render/content.d.ts +5 -0
- package/src/render/content.js +58 -0
- package/src/render/context.d.ts +82 -0
- package/src/render/grain.d.ts +7 -0
- package/src/render/grain.js +113 -0
- package/src/render/group.d.ts +23 -0
- package/src/render/group.js +67 -0
- package/src/render/key.d.ts +5 -0
- package/src/render/key.js +91 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/tiles.d.ts +6 -0
- package/src/render/tiles.js +77 -0
- package/src/render/use.d.ts +27 -0
- package/src/render/use.js +73 -0
- package/src/render/wrap.d.ts +14 -0
- package/src/render/wrap.js +199 -0
- package/src/render.js +303 -212
- package/src/text.d.ts +29 -0
- package/src/text.js +22 -15
- package/src/type/auto-color.d.ts +9 -0
- package/src/type/auto-color.js +44 -0
- package/src/type/fallback.d.ts +11 -0
- package/src/type/fallback.js +92 -0
- package/src/type/glyphs.d.ts +24 -0
- package/src/type/glyphs.js +87 -0
- package/src/type/index.d.ts +5 -0
- package/src/type/index.js +10 -0
- package/src/type/kern.d.ts +4 -0
- package/src/type/kern.js +144 -0
- package/src/type/layout-ref.d.ts +10 -0
- package/src/type/layout-ref.js +74 -0
- package/src/type/line.d.ts +11 -0
- package/src/type/line.js +105 -0
- package/src/type/live.d.ts +6 -0
- package/src/type/live.js +39 -0
- package/src/type/name.d.ts +4 -0
- package/src/type/name.js +32 -0
- package/src/type/path.d.ts +18 -0
- package/src/type/path.js +89 -0
- package/src/type/pixel.d.ts +9 -0
- package/src/type/pixel.js +115 -0
- package/src/type/sfnt.d.ts +20 -0
- package/src/type/sfnt.js +258 -0
- package/src/type/spans.d.ts +33 -0
- package/src/type/spans.js +138 -0
- package/src/type/symbols.d.ts +7 -0
- package/src/type/symbols.js +89 -0
- package/src/type/typeset.d.ts +10 -0
- package/src/type/typeset.js +352 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ordered (Bayer) dither in two colours, optionally pixelated first:
|
|
3
|
+
* computed on pixels where something can rasterise (the layer drawn once
|
|
4
|
+
* at one pixel per cell, thresholded here, embedded with nearest-neighbour
|
|
5
|
+
* scaling), else as an SVG filter.
|
|
6
|
+
*
|
|
7
|
+
* @module effects/dither
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { attrs, num } from '../svg.js';
|
|
11
|
+
import { len } from '../geometry.js';
|
|
12
|
+
import { encodePng, encodePngAsync, pngUri } from '../png.js';
|
|
13
|
+
import { pixelsOf } from '../raster/index.js';
|
|
14
|
+
import { LUMA, rgb, mat } from './util.js';
|
|
15
|
+
|
|
16
|
+
/** Bayer matrices. */
|
|
17
|
+
const BAYER = {
|
|
18
|
+
bayer4: [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5],
|
|
19
|
+
bayer8: (() => {
|
|
20
|
+
const m4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
|
|
21
|
+
const out = new Array(64);
|
|
22
|
+
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) {
|
|
23
|
+
const base = m4[(y % 4) * 4 + (x % 4)];
|
|
24
|
+
const q = (y >> 2) * 2 + (x >> 2);
|
|
25
|
+
out[y * 8 + x] = base * 4 + [0, 2, 3, 1][q];
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
})(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An ordered (Bayer) dither in two colours, optionally pixelated to a
|
|
33
|
+
* visible cell first. The threshold matrix is a tiny PNG tiled with feTile.
|
|
34
|
+
*
|
|
35
|
+
* @param {any} fx - `{ ink, paper, cell, contrast, brightness, pixelate, weights, matrix }`.
|
|
36
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function dither(fx, ctx) {
|
|
40
|
+
const [ir, ig, ib] = rgb(fx.ink ?? '#0b0b0b');
|
|
41
|
+
const [pr, pg, pb] = rgb(fx.paper ?? '#f4f1ea');
|
|
42
|
+
const c = Math.max(1, len(fx.cell, ctx.canvas, '0.65vmin'));
|
|
43
|
+
const contrast = fx.contrast ?? 1.45;
|
|
44
|
+
const bright = fx.brightness ?? 0.06;
|
|
45
|
+
const wts = fx.weights ?? LUMA;
|
|
46
|
+
const { n, px } = thresholds(fx);
|
|
47
|
+
const tile = pngUri(encodePng(px, n, n, 1));
|
|
48
|
+
let out = '';
|
|
49
|
+
let src = 'SourceGraphic';
|
|
50
|
+
if (fx.pixelate !== false) {
|
|
51
|
+
const d = Math.max(0.3, c / 2 - 0.3);
|
|
52
|
+
out += `<feFlood flood-color="#fff" x="${num(c / 2 - 0.3, 3)}" y="${num(c / 2 - 0.3, 3)}" width="0.6" height="0.6" result="dot"/><feComposite in="dot" in2="dot" operator="over" x="0" y="0" width="${num(c, 3)}" height="${num(c, 3)}" result="cell"/><feTile in="cell" result="grid"/>`
|
|
53
|
+
+ `<feComposite in="SourceGraphic" in2="grid" operator="in" result="sample"/><feMorphology in="sample" operator="dilate" radius="${num(d, 3)}" result="px"/>`;
|
|
54
|
+
src = 'px';
|
|
55
|
+
}
|
|
56
|
+
const o = 0.5 - contrast * 0.5 + bright;
|
|
57
|
+
const row = wts.map((/** @type {number} */ x) => x * contrast);
|
|
58
|
+
out += `<feColorMatrix in="${src}" type="matrix" values="${mat([...row, 0, o, ...row, 0, o, ...row, 0, o, 0, 0, 0, 1, 0])}" result="lum"/>`
|
|
59
|
+
+ `<feImage${attrs({ href: tile, x: 0, y: 0, width: c * n, height: c * n, preserveAspectRatio: 'none', 'image-rendering': 'pixelated', result: 'b' })}/><feTile in="b" result="bt"/>`
|
|
60
|
+
+ '<feComposite in="lum" in2="bt" operator="arithmetic" k2="1" k3="1" k4="-0.5"/>'
|
|
61
|
+
+ `<feComponentTransfer><feFuncR type="discrete" tableValues="${num(ir, 3)} ${num(pr, 3)}"/><feFuncG type="discrete" tableValues="${num(ig, 3)} ${num(pg, 3)}"/><feFuncB type="discrete" tableValues="${num(ib, 3)} ${num(pb, 3)}"/><feFuncA type="discrete" tableValues="1 1"/></feComponentTransfer>`
|
|
62
|
+
+ '<feComposite in2="SourceGraphic" operator="in"/>';
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The threshold matrix as 8-bit greys (the filter's tile, and the raster's thresholds).
|
|
68
|
+
*
|
|
69
|
+
* @param {any} fx
|
|
70
|
+
* @returns {{ n: number, px: Uint8Array }}
|
|
71
|
+
*/
|
|
72
|
+
function thresholds(fx) {
|
|
73
|
+
const matrix = /** @type {number[]} */ ((/** @type {any} */ (BAYER))[fx.matrix ?? 'bayer4'] ?? BAYER.bayer4);
|
|
74
|
+
const n = Math.round(Math.sqrt(matrix.length));
|
|
75
|
+
const px = new Uint8Array(n * n);
|
|
76
|
+
matrix.forEach((v, i) => { px[i] = Math.round((1 - (v + 0.5) / (n * n)) * 255); });
|
|
77
|
+
return { n, px };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The dither on pixels: the layer is drawn once at one pixel per cell
|
|
82
|
+
* (`pixelate`, the default) or at output resolution, thresholded against
|
|
83
|
+
* the matrix here, and embedded scaled up nearest-neighbour. Where the
|
|
84
|
+
* layer isn't opaque the result is clipped to its shape. The same
|
|
85
|
+
* arithmetic as the filter (sRGB, `contrast`, `brightness`, `weights`).
|
|
86
|
+
*
|
|
87
|
+
* @param {any} fx
|
|
88
|
+
* @param {{ defs: string, body: string }} m - The layer's markup so far.
|
|
89
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
90
|
+
* @param {string} id
|
|
91
|
+
* @returns {Promise<{ defs: string, body: string } | undefined>} Undefined where nothing can rasterise.
|
|
92
|
+
*/
|
|
93
|
+
export async function ditherRaster(fx, m, ctx, id) {
|
|
94
|
+
const c = Math.max(1e-3, len(fx.cell, ctx.canvas, '0.65vmin'));
|
|
95
|
+
// Pixels per unit: one per cell, or the output's resolution (at least one per cell).
|
|
96
|
+
const ppu = fx.pixelate === false ? Math.max(1 / c, /** @type {any} */ (ctx).res ?? 1) : 1 / c;
|
|
97
|
+
const w = Math.max(1, Math.ceil(ctx.width * ppu));
|
|
98
|
+
const h = Math.max(1, Math.ceil(ctx.height * ppu));
|
|
99
|
+
const uw = w / ppu;
|
|
100
|
+
const uh = h / ppu;
|
|
101
|
+
const doc = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${num(uw, 4)} ${num(uh, 4)}" preserveAspectRatio="none">${m.defs ? `<defs>${m.defs}</defs>` : ''}${m.body}</svg>`;
|
|
102
|
+
let src;
|
|
103
|
+
try {
|
|
104
|
+
src = await pixelsOf(doc, w, h, /** @type {any} */ (ctx).sharp);
|
|
105
|
+
} catch {
|
|
106
|
+
src = undefined;
|
|
107
|
+
}
|
|
108
|
+
if (!src) return undefined;
|
|
109
|
+
|
|
110
|
+
const ink = rgb(fx.ink ?? '#0b0b0b').map((v) => Math.round(v * 255));
|
|
111
|
+
const paper = rgb(fx.paper ?? '#f4f1ea').map((v) => Math.round(v * 255));
|
|
112
|
+
const contrast = fx.contrast ?? 1.45;
|
|
113
|
+
const o = 0.5 - contrast * 0.5 + (fx.brightness ?? 0.06);
|
|
114
|
+
const [wr, wg, wb] = /** @type {number[]} */ (fx.weights ?? LUMA).map((x) => (x * contrast) / 255);
|
|
115
|
+
const { n, px: t } = thresholds(fx);
|
|
116
|
+
const cellPx = c * ppu;
|
|
117
|
+
const out = new Uint8Array(w * h * 4);
|
|
118
|
+
let opaque = true;
|
|
119
|
+
for (let y = 0; y < h; y++) {
|
|
120
|
+
const ty = (Math.floor(y / cellPx + 1e-6) % n) * n;
|
|
121
|
+
for (let x = 0; x < w; x++) {
|
|
122
|
+
const i = (y * w + x) * 4;
|
|
123
|
+
const a = src[i + 3];
|
|
124
|
+
if (a < 255) opaque = false;
|
|
125
|
+
if (!a) continue;
|
|
126
|
+
// Ink where luminance and threshold fall short of 1 (the filter's discrete step at 0.5 after −0.5).
|
|
127
|
+
const lum = src[i] * wr + src[i + 1] * wg + src[i + 2] * wb + o;
|
|
128
|
+
const col = lum + t[ty + (Math.floor(x / cellPx + 1e-6) % n)] / 255 < 1 ? ink : paper;
|
|
129
|
+
out[i] = col[0]; out[i + 1] = col[1]; out[i + 2] = col[2]; out[i + 3] = 255;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const png = opaque ? await encodePngAsync(rgbOf(out), w, h, 3) : await encodePngAsync(out, w, h, 4);
|
|
133
|
+
const image = `<image${attrs({ x: 0, y: 0, width: num(uw, 4), height: num(uh, 4), preserveAspectRatio: 'none', 'image-rendering': 'pixelated', style: 'image-rendering:pixelated', href: pngUri(png) })}/>`;
|
|
134
|
+
if (opaque) return { defs: '', body: image };
|
|
135
|
+
// Clipped to the layer's own shape (its alpha, at full resolution).
|
|
136
|
+
const region = { x: -uw, y: -uh, width: uw * 3, height: uh * 3 };
|
|
137
|
+
const defs = `${m.defs}<filter${attrs({ id: `${id}w`, filterUnits: 'userSpaceOnUse', ...region })}><feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter>`
|
|
138
|
+
+ `<mask${attrs({ id: `${id}m`, maskUnits: 'userSpaceOnUse', ...region })}><g filter="url(#${id}w)">${m.body}</g></mask>`;
|
|
139
|
+
return { defs, body: `<g mask="url(#${id}m)">${image}</g>` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @param {Uint8Array} rgba @returns {Uint8Array} The RGB channels. */
|
|
143
|
+
function rgbOf(rgba) {
|
|
144
|
+
const out = new Uint8Array((rgba.length / 4) * 3);
|
|
145
|
+
for (let i = 0, j = 0; i < rgba.length; i += 4, j += 3) { out[j] = rgba[i]; out[j + 1] = rgba[i + 1]; out[j + 2] = rgba[i + 2]; }
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `feDropShadow` spelled out as the primitives it stands for, so its `in`
|
|
3
|
+
* is honoured (librsvg ignores `in` on `feDropShadow` and shadows the
|
|
4
|
+
* source graphic, which silently drops whatever the chain built before it).
|
|
5
|
+
*
|
|
6
|
+
* @module effects/drop-shadow
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { attrs } from '../svg.js';
|
|
10
|
+
|
|
11
|
+
/** @param {string} s @returns {Record<string, string>} An element's attributes. */
|
|
12
|
+
function attributes(s) {
|
|
13
|
+
/** @type {Record<string, string>} */
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const m of s.matchAll(/([\w:-]+)\s*=\s*("([^"]*)"|'([^']*)')/g)) out[m[1]] = m[3] ?? m[4];
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Replace every `<feDropShadow>` in filter markup with blur, offset, flood,
|
|
21
|
+
* composite and merge. Without an `in`, it takes the previous result (or
|
|
22
|
+
* the source graphic), exactly as `feDropShadow` does.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} markup - Filter primitives.
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* expandDropShadows('<feDropShadow in="felt" dy="2" stdDeviation="1"/>');
|
|
29
|
+
*/
|
|
30
|
+
export function expandDropShadows(markup) {
|
|
31
|
+
let n = 0;
|
|
32
|
+
return markup.replace(/<feDropShadow\b([^>]*?)\/?>(?:\s*<\/feDropShadow>)?/gi, (_, raw) => {
|
|
33
|
+
const a = attributes(raw);
|
|
34
|
+
const k = `ds${n++}`;
|
|
35
|
+
const shared = { x: a.x, y: a.y, width: a.width, height: a.height, 'color-interpolation-filters': a['color-interpolation-filters'] };
|
|
36
|
+
// An identity offset names the input when none is given (the previous result).
|
|
37
|
+
const input = a.in ?? `${k}i`;
|
|
38
|
+
const pick = a.in ? '' : `<feOffset${attrs({ dx: 0, dy: 0, result: input })}/>`;
|
|
39
|
+
return `${pick}<feGaussianBlur${attrs({ in: input, stdDeviation: a.stdDeviation ?? 2, ...shared })}/>`
|
|
40
|
+
+ `<feOffset${attrs({ dx: a.dx ?? 2, dy: a.dy ?? 2, result: `${k}o`, ...shared })}/>`
|
|
41
|
+
+ `<feFlood${attrs({ 'flood-color': a['flood-color'] ?? '#000', 'flood-opacity': a['flood-opacity'] ?? 1, ...shared })}/>`
|
|
42
|
+
+ `<feComposite${attrs({ in2: `${k}o`, operator: 'in', result: `${k}s`, ...shared })}/>`
|
|
43
|
+
+ `<feMerge${attrs({ result: a.result, ...shared })}><feMergeNode in="${k}s"/><feMergeNode in="${input}"/></feMerge>`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The `extrude` effect.
|
|
2
|
+
import type { RenderContext, Markup } from '../render/context.js';
|
|
3
|
+
|
|
4
|
+
/** A layer drawn again behind itself, a step further each time, in a darker colour. */
|
|
5
|
+
export function extrude(fx: { kind: 'extrude'; depth?: import('../index.js').Length; angle?: number; offset?: [import('../index.js').Length, import('../index.js').Length]; steps?: number; color?: string; to?: string }, m: Markup, ctx: RenderContext, id: string): Markup;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `extrude`: a layer drawn again and again behind itself, a step further
|
|
3
|
+
* each time, in a darker colour: block type and 3D marks in one effect.
|
|
4
|
+
*
|
|
5
|
+
* @module effects/extrude
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { attrs, num } from '../svg.js';
|
|
9
|
+
import { len } from '../geometry.js';
|
|
10
|
+
import { mixOklab, toHex } from '../color.js';
|
|
11
|
+
import { filterElement, flood } from './util.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The extrusion: `steps` copies (default enough for a solid side at the
|
|
15
|
+
* output size, at most 64) along `angle` (degrees, 0 up, 90 right;
|
|
16
|
+
* default 135) to `depth` (a length, default `'3vmin'`), or along `[x, y]`
|
|
17
|
+
* (`offset`). Copies are `color` (default near-black), fading to `to` at
|
|
18
|
+
* the far end; the layer is drawn on top. The markup is written once and
|
|
19
|
+
* reused (`<use>`).
|
|
20
|
+
*
|
|
21
|
+
* @param {any} fx - `{ kind: 'extrude', depth, angle, offset, steps, color, to }`.
|
|
22
|
+
* @param {{ defs: string, body: string }} m
|
|
23
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
24
|
+
* @param {string} id
|
|
25
|
+
* @returns {{ defs: string, body: string }}
|
|
26
|
+
*/
|
|
27
|
+
export function extrude(fx, m, ctx, id) {
|
|
28
|
+
const [ox, oy] = fx.offset
|
|
29
|
+
? [len(fx.offset[0], ctx.canvas), len(fx.offset[1], ctx.canvas)]
|
|
30
|
+
: (() => {
|
|
31
|
+
const d = len(fx.depth, ctx.canvas, '3vmin');
|
|
32
|
+
const a = ((fx.angle ?? 135) * Math.PI) / 180;
|
|
33
|
+
return [Math.sin(a) * d, -Math.cos(a) * d];
|
|
34
|
+
})();
|
|
35
|
+
const reach = Math.hypot(ox, oy);
|
|
36
|
+
const res = /** @type {any} */ (ctx).res ?? 1;
|
|
37
|
+
const steps = Math.max(1, Math.round(fx.steps ?? Math.min(64, Math.max(6, Math.ceil(reach * res)))));
|
|
38
|
+
const near = toHex(fx.color ?? '#141414');
|
|
39
|
+
const far = fx.to ? toHex(fx.to) : near;
|
|
40
|
+
// Colour bands (far first): one recolouring filter per band.
|
|
41
|
+
const bands = Math.min(steps, far === near ? 1 : 8);
|
|
42
|
+
const pad = reach / Math.min(ctx.width, ctx.height) + 0.05;
|
|
43
|
+
let defs = `${m.defs}<g id="${id}s">${m.body}</g>`;
|
|
44
|
+
let body = '';
|
|
45
|
+
for (let b = 0; b < bands; b++) {
|
|
46
|
+
const from = Math.floor((b * steps) / bands);
|
|
47
|
+
const to = Math.floor(((b + 1) * steps) / bands);
|
|
48
|
+
const color = mixOklab(far, near, bands === 1 ? 1 : b / (bands - 1));
|
|
49
|
+
defs += filterElement(`${id}e${b}`, ctx, pad, `<feColorMatrix in="SourceGraphic" type="matrix" values="${flood(color)}"/>`);
|
|
50
|
+
let copies = '';
|
|
51
|
+
for (let i = steps - from; i > steps - to; i--) copies += `<use${attrs({ href: `#${id}s`, transform: `translate(${num((ox * i) / steps, 3)} ${num((oy * i) / steps, 3)})` })}/>`;
|
|
52
|
+
body += `<g filter="url(#${id}e${b})">${copies}</g>`;
|
|
53
|
+
}
|
|
54
|
+
return { defs, body: `${body}<use href="#${id}s"/>` };
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Film grain, paper, dust and scratches.
|
|
2
|
+
import type { Effect } from './index.js';
|
|
3
|
+
import type { RenderContext } from '../render/context.js';
|
|
4
|
+
import type { Pixels } from './buffer.js';
|
|
5
|
+
|
|
6
|
+
/** The `grain` effect's filter primitives. */
|
|
7
|
+
export function grain(fx: Extract<Effect, { kind: 'grain' }>, ctx: RenderContext): string;
|
|
8
|
+
/** The `paper` effect's filter primitives. */
|
|
9
|
+
export function paper(fx: Extract<Effect, { kind: 'paper' }>, ctx: RenderContext): string;
|
|
10
|
+
/** `dust`, on pixels. */
|
|
11
|
+
export function dust(img: Pixels, fx: Extract<Effect, { kind: 'dust' }>, ctx: object): Uint8ClampedArray;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Film and paper: `grain` (film grain as a filter, overlaid so it lives in
|
|
3
|
+
* the midtones as real grain does; monochrome or colour), `paper` (the
|
|
4
|
+
* layer printed on paper or canvas: a lit relief overlaid) and `dust`
|
|
5
|
+
* (specks, hairs and scratches, seeded, drawn on pixels).
|
|
6
|
+
*
|
|
7
|
+
* @module effects/film
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { attrs, num } from '../svg.js';
|
|
11
|
+
import { len } from '../geometry.js';
|
|
12
|
+
import { encodePng, pngUri } from '../png.js';
|
|
13
|
+
import { createRng } from '../rng.js';
|
|
14
|
+
import { mat, seedOf } from './util.js';
|
|
15
|
+
import { px, seedFor } from './buffer.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `grain`: film grain, `amount` (0–1), `size` (a length: the grain), and
|
|
19
|
+
* `color` (true: each channel its own grain, as colour film; default
|
|
20
|
+
* monochrome). Overlaid, so it's strongest in the midtones.
|
|
21
|
+
*
|
|
22
|
+
* @param {import('./index.js').Effect & { kind: 'grain' }} fx
|
|
23
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function grain(fx, ctx) {
|
|
27
|
+
const size = Math.max(0.05, len(fx.size, ctx.canvas, '0.09vmin'));
|
|
28
|
+
const a = (fx.amount ?? 0.35) * 1.5;
|
|
29
|
+
const mono = !fx.color;
|
|
30
|
+
// Noise centred on mid-grey with its spread set by amount.
|
|
31
|
+
const row = (/** @type {number} */ c) => (mono ? [a, 0, 0, 0, 0.5 - a / 2] : [c === 0 ? a : 0, c === 1 ? a : 0, c === 2 ? a : 0, 0, 0.5 - a / 2]);
|
|
32
|
+
return `<feTurbulence type="fractalNoise" baseFrequency="${num(0.5 / size, 4)}" numOctaves="2" seed="${seedOf(ctx, fx)}" result="gn"/>`
|
|
33
|
+
+ `<feColorMatrix in="gn" type="matrix" values="${mat([...row(0), ...row(1), ...row(2), 0, 0, 0, 0, 1])}" result="gc"/>`
|
|
34
|
+
+ '<feBlend in="gc" in2="SourceGraphic" mode="overlay"/><feComposite in2="SourceAlpha" operator="in"/>';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @type {Map<string, string>} */
|
|
38
|
+
const WEAVES = new Map();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A tile of canvas weave as heights: over-under threads, 2×2 to the tile.
|
|
42
|
+
*
|
|
43
|
+
* @returns {string} A data URI.
|
|
44
|
+
*/
|
|
45
|
+
function weave() {
|
|
46
|
+
const hit = WEAVES.get('w');
|
|
47
|
+
if (hit) return hit;
|
|
48
|
+
const n = 32;
|
|
49
|
+
const out = new Uint8Array(n * n);
|
|
50
|
+
for (let y = 0; y < n; y++) {
|
|
51
|
+
for (let x = 0; x < n; x++) {
|
|
52
|
+
const u = (x + 0.5) / n;
|
|
53
|
+
const v = (y + 0.5) / n;
|
|
54
|
+
// Warp and weft threads, each a rounded ridge; which is on top alternates.
|
|
55
|
+
const warp = Math.cos(Math.PI * ((u * 2) % 1 - 0.5)) ** 0.6;
|
|
56
|
+
const weft = Math.cos(Math.PI * ((v * 2) % 1 - 0.5)) ** 0.6;
|
|
57
|
+
const top = (Math.floor(u * 2) + Math.floor(v * 2)) % 2 === 0;
|
|
58
|
+
out[y * n + x] = Math.round(255 * (top ? 0.25 + 0.75 * warp * (0.6 + 0.4 * weft) : 0.25 + 0.75 * weft * (0.6 + 0.4 * warp)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const uri = pngUri(encodePng(out, n, n, 1));
|
|
62
|
+
WEAVES.set('w', uri);
|
|
63
|
+
return uri;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `paper`: printed on a surface. `texture` `'paper'` (fibrous, uneven) or
|
|
68
|
+
* `'canvas'` (a woven tooth), `scale` (a length: the fibres, or a thread
|
|
69
|
+
* pair), `amount` (0–1), lit from `angle`; the lit relief is overlaid.
|
|
70
|
+
*
|
|
71
|
+
* @param {import('./index.js').Effect & { kind: 'paper' }} fx
|
|
72
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function paper(fx, ctx) {
|
|
76
|
+
const texture = fx.texture ?? 'paper';
|
|
77
|
+
if (texture !== 'paper' && texture !== 'canvas') throw new TypeError(`Unknown paper texture "${texture}" (paper, canvas)`);
|
|
78
|
+
const amount = fx.amount ?? 0.5;
|
|
79
|
+
const az = fx.angle ?? 225;
|
|
80
|
+
let relief;
|
|
81
|
+
if (texture === 'canvas') {
|
|
82
|
+
const s = len(fx.scale, ctx.canvas, '0.9vmin');
|
|
83
|
+
relief = `<feImage${attrs({ href: weave(), x: 0, y: 0, width: s, height: s, preserveAspectRatio: 'none', result: 'pt' })}/><feTile in="pt"/>`
|
|
84
|
+
+ '<feColorMatrix type="luminanceToAlpha" color-interpolation-filters="sRGB" result="ph"/>';
|
|
85
|
+
} else {
|
|
86
|
+
const s = len(fx.scale, ctx.canvas, '0.45vmin');
|
|
87
|
+
relief = `<feTurbulence type="fractalNoise" baseFrequency="${num(1 / s, 4)} ${num(0.6 / s, 4)}" numOctaves="4" seed="${seedOf(ctx, fx)}"/>`
|
|
88
|
+
+ '<feColorMatrix type="luminanceToAlpha" color-interpolation-filters="sRGB" result="ph"/>';
|
|
89
|
+
}
|
|
90
|
+
const depth = texture === 'canvas' ? 3 : 2.5;
|
|
91
|
+
// Lit flat, the surface is sin(elevation) grey; that maps to 0.5 (no change under overlay).
|
|
92
|
+
const elev = 50;
|
|
93
|
+
const flat = Math.sin((elev * Math.PI) / 180);
|
|
94
|
+
const k = (texture === 'canvas' ? 0.8 : 0.7) * amount;
|
|
95
|
+
const row = (/** @type {number} */ c) => [c === 0 ? k / flat : 0, c === 1 ? k / flat : 0, c === 2 ? k / flat : 0, 0, 0.5 - k];
|
|
96
|
+
return relief
|
|
97
|
+
+ `<feDiffuseLighting in="ph" surfaceScale="${num(depth, 3)}" diffuseConstant="1" lighting-color="#fff" color-interpolation-filters="sRGB" result="pl"><feDistantLight azimuth="${num(az)}" elevation="${elev}"/></feDiffuseLighting>`
|
|
98
|
+
// Lit ridges lighten, shaded ones darken: the relief overlaid.
|
|
99
|
+
+ `<feColorMatrix in="pl" type="matrix" values="${mat([...row(0), ...row(1), ...row(2), 0, 0, 0, 0, 1])}" result="pm"/>`
|
|
100
|
+
+ '<feBlend in="pm" in2="SourceGraphic" mode="overlay"/><feComposite in2="SourceAlpha" operator="in"/>';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stamp an anti-aliased stroke (a polyline of `[x, y]` points) or speck
|
|
105
|
+
* into a coverage buffer.
|
|
106
|
+
*
|
|
107
|
+
* @param {Float32Array} cov
|
|
108
|
+
* @param {number} w
|
|
109
|
+
* @param {number} h
|
|
110
|
+
* @param {[number, number][]} pts
|
|
111
|
+
* @param {number} r - Half-width, px.
|
|
112
|
+
* @param {number} o - Opacity.
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
115
|
+
function stroke(cov, w, h, pts, r, o) {
|
|
116
|
+
for (let s = 0; s < pts.length - (pts.length > 1 ? 1 : 0); s++) {
|
|
117
|
+
const [x0, y0] = pts[s];
|
|
118
|
+
const [x1, y1] = pts.length > 1 ? pts[s + 1] : pts[s];
|
|
119
|
+
const dx = x1 - x0;
|
|
120
|
+
const dy = y1 - y0;
|
|
121
|
+
const l2 = dx * dx + dy * dy || 1e-9;
|
|
122
|
+
const bx0 = Math.max(0, Math.floor(Math.min(x0, x1) - r - 1));
|
|
123
|
+
const bx1 = Math.min(w - 1, Math.ceil(Math.max(x0, x1) + r + 1));
|
|
124
|
+
const by0 = Math.max(0, Math.floor(Math.min(y0, y1) - r - 1));
|
|
125
|
+
const by1 = Math.min(h - 1, Math.ceil(Math.max(y0, y1) + r + 1));
|
|
126
|
+
for (let y = by0; y <= by1; y++) {
|
|
127
|
+
for (let x = bx0; x <= bx1; x++) {
|
|
128
|
+
const t = Math.max(0, Math.min(1, ((x + 0.5 - x0) * dx + (y + 0.5 - y0) * dy) / l2));
|
|
129
|
+
const d = Math.hypot(x + 0.5 - x0 - t * dx, y + 0.5 - y0 - t * dy);
|
|
130
|
+
const c = Math.max(0, Math.min(1, r + 0.5 - d)) * o;
|
|
131
|
+
const i = y * w + x;
|
|
132
|
+
if (c > cov[i]) cov[i] = c;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `dust`: dust and scratches on film, seeded: specks (`amount`, 0–1),
|
|
140
|
+
* curling hairs, and `scratches` running down the frame; `color`
|
|
141
|
+
* `'light'` (dust on a negative prints white), `'dark'`, or `'mixed'`.
|
|
142
|
+
*
|
|
143
|
+
* @param {import('./buffer.js').Pixels} img
|
|
144
|
+
* @param {import('./index.js').Effect & { kind: 'dust' }} fx
|
|
145
|
+
* @param {any} ctx
|
|
146
|
+
* @returns {Uint8ClampedArray}
|
|
147
|
+
*/
|
|
148
|
+
export function dust(img, fx, ctx) {
|
|
149
|
+
const { data, width: w, height: h } = img;
|
|
150
|
+
const rng = createRng(`dust:${seedFor(fx, ctx)}`);
|
|
151
|
+
const unit = px('0.1vmin', ctx, img, 0);
|
|
152
|
+
const amount = fx.amount ?? 0.5;
|
|
153
|
+
const mode = fx.color ?? 'mixed';
|
|
154
|
+
if (!['light', 'dark', 'mixed'].includes(mode)) throw new TypeError(`Unknown dust color "${mode}" (light, dark, mixed)`);
|
|
155
|
+
const light = new Float32Array(w * h);
|
|
156
|
+
const dark = new Float32Array(w * h);
|
|
157
|
+
const pick = () => (mode === 'light' ? light : mode === 'dark' ? dark : rng.chance(0.7) ? light : dark);
|
|
158
|
+
const specks = Math.round(amount * 260);
|
|
159
|
+
for (let i = 0; i < specks; i++) {
|
|
160
|
+
const x = rng.range(0, w);
|
|
161
|
+
const y = rng.range(0, h);
|
|
162
|
+
const r = unit * rng.range(0.3, 1) * (rng.chance(0.08) ? 3.5 : 1);
|
|
163
|
+
// A clump of two or three dots, so specks aren't perfect circles.
|
|
164
|
+
const buf = pick();
|
|
165
|
+
const o = rng.range(0.35, 0.95);
|
|
166
|
+
for (let k = 0, n = rng.int(1, 3); k < n; k++) stroke(buf, w, h, [[x + rng.range(-r, r), y + rng.range(-r, r)]], r * rng.range(0.5, 1), o);
|
|
167
|
+
}
|
|
168
|
+
const hairs = Math.round(amount * 9);
|
|
169
|
+
for (let i = 0; i < hairs; i++) {
|
|
170
|
+
/** @type {[number, number][]} */
|
|
171
|
+
const pts = [[rng.range(0, w), rng.range(0, h)]];
|
|
172
|
+
let a = rng.range(0, Math.PI * 2);
|
|
173
|
+
const seg = unit * rng.range(3, 7);
|
|
174
|
+
for (let k = 0, n = rng.int(12, 40); k < n; k++) {
|
|
175
|
+
a += rng.range(-0.35, 0.35);
|
|
176
|
+
const [px0, py0] = pts[pts.length - 1];
|
|
177
|
+
pts.push([px0 + Math.cos(a) * seg, py0 + Math.sin(a) * seg]);
|
|
178
|
+
}
|
|
179
|
+
stroke(pick(), w, h, pts, unit * rng.range(0.25, 0.5), rng.range(0.5, 0.9));
|
|
180
|
+
}
|
|
181
|
+
const scratches = fx.scratches ?? Math.round(amount * 4);
|
|
182
|
+
for (let i = 0; i < scratches; i++) {
|
|
183
|
+
let x = rng.range(0, w);
|
|
184
|
+
const y0 = rng.chance(0.5) ? 0 : rng.range(0, h * 0.6);
|
|
185
|
+
const y1 = rng.chance(0.5) ? h : rng.range(y0 + h * 0.2, h);
|
|
186
|
+
/** @type {[number, number][]} */
|
|
187
|
+
const pts = [];
|
|
188
|
+
for (let y = y0; y <= y1; y += h / 24) { pts.push([x, y]); x += rng.range(-0.6, 0.6) * unit; }
|
|
189
|
+
stroke(rng.chance(0.75) ? light : dark, w, h, pts, unit * rng.range(0.15, 0.4), rng.range(0.3, 0.7));
|
|
190
|
+
}
|
|
191
|
+
const out = new Uint8ClampedArray(data);
|
|
192
|
+
for (let p = 0, i = 0; p < light.length; p++, i += 4) {
|
|
193
|
+
const l = light[p];
|
|
194
|
+
const d = dark[p];
|
|
195
|
+
if (!l && !d) continue;
|
|
196
|
+
for (let c = 0; c < 3; c++) {
|
|
197
|
+
let v = out[i + c];
|
|
198
|
+
v += (250 - v) * l;
|
|
199
|
+
v -= (v - 12) * d;
|
|
200
|
+
out[i + c] = v;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Light spreading from the bright parts.
|
|
2
|
+
import type { Effect } from './index.js';
|
|
3
|
+
import type { RenderContext } from '../render/context.js';
|
|
4
|
+
|
|
5
|
+
/** The `bloom` effect's filter primitives. */
|
|
6
|
+
export function bloom(fx: Extract<Effect, { kind: 'bloom' }>, ctx: RenderContext): string;
|
|
7
|
+
/** The `halation` effect's filter primitives. */
|
|
8
|
+
export function halation(fx: Extract<Effect, { kind: 'halation' }>, ctx: RenderContext): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light that spreads from the bright parts, as SVG filters: `bloom` (the
|
|
3
|
+
* highlights glowing, at two scales, screened over the layer) and
|
|
4
|
+
* `halation` (film's red-orange halo round bright edges, where light
|
|
5
|
+
* bounced off the film base back through the red layer).
|
|
6
|
+
*
|
|
7
|
+
* @module effects/glow
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { num } from '../svg.js';
|
|
11
|
+
import { len } from '../geometry.js';
|
|
12
|
+
import { LUMA, rgb, mat } from './util.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `bloom`: what's brighter than `threshold` (0–1), blurred to `radius`
|
|
16
|
+
* and to three times that, screened over the layer at `intensity`, tinted
|
|
17
|
+
* by `color` when given. It spreads past the layer's edges (neon type).
|
|
18
|
+
*
|
|
19
|
+
* @param {import('./index.js').Effect & { kind: 'bloom' }} fx
|
|
20
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function bloom(fx, ctx) {
|
|
24
|
+
const t = Math.min(0.99, fx.threshold ?? 0.65);
|
|
25
|
+
const r = len(fx.radius, ctx.canvas, '1.6vmin');
|
|
26
|
+
const k = fx.intensity ?? 0.9;
|
|
27
|
+
const slope = 1 / (1 - t);
|
|
28
|
+
const tint = fx.color ? rgb(fx.color) : [1, 1, 1];
|
|
29
|
+
return `<feComponentTransfer in="SourceGraphic">${['R', 'G', 'B'].map((ch, i) => `<feFunc${ch} type="linear" slope="${num(slope * tint[i], 4)}" intercept="${num(-t * slope * tint[i], 4)}"/>`).join('')}</feComponentTransfer>`
|
|
30
|
+
+ '<feComposite in2="SourceAlpha" operator="in" result="bh"/>'
|
|
31
|
+
+ `<feGaussianBlur in="bh" stdDeviation="${num(r)}" result="b1"/><feGaussianBlur in="bh" stdDeviation="${num(r * 3)}" result="b2"/>`
|
|
32
|
+
+ `<feComposite in="b1" in2="b2" operator="arithmetic" k2="${num(0.6 * k, 3)}" k3="${num(0.7 * k, 3)}" result="bg"/>`
|
|
33
|
+
+ '<feBlend in="bg" in2="SourceGraphic" mode="screen"/>';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `halation`: the highlights above `threshold`, flooded with `color`
|
|
38
|
+
* (default film's red-orange), blurred to `radius` and screened on at
|
|
39
|
+
* `intensity`.
|
|
40
|
+
*
|
|
41
|
+
* @param {import('./index.js').Effect & { kind: 'halation' }} fx
|
|
42
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function halation(fx, ctx) {
|
|
46
|
+
const t = Math.min(0.99, fx.threshold ?? 0.72);
|
|
47
|
+
const [r, g, b] = rgb(fx.color ?? '#ff3d14');
|
|
48
|
+
const k = 1 / (1 - t);
|
|
49
|
+
const i = fx.intensity ?? 1;
|
|
50
|
+
return `<feColorMatrix in="SourceGraphic" type="matrix" values="${mat([0, 0, 0, 0, r, 0, 0, 0, 0, g, 0, 0, 0, 0, b, LUMA[0] * k, LUMA[1] * k, LUMA[2] * k, 0, -t * k])}"/>`
|
|
51
|
+
+ '<feComposite in2="SourceAlpha" operator="in"/>'
|
|
52
|
+
+ `<feGaussianBlur stdDeviation="${num(len(fx.radius, ctx.canvas, '0.9vmin'))}"/>`
|
|
53
|
+
+ `<feComponentTransfer><feFuncA type="linear" slope="${num(i, 3)}"/></feComponentTransfer>`
|
|
54
|
+
+ '<feComposite in2="SourceAlpha" operator="in" result="hl"/>'
|
|
55
|
+
+ '<feBlend in="hl" in2="SourceGraphic" mode="screen"/>';
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Colour grading.
|
|
2
|
+
import type { Effect } from './index.js';
|
|
3
|
+
import type { Pixels } from './buffer.js';
|
|
4
|
+
|
|
5
|
+
/** A monotone cubic through points, sampled as a table. */
|
|
6
|
+
export function curveTable(points: [number, number][], n?: number): number[];
|
|
7
|
+
/** A component transfer from three tables (R, G, B). */
|
|
8
|
+
export function transfer(tables: number[][]): string;
|
|
9
|
+
/** A gradient's colour at `t`, 0–1 channels. */
|
|
10
|
+
export function sampleGradient(colors: string[], stops: number[], t: number, space: 'srgb' | 'oklab'): [number, number, number];
|
|
11
|
+
/** A colour balance's tables, per channel. */
|
|
12
|
+
export function balanceTables(fx: { shadows?: number[]; midtones?: number[]; highlights?: number[] }): number[][];
|
|
13
|
+
/** The `curves` effect's filter primitives. */
|
|
14
|
+
export function curves(fx: Extract<Effect, { kind: 'curves' }>): string;
|
|
15
|
+
/** The `gradientMap` effect's filter primitives. */
|
|
16
|
+
export function gradientMap(fx: Extract<Effect, { kind: 'gradientMap' }>): string;
|
|
17
|
+
/** The `posterize` effect's filter primitives. */
|
|
18
|
+
export function posterize(fx: Extract<Effect, { kind: 'posterize' }>): string;
|
|
19
|
+
/** The `invert` effect's filter primitives. */
|
|
20
|
+
export function invert(fx: Extract<Effect, { kind: 'invert' }>): string;
|
|
21
|
+
/** The `hue` effect's filter primitives. */
|
|
22
|
+
export function hue(fx: Extract<Effect, { kind: 'hue' }>): string;
|
|
23
|
+
/** The `colorBalance` effect's filter primitives. */
|
|
24
|
+
export function colorBalance(fx: Extract<Effect, { kind: 'colorBalance' }>): string;
|
|
25
|
+
/** The `channelMixer` effect's filter primitives. */
|
|
26
|
+
export function channelMixer(fx: Extract<Effect, { kind: 'channelMixer' }>): string;
|
|
27
|
+
/** The `sepia` effect's filter primitives. */
|
|
28
|
+
export function sepia(fx: Extract<Effect, { kind: 'sepia' }>): string;
|
|
29
|
+
/** `vibrance`, on pixels. */
|
|
30
|
+
export function vibrance(img: Pixels, fx: Extract<Effect, { kind: 'vibrance' }>): Uint8ClampedArray;
|