@johnmorrisdotca/taisen-brackets 0.8.0 → 0.10.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 +21 -0
- package/README.md +5 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/judo-board.d.ts +38 -0
- package/dist/judo-board.js +9 -9
- package/dist/judo-road.d.ts +53 -0
- package/dist/judo-road.js +207 -0
- package/dist/judo-sheets.d.ts +65 -0
- package/dist/judo-sheets.js +268 -0
- package/dist/locales.js +5 -0
- package/dist/types.d.ts +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Judo, step 4: the road to the final of one judoka.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`judoWins(bracket, athleteId, options)`**: the contests an athlete has won in a bracket, oldest first, as `{ round, opponent, how }`: the round (a repechage and a bronze medal contest named as such), the opponent as an athlete (`name`, `code`, `flag`, `mark` of the team, or what `athlete(team)` gives) and how it was won (a judo result's method, golden score and time, or the score). A bye is a row for each round a top seed skipped (`byes: false` leaves them out). A name that is not in the bracket is a `RangeError`. `Team.code`, the three letters of the country, is new.
|
|
16
|
+
- **`judoRoadPanel({ athlete, wins }, options)`**: the panel for a broadcast or a hall screen. The athlete's block (flag, name, code, coach) in the colour of a judogi (`side`), and a row for each win, most recent first (`order`): the round on a pill, the opponent's flag, code and name, and how it was won; a bye is a quiet row. A wide screen puts the block on the left and the rows on the right, an upright one the block on top and the result under each name. `size`, `theme`, `flags`, `flagHref`, `locale`, `words`, `title`, `weight`, `rows`, `coach`. `describeJudoRoad`. The title is the road chart's word in each language.
|
|
17
|
+
- The demo has a section for it: any of the 16 athletes of the Judo example (a bronze medallist's repechage and bronze medal contest included), size, look, the colour of the block and the order, with the call that draws it; the state is in the address. Tests hold every text inside the drawing and every opponent inside its row, in every size and several languages.
|
|
18
|
+
|
|
19
|
+
## [0.9.0] - 2026-10-20
|
|
20
|
+
|
|
21
|
+
Judo, step 3: the contest card and the mat queue board.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`judoContestCard(contest, options)`**: the card shown when the athletes enter. The weight class, the round, the mat and the contest number in a header, then the white athlete over the blue one as two wide bands with the flag, the name as big as the band allows (two lines at a space when that is clearly bigger) and the three-letter code. `describeJudoCard`.
|
|
26
|
+
- **`judoMatQueue(mats, options)`**: the board of the contests to come, per mat. A mat is `{ mat, color?, contests }` and a contest `{ number, white, blue }`; each mat is a tall strip in its colour with its number big (white or near-black, whichever reads on the colour) and a row for each contest in order: the contest number (green for the one on now, `current: false` turns that off), the white athlete (name, code, flag) and the blue (flag, code, name). One to eight mats, in columns on a wide board (as many as leave each mat room for its names) and stacked on an upright tablet; `rows` cuts a long queue; `colors` and `JUDO_MAT_COLORS` (red, green, yellow, purple, orange, teal). `describeJudoQueue`.
|
|
27
|
+
- Both take the scoreboard's `size` (`tv`, `tablet`, `tablet-tall`, your own), `theme`, `swap`, `flags`, `flagHref`, `locale`, `words`, `title`, `id` and `className`; a missing name, a mat with no number, a colour that is not a hex code, nine mats or a bad size are a `RangeError`. Words for the two titles in the five languages (the French and Japanese were read by native-language reviewers).
|
|
28
|
+
- The demo has a section for them: the screen (queue or card), one to six mats, size, look and blue first, with the call that draws it; the state is in the address. Tests hold every text inside the drawing, in every size and several languages, and every name inside its side of its row.
|
|
29
|
+
|
|
9
30
|
## [0.8.0] - 2026-10-20
|
|
10
31
|
|
|
11
32
|
Judo, step 2: the scoreboard of one contest.
|
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ In the road look, `seeds: true` writes a small tag (`P1`, `WC2`) beside each tea
|
|
|
240
240
|
|
|
241
241
|
## Judo
|
|
242
242
|
|
|
243
|
-
A judo contest is a tie with a `judo` result: ippon, waza-ari, yuko and shido as the IJF's current Sport and Organisation Rules score them (yuko is back since 2025), golden score, the contest time and how it was won. The box writes the marks (`I`, `W`, `Y`), the shido as yellow and red cards and a note under the two rows, in the five languages. `judoBracket` makes a weight category's bracket with the IJF's repechage and its two bronze medal contests. [docs/judo.md](./docs/judo.md) quotes the rules it follows. `judoScoreboard` draws one contest's scoreboard for a given state (the athletes with flags and codes, ippon, waza-ari and yuko counters, shido cards, clock, golden score, osaekomi timer, weight class and round), on a TV or a tablet, as a screen board or as table-top flip cards: [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#board).
|
|
243
|
+
A judo contest is a tie with a `judo` result: ippon, waza-ari, yuko and shido as the IJF's current Sport and Organisation Rules score them (yuko is back since 2025), golden score, the contest time and how it was won. The box writes the marks (`I`, `W`, `Y`), the shido as yellow and red cards and a note under the two rows, in the five languages. `judoBracket` makes a weight category's bracket with the IJF's repechage and its two bronze medal contests. [docs/judo.md](./docs/judo.md) quotes the rules it follows. `judoScoreboard` draws one contest's scoreboard for a given state (the athletes with flags and codes, ippon, waza-ari and yuko counters, shido cards, clock, golden score, osaekomi timer, weight class and round), on a TV or a tablet, as a screen board or as table-top flip cards: [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#board). `judoContestCard` and `judoMatQueue` draw the card shown when the athletes enter and the board of the next contests per mat, with a big mat number and colour ([demo](https://johnmorrisdotca.github.io/taisen-brackets/#queue)). `judoWins` and `judoRoadPanel` draw the road to the final of one judoka, the contests they have won on the way, most recent first ([demo](https://johnmorrisdotca.github.io/taisen-brackets/#judoroad)).
|
|
244
244
|
|
|
245
245
|
<p align="center"><picture>
|
|
246
246
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/judo-desk-dark.webp">
|
|
@@ -293,6 +293,8 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
293
293
|
- **The champion in the centre**, with a slot for your trophy: your own SVG or image, or an emblem from [Dougu Sports](https://github.com/johnmorrisdotca/dougu-sports) (a hockey cup, a basketball trophy, a globe) through an optional plug-in, with no dependency added.
|
|
294
294
|
- **Accessible**: `role="img"` with a title and a description, a tooltip sentence on every tie, and `bracketTable`.
|
|
295
295
|
- **A judo scoreboard**: `judoScoreboard` draws one contest for a given state, in two styles (a board, and table-top flip cards) and three sizes, in five languages; live updating is yours.
|
|
296
|
+
- **The road to the final of a judoka**: `judoWins` reads an athlete's wins from a bracket (a top seed's bye included) and `judoRoadPanel` draws them with the opponent's flag, code and name and how each was won.
|
|
297
|
+
- **Judo screens for the hall**: `judoContestCard` (the category, the round, the white athlete over the blue one) and `judoMatQueue` (per mat, a big number and colour and the contests to come in order, with flags and codes), on a TV or a tablet, in five languages.
|
|
296
298
|
- **Judo scoring**: `Matchup.judo` takes ippon, waza-ari, yuko, shido, hansoku-make, golden score and the contest time, checked against the IJF rules (`judoOutcome`); a box shows the marks, the shido cards and how the contest was won, and `judoBracket` adds the repechage and the two bronze medal contests ([docs/judo.md](./docs/judo.md)).
|
|
297
299
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
298
300
|
- **Five languages**: English, French, Japanese, Spanish and German, for every word the drawing, the tooltips, the description and the table write, with the football terms of each language; countries named by `Intl` from their ISO code; numbers and dates by `Intl`. Add a language of your own.
|
|
@@ -653,7 +655,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
653
655
|
- `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
|
|
654
656
|
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
655
657
|
- `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
|
|
656
|
-
- `judoOutcome(result, winner?)`, `judoBracket(athletes, options?)`, `judoPodium(input)`, `judoCategoryLabel(category, locale?)`, `JUDO_WEIGHT_CLASSES`, `judoSeconds(time)`, `judoClock(seconds)`: judo results checked against the IJF rules, the bracket with its repechage and two bronze medal contests, the medallists, a weight category as a heading ([docs/judo.md](./docs/judo.md)). `judoScoreboard(contest, options?)`, `describeJudoContest(contest, options?)`, `JUDO_BOARD_SIZES`: the scoreboard of a contest as SVG and as a sentence. `Matchup.judo` and `Matchup.stage` (`"repechage"`, `"bronze"`) carry them into `drawBracket`.
|
|
658
|
+
- `judoOutcome(result, winner?)`, `judoBracket(athletes, options?)`, `judoPodium(input)`, `judoCategoryLabel(category, locale?)`, `JUDO_WEIGHT_CLASSES`, `judoSeconds(time)`, `judoClock(seconds)`: judo results checked against the IJF rules, the bracket with its repechage and two bronze medal contests, the medallists, a weight category as a heading ([docs/judo.md](./docs/judo.md)). `judoScoreboard(contest, options?)`, `describeJudoContest(contest, options?)`, `JUDO_BOARD_SIZES`: the scoreboard of a contest as SVG and as a sentence. `judoContestCard(contest, options?)`, `judoMatQueue(mats, options?)`, `describeJudoCard`, `describeJudoQueue`, `JUDO_MAT_COLORS`: the contest card and the mat queue board as SVG and as sentences. `judoWins(bracket, athlete, options?)`, `judoRoadPanel(source, options?)`, `describeJudoRoad`: an athlete's wins on the road to the final, and the panel. `Matchup.judo` and `Matchup.stage` (`"repechage"`, `"bronze"`) carry them into `drawBracket`.
|
|
657
659
|
- `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `mls`, `worldcup`). `LEAGUE_EMBLEMS`: each league's default emblem id.
|
|
658
660
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
659
661
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
@@ -745,7 +747,7 @@ Any current browser, and Node 22 or later. The package makes no network request
|
|
|
745
747
|
|
|
746
748
|
## Roadmap
|
|
747
749
|
|
|
748
|
-
- Judo:
|
|
750
|
+
- Judo: double and full repechage ([docs/judo.md](./docs/judo.md)).
|
|
749
751
|
- Double elimination, and the group tables on one page with the knockout they lead to.
|
|
750
752
|
- More languages (Portuguese, Italian, Korean, Chinese), and native review of the Spanish and German words.
|
|
751
753
|
- A custom element and a React helper, as in the other packages of the family.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,3 +21,7 @@ export { judoBracket, judoPodium, judoCategoryLabel, judoOutcome, judoSeconds, j
|
|
|
21
21
|
export type { JudoBracketOptions, JudoCategory, JudoMethod, JudoOutcome, JudoPodium, JudoResult, JudoSide, JudoSystem } from "./judo.ts";
|
|
22
22
|
export { judoScoreboard, describeJudoContest, JUDO_BOARD_SIZES } from "./judo-board.ts";
|
|
23
23
|
export type { JudoAthlete, JudoBoardOptions, JudoBoardSizeName, JudoContest } from "./judo-board.ts";
|
|
24
|
+
export { judoContestCard, describeJudoCard, judoMatQueue, describeJudoQueue, JUDO_MAT_COLORS } from "./judo-sheets.ts";
|
|
25
|
+
export type { JudoCardContest, JudoMat, JudoQueueContest, JudoQueueOptions } from "./judo-sheets.ts";
|
|
26
|
+
export { judoWins, judoRoadPanel, describeJudoRoad } from "./judo-road.ts";
|
|
27
|
+
export type { JudoRoadOptions, JudoRoadSource, JudoWin, JudoWinsOptions } from "./judo-road.ts";
|
package/dist/index.js
CHANGED
|
@@ -12,3 +12,5 @@ export { VERSION } from "./version.js";
|
|
|
12
12
|
export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.js";
|
|
13
13
|
export { judoBracket, judoPodium, judoCategoryLabel, judoOutcome, judoSeconds, judoClock, JUDO_WEIGHT_CLASSES } from "./judo.js";
|
|
14
14
|
export { judoScoreboard, describeJudoContest, JUDO_BOARD_SIZES } from "./judo-board.js";
|
|
15
|
+
export { judoContestCard, describeJudoCard, judoMatQueue, describeJudoQueue, JUDO_MAT_COLORS } from "./judo-sheets.js";
|
|
16
|
+
export { judoWins, judoRoadPanel, describeJudoRoad } from "./judo-road.js";
|
package/dist/judo-board.d.ts
CHANGED
|
@@ -78,6 +78,36 @@ export interface JudoBoardOptions {
|
|
|
78
78
|
id?: string;
|
|
79
79
|
className?: string;
|
|
80
80
|
}
|
|
81
|
+
export declare const clampN: (n: number, lo: number, hi: number) => number;
|
|
82
|
+
export declare const bw: (text: string, size: number) => number;
|
|
83
|
+
export interface Palette {
|
|
84
|
+
bg: string;
|
|
85
|
+
ink: string;
|
|
86
|
+
muted: string;
|
|
87
|
+
band: string;
|
|
88
|
+
box: string;
|
|
89
|
+
boxInk: string;
|
|
90
|
+
gold: string;
|
|
91
|
+
}
|
|
92
|
+
export declare const LIGHT: Palette;
|
|
93
|
+
export declare const DARK: Palette;
|
|
94
|
+
export declare const FONT = "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Hiragino Sans\", \"Yu Gothic UI\", \"Noto Sans CJK JP\", Meiryo, sans-serif";
|
|
95
|
+
/** The panels' own colours: the white and the blue judogi, the ink and the tiles on each. White on #1f4fa8 reads 8 to 1, and the dark ink on the white panel more. */
|
|
96
|
+
export declare const WHITE_PANEL: {
|
|
97
|
+
panel: string;
|
|
98
|
+
ink: string;
|
|
99
|
+
sub: string;
|
|
100
|
+
tile: string;
|
|
101
|
+
edge: string;
|
|
102
|
+
};
|
|
103
|
+
export declare const BLUE_PANEL: {
|
|
104
|
+
panel: "#1f4fa8";
|
|
105
|
+
ink: string;
|
|
106
|
+
sub: string;
|
|
107
|
+
tile: string;
|
|
108
|
+
edge: string;
|
|
109
|
+
};
|
|
110
|
+
export declare function rulesFor(p: Palette, scope: string, side: string): string;
|
|
81
111
|
/** The words of the state in a sentence, for the drawing's description. */
|
|
82
112
|
export declare function describeJudoContest(contest: JudoContest, options?: Pick<JudoBoardOptions, "locale" | "words">): string;
|
|
83
113
|
/**
|
|
@@ -85,3 +115,11 @@ export declare function describeJudoContest(contest: JudoContest, options?: Pick
|
|
|
85
115
|
* past 20 seconds is clamped to 20).
|
|
86
116
|
*/
|
|
87
117
|
export declare function judoScoreboard(contest: JudoContest, options?: JudoBoardOptions): string;
|
|
118
|
+
export interface R {
|
|
119
|
+
x: number;
|
|
120
|
+
y: number;
|
|
121
|
+
w: number;
|
|
122
|
+
h: number;
|
|
123
|
+
}
|
|
124
|
+
/** The size a text can be set at to fit a width, from the size asked for. */
|
|
125
|
+
export declare function fitSize(text: string, size: number, width: number, floor?: number): number;
|
package/dist/judo-board.js
CHANGED
|
@@ -10,15 +10,15 @@ export const JUDO_BOARD_SIZES = Object.freeze({
|
|
|
10
10
|
tablet: { width: 1280, height: 800 },
|
|
11
11
|
"tablet-tall": { width: 800, height: 1280 },
|
|
12
12
|
});
|
|
13
|
-
const clampN = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
14
|
-
const bw = (text, size) => emWidth(text) * size;
|
|
15
|
-
const LIGHT = { bg: "#ecebe4", ink: "#1b1e1c", muted: "#585d57", band: "#dcdad0", box: "#141716", boxInk: "#f4f1e6", gold: "#e0b43b" };
|
|
16
|
-
const DARK = { bg: "#0e1011", ink: "#eceae0", muted: "#9aa097", band: "#1b1f21", box: "#050606", boxInk: "#f4f1e6", gold: "#e0b43b" };
|
|
17
|
-
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
|
+
export const clampN = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
14
|
+
export const bw = (text, size) => emWidth(text) * size;
|
|
15
|
+
export const LIGHT = { bg: "#ecebe4", ink: "#1b1e1c", muted: "#585d57", band: "#dcdad0", box: "#141716", boxInk: "#f4f1e6", gold: "#e0b43b" };
|
|
16
|
+
export const DARK = { bg: "#0e1011", ink: "#eceae0", muted: "#9aa097", band: "#1b1f21", box: "#050606", boxInk: "#f4f1e6", gold: "#e0b43b" };
|
|
17
|
+
export const FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
|
|
18
18
|
/** The panels' own colours: the white and the blue judogi, the ink and the tiles on each. White on #1f4fa8 reads 8 to 1, and the dark ink on the white panel more. */
|
|
19
|
-
const WHITE_PANEL = { panel: "#f7f6f1", ink: "#15181a", sub: "#464b50", tile: "#e0dfd6", edge: "#bdbbae" };
|
|
20
|
-
const BLUE_PANEL = { panel: JUDO_COLORS.blue, ink: "#ffffff", sub: "#dbe5fa", tile: "#173b86", edge: "#0f2a63" };
|
|
21
|
-
function rulesFor(p, scope, side) {
|
|
19
|
+
export const WHITE_PANEL = { panel: "#f7f6f1", ink: "#15181a", sub: "#464b50", tile: "#e0dfd6", edge: "#bdbbae" };
|
|
20
|
+
export const BLUE_PANEL = { panel: JUDO_COLORS.blue, ink: "#ffffff", sub: "#dbe5fa", tile: "#173b86", edge: "#0f2a63" };
|
|
21
|
+
export function rulesFor(p, scope, side) {
|
|
22
22
|
const s = (sel, body) => `${side}${scope} ${sel}{${body}}`;
|
|
23
23
|
const v = (name, value) => `var(--jb-${name}, ${value})`;
|
|
24
24
|
return [
|
|
@@ -389,7 +389,7 @@ export function judoScoreboard(contest, options = {}) {
|
|
|
389
389
|
].filter(Boolean).join("\n");
|
|
390
390
|
}
|
|
391
391
|
/** The size a text can be set at to fit a width, from the size asked for. */
|
|
392
|
-
function fitSize(text, size, width, floor = 0.62) {
|
|
392
|
+
export function fitSize(text, size, width, floor = 0.62) {
|
|
393
393
|
const need = bw(text, size);
|
|
394
394
|
return need <= width ? size : Math.max(size * (width / need), size * floor);
|
|
395
395
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The road to the final of one judoka as a panel for a broadcast or a hall screen: the athlete (flag, name, code, coach) and the contests they have won on the way, most
|
|
3
|
+
* recent first, each with its round, the opponent (flag, code, name) and how it was won. `judoWins` reads the wins from a bracket; `judoRoadPanel` draws them. Static SVG for a
|
|
4
|
+
* given state, like the other judo drawings.
|
|
5
|
+
*/
|
|
6
|
+
import type { JudoAthlete, JudoBoardOptions } from "./judo-board.ts";
|
|
7
|
+
import type { BracketInput, BracketLayout, DrawOptions, Matchup, SideId, Team } from "./types.ts";
|
|
8
|
+
/** One win on the road: the round it was in, the opponent (none for a bye), and how it was won in words ("ippon · 1:12", "decision · GS · 5:12", or a score). */
|
|
9
|
+
export interface JudoWin {
|
|
10
|
+
round: string;
|
|
11
|
+
opponent?: JudoAthlete;
|
|
12
|
+
how: string;
|
|
13
|
+
bye?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface JudoWinsOptions {
|
|
16
|
+
locale?: DrawOptions["locale"];
|
|
17
|
+
words?: DrawOptions["words"];
|
|
18
|
+
/** The names of the rounds, as for `drawBracket`. */
|
|
19
|
+
roundNames?: DrawOptions["roundNames"];
|
|
20
|
+
/** Turns a bracket's team into the athlete the panel draws. Default: its name, flag, mark and `code`. */
|
|
21
|
+
athlete?: (team: Team) => JudoAthlete;
|
|
22
|
+
/** Write a bye for each round the athlete skipped before their first contest. Default true. */
|
|
23
|
+
byes?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The contests an athlete has won in a bracket, oldest first, with a bye for each round skipped at the start: the opponent, the round (a repechage and a bronze medal contest are named as
|
|
27
|
+
* such) and how it was won, from the judo result's notes or the score. Throws a `RangeError` for an athlete that is not in the bracket.
|
|
28
|
+
*/
|
|
29
|
+
export declare function judoWins(input: BracketLayout | readonly Matchup[] | BracketInput, athlete: SideId, options?: JudoWinsOptions): JudoWin[];
|
|
30
|
+
export interface JudoRoadSource {
|
|
31
|
+
athlete: JudoAthlete;
|
|
32
|
+
/** Oldest first, as `judoWins` gives them. */
|
|
33
|
+
wins: readonly JudoWin[];
|
|
34
|
+
}
|
|
35
|
+
export interface JudoRoadOptions extends Omit<JudoBoardOptions, "style" | "swap" | "coaches"> {
|
|
36
|
+
/** The weight class as a heading (`judoCategoryLabel`), written at the right of the header. */
|
|
37
|
+
weight?: string;
|
|
38
|
+
/** The colour of the athlete's block: the white judogi's or the blue's. Default `blue`. */
|
|
39
|
+
side?: "white" | "blue";
|
|
40
|
+
/** `latest` (the default) puts the most recent win first, as a broadcast does; `first` the oldest. */
|
|
41
|
+
order?: "latest" | "first";
|
|
42
|
+
/** The most wins drawn. Default: as many as fit at a readable size, up to 8. */
|
|
43
|
+
rows?: number;
|
|
44
|
+
/** Write the coach under the code. Default true. */
|
|
45
|
+
coach?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** The road as a sentence, the drawing's description. */
|
|
48
|
+
export declare function describeJudoRoad(source: JudoRoadSource, options?: Pick<JudoRoadOptions, "locale" | "words" | "weight" | "order">): string;
|
|
49
|
+
/**
|
|
50
|
+
* The road-to-the-final panel for an athlete: their block (flag, name, code, coach) in the colour of a judogi, and a row for each contest won, most recent first: the round, the
|
|
51
|
+
* opponent's flag, code and name, and how it was won. A bye is a quiet row. Throws a `RangeError` for an athlete with no name, a win with no round, or a bad size, theme or `rows`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function judoRoadPanel(source: JudoRoadSource, options?: JudoRoadOptions): string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { bw, clampN, fitSize } from "./judo-board.js";
|
|
2
|
+
import { judoNotes } from "./judo-text.js";
|
|
3
|
+
import { check, frameOf, text, teamOf, wrap } from "./judo-sheets.js";
|
|
4
|
+
import { layoutBracket } from "./layout.js";
|
|
5
|
+
import { fill, voiceOf } from "./locales.js";
|
|
6
|
+
import { drawMark } from "./marks.js";
|
|
7
|
+
import { roundLabel } from "./rounds.js";
|
|
8
|
+
import { fit, fitLines, num } from "./text.js";
|
|
9
|
+
const asAthlete = (t) => ({ name: t.name, ...(t.code ? { code: t.code } : {}), ...(t.flag ? { flag: t.flag } : {}), ...(t.mark ? { mark: t.mark } : {}) });
|
|
10
|
+
/**
|
|
11
|
+
* The contests an athlete has won in a bracket, oldest first, with a bye for each round skipped at the start: the opponent, the round (a repechage and a bronze medal contest are named as
|
|
12
|
+
* such) and how it was won, from the judo result's notes or the score. Throws a `RangeError` for an athlete that is not in the bracket.
|
|
13
|
+
*/
|
|
14
|
+
export function judoWins(input, athlete, options = {}) {
|
|
15
|
+
const layout = "ties" in input && "sides" in input ? input : layoutBracket(input);
|
|
16
|
+
const me = layout.teams.find((t) => t.id === athlete);
|
|
17
|
+
if (!me)
|
|
18
|
+
throw new RangeError(`taisen-brackets: ${JSON.stringify(athlete)} is not in the bracket`);
|
|
19
|
+
const v = voiceOf(options);
|
|
20
|
+
const w = v.words;
|
|
21
|
+
const map = options.athlete ?? asAthlete;
|
|
22
|
+
const label = (round) => roundLabel(layout, round, options.roundNames, v);
|
|
23
|
+
const order = (t) => (t.stage === "repechage" ? layout.rounds + 1 : t.stage === "bronze" ? layout.rounds + 2 : t.round);
|
|
24
|
+
const won = layout.ties
|
|
25
|
+
.filter((t) => t.winner !== null && t.sides[t.winner]?.id === athlete && (t.stage !== undefined || t.round > 0))
|
|
26
|
+
.sort((a, b) => order(a) - order(b) || a.index - b.index);
|
|
27
|
+
const rows = won.map((t) => {
|
|
28
|
+
const other = t.sides[(1 - t.winner)];
|
|
29
|
+
const how = t.judo
|
|
30
|
+
? judoNotes(t, v, false).join(" · ")
|
|
31
|
+
: t.result.score ? fill(w.scoreText, { a: v.number(t.result.score[t.winner]), b: v.number(t.result.score[(1 - t.winner)]) }) : "";
|
|
32
|
+
const round = t.stage === "repechage" ? w.judoRepechage : t.stage === "bronze" ? w.judoBronze : label(t.round);
|
|
33
|
+
return { round, ...(other ? { opponent: map(other) } : {}), how };
|
|
34
|
+
});
|
|
35
|
+
const firstTree = won.find((t) => t.stage === undefined);
|
|
36
|
+
const byes = [];
|
|
37
|
+
if ((options.byes ?? true) && firstTree && firstTree.round > 1)
|
|
38
|
+
for (let r = 1; r < firstTree.round; r++)
|
|
39
|
+
byes.push({ round: label(r), how: w.bye, bye: true });
|
|
40
|
+
return [...byes, ...rows];
|
|
41
|
+
}
|
|
42
|
+
const ROAD_CSS = [
|
|
43
|
+
"$S .jr-row{fill:#f7f6f1;stroke:#bdbbae;stroke-width:1.5}",
|
|
44
|
+
"$S .jr-pill{fill:#dcdbd2}",
|
|
45
|
+
"$S .jr-ink{fill:#15181a}",
|
|
46
|
+
"$S .jr-sub{fill:#4a4f54}",
|
|
47
|
+
"$S .jr-bye{fill:#e9e8e1;stroke:#bdbbae;stroke-width:1.5}",
|
|
48
|
+
].join("\n");
|
|
49
|
+
/** The road as a sentence, the drawing's description. */
|
|
50
|
+
export function describeJudoRoad(source, options = {}) {
|
|
51
|
+
const v = voiceOf(options);
|
|
52
|
+
const w = v.words;
|
|
53
|
+
const list = options.order === "first" ? [...source.wins] : [...source.wins].reverse();
|
|
54
|
+
const parts = [`${w.roadTitle}: ${source.athlete.name}${source.athlete.code ? ` (${source.athlete.code})` : ""}`];
|
|
55
|
+
if (options.weight)
|
|
56
|
+
parts.push(options.weight);
|
|
57
|
+
for (const x of list)
|
|
58
|
+
parts.push(x.bye ? `${x.round}: ${w.bye}` : `${x.round}: ${x.opponent?.name ?? ""}${x.how ? ` (${x.how})` : ""}`);
|
|
59
|
+
return parts.join(w.sep) + w.end;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The road-to-the-final panel for an athlete: their block (flag, name, code, coach) in the colour of a judogi, and a row for each contest won, most recent first: the round, the
|
|
63
|
+
* opponent's flag, code and name, and how it was won. A bye is a quiet row. Throws a `RangeError` for an athlete with no name, a win with no round, or a bad size, theme or `rows`.
|
|
64
|
+
*/
|
|
65
|
+
export function judoRoadPanel(source, options = {}) {
|
|
66
|
+
check(source.athlete, "road");
|
|
67
|
+
for (const x of source.wins) {
|
|
68
|
+
if (typeof x.round !== "string" || x.round === "")
|
|
69
|
+
throw new RangeError("taisen-brackets: every win on the road needs a round");
|
|
70
|
+
if (!x.bye && x.opponent)
|
|
71
|
+
check(x.opponent, `opponent in ${x.round}`);
|
|
72
|
+
}
|
|
73
|
+
if (options.side !== undefined && options.side !== "white" && options.side !== "blue")
|
|
74
|
+
throw new RangeError(`taisen-brackets: a road panel's side is "white" or "blue", not ${JSON.stringify(options.side)}`);
|
|
75
|
+
if (options.rows !== undefined && !(Number.isInteger(options.rows) && options.rows >= 1 && options.rows <= 20))
|
|
76
|
+
throw new RangeError("taisen-brackets: rows is a whole number from 1 to 20");
|
|
77
|
+
const f = frameOf(options, [source, options.side, options.order, options.rows, options.weight]);
|
|
78
|
+
const { W, H, v } = f;
|
|
79
|
+
const w = v.words;
|
|
80
|
+
const unit = Math.min(W, H);
|
|
81
|
+
const pad = Math.round(unit * 0.025);
|
|
82
|
+
const landscape = W >= H;
|
|
83
|
+
const body = [];
|
|
84
|
+
const headH = Math.round(H * (landscape ? 0.11 : 0.075));
|
|
85
|
+
body.push(`<rect class="jb-band" x="0" y="0" width="${num(W)}" height="${num(headH)}"/>`);
|
|
86
|
+
const hf = clampN(headH * 0.46, 13, 60);
|
|
87
|
+
const title = w.roadTitle;
|
|
88
|
+
{
|
|
89
|
+
const s = fitSize(title, hf, (W - pad * 2) * 0.55, 0.5);
|
|
90
|
+
body.push(text(pad, headH / 2 + s * 0.36, s, fit(title, s, (W - pad * 2) * 0.55, bw), "jb-ink", "start"));
|
|
91
|
+
}
|
|
92
|
+
if (options.weight) {
|
|
93
|
+
const s = fitSize(options.weight, hf, (W - pad * 2) * 0.4, 0.5);
|
|
94
|
+
body.push(text(W - pad, headH / 2 + s * 0.36, s, fit(options.weight, s, (W - pad * 2) * 0.4, bw), "jb-muted", "end"));
|
|
95
|
+
}
|
|
96
|
+
const top = headH + pad, availH = H - top - pad;
|
|
97
|
+
const side = options.side ?? "blue";
|
|
98
|
+
const A = source.athlete;
|
|
99
|
+
// The athlete's block.
|
|
100
|
+
const blockRect = landscape ? { x: pad, y: top, w: Math.round((W - pad * 3) * 0.4), h: availH } : { x: pad, y: top, w: W - pad * 2, h: Math.round(availH * 0.3) };
|
|
101
|
+
{
|
|
102
|
+
const g = [];
|
|
103
|
+
const rx = Math.round(unit * 0.014);
|
|
104
|
+
g.push(`<rect class="jb-panel" x="${num(blockRect.x)}" y="${num(blockRect.y)}" width="${num(blockRect.w)}" height="${num(blockRect.h)}" rx="${rx}"/>`);
|
|
105
|
+
const ip = Math.max(pad, blockRect.w * 0.05);
|
|
106
|
+
const inner = { x: blockRect.x + ip, y: blockRect.y + ip, w: blockRect.w - ip * 2, h: blockRect.h - ip * 2 };
|
|
107
|
+
const showCoach = (options.coach ?? true) && !!A.coach;
|
|
108
|
+
if (landscape) {
|
|
109
|
+
const flagD = Math.min(inner.w * 0.5, inner.h * 0.36);
|
|
110
|
+
g.push(drawMark(teamOf(A, "road"), inner.x + inner.w / 2, inner.y + flagD / 2, flagD, f.ctx));
|
|
111
|
+
const ty = inner.y + flagD + ip * 0.8;
|
|
112
|
+
const codeH = A.code ? inner.h * 0.07 : 0, coachH = showCoach ? inner.h * 0.045 : 0;
|
|
113
|
+
const fl = fitLines(A.name, inner.w, inner.h - flagD - ip * 0.8 - codeH - coachH - ip, Math.min(inner.h * 0.14, 150));
|
|
114
|
+
fl.lines.forEach((line, i) => g.push(text(inner.x + inner.w / 2, ty + fl.size * (i + 0.86), fl.size, fit(line, fl.size, inner.w + 1, bw), "jb-pink", "middle")));
|
|
115
|
+
let yy = ty + fl.height + ip * 0.3;
|
|
116
|
+
if (A.code) {
|
|
117
|
+
g.push(text(inner.x + inner.w / 2, yy + codeH * 0.9, codeH, A.code, "jb-psub", "middle", 800, ' letter-spacing=".1em"'));
|
|
118
|
+
yy += codeH + ip * 0.2;
|
|
119
|
+
}
|
|
120
|
+
if (showCoach)
|
|
121
|
+
g.push(text(inner.x + inner.w / 2, yy + coachH * 0.9, coachH, fit(fill(w.judoCoach, { name: A.coach }), coachH, inner.w, bw), "jb-psub", "middle", 600));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const flagD = Math.min(inner.h * 0.8, inner.w * 0.22);
|
|
125
|
+
g.push(drawMark(teamOf(A, "road"), inner.x + flagD / 2, inner.y + inner.h / 2, flagD, f.ctx));
|
|
126
|
+
const tx = inner.x + flagD + ip, tw = inner.w - flagD - ip;
|
|
127
|
+
const codeH = A.code ? inner.h * 0.2 : 0, coachH = showCoach ? inner.h * 0.14 : 0;
|
|
128
|
+
const fl = fitLines(A.name, tw, inner.h - codeH - coachH - ip * 0.4, Math.min(inner.h * 0.42, 130));
|
|
129
|
+
const blockH = fl.height + codeH + coachH + (A.code ? ip * 0.2 : 0);
|
|
130
|
+
let yy = inner.y + (inner.h - blockH) / 2;
|
|
131
|
+
fl.lines.forEach((line, i) => g.push(text(tx, yy + fl.size * (i + 0.86), fl.size, fit(line, fl.size, tw + 1, bw), "jb-pink", "start")));
|
|
132
|
+
yy += fl.height + ip * 0.2;
|
|
133
|
+
if (A.code) {
|
|
134
|
+
g.push(text(tx, yy + codeH * 0.9, codeH, A.code, "jb-psub", "start", 800, ' letter-spacing=".1em"'));
|
|
135
|
+
yy += codeH;
|
|
136
|
+
}
|
|
137
|
+
if (showCoach)
|
|
138
|
+
g.push(text(tx, yy + coachH * 0.95, coachH, fit(fill(w.judoCoach, { name: A.coach }), coachH, tw, bw), "jb-psub", "start", 600));
|
|
139
|
+
}
|
|
140
|
+
body.push(`<g class="${side === "white" ? "jb-w" : "jb-b"}">${g.join("")}</g>`);
|
|
141
|
+
}
|
|
142
|
+
// The list.
|
|
143
|
+
const list = options.order === "first" ? [...source.wins] : [...source.wins].reverse();
|
|
144
|
+
const listRect = landscape ? { x: blockRect.x + blockRect.w + pad, y: top, w: W - blockRect.x - blockRect.w - pad * 2, h: availH } : { x: pad, y: top + blockRect.h + pad, w: W - pad * 2, h: availH - blockRect.h - pad };
|
|
145
|
+
const minRow = Math.max(40, unit * 0.06);
|
|
146
|
+
const capacity = Math.min(options.rows ?? 8, Math.max(1, Math.floor(listRect.h / minRow)));
|
|
147
|
+
const shown = list.slice(0, capacity);
|
|
148
|
+
if (shown.length) {
|
|
149
|
+
const rowH = Math.min(listRect.h / shown.length, unit * 0.15);
|
|
150
|
+
const gap = Math.max(4, rowH * 0.08);
|
|
151
|
+
const h = rowH - gap;
|
|
152
|
+
// Every round pill is as wide as the widest round name needs; where the list is narrow the result goes under the name, not beside it.
|
|
153
|
+
const stacked = listRect.w < h * 8;
|
|
154
|
+
const pillFont = h * 0.24;
|
|
155
|
+
const upper = (t) => t.toLocaleUpperCase(v.locale.tag);
|
|
156
|
+
const ip0 = h * 0.2;
|
|
157
|
+
const pillW = Math.min(listRect.w * 0.32, Math.max(...shown.map((x) => bw(upper(x.round), pillFont) * 0.8), listRect.w * 0.16) + ip0 * 1.4);
|
|
158
|
+
shown.forEach((x, i) => {
|
|
159
|
+
const y = listRect.y + i * rowH;
|
|
160
|
+
const g = [];
|
|
161
|
+
const rx = Math.round(unit * 0.01);
|
|
162
|
+
g.push(`<rect class="${x.bye ? "jr-bye" : "jr-row"}" x="${num(listRect.x)}" y="${num(y)}" width="${num(listRect.w)}" height="${num(h)}" rx="${rx}"/>`);
|
|
163
|
+
const ip = ip0;
|
|
164
|
+
g.push(`<rect class="jr-pill" x="${num(listRect.x + ip * 0.6)}" y="${num(y + h * 0.18)}" width="${num(pillW)}" height="${num(h * 0.64)}" rx="${num(h * 0.32)}"/>`);
|
|
165
|
+
const label = upper(x.round);
|
|
166
|
+
const rs = fitSize(label, pillFont, pillW - ip * 0.8, 0.4);
|
|
167
|
+
g.push(text(listRect.x + ip * 0.6 + pillW / 2, y + h / 2 + rs * 0.36, rs, fit(label, rs, pillW - ip * 0.6, bw), "jr-sub", "middle", 800, ' letter-spacing=".03em"'));
|
|
168
|
+
let cx = listRect.x + ip * 0.6 + pillW + ip;
|
|
169
|
+
const right = listRect.x + listRect.w - ip;
|
|
170
|
+
if (x.bye) {
|
|
171
|
+
const s = fitSize(upper(x.how), h * 0.34, right - cx, 0.4);
|
|
172
|
+
g.push(text(cx, y + h / 2 + s * 0.36, s, upper(x.how), "jr-sub", "start"));
|
|
173
|
+
}
|
|
174
|
+
else if (x.opponent) {
|
|
175
|
+
const d = h * 0.6;
|
|
176
|
+
g.push(drawMark(teamOf(x.opponent, `win-${i}`), cx + d / 2, y + h / 2, d, f.ctx));
|
|
177
|
+
cx += d + ip * 0.6;
|
|
178
|
+
const cs = h * 0.24;
|
|
179
|
+
if (x.opponent.code) {
|
|
180
|
+
g.push(text(cx, y + h / 2 + cs * 0.36 - (stacked ? h * 0.2 : 0), cs, x.opponent.code, "jr-sub", "start", 800));
|
|
181
|
+
cx += bw(x.opponent.code, cs) + ip * 0.6;
|
|
182
|
+
}
|
|
183
|
+
if (stacked) {
|
|
184
|
+
const avail = right - cx;
|
|
185
|
+
const ns = fitSize(x.opponent.name, Math.min(h * 0.34, 64), avail, 0.35);
|
|
186
|
+
g.push(text(cx, y + h * 0.46, ns, fit(x.opponent.name, ns, avail + 1, bw), "jr-ink", "start"));
|
|
187
|
+
if (x.how) {
|
|
188
|
+
const hs = fitSize(x.how, h * 0.22, avail, 0.4);
|
|
189
|
+
g.push(text(cx, y + h * 0.82, hs, fit(x.how, hs, avail + 1, bw), "jr-sub", "start", 700));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const howW = Math.min(listRect.w * 0.3, bw(x.how, h * 0.28) * 0.85);
|
|
194
|
+
const nameAvail = right - howW - ip * 0.6 - cx;
|
|
195
|
+
const ns = fitSize(x.opponent.name, Math.min(h * 0.38, 64), nameAvail, 0.35);
|
|
196
|
+
g.push(text(cx, y + h / 2 + ns * 0.36, ns, fit(x.opponent.name, ns, nameAvail + 1, bw), "jr-ink", "start"));
|
|
197
|
+
if (x.how) {
|
|
198
|
+
const hs = fitSize(x.how, h * 0.28, howW, 0.4);
|
|
199
|
+
g.push(text(right, y + h / 2 + hs * 0.36, hs, fit(x.how, hs, howW + 1, bw), "jr-sub", "end", 700));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
body.push(`<g class="jr-win">${g.join("")}</g>`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return wrap(f, options.title ?? w.roadTitle, describeJudoRoad(source, options), body, "road", ROAD_CSS);
|
|
207
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two more drawings for a judo event's screens: the contest card (who fights, in which category and round: the white athlete over the blue one, big, for the
|
|
3
|
+
* moment the athletes enter) and the mat queue board (per mat, a big mat number and colour and the contests to come, in order, with their numbers, the white
|
|
4
|
+
* and the blue athlete with flags and codes). Static SVG for a given state, like the scoreboard; showing the next state is the caller's job.
|
|
5
|
+
*/
|
|
6
|
+
import type { JudoAthlete, JudoBoardOptions, JudoContest } from "./judo-board.ts";
|
|
7
|
+
import { type Voice } from "./locales.ts";
|
|
8
|
+
import { type MarkContext } from "./marks.ts";
|
|
9
|
+
import type { Team } from "./types.ts";
|
|
10
|
+
/** What the contest card shows: the two athletes and the header. The scores and the clock of a `JudoContest` are not drawn. */
|
|
11
|
+
export type JudoCardContest = Pick<JudoContest, "white" | "blue" | "weight" | "round" | "mat" | "contest">;
|
|
12
|
+
/** One contest of a mat's queue: its number (`"G-24"`, `24`) and the two athletes, and optionally the weight class and the round (for a caption). */
|
|
13
|
+
export interface JudoQueueContest {
|
|
14
|
+
number: string | number;
|
|
15
|
+
white: JudoAthlete;
|
|
16
|
+
blue: JudoAthlete;
|
|
17
|
+
}
|
|
18
|
+
/** One mat: its number or name, its colour (any CSS colour that a hex code gives, default by position) and its contests in the order they will be fought. */
|
|
19
|
+
export interface JudoMat {
|
|
20
|
+
mat: string | number;
|
|
21
|
+
color?: string;
|
|
22
|
+
contests: readonly JudoQueueContest[];
|
|
23
|
+
}
|
|
24
|
+
export interface JudoQueueOptions extends Omit<JudoBoardOptions, "style" | "coaches"> {
|
|
25
|
+
/** The most contests drawn for a mat. Default: as many as fit at a readable size, up to 12. */
|
|
26
|
+
rows?: number;
|
|
27
|
+
/** Show the first contest of each mat as the one on now (a green number). Default true. */
|
|
28
|
+
current?: boolean;
|
|
29
|
+
/** The colours the mats take in turn where a mat names none. Hex codes. */
|
|
30
|
+
colors?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/** The IJF-style mat colours, in turn: red, green, yellow, purple, orange, teal. Each reads with the ink the board picks for it. */
|
|
33
|
+
export declare const JUDO_MAT_COLORS: readonly string[];
|
|
34
|
+
/** White or near-black, whichever reads better on a hex colour. */
|
|
35
|
+
export declare function inkOn(color: string): string;
|
|
36
|
+
export declare function sheetStyle(scope: string, theme: "auto" | "light" | "dark", extra?: string): string;
|
|
37
|
+
export interface Frame {
|
|
38
|
+
W: number;
|
|
39
|
+
H: number;
|
|
40
|
+
v: Voice;
|
|
41
|
+
scope: string;
|
|
42
|
+
theme: "auto" | "light" | "dark";
|
|
43
|
+
ctx: MarkContext;
|
|
44
|
+
options: JudoBoardOptions;
|
|
45
|
+
}
|
|
46
|
+
export declare function frameOf(options: JudoBoardOptions, seed: unknown): Frame;
|
|
47
|
+
export declare function wrap(f: Frame, title: string, desc: string, body: string[], label: string, extraCss?: string): string;
|
|
48
|
+
export declare const text: (x: number, y: number, size: number, content: string, cls: string, anchor?: "start" | "middle" | "end", weight?: number, extra?: string) => string;
|
|
49
|
+
export declare const teamOf: (a: JudoAthlete, id: string) => Team;
|
|
50
|
+
export declare function check(a: JudoAthlete | undefined, side: string): void;
|
|
51
|
+
/** The contest card as a sentence, the drawing's description. */
|
|
52
|
+
export declare function describeJudoCard(contest: JudoCardContest, options?: Pick<JudoBoardOptions, "locale" | "words">): string;
|
|
53
|
+
/**
|
|
54
|
+
* The contest card: the weight class and the round on top, then the white athlete over the blue one, each as a wide band with the flag, the name as big as the band lets it be,
|
|
55
|
+
* and the country code. Throws a `RangeError` for an athlete with no name or a bad size or theme. `swap` draws blue first.
|
|
56
|
+
*/
|
|
57
|
+
export declare function judoContestCard(contest: JudoCardContest, options?: JudoBoardOptions): string;
|
|
58
|
+
/** The mat queue as a sentence: for each mat, its contests in order. */
|
|
59
|
+
export declare function describeJudoQueue(mats: JudoMat | readonly JudoMat[], options?: Pick<JudoBoardOptions, "locale" | "words">): string;
|
|
60
|
+
/**
|
|
61
|
+
* The mat queue board: for each mat a big number in the mat's colour and the contests to come, in order, each a row with the contest number, the white athlete and the blue
|
|
62
|
+
* one (name, code, flag). The mats are in columns on a wide board and stacked on an upright one. A mat with more contests than fit is cut at `rows`. Throws a `RangeError`
|
|
63
|
+
* for no mat, a mat with no number, a contest with no number or no athlete name, a colour that is not a hex code, or a bad size or theme.
|
|
64
|
+
*/
|
|
65
|
+
export declare function judoMatQueue(input: JudoMat | readonly JudoMat[], options?: JudoQueueOptions): string;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { BLUE_PANEL, DARK, FONT, JUDO_BOARD_SIZES, LIGHT, WHITE_PANEL, bw, clampN, fitSize, rulesFor } from "./judo-board.js";
|
|
2
|
+
import { fill, voiceOf } from "./locales.js";
|
|
3
|
+
import { drawMark, markDefs } from "./marks.js";
|
|
4
|
+
import { emWidth, esc, fit, fitLines, hash, num } from "./text.js";
|
|
5
|
+
/** The IJF-style mat colours, in turn: red, green, yellow, purple, orange, teal. Each reads with the ink the board picks for it. */
|
|
6
|
+
export const JUDO_MAT_COLORS = Object.freeze(["#d2232a", "#1e9c4c", "#e7b800", "#7a3fb4", "#e8741a", "#12877f"]);
|
|
7
|
+
/** White or near-black, whichever reads better on a hex colour. */
|
|
8
|
+
export function inkOn(color) {
|
|
9
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim())?.[1];
|
|
10
|
+
if (!hex)
|
|
11
|
+
return "#ffffff";
|
|
12
|
+
const full = hex.length === 3 ? [...hex].map((c) => c + c).join("") : hex;
|
|
13
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
14
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.4 ? "#ffffff" : "#15181a";
|
|
15
|
+
}
|
|
16
|
+
const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
17
|
+
export function sheetStyle(scope, theme, extra = "") {
|
|
18
|
+
const base = (p, before) => rulesFor(p, `.${scope}`, before);
|
|
19
|
+
const fixed = [
|
|
20
|
+
`.${scope} .jb-w .jb-panel{fill:var(--jb-white-panel, ${WHITE_PANEL.panel});stroke:${WHITE_PANEL.edge};stroke-width:2}`,
|
|
21
|
+
`.${scope} .jb-w .jb-pink{fill:${WHITE_PANEL.ink}}`,
|
|
22
|
+
`.${scope} .jb-w .jb-psub{fill:${WHITE_PANEL.sub}}`,
|
|
23
|
+
`.${scope} .jb-b .jb-panel{fill:var(--jb-blue-panel, ${BLUE_PANEL.panel});stroke:${BLUE_PANEL.edge};stroke-width:2}`,
|
|
24
|
+
`.${scope} .jb-b .jb-pink{fill:${BLUE_PANEL.ink}}`,
|
|
25
|
+
`.${scope} .jb-b .jb-psub{fill:${BLUE_PANEL.sub}}`,
|
|
26
|
+
`.${scope} .jq-num{fill:#d9d8cf}`,
|
|
27
|
+
`.${scope} .jq-numt{fill:#15181a}`,
|
|
28
|
+
`.${scope} .jq-now{fill:var(--jb-now, #1d9a4a)}`,
|
|
29
|
+
`.${scope} .jq-nowt{fill:#ffffff}`,
|
|
30
|
+
`.${scope} .tb-ring{fill:none;stroke:rgba(0,0,0,.35);stroke-width:2}`,
|
|
31
|
+
`.${scope} .tb-plain{fill:#d9d3c2}`,
|
|
32
|
+
`.${scope} .tb-initials{fill:#1f2320}`,
|
|
33
|
+
`.${scope} text{font-family:var(--jb-font, ${FONT});font-variant-numeric:tabular-nums}`,
|
|
34
|
+
].join("\n");
|
|
35
|
+
const tail = extra ? `${fixed}\n${extra.replaceAll("$S", `.${scope}`)}` : fixed;
|
|
36
|
+
if (theme === "light")
|
|
37
|
+
return `${base(LIGHT, "")}\n${tail}`;
|
|
38
|
+
if (theme === "dark")
|
|
39
|
+
return `${base(DARK, "")}\n${tail}`;
|
|
40
|
+
return `${base(LIGHT, "")}\n@media (prefers-color-scheme: dark){\n${base(DARK, ':root:not([data-theme="light"]) ')}\n}\n${base(DARK, ':root[data-theme="dark"] ')}\n${tail}`;
|
|
41
|
+
}
|
|
42
|
+
export function frameOf(options, seed) {
|
|
43
|
+
const v = voiceOf(options);
|
|
44
|
+
const size = typeof options.size === "string" ? JUDO_BOARD_SIZES[options.size] : options.size ?? JUDO_BOARD_SIZES.tv;
|
|
45
|
+
if (!size || !(size.width >= 200) || !(size.height >= 200))
|
|
46
|
+
throw new RangeError(`taisen-brackets: a size is "tv", "tablet", "tablet-tall" or { width, height } of 200 or more, not ${JSON.stringify(options.size)}`);
|
|
47
|
+
const theme = options.theme ?? "auto";
|
|
48
|
+
if (theme !== "auto" && theme !== "light" && theme !== "dark")
|
|
49
|
+
throw new RangeError(`taisen-brackets: a theme is "auto", "light" or "dark", not ${JSON.stringify(theme)}`);
|
|
50
|
+
const scope = options.id ?? `jb-${hash(JSON.stringify([seed, options.size, theme, options.swap, v.locale.code, options.flags ? 1 : 0, options.flagHref ? 1 : 0, options.title]))}`;
|
|
51
|
+
const ctx = { radii: new Set(), symbols: new Map(), prefix: scope, shape: "round", theme: "inherit", flagCache: new Map(), ...(options.flags ? { flags: options.flags } : {}), ...(options.flagHref ? { flagHref: options.flagHref } : {}) };
|
|
52
|
+
return { W: size.width, H: size.height, v, scope, theme, ctx, options };
|
|
53
|
+
}
|
|
54
|
+
export function wrap(f, title, desc, body, label, extraCss = "") {
|
|
55
|
+
return [
|
|
56
|
+
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(f.v.locale.code)}" class="taisen taisen-judo ${esc(f.scope)}${f.options.className ? ` ${esc(f.options.className)}` : ""}" viewBox="0 0 ${num(f.W)} ${num(f.H)}" width="${num(f.W)}" height="${num(f.H)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${esc(f.scope)}-t ${esc(f.scope)}-d" data-board="${label}">`,
|
|
57
|
+
`<title id="${esc(f.scope)}-t">${esc(title)}</title><desc id="${esc(f.scope)}-d">${esc(desc)}</desc>`,
|
|
58
|
+
`<style>${sheetStyle(f.scope, f.theme, extraCss)}</style>`,
|
|
59
|
+
markDefs(f.ctx) ? `<defs>${markDefs(f.ctx)}</defs>` : "",
|
|
60
|
+
`<rect class="jb-bg" width="${num(f.W)}" height="${num(f.H)}"/>`,
|
|
61
|
+
...body,
|
|
62
|
+
"</svg>",
|
|
63
|
+
].filter(Boolean).join("\n");
|
|
64
|
+
}
|
|
65
|
+
export const text = (x, y, size, content, cls, anchor = "start", weight = 800, extra = "") => `<text class="${cls}" x="${num(x)}" y="${num(y)}" font-size="${num(size)}" font-weight="${weight}" text-anchor="${anchor}"${extra}>${esc(content)}</text>`;
|
|
66
|
+
export const teamOf = (a, id) => ({ id, name: a.name, abbr: (a.code ?? a.name).slice(0, 3).toUpperCase(), ...(a.flag ? { flag: a.flag } : {}), ...(a.mark ? { mark: a.mark } : {}) });
|
|
67
|
+
export function check(a, side) {
|
|
68
|
+
if (!a || typeof a.name !== "string" || a.name.trim() === "")
|
|
69
|
+
throw new RangeError(`taisen-brackets: the ${side} athlete needs a name`);
|
|
70
|
+
}
|
|
71
|
+
// ------------------------------------------------------------------------------------------------ the contest card
|
|
72
|
+
/** The contest card as a sentence, the drawing's description. */
|
|
73
|
+
export function describeJudoCard(contest, options = {}) {
|
|
74
|
+
const v = voiceOf(options);
|
|
75
|
+
const w = v.words;
|
|
76
|
+
const parts = [];
|
|
77
|
+
if (contest.weight)
|
|
78
|
+
parts.push(contest.weight);
|
|
79
|
+
if (contest.round)
|
|
80
|
+
parts.push(contest.round);
|
|
81
|
+
if (contest.mat !== undefined)
|
|
82
|
+
parts.push(fill(w.judoMat, { n: String(contest.mat) }));
|
|
83
|
+
if (contest.contest !== undefined)
|
|
84
|
+
parts.push(fill(w.judoContest, { n: String(contest.contest) }));
|
|
85
|
+
parts.push(`${w.judoWhite}: ${contest.white.name}${contest.white.code ? ` (${contest.white.code})` : ""}`);
|
|
86
|
+
parts.push(`${w.judoBlue}: ${contest.blue.name}${contest.blue.code ? ` (${contest.blue.code})` : ""}`);
|
|
87
|
+
return parts.join(w.sep) + w.end;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The contest card: the weight class and the round on top, then the white athlete over the blue one, each as a wide band with the flag, the name as big as the band lets it be,
|
|
91
|
+
* and the country code. Throws a `RangeError` for an athlete with no name or a bad size or theme. `swap` draws blue first.
|
|
92
|
+
*/
|
|
93
|
+
export function judoContestCard(contest, options = {}) {
|
|
94
|
+
check(contest.white, "white");
|
|
95
|
+
check(contest.blue, "blue");
|
|
96
|
+
const f = frameOf(options, contest);
|
|
97
|
+
const { W, H, v } = f;
|
|
98
|
+
const w = v.words;
|
|
99
|
+
const unit = Math.min(W, H);
|
|
100
|
+
const pad = Math.round(unit * 0.025);
|
|
101
|
+
const landscape = W >= H;
|
|
102
|
+
const headH = Math.round(H * (landscape ? 0.15 : 0.1));
|
|
103
|
+
const body = [];
|
|
104
|
+
body.push(`<rect class="jb-band" x="0" y="0" width="${num(W)}" height="${num(headH)}"/>`);
|
|
105
|
+
const hf = clampN(headH * 0.5, 14, 90);
|
|
106
|
+
const where = [contest.mat !== undefined ? fill(w.judoMat, { n: String(contest.mat) }) : "", contest.contest !== undefined ? fill(w.judoContest, { n: String(contest.contest) }) : ""].filter(Boolean).join(" · ");
|
|
107
|
+
const inner = W - pad * 2;
|
|
108
|
+
const leftT = contest.weight ?? "";
|
|
109
|
+
const rightT = contest.round ?? "";
|
|
110
|
+
const midT = where;
|
|
111
|
+
if (leftT) {
|
|
112
|
+
const s = fitSize(leftT, hf, inner * 0.4, 0.5);
|
|
113
|
+
body.push(text(pad, headH / 2 + s * 0.36, s, fit(leftT, s, inner * 0.4, bw), "jb-ink", "start"));
|
|
114
|
+
}
|
|
115
|
+
if (rightT) {
|
|
116
|
+
const s = fitSize(rightT, hf, inner * 0.4, 0.5);
|
|
117
|
+
body.push(text(W - pad, headH / 2 + s * 0.36, s, fit(rightT, s, inner * 0.4, bw), "jb-ink", "end"));
|
|
118
|
+
}
|
|
119
|
+
if (midT && landscape) {
|
|
120
|
+
const s = fitSize(midT, hf * 0.7, inner * 0.28, 0.5);
|
|
121
|
+
body.push(text(W / 2, headH / 2 + s * 0.36, s, fit(midT, s, inner * 0.28, bw), "jb-muted", "middle"));
|
|
122
|
+
}
|
|
123
|
+
const top = headH + pad;
|
|
124
|
+
const avail = H - top - pad;
|
|
125
|
+
const bandH = (avail - pad) / 2;
|
|
126
|
+
const order = (options.swap ? [1, 0] : [0, 1]);
|
|
127
|
+
order.forEach((k, i) => {
|
|
128
|
+
const a = k === 0 ? contest.white : contest.blue;
|
|
129
|
+
const y = top + i * (bandH + pad);
|
|
130
|
+
const g = [];
|
|
131
|
+
g.push(`<rect class="jb-panel" x="${num(pad)}" y="${num(y)}" width="${num(W - pad * 2)}" height="${num(bandH)}" rx="${Math.round(unit * 0.014)}"/>`);
|
|
132
|
+
const ip = Math.max(pad, bandH * 0.1);
|
|
133
|
+
const flagD = Math.min(bandH - ip * 2, (W - pad * 2) * 0.22);
|
|
134
|
+
g.push(drawMark(teamOf(a, k === 0 ? "white" : "blue"), pad + ip + flagD / 2, y + bandH / 2, flagD, f.ctx));
|
|
135
|
+
const tx = pad + ip * 2 + flagD;
|
|
136
|
+
const tw = W - pad - ip - tx;
|
|
137
|
+
const codeH = a.code ? bandH * 0.2 : 0;
|
|
138
|
+
const fl = fitLines(a.name, tw, bandH - ip * 2 - codeH - (a.code ? ip * 0.4 : 0), Math.min(bandH * 0.5, 220));
|
|
139
|
+
const blockH = fl.height + (a.code ? ip * 0.4 + codeH : 0);
|
|
140
|
+
let yy = y + (bandH - blockH) / 2;
|
|
141
|
+
fl.lines.forEach((line, li) => g.push(text(tx, yy + fl.size * (li + 0.86), fl.size, fit(line, fl.size, tw + 1, bw), "jb-pink", "start")));
|
|
142
|
+
yy += fl.height + ip * 0.4;
|
|
143
|
+
if (a.code)
|
|
144
|
+
g.push(text(tx, yy + codeH * 0.86, codeH, a.code, "jb-psub", "start", 800, ' letter-spacing=".1em"'));
|
|
145
|
+
body.push(`<g class="${k === 0 ? "jb-w" : "jb-b"}">${g.join("")}</g>`);
|
|
146
|
+
});
|
|
147
|
+
const title = options.title ?? w.judoCardTitle;
|
|
148
|
+
return wrap(f, title, describeJudoCard(contest, options), body, "card");
|
|
149
|
+
}
|
|
150
|
+
// ------------------------------------------------------------------------------------------------ the mat queue board
|
|
151
|
+
/** The mat queue as a sentence: for each mat, its contests in order. */
|
|
152
|
+
export function describeJudoQueue(mats, options = {}) {
|
|
153
|
+
const v = voiceOf(options);
|
|
154
|
+
const w = v.words;
|
|
155
|
+
const list = Array.isArray(mats) ? mats : [mats];
|
|
156
|
+
return list.map((m) => `${fill(w.judoMat, { n: String(m.mat) })}: ${m.contests.map((c) => `${c.number} ${fill(w.tieVs, { a: `${c.white.name} (${w.judoWhite})`, b: `${c.blue.name} (${w.judoBlue})` })}`).join("; ") || "-"}`).join(w.sep) + w.end;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The mat queue board: for each mat a big number in the mat's colour and the contests to come, in order, each a row with the contest number, the white athlete and the blue
|
|
160
|
+
* one (name, code, flag). The mats are in columns on a wide board and stacked on an upright one. A mat with more contests than fit is cut at `rows`. Throws a `RangeError`
|
|
161
|
+
* for no mat, a mat with no number, a contest with no number or no athlete name, a colour that is not a hex code, or a bad size or theme.
|
|
162
|
+
*/
|
|
163
|
+
export function judoMatQueue(input, options = {}) {
|
|
164
|
+
const mats = Array.isArray(input) ? input : [input];
|
|
165
|
+
if (mats.length === 0)
|
|
166
|
+
throw new RangeError("taisen-brackets: a mat queue board needs at least one mat");
|
|
167
|
+
if (mats.length > 8)
|
|
168
|
+
throw new RangeError("taisen-brackets: a mat queue board draws eight mats at most");
|
|
169
|
+
for (const m of mats) {
|
|
170
|
+
if (m.mat === undefined || String(m.mat).trim() === "")
|
|
171
|
+
throw new RangeError("taisen-brackets: every mat needs a number or a name");
|
|
172
|
+
if (m.color !== undefined && !HEX.test(m.color))
|
|
173
|
+
throw new RangeError(`taisen-brackets: a mat's colour is a hex code ("#d2232a"), not ${JSON.stringify(m.color)}`);
|
|
174
|
+
m.contests.forEach((c, i) => {
|
|
175
|
+
if (c.number === undefined || String(c.number).trim() === "")
|
|
176
|
+
throw new RangeError(`taisen-brackets: contest ${i + 1} of mat ${m.mat} needs a number`);
|
|
177
|
+
check(c.white, `white of contest ${c.number}`);
|
|
178
|
+
check(c.blue, `blue of contest ${c.number}`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (options.rows !== undefined && !(Number.isInteger(options.rows) && options.rows >= 1 && options.rows <= 40))
|
|
182
|
+
throw new RangeError("taisen-brackets: rows is a whole number from 1 to 40");
|
|
183
|
+
const palette = options.colors ?? JUDO_MAT_COLORS;
|
|
184
|
+
for (const c of palette)
|
|
185
|
+
if (!HEX.test(c))
|
|
186
|
+
throw new RangeError(`taisen-brackets: a mat colour is a hex code, not ${JSON.stringify(c)}`);
|
|
187
|
+
const f = frameOf(options, mats);
|
|
188
|
+
const { W, H, v } = f;
|
|
189
|
+
const w = v.words;
|
|
190
|
+
const unit = Math.min(W, H);
|
|
191
|
+
const pad = Math.round(unit * 0.02);
|
|
192
|
+
const n = mats.length;
|
|
193
|
+
// As many columns as leave each mat room for its names: about six tenths of the short side.
|
|
194
|
+
const cols = Math.max(1, Math.min(n, Math.floor((W - pad) / (unit * 0.62 + pad))));
|
|
195
|
+
const rowsN = Math.ceil(n / cols);
|
|
196
|
+
const title = options.title ?? w.judoQueueTitle;
|
|
197
|
+
const body = [];
|
|
198
|
+
const cellW = (W - pad * (cols + 1)) / cols;
|
|
199
|
+
const cellH = (H - pad * (rowsN + 1)) / rowsN;
|
|
200
|
+
const longest = Math.max(...mats.map((m) => m.contests.length), 1);
|
|
201
|
+
const minRow = Math.max(36, unit * 0.045);
|
|
202
|
+
const fits = Math.max(1, Math.floor(cellH / minRow));
|
|
203
|
+
const capacity = Math.min(options.rows ?? 12, fits);
|
|
204
|
+
const shown = Math.min(longest, capacity);
|
|
205
|
+
const rowH = Math.min(cellH / Math.max(shown, 1), unit * 0.11);
|
|
206
|
+
// The widest contest number in ems, so that every mat's number column is the same width.
|
|
207
|
+
const widest = Math.max(...mats.flatMap((m) => m.contests.slice(0, shown).map((q) => emWidth(String(q.number)))), 1);
|
|
208
|
+
mats.forEach((m, mi) => {
|
|
209
|
+
const col = mi % cols, row = Math.floor(mi / cols);
|
|
210
|
+
const x = pad + col * (cellW + pad), y = pad + row * (cellH + pad);
|
|
211
|
+
const color = m.color ?? palette[mi % palette.length];
|
|
212
|
+
const g = [];
|
|
213
|
+
const stripW = Math.min(cellW * 0.12, cellH * 0.22 + 30);
|
|
214
|
+
const rx = Math.round(unit * 0.012);
|
|
215
|
+
g.push(`<rect x="${num(x)}" y="${num(y)}" width="${num(stripW)}" height="${num(cellH)}" rx="${rx}" style="fill:${color}"/>`);
|
|
216
|
+
const label = String(m.mat);
|
|
217
|
+
const ns = Math.min(stripW * 0.8, cellH * 0.5, (stripW - 12) / (label.length * 0.66));
|
|
218
|
+
g.push(`<g><title>${esc(fill(w.judoMat, { n: label }))}</title><text x="${num(x + stripW / 2)}" y="${num(y + Math.min(cellH * 0.5, rowH * 2.2) / 2 + ns * 0.4)}" font-size="${num(ns)}" font-weight="800" text-anchor="middle" style="fill:${inkOn(color)}">${esc(label)}</text></g>`);
|
|
219
|
+
const rx0 = x + stripW + pad * 0.5;
|
|
220
|
+
const rowW = cellW - stripW - pad * 0.5;
|
|
221
|
+
m.contests.slice(0, shown).forEach((c, ci) => {
|
|
222
|
+
const ry = y + ci * rowH;
|
|
223
|
+
const h = rowH - Math.max(2, rowH * 0.05);
|
|
224
|
+
const current = (options.current ?? true) && ci === 0;
|
|
225
|
+
const numFont = h * 0.4;
|
|
226
|
+
const numW = Math.min(rowW * 0.26, Math.max(h * 1.2, widest * numFont * 1.04 + h * 0.35));
|
|
227
|
+
g.push(`<rect class="${current ? "jq-now" : "jq-num"}" x="${num(rx0)}" y="${num(ry)}" width="${num(numW)}" height="${num(h)}"/>`);
|
|
228
|
+
const nl = String(c.number);
|
|
229
|
+
const ns2 = Math.min(numFont, (numW - 8) / (nl.length * 0.68));
|
|
230
|
+
g.push(text(rx0 + numW / 2, ry + h / 2 + ns2 * 0.36, ns2, nl, current ? "jq-nowt" : "jq-numt", "middle"));
|
|
231
|
+
const sideW = (rowW - numW) / 2;
|
|
232
|
+
const sides = [[options.swap ? 1 : 0, rx0 + numW], [options.swap ? 0 : 1, rx0 + numW + sideW]];
|
|
233
|
+
sides.forEach(([k, sx], si) => {
|
|
234
|
+
const a = k === 0 ? c.white : c.blue;
|
|
235
|
+
const cls = k === 0 ? "jb-w" : "jb-b";
|
|
236
|
+
const gg = [];
|
|
237
|
+
gg.push(`<rect class="jb-panel" x="${num(sx)}" y="${num(ry)}" width="${num(sideW)}" height="${num(h)}" style="stroke-width:0"/>`);
|
|
238
|
+
const u = Math.min(h, sideW * 0.27);
|
|
239
|
+
const d = u * 0.56, ip = u * 0.16;
|
|
240
|
+
const code = a.code ?? "";
|
|
241
|
+
const cs = u * 0.26;
|
|
242
|
+
const codeW = code ? bw(code, cs) + ip * 0.5 : 0;
|
|
243
|
+
const flagX = si === 0 ? sx + sideW - ip - d / 2 : sx + ip + d / 2;
|
|
244
|
+
gg.push(drawMark(teamOf(a, `${m.mat}-${c.number}-${k}`), flagX, ry + h / 2, d, f.ctx));
|
|
245
|
+
// The first column (left): name, then code, then flag at the right edge; the second: flag at the left edge, then code, then name.
|
|
246
|
+
const nameFont = Math.min(u * 0.36, 64);
|
|
247
|
+
if (si === 0) {
|
|
248
|
+
const avail = sideW - ip * 3 - d - codeW;
|
|
249
|
+
const sz = fitSize(a.name, nameFont, avail, 0.3);
|
|
250
|
+
gg.push(text(sx + ip, ry + h / 2 + sz * 0.36, sz, fit(a.name, sz, avail + 1, bw), "jb-pink", "start"));
|
|
251
|
+
if (code)
|
|
252
|
+
gg.push(text(sx + sideW - ip * 1.5 - d, ry + h / 2 + cs * 0.36, cs, code, "jb-psub", "end"));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const nx = sx + ip * 1.5 + d + codeW + ip * 0.5;
|
|
256
|
+
const avail = sx + sideW - ip - nx;
|
|
257
|
+
const sz = fitSize(a.name, nameFont, avail, 0.3);
|
|
258
|
+
if (code)
|
|
259
|
+
gg.push(text(sx + ip * 1.5 + d, ry + h / 2 + cs * 0.36, cs, code, "jb-psub", "start"));
|
|
260
|
+
gg.push(text(nx, ry + h / 2 + sz * 0.36, sz, fit(a.name, sz, avail + 1, bw), "jb-pink", "start"));
|
|
261
|
+
}
|
|
262
|
+
g.push(`<g class="${cls}">${gg.join("")}</g>`);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
body.push(`<g class="jq-mat">${g.join("")}</g>`);
|
|
266
|
+
});
|
|
267
|
+
return wrap(f, title, describeJudoQueue(mats, options), body, "queue");
|
|
268
|
+
}
|
package/dist/locales.js
CHANGED
|
@@ -27,6 +27,7 @@ const EN = {
|
|
|
27
27
|
judoRepechage: "Repechage", judoBronze: "Bronze medal contest", judoRepechageBronze: "Repechage and bronze medal contests", judoWhite: "white", judoBlue: "blue",
|
|
28
28
|
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Men", judoWomen: "Women",
|
|
29
29
|
judoOsaekomi: "Osaekomi", judoShido: "Shido", judoMat: "Mat {n}", judoContest: "Contest {n}", judoCoach: "Coach: {name}", judoBoardTitle: "Judo contest scoreboard",
|
|
30
|
+
judoCardTitle: "Judo contest card", judoQueueTitle: "Judo mat queue",
|
|
30
31
|
};
|
|
31
32
|
const FR = {
|
|
32
33
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -50,6 +51,7 @@ const FR = {
|
|
|
50
51
|
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",
|
|
51
52
|
judoWeightClass: "{sex} -{w}\u00a0kg", judoWeightOver: "{sex} +{w}\u00a0kg", judoMen: "Hommes", judoWomen: "Femmes",
|
|
52
53
|
judoOsaekomi: "Osaekomi", judoShido: "Shido", judoMat: "Tapis {n}", judoContest: "Combat {n}", judoCoach: "Entraîneur\u00a0: {name}", judoBoardTitle: "Tableau d’affichage d’un combat de judo",
|
|
54
|
+
judoCardTitle: "Présentation d’un combat de judo", judoQueueTitle: "Prochains combats par tapis",
|
|
53
55
|
};
|
|
54
56
|
const JA = {
|
|
55
57
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -73,6 +75,7 @@ const JA = {
|
|
|
73
75
|
judoRepechage: "敗者復活戦", judoBronze: "銅メダル決定戦", judoRepechageBronze: "敗者復活戦と銅メダル決定戦", judoWhite: "白", judoBlue: "青",
|
|
74
76
|
judoWeightClass: "{sex}{w}kg級", judoWeightOver: "{sex}{w}kg超級", judoMen: "男子", judoWomen: "女子",
|
|
75
77
|
judoOsaekomi: "抑え込み", judoShido: "指導", judoMat: "第{n}試合場", judoContest: "試合 {n}", judoCoach: "コーチ:{name}", judoBoardTitle: "柔道試合のスコアボード",
|
|
78
|
+
judoCardTitle: "柔道の対戦カード", judoQueueTitle: "各試合場の次の試合",
|
|
76
79
|
};
|
|
77
80
|
const ES = {
|
|
78
81
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -96,6 +99,7 @@ const ES = {
|
|
|
96
99
|
judoRepechage: "Repesca", judoBronze: "Combate por la medalla de bronce", judoRepechageBronze: "Repesca y combates por la medalla de bronce", judoWhite: "blanco", judoBlue: "azul",
|
|
97
100
|
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Hombres", judoWomen: "Mujeres",
|
|
98
101
|
judoOsaekomi: "Osaekomi", judoShido: "Shido", judoMat: "Tatami {n}", judoContest: "Combate {n}", judoCoach: "Entrenador: {name}", judoBoardTitle: "Marcador de un combate de judo",
|
|
102
|
+
judoCardTitle: "Ficha de un combate de judo", judoQueueTitle: "Próximos combates por tatami",
|
|
99
103
|
};
|
|
100
104
|
const DE = {
|
|
101
105
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -119,6 +123,7 @@ const DE = {
|
|
|
119
123
|
judoRepechage: "Hoffnungsrunde", judoBronze: "Kampf um Bronze", judoRepechageBronze: "Hoffnungsrunde und Kämpfe um Bronze", judoWhite: "weiß", judoBlue: "blau",
|
|
120
124
|
judoWeightClass: "{sex} -{w} kg", judoWeightOver: "{sex} +{w} kg", judoMen: "Herren", judoWomen: "Damen",
|
|
121
125
|
judoOsaekomi: "Osaekomi", judoShido: "Shido", judoMat: "Matte {n}", judoContest: "Kampf {n}", judoCoach: "Trainer: {name}", judoBoardTitle: "Anzeigetafel eines Judokampfes",
|
|
126
|
+
judoCardTitle: "Kampfkarte eines Judokampfes", judoQueueTitle: "Nächste Kämpfe je Matte",
|
|
122
127
|
};
|
|
123
128
|
/**
|
|
124
129
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface Team {
|
|
|
33
33
|
* renderer, or one that has no flag for the code, the team's `mark` is drawn, or its initials: it never throws.
|
|
34
34
|
*/
|
|
35
35
|
flag?: string;
|
|
36
|
+
/** The three letters of the team's country (`JPN`), which the judo screens write beside the name (`judoWins`, `judoRoadPanel`). */
|
|
37
|
+
code?: string;
|
|
36
38
|
}
|
|
37
39
|
/** One game of a tie: a match of a knockout, a game of a series, a leg of an aggregate, a replay. */
|
|
38
40
|
export interface Game {
|
|
@@ -392,6 +394,9 @@ export interface Words {
|
|
|
392
394
|
judoContest: string;
|
|
393
395
|
judoCoach: string;
|
|
394
396
|
judoBoardTitle: string;
|
|
397
|
+
/** The default titles of the contest card and of the mat queue board. */
|
|
398
|
+
judoCardTitle: string;
|
|
399
|
+
judoQueueTitle: string;
|
|
395
400
|
}
|
|
396
401
|
/** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
|
|
397
402
|
export interface Locale {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.10.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.10.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Taisen Brackets 対戦, knockout brackets as SVG: from a list of ties (games, best-of series, two legs, replays, byes, a third-place match) it works out the rounds and halves and draws the bracket with the halves meeting at the final, or left to right, wide, square, as a tall card or for a phone, in a classic or a road look (large round flags, big scores, a team's dotted path, a link on every box), in light, dark or a gold-on-navy poster look, in eight layouts (the tree, rows, shields, a score-update card, tall columns, the NBA's play-in, a series card and group tables), on one of twenty-five poster backgrounds (the playing surfaces of five sports among them) or your own, with each team's logo or round flag, the champion in the centre, and an accessible table; and the road to the final of either finalist. NHL, NBA, MLB, NFL, CFL, MLS and World Cup formats by era, in English, French, Japanese, Spanish and German. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bracket",
|