@johnmorrisdotca/taisen-brackets 0.6.0 → 0.7.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 +30 -2
- package/NOTICE.md +4 -2
- package/README.md +64 -23
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/judo-model.d.ts +66 -0
- package/dist/judo-model.js +102 -0
- package/dist/judo-text.d.ts +19 -0
- package/dist/judo-text.js +84 -0
- package/dist/judo.d.ts +44 -0
- package/dist/judo.js +75 -0
- package/dist/layout.js +2 -2
- package/dist/layouts.js +4 -3
- package/dist/locales.js +25 -0
- package/dist/matchup.js +41 -8
- package/dist/playin.js +2 -1
- package/dist/render.js +37 -18
- package/dist/table.js +3 -2
- package/dist/theme.d.ts +9 -0
- package/dist/theme.js +5 -0
- package/dist/types.d.ts +49 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/judo.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { layoutBracket } from "./layout.js";
|
|
2
|
+
import { singleElimination } from "./seeding.js";
|
|
3
|
+
import { fill, voiceOf } from "./locales.js";
|
|
4
|
+
export { JUDO_WEIGHT_CLASSES, judoOutcome, judoSeconds, judoClock } from "./judo-model.js";
|
|
5
|
+
/**
|
|
6
|
+
* The contests of one judo weight category for any number of athletes, seeded in the standard way (the top seeds have the byes when the number is
|
|
7
|
+
* not a power of two). The athlete drawn first is white and the second blue (`Matchup.sides`: first white).
|
|
8
|
+
*
|
|
9
|
+
* With `repechage` (IJF Sport and Organisation Rules, 2.5.2 "Quarter-final Repechage", the system of the Olympic Games): the athletes are in two tables
|
|
10
|
+
* and an elimination produces two finalists. "The athletes defeated in the quarter-final will compete in two (2) repechage contests. The winners of each of
|
|
11
|
+
* these two (2) repechage contests will compete in bronze medal contests against the loser of the semi-final contest of the respective opposite table.
|
|
12
|
+
* The winners (2) of those contests are placed third." The repechage and bronze contests are `stage: "repechage"` and `stage: "bronze"` matchups, fed by
|
|
13
|
+
* the losers of the tree's, and are drawn under it. Needs eight athletes or more. With `direct`, there are no such contests: the two losing semi-finalists are
|
|
14
|
+
* the bronze medallists (`judoPodium`).
|
|
15
|
+
*
|
|
16
|
+
* Double repechage and full repechage (2.5.3 and 2.5.4) are not drawn: their pools are laid out in the rules' Appendix G charts.
|
|
17
|
+
*/
|
|
18
|
+
export function judoBracket(athletes, options = {}) {
|
|
19
|
+
const system = options.system ?? (athletes.length >= 8 ? "repechage" : "direct");
|
|
20
|
+
if (system !== "repechage" && system !== "direct")
|
|
21
|
+
throw new RangeError(`taisen-brackets: a judo system is "repechage" or "direct", not ${JSON.stringify(system)}`);
|
|
22
|
+
if (athletes.length < 4)
|
|
23
|
+
throw new RangeError("taisen-brackets: a judo bracket needs four athletes or more (the two losing semi-finalists are bronze medallists)");
|
|
24
|
+
if (system === "repechage" && athletes.length < 8)
|
|
25
|
+
throw new RangeError("taisen-brackets: a quarter-final repechage needs eight athletes or more; use system: \"direct\" for fewer");
|
|
26
|
+
const prefix = options.prefix ?? "J";
|
|
27
|
+
const made = singleElimination(athletes, { prefix });
|
|
28
|
+
if (system === "direct")
|
|
29
|
+
return made;
|
|
30
|
+
const rounds = Math.max(...made.matchups.map((m) => m.round ?? 0));
|
|
31
|
+
const semis = made.matchups.filter((m) => m.round === rounds - 1);
|
|
32
|
+
const quarters = (semi) => {
|
|
33
|
+
const [a, b] = semi.feeds ?? [];
|
|
34
|
+
if (typeof a !== "string" || typeof b !== "string")
|
|
35
|
+
throw new Error("taisen-brackets: a semi-final of a repechage bracket needs two quarter-finals feeding it");
|
|
36
|
+
return [a, b];
|
|
37
|
+
};
|
|
38
|
+
const loser = (tie) => ({ tie, result: "loser" });
|
|
39
|
+
const extra = [];
|
|
40
|
+
semis.forEach((semi, k) => {
|
|
41
|
+
const [q1, q2] = quarters(semi);
|
|
42
|
+
extra.push({ id: `${prefix}rep-${k + 1}`, sides: [null, null], feeds: [loser(q1), loser(q2)], stage: "repechage" });
|
|
43
|
+
});
|
|
44
|
+
semis.forEach((semi, k) => {
|
|
45
|
+
// The repechage winner of one table meets the semi-final loser of the other.
|
|
46
|
+
const other = semis[1 - k];
|
|
47
|
+
extra.push({ id: `${prefix}bronze-${k + 1}`, sides: [null, null], feeds: [`${prefix}rep-${k + 1}`, loser(other.id)], stage: "bronze" });
|
|
48
|
+
});
|
|
49
|
+
return { teams: made.teams, matchups: [...made.matchups, ...extra] };
|
|
50
|
+
}
|
|
51
|
+
/** The gold, silver and two bronze medals of a bracket (matchups, input or layout), from the final, the bronze medal contests or the semi-finals. */
|
|
52
|
+
export function judoPodium(input) {
|
|
53
|
+
const layout = "ties" in input && "sides" in input ? input : layoutBracket(input);
|
|
54
|
+
const fin = layout.finalIndex === null ? null : layout.ties[layout.finalIndex];
|
|
55
|
+
const gold = fin && fin.winner !== null ? fin.sides[fin.winner] : null;
|
|
56
|
+
const silver = fin && fin.winner !== null ? fin.sides[(1 - fin.winner)] : null;
|
|
57
|
+
const bronzeTies = layout.ties.filter((t) => t.stage === "bronze");
|
|
58
|
+
let bronze;
|
|
59
|
+
if (bronzeTies.length)
|
|
60
|
+
bronze = bronzeTies.flatMap((t) => (t.winner === null || !t.sides[t.winner] ? [] : [t.sides[t.winner]]));
|
|
61
|
+
else {
|
|
62
|
+
const semis = layout.finalIndex === null ? [] : layout.ties.filter((t) => t.round === layout.rounds - 1 && t.stage === undefined && !t.third && layout.place[t.index] !== "o");
|
|
63
|
+
bronze = semis.flatMap((t) => (t.winner === null || !t.sides[(1 - t.winner)] ? [] : [t.sides[(1 - t.winner)]]));
|
|
64
|
+
}
|
|
65
|
+
return { gold, silver, bronze };
|
|
66
|
+
}
|
|
67
|
+
/** A weight category as a heading in a language: "Men -66 kg", "Women +78 kg". */
|
|
68
|
+
export function judoCategoryLabel(category, locale) {
|
|
69
|
+
if (category.sex !== "men" && category.sex !== "women")
|
|
70
|
+
throw new RangeError(`taisen-brackets: a judo category's sex is "men" or "women", not ${JSON.stringify(category.sex)}`);
|
|
71
|
+
if (!/^[-+]\d{2,3}$/.test(category.weight))
|
|
72
|
+
throw new RangeError(`taisen-brackets: a judo weight is "-66" or "+100", not ${JSON.stringify(category.weight)}`);
|
|
73
|
+
const w = voiceOf({ locale }).words;
|
|
74
|
+
return fill(category.weight.startsWith("+") ? w.judoWeightOver : w.judoWeightClass, { sex: category.sex === "men" ? w.judoMen : w.judoWomen, w: category.weight.slice(1) });
|
|
75
|
+
}
|
package/dist/layout.js
CHANGED
|
@@ -17,8 +17,8 @@ export function layoutBracket(input, options = {}) {
|
|
|
17
17
|
throw new Error(`taisen-brackets: two matchups have the id ${JSON.stringify(t.id)}`);
|
|
18
18
|
byId.set(t.id, t.index);
|
|
19
19
|
}
|
|
20
|
-
// A play-in game is outside the tree; so are a third-place match and a round-robin game.
|
|
21
|
-
const playIns = ties.filter((t) => matchups[t.index].stage
|
|
20
|
+
// A play-in game is outside the tree, and so are a judo repechage and bronze medal contest (a stage's ties are fed by the tree's losers); so are a third-place match and a round-robin game.
|
|
21
|
+
const playIns = ties.filter((t) => matchups[t.index].stage !== undefined).map((t) => t.index);
|
|
22
22
|
const isPlayIn = new Set(playIns);
|
|
23
23
|
const thirds = ties.filter((t) => t.third && !isPlayIn.has(t.index)).map((t) => t.index);
|
|
24
24
|
const robin = ties.filter((t) => !t.third && !isPlayIn.has(t.index) && matchups[t.index].round === 0).map((t) => t.index);
|
package/dist/layouts.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { abbrOf } from "./matchup.js";
|
|
2
|
+
import { sideScore } from "./judo-text.js";
|
|
2
3
|
import { drawMark } from "./marks.js";
|
|
3
4
|
import { emWidth, esc, fit, num } from "./text.js";
|
|
4
5
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
5
6
|
/** 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
7
|
const bw = (text, size) => emWidth(text) * size;
|
|
7
|
-
const scoreOf = (e, k) => (e.scoresOn
|
|
8
|
+
const scoreOf = (e, k) => (e.scoresOn ? sideScore(e.tie, k, e.v) : "");
|
|
8
9
|
const pensOf = (e, k) => (e.scoresOn && e.tie.result.pens ? `(${e.v.number(e.tie.result.pens[k])})` : "");
|
|
9
10
|
/** A small pill with a seed tag, its left edge at `x`, centred on `cy`. */
|
|
10
11
|
function tagPill(text, x, cy, size) {
|
|
@@ -260,7 +261,7 @@ export function seriesLineWidth(tie, v, size, scoresOn) {
|
|
|
260
261
|
const score = scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
|
|
261
262
|
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
|
}
|
|
263
|
-
const fillScore = (tie, v) => (tie.result.score ? v.words.scoreText.replace("{a}",
|
|
264
|
+
const fillScore = (tie, v, swap = false) => (tie.result.score ? v.words.scoreText.replace("{a}", sideScore(tie, swap ? 1 : 0, v)).replace("{b}", sideScore(tie, swap ? 0 : 1, v)) : "");
|
|
264
265
|
/** 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
266
|
function series(e) {
|
|
266
267
|
const { x, y, w, h, tie, v } = e;
|
|
@@ -274,7 +275,7 @@ function series(e) {
|
|
|
274
275
|
const [a, b] = e.order;
|
|
275
276
|
const cy = y + h / 2;
|
|
276
277
|
const score = e.scoresOn && tie.result.score ? fillScore(tie, v) : v.words.vs;
|
|
277
|
-
const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ?
|
|
278
|
+
const scoreOrdered = e.scoresOn && tie.result.score && a === 1 ? fillScore(tie, v, true) : score;
|
|
278
279
|
const sw = bw(scoreOrdered, size);
|
|
279
280
|
const cx = x + w / 2;
|
|
280
281
|
const ss = Math.max(size * 0.72, 7.5);
|
package/dist/locales.js
CHANGED
|
@@ -21,6 +21,11 @@ const EN = {
|
|
|
21
21
|
roadFinal: "The final: {a} {score} {b} ({how}).", roadChampion: "{name} won the final.", roadToPlay: "The final is still to be played.",
|
|
22
22
|
vs: "vs", at: "AT", groupName: "Group {n}", colPlayed: "P", colWon: "W", colDrawn: "D", colLost: "L", colDiff: "GD", colPoints: "Pts",
|
|
23
23
|
playIn: "Play-in", playInWinner: "Winner {a}/{b}", playInLoser: "Loser {a}/{b}", seedSlot: "Seed {n}", winnerOf: "Winner of {name}", loserOf: "Loser of {name}",
|
|
24
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
|
|
25
|
+
judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "decision", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
|
|
26
|
+
judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}: {n} shido", judoShidoShort: "shido {a}–{b}",
|
|
27
|
+
judoRepechage: "Repechage", judoBronze: "Bronze medal contest", judoRepechageBronze: "Repechage and bronze medal contests", judoWhite: "white", judoBlue: "blue",
|
|
28
|
+
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Men", judoWomen: "Women",
|
|
24
29
|
};
|
|
25
30
|
const FR = {
|
|
26
31
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -38,6 +43,11 @@ const FR = {
|
|
|
38
43
|
roadFinal: "La finale : {a} {score} {b} ({how}).", roadChampion: "{name} a remporté la finale.", roadToPlay: "La finale reste à jouer.",
|
|
39
44
|
vs: "vs", at: "À", groupName: "Groupe {n}", colPlayed: "PJ", colWon: "V", colDrawn: "N", colLost: "D", colDiff: "+/-", colPoints: "Pts",
|
|
40
45
|
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}",
|
|
46
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
|
|
47
|
+
judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "décision", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
|
|
48
|
+
judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}\u00a0: {n}\u00a0shido", judoShidoShort: "shido {a}–{b}",
|
|
49
|
+
judoRepechage: "Repêchage", judoBronze: "Combat pour la médaille de bronze", judoRepechageBronze: "Repêchages et combats pour la médaille de bronze", judoWhite: "blanc", judoBlue: "bleu",
|
|
50
|
+
judoWeightClass: "{sex} -{w}\u00a0kg", judoWeightOver: "{sex} +{w}\u00a0kg", judoMen: "Hommes", judoWomen: "Femmes",
|
|
41
51
|
};
|
|
42
52
|
const JA = {
|
|
43
53
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -55,6 +65,11 @@ const JA = {
|
|
|
55
65
|
roadFinal: "決勝は{a} {score} {b}({how})でした。", roadChampion: "{name}が優勝しました。", roadToPlay: "決勝はまだ行われていません。",
|
|
56
66
|
vs: "対", at: "@", groupName: "グループ{n}", colPlayed: "試合", colWon: "勝", colDrawn: "分", colLost: "敗", colDiff: "得失差", colPoints: "勝点",
|
|
57
67
|
playIn: "プレイイン", playInWinner: "{a}/{b}の勝者", playInLoser: "{a}/{b}の敗者", seedSlot: "第{n}シード", winnerOf: "{name}の勝者", loserOf: "{name}の敗者",
|
|
68
|
+
judoIppon: "一", judoWazaAri: "技", judoYuko: "有", judoIpponLong: "一本", judoWazaAriLong: "技あり", judoYukoLong: "有効",
|
|
69
|
+
judoMethodIppon: "一本", judoMethodAwasete: "合わせ技一本", judoMethodScore: "優勢勝ち", judoMethodHansoku: "反則勝ち", judoMethodFusen: "不戦勝", judoMethodKiken: "棄権勝ち",
|
|
70
|
+
judoGolden: "GS", judoGoldenLong: "ゴールデンスコア", judoTime: "{t}", judoShidoOf: "{name}:指導{n}", judoShidoShort: "指導{a}–{b}",
|
|
71
|
+
judoRepechage: "敗者復活戦", judoBronze: "銅メダル決定戦", judoRepechageBronze: "敗者復活戦と銅メダル決定戦", judoWhite: "白", judoBlue: "青",
|
|
72
|
+
judoWeightClass: "{sex}{w}kg級", judoWeightOver: "{sex}{w}kg超級", judoMen: "男子", judoWomen: "女子",
|
|
58
73
|
};
|
|
59
74
|
const ES = {
|
|
60
75
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -72,6 +87,11 @@ const ES = {
|
|
|
72
87
|
roadFinal: "La final: {a} {score} {b} ({how}).", roadChampion: "{name} ganó la final.", roadToPlay: "La final aún no se ha jugado.",
|
|
73
88
|
vs: "vs.", at: "EN", groupName: "Grupo {n}", colPlayed: "PJ", colWon: "G", colDrawn: "E", colLost: "P", colDiff: "DG", colPoints: "Pts",
|
|
74
89
|
playIn: "Play-in", playInWinner: "Ganador {a}/{b}", playInLoser: "Perdedor {a}/{b}", seedSlot: "Puesto {n}", winnerOf: "Ganador de {name}", loserOf: "Perdedor de {name}",
|
|
90
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "ippon", judoWazaAriLong: "waza-ari", judoYukoLong: "yuko",
|
|
91
|
+
judoMethodIppon: "ippon", judoMethodAwasete: "waza-ari awasete ippon", judoMethodScore: "decisión", judoMethodHansoku: "hansoku-make", judoMethodFusen: "fusen-gachi", judoMethodKiken: "kiken-gachi",
|
|
92
|
+
judoGolden: "GS", judoGoldenLong: "golden score", judoTime: "{t}", judoShidoOf: "{name}: {n} shido", judoShidoShort: "shido {a}–{b}",
|
|
93
|
+
judoRepechage: "Repesca", judoBronze: "Combate por la medalla de bronce", judoRepechageBronze: "Repesca y combates por la medalla de bronce", judoWhite: "blanco", judoBlue: "azul",
|
|
94
|
+
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Hombres", judoWomen: "Mujeres",
|
|
75
95
|
};
|
|
76
96
|
const DE = {
|
|
77
97
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -89,6 +109,11 @@ const DE = {
|
|
|
89
109
|
roadFinal: "Das Finale: {a} {score} {b} ({how}).", roadChampion: "{name} gewann das Finale.", roadToPlay: "Das Finale steht noch aus.",
|
|
90
110
|
vs: "vs.", at: "BEI", groupName: "Gruppe {n}", colPlayed: "Sp", colWon: "S", colDrawn: "U", colLost: "N", colDiff: "Diff.", colPoints: "Pkt",
|
|
91
111
|
playIn: "Play-in", playInWinner: "Sieger {a}/{b}", playInLoser: "Verlierer {a}/{b}", seedSlot: "Platz {n}", winnerOf: "Sieger von {name}", loserOf: "Verlierer von {name}",
|
|
112
|
+
judoIppon: "I", judoWazaAri: "W", judoYuko: "Y", judoIpponLong: "Ippon", judoWazaAriLong: "Waza-ari", judoYukoLong: "Yuko",
|
|
113
|
+
judoMethodIppon: "Ippon", judoMethodAwasete: "Waza-ari awasete Ippon", judoMethodScore: "Punktsieg", judoMethodHansoku: "Hansoku-make", judoMethodFusen: "Fusen-gachi", judoMethodKiken: "Kiken-gachi",
|
|
114
|
+
judoGolden: "GS", judoGoldenLong: "Golden Score", judoTime: "{t}", judoShidoOf: "{name}: {n} Shido", judoShidoShort: "Shido {a}–{b}",
|
|
115
|
+
judoRepechage: "Hoffnungsrunde", judoBronze: "Kampf um Bronze", judoRepechageBronze: "Hoffnungsrunde und Kämpfe um Bronze", judoWhite: "weiß", judoBlue: "blau",
|
|
116
|
+
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Herren", judoWomen: "Damen",
|
|
92
117
|
};
|
|
93
118
|
/**
|
|
94
119
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/matchup.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { judoHasScore, judoOutcome } from "./judo-model.js";
|
|
2
|
+
import { judoNotes, sideScoreLong } from "./judo-text.js";
|
|
1
3
|
import { abbreviate } from "./text.js";
|
|
2
4
|
import { fill } from "./locales.js";
|
|
3
5
|
/** A bracket's teams: the list given, plus a team made from every id that a matchup names and the list does not. */
|
|
@@ -77,18 +79,31 @@ export function resolveMatchup(m, index, teams) {
|
|
|
77
79
|
if (ids[0] !== null && ids[0] === ids[1])
|
|
78
80
|
throw new Error(`taisen-brackets: matchup ${index} has ${JSON.stringify(ids[0])} on both sides`);
|
|
79
81
|
const kind = kindOf(m);
|
|
80
|
-
|
|
81
|
-
let winner = settled;
|
|
82
|
+
let named;
|
|
82
83
|
if (m.winner !== undefined) {
|
|
83
84
|
if (m.winner === null)
|
|
84
|
-
|
|
85
|
+
named = null;
|
|
85
86
|
else if (ids[0] === m.winner)
|
|
86
|
-
|
|
87
|
+
named = 0;
|
|
87
88
|
else if (ids[1] === m.winner)
|
|
88
|
-
|
|
89
|
+
named = 1;
|
|
89
90
|
else
|
|
90
91
|
throw new Error(`taisen-brackets: matchup ${index}'s winner ${JSON.stringify(m.winner)} is not one of its sides`);
|
|
91
92
|
}
|
|
93
|
+
let judo;
|
|
94
|
+
let result;
|
|
95
|
+
let winner;
|
|
96
|
+
if (m.judo) {
|
|
97
|
+
judo = judoOutcome(m.judo, named ?? null);
|
|
98
|
+
if (judo.winner !== null && named !== undefined && named !== null && named !== judo.winner)
|
|
99
|
+
throw new RangeError(`taisen-brackets: matchup ${index}'s judo scores make side ${judo.winner} the winner, but its winner is side ${named}`);
|
|
100
|
+
winner = judo.winner;
|
|
101
|
+
result = { kind: "game", score: judoHasScore(judo) || winner !== null ? judo.points : null, played: judoHasScore(judo) || winner !== null ? 1 : 0, bestOf: null, pens: null, extraTime: false, replays: 0, games: [judo.points], decided: winner !== null };
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
({ result, winner } = settle(m, kind));
|
|
105
|
+
if (named !== undefined)
|
|
106
|
+
winner = named;
|
|
92
107
|
const sides = ids.map((id) => (id === null ? null : teams.get(id) ?? null));
|
|
93
108
|
return {
|
|
94
109
|
index,
|
|
@@ -102,6 +117,8 @@ export function resolveMatchup(m, index, teams) {
|
|
|
102
117
|
third: !!m.third,
|
|
103
118
|
walkover: !!m.walkover,
|
|
104
119
|
note: m.note ?? null,
|
|
120
|
+
...(judo ? { judo } : {}),
|
|
121
|
+
...(m.stage ? { stage: m.stage } : {}),
|
|
105
122
|
...(m.seeds ? { seeds: m.seeds } : {}),
|
|
106
123
|
...(m.home !== undefined ? { home: m.home } : {}),
|
|
107
124
|
};
|
|
@@ -112,7 +129,16 @@ export const abbrOf = (team, v) => (team ? team.abbr ?? abbreviate(v.team(team))
|
|
|
112
129
|
export function noteParts(tie, v) {
|
|
113
130
|
const r = tie.result;
|
|
114
131
|
const w = v.words;
|
|
115
|
-
const out = [];
|
|
132
|
+
const out = tie.judo ? judoNotes(tie, v, false) : [];
|
|
133
|
+
if (tie.stage === "repechage" || tie.stage === "bronze")
|
|
134
|
+
out.unshift(tie.stage === "repechage" ? w.judoRepechage : w.judoBronze);
|
|
135
|
+
if (tie.judo) {
|
|
136
|
+
if (tie.walkover)
|
|
137
|
+
out.push(w.walkover);
|
|
138
|
+
if (tie.note)
|
|
139
|
+
out.push(tie.note);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
116
142
|
if (r.kind === "series") {
|
|
117
143
|
if (r.decided && r.played > 0)
|
|
118
144
|
out.push(fill(w.inGames, { n: v.number(r.played) }));
|
|
@@ -139,7 +165,14 @@ export function noteParts(tie, v) {
|
|
|
139
165
|
export function noteLong(tie, v) {
|
|
140
166
|
const r = tie.result;
|
|
141
167
|
const w = v.words;
|
|
142
|
-
const out = [];
|
|
168
|
+
const out = tie.judo ? judoNotes(tie, v, true) : [];
|
|
169
|
+
if (tie.judo) {
|
|
170
|
+
if (tie.walkover)
|
|
171
|
+
out.push(w.walkover);
|
|
172
|
+
if (tie.note)
|
|
173
|
+
out.push(tie.note);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
143
176
|
if (r.kind === "series") {
|
|
144
177
|
if (r.decided && r.played > 0)
|
|
145
178
|
out.push(fill(w.inGames, { n: v.number(r.played) }));
|
|
@@ -169,7 +202,7 @@ export function describeTie(tie, v) {
|
|
|
169
202
|
const [a, b] = tie.sides;
|
|
170
203
|
const r = tie.result;
|
|
171
204
|
let text = r.score
|
|
172
|
-
? fill(w.tieScore, { a: name(a), b: name(b), sa:
|
|
205
|
+
? fill(w.tieScore, { a: name(a), b: name(b), sa: sideScoreLong(tie, 0, v), sb: sideScoreLong(tie, 1, v) })
|
|
173
206
|
: fill(w.tieVs, { a: name(a), b: name(b) });
|
|
174
207
|
const notes = noteLong(tie, v);
|
|
175
208
|
if (notes.length)
|
package/dist/playin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fill } from "./locales.js";
|
|
2
2
|
import { abbrOf, describeTie } from "./matchup.js";
|
|
3
|
+
import { sideScore } from "./judo-text.js";
|
|
3
4
|
import { drawMark } from "./marks.js";
|
|
4
5
|
import { bold, headTile, headingTexts, titleHeight } from "./furniture.js";
|
|
5
6
|
import { esc, fit, num } from "./text.js";
|
|
@@ -147,7 +148,7 @@ export function drawPlayIn(layout, o, v, size, ctx, road, linked) {
|
|
|
147
148
|
const team = t.sides[k];
|
|
148
149
|
const ry = rect.y + k * rowH;
|
|
149
150
|
const cy = ry + rowH / 2;
|
|
150
|
-
const score = o.scores !== false
|
|
151
|
+
const score = o.scores !== false ? sideScore(t, k, v) : "";
|
|
151
152
|
const cls = t.winner === null ? "tb-pending" : t.winner === k ? "tb-won" : "tb-lost";
|
|
152
153
|
const items = [];
|
|
153
154
|
const slotX = rect.x + rect.w - 5 - 26;
|
package/dist/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { layoutBracket } from "./layout.js";
|
|
2
2
|
import { abbrOf, describeTie, noteParts } from "./matchup.js";
|
|
3
|
+
import { shidoCards, sideScore } from "./judo-text.js";
|
|
3
4
|
import { cleanSvg, drawEmblem, drawMark, hasTrophy as hasTrophyOf, isEmblemId, markDefs, safeHref, safeUrl } from "./marks.js";
|
|
4
5
|
import { roundLabel } from "./rounds.js";
|
|
5
6
|
import { styleFor } from "./theme.js";
|
|
@@ -115,7 +116,7 @@ export function drawBracket(input, options = {}) {
|
|
|
115
116
|
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";
|
|
116
117
|
const road = options.look === "road";
|
|
117
118
|
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 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, ...(t.judo ? [t.judo.method, t.judo.goldenScore, t.judo.seconds, t.judo.sides] : [])]), 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
120
|
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() };
|
|
120
121
|
const path = pathOf(layout, options.highlightPath);
|
|
121
122
|
const linked = { count: 0 };
|
|
@@ -124,10 +125,10 @@ export function drawBracket(input, options = {}) {
|
|
|
124
125
|
// 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
126
|
const own = kind === "groups" ? drawGroups(layout, options, v, size, ctx, road, linked) : kind === "playin" ? drawPlayIn(layout, options, v, size, ctx, road, linked) : null;
|
|
126
127
|
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
|
+
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, judo: layout.ties.some((t) => t.judo !== undefined) });
|
|
128
129
|
const plan = planBracket(layout, options, v, size, orientation === "vertical", ctx, road, path, kind);
|
|
129
130
|
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
|
+
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, judo: layout.ties.some((t) => t.judo !== undefined) });
|
|
131
132
|
}
|
|
132
133
|
/** 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
134
|
function frame(f) {
|
|
@@ -135,7 +136,7 @@ function frame(f) {
|
|
|
135
136
|
const picture = resolveBackground(options.background, theme, f.width, f.height, prefix);
|
|
136
137
|
const defs = markDefs(ctx) + (picture?.defs ?? "");
|
|
137
138
|
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 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, judo: f.judo }, picture?.inks)}</style>`;
|
|
139
140
|
const bg = options.background === false ? "" : `<rect class="tb-bg" width="${num(f.width)}" height="${num(f.height)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
|
|
140
141
|
return [
|
|
141
142
|
`<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">`,
|
|
@@ -168,7 +169,7 @@ export function bracketStyle(options) {
|
|
|
168
169
|
if (theme === "none")
|
|
169
170
|
return "";
|
|
170
171
|
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
|
+
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, judo: options.judo === true }, picture?.inks);
|
|
172
173
|
}
|
|
173
174
|
/** 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
175
|
function eastFirst(input) {
|
|
@@ -181,6 +182,8 @@ function eastFirst(input) {
|
|
|
181
182
|
}
|
|
182
183
|
// ---------------------------------------------------------------------------------------------- the plan
|
|
183
184
|
function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
185
|
+
// The ties kept apart below the tree are a little taller in the road look when any of them has a note to write (a judo contest's method and time), so that the note has its line.
|
|
186
|
+
const OH = road && [...layout.other, ...layout.robin, ...layout.playIn].some((i) => noteParts(layout.ties[i], v).length > 0) ? 66 : OTHER_H;
|
|
184
187
|
// The layouts that draw each tie their own way take the tree's places and lines, and fill the boxes themselves.
|
|
185
188
|
const V = VARIANTS.includes(kind) ? kind : null;
|
|
186
189
|
const pad = vertical ? 14 : 22;
|
|
@@ -238,7 +241,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
238
241
|
const top = pad + titleH + (hasGroups ? groupRoom : 0) + labelRoom;
|
|
239
242
|
others = othersOf(layout);
|
|
240
243
|
ow = clamp(D * 1.1, 120, 200);
|
|
241
|
-
extraH0 = othersHeight(others.length, W0, pad, ow,
|
|
244
|
+
extraH0 = othersHeight(others.length, W0, pad, ow, OH) + thirdBlock;
|
|
242
245
|
const bodyH = size.height === null ? rows * 60 : size.height - top - pad - extraH0;
|
|
243
246
|
rowStep = bodyH / rows;
|
|
244
247
|
S = clamp(rowStep - (road ? 7 : 9), 24, road ? 132 : 104);
|
|
@@ -264,6 +267,8 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
264
267
|
const tagW = tagsOn ? Math.max(...layout.ties.flatMap((t) => [tagOf(t, 0), tagOf(t, 1)]).map((x) => boldWidth(x, 8))) + 8 : 0;
|
|
265
268
|
// 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.
|
|
266
269
|
const anyNotes = layout.ties.some((t) => noteParts(t, v).length > 0);
|
|
270
|
+
// The widest score a row may carry: two digits, or a judo contest's marks ("W2 Y3").
|
|
271
|
+
const probe = layout.ties.reduce((best, t) => [0, 1].reduce((b, k) => { const x = sideScore(t, k, v); return wide(x, 10) > wide(b, 10) ? x : b; }, best), "00");
|
|
267
272
|
// The road look's boxes of marks alone carry each score under its mark, unless marks alone were asked for.
|
|
268
273
|
const markScores = road && o.detail !== "mark" && (o.scores ?? true) && layout.ties.some((t) => t.result.score !== null);
|
|
269
274
|
const fitOf = (w, h, named) => {
|
|
@@ -273,7 +278,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
273
278
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
274
279
|
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
280
|
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(
|
|
281
|
+
const varying = md + (named === "full" ? longest : longestAbbr) * (font / 13) + wide(probe, score) + extra;
|
|
277
282
|
return Math.min(1, (w - fixed) / varying);
|
|
278
283
|
};
|
|
279
284
|
let fitNow = 1;
|
|
@@ -284,7 +289,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
284
289
|
const anyBye = layout.sides.some((sd) => sd.boxes.some((b) => b.byes.length > 0));
|
|
285
290
|
const anyPens = layout.ties.some((t) => t.result.pens !== null);
|
|
286
291
|
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(
|
|
292
|
+
const needed = 6 + md + 5 + longest * (font / 13) + 8 + boldWidth(probe, score) + extra + 14;
|
|
288
293
|
const named = o.detail && o.detail !== "auto" ? o.detail : w >= needed && h >= 36 ? "full" : w >= 76 && h >= 28 ? "compact" : "mark";
|
|
289
294
|
return named === "mark" && o.markShape === "none" ? "compact" : named;
|
|
290
295
|
};
|
|
@@ -389,7 +394,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
389
394
|
dl = pick(wl, hl);
|
|
390
395
|
const strays = othersOf(layout);
|
|
391
396
|
const ows = Math.min(availW, 200);
|
|
392
|
-
const strayH = othersHeight(strays.length, W0, pad, ows,
|
|
397
|
+
const strayH = othersHeight(strays.length, W0, pad, ows, OH);
|
|
393
398
|
const labelH = showLabels ? 22 : 8;
|
|
394
399
|
const content = (champ ? champH + 14 : 0) + labelH + hl + (withThird ? 8 + 18 + hl + 8 : 0) + strayH;
|
|
395
400
|
const top0 = pad + titleH;
|
|
@@ -410,7 +415,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
410
415
|
y += 8 + 18 + hl + 8;
|
|
411
416
|
}
|
|
412
417
|
if (strays.length > 0)
|
|
413
|
-
othersBlock(layout, strays, y + 6, W0, pad, ows,
|
|
418
|
+
othersBlock(layout, strays, y + 6, W0, pad, ows, OH, v, road, boxes, texts);
|
|
414
419
|
texts.push(...headings(v, o, W0, pad, poster));
|
|
415
420
|
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
421
|
}
|
|
@@ -518,7 +523,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
518
523
|
}
|
|
519
524
|
if (bottomThird)
|
|
520
525
|
thirdAtFoot(layout, height - pad - extraH0 + 6, width / 2, ow, OTHER_H, v, road, boxes, texts);
|
|
521
|
-
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow,
|
|
526
|
+
othersBlock(layout, others, height - pad - extraH0 + thirdBlock + 6, width, pad, ow, OH, v, road, boxes, texts);
|
|
522
527
|
texts.push(...headings(v, o, width, pad, poster));
|
|
523
528
|
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 } });
|
|
524
529
|
}
|
|
@@ -531,7 +536,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
531
536
|
const bandCount = halves ? 2 * bandsPerSide : nc + (champ ? 1 : 0);
|
|
532
537
|
others = othersOf(layout);
|
|
533
538
|
ow = Math.min(W0 - 2 * pad, 150);
|
|
534
|
-
const othersV = othersHeight(others.length, W0, pad, ow,
|
|
539
|
+
const othersV = othersHeight(others.length, W0, pad, ow, OH);
|
|
535
540
|
// 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.
|
|
536
541
|
const needOf = (h, gap) => {
|
|
537
542
|
const finalHere = V === "columns" ? Math.max(h, finalH) : detail === "mark" ? Math.max(h, 52) : h;
|
|
@@ -664,7 +669,7 @@ function planBracket(layout, o, v, size, vertical, ctx, road, path, kind) {
|
|
|
664
669
|
}
|
|
665
670
|
if (bottomThird)
|
|
666
671
|
thirdAtFoot(layout, totalT - pad + 6, W0 / 2, Math.min(W0 - 2 * pad, road ? Math.max(boxW, 200) : 200), thirdH2, v, road, boxes, texts);
|
|
667
|
-
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow,
|
|
672
|
+
othersBlock(layout, others, totalT - pad + thirdBlockV + 6, W0, pad, ow, OH, v, road, boxes, texts);
|
|
668
673
|
texts.push(...headings(v, o, W0, pad, poster));
|
|
669
674
|
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
675
|
}
|
|
@@ -868,7 +873,10 @@ function othersBlock(layout, list, y0, width, pad, ow, oh, v, road, boxes, texts
|
|
|
868
873
|
return;
|
|
869
874
|
const cols = Math.max(1, Math.floor((width - 2 * pad + 10) / (ow + 10)));
|
|
870
875
|
const x0 = pad + (width - 2 * pad - (Math.min(cols, list.length) * (ow + 10) - 10)) / 2;
|
|
871
|
-
|
|
876
|
+
const stages = new Set(list.map((i) => layout.ties[i].stage ?? "other"));
|
|
877
|
+
const w = v.words;
|
|
878
|
+
const heading = stages.size === 1 && stages.has("repechage") ? w.judoRepechage : stages.size === 1 && stages.has("bronze") ? w.judoBronze : [...stages].every((x) => x === "repechage" || x === "bronze") ? w.judoRepechageBronze : w.other;
|
|
879
|
+
texts.push(label(v, road, heading, width / 2, y0 + 12, "middle"));
|
|
872
880
|
list.forEach((i, n) => {
|
|
873
881
|
const rect = { x: x0 + (n % cols) * (ow + 10), y: y0 + 24 + Math.floor(n / cols) * (oh + 10), w: ow, h: oh };
|
|
874
882
|
boxes.push({ rect, tie: layout.ties[i], order: [0, 1], bye: null, byes: [], final: false, champion: false });
|
|
@@ -932,7 +940,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
932
940
|
const withScore = plan.markScores && scoresOn && r.score !== null;
|
|
933
941
|
const tags = [0, 1].map((k) => (plan.tags ? tagOf(tie, k) : ""));
|
|
934
942
|
const tagH = tags.some((t) => t !== "") ? 12 : 0;
|
|
935
|
-
const scoreOf = (k) => (withScore
|
|
943
|
+
const scoreOf = (k) => (withScore ? sideScore(tie, k, v) : "");
|
|
936
944
|
const pensOf = (k) => (withScore && r.pens ? `(${v.number(r.pens[k])})` : "");
|
|
937
945
|
const hasPens = withScore && r.pens !== null;
|
|
938
946
|
const lay = (m) => {
|
|
@@ -1028,7 +1036,13 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1028
1036
|
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
1037
|
tx += tw + 3;
|
|
1030
1038
|
}
|
|
1031
|
-
const score = scoresOn
|
|
1039
|
+
const score = scoresOn ? sideScore(tie, k, v) : "";
|
|
1040
|
+
// A judo contest: a bar at the row's edge in the colour of the side (the first is white, the second blue) and its shido cards beside the score.
|
|
1041
|
+
if (tie.judo)
|
|
1042
|
+
rowParts.push(`<rect class="${k === 0 ? "tb-jw" : "tb-jb"}" x="${num(x + 1.5)}" y="${num(cy - rowH * 0.3)}" width="3" height="${num(rowH * 0.6)}" rx="1.5"/>`);
|
|
1043
|
+
const cards = tie.judo && scoresOn ? shidoCards(tie, k) : [];
|
|
1044
|
+
const cardSize = Math.max(5, Math.min(9, rowH * 0.3));
|
|
1045
|
+
const cardsW = cards.length ? cards.length * (cardSize * 0.72 + 2) + 4 : 0;
|
|
1032
1046
|
const pensText = scoresOn && r.pens ? `(${v.number(r.pens[k])})` : "";
|
|
1033
1047
|
const bye = p.byes.includes(k);
|
|
1034
1048
|
const right = x + w - padX;
|
|
@@ -1040,7 +1054,8 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1040
1054
|
const byeFits = bye && right - tx - (scoreW + pensW) - (mw(v.words.bye, small2) + 6) - 6 >= mw(`${seed}${nm}`, font);
|
|
1041
1055
|
const byeText = byeFits ? v.words.bye : "";
|
|
1042
1056
|
const byeW = byeText ? mw(byeText, small2) : 0;
|
|
1043
|
-
const
|
|
1057
|
+
const cardsOn = cardsW > 0 && right - tx - (scoreW + pensW) - cardsW - 6 >= 36;
|
|
1058
|
+
const avail = right - tx - (scoreW + pensW) - (byeW ? byeW + 6 : 0) - 6 - (cardsOn ? cardsW : 0);
|
|
1044
1059
|
// A name that does not fit is set a little smaller before it is cut.
|
|
1045
1060
|
let nameFont = font;
|
|
1046
1061
|
while (nameFont > 9.5 && mw(`${seed}${nm}`, nameFont) > avail)
|
|
@@ -1049,6 +1064,10 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1049
1064
|
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>`);
|
|
1050
1065
|
if (score)
|
|
1051
1066
|
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>`);
|
|
1067
|
+
if (cardsOn) {
|
|
1068
|
+
const cw = cardSize * 0.72;
|
|
1069
|
+
cards.forEach((c, n) => rowParts.push(`<rect class="${c === "r" ? "tb-sh-r" : "tb-sh-y"}" x="${num(right - scoreW - pensW - cardsW + 2 + n * (cw + 2))}" y="${num(cy - cardSize / 2)}" width="${num(cw)}" height="${num(cardSize)}" rx="1"/>`));
|
|
1070
|
+
}
|
|
1052
1071
|
if (pensText)
|
|
1053
1072
|
rowParts.push(`<text class="tb-muted" x="${num(right - scoreW - (road ? 4 : 3))}" y="${num(cy + small * 0.35)}" font-size="${num(small)}" text-anchor="end">${esc(pensText)}</text>`);
|
|
1054
1073
|
if (byeText)
|
|
@@ -1059,7 +1078,7 @@ function drawBox(p, plan, o, v, ctx, linked, side) {
|
|
|
1059
1078
|
if (noteH) {
|
|
1060
1079
|
const f = road ? 9 : 9.5;
|
|
1061
1080
|
const line = road ? v.upper(notes.join(" · ")) : notes.join(" · ");
|
|
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>`);
|
|
1081
|
+
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, boldWidth))}</text>`);
|
|
1063
1082
|
}
|
|
1064
1083
|
}
|
|
1065
1084
|
const extra = tieClassOf(o, tie);
|
package/dist/table.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { layoutBracket } from "./layout.js";
|
|
2
2
|
import { noteLong } from "./matchup.js";
|
|
3
|
+
import { sideScore } from "./judo-text.js";
|
|
3
4
|
import { fill, voiceOf } from "./locales.js";
|
|
4
5
|
import { localizedLayout } from "./render.js";
|
|
5
6
|
import { roundLabel } from "./rounds.js";
|
|
@@ -23,12 +24,12 @@ export function bracketTable(input, options = {}) {
|
|
|
23
24
|
const text = team ? esc(team.name) : esc(w.tbd);
|
|
24
25
|
return t.winner === k ? `<strong>${text}</strong>` : text;
|
|
25
26
|
};
|
|
26
|
-
const score = (t) => (t.result.score ? fill(w.scoreText, { a:
|
|
27
|
+
const score = (t) => (t.result.score ? fill(w.scoreText, { a: sideScore(t, 0, v), b: sideScore(t, 1, v) }) : "");
|
|
27
28
|
// Where a side comes from, in words, when the tie says so: "Loser of A v B".
|
|
28
29
|
const named = (i) => fill(w.tieVs, { a: layout.ties[i].sides[0]?.name ?? w.tbd, b: layout.ties[i].sides[1]?.name ?? w.tbd });
|
|
29
30
|
const origins = (t) => (t.from ?? []).flatMap((s) => (s ? [fill(s.result === "loser" ? w.loserOf : w.winnerOf, { name: named(s.tie) })] : []));
|
|
30
31
|
const rows = order.map((t) => {
|
|
31
|
-
const roundText = t.third ? w.thirdPlace : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
|
|
32
|
+
const roundText = t.third ? w.thirdPlace : t.stage === "repechage" ? w.judoRepechage : t.stage === "bronze" ? w.judoBronze : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
|
|
32
33
|
const date = dated ? `<td>${esc(v.date(t.date) ?? "")}</td>` : "";
|
|
33
34
|
return `<tr><th scope="row">${esc(roundText)}</th>${date}<td>${name(t, 0)}</td><td>${esc(score(t))}</td><td>${name(t, 1)}</td><td>${esc([...noteLong(t, v), ...origins(t)].join(w.noteJoin))}</td></tr>`;
|
|
34
35
|
});
|
package/dist/theme.d.ts
CHANGED
|
@@ -36,7 +36,16 @@ export interface StyleParts {
|
|
|
36
36
|
bgAuto?: boolean;
|
|
37
37
|
/** The rules of the layouts of their own (slots, shields, conference colours, header tiles, arrows, group cards). */
|
|
38
38
|
layout?: boolean;
|
|
39
|
+
/** A judo bracket: the white and blue side bars and the shido cards (`--tb-judo-white`, `--tb-judo-blue`, `--tb-shido`, `--tb-hansoku`). */
|
|
40
|
+
judo?: boolean;
|
|
39
41
|
}
|
|
42
|
+
/** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
|
|
43
|
+
export declare const JUDO_COLORS: Readonly<{
|
|
44
|
+
white: "#ffffff";
|
|
45
|
+
blue: "#1f4fa8";
|
|
46
|
+
shido: "#f0c000";
|
|
47
|
+
hansoku: "#d3302a";
|
|
48
|
+
}>;
|
|
40
49
|
/** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
|
|
41
50
|
export declare const CONFERENCE_COLORS: readonly [string, string];
|
|
42
51
|
/** The colours a background says to draw in (`backgroundSvg`'s `ink`): what `styleFor` takes in place of a look's own palette. */
|
package/dist/theme.js
CHANGED
|
@@ -10,6 +10,8 @@ export const ROAD_THEMES = Object.freeze({
|
|
|
10
10
|
poster: { ring: "#f6f1e3", edge: "rgba(0,0,0,.5)", panel: "#1b2766", spine: "#d9b24c" },
|
|
11
11
|
});
|
|
12
12
|
const FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
|
|
13
|
+
/** The colours of a judo contest: the white and the blue judogi, the yellow shido card and the red hansoku-make card. */
|
|
14
|
+
export const JUDO_COLORS = Object.freeze({ white: "#ffffff", blue: "#1f4fa8", shido: "#f0c000", hansoku: "#d3302a" });
|
|
13
15
|
/** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
|
|
14
16
|
export const CONFERENCE_COLORS = Object.freeze(["#2457c5", "#b3261e"]);
|
|
15
17
|
/** A palette and a road palette from a background's ink. */
|
|
@@ -59,6 +61,9 @@ function rules(p, r, poster, withFont, parts, side) {
|
|
|
59
61
|
const [ca, cb] = CONFERENCE_COLORS;
|
|
60
62
|
list.push([[".tb-slot"], `fill:${v("plain")};fill-opacity:.55;stroke:${v("edge")};stroke-width:1`], [[".tb-won .tb-slot"], `fill-opacity:.9;stroke:${v("win")};stroke-width:1.6`], [[".tb-lost .tb-slot"], "fill-opacity:.35"], [[".tb-emptybar"], `fill:${v("plain")};opacity:.8`], [[".tb-seed"], "font-variant-numeric:tabular-nums"], [[".tb-seedn"], `fill:${v("muted")};opacity:.85`], [[".tb-shield"], `fill:none;stroke:${v("edge")};stroke-width:1.4`], [[".tb-won .tb-shield"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-shield.tb-empty"], `fill:${v("plain")};fill-opacity:.5`], [[".tb-vs", ".tb-code"], `fill:${v("muted")};font-weight:700`], [[".tb-c0 .tb-box"], `stroke:var(--tb-conf-a, ${ca});stroke-width:1.6`], [[".tb-c1 .tb-box"], `stroke:var(--tb-conf-b, ${cb});stroke-width:1.6`], [[".tb-atag"], `fill:${v("ink")}`], [[".tb-atag-t"], `fill:${v("bg")}`], [[".tb-c0 .tb-atag"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-c1 .tb-atag"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-c0 .tb-atag-t", ".tb-c1 .tb-atag-t"], "fill:#fff"], [[".tb-head"], `fill:${rv("panel", "panel")}`], [[".tb-head.tb-c0"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-head.tb-c1"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-head-t"], `fill:${v("ink")};font-weight:800`], [[".tb-head.tb-c0 + .tb-head-t", ".tb-head.tb-c1 + .tb-head-t"], "fill:#fff"], [[".tb-arrow"], `stroke:${v("line")};stroke-width:1.6;fill:none;stroke-linecap:round;stroke-linejoin:round`], [[".tb-arrow-loser"], "stroke-dasharray:5 4"], [[".tb-arrowhead"], `fill:${v("line")};stroke:none`], [[".tb-gcard"], `fill:${v("box")};stroke:${v("edge")};stroke-width:1`], [[".tb-gtile"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-gtile-t"], "fill:#fff;font-weight:800"], [[".tb-ghead"], `fill:${v("muted")};font-weight:700`], [[".tb-gcell"], `fill:${v("ink")};font-variant-numeric:tabular-nums`], [[".tb-gcell-p"], "font-weight:800"], [[".tb-gname"], `fill:${v("ink")};font-weight:700`], [[".tb-qual"], `fill:${v("win")};fill-opacity:.16`], [[".tb-gsep"], `stroke:${v("edge")};stroke-width:1;stroke-opacity:.6`]);
|
|
61
63
|
}
|
|
64
|
+
if (parts.judo === true) {
|
|
65
|
+
list.push([[".tb-jw"], `fill:var(--tb-judo-white, ${JUDO_COLORS.white});stroke:${v("edge")};stroke-width:.8`], [[".tb-jb"], `fill:var(--tb-judo-blue, ${JUDO_COLORS.blue});stroke:${v("edge")};stroke-width:.8`], [[".tb-sh-y"], `fill:var(--tb-shido, ${JUDO_COLORS.shido});stroke:rgba(0,0,0,.35);stroke-width:.6`], [[".tb-sh-r"], `fill:var(--tb-hansoku, ${JUDO_COLORS.hansoku});stroke:rgba(0,0,0,.35);stroke-width:.6`]);
|
|
66
|
+
}
|
|
62
67
|
if (parts.follow === true) {
|
|
63
68
|
list.push([[".tb-tie"], "opacity:.4"], [[".tb-tie.tb-on-path"], "opacity:1"], [[".tb-on-path .tb-box"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-line"], "stroke-opacity:.25"], [[".tb-line.tb-path"], `stroke:${v("win")};stroke-width:4.5;stroke-opacity:1;stroke-linecap:round;stroke-dasharray:.1 9`]);
|
|
64
69
|
}
|