@arraypress/covers 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/LICENSE +29 -0
- package/README.md +846 -2
- package/fonts/OFL.txt +93 -0
- package/fonts/inter-latin-400.woff +0 -0
- package/fonts/inter-latin-500.woff +0 -0
- package/fonts/inter-latin-600.woff +0 -0
- package/fonts/inter-latin-700.woff +0 -0
- package/package.json +124 -4
- package/recipes/aurora.json +132 -0
- package/recipes/bauhaus.json +113 -0
- package/recipes/bigType.json +103 -0
- package/recipes/brand.json +1143 -0
- package/recipes/contour.json +109 -0
- package/recipes/cutout.json +191 -0
- package/recipes/diagram.json +92 -0
- package/recipes/dither.json +170 -0
- package/recipes/duotone.json +75 -0
- package/recipes/gridTitle.json +136 -0
- package/recipes/iconTile.json +214 -0
- package/recipes/issue.json +221 -0
- package/recipes/mark.json +170 -0
- package/recipes/photo.json +156 -0
- package/recipes/riso.json +387 -0
- package/recipes/stripes.json +206 -0
- package/recipes/waveform.json +154 -0
- package/schema/recipe.schema.json +8822 -0
- package/src/animate/index.d.ts +19 -0
- package/src/animate/index.js +116 -0
- package/src/astro.d.ts +31 -0
- package/src/astro.js +86 -0
- package/src/builtins.js +4 -0
- package/src/color.js +246 -0
- package/src/diagram.js +248 -0
- package/src/effects/ascii.d.ts +7 -0
- package/src/effects/ascii.js +93 -0
- package/src/effects/buffer.d.ts +60 -0
- package/src/effects/buffer.js +472 -0
- package/src/effects/detail.d.ts +12 -0
- package/src/effects/detail.js +124 -0
- package/src/effects/digital.d.ts +14 -0
- package/src/effects/digital.js +216 -0
- package/src/effects/distort.d.ts +20 -0
- package/src/effects/distort.js +314 -0
- package/src/effects/dither.d.ts +8 -0
- package/src/effects/dither.js +147 -0
- package/src/effects/drop-shadow.d.ts +4 -0
- package/src/effects/drop-shadow.js +45 -0
- package/src/effects/extrude.d.ts +5 -0
- package/src/effects/extrude.js +55 -0
- package/src/effects/film.d.ts +11 -0
- package/src/effects/film.js +204 -0
- package/src/effects/glow.d.ts +8 -0
- package/src/effects/glow.js +56 -0
- package/src/effects/grade.d.ts +30 -0
- package/src/effects/grade.js +274 -0
- package/src/effects/halftone.d.ts +8 -0
- package/src/effects/halftone.js +95 -0
- package/src/effects/index.d.ts +125 -0
- package/src/effects/index.js +277 -0
- package/src/effects/ink.d.ts +12 -0
- package/src/effects/ink.js +70 -0
- package/src/effects/lens.d.ts +14 -0
- package/src/effects/lens.js +277 -0
- package/src/effects/light.d.ts +10 -0
- package/src/effects/light.js +59 -0
- package/src/effects/looks.d.ts +9 -0
- package/src/effects/looks.js +74 -0
- package/src/effects/mask.d.ts +52 -0
- package/src/effects/mask.js +314 -0
- package/src/effects/motion.d.ts +6 -0
- package/src/effects/motion.js +60 -0
- package/src/effects/newsprint.d.ts +6 -0
- package/src/effects/newsprint.js +89 -0
- package/src/effects/optics.d.ts +10 -0
- package/src/effects/optics.js +213 -0
- package/src/effects/options.d.ts +6 -0
- package/src/effects/options.js +106 -0
- package/src/effects/paint-presets.d.ts +10 -0
- package/src/effects/paint-presets.js +47 -0
- package/src/effects/paint.d.ts +49 -0
- package/src/effects/paint.js +66 -0
- package/src/effects/pixels.d.ts +24 -0
- package/src/effects/pixels.js +138 -0
- package/src/effects/polar.d.ts +10 -0
- package/src/effects/polar.js +265 -0
- package/src/effects/region.d.ts +8 -0
- package/src/effects/region.js +136 -0
- package/src/effects/round-border.d.ts +5 -0
- package/src/effects/round-border.js +62 -0
- package/src/effects/screen.d.ts +14 -0
- package/src/effects/screen.js +212 -0
- package/src/effects/sort.d.ts +8 -0
- package/src/effects/sort.js +125 -0
- package/src/effects/symmetry.d.ts +6 -0
- package/src/effects/symmetry.js +74 -0
- package/src/effects/tile.d.ts +7 -0
- package/src/effects/tile.js +55 -0
- package/src/effects/tone.d.ts +16 -0
- package/src/effects/tone.js +117 -0
- package/src/effects/transform.d.ts +35 -0
- package/src/effects/transform.js +127 -0
- package/src/effects/util.d.ts +16 -0
- package/src/effects/util.js +85 -0
- package/src/extract.js +91 -0
- package/src/flatten.js +234 -0
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +393 -0
- 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 +187 -0
- package/src/grid.js +41 -0
- package/src/handmade.js +253 -0
- package/src/icons.js +191 -0
- package/src/index.d.ts +1083 -0
- package/src/index.js +27 -0
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +616 -0
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +56 -0
- package/src/layers/barcode.d.ts +19 -0
- package/src/layers/barcode.js +63 -0
- package/src/layers/code128.d.ts +4 -0
- package/src/layers/code128.js +28 -0
- package/src/layers/expr.d.ts +6 -0
- package/src/layers/expr.js +121 -0
- package/src/layers/geometry.d.ts +6 -0
- package/src/layers/geometry.js +238 -0
- package/src/layers/index.d.ts +18 -0
- package/src/layers/index.js +15 -0
- package/src/layers/perforation.d.ts +14 -0
- package/src/layers/perforation.js +47 -0
- package/src/layers/repeat.d.ts +29 -0
- package/src/layers/repeat.js +90 -0
- package/src/layers/shape.d.ts +46 -0
- package/src/layers/shape.js +115 -0
- package/src/layers/soft.d.ts +5 -0
- package/src/layers/soft.js +43 -0
- package/src/layers/svg.d.ts +23 -0
- package/src/layers/svg.js +109 -0
- package/src/layers/table.d.ts +25 -0
- package/src/layers/table.js +80 -0
- package/src/layers/tear.d.ts +21 -0
- package/src/layers/tear.js +74 -0
- package/src/layers/texture.d.ts +17 -0
- package/src/layers/texture.js +106 -0
- package/src/layers/util.d.ts +8 -0
- package/src/layers/util.js +48 -0
- package/src/layers.js +1056 -0
- 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/palette.js +185 -0
- package/src/platform/bytes.d.ts +22 -0
- package/src/platform/bytes.js +103 -0
- package/src/platform/compress.d.ts +12 -0
- package/src/platform/compress.js +81 -0
- package/src/platform/harfbuzz.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +13 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +72 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/node.d.ts +26 -0
- package/src/platform/node.js +86 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/png.js +126 -0
- package/src/print.js +659 -0
- package/src/raster/index.d.ts +24 -0
- package/src/raster/index.js +171 -0
- package/src/recipes.js +79 -0
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -0
- package/src/render/content.d.ts +5 -0
- package/src/render/content.js +58 -0
- package/src/render/context.d.ts +82 -0
- package/src/render/grain.d.ts +7 -0
- package/src/render/grain.js +113 -0
- package/src/render/group.d.ts +23 -0
- package/src/render/group.js +67 -0
- package/src/render/key.d.ts +5 -0
- package/src/render/key.js +91 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/tiles.d.ts +6 -0
- package/src/render/tiles.js +77 -0
- package/src/render/use.d.ts +27 -0
- package/src/render/use.js +73 -0
- package/src/render/wrap.d.ts +14 -0
- package/src/render/wrap.js +199 -0
- package/src/render.js +553 -0
- package/src/rng.js +95 -0
- package/src/svg.js +69 -0
- package/src/text.d.ts +29 -0
- package/src/text.js +237 -0
- package/src/type/auto-color.d.ts +9 -0
- package/src/type/auto-color.js +44 -0
- package/src/type/fallback.d.ts +11 -0
- package/src/type/fallback.js +92 -0
- package/src/type/glyphs.d.ts +24 -0
- package/src/type/glyphs.js +87 -0
- package/src/type/index.d.ts +5 -0
- package/src/type/index.js +10 -0
- package/src/type/kern.d.ts +4 -0
- package/src/type/kern.js +144 -0
- package/src/type/layout-ref.d.ts +10 -0
- package/src/type/layout-ref.js +74 -0
- package/src/type/line.d.ts +11 -0
- package/src/type/line.js +105 -0
- package/src/type/live.d.ts +6 -0
- package/src/type/live.js +39 -0
- package/src/type/name.d.ts +4 -0
- package/src/type/name.js +32 -0
- package/src/type/path.d.ts +18 -0
- package/src/type/path.js +89 -0
- package/src/type/pixel.d.ts +9 -0
- package/src/type/pixel.js +115 -0
- package/src/type/sfnt.d.ts +20 -0
- package/src/type/sfnt.js +258 -0
- package/src/type/spans.d.ts +33 -0
- package/src/type/spans.js +138 -0
- package/src/type/symbols.d.ts +7 -0
- package/src/type/symbols.js +89 -0
- package/src/type/typeset.d.ts +10 -0
- package/src/type/typeset.js +352 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
package/src/diagram.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `diagram` layer: white line-art over a field (the engineering-blog
|
|
3
|
+
* look). One icon, or a few nodes (icons in outlined square or circle
|
|
4
|
+
* tiles) joined by arrows, with an optional dotted grid that breaks around
|
|
5
|
+
* the drawing instead of running through it.
|
|
6
|
+
*
|
|
7
|
+
* Layouts (node centres, relative to `position`):
|
|
8
|
+
*
|
|
9
|
+
* - `single` one node
|
|
10
|
+
* - `row` 2–4 nodes side by side; `arrows: 'loop'` adds an elbow back from the last to the first
|
|
11
|
+
* - `triangle` one on top, two below; arrows from the top to each (`'both'`: there and back)
|
|
12
|
+
* - `square` four corners, arrows round the cycle; a fifth node sits in the middle with dotted spokes and corner brackets
|
|
13
|
+
* - `stack` two on top merging into one below
|
|
14
|
+
* - `hub` a large first node with the rest around its edge, knocked out of it (a globe with databases)
|
|
15
|
+
*
|
|
16
|
+
* Everything is stroked in one colour at one width; stroke icons (Lucide)
|
|
17
|
+
* are rescaled so their lines match the tiles' lines.
|
|
18
|
+
*
|
|
19
|
+
* @module diagram
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { attrs, num, layerAttrs } from './svg.js';
|
|
23
|
+
import { len } from './geometry.js';
|
|
24
|
+
import { toHex } from './color.js';
|
|
25
|
+
import { resolveIcon, parseSvgIcon } from './icons.js';
|
|
26
|
+
import { gridPath } from './grid.js';
|
|
27
|
+
|
|
28
|
+
/** A four-point spark: the glyph for a node with no icon. */
|
|
29
|
+
const SPARK = 'M12 2c.4 4.9 2.1 7.6 7 8.6l3 .4-3 .4c-4.9 1-6.6 3.7-7 8.6-.4-4.9-2.1-7.6-7-8.6L2 11l3-.4c4.9-1 6.6-3.7 7-8.6Z';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a node's icon to markup + viewBox.
|
|
33
|
+
*
|
|
34
|
+
* @param {string | undefined} icon - Iconify name, raw SVG, or nothing (the spark).
|
|
35
|
+
* @returns {{ body: string, viewBox: number[] }}
|
|
36
|
+
*/
|
|
37
|
+
function iconData(icon) {
|
|
38
|
+
if (typeof icon === 'string' && icon.trim().startsWith('<')) return parseSvgIcon(icon);
|
|
39
|
+
if (typeof icon === 'string' && icon) return resolveIcon(icon);
|
|
40
|
+
return { body: `<path fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" d="${SPARK}"/>`, viewBox: [0, 0, 24, 24] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* An open arrowhead (two strokes) at `to`, pointing away from `from`.
|
|
45
|
+
*
|
|
46
|
+
* @param {[number, number]} from @param {[number, number]} to
|
|
47
|
+
* @param {number} size - Arm length.
|
|
48
|
+
* @returns {string} Path data.
|
|
49
|
+
*/
|
|
50
|
+
function head(from, to, size) {
|
|
51
|
+
const a = Math.atan2(to[1] - from[1], to[0] - from[0]);
|
|
52
|
+
const arm = (t) => [to[0] - size * Math.cos(a + t), to[1] - size * Math.sin(a + t)];
|
|
53
|
+
const [l, r] = [arm(0.55), arm(-0.55)];
|
|
54
|
+
return `M${num(l[0])} ${num(l[1])}L${num(to[0])} ${num(to[1])}L${num(r[0])} ${num(r[1])}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The `diagram` layer.
|
|
59
|
+
*
|
|
60
|
+
* @param {import('./index.js').DiagramLayer} layer
|
|
61
|
+
* @param {import('./layers.js').RenderContext} ctx
|
|
62
|
+
* @returns {import('./layers.js').Markup}
|
|
63
|
+
*/
|
|
64
|
+
export function diagram(layer, ctx) {
|
|
65
|
+
const { width: w, height: h } = ctx;
|
|
66
|
+
const nodesIn = layer.nodes?.length ? layer.nodes : [{ icon: layer.icon, shape: layer.shape ?? (layer.icon ? 'none' : 'square') }];
|
|
67
|
+
const n = nodesIn.length;
|
|
68
|
+
const layout = layer.layout ?? (n === 1 ? 'single' : n === 3 && layer.arrows === 'merge' ? 'stack' : n === 4 && layer.arrows === 'cycle' ? 'square' : 'row');
|
|
69
|
+
const vmin = Math.min(w, h) / 100;
|
|
70
|
+
const defaultSize = { single: nodesIn[0].shape === 'none' ? 40 : 22, row: n >= 4 ? 15 : 19, triangle: 19, square: 12, stack: 18, hub: 13 }[layout] ?? 18;
|
|
71
|
+
const s = len(layer.size, ctx.canvas, defaultSize * vmin);
|
|
72
|
+
const gap = len(layer.gap, ctx.canvas, s * 0.45);
|
|
73
|
+
const stroke = len(layer.stroke, ctx.canvas, '0.3vmin');
|
|
74
|
+
const col = layer.color ? toHex(layer.color) : '#ffffff';
|
|
75
|
+
const cx = (layer.position?.x ?? 0.5) * w;
|
|
76
|
+
const cy = (layer.position?.y ?? 0.5) * h;
|
|
77
|
+
const radius = len(layer.radius, ctx.canvas, s * 0.14);
|
|
78
|
+
const ah = Math.max(stroke * 3.5, s * 0.09);
|
|
79
|
+
const pad = s * 0.1;
|
|
80
|
+
|
|
81
|
+
// Node centres and sizes.
|
|
82
|
+
const d = s + gap;
|
|
83
|
+
/** @type {{ x: number, y: number, size: number, shape: string, icon?: string, brackets?: boolean, knock?: boolean }[]} */
|
|
84
|
+
const nodes = nodesIn.map((nd, i) => {
|
|
85
|
+
let x = 0;
|
|
86
|
+
let y = 0;
|
|
87
|
+
let size = nd.size != null ? len(nd.size, ctx.canvas) : s;
|
|
88
|
+
if (layout === 'row') x = (i - (n - 1) / 2) * d;
|
|
89
|
+
else if (layout === 'triangle') [x, y] = [[0, -d * 0.52], [-d * 0.6, d * 0.5], [d * 0.6, d * 0.5]][i] ?? [0, 0];
|
|
90
|
+
else if (layout === 'square') {
|
|
91
|
+
const half = (s + gap * 2.6) / 2 + s * 0.6;
|
|
92
|
+
[x, y] = [[-half, -half], [half, -half], [half, half], [-half, half], [0, 0]][i] ?? [0, 0];
|
|
93
|
+
} else if (layout === 'stack') [x, y] = [[-d * 0.58, -d * 0.5], [d * 0.58, -d * 0.5], [0, d * 0.55]][i] ?? [0, 0];
|
|
94
|
+
else if (layout === 'hub') {
|
|
95
|
+
if (i === 0) size = nd.size != null ? len(nd.size, ctx.canvas) : s * 3;
|
|
96
|
+
else {
|
|
97
|
+
const big = nodesIn[0].size != null ? len(nodesIn[0].size, ctx.canvas) : s * 3;
|
|
98
|
+
const spots = [[-1, -0.15], [1, -0.15], [0, 0.95], [0, -1]];
|
|
99
|
+
const [ux, uy] = spots[(i - 1) % spots.length];
|
|
100
|
+
[x, y] = [(ux * big) / 2, (uy * big) / 2];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
x: cx + x,
|
|
105
|
+
y: cy + y,
|
|
106
|
+
size,
|
|
107
|
+
shape: nd.shape ?? (layout === 'hub' ? 'none' : layout === 'square' ? 'circle' : 'square'),
|
|
108
|
+
icon: nd.icon,
|
|
109
|
+
brackets: nd.brackets ?? (layout === 'square' && i === 4),
|
|
110
|
+
knock: layout === 'hub' && i > 0,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/** The point on a node's outline toward another point, padded. */
|
|
115
|
+
const edge = (/** @type {typeof nodes[number]} */ nd, /** @type {[number, number]} */ toward) => {
|
|
116
|
+
const dx = toward[0] - nd.x;
|
|
117
|
+
const dy = toward[1] - nd.y;
|
|
118
|
+
const r = nd.size / 2 + pad;
|
|
119
|
+
if (nd.shape === 'circle' || nd.shape === 'none') {
|
|
120
|
+
const l = Math.hypot(dx, dy) || 1;
|
|
121
|
+
return /** @type {[number, number]} */ ([nd.x + (dx / l) * r, nd.y + (dy / l) * r]);
|
|
122
|
+
}
|
|
123
|
+
const t = r / Math.max(Math.abs(dx), Math.abs(dy), 1e-6);
|
|
124
|
+
return /** @type {[number, number]} */ ([nd.x + dx * t, nd.y + dy * t]);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const lines = [];
|
|
128
|
+
const dotted = [];
|
|
129
|
+
/** @param {number} a @param {number} b @param {number} [offset] - Perpendicular shift (for there-and-back pairs). */
|
|
130
|
+
const arrow = (a, b, offset = 0) => {
|
|
131
|
+
const A = nodes[a];
|
|
132
|
+
const B = nodes[b];
|
|
133
|
+
let p = edge(A, [B.x, B.y]);
|
|
134
|
+
let q = edge(B, [A.x, A.y]);
|
|
135
|
+
if (offset) {
|
|
136
|
+
const l = Math.hypot(q[0] - p[0], q[1] - p[1]) || 1;
|
|
137
|
+
const [ox, oy] = [(-(q[1] - p[1]) / l) * offset, ((q[0] - p[0]) / l) * offset];
|
|
138
|
+
p = [p[0] + ox, p[1] + oy];
|
|
139
|
+
q = [q[0] + ox, q[1] + oy];
|
|
140
|
+
}
|
|
141
|
+
lines.push(`M${num(p[0])} ${num(p[1])}L${num(q[0])} ${num(q[1])}${head(p, q, ah)}`);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const mode = layer.arrows ?? (layout === 'single' || layout === 'hub' ? false : layout === 'square' ? 'cycle' : true);
|
|
145
|
+
if (mode) {
|
|
146
|
+
if (layout === 'row') {
|
|
147
|
+
for (let i = 0; i < n - 1; i++) arrow(i, i + 1);
|
|
148
|
+
if (mode === 'loop' && n > 1) {
|
|
149
|
+
// An elbow under the row, from the last node back to the first.
|
|
150
|
+
const A = nodes[n - 1];
|
|
151
|
+
const B = nodes[0];
|
|
152
|
+
const top = A.y + A.size / 2 + pad;
|
|
153
|
+
const low = top + gap * 1.1;
|
|
154
|
+
const r = Math.min(gap * 0.35, s * 0.15);
|
|
155
|
+
lines.push(`M${num(A.x)} ${num(top)}V${num(low - r)}Q${num(A.x)} ${num(low)} ${num(A.x - r)} ${num(low)}H${num(B.x + r)}Q${num(B.x)} ${num(low)} ${num(B.x)} ${num(low - r)}V${num(top)}${head([B.x, top + 1], [B.x, top], ah)}`);
|
|
156
|
+
}
|
|
157
|
+
} else if (layout === 'triangle') {
|
|
158
|
+
const off = mode === 'both' ? s * 0.12 : 0;
|
|
159
|
+
for (const b of [1, 2]) {
|
|
160
|
+
if (nodes[b]) {
|
|
161
|
+
arrow(0, b, off);
|
|
162
|
+
if (mode === 'both') arrow(b, 0, off);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else if (layout === 'square') {
|
|
166
|
+
for (let i = 0; i < Math.min(4, n); i++) arrow(i, (i + 1) % Math.min(4, n));
|
|
167
|
+
if (n > 4) {
|
|
168
|
+
const C = nodes[4];
|
|
169
|
+
for (let i = 0; i < 4; i++) {
|
|
170
|
+
const p = edge(C, [nodes[i].x, nodes[i].y]);
|
|
171
|
+
const q = edge(nodes[i], [C.x, C.y]);
|
|
172
|
+
dotted.push(`M${num(p[0])} ${num(p[1])}L${num(q[0])} ${num(q[1])}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else if (layout === 'stack' && n >= 3) {
|
|
176
|
+
const [L, R, B] = nodes;
|
|
177
|
+
const top = L.y + L.size / 2 + pad;
|
|
178
|
+
const mid = (top + (B.y - B.size / 2 - pad)) / 2;
|
|
179
|
+
const r = Math.min(gap * 0.3, s * 0.12);
|
|
180
|
+
lines.push(`M${num(L.x)} ${num(top)}V${num(mid - r)}Q${num(L.x)} ${num(mid)} ${num(L.x + r)} ${num(mid)}H${num(R.x - r)}Q${num(R.x)} ${num(mid)} ${num(R.x)} ${num(mid - r)}V${num(top)}`);
|
|
181
|
+
const end = B.y - B.size / 2 - pad;
|
|
182
|
+
lines.push(`M${num(B.x)} ${num(mid)}V${num(end)}${head([B.x, end - 1], [B.x, end], ah)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Tiles, icons, brackets.
|
|
187
|
+
let tiles = '';
|
|
188
|
+
/** @type {string[]} */
|
|
189
|
+
const icons = [];
|
|
190
|
+
let knockouts = '';
|
|
191
|
+
for (const nd of nodes) {
|
|
192
|
+
const half = nd.size / 2;
|
|
193
|
+
if (nd.shape === 'square') tiles += `<rect${attrs({ x: nd.x - half, y: nd.y - half, width: nd.size, height: nd.size, rx: Math.min(radius, half) })}/>`;
|
|
194
|
+
else if (nd.shape === 'circle') tiles += `<circle${attrs({ cx: nd.x, cy: nd.y, r: half })}/>`;
|
|
195
|
+
if (nd.brackets) {
|
|
196
|
+
const o = half + pad * 1.6;
|
|
197
|
+
const arm = nd.size * 0.14;
|
|
198
|
+
for (const [sx, sy] of [[-1, -1], [1, -1], [1, 1], [-1, 1]]) {
|
|
199
|
+
const [bx, by] = [nd.x + sx * o, nd.y + sy * o];
|
|
200
|
+
lines.push(`M${num(bx - sx * arm)} ${num(by)}H${num(bx)}V${num(by - sy * arm)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (nd.knock) knockouts += `<circle${attrs({ cx: nd.x, cy: nd.y, r: half * 1.08, fill: '#000' })}/>`;
|
|
204
|
+
const data = iconData(nd.icon);
|
|
205
|
+
const [vx, vy, vw, vh] = data.viewBox;
|
|
206
|
+
const box = nd.shape === 'none' ? nd.size : nd.size * (layer.iconScale ?? 0.44);
|
|
207
|
+
const k = Math.min(box / vw, box / vh);
|
|
208
|
+
// Match the icon's line weight to the tiles' (in icon units).
|
|
209
|
+
const body = data.body.replace(/stroke-width="[^"]*"/g, `stroke-width="${num(stroke / k, 3)}"`);
|
|
210
|
+
icons.push(`<svg${attrs({ x: nd.x - (vw * k) / 2, y: nd.y - (vh * k) / 2, width: vw * k, height: vh * k, viewBox: `${num(vx)} ${num(vy)} ${num(vw)} ${num(vh)}`, overflow: 'visible', color: col })}>${body}</svg>`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let defs = '';
|
|
214
|
+
const paint = { fill: 'none', stroke: col, 'stroke-width': stroke, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' };
|
|
215
|
+
let out = '';
|
|
216
|
+
|
|
217
|
+
// The grid behind, broken around every node (and the arrows' span).
|
|
218
|
+
if (layer.grid) {
|
|
219
|
+
const g = layer.grid === true ? {} : layer.grid;
|
|
220
|
+
const div = g.divisions ?? 4;
|
|
221
|
+
const gpad = len(g.pad, ctx.canvas, s * 0.35);
|
|
222
|
+
const gaps = nodes.map((nd) => ({ x: nd.x - nd.size / 2 - gpad, y: nd.y - nd.size / 2 - gpad, width: nd.size + gpad * 2, height: nd.size + gpad * 2 }));
|
|
223
|
+
const xs0 = Math.min(...gaps.map((r) => r.x));
|
|
224
|
+
const ys0 = Math.min(...gaps.map((r) => r.y));
|
|
225
|
+
const xs1 = Math.max(...gaps.map((r) => r.x + r.width));
|
|
226
|
+
const ys1 = Math.max(...gaps.map((r) => r.y + r.height));
|
|
227
|
+
// Arrows run between nodes: keep the whole drawing's span clear too.
|
|
228
|
+
if (lines.length) gaps.push({ x: xs0, y: ys0, width: xs1 - xs0, height: ys1 - ys0 + (layout === 'row' && mode === 'loop' ? gap * 1.3 : 0) });
|
|
229
|
+
const xs = Array.from({ length: div - 1 }, (_, i) => ((i + 1) * w) / div);
|
|
230
|
+
const ys = Array.from({ length: div - 1 }, (_, i) => ((i + 1) * h) / div);
|
|
231
|
+
const gs = len(g.stroke, ctx.canvas, '0.3vmin');
|
|
232
|
+
out += `<path${attrs({ d: gridPath(xs, ys, w, h, gaps), fill: 'none', stroke: g.color ? toHex(g.color) : col, 'stroke-width': gs, 'stroke-linecap': 'round', 'stroke-dasharray': `0 ${num(gs * 3.2)}`, opacity: g.opacity ?? 0.6 })}/>`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (dotted.length) out += `<path${attrs({ d: dotted.join(''), ...paint, 'stroke-dasharray': `0 ${num(stroke * 3)}` })}/>`;
|
|
236
|
+
if (lines.length) out += `<path${attrs({ d: lines.join(''), ...paint })}/>`;
|
|
237
|
+
if (tiles) out += `<g${attrs(paint)}>${tiles}</g>`;
|
|
238
|
+
if (knockouts) {
|
|
239
|
+
// The hub is drawn with holes where its satellites sit.
|
|
240
|
+
const mid = `${ctx.id}k`;
|
|
241
|
+
defs += `<mask${attrs({ id: mid, maskUnits: 'userSpaceOnUse', x: 0, y: 0, width: w, height: h })}><rect${attrs({ width: w, height: h, fill: '#fff' })}/>${knockouts}</mask>`;
|
|
242
|
+
icons[0] = `<g mask="url(#${mid})">${icons[0]}</g>`;
|
|
243
|
+
}
|
|
244
|
+
out += icons.join('');
|
|
245
|
+
const a = attrs(layerAttrs(layer));
|
|
246
|
+
return { defs, body: a ? `<g${a}>${out}</g>` : out };
|
|
247
|
+
}
|
|
248
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The layer as text art.
|
|
2
|
+
import type { Effect } from './index.js';
|
|
3
|
+
import type { Pixels } from './buffer.js';
|
|
4
|
+
import type { PixelResult } from './pixels.js';
|
|
5
|
+
|
|
6
|
+
/** `ascii`, on pixels (one pixel per font pixel, scaled up crisp). */
|
|
7
|
+
export function ascii(img: Pixels, fx: Extract<Effect, { kind: 'ascii' }>, ctx: object): PixelResult;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ascii`: the layer as text art. Each cell's brightness picks a
|
|
3
|
+
* character from a ramp (ordered by how much ink each glyph of the
|
|
4
|
+
* built-in 5×7 pixel face carries), drawn in the cell's own colour or one
|
|
5
|
+
* ink, on a background. Rendered at one pixel per font pixel and embedded
|
|
6
|
+
* scaled up crisp, so it's tiny and sharp at any size.
|
|
7
|
+
*
|
|
8
|
+
* @module effects/ascii
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { px, premultiplied } from './buffer.js';
|
|
12
|
+
import { rgb } from './util.js';
|
|
13
|
+
import { pixelBitmap } from '../type/pixel.js';
|
|
14
|
+
|
|
15
|
+
/** The default ramp, light to dense. */
|
|
16
|
+
const RAMP = ' .:-=+*%#@';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `ascii`: `cell` (a length: a character's width; height is 8/6 of it),
|
|
20
|
+
* `ramp` (characters, sparse to dense, re-ordered by their ink), `color`
|
|
21
|
+
* (`'source'`, each character the colour under it, or one colour),
|
|
22
|
+
* `background` (a colour, or `'none'`), `invert` (dense where it's dark,
|
|
23
|
+
* for dark ink on paper), `contrast` (after the tones are stretched to the
|
|
24
|
+
* layer's own range).
|
|
25
|
+
*
|
|
26
|
+
* @param {import('./buffer.js').Pixels} img
|
|
27
|
+
* @param {import('./index.js').Effect & { kind: 'ascii' }} fx
|
|
28
|
+
* @param {any} ctx
|
|
29
|
+
* @returns {import('./pixels.js').PixelResult}
|
|
30
|
+
*/
|
|
31
|
+
export function ascii(img, fx, ctx) {
|
|
32
|
+
const { width: w, height: h } = img;
|
|
33
|
+
const cw = Math.max(3, px(fx.cell, ctx, img, '1.4vmin'));
|
|
34
|
+
const ch = (cw * 8) / 6;
|
|
35
|
+
const cols = Math.max(1, Math.round(w / cw));
|
|
36
|
+
const rows = Math.max(1, Math.round(h / ch));
|
|
37
|
+
// Glyphs that the face draws, by ink.
|
|
38
|
+
const glyphs = [...new Set([...(fx.ramp ?? RAMP)])].map((c) => ({ c, bits: c === ' ' ? '.'.repeat(35) : pixelBitmap(c) })).filter((g) => g.bits);
|
|
39
|
+
if (!glyphs.length) throw new TypeError('ascii: the ramp has no characters the pixel face draws (A–Z, 0–9, punctuation)');
|
|
40
|
+
glyphs.sort((a, b) => (a.bits?.split('#').length ?? 0) - (b.bits?.split('#').length ?? 0));
|
|
41
|
+
const src = premultiplied(img);
|
|
42
|
+
const acc = new Float64Array(cols * rows * 5);
|
|
43
|
+
for (let y = 0; y < h; y++) {
|
|
44
|
+
const cy = Math.min(rows - 1, ((y * rows) / h) | 0);
|
|
45
|
+
for (let x = 0; x < w; x++) {
|
|
46
|
+
const c = (cy * cols + Math.min(cols - 1, ((x * cols) / w) | 0)) * 5;
|
|
47
|
+
const i = (y * w + x) * 4;
|
|
48
|
+
acc[c] += src[i]; acc[c + 1] += src[i + 1]; acc[c + 2] += src[i + 2]; acc[c + 3] += src[i + 3]; acc[c + 4]++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const ink = fx.color && fx.color !== 'source' ? rgb(fx.color).map((v) => v * 255) : null;
|
|
52
|
+
const bg = fx.background === 'none' ? null : rgb(fx.background ?? '#0b0b0b').map((v) => v * 255);
|
|
53
|
+
const contrast = fx.contrast ?? 1.2;
|
|
54
|
+
// Tones stretched to the cells' own range, so a dim layer still reads.
|
|
55
|
+
const lums = new Float32Array(cols * rows);
|
|
56
|
+
let lo = 1;
|
|
57
|
+
let hi = 0;
|
|
58
|
+
for (let c = 0; c < lums.length; c++) {
|
|
59
|
+
const a = acc[c * 5 + 3];
|
|
60
|
+
if (!a) continue;
|
|
61
|
+
lums[c] = (0.2126 * acc[c * 5] + 0.7152 * acc[c * 5 + 1] + 0.0722 * acc[c * 5 + 2]) / a;
|
|
62
|
+
lo = Math.min(lo, lums[c]);
|
|
63
|
+
hi = Math.max(hi, lums[c]);
|
|
64
|
+
}
|
|
65
|
+
const span = hi - lo > 0.05 ? hi - lo : 1;
|
|
66
|
+
const W = cols * 6;
|
|
67
|
+
const H = rows * 8;
|
|
68
|
+
const out = new Uint8ClampedArray(W * H * 4);
|
|
69
|
+
for (let cy = 0; cy < rows; cy++) {
|
|
70
|
+
for (let cx = 0; cx < cols; cx++) {
|
|
71
|
+
const c = (cy * cols + cx) * 5;
|
|
72
|
+
const n = acc[c + 4] || 1;
|
|
73
|
+
const a = acc[c + 3] / n;
|
|
74
|
+
const k = a ? 255 / a : 0;
|
|
75
|
+
const r = (acc[c] / n) * k;
|
|
76
|
+
const g = (acc[c + 1] / n) * k;
|
|
77
|
+
const b = (acc[c + 2] / n) * k;
|
|
78
|
+
let l = (lums[cy * cols + cx] - (span === 1 ? 0 : lo)) / span;
|
|
79
|
+
l = Math.min(1, Math.max(0, (l - 0.5) * contrast + 0.5));
|
|
80
|
+
if (fx.invert) l = 1 - l;
|
|
81
|
+
const glyph = a < 8 ? glyphs[0] : glyphs[Math.min(glyphs.length - 1, Math.floor(l * glyphs.length))];
|
|
82
|
+
const col = ink ?? [r, g, b];
|
|
83
|
+
for (let gy = 0; gy < 8; gy++) {
|
|
84
|
+
for (let gx = 0; gx < 6; gx++) {
|
|
85
|
+
const on = gy < 7 && gx < 5 && glyph.bits?.[gy * 5 + gx] === '#';
|
|
86
|
+
const o = ((cy * 8 + gy) * W + cx * 6 + gx) * 4;
|
|
87
|
+
if (on) { out[o] = col[0]; out[o + 1] = col[1]; out[o + 2] = col[2]; out[o + 3] = 255; } else if (bg) { out[o] = bg[0]; out[o + 1] = bg[1]; out[o + 2] = bg[2]; out[o + 3] = 255; }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { data: out, width: W, height: H, pixelated: true };
|
|
93
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pixel helpers shared by the pixel effects.
|
|
2
|
+
import type { Length } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/** A layer drawn as pixels: RGBA, 8-bit, not premultiplied. */
|
|
5
|
+
export interface Pixels {
|
|
6
|
+
data: Uint8ClampedArray;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
/** Pixels per layer unit. */
|
|
10
|
+
scale: number;
|
|
11
|
+
/** Every pixel has alpha 255. */
|
|
12
|
+
opaque: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** sRGB byte → linear light, 0–1. */
|
|
16
|
+
export const TO_LINEAR: Float32Array;
|
|
17
|
+
/** Linear light (clipped) to an sRGB byte. */
|
|
18
|
+
export function toSrgb(v: number): number;
|
|
19
|
+
/** A length in pixels. */
|
|
20
|
+
export function px(v: Length | undefined, ctx: { canvas: { width: number; height: number } }, img: Pixels, fallback: Length): number;
|
|
21
|
+
/** A point given as fractions of the canvas, in pixels. */
|
|
22
|
+
export function pointPx(p: [number, number] | { x: number; y: number } | undefined, img: { width: number; height: number }, fallback?: [number, number]): [number, number];
|
|
23
|
+
/** A point as fractions. */
|
|
24
|
+
export function fraction(p: [number, number] | { x: number; y: number } | undefined, fallback: [number, number]): [number, number];
|
|
25
|
+
/** 0–1. */
|
|
26
|
+
export function clamp01(v: number): number;
|
|
27
|
+
/** Hermite step from a to b. */
|
|
28
|
+
export function smoothstep(a: number, b: number, v: number): number;
|
|
29
|
+
/** A premultiplied copy (the same buffer when opaque). */
|
|
30
|
+
export function premultiplied(img: Pixels): Uint8ClampedArray;
|
|
31
|
+
/** Undo premultiplying, in place. */
|
|
32
|
+
export function unpremultiply(d: Uint8ClampedArray): Uint8ClampedArray;
|
|
33
|
+
/** Bilinear RGBA sample, the edge held, written to `out` at `o`. */
|
|
34
|
+
export function bilinear(src: Uint8ClampedArray, w: number, h: number, x: number, y: number, out: Uint8ClampedArray | Float32Array, o: number): void;
|
|
35
|
+
/** Resample every pixel from where `map` says it came from. */
|
|
36
|
+
export function remap(img: Pixels, map: (x: number, y: number, at: Float64Array) => void): Uint8ClampedArray;
|
|
37
|
+
/** Box radii for three passes approximating a Gaussian. */
|
|
38
|
+
export function boxesFor(sigma: number): number[];
|
|
39
|
+
/** A box blur along rows (0) or columns (1), the edge held. */
|
|
40
|
+
export function boxPass(src: Uint8ClampedArray | Float32Array, dst: Uint8ClampedArray | Float32Array, w: number, h: number, ch: number, r: number, dir: 0 | 1): void;
|
|
41
|
+
/** A Gaussian blur in place (three box passes each way). */
|
|
42
|
+
export function gaussian<T extends Uint8ClampedArray | Float32Array>(data: T, w: number, h: number, ch: number, sigma: number, sigmaY?: number): T;
|
|
43
|
+
/** Downsample by a whole factor, averaging blocks. */
|
|
44
|
+
export function shrink(src: Uint8ClampedArray, w: number, h: number, f: number): { data: Uint8ClampedArray; width: number; height: number };
|
|
45
|
+
/** Upscale, bilinear. */
|
|
46
|
+
export function enlarge(src: Uint8ClampedArray, sw: number, sh: number, w: number, h: number): Uint8ClampedArray;
|
|
47
|
+
/** Upscale, nearest neighbour. */
|
|
48
|
+
export function nearest(src: Uint8ClampedArray, sw: number, sh: number, w: number, h: number): Uint8ClampedArray;
|
|
49
|
+
/** Rec. 709 luma of a pixel, 0–255. */
|
|
50
|
+
export function luma(d: ArrayLike<number>, i: number): number;
|
|
51
|
+
/** The effect's own seed (hashed), else one from the layer's stream. */
|
|
52
|
+
export function seedFor(fx: { seed?: number | string }, ctx: { rng: { int: (a: number, b: number) => number } }): number;
|
|
53
|
+
/** An integer hash of a lattice point and a seed, 0–1. */
|
|
54
|
+
export function hash2(x: number, y: number, seed: number): number;
|
|
55
|
+
/** Smooth value noise, -1–1. */
|
|
56
|
+
export function valueNoise(x: number, y: number, seed: number): number;
|
|
57
|
+
/** Fractal value noise, about -1–1. */
|
|
58
|
+
export function fractalNoise(x: number, y: number, seed: number, octaves?: number): number;
|
|
59
|
+
/** A fractal noise field for every pixel, -1–1. */
|
|
60
|
+
export function noiseField(w: number, h: number, period: number, seed: number, octaves?: number): Float32Array;
|