@johnmorrisdotca/kyogijo-stadiums 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +22 -7
- package/dist/judging-entry.d.ts +12 -0
- package/dist/judging-entry.js +8 -0
- package/dist/judging.card.d.ts +21 -0
- package/dist/judging.card.js +34 -0
- package/dist/judging.d.ts +55 -0
- package/dist/judging.data.d.ts +14 -0
- package/dist/judging.data.js +24 -0
- package/dist/judging.draw.d.ts +66 -0
- package/dist/judging.draw.js +288 -0
- package/dist/judging.fit.d.ts +51 -0
- package/dist/judging.fit.js +95 -0
- package/dist/judging.js +135 -0
- package/dist/judging.style.d.ts +8 -0
- package/dist/judging.style.js +30 -0
- package/dist/judging.turn.d.ts +35 -0
- package/dist/judging.turn.js +152 -0
- package/dist/judging.types.d.ts +72 -0
- package/dist/judging.types.js +1 -0
- package/dist/judging.words.d.ts +45 -0
- package/dist/judging.words.js +40 -0
- package/dist/multi.turn.js +1 -9
- package/dist/strings.d.ts +1 -1
- package/dist/strings.js +2 -2
- package/dist/turn.kit.d.ts +13 -2
- package/dist/turn.kit.js +9 -0
- package/dist/turn.markup.js +1 -1
- package/dist/turn.model.d.ts +2 -0
- package/dist/turn.style.js +15 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
// 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
|
+
// alternative is `judgingTable`.
|
|
4
|
+
import { drawKyogijo } from "./draw.js";
|
|
5
|
+
import { safeClass } from "./common.js";
|
|
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 } from "./judging.js";
|
|
11
|
+
import { KYOGIJO_JUDGING_STYLE } from "./judging.style.js";
|
|
12
|
+
import { JUDGING_WORDS } from "./judging.words.js";
|
|
13
|
+
import { fill } from "./strings.js";
|
|
14
|
+
import { LINE_HEIGHT } from "./text.js";
|
|
15
|
+
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 ? 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
|
+
/** The rows of one mat's chart, each as tall as its name and ID need, set in a chart `width` wide. */
|
|
53
|
+
function chartRows(mat, width, m, setting) {
|
|
54
|
+
const words = JUDGING_WORDS[setting.language];
|
|
55
|
+
const textW = width - m.rowPad * 1.6 - m.badge - m.gap * 0.6;
|
|
56
|
+
return mat.seats.map((a) => {
|
|
57
|
+
if (!a.assigned)
|
|
58
|
+
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) };
|
|
59
|
+
const fit = fitChart({ name: a.name, id: a.id, idLabel: words.chart.idLabel, width: textW, max: m.nameMax, min: m.nameMin });
|
|
60
|
+
const height = m.rowPad * 2 + m.role * LINE_HEIGHT + 6 * m.k + fit.height;
|
|
61
|
+
return { a, fit, height: Math.max(height, m.badge + 2 * m.rowPad) };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** The chart of one mat as markup at (x, y), `width` wide, and its height. */
|
|
65
|
+
function chartMarkup(mat, x, y, width, m, setting) {
|
|
66
|
+
const words = JUDGING_WORDS[setting.language];
|
|
67
|
+
const rows = chartRows(mat, width, m, setting);
|
|
68
|
+
const gap = 8 * m.k;
|
|
69
|
+
let cursor = 0;
|
|
70
|
+
let out = "";
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const { a, fit } = row;
|
|
73
|
+
const cx = m.rowPad * 0.8 + m.badge / 2;
|
|
74
|
+
const tx = m.rowPad * 0.8 + m.badge + m.gap * 0.6;
|
|
75
|
+
let inside = el("rect", { class: "ky-ju-row", x: 0, y: cursor, width, height: row.height, rx: 14 * m.k });
|
|
76
|
+
inside += el("circle", { class: a.assigned ? "ky-ju-badge" : "ky-ju-badge ky-ju-badge-empty", cx, cy: cursor + row.height / 2, r: m.badge / 2 });
|
|
77
|
+
inside += el("text", { class: "ky-ju-badge-n", x: cx, y: cursor + row.height / 2 + m.badge * 0.18, "font-size": m.badge * 0.52 }, String(a.seat.n));
|
|
78
|
+
let ty = cursor + m.rowPad + m.role * 0.82;
|
|
79
|
+
inside += el("text", { class: "ky-ju-role", x: tx, y: ty, "font-size": m.role }, esc(a.seat.label[setting.language]));
|
|
80
|
+
ty += m.role * 0.3 + 6 * m.k;
|
|
81
|
+
if (fit === undefined) {
|
|
82
|
+
inside += el("text", { class: "ky-ju-vacant", x: tx, y: ty + m.nameMin * 0.95, "font-size": m.nameMin }, esc(words.chart.vacant));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const line of fit.name) {
|
|
86
|
+
inside += el("text", { class: "ky-ju-name", x: tx, y: ty + fit.nameSize * 0.88, "font-size": fit.nameSize }, esc(line));
|
|
87
|
+
ty += fit.nameSize * LINE_HEIGHT;
|
|
88
|
+
}
|
|
89
|
+
if (fit.id.length > 0) {
|
|
90
|
+
ty += fit.name.length > 0 ? fit.nameSize * 0.28 : 0;
|
|
91
|
+
for (const line of fit.id) {
|
|
92
|
+
inside += el("text", { class: "ky-ju-id", x: tx, y: ty + fit.idSize * 0.88, "font-size": fit.idSize }, esc(line));
|
|
93
|
+
ty += fit.idSize * LINE_HEIGHT;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
out += el("g", { class: "ky-ju-chart-row", "data-mat": mat.n, "data-seat": a.seat.id }, inside);
|
|
98
|
+
cursor += row.height + gap;
|
|
99
|
+
}
|
|
100
|
+
const height = Math.max(0, cursor - gap);
|
|
101
|
+
return { markup: el("g", { class: "ky-ju-chart", "data-mat": mat.n, transform: `translate(${num(x)} ${num(y)})` }, out), height };
|
|
102
|
+
}
|
|
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 its chair and the table in front of it: a seat wide and as deep as both.
|
|
121
|
+
const cardW = (geom.pitch - 0.06) * s;
|
|
122
|
+
const cardH = (geom.table.y1 - (geom.chairs[0].y0 - 0.05)) * s;
|
|
123
|
+
const pad = Math.max(2 * m.k, cardW * 0.05);
|
|
124
|
+
const r = clamp(cardW * 0.2, 6 * m.k, 14 * m.k);
|
|
125
|
+
const innerW = cardW - 2 * pad;
|
|
126
|
+
const innerH = cardH - 2 * pad - r;
|
|
127
|
+
const fitted = fitSeats(mat.seats, { width: innerW, height: innerH, max: Math.min(cardW * 0.3, 26 * m.k), min: m.cardMin, idLabel: words.chart.idLabel });
|
|
128
|
+
const fits = mat.seats.map((a, i) => !a.assigned || fitted[i] !== undefined);
|
|
129
|
+
mat.seats.forEach((a, i) => {
|
|
130
|
+
const chair = geom.chairs[i];
|
|
131
|
+
const cx = X((chair.x0 + chair.x1) / 2);
|
|
132
|
+
const top = Y(chair.y0 - 0.05);
|
|
133
|
+
const fit = fitted[i];
|
|
134
|
+
let g = "";
|
|
135
|
+
if (fit !== undefined)
|
|
136
|
+
g += card(PLAN_CARD, fit, a.seat.n, cx - cardW / 2, top, cardW, cardH, pad, r, 3 * m.k);
|
|
137
|
+
else {
|
|
138
|
+
const br = clamp(geom.pitch * s * 0.3, 7 * m.k, 16 * m.k);
|
|
139
|
+
g += badge(PLAN_CARD, cx, Y((chair.y0 + chair.y1) / 2), br, a.seat.n, a.assigned);
|
|
140
|
+
}
|
|
141
|
+
markup += el("g", { class: "ky-ju-seat", "data-mat": mat.n, "data-seat": a.seat.id, "data-card": fit === undefined ? "no" : "yes" }, g);
|
|
142
|
+
});
|
|
143
|
+
return { markup, width: w, height: h, fits };
|
|
144
|
+
}
|
|
145
|
+
/** The picture of an assignment and what was worked out to make it. `mode` `chart` is the chart alone (for beside a 3D view). */
|
|
146
|
+
export function judgingDrawing(options, mode = "full") {
|
|
147
|
+
const setting = judgingSetting(mode === "chart" && options.width === undefined ? { ...options, width: 600 } : options);
|
|
148
|
+
const language = setting.language;
|
|
149
|
+
const width = setting.width;
|
|
150
|
+
const m = metricsOf(width);
|
|
151
|
+
const geom = judgingPlan(setting.style);
|
|
152
|
+
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 };
|
|
153
|
+
const gid = safeClass(options.id).replace(/ /g, "-") || `ky-ju-${hash(JSON.stringify([options, mode, VERSION]))}`;
|
|
154
|
+
const league = JUDGING_LEAGUE[setting.style].league;
|
|
155
|
+
const portrait = setting.orientation === "portrait";
|
|
156
|
+
// The surface, drawn by the package: once for its stylesheet, then once for each mat (its ids differ).
|
|
157
|
+
let style = "";
|
|
158
|
+
let look;
|
|
159
|
+
const surfaces = mode === "chart" ? [] : setting.mats.map((mat, i) => {
|
|
160
|
+
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" }));
|
|
161
|
+
if (i === 0) {
|
|
162
|
+
style = drawn.style;
|
|
163
|
+
look = drawn.look;
|
|
164
|
+
}
|
|
165
|
+
return drawn;
|
|
166
|
+
});
|
|
167
|
+
// 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.
|
|
168
|
+
const planBox = (withChart) => {
|
|
169
|
+
if (portrait)
|
|
170
|
+
return { w: width - 2 * m.pad, h: (withChart ? 0.66 : 0.86) * width };
|
|
171
|
+
return withChart ? { w: 0.48 * (width - 2 * m.pad - m.gap), h: 640 * m.k } : { w: width - 2 * m.pad, h: 0.82 * width };
|
|
172
|
+
};
|
|
173
|
+
const trial = (withChart) => {
|
|
174
|
+
const box = planBox(withChart);
|
|
175
|
+
return setting.mats.flatMap((mat, i) => planMarkup(mat, surfaces[i], geom, region, { x: 0, y: 0, ...box }, m, setting, "t").fits);
|
|
176
|
+
};
|
|
177
|
+
let chart = mode === "chart" || setting.chart === "always";
|
|
178
|
+
if (mode === "full" && setting.chart === "auto")
|
|
179
|
+
chart = !trial(false).every(Boolean);
|
|
180
|
+
if (mode === "full" && setting.chart === "never")
|
|
181
|
+
chart = false;
|
|
182
|
+
const cardsFit = mode === "chart" ? [] : trial(chart);
|
|
183
|
+
// The panels, one under another.
|
|
184
|
+
let body = "";
|
|
185
|
+
let y = m.pad;
|
|
186
|
+
const cards = [];
|
|
187
|
+
const styleText = fill(JUDGING_WORDS[language].styleName[setting.style], { count: setting.mats[0].seats.length });
|
|
188
|
+
setting.mats.forEach((mat, i) => {
|
|
189
|
+
let panel = el("text", { class: "ky-ju-mat", x: m.pad, y: y + 42 * m.k, "font-size": 38 * m.k }, esc(mat.label));
|
|
190
|
+
panel += el("text", { class: "ky-ju-style", x: width - m.pad, y: y + 40 * m.k, "font-size": 21 * m.k, "text-anchor": "end" }, esc(styleText));
|
|
191
|
+
const top = y + m.titleH;
|
|
192
|
+
let bottom = top;
|
|
193
|
+
if (mode === "full") {
|
|
194
|
+
const box = planBox(chart);
|
|
195
|
+
const plan = planMarkup(mat, surfaces[i], geom, region, { x: m.pad, y: top, ...box }, m, setting, `${gid}-c${mat.n}`);
|
|
196
|
+
panel += plan.markup;
|
|
197
|
+
bottom = top + plan.height;
|
|
198
|
+
mat.seats.forEach((a, k) => cards.push({ mat: mat.n, seat: a.seat.id, fits: plan.fits[k] }));
|
|
199
|
+
if (chart) {
|
|
200
|
+
const cx = portrait ? m.pad : m.pad + box.w + m.gap;
|
|
201
|
+
const cy = portrait ? bottom + m.gap : top;
|
|
202
|
+
const cw = portrait ? width - 2 * m.pad : width - m.pad - cx;
|
|
203
|
+
const c = chartMarkup(mat, cx, cy, cw, m, setting);
|
|
204
|
+
panel += c.markup;
|
|
205
|
+
bottom = Math.max(bottom, cy + c.height);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const c = chartMarkup(mat, m.pad, top, width - 2 * m.pad, m, setting);
|
|
210
|
+
panel += c.markup;
|
|
211
|
+
bottom = top + c.height;
|
|
212
|
+
}
|
|
213
|
+
body += el("g", { class: "ky-ju-panel", "data-mat": mat.n }, panel);
|
|
214
|
+
y = bottom + m.panelGap;
|
|
215
|
+
});
|
|
216
|
+
const height = Math.ceil(y - m.panelGap + m.pad);
|
|
217
|
+
const { title, description } = judgingText(setting, options, chart);
|
|
218
|
+
const titleId = `${gid}-t`;
|
|
219
|
+
const descId = `${gid}-d`;
|
|
220
|
+
const css = options.css === "none" ? "" : `<style>${style}\n${KYOGIJO_JUDGING_STYLE}</style>`;
|
|
221
|
+
const root = el("svg", {
|
|
222
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
223
|
+
class: `kyogijo ky-judging ky-judo${safeClass(options.className) === "" ? "" : ` ${safeClass(options.className)}`}`,
|
|
224
|
+
viewBox: `0 0 ${num(width)} ${num(height)}`,
|
|
225
|
+
role: "img",
|
|
226
|
+
"aria-labelledby": `${titleId} ${descId}`,
|
|
227
|
+
"data-sport": "judo",
|
|
228
|
+
"data-judging": setting.style,
|
|
229
|
+
"data-look": look ?? "yellow-red",
|
|
230
|
+
"data-mats": setting.total,
|
|
231
|
+
"data-chart": chart ? "yes" : "no",
|
|
232
|
+
"data-theme": options.theme === "light" || options.theme === "dark" ? options.theme : undefined,
|
|
233
|
+
"data-version": VERSION,
|
|
234
|
+
}, `<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);
|
|
235
|
+
void cardsFit;
|
|
236
|
+
return { svg: root, width, height, chart, cards, title, description };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 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
|
|
240
|
+
* grow to hold every name whole. `style` is the style the areas are judged in (`kata`: five judges); `mats` the mat areas, each with its `label` (`Mat 1`, `Mat 2` when missing) and its `officials`, each a `seat`, a `name`
|
|
241
|
+
* and an `id`. `mat` draws one mat alone (a tab); `chart` is `auto`, `always` or `never`; `focus: "bench"` shows the judges' bench close up so that the names in the seats are as large as they can be. Light and dark follow the
|
|
242
|
+
* page; `judgingTable` is the same thing as a table.
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { drawJudging } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
246
|
+
* const svg = drawJudging({
|
|
247
|
+
* style: "kata",
|
|
248
|
+
* mats: [{ label: "Mat 1", officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }, { seat: "central", name: "Daniel Okafor", id: "J-0377" }] }],
|
|
249
|
+
* });
|
|
250
|
+
* console.log(svg.startsWith("<svg"), svg.includes("Aiko Mori"), svg.includes("J-1042"));
|
|
251
|
+
* // true true true
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export function drawJudging(options) {
|
|
255
|
+
return judgingDrawing(options).svg;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The chart of an assignment alone: for each mat its seats in order, each with its number, its role, its name and, under the name, its ID, in rows that grow so that no name or ID is cut. For a page that draws
|
|
259
|
+
* the 3D view of the mat and puts the chart beside it.
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* import { judgingChart } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
263
|
+
* const svg = judgingChart({ style: "kata", mats: [{ officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] });
|
|
264
|
+
* console.log(svg.includes("ky-ju-chart-row"), svg.includes("Not assigned"));
|
|
265
|
+
* // true true
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export function judgingChart(options) {
|
|
269
|
+
return judgingDrawing(options, "chart").svg;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The same thing as the picture, as a table: each mat, each seat and who sits in it, with the ID. For a reader who cannot see the drawing, and for a page that wants the data.
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* import { judgingTable } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
276
|
+
* const table = judgingTable({ style: "kata", mats: [{ officials: [{ seat: 3, name: "Daniel Okafor", id: "J-0377" }] }] });
|
|
277
|
+
* console.log(table.startsWith("<table"), table.includes("Daniel Okafor"));
|
|
278
|
+
* // true true
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function judgingTable(options) {
|
|
282
|
+
const setting = judgingSetting(options);
|
|
283
|
+
const words = JUDGING_WORDS[setting.language];
|
|
284
|
+
const { title } = judgingText(setting, options, true);
|
|
285
|
+
const head = `<thead><tr><th scope="col">${esc(words.table.mat)}</th><th scope="col">${esc(words.table.seat)}</th><th scope="col">${esc(words.table.name)}</th><th scope="col">${esc(words.table.id)}</th></tr></thead>`;
|
|
286
|
+
const rows = setting.mats.flatMap((mat) => mat.seats.map((a) => `<tr><th scope="row">${esc(mat.label)}</th><td>${esc(a.seat.label[setting.language])}</td><td>${a.assigned ? esc(a.name) : esc(words.chart.vacant)}</td><td>${esc(a.id)}</td></tr>`)).join("");
|
|
287
|
+
return `<table class="ky-table" data-kind="judging"><caption>${esc(fill(words.table.caption, { title }))}</caption>${head}<tbody>${rows}</tbody></table>`;
|
|
288
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { JudgingAssignment } from "./judging.types.ts";
|
|
2
|
+
/** What a name and an ID were set as. */
|
|
3
|
+
export interface CardFit {
|
|
4
|
+
name: string[];
|
|
5
|
+
nameSize: number;
|
|
6
|
+
/** The ID's lines (with its label), or none. */
|
|
7
|
+
id: string[];
|
|
8
|
+
idSize: number;
|
|
9
|
+
/** The width of the widest line and the height of the whole, in the box's units. */
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
/** What to fit, and how. Sizes and the box are in the same units. */
|
|
14
|
+
export interface CardAsk {
|
|
15
|
+
name: string;
|
|
16
|
+
id: string;
|
|
17
|
+
/** The word before an ID (`ID`). */
|
|
18
|
+
idLabel: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
/** The biggest and the smallest size of the name; the ID is set at 0.62 of the name's size and no smaller than 0.75 of the least. */
|
|
22
|
+
max: number;
|
|
23
|
+
min: number;
|
|
24
|
+
/** The most lines the name may take. */
|
|
25
|
+
maxLines: number;
|
|
26
|
+
/** Whether a word wider than the box may be broken inside (the chart), or the card does not fit (a seat). */
|
|
27
|
+
breakWords: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** The lines a text is wrapped in at a size, no line wider than `width`, or undefined when a word is wider and breaking is not allowed. */
|
|
30
|
+
export declare function wrapText(text: string, width: number, size: number, breakWords: boolean, measure?: (text: string) => number): string[] | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Set a name, and an ID under it, in the biggest size that fits the box. Returns undefined when nothing from `max` down to `min` fits (for a card in a seat, that is "does not fit"); with `breakWords` and `maxLines` set
|
|
33
|
+
* as large as it takes, it always returns something, which may be taller than the box (the chart grows).
|
|
34
|
+
*/
|
|
35
|
+
export declare function fitCard(ask: CardAsk): CardFit | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* What the chart does: the biggest size at which the name takes three lines at most with no word broken, shrinking to the floor; then, at the floor, as many lines as it takes and a word wider than the row broken inside.
|
|
38
|
+
* It always fits, whatever the text, because the row grows.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fitChart(ask: Omit<CardAsk, "maxLines" | "breakWords" | "height">): CardFit;
|
|
41
|
+
/**
|
|
42
|
+
* Fit the name and ID of every seat of a mat into a card of one size, as one size for all of them, so that the row of cards reads evenly: each seat that has a name is fitted alone, the smallest size any of them needs is
|
|
43
|
+
* taken, and each is fitted again at that size. A seat whose name does not fit at the floor, or that has nobody in it, has no card (undefined).
|
|
44
|
+
*/
|
|
45
|
+
export declare function fitSeats(seats: readonly JudgingAssignment[], box: {
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
max: number;
|
|
49
|
+
min: number;
|
|
50
|
+
idLabel: string;
|
|
51
|
+
}): (CardFit | undefined)[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LINE_HEIGHT, textWidth } from "./text.js";
|
|
2
|
+
/** The gap between the name and the ID, as a share of the name's size. */
|
|
3
|
+
const GAP = 0.28;
|
|
4
|
+
/** An ID's width in ems: a face with fixed-width digits and letters, a little wider than the proportional one. */
|
|
5
|
+
const idWidth = (text) => textWidth(text, false) * 1.12;
|
|
6
|
+
/** The lines a text is wrapped in at a size, no line wider than `width`, or undefined when a word is wider and breaking is not allowed. */
|
|
7
|
+
export function wrapText(text, width, size, breakWords, measure = (t) => textWidth(t)) {
|
|
8
|
+
const words = text.trim().split(/\s+/).filter((w) => w !== "");
|
|
9
|
+
const lines = [];
|
|
10
|
+
let current = "";
|
|
11
|
+
const fits = (t) => measure(t) * size <= width + 1e-9;
|
|
12
|
+
for (const word of words) {
|
|
13
|
+
const joined = current === "" ? word : `${current} ${word}`;
|
|
14
|
+
if (fits(joined)) {
|
|
15
|
+
current = joined;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (current !== "")
|
|
19
|
+
lines.push(current);
|
|
20
|
+
current = "";
|
|
21
|
+
if (fits(word)) {
|
|
22
|
+
current = word;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!breakWords)
|
|
26
|
+
return undefined;
|
|
27
|
+
// A word wider than the box: broken inside, by characters.
|
|
28
|
+
let piece = "";
|
|
29
|
+
for (const ch of word) {
|
|
30
|
+
if (piece !== "" && !fits(piece + ch)) {
|
|
31
|
+
lines.push(piece);
|
|
32
|
+
piece = ch;
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
piece += ch;
|
|
36
|
+
}
|
|
37
|
+
current = piece;
|
|
38
|
+
}
|
|
39
|
+
if (current !== "")
|
|
40
|
+
lines.push(current);
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Set a name, and an ID under it, in the biggest size that fits the box. Returns undefined when nothing from `max` down to `min` fits (for a card in a seat, that is "does not fit"); with `breakWords` and `maxLines` set
|
|
45
|
+
* as large as it takes, it always returns something, which may be taller than the box (the chart grows).
|
|
46
|
+
*/
|
|
47
|
+
export function fitCard(ask) {
|
|
48
|
+
const label = ask.id === "" ? "" : `${ask.idLabel} ${ask.id}`.trim();
|
|
49
|
+
const floor = Math.min(ask.min, ask.max);
|
|
50
|
+
// From the biggest size down to the floor in forty steps (and the floor itself last), in whatever unit the box is in.
|
|
51
|
+
const step = Math.max((ask.max - floor) / 40, 1e-4);
|
|
52
|
+
const sizes = [];
|
|
53
|
+
for (let size = ask.max; size > floor + 1e-9; size -= step)
|
|
54
|
+
sizes.push(size);
|
|
55
|
+
sizes.push(floor);
|
|
56
|
+
for (const size of sizes) {
|
|
57
|
+
const lines = ask.name === "" ? [] : wrapText(ask.name, ask.width, size, ask.breakWords);
|
|
58
|
+
if (lines === undefined || lines.length > ask.maxLines)
|
|
59
|
+
continue;
|
|
60
|
+
const idSize = Math.max(size * 0.62, floor * 0.75);
|
|
61
|
+
const idLines = label === "" ? [] : wrapText(label, ask.width, idSize, ask.breakWords, idWidth);
|
|
62
|
+
if (idLines === undefined || (!ask.breakWords && idLines.length > 1))
|
|
63
|
+
continue;
|
|
64
|
+
const height = lines.length * size * LINE_HEIGHT + (idLines.length > 0 ? (lines.length > 0 ? size * GAP : 0) + idLines.length * idSize * LINE_HEIGHT : 0);
|
|
65
|
+
if (height > ask.height + 1e-9)
|
|
66
|
+
continue;
|
|
67
|
+
const width = Math.max(0, ...lines.map((l) => textWidth(l) * size), ...idLines.map((l) => idWidth(l) * idSize));
|
|
68
|
+
return { name: lines, nameSize: size, id: idLines, idSize, width, height };
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What the chart does: the biggest size at which the name takes three lines at most with no word broken, shrinking to the floor; then, at the floor, as many lines as it takes and a word wider than the row broken inside.
|
|
74
|
+
* It always fits, whatever the text, because the row grows.
|
|
75
|
+
*/
|
|
76
|
+
export function fitChart(ask) {
|
|
77
|
+
const whole = fitCard({ ...ask, height: Infinity, maxLines: 3, breakWords: false });
|
|
78
|
+
if (whole !== undefined)
|
|
79
|
+
return whole;
|
|
80
|
+
const fit = fitCard({ ...ask, max: ask.min, height: Infinity, maxLines: Infinity, breakWords: true });
|
|
81
|
+
if (fit === undefined)
|
|
82
|
+
throw new Error("a chart row always fits");
|
|
83
|
+
return fit;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fit the name and ID of every seat of a mat into a card of one size, as one size for all of them, so that the row of cards reads evenly: each seat that has a name is fitted alone, the smallest size any of them needs is
|
|
87
|
+
* taken, and each is fitted again at that size. A seat whose name does not fit at the floor, or that has nobody in it, has no card (undefined).
|
|
88
|
+
*/
|
|
89
|
+
export function fitSeats(seats, box) {
|
|
90
|
+
const ask = (a, max) => (a.assigned ? fitCard({ name: a.name, id: a.id, idLabel: box.idLabel, width: box.width, height: box.height, max, min: box.min, maxLines: 3, breakWords: false }) : undefined);
|
|
91
|
+
const alone = seats.map((a) => ask(a, box.max));
|
|
92
|
+
const sizes = alone.filter((f) => f !== undefined).map((f) => f.nameSize);
|
|
93
|
+
const common = sizes.length === 0 ? box.max : Math.min(...sizes);
|
|
94
|
+
return seats.map((a, i) => (alone[i] === undefined ? undefined : ask(a, common)));
|
|
95
|
+
}
|
package/dist/judging.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// A judging assignment worked out: the options held to what they may be, each mat's seats with who sits in them, and where the seats are on the plan. The plan is the package's own judo area (`budoPlan`), so that the
|
|
2
|
+
// seats named here are the seats drawn, in metres from the top left of the tatami. Nothing here draws.
|
|
3
|
+
import { budoPlan } from "./budo/plan.js";
|
|
4
|
+
import { JUDGING_LEAGUE, JUDGING_MATS_MAX, JUDGING_SEATS, JUDGING_STYLES } from "./judging.data.js";
|
|
5
|
+
import { JUDGING_WORDS } from "./judging.words.js";
|
|
6
|
+
import { fill } from "./strings.js";
|
|
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 });
|
|
9
|
+
/** Text made safe to show: control characters become spaces, runs of spaces one, the ends trimmed. */
|
|
10
|
+
const clean = (text) => text.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
|
|
11
|
+
/** The seats of a style. @throws TypeError for a style that is not one. */
|
|
12
|
+
export function seatsOf(style) {
|
|
13
|
+
if (typeof style !== "string" || !JUDGING_STYLES.includes(style))
|
|
14
|
+
throw new TypeError(`judging style is ${JUDGING_STYLES.map((s) => `"${s}"`).join(" or ")}`);
|
|
15
|
+
return JUDGING_SEATS[style];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The seat a name stands for: its number (`3`, `"3"`), its key (`"judge-3"`, `"Judge 3"`) or `"central"`.
|
|
19
|
+
* @throws RangeError for a seat the style does not have, naming the seats it has.
|
|
20
|
+
*/
|
|
21
|
+
export function seatOf(style, seat) {
|
|
22
|
+
const seats = seatsOf(style);
|
|
23
|
+
const text = typeof seat === "number" ? String(seat) : typeof seat === "string" ? seat.trim().toLowerCase() : "";
|
|
24
|
+
let found;
|
|
25
|
+
if (text === "central" || text === "centre" || text === "center")
|
|
26
|
+
found = seats.find((s) => s.role === "central");
|
|
27
|
+
else {
|
|
28
|
+
const m = /^(?:judge)?[-\s]?(\d{1,2})$/.exec(text);
|
|
29
|
+
if (m !== null)
|
|
30
|
+
found = seats.find((s) => s.n === Number(m[1]));
|
|
31
|
+
}
|
|
32
|
+
if (found === undefined)
|
|
33
|
+
throw new RangeError(`The ${style} area has no seat ${JSON.stringify(seat)}: its seats are ${seats.map((s) => s.id).join(", ")} (or their numbers, or "central")`);
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
/** The label of a mat: the caller's, held to its length, else the language's `Mat {n}`. */
|
|
37
|
+
export function matLabel(mat, n, language) {
|
|
38
|
+
if (mat.label === undefined)
|
|
39
|
+
return fill(JUDGING_WORDS[language].mat, { n });
|
|
40
|
+
if (typeof mat.label !== "string")
|
|
41
|
+
throw new TypeError("a mat's label is text");
|
|
42
|
+
const label = clean(mat.label);
|
|
43
|
+
if ([...label].length > JUDGING_LIMITS.label)
|
|
44
|
+
throw new RangeError(`A mat's label is at most ${JUDGING_LIMITS.label} characters`);
|
|
45
|
+
return label === "" ? fill(JUDGING_WORDS[language].mat, { n }) : label;
|
|
46
|
+
}
|
|
47
|
+
const oneOf = (value, allowed, fallback, label) => {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return fallback;
|
|
50
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
51
|
+
throw new TypeError(`${label} is ${allowed.map((a) => `"${a}"`).join(" or ")}`);
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Hold the options to what they may be and put each official in a seat.
|
|
56
|
+
* @throws TypeError for an option of the wrong kind, RangeError for a number of mats outside 1 to 6, a mat that does not exist, a seat the style does not have, a seat named twice on one mat or a text that is too long.
|
|
57
|
+
*/
|
|
58
|
+
export function judgingSetting(options) {
|
|
59
|
+
const style = options.style;
|
|
60
|
+
const seats = seatsOf(style);
|
|
61
|
+
if (!Array.isArray(options.mats) || options.mats.length < 1 || options.mats.length > JUDGING_MATS_MAX)
|
|
62
|
+
throw new RangeError(`An assignment has 1 to ${JUDGING_MATS_MAX} mats`);
|
|
63
|
+
const language = options.language === "ja" ? "ja" : "en";
|
|
64
|
+
const all = options.mats.map((mat, i) => {
|
|
65
|
+
if (typeof mat !== "object" || mat === null)
|
|
66
|
+
throw new TypeError("a mat is { label, officials }");
|
|
67
|
+
const given = new Map();
|
|
68
|
+
for (const official of mat.officials ?? []) {
|
|
69
|
+
if (typeof official !== "object" || official === null)
|
|
70
|
+
throw new TypeError("an official is { seat, name, id }");
|
|
71
|
+
const seat = seatOf(style, official.seat);
|
|
72
|
+
if (given.has(seat.id))
|
|
73
|
+
throw new RangeError(`Mat ${i + 1} names seat ${seat.id} twice`);
|
|
74
|
+
if (official.name !== undefined && typeof official.name !== "string")
|
|
75
|
+
throw new TypeError("an official's name is text");
|
|
76
|
+
if (official.id !== undefined && typeof official.id !== "string" && typeof official.id !== "number")
|
|
77
|
+
throw new TypeError("an official's id is text or a number");
|
|
78
|
+
const name = clean(official.name ?? "");
|
|
79
|
+
const id = clean(official.id === undefined ? "" : String(official.id));
|
|
80
|
+
if ([...name].length > JUDGING_LIMITS.name)
|
|
81
|
+
throw new RangeError(`A name is at most ${JUDGING_LIMITS.name} characters`);
|
|
82
|
+
if ([...id].length > JUDGING_LIMITS.id)
|
|
83
|
+
throw new RangeError(`An ID is at most ${JUDGING_LIMITS.id} characters`);
|
|
84
|
+
given.set(seat.id, { seat, name, id, assigned: name !== "" || id !== "" });
|
|
85
|
+
}
|
|
86
|
+
return { n: i + 1, label: matLabel(mat, i + 1, language), seats: seats.map((seat) => given.get(seat.id) ?? { seat, name: "", id: "", assigned: false }) };
|
|
87
|
+
});
|
|
88
|
+
let mats = all;
|
|
89
|
+
if (options.mat !== undefined) {
|
|
90
|
+
if (!Number.isInteger(options.mat) || options.mat < 1 || options.mat > all.length)
|
|
91
|
+
throw new RangeError(`mat is a whole number from 1 to ${all.length}`);
|
|
92
|
+
mats = [all[options.mat - 1]];
|
|
93
|
+
}
|
|
94
|
+
const width = options.width ?? 1000;
|
|
95
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width < 480 || width > 2000)
|
|
96
|
+
throw new RangeError("width is a number from 480 to 2000");
|
|
97
|
+
return {
|
|
98
|
+
style,
|
|
99
|
+
language,
|
|
100
|
+
mats,
|
|
101
|
+
total: all.length,
|
|
102
|
+
chart: oneOf(options.chart, ["auto", "always", "never"], "auto", "chart"),
|
|
103
|
+
orientation: oneOf(options.orientation, ["landscape", "portrait"], "landscape", "orientation"),
|
|
104
|
+
focus: oneOf(options.focus, ["mat", "bench"], "mat", "focus"),
|
|
105
|
+
width,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const plans = new Map();
|
|
109
|
+
/**
|
|
110
|
+
* The judges' bench close up: the seats and the table with a little round them and a few rows of mats in front, from the seats of one mat or of several side by side. `matTop` is where the mats start (the top of the first
|
|
111
|
+
* contest area), so that the picture shows how the bench sits at the mat's edge; `front` instead says how far in front of the table the picture reaches, in metres (the 3D view, which turns, wants as little of the floor as will do).
|
|
112
|
+
*/
|
|
113
|
+
export function benchOf(chairs, table, pitch, matTop, front) {
|
|
114
|
+
const side = (pitch - 0.5) / 2 + 0.15;
|
|
115
|
+
return { x0: Math.min(...chairs.map((c) => c.x0)) - side, y0: chairs[0].y0 - 0.9, x1: Math.max(...chairs.map((c) => c.x1)) + side, y1: front === undefined ? Math.max(table.y1 + 1.8, matTop + 1.2) : table.y1 + front };
|
|
116
|
+
}
|
|
117
|
+
/** The plan of one mat of a style, with its seats: the package's own judo area, so that the seats named are the seats drawn. */
|
|
118
|
+
export function judgingPlan(style) {
|
|
119
|
+
const hit = plans.get(style);
|
|
120
|
+
if (hit !== undefined)
|
|
121
|
+
return hit;
|
|
122
|
+
seatsOf(style);
|
|
123
|
+
const plan = budoPlan({ sport: "judo", league: JUDGING_LEAGUE[style].league, budo: { style, officials: true } });
|
|
124
|
+
const judges = plan.judo?.judges;
|
|
125
|
+
if (judges === undefined || judges.seats.length !== JUDGING_SEATS[style].length || judges.tables.length === 0)
|
|
126
|
+
throw new Error(`The ${style} plan has no judges' seats`);
|
|
127
|
+
const chairs = judges.seats.slice(0, JUDGING_SEATS[style].length);
|
|
128
|
+
const table = judges.tables[0];
|
|
129
|
+
const pitch = (chairs[chairs.length - 1].x0 - chairs[0].x0) / (chairs.length - 1);
|
|
130
|
+
const first = plan.judo.contests[0];
|
|
131
|
+
const bench = benchOf(chairs, table, pitch, first.y0);
|
|
132
|
+
const built = { plan, chairs, table, pitch, extent: plan.extent, bench };
|
|
133
|
+
plans.set(style, built);
|
|
134
|
+
return built;
|
|
135
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The colours of a judging picture in light and in dark, as custom properties (the 3D view's stylesheet carries them too, for its name cards). */
|
|
2
|
+
export declare const JUDGING_LIGHT = "--ky-ju-bg:#f7f5ef;--ky-ju-ink:#10151b;--ky-ju-muted:#434b54;--ky-ju-row:#ffffff;--ky-ju-rule:#b9b3a3;--ky-ju-badge:#10151b;--ky-ju-badge-ink:#ffffff;--ky-ju-card:#ffffff;--ky-ju-card-ink:#10151b;--ky-ju-card-edge:#10151b;--ky-ju-vacant:#6a717a;--ky-ju-desk:#e9e2d0;--ky-ju-desk-edge:#4a4f56";
|
|
3
|
+
export declare const JUDGING_DARK = "--ky-ju-bg:#0d1218;--ky-ju-ink:#f6f3ea;--ky-ju-muted:#bcc3cb;--ky-ju-row:#18212a;--ky-ju-rule:#475463;--ky-ju-badge:#f6f3ea;--ky-ju-badge-ink:#0d1218;--ky-ju-card:#18212a;--ky-ju-card-ink:#f6f3ea;--ky-ju-card-edge:#f6f3ea;--ky-ju-vacant:#98a1ab;--ky-ju-desk:#6f6859;--ky-ju-desk-edge:#05070a";
|
|
4
|
+
/**
|
|
5
|
+
* The rules of a judging assignment. `:where` keeps the specificity at nothing so that a page's own custom properties win; the picture follows the device and the page like a drawing does, and a `data-theme` of light or
|
|
6
|
+
* dark forces it. The colours are custom properties (`--ky-ju-*`) a page can set.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KYOGIJO_JUDGING_STYLE: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The rules of a judging assignment, light and dark: the picture's ground and ink, the chart's rows, the badge of a seat's number and the name card in a seat. Everything is high contrast on purpose: it is read live, on a tablet,
|
|
2
|
+
// at arm's length. The surface keeps its own rules, which the drawing carries as it always does.
|
|
3
|
+
/** The colours of a judging picture in light and in dark, as custom properties (the 3D view's stylesheet carries them too, for its name cards). */
|
|
4
|
+
export const JUDGING_LIGHT = "--ky-ju-bg:#f7f5ef;--ky-ju-ink:#10151b;--ky-ju-muted:#434b54;--ky-ju-row:#ffffff;--ky-ju-rule:#b9b3a3;--ky-ju-badge:#10151b;--ky-ju-badge-ink:#ffffff;--ky-ju-card:#ffffff;--ky-ju-card-ink:#10151b;--ky-ju-card-edge:#10151b;--ky-ju-vacant:#6a717a;--ky-ju-desk:#e9e2d0;--ky-ju-desk-edge:#4a4f56";
|
|
5
|
+
export const JUDGING_DARK = "--ky-ju-bg:#0d1218;--ky-ju-ink:#f6f3ea;--ky-ju-muted:#bcc3cb;--ky-ju-row:#18212a;--ky-ju-rule:#475463;--ky-ju-badge:#f6f3ea;--ky-ju-badge-ink:#0d1218;--ky-ju-card:#18212a;--ky-ju-card-ink:#f6f3ea;--ky-ju-card-edge:#f6f3ea;--ky-ju-vacant:#98a1ab;--ky-ju-desk:#6f6859;--ky-ju-desk-edge:#05070a";
|
|
6
|
+
/**
|
|
7
|
+
* The rules of a judging assignment. `:where` keeps the specificity at nothing so that a page's own custom properties win; the picture follows the device and the page like a drawing does, and a `data-theme` of light or
|
|
8
|
+
* dark forces it. The colours are custom properties (`--ky-ju-*`) a page can set.
|
|
9
|
+
*/
|
|
10
|
+
export const KYOGIJO_JUDGING_STYLE = `
|
|
11
|
+
:where(.kyogijo){${JUDGING_LIGHT}}
|
|
12
|
+
@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .kyogijo:not([data-theme=light])){${JUDGING_DARK}}}
|
|
13
|
+
:where(html[data-theme=dark] .kyogijo,.kyogijo[data-theme=dark]){${JUDGING_DARK}}
|
|
14
|
+
.kyogijo .ky-ju-ground{fill:var(--ky-ju-bg)}
|
|
15
|
+
.kyogijo .ky-ju-mat{fill:var(--ky-ju-ink);font-family:var(--ky-font);font-weight:800}
|
|
16
|
+
.kyogijo .ky-ju-style{fill:var(--ky-ju-muted);font-family:var(--ky-font);font-weight:700}
|
|
17
|
+
.kyogijo .ky-ju-row{fill:var(--ky-ju-row);stroke:var(--ky-ju-rule);stroke-width:1.5}
|
|
18
|
+
.kyogijo .ky-ju-role{fill:var(--ky-ju-muted);font-family:var(--ky-font);font-weight:700}
|
|
19
|
+
.kyogijo .ky-ju-name{fill:var(--ky-ju-ink);font-family:var(--ky-font);font-weight:800}
|
|
20
|
+
.kyogijo .ky-ju-id{fill:var(--ky-ju-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:700;font-variant-numeric:tabular-nums}
|
|
21
|
+
.kyogijo .ky-ju-vacant{fill:var(--ky-ju-vacant);font-family:var(--ky-font);font-weight:700;font-style:italic}
|
|
22
|
+
.kyogijo .ky-ju-badge{fill:var(--ky-ju-badge);stroke:var(--ky-ju-bg);stroke-width:2}
|
|
23
|
+
.kyogijo .ky-ju-badge-empty{fill:var(--ky-ju-bg);stroke:var(--ky-ju-vacant);stroke-width:2;stroke-dasharray:4 3}
|
|
24
|
+
.kyogijo .ky-ju-badge-n{fill:var(--ky-ju-badge-ink);font-family:var(--ky-font);font-weight:800;text-anchor:middle}
|
|
25
|
+
.kyogijo .ky-ju-badge-empty+.ky-ju-badge-n{fill:var(--ky-ju-vacant)}
|
|
26
|
+
.kyogijo .ky-ju-card{fill:var(--ky-ju-card);stroke:var(--ky-ju-card-edge);stroke-width:1.4}
|
|
27
|
+
.kyogijo .ky-ju-card-name{fill:var(--ky-ju-card-ink);font-family:var(--ky-font);font-weight:800;text-anchor:middle}
|
|
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
|
+
.kyogijo .ky-ju-plan-frame{fill:none;stroke:var(--ky-ju-rule);stroke-width:1.5}
|
|
30
|
+
`.trim();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { JudgingOptions } from "./judging.types.ts";
|
|
2
|
+
import { type KyogijoTurn } from "./turn.dom.ts";
|
|
3
|
+
import { type PresetTable } from "./turn.camera.ts";
|
|
4
|
+
import { type TurnModel, type TurnSettings } from "./turn.model.ts";
|
|
5
|
+
/** How high things stand, in metres (ours: no rule gives them): the table's top, a chair's seat, a name card on the table, the sign over it. */
|
|
6
|
+
export declare const JUDGING_HEIGHTS: Readonly<{
|
|
7
|
+
table: 0.75;
|
|
8
|
+
chair: 0.45;
|
|
9
|
+
card: 0.95;
|
|
10
|
+
sign: 0.7;
|
|
11
|
+
signAbove: 0.25;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* The four viewpoints of a judging view. A card stands upright, so a high eye squashes its type: the broadcast view is from the mat side, so that the judges' table is at the far side and the cards face the eye, at a
|
|
15
|
+
* pitch where the type is still two thirds as tall as it is wide; the others are as every surface's but for the pitch of the corner and the end.
|
|
16
|
+
*/
|
|
17
|
+
export declare const JUDGING_PRESETS: PresetTable;
|
|
18
|
+
/**
|
|
19
|
+
* A judging assignment as a 3D model: the judo floor with every mat's judges' chairs and table, a name card on the table at each seat that has a name and fits one, a number badge at the others, and the mat's name on a sign.
|
|
20
|
+
* A `TurnModel` like any other: `turnMarkupOf` and `mountJudgingTurn` make a view of it. `mat` in the options builds one mat alone (a tab); without it the mats stand side by side.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildJudgingTurn(options: JudgingOptions, settings?: TurnSettings): TurnModel;
|
|
23
|
+
/**
|
|
24
|
+
* Mount a judging assignment as a 3D view in a container: the same view as `mountKyogijoTurn`'s, with the controls, the pointer, the keys and the auto-turn of every 3D view, over the judo floor with each mat's judges'
|
|
25
|
+
* table, the name cards on it and the mat's name over it. Where the browser cannot turn it, the flat picture with its chart is shown.
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts no-run
|
|
28
|
+
* import { mountJudgingTurn } from "@johnmorrisdotca/kyogijo-stadiums/judging";
|
|
29
|
+
* const view = mountJudgingTurn(document.querySelector("#mat")!, { style: "kata", mats: [{ officials: [{ seat: 1, name: "Aiko Mori", id: "J-1042" }] }] });
|
|
30
|
+
* view.set({ yaw: 40 });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function mountJudgingTurn(host: HTMLElement, options: JudgingOptions, settings?: TurnSettings): KyogijoTurn<JudgingOptions>;
|
|
34
|
+
/** A judging assignment as the markup of a 3D view, still until a page's script makes it turn: pure, so a server can make it. */
|
|
35
|
+
export declare function judgingTurnMarkup(options: JudgingOptions, settings?: TurnSettings): string;
|