@johnmorrisdotca/kyogijo-stadiums 0.52.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 +8 -0
- package/README.md +141 -3
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/replay-entry.d.ts +3 -1
- package/dist/replay-entry.js +1 -0
- package/dist/replay.board.d.ts +39 -0
- package/dist/replay.board.js +62 -0
- package/dist/replay.dom.d.ts +3 -3
- package/dist/replay.dom.js +1 -1
- package/dist/replay.types.d.ts +14 -0
- package/dist/turn.kit.d.ts +3 -0
- package/dist/turn.replay.board.d.ts +23 -0
- package/dist/turn.replay.board.js +81 -0
- package/dist/turn.replay.d.ts +5 -0
- package/dist/turn.replay.dom.js +9 -1
- package/dist/turn.replay.js +14 -2
- package/dist/venue.boards.d.ts +24 -0
- package/dist/venue.boards.js +47 -10
- package/dist/venue.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ 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
|
+
|
|
10
18
|
## [0.52.0] - 2026-11-03
|
|
11
19
|
|
|
12
20
|
### 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>
|
|
@@ -157,7 +159,7 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
|
|
|
157
159
|
<td align="center" valign="top">
|
|
158
160
|
<picture>
|
|
159
161
|
<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">
|
|
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">
|
|
161
163
|
</picture>
|
|
162
164
|
<br><em>The same game in an arena, in front of the crowd</em>
|
|
163
165
|
</td>
|
|
@@ -171,6 +173,8 @@ Real buildings drawn in the outline OpenStreetMap maps for them, each with a pla
|
|
|
171
173
|
</tr>
|
|
172
174
|
</table>
|
|
173
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
|
+
|
|
174
178
|
**Surfaces** ([the surfaces](./docs/surfaces.md), [score and layout](./docs/score.md))
|
|
175
179
|
|
|
176
180
|
<table align="center">
|
|
@@ -267,6 +271,25 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
|
|
|
267
271
|
|
|
268
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).
|
|
269
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
|
+
|
|
270
293
|
## Use it in your project
|
|
271
294
|
|
|
272
295
|
### Install
|
|
@@ -435,9 +458,124 @@ Tell Vue the tag is a custom element with `compilerOptions.isCustomElement: (tag
|
|
|
435
458
|
<kyogijo-field sport="nhl" away="USA" home="Canada" away-score={away} home-score={home} />
|
|
436
459
|
```
|
|
437
460
|
|
|
438
|
-
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
|
+
```
|
|
439
578
|
|
|
440
|
-
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).
|
|
441
579
|
|
|
442
580
|
## API
|
|
443
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.
|
|
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/catalogue.data.js
CHANGED
|
@@ -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.
|
|
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/replay-entry.d.ts
CHANGED
|
@@ -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";
|
package/dist/replay-entry.js
CHANGED
|
@@ -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.dom.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReplayController } from "./replay.control.ts";
|
|
2
2
|
import { type ReplayGroup } from "./replay.ts";
|
|
3
|
-
import type { ReplayLook, ReplaySettings, ReplayTimeline } from "./replay.types.ts";
|
|
3
|
+
import type { ReplayChange, ReplayLook, ReplaySettings, ReplayTimeline } from "./replay.types.ts";
|
|
4
4
|
import type { Language, ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
5
5
|
/** The controls' stylesheet: written once beside the controls, scoped to them, in the page's own colours. */
|
|
6
6
|
export declare const REPLAY_STYLE: string;
|
|
@@ -41,8 +41,8 @@ export interface ReplayPainter {
|
|
|
41
41
|
export declare function replayPainter(source: PainterSource): ReplayPainter;
|
|
42
42
|
/** What a driver is given: how to paint a time, where its keys are heard, and what it labels. */
|
|
43
43
|
export interface DriverSource {
|
|
44
|
-
/** Move the drawing to a time. Called on every change of the clock. */
|
|
45
|
-
paint(t: number): void;
|
|
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
46
|
language: Language;
|
|
47
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
48
|
keysOn: HTMLElement;
|
package/dist/replay.dom.js
CHANGED
|
@@ -294,7 +294,7 @@ export function replayDriver(doc, timeline, settings, source) {
|
|
|
294
294
|
}
|
|
295
295
|
const stop = controller.subscribe((change) => {
|
|
296
296
|
sync(change);
|
|
297
|
-
source.paint(change.state.t);
|
|
297
|
+
source.paint(change.state.t, change);
|
|
298
298
|
if (settings.announce !== false && change.crossed.length > 0 && (change.reason === "tick" || change.reason === "step")) {
|
|
299
299
|
live.textContent = change.crossed.map((e) => describeEvent(e, language, info.start)).join(". ");
|
|
300
300
|
}
|
package/dist/replay.types.d.ts
CHANGED
|
@@ -50,6 +50,16 @@ export interface ReplayEvent {
|
|
|
50
50
|
/** What is said of it, over the standard words: "Goal, Rivera". */
|
|
51
51
|
label?: string;
|
|
52
52
|
}
|
|
53
|
+
/** A period of the game (or a half, a quarter, an inning) on the timeline's clock: what a scoreboard on the surface shows as the period and counts its clock in. */
|
|
54
|
+
export interface ReplayPeriod {
|
|
55
|
+
/** The period's number as the board writes it. Its place in the list (1 for the first) when missing. */
|
|
56
|
+
number?: number;
|
|
57
|
+
/** Where it begins and ends, in seconds on the timeline's clock. */
|
|
58
|
+
start: number;
|
|
59
|
+
end: number;
|
|
60
|
+
/** How long the period is on the game clock, in seconds, when the replay is re-timed (a made-up game played in minutes: 1200 for a hockey period). The board's clock runs through it from start to end. Else the clock runs in the timeline's own seconds. */
|
|
61
|
+
length?: number;
|
|
62
|
+
}
|
|
53
63
|
/** Tracking data over time on a surface. Do not change a timeline after it has been used: make a new one. */
|
|
54
64
|
export interface ReplayTimeline {
|
|
55
65
|
/** The positions, from the first moment to the last. */
|
|
@@ -69,6 +79,10 @@ export interface ReplayTimeline {
|
|
|
69
79
|
x?: readonly Landmark[];
|
|
70
80
|
y?: readonly Landmark[];
|
|
71
81
|
};
|
|
82
|
+
/** The periods of the game, in order. A scoreboard hung over a 3D view shows the one the clock is in. */
|
|
83
|
+
periods?: readonly ReplayPeriod[];
|
|
84
|
+
/** Whether a scoreboard's game clock counts the time left in the period (`down`, the default: hockey, basketball, football) or the time gone (`up`: soccer). */
|
|
85
|
+
clock?: "down" | "up";
|
|
72
86
|
}
|
|
73
87
|
/** How a frame is drawn. */
|
|
74
88
|
export interface ReplayLook {
|
package/dist/turn.kit.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HungBoard } from "./venue.boards.ts";
|
|
1
2
|
/** A point or a direction: x, y, z in feet. */
|
|
2
3
|
export type V3 = readonly [number, number, number];
|
|
3
4
|
/** What a plane is a part of: it is the class of its element (`ky-up-<kind>`) and what a test finds it by. */
|
|
@@ -74,6 +75,8 @@ export interface Upright {
|
|
|
74
75
|
/** A second height worth saying (a net's height at the centre, a post's crossbar), in feet. */
|
|
75
76
|
low?: number;
|
|
76
77
|
planes: Plane[];
|
|
78
|
+
/** What a scoreboard hung over the surface was made from, so that a replay can draw its faces again with the score and the clock of a time (`replayTurnModel`). */
|
|
79
|
+
hung?: HungBoard;
|
|
77
80
|
}
|
|
78
81
|
/** The four numbers a plane's SVG needs: its length along `u` and along `v`, in feet. */
|
|
79
82
|
export declare const size: (p: Pick<Plane, "u" | "v">) => [number, number];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReplayBoardState } from "./replay.board.ts";
|
|
2
|
+
import type { ReplayChange, ReplayTimeline } from "./replay.types.ts";
|
|
3
|
+
import type { Plane } from "./turn.kit.ts";
|
|
4
|
+
import type { TurnModel } from "./turn.model.ts";
|
|
5
|
+
import { type HungBoard } from "./venue.boards.ts";
|
|
6
|
+
/** The scoreboards a model has hung over its surface, with what each was made from. */
|
|
7
|
+
export declare const hungBoards: (model: Pick<TurnModel, "uprights">) => HungBoard[];
|
|
8
|
+
/** The planes of a hung scoreboard with the replay's board on it: the same planes (the same ids, in the same places), the faces drawn with the score, the period and the clock. */
|
|
9
|
+
export declare function hungBoardPlanes(hung: HungBoard, state: ReplayBoardState): Plane[];
|
|
10
|
+
/** What is written on a board, as one string: it changes when, and only when, the score, the period or the clock does. */
|
|
11
|
+
export declare const boardKey: (state: ReplayBoardState) => string;
|
|
12
|
+
/** The longest the game clock goes without a write while the replay plays, in milliseconds. */
|
|
13
|
+
export declare const CLOCK_EVERY = 1000;
|
|
14
|
+
/** A scoreboard in the page: its planes' SVG elements, what was last written to each, and what it shows. */
|
|
15
|
+
export interface BoardFollower {
|
|
16
|
+
/** Find the board's elements in the page after the view drew its world: the model was made at this time, so it already shows `state`. */
|
|
17
|
+
bind(root: ParentNode, model: TurnModel, state: ReplayBoardState | undefined): void;
|
|
18
|
+
/** Follow the clock to `t`: write the board if what it shows changed (and, while playing, if the clock has not just been written). */
|
|
19
|
+
follow(t: number, model: TurnModel, change?: ReplayChange): void;
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
22
|
+
/** The follower of a replay's scoreboard. It is idle (no elements, no work) where the view has none. */
|
|
23
|
+
export declare function boardFollower(timeline: ReplayTimeline, now: () => number): BoardFollower;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The scoreboard hung over a 3D view, following a replay: the faces drawn with the score, the period and the game clock of the time the replay is at (`replayBoard`), in the model (pure) and in the
|
|
2
|
+
// page (a few SVG strings written when the board changes, never the view). A goal, a new period, a seek, a step or a pause is written at once; the clock, while the replay plays, once a second at most.
|
|
3
|
+
import { replayBoard } from "./replay.board.js";
|
|
4
|
+
import { centreBoard, safeClock } from "./venue.boards.js";
|
|
5
|
+
/** The scoreboards a model has hung over its surface, with what each was made from. */
|
|
6
|
+
export const hungBoards = (model) => model.uprights.items.flatMap((item) => (item.kind === "hung-board" && item.hung !== undefined ? [item.hung] : []));
|
|
7
|
+
/** The planes of a hung scoreboard with the replay's board on it: the same planes (the same ids, in the same places), the faces drawn with the score, the period and the clock. */
|
|
8
|
+
export function hungBoardPlanes(hung, state) {
|
|
9
|
+
const clock = safeClock(state.clock);
|
|
10
|
+
const live = clock === "" ? undefined : { clock, ...(state.period === undefined ? {} : { period: state.period }) };
|
|
11
|
+
return centreBoard(hung.id, hung.box, hung.lift, { ...hung.look, score: state.score, ...(live === undefined ? {} : { live }) });
|
|
12
|
+
}
|
|
13
|
+
/** What is written on a board, as one string: it changes when, and only when, the score, the period or the clock does. */
|
|
14
|
+
export const boardKey = (state) => `${state.score[0]}-${state.score[1]} ${state.period ?? ""} ${state.clock}`;
|
|
15
|
+
/** The longest the game clock goes without a write while the replay plays, in milliseconds. */
|
|
16
|
+
export const CLOCK_EVERY = 1000;
|
|
17
|
+
const FACE = /-(south|north|east|west)$/;
|
|
18
|
+
/** The follower of a replay's scoreboard. It is idle (no elements, no work) where the view has none. */
|
|
19
|
+
export function boardFollower(timeline, now) {
|
|
20
|
+
let found = new Map();
|
|
21
|
+
let shown = "";
|
|
22
|
+
let state;
|
|
23
|
+
let wroteAt = Number.NEGATIVE_INFINITY;
|
|
24
|
+
/** The figures on the faces as text, for a test and for a reader of the page's markup. */
|
|
25
|
+
const label = (s) => {
|
|
26
|
+
for (const [id, one] of found) {
|
|
27
|
+
if (!FACE.test(id))
|
|
28
|
+
continue;
|
|
29
|
+
one.face.dataset.score = `${s.score[0]}-${s.score[1]}`;
|
|
30
|
+
one.face.dataset.period = s.period === undefined ? "" : String(s.period);
|
|
31
|
+
one.face.dataset.clock = s.clock;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
bind(root, model, at) {
|
|
36
|
+
found = new Map();
|
|
37
|
+
state = at;
|
|
38
|
+
if (at === undefined)
|
|
39
|
+
return;
|
|
40
|
+
const written = new Map(model.planes.filter((p) => p.kind === "hung-board").map((p) => [p.id, p.svg]));
|
|
41
|
+
for (const face of root.querySelectorAll(".ky-up-hung-board")) {
|
|
42
|
+
const svg = face.querySelector(":scope > svg");
|
|
43
|
+
const id = face.dataset.part ?? "";
|
|
44
|
+
if (svg !== null && written.has(id))
|
|
45
|
+
found.set(id, { svg, face, written: written.get(id) ?? "" });
|
|
46
|
+
}
|
|
47
|
+
shown = boardKey(at);
|
|
48
|
+
wroteAt = now();
|
|
49
|
+
label(at);
|
|
50
|
+
},
|
|
51
|
+
follow(t, model, change) {
|
|
52
|
+
if (found.size === 0)
|
|
53
|
+
return;
|
|
54
|
+
const next = replayBoard(timeline, t);
|
|
55
|
+
const key = boardKey(next);
|
|
56
|
+
if (key === shown)
|
|
57
|
+
return;
|
|
58
|
+
const moved = state === undefined || state.score[0] !== next.score[0] || state.score[1] !== next.score[1] || state.period !== next.period;
|
|
59
|
+
const at = now();
|
|
60
|
+
if (!moved && change?.reason === "tick" && change.state.playing && at - wroteAt < CLOCK_EVERY)
|
|
61
|
+
return;
|
|
62
|
+
shown = key;
|
|
63
|
+
state = next;
|
|
64
|
+
wroteAt = at;
|
|
65
|
+
for (const hung of hungBoards(model)) {
|
|
66
|
+
for (const plane of hungBoardPlanes(hung, next)) {
|
|
67
|
+
const one = found.get(plane.id);
|
|
68
|
+
if (one === undefined || one.written === plane.svg)
|
|
69
|
+
continue;
|
|
70
|
+
one.svg.innerHTML = plane.svg;
|
|
71
|
+
one.written = plane.svg;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
label(next);
|
|
75
|
+
},
|
|
76
|
+
clear() {
|
|
77
|
+
found = new Map();
|
|
78
|
+
state = undefined;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/turn.replay.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReplayBoardState } from "./replay.board.ts";
|
|
1
2
|
import { type ReplayGroup } from "./replay.ts";
|
|
2
3
|
import type { ReplayLook, ReplayObject, ReplayTeam, ReplayTimeline } from "./replay.types.ts";
|
|
3
4
|
import { type Plane } from "./turn.kit.ts";
|
|
@@ -10,6 +11,8 @@ export declare const keepOnFloor: (group: Pick<ReplayGroup, "id">) => boolean;
|
|
|
10
11
|
/** The settings a 3D replay takes beyond the flat replay's look: `stand: false` keeps the players flat on the floor, as the flat replay draws them. */
|
|
11
12
|
export interface TurnReplayLook extends ReplayLook {
|
|
12
13
|
stand?: boolean;
|
|
14
|
+
/** `false` leaves a scoreboard hung over the surface as the options made it; by default it shows the replay's score, period and clock (`replayBoard`). */
|
|
15
|
+
board?: boolean;
|
|
13
16
|
}
|
|
14
17
|
/**
|
|
15
18
|
* 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
|
|
@@ -44,6 +47,8 @@ export interface TurnReplayModel {
|
|
|
44
47
|
size: number;
|
|
45
48
|
/** The options the floor was drawn with, its `id` among them. */
|
|
46
49
|
floorOptions: SurfaceOptions | ScoreOptions;
|
|
50
|
+
/** What the hung scoreboard shows at the time, when the model has one and the look lets the replay drive it. */
|
|
51
|
+
board?: ReplayBoardState;
|
|
47
52
|
}
|
|
48
53
|
/** 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. */
|
|
49
54
|
export declare const replayFloorOptions: (model: Pick<TurnModel, "id" | "options">) => SurfaceOptions | ScoreOptions;
|
package/dist/turn.replay.dom.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createReplayView, replayDriver, replayPainter } from "./replay.dom.js";
|
|
2
2
|
import { useTurnReplay } from "./turn.dom.js";
|
|
3
3
|
import { drawFrameOf, replayPlacer, replaySnapshot } from "./replay.js";
|
|
4
|
+
import { boardFollower } from "./turn.replay.board.js";
|
|
4
5
|
import { keepOnFloor, moveDisc, replayTurnModel } from "./turn.replay.js";
|
|
5
6
|
/** Attach a replay to a 3D view. The controller it gives back has `destroy`, which takes the replay away and leaves the view as it was. */
|
|
6
7
|
export function attachTurnReplay(view, timeline, settings) {
|
|
@@ -12,6 +13,8 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
12
13
|
/** The element of each disc, and whether it was last shown. */
|
|
13
14
|
let discs = new Map();
|
|
14
15
|
let placedAt = Number.NaN;
|
|
16
|
+
/** The scoreboard hung over the surface, which follows the game. */
|
|
17
|
+
const board = boardFollower(timeline, () => doc.defaultView?.performance.now() ?? 0);
|
|
15
18
|
const painter = replayPainter({
|
|
16
19
|
doc,
|
|
17
20
|
timeline,
|
|
@@ -57,9 +60,11 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
57
60
|
const wrap = doc.createElement("div");
|
|
58
61
|
wrap.className = "ky-replay ky-turn-replay";
|
|
59
62
|
const driver = replayDriver(doc, timeline, settings, {
|
|
60
|
-
paint: (t) => {
|
|
63
|
+
paint: (t, change) => {
|
|
61
64
|
painter.paint(t);
|
|
62
65
|
place(t);
|
|
66
|
+
if (look.board !== false)
|
|
67
|
+
board.follow(t, view.model(), change);
|
|
63
68
|
view.repaint();
|
|
64
69
|
},
|
|
65
70
|
language,
|
|
@@ -107,6 +112,8 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
107
112
|
const t = driver.controller.state().t;
|
|
108
113
|
placedAt = t;
|
|
109
114
|
painter.bind(t);
|
|
115
|
+
// The scoreboard was drawn by the model at this time: its elements are found again.
|
|
116
|
+
board.bind(root, view.model(), laid?.board);
|
|
110
117
|
};
|
|
111
118
|
view.setDecorate((model) => {
|
|
112
119
|
laid = replayTurnModel(model, timeline, driver.controller.state().t, look);
|
|
@@ -130,6 +137,7 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
130
137
|
wrap.remove();
|
|
131
138
|
laid = undefined;
|
|
132
139
|
discs = new Map();
|
|
140
|
+
board.clear();
|
|
133
141
|
floorSvg = null;
|
|
134
142
|
view.setDecorate(undefined);
|
|
135
143
|
},
|
package/dist/turn.replay.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
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
3
|
// the same model, timeline, time and look give the same model. `turn.replay.dom.ts` moves it in a page.
|
|
4
4
|
import { esc, num } from "./format.js";
|
|
5
|
+
import { replayBoard } from "./replay.board.js";
|
|
5
6
|
import { drawFrameOf, replayCast, replayPlacer, replaySnapshot } from "./replay.js";
|
|
6
7
|
import { n } from "./turn.kit.js";
|
|
8
|
+
import { hungBoardPlanes, hungBoards } from "./turn.replay.board.js";
|
|
7
9
|
/** Who stands up on a 3D view: the players and the officials. The ball and the puck stay on the floor. */
|
|
8
10
|
export const STANDING = ["home", "away", "official"];
|
|
9
11
|
/** What stands up is in these groups of the flat replay; the 3D view's floor draws the rest (`keepOnFloor`). */
|
|
@@ -58,11 +60,21 @@ export const replayFloorOptions = (model) => ({ ...model.options, id: `${model.i
|
|
|
58
60
|
export function replayTurnModel(model, timeline, t, look = {}) {
|
|
59
61
|
const stand = look.stand !== false;
|
|
60
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 };
|
|
61
73
|
const floor = drawFrameOf(floorOptions, timeline, t, look, stand ? keepOnFloor : () => true);
|
|
62
74
|
const size = discSize(playingArea(model), look);
|
|
63
75
|
const discs = new Map();
|
|
64
76
|
if (!stand)
|
|
65
|
-
return { model: { ...
|
|
77
|
+
return { model: { ...base, floor }, discs, size, floorOptions, ...board };
|
|
66
78
|
const sport = String(model.options.sport ?? "");
|
|
67
79
|
const theme = model.options.theme;
|
|
68
80
|
const vars = floorVars(floor);
|
|
@@ -76,7 +88,7 @@ export function replayTurnModel(model, timeline, t, look = {}) {
|
|
|
76
88
|
const at = here.get(o.id);
|
|
77
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 });
|
|
78
90
|
}
|
|
79
|
-
return { model: { ...
|
|
91
|
+
return { model: { ...base, floor, planes: [...base.planes, ...[...discs.values()].map((disc) => disc.plane)] }, discs, size, floorOptions, ...board };
|
|
80
92
|
}
|
|
81
93
|
/** A number for a test or a page that wants to know where a disc stands: its middle on the floor, in feet. */
|
|
82
94
|
export const discAt = (plane) => `${num(plane.o[0] + plane.u[0] / 2)} ${num(plane.o[1])}`;
|
package/dist/venue.boards.d.ts
CHANGED
|
@@ -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.
|
package/dist/venue.boards.js
CHANGED
|
@@ -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
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -349,6 +349,8 @@
|
|
|
349
349
|
"screenshots:flythrough": "pnpm site && node scripts/readme-flythrough.mjs",
|
|
350
350
|
"test:readme": "pnpm build && node scripts/check-readme-examples.mjs",
|
|
351
351
|
"docs:make": "node scripts/docs.mjs",
|
|
352
|
+
"docs:site": "pnpm site",
|
|
353
|
+
"docs:check": "pnpm build && node scripts/docs-check.mjs",
|
|
352
354
|
"parks": "node scripts/build-parks.mjs",
|
|
353
355
|
"arenas": "node scripts/build-arenas.mjs",
|
|
354
356
|
"arenas:fetch": "node scripts/fetch-arenas.mjs",
|