@johnmorrisdotca/taisen-brackets 0.14.0 → 0.15.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 +13 -0
- package/README.md +11 -1
- package/dist/broadcast-kit.d.ts +92 -0
- package/dist/broadcast-kit.js +216 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/locales.js +5 -0
- package/dist/marks.d.ts +2 -1
- package/dist/marks.js +49 -1
- package/dist/results-board.d.ts +62 -0
- package/dist/results-board.js +173 -0
- 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,19 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.15.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
Broadcast boards, step 1: the results board.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`resultsBoard({ title, phase?, columns?, rows }, options)`**: a results board in the manner of a television results graphic, as one standalone SVG: a title band (the event in capitals, with an icon square), a light sub bar (the phase, and the heading of each score column), and a row for each athlete: a rank in a box (`rankStyle`: a red box, gold, silver and bronze boxes, or a plain numeral), the country code, a 4:3 flag, the name, a record badge (`record: "OR"`, `["WR", "NR"]`) and one or more columns of numbers at the sport's own precision (per column, then `precision`) or texts (`1:08.34`); `note` (`DNS`, `DQ`) in place of the values; equal results share a rank. Looks `blue`, `night` and `paper` (`BROADCAST_LOOKS`), icons `medal`, `stopwatch`, `none` or an `iconMark` of your own, `nameCase`, `nameStyle`, five languages, `--bc-*` custom properties, up to 24 rows. `describeResultsBoard`.
|
|
16
|
+
- **`resultsBoardSize(rowCount, { width })`**: the board's size before it is drawn, for placing it: the width (1280 by default, 720 to 4000) and a height of 64 + 36 + 20 + 54 a row, scaled. The SVG carries the same size, has nothing outside its panel and scales as a picture.
|
|
17
|
+
- **Flags drawn 4:3**: a board asks the `flags` function for the shape `rect` (Hata's `"4:3"`); a row with no flag is a rectangular badge with its code.
|
|
18
|
+
- Nothing of any games, federation, sponsor or broadcaster is drawn: no rings, pictograms, emblems or timing company's name; the type is a stack of free condensed fonts, none shipped.
|
|
19
|
+
- One word for the sub bar, in the five languages (Result, Résultat, `結果`, Resultado, Ergebnis: the plain word of each).
|
|
20
|
+
- The demo has a Broadcast boards section with five made-up finals (a gymnastics apparatus final, an all-around, a swimming final with a record badge, athletics 100 m and speed skating), the look, rank style, icon, width and capitals in the address, and the board's size shown. [docs/broadcast.md](docs/broadcast.md).
|
|
21
|
+
|
|
9
22
|
## [0.14.0] - 2026-10-20
|
|
10
23
|
|
|
11
24
|
A medal table for any sport.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Give it the games, series, two-legged ties or replays of a tournament and it wor
|
|
|
11
11
|
<img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
<p align="center"><a href="https://johnmorrisdotca.github.io/taisen-brackets/"><strong>Open the demo →</strong></a> · <a href="./docs/layout.md">Layout</a> · <a href="./docs/layouts.md">Layouts</a> · <a href="./docs/drawing.md">Drawing</a> · <a href="./docs/road.md">Road to the final</a> · <a href="./docs/backgrounds.md">Backgrounds</a> · <a href="./docs/judo.md">Judo</a> · <a href="./docs/rankings.md">Rankings</a> · <a href="./docs/medals.md">Medal table</a> · <a href="./docs/formats.md">Formats</a> · <a href="./docs/compare.md">How it compares</a></p>
|
|
14
|
+
<p align="center"><a href="https://johnmorrisdotca.github.io/taisen-brackets/"><strong>Open the demo →</strong></a> · <a href="./docs/layout.md">Layout</a> · <a href="./docs/layouts.md">Layouts</a> · <a href="./docs/drawing.md">Drawing</a> · <a href="./docs/road.md">Road to the final</a> · <a href="./docs/backgrounds.md">Backgrounds</a> · <a href="./docs/judo.md">Judo</a> · <a href="./docs/rankings.md">Rankings</a> · <a href="./docs/medals.md">Medal table</a> · <a href="./docs/broadcast.md">Broadcast boards</a> · <a href="./docs/formats.md">Formats</a> · <a href="./docs/compare.md">How it compares</a></p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<picture>
|
|
@@ -264,6 +264,14 @@ A judo contest is a tie with a `judo` result: ippon, waza-ari, yuko and shido as
|
|
|
264
264
|
<img alt="A medal table on a 4:5 card: ten countries with rank, flag or badge, name, gold, silver and bronze counts in tinted cells and the total." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/medal-table-portrait-light.webp" width="480">
|
|
265
265
|
</picture><br><em>A made-up medal table (invented results).</em></p>
|
|
266
266
|
|
|
267
|
+
## Broadcast boards
|
|
268
|
+
|
|
269
|
+
`resultsBoard` draws a results board in the manner of a television graphic: a title band with an icon, a sub bar, and a row for each athlete with a rank in a box, the country code, a 4:3 flag, the name, a record badge (`OR`, `WR`, `NR`) and one or more columns of scores or times at the sport's own precision (a gymnastics final, an all-around, a swimming final, athletics, speed skating). It is one standalone SVG of a width you choose and a height that follows from the rows (`resultsBoardSize`), made to be placed over a picture, in three looks and with no mark of any games or broadcaster. [docs/broadcast.md](./docs/broadcast.md) · [try it in the demo](https://johnmorrisdotca.github.io/taisen-brackets/#boards)
|
|
270
|
+
|
|
271
|
+
<p align="center"><picture>
|
|
272
|
+
<img alt="A results board in the blue look: a gymnastics apparatus final with a title band, a sub bar with three score columns, and eight rows with a red rank box, country code, flag, name and scores." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/results-apparatus-blue.webp" width="640">
|
|
273
|
+
</picture><br><em>A made-up apparatus final (invented athletes and scores).</em></p>
|
|
274
|
+
|
|
267
275
|
## In 30 seconds
|
|
268
276
|
|
|
269
277
|
```sh
|
|
@@ -314,6 +322,7 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
|
|
|
314
322
|
- **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.
|
|
315
323
|
- **Rankings**: `rankingList` draws a ranking from your data, text first: rank, flag (a country or a Canadian province), name, club, points, movement arrows, a podium, a champion row, ties and paging; `rankingNumberOnes` the number one of each category and `rankingEntryList` an event's entry list with each athlete's ranking and `rankingMatchup` a head-to-head card ([docs/rankings.md](./docs/rankings.md)).
|
|
316
324
|
- **A medal table for any sport**: `medalTable` ranks countries or teams gold first or by the total, with shared ranks, flags or badges, filters by sport, weight class and Games through `tallyMedals`, and former countries as rows of their own ([docs/medals.md](./docs/medals.md)).
|
|
325
|
+
- **Broadcast boards**: `resultsBoard` draws a results graphic (title band, rank boxes, 4:3 flags, record badges, columns of scores) as a standalone SVG that follows from its rows, to place over a picture, in three looks and with no mark of any games ([docs/broadcast.md](./docs/broadcast.md)).
|
|
317
326
|
- **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)).
|
|
318
327
|
- **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
|
|
319
328
|
- **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.
|
|
@@ -677,6 +686,7 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
|
|
|
677
686
|
- `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`.
|
|
678
687
|
- `rankingList(source, options?)`, `rankingRanks(entries, options?)`, `rankingCategoryLabel(category, locale?)`, `describeRanking(source, options?)`, `BOARD_SIZES`: a ranking list as SVG, its ranks as data, a category as a heading and the drawing as a sentence. `rankingNumberOnes(source, options?)`, `describeNumberOnes`, `NUMBER_ONE_COLORS`: the number one of each category as cards. `rankingEntryList(source, options?)`, `describeEntryList`: an event's entry list with each athlete's ranking. `rankingMatchup(source, options?)`, `describeMatchup`, `matchupRecord(meetings)`: the head-to-head card of two athletes and their meetings ([docs/rankings.md](./docs/rankings.md)).
|
|
679
688
|
- `medalTable(source, options?)`, `medalRanks(rows, options?)`, `tallyMedals(awards, filter?)`, `describeMedalTable(source, options?)`: a medal table as SVG, its ranks as data, a list of medals added up and filtered, and the table as a sentence ([docs/medals.md](./docs/medals.md)).
|
|
689
|
+
- `resultsBoard(source, options?)`, `resultsBoardSize(rowCount, options?)`, `describeResultsBoard(source, options?)`, `BROADCAST_LOOKS`: a broadcast-style results board as a standalone SVG, its size from its rows, and the board as a sentence ([docs/broadcast.md](./docs/broadcast.md)).
|
|
680
690
|
- `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.
|
|
681
691
|
- `isEmblemId(value)`, and the types `EmblemRenderer`, `EmblemRequest`, `EmblemStyle`: the trophy emblems (see "Emblems for the trophy").
|
|
682
692
|
- `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the broadcast-style boards share: the looks (blue, night, paper), the frame (a width, a voice, flags drawn 4:3), the stylesheet, the panel with its title bar, its icon and its
|
|
3
|
+
* sub bar, and the small shapes (a rank box, a record badge, a medal disc). A broadcast board is a standalone SVG of a given width and a height that follows from its rows
|
|
4
|
+
* (`resultsBoardSize`, `boutBoardSize`, `broadcastMedalSize`), with nothing outside the panel, so that it can be placed over a picture. It is made in the manner of a results graphic on a
|
|
5
|
+
* television broadcast (a title band, rows with a rank, a flag and a code, columns of scores, a badge for a record) and carries no mark of any games, federation or broadcaster.
|
|
6
|
+
*/
|
|
7
|
+
import type { DrawOptions, Locale, Words } from "./types.ts";
|
|
8
|
+
import { bw, clampN, fitSize } from "./rank-kit.ts";
|
|
9
|
+
import { type Voice } from "./locales.ts";
|
|
10
|
+
import { type MarkContext } from "./marks.ts";
|
|
11
|
+
export type BroadcastLook = "blue" | "night" | "paper";
|
|
12
|
+
/** The looks: `blue` (a broadcast's deep blue with a bright title band), `night` (near black with a cyan band) and `paper` (white, with the same blue band). */
|
|
13
|
+
export declare const BROADCAST_LOOKS: readonly BroadcastLook[];
|
|
14
|
+
/** The icons of the title band's square, drawn by the package: a medal and a stopwatch. `none` leaves the band whole. */
|
|
15
|
+
export type BroadcastIcon = "medal" | "stopwatch" | "none";
|
|
16
|
+
export interface BroadcastOptions {
|
|
17
|
+
/** The width of the board; the height follows from the rows. Default 1280, from 320 to 4000. */
|
|
18
|
+
width?: number;
|
|
19
|
+
/** `blue` (the default), `night` or `paper`. */
|
|
20
|
+
look?: BroadcastLook;
|
|
21
|
+
/** The icon in the square at the left of the title: `medal` (the default), `stopwatch`, `none`, or an `iconMark` of your own. */
|
|
22
|
+
icon?: BroadcastIcon;
|
|
23
|
+
/** A mark of your own for that square (inline SVG or an image address), in place of `icon`. */
|
|
24
|
+
iconMark?: string;
|
|
25
|
+
/** Draws a flag from its code, as for `drawBracket`; it is asked for the shape `rect` (Hata's `"4:3"`). */
|
|
26
|
+
flags?: DrawOptions["flags"];
|
|
27
|
+
/** The address of a flag as a picture to load, tried where `flags` gives none. */
|
|
28
|
+
flagHref?: DrawOptions["flagHref"];
|
|
29
|
+
locale?: string | Locale;
|
|
30
|
+
words?: Partial<Words>;
|
|
31
|
+
/** The `<title>` of the drawing. Default: the board's own heading. */
|
|
32
|
+
title?: string;
|
|
33
|
+
id?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The sizes in a drawing, in units of its width over 1280. */
|
|
37
|
+
export declare const BC: Readonly<{
|
|
38
|
+
title: 64;
|
|
39
|
+
sub: 36;
|
|
40
|
+
row: 52;
|
|
41
|
+
gap: 2;
|
|
42
|
+
pad: 10;
|
|
43
|
+
band: 38;
|
|
44
|
+
official: 40;
|
|
45
|
+
athlete: 62;
|
|
46
|
+
}>;
|
|
47
|
+
export interface BroadcastFrame {
|
|
48
|
+
W: number;
|
|
49
|
+
H: number;
|
|
50
|
+
u: number;
|
|
51
|
+
v: Voice;
|
|
52
|
+
scope: string;
|
|
53
|
+
ctx: MarkContext;
|
|
54
|
+
options: BroadcastOptions;
|
|
55
|
+
look: BroadcastLook;
|
|
56
|
+
}
|
|
57
|
+
/** The width and look checked, the voice, and the flag context (flags drawn 4:3). `height` is the board's, from its rows, in the unit of 1280 wide. */
|
|
58
|
+
export declare function broadcastFrame(options: BroadcastOptions, seed: unknown, heightUnits: number): BroadcastFrame;
|
|
59
|
+
export declare const BROADCAST_FONT = "\"Roboto Condensed\", \"Liberation Sans Narrow\", \"Archivo Narrow\", \"Helvetica Neue\", Arial, \"Hiragino Sans\", \"Yu Gothic UI\", \"Noto Sans CJK JP\", Meiryo, sans-serif";
|
|
60
|
+
/** The stylesheet of a board in a look. Every colour is a custom property with the look's value as its fallback (`--bc-title-a`, `--bc-row-a`, `--bc-rank` and the rest). */
|
|
61
|
+
export declare function broadcastStyle(scope: string, look: BroadcastLook, fixed?: string): string;
|
|
62
|
+
/** The gradients every board uses, by id: the panel's, the title band's, the sub bar's, the icon's and the gloss over a row. */
|
|
63
|
+
export declare function broadcastDefs(scope: string): string;
|
|
64
|
+
export declare function broadcastWrap(f: BroadcastFrame, title: string, desc: string, body: string[], kind: string, fixed?: string): string;
|
|
65
|
+
export declare const t: (x: number, y: number, size: number, content: string, cls: string, anchor?: "start" | "middle" | "end", weight?: number, extra?: string) => string;
|
|
66
|
+
export declare const box: (x: number, y: number, w: number, h: number, cls: string, rx?: number, extra?: string) => string;
|
|
67
|
+
/** The panel's background and edge, from (0, 0) to the board's size, with the corners cut a little. */
|
|
68
|
+
export declare function panel(f: BroadcastFrame): string;
|
|
69
|
+
/** The title band: the icon square at the left, the title in capitals, from y to y + the band's height. Returns the shapes and the x where the title text starts. */
|
|
70
|
+
export declare function titleBand(f: BroadcastFrame, y: number, title: string, right?: string): {
|
|
71
|
+
svg: string;
|
|
72
|
+
h: number;
|
|
73
|
+
};
|
|
74
|
+
/** The light sub bar under the title: `text` at the left in capitals, and labels over columns at the right (each `{ x, label }`, centred on x). */
|
|
75
|
+
export declare function subBar(f: BroadcastFrame, y: number, text: string, labels?: readonly {
|
|
76
|
+
x: number;
|
|
77
|
+
w: number;
|
|
78
|
+
label: string;
|
|
79
|
+
}[], bold?: boolean): {
|
|
80
|
+
svg: string;
|
|
81
|
+
h: number;
|
|
82
|
+
};
|
|
83
|
+
/** A rank in a box: `box` (the broadcast's red square), `medal` (gold, silver and bronze for the first three, else the box) or `plain` (a white numeral). */
|
|
84
|
+
export declare function rankBox(x: number, cy: number, w: number, h: number, text: string, rank: number | null, style: "box" | "medal" | "plain", u: number): string;
|
|
85
|
+
/** A record badge ("OR", "WR", "NR"): an orange rounded plate with the letters, right-aligned at `right`. Returns the shapes and the width. */
|
|
86
|
+
export declare function recordBadge(right: number, cy: number, h: number, text: string, u: number): {
|
|
87
|
+
svg: string;
|
|
88
|
+
w: number;
|
|
89
|
+
};
|
|
90
|
+
/** The gloss over a row: a faint white gradient from the top edge. */
|
|
91
|
+
export declare const gloss: (f: BroadcastFrame, x: number, y: number, w: number, h: number) => string;
|
|
92
|
+
export { bw, clampN, fitSize };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { bw, clampN, fitSize } from "./rank-kit.js";
|
|
2
|
+
import { voiceOf } from "./locales.js";
|
|
3
|
+
import { cleanSvg, markDefs, safeUrl } from "./marks.js";
|
|
4
|
+
import { esc, fit, hash, num } from "./text.js";
|
|
5
|
+
/** The looks: `blue` (a broadcast's deep blue with a bright title band), `night` (near black with a cyan band) and `paper` (white, with the same blue band). */
|
|
6
|
+
export const BROADCAST_LOOKS = Object.freeze(["blue", "night", "paper"]);
|
|
7
|
+
/** The sizes in a drawing, in units of its width over 1280. */
|
|
8
|
+
export const BC = Object.freeze({ title: 64, sub: 36, row: 52, gap: 2, pad: 10, band: 38, official: 40, athlete: 62 });
|
|
9
|
+
const safeId = (id) => (id === undefined ? undefined : /^[A-Za-z]/.test(id) ? id.replace(/[^\w-]/g, "-") : `bc-${id.replace(/[^\w-]/g, "-")}`);
|
|
10
|
+
/** The width and look checked, the voice, and the flag context (flags drawn 4:3). `height` is the board's, from its rows, in the unit of 1280 wide. */
|
|
11
|
+
export function broadcastFrame(options, seed, heightUnits) {
|
|
12
|
+
const width = options.width ?? 1280;
|
|
13
|
+
if (!(typeof width === "number" && width >= 320 && width <= 4000))
|
|
14
|
+
throw new RangeError(`taisen-brackets: a board's width is from 320 to 4000, not ${JSON.stringify(options.width)}`);
|
|
15
|
+
const look = options.look ?? "blue";
|
|
16
|
+
if (!BROADCAST_LOOKS.includes(look))
|
|
17
|
+
throw new RangeError(`taisen-brackets: a look is ${BROADCAST_LOOKS.map((l) => `"${l}"`).join(", ")}, not ${JSON.stringify(options.look)}`);
|
|
18
|
+
if (options.icon !== undefined && !["medal", "stopwatch", "none"].includes(options.icon))
|
|
19
|
+
throw new RangeError(`taisen-brackets: an icon is "medal", "stopwatch" or "none", not ${JSON.stringify(options.icon)}`);
|
|
20
|
+
const v = voiceOf(options);
|
|
21
|
+
const u = width / 1280;
|
|
22
|
+
const scope = safeId(options.id) ?? `bc-${hash(JSON.stringify([seed, width, look, options.icon, options.iconMark, v.locale.code, options.flags ? 1 : 0, options.flagHref ? 1 : 0, options.title]))}`;
|
|
23
|
+
const ctx = { radii: new Set(), symbols: new Map(), prefix: scope, shape: "rect", theme: "inherit", flagCache: new Map(), ...(options.flags ? { flags: options.flags } : {}), ...(options.flagHref ? { flagHref: options.flagHref } : {}) };
|
|
24
|
+
return { W: width, H: Math.round(heightUnits * u), u, v, scope, ctx, options, look };
|
|
25
|
+
}
|
|
26
|
+
const LOOKS = {
|
|
27
|
+
blue: {
|
|
28
|
+
panelTop: "#14449a", panelBottom: "#061a4d", edge: "#8fb4ee",
|
|
29
|
+
titleA: "#2a86e0", titleB: "#0c3b92", titleInk: "#ffffff",
|
|
30
|
+
subA: "#eef2f9", subB: "#c1ccdf", subInk: "#0c2252",
|
|
31
|
+
rowA: "#0b2a6c", rowB: "#123a88", ink: "#ffffff", sub: "#bcd0f3",
|
|
32
|
+
rank: "#b3161f", rankEdge: "#f2a6a6", rankInk: "#ffffff",
|
|
33
|
+
gold: "#e4b232", silver: "#c3cbd7", bronze: "#c9864a", medalInk: "#2a1c00",
|
|
34
|
+
badge: "#f28a1b", badgeInk: "#ffffff",
|
|
35
|
+
red: "#c9202b", blue: "#2c6bd2",
|
|
36
|
+
band: "#1fa6d4", bandInk: "#04243a",
|
|
37
|
+
iconA: "#f6bd3c", iconB: "#d9860f", iconInk: "#ffffff",
|
|
38
|
+
},
|
|
39
|
+
night: {
|
|
40
|
+
panelTop: "#161c29", panelBottom: "#080b11", edge: "#3b475c",
|
|
41
|
+
titleA: "#2c3648", titleB: "#141a26", titleInk: "#ffffff",
|
|
42
|
+
subA: "#2a3446", subB: "#1d2535", subInk: "#d3dcec",
|
|
43
|
+
rowA: "#10151f", rowB: "#181f2d", ink: "#f2f5fb", sub: "#9fadc4",
|
|
44
|
+
rank: "#14a9cc", rankEdge: "#9fe7f7", rankInk: "#031821",
|
|
45
|
+
gold: "#e4b232", silver: "#c3cbd7", bronze: "#c9864a", medalInk: "#2a1c00",
|
|
46
|
+
badge: "#ffb020", badgeInk: "#1b1200",
|
|
47
|
+
red: "#d3303d", blue: "#2d73e0",
|
|
48
|
+
band: "#14a9cc", bandInk: "#031821",
|
|
49
|
+
iconA: "#27c0e6", iconB: "#0f86a6", iconInk: "#ffffff",
|
|
50
|
+
},
|
|
51
|
+
paper: {
|
|
52
|
+
panelTop: "#ffffff", panelBottom: "#e8edf5", edge: "#b7c3d8",
|
|
53
|
+
titleA: "#2a6fc6", titleB: "#14346b", titleInk: "#ffffff",
|
|
54
|
+
subA: "#e9eef7", subB: "#d3dbea", subInk: "#14346b",
|
|
55
|
+
rowA: "#ffffff", rowB: "#eef2f9", ink: "#0f1b33", sub: "#566680",
|
|
56
|
+
rank: "#b3161f", rankEdge: "#7a0e15", rankInk: "#ffffff",
|
|
57
|
+
gold: "#e4b232", silver: "#c3cbd7", bronze: "#c9864a", medalInk: "#2a1c00",
|
|
58
|
+
badge: "#e56d0c", badgeInk: "#ffffff",
|
|
59
|
+
red: "#c9202b", blue: "#2c6bd2",
|
|
60
|
+
band: "#2a6fc6", bandInk: "#ffffff",
|
|
61
|
+
iconA: "#f6bd3c", iconB: "#d9860f", iconInk: "#ffffff",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
export const BROADCAST_FONT = '"Roboto Condensed", "Liberation Sans Narrow", "Archivo Narrow", "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
|
|
65
|
+
/** The stylesheet of a board in a look. Every colour is a custom property with the look's value as its fallback (`--bc-title-a`, `--bc-row-a`, `--bc-rank` and the rest). */
|
|
66
|
+
export function broadcastStyle(scope, look, fixed = "") {
|
|
67
|
+
const p = LOOKS[look];
|
|
68
|
+
const s = (sel, body) => `.${scope} ${sel}{${body}}`;
|
|
69
|
+
const v = (name, value) => `var(--bc-${name}, ${value})`;
|
|
70
|
+
return [
|
|
71
|
+
s(".bc-gp-a", `stop-color:${v("panel-top", p.panelTop)}`), s(".bc-gp-b", `stop-color:${v("panel-bottom", p.panelBottom)}`),
|
|
72
|
+
s(".bc-gt-a", `stop-color:${v("title-a", p.titleA)}`), s(".bc-gt-b", `stop-color:${v("title-b", p.titleB)}`),
|
|
73
|
+
s(".bc-gs-a", `stop-color:${v("sub-a", p.subA)}`), s(".bc-gs-b", `stop-color:${v("sub-b", p.subB)}`),
|
|
74
|
+
s(".bc-gi-a", `stop-color:${v("icon-a", p.iconA)}`), s(".bc-gi-b", `stop-color:${v("icon-b", p.iconB)}`),
|
|
75
|
+
s(".bc-edge", `fill:none;stroke:${v("edge", p.edge)};stroke-width:2`),
|
|
76
|
+
s(".bc-titleink", `fill:${v("title-ink", p.titleInk)}`),
|
|
77
|
+
s(".bc-subink", `fill:${v("sub-ink", p.subInk)}`),
|
|
78
|
+
s(".bc-rowa", `fill:${v("row-a", p.rowA)}`), s(".bc-rowb", `fill:${v("row-b", p.rowB)}`),
|
|
79
|
+
s(".bc-ink", `fill:${v("ink", p.ink)}`), s(".bc-sub", `fill:${v("sub", p.sub)}`),
|
|
80
|
+
s(".bc-rank", `fill:${v("rank", p.rank)};stroke:${v("rank-edge", p.rankEdge)};stroke-width:1.5`), s(".bc-rankink", `fill:${v("rank-ink", p.rankInk)}`),
|
|
81
|
+
s(".bc-g1", `fill:${v("gold", p.gold)};stroke:none`), s(".bc-g2", `fill:${v("silver", p.silver)};stroke:none`), s(".bc-g3", `fill:${v("bronze", p.bronze)};stroke:none`), s(".bc-medalink", `fill:${v("medal-ink", p.medalInk)}`),
|
|
82
|
+
s(".bc-badge", `fill:${v("badge", p.badge)}`), s(".bc-badgeink", `fill:${v("badge-ink", p.badgeInk)}`),
|
|
83
|
+
s(".bc-red", `fill:${v("red", p.red)}`), s(".bc-blue", `fill:${v("blue", p.blue)}`),
|
|
84
|
+
s(".bc-band", `fill:${v("band", p.band)}`), s(".bc-bandink", `fill:${v("band-ink", p.bandInk)}`),
|
|
85
|
+
s(".bc-iconink", `fill:none;stroke:${v("icon-ink", p.iconInk)};stroke-linecap:round;stroke-linejoin:round`), s(".bc-iconfill", `fill:${v("icon-ink", p.iconInk)}`),
|
|
86
|
+
s(".bc-gloss-a", "stop-color:#ffffff;stop-opacity:.16"), s(".bc-gloss-b", "stop-color:#ffffff;stop-opacity:0"),
|
|
87
|
+
s(".tb-ring", "fill:none;stroke:rgba(255,255,255,.55);stroke-width:1"), s(".tb-plain", "fill:#2c3e6a"), s(".tb-initials", "fill:#ffffff"),
|
|
88
|
+
s("text", `font-family:var(--bc-font, ${BROADCAST_FONT});font-variant-numeric:tabular-nums`),
|
|
89
|
+
fixed.replaceAll("$S", `.${scope}`),
|
|
90
|
+
].filter(Boolean).join("\n");
|
|
91
|
+
}
|
|
92
|
+
/** The gradients every board uses, by id: the panel's, the title band's, the sub bar's, the icon's and the gloss over a row. */
|
|
93
|
+
export function broadcastDefs(scope) {
|
|
94
|
+
const g = (id, a, b, horizontal) => `<linearGradient id="${esc(scope)}-${id}" x1="0" y1="0" x2="${horizontal ? 1 : 0}" y2="${horizontal ? 0 : 1}"><stop class="${a}" offset="0"/><stop class="${b}" offset="1"/></linearGradient>`;
|
|
95
|
+
return [g("gp", "bc-gp-a", "bc-gp-b", false), g("gt", "bc-gt-a", "bc-gt-b", true), g("gs", "bc-gs-a", "bc-gs-b", false), g("gi", "bc-gi-a", "bc-gi-b", false), g("gl", "bc-gloss-a", "bc-gloss-b", false)].join("");
|
|
96
|
+
}
|
|
97
|
+
export function broadcastWrap(f, title, desc, body, kind, fixed = "") {
|
|
98
|
+
const marks = markDefs(f.ctx);
|
|
99
|
+
return [
|
|
100
|
+
`<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(f.v.locale.code)}" class="taisen taisen-broadcast ${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="${kind}">`,
|
|
101
|
+
`<title id="${esc(f.scope)}-t">${esc(title)}</title><desc id="${esc(f.scope)}-d">${esc(desc)}</desc>`,
|
|
102
|
+
`<style>${broadcastStyle(f.scope, f.look, fixed)}</style>`,
|
|
103
|
+
`<defs>${broadcastDefs(f.scope)}${marks}</defs>`,
|
|
104
|
+
...body,
|
|
105
|
+
"</svg>",
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
// ------------------------------------------------------------------------------------------------ shapes
|
|
109
|
+
export const t = (x, y, size, content, cls, anchor = "start", weight = 700, extra = "") => `<text class="${cls}" x="${num(x)}" y="${num(y)}" font-size="${num(size)}" font-weight="${weight}" text-anchor="${anchor}"${extra}>${esc(content)}</text>`;
|
|
110
|
+
export const box = (x, y, w, h, cls, rx = 0, extra = "") => `<rect class="${cls}" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}"${rx ? ` rx="${num(rx)}"` : ""}${extra}/>`;
|
|
111
|
+
/** The panel's background and edge, from (0, 0) to the board's size, with the corners cut a little. */
|
|
112
|
+
export function panel(f) {
|
|
113
|
+
const r = 8 * f.u;
|
|
114
|
+
return `<rect fill="url(#${esc(f.scope)}-gp)" x="0" y="0" width="${num(f.W)}" height="${num(f.H)}" rx="${num(r)}"/><rect class="bc-edge" x="1" y="1" width="${num(f.W - 2)}" height="${num(f.H - 2)}" rx="${num(r)}"/>`;
|
|
115
|
+
}
|
|
116
|
+
/** The title band: the icon square at the left, the title in capitals, from y to y + the band's height. Returns the shapes and the x where the title text starts. */
|
|
117
|
+
export function titleBand(f, y, title, right) {
|
|
118
|
+
const { u, v } = f;
|
|
119
|
+
const h = BC.title * u;
|
|
120
|
+
const out = [];
|
|
121
|
+
const r = 8 * u;
|
|
122
|
+
// The band is square at the bottom and rounded at the top, like the panel.
|
|
123
|
+
out.push(`<path fill="url(#${esc(f.scope)}-gt)" d="M0 ${num(y + h)} V${num(y + r)} Q0 ${num(y)} ${num(r)} ${num(y)} H${num(f.W - r)} Q${num(f.W)} ${num(y)} ${num(f.W)} ${num(y + r)} V${num(y + h)} Z"/>`);
|
|
124
|
+
let x = 22 * u;
|
|
125
|
+
const icon = f.options.iconMark ? "custom" : f.options.icon ?? "medal";
|
|
126
|
+
if (icon !== "none") {
|
|
127
|
+
const s = h;
|
|
128
|
+
out.push(`<path fill="url(#${esc(f.scope)}-gi)" d="M0 ${num(y + s)} V${num(y + r)} Q0 ${num(y)} ${num(r)} ${num(y)} H${num(s)} V${num(y + s)} Z"/>`);
|
|
129
|
+
out.push(iconShape(f, icon, s / 2, y + s / 2, s * 0.62));
|
|
130
|
+
x = s + 20 * u;
|
|
131
|
+
}
|
|
132
|
+
const rightText = right ? v.upper(right) : "";
|
|
133
|
+
const rs = 20 * u;
|
|
134
|
+
const rw = rightText ? bw(rightText, rs) : 0;
|
|
135
|
+
const avail = f.W - x - 22 * u - (rw ? rw + 20 * u : 0);
|
|
136
|
+
const text = v.upper(title);
|
|
137
|
+
const ts = fitSize(text, 34 * u, avail, 0.55);
|
|
138
|
+
out.push(t(x, y + h / 2 + ts * 0.36, ts, fit(text, ts, avail + 1, bw), "bc-titleink", "start", 800));
|
|
139
|
+
if (rightText)
|
|
140
|
+
out.push(t(f.W - 22 * u, y + h / 2 + rs * 0.36, rs, rightText, "bc-titleink", "end", 700, ' letter-spacing=".04em"'));
|
|
141
|
+
return { svg: out.join(""), h };
|
|
142
|
+
}
|
|
143
|
+
/** An icon in the square, drawn in strokes: a medal on its ribbon, a stopwatch, or the caller's own mark. */
|
|
144
|
+
function iconShape(f, icon, cx, cy, size) {
|
|
145
|
+
const u = size / 100;
|
|
146
|
+
const sw = num(Math.max(2.2 * u * 1.6, 1.5));
|
|
147
|
+
if (icon === "stopwatch") {
|
|
148
|
+
return [
|
|
149
|
+
`<circle class="bc-iconink" cx="${num(cx)}" cy="${num(cy + 6 * u)}" r="${num(38 * u)}" stroke-width="${sw}"/>`,
|
|
150
|
+
`<path class="bc-iconink" d="M${num(cx - 10 * u)} ${num(cy - 40 * u)} H${num(cx + 10 * u)} M${num(cx)} ${num(cy - 40 * u)} V${num(cy - 32 * u)} M${num(cx + 30 * u)} ${num(cy - 22 * u)} l${num(8 * u)} ${num(-8 * u)}" stroke-width="${sw}"/>`,
|
|
151
|
+
`<path class="bc-iconink" d="M${num(cx)} ${num(cy + 6 * u)} L${num(cx + 18 * u)} ${num(cy - 10 * u)}" stroke-width="${sw}"/>`,
|
|
152
|
+
`<circle class="bc-iconfill" cx="${num(cx)}" cy="${num(cy + 6 * u)}" r="${num(4 * u)}"/>`,
|
|
153
|
+
].join("");
|
|
154
|
+
}
|
|
155
|
+
if (icon === "custom") {
|
|
156
|
+
const mark = (f.options.iconMark ?? "").trim();
|
|
157
|
+
const side = size * 1.1;
|
|
158
|
+
if (mark.startsWith("<")) {
|
|
159
|
+
const svg = cleanSvg(mark);
|
|
160
|
+
if (svg)
|
|
161
|
+
return `<svg x="${num(cx - side / 2)}" y="${num(cy - side / 2)}" width="${num(side)}" height="${num(side)}" viewBox="${esc(svg.viewBox)}">${svg.inner}</svg>`;
|
|
162
|
+
}
|
|
163
|
+
else if (safeUrl(mark))
|
|
164
|
+
return `<image href="${esc(mark)}" x="${num(cx - side / 2)}" y="${num(cy - side / 2)}" width="${num(side)}" height="${num(side)}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
// The medal: a disc with a star-less rim and a V of ribbon above it.
|
|
168
|
+
return [
|
|
169
|
+
`<path class="bc-iconink" d="M${num(cx - 26 * u)} ${num(cy - 44 * u)} L${num(cx - 6 * u)} ${num(cy - 8 * u)} M${num(cx + 26 * u)} ${num(cy - 44 * u)} L${num(cx + 6 * u)} ${num(cy - 8 * u)}" stroke-width="${sw}"/>`,
|
|
170
|
+
`<circle class="bc-iconink" cx="${num(cx)}" cy="${num(cy + 18 * u)}" r="${num(28 * u)}" stroke-width="${sw}"/>`,
|
|
171
|
+
`<circle class="bc-iconink" cx="${num(cx)}" cy="${num(cy + 18 * u)}" r="${num(17 * u)}" stroke-width="${num(Math.max(1.2 * u * 1.6, 1))}"/>`,
|
|
172
|
+
`<path class="bc-iconfill" d="M${num(cx)} ${num(cy + 7 * u)} l${num(3.4 * u)} ${num(7 * u)} l${num(7.6 * u)} ${num(1.1 * u)} l${num(-5.5 * u)} ${num(5.3 * u)} l${num(1.3 * u)} ${num(7.6 * u)} l${num(-6.8 * u)} ${num(-3.6 * u)} l${num(-6.8 * u)} ${num(3.6 * u)} l${num(1.3 * u)} ${num(-7.6 * u)} l${num(-5.5 * u)} ${num(-5.3 * u)} l${num(7.6 * u)} ${num(-1.1 * u)} Z"/>`,
|
|
173
|
+
].join("");
|
|
174
|
+
}
|
|
175
|
+
/** The light sub bar under the title: `text` at the left in capitals, and labels over columns at the right (each `{ x, label }`, centred on x). */
|
|
176
|
+
export function subBar(f, y, text, labels = [], bold = true) {
|
|
177
|
+
const { u, v } = f;
|
|
178
|
+
const h = BC.sub * u;
|
|
179
|
+
const out = [box(0, y, f.W, h, "", 0, ` fill="url(#${esc(f.scope)}-gs)"`)];
|
|
180
|
+
const fs = 20 * u;
|
|
181
|
+
const leftEnd = labels.length ? Math.min(...labels.map((l) => l.x - l.w / 2)) - 12 * u : f.W - 18 * u;
|
|
182
|
+
const txt = v.upper(text);
|
|
183
|
+
if (txt) {
|
|
184
|
+
// The letters are spaced, which the width estimate does not know of: leave 7% for it.
|
|
185
|
+
const room = (leftEnd - 20 * u) / 1.07;
|
|
186
|
+
const z = fitSize(txt, fs, room, 0.6);
|
|
187
|
+
out.push(t(20 * u, y + h / 2 + z * 0.36, z, fit(txt, z, room, bw), "bc-subink", "start", bold ? 800 : 700, ' letter-spacing=".02em"'));
|
|
188
|
+
}
|
|
189
|
+
for (const l of labels) {
|
|
190
|
+
const lab = v.upper(l.label);
|
|
191
|
+
const z = fitSize(lab, fs * 0.85, l.w / 1.07, 0.55);
|
|
192
|
+
out.push(t(l.x, y + h / 2 + z * 0.36, z, fit(lab, z, l.w / 1.07, bw), "bc-subink", "middle", 800, ' letter-spacing=".02em"'));
|
|
193
|
+
}
|
|
194
|
+
return { svg: out.join(""), h };
|
|
195
|
+
}
|
|
196
|
+
/** A rank in a box: `box` (the broadcast's red square), `medal` (gold, silver and bronze for the first three, else the box) or `plain` (a white numeral). */
|
|
197
|
+
export function rankBox(x, cy, w, h, text, rank, style, u) {
|
|
198
|
+
const size = fitSize(text, h * 0.62, w * 0.86, 0.5);
|
|
199
|
+
if (style === "plain") {
|
|
200
|
+
const ps = fitSize(text, 27 * u, w * 0.96, 0.5);
|
|
201
|
+
return t(x + w / 2, cy + ps * 0.36, ps, text, "bc-ink", "middle", 800);
|
|
202
|
+
}
|
|
203
|
+
const medal = style === "medal" && rank !== null && rank >= 1 && rank <= 3;
|
|
204
|
+
const cls = medal ? `bc-g${rank}` : "bc-rank";
|
|
205
|
+
const ink = medal ? "bc-medalink" : "bc-rankink";
|
|
206
|
+
return `${box(x, cy - h / 2, w, h, cls, 3 * u)}${t(x + w / 2, cy + size * 0.36, size, text, ink, "middle", 800)}`;
|
|
207
|
+
}
|
|
208
|
+
/** A record badge ("OR", "WR", "NR"): an orange rounded plate with the letters, right-aligned at `right`. Returns the shapes and the width. */
|
|
209
|
+
export function recordBadge(right, cy, h, text, u) {
|
|
210
|
+
const size = h * 0.62;
|
|
211
|
+
const w = bw(text, size) + h * 0.8;
|
|
212
|
+
return { svg: `${box(right - w, cy - h / 2, w, h, "bc-badge", h * 0.28)}${t(right - w / 2, cy + size * 0.36, size, text, "bc-badgeink", "middle", 800, ' letter-spacing=".04em"')}`, w: w + 6 * u };
|
|
213
|
+
}
|
|
214
|
+
/** The gloss over a row: a faint white gradient from the top edge. */
|
|
215
|
+
export const gloss = (f, x, y, w, h) => box(x, y, w, h * 0.55, "", 0, ` fill="url(#${esc(f.scope)}-gl)"`);
|
|
216
|
+
export { bw, clampN, fitSize };
|
package/dist/index.d.ts
CHANGED
|
@@ -35,3 +35,7 @@ export { rankingMatchup, describeMatchup, matchupRecord } from "./ranking-matchu
|
|
|
35
35
|
export type { MatchupOptions, MatchupSource, RankingMeeting } from "./ranking-matchup.ts";
|
|
36
36
|
export { medalTable, medalRanks, tallyMedals, describeMedalTable } from "./medals.ts";
|
|
37
37
|
export type { MedalAward, MedalFilter, MedalRow, MedalTableOptions, MedalTableSource } from "./medals.ts";
|
|
38
|
+
export { resultsBoard, resultsBoardSize, describeResultsBoard } from "./results-board.ts";
|
|
39
|
+
export type { ResultsBoardOptions, ResultsBoardSource, ResultsColumn, ResultsRow } from "./results-board.ts";
|
|
40
|
+
export { BROADCAST_LOOKS } from "./broadcast-kit.ts";
|
|
41
|
+
export type { BroadcastIcon, BroadcastLook, BroadcastOptions } from "./broadcast-kit.ts";
|
package/dist/index.js
CHANGED
|
@@ -19,3 +19,5 @@ export { BOARD_SIZES } from "./rank-kit.js";
|
|
|
19
19
|
export { rankingNumberOnes, describeNumberOnes, rankingEntryList, describeEntryList, NUMBER_ONE_COLORS } from "./ranking-cards.js";
|
|
20
20
|
export { rankingMatchup, describeMatchup, matchupRecord } from "./ranking-matchup.js";
|
|
21
21
|
export { medalTable, medalRanks, tallyMedals, describeMedalTable } from "./medals.js";
|
|
22
|
+
export { resultsBoard, resultsBoardSize, describeResultsBoard } from "./results-board.js";
|
|
23
|
+
export { BROADCAST_LOOKS } from "./broadcast-kit.js";
|
package/dist/locales.js
CHANGED
|
@@ -33,6 +33,7 @@ const EN = {
|
|
|
33
33
|
rankNumberOnes: "Number ones", rankNumberOneIn: "#1 in {category}", rankEntryTitle: "Entry list", rankColRanking: "Ranking", rankUnranked: "NR",
|
|
34
34
|
rankMatchTitle: "Head to head", rankMeetings: "Previous meetings", rankNoMeetings: "No previous meetings", rankMore: "+{n} more",
|
|
35
35
|
medalTitle: "Medal table", medalNone: "No medals yet", medalGold: "Gold", medalSilver: "Silver", medalBronze: "Bronze", medalGoldShort: "G", medalSilverShort: "S", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} gold, {s} silver, {b} bronze, {t} total", medalRank: "{n}",
|
|
36
|
+
rbResult: "Result",
|
|
36
37
|
};
|
|
37
38
|
const FR = {
|
|
38
39
|
tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
|
|
@@ -62,6 +63,7 @@ const FR = {
|
|
|
62
63
|
rankNumberOnes: "Les numéros un", rankNumberOneIn: "N°\u00a01 en {category}", rankEntryTitle: "Liste des engagés", rankColRanking: "Classement", rankUnranked: "NC",
|
|
63
64
|
rankMatchTitle: "Face-à-face", rankMeetings: "Confrontations précédentes", rankNoMeetings: "Aucune confrontation précédente", rankMore: "+{n} de plus",
|
|
64
65
|
medalTitle: "Tableau des médailles", medalNone: "Aucune médaille pour l’instant", medalGold: "Or", medalSilver: "Argent", medalBronze: "Bronze", medalGoldShort: "O", medalSilverShort: "A", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} or, {s} argent, {b} bronze, {t} au total", medalRank: "{n}",
|
|
66
|
+
rbResult: "Résultat",
|
|
65
67
|
};
|
|
66
68
|
const JA = {
|
|
67
69
|
tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
|
|
@@ -91,6 +93,7 @@ const JA = {
|
|
|
91
93
|
rankNumberOnes: "各階級の1位", rankNumberOneIn: "{category} 1位", rankEntryTitle: "エントリーリスト", rankColRanking: "ランキング", rankUnranked: "圏外",
|
|
92
94
|
rankMatchTitle: "直接対決", rankMeetings: "過去の対戦", rankNoMeetings: "過去の対戦はありません", rankMore: "ほか{n}試合",
|
|
93
95
|
medalTitle: "メダル獲得表", medalNone: "まだメダルはありません", medalGold: "金メダル", medalSilver: "銀メダル", medalBronze: "銅メダル", medalGoldShort: "金", medalSilverShort: "銀", medalBronzeShort: "銅", medalTotal: "合計", medalLine: "金{g}、銀{s}、銅{b}、合計{t}", medalRank: "{n}位",
|
|
96
|
+
rbResult: "結果",
|
|
94
97
|
};
|
|
95
98
|
const ES = {
|
|
96
99
|
tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
|
|
@@ -120,6 +123,7 @@ const ES = {
|
|
|
120
123
|
rankNumberOnes: "Los números uno", rankNumberOneIn: "N.º 1 en {category}", rankEntryTitle: "Lista de inscritos", rankColRanking: "Clasificación", rankUnranked: "SC",
|
|
121
124
|
rankMatchTitle: "Cara a cara", rankMeetings: "Enfrentamientos anteriores", rankNoMeetings: "Sin enfrentamientos anteriores", rankMore: "+{n} más",
|
|
122
125
|
medalTitle: "Medallero", medalNone: "Aún no hay medallas", medalGold: "Oro", medalSilver: "Plata", medalBronze: "Bronce", medalGoldShort: "O", medalSilverShort: "P", medalBronzeShort: "B", medalTotal: "Total", medalLine: "{g} oro, {s} plata, {b} bronce, {t} en total", medalRank: "{n}",
|
|
126
|
+
rbResult: "Resultado",
|
|
123
127
|
};
|
|
124
128
|
const DE = {
|
|
125
129
|
tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
|
|
@@ -149,6 +153,7 @@ const DE = {
|
|
|
149
153
|
rankNumberOnes: "Die Nummer Eins", rankNumberOneIn: "Nr. 1 in {category}", rankEntryTitle: "Meldeliste", rankColRanking: "Rangliste", rankUnranked: "o. R.",
|
|
150
154
|
rankMatchTitle: "Direkter Vergleich", rankMeetings: "Bisherige Duelle", rankNoMeetings: "Noch keine Duelle", rankMore: "+{n} weitere",
|
|
151
155
|
medalTitle: "Medaillenspiegel", medalNone: "Noch keine Medaillen", medalGold: "Gold", medalSilver: "Silber", medalBronze: "Bronze", medalGoldShort: "G", medalSilverShort: "S", medalBronzeShort: "B", medalTotal: "Gesamt", medalLine: "{g} Gold, {s} Silber, {b} Bronze, {t} gesamt", medalRank: "{n}",
|
|
156
|
+
rbResult: "Ergebnis",
|
|
152
157
|
};
|
|
153
158
|
/**
|
|
154
159
|
* The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
|
package/dist/marks.d.ts
CHANGED
|
@@ -33,7 +33,8 @@ export interface MarkContext {
|
|
|
33
33
|
inner: string;
|
|
34
34
|
}>;
|
|
35
35
|
prefix: string;
|
|
36
|
-
|
|
36
|
+
/** `rect` draws a flag 4:3: `size` is its height and its width a third more (`FlagRequest.shape` is then `rect`, which Hata calls `"4:3"`). */
|
|
37
|
+
shape: "round" | "square" | "rect" | "none";
|
|
37
38
|
/** The look: `road` draws a team's mark with a wide ring and a hairline outside it. */
|
|
38
39
|
look?: "classic" | "road";
|
|
39
40
|
/** Draws a team's `flag` code; with none a team's `mark` or initials are drawn. */
|
package/dist/marks.js
CHANGED
|
@@ -92,7 +92,7 @@ export function safeHref(url) {
|
|
|
92
92
|
function flagOf(code, size, ctx) {
|
|
93
93
|
if (typeof ctx.flags !== "function" || typeof code !== "string" || code.trim() === "")
|
|
94
94
|
return null;
|
|
95
|
-
const shape = ctx.shape === "square" ? "square" : "round";
|
|
95
|
+
const shape = ctx.shape === "square" ? "square" : ctx.shape === "rect" ? "rect" : "round";
|
|
96
96
|
const key = `${code.trim()}|${shape}|${Math.round(size)}`;
|
|
97
97
|
const cache = ctx.flagCache;
|
|
98
98
|
if (cache?.has(key))
|
|
@@ -124,6 +124,8 @@ export const clipId = (prefix, r) => `${prefix}-c${String(num(r)).replace(".", "
|
|
|
124
124
|
* with no mark, or one that cannot be used, is its initials on a plain disc. Returns SVG text.
|
|
125
125
|
*/
|
|
126
126
|
export function drawMark(team, cx, cy, size, ctx, abbrText) {
|
|
127
|
+
if (ctx.shape === "rect")
|
|
128
|
+
return drawRectMark(team, cx, cy, size, ctx, abbrText);
|
|
127
129
|
const r = size / 2;
|
|
128
130
|
if (!team)
|
|
129
131
|
return `<circle class="tb-plain tb-mark" cx="${num(cx)}" cy="${num(cy)}" r="${num(r)}" opacity=".5"/>`;
|
|
@@ -205,3 +207,49 @@ export function drawMark(team, cx, cy, size, ctx, abbrText) {
|
|
|
205
207
|
const ink = team.color ? inkOn(team.color) : null;
|
|
206
208
|
return `<g class="tb-mark">${shape}<text class="tb-initials" x="${num(cx)}" y="${num(cy + size * 0.12)}" font-size="${num(size * 0.36)}" font-weight="700" text-anchor="middle"${ink ? ` style="fill:${ink}"` : ""}>${esc(text)}</text></g>${ring}`;
|
|
207
209
|
}
|
|
210
|
+
/** A flag, a mark or a badge in a 4:3 box `h` high (the shape of a national flag, as a scoreboard draws it), centred at (cx, cy). */
|
|
211
|
+
function drawRectMark(team, cx, cy, h, ctx, abbrText) {
|
|
212
|
+
const w = h * (4 / 3);
|
|
213
|
+
const x = cx - w / 2, y = cy - h / 2;
|
|
214
|
+
const rx = Math.max(h * 0.08, 1);
|
|
215
|
+
const box = (cls, extra = "") => `<rect class="${cls}" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" rx="${num(rx)}"${extra}/>`;
|
|
216
|
+
if (!team)
|
|
217
|
+
return box("tb-plain tb-mark", ' opacity=".5"');
|
|
218
|
+
const ring = box("tb-ring");
|
|
219
|
+
const used = (key, markup) => {
|
|
220
|
+
let symbol = ctx.symbols.get(key);
|
|
221
|
+
if (!symbol) {
|
|
222
|
+
symbol = { id: `${ctx.prefix}-m${ctx.symbols.size + 1}`, ...markup };
|
|
223
|
+
ctx.symbols.set(key, symbol);
|
|
224
|
+
}
|
|
225
|
+
return `<g class="tb-mark"><use href="#${symbol.id}" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}"/></g>${ring}`;
|
|
226
|
+
};
|
|
227
|
+
const flag = flagOf(team.flag, w, ctx);
|
|
228
|
+
if (flag)
|
|
229
|
+
return used(`flag:${flag.viewBox}:${flag.inner}`, flag);
|
|
230
|
+
let fromCode;
|
|
231
|
+
if (typeof ctx.flagHref === "function" && typeof team.flag === "string" && team.flag.trim() !== "") {
|
|
232
|
+
try {
|
|
233
|
+
const href = ctx.flagHref(team.flag.trim());
|
|
234
|
+
if (typeof href === "string" && !href.trim().startsWith("<") && safeUrl(href))
|
|
235
|
+
fromCode = href;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
fromCode = undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const mark = (fromCode ?? team.mark)?.trim();
|
|
242
|
+
if (mark) {
|
|
243
|
+
if (mark.startsWith("<")) {
|
|
244
|
+
const svg = cleanSvg(mark);
|
|
245
|
+
if (svg)
|
|
246
|
+
return used(mark, svg);
|
|
247
|
+
}
|
|
248
|
+
else if (safeUrl(mark))
|
|
249
|
+
return used(`url:${mark}`, { viewBox: "0 0 100 100", inner: `<image href="${esc(mark)}" x="0" y="0" width="100" height="100" preserveAspectRatio="xMidYMid slice"/>` });
|
|
250
|
+
}
|
|
251
|
+
const text = (abbrText ?? team.abbr ?? abbreviate(team.name)).slice(0, 3);
|
|
252
|
+
const fillStyle = team.color ? ` style="fill:${esc(team.color)}"` : "";
|
|
253
|
+
const ink = team.color ? inkOn(team.color) : null;
|
|
254
|
+
return `<g class="tb-mark">${box("tb-plain", fillStyle)}<text class="tb-initials" x="${num(cx)}" y="${num(cy + h * 0.14)}" font-size="${num(Math.max(h * (text.length <= 2 ? 0.46 : 0.4), 9))}" font-weight="700" text-anchor="middle"${ink ? ` style="fill:${ink}"` : ""}>${esc(text)}</text></g>${ring}`;
|
|
255
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A results board in the manner of a television graphic: a title band (the event, in capitals, with an icon square), a sub bar (the phase, and the heading of each score column), and a row for
|
|
3
|
+
* each athlete: a rank in a box, the country code and a 4:3 flag, the name, a record badge ("OR", "WR", "NR") and one or more columns of scores or times at the sport's own precision. It is
|
|
4
|
+
* a standalone SVG of a width you choose and a height that follows from the rows (`resultsBoardSize`), made to be placed over a picture. No mark of any games or broadcaster is drawn.
|
|
5
|
+
*/
|
|
6
|
+
import { type BroadcastOptions } from "./broadcast-kit.ts";
|
|
7
|
+
import { type RankingName, type RankingOptions } from "./ranking.ts";
|
|
8
|
+
/** One line of a results board. Only `name` is needed. */
|
|
9
|
+
export interface ResultsRow {
|
|
10
|
+
/** The name as it is to be written ("A. SACRAMONE", "Svetlana Khorkina"), or its two parts (set by `nameStyle`). */
|
|
11
|
+
name: string | RankingName;
|
|
12
|
+
/** The rank: a number, or a text ("=3", "DQ"). Left out, the place in the list (equal results share one, see `ties`). */
|
|
13
|
+
rank?: number | string;
|
|
14
|
+
/** The country's three letters, written before the flag. */
|
|
15
|
+
code?: string;
|
|
16
|
+
/** A code for the `flags` function (`US`, `CA-BC`), drawn 4:3. */
|
|
17
|
+
flag?: string;
|
|
18
|
+
/** A mark of your own (an image address or inline SVG) in place of a flag. */
|
|
19
|
+
mark?: string;
|
|
20
|
+
/** One value for each column: a number (written to the column's `precision` decimals), a text (written as it is: `1:08.34`), or none. */
|
|
21
|
+
values?: readonly (number | string | null)[];
|
|
22
|
+
/** A record badge or several beside the last value: `"OR"`, `["WR", "NR"]`. */
|
|
23
|
+
record?: string | readonly string[];
|
|
24
|
+
/** Written in place of the values when there are none to show: `DNS`, `DNF`, `DQ`. */
|
|
25
|
+
note?: string;
|
|
26
|
+
}
|
|
27
|
+
/** A column of scores: its heading in the sub bar, and the decimals of its numbers. */
|
|
28
|
+
export interface ResultsColumn {
|
|
29
|
+
label?: string;
|
|
30
|
+
precision?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ResultsBoardSource {
|
|
33
|
+
/** The event, in capitals in the title band ("Women's apparatus final"). */
|
|
34
|
+
title: string;
|
|
35
|
+
/** The phase in the sub bar ("Result", "Final", "Preliminaries"). Default: the language's "Result". */
|
|
36
|
+
phase?: string;
|
|
37
|
+
/** The score columns, left to right. Default: one column per value of the longest row, with no heading. */
|
|
38
|
+
columns?: readonly ResultsColumn[];
|
|
39
|
+
rows: readonly ResultsRow[];
|
|
40
|
+
}
|
|
41
|
+
export interface ResultsBoardOptions extends BroadcastOptions, Pick<RankingOptions, "nameStyle"> {
|
|
42
|
+
/** `box` (the default: a red square, as a broadcast has it), `medal` (gold, silver and bronze boxes for the first three) or `plain` (a white numeral). */
|
|
43
|
+
rankStyle?: "box" | "medal" | "plain";
|
|
44
|
+
/** Decimals for numeric values where a column names none (`3` writes `38.387`). Default: as many as the number has, up to three. */
|
|
45
|
+
precision?: number;
|
|
46
|
+
/** Equal results share a rank (`1, 2, 2, 4`). Default true. */
|
|
47
|
+
ties?: boolean;
|
|
48
|
+
/** `upper` writes the names in capitals; `as-is` (the default) leaves them as given. */
|
|
49
|
+
nameCase?: "as-is" | "upper";
|
|
50
|
+
}
|
|
51
|
+
/** The width and the height of a results board with `rowCount` rows: the width given (1280 by default) and a height of 64 + 36 + 20 + 54 a row, in units of the width over 1280. */
|
|
52
|
+
export declare function resultsBoardSize(rowCount: number, options?: Pick<BroadcastOptions, "width">): {
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
};
|
|
56
|
+
/** The board as a sentence, the drawing's description. */
|
|
57
|
+
export declare function describeResultsBoard(source: ResultsBoardSource, options?: Pick<ResultsBoardOptions, "locale" | "words" | "nameStyle" | "precision" | "ties" | "nameCase">): string;
|
|
58
|
+
/**
|
|
59
|
+
* The results board as a standalone SVG. Throws a `RangeError` for a board with no title or no rows, more than 24 rows, a row with no name, a value that is not a number, a text or null, a precision
|
|
60
|
+
* that is not a whole number from 0 to 6, an unknown option value, or a width outside 720 to 4000.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resultsBoard(source: ResultsBoardSource, options?: ResultsBoardOptions): string;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A results board in the manner of a television graphic: a title band (the event, in capitals, with an icon square), a sub bar (the phase, and the heading of each score column), and a row for
|
|
3
|
+
* each athlete: a rank in a box, the country code and a 4:3 flag, the name, a record badge ("OR", "WR", "NR") and one or more columns of scores or times at the sport's own precision. It is
|
|
4
|
+
* a standalone SVG of a width you choose and a height that follows from the rows (`resultsBoardSize`), made to be placed over a picture. No mark of any games or broadcaster is drawn.
|
|
5
|
+
*/
|
|
6
|
+
import { BC, box, broadcastFrame, broadcastWrap, bw, fitSize, gloss, panel, rankBox, recordBadge, subBar, t, titleBand } from "./broadcast-kit.js";
|
|
7
|
+
import { voiceOf } from "./locales.js";
|
|
8
|
+
import { drawMark } from "./marks.js";
|
|
9
|
+
import { nameText } from "./ranking.js";
|
|
10
|
+
import { abbreviate, fit } from "./text.js";
|
|
11
|
+
const MAX_ROWS = 24;
|
|
12
|
+
/** The width and the height of a results board with `rowCount` rows: the width given (1280 by default) and a height of 64 + 36 + 20 + 54 a row, in units of the width over 1280. */
|
|
13
|
+
export function resultsBoardSize(rowCount, options = {}) {
|
|
14
|
+
const width = options.width ?? 1280;
|
|
15
|
+
const u = width / 1280;
|
|
16
|
+
return { width, height: Math.round((BC.title + BC.sub + BC.pad * 2 + rowCount * (BC.row + BC.gap)) * u) };
|
|
17
|
+
}
|
|
18
|
+
function validate(source, options) {
|
|
19
|
+
if (!source || typeof source.title !== "string" || source.title.trim() === "")
|
|
20
|
+
throw new RangeError("taisen-brackets: a results board needs a title");
|
|
21
|
+
if (!Array.isArray(source.rows) || source.rows.length === 0)
|
|
22
|
+
throw new RangeError("taisen-brackets: a results board needs at least one row");
|
|
23
|
+
if (source.rows.length > MAX_ROWS)
|
|
24
|
+
throw new RangeError(`taisen-brackets: a results board has at most ${MAX_ROWS} rows (it is a screen graphic): cut the list or draw a ranking list`);
|
|
25
|
+
source.rows.forEach((r, i) => {
|
|
26
|
+
const n = r?.name;
|
|
27
|
+
const ok = typeof n === "string" ? n.trim() !== "" : !!n && typeof n === "object" && typeof n.surname === "string" && n.surname.trim() !== "";
|
|
28
|
+
if (!ok)
|
|
29
|
+
throw new RangeError(`taisen-brackets: row ${i + 1} of the results board needs a name`);
|
|
30
|
+
if (r.values !== undefined) {
|
|
31
|
+
if (!Array.isArray(r.values))
|
|
32
|
+
throw new RangeError(`taisen-brackets: the values of row ${i + 1} are a list`);
|
|
33
|
+
r.values.forEach((x) => { if (x !== null && typeof x !== "number" && typeof x !== "string")
|
|
34
|
+
throw new RangeError(`taisen-brackets: a value of row ${i + 1} is a number, a text or null`); if (typeof x === "number" && !Number.isFinite(x))
|
|
35
|
+
throw new RangeError(`taisen-brackets: a value of row ${i + 1} is not a finite number`); });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
source.columns?.forEach((c) => { if (c.precision !== undefined && !(Number.isInteger(c.precision) && c.precision >= 0 && c.precision <= 6))
|
|
39
|
+
throw new RangeError("taisen-brackets: a column's precision is a whole number from 0 to 6"); });
|
|
40
|
+
if (options.precision !== undefined && !(Number.isInteger(options.precision) && options.precision >= 0 && options.precision <= 6))
|
|
41
|
+
throw new RangeError("taisen-brackets: precision is a whole number from 0 to 6");
|
|
42
|
+
if (options.rankStyle !== undefined && !["box", "medal", "plain"].includes(options.rankStyle))
|
|
43
|
+
throw new RangeError(`taisen-brackets: rankStyle is "box", "medal" or "plain", not ${JSON.stringify(options.rankStyle)}`);
|
|
44
|
+
if (options.nameCase !== undefined && !["as-is", "upper"].includes(options.nameCase))
|
|
45
|
+
throw new RangeError(`taisen-brackets: nameCase is "as-is" or "upper", not ${JSON.stringify(options.nameCase)}`);
|
|
46
|
+
if (options.nameStyle !== undefined && !["surname-first", "given-first", "as-is"].includes(options.nameStyle))
|
|
47
|
+
throw new RangeError(`taisen-brackets: nameStyle is "surname-first", "given-first" or "as-is", not ${JSON.stringify(options.nameStyle)}`);
|
|
48
|
+
}
|
|
49
|
+
const records = (r) => (r.record === undefined ? [] : typeof r.record === "string" ? [r.record] : [...r.record]).filter((x) => x.trim() !== "");
|
|
50
|
+
/** A value as it is written: a number to its precision, a text as it is, none as a dash. */
|
|
51
|
+
function valueText(x, precision, v) {
|
|
52
|
+
if (x === null || x === undefined)
|
|
53
|
+
return "";
|
|
54
|
+
if (typeof x === "string")
|
|
55
|
+
return x;
|
|
56
|
+
return precision === undefined ? v.number(x) : new Intl.NumberFormat(v.locale.tag, { minimumFractionDigits: precision, maximumFractionDigits: precision }).format(x);
|
|
57
|
+
}
|
|
58
|
+
/** The ranks as the board writes them, in the order of the rows. */
|
|
59
|
+
function ranksOf(rows, ties) {
|
|
60
|
+
const out = [];
|
|
61
|
+
rows.forEach((r, i) => {
|
|
62
|
+
if (r.rank !== undefined) {
|
|
63
|
+
out.push(r.rank);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const prev = rows[i - 1];
|
|
67
|
+
const same = ties && prev && prev.rank === undefined && r.values && prev.values && r.values.length > 0 && r.values.length === prev.values.length && r.values.every((x, k) => x === prev.values[k]);
|
|
68
|
+
out.push(same ? out[i - 1] : i + 1);
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** The board as a sentence, the drawing's description. */
|
|
73
|
+
export function describeResultsBoard(source, options = {}) {
|
|
74
|
+
const v = voiceOf(options);
|
|
75
|
+
const w = v.words;
|
|
76
|
+
const parts = [source.title, source.phase ?? w.rbResult];
|
|
77
|
+
const ranks = ranksOf(source.rows, options.ties ?? true);
|
|
78
|
+
source.rows.forEach((r, i) => {
|
|
79
|
+
const name = nameText(r.name, options.nameStyle ?? "surname-first", v);
|
|
80
|
+
const vals = (r.values ?? []).map((x, k) => valueText(x, source.columns?.[k]?.precision ?? options.precision, v)).filter(Boolean);
|
|
81
|
+
const bits = [String(ranks[i]), options.nameCase === "upper" ? v.upper(name) : name, r.code ?? "", ...(r.note ? [r.note] : vals), ...records(r)].filter(Boolean);
|
|
82
|
+
parts.push(bits.join(w.rankJoin));
|
|
83
|
+
});
|
|
84
|
+
return parts.join(w.sep) + w.end;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The results board as a standalone SVG. Throws a `RangeError` for a board with no title or no rows, more than 24 rows, a row with no name, a value that is not a number, a text or null, a precision
|
|
88
|
+
* that is not a whole number from 0 to 6, an unknown option value, or a width outside 720 to 4000.
|
|
89
|
+
*/
|
|
90
|
+
export function resultsBoard(source, options = {}) {
|
|
91
|
+
validate(source, options);
|
|
92
|
+
if (options.width !== undefined && options.width < 720)
|
|
93
|
+
throw new RangeError("taisen-brackets: a board's width is from 720 to 4000");
|
|
94
|
+
const n = source.rows.length;
|
|
95
|
+
const size = resultsBoardSize(n, options);
|
|
96
|
+
const f = broadcastFrame(options, [source, options.rankStyle, options.precision, options.ties, options.nameCase, options.nameStyle], size.height / (size.width / 1280));
|
|
97
|
+
const { W, u, v } = f;
|
|
98
|
+
const w = v.words;
|
|
99
|
+
const style = options.nameStyle ?? "surname-first";
|
|
100
|
+
const ranks = ranksOf(source.rows, options.ties ?? true);
|
|
101
|
+
const body = [panel(f)];
|
|
102
|
+
const title = titleBand(f, 0, source.title);
|
|
103
|
+
body.push(title.svg);
|
|
104
|
+
// The columns, from the right: the widest value or heading sets a column's width.
|
|
105
|
+
const cols = Math.max(source.columns?.length ?? 0, ...source.rows.map((r) => r.values?.length ?? 0));
|
|
106
|
+
const vs = 27 * u, ls = 17 * u;
|
|
107
|
+
const texts = source.rows.map((r) => Array.from({ length: cols }, (_, k) => valueText(r.values?.[k], source.columns?.[k]?.precision ?? options.precision, v)));
|
|
108
|
+
const widths = Array.from({ length: cols }, (_, k) => Math.max(...texts.map((row) => bw(row[k], vs)), bw(v.upper(source.columns?.[k]?.label ?? ""), ls * 1.1)) + 30 * u);
|
|
109
|
+
const right = W - 14 * u;
|
|
110
|
+
const centres = [];
|
|
111
|
+
let edge = right;
|
|
112
|
+
for (let k = cols - 1; k >= 0; k--) {
|
|
113
|
+
centres[k] = edge - widths[k] / 2;
|
|
114
|
+
edge -= widths[k];
|
|
115
|
+
}
|
|
116
|
+
const labels = source.columns?.map((c, k) => ({ x: centres[k], w: widths[k], label: c.label ?? "" })).filter((l) => l.label) ?? [];
|
|
117
|
+
const sub = subBar(f, title.h, source.phase ?? w.rbResult, labels);
|
|
118
|
+
body.push(sub.svg);
|
|
119
|
+
const top = title.h + sub.h + BC.pad * u;
|
|
120
|
+
const h = BC.row * u;
|
|
121
|
+
const anyFlag = source.rows.some((r) => r.flag || r.mark);
|
|
122
|
+
const anyCode = source.rows.some((r) => r.code);
|
|
123
|
+
const rankW = 46 * u;
|
|
124
|
+
const cs = 24 * u;
|
|
125
|
+
const codeW = anyCode ? Math.max(...source.rows.map((r) => bw(r.code ?? "", cs))) + 12 * u : 0;
|
|
126
|
+
const flagH = 28 * u, flagW = flagH * (4 / 3);
|
|
127
|
+
source.rows.forEach((r, i) => {
|
|
128
|
+
const y = top + i * (h + BC.gap * u);
|
|
129
|
+
const cy = y + h / 2;
|
|
130
|
+
const g = [];
|
|
131
|
+
g.push(box(0, y, W, h, i % 2 === 0 ? "bc-rowa" : "bc-rowb"));
|
|
132
|
+
g.push(gloss(f, 0, y, W, h));
|
|
133
|
+
let x = 12 * u;
|
|
134
|
+
const rank = ranks[i];
|
|
135
|
+
g.push(rankBox(x, cy, rankW, 36 * u, typeof rank === "number" ? v.number(rank) : rank, typeof rank === "number" ? rank : null, options.rankStyle ?? "box", u));
|
|
136
|
+
x += rankW + 14 * u;
|
|
137
|
+
if (anyCode) {
|
|
138
|
+
g.push(t(x, cy + cs * 0.36, cs, r.code ?? "", "bc-ink", "start", 700));
|
|
139
|
+
x += codeW;
|
|
140
|
+
}
|
|
141
|
+
if (anyFlag) {
|
|
142
|
+
if (r.flag || r.mark)
|
|
143
|
+
g.push(drawMark({ id: `r${i}`, name: String(typeof r.name === "string" ? r.name : r.name.surname), abbr: (r.code ?? abbreviate(typeof r.name === "string" ? r.name : r.name.surname)).slice(0, 3), ...(r.flag ? { flag: r.flag } : {}), ...(r.mark ? { mark: r.mark } : {}) }, x + flagW / 2, cy, flagH, f.ctx));
|
|
144
|
+
x += flagW + 14 * u;
|
|
145
|
+
}
|
|
146
|
+
// The values at the right, then the badges before the last value, then the name in what is left.
|
|
147
|
+
let nameRight = (cols > 0 ? edge : right) - 8 * u;
|
|
148
|
+
const vals = [];
|
|
149
|
+
if (r.note)
|
|
150
|
+
vals.push(t(right - 6 * u, cy + vs * 0.36, vs * 0.85, r.note, "bc-sub", "end", 700));
|
|
151
|
+
else
|
|
152
|
+
texts[i].forEach((text, k) => { if (text)
|
|
153
|
+
vals.push(t(centres[k] + widths[k] / 2 - 14 * u, cy + vs * 0.36, vs, text, "bc-ink", "end", k === cols - 1 ? 800 : 700)); });
|
|
154
|
+
if (r.note)
|
|
155
|
+
nameRight = right - bw(r.note, vs * 0.85) - 18 * u;
|
|
156
|
+
const badges = [];
|
|
157
|
+
let bx = cols > 0 && !r.note ? edge - 4 * u : nameRight;
|
|
158
|
+
for (const rec of records(r)) {
|
|
159
|
+
const b = recordBadge(bx, cy, 26 * u, rec, u);
|
|
160
|
+
badges.push(b.svg);
|
|
161
|
+
bx -= b.w;
|
|
162
|
+
}
|
|
163
|
+
if (badges.length)
|
|
164
|
+
nameRight = bx - 4 * u;
|
|
165
|
+
const name = options.nameCase === "upper" ? v.upper(nameText(r.name, style, v)) : nameText(r.name, style, v);
|
|
166
|
+
const avail = Math.max(40 * u, nameRight - x);
|
|
167
|
+
const ns = fitSize(name, 27 * u, avail, 0.6);
|
|
168
|
+
g.push(t(x, cy + ns * 0.36, ns, fit(name, ns, avail + 1, bw), "bc-ink", "start", 700));
|
|
169
|
+
g.push(...badges, ...vals);
|
|
170
|
+
body.push(`<g class="bc-row">${g.join("")}</g>`);
|
|
171
|
+
});
|
|
172
|
+
return broadcastWrap(f, options.title ?? source.title, describeResultsBoard(source, options), body, "results-board");
|
|
173
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -438,6 +438,8 @@ export interface Words {
|
|
|
438
438
|
medalLine: string;
|
|
439
439
|
/** A rank in the row sentence (`{n}`): "3", and "3位" in Japanese. */
|
|
440
440
|
medalRank: string;
|
|
441
|
+
/** The broadcast-style boards (`resultsBoard`, `boutBoard`, `broadcastMedalTable`): the sub bar\'s word for a result. */
|
|
442
|
+
rbResult: string;
|
|
441
443
|
}
|
|
442
444
|
/** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
|
|
443
445
|
export interface Locale {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.15.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.15.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/taisen-brackets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|