@johnmorrisdotca/kyogijo-stadiums 0.54.0 → 0.56.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.
@@ -0,0 +1,48 @@
1
+ export const RACE_WORDS = Object.freeze({
2
+ en: {
3
+ simulated: "A simulated race: made-up cars and drivers, worked out from a seed. No real race, driver or team.",
4
+ feedNote: "A race from a feed you chose: its names, times and results are the feed's, shown in this page only. Between the times it gives, each car runs at the lap's own speeds.",
5
+ lap: "Lap {lap} / {laps}",
6
+ finalLap: "Final lap",
7
+ flag: "Chequered flag",
8
+ grid: "On the grid",
9
+ elapsed: "Race time {time}",
10
+ group: "Race",
11
+ play: "Play the race",
12
+ pause: "Pause the race",
13
+ nextLap: "Next lap",
14
+ speed: "Speed",
15
+ speedValue: "{speed}×",
16
+ position: "Race time",
17
+ positionValue: "{time} of {total}",
18
+ board: { caption: "Timing board", position: "Pos", driver: "Driver", team: "Team", gap: "Gap", interval: "Interval", last: "Last lap", best: "Best lap", stops: "Pits", leader: "Leader", lapDown: "+1 lap", lapsDown: "+{n} laps", pit: "In the pit lane", out: "Out", finished: "Finished", fastest: "Fastest lap" },
19
+ car: "{position}: {name}, {team}",
20
+ leads: "{name} leads, lap {lap}.",
21
+ wins: "Chequered flag: {name} wins.",
22
+ description: "{cars} made-up cars race {laps} laps from the grid to the chequered flag: a simulation, not a real race.",
23
+ fromFeed: "{cars} cars race {laps} laps, from a feed the reader chose: where each car is between the times it gives is worked out at the lap's own speeds.",
24
+ },
25
+ ja: {
26
+ simulated: "シミュレーションによるレースです。架空のマシンとドライバーで、シード値から計算しています。実在のレース・ドライバー・チームとは関係ありません。",
27
+ feedNote: "選んだフィードのレースです。名前、タイム、結果はフィードのもので、このページの中だけに表示します。フィードが示す時刻の間は、各マシンがそのコースの速度で走るものとして計算しています。",
28
+ lap: "{lap} / {laps}周",
29
+ finalLap: "ファイナルラップ",
30
+ flag: "チェッカーフラッグ",
31
+ grid: "スタート前",
32
+ elapsed: "レース経過時間 {time}",
33
+ group: "レース",
34
+ play: "レースを再生",
35
+ pause: "レースを一時停止",
36
+ nextLap: "次の周へ",
37
+ speed: "再生速度",
38
+ speedValue: "{speed}倍",
39
+ position: "レース経過時間",
40
+ positionValue: "{time} / {total}",
41
+ board: { caption: "タイミングボード", position: "順位", driver: "ドライバー", team: "チーム", gap: "トップ差", interval: "前車との差", last: "前周タイム", best: "ベストラップ", stops: "ピット回数", leader: "トップ", lapDown: "+1周", lapsDown: "+{n}周", pit: "ピットレーン", out: "リタイア", finished: "完走", fastest: "ファステストラップ" },
42
+ car: "{position}位:{name}({team})",
43
+ leads: "{lap}周目、{name}がトップ。",
44
+ wins: "チェッカーフラッグ:{name}が優勝。",
45
+ description: "架空のマシン{cars}台が、グリッドからチェッカーフラッグまで{laps}周を走ります。実際のレースではなく、シミュレーションです。",
46
+ fromFeed: "選んだフィードから、{cars}台のマシンが{laps}周を走ります。フィードが示す時刻の間の各マシンの位置は、そのコースの速度から計算しています。",
47
+ },
48
+ });
@@ -97,5 +97,5 @@ export declare function mountKyogijoTurn(host: HTMLElement, options: SurfaceOpti
97
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
98
98
  * and the auto-turn, is the same.
99
99
  */
100
- export declare function mountTurn<O>(host: HTMLElement, options: O, settings: TurnSettings, build: (options: O, settings: TurnSettings) => TurnModel, replayable?: boolean): KyogijoTurn<O>;
100
+ export declare function mountTurn<O>(host: HTMLElement, options: O, settings: TurnSettings, build: (options: O, settings: TurnSettings) => TurnModel, replayable?: boolean, lend?: (internals: TurnInternals) => void): KyogijoTurn<O>;
101
101
  export {};
package/dist/turn.dom.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // tips it, a pinch or Ctrl and the wheel zoom, the arrow keys step it, four viewpoints and a reset are a button or a number key away, a reader who prefers
3
3
  // reduced motion gets jumps for glides, and where the browser cannot draw in 3D the flat drawing stays. Nothing is polled and nothing runs while the view is
4
4
  // still: the elements' `transform`s are written when the view changes, and a glide between viewpoints is a short run of animation frames.
5
+ import { displayProbe, frameBatch } from "./turn.frames.js";
5
6
  import { drawKyogijo } from "./draw.js";
6
7
  import { STEP, TURN_PRESET_NAMES, clampView, drag, nearest, nudge, presetOf, presetView, step, turnDirection, viewScale, wheelTip } from "./turn.camera.js";
7
8
  import { buildTurn, floorOptions, openingView, turnWords } from "./turn.model.js";
@@ -54,7 +55,7 @@ export function mountKyogijoTurn(host, options, settings = {}) {
54
55
  * 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
55
56
  * and the auto-turn, is the same.
56
57
  */
57
- export function mountTurn(host, options, settings, build, replayable = false) {
58
+ export function mountTurn(host, options, settings, build, replayable = false, lend) {
58
59
  const doc = host.ownerDocument;
59
60
  const win = doc.defaultView;
60
61
  /** The venue's settings as they are now: the mounted ones, altered by `update`. */
@@ -222,30 +223,10 @@ export function mountTurn(host, options, settings, build, replayable = false) {
222
223
  const probeDisplay = () => {
223
224
  if (win === null || probe !== undefined || displayMs !== undefined)
224
225
  return;
225
- const found = [];
226
- let before;
227
- const step = (stamp) => {
228
- probe = undefined;
229
- if (!alive || win === null)
230
- return;
231
- if (doc.hidden === true) {
232
- // Hidden: start over when it can be seen.
233
- found.length = 0;
234
- before = undefined;
235
- probe = win.setTimeout(() => (probe = win.requestAnimationFrame(step)), 400);
236
- return;
237
- }
238
- if (before !== undefined)
239
- found.push(stamp - before);
240
- before = stamp;
241
- if (found.length < 24)
242
- probe = win.requestAnimationFrame(step);
243
- else {
244
- displayMs = [...found].sort((a, b) => a - b)[Math.floor(found.length * 0.1)];
245
- governor?.display(displayMs);
246
- }
247
- };
248
- probe = win.setTimeout(() => (probe = win.requestAnimationFrame(step)), 350);
226
+ probe = displayProbe(win, () => doc.hidden === true, (ms) => {
227
+ displayMs = ms;
228
+ governor?.display(ms);
229
+ });
249
230
  };
250
231
  const stopMeter = () => {
251
232
  if (meterFrame !== undefined)
@@ -467,30 +448,9 @@ export function mountTurn(host, options, settings, build, replayable = false) {
467
448
  say(turnWords(model).reset);
468
449
  };
469
450
  // ---- One frame request a frame: the auto-turn and a replay on the view ask through this, and share one animation frame when both run. ----
470
- const queued = new Map();
471
- let queueId = 0;
472
- let pendingFrame = 0;
473
- const requestFrame = (callback) => {
474
- queueId += 1;
475
- queued.set(queueId, callback);
476
- if (pendingFrame === 0 && win !== null) {
477
- pendingFrame = win.requestAnimationFrame((stamp) => {
478
- pendingFrame = 0;
479
- const run = [...queued.values()];
480
- queued.clear();
481
- for (const one of run)
482
- one(stamp);
483
- });
484
- }
485
- return queueId;
486
- };
487
- const cancelFrame = (id) => {
488
- queued.delete(id);
489
- if (queued.size === 0 && pendingFrame !== 0) {
490
- win?.cancelAnimationFrame(pendingFrame);
491
- pendingFrame = 0;
492
- }
493
- };
451
+ const frames = frameBatch(win);
452
+ const requestFrame = (callback) => frames.request(callback);
453
+ const cancelFrame = (id) => frames.cancel(id);
494
454
  // ---- The auto-turn: a trophy case's slow turn. It runs only when asked, holds while the pointer holds the view, and does nothing while the page is hidden. ----
495
455
  const hidden = () => doc.hidden === true;
496
456
  const stopTick = () => {
@@ -729,7 +689,29 @@ export function mountTurn(host, options, settings, build, replayable = false) {
729
689
  // Nothing moves until the reader asks, unless the page did: `autoPlay` starts it, but not for a reader who prefers reduced motion (`"force"` does).
730
690
  if (settings.autoPlay === "force" || (settings.autoPlay === true && !calm()))
731
691
  setPlaying(true, true);
732
- return {
692
+ /** What the view lends a replay, or another moving layer of the package's own (a race on a circuit): its elements, its model, a way to lay planes on it, and its frames. */
693
+ const internalsOf = () => ({
694
+ doc,
695
+ root,
696
+ stage,
697
+ model: () => model,
698
+ setDecorate(next, after) {
699
+ decorate = next;
700
+ rendered = after;
701
+ // (A view that is going away is not drawn again.)
702
+ if (!alive)
703
+ return;
704
+ renderWorld();
705
+ paint(shown);
706
+ },
707
+ repaint: () => {
708
+ if (alive)
709
+ paint(shown);
710
+ },
711
+ requestFrame,
712
+ cancelFrame,
713
+ });
714
+ const mounted = {
733
715
  mode: "3d",
734
716
  view: () => ({ ...view }),
735
717
  set(next, glide = true) {
@@ -842,28 +824,7 @@ export function mountTurn(host, options, settings, build, replayable = false) {
842
824
  if (!replayable)
843
825
  throw new Error("a replay plays on one surface, not on a layout");
844
826
  replaying?.destroy();
845
- const internals = {
846
- doc,
847
- root,
848
- stage,
849
- model: () => model,
850
- setDecorate(next, after) {
851
- decorate = next;
852
- rendered = after;
853
- // (A view that is going away is not drawn again.)
854
- if (!alive)
855
- return;
856
- renderWorld();
857
- paint(shown);
858
- },
859
- repaint: () => {
860
- if (alive)
861
- paint(shown);
862
- },
863
- requestFrame,
864
- cancelFrame,
865
- };
866
- const mine = kitOf().attach(internals, timeline, replaySettings);
827
+ const mine = kitOf().attach(internalsOf(), timeline, replaySettings);
867
828
  replaying = mine;
868
829
  return {
869
830
  ...mine,
@@ -886,6 +847,9 @@ export function mountTurn(host, options, settings, build, replayable = false) {
886
847
  stop();
887
848
  stopTick();
888
849
  stopMeter();
850
+ // Nothing of the view asks for a frame once it is gone: the display's measure, and every callback of the shared frame (one asked for in this same tick too).
851
+ probe?.stop();
852
+ frames.clear();
889
853
  seen?.disconnect();
890
854
  doc.removeEventListener("visibilitychange", onVisible);
891
855
  clearTimeout(spoken);
@@ -893,4 +857,6 @@ export function mountTurn(host, options, settings, build, replayable = false) {
893
857
  host.replaceChildren();
894
858
  },
895
859
  };
860
+ lend?.(internalsOf());
861
+ return mounted;
896
862
  }
@@ -0,0 +1,33 @@
1
+ /** The part of a window the frames use. */
2
+ export interface FrameWindow {
3
+ requestAnimationFrame(callback: FrameRequestCallback): number;
4
+ cancelAnimationFrame(id: number): void;
5
+ setTimeout(callback: () => void, ms: number): number;
6
+ clearTimeout(id: number | undefined): void;
7
+ }
8
+ /** One animation frame a frame, shared: what `frameBatch` gives back. */
9
+ export interface FrameBatch {
10
+ /** Ask for the next frame: every callback asked for before it comes runs in that one frame. Gives an id for `cancel`. */
11
+ request(callback: FrameRequestCallback): number;
12
+ /** Take a callback back: it does not run, even when its frame has begun; the window's frame is cancelled when nothing is left. */
13
+ cancel(id: number): void;
14
+ /** Take everything back and cancel the window's frame: nothing asked for runs. */
15
+ clear(): void;
16
+ /** How many callbacks wait for the next frame. */
17
+ readonly waiting: number;
18
+ }
19
+ /**
20
+ * One window frame for everything a view moves: callbacks asked for in the same frame run together in the next. A callback taken back while its frame runs (one before it stopped the view) does not run, and `clear`
21
+ * leaves nothing that can ask for a frame.
22
+ */
23
+ export declare function frameBatch(win: Pick<FrameWindow, "requestAnimationFrame" | "cancelAnimationFrame"> | null): FrameBatch;
24
+ /** The measure of the display's interval: what `displayProbe` gives back. */
25
+ export interface DisplayProbe {
26
+ /** Stop it, wherever it is (waiting to begin, or between two frames): it asks for nothing after. */
27
+ stop(): void;
28
+ }
29
+ /**
30
+ * Measure the display's own interval once, a moment after a view is mounted: wait `delay` ms, then time `count` idle frames and give the lower decile of their intervals to `done` (a busy moment does not count). A hidden
31
+ * page starts over when it is shown. `stop` ends it at once.
32
+ */
33
+ export declare function displayProbe(win: FrameWindow, hidden: () => boolean, done: (ms: number) => void, delay?: number, count?: number): DisplayProbe;
@@ -0,0 +1,102 @@
1
+ // The animation frames of a 3D view: one frame a frame for everything that moves on it (the auto-turn, a replay, a race), and the one-time measure of the display's own interval. Each can be stopped at any moment,
2
+ // a request made in the same tick as the stop included, and once stopped nothing of it asks for a frame again. The page's window is passed in, so a test can count what is asked with a window of its own.
3
+ /**
4
+ * One window frame for everything a view moves: callbacks asked for in the same frame run together in the next. A callback taken back while its frame runs (one before it stopped the view) does not run, and `clear`
5
+ * leaves nothing that can ask for a frame.
6
+ */
7
+ export function frameBatch(win) {
8
+ const queued = new Map();
9
+ /** The callbacks of the frame that is running, until each has run: one taken back in the meantime is left out. */
10
+ let running;
11
+ let next = 0;
12
+ let pending = 0;
13
+ const flush = (stamp) => {
14
+ pending = 0;
15
+ running = new Map(queued);
16
+ queued.clear();
17
+ for (const [id, callback] of [...running]) {
18
+ if (running?.has(id) !== true)
19
+ continue;
20
+ running.delete(id);
21
+ callback(stamp);
22
+ }
23
+ running = undefined;
24
+ };
25
+ const drop = () => {
26
+ if (pending !== 0)
27
+ win?.cancelAnimationFrame(pending);
28
+ pending = 0;
29
+ };
30
+ return {
31
+ request(callback) {
32
+ next += 1;
33
+ queued.set(next, callback);
34
+ if (pending === 0 && win !== null)
35
+ pending = win.requestAnimationFrame(flush);
36
+ return next;
37
+ },
38
+ cancel(id) {
39
+ queued.delete(id);
40
+ running?.delete(id);
41
+ if (queued.size === 0)
42
+ drop();
43
+ },
44
+ clear() {
45
+ queued.clear();
46
+ running?.clear();
47
+ drop();
48
+ },
49
+ get waiting() {
50
+ return queued.size;
51
+ },
52
+ };
53
+ }
54
+ /**
55
+ * Measure the display's own interval once, a moment after a view is mounted: wait `delay` ms, then time `count` idle frames and give the lower decile of their intervals to `done` (a busy moment does not count). A hidden
56
+ * page starts over when it is shown. `stop` ends it at once.
57
+ */
58
+ export function displayProbe(win, hidden, done, delay = 350, count = 24) {
59
+ let timer;
60
+ let frame;
61
+ let stopped = false;
62
+ const found = [];
63
+ let before;
64
+ const wait = (ms) => {
65
+ timer = win.setTimeout(() => {
66
+ timer = undefined;
67
+ if (!stopped)
68
+ frame = win.requestAnimationFrame(step);
69
+ }, ms);
70
+ };
71
+ function step(stamp) {
72
+ frame = undefined;
73
+ if (stopped)
74
+ return;
75
+ if (hidden()) {
76
+ // Hidden: start over when it can be seen.
77
+ found.length = 0;
78
+ before = undefined;
79
+ wait(400);
80
+ return;
81
+ }
82
+ if (before !== undefined)
83
+ found.push(stamp - before);
84
+ before = stamp;
85
+ if (found.length < count)
86
+ frame = win.requestAnimationFrame(step);
87
+ else
88
+ done([...found].sort((a, b) => a - b)[Math.floor(found.length * 0.1)]);
89
+ }
90
+ wait(delay);
91
+ return {
92
+ stop() {
93
+ stopped = true;
94
+ if (timer !== undefined)
95
+ win.clearTimeout(timer);
96
+ if (frame !== undefined)
97
+ win.cancelAnimationFrame(frame);
98
+ timer = undefined;
99
+ frame = undefined;
100
+ },
101
+ };
102
+ }
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.54.0";
2
+ export declare const VERSION = "0.56.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.54.0";
2
+ export const VERSION = "0.56.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.54.0",
3
+ "version": "0.56.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",