@johnmorrisdotca/kyogijo-stadiums 0.53.0 → 0.55.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 +16 -0
- package/README.md +9 -2
- package/catalogue.json +1 -1
- package/dist/catalogue.data.js +1 -1
- package/dist/circuits-entry.d.ts +14 -0
- package/dist/circuits-entry.js +8 -0
- package/dist/circuits.pace.d.ts +96 -0
- package/dist/circuits.pace.js +131 -0
- package/dist/circuits.race.d.ts +80 -0
- package/dist/circuits.race.dom.d.ts +68 -0
- package/dist/circuits.race.dom.js +434 -0
- package/dist/circuits.race.draw.d.ts +59 -0
- package/dist/circuits.race.draw.js +153 -0
- package/dist/circuits.race.field.d.ts +26 -0
- package/dist/circuits.race.field.js +41 -0
- package/dist/circuits.race.js +292 -0
- package/dist/circuits.race.moment.d.ts +68 -0
- package/dist/circuits.race.moment.js +147 -0
- package/dist/circuits.race.words.d.ts +49 -0
- package/dist/circuits.race.words.js +44 -0
- package/dist/turn.dom.d.ts +1 -1
- package/dist/turn.dom.js +38 -72
- package/dist/turn.frames.d.ts +33 -0
- package/dist/turn.frames.js +102 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
lap: "Lap {lap} / {laps}",
|
|
5
|
+
finalLap: "Final lap",
|
|
6
|
+
flag: "Chequered flag",
|
|
7
|
+
grid: "On the grid",
|
|
8
|
+
elapsed: "Race time {time}",
|
|
9
|
+
group: "Race",
|
|
10
|
+
play: "Play the race",
|
|
11
|
+
pause: "Pause the race",
|
|
12
|
+
nextLap: "Next lap",
|
|
13
|
+
speed: "Speed",
|
|
14
|
+
speedValue: "{speed}×",
|
|
15
|
+
position: "Race time",
|
|
16
|
+
positionValue: "{time} of {total}",
|
|
17
|
+
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" },
|
|
18
|
+
car: "{position}: {name}, {team}",
|
|
19
|
+
leads: "{name} leads, lap {lap}.",
|
|
20
|
+
wins: "Chequered flag: {name} wins.",
|
|
21
|
+
description: "{cars} made-up cars race {laps} laps from the grid to the chequered flag: a simulation, not a real race.",
|
|
22
|
+
},
|
|
23
|
+
ja: {
|
|
24
|
+
simulated: "シミュレーションによるレースです。架空のマシンとドライバーで、シード値から計算しています。実在のレース・ドライバー・チームとは関係ありません。",
|
|
25
|
+
lap: "{lap} / {laps}周",
|
|
26
|
+
finalLap: "ファイナルラップ",
|
|
27
|
+
flag: "チェッカーフラッグ",
|
|
28
|
+
grid: "スタート前",
|
|
29
|
+
elapsed: "レース経過時間 {time}",
|
|
30
|
+
group: "レース",
|
|
31
|
+
play: "レースを再生",
|
|
32
|
+
pause: "レースを一時停止",
|
|
33
|
+
nextLap: "次の周へ",
|
|
34
|
+
speed: "再生速度",
|
|
35
|
+
speedValue: "{speed}倍",
|
|
36
|
+
position: "レース経過時間",
|
|
37
|
+
positionValue: "{time} / {total}",
|
|
38
|
+
board: { caption: "タイミングボード", position: "順位", driver: "ドライバー", team: "チーム", gap: "トップ差", interval: "前車との差", last: "前周タイム", best: "ベストラップ", stops: "ピット回数", leader: "トップ", lapDown: "+1周", lapsDown: "+{n}周", pit: "ピットレーン", out: "リタイア", finished: "完走", fastest: "ファステストラップ" },
|
|
39
|
+
car: "{position}位:{name}({team})",
|
|
40
|
+
leads: "{lap}周目、{name}がトップ。",
|
|
41
|
+
wins: "チェッカーフラッグ:{name}が優勝。",
|
|
42
|
+
description: "架空のマシン{cars}台が、グリッドからチェッカーフラッグまで{laps}周を走ります。実際のレースではなく、シミュレーションです。",
|
|
43
|
+
},
|
|
44
|
+
});
|
package/dist/turn.dom.d.ts
CHANGED
|
@@ -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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
2
|
+
export declare const VERSION = "0.55.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.55.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.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",
|