@johnmorrisdotca/taisen-brackets 0.4.0 → 0.6.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 +52 -0
- package/NOTICE.md +4 -0
- package/README.md +198 -27
- 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/furniture.d.ts +10 -0
- package/dist/furniture.js +29 -0
- package/dist/groups.d.ts +50 -0
- package/dist/groups.js +204 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -2
- package/dist/layout.js +56 -6
- package/dist/layouts.d.ts +48 -0
- package/dist/layouts.js +388 -0
- package/dist/locales.js +10 -0
- package/dist/marks.d.ts +4 -0
- package/dist/marks.js +32 -2
- package/dist/matchup.js +2 -0
- package/dist/playin.d.ts +8 -0
- package/dist/playin.js +232 -0
- package/dist/render.d.ts +8 -1
- package/dist/render.js +492 -79
- package/dist/road.draw.js +6 -3
- package/dist/road.js +1 -1
- package/dist/road.types.d.ts +1 -1
- package/dist/seeding.js +4 -2
- package/dist/table.js +6 -3
- package/dist/text.d.ts +1 -1
- package/dist/text.js +4 -4
- package/dist/theme.d.ts +13 -1
- package/dist/theme.js +29 -8
- package/dist/types.d.ts +116 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DrawOptions } from "./types.ts";
|
|
2
|
+
import type { Voice } from "./locales.ts";
|
|
3
|
+
/** The width of a text set in bold: the package's generous estimate. */
|
|
4
|
+
export declare const bold: (text: string, size: number) => number;
|
|
5
|
+
/** The height the title and the subtitle take, with the gap under them. */
|
|
6
|
+
export declare function titleHeight(o: DrawOptions): number;
|
|
7
|
+
/** The title and the subtitle, centred on the drawing. */
|
|
8
|
+
export declare function headingTexts(v: Voice, o: DrawOptions, width: number, pad: number): string[];
|
|
9
|
+
/** The big title tile over a half (`headers`): its group's name in the colour of the half, `width` wide at most, centred on `cx`, its text on the baseline `y`. */
|
|
10
|
+
export declare function headTile(text: string, cx: number, width: number, y: number, conf: 0 | 1, coloured: boolean): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { emWidth, esc, fit, num } from "./text.js";
|
|
2
|
+
/** The width of a text set in bold: the package's generous estimate. */
|
|
3
|
+
export const bold = (text, size) => emWidth(text) * size;
|
|
4
|
+
/** The height the title and the subtitle take, with the gap under them. */
|
|
5
|
+
export function titleHeight(o) {
|
|
6
|
+
const poster = o.theme === "poster";
|
|
7
|
+
return (o.title ? (poster ? 46 : 38) : 0) + (o.subtitle ? 24 : 0) + (o.title || o.subtitle ? 10 : 0);
|
|
8
|
+
}
|
|
9
|
+
/** The title and the subtitle, centred on the drawing. */
|
|
10
|
+
export function headingTexts(v, o, width, pad) {
|
|
11
|
+
const poster = o.theme === "poster";
|
|
12
|
+
const out = [];
|
|
13
|
+
let y = pad + 24;
|
|
14
|
+
if (o.title) {
|
|
15
|
+
const text = poster ? v.upper(o.title) : o.title;
|
|
16
|
+
out.push(`<text class="tb-title" x="${num(width / 2)}" y="${num(y)}" font-size="${poster ? 26 : 24}" font-weight="800" ${poster ? 'letter-spacing=".14em" ' : ""}text-anchor="middle">${esc(text)}</text>`);
|
|
17
|
+
y += poster ? 24 : 22;
|
|
18
|
+
}
|
|
19
|
+
if (o.subtitle)
|
|
20
|
+
out.push(`<text class="tb-sub" x="${num(width / 2)}" y="${num(y)}" font-size="14" text-anchor="middle">${esc(o.subtitle)}</text>`);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** The big title tile over a half (`headers`): its group's name in the colour of the half, `width` wide at most, centred on `cx`, its text on the baseline `y`. */
|
|
24
|
+
export function headTile(text, cx, width, y, conf, coloured) {
|
|
25
|
+
const w = Math.min(Math.max(width, 140), 360);
|
|
26
|
+
const size = 15;
|
|
27
|
+
const shown = fit(text, size, w - 20, (t, sz) => bold(t, sz) * 1.06);
|
|
28
|
+
return `<rect class="tb-head${coloured ? ` tb-c${conf}` : ""}" x="${num(cx - w / 2)}" y="${num(y - 17)}" width="${num(w)}" height="26" rx="7"/><text class="tb-head-t" x="${num(cx)}" y="${num(y + 1.5)}" font-size="${size}" font-weight="800" letter-spacing=".08em" text-anchor="middle">${esc(shown)}</text>`;
|
|
29
|
+
}
|
package/dist/groups.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BracketInput, BracketLayout, DrawOptions, Matchup, Size, Team } from "./types.ts";
|
|
2
|
+
import { type Voice } from "./locales.ts";
|
|
3
|
+
import { type MarkContext } from "./marks.ts";
|
|
4
|
+
/** One team's line of a group's table. */
|
|
5
|
+
export interface GroupRow {
|
|
6
|
+
team: Team;
|
|
7
|
+
played: number;
|
|
8
|
+
won: number;
|
|
9
|
+
drawn: number;
|
|
10
|
+
lost: number;
|
|
11
|
+
goalsFor: number;
|
|
12
|
+
goalsAgainst: number;
|
|
13
|
+
/** Goals for minus goals against. */
|
|
14
|
+
diff: number;
|
|
15
|
+
/** Three for a win, one for a draw. */
|
|
16
|
+
points: number;
|
|
17
|
+
}
|
|
18
|
+
/** One group: its name in the language and its teams, the best first. */
|
|
19
|
+
export interface GroupTable {
|
|
20
|
+
/** The group's key: the teams' `group`, or a letter made for a group the teams do not name. */
|
|
21
|
+
key: string;
|
|
22
|
+
/** "Group A" in the language, or the teams' own `group` where it is a name and not a letter. */
|
|
23
|
+
name: string;
|
|
24
|
+
rows: GroupRow[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The tables of the group stage: one for each group, from the round-robin games (`round: 0`) of the ties. A team's group is its `group`; teams with none take the group of the teams they
|
|
28
|
+
* played (the games make groups of the teams they join), and a group the teams do not name is lettered A, B, C. Rows are in order of points, then goal difference, then goals scored, then
|
|
29
|
+
* name (`standings: "record"` orders by wins, then fewest losses).
|
|
30
|
+
*/
|
|
31
|
+
export declare function groupStandings(input: BracketLayout | readonly Matchup[] | BracketInput, options?: Pick<DrawOptions, "locale" | "words" | "teamName" | "standings">): GroupTable[];
|
|
32
|
+
/** The table of the groups as HTML, for a reader the drawing does not serve: one `<table>` a group, its caption the group's name, a row a team. */
|
|
33
|
+
export declare function groupsTable(input: BracketLayout | readonly Matchup[] | BracketInput, options?: Pick<DrawOptions, "locale" | "words" | "teamName" | "standings"> & {
|
|
34
|
+
className?: string;
|
|
35
|
+
}): string;
|
|
36
|
+
/** What a layout that plans itself hands back to the frame. */
|
|
37
|
+
export interface OwnDrawing {
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
/** The parts, in drawing order. */
|
|
41
|
+
body: string[];
|
|
42
|
+
/** The title and the subtitle. */
|
|
43
|
+
texts: string[];
|
|
44
|
+
/** The description for a screen reader, in place of the tree's. */
|
|
45
|
+
desc?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Draws the group tables as cards in a grid. */
|
|
48
|
+
export declare function drawGroups(layout: BracketLayout, o: DrawOptions, v: Voice, size: Size, ctx: MarkContext, road: boolean, linked: {
|
|
49
|
+
count: number;
|
|
50
|
+
}): OwnDrawing;
|