@johnmorrisdotca/taisen-brackets 0.4.0 → 0.5.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.
@@ -0,0 +1,453 @@
1
+ // family-backgrounds: twenty-five original poster backgrounds, each drawn in code as SVG (gradients, radial glows, patterns, bezier bands, playing-surface markings), each in a dark
2
+ // and a light variant, and the text and line colours that stay readable over them. No image is used, nothing is fetched and nothing is imported.
3
+ //
4
+ // This file is the same, byte for byte, in every package of the family that draws a background (Taisen Brackets, Kyogijo and the rest): a package copies it at build time and
5
+ // never edits it. A change is made here, the version below is raised, and `test/family-backgrounds.test.ts` holds the sha256 of this file, so a copy that drifted is found.
6
+ // MIT licence. Every preset is the work of this package's author; none is traced from, or copies, anyone's artwork, logo, trophy or photograph.
7
+ //
8
+ // backgroundSvg("midnight", { width: 1080, height: 1350, theme: "dark" }) -> { defs, body, ink }
9
+ //
10
+ // `defs` goes inside the drawing's `<defs>`, `body` right after a plain rectangle of `ink.base`, and `ink` says which colours to draw text, lines, boxes and marks in.
11
+ export const FAMILY_BACKGROUNDS_VERSION = "1.0.0";
12
+ /** The presets, by name. The five that end in a playing surface read as one: pitch (soccer), ice (hockey), hardwood (basketball), infield (baseball) and turf (football). */
13
+ export const BACKGROUND_PRESETS = [
14
+ "midnight", "crimson", "royal", "emerald", "pitch", "ice", "hardwood", "infield", "turf", "ball-panels", "swirl", "waves", "glow",
15
+ "floodlights", "confetti", "carbon", "aurora", "sunburst", "chevrons", "halftone", "argyle", "contour", "stardust", "mesh", "spotlight",
16
+ ];
17
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
18
+ /** A number written without float noise. */
19
+ const r = (n) => String(Math.round(n * 10) / 10 === 0 ? 0 : Math.round(n * 10) / 10);
20
+ /** A colour as three channels, or `null` when it is not hex or `rgb()`. */
21
+ export function parseColor(text) {
22
+ const t = text.trim();
23
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(t)?.[1];
24
+ if (hex) {
25
+ const full = hex.length === 3 ? [...hex].map((c) => c + c).join("") : hex;
26
+ return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
27
+ }
28
+ const rgb = /^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/i.exec(t);
29
+ return rgb ? [clamp(+rgb[1], 0, 255), clamp(+rgb[2], 0, 255), clamp(+rgb[3], 0, 255)] : null;
30
+ }
31
+ const hex2 = (n) => Math.round(clamp(n, 0, 255)).toString(16).padStart(2, "0");
32
+ export const toHex = (c) => `#${hex2(c[0])}${hex2(c[1])}${hex2(c[2])}`;
33
+ const mix = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
34
+ const rgba = (c, a) => `rgba(${Math.round(c[0])},${Math.round(c[1])},${Math.round(c[2])},${a})`;
35
+ /** The relative luminance of a colour, as WCAG defines it. */
36
+ export function luminance(c) {
37
+ const [R, G, B] = c.map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
38
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
39
+ }
40
+ /** The WCAG contrast ratio of two colours, 1 to 21. */
41
+ export function contrastRatio(a, b) {
42
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
43
+ return (hi + 0.05) / (lo + 0.05);
44
+ }
45
+ const WHITE = [255, 255, 255];
46
+ const BLACK = [0, 0, 0];
47
+ const INK_LIGHT = [247, 244, 236];
48
+ const INK_DARK = [22, 24, 26];
49
+ // ------------------------------------------------------------------------------------------------ the pieces
50
+ /** A small seeded generator (mulberry32): the same seed always gives the same run of numbers. */
51
+ function seeded(seed) {
52
+ let a = (Math.floor(seed) * 2654435761) >>> 0;
53
+ return () => {
54
+ a = (a + 0x6d2b79f5) >>> 0;
55
+ let t = a;
56
+ t = Math.imul(t ^ (t >>> 15), t | 1);
57
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
58
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
59
+ };
60
+ }
61
+ /** A small stable hash of a string, as base 36. */
62
+ function hashOf(text) {
63
+ let h = 0x811c9dc5;
64
+ for (let i = 0; i < text.length; i++) {
65
+ h ^= text.charCodeAt(i);
66
+ h = Math.imul(h, 0x01000193) >>> 0;
67
+ }
68
+ return h.toString(36);
69
+ }
70
+ const stops = (list) => list.map(([o, c, a]) => `<stop offset="${o}" stop-color="${c}"${a !== undefined && a !== 1 ? ` stop-opacity="${a}"` : ""}/>`).join("");
71
+ /** A radial glow: `colour` at the centre fading to nothing at `rad`. */
72
+ const glow = (id, colour, a) => `<radialGradient id="${id}">${stops([[0, colour, a], [1, colour, 0]])}</radialGradient>`;
73
+ const circle = (cx, cy, rad, fill, extra = "") => `<circle cx="${r(cx)}" cy="${r(cy)}" r="${r(rad)}" fill="${fill}"${extra}/>`;
74
+ const fade = (g, base) => ` opacity="${Math.round(clamp(base * g.k, 0, 1) * 100) / 100}"`;
75
+ const PRESETS = {
76
+ midnight: {
77
+ dark: ["#0b1230", "#1b2160", "#7f8cff", "#c7ceff"], light: ["#eef1fb", "#d4dcf5", "#3d4fd0", "#7c8be0"], angle: 160,
78
+ draw: (g) => ({
79
+ defs: glow(`${g.id}a`, g.c[2], 0.9) + glow(`${g.id}b`, g.c[3], 0.6),
80
+ body: `<ellipse cx="${r(g.w * 0.5)}" cy="${r(g.h * 0.08)}" rx="${r(g.w * 0.8)}" ry="${r(g.h * 0.38)}" fill="url(#${g.id}a)"${fade(g, 0.55)}/><ellipse cx="${r(g.w * 0.85)}" cy="${r(g.h * 0.95)}" rx="${r(g.w * 0.6)}" ry="${r(g.h * 0.3)}" fill="url(#${g.id}b)"${fade(g, 0.35)}/>`,
81
+ }),
82
+ },
83
+ crimson: {
84
+ dark: ["#2b0610", "#7b1027", "#ff5a4d", "#ffb199"], light: ["#fdeeec", "#f6d0cf", "#c0392b", "#e58a80"], angle: 150,
85
+ draw: (g) => ({
86
+ defs: glow(`${g.id}a`, g.c[2], 0.9) + `<linearGradient id="${g.id}b" x1="0" y1="0" x2="1" y2="0">${stops([[0, g.c[3], 0], [0.5, g.c[3], 0.5], [1, g.c[3], 0]])}</linearGradient>`,
87
+ body: `<ellipse cx="${r(g.w * 0.1)}" cy="${r(g.h * 0.95)}" rx="${r(g.w * 0.75)}" ry="${r(g.h * 0.45)}" fill="url(#${g.id}a)"${fade(g, 0.6)}/><path d="M${r(g.w * 0.55)} 0H${r(g.w * 0.8)}L${r(g.w * 0.3)} ${r(g.h)}H${r(g.w * 0.05)}Z" fill="url(#${g.id}b)"${fade(g, 0.16)}/>`,
88
+ }),
89
+ },
90
+ royal: {
91
+ dark: ["#170a38", "#33197f", "#b69cff", "#5fb0ff"], light: ["#f2edff", "#d8cbff", "#5b3fd1", "#3a86d6"], angle: 135,
92
+ draw: (g) => ({
93
+ defs: glow(`${g.id}a`, g.c[2], 0.9) + glow(`${g.id}b`, g.c[3], 0.8),
94
+ body: `${circle(g.w * 0.05, g.h * 0.05, g.long * 0.6, `url(#${g.id}a)`, fade(g, 0.5))}${circle(g.w * 0.98, g.h * 0.92, g.long * 0.55, `url(#${g.id}b)`, fade(g, 0.4))}`,
95
+ }),
96
+ },
97
+ emerald: {
98
+ dark: ["#04251c", "#0d5a43", "#3ee0a0", "#a8f5d4"], light: ["#eaf8f2", "#c5ecdc", "#178a62", "#5cc9a0"], angle: 120,
99
+ draw: (g) => {
100
+ const band = (x, wd) => `<path d="M${r(x)} 0h${r(wd)}L${r(x - g.h * 0.5 + wd)} ${r(g.h)}h${r(-wd)}Z"/>`;
101
+ return {
102
+ defs: glow(`${g.id}a`, g.c[2], 0.9),
103
+ body: `${circle(g.w * 0.5, g.h * 1.05, g.long * 0.7, `url(#${g.id}a)`, fade(g, 0.4))}<g fill="${g.c[3]}"${fade(g, 0.07)}>${band(g.w * 0.55, g.w * 0.14)}${band(g.w * 0.8, g.w * 0.05)}${band(g.w * 1.05, g.w * 0.2)}${band(g.w * 0.28, g.w * 0.03)}</g>`,
104
+ };
105
+ },
106
+ },
107
+ pitch: {
108
+ dark: ["#0a3a1d", "#0f5429", "#e8f6ea", "#0a3a1d"], light: ["#cdebd0", "#b4dfb8", "#ffffff", "#9fd2a4"], angle: 180, surface: true,
109
+ draw: (g) => {
110
+ const land = g.w >= g.h;
111
+ const L = land ? g.w : g.h, S = land ? g.h : g.w;
112
+ const n = 12;
113
+ const stripe = L / n;
114
+ const line = `fill="none" stroke="${g.c[2]}" stroke-width="${r(Math.max(2, S * 0.006))}"${fade(g, 0.4)}`;
115
+ const t = land ? "" : ` transform="rotate(90)translate(0 ${r(-g.w)})"`;
116
+ const m = S * 0.07;
117
+ return {
118
+ defs: "",
119
+ body: `<g fill="${g.c[3]}"${fade(g, 0.3)}>${Array.from({ length: n / 2 }, (_, i) => (land ? `<rect x="${r((2 * i + 1) * stripe)}" y="0" width="${r(stripe)}" height="${r(g.h)}"/>` : `<rect x="0" y="${r((2 * i + 1) * stripe)}" width="${r(g.w)}" height="${r(stripe)}"/>`)).join("")}</g>` +
120
+ `<g${t} ${line}><rect x="${r(m)}" y="${r(m)}" width="${r(L - 2 * m)}" height="${r(S - 2 * m)}"/><path d="M${r(L / 2)} ${r(m)}V${r(S - m)}"/>${circle(L / 2, S / 2, S * 0.17, "none", ` stroke="${g.c[2]}" stroke-width="${r(Math.max(2, S * 0.006))}"`)}` +
121
+ `<rect x="${r(m)}" y="${r(S * 0.28)}" width="${r(L * 0.15)}" height="${r(S * 0.44)}"/><rect x="${r(L - m - L * 0.15)}" y="${r(S * 0.28)}" width="${r(L * 0.15)}" height="${r(S * 0.44)}"/><rect x="${r(m)}" y="${r(S * 0.4)}" width="${r(L * 0.05)}" height="${r(S * 0.2)}"/><rect x="${r(L - m - L * 0.05)}" y="${r(S * 0.4)}" width="${r(L * 0.05)}" height="${r(S * 0.2)}"/></g>`,
122
+ };
123
+ },
124
+ },
125
+ ice: {
126
+ dark: ["#0c2238", "#1d4a70", "#9fd4ff", "#ff6a6a"], light: ["#f3f9ff", "#d5eafa", "#3d8fd1", "#d94a4a"], angle: 170, surface: true,
127
+ draw: (g) => {
128
+ const { w, h } = g;
129
+ const marks = `fill="none" stroke-linecap="round"`;
130
+ const scratches = Array.from({ length: 26 }, () => {
131
+ const x = g.rnd() * w, y = g.rnd() * h, len = (0.1 + g.rnd() * 0.4) * g.long * 0.5, bend = (g.rnd() - 0.5) * len * 0.5, a = (g.rnd() - 0.5) * 1.1;
132
+ return `M${r(x)} ${r(y)}q${r(Math.cos(a) * len * 0.5 + bend * 0.2)} ${r(Math.sin(a) * len * 0.5 + bend)} ${r(Math.cos(a) * len)} ${r(Math.sin(a) * len)}`;
133
+ }).join("");
134
+ const land = w >= h;
135
+ const bw = Math.max(2, Math.min(w, h) * 0.007);
136
+ const cx = w / 2, cy = h / 2;
137
+ // The rink's markings, faint: the centre circle and spot, the red centre line, two blue lines, the faceoff spots.
138
+ const across = land
139
+ ? (p) => `M${r(p)} ${r(h * 0.04)}V${r(h * 0.96)}`
140
+ : (p) => `M${r(w * 0.04)} ${r(p)}H${r(w * 0.96)}`;
141
+ const along = land ? w : h;
142
+ const centre = land ? cx : cy;
143
+ const spot = (x, y) => circle(x, y, bw * 1.2, g.c[3]) + circle(x, y, bw * 7, "none", ` stroke="${g.c[3]}" stroke-width="${r(bw * 0.7)}"`);
144
+ const spots = [[-0.28, -0.22], [-0.28, 0.22], [0.28, -0.22], [0.28, 0.22]].map(([a, b]) => (land ? spot(cx + a * w, cy + b * h) : spot(cx + b * w, cy + a * h))).join("");
145
+ return {
146
+ defs: `<pattern id="${g.id}p" width="60" height="60" patternUnits="userSpaceOnUse"><circle cx="7" cy="9" r="1.1" fill="${g.c[2]}" opacity=".5"/><circle cx="38" cy="22" r=".9" fill="${g.c[2]}" opacity=".4"/><circle cx="21" cy="48" r="1.2" fill="${g.c[2]}" opacity=".5"/><circle cx="52" cy="53" r=".8" fill="${g.c[2]}" opacity=".4"/></pattern>`,
147
+ body: `<rect width="${r(w)}" height="${r(h)}" fill="url(#${g.id}p)"${fade(g, 0.4)}/><path d="${scratches}" ${marks} stroke="${g.dark ? "#ffffff" : "#6aa6d6"}" stroke-width="1.2"${fade(g, 0.22)}/>` +
148
+ `<g ${marks} stroke-width="${r(bw)}"${fade(g, 0.45)}><path d="${across(centre)}" stroke="${g.c[3]}" stroke-dasharray="${r(bw * 6)} ${r(bw * 3)}"/><path d="${across(centre - along * 0.2)}M${across(centre + along * 0.2).slice(1)}" stroke="${g.dark ? "#5aa0ff" : "#2d6fd0"}"/>${circle(cx, cy, Math.min(w, h) * 0.16, "none", ` stroke="${g.dark ? "#5aa0ff" : "#2d6fd0"}" stroke-width="${r(bw)}"`)}</g><g${fade(g, 0.4)}>${spots}${circle(cx, cy, bw * 2, g.dark ? "#5aa0ff" : "#2d6fd0")}</g>`,
149
+ };
150
+ },
151
+ },
152
+ hardwood: {
153
+ dark: ["#2a1507", "#4f2c10", "#e0a45a", "#f3d9ae"], light: ["#f3dcae", "#e2bb7a", "#8a5a1d", "#fff6e0"], angle: 180, surface: true,
154
+ draw: (g) => {
155
+ const ph = Math.max(22, g.short / 22);
156
+ const pw = ph * 7;
157
+ const tile = pw * 2;
158
+ const plank = (x, y, wd, shade) => `<rect x="${r(x)}" y="${r(y)}" width="${r(wd)}" height="${r(ph)}" fill="${shade ? g.c[2] : g.c[3]}" opacity="${shade ? 0.2 : 0.1}"/>`;
159
+ const line = `fill="none" stroke="${g.c[3]}" stroke-width="${r(Math.max(2, g.short * 0.006))}"${fade(g, 0.45)}`;
160
+ const land = g.w >= g.h;
161
+ const L = land ? g.w : g.h, S = land ? g.h : g.w;
162
+ const t = land ? "" : ` transform="rotate(90)translate(0 ${r(-g.w)})"`;
163
+ const key = S * 0.33;
164
+ return {
165
+ defs: `<pattern id="${g.id}p" width="${r(tile)}" height="${r(ph * 4)}" patternUnits="userSpaceOnUse">${plank(0, 0, pw * 1.3, 1)}${plank(pw * 1.3, 0, pw * 0.7, 0)}${plank(0, ph, pw * 0.5, 0)}${plank(pw * 0.5, ph, pw * 1.5, 1)}${plank(0, ph * 2, pw * 1.1, 0)}${plank(pw * 1.1, ph * 2, pw * 0.9, 1)}${plank(0, ph * 3, pw * 0.8, 1)}${plank(pw * 0.8, ph * 3, pw * 1.2, 0)}<path d="M0 0H${r(tile)}M0 ${r(ph)}H${r(tile)}M0 ${r(ph * 2)}H${r(tile)}M0 ${r(ph * 3)}H${r(tile)}" stroke="#000" stroke-width="1" opacity=".18"/><path d="M${r(pw * 1.3)} 0v${r(ph)}M${r(pw * 0.5)} ${r(ph)}v${r(ph)}M${r(pw * 1.1)} ${r(ph * 2)}v${r(ph)}M${r(pw * 0.8)} ${r(ph * 3)}v${r(ph)}" stroke="#000" stroke-width="1" opacity=".16"/></pattern>`,
166
+ body: `<rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}p)"${fade(g, 1.2)}/><g${t} ${line}><rect x="${r(S * 0.07)}" y="${r(S * 0.07)}" width="${r(L - S * 0.14)}" height="${r(S * 0.86)}"/><path d="M${r(L / 2)} ${r(S * 0.07)}V${r(S * 0.93)}"/>${circle(L / 2, S / 2, S * 0.14, "none", ` stroke="${g.c[3]}" stroke-width="${r(Math.max(2, S * 0.006))}"`)}<rect x="${r(S * 0.07)}" y="${r(S / 2 - key / 2)}" width="${r(L * 0.2)}" height="${r(key)}"/><rect x="${r(L - S * 0.07 - L * 0.2)}" y="${r(S / 2 - key / 2)}" width="${r(L * 0.2)}" height="${r(key)}"/></g>`,
167
+ };
168
+ },
169
+ },
170
+ infield: {
171
+ dark: ["#0c3a1e", "#14562b", "#c98a4b", "#e9f3e3"], light: ["#c8e8c6", "#a9d9a6", "#c58948", "#ffffff"], angle: 180, surface: true,
172
+ draw: (g) => {
173
+ const { w, h } = g;
174
+ const run = Math.min(w, h) * 0.5;
175
+ const hx = w / 2, hy = h * 0.96;
176
+ const first = [hx + run * 0.7, hy - run * 0.7], third = [hx - run * 0.7, hy - run * 0.7], second = [hx, hy - run * 1.4];
177
+ return {
178
+ defs: `<pattern id="${g.id}p" width="${r(g.long / 14)}" height="${r(g.long / 14)}" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="${r(g.long / 28)}" height="${r(g.long / 14)}" fill="${g.c[3]}" opacity=".13"/></pattern>`,
179
+ body: `<rect width="${r(w)}" height="${r(h)}" fill="url(#${g.id}p)"${fade(g, 1)}/>${circle(hx, hy, run * 1.25, g.c[2], fade(g, 0.4))}<path d="M${r(hx)} ${r(hy)}L${r(first[0])} ${r(first[1])}L${r(second[0])} ${r(second[1])}L${r(third[0])} ${r(third[1])}Z" fill="none" stroke="${g.c[3]}" stroke-width="${r(Math.max(2, run * 0.025))}" stroke-linejoin="round"${fade(g, 0.4)}/><g fill="${g.c[3]}"${fade(g, 0.5)}><rect x="${r(first[0] - run * 0.04)}" y="${r(first[1] - run * 0.04)}" width="${r(run * 0.08)}" height="${r(run * 0.08)}"/><rect x="${r(third[0] - run * 0.04)}" y="${r(third[1] - run * 0.04)}" width="${r(run * 0.08)}" height="${r(run * 0.08)}"/><rect x="${r(second[0] - run * 0.04)}" y="${r(second[1] - run * 0.04)}" width="${r(run * 0.08)}" height="${r(run * 0.08)}"/><path d="M${r(hx - run * 0.05)} ${r(hy - run * 0.04)}h${r(run * 0.1)}v${r(run * 0.04)}l${r(-run * 0.05)} ${r(run * 0.05)}l${r(-run * 0.05)} ${r(-run * 0.05)}Z"/></g>`,
180
+ };
181
+ },
182
+ },
183
+ turf: {
184
+ dark: ["#0b3322", "#115034", "#eef6ee", "#f0c64a"], light: ["#cbe9d2", "#b3dcbd", "#ffffff", "#c79417"], angle: 180, surface: true,
185
+ draw: (g) => {
186
+ const land = g.w >= g.h;
187
+ const L = land ? g.w : g.h, S = land ? g.h : g.w;
188
+ const zone = L * 0.1;
189
+ const yard = (L - 2 * zone) / 10;
190
+ const t = land ? "" : ` transform="rotate(90)translate(0 ${r(-g.w)})"`;
191
+ const lines = Array.from({ length: 11 }, (_, i) => `M${r(zone + i * yard)} 0V${r(S)}`).join("");
192
+ const hashes = Array.from({ length: 50 }, (_, i) => `M${r(zone + (i + 0.5) * yard * 0.2 * 1)} ${r(S * 0.4)}v${r(S * 0.025)}M${r(zone + (i + 0.5) * yard * 0.2)} ${r(S * 0.6)}v${r(-S * 0.025)}`).join("");
193
+ return {
194
+ defs: "",
195
+ body: `<g${t}><path d="M0 0h${r(zone)}v${r(S)}h${r(-zone)}ZM${r(L - zone)} 0h${r(zone)}v${r(S)}h${r(-zone)}Z" fill="${g.c[3]}"${fade(g, 0.2)}/><g fill="none" stroke="${g.c[2]}"${fade(g, 0.35)}><path d="${lines}" stroke-width="${r(Math.max(2, S * 0.007))}"/><path d="${hashes}" stroke-width="${r(Math.max(1.5, S * 0.004))}"/></g></g>`,
196
+ };
197
+ },
198
+ },
199
+ "ball-panels": {
200
+ dark: ["#0d1322", "#1c2744", "#7f9bff", "#050810"], light: ["#eef2fa", "#d6def0", "#4a64c8", "#a9b6dc"], angle: 150,
201
+ draw: (g) => {
202
+ const s = clamp(g.long / 12, 50, 110);
203
+ const hw = s * 1.732, hh = s * 3;
204
+ const hexPath = (cx, cy) => Array.from({ length: 6 }, (_, i) => `${i === 0 ? "M" : "L"}${r(cx + s * Math.cos((Math.PI / 3) * i + Math.PI / 6))} ${r(cy + s * Math.sin((Math.PI / 3) * i + Math.PI / 6))}`).join("") + "Z";
205
+ const pent = (cx, cy) => Array.from({ length: 5 }, (_, i) => `${i === 0 ? "M" : "L"}${r(cx + s * 0.62 * Math.cos((Math.PI * 2 * i) / 5 - Math.PI / 2))} ${r(cy + s * 0.62 * Math.sin((Math.PI * 2 * i) / 5 - Math.PI / 2))}`).join("") + "Z";
206
+ return {
207
+ defs: `${glow(`${g.id}a`, g.c[2], 0.8)}<pattern id="${g.id}p" width="${r(hw)}" height="${r(hh)}" patternUnits="userSpaceOnUse"><path d="${hexPath(0, 0)}${hexPath(hw, 0)}${hexPath(hw / 2, s * 1.5)}${hexPath(0, s * 3)}${hexPath(hw, s * 3)}" fill="none" stroke="${g.c[2]}" stroke-width="${r(s * 0.05)}" opacity=".5"/><path d="${pent(hw / 2, s * 1.5)}" fill="${g.c[3]}" opacity=".8"/></pattern>`,
208
+ body: `<rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}p)"${fade(g, 0.55)}/>${circle(g.w * 0.5, g.h * 0.45, g.long * 0.6, `url(#${g.id}a)`, fade(g, 0.4))}`,
209
+ };
210
+ },
211
+ },
212
+ swirl: {
213
+ dark: ["#0a1030", "#241a6b", "#ff4fa3", "#34c3ff"], light: ["#f4f0ff", "#ddd2ff", "#e0368a", "#1e9bd6"], angle: 160,
214
+ draw: (g) => {
215
+ const { w, h } = g;
216
+ const ribbon = (y0, y1, th, fill, op) => `<path d="M${r(-w * 0.1)} ${r(h * y0)}C${r(w * 0.3)} ${r(h * (y0 - 0.35))} ${r(w * 0.6)} ${r(h * (y1 + 0.3))} ${r(w * 1.1)} ${r(h * y1)}L${r(w * 1.1)} ${r(h * (y1 + th))}C${r(w * 0.6)} ${r(h * (y1 + th + 0.28))} ${r(w * 0.3)} ${r(h * (y0 + th - 0.3))} ${r(-w * 0.1)} ${r(h * (y0 + th))}Z" fill="${fill}"${fade(g, op)}/>`;
217
+ return { defs: "", body: ribbon(0.75, 0.2, 0.1, g.c[2], 0.5) + ribbon(0.85, 0.35, 0.16, g.c[3], 0.4) + ribbon(0.6, 0.05, 0.05, g.c[2], 0.35) + ribbon(1, 0.55, 0.12, g.c[3], 0.28) };
218
+ },
219
+ },
220
+ waves: {
221
+ dark: ["#04162c", "#0a3f6b", "#42b4ff", "#9be4ff"], light: ["#eaf6ff", "#c9e7fb", "#1f86d4", "#69c0ec"], angle: 180,
222
+ draw: (g) => {
223
+ const { w, h } = g;
224
+ const wave = (y, amp, ph, fill, op) => `<path d="M0 ${r(h * y)}C${r(w * 0.2)} ${r(h * y - amp * (1 + ph))} ${r(w * 0.3)} ${r(h * y + amp)} ${r(w * 0.5)} ${r(h * y)}S${r(w * 0.8)} ${r(h * y - amp * (1 - ph))} ${r(w)} ${r(h * y - amp * 0.3)}V${r(h)}H0Z" fill="${fill}"${fade(g, op)}/>`;
225
+ return { defs: glow(`${g.id}a`, g.c[3], 0.9), body: `${circle(w * 0.7, h * 0.12, g.long * 0.35, `url(#${g.id}a)`, fade(g, 0.3))}${wave(0.55, h * 0.05, 0.3, g.c[2], 0.16)}${wave(0.66, h * 0.045, -0.2, g.c[2], 0.2)}${wave(0.77, h * 0.04, 0.4, g.c[3], 0.16)}${wave(0.88, h * 0.035, -0.3, g.c[2], 0.26)}` };
226
+ },
227
+ },
228
+ glow: {
229
+ dark: ["#070b1c", "#141a40", "#ffcf5a", "#ff7a3d"], light: ["#fff8e6", "#ffe9b8", "#f0a000", "#ff7a3d"], angle: 180,
230
+ draw: (g) => {
231
+ const cx = g.w / 2, cy = g.h * 0.42;
232
+ return {
233
+ defs: glow(`${g.id}a`, g.c[2], 1) + glow(`${g.id}b`, g.c[3], 0.8),
234
+ body: `${circle(cx, cy, g.long * 0.62, `url(#${g.id}a)`, fade(g, 0.55))}${circle(cx, cy, g.long * 0.3, `url(#${g.id}b)`, fade(g, 0.35))}<g fill="none" stroke="${g.c[2]}" stroke-width="1.5"${fade(g, 0.25)}>${circle(cx, cy, g.short * 0.28, "none")}${circle(cx, cy, g.short * 0.42, "none")}${circle(cx, cy, g.short * 0.58, "none")}</g>`,
235
+ };
236
+ },
237
+ },
238
+ floodlights: {
239
+ dark: ["#060a1a", "#142046", "#fff3c4", "#7fb2ff"], light: ["#f1f4fb", "#d8e1f2", "#f0b429", "#4a82d6"], angle: 180,
240
+ draw: (g) => {
241
+ const { w, h } = g;
242
+ const cone = (x, spread, reach) => `<path d="M${r(x - w * 0.012)} 0H${r(x + w * 0.012)}L${r(x + spread * reach)} ${r(h * reach)}L${r(x - spread * reach)} ${r(h * reach)}Z"/>`;
243
+ const xs = [0.12, 0.31, 0.5, 0.69, 0.88];
244
+ return {
245
+ defs: `<linearGradient id="${g.id}a" x1="0" y1="0" x2="0" y2="1">${stops([[0, g.c[2], 0.9], [1, g.c[2], 0]])}</linearGradient>${glow(`${g.id}b`, g.c[2], 0.9)}`,
246
+ body: `<g fill="url(#${g.id}a)"${fade(g, 0.28)}>${xs.map((x, i) => cone(w * x, w * (i % 2 ? 0.12 : 0.17), 1)).join("")}</g>${xs.map((x) => circle(w * x, h * 0.012, Math.max(3, w * 0.012), g.c[2], fade(g, 0.8))).join("")}<ellipse cx="${r(w / 2)}" cy="${r(h)}" rx="${r(w * 0.7)}" ry="${r(h * 0.2)}" fill="url(#${g.id}b)"${fade(g, 0.3)}/>`,
247
+ };
248
+ },
249
+ },
250
+ confetti: {
251
+ dark: ["#150a30", "#3a0f5e", "#ff5fa2", "#ffd34e"], light: ["#fff3fa", "#ffe0f0", "#e0368a", "#e8a900"], angle: 150,
252
+ draw: (g) => {
253
+ const palette = [g.c[2], g.c[3], g.dark ? "#4fd1ff" : "#1e9bd6", g.dark ? "#8dff9c" : "#27a843"];
254
+ const size = clamp(g.short / 60, 7, 16);
255
+ const pieces = Array.from({ length: 44 }, (_, i) => {
256
+ const x = g.rnd() * g.w, y = g.rnd() * g.h, a = Math.round(g.rnd() * 180);
257
+ const kind = i % 3;
258
+ return `<${kind === 2 ? `circle cx="${r(x)}" cy="${r(y)}" r="${r(size * 0.35)}"` : `rect x="${r(x)}" y="${r(y)}" width="${r(size * (kind ? 0.5 : 1.1))}" height="${r(size * (kind ? 1.4 : 0.55))}" transform="rotate(${a} ${r(x)} ${r(y)})"`} fill="${palette[i % 4]}"/>`;
259
+ });
260
+ return { defs: "", body: `<g${fade(g, 0.55)}>${pieces.join("")}</g>` };
261
+ },
262
+ },
263
+ carbon: {
264
+ dark: ["#0b0c0e", "#1c1f24", "#7a8494", "#2a2e35"], light: ["#e1e4e8", "#c9ced6", "#6b7686", "#aeb5c0"], angle: 135,
265
+ draw: (g) => ({
266
+ defs: `<pattern id="${g.id}p" width="12" height="12" patternUnits="userSpaceOnUse"><rect width="6" height="6" fill="${g.c[3]}"/><rect x="6" y="6" width="6" height="6" fill="${g.c[3]}"/><rect x="6" width="6" height="6" fill="${g.c[2]}" opacity=".35"/><rect y="6" width="6" height="6" fill="${g.c[2]}" opacity=".35"/></pattern><linearGradient id="${g.id}a" x1="0" y1="0" x2="1" y2="1">${stops([[0, g.c[2], 0], [0.45, g.c[2], 0.5], [0.55, g.c[2], 0.5], [1, g.c[2], 0]])}</linearGradient>`,
267
+ body: `<rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}p)"${fade(g, 0.55)}/><rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}a)"${fade(g, 0.28)}/>`,
268
+ }),
269
+ },
270
+ aurora: {
271
+ dark: ["#030b1a", "#082a3a", "#3dffb0", "#9a6bff"], light: ["#eefaf6", "#d2eefa", "#12a875", "#7a4fe0"], angle: 180,
272
+ draw: (g) => {
273
+ const { w, h } = g;
274
+ const band = (x, wd, lean, id) => `<path d="M${r(w * x)} ${r(h * 0.02)}C${r(w * (x + lean))} ${r(h * 0.25)} ${r(w * (x - lean))} ${r(h * 0.45)} ${r(w * (x + lean * 0.5))} ${r(h * 0.8)}L${r(w * (x + wd + lean * 0.5))} ${r(h * 0.8)}C${r(w * (x + wd - lean))} ${r(h * 0.45)} ${r(w * (x + wd + lean))} ${r(h * 0.25)} ${r(w * (x + wd))} ${r(h * 0.02)}Z" fill="url(#${id})"/>`;
275
+ const stars = Array.from({ length: 36 }, () => circle(g.rnd() * w, g.rnd() * h * 0.6, 0.6 + g.rnd() * 1.1, g.dark ? "#fff" : g.c[3], ` opacity="${r(0.25 + g.rnd() * 0.5)}"`)).join("");
276
+ const vg = (id, c) => `<linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1">${stops([[0, c, 0], [0.25, c, 0.9], [1, c, 0]])}</linearGradient>`;
277
+ return {
278
+ defs: vg(`${g.id}a`, g.c[2]) + vg(`${g.id}b`, g.c[3]),
279
+ body: `<g${fade(g, 0.5)}>${band(0.05, 0.22, 0.12, `${g.id}a`)}${band(0.35, 0.28, -0.1, `${g.id}b`)}${band(0.62, 0.3, 0.1, `${g.id}a`)}</g>${g.dark ? `<g${fade(g, 0.9)}>${stars}</g>` : ""}`,
280
+ };
281
+ },
282
+ },
283
+ sunburst: {
284
+ dark: ["#1a0830", "#5a1458", "#ffb347", "#ff5f6d"], light: ["#fff6e8", "#ffe0bd", "#f08a00", "#e0455a"], angle: 180,
285
+ draw: (g) => {
286
+ const cx = g.w / 2, cy = g.h * 0.45, big = g.long * 1.2, n = 28;
287
+ const wedges = Array.from({ length: n }, (_, i) => {
288
+ const a0 = (Math.PI * 2 * i) / n, a1 = a0 + Math.PI / n;
289
+ return `M${r(cx)} ${r(cy)}L${r(cx + big * Math.cos(a0))} ${r(cy + big * Math.sin(a0))}L${r(cx + big * Math.cos(a1))} ${r(cy + big * Math.sin(a1))}Z`;
290
+ }).join("");
291
+ return { defs: glow(`${g.id}a`, g.c[2], 1) + `<radialGradient id="${g.id}b">${stops([[0, g.c[2], 0.9], [1, g.c[2], 0]])}</radialGradient>`, body: `<path d="${wedges}" fill="url(#${g.id}b)"${fade(g, 0.22)}/>${circle(cx, cy, g.long * 0.4, `url(#${g.id}a)`, fade(g, 0.45))}` };
292
+ },
293
+ },
294
+ chevrons: {
295
+ dark: ["#0a1a33", "#123e6e", "#5cc8ff", "#ffffff"], light: ["#edf5fd", "#cfe4f7", "#1d7fc4", "#7aa9d0"], angle: 180,
296
+ draw: (g) => {
297
+ const t = clamp(g.short / 14, 40, 80);
298
+ return {
299
+ defs: `<pattern id="${g.id}p" width="${r(t * 2)}" height="${r(t)}" patternUnits="userSpaceOnUse"><path d="M0 ${r(t * 0.85)}L${r(t)} ${r(t * 0.2)}L${r(t * 2)} ${r(t * 0.85)}" fill="none" stroke="${g.c[2]}" stroke-width="${r(t * 0.14)}" stroke-linejoin="round"/></pattern><linearGradient id="${g.id}a" x1="0" y1="0" x2="0" y2="1">${stops([[0, g.c[3], 0.5], [0.5, g.c[3], 0]])}</linearGradient>`,
300
+ body: `<rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}p)"${fade(g, 0.28)}/><rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}a)"${fade(g, 0.12)}/>`,
301
+ };
302
+ },
303
+ },
304
+ halftone: {
305
+ dark: ["#12081f", "#2d1157", "#ff4fb0", "#ffd24e"], light: ["#fdf2fb", "#f3dcf5", "#d02f93", "#e9a800"], angle: 135,
306
+ draw: (g) => {
307
+ const s = clamp(g.short / 40, 14, 26);
308
+ return {
309
+ defs: `<pattern id="${g.id}p" width="${r(s)}" height="${r(s)}" patternUnits="userSpaceOnUse"><circle cx="${r(s / 2)}" cy="${r(s / 2)}" r="${r(s * 0.3)}" fill="${g.c[2]}"/></pattern><linearGradient id="${g.id}a" x1="0" y1="1" x2="1" y2="0">${stops([[0, "#fff", 1], [0.65, "#fff", 0]])}</linearGradient><mask id="${g.id}m"><rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}a)"/></mask>${glow(`${g.id}b`, g.c[3], 0.9)}`,
310
+ body: `${circle(g.w * 0.1, g.h * 0.95, g.long * 0.5, `url(#${g.id}b)`, fade(g, 0.3))}<rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}p)" mask="url(#${g.id}m)"${fade(g, 0.5)}/>`,
311
+ };
312
+ },
313
+ },
314
+ argyle: {
315
+ dark: ["#10172c", "#1d2a52", "#e6b34a", "#7d8fd8"], light: ["#f1f3fa", "#dbe1f3", "#b8851a", "#5d70b8"], angle: 150,
316
+ draw: (g) => {
317
+ const t = clamp(g.short / 9, 60, 120);
318
+ return {
319
+ defs: `<pattern id="${g.id}p" width="${r(t)}" height="${r(t * 1.5)}" patternUnits="userSpaceOnUse"><path d="M${r(t / 2)} 0L${r(t)} ${r(t * 0.75)}L${r(t / 2)} ${r(t * 1.5)}L0 ${r(t * 0.75)}Z" fill="${g.c[3]}" opacity=".35"/><path d="M0 0L${r(t)} ${r(t * 1.5)}M${r(t)} 0L0 ${r(t * 1.5)}" stroke="${g.c[2]}" stroke-width="1.2" opacity=".6"/></pattern>`,
320
+ body: `<rect width="${r(g.w)}" height="${r(g.h)}" fill="url(#${g.id}p)"${fade(g, 0.45)}/>`,
321
+ };
322
+ },
323
+ },
324
+ contour: {
325
+ dark: ["#071a1c", "#0f3d40", "#5ee6d6", "#a0f5ea"], light: ["#eaf7f5", "#cdebe6", "#17917f", "#5bbfb0"], angle: 160,
326
+ draw: (g) => {
327
+ const ring = (cx, cy, rad) => {
328
+ const n = 9, pts = Array.from({ length: n }, (_, i) => {
329
+ const a = (Math.PI * 2 * i) / n, d = rad * (0.82 + g.rnd() * 0.36);
330
+ return [cx + d * Math.cos(a) * 1.25, cy + d * Math.sin(a)];
331
+ });
332
+ const mid = (a, b) => `${r((a[0] + b[0]) / 2)} ${r((a[1] + b[1]) / 2)}`;
333
+ return `M${mid(pts[n - 1], pts[0])}` + pts.map((p, i) => `Q${r(p[0])} ${r(p[1])} ${mid(p, pts[(i + 1) % n])}`).join("") + "Z";
334
+ };
335
+ const centres = [[g.w * (0.2 + g.rnd() * 0.15), g.h * (0.25 + g.rnd() * 0.15)], [g.w * (0.65 + g.rnd() * 0.2), g.h * (0.65 + g.rnd() * 0.2)]];
336
+ const d = centres.flatMap(([cx, cy]) => Array.from({ length: 7 }, (_, i) => ring(cx, cy, g.short * (0.07 + i * 0.075)))).join("");
337
+ return { defs: "", body: `<path d="${d}" fill="none" stroke="${g.c[2]}" stroke-width="${r(Math.max(1.2, g.short / 500))}"${fade(g, 0.3)}/>` };
338
+ },
339
+ },
340
+ stardust: {
341
+ dark: ["#05030f", "#1a1050", "#dfe4ff", "#b78bff"], light: ["#f4f2ff", "#e3defa", "#5a4bd0", "#8a6be0"], angle: 160,
342
+ draw: (g) => {
343
+ const { w, h } = g;
344
+ const dots = Array.from({ length: 70 }, () => `<circle cx="${r(g.rnd() * w)}" cy="${r(g.rnd() * h)}" r="${r(0.5 + g.rnd() ** 3 * 2.2)}" opacity="${r(0.2 + g.rnd() * 0.6)}"/>`).join("");
345
+ return {
346
+ defs: glow(`${g.id}a`, g.c[3], 0.9) + `<linearGradient id="${g.id}b" x1="0" y1="0" x2="1" y2="0">${stops([[0, g.c[3], 0], [0.5, g.c[3], 0.5], [1, g.c[3], 0]])}</linearGradient>`,
347
+ body: `<path d="M0 ${r(h * 0.75)}L${r(w * 0.7)} 0H${r(w)}V${r(h * 0.1)}L${r(w * 0.3)} ${r(h)}H0Z" fill="url(#${g.id}b)"${fade(g, 0.14)}/>${circle(w * 0.75, h * 0.2, g.long * 0.3, `url(#${g.id}a)`, fade(g, 0.4))}<g fill="${g.c[2]}"${fade(g, 1)}>${dots}</g>`,
348
+ };
349
+ },
350
+ },
351
+ mesh: {
352
+ dark: ["#0d1030", "#1d1a55", "#ff6aa8", "#3ee0ff"], light: ["#f5f2ff", "#e5def9", "#e0407f", "#1db4d6"], angle: 135,
353
+ draw: (g) => {
354
+ const blobs = [[0.1, 0.1, 0.6, g.c[2]], [0.95, 0.2, 0.5, g.c[3]], [0.2, 0.9, 0.55, g.c[3]], [0.9, 0.95, 0.6, g.c[2]], [0.5, 0.5, 0.35, g.dark ? "#8a6bff" : "#7a5ee0"]];
355
+ return {
356
+ defs: blobs.map((b, i) => glow(`${g.id}${i}`, b[3], 0.9)).join(""),
357
+ body: blobs.map((b, i) => circle(g.w * b[0], g.h * b[1], g.long * b[2], `url(#${g.id}${i})`, fade(g, 0.45))).join(""),
358
+ };
359
+ },
360
+ },
361
+ spotlight: {
362
+ dark: ["#05070f", "#161c34", "#fff1c8", "#7fb2ff"], light: ["#eef0f6", "#d3d9e8", "#f6c54a", "#7fa5e0"], angle: 180,
363
+ draw: (g) => {
364
+ const { w, h } = g;
365
+ return {
366
+ defs: `<linearGradient id="${g.id}a" x1="0" y1="0" x2="0" y2="1">${stops([[0, g.c[2], 0.85], [1, g.c[2], 0.05]])}</linearGradient>${glow(`${g.id}b`, g.c[2], 0.9)}<radialGradient id="${g.id}v" cx=".5" cy=".5" r=".75">${stops([[0.55, "#000", 0], [1, "#000", 0.55]])}</radialGradient>`,
367
+ body: `<path d="M${r(w * 0.47)} 0H${r(w * 0.53)}L${r(w * 0.88)} ${r(h * 0.92)}H${r(w * 0.12)}Z" fill="url(#${g.id}a)"${fade(g, 0.3)}/><ellipse cx="${r(w / 2)}" cy="${r(h * 0.93)}" rx="${r(w * 0.5)}" ry="${r(h * 0.1)}" fill="url(#${g.id}b)"${fade(g, 0.55)}/>${g.dark ? `<rect width="${r(w)}" height="${r(h)}" fill="url(#${g.id}v)"/>` : ""}`,
368
+ };
369
+ },
370
+ },
371
+ };
372
+ // ------------------------------------------------------------------------------------------------ the ink
373
+ /** The least contrast of a colour against each of several. */
374
+ const worst = (c, over) => Math.min(...over.map((o) => contrastRatio(c, o)));
375
+ function inkFor(c, k) {
376
+ const [c0, c1, c2, c3] = c.map((x) => parseColor(x) ?? [128, 128, 128]);
377
+ // The decoration's strongest effect on the ground: a blend of up to 40% of the glow and the pattern.
378
+ const reach = clamp(0.2 + 0.35 * k, 0.2, 0.5);
379
+ const grounds = [c0, c1, mix(c0, c2, reach), mix(c1, c2, reach), mix(c0, c3, reach), mix(c1, c3, reach)];
380
+ let over = grounds;
381
+ let useLight = worst(INK_LIGHT, over) >= worst(INK_DARK, over);
382
+ const text = useLight ? INK_LIGHT : INK_DARK;
383
+ // A ground too mid-toned for either ink is dimmed with a veil until the text reads at 4.5 to 1.
384
+ let scrim = "";
385
+ if (worst(text, over) < 4.6) {
386
+ const veil = useLight ? BLACK : WHITE;
387
+ let a = 0;
388
+ while (a < 0.85 && worst(text, over) < 4.6) {
389
+ a += 0.05;
390
+ over = grounds.map((g) => mix(g, veil, a));
391
+ }
392
+ scrim = `<rect width="100%" height="100%" fill="${toHex(veil)}" opacity="${Math.round(a * 100) / 100}"/>`;
393
+ }
394
+ const ground = over[0];
395
+ useLight = text === INK_LIGHT;
396
+ let muted = mix(text, ground, 0.34);
397
+ for (let t = 0.34; t > 0 && worst(muted, over) < 4.6; t -= 0.04)
398
+ muted = mix(text, ground, t);
399
+ const goldFallback = useLight ? [240, 198, 74] : [138, 90, 0];
400
+ const accent = [c2, c3, goldFallback].map((x) => ({ x, s: worst(x, over.slice(0, 2)) })).sort((a, b) => b.s - a.s).find((o) => o.s >= 3)?.x ?? goldFallback;
401
+ return {
402
+ scrim,
403
+ ink: {
404
+ base: toHex(c0),
405
+ dark: useLight,
406
+ text: toHex(text),
407
+ muted: toHex(muted),
408
+ line: toHex(mix(text, ground, 0.2)),
409
+ box: rgba(mix(ground, useLight ? BLACK : WHITE, useLight ? 0.55 : 0.6), 0.74),
410
+ edge: rgba(text, 0.3),
411
+ panel: toHex(mix(ground, useLight ? BLACK : WHITE, 0.5)),
412
+ plain: toHex(mix(ground, text, 0.22)),
413
+ ring: useLight ? "#f4f1e8" : "#ffffff",
414
+ spine: toHex(accent),
415
+ accent: toHex(accent),
416
+ dominant: over.map(toHex),
417
+ },
418
+ };
419
+ }
420
+ // ------------------------------------------------------------------------------------------------ the entry
421
+ /**
422
+ * One background: `defs` for the drawing's `<defs>`, `body` for what goes over a plain `ink.base` rectangle, and `ink`, the colours to draw text, lines and boxes in. The same preset and options
423
+ * always give the same markup; every id starts with `options.id`. An unknown preset throws.
424
+ */
425
+ export function backgroundSvg(preset, options) {
426
+ const def = PRESETS[preset];
427
+ if (!def)
428
+ throw new RangeError(`Unknown background "${String(preset)}": use one of ${BACKGROUND_PRESETS.join(", ")}`);
429
+ const w = Math.max(1, options.width), h = Math.max(1, options.height);
430
+ const dark = (options.theme ?? "dark") !== "light";
431
+ const palette = [...(dark ? def.dark : def.light)];
432
+ const given = (options.colors ?? []).slice(0, 4);
433
+ given.forEach((x, i) => { if (parseColor(x))
434
+ palette[i] = toHex(parseColor(x)); });
435
+ // One colour makes the ground by itself: its second is the same, a little deeper (dark) or lighter (light).
436
+ if (given.length === 1 && parseColor(given[0]))
437
+ palette[1] = toHex(mix(parseColor(given[0]), dark ? BLACK : WHITE, 0.35));
438
+ const intensity = clamp(options.intensity ?? 0.6, 0, 1);
439
+ const k = intensity / 0.6;
440
+ const id = options.id ?? `bg${hashOf(JSON.stringify([preset, w, h, dark, given, options.angle, intensity, options.seed]))}`;
441
+ const angle = options.angle ?? def.angle;
442
+ const ground = { w, h, c: palette, k, rnd: seeded(options.seed ?? 1), id, dark, long: Math.max(w, h), short: Math.min(w, h) };
443
+ const made = def.draw(ground);
444
+ const rad = (angle * Math.PI) / 180;
445
+ const dx = Math.sin(rad), dy = -Math.cos(rad);
446
+ const half = (Math.abs(w * dx) + Math.abs(h * dy)) / 2;
447
+ const defs = `<linearGradient id="${id}g" gradientUnits="userSpaceOnUse" x1="${r(w / 2 - dx * half)}" y1="${r(h / 2 - dy * half)}" x2="${r(w / 2 + dx * half)}" y2="${r(h / 2 + dy * half)}">${stops([[0, palette[0]], [1, palette[1]]])}</linearGradient><clipPath id="${id}c"><rect width="${r(w)}" height="${r(h)}"/></clipPath>${made.defs}`;
448
+ const delta = def.surface || options.angle === undefined ? 0 : options.angle - def.angle;
449
+ const decor = delta === 0 ? made.body : `<g transform="rotate(${r(delta)} ${r(w / 2)} ${r(h / 2)})">${made.body}</g>`;
450
+ const { ink, scrim } = inkFor(palette, k);
451
+ const body = `<g clip-path="url(#${id}c)"><rect width="${r(w)}" height="${r(h)}" fill="url(#${id}g)"/>${decor}${scrim.replace('width="100%" height="100%"', `width="${r(w)}" height="${r(h)}"`)}</g>`;
452
+ return { defs, body, ink };
453
+ }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,10 @@ export { THEMES, ROAD_THEMES } from "./theme.ts";
8
8
  export type { Palette, RoadPalette } from "./theme.ts";
9
9
  export { cleanSvg, safeUrl, safeHref, isEmblemId } from "./marks.ts";
10
10
  export { abbreviate } from "./text.ts";
11
+ export { BACKGROUND_PRESETS, FAMILY_BACKGROUNDS_VERSION, backgroundSvg, contrastRatio } from "./family-backgrounds.ts";
12
+ export type { BackgroundInk, BackgroundSvg, BackgroundSvgOptions } from "./family-backgrounds.ts";
13
+ export { resolveBackground } from "./background.ts";
14
+ export type { ResolvedBackground } from "./background.ts";
11
15
  export { VERSION } from "./version.ts";
12
16
  export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.ts";
13
17
  export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
package/dist/index.js CHANGED
@@ -5,5 +5,7 @@ export { roadToFinal } from "./road.js";
5
5
  export { THEMES, ROAD_THEMES } from "./theme.js";
6
6
  export { cleanSvg, safeUrl, safeHref, isEmblemId } from "./marks.js";
7
7
  export { abbreviate } from "./text.js";
8
+ export { BACKGROUND_PRESETS, FAMILY_BACKGROUNDS_VERSION, backgroundSvg, contrastRatio } from "./family-backgrounds.js";
9
+ export { resolveBackground } from "./background.js";
8
10
  export { VERSION } from "./version.js";
9
11
  export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";
package/dist/marks.d.ts CHANGED
@@ -38,6 +38,8 @@ export interface MarkContext {
38
38
  look?: "classic" | "road";
39
39
  /** Draws a team's `flag` code; with none a team's `mark` or initials are drawn. */
40
40
  flags?: FlagRenderer;
41
+ /** The address of a team's flag by its `flag` code, for a flag that is a picture to load and not markup to inline; tried where `flags` gives none. */
42
+ flagHref?: (code: string) => string | null | undefined;
41
43
  /** The theme a flag renderer is told about. */
42
44
  theme?: FlagRequest["theme"];
43
45
  /** The flags already asked for, by code, shape and size, so that a renderer is called once for each. */
package/dist/marks.js CHANGED
@@ -152,7 +152,19 @@ export function drawMark(team, cx, cy, size, ctx, abbrText) {
152
152
  }
153
153
  return `${wrapOpen}<use href="#${symbol.id}" x="${num(ox)}" y="${num(oy)}" width="${num(size)}" height="${num(size)}"/></g>${ring}`;
154
154
  }
155
- const mark = team.mark?.trim();
155
+ // A flag that is an address (`flagHref`) is the mark in place of the team's own, and, like every picture address, is written once.
156
+ let fromCode;
157
+ if (typeof ctx.flagHref === "function" && typeof team.flag === "string" && team.flag.trim() !== "") {
158
+ try {
159
+ const href = ctx.flagHref(team.flag.trim());
160
+ if (typeof href === "string" && !href.trim().startsWith("<") && safeUrl(href))
161
+ fromCode = href;
162
+ }
163
+ catch {
164
+ fromCode = undefined;
165
+ }
166
+ }
167
+ const mark = (fromCode ?? team.mark)?.trim();
156
168
  if (mark) {
157
169
  if (mark.startsWith("<")) {
158
170
  const svg = cleanSvg(mark);
@@ -166,10 +178,27 @@ export function drawMark(team, cx, cy, size, ctx, abbrText) {
166
178
  }
167
179
  }
168
180
  else if (safeUrl(mark)) {
169
- return `${wrapOpen}<image href="${esc(mark)}" x="${num(ox)}" y="${num(oy)}" width="${num(size)}" height="${num(size)}" preserveAspectRatio="xMidYMid slice"/></g>${ring}`;
181
+ // Each distinct address is written once, as a symbol that holds the image, and used wherever the team appears: a long data address or a URL repeated per use would be written again and again.
182
+ const key = `url:${mark}`;
183
+ let symbol = ctx.symbols.get(key);
184
+ if (!symbol) {
185
+ symbol = { id: `${ctx.prefix}-m${ctx.symbols.size + 1}`, viewBox: "0 0 100 100", inner: `<image href="${esc(mark)}" x="0" y="0" width="100" height="100" preserveAspectRatio="xMidYMid slice"/>` };
186
+ ctx.symbols.set(key, symbol);
187
+ }
188
+ return `${wrapOpen}<use href="#${symbol.id}" x="${num(ox)}" y="${num(oy)}" width="${num(size)}" height="${num(size)}"/></g>${ring}`;
170
189
  }
171
190
  }
172
191
  const text = (abbrText ?? team.abbr ?? abbreviate(team.name)).slice(0, 3);
192
+ if (road) {
193
+ // No flag or logo: the road look draws a tile with the abbreviation on it, so that a team without a picture looks meant. A team colour fills the tile (the letters are white or
194
+ // near-black, whichever reads on it); with none the tile is neutral.
195
+ const rx = size * 0.24;
196
+ const tile = `<rect class="tb-plain tb-tile" x="${num(x)}" y="${num(y)}" width="${num(size)}" height="${num(size)}" rx="${num(rx)}"${team.color ? ` style="fill:${esc(team.color)}"` : ""}/>`;
197
+ const tileInk = team.color ? inkOn(team.color) : null;
198
+ const tileRing = `<rect class="tb-ring tb-ring-r" x="${num(x + rw / 2)}" y="${num(y + rw / 2)}" width="${num(size - rw)}" height="${num(size - rw)}" rx="${num(Math.max(rx - rw / 2, 1))}" stroke-width="${num(rw)}"/><rect class="tb-edge" x="${num(x)}" y="${num(y)}" width="${num(size)}" height="${num(size)}" rx="${num(rx)}" stroke-width="${hair}"/>`;
199
+ const letters = [...text].length;
200
+ return `<g class="tb-mark">${tile}<text class="tb-initials" x="${num(cx)}" y="${num(cy + size * 0.13)}" font-size="${num(size * (letters <= 2 ? 0.4 : 0.31))}" font-weight="800" letter-spacing=".02em" text-anchor="middle"${tileInk ? ` style="fill:${tileInk}"` : ""}>${esc(text)}</text></g>${tileRing}`;
201
+ }
173
202
  const fill = team.color ? ` style="fill:${esc(team.color)}"` : "";
174
203
  const shape = clip ? `<circle class="tb-plain" cx="${num(cx)}" cy="${num(cy)}" r="${num(r)}"${fill}/>` : `<rect class="tb-plain" x="${num(x)}" y="${num(y)}" width="${num(size)}" height="${num(size)}" rx="2"${fill}/>`;
175
204
  const ink = team.color ? inkOn(team.color) : null;
package/dist/matchup.js CHANGED
@@ -102,6 +102,7 @@ export function resolveMatchup(m, index, teams) {
102
102
  third: !!m.third,
103
103
  walkover: !!m.walkover,
104
104
  note: m.note ?? null,
105
+ ...(m.seeds ? { seeds: m.seeds } : {}),
105
106
  };
106
107
  }
107
108
  /** The short name of a team for a small box. */