@johnmorrisdotca/kyogijo-stadiums 0.51.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,60 +33,30 @@ const ICONS = {
33
33
  const icon = (d) => `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="${d}"/></svg>`;
34
34
  const pathOf = (points) => points.map(([x, y], i) => `${i === 0 ? "M" : "L"}${num(x)} ${num(y)}`).join("");
35
35
  /**
36
- * Attach a replay to a container: the stage with the drawing, the controls under it, the keyboard and the live region. The drawing is written once
37
- * and then only the marks that move are moved, on `requestAnimationFrame`; a mark that comes or goes (an object that enters, an event that
38
- * begins) redraws the replay's own layers, never the surface. With `prefers-reduced-motion` nothing plays by itself and play steps instead.
36
+ * The painter of a replay: it moves the marks that move and nothing else. A mark that comes or goes (an object that enters, an event that begins) draws the replay's own group
37
+ * again, never the surface; a group whose marks cannot be matched to elements one for one is drawn again each time.
39
38
  */
40
- export function createReplayView(host, timeline, settings, initial) {
41
- const doc = host.ownerDocument;
42
- const info = replayInfo(timeline);
43
- const media = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
44
- const motionNow = () => (settings.motion === "smooth" ? "smooth" : settings.motion === "step" ? "step" : media?.matches === true ? "step" : "smooth");
45
- const controller = createReplay(timeline, { ...settings, motion: motionNow() });
46
- const id = initial.id ?? `ky-r${++counter}`;
47
- let options = { ...initial, id };
48
- let language = options.language === "ja" ? "ja" : "en";
49
- const words = () => KYOGIJO_STRINGS[language].replay;
50
- const root = doc.createElement("div");
51
- root.className = "ky-replay";
52
- const style = doc.createElement("style");
53
- style.textContent = REPLAY_STYLE;
54
- const stage = doc.createElement("div");
55
- stage.className = "ky-stage";
56
- stage.tabIndex = 0;
57
- stage.setAttribute("role", "group");
58
- const hint = doc.createElement("p");
59
- hint.className = "ky-hint";
60
- hint.id = `${id}-keys`;
61
- const bar = doc.createElement("div");
62
- bar.className = "ky-controls";
63
- bar.setAttribute("role", "group");
64
- const live = doc.createElement("div");
65
- live.className = "ky-live";
66
- live.setAttribute("role", "status");
67
- live.setAttribute("aria-live", "polite");
68
- root.append(style, stage, hint, bar, live);
69
- host.replaceChildren(root);
70
- // ---- The painter. ----
71
- let svg = null;
72
- let placer = replayPlacer(options, timeline);
39
+ export function replayPainter(source) {
40
+ const { doc, timeline, look } = source;
41
+ let placer = replayPlacer(source.options(), timeline);
73
42
  /** For each group of the replay in the drawing, the marks it holds (their keys): a group whose keys changed is drawn again, the others only move. */
74
43
  let shown = new Map();
75
44
  let bound = new Map();
76
45
  /** The groups whose marks cannot be matched to elements one for one, so that they are drawn again each time. */
77
46
  const loose = new Set();
78
47
  let painted = Number.NaN;
79
- const look = settings;
48
+ const keep = source.keep ?? (() => true);
49
+ const idOf = () => source.options().id ?? "";
80
50
  const groupsAt = (t) => {
81
51
  const snap = replaySnapshot(timeline, t, look);
82
- const groups = replayGroups(timeline, snap, look, language);
52
+ const groups = replayGroups(timeline, snap, look, source.language()).filter(keep);
83
53
  const places = placer(snap.objects.map((o) => [o.x, o.y]));
84
54
  return { groups, placed: new Map(snap.objects.map((o, i) => [o.id, places[i]])) };
85
55
  };
86
56
  const positions = (g, placed) => (g.kind === "lines" ? g.coords.map((path) => placer(path)) : g.keys.map((k) => placed.get(k) ?? [0, 0]));
87
- const holderOf = (g) => svg?.querySelector(`[id="${id}-${typeof g === "string" ? g : g.id}"]`) ?? null;
57
+ const holderOf = (g) => source.svg()?.querySelector(`[id="${idOf()}-${typeof g === "string" ? g : g.id}"]`) ?? null;
88
58
  /** Match the elements of a group that was just drawn to its marks, remembering where they are. */
89
- const bind = (g, placed) => {
59
+ const bindGroup = (g, placed) => {
90
60
  bound.delete(g.id);
91
61
  loose.delete(g.id);
92
62
  if (!g.moving)
@@ -104,8 +74,9 @@ export function createReplayView(host, timeline, settings, initial) {
104
74
  };
105
75
  /** Draw the groups that changed again, in their place among the others, and take away the ones that are gone. The surface and the groups that only move are left. */
106
76
  const patch = (groups, changed, gone, placed) => {
77
+ const svg = source.svg();
107
78
  const container = svg?.querySelector("g.ky-layers") ?? null;
108
- if (svg === null || container === null)
79
+ if (svg === null || svg === undefined || container === null)
109
80
  return false;
110
81
  for (const gid of gone) {
111
82
  holderOf(gid)?.remove();
@@ -115,7 +86,7 @@ export function createReplayView(host, timeline, settings, initial) {
115
86
  }
116
87
  for (const g of changed) {
117
88
  const scratch = doc.createElementNS(SVG_NS, "g");
118
- scratch.innerHTML = renderLayers(options, [g.layer]).svg;
89
+ scratch.innerHTML = renderLayers(source.options(), [g.layer]).svg;
119
90
  const made = scratch.querySelector("g.ky-layers > g.ky-layer");
120
91
  if (made === null)
121
92
  return false;
@@ -133,7 +104,7 @@ export function createReplayView(host, timeline, settings, initial) {
133
104
  before.before(made);
134
105
  }
135
106
  shown.set(g.id, g.keys.join(","));
136
- bind(g, placed);
107
+ bindGroup(g, placed);
137
108
  }
138
109
  container.setAttribute("data-layers", String(container.querySelectorAll(":scope > g.ky-layer").length));
139
110
  return true;
@@ -164,34 +135,59 @@ export function createReplayView(host, timeline, settings, initial) {
164
135
  }
165
136
  }
166
137
  };
167
- const paint = (t) => {
168
- if (svg === null || t === painted)
169
- return;
170
- painted = t;
171
- const { groups, placed } = groupsAt(t);
172
- const changed = groups.filter((g) => loose.has(g.id) || shown.get(g.id) !== g.keys.join(","));
173
- const gone = [...shown.keys()].filter((gid) => !groups.some((g) => g.id === gid));
174
- if (changed.length > 0 || gone.length > 0) {
175
- if (!patch(groups, changed, gone, placed)) {
176
- drawStage(t);
177
- return;
178
- }
179
- }
180
- move(groups, placed);
181
- };
182
- function drawStage(t) {
183
- stage.innerHTML = drawFrame(options, timeline, t, look);
184
- svg = stage.querySelector("svg.kyogijo");
185
- placer = replayPlacer(options, timeline);
138
+ const bind = (t) => {
139
+ placer = replayPlacer(source.options(), timeline);
186
140
  const { groups, placed } = groupsAt(t);
187
141
  shown = new Map(groups.map((g) => [g.id, g.keys.join(",")]));
188
142
  bound = new Map();
189
143
  loose.clear();
190
144
  painted = t;
191
145
  for (const g of groups)
192
- bind(g, placed);
193
- }
194
- // ---- The controls. ----
146
+ bindGroup(g, placed);
147
+ };
148
+ return {
149
+ bind,
150
+ paint(t) {
151
+ if (source.svg() === null || t === painted)
152
+ return;
153
+ painted = t;
154
+ const { groups, placed } = groupsAt(t);
155
+ const changed = groups.filter((g) => loose.has(g.id) || shown.get(g.id) !== g.keys.join(","));
156
+ const gone = [...shown.keys()].filter((gid) => !groups.some((g) => g.id === gid));
157
+ if (changed.length > 0 || gone.length > 0) {
158
+ if (!patch(groups, changed, gone, placed)) {
159
+ source.redraw(t);
160
+ bind(t);
161
+ return;
162
+ }
163
+ }
164
+ move(groups, placed);
165
+ },
166
+ };
167
+ }
168
+ /** Make the clock, the controls and the keys of a replay. Nothing runs until the reader (or `autoplay`) plays it, and nothing runs while it is paused, hidden or out of sight. */
169
+ export function replayDriver(doc, timeline, settings, source) {
170
+ const info = replayInfo(timeline);
171
+ const media = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
172
+ const motionNow = () => (settings.motion === "smooth" ? "smooth" : settings.motion === "step" ? "step" : media?.matches === true ? "step" : "smooth");
173
+ const controller = createReplay(timeline, { ...settings, motion: motionNow() });
174
+ const id = `ky-rd${++counter}`;
175
+ let language = source.language;
176
+ const words = () => KYOGIJO_STRINGS[language].replay;
177
+ const style = doc.createElement("style");
178
+ style.textContent = REPLAY_STYLE;
179
+ const hint = doc.createElement("p");
180
+ hint.className = "ky-hint";
181
+ hint.id = `${id}-keys`;
182
+ const bar = doc.createElement("div");
183
+ bar.className = "ky-controls";
184
+ bar.setAttribute("role", "group");
185
+ const live = doc.createElement("div");
186
+ live.className = "ky-live";
187
+ live.setAttribute("role", "status");
188
+ live.setAttribute("aria-live", "polite");
189
+ if (settings.controls === false)
190
+ bar.hidden = true;
195
191
  let sync = () => { };
196
192
  const buildBar = () => {
197
193
  const w = words();
@@ -241,17 +237,19 @@ export function createReplayView(host, timeline, settings, initial) {
241
237
  speed.hidden = stepMode;
242
238
  loop.hidden = stepMode;
243
239
  bar.append(back, play, forward, speed, loop, range, clock);
244
- hint.textContent = stepMode ? w.keysStep : w.keys;
245
- stage.setAttribute("aria-label", w.region);
246
- stage.setAttribute("aria-describedby", hint.id);
247
- stage.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight Home End");
248
- let shown = "";
240
+ hint.textContent = source.help === "turn" ? (stepMode ? w.keysTurnStep : w.keysTurn) : stepMode ? w.keysStep : w.keys;
241
+ if (source.labelled !== undefined) {
242
+ source.labelled.setAttribute("aria-label", w.region);
243
+ source.labelled.setAttribute("aria-describedby", hint.id);
244
+ source.labelled.setAttribute("aria-keyshortcuts", "Space ArrowLeft ArrowRight Shift+ArrowLeft Shift+ArrowRight Home End");
245
+ }
246
+ let said = "";
249
247
  sync = () => {
250
248
  const s = controller.state();
251
249
  const total = replayClock(info.duration);
252
250
  const text = `${replayClock(s.t - info.start)} / ${total}`;
253
- if (text !== shown) {
254
- shown = text;
251
+ if (text !== said) {
252
+ said = text;
255
253
  clock.textContent = text;
256
254
  range.setAttribute("aria-valuetext", fill(w.positionValue, { t: replaySpoken(s.t - info.start, w), total: replaySpoken(info.duration, w) }));
257
255
  }
@@ -267,79 +265,101 @@ export function createReplayView(host, timeline, settings, initial) {
267
265
  if (speed.value !== String(s.speed))
268
266
  speed.value = String(s.speed);
269
267
  loop.setAttribute("aria-pressed", String(s.loop));
270
- root.dataset.state = s.ended ? "ended" : s.playing ? "playing" : "paused";
271
- root.dataset.motion = s.motion;
268
+ source.holder.dataset.state = s.ended ? "ended" : s.playing ? "playing" : "paused";
269
+ source.holder.dataset.motion = s.motion;
272
270
  };
273
271
  sync(undefined);
274
272
  };
275
- if (settings.controls === false)
276
- bar.hidden = true;
277
- // ---- The clock on the page: animation frames while playing, and the events said aloud. ----
273
+ // ---- The clock on the page: animation frames while playing and in sight, and the events said aloud. ----
278
274
  let raf = 0;
279
275
  let last = 0;
280
- const frame = (now) => {
276
+ let onScreen = true;
277
+ const request = source.frames?.request ?? ((callback) => requestAnimationFrame(callback));
278
+ const cancel = source.frames?.cancel ?? ((id) => cancelAnimationFrame(id));
279
+ const want = () => {
280
+ if (raf !== 0 || !onScreen || !controller.state().playing)
281
+ return;
282
+ last = performance.now();
283
+ raf = request(frame);
284
+ };
285
+ function frame(now) {
281
286
  raf = 0;
282
- if (!controller.state().playing)
287
+ if (!controller.state().playing || !onScreen)
283
288
  return;
284
289
  const dt = Math.min(0.25, Math.max(0, (now - last) / 1000));
285
290
  last = now;
286
291
  controller.advance(dt);
287
- if (controller.state().playing && raf === 0)
288
- raf = requestAnimationFrame(frame);
289
- };
292
+ if (controller.state().playing && raf === 0 && onScreen)
293
+ raf = request(frame);
294
+ }
290
295
  const stop = controller.subscribe((change) => {
291
296
  sync(change);
292
- paint(change.state.t);
297
+ source.paint(change.state.t);
293
298
  if (settings.announce !== false && change.crossed.length > 0 && (change.reason === "tick" || change.reason === "step")) {
294
299
  live.textContent = change.crossed.map((e) => describeEvent(e, language, info.start)).join(". ");
295
300
  }
296
- if (change.reason === "play" && raf === 0) {
297
- last = performance.now();
298
- raf = requestAnimationFrame(frame);
299
- }
301
+ if (change.reason === "play")
302
+ want();
300
303
  if (!change.state.playing && raf !== 0) {
301
- cancelAnimationFrame(raf);
304
+ cancel(raf);
302
305
  raf = 0;
303
306
  }
304
307
  });
305
- const keys = (event) => {
308
+ // Scrolled out of sight, the clock holds (no frame is asked for) and goes on when it is back.
309
+ let seen;
310
+ if (source.seen !== undefined && typeof IntersectionObserver === "function") {
311
+ seen = new IntersectionObserver((entries) => {
312
+ onScreen = entries[entries.length - 1]?.isIntersecting !== false;
313
+ if (onScreen)
314
+ want();
315
+ else if (raf !== 0) {
316
+ cancel(raf);
317
+ raf = 0;
318
+ }
319
+ });
320
+ seen.observe(source.seen);
321
+ }
322
+ const act = (event, scope) => {
306
323
  if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey)
307
- return;
324
+ return false;
308
325
  const tag = event.target?.tagName ?? "";
309
326
  if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA")
310
- return;
311
- switch (event.key) {
312
- case " ":
313
- case "Spacebar":
327
+ return false;
328
+ // Over a 3D view the arrows and Home are the view's: the stage's own keys are Space, comma, full stop and End.
329
+ const key = scope === "stage" ? (event.code === "Comma" ? "back" : event.code === "Period" ? "on" : event.key === " " || event.key === "Spacebar" ? "toggle" : event.key === "End" ? "end" : "") : event.key === " " || event.key === "Spacebar" ? "toggle" : event.key === "ArrowRight" ? "on" : event.key === "ArrowLeft" ? "back" : event.key === "Home" ? "home" : event.key === "End" ? "end" : "";
330
+ switch (key) {
331
+ case "toggle":
314
332
  // A button takes the space itself.
315
333
  if (tag === "BUTTON")
316
- return;
334
+ return false;
317
335
  controller.toggle();
318
336
  break;
319
- case "ArrowRight":
337
+ case "on":
320
338
  if (event.shiftKey)
321
339
  controller.jump(5);
322
340
  else
323
341
  controller.step(1);
324
342
  break;
325
- case "ArrowLeft":
343
+ case "back":
326
344
  if (event.shiftKey)
327
345
  controller.jump(-5);
328
346
  else
329
347
  controller.step(-1);
330
348
  break;
331
- case "Home":
349
+ case "home":
332
350
  controller.seek(info.start);
333
351
  break;
334
- case "End":
352
+ case "end":
335
353
  controller.seek(info.end);
336
354
  break;
337
355
  default:
338
- return;
356
+ return false;
339
357
  }
340
358
  event.preventDefault();
359
+ return true;
341
360
  };
342
- root.addEventListener("keydown", keys);
361
+ const keys = (event) => void act(event, "bar");
362
+ source.keysOn.addEventListener("keydown", keys);
343
363
  // A page that is hidden does no work: the clock stops, and the reader starts it again.
344
364
  const hidden = () => {
345
365
  if (doc.visibilityState === "hidden")
@@ -357,31 +377,84 @@ export function createReplayView(host, timeline, settings, initial) {
357
377
  media.addListener(motion);
358
378
  }
359
379
  buildBar();
360
- drawStage(controller.state().t);
361
- if (settings.autoplay === true && controller.state().motion === "smooth" && doc.visibilityState !== "hidden")
362
- controller.play();
363
380
  return {
364
381
  controller,
365
- stage,
366
- draw(next) {
367
- const before = language;
368
- options = { ...next, id };
369
- language = options.language === "ja" ? "ja" : "en";
370
- // The controls are made again only when the words change: a reader's focus on one of them stays where it is when a score moves.
371
- if (language !== before)
372
- buildBar();
373
- drawStage(controller.state().t);
382
+ parts: { style, hint, bar, live },
383
+ setLanguage(next) {
384
+ if (next === language)
385
+ return;
386
+ language = next;
387
+ buildBar();
374
388
  },
389
+ key: act,
375
390
  destroy() {
376
391
  stop();
377
392
  if (raf !== 0)
378
- cancelAnimationFrame(raf);
393
+ cancel(raf);
394
+ raf = 0;
395
+ seen?.disconnect();
396
+ source.keysOn.removeEventListener("keydown", keys);
379
397
  doc.removeEventListener("visibilitychange", hidden);
380
398
  if (typeof media?.removeEventListener === "function")
381
399
  media.removeEventListener("change", motion);
382
400
  else if (typeof media?.removeListener === "function")
383
401
  media.removeListener(motion);
384
402
  controller.pause();
403
+ },
404
+ };
405
+ }
406
+ /**
407
+ * Attach a replay to a container: the stage with the drawing, the controls under it, the keyboard and the live region. The drawing is written once
408
+ * and then only the marks that move are moved, on `requestAnimationFrame`; a mark that comes or goes (an object that enters, an event that
409
+ * begins) redraws the replay's own layers, never the surface. With `prefers-reduced-motion` nothing plays by itself and play steps instead.
410
+ */
411
+ export function createReplayView(host, timeline, settings, initial) {
412
+ const doc = host.ownerDocument;
413
+ const id = initial.id ?? `ky-r${++counter}`;
414
+ let options = { ...initial, id };
415
+ let language = options.language === "ja" ? "ja" : "en";
416
+ const root = doc.createElement("div");
417
+ root.className = "ky-replay";
418
+ const stage = doc.createElement("div");
419
+ stage.className = "ky-stage";
420
+ stage.tabIndex = 0;
421
+ stage.setAttribute("role", "group");
422
+ let svg = null;
423
+ const painter = replayPainter({
424
+ doc,
425
+ timeline,
426
+ look: settings,
427
+ svg: () => svg,
428
+ options: () => options,
429
+ language: () => language,
430
+ redraw: (t) => drawStage(t),
431
+ });
432
+ function drawStage(t) {
433
+ stage.innerHTML = drawFrame(options, timeline, t, settings);
434
+ svg = stage.querySelector("svg.kyogijo");
435
+ }
436
+ const driver = replayDriver(doc, timeline, settings, { paint: (t) => painter.paint(t), language, keysOn: root, holder: root, labelled: stage });
437
+ const { style, hint, bar, live } = driver.parts;
438
+ root.append(style, stage, hint, bar, live);
439
+ host.replaceChildren(root);
440
+ drawStage(driver.controller.state().t);
441
+ painter.bind(driver.controller.state().t);
442
+ if (settings.autoplay === true && driver.controller.state().motion === "smooth" && doc.visibilityState !== "hidden")
443
+ driver.controller.play();
444
+ return {
445
+ controller: driver.controller,
446
+ stage,
447
+ draw(next) {
448
+ options = { ...next, id };
449
+ language = options.language === "ja" ? "ja" : "en";
450
+ // The controls are made again only when the words change: a reader's focus on one of them stays where it is when a score moves.
451
+ driver.setLanguage(language);
452
+ const t = driver.controller.state().t;
453
+ drawStage(t);
454
+ painter.bind(t);
455
+ },
456
+ destroy() {
457
+ driver.destroy();
385
458
  host.replaceChildren();
386
459
  },
387
460
  };
package/dist/replay.js CHANGED
@@ -160,6 +160,11 @@ export function replayInfo(timeline) {
160
160
  const p = prepare(timeline);
161
161
  return { start: p.start, end: p.end, duration: p.end - p.start, frames: p.frames.length, objects: p.ids.length, events: p.events.length, step: p.step };
162
162
  }
163
+ /** Everything a timeline tracks, each as it first appears (its id, team, number and label), in that order. */
164
+ export function replayCast(timeline) {
165
+ const p = prepare(timeline);
166
+ return p.ids.map((id) => p.first.get(id));
167
+ }
163
168
  /** The times of the frames, and of the events, in order: the moments a step can land on. */
164
169
  export function replayTimes(timeline) {
165
170
  return prepare(timeline).times;
@@ -541,10 +546,14 @@ export function replayPlacer(options, timeline) {
541
546
  * ```
542
547
  */
543
548
  export function drawFrame(options, timeline, t, look = {}) {
549
+ return drawFrameOf(options, timeline, t, look);
550
+ }
551
+ /** `drawFrame` with only the groups `keep` says yes to: a 3D view keeps the players off its floor, where they stand up instead. */
552
+ export function drawFrameOf(options, timeline, t, look = {}, keep = () => true) {
544
553
  const language = options.language === "ja" ? "ja" : "en";
545
554
  const id = options.id ?? replayId(options, timeline);
546
555
  const withId = { ...options, id };
547
- const layers = replayLayers(timeline, t, look, language);
556
+ const layers = replayGroups(timeline, replaySnapshot(timeline, t, look), look, language).filter(keep).map((group) => group.layer);
548
557
  let description = options.description;
549
558
  if (description === undefined) {
550
559
  // The drawing's own description is made from its layers' summaries; a replay's layers change with the time, so its sentence is fixed.
@@ -1006,6 +1006,9 @@ export interface KyogijoWords {
1006
1006
  /** What the replay is and the keys it answers to, in smooth motion and in steps. */
1007
1007
  keys: string;
1008
1008
  keysStep: string;
1009
+ /** The same on a 3D view, where the arrow keys turn the view: in smooth motion and in steps. */
1010
+ keysTurn: string;
1011
+ keysTurnStep: string;
1009
1012
  region: string;
1010
1013
  };
1011
1014
  /** The words of the 3D view: its name, the keys, the viewpoints, the controls, what is announced, and the names of what stands on the surface. */
@@ -444,6 +444,8 @@ export const WORDS_EN = {
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.",
446
446
  keysStep: "Replay of the play, in steps. Space or the right arrow key moves on, the left arrow key goes back, and Home and End go to the ends.",
447
+ keysTurn: "Replay of the play on the 3D view. Space plays or pauses; comma and full stop step back and on by a frame, and with Shift by 5 seconds; End goes to the end. The arrow keys still turn the view. On the replay's controls the arrow keys step, and Home and End go to the ends.",
448
+ keysTurnStep: "Replay of the play on the 3D view, in steps. Space or full stop moves on, comma goes back, and End goes to the end. The arrow keys still turn the view. On the replay's controls the right arrow key moves on and the left goes back.",
447
449
  region: "Replay",
448
450
  },
449
451
  turn: {
@@ -469,6 +469,8 @@ export const WORDS_JA = {
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キーで最後に移動します。",
471
471
  keysStep: "プレーのリプレイです。コマ送りで表示しています。スペースキーまたは右矢印キーで進み、左矢印キーで戻り、Homeキーで最初、Endキーで最後に移動します。",
472
+ keysTurn: "3Dビューでのプレーのリプレイです。スペースキーで再生と一時停止、カンマキーで1コマ戻り、ピリオドキーで1コマ進みます。Shiftキーを押しながらカンマキーかピリオドキーで5秒ずつ移動し、Endキーで最後に移動します。矢印キーではこれまでどおりビューを回せます。リプレイの操作ボタンにフォーカスがあるときは、左右の矢印キーで1コマずつ前後に移動し、Homeキーで最初、Endキーで最後に移動します。",
473
+ keysTurnStep: "3Dビューでのプレーのリプレイです。コマ送りで表示しています。スペースキーまたはピリオドキーで進み、カンマキーで戻り、Endキーで最後に移動します。矢印キーではこれまでどおりビューを回せます。リプレイの操作ボタンにフォーカスがあるときは、右矢印キーで進み、左矢印キーで戻ります。",
472
474
  region: "リプレイ",
473
475
  },
474
476
  turn: {
@@ -2,6 +2,10 @@ import "./full.ts";
2
2
  import "./venues.all.ts";
3
3
  export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.ts";
4
4
  export type { KyogijoTurn } from "./turn.dom.ts";
5
+ export { discSize, keepOnFloor, replayTurnModel } from "./turn.replay.ts";
6
+ export type { TurnReplayLook, TurnReplayModel } from "./turn.replay.ts";
7
+ export type { TurnReplaySettings } from "./turn.replay.dom.ts";
8
+ import "./turn.replay.dom.ts";
5
9
  export { buildTurn, floorOptions, openingView } from "./turn.model.ts";
6
10
  export { turnDirection } from "./turn.camera.ts";
7
11
  export type { TurnDirection } from "./turn.camera.ts";
@@ -3,6 +3,8 @@ import "./full.js";
3
3
  // The real venues of the NHL's arenas, as this entry has always had them (the lean district reads the ones it shows).
4
4
  import "./venues.all.js";
5
5
  export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.js";
6
+ export { discSize, keepOnFloor, replayTurnModel } from "./turn.replay.js";
7
+ import "./turn.replay.dom.js";
6
8
  export { buildTurn, floorOptions, openingView } from "./turn.model.js";
7
9
  export { turnDirection } from "./turn.camera.js";
8
10
  export { GOVERNOR_TUNING, QUALITIES, QUALITY_TIERS, TIER_PROFILES, createGovernor, handheld, isQuality, qualityOf, rasterOf, tierOf } from "./quality.js";
@@ -4,6 +4,20 @@ import type { ScoreOptions, SurfaceOptions } from "./types.ts";
4
4
  import type { VenueSettings } from "./venue.ts";
5
5
  import { type Quality, type QualityTier } from "./quality.ts";
6
6
  import type { OutsideLevel } from "./outside.paint.ts";
7
+ import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
8
+ import type { KyogijoReplay } from "./mount.ts";
9
+ import type { TurnInternals } from "./turn.replay.dom.ts";
10
+ import type { ReplayView } from "./replay.dom.ts";
11
+ /**
12
+ * The replay's own code, which `turn.replay.dom.ts` puts in when it is loaded (the `/turn` entry and the element load it): a page that only turns a view (a district, a tournament layout) carries none of the replay
13
+ * and its layers.
14
+ */
15
+ declare let replayKit: {
16
+ attach(view: TurnInternals, timeline: ReplayTimeline, settings: ReplaySettings): KyogijoReplay;
17
+ flat(host: HTMLElement, timeline: ReplayTimeline, settings: ReplaySettings, options: SurfaceOptions | ScoreOptions): ReplayView;
18
+ } | undefined;
19
+ /** Put the replay's code in: called once by `turn.replay.dom.ts`. */
20
+ export declare function useTurnReplay(kit: NonNullable<typeof replayKit>): void;
7
21
  /** What `mountKyogijoTurn` gives back. `O` is what `update` takes: the options of a drawing, or (for `mountMultiTurn`) of a tournament layout. */
8
22
  export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
9
23
  /** `3d`, or `flat` where the browser cannot draw the 3D view (or there is nothing to turn): the flat drawing is shown and the rest of this does nothing. */
@@ -52,6 +66,12 @@ export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
52
66
  setQuality(quality: Quality): void;
53
67
  /** The flat drawing that is the floor, or `null` after `destroy`. */
54
68
  svg(): SVGSVGElement | null;
69
+ /**
70
+ * Play tracking data on the view: the players stand up on the surface as discs that face the eye (home circles, away squares, numbers), the ball or the puck runs on the floor with its trail, and the passes, shots and
71
+ * goals are drawn on the floor as in the flat replay. The replay's controls go under the view's; the view still turns, tips and zooms while it plays. Space on the view plays and pauses, comma and full stop step; the
72
+ * arrow keys still turn the view. `destroy` on what it gives takes the replay away and leaves the view. A tournament layout has no replay. Where the browser cannot draw the 3D view, the flat replay plays instead.
73
+ */
74
+ replay(timeline: ReplayTimeline, settings?: ReplaySettings): KyogijoReplay;
55
75
  /** Stop listening and empty the container. */
56
76
  destroy(): void;
57
77
  }
@@ -77,4 +97,5 @@ export declare function mountKyogijoTurn(host: HTMLElement, options: SurfaceOpti
77
97
  * Mount any 3D model (`mountKyogijoTurn` is this with the model of one surface; `mountMultiTurn` with a tournament layout's): `build` makes the model from what `update` is given, and the rest, the controls, the pointer, the keys
78
98
  * and the auto-turn, is the same.
79
99
  */
80
- export declare function mountTurn<O>(host: HTMLElement, options: O, settings: TurnSettings, build: (options: O, settings: TurnSettings) => TurnModel): KyogijoTurn<O>;
100
+ export declare function mountTurn<O>(host: HTMLElement, options: O, settings: TurnSettings, build: (options: O, settings: TurnSettings) => TurnModel, replayable?: boolean): KyogijoTurn<O>;
101
+ export {};