@johnmorrisdotca/kyogijo-stadiums 0.60.1 → 0.62.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 +19 -0
- package/README.md +4 -2
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/replay-entry.d.ts +7 -3
- package/dist/replay-entry.js +3 -1
- package/dist/replay.board.d.ts +75 -4
- package/dist/replay.board.js +113 -13
- package/dist/replay.d.ts +1 -1
- package/dist/replay.deadtime.d.ts +58 -0
- package/dist/replay.deadtime.js +169 -0
- package/dist/replay.hockey.d.ts +143 -0
- package/dist/replay.hockey.js +276 -0
- package/dist/replay.js +12 -6
- package/dist/replay.types.d.ts +41 -0
- package/dist/sound-entry.d.ts +8 -0
- package/dist/sound-entry.js +6 -0
- package/dist/sound.dom.d.ts +113 -0
- package/dist/sound.dom.js +508 -0
- package/dist/sound.mix.d.ts +158 -0
- package/dist/sound.mix.js +234 -0
- package/dist/sound.sets.d.ts +70 -0
- package/dist/sound.sets.js +85 -0
- package/dist/sound.synth.d.ts +38 -0
- package/dist/sound.synth.js +303 -0
- package/dist/sound.words.d.ts +16 -0
- package/dist/sound.words.js +21 -0
- package/dist/strings.core.d.ts +1 -0
- package/dist/strings.en.js +1 -1
- package/dist/strings.ja.js +1 -1
- package/dist/turn.replay.board.d.ts +1 -1
- package/dist/turn.replay.board.js +20 -3
- package/dist/venue.boards.d.ts +6 -0
- package/dist/venue.boards.js +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/** A seeded generator of numbers from 0 to 1 (mulberry32): the noise is the same every time. */
|
|
2
|
+
export function seeded(seed) {
|
|
3
|
+
let a = seed >>> 0;
|
|
4
|
+
return () => {
|
|
5
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
6
|
+
let t = a;
|
|
7
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
8
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
9
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Samples of noise of a colour, from a seed, between -1 and 1. */
|
|
13
|
+
export function noiseSamples(length, seed, colour = "white") {
|
|
14
|
+
const random = seeded(seed);
|
|
15
|
+
const out = new Float32Array(length);
|
|
16
|
+
// Paul Kellet's economy filter for pink; a leaky sum for brown.
|
|
17
|
+
let b0 = 0;
|
|
18
|
+
let b1 = 0;
|
|
19
|
+
let b2 = 0;
|
|
20
|
+
let last = 0;
|
|
21
|
+
for (let i = 0; i < length; i++) {
|
|
22
|
+
const white = random() * 2 - 1;
|
|
23
|
+
if (colour === "pink") {
|
|
24
|
+
b0 = 0.99765 * b0 + white * 0.099046;
|
|
25
|
+
b1 = 0.963 * b1 + white * 0.2965164;
|
|
26
|
+
b2 = 0.57 * b2 + white * 1.0526913;
|
|
27
|
+
out[i] = (b0 + b1 + b2 + white * 0.1848) * 0.2;
|
|
28
|
+
}
|
|
29
|
+
else if (colour === "brown") {
|
|
30
|
+
last = (last + 0.02 * white) / 1.02;
|
|
31
|
+
out[i] = last * 3.5;
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
out[i] = white;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Samples of hands clapping: many short bursts of noise at random moments, `density` claps a second, for `length` samples at `rate`. */
|
|
39
|
+
export function applauseSamples(length, rate, seed, density = 220) {
|
|
40
|
+
const random = seeded(seed);
|
|
41
|
+
const out = new Float32Array(length);
|
|
42
|
+
const claps = Math.round((length / rate) * density);
|
|
43
|
+
const clap = Math.round(rate * 0.012);
|
|
44
|
+
for (let c = 0; c < claps; c++) {
|
|
45
|
+
const at = Math.floor(random() * length);
|
|
46
|
+
const loud = 0.25 + random() * 0.75;
|
|
47
|
+
for (let i = 0; i < clap; i++) {
|
|
48
|
+
// A loop: a clap near the end goes on at the start, so the loop has no seam.
|
|
49
|
+
const j = (at + i) % length;
|
|
50
|
+
out[j] += (random() * 2 - 1) * loud * Math.exp((-6 * i) / clap);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let peak = 0;
|
|
54
|
+
for (const s of out)
|
|
55
|
+
peak = Math.max(peak, Math.abs(s));
|
|
56
|
+
if (peak > 0)
|
|
57
|
+
for (let i = 0; i < length; i++)
|
|
58
|
+
out[i] /= peak;
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** Samples of people talking: noise whose level rises and falls with syllables, about four a second from each of many voices, summed. */
|
|
62
|
+
export function murmurSamples(length, rate, seed, voices = 24) {
|
|
63
|
+
const random = seeded(seed);
|
|
64
|
+
const noise = noiseSamples(length, seed + 1, "pink");
|
|
65
|
+
const envelope = new Float32Array(length);
|
|
66
|
+
for (let v = 0; v < voices; v++) {
|
|
67
|
+
let at = Math.floor(random() * rate * 0.3);
|
|
68
|
+
while (at < length) {
|
|
69
|
+
const syllable = Math.round(rate * (0.12 + random() * 0.16));
|
|
70
|
+
const loud = 0.3 + random() * 0.7;
|
|
71
|
+
for (let i = 0; i < syllable; i++)
|
|
72
|
+
envelope[(at + i) % length] += loud * Math.sin((Math.PI * i) / syllable);
|
|
73
|
+
at += syllable + Math.round(rate * random() * (random() < 0.15 ? 0.9 : 0.12));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let peak = 0;
|
|
77
|
+
for (let i = 0; i < length; i++) {
|
|
78
|
+
noise[i] *= envelope[i];
|
|
79
|
+
peak = Math.max(peak, Math.abs(noise[i]));
|
|
80
|
+
}
|
|
81
|
+
if (peak > 0)
|
|
82
|
+
for (let i = 0; i < length; i++)
|
|
83
|
+
noise[i] /= peak;
|
|
84
|
+
return noise;
|
|
85
|
+
}
|
|
86
|
+
/** A room's echo as an impulse response: noise falling 60 dB over `decay` seconds, two channels a little apart. */
|
|
87
|
+
export function impulseSamples(decay, rate, seed) {
|
|
88
|
+
const length = Math.max(1, Math.round(Math.min(4, Math.max(0.2, decay)) * rate));
|
|
89
|
+
const left = noiseSamples(length, seed, "white");
|
|
90
|
+
const right = noiseSamples(length, seed + 7, "white");
|
|
91
|
+
for (let i = 0; i < length; i++) {
|
|
92
|
+
const fall = Math.exp((-6.9 * i) / length);
|
|
93
|
+
left[i] *= fall;
|
|
94
|
+
right[i] *= fall;
|
|
95
|
+
}
|
|
96
|
+
return [left, right];
|
|
97
|
+
}
|
|
98
|
+
/** A buffer of the context from samples (one channel, or two). */
|
|
99
|
+
export function bufferOf(context, channels) {
|
|
100
|
+
const buffer = context.createBuffer(channels.length, channels[0].length, context.sampleRate);
|
|
101
|
+
channels.forEach((samples, i) => buffer.getChannelData(i).set(samples));
|
|
102
|
+
return buffer;
|
|
103
|
+
}
|
|
104
|
+
/** How many seconds each made layer loops for: long enough that the loop is not heard. */
|
|
105
|
+
export const LAYER_SECONDS = 6;
|
|
106
|
+
/**
|
|
107
|
+
* How much each made layer is turned down or up so that, through its filter, it is as loud as the demo's recordings are made (an RMS of -20 dBFS; the pool's water -22, and the building's air
|
|
108
|
+
* a quiet -30): measured once in a browser by rendering each layer offline.
|
|
109
|
+
*/
|
|
110
|
+
export const LAYER_TRIM = { room: 0.22, water: 0.45, murmur: 1.58, applause: 1.41, roar: 1.12 };
|
|
111
|
+
/** The buffer a layer loops when it has no recording. */
|
|
112
|
+
export function layerBuffer(context, layer) {
|
|
113
|
+
const rate = context.sampleRate;
|
|
114
|
+
const length = Math.round(LAYER_SECONDS * rate);
|
|
115
|
+
const seed = { room: 11, water: 23, murmur: 37, applause: 41, roar: 53 }[layer];
|
|
116
|
+
const colour = layer === "room" ? "brown" : layer === "water" ? "white" : "pink";
|
|
117
|
+
const make = (s) => (layer === "murmur" ? murmurSamples(length, rate, s) : layer === "applause" ? applauseSamples(length, rate, s) : noiseSamples(length, s, colour));
|
|
118
|
+
const channels = [make(seed), make(seed + 100)];
|
|
119
|
+
for (const samples of channels)
|
|
120
|
+
for (let i = 0; i < samples.length; i++)
|
|
121
|
+
samples[i] *= LAYER_TRIM[layer];
|
|
122
|
+
return bufferOf(context, channels);
|
|
123
|
+
}
|
|
124
|
+
/** The filter that shapes a made layer: the building's low air, water's hiss, talk's middle, a crowd's voice. */
|
|
125
|
+
export function layerFilter(context, layer) {
|
|
126
|
+
const filter = context.createBiquadFilter();
|
|
127
|
+
const shape = {
|
|
128
|
+
room: ["lowpass", 260, 0.7],
|
|
129
|
+
water: ["bandpass", 2400, 0.6],
|
|
130
|
+
murmur: ["bandpass", 700, 0.55],
|
|
131
|
+
applause: ["highpass", 900, 0.7],
|
|
132
|
+
roar: ["bandpass", 620, 0.9],
|
|
133
|
+
};
|
|
134
|
+
const [type, frequency, q] = shape[layer];
|
|
135
|
+
filter.type = type;
|
|
136
|
+
filter.frequency.value = frequency;
|
|
137
|
+
filter.Q.value = q;
|
|
138
|
+
return filter;
|
|
139
|
+
}
|
|
140
|
+
/** An envelope on a gain: up in `attack`, held, then down in `release`, from `when`. */
|
|
141
|
+
function envelope(gain, when, peak, attack, hold, release) {
|
|
142
|
+
gain.setValueAtTime(0, when);
|
|
143
|
+
gain.linearRampToValueAtTime(peak, when + attack);
|
|
144
|
+
gain.setValueAtTime(peak, when + attack + hold);
|
|
145
|
+
gain.linearRampToValueAtTime(0, when + attack + hold + release);
|
|
146
|
+
return when + attack + hold + release;
|
|
147
|
+
}
|
|
148
|
+
/** An oscillator from `when` to `until`, into `into`. */
|
|
149
|
+
function tone(context, into, type, frequency, when, until) {
|
|
150
|
+
const osc = context.createOscillator();
|
|
151
|
+
osc.type = type;
|
|
152
|
+
osc.frequency.value = frequency;
|
|
153
|
+
osc.connect(into);
|
|
154
|
+
osc.start(when);
|
|
155
|
+
osc.stop(until);
|
|
156
|
+
return osc;
|
|
157
|
+
}
|
|
158
|
+
/** A burst of noise from a buffer, from `when` to `until`, into `into`. */
|
|
159
|
+
function burst(context, into, noise, when, until) {
|
|
160
|
+
const source = context.createBufferSource();
|
|
161
|
+
source.buffer = noise;
|
|
162
|
+
source.loop = true;
|
|
163
|
+
source.connect(into);
|
|
164
|
+
source.start(when);
|
|
165
|
+
source.stop(until);
|
|
166
|
+
return source;
|
|
167
|
+
}
|
|
168
|
+
/** One blast of a pea whistle: a high tone with a fast trill, and a little breath. */
|
|
169
|
+
function whistleBlast(context, out, noise, when, length) {
|
|
170
|
+
const amp = context.createGain();
|
|
171
|
+
amp.connect(out);
|
|
172
|
+
const until = envelope(amp.gain, when, 0.35, 0.015, length, 0.06);
|
|
173
|
+
const osc = tone(context, amp, "sine", 2900, when, until);
|
|
174
|
+
const trill = context.createOscillator();
|
|
175
|
+
trill.frequency.value = 28;
|
|
176
|
+
const depth = context.createGain();
|
|
177
|
+
depth.gain.value = 140;
|
|
178
|
+
trill.connect(depth);
|
|
179
|
+
depth.connect(osc.frequency);
|
|
180
|
+
trill.start(when);
|
|
181
|
+
trill.stop(until);
|
|
182
|
+
const breath = context.createBiquadFilter();
|
|
183
|
+
breath.type = "bandpass";
|
|
184
|
+
breath.frequency.value = 2900;
|
|
185
|
+
breath.Q.value = 4;
|
|
186
|
+
const breathGain = context.createGain();
|
|
187
|
+
breathGain.gain.value = 0.25;
|
|
188
|
+
breath.connect(breathGain);
|
|
189
|
+
breathGain.connect(amp);
|
|
190
|
+
burst(context, breath, noise, when, until);
|
|
191
|
+
return until;
|
|
192
|
+
}
|
|
193
|
+
/** One strike of a ring's bell: three partials that ring down. */
|
|
194
|
+
function bellStrike(context, out, when) {
|
|
195
|
+
const until = when + 1.4;
|
|
196
|
+
for (const [frequency, loud] of [[830, 0.3], [1490, 0.18], [2210, 0.12]]) {
|
|
197
|
+
const amp = context.createGain();
|
|
198
|
+
amp.gain.setValueAtTime(loud, when);
|
|
199
|
+
amp.gain.exponentialRampToValueAtTime(0.0001, until);
|
|
200
|
+
amp.connect(out);
|
|
201
|
+
tone(context, amp, "sine", frequency, when, until);
|
|
202
|
+
}
|
|
203
|
+
return until;
|
|
204
|
+
}
|
|
205
|
+
/** A crowd's vowel: noise through a filter that opens from `from` to `to` hertz, swelling and falling away. */
|
|
206
|
+
function vowel(context, out, noise, when, from, to, peak, attack, release) {
|
|
207
|
+
const filter = context.createBiquadFilter();
|
|
208
|
+
filter.type = "bandpass";
|
|
209
|
+
filter.Q.value = 1.2;
|
|
210
|
+
filter.frequency.setValueAtTime(from, when);
|
|
211
|
+
filter.frequency.linearRampToValueAtTime(to, when + attack);
|
|
212
|
+
const amp = context.createGain();
|
|
213
|
+
filter.connect(amp);
|
|
214
|
+
amp.connect(out);
|
|
215
|
+
const until = envelope(amp.gain, when, peak, attack, 0.1, release);
|
|
216
|
+
burst(context, filter, noise, when, until);
|
|
217
|
+
return until;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Make a voice in the browser at `when` on the context's clock, into `out`, from the noise of `noise` (a pink loop). Returns when it ends. The horn is one chord held, never a tune; the
|
|
221
|
+
* swell is not here, as it moves the bed's roar (the engine does it).
|
|
222
|
+
*/
|
|
223
|
+
export function makeVoice(context, voice, out, buffers, when) {
|
|
224
|
+
const noise = buffers.noise();
|
|
225
|
+
switch (voice) {
|
|
226
|
+
case "whistle":
|
|
227
|
+
return whistleBlast(context, out, noise, when, 0.45);
|
|
228
|
+
case "whistle-end": {
|
|
229
|
+
const second = whistleBlast(context, out, noise, when, 0.22) + 0.12;
|
|
230
|
+
const third = whistleBlast(context, out, noise, second, 0.22) + 0.12;
|
|
231
|
+
return whistleBlast(context, out, noise, third, 1.3);
|
|
232
|
+
}
|
|
233
|
+
case "horn": {
|
|
234
|
+
const filter = context.createBiquadFilter();
|
|
235
|
+
filter.type = "lowpass";
|
|
236
|
+
filter.frequency.value = 1100;
|
|
237
|
+
filter.Q.value = 1;
|
|
238
|
+
const amp = context.createGain();
|
|
239
|
+
filter.connect(amp);
|
|
240
|
+
amp.connect(out);
|
|
241
|
+
const until = envelope(amp.gain, when, 0.22, 0.06, 2.6, 0.6);
|
|
242
|
+
for (const frequency of [98, 98.6, 123.5, 146.8])
|
|
243
|
+
tone(context, filter, "sawtooth", frequency, when, until);
|
|
244
|
+
return until;
|
|
245
|
+
}
|
|
246
|
+
case "buzzer": {
|
|
247
|
+
const filter = context.createBiquadFilter();
|
|
248
|
+
filter.type = "lowpass";
|
|
249
|
+
filter.frequency.value = 2200;
|
|
250
|
+
const amp = context.createGain();
|
|
251
|
+
filter.connect(amp);
|
|
252
|
+
amp.connect(out);
|
|
253
|
+
const until = envelope(amp.gain, when, 0.16, 0.01, 1.3, 0.04);
|
|
254
|
+
tone(context, filter, "square", 196, when, until);
|
|
255
|
+
tone(context, filter, "sawtooth", 392, when, until);
|
|
256
|
+
return until;
|
|
257
|
+
}
|
|
258
|
+
case "bell":
|
|
259
|
+
return [0, 0.3, 0.6].reduce((end, at) => Math.max(end, bellStrike(context, out, when + at)), when);
|
|
260
|
+
case "gun": {
|
|
261
|
+
const filter = context.createBiquadFilter();
|
|
262
|
+
filter.type = "highpass";
|
|
263
|
+
filter.frequency.value = 700;
|
|
264
|
+
const amp = context.createGain();
|
|
265
|
+
filter.connect(amp);
|
|
266
|
+
amp.connect(out);
|
|
267
|
+
amp.gain.setValueAtTime(0.9, when);
|
|
268
|
+
amp.gain.exponentialRampToValueAtTime(0.0001, when + 0.35);
|
|
269
|
+
burst(context, filter, noise, when, when + 0.35);
|
|
270
|
+
const thump = context.createGain();
|
|
271
|
+
thump.gain.setValueAtTime(0.6, when);
|
|
272
|
+
thump.gain.exponentialRampToValueAtTime(0.0001, when + 0.25);
|
|
273
|
+
thump.connect(out);
|
|
274
|
+
tone(context, thump, "sine", 85, when, when + 0.25);
|
|
275
|
+
return when + 0.35;
|
|
276
|
+
}
|
|
277
|
+
case "beep": {
|
|
278
|
+
const amp = context.createGain();
|
|
279
|
+
amp.connect(out);
|
|
280
|
+
const until = envelope(amp.gain, when, 0.18, 0.005, 0.45, 0.02);
|
|
281
|
+
tone(context, amp, "square", 1000, when, until);
|
|
282
|
+
return until;
|
|
283
|
+
}
|
|
284
|
+
case "ooh":
|
|
285
|
+
return vowel(context, out, noise, when, 360, 640, 1.2, 0.35, 1.5);
|
|
286
|
+
case "cheer":
|
|
287
|
+
return vowel(context, out, noise, when, 700, 1100, 1, 0.08, 1.2);
|
|
288
|
+
case "applause": {
|
|
289
|
+
const amp = context.createGain();
|
|
290
|
+
amp.connect(out);
|
|
291
|
+
const until = envelope(amp.gain, when, 0.6, 0.25, 1.8, 1.6);
|
|
292
|
+
const source = context.createBufferSource();
|
|
293
|
+
source.buffer = buffers.applause();
|
|
294
|
+
source.loop = true;
|
|
295
|
+
const filter = layerFilter(context, "applause");
|
|
296
|
+
source.connect(filter);
|
|
297
|
+
filter.connect(amp);
|
|
298
|
+
source.start(when);
|
|
299
|
+
source.stop(until);
|
|
300
|
+
return until;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SoundBedId, SoundLevel } from "./sound.sets.ts";
|
|
2
|
+
/** The words of the sound in a language. */
|
|
3
|
+
export interface SoundWords {
|
|
4
|
+
/** The sound's toggle and its tip. */
|
|
5
|
+
sound: string;
|
|
6
|
+
soundTip: string;
|
|
7
|
+
volume: string;
|
|
8
|
+
/** The group of the crowd's levels. */
|
|
9
|
+
crowdLevel: string;
|
|
10
|
+
/** The level that follows the game. */
|
|
11
|
+
auto: string;
|
|
12
|
+
levels: Readonly<Record<SoundLevel, string>>;
|
|
13
|
+
beds: Readonly<Record<SoundBedId, string>>;
|
|
14
|
+
}
|
|
15
|
+
/** The sound's words, by language. */
|
|
16
|
+
export declare const SOUND_WORDS: Readonly<Record<"en" | "ja", SoundWords>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The sound's words, by language. */
|
|
2
|
+
export const SOUND_WORDS = {
|
|
3
|
+
en: {
|
|
4
|
+
sound: "Sound",
|
|
5
|
+
soundTip: "The crowd and the game's sounds, made in your browser. Off until you turn it on.",
|
|
6
|
+
volume: "Volume",
|
|
7
|
+
crowdLevel: "Crowd",
|
|
8
|
+
auto: "Follow the game",
|
|
9
|
+
levels: { quiet: "Quiet", murmur: "Murmur", engaged: "Engaged", loud: "Loud", roar: "Roar" },
|
|
10
|
+
beds: { arena: "Arena", stadium: "Open stadium", ballpark: "Ballpark", hall: "Court hall", pool: "Pool", dojo: "Dojo", ring: "Ring", track: "Track" },
|
|
11
|
+
},
|
|
12
|
+
ja: {
|
|
13
|
+
sound: "サウンド",
|
|
14
|
+
soundTip: "観客と試合の音をブラウザーの中で作ります。オンにするまで鳴りません。",
|
|
15
|
+
volume: "音量",
|
|
16
|
+
crowdLevel: "観客",
|
|
17
|
+
auto: "試合に合わせる",
|
|
18
|
+
levels: { quiet: "静か", murmur: "ざわめき", engaged: "盛り上がり", loud: "歓声", roar: "大歓声" },
|
|
19
|
+
beds: { arena: "アリーナ", stadium: "屋外スタジアム", ballpark: "野球場", hall: "体育館", pool: "プール", dojo: "道場", ring: "リング", track: "陸上競技場" },
|
|
20
|
+
},
|
|
21
|
+
};
|
package/dist/strings.core.d.ts
CHANGED
package/dist/strings.en.js
CHANGED
|
@@ -439,7 +439,7 @@ export const WORDS_EN = {
|
|
|
439
439
|
grid: { title: "A grid of drawings ({n})", drawings: "Drawings ({n}): {list}.", key: "Key: {list}.", cell: "Drawing {n} of {total}", listSep: "; " },
|
|
440
440
|
replay: {
|
|
441
441
|
title: "Replay, {time}", objects: "{n} tracked objects", objectsOne: "{n} tracked object", events: "{n} events", eventsOne: "{n} event", players: "Players", others: "Ball, puck and officials", trail: "Trail of the last {s} seconds", numbers: "Shirt numbers", eventLines: "Passes and shots",
|
|
442
|
-
home: "home", away: "away", ball: "ball", puck: "puck", official: "official", pass: "Pass", shot: "Shot", goal: "Goal", whistle: "Whistle",
|
|
442
|
+
home: "home", away: "away", ball: "ball", puck: "puck", official: "official", pass: "Pass", shot: "Shot", goal: "Goal", whistle: "Whistle", faceoff: "Face-off",
|
|
443
443
|
announce: "{what}, {time}", announceTeam: "{what}, {team}, {time}",
|
|
444
444
|
group: "Replay controls", play: "Play", pause: "Pause", back: "Previous frame", forward: "Next frame", stepBack: "Back", stepForward: "Forward", speed: "Speed", loop: "Loop", position: "Position", time: "Time", event: "Event", spokenSeconds: "0:{ss}", spokenMinutes: "{m}:{ss}", spokenHours: "{h}:{mm}:{ss}", positionValue: "{t} of {total}",
|
|
445
445
|
keys: "Replay of the play. Space plays or pauses, the left and right arrow keys step by a frame, Shift with an arrow key moves 5 seconds, and Home and End go to the ends.",
|
package/dist/strings.ja.js
CHANGED
|
@@ -464,7 +464,7 @@ export const WORDS_JA = {
|
|
|
464
464
|
grid: { title: "図のグリッド({n}枚)", drawings: "図({n}枚):{list}。", key: "凡例:{list}。", cell: "{total}枚中{n}枚目の図", listSep: ";" },
|
|
465
465
|
replay: {
|
|
466
466
|
title: "リプレイ(再生時間{time})", objects: "追跡対象は{n}個", objectsOne: "追跡対象は{n}個", events: "イベント{n}件", eventsOne: "イベント{n}件", players: "選手", others: "ボール・パック・審判", trail: "直近{s}秒の軌跡", numbers: "背番号", eventLines: "パスとシュート",
|
|
467
|
-
home: "ホーム", away: "アウェイ", ball: "ボール", puck: "パック", official: "審判", pass: "パス", shot: "シュート", goal: "ゴール", whistle: "ホイッスル",
|
|
467
|
+
home: "ホーム", away: "アウェイ", ball: "ボール", puck: "パック", official: "審判", pass: "パス", shot: "シュート", goal: "ゴール", whistle: "ホイッスル", faceoff: "フェイスオフ",
|
|
468
468
|
announce: "{what}、{time}", announceTeam: "{team}の{what}、{time}",
|
|
469
469
|
group: "リプレイの操作", play: "再生", pause: "一時停止", back: "前のコマ", forward: "次のコマ", stepBack: "前へ", stepForward: "次へ", speed: "再生速度", loop: "繰り返し", position: "再生位置", time: "時間", event: "イベント", spokenSeconds: "{s}秒", spokenMinutes: "{m}分{s}秒", spokenHours: "{h}時間{m}分{s}秒", positionValue: "{total}中{t}",
|
|
470
470
|
keys: "プレーのリプレイです。スペースキーで再生と一時停止、左右の矢印キーで1コマずつ前後に移動、Shiftキーを押しながら矢印キーで5秒ずつ移動、Homeキーで最初、Endキーで最後に移動します。",
|
|
@@ -7,7 +7,7 @@ import { type HungBoard } from "./venue.boards.ts";
|
|
|
7
7
|
export declare const hungBoards: (model: Pick<TurnModel, "uprights">) => HungBoard[];
|
|
8
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
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
|
|
10
|
+
/** What is written on a board, as one string: it changes when, and only when, the score, the period, the clock or a penalty's clock does. */
|
|
11
11
|
export declare const boardKey: (state: ReplayBoardState) => string;
|
|
12
12
|
/** The longest the game clock goes without a write while the replay plays, in milliseconds. */
|
|
13
13
|
export declare const CLOCK_EVERY = 1000;
|
|
@@ -7,11 +7,20 @@ export const hungBoards = (model) => model.uprights.items.flatMap((item) => (ite
|
|
|
7
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
8
|
export function hungBoardPlanes(hung, state) {
|
|
9
9
|
const clock = safeClock(state.clock);
|
|
10
|
-
const live = clock === "" ? undefined : { clock, ...(state.period === undefined ? {} : { period: state.period }) };
|
|
10
|
+
const live = clock === "" ? undefined : { clock, ...(state.period === undefined ? {} : { period: state.period }), ...powerPlay(state) };
|
|
11
11
|
return centreBoard(hung.id, hung.box, hung.lift, { ...hung.look, score: state.score, ...(live === undefined ? {} : { live }) });
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/** A power play on the board: the clocks of each side's penalties that make it play short, and which side has more skaters. Nothing when no side plays short. */
|
|
14
|
+
function powerPlay(state) {
|
|
15
|
+
const short = (state.penalties ?? []).filter((p) => p.short);
|
|
16
|
+
if (short.length === 0 || state.strength === undefined)
|
|
17
|
+
return {};
|
|
18
|
+
const [home, away] = state.strength;
|
|
19
|
+
const pp = home > away ? "home" : away > home ? "away" : undefined;
|
|
20
|
+
return { penalties: { home: short.filter((p) => p.team === "home").map((p) => p.left), away: short.filter((p) => p.team === "away").map((p) => p.left), ...(pp === undefined ? {} : { pp }) } };
|
|
21
|
+
}
|
|
22
|
+
/** What is written on a board, as one string: it changes when, and only when, the score, the period, the clock or a penalty's clock does. */
|
|
23
|
+
export const boardKey = (state) => `${state.score[0]}-${state.score[1]} ${state.period ?? ""} ${state.clock}${state.strength === undefined ? "" : ` ${state.strength.join("-")} ${(state.penalties ?? []).map((p) => `${p.team}${p.left}${p.short ? "" : "x"}`).join(",")}`}`;
|
|
15
24
|
/** The longest the game clock goes without a write while the replay plays, in milliseconds. */
|
|
16
25
|
export const CLOCK_EVERY = 1000;
|
|
17
26
|
const FACE = /-(south|north|east|west)$/;
|
|
@@ -29,6 +38,14 @@ export function boardFollower(timeline, now) {
|
|
|
29
38
|
one.face.dataset.score = `${s.score[0]}-${s.score[1]}`;
|
|
30
39
|
one.face.dataset.period = s.period === undefined ? "" : String(s.period);
|
|
31
40
|
one.face.dataset.clock = s.clock;
|
|
41
|
+
if (s.strength === undefined) {
|
|
42
|
+
delete one.face.dataset.strength;
|
|
43
|
+
delete one.face.dataset.penalties;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
one.face.dataset.strength = s.strength.join("-");
|
|
47
|
+
one.face.dataset.penalties = (s.penalties ?? []).map((p) => `${p.team} ${p.left}`).join(", ");
|
|
48
|
+
}
|
|
32
49
|
}
|
|
33
50
|
};
|
|
34
51
|
return {
|
package/dist/venue.boards.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface BoardLive {
|
|
|
20
20
|
period?: number;
|
|
21
21
|
/** The clock as it is written, `19:20`: digits and one colon. */
|
|
22
22
|
clock: string;
|
|
23
|
+
/** A power play: each side's penalty clocks (`1:42`, the soonest first, two at most are drawn) in its block of colour, and `PP` in the block of the side with more skaters. */
|
|
24
|
+
penalties?: {
|
|
25
|
+
home: string[];
|
|
26
|
+
away: string[];
|
|
27
|
+
pp?: "home" | "away";
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
30
|
/** 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
31
|
export interface HungBoard {
|
package/dist/venue.boards.js
CHANGED
|
@@ -136,6 +136,8 @@ 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
|
+
/** The letters a seven-segment board writes for a power play. */
|
|
140
|
+
const LETTERS = { P: "abefg" };
|
|
139
141
|
/** How wide a colon is, as a share of the height of the digits beside it. */
|
|
140
142
|
const COLON = 0.3;
|
|
141
143
|
/** 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. */
|
|
@@ -165,7 +167,7 @@ function segments(text, x, y, high) {
|
|
|
165
167
|
at += cw;
|
|
166
168
|
return;
|
|
167
169
|
}
|
|
168
|
-
for (const seg of SEGMENTS[Number(ch)] ?? "") {
|
|
170
|
+
for (const seg of LETTERS[ch] ?? SEGMENTS[Number(ch)] ?? "") {
|
|
169
171
|
const [rx, ry, rw, rh] = rects[seg];
|
|
170
172
|
d += `M${n(at + rx)} ${n(y + ry)}h${n(rw)}v${n(rh)}h${n(-rw)}z`;
|
|
171
173
|
}
|
|
@@ -220,6 +222,23 @@ function boardFace(w, h, look) {
|
|
|
220
222
|
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
223
|
if (live !== undefined)
|
|
222
224
|
svg += liveRow(w, h, room, live, c2);
|
|
225
|
+
// A power play: in each side's block of colour, its penalty clocks (one or two, stacked), or PP for the side with more skaters, dark on the colour.
|
|
226
|
+
if (live?.penalties !== undefined) {
|
|
227
|
+
const pen = live.penalties;
|
|
228
|
+
for (const [side, x] of [["home", edge * 2], ["away", w - edge * 2 - block]]) {
|
|
229
|
+
const lines = (pen.pp === side ? ["PP"] : pen[side].filter((one) => safeClock(one) !== "").slice(0, 2));
|
|
230
|
+
if (lines.length === 0)
|
|
231
|
+
continue;
|
|
232
|
+
const rows = lines.length;
|
|
233
|
+
const high2 = Math.min((hb * 0.8) / rows / 1.15, block / ((lines[0].length - (lines[0].includes(":") ? 1 : 0)) * 0.69 + 0.4));
|
|
234
|
+
let d = "";
|
|
235
|
+
lines.forEach((text, i) => {
|
|
236
|
+
const width = segments(text, 0, 0, high2).width;
|
|
237
|
+
d += segments(text, x + (block - width) / 2, yb + (hb - rows * high2 * 1.15) / 2 + i * high2 * 1.15, high2).d;
|
|
238
|
+
});
|
|
239
|
+
svg += `<path class="u-led-bed" d="${d}"/>`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
223
242
|
return svg;
|
|
224
243
|
}
|
|
225
244
|
/**
|
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.62.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.62.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.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",
|
|
@@ -239,6 +239,11 @@
|
|
|
239
239
|
"import": "./dist/replay-entry.js",
|
|
240
240
|
"default": "./dist/replay-entry.js"
|
|
241
241
|
},
|
|
242
|
+
"./sound": {
|
|
243
|
+
"types": "./dist/sound-entry.d.ts",
|
|
244
|
+
"import": "./dist/sound-entry.js",
|
|
245
|
+
"default": "./dist/sound-entry.js"
|
|
246
|
+
},
|
|
242
247
|
"./scene": {
|
|
243
248
|
"types": "./dist/scene-entry.d.ts",
|
|
244
249
|
"import": "./dist/scene-entry.js",
|
|
@@ -371,6 +376,8 @@
|
|
|
371
376
|
"cityctx:fetch": "node scripts/fetch-cityctx.mjs",
|
|
372
377
|
"cityctx:vox": "python3.13 -I scripts/cityctx_vox.py",
|
|
373
378
|
"walk:fetch": "node scripts/fetch-walk.mjs",
|
|
379
|
+
"sounds": "node scripts/make-sounds.mjs",
|
|
380
|
+
"sounds:fetch": "node scripts/fetch-sounds.mjs",
|
|
374
381
|
"outside:cost": "node scripts/outside-cost.mjs",
|
|
375
382
|
"plateau": "node scripts/build-plateau.mjs",
|
|
376
383
|
"plateau:fetch": "node scripts/fetch-plateau.mjs",
|