@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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `texture` layer: paper, creases, concrete, speck, wood, folds, dust.
|
|
3
|
+
*
|
|
4
|
+
* @module layers/texture
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { attrs, num } from '@logomarks/image-effects/svg';
|
|
8
|
+
import { len } from '@logomarks/image-effects/length';
|
|
9
|
+
import { toHex } from '@logomarks/image-effects/color';
|
|
10
|
+
import { pos, wrap } from './util.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `texture` — a surface laid over (or under) the artwork: `paper` (fibres),
|
|
15
|
+
* `creases` (pasted paper lit from the side; soft-light), `concrete`,
|
|
16
|
+
* `speck` (white specks), `wood`, `folds` (straight fold lines with a lit
|
|
17
|
+
* and a shaded side), `dust` (seeded specks of dirt). `color`, `opacity`,
|
|
18
|
+
* `blend`, `frequency`, `octaves` and `region` adjust it.
|
|
19
|
+
*
|
|
20
|
+
* @param {any} layer
|
|
21
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
22
|
+
* @returns {import('../layers.js').Markup}
|
|
23
|
+
*/
|
|
24
|
+
export function texture(layer, ctx) {
|
|
25
|
+
const { width: w, height: h, rng } = ctx;
|
|
26
|
+
const kind = layer.kind ?? 'paper';
|
|
27
|
+
const id = `${ctx.id}t`;
|
|
28
|
+
const seed = layer.seed ?? rng.int(1, 9999);
|
|
29
|
+
const rgb = (/** @type {string} */ c) => [1, 3, 5].map((i) => num(parseInt(toHex(c).slice(i, i + 2), 16) / 255, 3));
|
|
30
|
+
const f = (/** @type {any} */ v, /** @type {string} */ d) => (v == null ? d : [].concat(v).map((x) => num(x, 4)).join(' '));
|
|
31
|
+
const rect = (/** @type {string} */ extra = '') => `<rect${attrs({ width: w, height: h, filter: `url(#${id})` })}${extra}/>`;
|
|
32
|
+
const region = `${attrs({ id, x: 0, y: 0, width: w, height: h, filterUnits: 'userSpaceOnUse', 'color-interpolation-filters': 'sRGB' })}`;
|
|
33
|
+
let defs = '';
|
|
34
|
+
let body = '';
|
|
35
|
+
let blend = layer.blend;
|
|
36
|
+
let opacity = layer.opacity;
|
|
37
|
+
// Feature sizes are tuned on a 400 px wide sheet; scale with the canvas.
|
|
38
|
+
const k = 400 / Math.min(w, h * 0.8);
|
|
39
|
+
const fq = (/** @type {number[]} */ ...v) => v.map((x) => x * k);
|
|
40
|
+
switch (kind) {
|
|
41
|
+
case 'paper': {
|
|
42
|
+
const [r, g, b] = rgb(layer.color ?? '#73664d');
|
|
43
|
+
defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, fq(0.55, 0.3).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 3}" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 -0.9 ${num(0.5 * (layer.amount ?? 1), 3)}"/></filter>`;
|
|
44
|
+
body = rect();
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'creases':
|
|
48
|
+
defs = `<filter${region}><feTurbulence type="turbulence" baseFrequency="${f(layer.frequency, fq(0.006, 0.022).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 3}" seed="${seed}" result="t"/><feDiffuseLighting in="t" surfaceScale="${num(layer.depth ?? 3.4, 3)}" lighting-color="#fff" result="lit"><feDistantLight azimuth="${layer.azimuth ?? 225}" elevation="${layer.elevation ?? 38}"/></feDiffuseLighting><feColorMatrix in="lit" type="saturate" values="0"/></filter>`;
|
|
49
|
+
body = rect();
|
|
50
|
+
blend ??= 'soft-light';
|
|
51
|
+
opacity ??= 0.95;
|
|
52
|
+
break;
|
|
53
|
+
case 'concrete': {
|
|
54
|
+
const [r, g, b] = rgb(layer.color ?? '#857f7a');
|
|
55
|
+
defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, fq(0.035, 0.05).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 4}" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0.6 0.6 0.6 0 -0.4"/></filter>`;
|
|
56
|
+
body = rect();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'speck': {
|
|
60
|
+
const [r, g, b] = rgb(layer.color ?? '#ffffff');
|
|
61
|
+
defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, num(0.7 * k, 4))}" numOctaves="1" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 -12 7.3"/></filter>`;
|
|
62
|
+
body = rect();
|
|
63
|
+
opacity ??= 0.25;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'wood': {
|
|
67
|
+
const [r, g, b] = rgb(layer.color ?? '#6b4224');
|
|
68
|
+
defs = `<filter${region}><feTurbulence type="fractalNoise" baseFrequency="${f(layer.frequency, fq(0.006, 0.16).map((x) => num(x, 4)).join(' '))}" numOctaves="${layer.octaves ?? 4}" seed="${seed}"/><feColorMatrix type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 1.4 0 0 0 -0.45"/></filter>`;
|
|
69
|
+
body = rect();
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'folds': {
|
|
73
|
+
// Straight folds: a lit side and a shaded side either side of each line.
|
|
74
|
+
const xs = layer.x ?? [];
|
|
75
|
+
const ys = layer.y ?? (layer.x ? [] : [0.5]);
|
|
76
|
+
const bw = len(layer.width, ctx.canvas, '1.6vmin');
|
|
77
|
+
const strength = layer.strength ?? 0.35;
|
|
78
|
+
const band = (/** @type {boolean} */ vertical, /** @type {number} */ at, /** @type {number} */ i) => {
|
|
79
|
+
const gid = `${id}${vertical ? 'v' : 'h'}${i}`;
|
|
80
|
+
const p = vertical ? at * w : at * h;
|
|
81
|
+
defs += `<linearGradient${attrs({ id: gid, gradientUnits: 'userSpaceOnUse', x1: vertical ? p - bw : 0, y1: vertical ? 0 : p - bw, x2: vertical ? p + bw : 0, y2: vertical ? 0 : p + bw })}><stop offset="0" stop-color="#000" stop-opacity="0"/><stop offset="0.48" stop-color="#000" stop-opacity="${num(strength, 3)}"/><stop offset="0.52" stop-color="#fff" stop-opacity="${num(strength * 1.4, 3)}"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>`;
|
|
82
|
+
body += vertical ? `<rect${attrs({ x: p - bw, y: 0, width: bw * 2, height: h, fill: `url(#${gid})` })}/>` : `<rect${attrs({ x: 0, y: p - bw, width: w, height: bw * 2, fill: `url(#${gid})` })}/>`;
|
|
83
|
+
};
|
|
84
|
+
xs.forEach((/** @type {number} */ x, /** @type {number} */ i) => band(true, x, i));
|
|
85
|
+
ys.forEach((/** @type {number} */ y, /** @type {number} */ i) => band(false, y, i));
|
|
86
|
+
blend ??= 'soft-light';
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'dust': {
|
|
90
|
+
const n = layer.count ?? 70;
|
|
91
|
+
const c = toHex(layer.color ?? ctx.palette.text);
|
|
92
|
+
const [lo, hi] = (layer.size ?? ['0.06vmin', '0.26vmin']).map((/** @type {any} */ v) => len(v, ctx.canvas));
|
|
93
|
+
for (let i = 0; i < n; i++) body += `<circle${attrs({ cx: rng.next() * w, cy: rng.next() * h, r: lo + rng.next() * (hi - lo) })}/>`;
|
|
94
|
+
body = `<g fill="${c}">${body}</g>`;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
throw new TypeError(`Unknown texture "${kind}" (paper, creases, concrete, speck, wood, folds, dust)`);
|
|
99
|
+
}
|
|
100
|
+
if (layer.region) {
|
|
101
|
+
const rg = layer.region;
|
|
102
|
+
defs += `<clipPath id="${id}c"><rect${attrs({ x: pos(rg.x ?? 0, 'x', ctx.canvas), y: pos(rg.y ?? 0, 'y', ctx.canvas), width: pos(rg.width ?? '100%', 'x', ctx.canvas), height: pos(rg.height ?? '100%', 'y', ctx.canvas) })}/></clipPath>`;
|
|
103
|
+
body = `<g clip-path="url(#${id}c)">${body}</g>`;
|
|
104
|
+
}
|
|
105
|
+
return { defs, body: wrap({ blend, opacity }, body) };
|
|
106
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Helpers the poster layers share.
|
|
2
|
+
|
|
3
|
+
/** A coordinate: px, `'50%'` of the canvas along `axis`, or a length. */
|
|
4
|
+
export function pos(v: unknown, axis: 'x' | 'y', canvas: { width: number; height: number }): number;
|
|
5
|
+
/** Markup wrapped in the layer's opacity and blend (when it has them). */
|
|
6
|
+
export function wrap(layer: { opacity?: number; blend?: string }, body: string): string;
|
|
7
|
+
/** The text of a string-or-object option. */
|
|
8
|
+
export function contentOf(v: unknown): string | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small helpers the poster layers share: coordinates in px or `'%'`, the
|
|
3
|
+
* opacity/blend wrapper, and the text of a string-or-object option.
|
|
4
|
+
*
|
|
5
|
+
* @module layers/util
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { attrs, esc } from '@logomarks/image-effects/svg';
|
|
9
|
+
import { len } from '@logomarks/image-effects/length';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A coordinate: px as a number, `'50%'` of the canvas along `axis`, or a
|
|
13
|
+
* length (`'4vmin'`).
|
|
14
|
+
*
|
|
15
|
+
* @param {unknown} v
|
|
16
|
+
* @param {'x' | 'y'} axis
|
|
17
|
+
* @param {{ width: number, height: number }} canvas
|
|
18
|
+
* @returns {number} px
|
|
19
|
+
*/
|
|
20
|
+
export function pos(v, axis, canvas) {
|
|
21
|
+
if (typeof v === 'string' && v.trim().endsWith('%')) return (parseFloat(v) / 100) * (axis === 'x' ? canvas.width : canvas.height);
|
|
22
|
+
return len(/** @type {any} */ (v), canvas, 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Opacity and blend-mode attributes shared by every layer.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ opacity?: number, blend?: string }} layer - The layer.
|
|
29
|
+
* @returns {Record<string, string | number | undefined>} Attributes for the layer's group.
|
|
30
|
+
*/
|
|
31
|
+
export function layerAttrs(layer) {
|
|
32
|
+
return {
|
|
33
|
+
opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined,
|
|
34
|
+
style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${layer.blend}` : undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wrap markup in a group with the layer's `opacity` and `blend` (only when
|
|
40
|
+
* it has them).
|
|
41
|
+
*
|
|
42
|
+
* @param {{ opacity?: number, blend?: string }} layer
|
|
43
|
+
* @param {string} body
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function wrap(layer, body) {
|
|
47
|
+
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 });
|
|
48
|
+
return a ? `<g${a}>${body}</g>` : body;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The text of a string-or-object option (`'Hello'` or `{ content: 'Hello' }`).
|
|
53
|
+
*
|
|
54
|
+
* @param {unknown} v
|
|
55
|
+
* @returns {string | undefined}
|
|
56
|
+
*/
|
|
57
|
+
export function contentOf(v) {
|
|
58
|
+
if (typeof v === 'string' || typeof v === 'number') return String(v);
|
|
59
|
+
if (v && typeof v === 'object' && 'content' in v) return /** @type {any} */ (v).content;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
package/src/layers.js
CHANGED
|
@@ -14,17 +14,25 @@
|
|
|
14
14
|
* @module layers
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
import { attrs, esc,
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
17
|
+
import { loadBytes, toBase64 } from './platform/index.js';
|
|
18
|
+
import { attrs, esc, num } from '@logomarks/image-effects/svg';
|
|
19
|
+
import { imageMime } from '@logomarks/image-effects';
|
|
20
|
+
import { len } from '@logomarks/image-effects/length';
|
|
21
|
+
import { place, anchorAlign, unit, regionPx } from './geometry.js';
|
|
22
|
+
import { toHex, mixOklab, toOklch, fromOklch } from '@logomarks/image-effects/color';
|
|
21
23
|
import { harfbuzz, loadFont, pickFont } from './fonts.js';
|
|
22
|
-
import { layout, runPath } from './text.js';
|
|
24
|
+
import { layout, runPath, caseText } from './text.js';
|
|
23
25
|
import { resolveIcon, parseSvgIcon } from './icons.js';
|
|
24
26
|
import { gridPath } from './grid.js';
|
|
25
27
|
import { diagram } from './diagram.js';
|
|
26
28
|
import { paper, scribble } from './handmade.js';
|
|
27
29
|
import { ink, dither, contour, waveform, tiles, rule } from './print.js';
|
|
30
|
+
import { typeset, wantsType } from './type/index.js';
|
|
31
|
+
import { recordLayout } from './type/layout-ref.js';
|
|
32
|
+
import { shape, svg, table, barcode, perforation, tear, texture } from './layers/index.js';
|
|
33
|
+
import { pos, layerAttrs } from './layers/util.js';
|
|
34
|
+
import { paint } from './paint/index.js';
|
|
35
|
+
import { imageSize } from './lint/image-size.js';
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
38
|
* Everything a layer renderer needs.
|
|
@@ -34,7 +42,7 @@ import { ink, dither, contour, waveform, tiles, rule } from './print.js';
|
|
|
34
42
|
* @property {number} height - Canvas height in px.
|
|
35
43
|
* @property {{ width: number, height: number }} canvas - The same, as an object.
|
|
36
44
|
* @property {import('./index.js').Palette} palette - The resolved palette.
|
|
37
|
-
* @property {import('
|
|
45
|
+
* @property {import('@logomarks/image-effects/rng').Rng} rng - This layer's own random stream.
|
|
38
46
|
* @property {string} id - A unique id prefix for this layer's defs.
|
|
39
47
|
* @property {import('./index.js').FontSource[]} fonts - Available fonts (Inter last).
|
|
40
48
|
* @property {any} [sharp] - The `sharp` module, when available.
|
|
@@ -406,35 +414,31 @@ export function stripes(layer, ctx) {
|
|
|
406
414
|
*/
|
|
407
415
|
export async function image(layer, ctx) {
|
|
408
416
|
if (!layer.src) throw new TypeError('An image layer needs a src (a file path or bytes)');
|
|
409
|
-
const
|
|
417
|
+
const L = /** @type {any} */ (layer);
|
|
418
|
+
// 1.1: an explicit box in px (x, y, width, height), as an SVG <image> would take.
|
|
419
|
+
const box = L.x != null || L.y != null || L.width != null || L.height != null
|
|
420
|
+
? { x: pos(L.x ?? 0, 'x', ctx.canvas), y: pos(L.y ?? 0, 'y', ctx.canvas), width: L.width != null ? pos(L.width, 'x', ctx.canvas) : ctx.width, height: L.height != null ? pos(L.height, 'y', ctx.canvas) : ctx.height }
|
|
421
|
+
: regionPx(layer.region, ctx.canvas);
|
|
410
422
|
const fit = layer.fit ?? 'cover';
|
|
411
423
|
const focus = layer.focus ?? 'center';
|
|
412
|
-
let bytes = typeof layer.src === 'string' ? await readSource(layer.src) : layer.src;
|
|
413
|
-
let mime =
|
|
414
|
-
let preserve = `${alignFor(focus)} ${fit === 'contain' ? 'meet' : 'slice'}`;
|
|
424
|
+
let bytes = typeof layer.src === 'string' || layer.src instanceof URL ? await readSource(layer.src) : await loadBytes(layer.src, 'image');
|
|
425
|
+
let mime = imageMime(bytes);
|
|
426
|
+
let preserve = fit === 'stretch' ? 'none' : `${alignFor(focus)} ${fit === 'contain' ? 'meet' : 'slice'}`;
|
|
415
427
|
let sharpBlurred = false;
|
|
416
428
|
|
|
429
|
+
/** Source pixels and what was embedded, for lint. */
|
|
430
|
+
let source = imageSize(bytes);
|
|
431
|
+
let embedded = source;
|
|
417
432
|
if (ctx.sharp && mime !== 'image/svg+xml') {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
let img = ctx.sharp(bytes).rotate().resize(w, hgt, { fit, position, background: { r: 0, g: 0, b: 0, alpha: 0 } });
|
|
422
|
-
// Stretch the tones first, so a duotone spans both of its colours.
|
|
423
|
-
if (layer.normalize ?? Boolean(layer.duotone || layer.grayscale)) img = img.normalise();
|
|
424
|
-
// Blur here rather than in SVG: sharp clamps at the edges, where an SVG
|
|
425
|
-
// blur (in librsvg) fades the photo to transparent.
|
|
426
|
-
if (layer.blur) {
|
|
427
|
-
img = img.blur(Math.max(0.3, len(layer.blur, ctx.canvas)));
|
|
428
|
-
sharpBlurred = true;
|
|
429
|
-
}
|
|
430
|
-
const meta = await ctx.sharp(bytes).metadata();
|
|
431
|
-
const alpha = meta.hasAlpha || fit === 'contain';
|
|
432
|
-
bytes = await (alpha ? img.png() : img.jpeg({ quality: 90 })).toBuffer();
|
|
433
|
-
mime = alpha ? 'image/png' : 'image/jpeg';
|
|
433
|
+
const out = await resample(ctx, bytes, box, fit, focus, layer);
|
|
434
|
+
({ bytes, mime, source, embedded } = out);
|
|
435
|
+
sharpBlurred = Boolean(layer.blur);
|
|
434
436
|
preserve = 'none';
|
|
435
437
|
} else if (!mime) {
|
|
436
438
|
throw new TypeError('Unrecognised image format (use PNG, JPEG, WebP, AVIF, GIF or SVG)');
|
|
437
439
|
}
|
|
440
|
+
const res = /** @type {any} */ (ctx).res ?? 1;
|
|
441
|
+
/** @type {any} */ (ctx).report?.({ kind: 'image', id: layer.id, drawn: { width: box.width * res, height: box.height * res }, source, embedded, fit });
|
|
438
442
|
|
|
439
443
|
const p = ctx.palette;
|
|
440
444
|
const id = `${ctx.id}f`;
|
|
@@ -454,7 +458,7 @@ export async function image(layer, ctx) {
|
|
|
454
458
|
? `<filter${attrs({ id, x: 0, y: 0, width: '100%', height: '100%', 'color-interpolation-filters': 'sRGB' })}>${filters.join('')}</filter>`
|
|
455
459
|
: '';
|
|
456
460
|
|
|
457
|
-
let body = `<image${attrs({ x: box.x, y: box.y, width: box.width, height: box.height, preserveAspectRatio: preserve, filter: defs ? `url(#${id})` : undefined, href: `data:${mime};base64,${
|
|
461
|
+
let body = `<image${attrs({ x: box.x, y: box.y, width: box.width, height: box.height, preserveAspectRatio: preserve, filter: defs ? `url(#${id})` : undefined, href: `data:${mime};base64,${toBase64(bytes)}` })}/>`;
|
|
458
462
|
if (layer.tint) {
|
|
459
463
|
const t = typeof layer.tint === 'string' ? { color: layer.tint } : layer.tint;
|
|
460
464
|
body += `<rect${attrs({ x: box.x, y: box.y, width: box.width, height: box.height, fill: color(t.color, p.accent), opacity: t.amount ?? 0.35, style: `mix-blend-mode:${t.blend ?? 'multiply'}` })}/>`;
|
|
@@ -462,6 +466,64 @@ export async function image(layer, ctx) {
|
|
|
462
466
|
return { defs, body: wrap(layer, body) };
|
|
463
467
|
}
|
|
464
468
|
|
|
469
|
+
/** Embeds up to this many pixels at top level are sized to their box, as in 1.0, even past the source's pixels. */
|
|
470
|
+
const CAP_FREE = 4e6;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resize and crop an image to its box with sharp, at the pixels it's drawn
|
|
474
|
+
* at (its box times the group and transform scale above it), never above
|
|
475
|
+
* the source's own pixels (the SVG scales it up instead, so a small photo
|
|
476
|
+
* isn't embedded at output size). Opaque results are JPEG, even from a
|
|
477
|
+
* source with an unused alpha channel; PNG only where there's transparency.
|
|
478
|
+
*
|
|
479
|
+
* @param {RenderContext} ctx
|
|
480
|
+
* @param {Uint8Array} bytes
|
|
481
|
+
* @param {{ width: number, height: number }} box
|
|
482
|
+
* @param {string} fit
|
|
483
|
+
* @param {any} focus
|
|
484
|
+
* @param {import('./index.js').ImageLayer} layer
|
|
485
|
+
* @returns {Promise<{ bytes: Uint8Array, mime: string, source: { width: number, height: number } | undefined, embedded: { width: number, height: number } }>}
|
|
486
|
+
*/
|
|
487
|
+
async function resample(ctx, bytes, box, fit, focus, layer) {
|
|
488
|
+
const sharp = /** @type {any} */ (ctx.sharp);
|
|
489
|
+
const meta = await sharp(bytes).metadata();
|
|
490
|
+
// EXIF orientations 5–8 turn the image a quarter.
|
|
491
|
+
const [sw, sh] = (meta.orientation ?? 1) >= 5 ? [meta.height, meta.width] : [meta.width, meta.height];
|
|
492
|
+
const res = /** @type {any} */ (ctx).res ?? 1;
|
|
493
|
+
let w = box.width * res;
|
|
494
|
+
let hgt = box.height * res;
|
|
495
|
+
// How much the fit would enlarge the source: past 1, embed at the source's
|
|
496
|
+
// own size. (A top-level image under 4 MP keeps 1.0's box-sized embed, so
|
|
497
|
+
// 1.0 covers stay byte-identical.)
|
|
498
|
+
const cap = res !== 1 || w * hgt > CAP_FREE;
|
|
499
|
+
const grow = !cap || fit === 'stretch' ? 1 : fit === 'contain' ? Math.min(w / sw, hgt / sh) : Math.max(w / sw, hgt / sh);
|
|
500
|
+
if (grow > 1) { w /= grow; hgt /= grow; }
|
|
501
|
+
if (cap && fit === 'stretch') { w = Math.min(w, sw); hgt = Math.min(hgt, sh); }
|
|
502
|
+
w = Math.max(1, Math.round(w));
|
|
503
|
+
hgt = Math.max(1, Math.round(hgt));
|
|
504
|
+
const position = typeof focus === 'string' && ['attention', 'entropy'].includes(focus) ? focus : (POSITIONS[focus] ?? 'centre');
|
|
505
|
+
let img = sharp(bytes).rotate().resize(w, hgt, { fit: fit === 'stretch' ? 'fill' : fit, position, background: { r: 0, g: 0, b: 0, alpha: 0 } });
|
|
506
|
+
// Stretch the tones first, so a duotone spans both of its colours.
|
|
507
|
+
if (layer.normalize ?? Boolean(layer.duotone || layer.grayscale)) img = img.normalise();
|
|
508
|
+
// Blur here rather than in SVG: sharp clamps at the edges, where an SVG
|
|
509
|
+
// blur (in librsvg) fades the photo to transparent. In the pixels drawn.
|
|
510
|
+
if (layer.blur) img = img.blur(Math.max(0.3, len(layer.blur, ctx.canvas) * res / Math.max(1, grow)));
|
|
511
|
+
const embedded = { width: w, height: hgt };
|
|
512
|
+
const source = { width: sw, height: sh };
|
|
513
|
+
if (!meta.hasAlpha && fit !== 'contain') return { bytes: await img.jpeg({ quality: 90 }).toBuffer(), mime: 'image/jpeg', source, embedded };
|
|
514
|
+
// An alpha channel: PNG only if it's used (a letterbox, a cutout).
|
|
515
|
+
const { data, info } = await img.clone().ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
516
|
+
const raw = { raw: { width: info.width, height: info.height, channels: 4 } };
|
|
517
|
+
let opaque = true;
|
|
518
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] !== 255) { opaque = false; break; }
|
|
519
|
+
if (opaque) return { bytes: await sharp(data, raw).removeAlpha().jpeg({ quality: 90 }).toBuffer(), mime: 'image/jpeg', source, embedded };
|
|
520
|
+
if (fit === 'contain' && !meta.hasAlpha && res === 1 && grow <= 1) {
|
|
521
|
+
// As 1.0 encoded it (the letterbox's alpha), byte for byte.
|
|
522
|
+
return { bytes: await img.png().toBuffer(), mime: 'image/png', source, embedded };
|
|
523
|
+
}
|
|
524
|
+
return { bytes: await sharp(data, raw).png().toBuffer(), mime: 'image/png', source, embedded };
|
|
525
|
+
}
|
|
526
|
+
|
|
465
527
|
/** Anchor name to sharp's crop `position`. */
|
|
466
528
|
const POSITIONS = /** @type {Record<string, string>} */ ({
|
|
467
529
|
center: 'centre', top: 'top', bottom: 'bottom', left: 'left', right: 'right',
|
|
@@ -484,27 +546,7 @@ function defaultDuotone(p) {
|
|
|
484
546
|
|
|
485
547
|
/** @param {string} src @returns {Promise<Uint8Array>} */
|
|
486
548
|
async function readSource(src) {
|
|
487
|
-
|
|
488
|
-
if (data) return Buffer.from(data[1], 'base64');
|
|
489
|
-
if (/^https?:/i.test(src)) throw new TypeError('Image src must be a local file or bytes: covers never fetch from the network');
|
|
490
|
-
return readFile(src);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Sniff an image's MIME type from its first bytes.
|
|
495
|
-
*
|
|
496
|
-
* @param {Uint8Array} b - Image bytes.
|
|
497
|
-
* @returns {string | undefined} The MIME type.
|
|
498
|
-
*/
|
|
499
|
-
export function sniff(b) {
|
|
500
|
-
if (b[0] === 0x89 && b[1] === 0x50) return 'image/png';
|
|
501
|
-
if (b[0] === 0xff && b[1] === 0xd8) return 'image/jpeg';
|
|
502
|
-
if (b[0] === 0x47 && b[1] === 0x49) return 'image/gif';
|
|
503
|
-
const head = Buffer.from(b.subarray(0, 32)).toString('latin1');
|
|
504
|
-
if (head.startsWith('RIFF') && head.slice(8, 12) === 'WEBP') return 'image/webp';
|
|
505
|
-
if (head.slice(4, 12) === 'ftypavif' || head.slice(4, 12) === 'ftypavis') return 'image/avif';
|
|
506
|
-
if (/^\s*(<\?xml|<svg)/i.test(Buffer.from(b.subarray(0, 256)).toString('utf8'))) return 'image/svg+xml';
|
|
507
|
-
return undefined;
|
|
549
|
+
return loadBytes(src, 'image');
|
|
508
550
|
}
|
|
509
551
|
|
|
510
552
|
// ---------------------------------------------------------------------------
|
|
@@ -766,7 +808,8 @@ export function icon(layer, ctx) {
|
|
|
766
808
|
const p = ctx.palette;
|
|
767
809
|
// A name that is markup is a raw SVG (so one `icon` option can carry either).
|
|
768
810
|
const name = typeof layer.name === 'string' ? layer.name.trim() : undefined;
|
|
769
|
-
const
|
|
811
|
+
const markup = /** @type {any} */ (layer).markup;
|
|
812
|
+
const svg = layer.svg ?? (name?.startsWith('<') ? name : undefined) ?? (markup ? `<svg viewBox="${(/** @type {any} */ (layer).viewBox ?? [0, 0, 512, 512]).join(' ')}"><g fill="currentColor">${markup}</g></svg>` : undefined);
|
|
770
813
|
const data = svg
|
|
771
814
|
? parseSvgIcon(svg)
|
|
772
815
|
: name
|
|
@@ -779,8 +822,11 @@ export function icon(layer, ctx) {
|
|
|
779
822
|
const tileOpts = layer.tile === true ? {} : layer.tile || null;
|
|
780
823
|
const size = len(layer.size, ctx.canvas, tileOpts ? '15vmin' : '24vmin');
|
|
781
824
|
const tileSize = tileOpts ? len(tileOpts.size, ctx.canvas, size / 0.5) : size;
|
|
782
|
-
const margin = len(layer.margin, ctx.canvas, unit(ctx.canvas) * 0.07);
|
|
783
|
-
|
|
825
|
+
const margin = len(layer.margin, ctx.canvas, /** @type {any} */ (ctx).inset ?? unit(ctx.canvas) * 0.07);
|
|
826
|
+
// 1.1: `x`, `y` place the icon's top-left corner in px (a mark at translate(x y) scale(size / 512)).
|
|
827
|
+
const at = /** @type {any} */ (layer).x != null || /** @type {any} */ (layer).y != null
|
|
828
|
+
? { x: pos(/** @type {any} */ (layer).x ?? 0, 'x', ctx.canvas), y: pos(/** @type {any} */ (layer).y ?? 0, 'y', ctx.canvas) }
|
|
829
|
+
: place({ width: tileSize, height: tileSize, canvas: ctx.canvas, anchor: layer.anchor ?? 'center', margin, position: layer.position, offset: layer.offset });
|
|
784
830
|
|
|
785
831
|
let defs = '';
|
|
786
832
|
let body = '';
|
|
@@ -807,6 +853,13 @@ export function icon(layer, ctx) {
|
|
|
807
853
|
const ih = vh * fitScale;
|
|
808
854
|
let inner = data.body;
|
|
809
855
|
if (layer.strokeWidth != null) inner = inner.replace(/stroke-width="[^"]*"/g, `stroke-width="${num(layer.strokeWidth)}"`);
|
|
856
|
+
// 1.1: `fill` paints the icon with a gradient, tile or preset, laid over its grid.
|
|
857
|
+
const fill = /** @type {any} */ (layer).fill;
|
|
858
|
+
if (fill != null && typeof fill === 'object') {
|
|
859
|
+
const fp = paint(fill, 'currentColor', { id: `${ctx.id}i`, box: { x: vx, y: vy, width: vw, height: vh }, canvas: ctx.canvas });
|
|
860
|
+
defs += fp.defs;
|
|
861
|
+
inner = inner.replace(/currentColor/g, fp.value);
|
|
862
|
+
}
|
|
810
863
|
body += `<svg${attrs({
|
|
811
864
|
x: at.x + (tileSize - iw) / 2,
|
|
812
865
|
y: at.y + (tileSize - ih) / 2,
|
|
@@ -814,7 +867,7 @@ export function icon(layer, ctx) {
|
|
|
814
867
|
height: ih,
|
|
815
868
|
viewBox: `${num(vx)} ${num(vy)} ${num(vw)} ${num(vh)}`,
|
|
816
869
|
overflow: 'visible',
|
|
817
|
-
color: color(layer.color, tileOpts ? p.accent : p.text),
|
|
870
|
+
color: typeof /** @type {any} */ (layer).fill === 'string' ? toHex(/** @type {any} */ (layer).fill) : color(layer.color, tileOpts ? p.accent : p.text),
|
|
818
871
|
})}>${inner}</svg>`;
|
|
819
872
|
return { defs, body: wrap(layer, body) };
|
|
820
873
|
}
|
|
@@ -843,14 +896,16 @@ function shadowPass(name, blur, dy, opacity) {
|
|
|
843
896
|
* @returns {Promise<Markup>}
|
|
844
897
|
*/
|
|
845
898
|
export async function text(layer, ctx) {
|
|
899
|
+
// 1.1 options (baseline placement, justification, fitting, paths, live text, auto colour).
|
|
900
|
+
if (wantsType(layer)) return typeset(layer, ctx);
|
|
846
901
|
// An override object in `content` (a recipe's "{title}") means its content.
|
|
847
902
|
const raw = layer.content && typeof layer.content === 'object' ? /** @type {any} */ (layer.content).content : layer.content;
|
|
848
|
-
const content =
|
|
903
|
+
const content = caseText(String(raw ?? ''), layer.transform);
|
|
849
904
|
if (!content.trim()) return { defs: '', body: '' };
|
|
850
905
|
const hb = await harfbuzz();
|
|
851
906
|
const p = ctx.palette;
|
|
852
907
|
const u = unit(ctx.canvas);
|
|
853
|
-
const margin = len(layer.margin, ctx.canvas, u * 0.07);
|
|
908
|
+
const margin = len(layer.margin, ctx.canvas, /** @type {any} */ (ctx).inset ?? u * 0.07);
|
|
854
909
|
const anchor = layer.anchor ?? 'bottom-left';
|
|
855
910
|
const align = layer.align ?? anchorAlign(anchor);
|
|
856
911
|
const bg = layer.background ? (typeof layer.background === 'string' ? { color: layer.background } : layer.background) : null;
|
|
@@ -886,7 +941,7 @@ export async function text(layer, ctx) {
|
|
|
886
941
|
const lsize = len(labelOpts.size, ctx.canvas, Math.max(12, set.size * 0.32, u * 0.02));
|
|
887
942
|
const lset = layout({
|
|
888
943
|
face: lface, hb,
|
|
889
|
-
text:
|
|
944
|
+
text: caseText(labelOpts.content, labelOpts.transform ?? 'uppercase'),
|
|
890
945
|
size: lsize,
|
|
891
946
|
letterSpacing: labelOpts.letterSpacing ?? 0.08,
|
|
892
947
|
maxWidth,
|
|
@@ -921,14 +976,16 @@ export async function text(layer, ctx) {
|
|
|
921
976
|
}
|
|
922
977
|
const d = set.lines.map((l, i) => runPath(face, l.run, set.size, lineX(l.run.width), y + cap + i * lh)).join('');
|
|
923
978
|
body += `<path${attrs({ d, fill: color(layer.color, p.text) })}/>`;
|
|
979
|
+
// Where the block landed, for later layers ({layout.<id>.bottom}); what was drawn, for lint (the label too).
|
|
980
|
+
recordLayout(ctx, layer.id, { x: left, y: top, width: blockW, height: blockH }, { x: lineX(set.lines[0]?.run.width ?? 0), y: y + cap, baseline: y + cap + (set.lines.length - 1) * lh, size: set.size, lines: set.lines.length });
|
|
981
|
+
const report = /** @type {any} */ (ctx).report;
|
|
982
|
+
if (report) {
|
|
983
|
+
report({ kind: 'text', id: layer.id, content, size: set.size, capHeight: cap, box: { x: at.x, y: at.y, width: boxW, height: boxH } });
|
|
984
|
+
if (label) report({ kind: 'text', id: layer.id, content: label.set.lines.map((l) => l.text).join('\n'), size: label.set.size, capHeight: label.cap, box: null });
|
|
985
|
+
}
|
|
924
986
|
return { defs: '', body: wrap(layer, body) };
|
|
925
987
|
}
|
|
926
988
|
|
|
927
|
-
/** @param {string} s @param {string | undefined} t @returns {string} */
|
|
928
|
-
function transform(s, t) {
|
|
929
|
-
const clean = s.replace(/[\t\r ]+/g, ' ').replace(/ *\n */g, '\n').trim();
|
|
930
|
-
return t === 'uppercase' ? clean.toUpperCase() : t === 'lowercase' ? clean.toLowerCase() : clean;
|
|
931
|
-
}
|
|
932
989
|
|
|
933
990
|
/**
|
|
934
991
|
* Padding as `[vertical, horizontal]` pixels.
|
|
@@ -969,4 +1026,15 @@ export function frame(layer, ctx) {
|
|
|
969
1026
|
}
|
|
970
1027
|
|
|
971
1028
|
/** Every layer type, by name. */
|
|
972
|
-
|
|
1029
|
+
/**
|
|
1030
|
+
* `group` (and `repeat`, `defs`, `use`) are drawn by the renderer (they
|
|
1031
|
+
* render nested stacks); this entry registers the type and explains itself
|
|
1032
|
+
* if called directly.
|
|
1033
|
+
*
|
|
1034
|
+
* @returns {never}
|
|
1035
|
+
*/
|
|
1036
|
+
function group() {
|
|
1037
|
+
throw new TypeError('A group layer is drawn by renderSvg()/renderImage(), not on its own');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export const LAYERS = Object.freeze({ solid, linear, radial, mesh, stripes, image, grain, pattern, shapes, icon, text, frame, diagram, paper, scribble, ink, dither, contour, waveform, tiles, rule, group, shape, svg, table, barcode, perforation, tear, texture, repeat: group, defs: group, use: group });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An image's pixel size from its header, without decoding it.
|
|
3
|
+
*
|
|
4
|
+
* @module lint/image-size
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An image's pixel size from its header (PNG, JPEG, WebP, GIF).
|
|
9
|
+
*
|
|
10
|
+
* @param {Uint8Array} b
|
|
11
|
+
* @returns {{ width: number, height: number } | undefined}
|
|
12
|
+
*/
|
|
13
|
+
export function imageSize(b) {
|
|
14
|
+
const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
15
|
+
if (b[0] === 0x89 && b[1] === 0x50) return { width: v.getUint32(16), height: v.getUint32(20) };
|
|
16
|
+
if (b[0] === 0x47 && b[1] === 0x49) return { width: v.getUint16(6, true), height: v.getUint16(8, true) };
|
|
17
|
+
if (b[0] === 0xff && b[1] === 0xd8) {
|
|
18
|
+
let i = 2;
|
|
19
|
+
while (i + 9 < b.length) {
|
|
20
|
+
if (b[i] !== 0xff) { i++; continue; }
|
|
21
|
+
const m = b[i + 1];
|
|
22
|
+
if (m >= 0xc0 && m <= 0xcf && m !== 0xc4 && m !== 0xc8 && m !== 0xcc) return { width: v.getUint16(i + 7), height: v.getUint16(i + 5) };
|
|
23
|
+
i += 2 + v.getUint16(i + 2);
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (String.fromCharCode(b[0], b[1], b[2], b[3]) === 'RIFF' && String.fromCharCode(b[8], b[9], b[10], b[11]) === 'WEBP') {
|
|
28
|
+
const kind = String.fromCharCode(b[12], b[13], b[14], b[15]);
|
|
29
|
+
if (kind === 'VP8X') return { width: 1 + (b[24] | (b[25] << 8) | (b[26] << 16)), height: 1 + (b[27] | (b[28] << 8) | (b[29] << 16)) };
|
|
30
|
+
if (kind === 'VP8L') { const x = v.getUint32(21, true); return { width: (x & 0x3fff) + 1, height: ((x >> 14) & 0x3fff) + 1 }; }
|
|
31
|
+
if (kind === 'VP8 ') return { width: v.getUint16(26, true) & 0x3fff, height: v.getUint16(28, true) & 0x3fff };
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Release cover art checks.
|
|
2
|
+
import type { Cover, CoverOptions } from '../index.js';
|
|
3
|
+
export { imageSize } from './image-size.js';
|
|
4
|
+
|
|
5
|
+
/** A problem `lint` found. */
|
|
6
|
+
export interface LintIssue {
|
|
7
|
+
rule: 'aspect' | 'resolution' | 'url' | 'handle' | 'contact' | 'price' | 'promo' | 'platform' | 'date' | 'physical' | 'legibility' | 'metadata' | 'barcode' | 'upscaled';
|
|
8
|
+
severity: 'error' | 'warning';
|
|
9
|
+
message: string;
|
|
10
|
+
layer?: string;
|
|
11
|
+
text?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/** Check a cover against Spotify's artwork requirements and distributors' content rules. */
|
|
16
|
+
export function lint(input: CoverOptions | Cover, options?: { target?: 'spotify' | 'release'; artist?: string; title?: string; size?: number }): Promise<{ ok: boolean; target: string; issues: LintIssue[] }>;
|