@johnmorrisdotca/taisen-brackets 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/NOTICE.md +2 -0
- package/README.md +64 -23
- package/dist/background.d.ts +19 -0
- package/dist/background.js +66 -0
- package/dist/backgrounds-entry.d.ts +2 -0
- package/dist/backgrounds-entry.js +1 -0
- package/dist/family-backgrounds.d.ts +61 -0
- package/dist/family-backgrounds.js +453 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/marks.d.ts +2 -0
- package/dist/marks.js +31 -2
- package/dist/matchup.js +1 -0
- package/dist/render.js +282 -58
- package/dist/road.draw.js +6 -3
- package/dist/road.js +1 -1
- package/dist/road.types.d.ts +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.js +4 -4
- package/dist/theme.d.ts +9 -1
- package/dist/theme.js +23 -8
- package/dist/types.d.ts +50 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -2
package/dist/render.js
CHANGED
|
@@ -3,7 +3,8 @@ 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";
|
|
8
9
|
/** 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
10
|
export const SIZES = Object.freeze({
|
|
@@ -14,6 +15,43 @@ export const SIZES = Object.freeze({
|
|
|
14
15
|
});
|
|
15
16
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
16
17
|
const isLayout = (x) => typeof x === "object" && x !== null && "ties" in x && "sides" in x && "rounds" in x;
|
|
18
|
+
/** 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. */
|
|
19
|
+
const boldWidth = (text, size) => emWidth(text) * size;
|
|
20
|
+
/** The seed tag of a side: the tie's own, else the team's tag, else its seed number; `""` for none. */
|
|
21
|
+
function tagOf(tie, k) {
|
|
22
|
+
const given = tie.seeds?.[k];
|
|
23
|
+
const team = tie.sides[k];
|
|
24
|
+
const text = given ?? team?.tag ?? team?.seed;
|
|
25
|
+
return text === undefined || text === null ? "" : String(text).slice(0, 4);
|
|
26
|
+
}
|
|
27
|
+
/** 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. */
|
|
28
|
+
function tieClassOf(o, tie) {
|
|
29
|
+
if (typeof o.tieClass !== "function")
|
|
30
|
+
return "";
|
|
31
|
+
let asked;
|
|
32
|
+
try {
|
|
33
|
+
asked = o.tieClass(tie);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
if (typeof asked !== "string")
|
|
39
|
+
return "";
|
|
40
|
+
return asked.split(/\s+/).filter((word) => /^[A-Za-z_][\w-]*$/.test(word)).map((word) => ` ${word}`).join("");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 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
|
|
44
|
+
* between the edges `lo` and `hi`; `null` when there is none.
|
|
45
|
+
*/
|
|
46
|
+
function freeX(segs, y0, y1, w, pref, lo, hi) {
|
|
47
|
+
const margin = 4;
|
|
48
|
+
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]);
|
|
49
|
+
const ok = (x) => x >= lo && x + w <= hi && blocks.every(([a, b]) => x + w <= a || x >= b);
|
|
50
|
+
if (ok(pref))
|
|
51
|
+
return pref;
|
|
52
|
+
const found = [lo, hi - w, ...blocks.flatMap(([a, b]) => [b, a - w])].filter(ok);
|
|
53
|
+
return found.length === 0 ? null : found.reduce((best, x) => (Math.abs(x - pref) < Math.abs(best - pref) ? x : best));
|
|
54
|
+
}
|
|
17
55
|
/** 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
56
|
function metrics(rowH, road) {
|
|
19
57
|
if (road)
|
|
@@ -63,17 +101,19 @@ export function drawBracket(input, options = {}) {
|
|
|
63
101
|
const size = sizeName ? SIZES[sizeName] : { width: options.size.width, height: options.size.height ?? null };
|
|
64
102
|
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
103
|
const road = options.look === "road";
|
|
66
|
-
const
|
|
67
|
-
const
|
|
104
|
+
const hasBg = typeof options.background === "string" || (typeof options.background === "object" && options.background !== null);
|
|
105
|
+
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"] : [])]))}`;
|
|
106
|
+
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() };
|
|
68
107
|
const path = pathOf(layout, options.highlightPath);
|
|
69
108
|
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path);
|
|
70
109
|
const linked = { count: 0 };
|
|
71
110
|
const drawn = plan.boxes.map((b) => drawBox(b, plan, options, v, ctx, linked)); // draws the marks, so before the defs are written
|
|
72
111
|
const title = options.title ?? v.words.title;
|
|
73
112
|
const desc = describeBracket(layout, options);
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
113
|
+
const picture = resolveBackground(options.background, theme, plan.width, plan.height, prefix);
|
|
114
|
+
const defs = markDefs(ctx) + (picture?.defs ?? "");
|
|
115
|
+
const style = theme === "none" ? "" : `<style>${styleFor(`.${prefix}`, theme, { road, links: linked.count > 0, follow: path.size > 0, bgAuto: picture?.auto }, picture?.inks)}</style>`;
|
|
116
|
+
const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(plan.width)}" height="${num(plan.height)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
|
|
77
117
|
return [
|
|
78
118
|
`<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">`,
|
|
79
119
|
`<title id="${prefix}-t">${esc(title)}</title><desc id="${prefix}-d">${esc(desc)}</desc>`,
|
|
@@ -96,6 +136,11 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
96
136
|
const links = [];
|
|
97
137
|
const texts = [];
|
|
98
138
|
const extras = [];
|
|
139
|
+
// The connector lines as they are drawn, so that the names of the rounds can be placed clear of them.
|
|
140
|
+
const segs = [];
|
|
141
|
+
const addLine = (...pts) => pts.slice(1).forEach(([x, y], i) => segs.push({ x0: pts[i][0], y0: pts[i][1], x1: x, y1: y }));
|
|
142
|
+
const clear = (lo, hi) => (w, pref, y) => freeX(segs, y - 16, y + 7, w, pref, lo, hi);
|
|
143
|
+
const finish = (plan) => ({ ...plan, texts: plan.texts.map((t) => (typeof t === "function" ? t() : t)) });
|
|
99
144
|
const W0 = size.width;
|
|
100
145
|
const titleH = (o.title ? (poster ? 46 : 38) : 0) + (o.subtitle ? 24 : 0) + (o.title || o.subtitle ? 10 : 0);
|
|
101
146
|
const hasGroups = layout.sides.some((s) => s.label);
|
|
@@ -121,6 +166,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
121
166
|
let others = [];
|
|
122
167
|
let ow = 0;
|
|
123
168
|
let extraH0 = 0;
|
|
169
|
+
let labelRoom = showLabels ? 24 : 6;
|
|
124
170
|
if (!vertical) {
|
|
125
171
|
const availW = W0 - 2 * pad;
|
|
126
172
|
const g = 0.2;
|
|
@@ -131,7 +177,10 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
131
177
|
D = clamp(d, 40, 220);
|
|
132
178
|
gapT = D * g;
|
|
133
179
|
centreT = Math.max(centreMin, D * 1.2);
|
|
134
|
-
|
|
180
|
+
// 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.
|
|
181
|
+
if (showLabels && layout.columns.some((c) => !nameFits(v, road, roundLabel(layout, c, o.roundNames, v), D + gapT * 0.8)))
|
|
182
|
+
labelRoom = 34;
|
|
183
|
+
const top = pad + titleH + (hasGroups ? 28 : 0) + labelRoom;
|
|
135
184
|
others = othersOf(layout);
|
|
136
185
|
ow = clamp(D * 1.1, 120, 200);
|
|
137
186
|
extraH0 = othersHeight(others.length, W0, pad, ow, OTHER_H) + thirdBlock;
|
|
@@ -151,8 +200,12 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
151
200
|
boxW = S;
|
|
152
201
|
}
|
|
153
202
|
// Full boxes (names) only where the longest name fits beside its mark and a score.
|
|
154
|
-
const
|
|
155
|
-
const
|
|
203
|
+
const wide = boldWidth;
|
|
204
|
+
const longest = Math.max(0, ...layout.teams.map((t) => wide(t.name, 13)));
|
|
205
|
+
const longestAbbr = Math.max(0, ...layout.teams.map((t) => wide(abbrOf(t, v), 13)));
|
|
206
|
+
// Seed tags (the road look with `seeds`): a small pill beside each mark, or above it in a box of marks alone.
|
|
207
|
+
const tagsOn = road && (o.seeds ?? false) && layout.ties.some((t) => tagOf(t, 0) !== "" || tagOf(t, 1) !== "");
|
|
208
|
+
const tagW = tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 8))) + 8 : 0;
|
|
156
209
|
// 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
210
|
const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
|
|
158
211
|
// The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
|
|
@@ -162,9 +215,9 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
162
215
|
const { font, md, score } = metrics(rowH, road);
|
|
163
216
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
164
217
|
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) +
|
|
218
|
+
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);
|
|
219
|
+
const fixed = clamp(rowH * 0.16, 5, 12) + clamp(rowH * 0.18, 5, 10) + 8 + 10 + (tagW > 0 ? tagW + 3 : 0);
|
|
220
|
+
const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide("00", score) + extra;
|
|
168
221
|
return Math.min(1, (w - fixed) / varying);
|
|
169
222
|
};
|
|
170
223
|
let fitNow = 1;
|
|
@@ -174,14 +227,17 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
174
227
|
// Room for the score, and for the words some boxes carry beside it: a bye, a shoot-out's score.
|
|
175
228
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
176
229
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
177
|
-
const extra = (anyBye ?
|
|
178
|
-
const needed = 6 + md + 5 + longest * (font / 13) + 8 +
|
|
230
|
+
const extra = (anyBye ? boldWidth(v.words.bye, Math.max(font - 2, 8)) + 8 : 0) + (anyPens ? boldWidth("(00)", Math.max(font - 2, 8)) + 3 : 0);
|
|
231
|
+
const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth("00", score) + extra + 14;
|
|
179
232
|
const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
|
|
180
233
|
return named === "mark" && o.markShape === "none" ? "compact" : named;
|
|
181
234
|
};
|
|
182
235
|
// 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
236
|
// else the abbreviation (compact) where rows of 18 or more fit; else only the marks.
|
|
184
|
-
const
|
|
237
|
+
const codesOn = road && (o.codes ?? false);
|
|
238
|
+
const markCols = road && (markScores || tagsOn || codesOn || o.vs === true);
|
|
239
|
+
const anyPensAll = layout.ties.some((t) => t.result.pens !== null);
|
|
240
|
+
const roadPick = (w, hMax, grow = false) => {
|
|
185
241
|
const search = (named, hMin) => {
|
|
186
242
|
let h = hMax;
|
|
187
243
|
while (h > hMin && fitOf(w, h, named) < 0.94)
|
|
@@ -198,15 +254,18 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
198
254
|
const compact = search("compact", 36);
|
|
199
255
|
chosen = full.ok ? full : compact.ok ? compact : o.markShape === "none" ? compact : null;
|
|
200
256
|
}
|
|
201
|
-
|
|
202
|
-
|
|
257
|
+
// 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.
|
|
258
|
+
const m = Math.min((w - 12) / 2 - 3, 56);
|
|
259
|
+
const sf = clamp(m * 0.5, 9, 24);
|
|
260
|
+
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);
|
|
261
|
+
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
262
|
if (chosen.detail !== "mark")
|
|
204
263
|
fitNow = Math.min(1, fitOf(w, chosen.h, chosen.detail));
|
|
205
264
|
return chosen;
|
|
206
265
|
};
|
|
207
266
|
let detail;
|
|
208
267
|
if (road) {
|
|
209
|
-
const picked = roadPick(boxW, vertical ? clamp(boxW * 0.42, 48, 124) : boxH);
|
|
268
|
+
const picked = roadPick(boxW, vertical ? clamp(boxW * 0.42, 48, 124) : boxH, vertical);
|
|
210
269
|
detail = picked.detail;
|
|
211
270
|
if (vertical)
|
|
212
271
|
boxH = picked.h;
|
|
@@ -220,9 +279,10 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
220
279
|
if (vertical)
|
|
221
280
|
boxH = detail === "full" ? 50 : detail === "compact" ? 42 : boxW >= 76 ? 38 : 2 * Math.min(boxW - 8, 24) + 8;
|
|
222
281
|
}
|
|
282
|
+
// 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
283
|
if (vertical) {
|
|
224
284
|
D = boxH;
|
|
225
|
-
gapT = showLabels ?
|
|
285
|
+
gapT = showLabels ? 32 : 16;
|
|
226
286
|
}
|
|
227
287
|
// The champion's block: a trophy, a large mark, the name and the word; and the third-place match below the final.
|
|
228
288
|
const champMark = vertical ? (road ? clamp(size.width * 0.14, 54, 96) : 54) : clamp(centreT * (road ? 0.4 : 0.34), 44, road ? 96 : 84);
|
|
@@ -243,7 +303,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
243
303
|
sizes.forEach((sz) => { starts.push(t); t += sz + gapT; });
|
|
244
304
|
const width = Math.max(W0, total + 2 * pad);
|
|
245
305
|
// ----- span positions
|
|
246
|
-
const top = pad + titleH + (hasGroups ? 28 : 0) +
|
|
306
|
+
const top = pad + titleH + (hasGroups ? 28 : 0) + labelRoom;
|
|
247
307
|
const treeH = rows * rowStep;
|
|
248
308
|
// The final sits at the middle of the rows; the champion above it and the third place below need room.
|
|
249
309
|
const upNeed = (champ ? champH + 8 : 0) + 22 + finalH / 2;
|
|
@@ -310,7 +370,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
310
370
|
return;
|
|
311
371
|
const ks = layout.columns.map((_, ci) => slotOfColumn(sd.key, ci));
|
|
312
372
|
const x0 = Math.min(...ks.map((k) => starts[k])), x1 = Math.max(...ks.map((k) => starts[k] + D));
|
|
313
|
-
texts.push(`<text class="tb-half" x="${num((x0 + x1) / 2)}" y="${num(top - (showLabels ?
|
|
373
|
+
texts.push(`<text class="tb-half" x="${num((x0 + x1) / 2)}" y="${num(top - (showLabels ? labelRoom + 12 : 14))}" font-size="${poster ? 16 : 15}" font-weight="700" letter-spacing=".1em" text-anchor="middle">${esc(poster ? v.upper(sd.label) : sd.label)}</text>`);
|
|
314
374
|
});
|
|
315
375
|
}
|
|
316
376
|
// lines
|
|
@@ -325,13 +385,14 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
325
385
|
const x0 = goingRight ? a.x + a.w : a.x, x1 = goingRight ? b.x : b.x + b.w;
|
|
326
386
|
const y0 = a.y + a.h / 2, y1 = b.y + b.h / 2;
|
|
327
387
|
const xm = (x0 + x1) / 2;
|
|
388
|
+
addLine([x0, y0], [xm, y0], [xm, y1], [x1, y1]);
|
|
328
389
|
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
390
|
}
|
|
330
391
|
if (bottomThird)
|
|
331
392
|
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
|
|
332
393
|
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OTHER_H, v, road, boxes, texts);
|
|
333
394
|
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 };
|
|
395
|
+
return finish({ width, height, boxes, links: orderLinks(links), texts, extras, detail, vertical, road, path, fit: fitNow, notes: anyNotes, markScores, markCols, tags: tagsOn, codes: codesOn });
|
|
335
396
|
}
|
|
336
397
|
// ----- vertical: the rounds are rows. The left half is the top; the final is a band in the middle.
|
|
337
398
|
const groupBand = hasGroups ? 26 : 0;
|
|
@@ -394,7 +455,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
394
455
|
if (champ)
|
|
395
456
|
extras.push(...championBlock(layout.champion, midX, pad + titleH + 4, champMark, trophyH, nameFont, o, v, ctx, poster));
|
|
396
457
|
if (showLabels) {
|
|
397
|
-
|
|
458
|
+
const at = y + 12;
|
|
459
|
+
texts.push(() => label(v, road, roundLabel(layout, layout.rounds, o.roundNames, v), midX, at, "middle", 1e9, clear(pad - 4, W0 - pad + 4)));
|
|
398
460
|
y += 16;
|
|
399
461
|
}
|
|
400
462
|
const rect = { x: midX - finalW / 2, y: y + 6, w: finalW, h: finalH };
|
|
@@ -409,8 +471,24 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
409
471
|
layout.columns.forEach((c, ci) => {
|
|
410
472
|
const name = roundLabel(layout, c, o.roundNames, v);
|
|
411
473
|
const sides = halves ? (layout.sides.length > 1 ? ["l", "r"] : ["l"]) : ["l"];
|
|
412
|
-
for (const sd of sides)
|
|
413
|
-
|
|
474
|
+
for (const sd of sides) {
|
|
475
|
+
const at = bandStart(sd, ci) - 8;
|
|
476
|
+
texts.push(() => {
|
|
477
|
+
if (road)
|
|
478
|
+
return label(v, road, name, pad, at, "start", 1e9, clear(pad - 4, W0 - pad + 4));
|
|
479
|
+
const shown = v.upper(name);
|
|
480
|
+
let size = 11, left = pad - 4;
|
|
481
|
+
for (const s of [11, 9.5, 8.5]) {
|
|
482
|
+
size = s;
|
|
483
|
+
const found = freeX(segs, at - 16, at + 7, boldWidth(shown, s) * 1.06 + 8, pad - 4, pad - 4, W0 - pad + 4);
|
|
484
|
+
if (found !== null) {
|
|
485
|
+
left = found;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
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>`;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
414
492
|
});
|
|
415
493
|
}
|
|
416
494
|
if (hasGroups) {
|
|
@@ -431,14 +509,16 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path) {
|
|
|
431
509
|
const down = b.y > a.y;
|
|
432
510
|
const y0 = down ? a.y + a.h : a.y, y1 = down ? b.y : b.y + b.h;
|
|
433
511
|
const x0c = a.x + a.w / 2, x1c = b.x + b.w / 2;
|
|
434
|
-
|
|
512
|
+
// 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.
|
|
513
|
+
const ym = showLabels ? (down ? y0 + 6 : y1 + 6) : down ? y0 + gapT * 0.4 : y0 - gapT * 0.4;
|
|
514
|
+
addLine([x0c, y0], [x0c, ym], [x1c, ym], [x1c, y1]);
|
|
435
515
|
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
516
|
}
|
|
437
517
|
if (bottomThird)
|
|
438
518
|
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
|
|
439
519
|
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OTHER_H, v, road, boxes, texts);
|
|
440
520
|
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 };
|
|
521
|
+
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 });
|
|
442
522
|
}
|
|
443
523
|
/** A line's class: `tb-path` when both ties it joins are on the highlighted team's path. */
|
|
444
524
|
function lineClass(path, from, to) {
|
|
@@ -453,18 +533,108 @@ const centreMinLtr = (d) => d * 1.1;
|
|
|
453
533
|
function lineAttrs(o) {
|
|
454
534
|
return o.theme === "none" ? ' fill="none" stroke="currentColor" stroke-opacity=".5"' : "";
|
|
455
535
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
536
|
+
/** 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. */
|
|
537
|
+
function twoLines(text, size, measure) {
|
|
538
|
+
const words = text.split(" ");
|
|
539
|
+
let best = null;
|
|
540
|
+
for (let i = 1; i < words.length; i += 1) {
|
|
541
|
+
const pair = [words.slice(0, i).join(" "), words.slice(i).join(" ")];
|
|
542
|
+
if (best === null || Math.max(measure(pair[0], size), measure(pair[1], size)) < Math.max(measure(best[0], size), measure(best[1], size)))
|
|
543
|
+
best = pair;
|
|
544
|
+
}
|
|
545
|
+
return best;
|
|
546
|
+
}
|
|
547
|
+
/** The bold, letter-spaced width of a round's name (as a pill or a heading sets it). */
|
|
548
|
+
const nameWidth = (t, size) => boldWidth(t, size) * 1.06;
|
|
549
|
+
/**
|
|
550
|
+
* 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
|
|
551
|
+
* 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.
|
|
552
|
+
*/
|
|
553
|
+
function nameFits(v, road, text, maxWidth) {
|
|
460
554
|
const upper = v.upper(text);
|
|
555
|
+
return nameWidth(upper, 9) + (road ? 8 : 0) <= maxWidth;
|
|
556
|
+
}
|
|
557
|
+
function label(v, road, text, x, y, anchor, maxWidth = 1e9, place) {
|
|
558
|
+
const upper = v.upper(text);
|
|
559
|
+
if (!road) {
|
|
560
|
+
// 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.
|
|
561
|
+
if (maxWidth < 1e9 && nameWidth(upper, 11) > maxWidth) {
|
|
562
|
+
let size = 11;
|
|
563
|
+
while (size > 9.5 && nameWidth(upper, size) > maxWidth)
|
|
564
|
+
size -= 0.5;
|
|
565
|
+
if (nameWidth(upper, size) <= maxWidth)
|
|
566
|
+
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>`;
|
|
567
|
+
for (const s of [9.5, 9, 8.5]) {
|
|
568
|
+
const pair = twoLines(upper, s, nameWidth);
|
|
569
|
+
if (pair && Math.max(nameWidth(pair[0], s), nameWidth(pair[1], s)) <= maxWidth)
|
|
570
|
+
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("");
|
|
571
|
+
}
|
|
572
|
+
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>`;
|
|
573
|
+
}
|
|
574
|
+
const shown = upper;
|
|
575
|
+
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>`;
|
|
576
|
+
if (!place)
|
|
577
|
+
return plain;
|
|
578
|
+
// The name is kept clear of the connector lines: where it cannot stand where it was put, it moves along its row.
|
|
579
|
+
for (const size of [11, 9.5, 8.5]) {
|
|
580
|
+
const w = nameWidth(shown, size) + 8;
|
|
581
|
+
const pref = anchor === "middle" ? x - w / 2 : x - 4;
|
|
582
|
+
const left = place(w, pref, y);
|
|
583
|
+
if (left === null)
|
|
584
|
+
continue;
|
|
585
|
+
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>`;
|
|
586
|
+
}
|
|
587
|
+
return plain;
|
|
588
|
+
}
|
|
589
|
+
// 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
590
|
let s = 11;
|
|
462
|
-
while (s > 9 &&
|
|
591
|
+
while (s > 9 && nameWidth(upper, s) > maxWidth - 18)
|
|
463
592
|
s -= 0.5;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
593
|
+
let rows = [upper];
|
|
594
|
+
let side = 9;
|
|
595
|
+
if (nameWidth(upper, s) > maxWidth - 18) {
|
|
596
|
+
if (nameWidth(upper, s) <= maxWidth - 8)
|
|
597
|
+
side = 4;
|
|
598
|
+
else {
|
|
599
|
+
rows = [fit(upper, s, maxWidth - 12, nameWidth)];
|
|
600
|
+
for (const size of [9, 8.5, 8]) {
|
|
601
|
+
const pair = twoLines(upper, size, nameWidth);
|
|
602
|
+
if (pair && Math.max(nameWidth(pair[0], size), nameWidth(pair[1], size)) <= maxWidth - 12) {
|
|
603
|
+
rows = pair;
|
|
604
|
+
s = size;
|
|
605
|
+
side = 6;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
if (pair && size === 8) {
|
|
609
|
+
rows = pair.map((line) => fit(line, 8, maxWidth - 12, nameWidth));
|
|
610
|
+
s = 8;
|
|
611
|
+
side = 6;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const widest = Math.max(...rows.map((r) => nameWidth(r, s)));
|
|
617
|
+
let w = widest + 2 * side;
|
|
618
|
+
let x0 = anchor === "middle" ? x - w / 2 : x;
|
|
619
|
+
let fs = s;
|
|
620
|
+
if (place && rows.length === 1) {
|
|
621
|
+
// Where the lines leave no room, the name is set smaller (to 8) and the pill made snugger (side 6, then 4).
|
|
622
|
+
search: for (const size of s > 8 ? [s, 8.5, 8].filter((z) => z <= s) : [s]) {
|
|
623
|
+
for (const sd of [9, 6, 4]) {
|
|
624
|
+
w = nameWidth(rows[0], size) + 2 * sd;
|
|
625
|
+
const pref = anchor === "middle" ? x - w / 2 : x;
|
|
626
|
+
const found = place(w, pref, y);
|
|
627
|
+
x0 = found ?? pref;
|
|
628
|
+
fs = size;
|
|
629
|
+
if (found !== null)
|
|
630
|
+
break search;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
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>`;
|
|
635
|
+
if (rows.length === 2)
|
|
636
|
+
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)}`;
|
|
637
|
+
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
638
|
}
|
|
469
639
|
function headings(v, o, width, pad, poster) {
|
|
470
640
|
const out = [];
|
|
@@ -568,25 +738,69 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
568
738
|
return inner;
|
|
569
739
|
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
740
|
};
|
|
571
|
-
if (detail === "mark" && plan.
|
|
572
|
-
// Two columns
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
const
|
|
741
|
+
if (detail === "mark" && plan.markCols) {
|
|
742
|
+
// 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.
|
|
743
|
+
const padX = 6, padY = 5;
|
|
744
|
+
const colW = (w - 2 * padX) / 2;
|
|
745
|
+
const withScore = plan.markScores && scoresOn && r.score !== null;
|
|
746
|
+
const tags = [0, 1].map((k) => (plan.tags ? tagOf(tie, k) : ""));
|
|
747
|
+
const tagH = tags.some((t) => t !== "") ? 12 : 0;
|
|
748
|
+
const scoreOf = (k) => (withScore && r.score ? v.number(r.score[k]) : "");
|
|
749
|
+
const pensOf = (k) => (withScore && r.pens ? `(${v.number(r.pens[k])})` : "");
|
|
750
|
+
const hasPens = withScore && r.pens !== null;
|
|
751
|
+
const lay = (m) => {
|
|
752
|
+
const cf = clamp(m * 0.24, 7, 11);
|
|
753
|
+
let sf = clamp(m * 0.5, 9, 24);
|
|
754
|
+
const pf = (size) => Math.max(size * 0.55, 8);
|
|
755
|
+
// The score and the shoot-out's score share a line where they fit the column; else the shoot-out goes under the score.
|
|
756
|
+
const need = (size) => Math.max(...[0, 1].map((k) => boldWidth(scoreOf(k), size) + (pensOf(k) ? boldWidth(pensOf(k), pf(size)) + 3 : 0)));
|
|
757
|
+
if (withScore)
|
|
758
|
+
while (sf > 10 && need(sf) > colW - 2)
|
|
759
|
+
sf -= 0.5;
|
|
760
|
+
const inline = !withScore || need(sf) <= colW - 2;
|
|
761
|
+
const height = tagH + m + (plan.codes ? 3 + cf * 1.25 : 0) + (withScore ? 3 + sf * 1.15 + (hasPens && !inline ? pf(sf) * 1.15 : 0) : 0);
|
|
762
|
+
return { cf, sf, pf: pf(sf), inline, height };
|
|
763
|
+
};
|
|
764
|
+
let m = Math.max(10, Math.min(colW - 3, 56, h - 2 * padY));
|
|
765
|
+
let L = lay(m);
|
|
766
|
+
while (m > 10 && L.height > h - 2 * padY) {
|
|
767
|
+
m -= 0.5;
|
|
768
|
+
L = lay(m);
|
|
769
|
+
}
|
|
770
|
+
const top = y + (h - L.height) / 2;
|
|
771
|
+
const midY = top + tagH + m / 2;
|
|
579
772
|
rowsOrder.forEach((k, n) => {
|
|
580
773
|
const team = tie.sides[k];
|
|
581
|
-
const cx = x +
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
const
|
|
586
|
-
inner.push(`<
|
|
774
|
+
const cx = x + padX + colW / 2 + n * colW;
|
|
775
|
+
const inner = [];
|
|
776
|
+
let yy = top;
|
|
777
|
+
if (tagH && tags[k]) {
|
|
778
|
+
const tw = boldWidth(tags[k], 8) + 8;
|
|
779
|
+
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>`);
|
|
780
|
+
}
|
|
781
|
+
yy += tagH;
|
|
782
|
+
inner.push(drawMark(team, cx, yy + m / 2, m, ctx));
|
|
783
|
+
yy += m;
|
|
784
|
+
if (plan.codes) {
|
|
785
|
+
yy += 3;
|
|
786
|
+
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>`);
|
|
787
|
+
yy += L.cf * 1.25;
|
|
788
|
+
}
|
|
789
|
+
if (withScore) {
|
|
790
|
+
yy += 3;
|
|
791
|
+
const pens = pensOf(k);
|
|
792
|
+
const span = pens && L.inline ? `<tspan class="tb-muted" dx="3" font-size="${num(L.pf)}" font-weight="600">${esc(pens)}</tspan>` : "";
|
|
793
|
+
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>`);
|
|
794
|
+
yy += L.sf * 1.15;
|
|
795
|
+
if (pens && !L.inline)
|
|
796
|
+
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
797
|
}
|
|
588
798
|
parts.push(row(k, `<g class="${cls(k)}">${inner.join("")}</g>`, cx - colW / 2, y, colW, h));
|
|
589
799
|
});
|
|
800
|
+
// "vs" between the two marks, where they leave room for it.
|
|
801
|
+
const vf = clamp(m * 0.26, 7, 11);
|
|
802
|
+
if (o.vs === true && colW - m >= boldWidth(v.words.roadVs, vf) + 4)
|
|
803
|
+
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
804
|
}
|
|
591
805
|
else if (detail === "mark") {
|
|
592
806
|
const stack = Math.min((h - 8) / 2, w - 8, road ? 60 : 44);
|
|
@@ -608,6 +822,8 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
608
822
|
const gap = road ? clamp(rowH * 0.18, 5, 10) : 5;
|
|
609
823
|
const small = Math.max(road ? scoreFont * 0.55 : font - 2, 8);
|
|
610
824
|
const small2 = Math.max(font - 2, 8);
|
|
825
|
+
const mw = boldWidth;
|
|
826
|
+
const tf = clamp(font * 0.62, 7, 9.5);
|
|
611
827
|
rowsOrder.forEach((k, n) => {
|
|
612
828
|
const team = tie.sides[k];
|
|
613
829
|
const ry = y + n * rowH;
|
|
@@ -618,24 +834,31 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
618
834
|
rowParts.push(drawMark(team, x + padX + md / 2, cy, md, ctx));
|
|
619
835
|
tx = x + padX + md + gap;
|
|
620
836
|
}
|
|
837
|
+
// The seed tag, a small pill between the mark and the name.
|
|
838
|
+
const tag = plan.tags ? tagOf(tie, k) : "";
|
|
839
|
+
if (tag) {
|
|
840
|
+
const tw = boldWidth(tag, tf) + 8;
|
|
841
|
+
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>`);
|
|
842
|
+
tx += tw + 3;
|
|
843
|
+
}
|
|
621
844
|
const score = scoresOn && r.score ? v.number(r.score[k]) : "";
|
|
622
845
|
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
623
846
|
const bye = p.byes.includes(k);
|
|
624
847
|
const right = x + w - padX;
|
|
625
|
-
const scoreW = score ?
|
|
626
|
-
const pensW = pensText ?
|
|
627
|
-
const seed = seedsOn && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
848
|
+
const scoreW = score ? mw(score, scoreFont) : 0;
|
|
849
|
+
const pensW = pensText ? mw(pensText, small) + (road ? 4 : 3) : 0;
|
|
850
|
+
const seed = seedsOn && !road && team?.seed !== undefined ? `${v.number(team.seed)} ` : "";
|
|
628
851
|
const nm = team ? (detail === "full" ? team.name : abbrOf(team, v)) : v.words.tbd;
|
|
629
852
|
// 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) - (
|
|
853
|
+
const byeFits = bye && right - tx - (scoreW + pensW) - (mw(v.words.bye, small2) + 6) - 6 >= mw(`${seed}${nm}`, font);
|
|
631
854
|
const byeText = byeFits ? v.words.bye : "";
|
|
632
|
-
const byeW = byeText ?
|
|
855
|
+
const byeW = byeText ? mw(byeText, small2) : 0;
|
|
633
856
|
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6;
|
|
634
857
|
// A name that does not fit is set a little smaller before it is cut.
|
|
635
858
|
let nameFont = font;
|
|
636
|
-
while (nameFont > 9.5 &&
|
|
859
|
+
while (nameFont > 9.5 && mw(`${seed}${nm}`, nameFont) > avail)
|
|
637
860
|
nameFont -= 0.5;
|
|
638
|
-
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8));
|
|
861
|
+
const text = fit(`${seed}${nm}`, nameFont, Math.max(avail, 8), mw);
|
|
639
862
|
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
863
|
if (score)
|
|
641
864
|
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 +875,8 @@ function drawBox(p, plan, o, v, ctx, linked) {
|
|
|
652
875
|
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
876
|
}
|
|
654
877
|
}
|
|
655
|
-
const
|
|
878
|
+
const extra = tieClassOf(o, tie);
|
|
879
|
+
const cls2 = `tb-tie${p.final ? " tb-final" : ""}${p.champion ? " tb-has-champ" : ""}${plan.path.has(tie.index) ? " tb-on-path" : ""}${extra}`;
|
|
656
880
|
const body = boxHref !== null ? anchor(boxHref, description, parts.join("")) : parts.join("");
|
|
657
881
|
return `<g class="${cls2}"><title>${esc(description)}</title>${body}</g>`;
|
|
658
882
|
}
|
package/dist/road.draw.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The road chart, drawn: the two finalists' big round marks with their names, the round names down the middle on a dotted road, each finalist's matches on its side as
|
|
2
2
|
// the round marks of both teams with the score between them, and the group stage as a compact row at the foot. Each size has its own arrangement: `card`, `square` and
|
|
3
3
|
// `phone` stack the finalists over the rounds; `wide` puts them at the two ends. Nothing is measured in a browser: type is set by estimates (`emWidth`).
|
|
4
|
+
import { resolveBackground } from "./background.js";
|
|
4
5
|
import { drawMark, markDefs } from "./marks.js";
|
|
5
6
|
import { codeOf, decidedWords, nameOf, summaryOf } from "./road.model.js";
|
|
6
7
|
import { SIZES } from "./render.js";
|
|
@@ -36,6 +37,7 @@ export function drawRoad(model, o, gid) {
|
|
|
36
37
|
shape: o.markShape === "square" ? "square" : "round",
|
|
37
38
|
look: "road",
|
|
38
39
|
flags: o.flags,
|
|
40
|
+
flagHref: o.flagHref,
|
|
39
41
|
theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit",
|
|
40
42
|
flagCache: new Map(),
|
|
41
43
|
};
|
|
@@ -232,9 +234,10 @@ export function drawRoad(model, o, gid) {
|
|
|
232
234
|
if (model.subtitle !== undefined)
|
|
233
235
|
head += text("tb-r-s", cx, pad + titleFit.height + subSize * 0.7, subSize, model.subtitle);
|
|
234
236
|
const foot = endSize === 0 ? "" : text("tb-r-e", cx, H - pad - endSize * 0.2, endSize, note);
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
237
|
+
const picture = resolveBackground(o.background, theme, W, H, gid);
|
|
238
|
+
const defs = markDefs(ctx) + (picture?.defs ?? "");
|
|
239
|
+
const style = theme === "none" ? "" : `<style>${styleFor(`.${gid}`, theme, { chart: true, bgAuto: picture?.auto }, picture?.inks)}</style>`;
|
|
240
|
+
const bg = o.background === false ? "" : `<rect class="tb-bg" x="0" y="0" width="${num(W)}" height="${num(H)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
|
|
238
241
|
const extra = o.className ? ` ${esc(o.className)}` : "";
|
|
239
242
|
return [
|
|
240
243
|
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(model.voice.locale.code)}" class="taisen tb-road ${gid}${extra}" viewBox="0 0 ${num(W)} ${num(H)}" width="${num(W)}" height="${num(H)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${gid}-t ${gid}-d" data-size="${size}">`,
|
package/dist/road.js
CHANGED
|
@@ -44,7 +44,7 @@ export function roadToFinal(input, options = {}) {
|
|
|
44
44
|
const merged = { ...options, ...extra };
|
|
45
45
|
check(merged);
|
|
46
46
|
const m = readModel(input, merged);
|
|
47
|
-
const prefix = merged.id ?? `tb-${hash(JSON.stringify([m.voice.locale.code, m.finalists.map((t) => t.name), [...m.path[0].values()].map((v) => [v.round, v.mine, v.theirs]), [...m.path[1].values()].map((v) => [v.round, v.mine, v.theirs]), m.rounds, merged.size, merged.theme, merged.title, merged.subtitle, merged.final ?? null, merged.endnote, merged.winner, merged.codes, merged.markShape]))}`;
|
|
47
|
+
const prefix = merged.id ?? `tb-${hash(JSON.stringify([m.voice.locale.code, m.finalists.map((t) => t.name), [...m.path[0].values()].map((v) => [v.round, v.mine, v.theirs]), [...m.path[1].values()].map((v) => [v.round, v.mine, v.theirs]), m.rounds, merged.size, merged.theme, merged.title, merged.subtitle, merged.final ?? null, merged.endnote, merged.winner, merged.codes, merged.markShape, ...(typeof merged.background === "string" || typeof merged.background === "object" ? [merged.background] : [])]))}`;
|
|
48
48
|
return drawRoad(m, merged, prefix);
|
|
49
49
|
};
|
|
50
50
|
return {
|
package/dist/road.types.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface RoadFinal {
|
|
|
49
49
|
/** The sizes of the road chart are the bracket's: `card` (the default here), `square`, `wide` (the finalists at the two ends) and `phone`. */
|
|
50
50
|
export type RoadSize = SizeName;
|
|
51
51
|
/** What `roadToFinal` takes besides the finalists or the tournament. */
|
|
52
|
-
export interface RoadOptions extends Pick<DrawOptions, "locale" | "words" | "teamName" | "roundNames" | "markShape" | "flags" | "theme" | "background" | "id" | "className"> {
|
|
52
|
+
export interface RoadOptions extends Pick<DrawOptions, "locale" | "words" | "teamName" | "roundNames" | "markShape" | "flags" | "flagHref" | "theme" | "background" | "id" | "className"> {
|
|
53
53
|
/** With a tournament: the finalist whose road is on the left (by team id); the other finalist is its opponent in the final. Left out, the first side of the final. */
|
|
54
54
|
team?: SideId;
|
|
55
55
|
/** The tournament, set large at the top. The language's "Road to the final" when missing. */
|
package/dist/text.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function esc(text: string | number): string;
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function textWidth(text: string, size: number): number;
|
|
9
9
|
/** The text cut to fit a width, with an ellipsis where it was cut. */
|
|
10
|
-
export declare function fit(text: string, size: number, width: number): string;
|
|
10
|
+
export declare function fit(text: string, size: number, width: number, measure?: (text: string, size: number) => number): string;
|
|
11
11
|
/** A short name for a small box: the initials of a name of several words ("NYR"), else its first three letters; capitals. */
|
|
12
12
|
export declare function abbreviate(name: string): string;
|
|
13
13
|
/** A small, stable hash of a string as lowercase base 36, for ids that depend on the content and nothing else. */
|