@johnmorrisdotca/taisen-brackets 0.8.0 → 0.9.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 CHANGED
@@ -6,6 +6,17 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] - 2026-10-20
10
+
11
+ Judo, step 3: the contest card and the mat queue board.
12
+
13
+ ### Added
14
+
15
+ - **`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`.
16
+ - **`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`.
17
+ - 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).
18
+ - 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.
19
+
9
20
  ## [0.8.0] - 2026-10-20
10
21
 
11
22
  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)).
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,7 @@ 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
+ - **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
297
  - **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
298
  - **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
298
299
  - **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 +654,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
653
654
  - `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
654
655
  - `matchupsFromGames(games, options)`: matchups from a flat list of games.
655
656
  - `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`.
657
+ - `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. `Matchup.judo` and `Matchup.stage` (`"repechage"`, `"bronze"`) carry them into `drawBracket`.
657
658
  - `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
659
  - `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
659
660
  - `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
@@ -745,7 +746,7 @@ Any current browser, and Node 22 or later. The package makes no network request
745
746
 
746
747
  ## Roadmap
747
748
 
748
- - Judo: a contest card and a mat queue board, the road to the final of a judoka, double and full repechage ([docs/judo.md](./docs/judo.md)).
749
+ - Judo: the road to the final of a judoka, double and full repechage ([docs/judo.md](./docs/judo.md)).
749
750
  - Double elimination, and the group tables on one page with the knockout they lead to.
750
751
  - More languages (Portuguese, Italian, Korean, Chinese), and native review of the Spanish and German words.
751
752
  - A custom element and a React helper, as in the other packages of the family.
package/dist/index.d.ts CHANGED
@@ -21,3 +21,5 @@ 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";
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ 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";
@@ -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;
@@ -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,45 @@
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
+ /** What the contest card shows: the two athletes and the header. The scores and the clock of a `JudoContest` are not drawn. */
8
+ export type JudoCardContest = Pick<JudoContest, "white" | "blue" | "weight" | "round" | "mat" | "contest">;
9
+ /** 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). */
10
+ export interface JudoQueueContest {
11
+ number: string | number;
12
+ white: JudoAthlete;
13
+ blue: JudoAthlete;
14
+ }
15
+ /** 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. */
16
+ export interface JudoMat {
17
+ mat: string | number;
18
+ color?: string;
19
+ contests: readonly JudoQueueContest[];
20
+ }
21
+ export interface JudoQueueOptions extends Omit<JudoBoardOptions, "style" | "coaches"> {
22
+ /** The most contests drawn for a mat. Default: as many as fit at a readable size, up to 12. */
23
+ rows?: number;
24
+ /** Show the first contest of each mat as the one on now (a green number). Default true. */
25
+ current?: boolean;
26
+ /** The colours the mats take in turn where a mat names none. Hex codes. */
27
+ colors?: readonly string[];
28
+ }
29
+ /** The IJF-style mat colours, in turn: red, green, yellow, purple, orange, teal. Each reads with the ink the board picks for it. */
30
+ export declare const JUDO_MAT_COLORS: readonly string[];
31
+ /** The contest card as a sentence, the drawing's description. */
32
+ export declare function describeJudoCard(contest: JudoCardContest, options?: Pick<JudoBoardOptions, "locale" | "words">): string;
33
+ /**
34
+ * 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,
35
+ * and the country code. Throws a `RangeError` for an athlete with no name or a bad size or theme. `swap` draws blue first.
36
+ */
37
+ export declare function judoContestCard(contest: JudoCardContest, options?: JudoBoardOptions): string;
38
+ /** The mat queue as a sentence: for each mat, its contests in order. */
39
+ export declare function describeJudoQueue(mats: JudoMat | readonly JudoMat[], options?: Pick<JudoBoardOptions, "locale" | "words">): string;
40
+ /**
41
+ * 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
42
+ * 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`
43
+ * 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.
44
+ */
45
+ export declare function judoMatQueue(input: JudoMat | readonly JudoMat[], options?: JudoQueueOptions): string;
@@ -0,0 +1,267 @@
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
+ 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
+ function sheetStyle(scope, theme) {
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
+ if (theme === "light")
36
+ return `${base(LIGHT, "")}\n${fixed}`;
37
+ if (theme === "dark")
38
+ return `${base(DARK, "")}\n${fixed}`;
39
+ 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${fixed}`;
40
+ }
41
+ function frameOf(options, seed) {
42
+ const v = voiceOf(options);
43
+ const size = typeof options.size === "string" ? JUDO_BOARD_SIZES[options.size] : options.size ?? JUDO_BOARD_SIZES.tv;
44
+ if (!size || !(size.width >= 200) || !(size.height >= 200))
45
+ throw new RangeError(`taisen-brackets: a size is "tv", "tablet", "tablet-tall" or { width, height } of 200 or more, not ${JSON.stringify(options.size)}`);
46
+ const theme = options.theme ?? "auto";
47
+ if (theme !== "auto" && theme !== "light" && theme !== "dark")
48
+ throw new RangeError(`taisen-brackets: a theme is "auto", "light" or "dark", not ${JSON.stringify(theme)}`);
49
+ 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]))}`;
50
+ 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 } : {}) };
51
+ return { W: size.width, H: size.height, v, scope, theme, ctx, options };
52
+ }
53
+ function wrap(f, title, desc, body, label) {
54
+ return [
55
+ `<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}">`,
56
+ `<title id="${esc(f.scope)}-t">${esc(title)}</title><desc id="${esc(f.scope)}-d">${esc(desc)}</desc>`,
57
+ `<style>${sheetStyle(f.scope, f.theme)}</style>`,
58
+ markDefs(f.ctx) ? `<defs>${markDefs(f.ctx)}</defs>` : "",
59
+ `<rect class="jb-bg" width="${num(f.W)}" height="${num(f.H)}"/>`,
60
+ ...body,
61
+ "</svg>",
62
+ ].filter(Boolean).join("\n");
63
+ }
64
+ 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>`;
65
+ 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 } : {}) });
66
+ function check(a, side) {
67
+ if (!a || typeof a.name !== "string" || a.name.trim() === "")
68
+ throw new RangeError(`taisen-brackets: the ${side} athlete needs a name`);
69
+ }
70
+ // ------------------------------------------------------------------------------------------------ the contest card
71
+ /** The contest card as a sentence, the drawing's description. */
72
+ export function describeJudoCard(contest, options = {}) {
73
+ const v = voiceOf(options);
74
+ const w = v.words;
75
+ const parts = [];
76
+ if (contest.weight)
77
+ parts.push(contest.weight);
78
+ if (contest.round)
79
+ parts.push(contest.round);
80
+ if (contest.mat !== undefined)
81
+ parts.push(fill(w.judoMat, { n: String(contest.mat) }));
82
+ if (contest.contest !== undefined)
83
+ parts.push(fill(w.judoContest, { n: String(contest.contest) }));
84
+ parts.push(`${w.judoWhite}: ${contest.white.name}${contest.white.code ? ` (${contest.white.code})` : ""}`);
85
+ parts.push(`${w.judoBlue}: ${contest.blue.name}${contest.blue.code ? ` (${contest.blue.code})` : ""}`);
86
+ return parts.join(w.sep) + w.end;
87
+ }
88
+ /**
89
+ * 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,
90
+ * and the country code. Throws a `RangeError` for an athlete with no name or a bad size or theme. `swap` draws blue first.
91
+ */
92
+ export function judoContestCard(contest, options = {}) {
93
+ check(contest.white, "white");
94
+ check(contest.blue, "blue");
95
+ const f = frameOf(options, contest);
96
+ const { W, H, v } = f;
97
+ const w = v.words;
98
+ const unit = Math.min(W, H);
99
+ const pad = Math.round(unit * 0.025);
100
+ const landscape = W >= H;
101
+ const headH = Math.round(H * (landscape ? 0.15 : 0.1));
102
+ const body = [];
103
+ body.push(`<rect class="jb-band" x="0" y="0" width="${num(W)}" height="${num(headH)}"/>`);
104
+ const hf = clampN(headH * 0.5, 14, 90);
105
+ 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(" · ");
106
+ const inner = W - pad * 2;
107
+ const leftT = contest.weight ?? "";
108
+ const rightT = contest.round ?? "";
109
+ const midT = where;
110
+ if (leftT) {
111
+ const s = fitSize(leftT, hf, inner * 0.4, 0.5);
112
+ body.push(text(pad, headH / 2 + s * 0.36, s, fit(leftT, s, inner * 0.4, bw), "jb-ink", "start"));
113
+ }
114
+ if (rightT) {
115
+ const s = fitSize(rightT, hf, inner * 0.4, 0.5);
116
+ body.push(text(W - pad, headH / 2 + s * 0.36, s, fit(rightT, s, inner * 0.4, bw), "jb-ink", "end"));
117
+ }
118
+ if (midT && landscape) {
119
+ const s = fitSize(midT, hf * 0.7, inner * 0.28, 0.5);
120
+ body.push(text(W / 2, headH / 2 + s * 0.36, s, fit(midT, s, inner * 0.28, bw), "jb-muted", "middle"));
121
+ }
122
+ const top = headH + pad;
123
+ const avail = H - top - pad;
124
+ const bandH = (avail - pad) / 2;
125
+ const order = (options.swap ? [1, 0] : [0, 1]);
126
+ order.forEach((k, i) => {
127
+ const a = k === 0 ? contest.white : contest.blue;
128
+ const y = top + i * (bandH + pad);
129
+ const g = [];
130
+ 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)}"/>`);
131
+ const ip = Math.max(pad, bandH * 0.1);
132
+ const flagD = Math.min(bandH - ip * 2, (W - pad * 2) * 0.22);
133
+ g.push(drawMark(teamOf(a, k === 0 ? "white" : "blue"), pad + ip + flagD / 2, y + bandH / 2, flagD, f.ctx));
134
+ const tx = pad + ip * 2 + flagD;
135
+ const tw = W - pad - ip - tx;
136
+ const codeH = a.code ? bandH * 0.2 : 0;
137
+ const fl = fitLines(a.name, tw, bandH - ip * 2 - codeH - (a.code ? ip * 0.4 : 0), Math.min(bandH * 0.5, 220));
138
+ const blockH = fl.height + (a.code ? ip * 0.4 + codeH : 0);
139
+ let yy = y + (bandH - blockH) / 2;
140
+ 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")));
141
+ yy += fl.height + ip * 0.4;
142
+ if (a.code)
143
+ g.push(text(tx, yy + codeH * 0.86, codeH, a.code, "jb-psub", "start", 800, ' letter-spacing=".1em"'));
144
+ body.push(`<g class="${k === 0 ? "jb-w" : "jb-b"}">${g.join("")}</g>`);
145
+ });
146
+ const title = options.title ?? w.judoCardTitle;
147
+ return wrap(f, title, describeJudoCard(contest, options), body, "card");
148
+ }
149
+ // ------------------------------------------------------------------------------------------------ the mat queue board
150
+ /** The mat queue as a sentence: for each mat, its contests in order. */
151
+ export function describeJudoQueue(mats, options = {}) {
152
+ const v = voiceOf(options);
153
+ const w = v.words;
154
+ const list = Array.isArray(mats) ? mats : [mats];
155
+ 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;
156
+ }
157
+ /**
158
+ * 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
159
+ * 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`
160
+ * 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.
161
+ */
162
+ export function judoMatQueue(input, options = {}) {
163
+ const mats = Array.isArray(input) ? input : [input];
164
+ if (mats.length === 0)
165
+ throw new RangeError("taisen-brackets: a mat queue board needs at least one mat");
166
+ if (mats.length > 8)
167
+ throw new RangeError("taisen-brackets: a mat queue board draws eight mats at most");
168
+ for (const m of mats) {
169
+ if (m.mat === undefined || String(m.mat).trim() === "")
170
+ throw new RangeError("taisen-brackets: every mat needs a number or a name");
171
+ if (m.color !== undefined && !HEX.test(m.color))
172
+ throw new RangeError(`taisen-brackets: a mat's colour is a hex code ("#d2232a"), not ${JSON.stringify(m.color)}`);
173
+ m.contests.forEach((c, i) => {
174
+ if (c.number === undefined || String(c.number).trim() === "")
175
+ throw new RangeError(`taisen-brackets: contest ${i + 1} of mat ${m.mat} needs a number`);
176
+ check(c.white, `white of contest ${c.number}`);
177
+ check(c.blue, `blue of contest ${c.number}`);
178
+ });
179
+ }
180
+ if (options.rows !== undefined && !(Number.isInteger(options.rows) && options.rows >= 1 && options.rows <= 40))
181
+ throw new RangeError("taisen-brackets: rows is a whole number from 1 to 40");
182
+ const palette = options.colors ?? JUDO_MAT_COLORS;
183
+ for (const c of palette)
184
+ if (!HEX.test(c))
185
+ throw new RangeError(`taisen-brackets: a mat colour is a hex code, not ${JSON.stringify(c)}`);
186
+ const f = frameOf(options, mats);
187
+ const { W, H, v } = f;
188
+ const w = v.words;
189
+ const unit = Math.min(W, H);
190
+ const pad = Math.round(unit * 0.02);
191
+ const n = mats.length;
192
+ // As many columns as leave each mat room for its names: about six tenths of the short side.
193
+ const cols = Math.max(1, Math.min(n, Math.floor((W - pad) / (unit * 0.62 + pad))));
194
+ const rowsN = Math.ceil(n / cols);
195
+ const title = options.title ?? w.judoQueueTitle;
196
+ const body = [];
197
+ const cellW = (W - pad * (cols + 1)) / cols;
198
+ const cellH = (H - pad * (rowsN + 1)) / rowsN;
199
+ const longest = Math.max(...mats.map((m) => m.contests.length), 1);
200
+ const minRow = Math.max(36, unit * 0.045);
201
+ const fits = Math.max(1, Math.floor(cellH / minRow));
202
+ const capacity = Math.min(options.rows ?? 12, fits);
203
+ const shown = Math.min(longest, capacity);
204
+ const rowH = Math.min(cellH / Math.max(shown, 1), unit * 0.11);
205
+ // The widest contest number in ems, so that every mat's number column is the same width.
206
+ const widest = Math.max(...mats.flatMap((m) => m.contests.slice(0, shown).map((q) => emWidth(String(q.number)))), 1);
207
+ mats.forEach((m, mi) => {
208
+ const col = mi % cols, row = Math.floor(mi / cols);
209
+ const x = pad + col * (cellW + pad), y = pad + row * (cellH + pad);
210
+ const color = m.color ?? palette[mi % palette.length];
211
+ const g = [];
212
+ const stripW = Math.min(cellW * 0.12, cellH * 0.22 + 30);
213
+ const rx = Math.round(unit * 0.012);
214
+ g.push(`<rect x="${num(x)}" y="${num(y)}" width="${num(stripW)}" height="${num(cellH)}" rx="${rx}" style="fill:${color}"/>`);
215
+ const label = String(m.mat);
216
+ const ns = Math.min(stripW * 0.8, cellH * 0.5, (stripW - 12) / (label.length * 0.66));
217
+ 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>`);
218
+ const rx0 = x + stripW + pad * 0.5;
219
+ const rowW = cellW - stripW - pad * 0.5;
220
+ m.contests.slice(0, shown).forEach((c, ci) => {
221
+ const ry = y + ci * rowH;
222
+ const h = rowH - Math.max(2, rowH * 0.05);
223
+ const current = (options.current ?? true) && ci === 0;
224
+ const numFont = h * 0.4;
225
+ const numW = Math.min(rowW * 0.26, Math.max(h * 1.2, widest * numFont * 1.04 + h * 0.35));
226
+ g.push(`<rect class="${current ? "jq-now" : "jq-num"}" x="${num(rx0)}" y="${num(ry)}" width="${num(numW)}" height="${num(h)}"/>`);
227
+ const nl = String(c.number);
228
+ const ns2 = Math.min(numFont, (numW - 8) / (nl.length * 0.68));
229
+ g.push(text(rx0 + numW / 2, ry + h / 2 + ns2 * 0.36, ns2, nl, current ? "jq-nowt" : "jq-numt", "middle"));
230
+ const sideW = (rowW - numW) / 2;
231
+ const sides = [[options.swap ? 1 : 0, rx0 + numW], [options.swap ? 0 : 1, rx0 + numW + sideW]];
232
+ sides.forEach(([k, sx], si) => {
233
+ const a = k === 0 ? c.white : c.blue;
234
+ const cls = k === 0 ? "jb-w" : "jb-b";
235
+ const gg = [];
236
+ gg.push(`<rect class="jb-panel" x="${num(sx)}" y="${num(ry)}" width="${num(sideW)}" height="${num(h)}" style="stroke-width:0"/>`);
237
+ const u = Math.min(h, sideW * 0.27);
238
+ const d = u * 0.56, ip = u * 0.16;
239
+ const code = a.code ?? "";
240
+ const cs = u * 0.26;
241
+ const codeW = code ? bw(code, cs) + ip * 0.5 : 0;
242
+ const flagX = si === 0 ? sx + sideW - ip - d / 2 : sx + ip + d / 2;
243
+ gg.push(drawMark(teamOf(a, `${m.mat}-${c.number}-${k}`), flagX, ry + h / 2, d, f.ctx));
244
+ // The first column (left): name, then code, then flag at the right edge; the second: flag at the left edge, then code, then name.
245
+ const nameFont = Math.min(u * 0.36, 64);
246
+ if (si === 0) {
247
+ const avail = sideW - ip * 3 - d - codeW;
248
+ const sz = fitSize(a.name, nameFont, avail, 0.3);
249
+ gg.push(text(sx + ip, ry + h / 2 + sz * 0.36, sz, fit(a.name, sz, avail + 1, bw), "jb-pink", "start"));
250
+ if (code)
251
+ gg.push(text(sx + sideW - ip * 1.5 - d, ry + h / 2 + cs * 0.36, cs, code, "jb-psub", "end"));
252
+ }
253
+ else {
254
+ const nx = sx + ip * 1.5 + d + codeW + ip * 0.5;
255
+ const avail = sx + sideW - ip - nx;
256
+ const sz = fitSize(a.name, nameFont, avail, 0.3);
257
+ if (code)
258
+ gg.push(text(sx + ip * 1.5 + d, ry + h / 2 + cs * 0.36, cs, code, "jb-psub", "start"));
259
+ gg.push(text(nx, ry + h / 2 + sz * 0.36, sz, fit(a.name, sz, avail + 1, bw), "jb-pink", "start"));
260
+ }
261
+ g.push(`<g class="${cls}">${gg.join("")}</g>`);
262
+ });
263
+ });
264
+ body.push(`<g class="jq-mat">${g.join("")}</g>`);
265
+ });
266
+ return wrap(f, title, describeJudoQueue(mats, options), body, "queue");
267
+ }
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
@@ -392,6 +392,9 @@ export interface Words {
392
392
  judoContest: string;
393
393
  judoCoach: string;
394
394
  judoBoardTitle: string;
395
+ /** The default titles of the contest card and of the mat queue board. */
396
+ judoCardTitle: string;
397
+ judoQueueTitle: string;
395
398
  }
396
399
  /** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
397
400
  export interface Locale {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.8.0";
1
+ export declare const VERSION = "0.9.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.8.0";
1
+ export const VERSION = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/taisen-brackets",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",