@johnmorrisdotca/taisen-brackets 0.9.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 +10 -0
- package/README.md +4 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/judo-road.d.ts +53 -0
- package/dist/judo-road.js +207 -0
- package/dist/judo-sheets.d.ts +20 -0
- package/dist/judo-sheets.js +12 -11
- package/dist/types.d.ts +2 -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,16 @@ 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
|
+
|
|
9
19
|
## [0.9.0] - 2026-10-20
|
|
10
20
|
|
|
11
21
|
Judo, step 3: the contest card and the mat queue board.
|
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). `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)).
|
|
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,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
|
+
- **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.
|
|
296
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.
|
|
297
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)).
|
|
298
299
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
@@ -654,7 +655,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
654
655
|
- `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
|
|
655
656
|
- `matchupsFromGames(games, options)`: matchups from a flat list of games.
|
|
656
657
|
- `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
|
|
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`.
|
|
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`.
|
|
658
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.
|
|
659
660
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
660
661
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
@@ -746,7 +747,7 @@ Any current browser, and Node 22 or later. The package makes no network request
|
|
|
746
747
|
|
|
747
748
|
## Roadmap
|
|
748
749
|
|
|
749
|
-
- Judo:
|
|
750
|
+
- Judo: double and full repechage ([docs/judo.md](./docs/judo.md)).
|
|
750
751
|
- Double elimination, and the group tables on one page with the knockout they lead to.
|
|
751
752
|
- More languages (Portuguese, Italian, Korean, Chinese), and native review of the Spanish and German words.
|
|
752
753
|
- A custom element and a React helper, as in the other packages of the family.
|
package/dist/index.d.ts
CHANGED
|
@@ -23,3 +23,5 @@ export { judoScoreboard, describeJudoContest, JUDO_BOARD_SIZES } from "./judo-bo
|
|
|
23
23
|
export type { JudoAthlete, JudoBoardOptions, JudoBoardSizeName, JudoContest } from "./judo-board.ts";
|
|
24
24
|
export { judoContestCard, describeJudoCard, judoMatQueue, describeJudoQueue, JUDO_MAT_COLORS } from "./judo-sheets.ts";
|
|
25
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
|
@@ -13,3 +13,4 @@ export { playoffFormat, playoffEras, LEAGUES, LEAGUE_EMBLEMS } from "./presets.j
|
|
|
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
15
|
export { judoContestCard, describeJudoCard, judoMatQueue, describeJudoQueue, JUDO_MAT_COLORS } from "./judo-sheets.js";
|
|
16
|
+
export { judoWins, judoRoadPanel, describeJudoRoad } from "./judo-road.js";
|
|
@@ -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
|
+
}
|
package/dist/judo-sheets.d.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
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
5
|
*/
|
|
6
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";
|
|
7
10
|
/** What the contest card shows: the two athletes and the header. The scores and the clock of a `JudoContest` are not drawn. */
|
|
8
11
|
export type JudoCardContest = Pick<JudoContest, "white" | "blue" | "weight" | "round" | "mat" | "contest">;
|
|
9
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). */
|
|
@@ -28,6 +31,23 @@ export interface JudoQueueOptions extends Omit<JudoBoardOptions, "style" | "coac
|
|
|
28
31
|
}
|
|
29
32
|
/** The IJF-style mat colours, in turn: red, green, yellow, purple, orange, teal. Each reads with the ink the board picks for it. */
|
|
30
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;
|
|
31
51
|
/** The contest card as a sentence, the drawing's description. */
|
|
32
52
|
export declare function describeJudoCard(contest: JudoCardContest, options?: Pick<JudoBoardOptions, "locale" | "words">): string;
|
|
33
53
|
/**
|
package/dist/judo-sheets.js
CHANGED
|
@@ -5,7 +5,7 @@ import { emWidth, esc, fit, fitLines, hash, num } from "./text.js";
|
|
|
5
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
6
|
export const JUDO_MAT_COLORS = Object.freeze(["#d2232a", "#1e9c4c", "#e7b800", "#7a3fb4", "#e8741a", "#12877f"]);
|
|
7
7
|
/** White or near-black, whichever reads better on a hex colour. */
|
|
8
|
-
function inkOn(color) {
|
|
8
|
+
export function inkOn(color) {
|
|
9
9
|
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim())?.[1];
|
|
10
10
|
if (!hex)
|
|
11
11
|
return "#ffffff";
|
|
@@ -14,7 +14,7 @@ function inkOn(color) {
|
|
|
14
14
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.4 ? "#ffffff" : "#15181a";
|
|
15
15
|
}
|
|
16
16
|
const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
17
|
-
function sheetStyle(scope, theme) {
|
|
17
|
+
export function sheetStyle(scope, theme, extra = "") {
|
|
18
18
|
const base = (p, before) => rulesFor(p, `.${scope}`, before);
|
|
19
19
|
const fixed = [
|
|
20
20
|
`.${scope} .jb-w .jb-panel{fill:var(--jb-white-panel, ${WHITE_PANEL.panel});stroke:${WHITE_PANEL.edge};stroke-width:2}`,
|
|
@@ -32,13 +32,14 @@ function sheetStyle(scope, theme) {
|
|
|
32
32
|
`.${scope} .tb-initials{fill:#1f2320}`,
|
|
33
33
|
`.${scope} text{font-family:var(--jb-font, ${FONT});font-variant-numeric:tabular-nums}`,
|
|
34
34
|
].join("\n");
|
|
35
|
+
const tail = extra ? `${fixed}\n${extra.replaceAll("$S", `.${scope}`)}` : fixed;
|
|
35
36
|
if (theme === "light")
|
|
36
|
-
return `${base(LIGHT, "")}\n${
|
|
37
|
+
return `${base(LIGHT, "")}\n${tail}`;
|
|
37
38
|
if (theme === "dark")
|
|
38
|
-
return `${base(DARK, "")}\n${
|
|
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${
|
|
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}`;
|
|
40
41
|
}
|
|
41
|
-
function frameOf(options, seed) {
|
|
42
|
+
export function frameOf(options, seed) {
|
|
42
43
|
const v = voiceOf(options);
|
|
43
44
|
const size = typeof options.size === "string" ? JUDO_BOARD_SIZES[options.size] : options.size ?? JUDO_BOARD_SIZES.tv;
|
|
44
45
|
if (!size || !(size.width >= 200) || !(size.height >= 200))
|
|
@@ -50,20 +51,20 @@ function frameOf(options, seed) {
|
|
|
50
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 } : {}) };
|
|
51
52
|
return { W: size.width, H: size.height, v, scope, theme, ctx, options };
|
|
52
53
|
}
|
|
53
|
-
function wrap(f, title, desc, body, label) {
|
|
54
|
+
export function wrap(f, title, desc, body, label, extraCss = "") {
|
|
54
55
|
return [
|
|
55
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}">`,
|
|
56
57
|
`<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
|
+
`<style>${sheetStyle(f.scope, f.theme, extraCss)}</style>`,
|
|
58
59
|
markDefs(f.ctx) ? `<defs>${markDefs(f.ctx)}</defs>` : "",
|
|
59
60
|
`<rect class="jb-bg" width="${num(f.W)}" height="${num(f.H)}"/>`,
|
|
60
61
|
...body,
|
|
61
62
|
"</svg>",
|
|
62
63
|
].filter(Boolean).join("\n");
|
|
63
64
|
}
|
|
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) {
|
|
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) {
|
|
67
68
|
if (!a || typeof a.name !== "string" || a.name.trim() === "")
|
|
68
69
|
throw new RangeError(`taisen-brackets: the ${side} athlete needs a name`);
|
|
69
70
|
}
|
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 {
|
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",
|