@johnmorrisdotca/kyogijo-stadiums 0.51.0 → 0.53.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.
@@ -0,0 +1,94 @@
1
+ // A replay on the 3D view, as data: the floor is the replay's frame with the ball or the puck, its trail and the events on it, and every player and official stands up on it as a
2
+ // disc that faces the eye (a circle for the home side, a square for the away side, a triangle for an official, each with its number), the marks of the flat replay stood up. Pure:
3
+ // the same model, timeline, time and look give the same model. `turn.replay.dom.ts` moves it in a page.
4
+ import { esc, num } from "./format.js";
5
+ import { replayBoard } from "./replay.board.js";
6
+ import { drawFrameOf, replayCast, replayPlacer, replaySnapshot } from "./replay.js";
7
+ import { n } from "./turn.kit.js";
8
+ import { hungBoardPlanes, hungBoards } from "./turn.replay.board.js";
9
+ /** Who stands up on a 3D view: the players and the officials. The ball and the puck stay on the floor. */
10
+ export const STANDING = ["home", "away", "official"];
11
+ /** What stands up is in these groups of the flat replay; the 3D view's floor draws the rest (`keepOnFloor`). */
12
+ const STOOD = new Set(["replay-players", "replay-numbers", "replay-officials"]);
13
+ /** Whether a group of the replay stays on the floor of a 3D view: the trails, the passes, the shots, the goals and the ball do, and the players, their numbers and the officials stand up. */
14
+ export const keepOnFloor = (group) => !STOOD.has(group.id);
15
+ /**
16
+ * The size of a disc, in feet: a share of the playing surface's diagonal (the area the view is fitted to, not the ground or the building round it), so that it reads on a rink and on a pitch
17
+ * alike (about 6.5 feet on a hockey rink, 12 on a soccer pitch), times the look's `scale`. It is a mark, not a person: its size says nothing of a player's height.
18
+ */
19
+ export function discSize(area, look = {}) {
20
+ const scale = typeof look.scale === "number" && Number.isFinite(look.scale) && look.scale > 0 ? look.scale : 1;
21
+ return Math.hypot(area.w, area.h) * 0.03 * scale;
22
+ }
23
+ /** The playing surface of a model, in feet: what stands on it says, else the whole floor. */
24
+ export const playingArea = (model) => {
25
+ const area = model.uprights.area;
26
+ return area === undefined ? model.viewBox : { w: area.x1 - area.x0, h: area.y1 - area.y0 };
27
+ };
28
+ /** The custom properties a floor drawing sets on its root (the teams' colours among them): the discs stand outside that SVG and take the same. */
29
+ export function floorVars(floor) {
30
+ const style = /^<svg[^>]*\sstyle="([^"]*)"/.exec(floor)?.[1];
31
+ return style === undefined ? "" : style.split(";").filter((rule) => /^--ky-[\w-]+:/.test(rule)).join(";");
32
+ }
33
+ /** What is drawn on a disc of `d` feet: the flat replay's mark for the side, and the number over it, in the drawing's own classes so that they take its colours, light and dark. */
34
+ export function discMarkup(o, d, sport, theme, vars) {
35
+ const r = d / 2;
36
+ const edge = d * 0.06;
37
+ const shape = o.team === "home"
38
+ ? `<circle class="ky-h" cx="${n(r)}" cy="${n(r)}" r="${n(r - edge)}"/>`
39
+ : o.team === "away"
40
+ ? `<rect class="ky-a" x="${n(edge)}" y="${n(edge)}" width="${n(d - 2 * edge)}" height="${n(d - 2 * edge)}" rx="${n(d * 0.08)}"/>`
41
+ : `<path d="M${n(r)} ${n(edge)}L${n(d - edge)} ${n(d - edge)}L${n(edge)} ${n(d - edge)}Z"/>`;
42
+ const number = o.number === undefined || o.number === "" || o.team === "official" ? "" : `<text class="ky-lt" x="${n(r)}" y="${n(r)}" text-anchor="middle" dominant-baseline="central" font-size="${n(d * 0.46)}">${esc(String(o.number))}</text>`;
43
+ const themed = theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : "";
44
+ return `<g class="kyogijo" data-sport="${esc(sport)}"${themed}${vars === "" ? "" : ` style="${esc(vars)}"`}><g class="ky-pts" style="--w:${n(d * 0.05)}">${shape}</g>${number}</g>`;
45
+ }
46
+ /** The plane of one disc: a billboard that leans to face the eye, its lower edge on the floor when it stands, its middle over (x, y). */
47
+ export function discPlane(o, x, y, d, lift, svg) {
48
+ return { id: `replay-${o.id}`, kind: "player", o: [x - d / 2, y, lift + d], u: [d, 0, 0], v: [0, 0, -d], svg, billboard: true, lean: true };
49
+ }
50
+ /** Put a disc over a new place on the floor, as `discPlane` does: the plane is the caller's, changed in place, so that a page moving many does not make new ones. */
51
+ export function moveDisc(plane, x, y, d, lift) {
52
+ plane.o = [x - d / 2, y, lift + d];
53
+ }
54
+ /** The floor's options for a replay: the model's own, and an id of its own, so that the replay's groups are found in it. */
55
+ export const replayFloorOptions = (model) => ({ ...model.options, id: `${model.id}-replay` });
56
+ /**
57
+ * A 3D view's model with a replay on it at a time: the floor drawn as the replay's frame (without the players when they stand), and a disc for each player and official the timeline
58
+ * tracks, after the model's own planes. One that is not on the surface at the time is still a plane, and says so (`on`), so that the elements stay the same while it plays. Pure.
59
+ */
60
+ export function replayTurnModel(model, timeline, t, look = {}) {
61
+ const stand = look.stand !== false;
62
+ const floorOptions = replayFloorOptions(model);
63
+ // A scoreboard hung over the surface follows the game: its faces are drawn again with the score, the period and the clock at the time.
64
+ const hung = look.board === false ? [] : hungBoards(model);
65
+ const state = hung.length === 0 ? undefined : replayBoard(timeline, t);
66
+ const drawn = new Map();
67
+ if (state !== undefined)
68
+ for (const h of hung)
69
+ for (const plane of hungBoardPlanes(h, state))
70
+ drawn.set(plane.id, plane);
71
+ const base = drawn.size === 0 ? model : { ...model, planes: model.planes.map((p) => drawn.get(p.id) ?? p) };
72
+ const board = state === undefined ? {} : { board: state };
73
+ const floor = drawFrameOf(floorOptions, timeline, t, look, stand ? keepOnFloor : () => true);
74
+ const size = discSize(playingArea(model), look);
75
+ const discs = new Map();
76
+ if (!stand)
77
+ return { model: { ...base, floor }, discs, size, floorOptions, ...board };
78
+ const sport = String(model.options.sport ?? "");
79
+ const theme = model.options.theme;
80
+ const vars = floorVars(floor);
81
+ const lift = model.uprights.lift;
82
+ const snap = replaySnapshot(timeline, t, look);
83
+ const placed = replayPlacer(floorOptions, timeline)(snap.objects.map((o) => [o.x, o.y]));
84
+ const here = new Map(snap.objects.map((o, i) => [o.id, placed[i]]));
85
+ for (const o of replayCast(timeline)) {
86
+ if (!STANDING.includes(o.team))
87
+ continue;
88
+ const at = here.get(o.id);
89
+ discs.set(o.id, { plane: discPlane(o, at?.[0] ?? 0, at?.[1] ?? 0, size, lift, discMarkup(o, size, sport, theme, vars)), on: at !== undefined });
90
+ }
91
+ return { model: { ...base, floor, planes: [...base.planes, ...[...discs.values()].map((disc) => disc.plane)] }, discs, size, floorOptions, ...board };
92
+ }
93
+ /** A number for a test or a page that wants to know where a disc stands: its middle on the floor, in feet. */
94
+ export const discAt = (plane) => `${num(plane.o[0] + plane.u[0] / 2)} ${num(plane.o[1])}`;
@@ -11,6 +11,28 @@ export interface Boards {
11
11
  score: [number, number];
12
12
  /** Each lit panel has a halo under it (the venue is lit as at night). */
13
13
  glow: boolean;
14
+ /** What a replay adds to the scoreboard's face: the period and the game clock. Missing, the face is the score alone, as it always was. */
15
+ live?: BoardLive;
16
+ }
17
+ /** The period and the clock a scoreboard shows while a game is replayed on it. */
18
+ export interface BoardLive {
19
+ /** The period (1, 2, 3...), or `undefined` for a game with none. */
20
+ period?: number;
21
+ /** The clock as it is written, `19:20`: digits and one colon. */
22
+ clock: string;
23
+ }
24
+ /** The scoreboard hung over a surface, with what it is made from: a replay draws its faces again with the score and the clock of the time it is at. */
25
+ export interface HungBoard {
26
+ id: string;
27
+ box: {
28
+ x: number;
29
+ y: number;
30
+ w: number;
31
+ h: number;
32
+ };
33
+ /** How high the surface stands, in feet. */
34
+ lift: number;
35
+ look: Boards;
14
36
  }
15
37
  /** The package's own colours of light: amber, blue and a white with a little blue in it. */
16
38
  export declare const BOARD_COLORS: readonly [string, string, string];
@@ -72,6 +94,8 @@ export declare function boardSize(w: number, h: number): {
72
94
  };
73
95
  /** Whether a sport has a scoreboard hung over its surface: a ballpark's stands behind the outfield, so it has none here. */
74
96
  export declare const hangsOver: (sport: string | undefined) => boolean;
97
+ /** A clock a scoreboard may write: one or two digits, a colon and two digits. Anything else is left off (the text goes into a drawing). */
98
+ export declare const safeClock: (value: unknown) => string;
75
99
  /**
76
100
  * The scoreboard hung over the middle of a surface: a four sided board with its score on every face, a ring of display under it, a cap on top and a strut up to the rafters. `box` is the surface, `lift` how high the surface stands.
77
101
  * Its faces look outwards, each the right way round for an eye outside it.
@@ -136,9 +136,10 @@ export function boardSize(w, h) {
136
136
  export const hangsOver = (sport) => sport !== "mlb";
137
137
  /** The segments of a seven-segment digit, a to g, that are lit for each digit. */
138
138
  const SEGMENTS = ["abcdef", "bc", "abged", "abgcd", "fgbc", "afgcd", "afgedc", "abc", "abcdefg", "abcdfg"];
139
- /** A number of one or two digits in seven segments, `high` feet tall, from (x, y) at its top left: the path and the width it takes. */
140
- function digits(value, x, y, high) {
141
- const text = String(Math.min(99, Math.max(0, Math.trunc(value))));
139
+ /** How wide a colon is, as a share of the height of the digits beside it. */
140
+ const COLON = 0.3;
141
+ /** A text of digits and colons in seven segments, `high` feet tall, from (x, y) at its top left: the path and the width it takes. */
142
+ function segments(text, x, y, high) {
142
143
  const w = high * 0.56;
143
144
  const t = high * 0.13;
144
145
  const half = high / 2;
@@ -152,13 +153,46 @@ function digits(value, x, y, high) {
152
153
  g: [t * 0.6, half - t / 2, w - t * 1.2, t],
153
154
  };
154
155
  let d = "";
156
+ let at = x;
155
157
  [...text].forEach((ch, i) => {
156
- for (const s of SEGMENTS[Number(ch)]) {
157
- const [rx, ry, rw, rh] = rects[s];
158
- d += `M${n(x + i * (w + t) + rx)} ${n(y + ry)}h${n(rw)}v${n(rh)}h${n(-rw)}z`;
158
+ if (i > 0)
159
+ at += t;
160
+ if (ch === ":") {
161
+ const cw = high * COLON;
162
+ const dot = t * 1.1;
163
+ for (const cy of [high * 0.3, high * 0.7])
164
+ d += `M${n(at + (cw - dot) / 2)} ${n(y + cy - dot / 2)}h${n(dot)}v${n(dot)}h${n(-dot)}z`;
165
+ at += cw;
166
+ return;
167
+ }
168
+ for (const seg of SEGMENTS[Number(ch)] ?? "") {
169
+ const [rx, ry, rw, rh] = rects[seg];
170
+ d += `M${n(at + rx)} ${n(y + ry)}h${n(rw)}v${n(rh)}h${n(-rw)}z`;
159
171
  }
172
+ at += w;
160
173
  });
161
- return { d, width: text.length * w + (text.length - 1) * t };
174
+ return { d, width: at - x };
175
+ }
176
+ /** A number of one or two digits in seven segments, `high` feet tall, from (x, y) at its top left: the path and the width it takes. */
177
+ const digits = (value, x, y, high) => segments(String(Math.min(99, Math.max(0, Math.trunc(value)))), x, y, high);
178
+ /** A clock a scoreboard may write: one or two digits, a colon and two digits. Anything else is left off (the text goes into a drawing). */
179
+ export const safeClock = (value) => (typeof value === "string" && /^\d{1,2}:\d{2}$/.test(value) ? value : "");
180
+ /** The row over the score while a game is replayed: the period in a small frame and the game clock beside it, in seven segments, centred on the face between its end blocks. A game with no period has the clock alone. */
181
+ function liveRow(w, h, room, live, color) {
182
+ const high = Math.min(h * 0.24, room / 6);
183
+ const y = h * 0.18;
184
+ const clockWidth = segments(live.clock, 0, y, high).width;
185
+ const period = live.period === undefined ? "" : String(Math.min(99, Math.max(0, Math.trunc(live.period))));
186
+ const pad = high * 0.3;
187
+ const frame = period === "" ? 0 : segments(period, 0, y, high).width + pad * 2;
188
+ const gap = period === "" ? 0 : high * 0.5;
189
+ const x0 = (w - (frame + gap + clockWidth)) / 2;
190
+ let svg = "";
191
+ if (period !== "") {
192
+ const line = high * 0.07;
193
+ svg += `<path class="u-led" fill="none" stroke="${color}" stroke-width="${n(line)}" d="M${n(x0)} ${n(y - pad * 0.6)}h${n(frame)}v${n(high + pad * 1.2)}h${n(-frame)}z"/><path class="u-led" fill="${color}" d="${segments(period, x0 + pad, y, high).d}"/>`;
194
+ }
195
+ return svg + `<path class="u-led" fill="${color}" d="${segments(live.clock, x0 + frame + gap, y, high).d}"/>`;
162
196
  }
163
197
  /** The face of the main board, `w` by `h` feet: a dark screen with a band of light along its top and bottom, a block of colour at each end, and the score between them. */
164
198
  function boardFace(w, h, look) {
@@ -171,18 +205,21 @@ function boardFace(w, h, look) {
171
205
  const block = Math.min(w * 0.2, h * 0.5);
172
206
  const [yb, hb] = [h * 0.28, h * 0.44];
173
207
  svg += `<path class="u-led" fill="${c0}" d="M${n(edge * 2)} ${n(yb)}h${n(block)}v${n(hb)}h${n(-block)}z"/><path class="u-led" fill="${c1}" d="M${n(w - edge * 2 - block)} ${n(yb)}h${n(block)}v${n(hb)}h${n(-block)}z"/>`;
174
- // The score: two numbers and a dash, as large as the face between the blocks lets them be.
208
+ // The score: two numbers and a dash, as large as the face between the blocks lets them be. A replayed game also has the clock and the period over it, so the score is a little smaller and lower.
175
209
  const room = w - 2 * (block + edge * 4);
210
+ const live = look.live !== undefined && safeClock(look.live.clock) !== "" ? look.live : undefined;
176
211
  const digitCount = String(look.score[0]).length + String(look.score[1]).length;
177
212
  const dash = 0.4;
178
- const high = Math.min(h * 0.5, room / (digitCount * 0.56 + (digitCount - 2) * 0.13 + dash + 0.5));
213
+ const high = Math.min(h * (live === undefined ? 0.5 : 0.33), room / (digitCount * 0.56 + (digitCount - 2) * 0.13 + dash + 0.5));
179
214
  const [home, away] = [digits(look.score[0], 0, 0, high), digits(look.score[1], 0, 0, high)];
180
215
  const gap = high * 0.3;
181
216
  const total = home.width + gap + high * dash + gap + away.width;
182
217
  const x0 = (w - total) / 2;
183
- const y0 = (h - high) / 2;
218
+ const y0 = live === undefined ? (h - high) / 2 : h * 0.46;
184
219
  const [h0, a0] = [digits(look.score[0], x0, y0, high), digits(look.score[1], x0 + home.width + gap * 2 + high * dash, y0, high)];
185
220
  svg += `<path class="u-led" fill="${c2}" d="${h0.d}${a0.d}M${n(x0 + home.width + gap)} ${n(y0 + high / 2 - high * 0.065)}h${n(high * dash)}v${n(high * 0.13)}h${n(-high * dash)}z"/>`;
221
+ if (live !== undefined)
222
+ svg += liveRow(w, h, room, live, c2);
186
223
  return svg;
187
224
  }
188
225
  /**
package/dist/venue.js CHANGED
@@ -227,7 +227,7 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
227
227
  // The scoreboard hung over the middle of the surface (a ballpark has none, and a hall's own scoreboards stand in its place).
228
228
  if (boards?.scoreboard === true && v.noCentre !== true && hangsOver(String(o.sport))) {
229
229
  const planes = centreBoard("centre-board", stands.box, lift, boards);
230
- items.push({ id: "centre-board", kind: "hung-board", at: centre, height: topOf(planes), planes });
230
+ items.push({ id: "centre-board", kind: "hung-board", at: centre, height: topOf(planes), planes, hung: { id: "centre-board", box: stands.box, lift, look: boards } });
231
231
  }
232
232
  if (wallSeats.length > 0)
233
233
  items.push({ id: "wall-seats", kind: "wall-seats", at: centre, height: topOf(wallSeats), planes: wallSeats });
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export declare const VERSION = "0.51.0";
2
+ export declare const VERSION = "0.53.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export const VERSION = "0.51.0";
2
+ export const VERSION = "0.53.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "Kyogijo Stadiums 競技場, 40 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, the MMA cage, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
5
5
  "keywords": [
6
6
  "sports",
@@ -67,6 +67,7 @@
67
67
  "type": "module",
68
68
  "sideEffects": [
69
69
  "./dist/element-define.js",
70
+ "./dist/turn.replay.dom.js",
70
71
  "./dist/frames.providers.js",
71
72
  "./dist/full.js",
72
73
  "./dist/arenas.js",
@@ -348,6 +349,8 @@
348
349
  "screenshots:flythrough": "pnpm site && node scripts/readme-flythrough.mjs",
349
350
  "test:readme": "pnpm build && node scripts/check-readme-examples.mjs",
350
351
  "docs:make": "node scripts/docs.mjs",
352
+ "docs:site": "pnpm site",
353
+ "docs:check": "pnpm build && node scripts/docs-check.mjs",
351
354
  "parks": "node scripts/build-parks.mjs",
352
355
  "arenas": "node scripts/build-arenas.mjs",
353
356
  "arenas:fetch": "node scripts/fetch-arenas.mjs",