@johnmorrisdotca/taisen-brackets 0.4.0 → 0.6.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/dist/road.draw.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The road chart, drawn: the two finalists' big round marks with their names, the round names down the middle on a dotted road, each finalist's matches on its side as
2
2
  // the round marks of both teams with the score between them, and the group stage as a compact row at the foot. Each size has its own arrangement: `card`, `square` and
3
3
  // `phone` stack the finalists over the rounds; `wide` puts them at the two ends. Nothing is measured in a browser: type is set by estimates (`emWidth`).
4
+ import { resolveBackground } from "./background.js";
4
5
  import { drawMark, markDefs } from "./marks.js";
5
6
  import { codeOf, decidedWords, nameOf, summaryOf } from "./road.model.js";
6
7
  import { SIZES } from "./render.js";
@@ -36,6 +37,7 @@ export function drawRoad(model, o, gid) {
36
37
  shape: o.markShape === "square" ? "square" : "round",
37
38
  look: "road",
38
39
  flags: o.flags,
40
+ flagHref: o.flagHref,
39
41
  theme: theme === "light" ? "light" : theme === "dark" || theme === "poster" ? "dark" : "inherit",
40
42
  flagCache: new Map(),
41
43
  };
@@ -232,9 +234,10 @@ export function drawRoad(model, o, gid) {
232
234
  if (model.subtitle !== undefined)
233
235
  head += text("tb-r-s", cx, pad + titleFit.height + subSize * 0.7, subSize, model.subtitle);
234
236
  const foot = endSize === 0 ? "" : text("tb-r-e", cx, H - pad - endSize * 0.2, endSize, note);
235
- const defs = markDefs(ctx);
236
- const style = theme === "none" ? "" : `<style>${styleFor(`.${gid}`, theme, { chart: true })}</style>`;
237
- const bg = o.background === false ? "" : `<rect class="tb-bg" x="0" y="0" width="${num(W)}" height="${num(H)}"${theme === "none" ? ' fill="none"' : ""}/>`;
237
+ const picture = resolveBackground(o.background, theme, W, H, gid);
238
+ const defs = markDefs(ctx) + (picture?.defs ?? "");
239
+ const style = theme === "none" ? "" : `<style>${styleFor(`.${gid}`, theme, { chart: true, bgAuto: picture?.auto }, picture?.inks)}</style>`;
240
+ const bg = o.background === false ? "" : `<rect class="tb-bg" x="0" y="0" width="${num(W)}" height="${num(H)}"${theme === "none" ? ' fill="none"' : ""}/>${picture?.body ?? ""}`;
238
241
  const extra = o.className ? ` ${esc(o.className)}` : "";
239
242
  return [
240
243
  `<svg xmlns="http://www.w3.org/2000/svg" lang="${esc(model.voice.locale.code)}" class="taisen tb-road ${gid}${extra}" viewBox="0 0 ${num(W)} ${num(H)}" width="${num(W)}" height="${num(H)}" style="max-width:100%;height:auto" role="img" aria-labelledby="${gid}-t ${gid}-d" data-size="${size}">`,
package/dist/road.js CHANGED
@@ -44,7 +44,7 @@ export function roadToFinal(input, options = {}) {
44
44
  const merged = { ...options, ...extra };
45
45
  check(merged);
46
46
  const m = readModel(input, merged);
47
- const prefix = merged.id ?? `tb-${hash(JSON.stringify([m.voice.locale.code, m.finalists.map((t) => t.name), [...m.path[0].values()].map((v) => [v.round, v.mine, v.theirs]), [...m.path[1].values()].map((v) => [v.round, v.mine, v.theirs]), m.rounds, merged.size, merged.theme, merged.title, merged.subtitle, merged.final ?? null, merged.endnote, merged.winner, merged.codes, merged.markShape]))}`;
47
+ const prefix = merged.id ?? `tb-${hash(JSON.stringify([m.voice.locale.code, m.finalists.map((t) => t.name), [...m.path[0].values()].map((v) => [v.round, v.mine, v.theirs]), [...m.path[1].values()].map((v) => [v.round, v.mine, v.theirs]), m.rounds, merged.size, merged.theme, merged.title, merged.subtitle, merged.final ?? null, merged.endnote, merged.winner, merged.codes, merged.markShape, ...(typeof merged.background === "string" || typeof merged.background === "object" ? [merged.background] : [])]))}`;
48
48
  return drawRoad(m, merged, prefix);
49
49
  };
50
50
  return {
@@ -49,7 +49,7 @@ export interface RoadFinal {
49
49
  /** The sizes of the road chart are the bracket's: `card` (the default here), `square`, `wide` (the finalists at the two ends) and `phone`. */
50
50
  export type RoadSize = SizeName;
51
51
  /** What `roadToFinal` takes besides the finalists or the tournament. */
52
- export interface RoadOptions extends Pick<DrawOptions, "locale" | "words" | "teamName" | "roundNames" | "markShape" | "flags" | "theme" | "background" | "id" | "className"> {
52
+ export interface RoadOptions extends Pick<DrawOptions, "locale" | "words" | "teamName" | "roundNames" | "markShape" | "flags" | "flagHref" | "theme" | "background" | "id" | "className"> {
53
53
  /** With a tournament: the finalist whose road is on the left (by team id); the other finalist is its opponent in the final. Left out, the first side of the final. */
54
54
  team?: SideId;
55
55
  /** The tournament, set large at the top. The language's "Road to the final" when missing. */
package/dist/seeding.js CHANGED
@@ -80,10 +80,12 @@ export function advanceWinners(matchups) {
80
80
  m.feeds.forEach((feed, k) => {
81
81
  if (feed == null)
82
82
  return;
83
- const child = at.get(feed);
83
+ const child = at.get(typeof feed === "string" ? feed : feed.tie);
84
84
  if (!child || child.winner === null)
85
85
  return;
86
- const team = child.sides[m.third ? (1 - child.winner) : child.winner];
86
+ // The loser goes on from a third-place match's feeders, and from a feed that asks for the loser (a play-in's).
87
+ const takesLoser = m.third || (typeof feed !== "string" && feed.result === "loser");
88
+ const team = child.sides[takesLoser ? (1 - child.winner) : child.winner];
87
89
  const here = sides[k];
88
90
  if (team && (here !== null && typeof here === "object" ? here.id : here) !== team.id) {
89
91
  sides[k] = team.id;
package/dist/table.js CHANGED
@@ -15,7 +15,7 @@ export function bracketTable(input, options = {}) {
15
15
  const w = v.words;
16
16
  const caption = options.caption ?? fill(layout.rounds === 1 ? w.summaryOne : w.summary, { n: v.number(layout.teams.length), r: v.number(layout.rounds) });
17
17
  const order = layout.ties
18
- .filter((t) => t.round > 0 || layout.robin.includes(t.index))
18
+ .filter((t) => t.round > 0 || layout.robin.includes(t.index) || layout.playIn.includes(t.index))
19
19
  .sort((a, b) => Number(a.third) - Number(b.third) || a.round - b.round || (a.date < b.date ? -1 : a.date > b.date ? 1 : a.index - b.index));
20
20
  const dated = order.some((t) => v.date(t.date) !== null);
21
21
  const name = (t, k) => {
@@ -24,10 +24,13 @@ export function bracketTable(input, options = {}) {
24
24
  return t.winner === k ? `<strong>${text}</strong>` : text;
25
25
  };
26
26
  const score = (t) => (t.result.score ? fill(w.scoreText, { a: v.number(t.result.score[0]), b: v.number(t.result.score[1]) }) : "");
27
+ // Where a side comes from, in words, when the tie says so: "Loser of A v B".
28
+ const named = (i) => fill(w.tieVs, { a: layout.ties[i].sides[0]?.name ?? w.tbd, b: layout.ties[i].sides[1]?.name ?? w.tbd });
29
+ const origins = (t) => (t.from ?? []).flatMap((s) => (s ? [fill(s.result === "loser" ? w.loserOf : w.winnerOf, { name: named(s.tie) })] : []));
27
30
  const rows = order.map((t) => {
28
- const roundText = t.third ? w.thirdPlace : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
31
+ const roundText = t.third ? w.thirdPlace : t.playIn ? w.playIn : t.round === 0 ? "" : roundLabel(layout, t.round, options.roundNames, v);
29
32
  const date = dated ? `<td>${esc(v.date(t.date) ?? "")}</td>` : "";
30
- return `<tr><th scope="row">${esc(roundText)}</th>${date}<td>${name(t, 0)}</td><td>${esc(score(t))}</td><td>${name(t, 1)}</td><td>${esc(noteLong(t, v).join(w.noteJoin))}</td></tr>`;
33
+ return `<tr><th scope="row">${esc(roundText)}</th>${date}<td>${name(t, 0)}</td><td>${esc(score(t))}</td><td>${name(t, 1)}</td><td>${esc([...noteLong(t, v), ...origins(t)].join(w.noteJoin))}</td></tr>`;
31
34
  });
32
35
  return [
33
36
  `<table class="${esc(options.className ?? "tb-table")}" lang="${esc(v.locale.code)}">`,
package/dist/text.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare function esc(text: string | number): string;
7
7
  */
8
8
  export declare function textWidth(text: string, size: number): number;
9
9
  /** The text cut to fit a width, with an ellipsis where it was cut. */
10
- export declare function fit(text: string, size: number, width: number): string;
10
+ export declare function fit(text: string, size: number, width: number, measure?: (text: string, size: number) => number): string;
11
11
  /** A short name for a small box: the initials of a name of several words ("NYR"), else its first three letters; capitals. */
12
12
  export declare function abbreviate(name: string): string;
13
13
  /** A small, stable hash of a string as lowercase base 36, for ids that depend on the content and nothing else. */
package/dist/text.js CHANGED
@@ -29,13 +29,13 @@ export function textWidth(text, size) {
29
29
  return em * size;
30
30
  }
31
31
  /** The text cut to fit a width, with an ellipsis where it was cut. */
32
- export function fit(text, size, width) {
33
- if (textWidth(text, size) <= width)
32
+ export function fit(text, size, width, measure = textWidth) {
33
+ if (measure(text, size) <= width)
34
34
  return text;
35
35
  const chars = [...text];
36
- while (chars.length > 1 && textWidth(`${chars.join("")}…`, size) > width)
36
+ while (chars.length > 1 && measure(`${chars.join("")}…`, size) > width)
37
37
  chars.pop();
38
- return chars.length <= 1 && textWidth(text, size) > width * 2 ? "…" : `${chars.join("").trimEnd()}…`;
38
+ return chars.length <= 1 && measure(text, size) > width * 2 ? "…" : `${chars.join("").trimEnd()}…`;
39
39
  }
40
40
  /** A short name for a small box: the initials of a name of several words ("NYR"), else its first three letters; capitals. */
41
41
  export function abbreviate(name) {
package/dist/theme.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { BackgroundInk } from "./family-backgrounds.ts";
1
2
  import type { ThemeName } from "./types.ts";
2
3
  /** The colours of one look. Each is the fallback of a CSS custom property of the same name: `--tb-ink` and so on. */
3
4
  export interface Palette {
@@ -31,10 +32,21 @@ export interface StyleParts {
31
32
  links?: boolean;
32
33
  /** A team's path is highlighted: the other ties are quieter and the path is dotted. */
33
34
  follow?: boolean;
35
+ /** A background with a light and a dark picture in one drawing: the one that does not fit the look is hidden. */
36
+ bgAuto?: boolean;
37
+ /** The rules of the layouts of their own (slots, shields, conference colours, header tiles, arrows, group cards). */
38
+ layout?: boolean;
39
+ }
40
+ /** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
41
+ export declare const CONFERENCE_COLORS: readonly [string, string];
42
+ /** The colours a background says to draw in (`backgroundSvg`'s `ink`): what `styleFor` takes in place of a look's own palette. */
43
+ export interface BackgroundInks {
44
+ light?: BackgroundInk;
45
+ dark?: BackgroundInk;
34
46
  }
35
47
  /**
36
48
  * The `<style>` text of a drawing. `auto` is the light look and, under `prefers-color-scheme: dark` or a page that says
37
49
  * `data-theme="dark"` on `<html>`, the dark one; `light`, `dark` and `poster` are one look always. Every colour can be
38
50
  * set from the page with a `--tb-*` custom property. `parts` adds the rules the road look, the road chart, links and a highlighted path need.
39
51
  */
40
- export declare function styleFor(scope: string, theme: Exclude<ThemeName, "none">, parts?: StyleParts): string;
52
+ export declare function styleFor(scope: string, theme: Exclude<ThemeName, "none">, parts?: StyleParts, inks?: BackgroundInks): string;
package/dist/theme.js CHANGED
@@ -10,8 +10,17 @@ export const ROAD_THEMES = Object.freeze({
10
10
  poster: { ring: "#f6f1e3", edge: "rgba(0,0,0,.5)", panel: "#1b2766", spine: "#d9b24c" },
11
11
  });
12
12
  const FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Hiragino Sans", "Yu Gothic UI", "Noto Sans CJK JP", Meiryo, sans-serif';
13
+ /** The colours of the two halves (conferences) of the layouts that colour them, each with white letters on it: `--tb-conf-a` and `--tb-conf-b`. Both read 5 to 1 or more against white. */
14
+ export const CONFERENCE_COLORS = Object.freeze(["#2457c5", "#b3261e"]);
15
+ /** A palette and a road palette from a background's ink. */
16
+ function fromInk(ink) {
17
+ return [
18
+ { bg: ink.base, ink: ink.text, muted: ink.muted, line: ink.line, box: ink.box, edge: ink.edge, win: ink.accent, gold: ink.accent, plain: ink.plain },
19
+ { ring: ink.ring, edge: ink.dark ? "rgba(0,0,0,.55)" : "rgba(0,0,0,.35)", panel: ink.panel, spine: ink.spine },
20
+ ];
21
+ }
13
22
  /** The rules for one palette, each colour read from its custom property with the palette's value as the fallback. */
14
- function rules(p, r, poster, withFont, parts) {
23
+ function rules(p, r, poster, withFont, parts, side) {
15
24
  const v = (name) => `var(--tb-${name}, ${p[name]})`;
16
25
  const rv = (name, css) => `var(--tb-${css}, ${r[name]})`;
17
26
  const road = parts.road === true || parts.chart === true;
@@ -38,7 +47,7 @@ function rules(p, r, poster, withFont, parts) {
38
47
  [[".tb-halo"], `paint-order:stroke;stroke:${v("bg")};stroke-width:4px;stroke-linejoin:round`],
39
48
  ];
40
49
  if (road) {
41
- list.push([[".tb-edge"], `fill:none;stroke:${rv("edge", "mark-edge")}`], [[".tb-lost .tb-ring-r", ".tb-lost .tb-edge"], "opacity:.55"], [[".tb-panel"], `fill:${rv("panel", "panel")}`], [[".tb-lab"], `fill:${v("ink")};font-weight:700`], [[".tb-spine"], `stroke:${rv("spine", "spine")};fill:none;stroke-linecap:round`]);
50
+ list.push([[".tb-edge"], `fill:none;stroke:${rv("edge", "mark-edge")}`], [[".tb-lost .tb-ring-r", ".tb-lost .tb-edge"], "opacity:.55"], [[".tb-panel"], `fill:${rv("panel", "panel")}`], [[".tb-lab"], `fill:${v("ink")};font-weight:700`], [[".tb-spine"], `stroke:${rv("spine", "spine")};fill:none;stroke-linecap:round`], [[".tb-tagbox"], `fill:${rv("panel", "panel")}`], [[".tb-tag"], `fill:${v("ink")};font-weight:700`], [[".tb-code", ".tb-vs"], `fill:${v("muted")};font-weight:700`]);
42
51
  }
43
52
  if (parts.road === true) {
44
53
  list.push([[".tb-name"], "font-weight:700"], [[".tb-score"], "font-weight:800"], [[".tb-lost .tb-score"], `fill:${v("muted")};opacity:1`], [[".tb-line"], `stroke-linecap:round;stroke-linejoin:round;stroke-width:${poster ? 2 : 1.8};stroke-opacity:.55`]);
@@ -46,12 +55,20 @@ function rules(p, r, poster, withFont, parts) {
46
55
  if (parts.chart === true) {
47
56
  list.push([[".tb-r-t"], `fill:${v("ink")};font-weight:800`], [[".tb-r-s", ".tb-r-e", ".tb-r-sub", ".tb-r-code", ".tb-r-vs", ".tb-r-dash"], `fill:${v("muted")};font-weight:700`], [[".tb-r-name", ".tb-r-n"], `fill:${v("ink")};font-weight:800`], [[".tb-r-n.tb-r-low"], `fill:${v("muted")}`], [[".tb-r-dim"], "opacity:.5"], [["text"], "font-variant-numeric:tabular-nums"]);
48
57
  }
58
+ if (parts.layout === true) {
59
+ const [ca, cb] = CONFERENCE_COLORS;
60
+ list.push([[".tb-slot"], `fill:${v("plain")};fill-opacity:.55;stroke:${v("edge")};stroke-width:1`], [[".tb-won .tb-slot"], `fill-opacity:.9;stroke:${v("win")};stroke-width:1.6`], [[".tb-lost .tb-slot"], "fill-opacity:.35"], [[".tb-emptybar"], `fill:${v("plain")};opacity:.8`], [[".tb-seed"], "font-variant-numeric:tabular-nums"], [[".tb-seedn"], `fill:${v("muted")};opacity:.85`], [[".tb-shield"], `fill:none;stroke:${v("edge")};stroke-width:1.4`], [[".tb-won .tb-shield"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-shield.tb-empty"], `fill:${v("plain")};fill-opacity:.5`], [[".tb-vs", ".tb-code"], `fill:${v("muted")};font-weight:700`], [[".tb-c0 .tb-box"], `stroke:var(--tb-conf-a, ${ca});stroke-width:1.6`], [[".tb-c1 .tb-box"], `stroke:var(--tb-conf-b, ${cb});stroke-width:1.6`], [[".tb-atag"], `fill:${v("ink")}`], [[".tb-atag-t"], `fill:${v("bg")}`], [[".tb-c0 .tb-atag"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-c1 .tb-atag"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-c0 .tb-atag-t", ".tb-c1 .tb-atag-t"], "fill:#fff"], [[".tb-head"], `fill:${rv("panel", "panel")}`], [[".tb-head.tb-c0"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-head.tb-c1"], `fill:var(--tb-conf-b, ${cb})`], [[".tb-head-t"], `fill:${v("ink")};font-weight:800`], [[".tb-head.tb-c0 + .tb-head-t", ".tb-head.tb-c1 + .tb-head-t"], "fill:#fff"], [[".tb-arrow"], `stroke:${v("line")};stroke-width:1.6;fill:none;stroke-linecap:round;stroke-linejoin:round`], [[".tb-arrow-loser"], "stroke-dasharray:5 4"], [[".tb-arrowhead"], `fill:${v("line")};stroke:none`], [[".tb-gcard"], `fill:${v("box")};stroke:${v("edge")};stroke-width:1`], [[".tb-gtile"], `fill:var(--tb-conf-a, ${ca})`], [[".tb-gtile-t"], "fill:#fff;font-weight:800"], [[".tb-ghead"], `fill:${v("muted")};font-weight:700`], [[".tb-gcell"], `fill:${v("ink")};font-variant-numeric:tabular-nums`], [[".tb-gcell-p"], "font-weight:800"], [[".tb-gname"], `fill:${v("ink")};font-weight:700`], [[".tb-qual"], `fill:${v("win")};fill-opacity:.16`], [[".tb-gsep"], `stroke:${v("edge")};stroke-width:1;stroke-opacity:.6`]);
61
+ }
49
62
  if (parts.follow === true) {
50
63
  list.push([[".tb-tie"], "opacity:.4"], [[".tb-tie.tb-on-path"], "opacity:1"], [[".tb-on-path .tb-box"], `stroke:${v("win")};stroke-width:2.2`], [[".tb-line"], "stroke-opacity:.25"], [[".tb-line.tb-path"], `stroke:${v("win")};stroke-width:4.5;stroke-opacity:1;stroke-linecap:round;stroke-dasharray:.1 9`]);
51
64
  }
52
65
  if (parts.links === true) {
53
66
  list.push([[".tb-link"], "cursor:pointer;outline:none"], [[".tb-link:hover .tb-box", ".tb-link:focus-visible .tb-box"], `stroke:${v("win")};stroke-width:2.5`], [[".tb-link:hover .tb-name", ".tb-link:focus-visible .tb-name"], "text-decoration:underline"], [[".tb-link:focus-visible"], `outline:2px solid ${v("win")};outline-offset:2px`]);
54
67
  }
68
+ if (parts.bgAuto === true)
69
+ list.push(side === "dark" ? [[".tb-bgl"], "display:none"] : [[".tb-bgd"], "display:none"]);
70
+ if (parts.bgAuto === true && side === "dark")
71
+ list.push([[".tb-bgd"], "display:inline"]);
55
72
  if (withFont)
56
73
  list.push([["text"], `font-family:var(--tb-font, ${FONT})`]);
57
74
  return list;
@@ -62,16 +79,20 @@ const write = (scope, list, before = "") => list.map(([sel, body]) => `${sel.map
62
79
  * `data-theme="dark"` on `<html>`, the dark one; `light`, `dark` and `poster` are one look always. Every colour can be
63
80
  * set from the page with a `--tb-*` custom property. `parts` adds the rules the road look, the road chart, links and a highlighted path need.
64
81
  */
65
- export function styleFor(scope, theme, parts = {}) {
82
+ export function styleFor(scope, theme, parts = {}, inks = {}) {
83
+ // A look takes the ink of the picture drawn for it; a picture asked for in the other variant (`background.theme`) gives its ink to both.
84
+ const only = inks.light ?? inks.dark;
85
+ const [lp, lr] = (inks.light ?? only) ? fromInk((inks.light ?? only)) : [THEMES.light, ROAD_THEMES.light];
86
+ const [dp, dr] = (inks.dark ?? only) ? fromInk((inks.dark ?? only)) : [THEMES.dark, ROAD_THEMES.dark];
66
87
  if (theme === "light")
67
- return write(scope, rules(THEMES.light, ROAD_THEMES.light, false, true, parts));
88
+ return write(scope, rules(lp, lr, false, true, parts, "light"));
68
89
  if (theme === "dark")
69
- return write(scope, rules(THEMES.dark, ROAD_THEMES.dark, false, true, parts));
90
+ return write(scope, rules(dp, dr, false, true, parts, "dark"));
70
91
  if (theme === "poster")
71
- return write(scope, rules(THEMES.poster, ROAD_THEMES.poster, true, true, parts));
72
- const dark = rules(THEMES.dark, ROAD_THEMES.dark, false, false, parts);
92
+ return write(scope, only ? rules(dp, dr, true, true, parts, "dark") : rules(THEMES.poster, ROAD_THEMES.poster, true, true, parts, "dark"));
93
+ const dark = rules(dp, dr, false, false, parts, "dark");
73
94
  return [
74
- write(scope, rules(THEMES.light, ROAD_THEMES.light, false, true, parts)),
95
+ write(scope, rules(lp, lr, false, true, parts, "light")),
75
96
  `@media (prefers-color-scheme: dark){\n${write(scope, dark, ':root:not([data-theme="light"]) ')}\n}`,
76
97
  write(scope, dark, ':root[data-theme="dark"] '),
77
98
  ].join("\n");
package/dist/types.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface Team {
12
12
  abbr?: string;
13
13
  /** The seed, shown beside the name when `seeds` is on. */
14
14
  seed?: number;
15
+ /** A tag for the road look's seed tags (`C1`, `WC2`, `M3`): written small beside the mark when `seeds` is on, in place of the number. A tie's `seeds` win over it. */
16
+ tag?: string;
15
17
  /**
16
18
  * The team's mark: a logo or a flag. Either the address of an image (`https://`, `data:image/…`, or a relative
17
19
  * path) or inline SVG markup (`<svg …>`; a round flag from `@johnmorrisdotca/hata` goes here as it is).
@@ -54,6 +56,11 @@ export type MatchupKind =
54
56
  | "aggregate"
55
57
  /** A drawn game and the game replayed: the last one decides. */
56
58
  | "replay";
59
+ /** What takes a side of a tie: a tie's id (its winner, as `feeds` always meant) or `{ tie, result }`, which can name the tie's loser as well (a play-in's loser goes on to a decider). */
60
+ export type Feed = string | {
61
+ tie: string;
62
+ result: "winner" | "loser";
63
+ };
57
64
  /**
58
65
  * One tie: two sides and who went through. Everything but `sides` is optional, so that a bracket can be drawn from
59
66
  * what is known (the sides of a first round, a few results) and fill in as the games are played.
@@ -78,7 +85,7 @@ export interface Matchup {
78
85
  * Which ties feed this one: `feeds[0]` the one whose winner takes the first side, `feeds[1]` the second. Names the
79
86
  * tree outright, for a bracket whose sides are not known yet. Left out, a winner goes on to the next tie its team plays.
80
87
  */
81
- feeds?: readonly [string | null, string | null];
88
+ feeds?: readonly [Feed | null, Feed | null];
82
89
  /** How the tie is contested. Left out: replay if a game replays, series if there is a `bestOf` or several games, else game. */
83
90
  kind?: MatchupKind;
84
91
  /** The length of the series (7 for a best-of-seven). */
@@ -99,7 +106,13 @@ export interface Matchup {
99
106
  walkover?: boolean;
100
107
  /** This is the third-place match: it leads nowhere and is drawn apart. */
101
108
  third?: boolean;
109
+ /** `"play-in"`: a game of a play-in (the NBA's) that is outside the rounds of the tree: its winner (and loser, see `feeds`) goes on to a place the tree's ties name. Drawn by the `playin` layout and, apart, under the other layouts' trees. */
110
+ stage?: "play-in";
111
+ /** The side playing at home (0 or 1), for the `at` wording of the `columns` layout: the home side is drawn second, after "AT". Left out, the second side. */
112
+ home?: 0 | 1;
102
113
  note?: string;
114
+ /** The seed tags of the two sides for this tie (`["C1", "WC2"]`), written beside the marks in the road look when `seeds` is on; a side's `Team.tag` is used where this has none. */
115
+ seeds?: readonly [string | number | null, string | number | null];
103
116
  }
104
117
  /** What a tie came to, worked out from its games. */
105
118
  export interface Result {
@@ -134,6 +147,19 @@ export interface Tie {
134
147
  third: boolean;
135
148
  walkover: boolean;
136
149
  note: string | null;
150
+ /** The seed tags given for the tie's sides (`Matchup.seeds`). */
151
+ seeds?: readonly [string | number | null, string | number | null];
152
+ /** Where each side comes from, when the matchup's `feeds` name it: another tie's winner or its loser. Left out for a tie that says nothing of the kind. */
153
+ from?: readonly [Source | null, Source | null];
154
+ /** The tie is a play-in game (`Matchup.stage`). */
155
+ playIn?: boolean;
156
+ /** The side at home (`Matchup.home`), when given. */
157
+ home?: 0 | 1;
158
+ }
159
+ /** Where a side of a tie comes from: the winner or the loser of the tie at `tie` (its index). */
160
+ export interface Source {
161
+ tie: number;
162
+ result: "winner" | "loser";
137
163
  }
138
164
  /** Where the layout put a tie. */
139
165
  export type Place = "l" | "r" | "f" | "o";
@@ -185,6 +211,8 @@ export interface BracketLayout {
185
211
  /** Ties that lead nowhere near the final, and round-robin games. */
186
212
  other: number[];
187
213
  robin: number[];
214
+ /** The play-in games (`Matchup.stage`), kept out of the tree. */
215
+ playIn: number[];
188
216
  /** The third-place match, when there is one. */
189
217
  third: number | null;
190
218
  /** Where each tie is: a half, the final, or elsewhere; by tie index. */
@@ -291,6 +319,29 @@ export interface Words {
291
319
  /** `{name}`. */
292
320
  roadChampion: string;
293
321
  roadToPlay: string;
322
+ /** "vs": the tag between two sides of a tie in the `columns` layout, and between the shields. */
323
+ vs: string;
324
+ /** The word between the two sides of a tie of the `columns` layout that names the home side (`at: true`): "AT". */
325
+ at: string;
326
+ /** `{n}`: a group's name, "Group A" (`n` is the letter). */
327
+ groupName: string;
328
+ /** The heads of a group table: played, won, drawn, lost, goal difference, points. */
329
+ colPlayed: string;
330
+ colWon: string;
331
+ colDrawn: string;
332
+ colLost: string;
333
+ colDiff: string;
334
+ colPoints: string;
335
+ /** The heading over a bracket's play-in games. */
336
+ playIn: string;
337
+ /** `{a}`, `{b}`: the seeds of the two games a box takes the winner (or the loser) of: "Winner 9/10", "Loser 7/8". */
338
+ playInWinner: string;
339
+ playInLoser: string;
340
+ /** `{n}`: a seed's place, "Seed 7". */
341
+ seedSlot: string;
342
+ /** `{name}`: a side that is the winner or loser of another tie, in a sentence or a table: "Loser of {name}". */
343
+ winnerOf: string;
344
+ loserOf: string;
294
345
  }
295
346
  /** A language: its words, the names of the rounds (by their English names), and the tag `Intl` formats numbers and dates for. */
296
347
  export interface Locale {
@@ -343,7 +394,50 @@ export interface EmblemRequest {
343
394
  }
344
395
  /** A function from an emblem id and a request to an SVG string; `renderEmblem` of `@johnmorrisdotca/dougu-sports/emblems` is one. */
345
396
  export type EmblemRenderer = (id: string, options: EmblemRequest) => string;
397
+ /** A background preset's name. */
398
+ export type { BackgroundPresetName } from "./family-backgrounds.ts";
399
+ import type { BackgroundPresetName } from "./family-backgrounds.ts";
400
+ /** A preset, tuned: your own colours (hex or `rgb()`, the first two the ground), the gradient's angle, how strong the pattern is (0 to 1, default 0.6), a seed for the scattered parts, and which of its two variants. */
401
+ export interface BackgroundOptions {
402
+ /** Default `midnight`. */
403
+ preset?: BackgroundPresetName;
404
+ colors?: readonly string[];
405
+ angle?: number;
406
+ intensity?: number;
407
+ seed?: number;
408
+ /** `light` or `dark`: the variant to draw. By default the one of the drawing's look. */
409
+ theme?: "light" | "dark";
410
+ }
411
+ /** Your own picture as the background: an image address (http(s), relative or `data:image/…`; anything else is refused), laid under a veil so the text reads. */
412
+ export interface BackgroundImageOptions {
413
+ image: string;
414
+ /** `cover` (the default) fills the drawing; `contain` shows all of the picture. */
415
+ fit?: "cover" | "contain";
416
+ /** How dark (or, in a light look, how pale) the veil is, 0 to 1. Default 0.5. */
417
+ dim?: number;
418
+ theme?: "light" | "dark";
419
+ }
420
+ /** How the ties are drawn: `tree` (the default) is the bracket as ever; the others are other ways to draw the same ties. */
421
+ export type LayoutName = "tree" | "rows" | "shields" | "update" | "columns" | "playin" | "series" | "groups";
346
422
  export interface DrawOptions extends LayoutOptions {
423
+ /**
424
+ * `tree` (the default): the halves meeting at the final. `rows`: each team a row, its name on the left and its mark on the right, a slot for the result toward the centre. `shields`: marks in shields with "vs",
425
+ * the matchups on both sides of the trophy. `update`: a compact score-update card. `columns`: tall slots that grow toward the centre, the conferences in two colours, "VS" (or "AT") on a tag, big seed numbers.
426
+ * `playin`: the play-in games with their winners and losers going on. `series`: a tall card, one line a series ("BOS 4-0 MIA"), the first half above and the second below. `groups`: the group-stage tables.
427
+ */
428
+ layout?: LayoutName;
429
+ /** Big title tiles (the halves' groups: West and East, AFC and NFC) over each half, in any layout: each half's `group` name in the colour of that half. Default false. */
430
+ headers?: boolean;
431
+ /** In the `columns` layout, write "AT" (the word `at` of the language) in place of "VS" between the two sides of a tie: the first side is the visitor and the second the home side (`Matchup.home` says otherwise). Default false. */
432
+ at?: boolean;
433
+ /** In the `groups` layout, the columns of a group's table: `points` (played, won, drawn, lost, goal difference, points) or `record` (won and lost, softball's). Default `points`. */
434
+ standings?: "points" | "record";
435
+ /** In the `groups` layout, how many teams of each group go through: their rows are marked. Default 0, none. */
436
+ qualify?: number;
437
+ /** Write the `<style>` into the drawing. Default true; false leaves it out, for a second drawing of a page that shares the first's (`styleScope`, `bracketStyle`). */
438
+ style?: boolean;
439
+ /** A class that carries this drawing's rules in place of its own: two drawings with the same `styleScope` take their colours from one `<style>`, and the second can leave it out (`style: false`). A word of letters, digits, `-` and `_`. */
440
+ styleScope?: string;
347
441
  /** `wide` 1200 by 675, `square` 1080 by 1080, `card` 1080 by 1350 (a tall post), `phone` 390 wide and as tall as it needs; or a size of your own. Default `wide`. */
348
442
  size?: SizeName | {
349
443
  width: number;
@@ -354,8 +448,16 @@ export interface DrawOptions extends LayoutOptions {
354
448
  detail?: Detail;
355
449
  /** `auto` follows the page's light or dark; `poster` is dark with gold lines; `none` writes no style, for your own CSS. Default `auto`. */
356
450
  theme?: ThemeName;
357
- /** Draw a background. Default true; false for a transparent drawing. */
358
- background?: boolean;
451
+ /**
452
+ * The background. `true` (the default) is the look's plain colour and `false` a transparent drawing. A preset's name (`"midnight"`, `"ice"`, see `BACKGROUND_PRESETS`) draws that
453
+ * poster background in the look's light or dark variant (`auto` draws both and shows the one that fits the page), and the text, lines, boxes and marks take the colours that
454
+ * read over it. An object tunes a preset (`{ preset, colors, angle, intensity, seed }`) or draws your own picture (`{ image, fit, dim }`).
455
+ */
456
+ background?: boolean | BackgroundPresetName | BackgroundOptions | BackgroundImageOptions;
457
+ /** In the road look, write a small `vs` between the two marks of a box of marks alone. Default false. */
458
+ vs?: boolean;
459
+ /** In the road look, write each team's three-letter code under its mark in a box of marks alone. Default false for a bracket (a chart's `codes` is on by default). */
460
+ codes?: boolean;
359
461
  title?: string;
360
462
  subtitle?: string;
361
463
  /** Names of the rounds, first round first (or just the last ones). Default: Final, Semi-finals, Quarter-finals, Round of 16… */
@@ -368,7 +470,7 @@ export interface DrawOptions extends LayoutOptions {
368
470
  roundLabels?: boolean;
369
471
  /** Show the scores. Default true. */
370
472
  scores?: boolean;
371
- /** Show seeds before names. Default false. */
473
+ /** Show seeds: the number before the name in the classic look, and in the road look a small tag (`C1`, `WC2`: `Matchup.seeds`, else `Team.tag`, else `Team.seed`) beside each mark. Default false. */
372
474
  seeds?: boolean;
373
475
  /** `round` (the default), `square`, or `none` (names only). */
374
476
  markShape?: "round" | "square" | "none";
@@ -394,6 +496,16 @@ export interface DrawOptions extends LayoutOptions {
394
496
  * made from `@johnmorrisdotca/hata`. With none, or when it returns nothing for a code, the team's `mark` or its initials are drawn.
395
497
  */
396
498
  flags?: FlagRenderer;
499
+ /**
500
+ * The address of the flag a team's `flag` code names, for a flag that is a picture to load (a file on your own server) and not SVG to inline, which can be 80 kB for one flag. Tried where `flags` gives
501
+ * none; the address must be http(s), relative or `data:image/…`, and is written once as a symbol however many times the flag appears.
502
+ */
503
+ flagHref?: (code: string) => string | null | undefined;
504
+ /**
505
+ * A class for each tie's group, from the tie: `(tie) => tie.sides.some(inDivision) ? "mine" : "other"`. A word of letters, digits, `-` and `_` is added to the group's classes (anything else is left out),
506
+ * so that a page's own style can highlight or dim ties: `.mine .tb-box { stroke: … }`. `undefined` or an empty text adds none.
507
+ */
508
+ tieClass?: (tie: Tie) => string | null | undefined;
397
509
  /**
398
510
  * `classic` (the default) or `road`: the look of the road-to-the-final chart, with large round marks, big bold scores, the loser quiet, the
399
511
  * rounds named on pills and a dotted path. Every other option works in both.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.4.0";
1
+ export declare const VERSION = "0.6.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.4.0";
1
+ export const VERSION = "0.6.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/taisen-brackets",
3
- "version": "0.4.0",
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, 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.",
3
+ "version": "0.6.0",
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",
7
7
  "tournament",
@@ -30,7 +30,9 @@
30
30
  "zero-dependencies",
31
31
  "browser",
32
32
  "node",
33
- "itsutsu"
33
+ "itsutsu",
34
+ "play-in",
35
+ "group-stage"
34
36
  ],
35
37
  "homepage": "https://johnmorrisdotca.github.io/taisen-brackets/",
36
38
  "repository": {
@@ -67,6 +69,11 @@
67
69
  "types": "./dist/road-entry.d.ts",
68
70
  "import": "./dist/road-entry.js",
69
71
  "default": "./dist/road-entry.js"
72
+ },
73
+ "./backgrounds": {
74
+ "types": "./dist/backgrounds-entry.d.ts",
75
+ "import": "./dist/backgrounds-entry.js",
76
+ "default": "./dist/backgrounds-entry.js"
70
77
  }
71
78
  },
72
79
  "files": [
@@ -93,6 +100,7 @@
93
100
  "flags": "node scripts/make-flags.mjs",
94
101
  "site": "pnpm build && node scripts/site.mjs",
95
102
  "test:demo": "pnpm site && playwright test",
103
+ "images": "pnpm site && node scripts/screenshots.mjs",
96
104
  "screenshots": "pnpm site && node scripts/screenshots.mjs"
97
105
  },
98
106
  "peerDependencies": {