@johnmorrisdotca/kyogijo-stadiums 0.51.0 → 0.52.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 CHANGED
@@ -7,6 +7,18 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.52.0] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - **A replay plays on the 3D view (KYOGIJO-STADIUMS-23)** ([the replay in 3D](./docs/replay.md#in-3d)): `view.replay(timeline, settings?)` on the handle of `mountKyogijoTurn` gives the same controller as `mount.replay`, and the rink can be turned, tipped and zoomed while the game plays. The players and officials stand up as discs that face the eye (the flat replay's circles, squares, triangles and numbers, turned up), and the ball or the puck, its trail, the passes, the shots and the goals stay on the floor. `stand: false` keeps the players flat. On the view, Space plays and pauses, comma and full stop step, and the arrow keys still turn it; on the replay's controls the arrows step. Nothing runs while it is paused, hidden or out of sight, the auto-turn and the replay share one animation frame, only what moves is written, and destroy leaves nothing; browser tests hold each of these. `replayTurnModel` is the pure model of a frame, and `<kyogijo-field turn>` with a `timeline` plays it on its 3D view.
15
+ - **An example game (KYOGIJO-STADIUMS-25)** ([example games](./docs/example-games.md)): `examples/games/canada-japan.json`, a whole made-up game of Canada and Japan, three periods, shaped from a public NHL play-by-play with every team, player, place, time and the result changed. It is in the repository and on the demo's site, not in the npm package.
16
+ - The demo: a section, "A game on the ice, in 3D", plays the example game in 3D or flat, with a button for each period, the score on the board as it goes, and the code with a Copy button; the arena at the top of the page has **Play a game**, which loads the game when it is first pressed and plays it on the arena's ice in front of its crowd.
17
+
18
+ ### Changed
19
+
20
+ - The flat replay's page side is a painter and a driver that the 3D replay shares (`replay.dom.ts`); the flat replay behaves as before.
21
+
10
22
  ## [0.51.0] - 2026-11-03
11
23
 
12
24
  ### Added
package/README.md CHANGED
@@ -36,7 +36,7 @@ Kyogijo Stadiums draws the places a game is played on, with the score of the gam
36
36
 
37
37
  ## The showcase
38
38
 
39
- Real buildings drawn in the outline OpenStreetMap maps for them, each with a plausible bowl of the seats, tiers and roof its sources give. In the first three rows the picture in the middle turns, slowly, and the others stand still; the last row is still.
39
+ Real buildings drawn in the outline OpenStreetMap maps for them, each with a plausible bowl of the seats, tiers and roof its sources give. In the rows of arenas, of halls and of districts the picture in the middle turns, slowly, and the others stand still; the other rows are still.
40
40
 
41
41
  **Arenas and stadiums** ([real arenas](./docs/venues.md), [real stadiums](./docs/stadiums.md))
42
42
 
@@ -143,6 +143,34 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
143
143
  </tr>
144
144
  </table>
145
145
 
146
+ **A game on the ice** ([the replay in 3D](./docs/replay.md#in-3d), [the example game](./docs/example-games.md))
147
+
148
+ <table align="center">
149
+ <tr>
150
+ <td align="center" valign="top">
151
+ <picture>
152
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-3d-desk-dark.webp">
153
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-3d-desk-light.webp" alt="A made-up game of Canada and Japan replayed on the rink in 3D from the main camera: the players standing up as discs with their numbers, red circles and blue squares, the score hung over centre ice, and a shot's arrow and a whistle's cross on the ice." width="250">
154
+ </picture>
155
+ <br><em>A made-up game, replayed in 3D</em>
156
+ </td>
157
+ <td align="center" valign="top">
158
+ <picture>
159
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-game-3d-desk-dark.webp">
160
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-game-3d-desk-light.webp" alt="The same game on the ice of a real arena, in front of its crowd: the players standing as discs on the rink inside two tiers of seats." width="250">
161
+ </picture>
162
+ <br><em>The same game in an arena, in front of the crowd</em>
163
+ </td>
164
+ <td align="center" valign="top">
165
+ <picture>
166
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-flat-desk-dark.webp">
167
+ <img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-flat-desk-light.webp" alt="The same moment of the game flat: each side's flag and name at the end it defends, the score panel at centre ice, the players as circles and squares with their numbers, a goal's star at the net." width="250">
168
+ </picture>
169
+ <br><em>The same moment, flat</em>
170
+ </td>
171
+ </tr>
172
+ </table>
173
+
146
174
  **Surfaces** ([the surfaces](./docs/surfaces.md), [score and layout](./docs/score.md))
147
175
 
148
176
  <table align="center">
@@ -217,7 +245,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
217
245
  - **A 3D view** of any surface or venue that turns by mouse, touch or keys, with the roof and the walls to switch and a quality level for every device. [The 3D view](./docs/turn.md), [quality](./docs/quality.md).
218
246
  - **Circuits**: a famous race track as OpenStreetMap maps it, the Las Vegas Strip Circuit first, with its turns numbered and its pit building standing in 3D. [Circuits](./docs/circuits.md).
219
247
  - **Tournaments**: several surfaces on one floor, who judges where, boards laid over a surface. [Layouts](./docs/multi.md), [judging](./docs/judging.md), [boards](./docs/boards.md).
220
- - **Data on a surface**: points, heat maps, shot and pass charts, team shapes and a replay, in 48 providers' coordinate frames. [Layers](./docs/layers.md), [frames](./docs/frames.md), [replay](./docs/replay.md).
248
+ - **Data on a surface**: points, heat maps, shot and pass charts, team shapes and a replay, flat or in 3D with the players standing on the ice, in 48 providers' coordinate frames. [Layers](./docs/layers.md), [frames](./docs/frames.md), [replay](./docs/replay.md), [an example game](./docs/example-games.md).
221
249
  - **Looks and flags**: eight named looks, a flag or crest on each half, trophies and crests from Dougu Sports, flags from Hata, light and dark. [Theming](./docs/theming.md), [the pageant](./docs/pageant.md).
222
250
  - **Four ways in**: a function that returns SVG text, a helper that draws into a container, a custom element, a frame for pages with no script; renderers for a canvas, d3, Observable Plot, GeoJSON and PNG. [Entry points](./docs/entry-points.md), [renderers](./docs/renderers.md).
223
251
  - **Accessible and small**: each drawing is an `img` with a title and a description, in English or Japanese; no dependencies, no network requests, nothing stored.
package/catalogue.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "generatedAt": "2026-11-03",
4
4
  "package": {
5
5
  "name": "@johnmorrisdotca/kyogijo-stadiums",
6
- "version": "0.51.0"
6
+ "version": "0.52.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
@@ -3,7 +3,7 @@ export const CATALOGUE_DATA = {
3
3
  "generatedAt": "2026-11-03",
4
4
  "package": {
5
5
  "name": "@johnmorrisdotca/kyogijo-stadiums",
6
- "version": "0.51.0"
6
+ "version": "0.52.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
package/dist/element.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type KyogijoReplay } from "./mount.ts";
2
2
  import { type KyogijoTurn } from "./turn.dom.ts";
3
+ import "./turn.replay.dom.ts";
3
4
  import type { TurnSettings } from "./turn.model.ts";
4
5
  import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
5
6
  import type { ScoreOptions, SurfaceOptions } from "./types.ts";
package/dist/element.js CHANGED
@@ -9,13 +9,15 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
- var _KyogijoField_instances, _KyogijoField_mount, _KyogijoField_host, _KyogijoField_options, _KyogijoField_theme, _KyogijoField_timeline, _KyogijoField_replaySettings, _KyogijoField_replay, _KyogijoField_replayOf, _KyogijoField_turn, _KyogijoField_turnSettings, _KyogijoField_attach, _KyogijoField_draw;
12
+ var _KyogijoField_instances, _KyogijoField_mount, _KyogijoField_host, _KyogijoField_options, _KyogijoField_theme, _KyogijoField_timeline, _KyogijoField_replaySettings, _KyogijoField_replay, _KyogijoField_replayOf, _KyogijoField_turn, _KyogijoField_turnSettings, _KyogijoField_attach, _KyogijoField_draw, _KyogijoField_follow;
13
13
  import { SPORTS } from "./draw.js";
14
14
  import { leagueIds } from "./leagues.js";
15
15
  import { parseRange } from "./ranges.names.js";
16
16
  import { mountKyogijo } from "./mount.js";
17
17
  import { TURN_PRESET_NAMES } from "./turn.camera.js";
18
18
  import { mountKyogijoTurn } from "./turn.dom.js";
19
+ // A timeline on the element's 3D view plays with the replay's code for the view.
20
+ import "./turn.replay.dom.js";
19
21
  /** The tag of the element. */
20
22
  export const KYOGIJO_TAG = "kyogijo-field";
21
23
  /** The attributes the element reads. */
@@ -355,7 +357,8 @@ export class KyogijoField extends Base {
355
357
  }
356
358
  }
357
359
  _KyogijoField_mount = new WeakMap(), _KyogijoField_host = new WeakMap(), _KyogijoField_options = new WeakMap(), _KyogijoField_theme = new WeakMap(), _KyogijoField_timeline = new WeakMap(), _KyogijoField_replaySettings = new WeakMap(), _KyogijoField_replay = new WeakMap(), _KyogijoField_replayOf = new WeakMap(), _KyogijoField_turn = new WeakMap(), _KyogijoField_turnSettings = new WeakMap(), _KyogijoField_instances = new WeakSet(), _KyogijoField_attach = function _KyogijoField_attach(again) {
358
- if (__classPrivateFieldGet(this, _KyogijoField_mount, "f") === undefined || __classPrivateFieldGet(this, _KyogijoField_timeline, "f") === undefined)
360
+ const on = __classPrivateFieldGet(this, _KyogijoField_turn, "f") ?? __classPrivateFieldGet(this, _KyogijoField_mount, "f");
361
+ if (on === undefined || __classPrivateFieldGet(this, _KyogijoField_timeline, "f") === undefined)
359
362
  return;
360
363
  const settings = replaySettingsFromAttributes((name) => this.getAttribute(name), __classPrivateFieldGet(this, _KyogijoField_replaySettings, "f"));
361
364
  if (__classPrivateFieldGet(this, _KyogijoField_replay, "f") !== undefined && __classPrivateFieldGet(this, _KyogijoField_replayOf, "f") === __classPrivateFieldGet(this, _KyogijoField_timeline, "f") && !again) {
@@ -368,7 +371,7 @@ _KyogijoField_mount = new WeakMap(), _KyogijoField_host = new WeakMap(), _Kyogij
368
371
  const from = __classPrivateFieldGet(this, _KyogijoField_replay, "f") === undefined || __classPrivateFieldGet(this, _KyogijoField_replayOf, "f") !== __classPrivateFieldGet(this, _KyogijoField_timeline, "f") ? undefined : __classPrivateFieldGet(this, _KyogijoField_replay, "f").state();
369
372
  __classPrivateFieldGet(this, _KyogijoField_replay, "f")?.destroy();
370
373
  __classPrivateFieldSet(this, _KyogijoField_replayOf, __classPrivateFieldGet(this, _KyogijoField_timeline, "f"), "f");
371
- __classPrivateFieldSet(this, _KyogijoField_replay, __classPrivateFieldGet(this, _KyogijoField_mount, "f").replay(__classPrivateFieldGet(this, _KyogijoField_timeline, "f"), from === undefined ? settings : { ...settings, start: from.t - from.start }), "f");
374
+ __classPrivateFieldSet(this, _KyogijoField_replay, on.replay(__classPrivateFieldGet(this, _KyogijoField_timeline, "f"), from === undefined ? settings : { ...settings, start: from.t - from.start }), "f");
372
375
  }, _KyogijoField_draw = function _KyogijoField_draw() {
373
376
  if (__classPrivateFieldGet(this, _KyogijoField_host, "f") === undefined || !this.isConnected)
374
377
  return;
@@ -377,7 +380,7 @@ _KyogijoField_mount = new WeakMap(), _KyogijoField_host = new WeakMap(), _Kyogij
377
380
  if (options.theme === undefined && (page === "light" || page === "dark"))
378
381
  options.theme = page;
379
382
  if (turnWanted((name) => this.getAttribute(name))) {
380
- // The 3D view takes the container over from the flat drawing; a replay does not play on it.
383
+ // The 3D view takes the container over from the flat drawing; a replay plays on it, the players standing up.
381
384
  if (__classPrivateFieldGet(this, _KyogijoField_mount, "f") !== undefined) {
382
385
  __classPrivateFieldGet(this, _KyogijoField_mount, "f").destroy();
383
386
  __classPrivateFieldSet(this, _KyogijoField_mount, undefined, "f");
@@ -385,20 +388,28 @@ _KyogijoField_mount = new WeakMap(), _KyogijoField_host = new WeakMap(), _Kyogij
385
388
  __classPrivateFieldSet(this, _KyogijoField_replayOf, undefined, "f");
386
389
  }
387
390
  const settings = turnSettingsFromAttributes((name) => this.getAttribute(name), __classPrivateFieldGet(this, _KyogijoField_turnSettings, "f"));
388
- if (__classPrivateFieldGet(this, _KyogijoField_turn, "f") === undefined)
391
+ if (__classPrivateFieldGet(this, _KyogijoField_turn, "f") === undefined) {
389
392
  __classPrivateFieldSet(this, _KyogijoField_turn, mountKyogijoTurn(__classPrivateFieldGet(this, _KyogijoField_host, "f"), options, settings), "f");
393
+ __classPrivateFieldSet(this, _KyogijoField_replay, undefined, "f");
394
+ __classPrivateFieldSet(this, _KyogijoField_replayOf, undefined, "f");
395
+ }
390
396
  else
391
397
  __classPrivateFieldGet(this, _KyogijoField_turn, "f").update(options);
398
+ __classPrivateFieldGet(this, _KyogijoField_instances, "m", _KyogijoField_follow).call(this);
392
399
  return;
393
400
  }
394
401
  if (__classPrivateFieldGet(this, _KyogijoField_turn, "f") !== undefined) {
395
402
  __classPrivateFieldGet(this, _KyogijoField_turn, "f").destroy();
396
403
  __classPrivateFieldSet(this, _KyogijoField_turn, undefined, "f");
404
+ __classPrivateFieldSet(this, _KyogijoField_replay, undefined, "f");
405
+ __classPrivateFieldSet(this, _KyogijoField_replayOf, undefined, "f");
397
406
  }
398
407
  if (__classPrivateFieldGet(this, _KyogijoField_mount, "f") === undefined)
399
408
  __classPrivateFieldSet(this, _KyogijoField_mount, mountKyogijo(__classPrivateFieldGet(this, _KyogijoField_host, "f"), options), "f");
400
409
  else
401
410
  __classPrivateFieldGet(this, _KyogijoField_mount, "f").update(options);
411
+ __classPrivateFieldGet(this, _KyogijoField_instances, "m", _KyogijoField_follow).call(this);
412
+ }, _KyogijoField_follow = function _KyogijoField_follow() {
402
413
  if (__classPrivateFieldGet(this, _KyogijoField_timeline, "f") === undefined) {
403
414
  __classPrivateFieldGet(this, _KyogijoField_replay, "f")?.destroy();
404
415
  __classPrivateFieldSet(this, _KyogijoField_replay, undefined, "f");
@@ -62,6 +62,7 @@ export function mountJudgingBroadcast(host, options, settings = {}) {
62
62
  direction: () => turn.direction(),
63
63
  setDirection: (direction) => turn.setDirection(direction),
64
64
  togglePlay: () => turn.togglePlay(),
65
+ replay: (timeline, replaySettings) => turn.replay(timeline, replaySettings),
65
66
  get quality() {
66
67
  return turn.quality;
67
68
  },
package/dist/replay.d.ts CHANGED
@@ -17,6 +17,8 @@ export declare function replayInfo(timeline: ReplayTimeline): {
17
17
  events: number;
18
18
  step: number;
19
19
  };
20
+ /** Everything a timeline tracks, each as it first appears (its id, team, number and label), in that order. */
21
+ export declare function replayCast(timeline: ReplayTimeline): readonly ReplayObject[];
20
22
  /** The times of the frames, and of the events, in order: the moments a step can land on. */
21
23
  export declare function replayTimes(timeline: ReplayTimeline): readonly number[];
22
24
  /** The events of a timeline, in time order. */
@@ -151,6 +153,8 @@ export declare function replayPlacer(options: SurfaceOptions | ScoreOptions, tim
151
153
  * ```
152
154
  */
153
155
  export declare function drawFrame(options: SurfaceOptions | ScoreOptions, timeline: ReplayTimeline, t: number, look?: ReplayLook): string;
156
+ /** `drawFrame` with only the groups `keep` says yes to: a 3D view keeps the players off its floor, where they stand up instead. */
157
+ export declare function drawFrameOf(options: SurfaceOptions | ScoreOptions, timeline: ReplayTimeline, t: number, look?: ReplayLook, keep?: (group: ReplayGroup) => boolean): string;
154
158
  /** The prefix of the ids in a replay's drawing when the options give none: a hash of the options and of the timeline's size, not of the time. */
155
159
  export declare const replayId: (options: SurfaceOptions | ScoreOptions, timeline: ReplayTimeline) => string;
156
160
  /**
@@ -1,6 +1,7 @@
1
1
  import { type ReplayController } from "./replay.control.ts";
2
- import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
3
- import type { ScoreOptions, SurfaceOptions } from "./types.ts";
2
+ import { type ReplayGroup } from "./replay.ts";
3
+ import type { ReplayLook, ReplaySettings, ReplayTimeline } from "./replay.types.ts";
4
+ import type { Language, ScoreOptions, SurfaceOptions } from "./types.ts";
4
5
  /** The controls' stylesheet: written once beside the controls, scoped to them, in the page's own colours. */
5
6
  export declare const REPLAY_STYLE: string;
6
7
  /** A replay attached to a container: the controller, the stage the drawing goes in, and how to draw it again. */
@@ -13,6 +14,70 @@ export interface ReplayView {
13
14
  /** Stop the clock and the listeners, and empty the container. */
14
15
  destroy(): void;
15
16
  }
17
+ /** What a painter is given: where the drawing is, what it was drawn from, and how to draw it whole again. */
18
+ export interface PainterSource {
19
+ doc: Document;
20
+ timeline: ReplayTimeline;
21
+ look: ReplayLook;
22
+ /** The drawing the replay's groups are in (`svg.kyogijo`), or `null` before it is written. */
23
+ svg(): SVGSVGElement | null;
24
+ /** The options it was drawn with, its `id` among them. */
25
+ options(): SurfaceOptions | ScoreOptions;
26
+ language(): Language;
27
+ /** Which groups of the replay are in this drawing: all when missing (a 3D view stands the players up and leaves the rest on the floor). */
28
+ keep?: (group: ReplayGroup) => boolean;
29
+ /** Write the drawing whole again at a time; the painter binds itself to it afterwards. */
30
+ redraw(t: number): void;
31
+ }
32
+ /** The painter of a replay's drawing: `paint` moves the marks to a time; `bind` matches the elements of a drawing just written at `t`. */
33
+ export interface ReplayPainter {
34
+ paint(t: number): void;
35
+ bind(t: number): void;
36
+ }
37
+ /**
38
+ * The painter of a replay: it moves the marks that move and nothing else. A mark that comes or goes (an object that enters, an event that begins) draws the replay's own group
39
+ * again, never the surface; a group whose marks cannot be matched to elements one for one is drawn again each time.
40
+ */
41
+ export declare function replayPainter(source: PainterSource): ReplayPainter;
42
+ /** What a driver is given: how to paint a time, where its keys are heard, and what it labels. */
43
+ export interface DriverSource {
44
+ /** Move the drawing to a time. Called on every change of the clock. */
45
+ paint(t: number): void;
46
+ language: Language;
47
+ /** The element whose keys play, step and seek (its descendants too): the stage and the bar in a flat replay, the bar alone over a 3D view. */
48
+ keysOn: HTMLElement;
49
+ /** The element that says what the replay is doing (`data-state`, `data-motion`). */
50
+ holder: HTMLElement;
51
+ /** The element that names the replay and points at its key help (the flat drawing's stage), if any. */
52
+ labelled?: HTMLElement;
53
+ /** The element whose being in sight lets the clock run: no frame is asked for while it is scrolled out of sight. */
54
+ seen?: Element;
55
+ /** The key help: `keys` and `keysStep` by default, `keysTurn` and `keysTurnStep` over a 3D view. */
56
+ help?: "flat" | "turn";
57
+ /** Where frames are asked for: the page's `requestAnimationFrame` by default; a 3D view lends its own, so that a turning view and its replay share one frame. */
58
+ frames?: {
59
+ request(callback: FrameRequestCallback): number;
60
+ cancel(id: number): void;
61
+ };
62
+ }
63
+ /** The page's side of a replay without its drawing: the clock on animation frames, the controls, the keys, the live region, a hidden page and reduced motion. */
64
+ export interface ReplayDriver {
65
+ controller: ReplayController;
66
+ /** The style, the key help, the bar and the live region, in that order, for the caller to put in the page. */
67
+ parts: {
68
+ style: HTMLStyleElement;
69
+ hint: HTMLElement;
70
+ bar: HTMLElement;
71
+ live: HTMLElement;
72
+ };
73
+ /** The words changed: the controls are made again. */
74
+ setLanguage(language: Language): void;
75
+ /** The play, pause or step the keys of a 3D view's stage ask for, by the names of the keys: the same moves as the bar's keys. */
76
+ key(event: KeyboardEvent, scope: "stage" | "bar"): boolean;
77
+ destroy(): void;
78
+ }
79
+ /** Make the clock, the controls and the keys of a replay. Nothing runs until the reader (or `autoplay`) plays it, and nothing runs while it is paused, hidden or out of sight. */
80
+ export declare function replayDriver(doc: Document, timeline: ReplayTimeline, settings: ReplaySettings, source: DriverSource): ReplayDriver;
16
81
  /**
17
82
  * Attach a replay to a container: the stage with the drawing, the controls under it, the keyboard and the live region. The drawing is written once
18
83
  * and then only the marks that move are moved, on `requestAnimationFrame`; a mark that comes or goes (an object that enters, an event that