@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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.53.0] - 2026-11-03
11
+
12
+ - **The arena's scoreboard shows the game being played (KYOGIJO-STADIUMS-35)** ([the scoreboard over the arena](./docs/replay.md#the-scoreboard-over-the-arena-follows-the-game)): while a replay plays on a 3D view whose venue has its LED boards on, the scoreboard hung over centre ice shows the replay's score, period and game clock instead of the fixed score of its options. `replayBoard(timeline, t)` (pure, from `replay`) works the board out from the timeline's goals and its new optional `periods` (`start`, `end`, `number`, `length`) and `clock` (`down` or `up`). The board is written when it changes: at once for a goal, a period, a seek, a step or a pause; the clock at most once a second while playing; nothing runs while paused, hidden or out of sight. Without a replay, or after `destroy`, the board is as before, and `view.replay(timeline, { board: false })` leaves it so. The demo's hero passes the example game's periods (a period is twenty minutes on the board's clock). About 4 kB more source for the 3D replay (its budget in `replay.budget.test.js` is 24 kB now).
13
+
14
+ - Tests and docs, no change to the package: `e2e/centre.demo.mjs` holds that a drag cannot take the 3D view's stadium out of sight (it has no pan, so no Centre button) and that Reset and Home bring it back, and `docs/turn.md` says so.
15
+ - The README opens with the package's thumbnail, and the demo carries it as its link preview (og:image) and publishes the card, a dark card and an icon on Pages at `thumbnail.png`, `thumbnail-dark.png` and `icon.png`.
16
+ - Docs only (MODULES-247): twelve guides in `docs/guides/` (getting started, a score on a surface, looks and colours, embedding, frameworks, real venues, data on a surface, the 3D view, accessibility and languages, architecture, troubleshooting, upgrading), each with a Japanese twin in `docs/guides/ja/`, in the docs site's contents as its first group (`GUIDES` in `scripts/docs-nav.mjs`; the twins are pages of their own, `guides-ja-<name>.html`); `pnpm docs:site` (the docs site is built by `pnpm site`) and `pnpm docs:check` (`scripts/docs-check.mjs`: the guides, the README, the changelog, the tests of the pages and every example against the built package), run by a `docs` job of the CI; the README has a Guides section and more examples (Angular, a CDN module, a frame, a server, layers and zones, Japanese, GeoJSON and PNG, TypeScript and a bundler); `demo/builder.html`, the way into the stage's Embed section, with `e2e/builder.demo.mjs`. The examples of the guides are run by `pnpm test:readme` and `src/docs.examples.test.js`.
17
+
18
+ ## [0.52.0] - 2026-11-03
19
+
20
+ ### Added
21
+
22
+ - **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.
23
+ - **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.
24
+ - 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.
25
+
26
+ ### Changed
27
+
28
+ - 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.
29
+
10
30
  ## [0.51.0] - 2026-11-03
11
31
 
12
32
  ### Added
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <p align="center"><a href="https://johnmorrisdotca.github.io/kyogijo-stadiums/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/thumbnail-dark.png"><img src="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/thumbnail.png" alt="Kyogijo Stadiums: a ballpark, an NHL rink and an NBA court, each drawn to scale as SVG." width="1200"></picture></a></p>
2
+
1
3
  <h1 align="center">Kyogijo Stadiums <sub>競技場</sub></h1>
2
4
 
3
5
  <p align="center"><strong>Sports surfaces as SVG, drawn to scale, with the score on them.</strong><br>
@@ -36,7 +38,7 @@ Kyogijo Stadiums draws the places a game is played on, with the score of the gam
36
38
 
37
39
  ## The showcase
38
40
 
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.
41
+ 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
42
 
41
43
  **Arenas and stadiums** ([real arenas](./docs/venues.md), [real stadiums](./docs/stadiums.md))
42
44
 
@@ -143,6 +145,36 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
143
145
  </tr>
144
146
  </table>
145
147
 
148
+ **A game on the ice** ([the replay in 3D](./docs/replay.md#in-3d), [the example game](./docs/example-games.md))
149
+
150
+ <table align="center">
151
+ <tr>
152
+ <td align="center" valign="top">
153
+ <picture>
154
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-3d-desk-dark.webp">
155
+ <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">
156
+ </picture>
157
+ <br><em>A made-up game, replayed in 3D</em>
158
+ </td>
159
+ <td align="center" valign="top">
160
+ <picture>
161
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/hero-game-3d-desk-dark.webp">
162
+ <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, and the scoreboard hung over centre ice showing the score, the period and the game clock." width="250">
163
+ </picture>
164
+ <br><em>The same game in an arena, in front of the crowd</em>
165
+ </td>
166
+ <td align="center" valign="top">
167
+ <picture>
168
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kyogijo-stadiums/main/docs/images/replay-game-flat-desk-dark.webp">
169
+ <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">
170
+ </picture>
171
+ <br><em>The same moment, flat</em>
172
+ </td>
173
+ </tr>
174
+ </table>
175
+
176
+ The scoreboard hung over an arena's ice follows the replay: its score, period and game clock come from the same timeline and clock as the controls ([the scoreboard over the arena](./docs/replay.md#the-scoreboard-over-the-arena-follows-the-game)).
177
+
146
178
  **Surfaces** ([the surfaces](./docs/surfaces.md), [score and layout](./docs/score.md))
147
179
 
148
180
  <table align="center">
@@ -217,7 +249,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
217
249
  - **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
250
  - **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
251
  - **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).
252
+ - **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
253
  - **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
254
  - **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
255
  - **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.
@@ -239,6 +271,25 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
239
271
 
240
272
  The full list of entry points is in [entry points](./docs/entry-points.md); every export is in the [API reference](https://johnmorrisdotca.github.io/kyogijo-stadiums/api.html).
241
273
 
274
+ ## Guides
275
+
276
+ Twelve guides, each with a Japanese twin in [`docs/guides/ja/`](./docs/guides/ja/), written for someone using the package: every example is run against the built package by `pnpm test:readme`, and the guides are held with the rest of the documentation by `pnpm docs:check`.
277
+
278
+ | Guide | What it is for |
279
+ | --- | --- |
280
+ | [Getting started: a results page](./docs/guides/getting-started.md) | A strip for every game in a list, one large drawing, and the same drawing tall for a phone. |
281
+ | [A score on a surface](./docs/guides/score-and-layout.md) | The teams, the state of the game, which side the home team is on, the three sizes. |
282
+ | [Looks, colours and dark mode](./docs/guides/looks-and-colours.md) | Eight named looks, colours from code, a court of your own, many drawings on one page. |
283
+ | [Embedding](./docs/guides/embed-in-a-page.md) | A container that follows its box, the custom element, a frame for a page with no script. |
284
+ | [React, Vue, Svelte and Angular](./docs/guides/frameworks.md) | The string or the element in each, and a server with no framework. |
285
+ | [Ballparks, grounds, arenas and real venues](./docs/guides/real-venues.md) | From a park's fence to a district of buildings on one plan, and the catalogue of what exists. |
286
+ | [Data on a surface](./docs/guides/data-on-a-surface.md) | Layers, coordinate frames, zones by the rules, a click as a place, charts. |
287
+ | [The 3D view](./docs/guides/the-3d-view.md) | The floor that turns, what stands on it, stands and roof, quality on a phone. |
288
+ | [Accessibility, English and Japanese](./docs/guides/accessibility-and-languages.md) | What a drawing says for itself, and what the page around it must say. |
289
+ | [Architecture](./docs/guides/architecture.md) | The rules a drawing is made by, and the checks that keep them true. |
290
+ | [Troubleshooting](./docs/guides/troubleshooting.md) | Nothing appears, a colour is wrong, a snapshot fails after an upgrade. |
291
+ | [Upgrading](./docs/guides/upgrading.md) | What each breaking release asks of you, and how to make the next upgrade cheap. |
292
+
242
293
  ## Use it in your project
243
294
 
244
295
  ### Install
@@ -407,9 +458,124 @@ Tell Vue the tag is a custom element with `compilerOptions.isCustomElement: (tag
407
458
  <kyogijo-field sport="nhl" away="USA" home="Canada" away-score={away} home-score={home} />
408
459
  ```
409
460
 
410
- A page that moves (`mountKyogijo`), a file written on a server and a frame for a page with no script are in [getting started](./docs/getting-started.md).
461
+ A page that moves (`mountKyogijo`), a file written on a server and a frame for a page with no script are in [getting started](./docs/getting-started.md), and every framework below has its own [guide](./docs/guides/frameworks.md).
462
+
463
+ ### In Angular
464
+
465
+ ```ts no-check
466
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
467
+ import "@johnmorrisdotca/kyogijo-stadiums/element/define";
468
+
469
+ @Component({
470
+ selector: "app-game",
471
+ standalone: true,
472
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
473
+ template: `<kyogijo-field sport="nhl" orientation="auto" away="USA" home="Canada" [attr.away-score]="away" [attr.home-score]="home"></kyogijo-field>`,
474
+ })
475
+ export class GameComponent {
476
+ away = 3;
477
+ home = 2;
478
+ }
479
+ ```
480
+
481
+ ### From a CDN, as a module
482
+
483
+ ```html
484
+ <div id="game"></div>
485
+ <script type="module">
486
+ import { drawKyogijo } from "https://cdn.jsdelivr.net/npm/@johnmorrisdotca/kyogijo-stadiums@0/+esm";
487
+ document.querySelector("#game").innerHTML = drawKyogijo({ sport: "nba", away: { name: "Australia", score: 108 }, home: { name: "Japan", score: 112 }, status: "final" });
488
+ </script>
489
+ ```
490
+
491
+ ### With a bundler and TypeScript
492
+
493
+ ```ts
494
+ import { SPORTS, drawKyogijo, type ScoreOptions, type Sport } from "@johnmorrisdotca/kyogijo-stadiums";
495
+
496
+ // The options are typed, so an editor lists the sports, the statuses and every option with its doc comment; a sport is one of SPORTS.
497
+ const sport: Sport = "nhl";
498
+ const game: ScoreOptions = { sport, home: { name: "Japan", score: 7 }, away: { name: "Canada", score: 5 }, status: "final" };
499
+ console.log(SPORTS.includes(sport), drawKyogijo(game).includes("Canada 5, Japan 7"));
500
+ // true true
501
+ ```
502
+
503
+ A bundler (Vite, webpack, esbuild or Rollup) needs nothing set: each entry is its own file, so `import ... from "@johnmorrisdotca/kyogijo-stadiums/parks"` brings in the parks and nothing else.
504
+
505
+ ```ts no-check
506
+ // vite.config.ts: nothing is needed for the package. Tell Vue the tag is a custom element, if you use it.
507
+ import vue from "@vitejs/plugin-vue";
508
+
509
+ export default { plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag: string) => tag === "kyogijo-field" } } })] };
510
+ ```
511
+
512
+ ### In a frame, with no script of your own
513
+
514
+ ```html
515
+ <iframe
516
+ src="https://johnmorrisdotca.github.io/kyogijo-stadiums/embed.html?sport=nhl&away=USA&home=Canada&ascore=3&hscore=2&status=final-ot"
517
+ title="USA 3, Canada 2, Final/OT"
518
+ loading="lazy"
519
+ style="width:100%;aspect-ratio:2.3;border:0"
520
+ ></iframe>
521
+ ```
522
+
523
+ ### On a server
524
+
525
+ ```js no-run
526
+ import { createServer } from "node:http";
527
+ import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
528
+
529
+ // An image address: the same options give the same bytes, so a CDN can keep it.
530
+ createServer((request, response) => {
531
+ const sport = new URL(request.url ?? "/", "http://localhost").searchParams.get("sport") === "nba" ? "nba" : "nhl";
532
+ response.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "public, max-age=3600" });
533
+ response.end(drawKyogijo({ sport, away: { name: "USA", score: 3 }, home: { name: "Canada", score: 2 }, status: "final" }));
534
+ }).listen(6900);
535
+ ```
536
+
537
+ ### Shots on the rink, in the provider's numbers
538
+
539
+ ```ts
540
+ import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
541
+ import { zoneAt } from "@johnmorrisdotca/kyogijo-stadiums/zones";
542
+
543
+ // The NHL API's numbers are feet from centre ice: `from` puts them on the rink, and `zoneAt` says where one is by the rules.
544
+ const shots = [{ x: 69, y: 22, team: "home", outcome: "goal" }, { x: -61, y: 12, team: "away", outcome: "blocked" }];
545
+ const svg = drawKyogijo({ sport: "nhl", layers: [{ kind: "points", name: "shots", from: "nhl-api", data: shots }] });
546
+ console.log(svg.includes("2 shots"), zoneAt("nhl", [69, 22], { frame: "nhl-api" }).subzone);
547
+ // true faceoff-circle
548
+ ```
549
+
550
+ ### In Japanese
551
+
552
+ ```ts
553
+ import { describe, drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
554
+
555
+ const game = { sport: "nhl", away: { name: "USA", score: 3 }, home: { name: "Canada", score: 2 }, status: "final" } as const;
556
+ console.log(describe({ ...game, language: "ja" }).title, drawKyogijo({ ...game, language: "ja" }).includes("試合終了"));
557
+ // USA 3、Canada 2、試合終了 true
558
+ ```
559
+
560
+ ### As data, for d3, a canvas, GeoJSON or a PNG
561
+
562
+ ```ts
563
+ import { sceneToGeoJSON } from "@johnmorrisdotca/kyogijo-stadiums/geojson";
564
+
565
+ // The same drawing as features, in feet from the left end of the rink.
566
+ const features = sceneToGeoJSON({ sport: "nhl", id: "rink" });
567
+ console.log(features.type, features.features.length > 10);
568
+ // FeatureCollection true
569
+ ```
570
+
571
+ ```js no-run
572
+ import { writeFileSync } from "node:fs";
573
+ import { toPNG } from "@johnmorrisdotca/kyogijo-stadiums/png";
574
+
575
+ // Needs the optional package @resvg/resvg-js; without it the call says so (PNG_PEER_MISSING).
576
+ writeFileSync("rink.png", await toPNG({ sport: "nhl", away: { name: "USA", score: 3 }, home: { name: "Canada", score: 2 }, status: "final" }, { width: 1200 }));
577
+ ```
411
578
 
412
- A page that moves (`mountKyogijo`), a file written on a server and a frame for a page with no script are in [getting started](./docs/getting-started.md).
413
579
 
414
580
  ## API
415
581
 
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.53.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.53.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
  },
@@ -2,9 +2,11 @@ import "./full.ts";
2
2
  import "./venues.all.ts";
3
3
  export { activeEvents, describeEvent, drawFrame, eventsBetween, interpolate, possession, replayClock, replayEvents, replayInfo, replayLayers, replaySnapshot, replaySummary, replayTable, replayTimes, trails } from "./replay.ts";
4
4
  export type { ReplaySnapshot } from "./replay.ts";
5
+ export { boardClock, replayBoard, replayPeriods } from "./replay.board.ts";
6
+ export type { ReplayBoardState } from "./replay.board.ts";
5
7
  export { alongPool, distanceAt, progressOf, raceTimeline } from "./pool.replay.ts";
6
8
  export type { Progress, RaceReplayOptions } from "./pool.replay.ts";
7
9
  export { formatTime, parseTime } from "./pool.race.ts";
8
10
  export { REPLAY_SPEEDS, REPLAY_SPEED_RANGE, createReplay } from "./replay.control.ts";
9
11
  export type { ReplayController } from "./replay.control.ts";
10
- export type { ActiveEvent, Possession, ReplayChange, ReplayEvent, ReplayEventKind, ReplayFrame, ReplayLook, ReplayObject, ReplayReason, ReplaySettings, ReplayState, ReplayTeam, ReplayTimeline, ReplayTrail, } from "./replay.types.ts";
12
+ export type { ActiveEvent, Possession, ReplayChange, ReplayEvent, ReplayEventKind, ReplayFrame, ReplayLook, ReplayObject, ReplayPeriod, ReplayReason, ReplaySettings, ReplayState, ReplayTeam, ReplayTimeline, ReplayTrail, } from "./replay.types.ts";
@@ -4,6 +4,7 @@ import "./full.js";
4
4
  // The real venues of the NHL's arenas, as this entry has always had them (the lean district reads the ones it shows).
5
5
  import "./venues.all.js";
6
6
  export { activeEvents, describeEvent, drawFrame, eventsBetween, interpolate, possession, replayClock, replayEvents, replayInfo, replayLayers, replaySnapshot, replaySummary, replayTable, replayTimes, trails } from "./replay.js";
7
+ export { boardClock, replayBoard, replayPeriods } from "./replay.board.js";
7
8
  export { alongPool, distanceAt, progressOf, raceTimeline } from "./pool.replay.js";
8
9
  export { formatTime, parseTime } from "./pool.race.js";
9
10
  export { REPLAY_SPEEDS, REPLAY_SPEED_RANGE, createReplay } from "./replay.control.js";
@@ -0,0 +1,39 @@
1
+ import type { ReplayPeriod, ReplayTimeline } from "./replay.types.ts";
2
+ /** The board at a time. */
3
+ export interface ReplayBoardState {
4
+ /** The goals so far: the events of kind `goal` at or before the time, by `team`. */
5
+ score: [number, number];
6
+ /** The period's number, or `undefined` when the timeline has no periods. */
7
+ period?: number;
8
+ /** The game clock as it is written, `19:20`: the time left in the period, or the time gone when the timeline counts `up`. Whole seconds, so it changes once a second of game time at most. */
9
+ clock: string;
10
+ }
11
+ /** Seconds on a board's clock, `19:20`. A board has two digits of minutes, so more than 99:59 is shown as 99:59. */
12
+ export declare function boardClock(seconds: number): string;
13
+ /** The periods of a timeline in order, each with its number; the whole replay as one period with no number when it gives none. */
14
+ export declare function replayPeriods(timeline: ReplayTimeline): readonly (ReplayPeriod & {
15
+ number?: number;
16
+ })[];
17
+ /**
18
+ * The scoreboard of a replay at a time `t`, in seconds on the timeline's clock. The score is the goals at or before it; the period is the last one that has begun (the first, before any has);
19
+ * the clock runs through the period from its full length to nothing, or from nothing up when the timeline counts `up`. A period with a `length` of its own runs its clock through that many
20
+ * seconds however long it lasts on the timeline, so a game re-timed to minutes can read like the real one.
21
+ * @example
22
+ * ```ts
23
+ * import { replayBoard } from "@johnmorrisdotca/kyogijo-stadiums/replay";
24
+ * const frames = [0, 60, 120].map((t) => ({ t, objects: [{ id: "puck", team: "puck" as const, x: 0, y: 0 }] }));
25
+ * const timeline = {
26
+ * frames,
27
+ * events: [{ t: 30, kind: "goal", team: "home" as const }],
28
+ * periods: [{ start: 0, end: 60, length: 1200 }, { start: 60, end: 120, length: 1200 }],
29
+ * };
30
+ * const at = (t: number) => JSON.stringify(replayBoard(timeline, t));
31
+ * console.log(at(0));
32
+ * console.log(at(30));
33
+ * console.log(at(90));
34
+ * // {"score":[0,0],"period":1,"clock":"20:00"}
35
+ * // {"score":[1,0],"period":1,"clock":"10:00"}
36
+ * // {"score":[1,0],"period":2,"clock":"10:00"}
37
+ * ```
38
+ */
39
+ export declare function replayBoard(timeline: ReplayTimeline, t: number): ReplayBoardState;
@@ -0,0 +1,62 @@
1
+ // What a scoreboard shows of a replayed game at a time: the score, the period and the game clock, worked out from the same timeline the replay plays (its goals and its periods). Pure: a timeline and a time
2
+ // give the same board every time, so a seek, a step and a period's button all land on the right figures at once, and a replay that goes back finds the score as it was. The 3D view draws it on the scoreboard
3
+ // hung over the surface (`turn.replay.ts`); a page that draws its own board reads it the same way.
4
+ import { replayEvents, replayInfo } from "./replay.js";
5
+ const isNum = (value) => typeof value === "number" && Number.isFinite(value);
6
+ const two = (n) => String(n).padStart(2, "0");
7
+ /** Seconds on a board's clock, `19:20`. A board has two digits of minutes, so more than 99:59 is shown as 99:59. */
8
+ export function boardClock(seconds) {
9
+ const whole = Math.min(99 * 60 + 59, Math.max(0, Math.floor(isNum(seconds) ? seconds : 0)));
10
+ return `${Math.floor(whole / 60)}:${two(whole % 60)}`;
11
+ }
12
+ /** The periods of a timeline in order, each with its number; the whole replay as one period with no number when it gives none. */
13
+ export function replayPeriods(timeline) {
14
+ const given = (timeline.periods ?? []).filter((p) => isNum(p?.start) && isNum(p?.end) && p.end >= p.start);
15
+ if (given.length === 0) {
16
+ const info = replayInfo(timeline);
17
+ return [{ start: info.start, end: info.end }];
18
+ }
19
+ return [...given].sort((a, b) => a.start - b.start).map((p, i) => ({ ...p, number: isNum(p.number) ? p.number : i + 1 }));
20
+ }
21
+ /**
22
+ * The scoreboard of a replay at a time `t`, in seconds on the timeline's clock. The score is the goals at or before it; the period is the last one that has begun (the first, before any has);
23
+ * the clock runs through the period from its full length to nothing, or from nothing up when the timeline counts `up`. A period with a `length` of its own runs its clock through that many
24
+ * seconds however long it lasts on the timeline, so a game re-timed to minutes can read like the real one.
25
+ * @example
26
+ * ```ts
27
+ * import { replayBoard } from "@johnmorrisdotca/kyogijo-stadiums/replay";
28
+ * const frames = [0, 60, 120].map((t) => ({ t, objects: [{ id: "puck", team: "puck" as const, x: 0, y: 0 }] }));
29
+ * const timeline = {
30
+ * frames,
31
+ * events: [{ t: 30, kind: "goal", team: "home" as const }],
32
+ * periods: [{ start: 0, end: 60, length: 1200 }, { start: 60, end: 120, length: 1200 }],
33
+ * };
34
+ * const at = (t: number) => JSON.stringify(replayBoard(timeline, t));
35
+ * console.log(at(0));
36
+ * console.log(at(30));
37
+ * console.log(at(90));
38
+ * // {"score":[0,0],"period":1,"clock":"20:00"}
39
+ * // {"score":[1,0],"period":1,"clock":"10:00"}
40
+ * // {"score":[1,0],"period":2,"clock":"10:00"}
41
+ * ```
42
+ */
43
+ export function replayBoard(timeline, t) {
44
+ const score = [0, 0];
45
+ for (const e of replayEvents(timeline)) {
46
+ if (e.t > t)
47
+ break;
48
+ if (e.kind === "goal" && (e.team === "home" || e.team === "away"))
49
+ score[e.team === "home" ? 0 : 1] += 1;
50
+ }
51
+ const periods = replayPeriods(timeline);
52
+ let at = periods[0];
53
+ for (const p of periods)
54
+ if (p.start <= t)
55
+ at = p;
56
+ const span = at.end - at.start;
57
+ const share = span > 0 ? Math.min(1, Math.max(0, (t - at.start) / span)) : 0;
58
+ const length = isNum(at.length) && at.length > 0 ? at.length : span;
59
+ // Down: the time left, rounded up, so that the full length shows until the period has moved and nothing shows only at its end. Up: the whole seconds gone.
60
+ const clock = timeline.clock === "up" ? boardClock(share * length) : boardClock(Math.ceil((1 - share) * length - 1e-9));
61
+ return at.number === undefined ? { score, clock } : { score, period: at.number, clock };
62
+ }
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 { ReplayChange, 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, with the change (what moved it, and the state after). */
45
+ paint(t: number, change?: ReplayChange): 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