@johnmorrisdotca/taisen-brackets 0.3.0 → 0.4.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 +19 -1
- package/NOTICE.md +3 -2
- package/README.md +140 -9
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/locales.js +30 -0
- package/dist/marks.d.ts +16 -1
- package/dist/marks.js +58 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +236 -74
- package/dist/road-entry.d.ts +2 -0
- package/dist/road-entry.js +1 -0
- package/dist/road.d.ts +28 -0
- package/dist/road.draw.d.ts +4 -0
- package/dist/road.draw.js +256 -0
- package/dist/road.js +58 -0
- package/dist/road.model.d.ts +63 -0
- package/dist/road.model.js +312 -0
- package/dist/road.types.d.ts +115 -0
- package/dist/road.types.js +1 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +46 -0
- package/dist/theme.d.ts +21 -2
- package/dist/theme.js +35 -11
- package/dist/types.d.ts +70 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -3
package/dist/render.js
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { layoutBracket } from "./layout.js";
|
|
2
2
|
import { abbrOf, describeTie, noteParts } from "./matchup.js";
|
|
3
|
-
import { cleanSvg,
|
|
3
|
+
import { cleanSvg, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, markDefs, safeHref, safeUrl } from "./marks.js";
|
|
4
4
|
import { roundLabel } from "./rounds.js";
|
|
5
5
|
import { styleFor } from "./theme.js";
|
|
6
6
|
import { esc, fit, hash, num, textWidth } from "./text.js";
|
|
7
7
|
import { fill, voiceOf } from "./locales.js";
|
|
8
|
-
/** The sizes by name. A phone's drawing is 390 wide and as tall as the bracket needs. */
|
|
8
|
+
/** The sizes by name. A phone's drawing is 390 wide and as tall as the bracket needs; a card is 1080 by 1350, a tall post. */
|
|
9
9
|
export const SIZES = Object.freeze({
|
|
10
10
|
wide: { width: 1200, height: 675 },
|
|
11
11
|
square: { width: 1080, height: 1080 },
|
|
12
|
+
card: { width: 1080, height: 1350 },
|
|
12
13
|
phone: { width: 390, height: null },
|
|
13
14
|
});
|
|
14
15
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
15
16
|
const isLayout = (x) => typeof x === "object" && x !== null && "ties" in x && "sides" in x && "rounds" in x;
|
|
17
|
+
/** The sizes of one row of a box: the name, the mark and the score. The road look has larger marks and a big, heavy score. */
|
|
18
|
+
function metrics(rowH, road) {
|
|
19
|
+
if (road)
|
|
20
|
+
return { font: clamp(rowH * 0.46, 9, 20), md: clamp(rowH * 0.8, 12, 56), score: clamp(rowH * 0.68, 11, 38) };
|
|
21
|
+
const font = clamp(rowH * 0.5, 8.5, 16);
|
|
22
|
+
return { font, md: clamp(rowH * 0.7, 10, 28), score: font + 1 };
|
|
23
|
+
}
|
|
24
|
+
/** The ties a team played on its way to the final: the tree's ties it is a side of, never the third-place match or a round-robin game. */
|
|
25
|
+
function pathOf(layout, team) {
|
|
26
|
+
const out = new Set();
|
|
27
|
+
if (team === undefined || team === null)
|
|
28
|
+
return out;
|
|
29
|
+
const id = String(team);
|
|
30
|
+
for (const t of layout.ties)
|
|
31
|
+
if (!t.third && t.round > 0 && t.sides.some((s) => s !== null && String(s.id) === id))
|
|
32
|
+
out.add(t.index);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
16
35
|
/** The layout with each team's name in the language: what the drawing, the table and the description are written from. */
|
|
17
36
|
export function localizedLayout(layout, v) {
|
|
18
37
|
const named = new Map(layout.teams.map((t) => [t.id, { ...t, name: v.team(t) }]));
|
|
@@ -42,21 +61,21 @@ export function drawBracket(input, options = {}) {
|
|
|
42
61
|
const theme = options.theme ?? "auto";
|
|
43
62
|
const sizeName = typeof options.size === "string" || options.size === undefined ? options.size ?? "wide" : null;
|
|
44
63
|
const size = sizeName ? SIZES[sizeName] : { width: options.size.width, height: options.size.height ?? null };
|
|
45
|
-
const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : sizeName === "phone" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
64
|
+
const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : sizeName === "phone" || sizeName === "card" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
|
|
65
|
+
const road = options.look === "road";
|
|
66
|
+
const prefix = options.id ?? `tb-${hash(JSON.stringify([v.locale.code, layout.teams.map((t) => t.name), layout.ties.map((t) => [t.id, t.sides.map((s) => s?.id ?? null), t.winner, t.result.score, t.round]), options.size, orientation, options.detail, theme, options.title, options.subtitle, options.seeds, options.scores, options.champion, options.markShape, options.roundLabels, options.thirdPlace, options.trophy ? 1 : 0, road ? 1 : 0, options.highlightPath ?? null, options.flags ? 1 : 0]))}`;
|
|
67
|
+
const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: options.markShape ?? "round", look: road ? "road" : "classic", flags: options.flags, theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit", flagCache: new Map() };
|
|
68
|
+
const path = pathOf(layout, options.highlightPath);
|
|
69
|
+
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path);
|
|
70
|
+
const linked = { count: 0 };
|
|
71
|
+
const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx, linked)); // draws the marks, so before the defs are written
|
|
50
72
|
const title = options.title ?? v.words.title;
|
|
51
73
|
const desc = describeBracket(layout, options);
|
|
52
|
-
const defs =
|
|
53
|
-
|
|
54
|
-
...[...ctx.symbols.values()].map((s) => `<symbol id="${s.id}" viewBox="${esc(s.viewBox)}" preserveAspectRatio="xMidYMid slice">${s.inner}</symbol>`),
|
|
55
|
-
].join("");
|
|
56
|
-
const style = theme === "none" ? "" : `<style>${styleFor(`.${prefix}`, theme)}</style>`;
|
|
74
|
+
const defs = markDefs(ctx);
|
|
75
|
+
const style = theme === "none" ? "" : `<style>${styleFor(`.${prefix}`, theme, { road, links: linked.count > 0, follow: path.size > 0 })}</style>`;
|
|
57
76
|
const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(plan.width)}" height="${num(plan.height)}"${theme === "none" ? ' fill="none"' : ""}/>`;
|
|
58
77
|
return [
|
|
59
|
-
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(plan.width)} ${num(plan.height)}" width="${num(plan.width)}" height="${num(plan.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
|
|
78
|
+
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${road ? " tb-look-road" : ""}${path.size > 0 ? " tb-follow" : ""}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(plan.width)} ${num(plan.height)}" width="${num(plan.width)}" height="${num(plan.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
|
|
60
79
|
`<title id="${prefix}-t">${esc(title)}</title><desc id="${prefix}-d">${esc(desc)}</desc>`,
|
|
61
80
|
style,
|
|
62
81
|
defs ? `<defs>${defs}</defs>` : "",
|
|
@@ -69,7 +88,7 @@ export function drawBracket(input, options = {}) {
|
|
|
69
88
|
].filter(Boolean).join("\n");
|
|
70
89
|
}
|
|
71
90
|
// ---------------------------------------------------------------------------------------------- the plan
|
|
72
|
-
function planBracket(layout, o, v, size, vertical, ctx) {
|
|
91
|
+
function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
73
92
|
const pad = vertical ? 14 : 22;
|
|
74
93
|
const showLabels = o.roundLabels ?? true;
|
|
75
94
|
const poster = o.theme === "poster";
|
|
@@ -118,7 +137,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
118
137
|
extraH0 = othersHeight(others.length, W0, pad, ow, OTHER_H) + thirdBlock;
|
|
119
138
|
const bodyH = size.height === null ? rows * 60 : size.height - top - pad - extraH0;
|
|
120
139
|
rowStep = bodyH / rows;
|
|
121
|
-
S = clamp(rowStep - 9, 24, 104);
|
|
140
|
+
S = clamp(rowStep - (road ? 7 : 9), 24, road ? 132 : 104);
|
|
122
141
|
rowStep = Math.max(rowStep, S + 6);
|
|
123
142
|
rowStep = Math.min(rowStep, S + 40);
|
|
124
143
|
boxW = D;
|
|
@@ -127,38 +146,91 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
127
146
|
else {
|
|
128
147
|
const availW = W0 - 2 * pad;
|
|
129
148
|
const slot = availW / Math.max(rows, 1);
|
|
130
|
-
S = clamp(slot - 5, 30, 150);
|
|
149
|
+
S = clamp(slot - 5, 30, road ? 340 : 150);
|
|
131
150
|
rowStep = Math.min(slot, S + 5);
|
|
132
151
|
boxW = S;
|
|
133
152
|
}
|
|
134
153
|
// Full boxes (names) only where the longest name fits beside its mark and a score.
|
|
135
154
|
const longest = Math.max(0, ...layout.teams.map((t) => textWidth(t.name, 13)));
|
|
155
|
+
const longestAbbr = Math.max(0, ...layout.teams.map((t) => textWidth(abbrOf(t, v), 13)));
|
|
156
|
+
// The road look's marks and scores are large: they are scaled down together until a row fits its box, before a name is dropped for its abbreviation.
|
|
157
|
+
const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
|
|
158
|
+
// The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
|
|
159
|
+
const markScores = road && o.detail !== "mark" && (o.scores ?? true) && layout.ties.some((t) => t.result.score !== null);
|
|
160
|
+
const fitOf = (w, h, named) => {
|
|
161
|
+
const rowH = (h - (anyNotes && h >= 62 ? 15 : 0)) / 2;
|
|
162
|
+
const { font, md, score } = metrics(rowH, road);
|
|
163
|
+
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
164
|
+
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
165
|
+
const extra = (anyBye ? textWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? textWidth("(00)", Math.max(score * 0.55, 8)) + 3 : 0);
|
|
166
|
+
const fixed = clamp(rowH * 0.16, 5, 12) + clamp(rowH * 0.18, 5, 10) + 8 + 10;
|
|
167
|
+
const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + textWidth("00", score) + extra;
|
|
168
|
+
return Math.min(1, (w - fixed) / varying);
|
|
169
|
+
};
|
|
170
|
+
let fitNow = 1;
|
|
136
171
|
const pick = (w, h) => {
|
|
137
172
|
const rowH = h / 2;
|
|
138
|
-
const font =
|
|
139
|
-
const md = clamp(rowH * 0.7, 10, 28);
|
|
173
|
+
const { font, md, score } = metrics(rowH, false);
|
|
140
174
|
// Room for the score, and for the words some boxes carry beside it: a bye, a shoot-out's score.
|
|
141
175
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
142
176
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
143
177
|
const extra = (anyBye ? textWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? textWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
|
|
144
|
-
const needed = 6 + md + 5 + longest * (font / 13) + 8 + textWidth("00",
|
|
178
|
+
const needed = 6 + md + 5 + longest * (font / 13) + 8 + textWidth("00", score) + extra + 14;
|
|
145
179
|
const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
|
|
146
180
|
return named === "mark" && o.markShape === "none" ? "compact" : named;
|
|
147
181
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
// The road look: a box is as high as its width lets the large marks and scores be. A name is kept (full) only where that leaves rows of 28 or more;
|
|
183
|
+
// else the abbreviation (compact) where rows of 18 or more fit; else only the marks.
|
|
184
|
+
const roadPick = (w, hMax) => {
|
|
185
|
+
const search = (named, hMin) => {
|
|
186
|
+
let h = hMax;
|
|
187
|
+
while (h > hMin && fitOf(w, h, named) < 0.94)
|
|
188
|
+
h -= 2;
|
|
189
|
+
h = Math.max(h, Math.min(hMin, hMax));
|
|
190
|
+
return { detail: named, h, ok: fitOf(w, h, named) >= 0.94 };
|
|
191
|
+
};
|
|
192
|
+
const forced = o.detail && o.detail !== "auto" ? o.detail : null;
|
|
193
|
+
let chosen = null;
|
|
194
|
+
if (forced === "full" || forced === "compact")
|
|
195
|
+
chosen = search(forced, forced === "full" ? 56 : 36);
|
|
196
|
+
else if (forced === null) {
|
|
197
|
+
const full = search("full", 56);
|
|
198
|
+
const compact = search("compact", 36);
|
|
199
|
+
chosen = full.ok ? full : compact.ok ? compact : o.markShape === "none" ? compact : null;
|
|
200
|
+
}
|
|
201
|
+
const m = Math.min((w - 12) / 2 - 2, 56);
|
|
202
|
+
chosen ??= { detail: o.markShape === "none" ? "compact" : "mark", h: Math.min(hMax, markScores ? m + clamp(m * 0.5, 10, 24) * 1.3 + 14 : 2 * Math.min(w - 8, 60) + 8) };
|
|
203
|
+
if (chosen.detail !== "mark")
|
|
204
|
+
fitNow = Math.min(1, fitOf(w, chosen.h, chosen.detail));
|
|
205
|
+
return chosen;
|
|
206
|
+
};
|
|
207
|
+
let detail;
|
|
208
|
+
if (road) {
|
|
209
|
+
const picked = roadPick(boxW, vertical ? clamp(boxW * 0.42, 48, 124) : boxH);
|
|
210
|
+
detail = picked.detail;
|
|
211
|
+
if (vertical)
|
|
212
|
+
boxH = picked.h;
|
|
213
|
+
else {
|
|
214
|
+
S = picked.h;
|
|
215
|
+
boxH = S;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
detail = pick(boxW, vertical ? 48 : boxH);
|
|
220
|
+
if (vertical)
|
|
221
|
+
boxH = detail === "full" ? 50 : detail === "compact" ? 42 : boxW >= 76 ? 38 : 2 * Math.min(boxW - 8, 24) + 8;
|
|
222
|
+
}
|
|
151
223
|
if (vertical) {
|
|
152
224
|
D = boxH;
|
|
153
225
|
gapT = showLabels ? 26 : 16;
|
|
154
226
|
}
|
|
155
227
|
// The champion's block: a trophy, a large mark, the name and the word; and the third-place match below the final.
|
|
156
|
-
const champMark = vertical ? 54 : clamp(centreT * 0.34, 44, 84);
|
|
228
|
+
const champMark = vertical ? (road ? clamp(size.width * 0.14, 54, 96) : 54) : clamp(centreT * (road ? 0.4 : 0.34), 44, road ? 96 : 84);
|
|
157
229
|
const trophyH = hasTrophy ? (vertical ? 56 : clamp(centreT * 0.45, 48, 84)) : 0;
|
|
158
230
|
const nameFont = clamp(champMark * 0.3, 13, 20);
|
|
159
231
|
const champH = champ ? trophyH + (hasTrophy ? 6 : 0) + champMark + 8 + nameFont + 6 + 11 + 2 : 0;
|
|
160
232
|
const finalW = halves ? (vertical ? Math.min(size.width - 2 * pad, Math.max(boxW, 160)) : Math.min(centreT - 12, Math.max(D * 1.1, 120))) : D;
|
|
161
|
-
|
|
233
|
+
let finalH = vertical ? (detail === "mark" ? Math.max(boxH, 52) : boxH) : boxH;
|
|
162
234
|
const thirdH = withThird && !bottomThird ? 18 + boxH + 14 : 0;
|
|
163
235
|
// ----- the slot positions along t
|
|
164
236
|
if (!vertical) {
|
|
@@ -201,14 +273,14 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
201
273
|
rects.set(layout.final.i, rect);
|
|
202
274
|
boxes.push({ rect, tie: layout.ties[layout.final.i], order: layout.final.order, bye: null, byes: [], final: true, champion: champ });
|
|
203
275
|
if (showLabels)
|
|
204
|
-
texts.push(label(v, roundLabel(layout, layout.rounds, o.roundNames, v), midX, rect.y - 7, "middle"));
|
|
276
|
+
texts.push(label(v, road, roundLabel(layout, layout.rounds, o.roundNames, v), midX, rect.y - 7, "middle"));
|
|
205
277
|
if (champ)
|
|
206
278
|
extras.push(...championBlock(layout.champion, midX, rect.y - 22 - champH, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
207
279
|
if (withThird) {
|
|
208
280
|
const t3 = layout.ties[layout.third];
|
|
209
281
|
const r3 = { x: midX - finalW / 2, y: rect.y + rect.h + 14 + 18, w: finalW, h: boxH };
|
|
210
282
|
rects.set(layout.third, r3);
|
|
211
|
-
texts.push(label(v, v.words.thirdPlace, midX, r3.y - 6, "middle"));
|
|
283
|
+
texts.push(label(v, road, v.words.thirdPlace, midX, r3.y - 6, "middle"));
|
|
212
284
|
boxes.push({ rect: r3, tie: t3, order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
213
285
|
}
|
|
214
286
|
}
|
|
@@ -229,7 +301,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
229
301
|
const ks = halves ? [slotOfColumn("l", ci), slotOfColumn("r", ci)] : [ci];
|
|
230
302
|
const used = layout.sides.length > 1 || !halves ? (halves ? [0, 1] : [0]) : [0];
|
|
231
303
|
ks.forEach((k, n) => { if (used.includes(n) && (!halves || n < layout.sides.length))
|
|
232
|
-
texts.push(label(v, name, starts[k] + D / 2, top - 9, "middle", D + gapT * 0.8)); });
|
|
304
|
+
texts.push(label(v, road, name, starts[k] + D / 2, top - 9, "middle", D + gapT * 0.8)); });
|
|
233
305
|
}
|
|
234
306
|
}
|
|
235
307
|
if (hasGroups) {
|
|
@@ -253,26 +325,48 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
253
325
|
const x0 = goingRight ? a.x + a.w : a.x, x1 = goingRight ? b.x : b.x + b.w;
|
|
254
326
|
const y0 = a.y + a.h / 2, y1 = b.y + b.h / 2;
|
|
255
327
|
const xm = (x0 + x1) / 2;
|
|
256
|
-
links.push(`<path class="
|
|
328
|
+
links.push(`<path class="${lineClass(path, t.index, p)}" d="M${num(x0)} ${num(y0)}H${num(xm)}V${num(y1)}H${num(x1)}"${lineAttrs(o)}/>`);
|
|
257
329
|
}
|
|
258
330
|
if (bottomThird)
|
|
259
|
-
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, boxes, texts);
|
|
260
|
-
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, boxes, texts);
|
|
331
|
+
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
|
|
332
|
+
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, road, boxes, texts);
|
|
261
333
|
texts.push(...headings(v, o, width, pad, poster));
|
|
262
|
-
return { width, height, boxes, links, texts, extras, detail, vertical };
|
|
334
|
+
return { width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores };
|
|
263
335
|
}
|
|
264
336
|
// ----- vertical: the rounds are rows. The left half is the top; the final is a band in the middle.
|
|
265
|
-
const bandT = D + gapT;
|
|
266
337
|
const groupBand = hasGroups ? 26 : 0;
|
|
267
338
|
// On a tall page the champion sits at the top, as the page's first thing; the lines into the final must not run through it.
|
|
268
339
|
const champTop = halves && champ ? champH + 14 : 0;
|
|
269
|
-
const centreBand = (showLabels ? 32 : 0) + 6 + finalH + 14;
|
|
270
340
|
const topT = pad + titleH + champTop + groupBand + (showLabels ? 14 : 0);
|
|
271
341
|
const bandsPerSide = nc;
|
|
272
|
-
const
|
|
342
|
+
const bandCount = halves ? 2 * bandsPerSide : nc + (champ ? 1 : 0);
|
|
273
343
|
others = othersOf(layout);
|
|
274
344
|
ow = Math.min(W0 - 2 * pad, 150);
|
|
275
|
-
const
|
|
345
|
+
const othersV = othersHeight(others.length, W0, pad, ow, OTHER_H);
|
|
346
|
+
// The height the page needs for boxes `h` high with `gap` between the rounds, and what it keeps for the final's band and the third-place match.
|
|
347
|
+
const needOf = (h, gap) => {
|
|
348
|
+
const finalHere = detail === "mark" ? Math.max(h, 52) : h;
|
|
349
|
+
const centre = (showLabels ? 32 : 0) + 6 + finalHere + 14;
|
|
350
|
+
const thirdV = bottomThird ? 18 + (road ? h : 52) + 16 : 0;
|
|
351
|
+
const band = h + gap;
|
|
352
|
+
return (halves ? topT + bandsPerSide * band + centre + bandsPerSide * band + groupBand + pad : topT + (nc + (champ ? 1 : 0)) * band + (champ ? champH : 0) + pad) + othersV + thirdV;
|
|
353
|
+
};
|
|
354
|
+
// The road look fits a tall page: the boxes are made lower until the rounds fit, or, with room to spare, the room left over is shared between the rounds.
|
|
355
|
+
if (road && size.height !== null) {
|
|
356
|
+
while (boxH > 40 && needOf(boxH, gapT) > size.height)
|
|
357
|
+
boxH -= 2;
|
|
358
|
+
const slack = size.height - needOf(boxH, gapT);
|
|
359
|
+
if (slack > 0 && bandCount > 0)
|
|
360
|
+
gapT += Math.min(slack / bandCount, boxH * 0.5);
|
|
361
|
+
D = boxH;
|
|
362
|
+
finalH = detail === "mark" ? Math.max(boxH, 52) : boxH;
|
|
363
|
+
}
|
|
364
|
+
const centreBand = (showLabels ? 32 : 0) + 6 + finalH + 14;
|
|
365
|
+
const thirdH2 = road ? boxH : 52;
|
|
366
|
+
const thirdBlockV = bottomThird ? 18 + thirdH2 + 16 : 0;
|
|
367
|
+
const bandT = D + gapT;
|
|
368
|
+
const totalT = needOf(boxH, gapT) - othersV - thirdBlockV;
|
|
369
|
+
const extraV = othersV + thirdBlockV;
|
|
276
370
|
const height = Math.max(size.height ?? 0, totalT + extraV);
|
|
277
371
|
const treeW = rows * rowStep;
|
|
278
372
|
const x0 = pad + (W0 - 2 * pad - treeW) / 2;
|
|
@@ -300,7 +394,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
300
394
|
if (champ)
|
|
301
395
|
extras.push(...championBlock(layout.champion, midX, pad + titleH + 4, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
302
396
|
if (showLabels) {
|
|
303
|
-
texts.push(label(v, roundLabel(layout, layout.rounds, o.roundNames, v), midX, y + 12, "middle"));
|
|
397
|
+
texts.push(label(v, road, roundLabel(layout, layout.rounds, o.roundNames, v), midX, y + 12, "middle"));
|
|
304
398
|
y += 16;
|
|
305
399
|
}
|
|
306
400
|
const rect = { x: midX - finalW / 2, y: y + 6, w: finalW, h: finalH };
|
|
@@ -316,7 +410,7 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
316
410
|
const name = roundLabel(layout, c, o.roundNames, v);
|
|
317
411
|
const sides = halves ? (layout.sides.length > 1 ? ["l", "r"] : ["l"]) : ["l"];
|
|
318
412
|
for (const sd of sides)
|
|
319
|
-
texts.push(`<text class="tb-round tb-halo" x="${num(pad)}" y="${num(bandStart(sd, ci) - 8)}" font-size="11" font-weight="600" letter-spacing=".08em">${esc(v.upper(name))}</text>`);
|
|
413
|
+
texts.push(road ? label(v, road, name, pad, bandStart(sd, ci) - 8, "start") : `<text class="tb-round tb-halo" x="${num(pad)}" y="${num(bandStart(sd, ci) - 8)}" font-size="11" font-weight="600" letter-spacing=".08em">${esc(v.upper(name))}</text>`);
|
|
320
414
|
});
|
|
321
415
|
}
|
|
322
416
|
if (hasGroups) {
|
|
@@ -338,21 +432,39 @@ function planBracket(layout, o, v, size, vertical, ctx) {
|
|
|
338
432
|
const y0 = down ? a.y + a.h : a.y, y1 = down ? b.y : b.y + b.h;
|
|
339
433
|
const x0c = a.x + a.w / 2, x1c = b.x + b.w / 2;
|
|
340
434
|
const ym = down ? y0 + gapT * 0.4 : y0 - gapT * 0.4;
|
|
341
|
-
links.push(`<path class="
|
|
435
|
+
links.push(`<path class="${lineClass(path, t.index, p)}" d="M${num(x0c)} ${num(y0)}V${num(ym)}H${num(x1c)}V${num(y1)}"${lineAttrs(o)}/>`);
|
|
342
436
|
}
|
|
343
437
|
if (bottomThird)
|
|
344
|
-
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, 200),
|
|
345
|
-
othersBlock(layout, others, totalT - pad +
|
|
438
|
+
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
|
|
439
|
+
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OTHER_H, v, road, boxes, texts);
|
|
346
440
|
texts.push(...headings(v, o, W0, pad, poster));
|
|
347
|
-
return { width: W0, height, boxes, links, texts, extras, detail, vertical };
|
|
441
|
+
return { width: W0, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores };
|
|
442
|
+
}
|
|
443
|
+
/** A line's class: `tb-path` when both ties it joins are on the highlighted team's path. */
|
|
444
|
+
function lineClass(path, from, to) {
|
|
445
|
+
return path.has(from) && path.has(to) ? "tb-line tb-path" : "tb-line";
|
|
446
|
+
}
|
|
447
|
+
/** The lines of the highlighted path are drawn last, over the others. */
|
|
448
|
+
function orderLinks(links) {
|
|
449
|
+
return [...links.filter((l) => !l.includes("tb-path")), ...links.filter((l) => l.includes("tb-path"))];
|
|
348
450
|
}
|
|
349
451
|
const OTHER_H = 50;
|
|
350
452
|
const centreMinLtr = (d) => d * 1.1;
|
|
351
453
|
function lineAttrs(o) {
|
|
352
454
|
return o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".5"' : "";
|
|
353
455
|
}
|
|
354
|
-
function label(v, text, x, y, anchor, maxWidth = 1e9) {
|
|
355
|
-
|
|
456
|
+
function label(v, road, text, x, y, anchor, maxWidth = 1e9) {
|
|
457
|
+
if (!road)
|
|
458
|
+
return `<text class="tb-round" x="${num(x)}" y="${num(y)}" font-size="11" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(fit(v.upper(text), 11 * 1.18, maxWidth))}</text>`;
|
|
459
|
+
// The road look names a round on a pill, set a little smaller (to 9) before the name is cut.
|
|
460
|
+
const upper = v.upper(text);
|
|
461
|
+
let s = 11;
|
|
462
|
+
while (s > 9 && textWidth(upper, s * 1.18) > maxWidth - 18)
|
|
463
|
+
s -= 0.5;
|
|
464
|
+
const shown = fit(upper, s * 1.18, maxWidth - 18);
|
|
465
|
+
const w = textWidth(shown, s * 1.18) + 18;
|
|
466
|
+
const x0 = anchor === "middle" ? x - w / 2 : x;
|
|
467
|
+
return `<rect class="tb-panel" x="${num(x0)}" y="${num(y - 14.5)}" width="${num(w)}" height="20" rx="10"/><text class="tb-lab" x="${num(x0 + w / 2)}" y="${num(y)}" font-size="${num(s)}" font-weight="700" letter-spacing=".08em" text-anchor="middle">${esc(shown)}</text>`;
|
|
356
468
|
}
|
|
357
469
|
function headings(v, o, width, pad, poster) {
|
|
358
470
|
const out = [];
|
|
@@ -401,75 +513,123 @@ function othersHeight(count, width, pad, ow, oh) {
|
|
|
401
513
|
const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
|
|
402
514
|
return 26 + Math.ceil(count / cols) * (oh + 10) + 6;
|
|
403
515
|
}
|
|
404
|
-
function othersBlock(layout, list, y0, width, pad, ow, oh, v, boxes, texts) {
|
|
516
|
+
function othersBlock(layout, list, y0, width, pad, ow, oh, v, road, boxes, texts) {
|
|
405
517
|
if (!list.length)
|
|
406
518
|
return;
|
|
407
519
|
const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
|
|
408
520
|
const x0 = pad + (width - 2 * pad - (Math.min(cols, list.length) * (ow + 10) - 10)) / 2;
|
|
409
|
-
texts.push(label(v, v.words.other, width / 2, y0 + 12, "middle"));
|
|
521
|
+
texts.push(label(v, road, v.words.other, width / 2, y0 + 12, "middle"));
|
|
410
522
|
list.forEach((i, n) => {
|
|
411
523
|
const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
|
|
412
524
|
boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
413
525
|
});
|
|
414
526
|
}
|
|
415
527
|
/** The third-place match with its label, centred on `cx`, its label at `y0`. */
|
|
416
|
-
function thirdAtFoot(layout, y0, cx, w, h, v, boxes, texts) {
|
|
417
|
-
texts.push(label(v, v.words.thirdPlace, cx, y0 + 12, "middle"));
|
|
528
|
+
function thirdAtFoot(layout, y0, cx, w, h, v, road, boxes, texts) {
|
|
529
|
+
texts.push(label(v, road, v.words.thirdPlace, cx, y0 + 12, "middle"));
|
|
418
530
|
boxes.push({ rect: { x: cx - w / 2, y: y0 + 18, w, h }, tie: layout.ties[layout.third], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
419
531
|
}
|
|
420
|
-
|
|
421
|
-
function
|
|
532
|
+
/** The address `href` gives for a tie (or for one side of it), when it is one a drawing may link to; `null` otherwise. */
|
|
533
|
+
function hrefOf(o, tie, side) {
|
|
534
|
+
if (typeof o.href !== "function")
|
|
535
|
+
return null;
|
|
536
|
+
const url = side === undefined ? o.href(tie) : o.href(tie, side);
|
|
537
|
+
return typeof url === "string" && safeHref(url) ? url.trim() : null;
|
|
538
|
+
}
|
|
539
|
+
function drawBox(p, plan, o, v, ctx, linked) {
|
|
422
540
|
const { rect, tie } = p;
|
|
423
541
|
const { x, y, w, h } = rect;
|
|
424
542
|
const detail = plan.detail;
|
|
543
|
+
const road = plan.road;
|
|
425
544
|
const scoresOn = o.scores ?? true;
|
|
426
545
|
const seedsOn = o.seeds ?? false;
|
|
427
546
|
const highlight = o.winnerHighlight ?? true;
|
|
547
|
+
const description = describeTie(tie, v);
|
|
548
|
+
// A link on the whole box wins; with none, each side that has an address is a link on its own row.
|
|
549
|
+
const boxHref = hrefOf(o, tie);
|
|
550
|
+
const sideHref = (k) => (boxHref !== null || tie.sides[k] === null ? null : hrefOf(o, tie, k));
|
|
551
|
+
const anchor = (url, name, inner) => {
|
|
552
|
+
linked.count++;
|
|
553
|
+
return `<a class="tb-link" href="${esc(url)}" aria-label="${esc(name)}">${inner}</a>`;
|
|
554
|
+
};
|
|
428
555
|
const parts = [];
|
|
429
|
-
parts.push(`<rect class="tb-box" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" rx="${num(Math.min(7, h / 4))}"${o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".4"' : ""}/>`);
|
|
556
|
+
parts.push(`<rect class="tb-box" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" rx="${num(road ? Math.min(14, h / 3.2) : Math.min(7, h / 4))}"${boxHref !== null ? ' pointer-events="all"' : ""}${o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".4"' : ""}/>`);
|
|
430
557
|
const r = tie.result;
|
|
431
558
|
const notes = noteParts(tie, v);
|
|
432
|
-
|
|
559
|
+
// In the road look every box keeps the strip for the note when any tie has one, so that all the rows are the same size.
|
|
560
|
+
const noteH = detail !== "mark" && h >= (road ? 62 : 50) && (road ? plan.notes : notes.length > 0) ? (road ? 15 : 13) : 0;
|
|
433
561
|
const rowH = (h - noteH) / 2;
|
|
434
562
|
const rowsOrder = p.order;
|
|
435
563
|
const cls = (k) => (highlight && tie.winner !== null ? (tie.winner === k ? "tb-won" : "tb-lost") : "tb-pending");
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
const
|
|
564
|
+
const row = (k, inner, rx, ry, rw, rh) => {
|
|
565
|
+
const url = sideHref(k);
|
|
566
|
+
const team = tie.sides[k];
|
|
567
|
+
if (url === null || team === null)
|
|
568
|
+
return inner;
|
|
569
|
+
return anchor(url, `${team.name}: ${description}`, `<rect class="tb-hit" x="${num(rx)}" y="${num(ry)}" width="${num(rw)}" height="${num(rh)}" fill="transparent"/>${inner}`);
|
|
570
|
+
};
|
|
571
|
+
if (detail === "mark" && plan.markScores) {
|
|
572
|
+
// Two columns: each team's mark with its score under it.
|
|
573
|
+
const colW = (w - 12) / 2;
|
|
574
|
+
const m = Math.min(colW - 2, 56, h - 14);
|
|
575
|
+
const sf = clamp(m * 0.5, 10, 24);
|
|
576
|
+
const withScore = scoresOn && r.score !== null;
|
|
577
|
+
const markH = withScore ? m : h - 12;
|
|
578
|
+
const top = y + (h - (m + (withScore ? sf * 1.3 : 0))) / 2;
|
|
579
|
+
rowsOrder.forEach((k, n) => {
|
|
580
|
+
const team = tie.sides[k];
|
|
581
|
+
const cx = x + 6 + colW / 2 + n * colW;
|
|
582
|
+
const cy = top + m / 2;
|
|
583
|
+
const inner = [drawMark(team, cx, cy, withScore ? m : markH, ctx)];
|
|
584
|
+
if (withScore && r.score) {
|
|
585
|
+
const pens = r.pens ? ` (${v.number(r.pens[k])})` : "";
|
|
586
|
+
inner.push(`<text class="tb-score" x="${num(cx)}" y="${num(top + m + sf * 1.05)}" font-size="${num(sf)}" text-anchor="middle" font-variant-numeric="tabular-nums">${esc(v.number(r.score[k]))}${r.pens ? `<tspan class="tb-muted" font-size="${num(sf * 0.6)}" font-weight="600">${esc(pens)}</tspan>` : ""}</text>`);
|
|
587
|
+
}
|
|
588
|
+
parts.push(row(k, `<g class="${cls(k)}">${inner.join("")}</g>`, cx - colW / 2, y, colW, h));
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
else if (detail === "mark") {
|
|
592
|
+
const stack = Math.min((h - 8) / 2, w - 8, road ? 60 : 44);
|
|
593
|
+
const side = Math.min(h - 8, (w - 12) / 2, road ? 60 : 44);
|
|
439
594
|
const stacked = stack >= side;
|
|
440
595
|
const md2 = Math.max(stacked ? stack : side, 8);
|
|
441
596
|
rowsOrder.forEach((k, n) => {
|
|
442
597
|
const team = tie.sides[k];
|
|
443
598
|
const cx = stacked ? x + w / 2 : x + w / 2 + (n === 0 ? -1 : 1) * (md2 / 2 + 3);
|
|
444
599
|
const cy = stacked ? y + h / 2 + (n === 0 ? -1 : 1) * (md2 / 2 + 1) : y + h / 2;
|
|
445
|
-
parts.push(`<g class="${cls(k)}">${drawMark(team, cx, cy, md2, ctx)}</g
|
|
600
|
+
parts.push(row(k, `<g class="${cls(k)}">${drawMark(team, cx, cy, md2, ctx)}</g>`, cx - md2 / 2, cy - md2 / 2, md2, md2));
|
|
446
601
|
});
|
|
447
602
|
}
|
|
448
603
|
else {
|
|
449
|
-
const
|
|
450
|
-
const
|
|
604
|
+
const base = metrics(rowH, road);
|
|
605
|
+
const f = plan.fit;
|
|
606
|
+
const font = road ? Math.max(base.font * f, 8.5) : base.font, md = road ? base.md * f : base.md, scoreFont = road ? base.score * f : base.score;
|
|
607
|
+
const padX = road ? clamp(rowH * 0.16, 5, 12) : 6;
|
|
608
|
+
const gap = road ? clamp(rowH * 0.18, 5, 10) : 5;
|
|
609
|
+
const small = Math.max(road ? scoreFont * 0.55 : font - 2, 8);
|
|
610
|
+
const small2 = Math.max(font - 2, 8);
|
|
451
611
|
rowsOrder.forEach((k, n) => {
|
|
452
612
|
const team = tie.sides[k];
|
|
453
613
|
const ry = y + n * rowH;
|
|
454
614
|
const cy = ry + rowH / 2;
|
|
455
615
|
const rowParts = [];
|
|
456
|
-
let tx = x +
|
|
616
|
+
let tx = x + padX;
|
|
457
617
|
if (o.markShape !== "none") {
|
|
458
|
-
rowParts.push(drawMark(team, x +
|
|
459
|
-
tx = x +
|
|
618
|
+
rowParts.push(drawMark(team, x + padX + md / 2, cy, md, ctx));
|
|
619
|
+
tx = x + padX + md + gap;
|
|
460
620
|
}
|
|
461
621
|
const score = scoresOn && r.score ? v.number(r.score[k]) : "";
|
|
462
622
|
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
463
623
|
const bye = p.byes.includes(k);
|
|
464
|
-
const right = x + w -
|
|
465
|
-
const scoreW = score ? textWidth(score,
|
|
466
|
-
const pensW = pensText ? textWidth(pensText,
|
|
624
|
+
const right = x + w - padX;
|
|
625
|
+
const scoreW = score ? textWidth(score, scoreFont) : 0;
|
|
626
|
+
const pensW = pensText ? textWidth(pensText, small) + 3 : 0;
|
|
467
627
|
const seed = seedsOn && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
468
628
|
const nm = team ? (detail === "full" ? team.name : abbrOf(team, v)) : v.words.tbd;
|
|
469
629
|
// The word for a bye goes beside the score only where the name still fits beside it.
|
|
470
|
-
const byeFits = bye && right - tx - (scoreW + pensW) - (textWidth(v.words.bye,
|
|
630
|
+
const byeFits = bye && right - tx - (scoreW + pensW) - (textWidth(v.words.bye, small2) + 6) - 6 >= textWidth(`${seed}${nm}`, font);
|
|
471
631
|
const byeText = byeFits ? v.words.bye : "";
|
|
472
|
-
const byeW = byeText ? textWidth(byeText,
|
|
632
|
+
const byeW = byeText ? textWidth(byeText, small2) : 0;
|
|
473
633
|
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
|
|
474
634
|
// A name that does not fit is set a little smaller before it is cut.
|
|
475
635
|
let nameFont = font;
|
|
@@ -478,19 +638,21 @@ function drawBox(p, plan, o, v, ctx) {
|
|
|
478
638
|
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8));
|
|
479
639
|
rowParts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(tx)}" y="${num(cy + nameFont * 0.35)}" font-size="${num(nameFont)}">${esc(text)}</text>`);
|
|
480
640
|
if (score)
|
|
481
|
-
rowParts.push(`<text class="tb-score" x="${num(right)}" y="${num(cy +
|
|
641
|
+
rowParts.push(`<text class="tb-score" x="${num(right)}" y="${num(cy + scoreFont * 0.35)}" font-size="${num(scoreFont)}" text-anchor="end" font-variant-numeric="tabular-nums">${esc(score)}</text>`);
|
|
482
642
|
if (pensText)
|
|
483
|
-
rowParts.push(`<text class="tb-muted" x="${num(right - scoreW - 3)}" y="${num(cy +
|
|
643
|
+
rowParts.push(`<text class="tb-muted" x="${num(right - scoreW - (road ? 4 : 3))}" y="${num(cy + small * 0.35)}" font-size="${num(small)}" text-anchor="end">${esc(pensText)}</text>`);
|
|
484
644
|
if (byeText)
|
|
485
|
-
rowParts.push(`<text class="tb-muted" x="${num(right - (scoreW + pensW) - (scoreW || pensW ? 6 : 0))}" y="${num(cy +
|
|
486
|
-
parts.push(`<g class="${cls(k)}">${rowParts.join("")}</g
|
|
645
|
+
rowParts.push(`<text class="tb-muted" x="${num(right - (scoreW + pensW) - (scoreW || pensW ? 6 : 0))}" y="${num(cy + small2 * 0.35)}" font-size="${num(small2)}" font-style="italic" text-anchor="end">${esc(byeText)}</text>`);
|
|
646
|
+
parts.push(row(k, `<g class="${cls(k)}">${rowParts.join("")}</g>`, x, ry, w, rowH));
|
|
487
647
|
});
|
|
488
|
-
parts.push(`<line class="tb-line" x1="${num(x + 4)}" x2="${num(x + w - 4)}" y1="${num(y + rowH)}" y2="${num(y + rowH)}" stroke-opacity=".35"${o.theme === "none" ? ' stroke="currentColor"' : ""}/>`);
|
|
648
|
+
parts.push(`<line class="${road ? "tb-sep" : "tb-line"}" x1="${num(x + 4)}" x2="${num(x + w - 4)}" y1="${num(y + rowH)}" y2="${num(y + rowH)}" stroke-opacity=".35"${o.theme === "none" ? ' stroke="currentColor"' : ""}/>`);
|
|
489
649
|
if (noteH) {
|
|
490
|
-
const f = 9.5;
|
|
491
|
-
|
|
650
|
+
const f = road ? 9 : 9.5;
|
|
651
|
+
const line = road ? v.upper(notes.join(" · ")) : notes.join(" · ");
|
|
652
|
+
parts.push(`<text class="tb-muted${road ? " tb-note" : ""}" x="${num(x + w / 2)}" y="${num(y + h - (road ? 5 : 4))}" font-size="${f}"${road ? ' font-weight="700" letter-spacing=".05em"' : ""} text-anchor="middle">${esc(fit(line, f * (road ? 1.1 : 1), w - 8))}</text>`);
|
|
492
653
|
}
|
|
493
654
|
}
|
|
494
|
-
const cls2 = `tb-tie${p.final ? " tb-final" : ""}${p.champion ? " tb-has-champ" : ""}`;
|
|
495
|
-
|
|
655
|
+
const cls2 = `tb-tie${p.final ? " tb-final" : ""}${p.champion ? " tb-has-champ" : ""}${plan.path.has(tie.index) ? " tb-on-path" : ""}`;
|
|
656
|
+
const body = boxHref !== null ? anchor(boxHref, description, parts.join("")) : parts.join("");
|
|
657
|
+
return `<g class="${cls2}"><title>${esc(description)}</title>${body}</g>`;
|
|
496
658
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { roadToFinal } from "./road.js";
|
package/dist/road.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RoadChart, RoadInput, RoadOptions } from "./road.types.ts";
|
|
2
|
+
export type { RoadChart, RoadDecided, RoadFinal, RoadFinalist, RoadGroupMatch, RoadInput, RoadMatch, RoadOptions, RoadRecord, RoadScore, RoadSize, RoadTeam, RoadTotals } from "./road.types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The road to the final: the two finalists as large round marks with their names at the top, each one's matches on its side (the round mark of both teams with the
|
|
5
|
+
* score between them, and FT, AET or the shoot-out under it), the round names down the middle, and the group stage as a compact row of small marks, codes and scores.
|
|
6
|
+
* `input` is a tournament, the same one `drawBracket` draws (a list of matchups, `{ matchups, teams }` or a layout): the two sides of its final are the finalists, each one's
|
|
7
|
+
* played ties (round-robin games, `round: 0`, as the group stage) are its road, and `team` puts one finalist on the left. Or it is the two finalists, each a team (`name`,
|
|
8
|
+
* `abbr`, `mark` or `logo` for the flag or crest, `flag` for a country code drawn by `flags`, `color`) with its `path`, the matches before the final, the earliest first,
|
|
9
|
+
* and optionally its `group`; a match is `{ round, opponent, score: [for, against], decidedBy, pens }`, a round a key (`quarter-final`) with words in every language, or any
|
|
10
|
+
* key you name in `labels`, so a series (`First round`, `4-2`) draws as well as a knockout. A team with no mark is its initials on its colour. The side that won each match
|
|
11
|
+
* is brighter; the sizes are `card` (1080 by 1350, the default), `square`, `wide` (1200 by 675) and `phone` (390 wide). `table()` lists every match as text.
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { roadToFinal, type RoadFinalist } from "@johnmorrisdotca/taisen-brackets";
|
|
15
|
+
* const owls: RoadFinalist = { name: "Northern Owls", abbr: "NOW", color: "#3a2d6b", path: [
|
|
16
|
+
* { round: "quarter-final", opponent: { name: "Coastal Pines", abbr: "CPN" }, score: [2, 0] },
|
|
17
|
+
* { round: "semi-final", opponent: { name: "Valley Stars", abbr: "VAL" }, score: [1, 1], decidedBy: "pens", pens: [4, 3] },
|
|
18
|
+
* ] };
|
|
19
|
+
* const gulls: RoadFinalist = { name: "Harbour City Gulls", abbr: "HCG", color: "#1d4f91", path: [
|
|
20
|
+
* { round: "quarter-final", opponent: { name: "Summit Peaks", abbr: "SMT" }, score: [3, 1] },
|
|
21
|
+
* { round: "semi-final", opponent: { name: "Metro Bolts", abbr: "MTB" }, score: [2, 1], decidedBy: "aet" },
|
|
22
|
+
* ] };
|
|
23
|
+
* const chart = roadToFinal([owls, gulls], { title: "Northern Cup 2030", size: "square" });
|
|
24
|
+
* console.log(chart.totals.rounds.join(" "), chart.draw().startsWith("<svg"), chart.table().includes("Semi-final"));
|
|
25
|
+
* // quarter-final semi-final true true
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function roadToFinal(input: RoadInput, options?: RoadOptions): RoadChart;
|