@arraypress/covers 1.0.0 → 1.3.1
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 +118 -0
- package/README.md +18 -524
- package/package.json +34 -5
- package/recipes/brand.json +1143 -0
- 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-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/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/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 +7624 -827
- package/src/animate/index.d.ts +9 -0
- package/src/animate/index.js +11 -0
- 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/builtins.js +4 -0
- package/src/diagram.js +4 -3
- package/src/extract.js +6 -5
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +245 -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 +5 -35
- package/src/grid.js +1 -1
- package/src/handmade.js +9 -7
- package/src/icons.js +42 -9
- package/src/index.d.ts +232 -55
- package/src/index.js +14 -6
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +201 -27
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +43 -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 +108 -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 +61 -0
- package/src/layers.js +129 -61
- 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/paint/index.d.ts +49 -0
- package/src/paint/index.js +66 -0
- package/src/paint/presets.d.ts +10 -0
- package/src/paint/presets.js +47 -0
- package/src/paint/tile.d.ts +7 -0
- package/src/paint/tile.js +55 -0
- 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.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +15 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +71 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/print.js +16 -13
- package/src/recipes.js +5 -4
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -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 +90 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/transform.d.ts +36 -0
- package/src/render/transform.js +139 -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 +144 -0
- package/src/render.js +343 -215
- package/src/text.d.ts +29 -0
- package/src/text.js +23 -16
- 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 +7 -0
- package/src/type/pixel.js +85 -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 +363 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
- package/src/color.js +0 -246
- package/src/png.js +0 -77
- package/src/rng.js +0 -95
- package/src/svg.js +0 -69
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-hash cache keys (Web Crypto: the same keys on every platform).
|
|
3
|
+
*
|
|
4
|
+
* @module render/key
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadBytes, hexDigest, u8, isBytes } from '../platform/index.js';
|
|
8
|
+
import { fontBytes } from '../fonts.js';
|
|
9
|
+
import { cover } from '../render.js';
|
|
10
|
+
import { VERSION } from '../version.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A stable cache key for a cover and its output options: a SHA-256 of the
|
|
14
|
+
* resolved cover (font and image bytes hashed by content), the output
|
|
15
|
+
* format and quality, and this package's version.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../index.js').CoverOptions | import('../index.js').Cover} input - The cover.
|
|
18
|
+
* @param {import('../index.js').RenderImageOptions} [options]
|
|
19
|
+
* @returns {Promise<string>} 40 hex characters.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* await coverKey({ recipe: 'aurora', seed: 'a' }, { format: 'webp' }); // 'f3a9…'
|
|
23
|
+
*/
|
|
24
|
+
export async function coverKey(input, options = {}) {
|
|
25
|
+
const c = cover(input);
|
|
26
|
+
const head = `covers@${VERSION}\n${options.format ?? 'webp'}:${options.quality ?? ''}:${options.effort ?? ''}:${options.lossless ?? c.output?.lossless ?? ''}\n`;
|
|
27
|
+
// Files a layer reads (an image's src, a mesh's photo, a waveform's audio) are hashed by content.
|
|
28
|
+
const file = async (/** @type {any} */ v) => (typeof v === 'string' && !v.startsWith('data:') ? digest(await loadBytes(v)) : isBytes(v) ? digest(v) : v);
|
|
29
|
+
const layers = await Promise.all(c.layers.map(async (l) => {
|
|
30
|
+
const x = /** @type {any} */ (l);
|
|
31
|
+
if (l.type === 'image') return { ...l, src: await file(x.src) };
|
|
32
|
+
if (l.type === 'mesh' && x.from) return { ...l, from: await file(x.from) };
|
|
33
|
+
if (l.type === 'waveform' && x.audio) return { ...l, audio: await file(x.audio) };
|
|
34
|
+
return l;
|
|
35
|
+
}));
|
|
36
|
+
const body = await hashBytes({
|
|
37
|
+
width: c.width,
|
|
38
|
+
height: c.height,
|
|
39
|
+
seed: c.seed,
|
|
40
|
+
palette: c.palette,
|
|
41
|
+
layers,
|
|
42
|
+
radius: c.radius,
|
|
43
|
+
fonts: await Promise.all(c.fonts.map(async (f) => ({ ...f, data: await digest(f.data ?? await fontBytes(f)) }))),
|
|
44
|
+
...(c.print ? { print: c.print } : {}),
|
|
45
|
+
...(c.side && c.side !== 'front' ? { side: c.side } : {}),
|
|
46
|
+
});
|
|
47
|
+
return (await hexDigest('SHA-256', head + stable(body))).slice(0, 40);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {Uint8Array | ArrayBuffer} data @returns {Promise<string>} SHA-1 hex. */
|
|
51
|
+
function digest(data) {
|
|
52
|
+
return hexDigest('SHA-1', u8(data));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Replace every byte array in a value with its SHA-1 (what {@link stable}
|
|
57
|
+
* writes for bytes), so the hashing itself can stay synchronous.
|
|
58
|
+
*
|
|
59
|
+
* @param {unknown} v
|
|
60
|
+
* @returns {Promise<unknown>}
|
|
61
|
+
*/
|
|
62
|
+
async function hashBytes(v) {
|
|
63
|
+
if (isBytes(v)) return { [BYTES]: await digest(/** @type {any} */ (v)) };
|
|
64
|
+
if (Array.isArray(v)) return Promise.all(v.map(hashBytes));
|
|
65
|
+
if (v && typeof v === 'object' && !(v instanceof URL)) {
|
|
66
|
+
const out = /** @type {Record<string, unknown>} */ ({});
|
|
67
|
+
for (const [k, x] of Object.entries(v)) out[k] = await hashBytes(x);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const BYTES = Symbol('bytes');
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* JSON with sorted keys and bytes hashed, for hashing.
|
|
77
|
+
*
|
|
78
|
+
* @param {unknown} v
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function stable(v) {
|
|
82
|
+
if (v && typeof v === 'object' && BYTES in v) return JSON.stringify(/** @type {any} */ (v)[BYTES]);
|
|
83
|
+
if (Array.isArray(v)) return `[${v.map(stable).join(',')}]`;
|
|
84
|
+
if (v instanceof URL) return JSON.stringify(v.href);
|
|
85
|
+
if (v && typeof v === 'object') {
|
|
86
|
+
return `{${Object.keys(v).sort().filter((k) => /** @type {any} */ (v)[k] !== undefined).map((k) => `${JSON.stringify(k)}:${stable(/** @type {any} */ (v)[k])}`).join(',')}}`;
|
|
87
|
+
}
|
|
88
|
+
if (typeof v === 'function') return JSON.stringify(String(v));
|
|
89
|
+
return JSON.stringify(v) ?? 'null';
|
|
90
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Scaling what a render reports.
|
|
2
|
+
import type { DrawnText, DrawnImage } from './context.js';
|
|
3
|
+
|
|
4
|
+
/** A report hook for a stack drawn `k` times larger (text sizes become canvas px). */
|
|
5
|
+
export function scaledReport(report: ((e: DrawnText | DrawnImage) => void) | undefined, k: number): ((e: DrawnText | DrawnImage) => void) | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a render reports for `lint`, scaled to canvas px.
|
|
3
|
+
*
|
|
4
|
+
* @module render/report
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A report hook for a stack drawn `k` times larger: text sizes become
|
|
9
|
+
* canvas px (the box is dropped, being in the stack's own units). Other
|
|
10
|
+
* entries carry their own scale and pass through.
|
|
11
|
+
*
|
|
12
|
+
* @param {((e: any) => void) | undefined} report
|
|
13
|
+
* @param {number} k
|
|
14
|
+
* @returns {((e: any) => void) | undefined}
|
|
15
|
+
*/
|
|
16
|
+
export function scaledReport(report, k) {
|
|
17
|
+
if (!report || k === 1) return report;
|
|
18
|
+
return (e) => report(e.kind === 'text' ? { ...e, size: e.size * k, capHeight: e.capHeight * k, box: null } : e);
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// A layer's `transform` option as an SVG transform.
|
|
2
|
+
import type { Length } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/** A transform any layer takes (about `origin`, default the canvas centre). */
|
|
5
|
+
export interface LayerTransform {
|
|
6
|
+
translate?: [Length, Length] | { x?: Length; y?: Length };
|
|
7
|
+
/** Degrees. */
|
|
8
|
+
rotate?: number;
|
|
9
|
+
scale?: number | [number, number];
|
|
10
|
+
skewX?: number;
|
|
11
|
+
skewY?: number;
|
|
12
|
+
/** `'x'` flips left–right, `'y'` top–bottom. */
|
|
13
|
+
mirror?: 'x' | 'y' | 'both';
|
|
14
|
+
/** `[x, y]` px, or `'center'`. */
|
|
15
|
+
origin?: [Length, Length] | { x?: Length; y?: Length } | 'center';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A transform's `origin` in px (`'center'`, the default, is the canvas centre). */
|
|
19
|
+
export function originOf(origin: LayerTransform['origin'], canvas: { width: number; height: number }): [number, number];
|
|
20
|
+
/** A `transform` option as an SVG transform string (a string is used as written). */
|
|
21
|
+
export function transformAttr(t: LayerTransform | string | undefined, canvas: { width: number; height: number }): string | undefined;
|
|
22
|
+
|
|
23
|
+
/** An affine matrix `[a, b, c, d, e, f]`, as SVG's `matrix()`. */
|
|
24
|
+
export type Affine = [number, number, number, number, number, number];
|
|
25
|
+
/** A transform (string or object form) as one matrix. */
|
|
26
|
+
export function transformMatrix(t: LayerTransform | string | undefined, canvas: { width: number; height: number }): Affine;
|
|
27
|
+
/** How much a matrix enlarges what it draws: smallest and largest stretch, and their geometric mean. */
|
|
28
|
+
export function stretchOf(m: Affine): { min: number; max: number; mean: number };
|
|
29
|
+
/** The inverse of an affine matrix. */
|
|
30
|
+
export function invert(m: Affine): Affine;
|
|
31
|
+
/** A point through an affine matrix. */
|
|
32
|
+
export function apply(m: Affine, x: number, y: number): [number, number];
|
|
33
|
+
/** The identity matrix. */
|
|
34
|
+
export const IDENTITY: Readonly<Affine>;
|
|
35
|
+
/** `m × n`: `n` applied first, in `m`'s space. */
|
|
36
|
+
export function compose(m: Affine, n: Affine): Affine;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A layer's `transform` option as an SVG transform.
|
|
3
|
+
*
|
|
4
|
+
* @module render/transform
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { num } from '@logomarks/image-effects/svg';
|
|
8
|
+
import { pos } from '../layers/util.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A layer's `transform` as an SVG transform string.
|
|
12
|
+
*
|
|
13
|
+
* Object form: `translate` (`[x, y]` or `{ x, y }`, lengths), `rotate`
|
|
14
|
+
* (degrees), `scale` (a number or `[sx, sy]`), `skewX`, `skewY`, `mirror`
|
|
15
|
+
* (`'x'` flips left–right, `'y'` top–bottom), all about `origin` (`[x, y]`
|
|
16
|
+
* px or `'center'`, the default). A string is used as written.
|
|
17
|
+
*
|
|
18
|
+
* @param {any} t
|
|
19
|
+
* @param {{ width: number, height: number }} canvas
|
|
20
|
+
* @returns {string | undefined}
|
|
21
|
+
*/
|
|
22
|
+
export function transformAttr(t, canvas) {
|
|
23
|
+
if (!t) return undefined;
|
|
24
|
+
if (typeof t === 'string') return t;
|
|
25
|
+
const parts = [];
|
|
26
|
+
const tr = Array.isArray(t.translate) ? { x: t.translate[0], y: t.translate[1] } : t.translate;
|
|
27
|
+
if (tr) parts.push(`translate(${num(pos(tr.x ?? 0, 'x', canvas))} ${num(pos(tr.y ?? 0, 'y', canvas))})`);
|
|
28
|
+
const o = originOf(t.origin, canvas);
|
|
29
|
+
const inner = [];
|
|
30
|
+
if (t.rotate) inner.push(`rotate(${num(t.rotate, 3)})`);
|
|
31
|
+
let [sx, sy] = t.scale == null ? [1, 1] : Array.isArray(t.scale) ? t.scale : [t.scale, t.scale];
|
|
32
|
+
if (t.mirror === 'x' || t.mirror === 'both') sx = -sx;
|
|
33
|
+
if (t.mirror === 'y' || t.mirror === 'both') sy = -sy;
|
|
34
|
+
if (sx !== 1 || sy !== 1) inner.push(`scale(${num(sx, 4)} ${num(sy, 4)})`);
|
|
35
|
+
if (t.skewX) inner.push(`skewX(${num(t.skewX, 3)})`);
|
|
36
|
+
if (t.skewY) inner.push(`skewY(${num(t.skewY, 3)})`);
|
|
37
|
+
if (inner.length) parts.push(`translate(${num(o[0])} ${num(o[1])}) ${inner.join(' ')} translate(${num(-o[0])} ${num(-o[1])})`);
|
|
38
|
+
return parts.length ? parts.join(' ') : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A transform's `origin` in px: `[x, y]` or `{ x, y }` (px, `'%'` or
|
|
43
|
+
* lengths), or `'center'` (the default).
|
|
44
|
+
*
|
|
45
|
+
* @param {unknown} origin
|
|
46
|
+
* @param {{ width: number, height: number }} canvas
|
|
47
|
+
* @returns {[number, number]}
|
|
48
|
+
*/
|
|
49
|
+
export function originOf(origin, canvas) {
|
|
50
|
+
if (origin === undefined || origin === 'center') return [canvas.width / 2, canvas.height / 2];
|
|
51
|
+
const o = /** @type {any} */ (origin);
|
|
52
|
+
return Array.isArray(o) ? [pos(o[0], 'x', canvas), pos(o[1], 'y', canvas)] : [pos(o.x, 'x', canvas), pos(o.y, 'y', canvas)];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @typedef {[number, number, number, number, number, number]} Affine - `[a, b, c, d, e, f]`, as SVG's `matrix()`. */
|
|
56
|
+
|
|
57
|
+
/** @param {Affine} m @param {Affine} n @returns {Affine} `m × n`. */
|
|
58
|
+
const multiply = (m, n) => [
|
|
59
|
+
m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1],
|
|
60
|
+
m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3],
|
|
61
|
+
m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/** @param {string} name @param {number[]} v @returns {Affine} One SVG transform function as a matrix. */
|
|
65
|
+
function fn(name, v) {
|
|
66
|
+
const r = ((v[0] ?? 0) * Math.PI) / 180;
|
|
67
|
+
switch (name) {
|
|
68
|
+
case 'translate': return [1, 0, 0, 1, v[0] ?? 0, v[1] ?? 0];
|
|
69
|
+
case 'scale': return [v[0] ?? 1, 0, 0, v[1] ?? v[0] ?? 1, 0, 0];
|
|
70
|
+
case 'rotate': {
|
|
71
|
+
const m = /** @type {Affine} */ ([Math.cos(r), Math.sin(r), -Math.sin(r), Math.cos(r), 0, 0]);
|
|
72
|
+
if (v.length < 3) return m;
|
|
73
|
+
return multiply(multiply([1, 0, 0, 1, v[1], v[2]], m), [1, 0, 0, 1, -v[1], -v[2]]);
|
|
74
|
+
}
|
|
75
|
+
case 'skewX': return [1, 0, Math.tan(r), 1, 0, 0];
|
|
76
|
+
case 'skewY': return [1, Math.tan(r), 0, 1, 0, 0];
|
|
77
|
+
case 'matrix': return /** @type {Affine} */ (v.slice(0, 6));
|
|
78
|
+
default: return [1, 0, 0, 1, 0, 0];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A transform (an SVG transform string, or the object form) as one matrix.
|
|
84
|
+
*
|
|
85
|
+
* @param {any} t
|
|
86
|
+
* @param {{ width: number, height: number }} canvas
|
|
87
|
+
* @returns {Affine}
|
|
88
|
+
*/
|
|
89
|
+
export function transformMatrix(t, canvas) {
|
|
90
|
+
const s = transformAttr(t, canvas);
|
|
91
|
+
/** @type {Affine} */
|
|
92
|
+
let m = [1, 0, 0, 1, 0, 0];
|
|
93
|
+
if (!s) return m;
|
|
94
|
+
for (const [, name, args] of s.matchAll(/(\w+)\s*\(([^)]*)\)/g)) m = multiply(m, fn(name, args.split(/[\s,]+/).filter(Boolean).map(Number)));
|
|
95
|
+
return m;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* How much a matrix enlarges what it draws: its smallest and largest
|
|
100
|
+
* stretch (singular values), and their geometric mean.
|
|
101
|
+
*
|
|
102
|
+
* @param {Affine} m
|
|
103
|
+
* @returns {{ min: number, max: number, mean: number }}
|
|
104
|
+
*/
|
|
105
|
+
export function stretchOf(m) {
|
|
106
|
+
const [a, b, c, d] = m;
|
|
107
|
+
const sum = a * a + b * b + c * c + d * d;
|
|
108
|
+
const det = Math.abs(a * d - b * c);
|
|
109
|
+
const root = Math.sqrt(Math.max(0, sum * sum - 4 * det * det));
|
|
110
|
+
return { min: Math.sqrt((sum - root) / 2), max: Math.sqrt((sum + root) / 2), mean: Math.sqrt(det) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The inverse of an affine matrix.
|
|
115
|
+
*
|
|
116
|
+
* @param {Affine} m
|
|
117
|
+
* @returns {Affine}
|
|
118
|
+
*/
|
|
119
|
+
export function invert(m) {
|
|
120
|
+
const [a, b, c, d, e, f] = m;
|
|
121
|
+
const det = a * d - b * c || 1e-12;
|
|
122
|
+
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A point through an affine matrix.
|
|
127
|
+
*
|
|
128
|
+
* @param {Affine} m @param {number} x @param {number} y
|
|
129
|
+
* @returns {[number, number]}
|
|
130
|
+
*/
|
|
131
|
+
export function apply(m, x, y) {
|
|
132
|
+
return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The identity matrix. @type {Affine} */
|
|
136
|
+
export const IDENTITY = Object.freeze(/** @type {Affine} */ ([1, 0, 0, 1, 0, 0]));
|
|
137
|
+
|
|
138
|
+
/** @param {Affine} m @param {Affine} n @returns {Affine} `m` then `n` (n applied first, in m's space). */
|
|
139
|
+
export const compose = (m, n) => multiply(m, n);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Reusable stacks: `defs` and `use`.
|
|
2
|
+
import type { Layer, BaseLayer, Length } from '../index.js';
|
|
3
|
+
import type { LayerTransform } from './transform.js';
|
|
4
|
+
import type { RenderContext, Markup } from './context.js';
|
|
5
|
+
|
|
6
|
+
/** `defs`: named layers (each with an `id`) that `use` draws; nothing is drawn here. */
|
|
7
|
+
export interface DefsLayer extends BaseLayer { type: 'defs'; layers: Layer[] }
|
|
8
|
+
|
|
9
|
+
/** `use`: a named layer drawn again, placed. */
|
|
10
|
+
export interface UseLayer extends BaseLayer {
|
|
11
|
+
type: 'use';
|
|
12
|
+
/** The `id` of a layer in an earlier `defs`. */
|
|
13
|
+
ref: string;
|
|
14
|
+
x?: Length | `${number}%`; y?: Length | `${number}%`;
|
|
15
|
+
/** Degrees, about the copy's origin. */
|
|
16
|
+
rotate?: number;
|
|
17
|
+
scale?: number | [number, number];
|
|
18
|
+
/** A transform instead of x, y, rotate and scale. */
|
|
19
|
+
place?: LayerTransform | string;
|
|
20
|
+
/** Options for this copy only (drawn on its own). */
|
|
21
|
+
with?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Register a defs layer's named layers. */
|
|
25
|
+
export function registerDefs(layer: DefsLayer, ctx: RenderContext): Markup;
|
|
26
|
+
/** Draw a named layer, placed. */
|
|
27
|
+
export function renderUse(layer: UseLayer, ctx: RenderContext): Promise<Markup>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable stacks: a `defs` layer names layers (by `id`) without drawing
|
|
3
|
+
* them; a `use` layer draws one, placed (`x`, `y`, `scale`, `rotate`, or a
|
|
4
|
+
* `transform`). A plain `use` draws the same markup every time (it's drawn
|
|
5
|
+
* once and referenced); `with` overrides options for that copy, which is
|
|
6
|
+
* drawn on its own.
|
|
7
|
+
*
|
|
8
|
+
* @module render/use
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { attrs, num } from '@logomarks/image-effects/svg';
|
|
12
|
+
import { pos, wrap } from '../layers/util.js';
|
|
13
|
+
import { transformAttr } from './transform.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `defs`: register named layers for `use`.
|
|
17
|
+
*
|
|
18
|
+
* @param {any} layer
|
|
19
|
+
* @param {any} ctx
|
|
20
|
+
* @returns {{ defs: string, body: string }}
|
|
21
|
+
*/
|
|
22
|
+
export function registerDefs(layer, ctx) {
|
|
23
|
+
for (const l of layer.layers ?? []) {
|
|
24
|
+
if (!l?.id) throw new TypeError('Every layer in a defs layer needs an id (use refers to it)');
|
|
25
|
+
ctx.defs.set(l.id, l);
|
|
26
|
+
}
|
|
27
|
+
return { defs: '', body: '' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `use`: draw a named layer, placed.
|
|
32
|
+
*
|
|
33
|
+
* @param {any} layer
|
|
34
|
+
* @param {any} ctx
|
|
35
|
+
* @returns {Promise<{ defs: string, body: string }>}
|
|
36
|
+
*/
|
|
37
|
+
export async function renderUse(layer, ctx) {
|
|
38
|
+
const def = ctx.defs?.get(layer.ref);
|
|
39
|
+
if (!def) throw new TypeError(`use: no layer "${layer.ref}" in a defs layer drawn before it`);
|
|
40
|
+
const place = placement(layer, ctx);
|
|
41
|
+
if (layer.with) {
|
|
42
|
+
const parts = await ctx.stack([{ ...def, ...layer.with }], { tag: 'u' });
|
|
43
|
+
const body = parts.map((/** @type {any} */ p) => p.body).join('');
|
|
44
|
+
return { defs: parts.map((/** @type {any} */ p) => p.defs).join(''), body: wrap(layer, place ? `<g transform="${place}">${body}</g>` : body) };
|
|
45
|
+
}
|
|
46
|
+
// Drawn once per canvas size, then referenced.
|
|
47
|
+
const key = `${layer.ref}@${num(ctx.width)}x${num(ctx.height)}`;
|
|
48
|
+
let drawn = ctx.defs.get(`#${key}`);
|
|
49
|
+
let defs = '';
|
|
50
|
+
if (!drawn) {
|
|
51
|
+
const parts = await ctx.stack([def], { tag: 'u' });
|
|
52
|
+
drawn = { id: `${ctx.id}u` };
|
|
53
|
+
ctx.defs.set(`#${key}`, drawn);
|
|
54
|
+
defs = parts.map((/** @type {any} */ p) => p.defs).join('') + `<g id="${drawn.id}">${parts.map((/** @type {any} */ p) => p.body).join('')}</g>`;
|
|
55
|
+
}
|
|
56
|
+
return { defs, body: wrap(layer, `<use${attrs({ href: `#${drawn.id}`, transform: place })}/>`) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Where a copy goes: `transform` as written, else `x`, `y`, `rotate`
|
|
61
|
+
* (about the copy's origin) and `scale`.
|
|
62
|
+
*
|
|
63
|
+
* @param {any} l @param {any} ctx
|
|
64
|
+
* @returns {string | undefined}
|
|
65
|
+
*/
|
|
66
|
+
function placement(l, ctx) {
|
|
67
|
+
if (l.place) return transformAttr(l.place, ctx.canvas);
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (l.x != null || l.y != null) parts.push(`translate(${num(pos(l.x ?? 0, 'x', ctx.canvas), 3)} ${num(pos(l.y ?? 0, 'y', ctx.canvas), 3)})`);
|
|
70
|
+
if (l.rotate) parts.push(`rotate(${num(l.rotate, 3)})`);
|
|
71
|
+
if (l.scale != null && l.scale !== 1) parts.push(Array.isArray(l.scale) ? `scale(${num(l.scale[0], 4)} ${num(l.scale[1], 4)})` : `scale(${num(l.scale, 4)})`);
|
|
72
|
+
return parts.length ? parts.join(' ') : undefined;
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The options every layer takes on top of its own.
|
|
2
|
+
import type { Layer, Cover } from '../index.js';
|
|
3
|
+
import type { RenderContext, Markup } from './context.js';
|
|
4
|
+
|
|
5
|
+
/** Options handled for every layer type. */
|
|
6
|
+
export const WRAP_KEYS: readonly ('effects' | 'mask' | 'knockout' | 'clip' | 'area')[];
|
|
7
|
+
/** Case keywords (a text layer's `transform` is its case when it's one of these). */
|
|
8
|
+
export const CASES: ReadonlySet<string>;
|
|
9
|
+
/** Whether a layer uses any of them (1.0 layers take the 1.0 path). */
|
|
10
|
+
export function wrapped(layer: Layer): boolean;
|
|
11
|
+
/** Draw a layer with its area, effects, transform and masks; blend and opacity outermost. */
|
|
12
|
+
export function renderWrapped(layer: Layer, ctx: RenderContext, scope: { c: Cover }, renderOne: (layer: Layer, ctx: RenderContext) => Promise<Markup>): Promise<Markup>;
|
|
13
|
+
/** A layer's effects, then its masks, round its markup (in `ctx`'s units: a group's grid inside a group). */
|
|
14
|
+
export function decorate(m: Markup, layer: Layer, ctx: RenderContext): Promise<Markup>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options every layer takes on top of its own: `area`, `effects`,
|
|
3
|
+
* `transform`, `mask`, `knockout` and `clip`.
|
|
4
|
+
*
|
|
5
|
+
* @module render/wrap
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { attrs, esc, num } from '@logomarks/image-effects/svg';
|
|
9
|
+
import { drawEffects, contentBox } from '@logomarks/image-effects';
|
|
10
|
+
import { transformAttr, transformMatrix, stretchOf, compose, IDENTITY } from './transform.js';
|
|
11
|
+
import { renderGroup } from './group.js';
|
|
12
|
+
import { scaledReport } from './report.js';
|
|
13
|
+
|
|
14
|
+
/** Layer options handled here, for every layer type. */
|
|
15
|
+
export const WRAP_KEYS = ['effects', 'mask', 'knockout', 'clip', 'area'];
|
|
16
|
+
|
|
17
|
+
/** Text-case keywords (the text layer's `transform` is its case when it's one of these). */
|
|
18
|
+
export const CASES = new Set(['uppercase', 'lowercase', 'none']);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether a layer uses the common 1.1 options (so 1.0 layers take exactly
|
|
22
|
+
* the 1.0 path).
|
|
23
|
+
*
|
|
24
|
+
* @param {any} layer
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function wrapped(layer) {
|
|
28
|
+
if (WRAP_KEYS.some((k) => layer[k] != null)) return true;
|
|
29
|
+
return isGeometric(layer);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {any} layer @returns {boolean} Whether `transform` is geometry (not a text case). */
|
|
33
|
+
const isGeometric = (layer) => layer.transform != null && !(layer.type === 'text' && typeof layer.transform === 'string' && CASES.has(layer.transform));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A layer with `area`, `effects`, `transform`, `mask`, `knockout` or
|
|
37
|
+
* `clip`. Blend and opacity go on the outermost group, so a masked or
|
|
38
|
+
* filtered layer still blends with what's under it. On a `group` with a
|
|
39
|
+
* `viewBox`, effects and masks work in the group's own grid.
|
|
40
|
+
*
|
|
41
|
+
* @param {any} layer
|
|
42
|
+
* @param {any} ctx
|
|
43
|
+
* @param {{ c: import('../index.js').Cover }} scope
|
|
44
|
+
* @param {(layer: any, ctx: any) => Promise<{ defs: string, body: string }>} renderOne - Draws the layer itself.
|
|
45
|
+
* @returns {Promise<{ defs: string, body: string }>}
|
|
46
|
+
*/
|
|
47
|
+
export async function renderWrapped(layer, ctx, scope, renderOne) {
|
|
48
|
+
const inner = { ...layer, opacity: undefined, blend: undefined, effects: undefined, mask: undefined, knockout: undefined, clip: undefined, area: undefined };
|
|
49
|
+
const geometric = isGeometric(layer);
|
|
50
|
+
if (geometric) inner.transform = undefined;
|
|
51
|
+
const grid = layer.type === 'group' && layer.viewBox != null;
|
|
52
|
+
|
|
53
|
+
const box = areaBox(layer.area, ctx, scope);
|
|
54
|
+
/** @type {any} */
|
|
55
|
+
let lctx = box ? { ...ctx, width: box.width, height: box.height, canvas: { width: box.width, height: box.height }, inset: undefined, matrix: compose(ctx.matrix ?? IDENTITY, [1, 0, 0, 1, box.x, box.y]) } : ctx;
|
|
56
|
+
// What's drawn inside a transform is drawn that much larger (images resample for it, lint measures it).
|
|
57
|
+
if (geometric) {
|
|
58
|
+
const t = transformMatrix(layer.transform, lctx.canvas);
|
|
59
|
+
const k = stretchOf(t);
|
|
60
|
+
lctx = { ...lctx, matrix: compose(lctx.matrix ?? IDENTITY, t) };
|
|
61
|
+
if (k.max !== 1 || k.mean !== 1) Object.assign(lctx, { scale: (lctx.scale ?? 1) * k.mean, res: (lctx.res ?? 1) * k.max, report: scaledReport(lctx.report, k.mean) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let { defs, body } = grid
|
|
65
|
+
? await renderGroup(inner, lctx, (/** @type {any} */ part, /** @type {any} */ gctx) => decorate(part, layer, gctx))
|
|
66
|
+
: await drawEffects(await renderOne(inner, lctx), layer.effects, lctx);
|
|
67
|
+
if (geometric) {
|
|
68
|
+
const t = transformAttr(layer.transform, lctx.canvas);
|
|
69
|
+
if (t) body = `<g transform="${t}">${body}</g>`;
|
|
70
|
+
}
|
|
71
|
+
if (box) body = `<g transform="translate(${num(box.x)} ${num(box.y)})">${body}</g>`;
|
|
72
|
+
if (!grid) ({ defs, body } = await decorateMasks({ defs, body }, layer, ctx));
|
|
73
|
+
const a = attrs({ opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined, style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${esc(layer.blend)}` : undefined });
|
|
74
|
+
if (a) body = `<g${a}>${body}</g>`;
|
|
75
|
+
return { defs, body };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `area`: the box a layer is drawn in (the trim, the safe area, or
|
|
80
|
+
* fractions of the canvas), or null for the whole canvas.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} a
|
|
83
|
+
* @param {any} ctx
|
|
84
|
+
* @param {{ c: import('../index.js').Cover }} scope
|
|
85
|
+
* @returns {{ x: number, y: number, width: number, height: number } | null}
|
|
86
|
+
*/
|
|
87
|
+
function areaBox(a, ctx, scope) {
|
|
88
|
+
if (!a || a === 'bleed') return null;
|
|
89
|
+
if (a === 'trim' || a === 'safe') {
|
|
90
|
+
const print = /** @type {any} */ (scope.c).print;
|
|
91
|
+
const inset = print ? print.bleed + (a === 'safe' ? print.safe : 0) : 0;
|
|
92
|
+
return { x: inset, y: inset, width: ctx.width - 2 * inset, height: ctx.height - 2 * inset };
|
|
93
|
+
}
|
|
94
|
+
return { x: (a.x ?? 0) * ctx.width, y: (a.y ?? 0) * ctx.height, width: (a.width ?? 1 - (a.x ?? 0)) * ctx.width, height: (a.height ?? 1 - (a.y ?? 0)) * ctx.height };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A layer's effects, then its masks, round its markup.
|
|
99
|
+
*
|
|
100
|
+
* @param {{ defs: string, body: string }} m
|
|
101
|
+
* @param {any} layer
|
|
102
|
+
* @param {any} ctx
|
|
103
|
+
* @returns {Promise<{ defs: string, body: string }>}
|
|
104
|
+
*/
|
|
105
|
+
export async function decorate(m, layer, ctx) {
|
|
106
|
+
return decorateMasks(await drawEffects(m, layer.effects, ctx), layer, ctx);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Masks: sub-stacks drawn as a luminance mask. `knockout` cuts its layers
|
|
111
|
+
* out of this one; `clip` shows this one only where they paint; `mask`
|
|
112
|
+
* uses their luminance as drawn (a white-to-black gradient fades).
|
|
113
|
+
*
|
|
114
|
+
* @param {{ defs: string, body: string }} m
|
|
115
|
+
* @param {any} layer
|
|
116
|
+
* @param {any} ctx
|
|
117
|
+
* @returns {Promise<{ defs: string, body: string }>}
|
|
118
|
+
*/
|
|
119
|
+
async function decorateMasks(m, layer, ctx) {
|
|
120
|
+
let { defs, body } = m;
|
|
121
|
+
const masks = [['mask', layer.mask], ['knockout', layer.knockout], ['clip', layer.clip]].filter(([, v]) => v != null);
|
|
122
|
+
for (const [kind, list] of masks) {
|
|
123
|
+
const sub = await ctx.stack(Array.isArray(list) ? list : [list], { tag: `${kind[0]}` });
|
|
124
|
+
const mid = `${ctx.id}${kind[0]}m`;
|
|
125
|
+
const fid = `${ctx.id}${kind[0]}f`;
|
|
126
|
+
const subDefs = sub.map((/** @type {any} */ p) => p.defs).join('');
|
|
127
|
+
let subBody = sub.map((/** @type {any} */ p) => p.body).join('');
|
|
128
|
+
const pad = Math.max(ctx.width, ctx.height);
|
|
129
|
+
const region = { x: -pad, y: -pad, width: ctx.width + 2 * pad, height: ctx.height + 2 * pad };
|
|
130
|
+
let mdefs = subDefs;
|
|
131
|
+
if (kind === 'knockout' || kind === 'clip') {
|
|
132
|
+
// Whatever the sub-layers paint becomes black (cut) or white (kept), keeping their alpha
|
|
133
|
+
// (in renderImage, over just what they paint).
|
|
134
|
+
const v = kind === 'knockout' ? 0 : 1;
|
|
135
|
+
const painted = await contentBox({ defs: subDefs, body: subBody }, ctx);
|
|
136
|
+
mdefs += `<filter${attrs({ id: fid, filterUnits: 'userSpaceOnUse', ...(painted ?? region), 'color-interpolation-filters': 'sRGB' })}><feColorMatrix type="matrix" values="0 0 0 0 ${v} 0 0 0 0 ${v} 0 0 0 0 ${v} 0 0 0 1 0"/></filter>`;
|
|
137
|
+
subBody = `<g filter="url(#${fid})">${subBody}</g>`;
|
|
138
|
+
if (kind === 'knockout') subBody = `<rect${attrs({ ...region, fill: '#fff' })}/>${subBody}`;
|
|
139
|
+
}
|
|
140
|
+
defs += `${mdefs}<mask${attrs({ id: mid, maskUnits: 'userSpaceOnUse', ...region })}>${subBody}</mask>`;
|
|
141
|
+
body = `<g mask="url(#${mid})">${body}</g>`;
|
|
142
|
+
}
|
|
143
|
+
return { defs, body };
|
|
144
|
+
}
|