@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.
- package/CHANGELOG.md +27 -0
- package/NOTICE.md +2 -0
- package/README.md +64 -23
- package/dist/background.d.ts +19 -0
- package/dist/background.js +66 -0
- package/dist/backgrounds-entry.d.ts +2 -0
- package/dist/backgrounds-entry.js +1 -0
- package/dist/family-backgrounds.d.ts +61 -0
- package/dist/family-backgrounds.js +453 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/marks.d.ts +2 -0
- package/dist/marks.js +31 -2
- package/dist/matchup.js +1 -0
- package/dist/render.js +282 -58
- package/dist/road.draw.js +6 -3
- package/dist/road.js +1 -1
- package/dist/road.types.d.ts +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.js +4 -4
- package/dist/theme.d.ts +9 -1
- package/dist/theme.js +23 -8
- package/dist/types.d.ts +50 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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