@arraypress/covers 1.1.0 → 1.4.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 +60 -0
- package/README.md +18 -809
- package/package.json +3 -1
- package/recipes/flyers/afterglow-motion.json +540 -0
- package/recipes/flyers/afterglow.json +70 -0
- package/recipes/flyers/arcfield-mirror-motion.json +397 -0
- package/recipes/flyers/arcfield-mirror.json +364 -0
- package/recipes/flyers/bauhaus-oktav.json +319 -0
- package/recipes/flyers/bauhaus-ritmo.json +735 -0
- package/recipes/flyers/bauhaus-volta.json +394 -0
- package/recipes/flyers/bloomfall.json +99 -0
- package/recipes/flyers/drag-solenne-motion.json +444 -0
- package/recipes/flyers/drag-solenne.json +426 -0
- package/recipes/flyers/fennick-patches.json +874 -0
- package/recipes/flyers/ferro-pass.json +398 -0
- package/recipes/flyers/ferrova.json +812 -0
- package/recipes/flyers/grid-poster.json +91 -0
- package/recipes/flyers/grid-rest-rhythm.json +125 -0
- package/recipes/flyers/groovy-honeycomb.json +816 -0
- package/recipes/flyers/groovy-marigold.json +479 -0
- package/recipes/flyers/groovy-peachfuzz.json +974 -0
- package/recipes/flyers/halden-orbit-motion.json +381 -0
- package/recipes/flyers/halden-orbit.json +356 -0
- package/recipes/flyers/halvard.json +120 -0
- package/recipes/flyers/kairo-weekender.json +696 -0
- package/recipes/flyers/linger-motion.json +518 -0
- package/recipes/flyers/linger.json +67 -0
- package/recipes/flyers/little-orchard.json +105 -0
- package/recipes/flyers/lofi-hexmoth.json +912 -0
- package/recipes/flyers/lofi-lowbit-motion.json +888 -0
- package/recipes/flyers/lofi-lowbit.json +880 -0
- package/recipes/flyers/lofi-nullpoint.json +3013 -0
- package/recipes/flyers/logofolio.json +60 -0
- package/recipes/flyers/lumio-stack-motion.json +648 -0
- package/recipes/flyers/lumio-stack.json +438 -0
- package/recipes/flyers/magazine-cover.json +168 -0
- package/recipes/flyers/magazine-extended.json +269 -0
- package/recipes/flyers/magazine-knockout.json +40 -0
- package/recipes/flyers/northfold-varsity.json +618 -0
- package/recipes/flyers/open-studio.json +69 -0
- package/recipes/flyers/orrin-merch.json +715 -0
- package/recipes/flyers/oxyde.json +113 -0
- package/recipes/flyers/pixel-mosaic.json +58 -0
- package/recipes/flyers/quiet-fields.json +610 -0
- package/recipes/flyers/scrapbook-halcyon.json +948 -0
- package/recipes/flyers/solenne-ticket.json +730 -0
- package/recipes/flyers/solvane-motion.json +576 -0
- package/recipes/flyers/solvane.json +98 -0
- package/recipes/flyers/subduct.json +99 -0
- package/recipes/flyers/sunvale-lineup.json +584 -0
- package/recipes/flyers/tidewell.json +91 -0
- package/recipes/flyers/tour-kairo.json +535 -0
- package/recipes/flyers/velora-stickers.json +885 -0
- package/recipes/flyers/wheatpaste-black.json +313 -0
- package/recipes/flyers/wheatpaste-mark.json +198 -0
- package/recipes/flyers/wheatpaste-torn.json +114 -0
- package/recipes/flyers/wheatpaste-wall.json +984 -0
- package/recipes/flyers/wheatpaste-words.json +233 -0
- package/recipes/release/aura.json +137 -0
- package/recipes/release/chrome.json +433 -0
- package/recipes/release/collage.json +372 -0
- package/recipes/release/duotone.json +113 -0
- package/recipes/release/film-still.json +102 -0
- package/recipes/release/headline.json +70 -0
- package/recipes/release/instant.json +170 -0
- package/recipes/release/label.json +364 -0
- package/recipes/release/liquid-glass.json +301 -0
- package/recipes/release/odd-tile-dark.json +688 -0
- package/recipes/release/odd-tile-dense.json +1056 -0
- package/recipes/release/odd-tile-shift.json +541 -0
- package/recipes/release/odd-tile-split.json +571 -0
- package/recipes/release/odd-tile.json +633 -0
- package/recipes/release/off-edge.json +96 -0
- package/recipes/release/plinth.json +794 -0
- package/recipes/release/riso.json +108 -0
- package/recipes/release/series/fluid-label.json +561 -0
- package/recipes/release/series/fluid-pixel.json +334 -0
- package/recipes/release/series/hairline.json +305 -0
- package/recipes/release/series/halo.json +282 -0
- package/recipes/release/series/mark-crop.json +135 -0
- package/recipes/release/series/mark-window.json +150 -0
- package/recipes/release/series/photo-label.json +475 -0
- package/recipes/release/series/smear-field.json +345 -0
- package/recipes/release/series/smear.json +202 -0
- package/recipes/release/series/spin.json +172 -0
- package/recipes/release/series/split-ghost.json +230 -0
- package/recipes/release/series/zoom.json +168 -0
- package/recipes/release/small-print.json +114 -0
- package/recipes/release/stack.json +156 -0
- package/recipes/release/swiss.json +117 -0
- package/schema/recipe.schema.json +638 -69
- package/src/animate/index.d.ts +9 -19
- package/src/animate/index.js +6 -111
- package/src/animate/keyframes.d.ts +21 -0
- package/src/animate/keyframes.js +119 -0
- package/src/animate/marquee.d.ts +30 -0
- package/src/animate/marquee.js +93 -0
- package/src/animate/morph.d.ts +8 -0
- package/src/animate/morph.js +99 -0
- package/src/animate/motion.d.ts +59 -0
- package/src/animate/motion.js +63 -0
- package/src/animate/sheet.d.ts +19 -0
- package/src/animate/sheet.js +61 -0
- package/src/animate/timing.d.ts +28 -0
- package/src/animate/timing.js +146 -0
- package/src/animate/tracks.d.ts +18 -0
- package/src/animate/tracks.js +139 -0
- package/src/diagram.js +4 -3
- package/src/extract.js +2 -2
- package/src/fonts.js +1 -2
- package/src/geometry.js +4 -34
- package/src/grid.js +1 -1
- package/src/handmade.js +9 -7
- package/src/icons.js +1 -2
- package/src/index.d.ts +47 -54
- package/src/index.js +5 -5
- package/src/inter-data.js +1 -1
- package/src/json-recipe.js +28 -7
- package/src/layer-options.js +26 -4
- package/src/layers/barcode.js +3 -3
- package/src/layers/geometry.js +5 -5
- package/src/layers/index.d.ts +6 -0
- package/src/layers/index.js +3 -0
- package/src/layers/mark-geometry.d.ts +15 -0
- package/src/layers/mark-geometry.js +46 -0
- package/src/layers/marks.d.ts +25 -0
- package/src/layers/marks.js +102 -0
- package/src/layers/mosaic.d.ts +32 -0
- package/src/layers/mosaic.js +155 -0
- package/src/layers/perforation.js +3 -3
- package/src/layers/repeat.js +2 -2
- package/src/layers/shape.d.ts +2 -2
- package/src/layers/shape.js +5 -5
- package/src/layers/soft.js +2 -2
- package/src/layers/svg.js +2 -3
- package/src/layers/table.js +3 -3
- package/src/layers/tear.d.ts +1 -1
- package/src/layers/tear.js +4 -4
- package/src/layers/texture.js +3 -3
- package/src/layers/util.js +15 -2
- package/src/layers.js +17 -32
- package/src/{effects/paint.js → paint/index.js} +5 -5
- package/src/{effects/paint-presets.d.ts → paint/presets.d.ts} +1 -1
- package/src/{effects/paint-presets.js → paint/presets.js} +1 -1
- package/src/{effects → paint}/tile.d.ts +1 -1
- package/src/{effects → paint}/tile.js +4 -4
- package/src/palette.js +1 -1
- package/src/platform/files.d.ts +6 -0
- package/src/platform/files.js +41 -0
- package/src/platform/harfbuzz.js +1 -1
- package/src/platform/index.d.ts +3 -3
- package/src/platform/index.js +6 -4
- package/src/platform/load.js +1 -2
- package/src/print.js +9 -7
- package/src/recipes.js +1 -1
- package/src/render/backdrop.js +2 -2
- package/src/render/grain.js +1 -1
- package/src/render/group.js +3 -3
- package/src/render/key.js +1 -2
- package/src/{effects → render}/transform.d.ts +2 -1
- package/src/{effects → render}/transform.js +17 -5
- package/src/render/use.d.ts +1 -1
- package/src/render/use.js +2 -2
- package/src/render/wrap.js +5 -60
- package/src/render.js +55 -18
- package/src/text.js +1 -1
- package/src/type/auto-color.js +1 -1
- package/src/type/fallback.js +1 -1
- package/src/type/glyphs.js +2 -2
- package/src/type/line.js +1 -1
- package/src/type/live.js +1 -1
- package/src/type/path.js +2 -2
- package/src/type/pixel.d.ts +0 -2
- package/src/type/pixel.js +3 -33
- package/src/type/spans.d.ts +5 -1
- package/src/type/spans.js +28 -5
- package/src/type/symbols.d.ts +1 -1
- package/src/type/symbols.js +20 -2
- package/src/type/typeset.js +25 -10
- package/src/version.js +1 -1
- package/src/color.js +0 -246
- package/src/effects/ascii.d.ts +0 -7
- package/src/effects/ascii.js +0 -93
- package/src/effects/buffer.d.ts +0 -60
- package/src/effects/buffer.js +0 -472
- package/src/effects/detail.d.ts +0 -12
- package/src/effects/detail.js +0 -124
- package/src/effects/digital.d.ts +0 -14
- package/src/effects/digital.js +0 -216
- package/src/effects/distort.d.ts +0 -20
- package/src/effects/distort.js +0 -314
- package/src/effects/dither.d.ts +0 -8
- package/src/effects/dither.js +0 -147
- package/src/effects/drop-shadow.d.ts +0 -4
- package/src/effects/drop-shadow.js +0 -45
- package/src/effects/extrude.d.ts +0 -5
- package/src/effects/extrude.js +0 -55
- package/src/effects/film.d.ts +0 -11
- package/src/effects/film.js +0 -204
- package/src/effects/glow.d.ts +0 -8
- package/src/effects/glow.js +0 -56
- package/src/effects/grade.d.ts +0 -30
- package/src/effects/grade.js +0 -274
- package/src/effects/halftone.d.ts +0 -8
- package/src/effects/halftone.js +0 -95
- package/src/effects/index.d.ts +0 -125
- package/src/effects/index.js +0 -277
- package/src/effects/ink.d.ts +0 -12
- package/src/effects/ink.js +0 -70
- package/src/effects/lens.d.ts +0 -14
- package/src/effects/lens.js +0 -277
- package/src/effects/light.d.ts +0 -10
- package/src/effects/light.js +0 -59
- package/src/effects/looks.d.ts +0 -9
- package/src/effects/looks.js +0 -74
- package/src/effects/mask.d.ts +0 -52
- package/src/effects/mask.js +0 -314
- package/src/effects/motion.d.ts +0 -6
- package/src/effects/motion.js +0 -60
- package/src/effects/newsprint.d.ts +0 -6
- package/src/effects/newsprint.js +0 -89
- package/src/effects/optics.d.ts +0 -10
- package/src/effects/optics.js +0 -213
- package/src/effects/options.d.ts +0 -6
- package/src/effects/options.js +0 -106
- package/src/effects/pixels.d.ts +0 -24
- package/src/effects/pixels.js +0 -138
- package/src/effects/polar.d.ts +0 -10
- package/src/effects/polar.js +0 -265
- package/src/effects/region.d.ts +0 -8
- package/src/effects/region.js +0 -136
- package/src/effects/round-border.d.ts +0 -5
- package/src/effects/round-border.js +0 -62
- package/src/effects/screen.d.ts +0 -14
- package/src/effects/screen.js +0 -212
- package/src/effects/sort.d.ts +0 -8
- package/src/effects/sort.js +0 -125
- package/src/effects/symmetry.d.ts +0 -6
- package/src/effects/symmetry.js +0 -74
- package/src/effects/tone.d.ts +0 -16
- package/src/effects/tone.js +0 -117
- package/src/effects/util.d.ts +0 -16
- package/src/effects/util.js +0 -85
- package/src/platform/bytes.d.ts +0 -22
- package/src/platform/bytes.js +0 -103
- package/src/platform/compress.d.ts +0 -12
- package/src/platform/compress.js +0 -81
- package/src/platform/node.d.ts +0 -26
- package/src/platform/node.js +0 -86
- package/src/png.js +0 -126
- package/src/raster/index.d.ts +0 -24
- package/src/raster/index.js +0 -171
- package/src/render/content.d.ts +0 -5
- package/src/render/content.js +0 -58
- package/src/render/tiles.d.ts +0 -6
- package/src/render/tiles.js +0 -77
- package/src/rng.js +0 -95
- package/src/svg.js +0 -69
- /package/src/{effects/paint.d.ts → paint/index.d.ts} +0 -0
package/src/effects/optics.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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;
|
package/src/effects/optics.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
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
|
-
}
|
package/src/effects/options.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
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>>>>;
|
package/src/effects/options.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
};
|
package/src/effects/pixels.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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>;
|
package/src/effects/pixels.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Effects computed on pixels, where SVG filters can't do them (spin and
|
|
3
|
-
* zoom blur, bokeh, distortions, pixel sorting, ASCII…): the layer is
|
|
4
|
-
* drawn once at the pixels it's shown at (sharp in Node, tiled past a few
|
|
5
|
-
* megapixels; a canvas in a page), processed here on a typed array, mixed
|
|
6
|
-
* with the original through the effect's `mask`, and embedded back as one
|
|
7
|
-
* image (JPEG where it's opaque, PNG where it isn't or where flat colour
|
|
8
|
-
* matters), cropped to what it paints. Where nothing can draw pixels (a
|
|
9
|
-
* Worker) the caller falls back to the effect's filter, if it has one.
|
|
10
|
-
*
|
|
11
|
-
* @module effects/pixels
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { attrs, num } from '../svg.js';
|
|
15
|
-
import { pixelsOf, encodePixels } from '../raster/index.js';
|
|
16
|
-
import { svgPixels } from '../render/tiles.js';
|
|
17
|
-
import { toBase64 } from '../platform/bytes.js';
|
|
18
|
-
import { nearest } from './buffer.js';
|
|
19
|
-
import { maskGrid, lerpPixels } from './mask.js';
|
|
20
|
-
|
|
21
|
-
/** The most pixels an effect works on (about 3550²); past that it works smaller and the image scales up. */
|
|
22
|
-
export const MAX_PIXELS = 12.6e6;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* What a pixel effect returns: the processed pixels, at the layer's
|
|
26
|
-
* raster size or (with `pixelated`) a grid of cells scaled up crisp.
|
|
27
|
-
*
|
|
28
|
-
* @typedef {object} PixelResult
|
|
29
|
-
* @property {Uint8ClampedArray} data
|
|
30
|
-
* @property {number} width
|
|
31
|
-
* @property {number} height
|
|
32
|
-
* @property {boolean} [lossless] - Embed as PNG even when opaque (flat colour).
|
|
33
|
-
* @property {boolean} [pixelated] - Smaller than the layer: scaled up nearest-neighbour.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A layer's markup as pixels, `w`×`h` over its canvas.
|
|
38
|
-
*
|
|
39
|
-
* @param {{ defs: string, body: string }} m
|
|
40
|
-
* @param {any} ctx
|
|
41
|
-
* @param {number} w
|
|
42
|
-
* @param {number} h
|
|
43
|
-
* @returns {Promise<Uint8ClampedArray | undefined>} RGBA, or undefined where nothing can draw.
|
|
44
|
-
*/
|
|
45
|
-
export async function drawLayer(m, ctx, w, h) {
|
|
46
|
-
const sx = w / ctx.width;
|
|
47
|
-
const sy = h / ctx.height;
|
|
48
|
-
const doc = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${m.defs ? `<defs>${m.defs}</defs>` : ''}<g transform="scale(${num(sx, 6)} ${num(sy, 6)})">${m.body}</g></svg>`;
|
|
49
|
-
try {
|
|
50
|
-
const out = ctx.sharp ? await svgPixels(ctx.sharp, doc, w, h) : await pixelsOf(doc, w, h);
|
|
51
|
-
return out ? new Uint8ClampedArray(out.buffer, out.byteOffset, out.byteLength) : undefined;
|
|
52
|
-
} catch {
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The raster size for a layer: its pixels as shown (`ctx.res` per unit),
|
|
59
|
-
* at most {@link MAX_PIXELS} (or the effect's own cap).
|
|
60
|
-
*
|
|
61
|
-
* @param {any} ctx
|
|
62
|
-
* @param {number} [cap=MAX_PIXELS]
|
|
63
|
-
* @returns {{ w: number, h: number, scale: number }}
|
|
64
|
-
*/
|
|
65
|
-
export function rasterSize(ctx, cap = MAX_PIXELS) {
|
|
66
|
-
const res = Math.min(ctx.res ?? 1, Math.sqrt(cap / (ctx.width * ctx.height)));
|
|
67
|
-
const w = Math.max(1, Math.round(ctx.width * res));
|
|
68
|
-
const h = Math.max(1, Math.round(ctx.height * res));
|
|
69
|
-
return { w, h, scale: Math.sqrt((w / ctx.width) * (h / ctx.height)) };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Run a pixel effect on a layer's markup.
|
|
74
|
-
*
|
|
75
|
-
* @param {any} fx - The effect (its `mask` is applied here).
|
|
76
|
-
* @param {{ defs: string, body: string }} m - The layer so far.
|
|
77
|
-
* @param {any} ctx
|
|
78
|
-
* @param {(img: import('./buffer.js').Pixels) => PixelResult | Uint8ClampedArray | Promise<PixelResult | Uint8ClampedArray>} run
|
|
79
|
-
* @param {{ cap?: number }} [o]
|
|
80
|
-
* @returns {Promise<{ defs: string, body: string } | undefined>} Undefined where nothing can draw pixels.
|
|
81
|
-
*/
|
|
82
|
-
export async function pixelEffect(fx, m, ctx, run, o = {}) {
|
|
83
|
-
const { w, h, scale } = rasterSize(ctx, o.cap);
|
|
84
|
-
const data = await drawLayer(m, ctx, w, h);
|
|
85
|
-
if (!data) return undefined;
|
|
86
|
-
let opaque = true;
|
|
87
|
-
for (let i = 3; i < data.length; i += 4) if (data[i] !== 255) { opaque = false; break; }
|
|
88
|
-
const img = { data, width: w, height: h, scale, opaque };
|
|
89
|
-
const got = await run(img);
|
|
90
|
-
/** @type {PixelResult} */
|
|
91
|
-
let res = got instanceof Uint8ClampedArray ? { data: got, width: w, height: h } : got;
|
|
92
|
-
if (fx.mask) {
|
|
93
|
-
const grid = await maskGrid(fx.mask, ctx, { x: 0, y: 0, width: ctx.width, height: ctx.height }, w, h);
|
|
94
|
-
if (grid) {
|
|
95
|
-
const full = res.width === w && res.height === h ? res.data : nearest(res.data, res.width, res.height, w, h);
|
|
96
|
-
res = { data: lerpPixels(data, full, grid), width: w, height: h, lossless: res.lossless };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return embed(res, ctx);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Pixels as an `<image>` over the layer's canvas, cropped to what's painted.
|
|
104
|
-
*
|
|
105
|
-
* @param {PixelResult} res
|
|
106
|
-
* @param {any} ctx
|
|
107
|
-
* @returns {Promise<{ defs: string, body: string }>}
|
|
108
|
-
*/
|
|
109
|
-
async function embed(res, ctx) {
|
|
110
|
-
const { data, width: w, height: h } = res;
|
|
111
|
-
let x0 = w; let y0 = h; let x1 = -1; let y1 = -1;
|
|
112
|
-
let opaque = true;
|
|
113
|
-
for (let y = 0; y < h; y++) {
|
|
114
|
-
for (let x = 0; x < w; x++) {
|
|
115
|
-
const a = data[(y * w + x) * 4 + 3];
|
|
116
|
-
if (a !== 255) opaque = false;
|
|
117
|
-
if (!a) continue;
|
|
118
|
-
if (x < x0) x0 = x;
|
|
119
|
-
if (x > x1) x1 = x;
|
|
120
|
-
if (y < y0) y0 = y;
|
|
121
|
-
if (y > y1) y1 = y;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (x1 < 0) return { defs: '', body: '' };
|
|
125
|
-
let crop = data;
|
|
126
|
-
const cw = x1 - x0 + 1;
|
|
127
|
-
const ch = y1 - y0 + 1;
|
|
128
|
-
if (cw !== w || ch !== h) {
|
|
129
|
-
crop = new Uint8ClampedArray(cw * ch * 4);
|
|
130
|
-
for (let y = 0; y < ch; y++) crop.set(data.subarray(((y0 + y) * w + x0) * 4, ((y0 + y) * w + x1 + 1) * 4), y * cw * 4);
|
|
131
|
-
}
|
|
132
|
-
const { mime, bytes } = await encodePixels(crop, cw, ch, { opaque, lossless: res.lossless || res.pixelated }, ctx.sharp);
|
|
133
|
-
const kx = ctx.width / w;
|
|
134
|
-
const ky = ctx.height / h;
|
|
135
|
-
const crisp = res.pixelated ? { 'image-rendering': 'pixelated', style: 'image-rendering:pixelated' } : {};
|
|
136
|
-
const image = `<image${attrs({ x: num(x0 * kx, 4), y: num(y0 * ky, 4), width: num(cw * kx, 4), height: num(ch * ky, 4), preserveAspectRatio: 'none', ...crisp, href: `data:${mime};base64,${toBase64(bytes)}` })}/>`;
|
|
137
|
-
return { defs: '', body: image };
|
|
138
|
-
}
|
package/src/effects/polar.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Blurs about a centre, on pixels.
|
|
2
|
-
import type { Effect } from './index.js';
|
|
3
|
-
import type { Pixels } from './buffer.js';
|
|
4
|
-
|
|
5
|
-
/** `spin`: a rotational blur. */
|
|
6
|
-
export function spin(img: Pixels, fx: Extract<Effect, { kind: 'spin' }>): Uint8ClampedArray;
|
|
7
|
-
/** `zoom`: a radial blur toward the centre. */
|
|
8
|
-
export function zoom(img: Pixels, fx: Extract<Effect, { kind: 'zoom' }>): Uint8ClampedArray;
|
|
9
|
-
/** `rays`: god rays from a light. */
|
|
10
|
-
export function rays(img: Pixels, fx: Extract<Effect, { kind: 'rays' }>): Uint8ClampedArray;
|