@johnmorrisdotca/taisen-brackets 0.5.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 +25 -0
- package/NOTICE.md +2 -0
- package/README.md +159 -29
- 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 +4 -2
- package/dist/index.js +3 -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 +2 -0
- package/dist/marks.js +1 -0
- package/dist/matchup.js +1 -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 +223 -34
- package/dist/seeding.js +4 -2
- package/dist/table.js +6 -3
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +6 -0
- package/dist/types.d.ts +66 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
package/dist/layouts.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { abbrOf } from "./matchup.js";
|
|
2
|
+
import { drawMark } from "./marks.js";
|
|
3
|
+
import { emWidth, esc, fit, num } from "./text.js";
|
|
4
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
5
|
+
/** The width of a text set in bold at a size: the package's generous estimate, so that a text stays inside its place in any font. */
|
|
6
|
+
const bw = (text, size) => emWidth(text) * size;
|
|
7
|
+
const scoreOf = (e, k) => (e.scoresOn && e.tie.result.score ? e.v.number(e.tie.result.score[k]) : "");
|
|
8
|
+
const pensOf = (e, k) => (e.scoresOn && e.tie.result.pens ? `(${e.v.number(e.tie.result.pens[k])})` : "");
|
|
9
|
+
/** A small pill with a seed tag, its left edge at `x`, centred on `cy`. */
|
|
10
|
+
function tagPill(text, x, cy, size) {
|
|
11
|
+
const w = bw(text, size) + 8;
|
|
12
|
+
return { w, svg: `<rect class="tb-tagbox" x="${num(x)}" y="${num(cy - (size + 4) / 2)}" width="${num(w)}" height="${num(size + 4)}" rx="${num((size + 4) / 2)}"/><text class="tb-tag" x="${num(x + w / 2)}" y="${num(cy + size * 0.35)}" font-size="${num(size)}" text-anchor="middle">${esc(text)}</text>` };
|
|
13
|
+
}
|
|
14
|
+
/** The shield's outline as a path, `size` across and as high, its top left at (x, y). */
|
|
15
|
+
export function shieldPath(x, y, size) {
|
|
16
|
+
const p = (u, v) => `${num(x + u * size)} ${num(y + v * size)}`;
|
|
17
|
+
return `M${p(0, 0)}H${num(x + size)}V${num(y + 0.52 * size)}C${p(1, 0.8)} ${p(0.74, 0.95)} ${p(0.5, 1)}C${p(0.26, 0.95)} ${p(0, 0.8)} ${p(0, 0.52)}Z`;
|
|
18
|
+
}
|
|
19
|
+
/** The shield every mark of the drawing is clipped to, written once. Its path is in the unit square of whatever it clips. */
|
|
20
|
+
function shieldClip(ctx) {
|
|
21
|
+
const id = `${ctx.prefix}-sh`;
|
|
22
|
+
ctx.defs ??= new Map();
|
|
23
|
+
if (!ctx.defs.has(id))
|
|
24
|
+
ctx.defs.set(id, `<clipPath id="${id}" clipPathUnits="objectBoundingBox"><path d="M0 0H1V.52C1 .8 .74 .95 .5 1C.26 .95 0 .8 0 .52Z"/></clipPath>`);
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
/** The parts of one box, after its own `<rect class="tb-box">`. */
|
|
28
|
+
export function paintVariant(e) {
|
|
29
|
+
// A box too narrow for a mark and a slot is its two marks stacked with their codes and scores, as the shields are.
|
|
30
|
+
if (e.w < 72 && e.kind !== "shields")
|
|
31
|
+
return shields(e);
|
|
32
|
+
switch (e.kind) {
|
|
33
|
+
case "rows": return rows(e);
|
|
34
|
+
case "shields": return shields(e);
|
|
35
|
+
case "update": return update(e);
|
|
36
|
+
case "columns": return columns(e);
|
|
37
|
+
case "series": return series(e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------------------------- rows
|
|
41
|
+
/** Each team a row: its name on the left, its mark on the right, a slot for the result at the end that faces the centre; a seed tag at the outer corner. What does not fit is left out, the tag first, then the name's length, then the name. */
|
|
42
|
+
function rows(e) {
|
|
43
|
+
const { x, y, w, h, tie, v, ctx } = e;
|
|
44
|
+
const out = [];
|
|
45
|
+
const rowH = h / 2;
|
|
46
|
+
const toRight = !(e.side === "r" && !e.vertical);
|
|
47
|
+
const padX = e.road ? clamp(rowH * 0.16, 5, 10) : 6;
|
|
48
|
+
const gap = 5;
|
|
49
|
+
const font = clamp(rowH * (e.road ? 0.46 : 0.5), 8.5, e.road ? 18 : 15);
|
|
50
|
+
const sf = clamp(rowH * (e.road ? 0.58 : 0.5), 9, e.road ? 20 : 15);
|
|
51
|
+
const tf = clamp(font * 0.62, 7, 9.5);
|
|
52
|
+
const scoreTexts = [0, 1].map((k) => scoreOf(e, k) + pensOf(e, k));
|
|
53
|
+
const slotW = e.scoresOn ? clamp(Math.max(...scoreTexts.map((t) => bw(t, sf))) + 12, 24, 46) : 0;
|
|
54
|
+
const tagWide = e.tags && e.em.tag > 0 ? e.em.tag * tf + 12 : 0;
|
|
55
|
+
const mdMax = ctx.shape === "none" ? 0 : clamp(rowH - (e.road ? 6 : 9), 10, e.road ? 40 : 28);
|
|
56
|
+
const mdMin = mdMax === 0 ? 0 : Math.min(mdMax, 16);
|
|
57
|
+
const nameOf = (k, how) => (tie.sides[k] ? (how === "full" ? tie.sides[k].name : abbrOf(tie.sides[k], v)) : v.words.tbd);
|
|
58
|
+
// The fullest set of parts that fits: the names as they are, then as three letters, then none; each at the size the row gives it, or a smaller one; with the seed tag where there is room for it.
|
|
59
|
+
const fixed = 2 * padX + slotW + (e.scoresOn ? gap : 0);
|
|
60
|
+
const nominal = Math.min(font, 14);
|
|
61
|
+
const tries = [];
|
|
62
|
+
const hows = e.detail === "full" ? ["full", "abbr", null] : e.detail === "compact" ? ["abbr", null] : [null];
|
|
63
|
+
for (const how of hows)
|
|
64
|
+
for (const size of how === null ? [0] : [nominal, 10])
|
|
65
|
+
for (const tag of tagWide > 0 ? [true, false] : [false])
|
|
66
|
+
tries.push({ how, size, tag });
|
|
67
|
+
let chosen = { how: null, size: 0, tag: tagWide > 0 && w - fixed - tagWide - mdMin >= 0, md: Math.max(Math.min(mdMax, w - fixed), 0) };
|
|
68
|
+
for (const t of tries) {
|
|
69
|
+
const need = fixed + (t.tag ? tagWide : 0) + mdMin + (t.how ? gap + (t.how === "full" ? e.em.full : e.em.abbr) * t.size : 0);
|
|
70
|
+
if (need <= w) {
|
|
71
|
+
const spare = w - need;
|
|
72
|
+
chosen = { how: t.how, size: t.size, tag: t.tag, md: mdMax === 0 ? 0 : Math.min(mdMax, mdMin + spare) };
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (mdMax > 0 && chosen.md < 8)
|
|
77
|
+
chosen.md = 0;
|
|
78
|
+
const md = chosen.md;
|
|
79
|
+
e.order.forEach((k, n) => {
|
|
80
|
+
const team = tie.sides[k];
|
|
81
|
+
const ry = y + n * rowH;
|
|
82
|
+
const cy = ry + rowH / 2;
|
|
83
|
+
const parts = [];
|
|
84
|
+
const outer = toRight ? x + padX : x + w - padX;
|
|
85
|
+
const slotX = toRight ? x + w - padX - slotW : x + padX;
|
|
86
|
+
if (e.scoresOn) {
|
|
87
|
+
const slotH = Math.min(rowH - 6, e.road ? 34 : 26);
|
|
88
|
+
parts.push(`<rect class="tb-slot" x="${num(slotX)}" y="${num(cy - slotH / 2)}" width="${num(slotW)}" height="${num(slotH)}" rx="${num(Math.min(5, slotH / 3))}"/>`);
|
|
89
|
+
const s = scoreOf(e, k);
|
|
90
|
+
if (s) {
|
|
91
|
+
const pens = pensOf(e, k);
|
|
92
|
+
const span = pens ? `<tspan class="tb-muted" dx="2" font-size="${num(Math.max(sf * 0.6, 7))}" font-weight="600">${esc(pens)}</tspan>` : "";
|
|
93
|
+
parts.push(`<text class="tb-score" x="${num(slotX + slotW / 2)}" y="${num(cy + sf * 0.35)}" font-size="${num(sf)}" text-anchor="middle" font-variant-numeric="tabular-nums">${esc(s)}${span}</text>`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const inner = e.scoresOn ? (toRight ? slotX : slotX + slotW) : toRight ? x + w - padX : x + padX;
|
|
97
|
+
const markCx = toRight ? inner - (e.scoresOn ? gap : 0) - md / 2 : inner + (e.scoresOn ? gap : 0) + md / 2;
|
|
98
|
+
if (md > 0)
|
|
99
|
+
parts.push(drawMark(team, markCx, cy, md, ctx));
|
|
100
|
+
const markEdge = md > 0 ? (toRight ? markCx - md / 2 : markCx + md / 2) : inner;
|
|
101
|
+
let from = outer;
|
|
102
|
+
const tag = chosen.tag ? e.tagOf(tie, k) : "";
|
|
103
|
+
if (tag) {
|
|
104
|
+
const tw = bw(tag, tf) + 8;
|
|
105
|
+
const pill = tagPill(tag, toRight ? from : from - tw, cy, tf);
|
|
106
|
+
parts.push(pill.svg);
|
|
107
|
+
from = toRight ? from + pill.w + 4 : from - pill.w - 4;
|
|
108
|
+
}
|
|
109
|
+
if (chosen.how !== null) {
|
|
110
|
+
const room = Math.max(toRight ? markEdge - gap - from : from - (markEdge + gap), 8);
|
|
111
|
+
const nm = nameOf(k, chosen.how);
|
|
112
|
+
let nf = chosen.size;
|
|
113
|
+
while (nf > 8 && bw(nm, nf) > room)
|
|
114
|
+
nf -= 0.5;
|
|
115
|
+
parts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(from)}" y="${num(cy + nf * 0.35)}" font-size="${num(nf)}"${toRight ? "" : ' text-anchor="end"'}>${esc(fit(nm, nf, room, bw))}</text>`);
|
|
116
|
+
}
|
|
117
|
+
out.push(e.link(k, `<g class="${e.cls(k)}">${parts.join("")}</g>`, x, ry, w, rowH));
|
|
118
|
+
});
|
|
119
|
+
out.push(`<line class="${e.road ? "tb-sep" : "tb-line"}" x1="${num(x + 4)}" x2="${num(x + w - 4)}" y1="${num(y + rowH)}" y2="${num(y + rowH)}" stroke-opacity=".35"${e.o.theme === "none" ? ' stroke="currentColor"' : ""}/>`);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------------------------- shields
|
|
123
|
+
/** The two marks in shields with "vs" between, a seed tag over each, its three letters under it, and the score under that; a box too small for all of it leaves the tag, the letters, then the score out. */
|
|
124
|
+
function shields(e) {
|
|
125
|
+
const { x, y, w, h, tie, v, ctx } = e;
|
|
126
|
+
const out = [];
|
|
127
|
+
const padX = 5, padY = 4;
|
|
128
|
+
const colW = (w - 2 * padX) / 2;
|
|
129
|
+
const tags = [0, 1].map((k) => (e.tags ? e.tagOf(tie, k) : ""));
|
|
130
|
+
const haveTag = tags.some((t) => t !== "");
|
|
131
|
+
const haveScore = e.scoresOn && tie.result.score !== null;
|
|
132
|
+
const hasPens = haveScore && tie.result.pens !== null;
|
|
133
|
+
const vsText = v.words.vs;
|
|
134
|
+
const vf = (m) => clamp(m * 0.28, 7, 12);
|
|
135
|
+
const lay = (m, tag, code, score) => {
|
|
136
|
+
const cf = clamp(m * 0.26, 7, 11);
|
|
137
|
+
const sf = clamp(m * 0.42, 9, 22);
|
|
138
|
+
const pf = Math.max(sf * 0.55, 7);
|
|
139
|
+
const tagH = tag ? 12 : 0;
|
|
140
|
+
const height = tagH + m + (code ? 3 + cf * 1.25 : 0) + (score ? 3 + sf * 1.15 + (hasPens ? pf * 1.15 : 0) : 0);
|
|
141
|
+
return { cf, sf, pf, tagH, height, code, score };
|
|
142
|
+
};
|
|
143
|
+
// The fullest set of parts that leaves a mark of at least 10 (at least 8, for the smallest box of all).
|
|
144
|
+
const sets = [[haveTag, true, haveScore], [false, true, haveScore], [false, false, haveScore], [false, false, false]];
|
|
145
|
+
let m = 8;
|
|
146
|
+
let L = lay(m, false, false, false);
|
|
147
|
+
for (const [tag, code, score] of sets) {
|
|
148
|
+
// The marks leave room between them for the "vs".
|
|
149
|
+
const room = Math.min(colW - 2 - (colW >= 26 ? bw(vsText, 10) + 6 : 0), 56, h - 2 * padY);
|
|
150
|
+
let cand = room;
|
|
151
|
+
let got = lay(cand, tag, code, score);
|
|
152
|
+
while (cand > 10 && got.height > h - 2 * padY) {
|
|
153
|
+
cand -= 0.5;
|
|
154
|
+
got = lay(cand, tag, code, score);
|
|
155
|
+
}
|
|
156
|
+
if (cand >= 10 && got.height <= h - 2 * padY) {
|
|
157
|
+
m = cand;
|
|
158
|
+
L = got;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (L.height > h - 2 * padY || m > colW - 2) {
|
|
163
|
+
m = Math.max(6, Math.min(colW - 2, h - 2 * padY));
|
|
164
|
+
L = lay(m, false, false, false);
|
|
165
|
+
}
|
|
166
|
+
const top = y + (h - L.height) / 2;
|
|
167
|
+
const clip = shieldClip(ctx);
|
|
168
|
+
const square = { ...ctx, shape: ctx.shape === "none" ? "none" : "square" };
|
|
169
|
+
e.order.forEach((k, n) => {
|
|
170
|
+
const team = tie.sides[k];
|
|
171
|
+
const cx = x + padX + colW / 2 + n * colW;
|
|
172
|
+
const parts = [];
|
|
173
|
+
let yy = top;
|
|
174
|
+
if (L.tagH && tags[k]) {
|
|
175
|
+
const tw = bw(tags[k], 8) + 8;
|
|
176
|
+
parts.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>`);
|
|
177
|
+
}
|
|
178
|
+
yy += L.tagH;
|
|
179
|
+
const sx = cx - m / 2;
|
|
180
|
+
if (team && ctx.shape !== "none")
|
|
181
|
+
parts.push(`<g clip-path="url(#${clip})">${drawMark(team, cx, yy + m / 2, m, square)}</g>`);
|
|
182
|
+
parts.push(`<path class="tb-shield${team ? "" : " tb-empty"}" d="${shieldPath(sx, yy, m)}"/>`);
|
|
183
|
+
yy += m;
|
|
184
|
+
if (L.code) {
|
|
185
|
+
yy += 3;
|
|
186
|
+
const code = team ? abbrOf(team, v) : v.words.tbd;
|
|
187
|
+
const room = colW - 2;
|
|
188
|
+
if (bw(code, L.cf) <= room)
|
|
189
|
+
parts.push(`<text class="tb-code" x="${num(cx)}" y="${num(yy + L.cf * 0.75)}" font-size="${num(L.cf)}" text-anchor="middle">${esc(code)}</text>`);
|
|
190
|
+
yy += L.cf * 1.25;
|
|
191
|
+
}
|
|
192
|
+
if (L.score) {
|
|
193
|
+
yy += 3;
|
|
194
|
+
parts.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(e, k))}</text>`);
|
|
195
|
+
yy += L.sf * 1.15;
|
|
196
|
+
const pens = pensOf(e, k);
|
|
197
|
+
if (pens)
|
|
198
|
+
parts.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>`);
|
|
199
|
+
}
|
|
200
|
+
out.push(e.link(k, `<g class="${e.cls(k)}">${parts.join("")}</g>`, cx - colW / 2, y, colW, h));
|
|
201
|
+
});
|
|
202
|
+
const f = vf(m);
|
|
203
|
+
if (colW - m >= bw(vsText, f) + 4)
|
|
204
|
+
out.push(`<text class="tb-vs" x="${num(x + w / 2)}" y="${num(top + L.tagH + m / 2 + f * 0.35)}" font-size="${num(f)}" text-anchor="middle">${esc(vsText)}</text>`);
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------------------------- update
|
|
208
|
+
/** A compact card: each team's abbreviation and seed on the left, its score in a cell on the right; a team not known yet is an empty bar. */
|
|
209
|
+
function update(e) {
|
|
210
|
+
const { x, y, w, h, tie, v } = e;
|
|
211
|
+
const out = [];
|
|
212
|
+
const rowH = h / 2;
|
|
213
|
+
const padX = e.road ? clamp(rowH * 0.18, 5, 10) : 6;
|
|
214
|
+
const font = clamp(rowH * (e.road ? 0.5 : 0.52), 8.5, e.road ? 19 : 15);
|
|
215
|
+
const sf = clamp(rowH * (e.road ? 0.6 : 0.52), 9, e.road ? 20 : 15);
|
|
216
|
+
const cellH = Math.min(rowH - 6, e.road ? 30 : 24);
|
|
217
|
+
const texts = [0, 1].map((k) => scoreOf(e, k));
|
|
218
|
+
const cellW = e.scoresOn ? clamp(Math.max(...texts.map((t) => bw(t, sf))) + 10, cellH, 44) : 0;
|
|
219
|
+
const sm = Math.max(font * 0.72, 7.5);
|
|
220
|
+
e.order.forEach((k, n) => {
|
|
221
|
+
const team = tie.sides[k];
|
|
222
|
+
const ry = y + n * rowH;
|
|
223
|
+
const cy = ry + rowH / 2;
|
|
224
|
+
const parts = [];
|
|
225
|
+
const cellX = x + w - padX - cellW;
|
|
226
|
+
if (e.scoresOn) {
|
|
227
|
+
parts.push(`<rect class="tb-slot" x="${num(cellX)}" y="${num(cy - cellH / 2)}" width="${num(cellW)}" height="${num(cellH)}" rx="${num(Math.min(5, cellH / 3))}"/>`);
|
|
228
|
+
const s = scoreOf(e, k);
|
|
229
|
+
if (s)
|
|
230
|
+
parts.push(`<text class="tb-score" x="${num(cellX + cellW / 2)}" y="${num(cy + sf * 0.35)}" font-size="${num(sf)}" text-anchor="middle" font-variant-numeric="tabular-nums">${esc(s)}</text>`);
|
|
231
|
+
}
|
|
232
|
+
const right = (e.scoresOn ? cellX - 5 : x + w - padX);
|
|
233
|
+
if (team) {
|
|
234
|
+
const code = abbrOf(team, v);
|
|
235
|
+
const tag = e.tags ? e.tagOf(tie, k) : "";
|
|
236
|
+
let nf = font;
|
|
237
|
+
const need = (size) => bw(code, size) + (tag ? bw(tag, Math.max(size * 0.72, 7.5)) + 5 : 0);
|
|
238
|
+
while (nf > 9 && need(nf) > right - (x + padX))
|
|
239
|
+
nf -= 0.5;
|
|
240
|
+
const shown = fit(code, nf, right - (x + padX), bw);
|
|
241
|
+
parts.push(`<text class="tb-name" x="${num(x + padX)}" y="${num(cy + nf * 0.35)}" font-size="${num(nf)}" font-weight="700">${esc(shown)}</text>`);
|
|
242
|
+
const at = x + padX + bw(shown, nf) + 5;
|
|
243
|
+
const sz = Math.max(nf * 0.72, 7.5);
|
|
244
|
+
if (tag && at + bw(tag, sz) <= right)
|
|
245
|
+
parts.push(`<text class="tb-muted tb-seed" x="${num(at)}" y="${num(cy + sz * 0.35)}" font-size="${num(sz)}" font-weight="600">${esc(tag)}</text>`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
parts.push(`<rect class="tb-emptybar" x="${num(x + padX)}" y="${num(cy - 2.5)}" width="${num(Math.max(Math.min(right - (x + padX), 34), 8))}" height="5" rx="2.5"/>`);
|
|
249
|
+
}
|
|
250
|
+
out.push(e.link(k, `<g class="${e.cls(k)}">${parts.join("")}</g>`, x, ry, w, rowH));
|
|
251
|
+
});
|
|
252
|
+
void sm;
|
|
253
|
+
out.push(`<line class="${e.road ? "tb-sep" : "tb-line"}" x1="${num(x + 4)}" x2="${num(x + w - 4)}" y1="${num(y + rowH)}" y2="${num(y + rowH)}" stroke-opacity=".35"${e.o.theme === "none" ? ' stroke="currentColor"' : ""}/>`);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
// ---------------------------------------------------------------------------------------------- series
|
|
257
|
+
/** The width a one-line series needs: "1 BOS 4–0 MIA 8" set at `size`. */
|
|
258
|
+
export function seriesLineWidth(tie, v, size, scoresOn) {
|
|
259
|
+
const code = (k) => (tie.sides[k] ? abbrOf(tie.sides[k], v) : v.words.tbd);
|
|
260
|
+
const score = scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
|
|
261
|
+
return bw(code(0), size) + bw(code(1), size) + bw(score, size) + 4 * size * 0.6 + 8 * 2 + 2 * bw("00", size * 0.72);
|
|
262
|
+
}
|
|
263
|
+
const fillScore = (tie, v) => (tie.result.score ? v.words.scoreText.replace("{a}", v.number(tie.result.score[0])).replace("{b}", v.number(tie.result.score[1])) : "");
|
|
264
|
+
/** One line a series: "(1) BOS 4–0 MIA (8)", the winner in bold and the loser quiet. Where the box is too narrow for a line, two compact rows. */
|
|
265
|
+
function series(e) {
|
|
266
|
+
const { x, y, w, h, tie, v } = e;
|
|
267
|
+
const font = clamp(h * 0.38, 8.5, e.road ? 16 : 14);
|
|
268
|
+
const need = (size) => seriesLineWidth(tie, v, size, e.scoresOn);
|
|
269
|
+
let size = font;
|
|
270
|
+
while (size > 8.5 && need(size) > w - 8)
|
|
271
|
+
size -= 0.5;
|
|
272
|
+
if (need(size) > w - 8)
|
|
273
|
+
return update(e);
|
|
274
|
+
const [a, b] = e.order;
|
|
275
|
+
const cy = y + h / 2;
|
|
276
|
+
const score = e.scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
|
|
277
|
+
const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ? v.words.scoreText.replace("{a}", v.number(tie.result.score[1])).replace("{b}", v.number(tie.result.score[0])) : score;
|
|
278
|
+
const sw = bw(scoreOrdered, size);
|
|
279
|
+
const cx = x + w / 2;
|
|
280
|
+
const ss = Math.max(size * 0.72, 7.5);
|
|
281
|
+
const part = (k, left) => {
|
|
282
|
+
const team = tie.sides[k];
|
|
283
|
+
const code = team ? abbrOf(team, v) : v.words.tbd;
|
|
284
|
+
const tag = e.tags ? e.tagOf(tie, k) : "";
|
|
285
|
+
const edge = left ? cx - sw / 2 - 6 : cx + sw / 2 + 6;
|
|
286
|
+
const seed = tag ? `<tspan class="tb-muted tb-seed" font-size="${num(ss)}" font-weight="600">${esc(tag)}</tspan>` : "";
|
|
287
|
+
// The left team's seed comes before its code, the right team's after: the two seeds are the line's ends.
|
|
288
|
+
const body = left ? `${seed}${tag ? " " : ""}${esc(code)}` : `${esc(code)}${tag ? " " : ""}${seed}`;
|
|
289
|
+
return `<g class="${e.cls(k)}"><text class="tb-name${team ? "" : " tb-muted"}" x="${num(edge)}" y="${num(cy + size * 0.35)}" font-size="${num(size)}"${left ? ' text-anchor="end"' : ""}>${body}</text></g>`;
|
|
290
|
+
};
|
|
291
|
+
return [
|
|
292
|
+
e.link(a, part(a, true), x, y, w / 2, h),
|
|
293
|
+
`<text class="tb-score tb-series" x="${num(cx)}" y="${num(cy + size * 0.35)}" font-size="${num(size)}" text-anchor="middle" font-variant-numeric="tabular-nums">${esc(scoreOrdered)}</text>`,
|
|
294
|
+
e.link(b, part(b, false), x + w / 2, y, w / 2, h),
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
// ---------------------------------------------------------------------------------------------- columns
|
|
298
|
+
/** Tall slots, the two sides of a tie one over the other (the final's side by side) with a "VS" or "AT" tag between them, and each team's seed as a big number in its slot. */
|
|
299
|
+
function columns(e) {
|
|
300
|
+
const { x, y, w, h, tie, v, ctx } = e;
|
|
301
|
+
const out = [];
|
|
302
|
+
const pad = 4;
|
|
303
|
+
const tagText = v.upper(e.o.at === true ? v.words.at : v.words.vs);
|
|
304
|
+
const tf = 8;
|
|
305
|
+
const tagW = bw(tagText, tf) + 10;
|
|
306
|
+
const tagH = 13;
|
|
307
|
+
const sideBySide = e.final;
|
|
308
|
+
// `at`: the visiting side is drawn first and the side at home second.
|
|
309
|
+
let order = e.order;
|
|
310
|
+
if (e.o.at === true && tie.home !== undefined && order[1] !== tie.home)
|
|
311
|
+
order = [order[1], order[0]];
|
|
312
|
+
const slots = sideBySide
|
|
313
|
+
? [0, 1].map((n) => ({ x: x + pad + n * ((w - 2 * pad + (tagW + 6)) / 2), y: y + pad, w: (w - 2 * pad - (tagW + 6)) / 2, h: h - 2 * pad }))
|
|
314
|
+
: [0, 1].map((n) => ({ x: x + pad, y: y + pad + n * ((h - 2 * pad + tagH + 4) / 2), w: w - 2 * pad, h: (h - 2 * pad - tagH - 4) / 2 }));
|
|
315
|
+
order.forEach((k, n) => {
|
|
316
|
+
const team = tie.sides[k];
|
|
317
|
+
const s = slots[n];
|
|
318
|
+
const parts = [];
|
|
319
|
+
parts.push(`<rect class="tb-slot tb-cslot" x="${num(s.x)}" y="${num(s.y)}" width="${num(s.w)}" height="${num(s.h)}" rx="${num(Math.min(6, s.h / 4))}"/>`);
|
|
320
|
+
const rawSeed = tie.sides[k]?.seed ?? e.tagOf(tie, k);
|
|
321
|
+
const seed = rawSeed === undefined || rawSeed === null || rawSeed === "" ? "" : String(rawSeed).slice(0, 3);
|
|
322
|
+
const seedF = seed ? clamp(Math.min(s.h * 0.4, s.w * 0.3), 11, 26) : 0;
|
|
323
|
+
const seedW = seed ? bw(seed, seedF) + 6 : 0;
|
|
324
|
+
// The seed is a big number at the slot's outer edge; the rest is laid out in what is left.
|
|
325
|
+
let cx0 = s.x + 4 + seedW, cw = s.w - 8 - seedW;
|
|
326
|
+
if (seed && cw < 36) {
|
|
327
|
+
cx0 = s.x + 4;
|
|
328
|
+
cw = s.w - 8;
|
|
329
|
+
}
|
|
330
|
+
const showSeed = seed !== "" && cx0 > s.x + 4;
|
|
331
|
+
if (showSeed)
|
|
332
|
+
parts.push(`<text class="tb-seedn" x="${num(s.x + 6)}" y="${num(s.y + s.h / 2 + seedF * 0.35)}" font-size="${num(seedF)}" font-weight="800">${esc(seed)}</text>`);
|
|
333
|
+
const score = scoreOf(e, k);
|
|
334
|
+
const pens = pensOf(e, k);
|
|
335
|
+
const code = team ? abbrOf(team, v) : v.words.tbd;
|
|
336
|
+
const tall = s.h >= 64 && cw >= 34;
|
|
337
|
+
if (tall) {
|
|
338
|
+
const cf = clamp(s.h * 0.15, 8, 12);
|
|
339
|
+
let sf = score ? clamp(s.h * 0.3, 10, 22) : 0;
|
|
340
|
+
const wantMark = ctx.shape === "none" ? 0 : 1;
|
|
341
|
+
const stackH = (m) => m * wantMark + (wantMark ? 3 : 0) + cf * 1.25 + (score ? 3 + sf * 1.15 : 0);
|
|
342
|
+
let m = Math.max(10, Math.min(cw - 4, 46, s.h - 8));
|
|
343
|
+
while (m > 10 && stackH(m) > s.h - 8)
|
|
344
|
+
m -= 0.5;
|
|
345
|
+
while (sf > 10 && stackH(m) > s.h - 8)
|
|
346
|
+
sf -= 0.5;
|
|
347
|
+
let yy = s.y + (s.h - stackH(m)) / 2;
|
|
348
|
+
const cx = cx0 + cw / 2;
|
|
349
|
+
if (wantMark) {
|
|
350
|
+
parts.push(drawMark(team, cx, yy + m / 2, m, ctx));
|
|
351
|
+
yy += m + 3;
|
|
352
|
+
}
|
|
353
|
+
parts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(cx)}" y="${num(yy + cf * 0.75)}" font-size="${num(cf)}" font-weight="700" text-anchor="middle">${esc(e.detail === "full" && team && bw(team.name, cf) <= cw ? team.name : fit(code, cf, cw, bw))}</text>`);
|
|
354
|
+
yy += cf * 1.25;
|
|
355
|
+
if (score) {
|
|
356
|
+
yy += 3;
|
|
357
|
+
const span = pens ? `<tspan class="tb-muted" dx="2" font-size="${num(Math.max(sf * 0.55, 7))}" font-weight="600">${esc(pens)}</tspan>` : "";
|
|
358
|
+
parts.push(`<text class="tb-score" x="${num(cx)}" y="${num(yy + sf * 0.8)}" font-size="${num(sf)}" text-anchor="middle" font-variant-numeric="tabular-nums">${esc(score)}${span}</text>`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// A low slot: the mark, the three letters and the score in a row.
|
|
363
|
+
const cy = s.y + s.h / 2;
|
|
364
|
+
const m = ctx.shape === "none" ? 0 : clamp(s.h - 8, 10, 30);
|
|
365
|
+
const sf = clamp(s.h * 0.5, 9, 16);
|
|
366
|
+
const cf = clamp(s.h * 0.4, 8, 13);
|
|
367
|
+
let from = cx0;
|
|
368
|
+
if (m) {
|
|
369
|
+
parts.push(drawMark(team, from + m / 2, cy, m, ctx));
|
|
370
|
+
from += m + 4;
|
|
371
|
+
}
|
|
372
|
+
const scoreW = score ? bw(score + pens, sf) + 4 : 0;
|
|
373
|
+
const room = cx0 + cw - from - scoreW;
|
|
374
|
+
if (room >= 12)
|
|
375
|
+
parts.push(`<text class="tb-name${team ? "" : " tb-muted"}" x="${num(from)}" y="${num(cy + cf * 0.35)}" font-size="${num(cf)}" font-weight="700">${esc(fit(code, cf, room, bw))}</text>`);
|
|
376
|
+
if (score) {
|
|
377
|
+
const span = pens ? `<tspan class="tb-muted" dx="2" font-size="${num(Math.max(sf * 0.55, 7))}" font-weight="600">${esc(pens)}</tspan>` : "";
|
|
378
|
+
parts.push(`<text class="tb-score" x="${num(cx0 + cw)}" y="${num(cy + sf * 0.35)}" font-size="${num(sf)}" text-anchor="end" font-variant-numeric="tabular-nums">${esc(score)}${span}</text>`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
out.push(e.link(k, `<g class="${e.cls(k)}">${parts.join("")}</g>`, s.x, s.y, s.w, s.h));
|
|
382
|
+
});
|
|
383
|
+
// The tag between the two slots.
|
|
384
|
+
const tcx = sideBySide ? slots[0].x + slots[0].w + (slots[1].x - slots[0].x - slots[0].w) / 2 : x + w / 2;
|
|
385
|
+
const tcy = sideBySide ? y + h / 2 : slots[0].y + slots[0].h + (slots[1].y - slots[0].y - slots[0].h) / 2;
|
|
386
|
+
out.push(`<rect class="tb-atag" x="${num(tcx - tagW / 2)}" y="${num(tcy - tagH / 2)}" width="${num(tagW)}" height="${num(tagH)}" rx="${num(tagH / 2)}"/><text class="tb-atag-t" x="${num(tcx)}" y="${num(tcy + tf * 0.35)}" font-size="${tf}" font-weight="800" letter-spacing=".08em" text-anchor="middle">${esc(tagText)}</text>`);
|
|
387
|
+
return out;
|
|
388
|
+
}
|
package/dist/locales.js
CHANGED
|
@@ -19,6 +19,8 @@ const EN = {
|
|
|
19
19
|
roadCaption: "{a} and {b}: every match on the way to the final. Scores are written with the team in the first column first.",
|
|
20
20
|
roadName: "{a} and {b}: {title}", roadSummary: "Matches played before the final: {a} ({an}) and {b} ({bn}).",
|
|
21
21
|
roadFinal: "The final: {a} {score} {b} ({how}).", roadChampion: "{name} won the final.", roadToPlay: "The final is still to be played.",
|
|
22
|
+
vs: "vs", at: "AT", groupName: "Group {n}", colPlayed: "P", colWon: "W", colDrawn: "D", colLost: "L", colDiff: "GD", colPoints: "Pts",
|
|
23
|
+
playIn: "Play-in", playInWinner: "Winner {a}/{b}", playInLoser: "Loser {a}/{b}", seedSlot: "Seed {n}", winnerOf: "Winner of {name}", loserOf: "Loser of {name}",
|
|
22
24
|
};
|
|
23
25
|
const FR = {
|
|
24
26
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -34,6 +36,8 @@ const FR = {
|
|
|
34
36
|
roadCaption: "{a} et {b} : tous les matchs jusqu’à la finale. Chaque score commence par celui de l’équipe de la première colonne.",
|
|
35
37
|
roadName: "{a} et {b} : {title}", roadSummary: "Matchs joués avant la finale : {a} ({an}) et {b} ({bn}).",
|
|
36
38
|
roadFinal: "La finale : {a} {score} {b} ({how}).", roadChampion: "{name} a remporté la finale.", roadToPlay: "La finale reste à jouer.",
|
|
39
|
+
vs: "vs", at: "À", groupName: "Groupe {n}", colPlayed: "PJ", colWon: "V", colDrawn: "N", colLost: "D", colDiff: "+/-", colPoints: "Pts",
|
|
40
|
+
playIn: "Play-in", playInWinner: "Gagnant {a}/{b}", playInLoser: "Perdant {a}/{b}", seedSlot: "N°\u00a0{n}", winnerOf: "Gagnant du match {name}", loserOf: "Perdant du match {name}",
|
|
37
41
|
};
|
|
38
42
|
const JA = {
|
|
39
43
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -49,6 +53,8 @@ const JA = {
|
|
|
49
53
|
roadCaption: "{a}と{b}:決勝までの全試合。スコアは「チーム」列のチームの得点を先に書いています。",
|
|
50
54
|
roadName: "{a}と{b}:{title}", roadSummary: "{a}は決勝までに{an}試合、{b}は{bn}試合を戦いました。",
|
|
51
55
|
roadFinal: "決勝は{a} {score} {b}({how})でした。", roadChampion: "{name}が優勝しました。", roadToPlay: "決勝はまだ行われていません。",
|
|
56
|
+
vs: "対", at: "@", groupName: "グループ{n}", colPlayed: "試合", colWon: "勝", colDrawn: "分", colLost: "敗", colDiff: "得失差", colPoints: "勝点",
|
|
57
|
+
playIn: "プレイイン", playInWinner: "{a}/{b}の勝者", playInLoser: "{a}/{b}の敗者", seedSlot: "第{n}シード", winnerOf: "{name}の勝者", loserOf: "{name}の敗者",
|
|
52
58
|
};
|
|
53
59
|
const ES = {
|
|
54
60
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -64,6 +70,8 @@ const ES = {
|
|
|
64
70
|
roadCaption: "{a} y {b}: todos los partidos hasta la final. Los marcadores empiezan por el equipo de la primera columna.",
|
|
65
71
|
roadName: "{a} y {b}: {title}", roadSummary: "Partidos jugados hasta la final: {a} ({an}) y {b} ({bn}).",
|
|
66
72
|
roadFinal: "La final: {a} {score} {b} ({how}).", roadChampion: "{name} ganó la final.", roadToPlay: "La final aún no se ha jugado.",
|
|
73
|
+
vs: "vs.", at: "EN", groupName: "Grupo {n}", colPlayed: "PJ", colWon: "G", colDrawn: "E", colLost: "P", colDiff: "DG", colPoints: "Pts",
|
|
74
|
+
playIn: "Play-in", playInWinner: "Ganador {a}/{b}", playInLoser: "Perdedor {a}/{b}", seedSlot: "Puesto {n}", winnerOf: "Ganador de {name}", loserOf: "Perdedor de {name}",
|
|
67
75
|
};
|
|
68
76
|
const DE = {
|
|
69
77
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -79,6 +87,8 @@ const DE = {
|
|
|
79
87
|
roadCaption: "{a} und {b}: alle Spiele auf dem Weg ins Finale. Die Ergebnisse beginnen mit dem Team der ersten Spalte.",
|
|
80
88
|
roadName: "{a} und {b}: {title}", roadSummary: "Spiele bis zum Finale: {a} ({an}) und {b} ({bn}).",
|
|
81
89
|
roadFinal: "Das Finale: {a} {score} {b} ({how}).", roadChampion: "{name} gewann das Finale.", roadToPlay: "Das Finale steht noch aus.",
|
|
90
|
+
vs: "vs.", at: "BEI", groupName: "Gruppe {n}", colPlayed: "Sp", colWon: "S", colDrawn: "U", colLost: "N", colDiff: "Diff.", colPoints: "Pkt",
|
|
91
|
+
playIn: "Play-in", playInWinner: "Sieger {a}/{b}", playInLoser: "Verlierer {a}/{b}", seedSlot: "Platz {n}", winnerOf: "Sieger von {name}", loserOf: "Verlierer von {name}",
|
|
82
92
|
};
|
|
83
93
|
/**
|
|
84
94
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/marks.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface MarkContext {
|
|
|
47
47
|
viewBox: string;
|
|
48
48
|
inner: string;
|
|
49
49
|
} | null>;
|
|
50
|
+
/** Other `<defs>` the drawing's shapes need (the shields' clip), each written once under its id. */
|
|
51
|
+
defs?: Map<string, string>;
|
|
50
52
|
}
|
|
51
53
|
/** Whether an address may be a link in a drawing: http(s), a relative path or a `#` fragment. `javascript:`, `data:` and every other scheme are refused. */
|
|
52
54
|
export declare function safeHref(url: string): boolean;
|
package/dist/marks.js
CHANGED
|
@@ -114,6 +114,7 @@ export function markDefs(ctx) {
|
|
|
114
114
|
return [
|
|
115
115
|
...[...ctx.radii].sort((a, b) => a - b).map((r) => `<clipPath id="${clipId(ctx.prefix, r)}"><circle r="${num(r)}"/></clipPath>`),
|
|
116
116
|
...[...ctx.symbols.values()].map((s) => `<symbol id="${s.id}" viewBox="${esc(s.viewBox)}" preserveAspectRatio="xMidYMid slice">${s.inner}</symbol>`),
|
|
117
|
+
...(ctx.defs ? [...ctx.defs.values()] : []),
|
|
117
118
|
].join("");
|
|
118
119
|
}
|
|
119
120
|
/** The id of the circular clip for a radius. */
|
package/dist/matchup.js
CHANGED
|
@@ -103,6 +103,7 @@ export function resolveMatchup(m, index, teams) {
|
|
|
103
103
|
walkover: !!m.walkover,
|
|
104
104
|
note: m.note ?? null,
|
|
105
105
|
...(m.seeds ? { seeds: m.seeds } : {}),
|
|
106
|
+
...(m.home !== undefined ? { home: m.home } : {}),
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
/** The short name of a team for a small box. */
|
package/dist/playin.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BracketLayout, DrawOptions, Size } from "./types.ts";
|
|
2
|
+
import { type Voice } from "./locales.ts";
|
|
3
|
+
import { type MarkContext } from "./marks.ts";
|
|
4
|
+
import type { OwnDrawing } from "./groups.ts";
|
|
5
|
+
/** Draws the play-in games of each half. */
|
|
6
|
+
export declare function drawPlayIn(layout: BracketLayout, o: DrawOptions, v: Voice, size: Size, ctx: MarkContext, road: boolean, linked: {
|
|
7
|
+
count: number;
|
|
8
|
+
}): OwnDrawing;
|