@johnmorrisdotca/taisen-brackets 0.3.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,61 @@
1
+ export declare const FAMILY_BACKGROUNDS_VERSION = "1.0.0";
2
+ /** 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). */
3
+ export declare const BACKGROUND_PRESETS: readonly ["midnight", "crimson", "royal", "emerald", "pitch", "ice", "hardwood", "infield", "turf", "ball-panels", "swirl", "waves", "glow", "floodlights", "confetti", "carbon", "aurora", "sunburst", "chevrons", "halftone", "argyle", "contour", "stardust", "mesh", "spotlight"];
4
+ export type BackgroundPresetName = (typeof BACKGROUND_PRESETS)[number];
5
+ export interface BackgroundSvgOptions {
6
+ width: number;
7
+ height: number;
8
+ /** `dark` (the default) or `light`: which of the preset's two palettes. With your own `colors`, the ink follows what they are. */
9
+ theme?: "dark" | "light";
10
+ /** Your own colours, as hex (`#0b1230`) or `rgb()`: the first two make the ground, the third and fourth the glow and the pattern. Fewer than four keep the preset's for the rest. */
11
+ colors?: readonly string[];
12
+ /** The angle of the ground's gradient and of the pattern over it, in degrees as CSS has it (180 is top to bottom). The preset's own by default; a playing surface keeps its lines straight. */
13
+ angle?: number;
14
+ /** How strong the pattern and glow are, 0 to 1. Default 0.6. */
15
+ intensity?: number;
16
+ /** Moves the scattered parts (confetti, stars, scratches, contours). Default 1. */
17
+ seed?: number;
18
+ /** The prefix of the ids the background writes. Default: a hash of its options. */
19
+ id?: string;
20
+ }
21
+ /** The colours to draw over a background. Every text colour is checked against `dominant`, the colours the background mostly is, for a contrast of 4.5 to 1 (3 to 1 for `muted`). */
22
+ export interface BackgroundInk {
23
+ /** The plain colour to fill the picture with before `body`: the ground's first colour. */
24
+ base: string;
25
+ /** Whether the ground is dark (so the text is light). */
26
+ dark: boolean;
27
+ text: string;
28
+ muted: string;
29
+ /** The connecting lines. */
30
+ line: string;
31
+ /** The fill and edge of a box laid on the picture; both are `rgba()`. */
32
+ box: string;
33
+ edge: string;
34
+ /** The pill behind a round's name, a team's plain disc, the ring round a mark, the dotted road and the accent for a winner, a champion and a highlight. */
35
+ panel: string;
36
+ plain: string;
37
+ ring: string;
38
+ spine: string;
39
+ accent: string;
40
+ /** The colours the picture is mostly, worst cases included: the ground's two colours and each blended with the glow at its strongest. */
41
+ dominant: string[];
42
+ }
43
+ export interface BackgroundSvg {
44
+ defs: string;
45
+ body: string;
46
+ ink: BackgroundInk;
47
+ }
48
+ type Rgb = readonly [number, number, number];
49
+ /** A colour as three channels, or `null` when it is not hex or `rgb()`. */
50
+ export declare function parseColor(text: string): Rgb | null;
51
+ export declare const toHex: (c: Rgb) => string;
52
+ /** The relative luminance of a colour, as WCAG defines it. */
53
+ export declare function luminance(c: Rgb): number;
54
+ /** The WCAG contrast ratio of two colours, 1 to 21. */
55
+ export declare function contrastRatio(a: Rgb, b: Rgb): number;
56
+ /**
57
+ * 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
58
+ * always give the same markup; every id starts with `options.id`. An unknown preset throws.
59
+ */
60
+ export declare function backgroundSvg(preset: BackgroundPresetName, options: BackgroundSvgOptions): BackgroundSvg;
61
+ export {};
@@ -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
@@ -2,10 +2,16 @@ export * from "./layout-entry.ts";
2
2
  export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.ts";
3
3
  export { bracketTable } from "./table.ts";
4
4
  export type { TableOptions } from "./table.ts";
5
- export { THEMES } from "./theme.ts";
6
- export type { Palette } from "./theme.ts";
7
- export { cleanSvg, safeUrl, isEmblemId } from "./marks.ts";
5
+ export { roadToFinal } from "./road.ts";
6
+ export type { RoadChart, RoadDecided, RoadFinal, RoadFinalist, RoadGroupMatch, RoadInput, RoadMatch, RoadOptions, RoadRecord, RoadScore, RoadSize, RoadTeam, RoadTotals } from "./road.ts";
7
+ export { THEMES, ROAD_THEMES } from "./theme.ts";
8
+ export type { Palette, RoadPalette } from "./theme.ts";
9
+ export { cleanSvg, safeUrl, safeHref, isEmblemId } from "./marks.ts";
8
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";
9
15
  export { VERSION } from "./version.ts";
10
16
  export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.ts";
11
17
  export type { League, PlayoffFormat, PlayoffRound } from "./presets.ts";
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export * from "./layout-entry.js";
2
2
  export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.js";
3
3
  export { bracketTable } from "./table.js";
4
- export { THEMES } from "./theme.js";
5
- export { cleanSvg, safeUrl, isEmblemId } from "./marks.js";
4
+ export { roadToFinal } from "./road.js";
5
+ export { THEMES, ROAD_THEMES } from "./theme.js";
6
+ export { cleanSvg, safeUrl, safeHref, isEmblemId } from "./marks.js";
6
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";
7
10
  export { VERSION } from "./version.js";
8
11
  export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";