@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
package/dist/render.js
CHANGED
|
@@ -3,8 +3,16 @@ import { abbrOf, describeTie, noteParts } from "./matchup.js";
|
|
|
3
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
|
-
import {
|
|
6
|
+
import { resolveBackground } from "./background.js";
|
|
7
|
+
import { emWidth, esc, fit, hash, num } from "./text.js";
|
|
7
8
|
import { fill, voiceOf } from "./locales.js";
|
|
9
|
+
import { paintVariant, seriesLineWidth } from "./layouts.js";
|
|
10
|
+
import { drawGroups } from "./groups.js";
|
|
11
|
+
import { headTile } from "./furniture.js";
|
|
12
|
+
import { drawPlayIn } from "./playin.js";
|
|
13
|
+
/** The names of the layouts, the tree first. */
|
|
14
|
+
export const LAYOUTS = Object.freeze(["tree", "rows", "shields", "update", "columns", "playin", "series", "groups"]);
|
|
15
|
+
const VARIANTS = ["rows", "shields", "update", "columns", "series"];
|
|
8
16
|
/** 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
17
|
export const SIZES = Object.freeze({
|
|
10
18
|
wide: { width: 1200, height: 675 },
|
|
@@ -14,6 +22,43 @@ export const SIZES = Object.freeze({
|
|
|
14
22
|
});
|
|
15
23
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
16
24
|
const isLayout = (x) => typeof x === "object" && x !== null && "ties" in x && "sides" in x && "rounds" in x;
|
|
25
|
+
/** The width of a text in the road look, set in a bold face: a little generous, so that it stays inside its place whichever font the page falls back to. */
|
|
26
|
+
const boldWidth = (text, size) => emWidth(text) * size;
|
|
27
|
+
/** The seed tag of a side: the tie's own, else the team's tag, else its seed number; `""` for none. */
|
|
28
|
+
function tagOf(tie, k) {
|
|
29
|
+
const given = tie.seeds?.[k];
|
|
30
|
+
const team = tie.sides[k];
|
|
31
|
+
const text = given ?? team?.tag ?? team?.seed;
|
|
32
|
+
return text === undefined || text === null ? "" : String(text).slice(0, 4);
|
|
33
|
+
}
|
|
34
|
+
/** The classes `tieClass` asks for on a tie's group, each a word of letters, digits, `-` and `_`; a leading space for each, `""` for none. A function that throws adds none. */
|
|
35
|
+
function tieClassOf(o, tie) {
|
|
36
|
+
if (typeof o.tieClass !== "function")
|
|
37
|
+
return "";
|
|
38
|
+
let asked;
|
|
39
|
+
try {
|
|
40
|
+
asked = o.tieClass(tie);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
if (typeof asked !== "string")
|
|
46
|
+
return "";
|
|
47
|
+
return asked.split(/\s+/).filter((word) => /^[A-Za-z_][\w-]*$/.test(word)).map((word) => ` ${word}`).join("");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Where a pill or a name of `w` wide can stand in the band `y0`..`y1` so that no connector line is within 5 of it: `pref` (its left edge) when that is clear, else the nearest clear place
|
|
51
|
+
* between the edges `lo` and `hi`; `null` when there is none.
|
|
52
|
+
*/
|
|
53
|
+
function freeX(segs, y0, y1, w, pref, lo, hi) {
|
|
54
|
+
const margin = 4;
|
|
55
|
+
const blocks = segs.filter((s) => Math.max(s.y0, s.y1) >= y0 - 1 && Math.min(s.y0, s.y1) <= y1 + 1).map((s) => [Math.min(s.x0, s.x1) - margin, Math.max(s.x0, s.x1) + margin]);
|
|
56
|
+
const ok = (x) => x >= lo && x + w <= hi && blocks.every(([a, b]) => x + w <= a || x >= b);
|
|
57
|
+
if (ok(pref))
|
|
58
|
+
return pref;
|
|
59
|
+
const found = [lo, hi - w, ...blocks.flatMap(([a, b]) => [b, a - w])].filter(ok);
|
|
60
|
+
return found.length === 0 ? null : found.reduce((best, x) => (Math.abs(x - pref) < Math.abs(best - pref) ? x : best));
|
|
61
|
+
}
|
|
17
62
|
/** 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
63
|
function metrics(rowH, road) {
|
|
19
64
|
if (road)
|
|
@@ -57,38 +102,87 @@ export function describeBracket(input, options = {}) {
|
|
|
57
102
|
*/
|
|
58
103
|
export function drawBracket(input, options = {}) {
|
|
59
104
|
const v = voiceOf(options);
|
|
60
|
-
const
|
|
105
|
+
const kind = options.layout ?? "tree";
|
|
106
|
+
if (!LAYOUTS.includes(kind))
|
|
107
|
+
throw new RangeError(`taisen-brackets: unknown layout ${JSON.stringify(String(kind))}: use one of ${LAYOUTS.join(", ")}`);
|
|
108
|
+
// The series card lists the east first: with no order asked for, a group named east goes before one named west.
|
|
109
|
+
const groupOrder = kind === "series" && options.groupOrder === undefined && !isLayout(input) ? eastFirst(input) : options.groupOrder;
|
|
110
|
+
const layout = localizedLayout(isLayout(input) ? input : layoutBracket(input, groupOrder ? { ...options, groupOrder } : options), v);
|
|
61
111
|
const theme = options.theme ?? "auto";
|
|
62
|
-
|
|
112
|
+
// The series card is a tall card unless a size is asked for.
|
|
113
|
+
const sizeName = typeof options.size === "string" || options.size === undefined ? options.size ?? (kind === "series" ? "card" : "wide") : null;
|
|
63
114
|
const size = sizeName ? SIZES[sizeName] : { width: options.size.width, height: options.size.height ?? null };
|
|
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";
|
|
115
|
+
const orientation = options.orientation && options.orientation !== "auto" ? options.orientation : kind === "series" || sizeName === "phone" || sizeName === "card" || (size.height !== null && size.width < size.height * 0.8) ? "vertical" : "horizontal";
|
|
65
116
|
const road = options.look === "road";
|
|
66
|
-
const
|
|
67
|
-
const
|
|
117
|
+
const hasBg = typeof options.background === "string" || (typeof options.background === "object" && options.background !== null);
|
|
118
|
+
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, ...(hasBg ? [options.background, options.vs, options.codes, options.seeds] : []), ...(options.flagHref ? ["h"] : []), ...(kind !== "tree" ? [kind, options.at, options.standings, options.qualify] : []), ...(options.headers ? ["head"] : [])]))}`;
|
|
119
|
+
const ctx = { radii: new Set(), symbols: new Map(), prefix, shape: options.markShape ?? "round", look: road ? "road" : "classic", flags: options.flags, flagHref: options.flagHref, theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit", flagCache: new Map(), defs: new Map() };
|
|
68
120
|
const path = pathOf(layout, options.highlightPath);
|
|
69
|
-
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path);
|
|
70
121
|
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
|
|
72
122
|
const title = options.title ?? v.words.title;
|
|
73
123
|
const desc = describeBracket(layout, options);
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
124
|
+
// The group tables and the play-in are not the tree: they plan and draw their own parts, and share the frame (the style, the background, the title) with it.
|
|
125
|
+
const own = kind === "groups" ? drawGroups(layout, options, v, size, ctx, road, linked) : kind === "playin" ? drawPlayIn(layout, options, v, size, ctx, road, linked) : null;
|
|
126
|
+
if (own)
|
|
127
|
+
return frame({ prefix, v, options, theme, road, kind, title, desc: own.desc ?? desc, width: own.width, height: own.height, followed: false, links: [], parts: [...own.body, ...own.texts], linkCount: linked.count, ctx });
|
|
128
|
+
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path, kind);
|
|
129
|
+
const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx, linked, layout.place[b.tie.index])); // draws the marks, so before the defs are written
|
|
130
|
+
return frame({ prefix, v, options, theme, road, kind, title, desc, width: plan.width, height: plan.height, followed: path.size > 0, links: plan.links, parts: [...drawn, ...plan.texts, ...plan.extras], linkCount: linked.count, ctx });
|
|
131
|
+
}
|
|
132
|
+
/** The drawing's frame: the root `<svg>` with its title, its style, the defs the marks wrote and the background, round the parts a layout drew. */
|
|
133
|
+
function frame(f) {
|
|
134
|
+
const { prefix, v, options, theme, road, ctx } = f;
|
|
135
|
+
const picture = resolveBackground(options.background, theme, f.width, f.height, prefix);
|
|
136
|
+
const defs = markDefs(ctx) + (picture?.defs ?? "");
|
|
137
|
+
const scope = scopeOf(options, prefix);
|
|
138
|
+
const style = theme === "none" || options.style === false ? "" : `<style>${styleFor(`.${scope}`, theme, { road, links: f.linkCount > 0, follow: f.followed, bgAuto: picture?.auto, layout: f.kind !== "tree" || options.headers === true }, picture?.inks)}</style>`;
|
|
139
|
+
const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(f.width)}" height="${num(f.height)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
|
|
77
140
|
return [
|
|
78
|
-
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${road ? " tb-look-road" : ""}${
|
|
79
|
-
`<title id="${prefix}-t">${esc(title)}</title><desc id="${prefix}-d">${esc(desc)}</desc>`,
|
|
141
|
+
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(v.locale.code)}" class="taisen ${prefix}${scope !== prefix ? ` ${scope}` : ""}${road ? " tb-look-road" : ""}${f.followed ? " tb-follow" : ""}${options.className ? ` ${esc(options.className)}` : ""}" viewBox="0 0 ${num(f.width)} ${num(f.height)}" width="${num(f.width)}" height="${num(f.height)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${prefix}-t ${prefix}-d">`,
|
|
142
|
+
`<title id="${prefix}-t">${esc(f.title)}</title><desc id="${prefix}-d">${esc(f.desc)}</desc>`,
|
|
80
143
|
style,
|
|
81
144
|
defs ? `<defs>${defs}</defs>` : "",
|
|
82
145
|
bg,
|
|
83
|
-
...
|
|
84
|
-
...
|
|
85
|
-
...plan.texts,
|
|
86
|
-
...plan.extras,
|
|
146
|
+
...f.links,
|
|
147
|
+
...f.parts,
|
|
87
148
|
"</svg>",
|
|
88
149
|
].filter(Boolean).join("\n");
|
|
89
150
|
}
|
|
151
|
+
/** The class that carries a drawing's rules: its `styleScope` when it has one, else its own id prefix. */
|
|
152
|
+
function scopeOf(o, prefix) {
|
|
153
|
+
if (o.styleScope === undefined)
|
|
154
|
+
return prefix;
|
|
155
|
+
if (typeof o.styleScope !== "string" || !/^[A-Za-z_][\w-]*$/.test(o.styleScope))
|
|
156
|
+
throw new RangeError("taisen-brackets: styleScope is one word of letters, digits, - and _");
|
|
157
|
+
return o.styleScope;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The stylesheet of a drawing with these options, for a page that draws the same bracket twice (a desk's size and a phone's): put it in the page once, give each drawing the same `styleScope`
|
|
161
|
+
* and `style: false`, and every drawing takes its colours from it. Needs `styleScope`; `theme`, `look`, `background`, `layout`, `headers`, and whether there is an `href` or a `highlightPath` decide which rules are in it.
|
|
162
|
+
*/
|
|
163
|
+
export function bracketStyle(options) {
|
|
164
|
+
if (options.styleScope === undefined)
|
|
165
|
+
throw new RangeError("taisen-brackets: bracketStyle needs a styleScope");
|
|
166
|
+
const scope = scopeOf(options, "");
|
|
167
|
+
const theme = options.theme ?? "auto";
|
|
168
|
+
if (theme === "none")
|
|
169
|
+
return "";
|
|
170
|
+
const picture = resolveBackground(options.background, theme, 100, 100, "x");
|
|
171
|
+
return styleFor(`.${scope}`, theme, { road: options.look === "road", links: typeof options.href === "function", follow: options.highlightPath !== undefined, bgAuto: picture?.auto, layout: (options.layout ?? "tree") !== "tree" || options.headers === true }, picture?.inks);
|
|
172
|
+
}
|
|
173
|
+
/** The groups' names with the east first, for the series card: the names the teams carry, ordered so that a name with "east" comes before one with "west". */
|
|
174
|
+
function eastFirst(input) {
|
|
175
|
+
const teams = Array.isArray(input) ? [] : input.teams ?? [];
|
|
176
|
+
const names = [...new Set(teams.map((t) => t.group).filter((g) => typeof g === "string"))];
|
|
177
|
+
if (names.length < 2)
|
|
178
|
+
return undefined;
|
|
179
|
+
const rank = (g) => (/east/i.test(g) ? 0 : /west/i.test(g) ? 2 : 1);
|
|
180
|
+
return names.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : 1));
|
|
181
|
+
}
|
|
90
182
|
// ---------------------------------------------------------------------------------------------- the plan
|
|
91
|
-
function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
183
|
+
function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
184
|
+
// The layouts that draw each tie their own way take the tree's places and lines, and fill the boxes themselves.
|
|
185
|
+
const V = VARIANTS.includes(kind) ? kind : null;
|
|
92
186
|
const pad = vertical ? 14 : 22;
|
|
93
187
|
const showLabels = o.roundLabels ?? true;
|
|
94
188
|
const poster = o.theme === "poster";
|
|
@@ -96,9 +190,15 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
96
190
|
const links = [];
|
|
97
191
|
const texts = [];
|
|
98
192
|
const extras = [];
|
|
193
|
+
// The connector lines as they are drawn, so that the names of the rounds can be placed clear of them.
|
|
194
|
+
const segs = [];
|
|
195
|
+
const addLine = (...pts) => pts.slice(1).forEach(([x, y], i) => segs.push({ x0: pts[i][0], y0: pts[i][1], x1: x, y1: y }));
|
|
196
|
+
const clear = (lo, hi) => (w, pref, y) => freeX(segs, y - 16, y + 7, w, pref, lo, hi);
|
|
197
|
+
const finish = (plan) => ({ ...plan, texts: plan.texts.map((t) => (typeof t === "function" ? t() : t)) });
|
|
99
198
|
const W0 = size.width;
|
|
100
199
|
const titleH = (o.title ? (poster ? 46 : 38) : 0) + (o.subtitle ? 24 : 0) + (o.title || o.subtitle ? 10 : 0);
|
|
101
200
|
const hasGroups = layout.sides.some((s) => s.label);
|
|
201
|
+
const groupRoom = o.headers === true ? 34 : 28;
|
|
102
202
|
const champ = (o.champion ?? true) && layout.champion !== null && layout.finalIndex !== null;
|
|
103
203
|
const withThird = layout.third !== null && (o.thirdPlace ?? "below") !== "hide";
|
|
104
204
|
// Beside the final in a horizontal bracket of two halves, under the final; elsewhere the lines into the final would run through it, so it goes at the foot.
|
|
@@ -121,6 +221,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
121
221
|
let others = [];
|
|
122
222
|
let ow = 0;
|
|
123
223
|
let extraH0 = 0;
|
|
224
|
+
let labelRoom = showLabels ? 24 : 6;
|
|
124
225
|
if (!vertical) {
|
|
125
226
|
const availW = W0 - 2 * pad;
|
|
126
227
|
const g = 0.2;
|
|
@@ -131,7 +232,10 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
131
232
|
D = clamp(d, 40, 220);
|
|
132
233
|
gapT = D * g;
|
|
133
234
|
centreT = Math.max(centreMin, D * 1.2);
|
|
134
|
-
|
|
235
|
+
// The names of the rounds head their columns: a name that cannot be one line in its column is set on two, which needs a taller strip over the boxes.
|
|
236
|
+
if (showLabels && layout.columns.some((c) => !nameFits(v, road, roundLabel(layout, c, o.roundNames, v), D + gapT * 0.8)))
|
|
237
|
+
labelRoom = 34;
|
|
238
|
+
const top = pad + titleH + (hasGroups ? groupRoom : 0) + labelRoom;
|
|
135
239
|
others = othersOf(layout);
|
|
136
240
|
ow = clamp(D * 1.1, 120, 200);
|
|
137
241
|
extraH0 = othersHeight(others.length, W0, pad, ow, OTHER_H) + thirdBlock;
|
|
@@ -151,8 +255,13 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
151
255
|
boxW = S;
|
|
152
256
|
}
|
|
153
257
|
// Full boxes (names) only where the longest name fits beside its mark and a score.
|
|
154
|
-
const
|
|
155
|
-
const
|
|
258
|
+
const wide = boldWidth;
|
|
259
|
+
const longest = Math.max(0, ...layout.teams.map((t) => wide(t.name, 13)));
|
|
260
|
+
const longestAbbr = Math.max(0, ...layout.teams.map((t) => wide(abbrOf(t, v), 13)));
|
|
261
|
+
// Seed tags (the road look with `seeds`): a small pill beside each mark, or above it in a box of marks alone.
|
|
262
|
+
const seedsDefault = V === "rows" || V === "update" || V === "series" || V === "columns";
|
|
263
|
+
const tagsOn = (road || (V !== null && V !== "columns")) && (o.seeds ?? seedsDefault) && layout.ties.some((t) => tagOf(t, 0) !== "" || tagOf(t, 1) !== "");
|
|
264
|
+
const tagW = tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 8))) + 8 : 0;
|
|
156
265
|
// 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
266
|
const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
|
|
158
267
|
// The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
|
|
@@ -162,9 +271,9 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
162
271
|
const { font, md, score } = metrics(rowH, road);
|
|
163
272
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
164
273
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
165
|
-
const extra = (anyBye ?
|
|
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) +
|
|
274
|
+
const extra = (anyBye ? wide(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? wide("(00)", Math.max(score * 0.55, 8)) + 5 : 0);
|
|
275
|
+
const fixed = clamp(rowH * 0.16, 5, 12) + clamp(rowH * 0.18, 5, 10) + 8 + 10 + (tagW > 0 ? tagW + 3 : 0);
|
|
276
|
+
const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide("00", score) + extra;
|
|
168
277
|
return Math.min(1, (w - fixed) / varying);
|
|
169
278
|
};
|
|
170
279
|
let fitNow = 1;
|
|
@@ -174,14 +283,17 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
174
283
|
// Room for the score, and for the words some boxes carry beside it: a bye, a shoot-out's score.
|
|
175
284
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
176
285
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
177
|
-
const extra = (anyBye ?
|
|
178
|
-
const needed = 6 + md + 5 + longest * (font / 13) + 8 +
|
|
286
|
+
const extra = (anyBye ? boldWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? boldWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
|
|
287
|
+
const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth("00", score) + extra + 14;
|
|
179
288
|
const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
|
|
180
289
|
return named === "mark" && o.markShape === "none" ? "compact" : named;
|
|
181
290
|
};
|
|
182
291
|
// 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
292
|
// else the abbreviation (compact) where rows of 18 or more fit; else only the marks.
|
|
184
|
-
const
|
|
293
|
+
const codesOn = road && (o.codes ?? false);
|
|
294
|
+
const markCols = road && (markScores || tagsOn || codesOn || o.vs === true);
|
|
295
|
+
const anyPensAll = layout.ties.some((t) => t.result.pens !== null);
|
|
296
|
+
const roadPick = (w, hMax, grow = false) => {
|
|
185
297
|
const search = (named, hMin) => {
|
|
186
298
|
let h = hMax;
|
|
187
299
|
while (h > hMin && fitOf(w, h, named) < 0.94)
|
|
@@ -198,15 +310,38 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
198
310
|
const compact = search("compact", 36);
|
|
199
311
|
chosen = full.ok ? full : compact.ok ? compact : o.markShape === "none" ? compact : null;
|
|
200
312
|
}
|
|
201
|
-
|
|
202
|
-
|
|
313
|
+
// A box of marks alone is as high as its marks, tags, codes and scores need; on a tall page it may be higher than the usual box, in a row it may not.
|
|
314
|
+
const m = Math.min((w - 12) / 2 - 3, 56);
|
|
315
|
+
const sf = clamp(m * 0.5, 9, 24);
|
|
316
|
+
const need = 10 + m + 3 + (markScores ? sf * 1.15 + (anyPensAll && m < 40 ? sf * 0.7 : 0) : 0) + (codesOn ? clamp(m * 0.24, 7, 11) * 1.25 : 0) + (tagsOn ? 12 : 0);
|
|
317
|
+
chosen ??= { detail: o.markShape === "none" ? "compact" : "mark", h: markCols ? Math.min(grow ? Math.max(hMax, need) : hMax, need) : Math.min(hMax, 2 * Math.min(w - 8, 60) + 8) };
|
|
203
318
|
if (chosen.detail !== "mark")
|
|
204
319
|
fitNow = Math.min(1, fitOf(w, chosen.h, chosen.detail));
|
|
205
320
|
return chosen;
|
|
206
321
|
};
|
|
322
|
+
// The layouts of their own: how much a box says follows from its width and height; a tall page gives each a height of its own.
|
|
323
|
+
const variantDetail = (w) => {
|
|
324
|
+
if (o.detail && o.detail !== "auto")
|
|
325
|
+
return o.detail;
|
|
326
|
+
if (V === "shields")
|
|
327
|
+
return "mark";
|
|
328
|
+
if (V === "update" || V === "series")
|
|
329
|
+
return w >= 56 ? "compact" : "mark";
|
|
330
|
+
// The box's own painter leaves out what does not fit (the tag, then the names' length); the names are asked for where there is room for most of them.
|
|
331
|
+
if (V === "columns")
|
|
332
|
+
return w >= 110 ? "full" : w >= 60 ? "compact" : "mark";
|
|
333
|
+
return w >= 150 ? "full" : w >= 84 ? "compact" : "mark";
|
|
334
|
+
};
|
|
335
|
+
const oneLine = V === "series" && vertical && layout.ties.every((t) => seriesLineWidth(t, v, 10, o.scores ?? true) <= boxW - 8);
|
|
336
|
+
const variantH = V === "rows" ? 54 : V === "shields" ? 76 : V === "update" ? 46 : V === "columns" ? 84 : oneLine ? 32 : 46;
|
|
207
337
|
let detail;
|
|
208
|
-
if (
|
|
209
|
-
|
|
338
|
+
if (V !== null) {
|
|
339
|
+
detail = variantDetail(boxW);
|
|
340
|
+
if (vertical)
|
|
341
|
+
boxH = variantH;
|
|
342
|
+
}
|
|
343
|
+
else if (road) {
|
|
344
|
+
const picked = roadPick(boxW, vertical ? clamp(boxW * 0.42, 48, 124) : boxH, vertical);
|
|
210
345
|
detail = picked.detail;
|
|
211
346
|
if (vertical)
|
|
212
347
|
boxH = picked.h;
|
|
@@ -220,9 +355,10 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
220
355
|
if (vertical)
|
|
221
356
|
boxH = detail === "full" ? 50 : detail === "compact" ? 42 : boxW >= 76 ? 38 : 2 * Math.min(boxW - 8, 24) + 8;
|
|
222
357
|
}
|
|
358
|
+
// The names of the rounds stand on their own strip between the rounds, under the lines that join them: 6 for the bus, 20 for the pill, and room round both.
|
|
223
359
|
if (vertical) {
|
|
224
360
|
D = boxH;
|
|
225
|
-
gapT = showLabels ?
|
|
361
|
+
gapT = showLabels ? 32 : 16;
|
|
226
362
|
}
|
|
227
363
|
// The champion's block: a trophy, a large mark, the name and the word; and the third-place match below the final.
|
|
228
364
|
const champMark = vertical ? (road ? clamp(size.width * 0.14, 54, 96) : 54) : clamp(centreT * (road ? 0.4 : 0.34), 44, road ? 96 : 84);
|
|
@@ -231,7 +367,53 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
231
367
|
const champH = champ ? trophyH + (hasTrophy ? 6 : 0) + champMark + 8 + nameFont + 6 + 11 + 2 : 0;
|
|
232
368
|
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;
|
|
233
369
|
let finalH = vertical ? (detail === "mark" ? Math.max(boxH, 52) : boxH) : boxH;
|
|
370
|
+
// The final of the columns layout is two tall slots side by side.
|
|
371
|
+
if (V === "columns")
|
|
372
|
+
finalH = vertical ? Math.max(boxH, 96) : Math.min(Math.max(boxH * 1.7, 120), 220);
|
|
234
373
|
const thirdH = withThird && !bottomThird ? 18 + boxH + 14 : 0;
|
|
374
|
+
// A final alone (the World Cups of 1974 and 1978 ended with one and a third-place match): there is no tree to join, so it is centred, the champion over it and the third place under it.
|
|
375
|
+
const lone = !halves && layout.sides.length === 1 && layout.sides[0].boxes.length === 1 && layout.finalIndex !== null && layout.sides[0].boxes[0].i === layout.finalIndex;
|
|
376
|
+
if (lone) {
|
|
377
|
+
const availW = W0 - 2 * pad;
|
|
378
|
+
const wl = vertical ? Math.min(availW, road ? 340 : 300) : clamp(availW * 0.3, 240, 380);
|
|
379
|
+
let hl = V === "columns" ? 120 : V === "shields" ? 86 : V !== null ? 72 : road ? 0 : 92;
|
|
380
|
+
let dl;
|
|
381
|
+
if (V !== null)
|
|
382
|
+
dl = variantDetail(wl);
|
|
383
|
+
else if (road) {
|
|
384
|
+
const picked = roadPick(wl, 132);
|
|
385
|
+
dl = picked.detail;
|
|
386
|
+
hl = picked.h;
|
|
387
|
+
}
|
|
388
|
+
else
|
|
389
|
+
dl = pick(wl, hl);
|
|
390
|
+
const strays = othersOf(layout);
|
|
391
|
+
const ows = Math.min(availW, 200);
|
|
392
|
+
const strayH = othersHeight(strays.length, W0, pad, ows, OTHER_H);
|
|
393
|
+
const labelH = showLabels ? 22 : 8;
|
|
394
|
+
const content = (champ ? champH + 14 : 0) + labelH + hl + (withThird ? 8 + 18 + hl + 8 : 0) + strayH;
|
|
395
|
+
const top0 = pad + titleH;
|
|
396
|
+
const height = Math.max(size.height ?? 0, top0 + content + pad);
|
|
397
|
+
const cx = W0 / 2;
|
|
398
|
+
let y = top0 + Math.max(6, (height - top0 - pad - content) / 2);
|
|
399
|
+
if (champ) {
|
|
400
|
+
extras.push(...championBlock(layout.champion, cx, y, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
401
|
+
y += champH + 14;
|
|
402
|
+
}
|
|
403
|
+
if (showLabels)
|
|
404
|
+
texts.push(label(v, road, roundLabel(layout, layout.rounds, o.roundNames, v), cx, y + 12, "middle"));
|
|
405
|
+
y += labelH;
|
|
406
|
+
boxes.push({ rect: { x: cx - wl / 2, y, w: wl, h: hl }, tie: layout.ties[layout.finalIndex], order: layout.sides[0].boxes[0].order, bye: null, byes: [], final: true, champion: champ });
|
|
407
|
+
y += hl;
|
|
408
|
+
if (withThird) {
|
|
409
|
+
thirdAtFoot(layout, y + 8, cx, wl, hl, v, road, boxes, texts);
|
|
410
|
+
y += 8 + 18 + hl + 8;
|
|
411
|
+
}
|
|
412
|
+
if (strays.length > 0)
|
|
413
|
+
othersBlock(layout, strays, y + 6, W0, pad, ows, OTHER_H, v, road, boxes, texts);
|
|
414
|
+
texts.push(...headings(v, o, W0, pad, poster));
|
|
415
|
+
return finish({ width: W0, height, boxes, links: [], texts, extras, detail: dl, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
|
|
416
|
+
}
|
|
235
417
|
// ----- the slot positions along t
|
|
236
418
|
if (!vertical) {
|
|
237
419
|
const sizes = [];
|
|
@@ -243,7 +425,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
243
425
|
sizes.forEach((sz) => { starts.push(t); t += sz + gapT; });
|
|
244
426
|
const width = Math.max(W0, total + 2 * pad);
|
|
245
427
|
// ----- span positions
|
|
246
|
-
const top = pad + titleH + (hasGroups ?
|
|
428
|
+
const top = pad + titleH + (hasGroups ? groupRoom : 0) + labelRoom;
|
|
247
429
|
const treeH = rows * rowStep;
|
|
248
430
|
// The final sits at the middle of the rows; the champion above it and the third place below need room.
|
|
249
431
|
const upNeed = (champ ? champH + 8 : 0) + 22 + finalH / 2;
|
|
@@ -294,6 +476,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
294
476
|
extras.push(...championBlock(layout.champion, cx, fy - champH / 2, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
295
477
|
}
|
|
296
478
|
}
|
|
479
|
+
if (V === "columns")
|
|
480
|
+
growColumns(layout, rects, false, S, treeH, top, height - pad - extraH0);
|
|
297
481
|
if (showLabels) {
|
|
298
482
|
for (const c of layout.columns) {
|
|
299
483
|
const name = roundLabel(layout, c, o.roundNames, v);
|
|
@@ -310,13 +494,17 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
310
494
|
return;
|
|
311
495
|
const ks = layout.columns.map((_, ci) => slotOfColumn(sd.key, ci));
|
|
312
496
|
const x0 = Math.min(...ks.map((k) => starts[k])), x1 = Math.max(...ks.map((k) => starts[k] + D));
|
|
313
|
-
|
|
497
|
+
const labelY = top - (showLabels ? labelRoom + 12 : 14);
|
|
498
|
+
if (o.headers === true)
|
|
499
|
+
texts.push(headTile(poster ? v.upper(sd.label) : sd.label, (x0 + x1) / 2, x1 - x0, labelY, sd.key === "l" ? 0 : 1, layout.sides.length === 2));
|
|
500
|
+
else
|
|
501
|
+
texts.push(`<text class="tb-half" x="${num((x0 + x1) / 2)}" y="${num(labelY)}" font-size="${poster ? 16 : 15}" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(poster ? v.upper(sd.label) : sd.label)}</text>`);
|
|
314
502
|
});
|
|
315
503
|
}
|
|
316
504
|
// lines
|
|
317
505
|
for (const t of layout.ties) {
|
|
318
506
|
const p = layout.parent[t.index];
|
|
319
|
-
if (p === -1)
|
|
507
|
+
if (p === -1 || V === "shields")
|
|
320
508
|
continue;
|
|
321
509
|
const a = rects.get(t.index), b = rects.get(p);
|
|
322
510
|
if (!a || !b || layout.place[t.index] === "o")
|
|
@@ -325,16 +513,17 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
325
513
|
const x0 = goingRight ? a.x + a.w : a.x, x1 = goingRight ? b.x : b.x + b.w;
|
|
326
514
|
const y0 = a.y + a.h / 2, y1 = b.y + b.h / 2;
|
|
327
515
|
const xm = (x0 + x1) / 2;
|
|
516
|
+
addLine([x0, y0], [xm, y0], [xm, y1], [x1, y1]);
|
|
328
517
|
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)}/>`);
|
|
329
518
|
}
|
|
330
519
|
if (bottomThird)
|
|
331
520
|
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
|
|
332
521
|
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, road, boxes, texts);
|
|
333
522
|
texts.push(...headings(v, o, width, pad, poster));
|
|
334
|
-
return { width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores };
|
|
523
|
+
return finish({ width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
|
|
335
524
|
}
|
|
336
525
|
// ----- vertical: the rounds are rows. The left half is the top; the final is a band in the middle.
|
|
337
|
-
const groupBand = hasGroups ? 26 : 0;
|
|
526
|
+
const groupBand = hasGroups ? (o.headers === true ? 34 : 26) : 0;
|
|
338
527
|
// 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.
|
|
339
528
|
const champTop = halves && champ ? champH + 14 : 0;
|
|
340
529
|
const topT = pad + titleH + champTop + groupBand + (showLabels ? 14 : 0);
|
|
@@ -345,14 +534,14 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
345
534
|
const othersV = othersHeight(others.length, W0, pad, ow, OTHER_H);
|
|
346
535
|
// 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
536
|
const needOf = (h, gap) => {
|
|
348
|
-
const finalHere = detail === "mark" ? Math.max(h, 52) : h;
|
|
537
|
+
const finalHere = V === "columns" ? Math.max(h, finalH) : detail === "mark" ? Math.max(h, 52) : h;
|
|
349
538
|
const centre = (showLabels ? 32 : 0) + 6 + finalHere + 14;
|
|
350
539
|
const thirdV = bottomThird ? 18 + (road ? h : 52) + 16 : 0;
|
|
351
540
|
const band = h + gap;
|
|
352
541
|
return (halves ? topT + bandsPerSide * band + centre + bandsPerSide * band + groupBand + pad : topT + (nc + (champ ? 1 : 0)) * band + (champ ? champH : 0) + pad) + othersV + thirdV;
|
|
353
542
|
};
|
|
354
543
|
// 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) {
|
|
544
|
+
if (road && size.height !== null && V === null) {
|
|
356
545
|
while (boxH > 40 && needOf(boxH, gapT) > size.height)
|
|
357
546
|
boxH -= 2;
|
|
358
547
|
const slack = size.height - needOf(boxH, gapT);
|
|
@@ -361,6 +550,21 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
361
550
|
D = boxH;
|
|
362
551
|
finalH = detail === "mark" ? Math.max(boxH, 52) : boxH;
|
|
363
552
|
}
|
|
553
|
+
// The layouts of their own fill a tall page too: the boxes are made taller, and the rounds are spaced apart, until the page is as high as it was asked to be.
|
|
554
|
+
if (V !== null && size.height !== null && vertical) {
|
|
555
|
+
const cap = V === "series" ? (oneLine ? 52 : 60) : V === "columns" ? 150 : V === "shields" ? 110 : V === "rows" ? 96 : 80;
|
|
556
|
+
while (boxH < cap && needOf(boxH + 2, gapT) <= size.height) {
|
|
557
|
+
boxH += 2;
|
|
558
|
+
if (V !== "columns")
|
|
559
|
+
finalH = boxH;
|
|
560
|
+
}
|
|
561
|
+
D = boxH;
|
|
562
|
+
const slack = size.height - needOf(boxH, gapT);
|
|
563
|
+
if (slack > 0 && bandCount > 0)
|
|
564
|
+
gapT += Math.min(slack / bandCount, 56);
|
|
565
|
+
if (V !== "columns")
|
|
566
|
+
finalH = boxH;
|
|
567
|
+
}
|
|
364
568
|
const centreBand = (showLabels ? 32 : 0) + 6 + finalH + 14;
|
|
365
569
|
const thirdH2 = road ? boxH : 52;
|
|
366
570
|
const thirdBlockV = bottomThird ? 18 + thirdH2 + 16 : 0;
|
|
@@ -394,7 +598,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
394
598
|
if (champ)
|
|
395
599
|
extras.push(...championBlock(layout.champion, midX, pad + titleH + 4, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
396
600
|
if (showLabels) {
|
|
397
|
-
|
|
601
|
+
const at = y + 12;
|
|
602
|
+
texts.push(() => label(v, road, roundLabel(layout, layout.rounds, o.roundNames, v), midX, at, "middle", 1e9, clear(pad - 4, W0 - pad + 4)));
|
|
398
603
|
y += 16;
|
|
399
604
|
}
|
|
400
605
|
const rect = { x: midX - finalW / 2, y: y + 6, w: finalW, h: finalH };
|
|
@@ -405,12 +610,30 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
405
610
|
const fr = rects.get(layout.finalIndex);
|
|
406
611
|
extras.push(...championBlock(layout.champion, fr.x + fr.w / 2, fr.y + fr.h + 12, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
407
612
|
}
|
|
613
|
+
if (V === "columns")
|
|
614
|
+
growColumns(layout, rects, true, S, treeW, pad, W0 - pad);
|
|
408
615
|
if (showLabels) {
|
|
409
616
|
layout.columns.forEach((c, ci) => {
|
|
410
617
|
const name = roundLabel(layout, c, o.roundNames, v);
|
|
411
618
|
const sides = halves ? (layout.sides.length > 1 ? ["l", "r"] : ["l"]) : ["l"];
|
|
412
|
-
for (const sd of sides)
|
|
413
|
-
|
|
619
|
+
for (const sd of sides) {
|
|
620
|
+
const at = bandStart(sd, ci) - 8;
|
|
621
|
+
texts.push(() => {
|
|
622
|
+
if (road)
|
|
623
|
+
return label(v, road, name, pad, at, "start", 1e9, clear(pad - 4, W0 - pad + 4));
|
|
624
|
+
const shown = v.upper(name);
|
|
625
|
+
let size = 11, left = pad - 4;
|
|
626
|
+
for (const s of [11, 9.5, 8.5]) {
|
|
627
|
+
size = s;
|
|
628
|
+
const found = freeX(segs, at - 16, at + 7, boldWidth(shown, s) * 1.06 + 8, pad - 4, pad - 4, W0 - pad + 4);
|
|
629
|
+
if (found !== null) {
|
|
630
|
+
left = found;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return `<text class="tb-round tb-halo" x="${num(left + 4)}" y="${num(at)}" font-size="${size}" font-weight="600" letter-spacing=".08em">${esc(shown)}</text>`;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
414
637
|
});
|
|
415
638
|
}
|
|
416
639
|
if (hasGroups) {
|
|
@@ -418,12 +641,15 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
418
641
|
if (!sd.label)
|
|
419
642
|
return;
|
|
420
643
|
const y = sd.key === "l" ? pad + titleH + champTop + 16 : totalT - pad - 2;
|
|
421
|
-
|
|
644
|
+
if (o.headers === true)
|
|
645
|
+
texts.push(headTile(v.upper(sd.label), midX, W0 - 2 * pad, y, sd.key === "l" ? 0 : 1, layout.sides.length === 2));
|
|
646
|
+
else
|
|
647
|
+
texts.push(`<text class="tb-half" x="${num(midX)}" y="${num(y)}" font-size="14" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(v.upper(sd.label))}</text>`);
|
|
422
648
|
});
|
|
423
649
|
}
|
|
424
650
|
for (const t of layout.ties) {
|
|
425
651
|
const p = layout.parent[t.index];
|
|
426
|
-
if (p === -1)
|
|
652
|
+
if (p === -1 || V === "shields")
|
|
427
653
|
continue;
|
|
428
654
|
const a = rects.get(t.index), b = rects.get(p);
|
|
429
655
|
if (!a || !b || layout.place[t.index] === "o")
|
|
@@ -431,14 +657,48 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
431
657
|
const down = b.y > a.y;
|
|
432
658
|
const y0 = down ? a.y + a.h : a.y, y1 = down ? b.y : b.y + b.h;
|
|
433
659
|
const x0c = a.x + a.w / 2, x1c = b.x + b.w / 2;
|
|
434
|
-
|
|
660
|
+
// With the rounds named, the line turns just outside the box it leaves (going down) or just outside the box it enters (going up), so that the strip in front of the later round is free for its name.
|
|
661
|
+
const ym = showLabels ? (down ? y0 + 6 : y1 + 6) : down ? y0 + gapT * 0.4 : y0 - gapT * 0.4;
|
|
662
|
+
addLine([x0c, y0], [x0c, ym], [x1c, ym], [x1c, y1]);
|
|
435
663
|
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)}/>`);
|
|
436
664
|
}
|
|
437
665
|
if (bottomThird)
|
|
438
666
|
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
|
|
439
667
|
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OTHER_H, v, road, boxes, texts);
|
|
440
668
|
texts.push(...headings(v, o, W0, pad, poster));
|
|
441
|
-
return { width: W0, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores };
|
|
669
|
+
return finish({ width: W0, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn, variant: V, em: { full: longest / 13, abbr: longestAbbr / 13, tag: tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 1))) : 0 } });
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* The columns layout's tall slots: a tie of a later round is made taller than the first round's, as tall as the room between its neighbours of the same column leaves (`base` times 1.55 for each round in, at most),
|
|
673
|
+
* on the same centre. `span` is the room across the tree, for a column with a single tie.
|
|
674
|
+
*/
|
|
675
|
+
function growColumns(layout, rects, vertical, base, span, lo, hi) {
|
|
676
|
+
const centre = (r) => (vertical ? r.x + r.w / 2 : r.y + r.h / 2);
|
|
677
|
+
for (const sd of layout.sides) {
|
|
678
|
+
layout.columns.forEach((col, ci) => {
|
|
679
|
+
const here = sd.boxes.filter((b) => b.round === col).map((b) => rects.get(b.i)).filter((r) => r !== undefined);
|
|
680
|
+
if (here.length === 0)
|
|
681
|
+
return;
|
|
682
|
+
const at = here.map(centre).sort((a, b) => a - b);
|
|
683
|
+
let gap = Infinity;
|
|
684
|
+
for (let i = 1; i < at.length; i++)
|
|
685
|
+
gap = Math.min(gap, at[i] - at[i - 1]);
|
|
686
|
+
const want = Math.max(base, Math.min(base * 1.55 ** ci, gap === Infinity ? span * 0.6 : gap - 8));
|
|
687
|
+
for (const r of here) {
|
|
688
|
+
const c = centre(r);
|
|
689
|
+
// Never past the room there is on either side of the centre.
|
|
690
|
+
const size = Math.max(Math.min(base, 2 * (c - lo), 2 * (hi - c)), Math.min(want, 2 * (c - lo), 2 * (hi - c)));
|
|
691
|
+
if (vertical) {
|
|
692
|
+
r.x = c - size / 2;
|
|
693
|
+
r.w = size;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
r.y = c - size / 2;
|
|
697
|
+
r.h = size;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
442
702
|
}
|
|
443
703
|
/** A line's class: `tb-path` when both ties it joins are on the highlighted team's path. */
|
|
444
704
|
function lineClass(path, from, to) {
|
|
@@ -453,18 +713,108 @@ const centreMinLtr = (d) => d * 1.1;
|
|
|
453
713
|
function lineAttrs(o) {
|
|
454
714
|
return o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".5"' : "";
|
|
455
715
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
716
|
+
/** The two lines a name is best broken into at a space (the widest line as narrow as it can be), or `null` for a name of one word. */
|
|
717
|
+
function twoLines(text, size, measure) {
|
|
718
|
+
const words = text.split(" ");
|
|
719
|
+
let best = null;
|
|
720
|
+
for (let i = 1; i < words.length; i += 1) {
|
|
721
|
+
const pair = [words.slice(0, i).join(" "), words.slice(i).join(" ")];
|
|
722
|
+
if (best === null || Math.max(measure(pair[0], size), measure(pair[1], size)) < Math.max(measure(best[0], size), measure(best[1], size)))
|
|
723
|
+
best = pair;
|
|
724
|
+
}
|
|
725
|
+
return best;
|
|
726
|
+
}
|
|
727
|
+
/** The bold, letter-spaced width of a round's name (as a pill or a heading sets it). */
|
|
728
|
+
const nameWidth = (t, size) => boldWidth(t, size) * 1.06;
|
|
729
|
+
/**
|
|
730
|
+
* How a round's name is set within `maxWidth` (the column it heads): on one line at 11, smaller down to 9 (the pill's own room is 9 either side, or 4 where that is all there is), or, where it still does not
|
|
731
|
+
* fit, on two lines at 9, 8.5 and 8: a name is never cut while a smaller or a second line will hold it. The road look's pill and the classic look's heading take the same rule.
|
|
732
|
+
*/
|
|
733
|
+
function nameFits(v, road, text, maxWidth) {
|
|
734
|
+
const upper = v.upper(text);
|
|
735
|
+
return nameWidth(upper, 9) + (road ? 8 : 0) <= maxWidth;
|
|
736
|
+
}
|
|
737
|
+
function label(v, road, text, x, y, anchor, maxWidth = 1e9, place) {
|
|
460
738
|
const upper = v.upper(text);
|
|
739
|
+
if (!road) {
|
|
740
|
+
// A column's heading: 11 down to 9.5 on one line, then two lines at 9.5, 9 and 8.5; the last resort cuts it.
|
|
741
|
+
if (maxWidth < 1e9 && nameWidth(upper, 11) > maxWidth) {
|
|
742
|
+
let size = 11;
|
|
743
|
+
while (size > 9.5 && nameWidth(upper, size) > maxWidth)
|
|
744
|
+
size -= 0.5;
|
|
745
|
+
if (nameWidth(upper, size) <= maxWidth)
|
|
746
|
+
return `<text class="tb-round" x="${num(x)}" y="${num(y)}" font-size="${size}" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(upper)}</text>`;
|
|
747
|
+
for (const s of [9.5, 9, 8.5]) {
|
|
748
|
+
const pair = twoLines(upper, s, nameWidth);
|
|
749
|
+
if (pair && Math.max(nameWidth(pair[0], s), nameWidth(pair[1], s)) <= maxWidth)
|
|
750
|
+
return pair.map((line, i) => `<text class="tb-round" x="${num(x)}" y="${num(y - (1 - i) * 10)}" font-size="${s}" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(line)}</text>`).join("");
|
|
751
|
+
}
|
|
752
|
+
return `<text class="tb-round" x="${num(x)}" y="${num(y)}" font-size="8.5" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(fit(upper, 8.5, maxWidth, nameWidth))}</text>`;
|
|
753
|
+
}
|
|
754
|
+
const shown = upper;
|
|
755
|
+
const plain = `<text class="tb-round" x="${num(x)}" y="${num(y)}" font-size="11" font-weight="600" letter-spacing=".08em" text-anchor="${anchor}">${esc(shown)}</text>`;
|
|
756
|
+
if (!place)
|
|
757
|
+
return plain;
|
|
758
|
+
// The name is kept clear of the connector lines: where it cannot stand where it was put, it moves along its row.
|
|
759
|
+
for (const size of [11, 9.5, 8.5]) {
|
|
760
|
+
const w = nameWidth(shown, size) + 8;
|
|
761
|
+
const pref = anchor === "middle" ? x - w / 2 : x - 4;
|
|
762
|
+
const left = place(w, pref, y);
|
|
763
|
+
if (left === null)
|
|
764
|
+
continue;
|
|
765
|
+
return left === pref && size === 11 ? plain : `<text class="tb-round" x="${num(left + 4)}" y="${num(y)}" font-size="${size}" font-weight="600" letter-spacing=".08em" text-anchor="start">${esc(shown)}</text>`;
|
|
766
|
+
}
|
|
767
|
+
return plain;
|
|
768
|
+
}
|
|
769
|
+
// The road look names a round on a pill. Down to 9 on one line (with 9 either side), then a snugger pill (4 either side), then two lines at 9, 8.5 and 8.
|
|
461
770
|
let s = 11;
|
|
462
|
-
while (s > 9 &&
|
|
771
|
+
while (s > 9 && nameWidth(upper, s) > maxWidth - 18)
|
|
463
772
|
s -= 0.5;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
773
|
+
let rows = [upper];
|
|
774
|
+
let side = 9;
|
|
775
|
+
if (nameWidth(upper, s) > maxWidth - 18) {
|
|
776
|
+
if (nameWidth(upper, s) <= maxWidth - 8)
|
|
777
|
+
side = 4;
|
|
778
|
+
else {
|
|
779
|
+
rows = [fit(upper, s, maxWidth - 12, nameWidth)];
|
|
780
|
+
for (const size of [9, 8.5, 8]) {
|
|
781
|
+
const pair = twoLines(upper, size, nameWidth);
|
|
782
|
+
if (pair && Math.max(nameWidth(pair[0], size), nameWidth(pair[1], size)) <= maxWidth - 12) {
|
|
783
|
+
rows = pair;
|
|
784
|
+
s = size;
|
|
785
|
+
side = 6;
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
if (pair && size === 8) {
|
|
789
|
+
rows = pair.map((line) => fit(line, 8, maxWidth - 12, nameWidth));
|
|
790
|
+
s = 8;
|
|
791
|
+
side = 6;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const widest = Math.max(...rows.map((r) => nameWidth(r, s)));
|
|
797
|
+
let w = widest + 2 * side;
|
|
798
|
+
let x0 = anchor === "middle" ? x - w / 2 : x;
|
|
799
|
+
let fs = s;
|
|
800
|
+
if (place && rows.length === 1) {
|
|
801
|
+
// Where the lines leave no room, the name is set smaller (to 8) and the pill made snugger (side 6, then 4).
|
|
802
|
+
search: for (const size of s > 8 ? [s, 8.5, 8].filter((z) => z <= s) : [s]) {
|
|
803
|
+
for (const sd of [9, 6, 4]) {
|
|
804
|
+
w = nameWidth(rows[0], size) + 2 * sd;
|
|
805
|
+
const pref = anchor === "middle" ? x - w / 2 : x;
|
|
806
|
+
const found = place(w, pref, y);
|
|
807
|
+
x0 = found ?? pref;
|
|
808
|
+
fs = size;
|
|
809
|
+
if (found !== null)
|
|
810
|
+
break search;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const put = (line, by) => `<text class="tb-lab" x="${num(x0 + w / 2)}" y="${num(by)}" font-size="${num(fs)}" font-weight="700" letter-spacing=".08em" text-anchor="middle">${esc(line)}</text>`;
|
|
815
|
+
if (rows.length === 2)
|
|
816
|
+
return `<rect class="tb-panel" x="${num(x0)}" y="${num(y - 23.5)}" width="${num(w)}" height="29" rx="12"/>${put(rows[0], y - 12)}${put(rows[1], y - 1.5)}`;
|
|
817
|
+
return `<rect class="tb-panel" x="${num(x0)}" y="${num(y - 14.5)}" width="${num(w)}" height="20" rx="10"/>${put(rows[0], y)}`;
|
|
468
818
|
}
|
|
469
819
|
function headings(v, o, width, pad, poster) {
|
|
470
820
|
const out = [];
|
|
@@ -505,7 +855,7 @@ function championBlock(team, cx, top, mark, trophyH, nameFont, o, v, ctx, poster
|
|
|
505
855
|
}
|
|
506
856
|
/** The ties that are not part of the tree (they lead nowhere near the final, or are round-robin games), drawn in a grid below it. */
|
|
507
857
|
function othersOf(layout) {
|
|
508
|
-
return [...layout.other, ...layout.robin];
|
|
858
|
+
return [...layout.other, ...layout.robin, ...layout.playIn];
|
|
509
859
|
}
|
|
510
860
|
function othersHeight(count, width, pad, ow, oh) {
|
|
511
861
|
if (!count)
|
|
@@ -536,7 +886,7 @@ function hrefOf(o, tie, side) {
|
|
|
536
886
|
const url = side === undefined ? o.href(tie) : o.href(tie, side);
|
|
537
887
|
return typeof url === "string" && safeHref(url) ? url.trim() : null;
|
|
538
888
|
}
|
|
539
|
-
function drawBox(p, plan, o, v, ctx, linked) {
|
|
889
|
+
function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
540
890
|
const { rect, tie } = p;
|
|
541
891
|
const { x, y, w, h } = rect;
|
|
542
892
|
const detail = plan.detail;
|
|
@@ -568,25 +918,76 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
568
918
|
return inner;
|
|
569
919
|
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
920
|
};
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
921
|
+
if (plan.variant !== null) {
|
|
922
|
+
const env = {
|
|
923
|
+
kind: plan.variant, x, y, w, h, tie, order: p.order, side, final: p.final, road, detail, vertical: plan.vertical, tags: plan.tags, codes: plan.codes, scoresOn, v, ctx, o,
|
|
924
|
+
cls, link: row, tagOf, em: plan.em,
|
|
925
|
+
};
|
|
926
|
+
parts.push(...paintVariant(env));
|
|
927
|
+
}
|
|
928
|
+
else if (detail === "mark" && plan.markCols) {
|
|
929
|
+
// Two columns, each a stack: the seed tag, the mark, its three letters, the score. The marks are as large as the box lets the whole stack be, and every part is inside the box.
|
|
930
|
+
const padX = 6, padY = 5;
|
|
931
|
+
const colW = (w - 2 * padX) / 2;
|
|
932
|
+
const withScore = plan.markScores && scoresOn && r.score !== null;
|
|
933
|
+
const tags = [0, 1].map((k) => (plan.tags ? tagOf(tie, k) : ""));
|
|
934
|
+
const tagH = tags.some((t) => t !== "") ? 12 : 0;
|
|
935
|
+
const scoreOf = (k) => (withScore && r.score ? v.number(r.score[k]) : "");
|
|
936
|
+
const pensOf = (k) => (withScore && r.pens ? `(${v.number(r.pens[k])})` : "");
|
|
937
|
+
const hasPens = withScore && r.pens !== null;
|
|
938
|
+
const lay = (m) => {
|
|
939
|
+
const cf = clamp(m * 0.24, 7, 11);
|
|
940
|
+
let sf = clamp(m * 0.5, 9, 24);
|
|
941
|
+
const pf = (size) => Math.max(size * 0.55, 8);
|
|
942
|
+
// The score and the shoot-out's score share a line where they fit the column; else the shoot-out goes under the score.
|
|
943
|
+
const need = (size) => Math.max(...[0, 1].map((k) => boldWidth(scoreOf(k), size) + (pensOf(k) ? boldWidth(pensOf(k), pf(size)) + 3 : 0)));
|
|
944
|
+
if (withScore)
|
|
945
|
+
while (sf > 10 && need(sf) > colW - 2)
|
|
946
|
+
sf -= 0.5;
|
|
947
|
+
const inline = !withScore || need(sf) <= colW - 2;
|
|
948
|
+
const height = tagH + m + (plan.codes ? 3 + cf * 1.25 : 0) + (withScore ? 3 + sf * 1.15 + (hasPens && !inline ? pf(sf) * 1.15 : 0) : 0);
|
|
949
|
+
return { cf, sf, pf: pf(sf), inline, height };
|
|
950
|
+
};
|
|
951
|
+
let m = Math.max(10, Math.min(colW - 3, 56, h - 2 * padY));
|
|
952
|
+
let L = lay(m);
|
|
953
|
+
while (m > 10 && L.height > h - 2 * padY) {
|
|
954
|
+
m -= 0.5;
|
|
955
|
+
L = lay(m);
|
|
956
|
+
}
|
|
957
|
+
const top = y + (h - L.height) / 2;
|
|
958
|
+
const midY = top + tagH + m / 2;
|
|
579
959
|
rowsOrder.forEach((k, n) => {
|
|
580
960
|
const team = tie.sides[k];
|
|
581
|
-
const cx = x +
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
const
|
|
586
|
-
inner.push(`<
|
|
961
|
+
const cx = x + padX + colW / 2 + n * colW;
|
|
962
|
+
const inner = [];
|
|
963
|
+
let yy = top;
|
|
964
|
+
if (tagH && tags[k]) {
|
|
965
|
+
const tw = boldWidth(tags[k], 8) + 8;
|
|
966
|
+
inner.push(`<rect class="tb-tagbox" x="${num(cx - tw / 2)}" y="${num(yy)}" width="${num(tw)}" height="11" rx="5.5"/><text class="tb-tag" x="${num(cx)}" y="${num(yy + 8.2)}" font-size="8" text-anchor="middle">${esc(tags[k])}</text>`);
|
|
967
|
+
}
|
|
968
|
+
yy += tagH;
|
|
969
|
+
inner.push(drawMark(team, cx, yy + m / 2, m, ctx));
|
|
970
|
+
yy += m;
|
|
971
|
+
if (plan.codes) {
|
|
972
|
+
yy += 3;
|
|
973
|
+
inner.push(`<text class="tb-code" x="${num(cx)}" y="${num(yy + L.cf * 0.75)}" font-size="${num(L.cf)}" text-anchor="middle">${esc(fit(abbrOf(team, v), L.cf, colW - 2, boldWidth))}</text>`);
|
|
974
|
+
yy += L.cf * 1.25;
|
|
975
|
+
}
|
|
976
|
+
if (withScore) {
|
|
977
|
+
yy += 3;
|
|
978
|
+
const pens = pensOf(k);
|
|
979
|
+
const span = pens && L.inline ? `<tspan class="tb-muted" dx="3" font-size="${num(L.pf)}" font-weight="600">${esc(pens)}</tspan>` : "";
|
|
980
|
+
inner.push(`<text class="tb-score" x="${num(cx)}" y="${num(yy + L.sf * 0.8)}" font-size="${num(L.sf)}" text-anchor="middle" font-variant-numeric="tabular-nums">${esc(scoreOf(k))}${span}</text>`);
|
|
981
|
+
yy += L.sf * 1.15;
|
|
982
|
+
if (pens && !L.inline)
|
|
983
|
+
inner.push(`<text class="tb-muted" x="${num(cx)}" y="${num(yy + L.pf * 0.8)}" font-size="${num(L.pf)}" font-weight="600" text-anchor="middle">${esc(pens)}</text>`);
|
|
587
984
|
}
|
|
588
985
|
parts.push(row(k, `<g class="${cls(k)}">${inner.join("")}</g>`, cx - colW / 2, y, colW, h));
|
|
589
986
|
});
|
|
987
|
+
// "vs" between the two marks, where they leave room for it.
|
|
988
|
+
const vf = clamp(m * 0.26, 7, 11);
|
|
989
|
+
if (o.vs === true && colW - m >= boldWidth(v.words.roadVs, vf) + 4)
|
|
990
|
+
parts.push(`<text class="tb-vs" x="${num(x + w / 2)}" y="${num(midY + vf * 0.35)}" font-size="${num(vf)}" text-anchor="middle">${esc(v.words.roadVs)}</text>`);
|
|
590
991
|
}
|
|
591
992
|
else if (detail === "mark") {
|
|
592
993
|
const stack = Math.min((h - 8) / 2, w - 8, road ? 60 : 44);
|
|
@@ -608,6 +1009,8 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
608
1009
|
const gap = road ? clamp(rowH * 0.18, 5, 10) : 5;
|
|
609
1010
|
const small = Math.max(road ? scoreFont * 0.55 : font - 2, 8);
|
|
610
1011
|
const small2 = Math.max(font - 2, 8);
|
|
1012
|
+
const mw = boldWidth;
|
|
1013
|
+
const tf = clamp(font * 0.62, 7, 9.5);
|
|
611
1014
|
rowsOrder.forEach((k, n) => {
|
|
612
1015
|
const team = tie.sides[k];
|
|
613
1016
|
const ry = y + n * rowH;
|
|
@@ -618,24 +1021,31 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
618
1021
|
rowParts.push(drawMark(team, x + padX + md / 2, cy, md, ctx));
|
|
619
1022
|
tx = x + padX + md + gap;
|
|
620
1023
|
}
|
|
1024
|
+
// The seed tag, a small pill between the mark and the name.
|
|
1025
|
+
const tag = plan.tags ? tagOf(tie, k) : "";
|
|
1026
|
+
if (tag) {
|
|
1027
|
+
const tw = boldWidth(tag, tf) + 8;
|
|
1028
|
+
rowParts.push(`<rect class="tb-tagbox" x="${num(tx)}" y="${num(cy - (tf + 4) / 2)}" width="${num(tw)}" height="${num(tf + 4)}" rx="${num((tf + 4) / 2)}"/><text class="tb-tag" x="${num(tx + tw / 2)}" y="${num(cy + tf * 0.35)}" font-size="${num(tf)}" text-anchor="middle">${esc(tag)}</text>`);
|
|
1029
|
+
tx += tw + 3;
|
|
1030
|
+
}
|
|
621
1031
|
const score = scoresOn && r.score ? v.number(r.score[k]) : "";
|
|
622
1032
|
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
623
1033
|
const bye = p.byes.includes(k);
|
|
624
1034
|
const right = x + w - padX;
|
|
625
|
-
const scoreW = score ?
|
|
626
|
-
const pensW = pensText ?
|
|
627
|
-
const seed = seedsOn && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
1035
|
+
const scoreW = score ? mw(score, scoreFont) : 0;
|
|
1036
|
+
const pensW = pensText ? mw(pensText, small) + (road ? 4 : 3) : 0;
|
|
1037
|
+
const seed = seedsOn && !road && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
628
1038
|
const nm = team ? (detail === "full" ? team.name : abbrOf(team, v)) : v.words.tbd;
|
|
629
1039
|
// The word for a bye goes beside the score only where the name still fits beside it.
|
|
630
|
-
const byeFits = bye && right - tx - (scoreW + pensW) - (
|
|
1040
|
+
const byeFits = bye && right - tx - (scoreW + pensW) - (mw(v.words.bye, small2) + 6) - 6 >= mw(`${seed}${nm}`, font);
|
|
631
1041
|
const byeText = byeFits ? v.words.bye : "";
|
|
632
|
-
const byeW = byeText ?
|
|
1042
|
+
const byeW = byeText ? mw(byeText, small2) : 0;
|
|
633
1043
|
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
|
|
634
1044
|
// A name that does not fit is set a little smaller before it is cut.
|
|
635
1045
|
let nameFont = font;
|
|
636
|
-
while (nameFont > 9.5 &&
|
|
1046
|
+
while (nameFont > 9.5 && mw(`${seed}${nm}`, nameFont) > avail)
|
|
637
1047
|
nameFont -= 0.5;
|
|
638
|
-
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8));
|
|
1048
|
+
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8), mw);
|
|
639
1049
|
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>`);
|
|
640
1050
|
if (score)
|
|
641
1051
|
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>`);
|
|
@@ -652,7 +1062,10 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
652
1062
|
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>`);
|
|
653
1063
|
}
|
|
654
1064
|
}
|
|
655
|
-
const
|
|
1065
|
+
const extra = tieClassOf(o, tie);
|
|
1066
|
+
// The layouts of their own colour a half by its side: the first half (the left, or the top) is `tb-c0`, the second `tb-c1`.
|
|
1067
|
+
const conf = plan.variant !== null && (side === "l" || side === "r") ? ` tb-c${side === "l" ? 0 : 1}` : "";
|
|
1068
|
+
const cls2 = `tb-tie${p.final ? " tb-final" : ""}${p.champion ? " tb-has-champ" : ""}${plan.path.has(tie.index) ? " tb-on-path" : ""}${conf}${extra}`;
|
|
656
1069
|
const body = boxHref !== null ? anchor(boxHref, description, parts.join("")) : parts.join("");
|
|
657
1070
|
return `<g class="${cls2}"><title>${esc(description)}</title>${body}</g>`;
|
|
658
1071
|
}
|