@johnmorrisdotca/kyogijo-stadiums 0.18.1 → 0.19.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.
@@ -1,58 +1,24 @@
1
1
  // A judging assignment drawn: for each mat, the package's own judo area with a badge at every judge's seat and, where a name and its ID fit in the seat, a card with both; beside it (or under it) the chart of mat, seat,
2
2
  // name and ID, whose rows grow to hold every name and ID whole. The surface is not drawn a second way: its markup is the drawing's own, moved and clipped. The same options always give the same string. The text
3
3
  // alternative is `judgingTable`.
4
- import { drawKyogijo } from "./draw.js";
5
- import { safeClass } from "./common.js";
4
+ export { judgingText } from "./judging.text.js";
5
+ import { metaMarkup, safeClass } from "./common.js";
6
6
  import { el, esc, hash, num } from "./format.js";
7
- import { PLAN_CARD, badge, card } from "./judging.card.js";
8
- import { fitChart, fitSeats } from "./judging.fit.js";
9
- import { JUDGING_LEAGUE } from "./judging.data.js";
10
- import { judgingPlan, judgingSetting, seatIn } from "./judging.js";
7
+ import { broadcastDrawing } from "./judging.broadcast.js";
8
+ import { fitChart } from "./judging.fit.js";
9
+ import { metricsOf, planMarkup, surfacesOf } from "./judging.plan.js";
10
+ import { judgingPlan, judgingSetting } from "./judging.js";
11
+ import { judgingText } from "./judging.text.js";
11
12
  import { KYOGIJO_JUDGING_STYLE } from "./judging.style.js";
12
13
  import { JUDGING_WORDS } from "./judging.words.js";
13
14
  import { fill } from "./strings.js";
14
15
  import { LINE_HEIGHT } from "./text.js";
15
16
  import { VERSION } from "./version.js";
16
- /** Feet in a metre: the surface's own drawing is in feet. */
17
- const FT = 1 / 0.3048;
18
- const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
19
- /** One mat's surface as markup: the drawing's own, without its title, its stylesheet and its apron. */
20
- function inner(svg) {
21
- const look = /^<svg[^>]* data-look="([^"]*)"/.exec(svg)?.[1];
22
- const style = /<style>([\s\S]*?)<\/style>/.exec(svg)?.[1] ?? "";
23
- let body = svg.replace(/<style>[\s\S]*?<\/style>/, "");
24
- body = body.slice(body.indexOf("</desc>") + "</desc>".length, body.lastIndexOf("</svg>"));
25
- body = body.replace(/<rect class="ky-apron"[^>]*\/>/g, "");
26
- return { body, style, look };
27
- }
28
- /** The sentence that names one seat and who sits in it. */
29
- function seatSentence(a, setting) {
30
- const w = JUDGING_WORDS[setting.language].say;
31
- const seat = a.seat.label[setting.language];
32
- if (!a.assigned)
33
- return fill(w.seatVacant, { seat });
34
- if (a.id === "")
35
- return fill(w.seatNoId, { seat, name: a.name });
36
- return fill(w.seat, { seat, name: a.name === "" ? "—" : a.name, id: a.id });
37
- }
38
- /** The title and the description of an assignment: each mat with each seat and who is in it. */
39
- export function judgingText(setting, options, chart) {
40
- const words = JUDGING_WORDS[setting.language];
41
- const style = fill(words.styleName[setting.style], { count: setting.mats[0].seats.length });
42
- const mats = setting.total === 1 ? words.say.matsOne : fill(words.say.matsMany, { n: setting.total });
43
- const title = options.title ?? fill(words.say.title, { style, mats });
44
- const sentences = setting.mats.map((m) => fill(words.say.mat, { label: m.label, seats: m.seats.map((a) => seatSentence(a, setting)).join(words.say.join) }));
45
- const description = options.description ?? [...sentences, chart && setting.seatNames ? words.say.names : words.say.chartOnly].join(setting.language === "ja" ? "" : " ");
46
- return { title, description };
47
- }
48
- const metricsOf = (width) => {
49
- const k = width / 1000;
50
- return { k, pad: 28 * k, gap: 28 * k, titleH: 62 * k, panelGap: 40 * k, badge: 60 * k, rowPad: 14 * k, role: 17 * k, nameMax: 34 * k, nameMin: 22 * k, cardMin: 12 * k };
51
- };
52
17
  /** The rows of one mat's chart, each as tall as its name and ID need, set in a chart `width` wide. */
53
18
  function chartRows(mat, width, m, setting) {
54
19
  const words = JUDGING_WORDS[setting.language];
55
- const textW = width - m.rowPad * 1.6 - m.badge - m.gap * 0.6;
20
+ // `textWidth` is an estimate, and a face wider than the one it estimates (the DejaVu and Verdana of a Linux or Windows screen) would put a line across the row's edge: a margin of 6 % keeps every line inside it.
21
+ const textW = (width - m.rowPad * 1.6 - m.badge - m.gap * 0.6) * 0.94;
56
22
  return mat.seats.map((a) => {
57
23
  if (!a.assigned)
58
24
  return { a, fit: undefined, height: Math.max(m.badge + 2 * m.rowPad, m.rowPad * 2 + m.role * LINE_HEIGHT + 6 * m.k + m.nameMin * LINE_HEIGHT) };
@@ -100,76 +66,20 @@ function chartMarkup(mat, x, y, width, m, setting) {
100
66
  const height = Math.max(0, cursor - gap);
101
67
  return { markup: el("g", { class: "ky-ju-chart", "data-mat": mat.n, transform: `translate(${num(x)} ${num(y)})` }, out), height };
102
68
  }
103
- /** The plan of one mat at (x, y) in a box: the surface moved and clipped to the region shown, with a badge or a card at every seat. */
104
- function planMarkup(mat, surface, geom, region, box, m, setting, clip) {
105
- const rw = region.x1 - region.x0;
106
- const rh = region.y1 - region.y0;
107
- const s = Math.min(box.w / rw, box.h / rh);
108
- const w = rw * s;
109
- const h = rh * s;
110
- const x = box.x + (box.w - w) / 2;
111
- const y = box.y;
112
- const sf = s / FT;
113
- const X = (mx) => x + (mx - region.x0) * s;
114
- const Y = (my) => y + (my - region.y0) * s;
115
- const words = JUDGING_WORDS[setting.language];
116
- let markup = el("clipPath", { id: clip }, el("rect", { x: num(region.x0 * FT), y: num(region.y0 * FT), width: num(rw * FT), height: num(rh * FT) }));
117
- markup += el("rect", { class: "ky-apron", x, y, width: w, height: h });
118
- markup += el("g", { class: "ky-ju-plan", "clip-path": `url(#${clip})`, transform: `translate(${num(x - region.x0 * FT * sf)} ${num(y - region.y0 * FT * sf)}) scale(${(sf).toFixed(5)})` }, surface.body);
119
- markup += el("rect", { class: "ky-ju-plan-frame", x, y, width: w, height: h });
120
- // The card of a seat is the place the plan gives it: for a kata judge its chair and the table in front of it, for a contest's referee and side judges a card of their own.
121
- const boxes = mat.seats.map((a) => {
122
- const c = geom.places[a.seat.n - 1].card;
123
- const cw = (c.x1 - c.x0) * s;
124
- const ch = (c.y1 - c.y0) * s;
125
- const pad = Math.max(2 * m.k, cw * 0.05);
126
- const r = clamp(cw * 0.2, 6 * m.k, 14 * m.k);
127
- return { cw, ch, pad, r, width: cw - 2 * pad, height: ch - 2 * pad - r, max: Math.min(cw * 0.3, 26 * m.k) };
128
- });
129
- // The names are on the seats only when asked for; otherwise every seat is its number, and the chart is the key.
130
- const fitted = setting.seatNames ? fitSeats(mat.seats, boxes, { min: m.cardMin, idLabel: words.chart.idLabel }) : mat.seats.map(() => undefined);
131
- // A seat outside the picture (the referee in a close-up of the bench) has its name in the chart only: it does not fit.
132
- const shown = mat.seats.map((a) => seatIn(geom.places[a.seat.n - 1], region));
133
- const fits = mat.seats.map((a, i) => !a.assigned || (shown[i] && fitted[i] !== undefined));
134
- mat.seats.forEach((a, i) => {
135
- if (!shown[i])
136
- return;
137
- const place = geom.places[a.seat.n - 1];
138
- const b = boxes[i];
139
- const fit = fitted[i];
140
- let g = "";
141
- if (fit !== undefined)
142
- g += card(PLAN_CARD, fit, a.seat.n, X(place.card.x0), Y(place.card.y0), b.cw, b.ch, b.pad, b.r, 3 * m.k);
143
- else {
144
- const br = clamp(Math.min(b.cw, 1.25 * s) * 0.42, 10 * m.k, 30 * m.k);
145
- g += badge(PLAN_CARD, X((place.chair.x0 + place.chair.x1) / 2), Y((place.chair.y0 + place.chair.y1) / 2), br, a.seat.n, a.assigned);
146
- }
147
- markup += el("g", { class: "ky-ju-seat", "data-mat": mat.n, "data-seat": a.seat.id, "data-card": fit === undefined ? "no" : "yes" }, g);
148
- });
149
- return { markup, width: w, height: h, fits };
150
- }
151
69
  /** The picture of an assignment and what was worked out to make it. `mode` `chart` is the chart alone (for beside a 3D view). */
152
70
  export function judgingDrawing(options, mode = "full") {
153
71
  const setting = judgingSetting(mode === "chart" && options.width === undefined ? { ...options, width: 600 } : options);
72
+ if (setting.view === "broadcast" && mode === "full")
73
+ return broadcastDrawing(options, setting);
154
74
  const language = setting.language;
155
75
  const width = setting.width;
156
76
  const m = metricsOf(width);
157
77
  const geom = judgingPlan(setting.style);
158
78
  const region = setting.focus === "bench" ? geom.bench : { x0: geom.extent.x0 - 0.3, y0: geom.extent.y0 - 0.3, x1: geom.extent.x1 + 0.3, y1: geom.extent.y1 + 0.3 };
159
79
  const gid = safeClass(options.id).replace(/ /g, "-") || `ky-ju-${hash(JSON.stringify([options, mode, VERSION]))}`;
160
- const league = JUDGING_LEAGUE[setting.style].league;
161
80
  const portrait = setting.orientation === "portrait";
162
81
  // The surface, drawn by the package: once for its stylesheet, then once for each mat (its ids differ).
163
- let style = "";
164
- let look;
165
- const surfaces = mode === "chart" ? [] : setting.mats.map((mat, i) => {
166
- const drawn = inner(drawKyogijo({ sport: "judo", league, budo: { style: setting.style, officials: true }, language, id: `${gid}-s${mat.n}`, style: i === 0 && options.css !== "none" ? "embed" : "none" }));
167
- if (i === 0) {
168
- style = drawn.style;
169
- look = drawn.look;
170
- }
171
- return drawn;
172
- });
82
+ const { surfaces, style, look } = surfacesOf(setting, options, gid, mode !== "chart");
173
83
  // Whether the chart is drawn: asked for, or every name fits in its seat when the plan has the room that the chart would take.
174
84
  const planBox = (withChart) => {
175
85
  if (portrait)
@@ -238,9 +148,9 @@ export function judgingDrawing(options, mode = "full") {
238
148
  "data-chart": chart ? "yes" : "no",
239
149
  "data-theme": options.theme === "light" || options.theme === "dark" ? options.theme : undefined,
240
150
  "data-version": VERSION,
241
- }, `<title id="${titleId}">${esc(title)}</title><desc id="${descId}">${esc(description)}</desc>` + css + el("rect", { class: "ky-ju-ground", x: 0, y: 0, width, height }) + body);
151
+ }, metaMarkup(titleId, title, descId, description) + css + el("rect", { class: "ky-ju-ground", x: 0, y: 0, width, height }) + body);
242
152
  void cardsFit;
243
- return { svg: root, width, height, chart, cards, title, description };
153
+ return { svg: root, width, height, chart, cards, title, description, boards: [] };
244
154
  }
245
155
  /**
246
156
  * A judging assignment as one SVG string: for each mat, the judo area with a badge at each judge's seat and, where the name and ID fit in the seat, a card with both, and beside it the chart of seat, name and ID, whose rows
package/dist/judging.js CHANGED
@@ -5,7 +5,7 @@ import { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_SEAT_WORDS, JU
5
5
  import { JUDGING_WORDS } from "./judging.words.js";
6
6
  import { fill } from "./strings.js";
7
7
  /** The longest name, ID and mat label that are taken, in characters. */
8
- export const JUDGING_LIMITS = Object.freeze({ name: 120, id: 64, label: 40 });
8
+ export const JUDGING_LIMITS = Object.freeze({ name: 120, id: 64, label: 40, line: 60 });
9
9
  /** Text made safe to show: control characters become spaces, runs of spaces one, the ends trimmed. */
10
10
  const clean = (text) => text.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
11
11
  /** The seats of a style: every one, or without the optional ones (the side judges of a contest). @throws TypeError for a style that is not one. */
@@ -42,6 +42,17 @@ export function matLabel(mat, n, language) {
42
42
  throw new RangeError(`A mat's label is at most ${JUDGING_LIMITS.label} characters`);
43
43
  return label === "" ? fill(JUDGING_WORDS[language].mat, { n }) : label;
44
44
  }
45
+ /** The line of a mat's broadcast board: the caller's, held to its length, or nothing. */
46
+ export function matLine(mat) {
47
+ if (mat.line === undefined)
48
+ return "";
49
+ if (typeof mat.line !== "string")
50
+ throw new TypeError("a mat's line is text");
51
+ const line = clean(mat.line);
52
+ if ([...line].length > JUDGING_LIMITS.line)
53
+ throw new RangeError(`A mat's line is at most ${JUDGING_LIMITS.line} characters`);
54
+ return line;
55
+ }
45
56
  const oneOf = (value, allowed, fallback, label) => {
46
57
  if (value === undefined)
47
58
  return fallback;
@@ -89,7 +100,7 @@ export function judgingSetting(options) {
89
100
  throw new TypeError("seatNames is true or false");
90
101
  // The seats that are drawn: every one that is not optional, and an optional one when asked for or when a mat names it.
91
102
  const shown = seats.filter((seat) => seat.optional !== true || options.sideJudges === true || given.some((m) => m.has(seat.id)));
92
- const all = options.mats.map((mat, i) => ({ n: i + 1, label: matLabel(mat, i + 1, language), seats: shown.map((seat) => given[i].get(seat.id) ?? { seat, name: "", id: "", assigned: false }) }));
103
+ const all = options.mats.map((mat, i) => ({ n: i + 1, label: matLabel(mat, i + 1, language), line: matLine(mat), seats: shown.map((seat) => given[i].get(seat.id) ?? { seat, name: "", id: "", assigned: false }) }));
93
104
  let mats = all;
94
105
  if (options.mat !== undefined) {
95
106
  if (!Number.isInteger(options.mat) || options.mat < 1 || options.mat > all.length)
@@ -105,6 +116,7 @@ export function judgingSetting(options) {
105
116
  mats,
106
117
  total: all.length,
107
118
  chart: oneOf(options.chart, ["auto", "always", "never"], "auto", "chart"),
119
+ view: oneOf(options.view, ["plan", "broadcast"], "plan", "view"),
108
120
  orientation: oneOf(options.orientation, ["landscape", "portrait"], "landscape", "orientation"),
109
121
  focus: oneOf(options.focus, ["mat", "bench"], "mat", "focus"),
110
122
  width,
@@ -0,0 +1,16 @@
1
+ import type { JudgingOptions } from "./judging.types.ts";
2
+ import type { KyogijoTurn } from "./turn.dom.ts";
3
+ import type { TurnSettings } from "./turn.model.ts";
4
+ /** The stylesheet of the broadcast view's layout: the view 16:9, the board over its top right corner (a share of the view's width, so it grows and shrinks with it), under it where the view is narrower than 640 px. */
5
+ export declare const KYOGIJO_JUDGING_BROADCAST_STYLE: string;
6
+ /**
7
+ * Mount an assignment as the broadcast view: the 3D view of the mat, 16:9, with the board of the officials over it at its side. It returns the same handle as every 3D view (`preset`, `set`, `update`, `play`, `destroy`):
8
+ * `update` takes the next options and redraws both the view and the board. The options are `JudgingOptions`; `view` is always `broadcast` here. Pass `{ preset: "end" }` to open on another viewpoint.
9
+ * @example
10
+ * ```ts no-run
11
+ * import { mountJudgingBroadcast } from "@johnmorrisdotca/kyogijo-stadiums/judging";
12
+ * const view = mountJudgingBroadcast(document.querySelector("#mat")!, { style: "kata", mat: 1, mats: [{ label: "Mat 1", line: "Round 2", officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] });
13
+ * view.preset("corner");
14
+ * ```
15
+ */
16
+ export declare function mountJudgingBroadcast(host: HTMLElement, options: JudgingOptions, settings?: TurnSettings): KyogijoTurn<JudgingOptions>;
@@ -0,0 +1,69 @@
1
+ // The broadcast view in a page: the 3D view of the mat (the Broadcast camera by default, with every control the other 3D views have) in a 16:9 box, and over it, at its side, the board of the officials like a television
2
+ // graphic. The board is the same drawing as the flat broadcast picture's (`judging.board.ts`), laid over the view by the page's own layout, so it scales evenly with the view and never takes the pointer: a drag on it
3
+ // turns the mat. On a narrow screen the board has no room beside the mat without covering it, so it stands under the view instead. Where the browser cannot turn the view, the flat broadcast picture, which carries its
4
+ // own board, is shown alone.
5
+ import { judgingBoard } from "./judging.board.js";
6
+ import { judgingSetting } from "./judging.js";
7
+ import { mountJudgingTurn } from "./judging.turn.js";
8
+ /** The stylesheet of the broadcast view's layout: the view 16:9, the board over its top right corner (a share of the view's width, so it grows and shrinks with it), under it where the view is narrower than 640 px. */
9
+ export const KYOGIJO_JUDGING_BROADCAST_STYLE = `
10
+ .ky-ju-bc{position:relative;container-type:inline-size;width:100%}
11
+ .ky-ju-bc{--ky-ju-bc-bg:#dcd8c9}
12
+ @media (prefers-color-scheme:dark){html:not([data-theme=light]) .ky-ju-bc{--ky-ju-bc-bg:#1b1f1c}}
13
+ html[data-theme=dark] .ky-ju-bc{--ky-ju-bc-bg:#1b1f1c}
14
+ .ky-ju-bc .ky-turn[data-theme=light] .ky-turn-view{background:#dcd8c9}
15
+ .ky-ju-bc .ky-turn[data-theme=dark] .ky-turn-view{background:#1b1f1c}
16
+ .ky-ju-bc .ky-turn .ky-turn-view{--ky-turn-ratio:16/9;min-height:0;background:var(--ky-ju-bc-bg);border-radius:12px}
17
+ .ky-ju-bc-board{position:absolute;z-index:40;top:3cqw;right:2cqw;width:30cqw;pointer-events:none}
18
+ .ky-ju-bc-board svg{display:block;width:100%;height:auto}
19
+ .ky-ju-bc-board[hidden]{display:none}
20
+ @container (max-width:639px){.ky-ju-bc-board{position:static;width:100%;max-width:480px;margin:8px auto 0}}
21
+ `.trim();
22
+ /**
23
+ * Mount an assignment as the broadcast view: the 3D view of the mat, 16:9, with the board of the officials over it at its side. It returns the same handle as every 3D view (`preset`, `set`, `update`, `play`, `destroy`):
24
+ * `update` takes the next options and redraws both the view and the board. The options are `JudgingOptions`; `view` is always `broadcast` here. Pass `{ preset: "end" }` to open on another viewpoint.
25
+ * @example
26
+ * ```ts no-run
27
+ * import { mountJudgingBroadcast } from "@johnmorrisdotca/kyogijo-stadiums/judging";
28
+ * const view = mountJudgingBroadcast(document.querySelector("#mat")!, { style: "kata", mat: 1, mats: [{ label: "Mat 1", line: "Round 2", officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] });
29
+ * view.preset("corner");
30
+ * ```
31
+ */
32
+ export function mountJudgingBroadcast(host, options, settings = {}) {
33
+ const asked = (o) => ({ ...o, view: "broadcast" });
34
+ // Held to what it may be before anything is written.
35
+ judgingSetting(asked(options));
36
+ host.innerHTML = `<div class="ky-ju-bc"><style>${KYOGIJO_JUDGING_BROADCAST_STYLE}</style><div class="ky-ju-bc-view"></div><div class="ky-ju-bc-board"></div></div>`;
37
+ const viewHost = host.querySelector(".ky-ju-bc-view");
38
+ const boardHost = host.querySelector(".ky-ju-bc-board");
39
+ const turn = mountJudgingTurn(viewHost, asked(options), settings);
40
+ const paint = (o) => {
41
+ boardHost.hidden = turn.mode !== "3d";
42
+ boardHost.innerHTML = turn.mode === "3d" ? judgingBoard(asked(o)) : "";
43
+ };
44
+ paint(options);
45
+ return {
46
+ get mode() {
47
+ return turn.mode;
48
+ },
49
+ get playing() {
50
+ return turn.playing;
51
+ },
52
+ view: () => turn.view(),
53
+ set: (view, glide) => turn.set(view, glide),
54
+ preset: (name) => turn.preset(name),
55
+ reset: () => turn.reset(),
56
+ update: (next) => {
57
+ turn.update(asked(next));
58
+ paint(next);
59
+ },
60
+ play: () => turn.play(),
61
+ pause: () => turn.pause(),
62
+ togglePlay: () => turn.togglePlay(),
63
+ svg: () => turn.svg(),
64
+ destroy: () => {
65
+ turn.destroy();
66
+ host.replaceChildren();
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,52 @@
1
+ import { type JudgingPlan, type JudgingSetting } from "./judging.ts";
2
+ import type { JudgingResolved } from "./judging.types.ts";
3
+ import type { MBox } from "./budo/plan.ts";
4
+ export declare const clamp: (v: number, lo: number, hi: number) => number;
5
+ /** One mat's surface as markup: the drawing's own, without its title, its stylesheet and its apron. */
6
+ export declare function inner(svg: string): {
7
+ body: string;
8
+ style: string;
9
+ look: string | undefined;
10
+ };
11
+ /** The numbers a drawing is laid out in, all in the picture's own units and proportional to its width, so that the picture is the same at any width. */
12
+ export interface Metrics {
13
+ k: number;
14
+ pad: number;
15
+ gap: number;
16
+ titleH: number;
17
+ panelGap: number;
18
+ badge: number;
19
+ rowPad: number;
20
+ role: number;
21
+ nameMax: number;
22
+ nameMin: number;
23
+ cardMin: number;
24
+ }
25
+ export declare const metricsOf: (width: number) => Metrics;
26
+ /** The plan of one mat at (x, y) in a box: the surface moved and clipped to the region shown, with a badge or a card at every seat. */
27
+ export declare function planMarkup(mat: JudgingResolved, surface: {
28
+ body: string;
29
+ }, geom: JudgingPlan, region: MBox, box: {
30
+ x: number;
31
+ y: number;
32
+ w: number;
33
+ h: number;
34
+ }, m: Metrics, setting: JudgingSetting, clip: string, frame?: boolean): {
35
+ markup: string;
36
+ width: number;
37
+ height: number;
38
+ fits: boolean[];
39
+ x: number;
40
+ y: number;
41
+ scale: number;
42
+ };
43
+ /** Each mat's surface, drawn by the package: once for its stylesheet and its look, then once for each mat (its ids differ). `mode` `chart` draws none. */
44
+ export declare function surfacesOf(setting: JudgingSetting, o: {
45
+ css?: "embed" | "none";
46
+ }, gid: string, drawn?: boolean): {
47
+ surfaces: {
48
+ body: string;
49
+ }[];
50
+ style: string;
51
+ look: string | undefined;
52
+ };
@@ -0,0 +1,91 @@
1
+ // The plan of a judging assignment, as the plan view and the broadcast view both draw it: the package's own judo area moved and clipped to the part that is shown, with a badge (or a name card) at every seat, and the
2
+ // numbers a picture is laid out in. Kept apart from `judging.draw.ts` so that the two views share it.
3
+ import { drawKyogijo } from "./draw.js";
4
+ import { el, num } from "./format.js";
5
+ import { PLAN_CARD, badge, card } from "./judging.card.js";
6
+ import { fitSeats } from "./judging.fit.js";
7
+ import { JUDGING_LEAGUE } from "./judging.data.js";
8
+ import { seatIn } from "./judging.js";
9
+ import { JUDGING_WORDS } from "./judging.words.js";
10
+ /** Feet in a metre: the surface's own drawing is in feet. */
11
+ const FT = 1 / 0.3048;
12
+ export const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
13
+ /** One mat's surface as markup: the drawing's own, without its title, its stylesheet and its apron. */
14
+ export function inner(svg) {
15
+ const look = /^<svg[^>]* data-look="([^"]*)"/.exec(svg)?.[1];
16
+ const style = /<style>([\s\S]*?)<\/style>/.exec(svg)?.[1] ?? "";
17
+ let body = svg.replace(/<style>[\s\S]*?<\/style>/, "");
18
+ body = body.slice(body.indexOf("</desc>") + "</desc>".length, body.lastIndexOf("</svg>"));
19
+ body = body.replace(/<rect class="ky-apron"[^>]*\/>/g, "");
20
+ return { body, style, look };
21
+ }
22
+ export const metricsOf = (width) => {
23
+ const k = width / 1000;
24
+ return { k, pad: 28 * k, gap: 28 * k, titleH: 62 * k, panelGap: 40 * k, badge: 60 * k, rowPad: 14 * k, role: 17 * k, nameMax: 34 * k, nameMin: 22 * k, cardMin: 12 * k };
25
+ };
26
+ /** The plan of one mat at (x, y) in a box: the surface moved and clipped to the region shown, with a badge or a card at every seat. */
27
+ export function planMarkup(mat, surface, geom, region, box, m, setting, clip, frame = true) {
28
+ const rw = region.x1 - region.x0;
29
+ const rh = region.y1 - region.y0;
30
+ const s = Math.min(box.w / rw, box.h / rh);
31
+ const w = rw * s;
32
+ const h = rh * s;
33
+ const x = box.x + (box.w - w) / 2;
34
+ const y = box.y;
35
+ const sf = s / FT;
36
+ const X = (mx) => x + (mx - region.x0) * s;
37
+ const Y = (my) => y + (my - region.y0) * s;
38
+ const words = JUDGING_WORDS[setting.language];
39
+ let markup = el("clipPath", { id: clip }, el("rect", { x: num(region.x0 * FT), y: num(region.y0 * FT), width: num(rw * FT), height: num(rh * FT) }));
40
+ markup += el("rect", { class: "ky-apron", x, y, width: w, height: h });
41
+ markup += el("g", { class: "ky-ju-plan", "clip-path": `url(#${clip})`, transform: `translate(${num(x - region.x0 * FT * sf)} ${num(y - region.y0 * FT * sf)}) scale(${(sf).toFixed(5)})` }, surface.body);
42
+ if (frame)
43
+ markup += el("rect", { class: "ky-ju-plan-frame", x, y, width: w, height: h });
44
+ // The card of a seat is the place the plan gives it: for a kata judge its chair and the table in front of it, for a contest's referee and side judges a card of their own.
45
+ const boxes = mat.seats.map((a) => {
46
+ const c = geom.places[a.seat.n - 1].card;
47
+ const cw = (c.x1 - c.x0) * s;
48
+ const ch = (c.y1 - c.y0) * s;
49
+ const pad = Math.max(2 * m.k, cw * 0.05);
50
+ const r = clamp(cw * 0.2, 6 * m.k, 14 * m.k);
51
+ return { cw, ch, pad, r, width: cw - 2 * pad, height: ch - 2 * pad - r, max: Math.min(cw * 0.3, 26 * m.k) };
52
+ });
53
+ // The names are on the seats only when asked for; otherwise every seat is its number, and the chart is the key.
54
+ const fitted = setting.seatNames ? fitSeats(mat.seats, boxes, { min: m.cardMin, idLabel: words.chart.idLabel }) : mat.seats.map(() => undefined);
55
+ // A seat outside the picture (the referee in a close-up of the bench) has its name in the chart only: it does not fit.
56
+ const shown = mat.seats.map((a) => seatIn(geom.places[a.seat.n - 1], region));
57
+ const fits = mat.seats.map((a, i) => !a.assigned || (shown[i] && fitted[i] !== undefined));
58
+ mat.seats.forEach((a, i) => {
59
+ if (!shown[i])
60
+ return;
61
+ const place = geom.places[a.seat.n - 1];
62
+ const b = boxes[i];
63
+ const fit = fitted[i];
64
+ let g = "";
65
+ if (fit !== undefined)
66
+ g += card(PLAN_CARD, fit, a.seat.n, X(place.card.x0), Y(place.card.y0), b.cw, b.ch, b.pad, b.r, 3 * m.k);
67
+ else {
68
+ const br = clamp(Math.min(b.cw, 1.25 * s) * 0.42, 10 * m.k, 30 * m.k);
69
+ g += badge(PLAN_CARD, X((place.chair.x0 + place.chair.x1) / 2), Y((place.chair.y0 + place.chair.y1) / 2), br, a.seat.n, a.assigned);
70
+ }
71
+ markup += el("g", { class: "ky-ju-seat", "data-mat": mat.n, "data-seat": a.seat.id, "data-card": fit === undefined ? "no" : "yes" }, g);
72
+ });
73
+ return { markup, width: w, height: h, fits, x, y, scale: s };
74
+ }
75
+ /** Each mat's surface, drawn by the package: once for its stylesheet and its look, then once for each mat (its ids differ). `mode` `chart` draws none. */
76
+ export function surfacesOf(setting, o, gid, drawn = true) {
77
+ const league = JUDGING_LEAGUE[setting.style].league;
78
+ let style = "";
79
+ let look;
80
+ const surfaces = drawn
81
+ ? setting.mats.map((mat, i) => {
82
+ const one = inner(drawKyogijo({ sport: "judo", league, budo: { style: setting.style, officials: true }, language: setting.language, id: `${gid}-s${mat.n}`, style: i === 0 && o.css !== "none" ? "embed" : "none" }));
83
+ if (i === 0) {
84
+ style = one.style;
85
+ look = one.look;
86
+ }
87
+ return one;
88
+ })
89
+ : [];
90
+ return { surfaces, style, look };
91
+ }
@@ -0,0 +1,58 @@
1
+ import type { JudgingOptions } from "./judging.types.ts";
2
+ /** The paper sizes, in millimetres (ISO 216 for A4, ANSI for US letter), portrait. */
3
+ export declare const JUDGING_PAPER: Readonly<{
4
+ a4: Readonly<{
5
+ name: "A4";
6
+ width: 210;
7
+ height: 297;
8
+ }>;
9
+ letter: Readonly<{
10
+ name: "US letter";
11
+ width: 215.9;
12
+ height: 279.4;
13
+ }>;
14
+ }>;
15
+ /** What a printout is asked for: the paper, how it lies and the margin all round, in millimetres. */
16
+ export interface JudgingPrintOptions {
17
+ /** `a4` (the default) or `letter`. */
18
+ paper?: keyof typeof JUDGING_PAPER;
19
+ /** `portrait` (the default: the plan above the chart) or `landscape` (the chart beside the plan). */
20
+ orientation?: "portrait" | "landscape";
21
+ /** The margin on all four sides, 5 to 30 mm (12 by default). */
22
+ margin?: number;
23
+ }
24
+ /**
25
+ * The stylesheet of a printout: the `@page` rule (the paper's name, how it lies and the margin), a page break after every sheet but the last, and each sheet as tall as the printable area less a millimetre (a sheet exactly as
26
+ * tall as the page makes some browsers print a blank page after it). The colours are kept as they are on paper (`print-color-adjust`).
27
+ * @example
28
+ * ```ts
29
+ * import { judgingPrintStyle } from "@johnmorrisdotca/kyogijo-stadiums/judging";
30
+ * console.log(judgingPrintStyle({ paper: "letter" }).includes("size:Letter portrait"));
31
+ * // true
32
+ * ```
33
+ */
34
+ export declare function judgingPrintStyle(print?: JudgingPrintOptions): string;
35
+ /**
36
+ * The pages of a printout as markup: one `<section class="ky-ju-sheet">` for each mat (all of them, or the one `mat` names), each holding that mat's picture in the plan-and-chart layout, light, and held to the page's
37
+ * shape (portrait or landscape). The picture is anchored at the top of its page and scaled as a whole, so no type in it is stretched. Add `judgingPrintStyle` for the pages to be what is printed.
38
+ * @example
39
+ * ```ts
40
+ * import { judgingPrintSheets } from "@johnmorrisdotca/kyogijo-stadiums/judging";
41
+ * const html = judgingPrintSheets({ style: "kata", mats: [{ officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }, {}] });
42
+ * console.log(html.split("ky-ju-sheet\"").length - 1);
43
+ * // 2
44
+ * ```
45
+ */
46
+ export declare function judgingPrintSheets(options: JudgingOptions, print?: JudgingPrintOptions): string;
47
+ /**
48
+ * A whole HTML document to print or save as a PDF: the pages (`judgingPrintSheets`) under the printout's stylesheet, which on a screen shows each sheet as a page of the paper laid on a grey desk. Open it in a browser and
49
+ * print; choose "Save as PDF" for a file. Nothing in it is fetched.
50
+ * @example
51
+ * ```ts
52
+ * import { judgingPrint } from "@johnmorrisdotca/kyogijo-stadiums/judging";
53
+ * const html = judgingPrint({ style: "kata", mats: [{ label: "Mat 1", officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] }, { paper: "letter" });
54
+ * console.log(html.startsWith("<!doctype html>"), html.includes("size:Letter portrait"));
55
+ * // true true
56
+ * ```
57
+ */
58
+ export declare function judgingPrint(options: JudgingOptions, print?: JudgingPrintOptions): string;
@@ -0,0 +1,86 @@
1
+ // A judging assignment on paper: one mat to a page, on A4 or US letter, in the plan-and-chart layout (the one that prints well), always in the light colours, with the picture scaled evenly to fill the page's printable
2
+ // area and nothing else on it. `judgingPrintStyle` is the stylesheet (the `@page` size and margins and the page breaks), `judgingPrintSheets` the pages as markup for a page that already has the style, and `judgingPrint`
3
+ // a whole HTML document that opens on screen as the pages laid one under another and prints as they are. Pure: the same options give the same text.
4
+ import { esc } from "./format.js";
5
+ import { drawJudging } from "./judging.draw.js";
6
+ import { judgingSetting } from "./judging.js";
7
+ import { judgingText } from "./judging.text.js";
8
+ /** The paper sizes, in millimetres (ISO 216 for A4, ANSI for US letter), portrait. */
9
+ export const JUDGING_PAPER = Object.freeze({ a4: Object.freeze({ name: "A4", width: 210, height: 297 }), letter: Object.freeze({ name: "US letter", width: 215.9, height: 279.4 }) });
10
+ function ask(print) {
11
+ const paper = print?.paper ?? "a4";
12
+ if (!(paper in JUDGING_PAPER))
13
+ throw new TypeError(`paper is ${Object.keys(JUDGING_PAPER).map((p) => `"${p}"`).join(" or ")}`);
14
+ const orientation = print?.orientation ?? "portrait";
15
+ if (orientation !== "portrait" && orientation !== "landscape")
16
+ throw new TypeError('orientation is "portrait" or "landscape"');
17
+ const margin = print?.margin ?? 12;
18
+ if (typeof margin !== "number" || !Number.isFinite(margin) || margin < 5 || margin > 30)
19
+ throw new RangeError("margin is a number of millimetres from 5 to 30");
20
+ const size = JUDGING_PAPER[paper];
21
+ const [w, h] = orientation === "portrait" ? [size.width, size.height] : [size.height, size.width];
22
+ return { paper, orientation, margin, width: w - 2 * margin, height: h - 2 * margin };
23
+ }
24
+ /** A length in millimetres as CSS, trimmed of the noise of a float. */
25
+ const mm = (value) => `${Math.round(value * 100) / 100}mm`;
26
+ /**
27
+ * The stylesheet of a printout: the `@page` rule (the paper's name, how it lies and the margin), a page break after every sheet but the last, and each sheet as tall as the printable area less a millimetre (a sheet exactly as
28
+ * tall as the page makes some browsers print a blank page after it). The colours are kept as they are on paper (`print-color-adjust`).
29
+ * @example
30
+ * ```ts
31
+ * import { judgingPrintStyle } from "@johnmorrisdotca/kyogijo-stadiums/judging";
32
+ * console.log(judgingPrintStyle({ paper: "letter" }).includes("size:Letter portrait"));
33
+ * // true
34
+ * ```
35
+ */
36
+ export function judgingPrintStyle(print) {
37
+ const a = ask(print);
38
+ const name = a.paper === "a4" ? "A4" : "Letter";
39
+ return [
40
+ `@page{size:${name} ${a.orientation};margin:${mm(a.margin)}}`,
41
+ `.ky-ju-sheets{margin:0;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}`,
42
+ `.ky-ju-sheet{box-sizing:border-box;display:block;width:${mm(a.width)};height:${mm(a.height - 1)};margin:0;padding:0;overflow:hidden;break-after:page;page-break-after:always;break-inside:avoid}`,
43
+ `.ky-ju-sheet:last-child{break-after:auto;page-break-after:auto}`,
44
+ `.ky-ju-sheet>svg.kyogijo{display:block;width:100%;height:100%}`,
45
+ `.ky-ju-sheet .ky-ju-ground{fill:#fff}`,
46
+ ].join("\n");
47
+ }
48
+ /**
49
+ * The pages of a printout as markup: one `<section class="ky-ju-sheet">` for each mat (all of them, or the one `mat` names), each holding that mat's picture in the plan-and-chart layout, light, and held to the page's
50
+ * shape (portrait or landscape). The picture is anchored at the top of its page and scaled as a whole, so no type in it is stretched. Add `judgingPrintStyle` for the pages to be what is printed.
51
+ * @example
52
+ * ```ts
53
+ * import { judgingPrintSheets } from "@johnmorrisdotca/kyogijo-stadiums/judging";
54
+ * const html = judgingPrintSheets({ style: "kata", mats: [{ officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }, {}] });
55
+ * console.log(html.split("ky-ju-sheet\"").length - 1);
56
+ * // 2
57
+ * ```
58
+ */
59
+ export function judgingPrintSheets(options, print) {
60
+ const a = ask(print);
61
+ const setting = judgingSetting(options);
62
+ const sheets = setting.mats.map((mat) => {
63
+ const svg = drawJudging({ ...options, mat: mat.n, view: "plan", theme: "light", orientation: a.orientation, ...(options.id === undefined ? {} : { id: `${options.id}-m${mat.n}` }) });
64
+ return `<section class="ky-ju-sheet" data-mat="${mat.n}">${svg.replace("<svg ", '<svg preserveAspectRatio="xMidYMin meet" ')}</section>`;
65
+ });
66
+ return `<div class="ky-ju-sheets" data-paper="${a.paper}" data-orientation="${a.orientation}">${sheets.join("")}</div>`;
67
+ }
68
+ /**
69
+ * A whole HTML document to print or save as a PDF: the pages (`judgingPrintSheets`) under the printout's stylesheet, which on a screen shows each sheet as a page of the paper laid on a grey desk. Open it in a browser and
70
+ * print; choose "Save as PDF" for a file. Nothing in it is fetched.
71
+ * @example
72
+ * ```ts
73
+ * import { judgingPrint } from "@johnmorrisdotca/kyogijo-stadiums/judging";
74
+ * const html = judgingPrint({ style: "kata", mats: [{ label: "Mat 1", officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] }, { paper: "letter" });
75
+ * console.log(html.startsWith("<!doctype html>"), html.includes("size:Letter portrait"));
76
+ * // true true
77
+ * ```
78
+ */
79
+ export function judgingPrint(options, print) {
80
+ const a = ask(print);
81
+ const setting = judgingSetting(options);
82
+ const { title } = judgingText(setting, options, true, "plan");
83
+ const screen = `@media screen{body{background:#cfd3d6}.ky-ju-sheet{background:#fff;margin:8mm auto;padding:${mm(a.margin)};width:${mm(a.width + 2 * a.margin)};height:${mm(a.height + 2 * a.margin)};box-shadow:0 2px 12px rgba(0,0,0,.3)}}`;
84
+ const head = `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>body{margin:0;color:#10151b;font:16px system-ui,sans-serif}\n${judgingPrintStyle(print)}\n${screen}</style>`;
85
+ return `<!doctype html><html lang="${setting.language}"><head>${head}</head><body>${judgingPrintSheets(options, print)}</body></html>`;
86
+ }
@@ -6,3 +6,8 @@ export declare const JUDGING_DARK = "--ky-ju-bg:#0d1218;--ky-ju-ink:#f6f3ea;--ky
6
6
  * dark forces it. The colours are custom properties (`--ky-ju-*`) a page can set.
7
7
  */
8
8
  export declare const KYOGIJO_JUDGING_STYLE: string;
9
+ /**
10
+ * What a board needs of the drawing's own rules, for a board drawn on its own (`judgingBoard`, laid over the 3D view): the plate, the chips, the ink and the face, scoped to the board so that they cannot change the
11
+ * surface it lies over (the stylesheet of a page is shared by every drawing in it, and the drawing's whole stylesheet would set the surface's colours again). The values are the drawing's.
12
+ */
13
+ export declare const KYOGIJO_JUDGING_BOARD_STYLE: string;
@@ -27,4 +27,26 @@ export const KYOGIJO_JUDGING_STYLE = `
27
27
  .kyogijo .ky-ju-card-name{fill:var(--ky-ju-card-ink);font-family:var(--ky-font);font-weight:800;text-anchor:middle}
28
28
  .kyogijo .ky-ju-card-id{fill:var(--ky-ju-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:700;text-anchor:middle;font-variant-numeric:tabular-nums}
29
29
  .kyogijo .ky-ju-plan-frame{fill:none;stroke:var(--ky-ju-rule);stroke-width:1.5}
30
+ .kyogijo .ky-ju-board .ky-plate-bg{stroke:var(--ky-plate-ink);stroke-opacity:.16;stroke-width:1.5}
31
+ .kyogijo .ky-ju-board .ky-comp.ky-ju-board-title{opacity:1;font-weight:800}
32
+ .kyogijo .ky-ju-board .ky-ju-board-id{font-weight:800;font-variant-numeric:tabular-nums}
33
+ .kyogijo .ky-ju-chip-empty{fill:none;stroke:var(--ky-plate-ink);stroke-opacity:.5;stroke-width:1.5;stroke-dasharray:5 4}
34
+ .kyogijo .ky-ju-board-vacant{fill:var(--ky-plate-ink);opacity:.72;font-family:var(--ky-font);font-weight:700;font-style:italic}
35
+ .kyogijo .ky-ju-board .ky-ju-badge-empty{fill:none;stroke:var(--ky-plate-ink)}
36
+ .kyogijo .ky-ju-board .ky-ju-badge-empty+.ky-ju-badge-n{fill:var(--ky-plate-ink)}
37
+ `.trim();
38
+ /**
39
+ * What a board needs of the drawing's own rules, for a board drawn on its own (`judgingBoard`, laid over the 3D view): the plate, the chips, the ink and the face, scoped to the board so that they cannot change the
40
+ * surface it lies over (the stylesheet of a page is shared by every drawing in it, and the drawing's whole stylesheet would set the surface's colours again). The values are the drawing's.
41
+ */
42
+ export const KYOGIJO_JUDGING_BOARD_STYLE = `
43
+ :where(.ky-judging-board){--ky-ink:#1f2320;--ky-plate:rgba(18,24,21,.9);--ky-plate-ink:#fff;--ky-left:#2f6db0;--ky-left-ink:#fff;--ky-font:system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif;display:block;width:100%;height:auto;font-family:var(--ky-font)}
44
+ @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-judging-board:not([data-theme=light])){--ky-ink:#ece8dc;--ky-plate:rgba(0,0,0,.82)}}
45
+ :where(html[data-theme=dark] .ky-judging-board,.ky-judging-board[data-theme=dark]){--ky-ink:#ece8dc;--ky-plate:rgba(0,0,0,.82)}
46
+ .ky-judging-board .ky-text{fill:var(--ky-ink);font-family:var(--ky-font);font-weight:800;letter-spacing:0;font-variant-numeric:tabular-nums}
47
+ .ky-judging-board .ky-ink-left{fill:var(--ky-left-ink)}
48
+ .ky-judging-board .ky-panel-left{fill:var(--ky-left)}
49
+ .ky-judging-board .ky-plate-bg{fill:var(--ky-plate)}
50
+ .ky-judging-board .ky-score{fill:var(--ky-plate-ink);font-weight:800}
51
+ .ky-judging-board .ky-comp,.ky-judging-board .ky-foot{fill:var(--ky-plate-ink);text-anchor:middle;opacity:.82;font-weight:700}
30
52
  `.trim();
@@ -0,0 +1,7 @@
1
+ import type { JudgingSetting } from "./judging.ts";
2
+ import type { JudgingOptions } from "./judging.types.ts";
3
+ /** The title and the description of an assignment: each mat with each seat and who is in it. */
4
+ export declare function judgingText(setting: JudgingSetting, options: Pick<JudgingOptions, "title" | "description">, chart: boolean, view?: "plan" | "broadcast"): {
5
+ title: string;
6
+ description: string;
7
+ };