@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,10 @@
|
|
|
1
|
+
// Light on the lens and the film.
|
|
2
|
+
import type { Effect } from './index.js';
|
|
3
|
+
import type { RenderContext } from '../render/context.js';
|
|
4
|
+
|
|
5
|
+
/** The `vignette` effect's filter primitives. */
|
|
6
|
+
export function vignette(fx: Extract<Effect, { kind: 'vignette' }>, ctx: RenderContext): string;
|
|
7
|
+
/** The `lightLeak` effect's filter primitives. */
|
|
8
|
+
export function lightLeak(fx: Extract<Effect, { kind: 'lightLeak' }>, ctx: RenderContext): string;
|
|
9
|
+
/** The `lensFlare` effect's filter primitives. */
|
|
10
|
+
export function lensFlare(fx: Extract<Effect, { kind: 'lensFlare' }>, ctx: RenderContext): string;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light on the lens and the film, as SVG filters: `vignette` (edges
|
|
3
|
+
* darkened, or lightened, by shape, size, softness and colour),
|
|
4
|
+
* `lightLeak` (warm light fogging the film from an edge) and `lensFlare`
|
|
5
|
+
* (a soft glow at the light, ghosts along the line through the centre,
|
|
6
|
+
* an optional anamorphic streak).
|
|
7
|
+
*
|
|
8
|
+
* Their light is drawn from small greyscale images (a radial falloff
|
|
9
|
+
* computed here) that the filter scales and tints, so they stay smooth at
|
|
10
|
+
* any size and cost no more at 3000 px than at 300.
|
|
11
|
+
*
|
|
12
|
+
* @module effects/optics
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { attrs, num } from '../svg.js';
|
|
16
|
+
import { len } from '../geometry.js';
|
|
17
|
+
import { encodePng, pngUri } from '../png.js';
|
|
18
|
+
import { createRng } from '../rng.js';
|
|
19
|
+
import { rgb, mat } from './util.js';
|
|
20
|
+
import { fraction, smoothstep } from './buffer.js';
|
|
21
|
+
|
|
22
|
+
/** @type {Map<string, string>} */
|
|
23
|
+
const SPRITES = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A greyscale sprite from a radial profile, cached: `profile(r)` for r
|
|
27
|
+
* from 0 (centre) to 1 (edge of the square).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} key
|
|
30
|
+
* @param {(r: number) => number} profile
|
|
31
|
+
* @param {number} [size=96]
|
|
32
|
+
* @param {number} [aspect=1] - Width over height of the sprite's pixels.
|
|
33
|
+
* @returns {string} A data URI.
|
|
34
|
+
*/
|
|
35
|
+
function sprite(key, profile, size = 96, aspect = 1) {
|
|
36
|
+
const id = `${key}:${size}:${aspect}`;
|
|
37
|
+
const hit = SPRITES.get(id);
|
|
38
|
+
if (hit) return hit;
|
|
39
|
+
const w = Math.round(size * Math.max(1, aspect));
|
|
40
|
+
const h = Math.round(size / Math.min(1, aspect));
|
|
41
|
+
const px = new Uint8Array(w * h);
|
|
42
|
+
for (let y = 0; y < h; y++) {
|
|
43
|
+
for (let x = 0; x < w; x++) {
|
|
44
|
+
const r = Math.hypot(((x + 0.5) / w) * 2 - 1, ((y + 0.5) / h) * 2 - 1);
|
|
45
|
+
px[y * w + x] = Math.round(255 * Math.min(1, Math.max(0, profile(r))));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const uri = pngUri(encodePng(px, w, h, 1));
|
|
49
|
+
SPRITES.set(id, uri);
|
|
50
|
+
return uri;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The sprites flares and leaks are drawn from. */
|
|
54
|
+
const GLOW = () => sprite('glow', (r) => Math.exp(-6 * r * r) * (1 - smoothstep(0.85, 1, r)));
|
|
55
|
+
const GHOST = () => sprite('ghost', (r) => (1 - smoothstep(0.78, 0.96, r)) * (0.55 + 0.45 * smoothstep(0.3, 0.9, r)));
|
|
56
|
+
const RING = () => sprite('ring', (r) => Math.exp(-(((r - 0.86) / 0.05) ** 2)));
|
|
57
|
+
const SOFT = () => sprite('soft', (r) => 1 - smoothstep(0, 1, r));
|
|
58
|
+
const STREAK = () => sprite('streak', (r) => Math.exp(-5 * r * r) * (1 - smoothstep(0.9, 1, r)), 64, 8);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One sprite placed and tinted: an feImage over a box, its grey becoming
|
|
62
|
+
* the alpha of a colour (light, for adding up).
|
|
63
|
+
*
|
|
64
|
+
* @param {string} href
|
|
65
|
+
* @param {{ x: number, y: number, width: number, height: number }} box
|
|
66
|
+
* @param {[number, number, number]} c - 0–1 channels.
|
|
67
|
+
* @param {number} o - Opacity.
|
|
68
|
+
* @param {string} result
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function light(href, box, c, o, result) {
|
|
72
|
+
return `<feImage${attrs({ href, ...box, preserveAspectRatio: 'none' })}/>`
|
|
73
|
+
+ `<feColorMatrix type="matrix" values="${mat([0, 0, 0, 0, c[0], 0, 0, 0, 0, c[1], 0, 0, 0, 0, c[2], o, 0, 0, 0, 0])}" color-interpolation-filters="sRGB" result="${result}"/>`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Lights added together, screened over the layer and kept to its shape.
|
|
78
|
+
*
|
|
79
|
+
* @param {string[]} names - Their results.
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
function screenOn(names) {
|
|
83
|
+
let out = '';
|
|
84
|
+
let acc = names[0];
|
|
85
|
+
for (let i = 1; i < names.length; i++) {
|
|
86
|
+
out += `<feComposite in="${acc}" in2="${names[i]}" operator="arithmetic" k2="1" k3="1" result="la${i}"/>`;
|
|
87
|
+
acc = `la${i}`;
|
|
88
|
+
}
|
|
89
|
+
return `${out}<feBlend in="${acc}" in2="SourceGraphic" mode="screen"/><feComposite in2="SourceAlpha" operator="in"/>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `vignette`: `shape` (`'ellipse'` following the canvas, `'circle'`, or
|
|
94
|
+
* `'rect'`, a soft rounded rectangle), `size` (where it starts, 0–1 of
|
|
95
|
+
* the way to the corners), `softness` (0–1), `amount` (0–1) and `color`
|
|
96
|
+
* (black; white for a faded print), about `center`.
|
|
97
|
+
*
|
|
98
|
+
* @param {import('./index.js').Effect & { kind: 'vignette' }} fx
|
|
99
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function vignette(fx, ctx) {
|
|
103
|
+
const { width: W, height: H } = ctx;
|
|
104
|
+
const shape = fx.shape ?? 'ellipse';
|
|
105
|
+
if (!['ellipse', 'circle', 'rect'].includes(shape)) throw new TypeError(`Unknown vignette shape "${shape}" (ellipse, circle, rect)`);
|
|
106
|
+
const size = fx.size ?? 0.55;
|
|
107
|
+
const soft = Math.max(0.02, fx.softness ?? 0.6);
|
|
108
|
+
const [cx, cy] = fraction(/** @type {any} */ (fx.center), [0.5, 0.5]);
|
|
109
|
+
// The image covers the canvas; distance 1 is a corner (from the centre).
|
|
110
|
+
const n = 128;
|
|
111
|
+
const ax = shape === 'circle' ? Math.max(W, H) / Math.hypot(W, H) * Math.SQRT2 : 1;
|
|
112
|
+
const px = new Uint8Array(n * n);
|
|
113
|
+
for (let j = 0; j < n; j++) {
|
|
114
|
+
for (let i = 0; i < n; i++) {
|
|
115
|
+
let u = ((i + 0.5) / n - cx) * 2;
|
|
116
|
+
let v = ((j + 0.5) / n - cy) * 2;
|
|
117
|
+
if (shape === 'circle') { u *= (W / Math.max(W, H)) * ax; v *= (H / Math.max(W, H)) * ax; }
|
|
118
|
+
const d = shape === 'rect' ? (Math.abs(u) ** 6 + Math.abs(v) ** 6) ** (1 / 6) : Math.hypot(u, v) / Math.SQRT2;
|
|
119
|
+
px[j * n + i] = Math.round(255 * smoothstep(size, size + soft * (1.15 - size) + 0.05, d));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const [r, g, b] = rgb(fx.color ?? '#000000');
|
|
123
|
+
const a = fx.amount ?? 0.6;
|
|
124
|
+
return `<feImage${attrs({ href: pngUri(encodePng(px, n, n, 1)), x: 0, y: 0, width: W, height: H, preserveAspectRatio: 'none' })}/>`
|
|
125
|
+
+ `<feColorMatrix type="matrix" values="${mat([0, 0, 0, 0, r, 0, 0, 0, 0, g, 0, 0, 0, 0, b, a, 0, 0, 0, 0])}" color-interpolation-filters="sRGB" result="vg"/>`
|
|
126
|
+
+ '<feComposite in="vg" in2="SourceGraphic" operator="atop"/>';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `lightLeak`: warm light fogging the film from an `edge` (`'left'`,
|
|
131
|
+
* `'right'`, `'top'`, `'bottom'`, or seeded), a few soft blobs in
|
|
132
|
+
* `colors`, screened on at `intensity`.
|
|
133
|
+
*
|
|
134
|
+
* @param {import('./index.js').Effect & { kind: 'lightLeak' }} fx
|
|
135
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
export function lightLeak(fx, ctx) {
|
|
139
|
+
const { width: W, height: H } = ctx;
|
|
140
|
+
const rng = createRng(`leak:${fx.seed ?? ctx.rng.int(1, 9999)}`);
|
|
141
|
+
const edge = fx.edge ?? rng.pick(['left', 'right', 'top', 'bottom']);
|
|
142
|
+
if (!['left', 'right', 'top', 'bottom'].includes(edge)) throw new TypeError(`Unknown lightLeak edge "${edge}" (left, right, top, bottom)`);
|
|
143
|
+
const colors = (fx.colors ?? ['#ff4d1a', '#ff9a2e', '#ffd36b', '#ff2a5c']).map(rgb);
|
|
144
|
+
const k = fx.intensity ?? 0.85;
|
|
145
|
+
const across = edge === 'left' || edge === 'right';
|
|
146
|
+
const long = across ? H : W;
|
|
147
|
+
const short = across ? W : H;
|
|
148
|
+
const count = 3 + rng.int(0, 2);
|
|
149
|
+
let out = '';
|
|
150
|
+
const names = [];
|
|
151
|
+
for (let i = 0; i < count; i++) {
|
|
152
|
+
const along = rng.range(-0.1, 1.1) * long;
|
|
153
|
+
const depth = rng.range(-0.18, 0.12) * short;
|
|
154
|
+
const rl = rng.range(0.35, 0.75) * long;
|
|
155
|
+
const rs = rng.range(0.25, 0.55) * short * (i === 0 ? 1.4 : 1);
|
|
156
|
+
const x = across ? (edge === 'left' ? depth : W - depth) : along;
|
|
157
|
+
const y = across ? along : (edge === 'top' ? depth : H - depth);
|
|
158
|
+
const box = across ? { x: x - rs, y: y - rl, width: 2 * rs, height: 2 * rl } : { x: x - rl, y: y - rs, width: 2 * rl, height: 2 * rs };
|
|
159
|
+
out += light(SOFT(), box, colors[i % colors.length], k * rng.range(0.55, 1), `lk${i}`);
|
|
160
|
+
names.push(`lk${i}`);
|
|
161
|
+
}
|
|
162
|
+
// The hot core at the very edge.
|
|
163
|
+
const core = across ? { x: (edge === 'left' ? 0 : W) - short * 0.12, y: rng.range(0.1, 0.6) * H, width: short * 0.24, height: H * 0.5 } : { x: rng.range(0.1, 0.6) * W, y: (edge === 'top' ? 0 : H) - short * 0.12, width: W * 0.5, height: short * 0.24 };
|
|
164
|
+
out += light(GLOW(), core, [1, 0.85, 0.62], k * 0.55, 'lkc');
|
|
165
|
+
names.push('lkc');
|
|
166
|
+
return out + screenOn(names);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `lensFlare`: a light at `at` (fractions), its glow (`size`), a faint
|
|
171
|
+
* halo ring, ghosts along the line through the centre (seeded sizes and
|
|
172
|
+
* tints), an anamorphic `streak` when asked, all at `intensity` and
|
|
173
|
+
* tinted toward `color`.
|
|
174
|
+
*
|
|
175
|
+
* @param {import('./index.js').Effect & { kind: 'lensFlare' }} fx
|
|
176
|
+
* @param {import('../render/context.js').RenderContext} ctx
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
export function lensFlare(fx, ctx) {
|
|
180
|
+
const { width: W, height: H } = ctx;
|
|
181
|
+
const rng = createRng(`flare:${fx.seed ?? ctx.rng.int(1, 9999)}`);
|
|
182
|
+
const [fx0, fy0] = fraction(/** @type {any} */ (fx.at), [0.78, 0.22]);
|
|
183
|
+
const sx = fx0 * W;
|
|
184
|
+
const sy = fy0 * H;
|
|
185
|
+
const size = len(fx.size, ctx.canvas, '22vmin');
|
|
186
|
+
const k = fx.intensity ?? 0.8;
|
|
187
|
+
const tint = rgb(fx.color ?? '#ffd9a8');
|
|
188
|
+
/** @param {number} r @returns {[number, number, number]} A tint nudged toward a hue. */
|
|
189
|
+
const nudge = (r) => /** @type {[number, number, number]} */ (tint.map((v, i) => Math.min(1, v * (0.7 + 0.6 * [r, 1 - r, 0.5 + r / 2][i]))));
|
|
190
|
+
const box = (/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ r) => ({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });
|
|
191
|
+
let out = light(GLOW(), box(sx, sy, size), tint, k, 'f0');
|
|
192
|
+
const names = ['f0'];
|
|
193
|
+
out += light(GLOW(), box(sx, sy, size * 0.22), [1, 1, 1], k, 'f1');
|
|
194
|
+
names.push('f1');
|
|
195
|
+
out += light(RING(), box(sx, sy, size * 1.6), nudge(0.7), k * 0.12, 'f2');
|
|
196
|
+
names.push('f2');
|
|
197
|
+
// Ghosts: along the line from the light through the centre and beyond.
|
|
198
|
+
const dx = W / 2 - sx;
|
|
199
|
+
const dy = H / 2 - sy;
|
|
200
|
+
const ghosts = fx.ghosts ?? 6;
|
|
201
|
+
for (let i = 0; i < ghosts; i++) {
|
|
202
|
+
const t = [0.35, 0.62, 0.9, 1.25, 1.55, 1.9, 2.2, 0.15][i % 8] + rng.range(-0.05, 0.05);
|
|
203
|
+
const r = size * rng.range(0.07, 0.32);
|
|
204
|
+
out += light(GHOST(), box(sx + dx * t, sy + dy * t, r), nudge(rng.next()), k * rng.range(0.05, 0.16), `g${i}`);
|
|
205
|
+
names.push(`g${i}`);
|
|
206
|
+
}
|
|
207
|
+
if (fx.streak) {
|
|
208
|
+
const s = typeof fx.streak === 'number' ? fx.streak : 1;
|
|
209
|
+
out += light(STREAK(), { x: sx - W * 0.7 * s, y: sy - size * 0.06, width: W * 1.4 * s, height: size * 0.12 }, [0.75, 0.85, 1], k * 0.7, 'fs');
|
|
210
|
+
names.push('fs');
|
|
211
|
+
}
|
|
212
|
+
return out + screenOn(names);
|
|
213
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Every effect kind's options, with descriptions.
|
|
2
|
+
|
|
3
|
+
/** Options every effect takes. */
|
|
4
|
+
export const EFFECT_COMMON: Readonly<Record<string, string>>;
|
|
5
|
+
/** Each kind's options: name → description. */
|
|
6
|
+
export const EFFECT_OPTIONS: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every effect kind's options, with one-line descriptions. `defineRecipe`
|
|
3
|
+
* rejects kinds and options not listed here, `scripts/schema.mjs` builds
|
|
4
|
+
* the JSON Schema's autocomplete from it, and the README's table follows
|
|
5
|
+
* it. Add an option to an effect, add it here.
|
|
6
|
+
*
|
|
7
|
+
* @module effects/options
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Options every effect takes. @type {Record<string, string>} */
|
|
11
|
+
export const EFFECT_COMMON = {
|
|
12
|
+
kind: 'The effect.',
|
|
13
|
+
mask: '{ gradient: linear | radial | band, from, to, center, angle, aspect, invert, ease } or { layers, invert }: where the effect applies.',
|
|
14
|
+
colorSpace: 'sRGB (default) | linearRGB: where filter primitives do their arithmetic.',
|
|
15
|
+
pad: 'Filter region padding, a fraction of the canvas.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const BLUR_CENTRE = 'Centre, [x, y] fractions. Default [0.5, 0.5].';
|
|
19
|
+
|
|
20
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
21
|
+
export const EFFECT_OPTIONS = {
|
|
22
|
+
// 1.1's first set.
|
|
23
|
+
blur: { radius: 'Length.', x: 'Length (an axis-aligned blur).', y: 'Length.', edge: 'duplicate: hold the edge.' },
|
|
24
|
+
motion: { angle: 'Degrees.', length: 'Length: the blur along the angle (a standard deviation).', spread: 'Length across.' },
|
|
25
|
+
shadow: { x: 'Length.', y: 'Length.', blur: 'Length.', color: 'Colour.', opacity: '0–1.', spread: 'Length.' },
|
|
26
|
+
glow: { x: 'Length.', y: 'Length.', blur: 'Length.', color: 'Colour.', opacity: '0–1.', spread: 'Length.' },
|
|
27
|
+
border: { width: 'Length.', color: 'Colour.', line: 'true or a colour: the cut line.', lineWidth: 'Length.', shape: 'die | circle', cx: 'Length.', cy: 'Length.' },
|
|
28
|
+
wear: { rough: 'Length.', uneven: '0–1.', specks: '0–1.', seed: 'Number.' },
|
|
29
|
+
rough: { scale: 'Length.', frequency: 'Number or [x, y].', octaves: 'Number.', seed: 'Number.' },
|
|
30
|
+
warp: { scale: 'Length.', frequency: 'Number or [x, y].', octaves: 'Number.', seed: 'Number.' },
|
|
31
|
+
xerox: { rough: 'Length.', dropout: '0–1.', seed: 'Number.' },
|
|
32
|
+
photocopy: { ink: 'Colour.', contrast: 'Number.', lift: 'Number.', noise: '0–1.', frequency: 'Number.', seed: 'Number.' },
|
|
33
|
+
duotone: { colors: 'Two or more, shadows first.', contrast: 'Number.' },
|
|
34
|
+
grayscale: { slope: 'Number.', intercept: 'Number.', contrast: 'Number.', brightness: 'Number.', black: 'Input black point, 0–1.', white: 'Input white point, 0–1.', gamma: 'Midtones (>1 lighter).', output: '[lo, hi] output range, 0–1.' },
|
|
35
|
+
levels: { slope: 'Number.', intercept: 'Number.', contrast: 'Number.', brightness: 'Number.', black: 'Input black point, 0–1.', white: 'Input white point, 0–1.', gamma: 'Midtones (>1 lighter).', output: '[lo, hi] output range, 0–1.' },
|
|
36
|
+
saturate: { amount: 'Number (1 unchanged).' },
|
|
37
|
+
matrix: { values: '20 numbers.' },
|
|
38
|
+
grit: { amount: '0–1.', frequency: 'Number.', seed: 'Number.' },
|
|
39
|
+
halftone: { ink: 'Colour.', cell: 'Length.', angle: 'Degrees.', contrast: 'Number.', lift: 'Number.', softness: 'Number.', shape: 'dot (default) | line | square | diamond | cross' },
|
|
40
|
+
dither: { ink: 'Colour.', paper: 'Colour.', cell: 'Length.', contrast: 'Number.', brightness: 'Number.', pixelate: 'Boolean.', weights: '[r, g, b].', matrix: 'bayer4 | bayer8' },
|
|
41
|
+
threshold: { level: '0–1.', ink: 'Colour.', paper: 'Colour or none.' },
|
|
42
|
+
filter: { markup: 'Raw filter primitives.' },
|
|
43
|
+
extrude: { depth: 'Length.', angle: 'Degrees.', offset: '[x, y] lengths.', steps: 'Number.', color: 'Colour.', to: 'Colour.' },
|
|
44
|
+
|
|
45
|
+
// Blurs.
|
|
46
|
+
zoom: { center: BLUR_CENTRE, amount: '0–0.95: how much of the way to the centre each point is smeared. Default 0.2.' },
|
|
47
|
+
spin: { center: BLUR_CENTRE, angle: 'Degrees of rotation. Default 12.' },
|
|
48
|
+
tilt: { center: 'A point on the sharp line, [x, y] fractions.', angle: 'Degrees (0: a horizontal band).', width: 'Length: half the sharp band. Default "8vmin".', falloff: 'Length over which the blur grows. Default "22vmin".', radius: 'Length: the strongest blur. Default "1.4vmin".', saturate: 'Number (the miniature look: 1.2).' },
|
|
49
|
+
lens: { radius: 'Length: the aperture. Default "1.2vmin".', blades: '0 (round) or 3–12 sides.', rotation: 'Degrees.', highlights: 'Boost of the bright parts (bokeh). Default 1.', threshold: '0–1: what counts as a highlight. Default 0.75.' },
|
|
50
|
+
|
|
51
|
+
// Distortions.
|
|
52
|
+
wave: { amplitude: 'Length.', wavelength: 'Length.', angle: 'Degrees the wave travels.', phase: '0–1 of a period.', shape: 'sine | triangle | square' },
|
|
53
|
+
ripple: { center: BLUR_CENTRE, amplitude: 'Length.', wavelength: 'Length.', phase: '0–1.', radius: 'Length it fades out by.' },
|
|
54
|
+
twirl: { center: BLUR_CENTRE, angle: 'Degrees at the centre. Default 180.', radius: 'Length.' },
|
|
55
|
+
bulge: { center: BLUR_CENTRE, amount: '0–1.', radius: 'Length.' },
|
|
56
|
+
pinch: { center: BLUR_CENTRE, amount: '0–1.', radius: 'Length.' },
|
|
57
|
+
fisheye: { center: BLUR_CENTRE, amount: '0–1. Default 0.6.' },
|
|
58
|
+
glass: { texture: 'frosted | reeded | pebbled', size: 'Length: grain, rib or lump.', amount: 'Frosted, pebbled: a length; reeded: 0–1.', angle: 'Reeded: rib angle, degrees.', shade: 'Reeded: rib shading, 0–1.', seed: 'Number or string.' },
|
|
59
|
+
displace: { amount: 'Length.', map: '{ layers }: displace by their luminance (default seeded noise).', mode: 'gradient (default) | channels', size: 'Noise: length.', octaves: 'Noise detail.', seed: 'Number or string.' },
|
|
60
|
+
kaleidoscope: { segments: 'Wedges. Default 8.', center: BLUR_CENTRE, angle: 'Degrees: where the source wedge starts (default -90, up).', scale: 'Zoom of the source. Default 1.' },
|
|
61
|
+
mirror: { keep: 'left | right | top | bottom | top-left | top-right | bottom-left | bottom-right', at: 'The mirror line: a fraction, or [x, y].' },
|
|
62
|
+
stretch: { at: '0–1: the row or column.', direction: 'down | up | left | right', length: '0–1 of the way to the edge.', fade: '0–1.' },
|
|
63
|
+
sort: { direction: 'horizontal | vertical', threshold: '[lo, hi] luminance, 0–1.', by: 'luma | hue | saturation', reverse: 'Boolean.', length: '0–1: break runs at seeded lengths.', seed: 'Number or string.' },
|
|
64
|
+
|
|
65
|
+
// Colour.
|
|
66
|
+
curves: { points: '[[in, out], …] 0–1, all channels.', red: 'Points.', green: 'Points.', blue: 'Points.' },
|
|
67
|
+
gradientMap: { colors: 'Shadows to highlights.', stops: 'Positions, 0–1.', space: 'srgb | oklab' },
|
|
68
|
+
posterize: { levels: 'Tones per channel. Default 4.' },
|
|
69
|
+
invert: { amount: '0–1. Default 1.' },
|
|
70
|
+
hue: { angle: 'Degrees.', saturation: '-1–1.', lightness: '-1–1.' },
|
|
71
|
+
vibrance: { amount: '-1–1. Default 0.5.', saturation: '-1–1.' },
|
|
72
|
+
colorBalance: { shadows: '[r, g, b], -1–1.', midtones: '[r, g, b].', highlights: '[r, g, b].' },
|
|
73
|
+
channelMixer: { red: '[r, g, b, constant].', green: 'Row.', blue: 'Row.', monochrome: 'Boolean.', gray: 'Row (monochrome).' },
|
|
74
|
+
sepia: { amount: '0–1.' },
|
|
75
|
+
look: { name: 'fade | crossProcess | bleachBypass | tealOrange | warmFilm | coolFilm | noir', amount: '0–1.' },
|
|
76
|
+
|
|
77
|
+
// Light.
|
|
78
|
+
bloom: { threshold: '0–1.', radius: 'Length.', intensity: 'Number.', color: 'Tint.' },
|
|
79
|
+
halation: { threshold: '0–1.', radius: 'Length.', intensity: 'Number.', color: 'Default red-orange.' },
|
|
80
|
+
vignette: { shape: 'ellipse | circle | rect', size: '0–1: where it starts.', softness: '0–1.', amount: '0–1.', color: 'Default black.', center: BLUR_CENTRE },
|
|
81
|
+
lightLeak: { edge: 'left | right | top | bottom (default seeded)', colors: 'Colours.', intensity: 'Number.', seed: 'Number or string.' },
|
|
82
|
+
lensFlare: { at: 'The light, [x, y] fractions.', size: 'Length.', intensity: 'Number.', color: 'Tint.', ghosts: 'Count. Default 6.', streak: 'true or a width factor: an anamorphic streak.', seed: 'Number or string.' },
|
|
83
|
+
rays: { center: 'The light, [x, y] fractions (default the brightest patch).', threshold: '0–1.', length: 'How far rays reach, a fraction of the frame. Default 0.5.', intensity: 'Number.', color: 'Tint.' },
|
|
84
|
+
|
|
85
|
+
// Texture and print.
|
|
86
|
+
grain: { amount: '0–1.', size: 'Length.', color: 'Boolean: colour grain.', seed: 'Number.' },
|
|
87
|
+
dust: { amount: '0–1.', scratches: 'Count.', color: 'light | dark | mixed', seed: 'Number or string.' },
|
|
88
|
+
paper: { texture: 'paper | canvas', scale: 'Length.', amount: '0–1.', angle: 'Light azimuth, degrees.', seed: 'Number.' },
|
|
89
|
+
scanlines: { spacing: 'Length.', thickness: '0–1.', amount: '0–1.' },
|
|
90
|
+
crt: { mask: 'aperture | shadow', size: 'Length: a triad.', amount: '0–1.', scanlines: '0–1.', curvature: '0–1.' },
|
|
91
|
+
vhs: { shift: 'Length: chroma shift.', bleed: 'Length: chroma smear.', jitter: 'Length.', tracking: '0–1.', noise: '0–1.', seed: 'Number or string.' },
|
|
92
|
+
jpeg: { quality: '1–100. Default 12.', block: 'Length: a JPEG pixel.', passes: 'Re-saves, 1–8.', subsample: 'Boolean. Default true.' },
|
|
93
|
+
newsprint: { cell: 'Length: the screen.', angles: '[c, m, y, k] degrees.', paper: 'Colour.', gain: 'Dot gain, 0–0.3.', gcr: '0–1.' },
|
|
94
|
+
|
|
95
|
+
// Digital.
|
|
96
|
+
chromatic: { amount: 'Length.', angle: 'Degrees (directional).', mode: 'directional (default) | radial', center: BLUR_CENTRE },
|
|
97
|
+
rgbSplit: { amount: 'Length.', angle: 'Degrees.' },
|
|
98
|
+
glitch: { amount: '0–1.', shift: 'Length.', slices: 'Count.', blocks: 'Count.', seed: 'Number or string.' },
|
|
99
|
+
pixelate: { size: 'Length.', shape: 'square | hex' },
|
|
100
|
+
mosaic: { size: 'Length.', shape: 'square | hex' },
|
|
101
|
+
ascii: { cell: 'Length.', ramp: 'Characters, sparse to dense.', color: 'source | a colour.', background: 'Colour or none.', invert: 'Boolean.', contrast: 'Number.' },
|
|
102
|
+
emboss: { angle: 'Degrees.', amount: 'Number.', radius: 'Length.', color: 'Boolean: over the layer.' },
|
|
103
|
+
edges: { radius: 'Length.', amount: 'Number.', style: 'ink | glow', color: 'Ink colour.', mono: 'Boolean.' },
|
|
104
|
+
sketch: { radius: 'Length.', darkness: '0–1.', color: 'Boolean: coloured pencil.' },
|
|
105
|
+
sharpen: { amount: 'Number.', radius: 'Length.' },
|
|
106
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Named paints: chrome, steel and gold, painted as a reflected horizon.
|
|
2
|
+
import type { GradientPaint } from './paint.js';
|
|
3
|
+
import type { Color, Length } from '../index.js';
|
|
4
|
+
|
|
5
|
+
/** Paint presets, by name. */
|
|
6
|
+
export const PRESETS: Readonly<Record<'chrome' | 'steel' | 'gold', Readonly<GradientPaint>>>;
|
|
7
|
+
/** The keyline metal paints bring: a thin light edge, then a dark one. */
|
|
8
|
+
export const KEYLINE: readonly { readonly color: Color; readonly width: Length }[];
|
|
9
|
+
/** The keyline a fill brings with it (a preset's, unless `keyline: false`). */
|
|
10
|
+
export function presetStrokes(fill: unknown): { color: Color; width: Length }[] | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named paints. `chrome` is Y2K chrome as it's painted by hand: a
|
|
3
|
+
* reflected horizon, not a lighting filter. Sky at the top, a sharp dark
|
|
4
|
+
* horizon a little past the middle, warm ground below it, and a light
|
|
5
|
+
* bounce at the very bottom. Pair it with a light keyline (`strokes`).
|
|
6
|
+
*
|
|
7
|
+
* @module effects/paint-presets
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Paint presets, by name. A paint `{ preset, …fields }` overrides any of them. */
|
|
11
|
+
export const PRESETS = Object.freeze({
|
|
12
|
+
chrome: Object.freeze({
|
|
13
|
+
type: 'linear',
|
|
14
|
+
angle: 180,
|
|
15
|
+
colors: ['#f4f8ff', '#9fc3ef', '#e8f1ff', '#2b3446', '#7a5a3c', '#d9b48a', '#fff6e6'],
|
|
16
|
+
stops: [0, 0.34, 0.545, 0.555, 0.62, 0.86, 1],
|
|
17
|
+
}),
|
|
18
|
+
/** The same horizon, cooler: steel. */
|
|
19
|
+
steel: Object.freeze({
|
|
20
|
+
type: 'linear',
|
|
21
|
+
angle: 180,
|
|
22
|
+
colors: ['#ffffff', '#b9c4d4', '#eef2f7', '#30363f', '#5d6672', '#c7ced8', '#ffffff'],
|
|
23
|
+
stops: [0, 0.34, 0.545, 0.555, 0.62, 0.86, 1],
|
|
24
|
+
}),
|
|
25
|
+
/** Gold leaf: the horizon in warm metal. */
|
|
26
|
+
gold: Object.freeze({
|
|
27
|
+
type: 'linear',
|
|
28
|
+
angle: 180,
|
|
29
|
+
colors: ['#fff6d6', '#e9c46a', '#fff1c2', '#6b4210', '#a8701f', '#f2cf73', '#fff8e0'],
|
|
30
|
+
stops: [0, 0.34, 0.545, 0.555, 0.62, 0.86, 1],
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** The keyline that goes round metal paints: a thin light edge, then a dark one (widest first, under the fill). */
|
|
35
|
+
export const KEYLINE = Object.freeze([Object.freeze({ color: '#0b0f18', width: '0.9vmin' }), Object.freeze({ color: '#ffffff', width: '0.35vmin' })]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The keyline a fill brings with it: a preset paint's, unless the paint
|
|
39
|
+
* says `keyline: false` (or gives its own `keyline` list).
|
|
40
|
+
*
|
|
41
|
+
* @param {any} fill
|
|
42
|
+
* @returns {{ color: string, width: string | number }[] | undefined}
|
|
43
|
+
*/
|
|
44
|
+
export function presetStrokes(fill) {
|
|
45
|
+
if (!fill || typeof fill !== 'object' || fill.preset == null || fill.keyline === false) return undefined;
|
|
46
|
+
return Array.isArray(fill.keyline) ? fill.keyline : [...KEYLINE];
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Paints: a colour, 'none', or a gradient over a shape's bounds.
|
|
2
|
+
import type { Color, Length } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/** A gradient over a shape's bounds. */
|
|
5
|
+
export interface GradientPaint {
|
|
6
|
+
type: 'linear' | 'radial';
|
|
7
|
+
colors: Color[];
|
|
8
|
+
stops?: number[];
|
|
9
|
+
opacities?: number[];
|
|
10
|
+
/** Linear: CSS degrees. Default 180 (top to bottom). */
|
|
11
|
+
angle?: number;
|
|
12
|
+
/** `'reflect'` / `'repeat'` with a `length` run: holographic foil, stripes. */
|
|
13
|
+
spread?: 'pad' | 'reflect' | 'repeat';
|
|
14
|
+
length?: Length;
|
|
15
|
+
cx?: number; cy?: number; r?: Length; fx?: number; fy?: number;
|
|
16
|
+
/** Radial: elliptical radii (lengths, or fractions ≤ 1 of the box). */
|
|
17
|
+
rx?: Length; ry?: Length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A pattern repeated over a shape. */
|
|
21
|
+
export interface TilePaint {
|
|
22
|
+
type: 'tile';
|
|
23
|
+
kind?: 'checker' | 'stripes' | 'dots' | 'grid' | 'custom';
|
|
24
|
+
/** One cell (or `[width, height]`). Default `'4vmin'`. */
|
|
25
|
+
size?: Length | [Length, Length];
|
|
26
|
+
/** `[ground, ink]`; the ground may be `'none'`. */
|
|
27
|
+
colors?: [Color | 'none', Color];
|
|
28
|
+
/** The stripe, dot or line share of a cell. */
|
|
29
|
+
ratio?: number;
|
|
30
|
+
/** Degrees. */
|
|
31
|
+
angle?: number;
|
|
32
|
+
/** `custom`: the tile's markup (sanitised). */
|
|
33
|
+
markup?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A named paint (`'chrome'`, `'steel'`, `'gold'`): a reflected-horizon gradient with its keyline; fields override it. */
|
|
37
|
+
export interface PresetPaint extends Partial<Omit<GradientPaint, 'type'>> {
|
|
38
|
+
preset: 'chrome' | 'steel' | 'gold';
|
|
39
|
+
type?: 'linear' | 'radial';
|
|
40
|
+
/** The keyline it brings (`false` for none). */
|
|
41
|
+
keyline?: false | { color: Color; width: Length }[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A fill or stroke: a colour, `'none'`, a gradient, a tile or a preset. */
|
|
45
|
+
export type Paint = Color | 'none' | GradientPaint | TilePaint | PresetPaint;
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/** A paint as an SVG fill value, plus any gradient it needs in `<defs>`. */
|
|
49
|
+
export function paint(v: Paint | undefined, fallback: string, o: { id: string; box: { x: number; y: number; width: number; height: number }; canvas: { width: number; height: number } }): { defs: string; value: string };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paints: a colour, `'none'`, a linear or radial gradient laid over a
|
|
3
|
+
* shape's bounds (reflected foil with `spread` and `length`; elliptical
|
|
4
|
+
* radial fields with `rx`/`ry`), a `tile` pattern (checker, stripes, dots,
|
|
5
|
+
* grid or a custom tile), or a named preset (`'chrome'`).
|
|
6
|
+
*
|
|
7
|
+
* @module effects/paint
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { attrs, num } from '../svg.js';
|
|
11
|
+
import { len } from '../geometry.js';
|
|
12
|
+
import { toHex } from '../color.js';
|
|
13
|
+
import { PRESETS } from './paint-presets.js';
|
|
14
|
+
import { tile } from './tile.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A fill or stroke: a colour, `'none'`, a gradient object
|
|
18
|
+
* `{ type: 'linear' | 'radial', colors, stops?, opacities?, angle?, spread?: 'pad' | 'reflect' | 'repeat', length?, cx?, cy?, r?, rx?, ry? }`,
|
|
19
|
+
* a tile `{ type: 'tile', kind, size, colors, … }`, or a preset
|
|
20
|
+
* `{ preset: 'chrome', … }` (its fields override the preset's), laid over
|
|
21
|
+
* `box` (the shape's bounds, else the canvas). `length` sets a linear
|
|
22
|
+
* gradient's run in px (with `reflect` it repeats: holographic foil).
|
|
23
|
+
*
|
|
24
|
+
* @param {any} v - The paint.
|
|
25
|
+
* @param {string} fallback - Used when `v` is unset.
|
|
26
|
+
* @param {{ id: string, box: { x: number, y: number, width: number, height: number }, canvas: { width: number, height: number } }} o
|
|
27
|
+
* @returns {{ defs: string, value: string }}
|
|
28
|
+
*/
|
|
29
|
+
export function paint(v, fallback, o) {
|
|
30
|
+
if (v == null || v === '') return { defs: '', value: fallback };
|
|
31
|
+
if (typeof v === 'string') return { defs: '', value: v === 'none' || v === 'currentColor' || v.startsWith('url(') ? v : toHex(v) };
|
|
32
|
+
if (v.preset != null) {
|
|
33
|
+
const base = PRESETS[v.preset];
|
|
34
|
+
if (!base) throw new TypeError(`Unknown paint preset "${v.preset}" (${Object.keys(PRESETS).join(', ')})`);
|
|
35
|
+
const { preset, keyline, ...own } = v;
|
|
36
|
+
return paint({ ...base, ...own }, fallback, o);
|
|
37
|
+
}
|
|
38
|
+
if (v.type === 'tile') return tile(v, o);
|
|
39
|
+
const colors = (v.colors ?? []).map(toHex);
|
|
40
|
+
if (colors.length < 2) throw new TypeError('A gradient paint needs two or more colours');
|
|
41
|
+
const offsets = v.stops ?? colors.map((/** @type {string} */ _, /** @type {number} */ i) => i / (colors.length - 1));
|
|
42
|
+
const stops = colors.map((c, i) => `<stop${attrs({ offset: num(offsets[i], 4), 'stop-color': c, 'stop-opacity': v.opacities?.[i] != null && v.opacities[i] < 1 ? num(v.opacities[i], 3) : undefined })}/>`).join('');
|
|
43
|
+
const { box } = o;
|
|
44
|
+
const spread = v.spread && v.spread !== 'pad' ? v.spread : undefined;
|
|
45
|
+
if (v.type === 'radial') {
|
|
46
|
+
const cx = box.x + (v.cx ?? 0.5) * box.width;
|
|
47
|
+
const cy = box.y + (v.cy ?? 0.5) * box.height;
|
|
48
|
+
// Elliptical: rx and ry (lengths, or fractions ≤ 1 of the box's width and height).
|
|
49
|
+
const radius = (/** @type {any} */ r, /** @type {number} */ side) => (typeof r === 'number' && r <= 1 ? r * side : len(r, o.canvas));
|
|
50
|
+
const rx = v.rx != null ? radius(v.rx, box.width) : undefined;
|
|
51
|
+
const ry = v.ry != null ? radius(v.ry, box.height) : undefined;
|
|
52
|
+
const r = rx ?? ry ?? (v.r != null ? len(v.r, o.canvas) : Math.hypot(box.width, box.height) / 2);
|
|
53
|
+
const squash = rx != null && ry != null && rx > 0 ? ry / rx : 1;
|
|
54
|
+
const gradientTransform = squash !== 1 ? `translate(${num(cx)} ${num(cy)}) scale(1 ${num(squash, 5)}) translate(${num(-cx)} ${num(-cy)})` : undefined;
|
|
55
|
+
return { defs: `<radialGradient${attrs({ id: o.id, gradientUnits: 'userSpaceOnUse', cx, cy, r, fx: v.fx != null ? box.x + v.fx * box.width : undefined, fy: v.fy != null ? box.y + v.fy * box.height : undefined, spreadMethod: spread, gradientTransform })}>${stops}</radialGradient>`, value: `url(#${o.id})` };
|
|
56
|
+
}
|
|
57
|
+
const a = ((v.angle ?? 180) * Math.PI) / 180;
|
|
58
|
+
const cx = box.x + box.width / 2;
|
|
59
|
+
const cy = box.y + box.height / 2;
|
|
60
|
+
const L = v.length != null ? len(v.length, o.canvas) : Math.abs(box.width * Math.sin(a)) + Math.abs(box.height * Math.cos(a));
|
|
61
|
+
const dx = (Math.sin(a) * L) / 2;
|
|
62
|
+
const dy = (-Math.cos(a) * L) / 2;
|
|
63
|
+
const x1 = v.length != null ? box.x : cx - dx;
|
|
64
|
+
const y1 = v.length != null ? box.y : cy - dy;
|
|
65
|
+
return { defs: `<linearGradient${attrs({ id: o.id, gradientUnits: 'userSpaceOnUse', x1, y1, x2: x1 + 2 * dx, y2: y1 + 2 * dy, spreadMethod: spread })}>${stops}</linearGradient>`, value: `url(#${o.id})` };
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Effects computed on pixels: draw, process, mix through the mask, embed.
|
|
2
|
+
import type { Markup } from '../render/context.js';
|
|
3
|
+
import type { Pixels } from './buffer.js';
|
|
4
|
+
|
|
5
|
+
/** The most pixels an effect works on. */
|
|
6
|
+
export const MAX_PIXELS: number;
|
|
7
|
+
|
|
8
|
+
/** What a pixel effect returns. */
|
|
9
|
+
export interface PixelResult {
|
|
10
|
+
data: Uint8ClampedArray;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
/** Embed as PNG even when opaque. */
|
|
14
|
+
lossless?: boolean;
|
|
15
|
+
/** Smaller than the layer: scaled up nearest-neighbour. */
|
|
16
|
+
pixelated?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A layer's markup as RGBA pixels, `w`×`h` over its canvas; undefined where nothing can draw. */
|
|
20
|
+
export function drawLayer(m: Markup, ctx: object, w: number, h: number): Promise<Uint8ClampedArray | undefined>;
|
|
21
|
+
/** The raster size for a layer (its pixels as shown, capped). */
|
|
22
|
+
export function rasterSize(ctx: object, cap?: number): { w: number; h: number; scale: number };
|
|
23
|
+
/** Run a pixel effect on a layer's markup; undefined where nothing can draw pixels. */
|
|
24
|
+
export function pixelEffect(fx: { mask?: import('./mask.js').EffectMask }, m: Markup, ctx: object, run: (img: Pixels) => PixelResult | Uint8ClampedArray | Promise<PixelResult | Uint8ClampedArray>, o?: { cap?: number }): Promise<Markup | undefined>;
|