@johnmorrisdotca/kyogijo-stadiums 0.60.1 → 0.61.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,15 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.61.0] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - **Sound: the crowd of each kind of venue, at a level (KYOGIJO-STADIUMS-36)** ([sound](./docs/sound.md)): `@johnmorrisdotca/kyogijo-stadiums/sound` has `createKyogijoSound`, a surface's crowd made in the browser with Web Audio (the building's air, people talking, applause and the crowd's voice, and the pool's water), at five levels from quiet to a roar, heard in the room its venue makes (its roof and its seats: an arena rings, an open stadium does not), as large as the crowd in it and drier from above in 2D. An index of sound sets (`SOUND_BEDS`, `SOUND_SETS`, `SOUND_OF_SPORT`) gives every surface a bed for its kind of venue (arena, open stadium, ballpark, court hall, pool, dojo, ring, track) and the levels it offers (a kyudo hall only two); a surface with none has no sound. Off until turned on: nothing is fetched, made or decoded before, one audio context serves the page, and nothing plays or decodes while it is off, muted, hidden or out of sight. `soundRoom`, `soundMix` and `levelFor` are the rules, pure.
15
+ - The demo: a Sound button, a volume slider and the crowd's levels as buttons, kept in the address (`sound=on`, `volume`, `level`), on the real arenas (venues.html), under the surface in Try it (the levels of each surface's venue, none with the crowd off) and over the arena at the top of the page. Its recordings (`demo/sounds/`) are CC0 or in the public domain, each listed with its source, author and licence in [sound credits](./docs/sound-credits.md), made by `pnpm sounds:fetch` and `pnpm sounds` (loudness matched, seamless loops, Opus with AAC); the most a sport loads is under 110 kB. They are not in the npm package.
16
+ - **Sound: a game's cues from its replay (KYOGIJO-STADIUMS-36)** ([game cues](./docs/sound.md#game-cues)): `sound.follow(controller, { periods, scoreAt })` plays each sport's cues as the replay goes by them (hockey's goal horn and the crowd's swell, a whistle at a stoppage, the buzzer at a period's end, a ring's bell, a starting gun, an "ooh" at a near miss), none on a seek or while paused, and under `level: "auto"` the crowd follows the game (`excitementAt`: loud late in a close game, a roar after a goal). `cueKindOf` reads a timeline's own words (`touchdown`, `try`, `ippon`, `foul`...); `cuesOf` and `cuesOfChange` are the rules, pure. The demo's game section and every Play a game (the arena at the top of the page, Try it and the real arenas) have the sound, with Follow the game among the levels; `play-game.js` tells a page of the replay it puts on the view (`onReplay`).
17
+ - **Sound: live events (KYOGIJO-STADIUMS-36)** ([live events](./docs/sound.md#live-events)): `sound.cue({ kind, team })` plays an event's cues as it happens, and `sound.game(moment)` gives the game's state to the level under `auto`; a goal lifts it to a roar for six seconds. The demo's live score has the sound, and pushes each goal and the game's end as they come.
18
+
10
19
  ## [0.60.1] - 2026-11-03
11
20
 
12
21
  ### Fixed
package/README.md CHANGED
@@ -260,6 +260,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
260
260
  - **Bring your own race feed**: a race plays from a feed you choose, read by your own browser from an address, pasted or from a file, and followed while it is live: kyogijo's open race timeline or OpenF1-style lap rows, with our made-up sample feed first. [The race feed](./docs/race-feed.md), [Where to find a feed](./docs/where-to-find-a-feed.md).
261
261
  - **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).
262
262
  - **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).
263
+ - **Sound**: the crowd of each kind of venue at a level from quiet to a roar, heard in the room its venue makes, and its game's sounds (the goal horn, the whistle, the buzzer, the bell) from a replay or live events; made in the browser, off until turned on. [Sound](./docs/sound.md).
263
264
  - **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).
264
265
  - **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).
265
266
  - **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.
@@ -275,6 +276,7 @@ console.log(svg.startsWith("<svg"), svg.includes("USA 3, Canada 2"));
275
276
  | `./circuits` | Race tracks as OpenStreetMap maps them, in 2D and 3D, and a simulated race on them with its clock and timing board ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/circuits.html?race=1)). |
276
277
  | `./multi`, `./judging`, `./layouts` | Several surfaces for one event ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/halls.html)); who judges where; small multiples and backgrounds. |
277
278
  | `./layers`, `./charts`, `./frames`, `./zones`, `./pointer`, `./replay` | Data layers, charts, the providers' frames, zones by the rules, a click as a place, tracking data played on a surface. |
279
+ | `./sound` | A surface's crowd and its game's sounds, made in the browser with Web Audio ([demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/venues.html?sound=on)). |
278
280
  | `./athletics`, `./budo`, `./skating` | The athletics stadium, the budō (areas, dohyō, the dojo builder) and the skating rinks. |
279
281
  | `./scene`, `./canvas`, `./d3`, `./plot`, `./geojson`, `./png` | The drawing as data, on a canvas, in d3, as an Observable Plot mark, as GeoJSON, as a PNG. |
280
282
  | `./mount`, `./react`, `./element`, `./element/define` | Draw into a container; React props; the `<kyogijo-field>` element and its registration. |
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.60.1"
6
+ "version": "0.61.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.60.1"
6
+ "version": "0.61.0"
7
7
  },
8
8
  "address": "https://johnmorrisdotca.github.io/kyogijo-stadiums/catalogue.json",
9
9
  "site": "https://johnmorrisdotca.github.io/kyogijo-stadiums/",
@@ -0,0 +1,8 @@
1
+ export { SOUND_TAIL, createKyogijoSound } from "./sound.dom.ts";
2
+ export type { KyogijoSound, SoundAudioContext, SoundFetch, SoundFileName, SoundFollow, SoundLevelChoice, SoundQuiet, SoundSettings, SoundState } from "./sound.dom.ts";
3
+ export { LEVEL_MIX, cueKindOf, cuesOf, cuesOfChange, excitementAt, levelFor, soundLevelsOf, soundMix, soundRoom, soundSetOf, soundVoicesOf } from "./sound.mix.ts";
4
+ export type { GameMoment, SoundContext, SoundCue, SoundMix, SoundPeriod, SoundRoom, SoundVenue } from "./sound.mix.ts";
5
+ export { CUE_KINDS, SOUND_BEDS, SOUND_LEVELS, SOUND_OF_SPORT, SOUND_SETS } from "./sound.sets.ts";
6
+ export type { CueKind, SoundBed, SoundBedId, SoundLayer, SoundLevel, SoundSet, SoundVoice } from "./sound.sets.ts";
7
+ export { SOUND_WORDS } from "./sound.words.ts";
8
+ export type { SoundWords } from "./sound.words.ts";
@@ -0,0 +1,6 @@
1
+ // The sound entry (`@johnmorrisdotca/kyogijo-stadiums/sound`): a surface's sound in a page, a crowd bed for its kind of venue at a level, heard in the room its venue makes, and the cues of
2
+ // its sport (a goal horn, a whistle, a buzzer, a bell) from a replay it follows or from events pushed as they happen. Made in the browser with Web Audio; recordings are a page's own files.
3
+ export { SOUND_TAIL, createKyogijoSound } from "./sound.dom.js";
4
+ export { LEVEL_MIX, cueKindOf, cuesOf, cuesOfChange, excitementAt, levelFor, soundLevelsOf, soundMix, soundRoom, soundSetOf, soundVoicesOf } from "./sound.mix.js";
5
+ export { CUE_KINDS, SOUND_BEDS, SOUND_LEVELS, SOUND_OF_SPORT, SOUND_SETS } from "./sound.sets.js";
6
+ export { SOUND_WORDS } from "./sound.words.js";
@@ -0,0 +1,113 @@
1
+ import type { GameMoment, SoundContext, SoundCue, SoundPeriod, SoundVenue } from "./sound.mix.ts";
2
+ import type { SoundLayer, SoundLevel, SoundVoice } from "./sound.sets.ts";
3
+ import type { ReplayController } from "./replay.control.ts";
4
+ import type { ReplayEvent } from "./replay.types.ts";
5
+ /** The recordings a page may give, by the layer or the voice they stand for; anything not given is made in the browser. */
6
+ export type SoundFileName = SoundLayer | SoundVoice;
7
+ /** What the engine needs of an audio context: a page's `AudioContext`, or a stand-in in a test. */
8
+ export type SoundAudioContext = BaseAudioContext & {
9
+ resume(): Promise<void>;
10
+ suspend(): Promise<void>;
11
+ };
12
+ /** How the engine fetches a recording: `fetch` by default. */
13
+ export type SoundFetch = (url: string) => Promise<{
14
+ ok: boolean;
15
+ arrayBuffer(): Promise<ArrayBuffer>;
16
+ }>;
17
+ /** A crowd level, or `auto`: the level follows the game a sound follows (`excitementAt`), and is `engaged` with no game. */
18
+ export type SoundLevelChoice = SoundLevel | "auto";
19
+ /** The settings of a surface's sound. */
20
+ export interface SoundSettings {
21
+ /** The surface (`nhl`, `soccer`, ...). A surface with no sound set makes no sound and offers no levels (`state().has`). */
22
+ sport: string;
23
+ venue?: SoundVenue;
24
+ /** The share of the seats taken, 0 to 1: 0 is no crowd (a page's Crowd off). */
25
+ crowd?: number;
26
+ view?: "2d" | "3d";
27
+ /** The crowd's level: `engaged` by default. */
28
+ level?: SoundLevelChoice;
29
+ /** 0 to 1: 0.6 by default. 0 is muted, and nothing is heard or made. */
30
+ volume?: number;
31
+ /** Recordings by name, for the voices and layers a page has files for (the demo's: `demo/sounds/`). Fetched only when the sound is first turned on, and decoded only when it is heard. */
32
+ files?: Partial<Record<SoundFileName, string>>;
33
+ /** Makes the page's one audio context: `new AudioContext()` by default. A test gives a stand-in. */
34
+ audio?: () => SoundAudioContext;
35
+ fetch?: SoundFetch;
36
+ /** The element the surface is drawn in: the sound stops while it is out of sight. */
37
+ host?: Element;
38
+ /** The page, for whether it is hidden: `document` by default. */
39
+ doc?: Document;
40
+ /** Told of every cue as it plays: a page may show it. */
41
+ onCue?: (cue: SoundCue) => void;
42
+ }
43
+ /** Why a sound is not heard. */
44
+ export type SoundQuiet = "no-sound" | "off" | "muted" | "hidden" | "out-of-sight" | "paused";
45
+ /** What a sound is doing. */
46
+ export interface SoundState {
47
+ /** The surface has a sound set: a page shows the sound's controls only when it has. */
48
+ has: boolean;
49
+ on: boolean;
50
+ /** Heard now: on, with volume, the page shown, the surface in sight and its game not paused. */
51
+ audible: boolean;
52
+ /** Why it is not heard, when it is not. */
53
+ quiet?: SoundQuiet;
54
+ sport: string;
55
+ volume: number;
56
+ level: SoundLevelChoice;
57
+ /** The level heard: the choice, the game's under `auto`, held to what the surface offers. */
58
+ heard: SoundLevel;
59
+ /** The levels the surface offers, quietest first. */
60
+ levels: readonly SoundLevel[];
61
+ /** The voices its cues use. */
62
+ voices: readonly SoundVoice[];
63
+ /** The recordings fetched (they are decoded when heard). */
64
+ fetched: readonly SoundFileName[];
65
+ /** Following a replay. */
66
+ following: boolean;
67
+ }
68
+ /** Following a replay: its periods and its score at a time, for the cues of the periods' ends and for the level under `auto`. */
69
+ export interface SoundFollow {
70
+ periods?: readonly SoundPeriod[];
71
+ scoreAt?: (t: number) => {
72
+ home: number;
73
+ away: number;
74
+ };
75
+ }
76
+ /** A surface's sound in a page. */
77
+ export interface KyogijoSound {
78
+ /** Turn it on: call it from a press, so that the browser lets the audio start. The first call makes the audio context and fetches the recordings. */
79
+ enable(): Promise<void>;
80
+ /** Turn it off: nothing is heard, made or decoded, and the audio context is suspended when no other sound of the page is heard. */
81
+ disable(): void;
82
+ setVolume(volume: number): void;
83
+ setLevel(level: SoundLevelChoice): void;
84
+ /** Change the context: another surface, venue, crowd or view. The levels and cues offered follow the surface. */
85
+ setContext(context: Partial<SoundContext>): void;
86
+ /** Follow a replay's controller: the cues of its events as they go by while it plays, quiet while it is paused, the level under `auto` from its game. Returns a function that stops it. */
87
+ follow(controller: ReplayController, game?: SoundFollow): () => void;
88
+ /** A live event, pushed as it happens: its cues play now. A goal lifts the level under `auto` for a few seconds. */
89
+ cue(event: Pick<ReplayEvent, "kind" | "team">): void;
90
+ /** The live game's state for the level under `auto`, or `undefined` for none. */
91
+ game(moment: GameMoment | undefined): void;
92
+ state(): SoundState;
93
+ /** Stop and take everything away. */
94
+ destroy(): void;
95
+ }
96
+ /** How long the sound is held on after a game's clock stopped on a cue (its end), in seconds. */
97
+ export declare const SOUND_TAIL = 5;
98
+ /**
99
+ * The sound of a surface: its crowd bed at a level and the cues of its game. Nothing happens until `enable()`, which a page calls from a press.
100
+ * @example
101
+ * ```ts no-run
102
+ * import { createKyogijoSound } from "@johnmorrisdotca/kyogijo-stadiums/sound";
103
+ * import { mountKyogijoTurn } from "@johnmorrisdotca/kyogijo-stadiums/turn";
104
+ *
105
+ * const host = document.querySelector<HTMLElement>("#rink")!;
106
+ * const sound = createKyogijoSound({ sport: "nhl", venue: { roof: "fixed", seats: 18000 }, view: "3d", level: "auto", host });
107
+ * document.querySelector("#sound")!.addEventListener("click", () => sound.enable());
108
+ * const view = mountKyogijoTurn(host, { sport: "nhl" });
109
+ * const replay = view.replay({ frames: [{ t: 0, objects: [] }, { t: 60, objects: [] }], events: [{ t: 30, kind: "goal", team: "home" }] });
110
+ * sound.follow(replay, { periods: [{ start: 0, end: 60 }] });
111
+ * ```
112
+ */
113
+ export declare function createKyogijoSound(settings: SoundSettings): KyogijoSound;