@johnmorrisdotca/kyogijo-stadiums 0.58.0 → 0.60.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 +12 -0
- package/README.md +2 -0
- package/catalogue.json +1 -1
- package/dist/athletics/plan.d.ts +2 -0
- package/dist/athletics/plan.js +20 -4
- package/dist/catalogue.data.js +1 -1
- package/dist/crowd.d.ts +18 -0
- package/dist/crowd.js +88 -18
- package/dist/draw.js +22 -7
- package/dist/element.d.ts +3 -3
- package/dist/element.js +10 -5
- package/dist/replay-entry.d.ts +2 -0
- package/dist/replay-entry.js +1 -0
- package/dist/replay.control.d.ts +4 -0
- package/dist/replay.control.js +16 -0
- package/dist/replay.dom.js +20 -3
- package/dist/replay.follow.d.ts +164 -0
- package/dist/replay.follow.dom.d.ts +30 -0
- package/dist/replay.follow.dom.js +118 -0
- package/dist/replay.follow.js +279 -0
- package/dist/replay.types.d.ts +6 -1
- package/dist/strings.core.d.ts +10 -0
- package/dist/strings.en.js +1 -0
- package/dist/strings.ja.js +1 -0
- package/dist/surfaces/artistic.js +3 -1
- package/dist/surfaces/athletics.js +2 -0
- package/dist/surfaces/hockey.js +3 -1
- package/dist/surfaces/surface.d.ts +7 -0
- package/dist/surfaces/surface.js +9 -0
- package/dist/turn.camera.d.ts +3 -2
- package/dist/turn.camera.js +9 -4
- package/dist/turn.dom.d.ts +5 -0
- package/dist/turn.dom.js +63 -6
- package/dist/turn.follow.d.ts +22 -0
- package/dist/turn.follow.js +46 -0
- package/dist/turn.markup.d.ts +1 -1
- package/dist/turn.markup.js +14 -3
- package/dist/turn.replay.dom.d.ts +5 -0
- package/dist/turn.replay.dom.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -59,7 +59,9 @@ export function artistic(o, plan, ctx = {}) {
|
|
|
59
59
|
}
|
|
60
60
|
const top = -jy + 0.4;
|
|
61
61
|
const bottom = sy + start.depth + 0.4 - W;
|
|
62
|
-
|
|
62
|
+
// The deck is narrower along the sides than at the ends; a margin that is asked for (the ground the stands of a crowd stand on) is the same all round.
|
|
63
|
+
const across = o.margin === undefined ? m * 0.4 : m;
|
|
64
|
+
const view0 = { x: -m, y: -feet(top) - across, w: feet(L) + 2 * m, h: feet(W) + feet(top + bottom) + 2 * across };
|
|
63
65
|
base = (ctx.bare === true ? "" : deckMarkup(o, ids, plan.deck, view0.x, view0.y, view0.w, view0.h)) + base + deck;
|
|
64
66
|
const Lf = feet(L);
|
|
65
67
|
const Wf = feet(W);
|
|
@@ -96,6 +96,8 @@ export function athleticsSurface(o, plan, embedded, margin) {
|
|
|
96
96
|
onColour: false,
|
|
97
97
|
};
|
|
98
98
|
surface.box = { x: 0, y: 0, w: W, h: H };
|
|
99
|
+
if (plan.outline !== undefined && sv === undefined)
|
|
100
|
+
surface.floor = plan.outline.map(([x, y]) => [(x - ox) * K, (y - oy) * K]);
|
|
99
101
|
surface.lineRef = { width: thick, feet: K };
|
|
100
102
|
const ranges = {};
|
|
101
103
|
for (const [name, r] of Object.entries(plan.ranges))
|
package/dist/surfaces/hockey.js
CHANGED
|
@@ -2,7 +2,7 @@ import { el, num } from "../format.js";
|
|
|
2
2
|
import { rulesFor } from "../eras.js";
|
|
3
3
|
import { NHL } from "../geometry.js";
|
|
4
4
|
import { NHL_RINK } from "../leagues/hockey.js";
|
|
5
|
-
import { atEnds, edges, ft, quarterTurn, roundedRect } from "./surface.js";
|
|
5
|
+
import { atEnds, edges, ft, quarterTurn, roundedOutline, roundedRect } from "./surface.js";
|
|
6
6
|
/** Where the boards' curve is at a distance x from the end: the y at which it meets a line across the rink. The corner's radius is the NHL's unless given. */
|
|
7
7
|
export function boardsY(x, radius = NHL.cornerRadius) {
|
|
8
8
|
const r = radius;
|
|
@@ -128,6 +128,8 @@ export function hockey(o, era, rink = NHL_RINK) {
|
|
|
128
128
|
};
|
|
129
129
|
surface.lineRef = { width: thin, feet: 1 };
|
|
130
130
|
surface.box = { x: 0, y: 0, w: L, h: W };
|
|
131
|
+
// The floor the stands of a crowd clear: the boards round the rounded corners, not the apron's square corners.
|
|
132
|
+
surface.floor = roundedOutline(-bw, -bw, L + 2 * bw, W + 2 * bw, r + bw);
|
|
131
133
|
surface.centreRadius = rink.circleRadius;
|
|
132
134
|
surface.anchors = { kind: "hockey", length: L, width: W, blueLine, goalLine: g, cornerRadius: r, net: net_ };
|
|
133
135
|
// The zones of the ice: from the end boards to a blue line, and between the blue lines.
|
|
@@ -153,6 +153,11 @@ export interface Surface {
|
|
|
153
153
|
width: number;
|
|
154
154
|
feet: number;
|
|
155
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* For a surface whose floor is not the rectangle of its view (an athletics track, its sprint straight and runways): the outline of everything it draws round its playing area, as points in the wide frame
|
|
158
|
+
* in feet. The stands of a crowd clear it.
|
|
159
|
+
*/
|
|
160
|
+
floor?: (readonly [number, number])[];
|
|
156
161
|
/** For a surface with a crowd: the margin it was drawn with, which holds the stands. */
|
|
157
162
|
crowdMargin?: number;
|
|
158
163
|
/** A real stadium of more than one configuration laid out in the venue's own frame: where the middle of its field is in the surface's frame (the surface is shifted to lie there), and the ground, building, stands and crowd under it, in the venue's frame. */
|
|
@@ -254,6 +259,8 @@ export declare function quarterTurn(view: {
|
|
|
254
259
|
};
|
|
255
260
|
/** A box from its edges: the left, the top, the right and the bottom. */
|
|
256
261
|
export declare const edges: (x0: number, y0: number, x1: number, y1: number) => Box;
|
|
262
|
+
/** A rounded rectangle's outline as points (each corner in 12 steps), for a surface's `floor`. */
|
|
263
|
+
export declare function roundedOutline(x: number, y: number, w: number, h: number, r: number): [number, number][];
|
|
257
264
|
/** A range that has an end: its box at the left end and at the right, made by a function of the end. */
|
|
258
265
|
export declare const atEnds: (box: (end: "left" | "right") => Box) => {
|
|
259
266
|
left: Box;
|
package/dist/surfaces/surface.js
CHANGED
|
@@ -45,5 +45,14 @@ export function quarterTurn(view) {
|
|
|
45
45
|
}
|
|
46
46
|
/** A box from its edges: the left, the top, the right and the bottom. */
|
|
47
47
|
export const edges = (x0, y0, x1, y1) => ({ x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) });
|
|
48
|
+
/** A rounded rectangle's outline as points (each corner in 12 steps), for a surface's `floor`. */
|
|
49
|
+
export function roundedOutline(x, y, w, h, r) {
|
|
50
|
+
const rr = Math.max(0, Math.min(r, w / 2, h / 2));
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const [cx, cy, a0] of [[x + w - rr, y + rr, -90], [x + w - rr, y + h - rr, 0], [x + rr, y + h - rr, 90], [x + rr, y + rr, 180]])
|
|
53
|
+
for (let i = 0; i <= 12; i += 1)
|
|
54
|
+
out.push([cx + rr * Math.cos(((a0 + i * 7.5) * Math.PI) / 180), cy + rr * Math.sin(((a0 + i * 7.5) * Math.PI) / 180)]);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
48
57
|
/** A range that has an end: its box at the left end and at the right, made by a function of the end. */
|
|
49
58
|
export const atEnds = (box) => ({ left: box("left"), right: box("right") });
|
package/dist/turn.camera.d.ts
CHANGED
|
@@ -129,9 +129,10 @@ export declare function multiply(a: Mat4, b: Mat4): number[];
|
|
|
129
129
|
/**
|
|
130
130
|
* The matrix that puts a point of the scene on the screen for a view in a placement: the scene (whose middle is the origin) is scaled, turned about the vertical,
|
|
131
131
|
* tipped back, shifted to where the fit puts it, and seen with a perspective `distance` pixels deep. This is `project`'s arithmetic as one matrix, so that an
|
|
132
|
-
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right.
|
|
132
|
+
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right. `focus`, a point of the floor
|
|
133
|
+
* in the scene's pixels from its middle, is put where the middle would be (and, by its third number from 0 to 1, that share of the way to the container's middle): the camera looks at it, and turns and tips about it.
|
|
133
134
|
*/
|
|
134
|
-
export declare function cameraMatrix(view: TurnView, at: Placement, distance: number): number[];
|
|
135
|
+
export declare function cameraMatrix(view: TurnView, at: Placement, distance: number, focus?: readonly [number, number] | readonly [number, number, number]): number[];
|
|
135
136
|
/** A matrix as the CSS `transform` that applies it. */
|
|
136
137
|
export declare const matrix3d: (m: Mat4) => string;
|
|
137
138
|
/** How far a point is from the eye's plane after the scene is turned and tipped, in pixels towards the eye: bigger is nearer. Planes are drawn far first. */
|
package/dist/turn.camera.js
CHANGED
|
@@ -208,9 +208,10 @@ const translation = (x, y, z) => [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1
|
|
|
208
208
|
/**
|
|
209
209
|
* The matrix that puts a point of the scene on the screen for a view in a placement: the scene (whose middle is the origin) is scaled, turned about the vertical,
|
|
210
210
|
* tipped back, shifted to where the fit puts it, and seen with a perspective `distance` pixels deep. This is `project`'s arithmetic as one matrix, so that an
|
|
211
|
-
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right.
|
|
211
|
+
* element can be given its whole place in one `matrix3d` and need no 3D context of the browser's: a browser that draws nested 3D layers flat still draws it right. `focus`, a point of the floor
|
|
212
|
+
* in the scene's pixels from its middle, is put where the middle would be (and, by its third number from 0 to 1, that share of the way to the container's middle): the camera looks at it, and turns and tips about it.
|
|
212
213
|
*/
|
|
213
|
-
export function cameraMatrix(view, at, distance) {
|
|
214
|
+
export function cameraMatrix(view, at, distance, focus) {
|
|
214
215
|
const k = at.k * view.zoom;
|
|
215
216
|
const [yaw, pitch] = [view.yaw * RAD, view.pitch * RAD];
|
|
216
217
|
const [cy, sy, cp, sp] = [Math.cos(yaw), Math.sin(yaw), Math.cos(pitch), Math.sin(pitch)];
|
|
@@ -218,10 +219,14 @@ export function cameraMatrix(view, at, distance) {
|
|
|
218
219
|
const rotateZ = [cy, sy, 0, 0, -sy, cy, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
219
220
|
const rotateX = [1, 0, 0, 0, 0, cp, sp, 0, 0, -sp, cp, 0, 0, 0, 0, 1];
|
|
220
221
|
const eye = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -1 / distance, 0, 0, 0, 1];
|
|
221
|
-
|
|
222
|
+
// A focus is brought toward the middle of the container by its share (the fit puts the scene's middle where its whole box is centred, which a tall scoreboard moves).
|
|
223
|
+
const centring = 1 - Math.min(1, Math.max(0, focus?.[2] ?? 0));
|
|
224
|
+
let m = multiply(eye, translation(at.tx * centring, at.ty * centring, 0));
|
|
222
225
|
m = multiply(m, rotateX);
|
|
223
226
|
m = multiply(m, rotateZ);
|
|
224
|
-
|
|
227
|
+
m = multiply(m, scale);
|
|
228
|
+
// A point of the floor to look at (a camera following the play), in the scene's pixels from its middle: it takes the place the middle had.
|
|
229
|
+
return focus === undefined || (focus[0] === 0 && focus[1] === 0) ? m : multiply(m, translation(-focus[0], -focus[1], 0));
|
|
225
230
|
}
|
|
226
231
|
/**
|
|
227
232
|
* A number of a matrix as CSS writes it: to ten decimals, with no exponent. The perspective terms of a camera matrix are tiny (a few ten-thousandths) and are multiplied by
|
package/dist/turn.dom.d.ts
CHANGED
|
@@ -81,6 +81,11 @@ export declare function turnSupported(win?: {
|
|
|
81
81
|
supports?: (property: string, value: string) => boolean;
|
|
82
82
|
} | null;
|
|
83
83
|
} | undefined): boolean;
|
|
84
|
+
/** Where a camera that follows the play looks: a point of the floor, in the scene's pixels from its middle, and a zoom on top of the view's own. */
|
|
85
|
+
export interface Aim {
|
|
86
|
+
focus: [number, number];
|
|
87
|
+
zoom: number;
|
|
88
|
+
}
|
|
84
89
|
/**
|
|
85
90
|
* Mount a surface as a 3D view in a container, over the static drawing it would otherwise hold: the drawing is the floor, tilted and turned by the pointer, the
|
|
86
91
|
* touch, the wheel and the keys, with what stands on the surface (a net and its posts, a basket, a goal, the posts of a football field) standing on it.
|
package/dist/turn.dom.js
CHANGED
|
@@ -35,6 +35,13 @@ export function turnSupported(win = typeof window === "undefined" ? undefined :
|
|
|
35
35
|
return css.supports("transform", "matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)");
|
|
36
36
|
}
|
|
37
37
|
const KEYS = { " ": true, ArrowLeft: true, ArrowRight: true, ArrowUp: true, ArrowDown: true, "+": true, "=": true, "-": true, _: true, "0": true, Home: true, "1": true, "2": true, "3": true, "4": true };
|
|
38
|
+
/** The point an aim looks at, brought toward the middle of the view as it closes in: not at all at its zoom of 1, so that turning it on does not move the view. */
|
|
39
|
+
const focusOf = (aim) => (aim === undefined ? undefined : [aim.focus[0], aim.focus[1], 1 - 1 / Math.max(1, aim.zoom)]);
|
|
40
|
+
/** An aim part of the way from one to another (none is the middle at a zoom of 1). */
|
|
41
|
+
const aimBetween = (from, to, t) => {
|
|
42
|
+
const [a, b] = [from ?? { focus: [0, 0], zoom: 1 }, to ?? { focus: [0, 0], zoom: 1 }];
|
|
43
|
+
return { focus: [a.focus[0] + (b.focus[0] - a.focus[0]) * t, a.focus[1] + (b.focus[1] - a.focus[1]) * t], zoom: a.zoom * Math.pow(b.zoom / a.zoom, t) };
|
|
44
|
+
};
|
|
38
45
|
/** The view part of the way from one to another, `t` from 0 to 1, the turn taken the short way round. */
|
|
39
46
|
const between = (from, to, t) => ({ yaw: from.yaw + (nearest(from.yaw, to.yaw) - from.yaw) * t, pitch: from.pitch + (to.pitch - from.pitch) * t, zoom: from.zoom * Math.pow(to.zoom / from.zoom, t) });
|
|
40
47
|
/**
|
|
@@ -176,6 +183,14 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
176
183
|
let shown = view;
|
|
177
184
|
let opening = model.view;
|
|
178
185
|
let flight;
|
|
186
|
+
/** What a camera that follows the play looks at (a point of the floor, and a zoom on top of the view's), where it is going in a glide, and who is told when the reader takes the view over. */
|
|
187
|
+
let aim;
|
|
188
|
+
let aimTo;
|
|
189
|
+
const users = new Set();
|
|
190
|
+
const took = (kind) => {
|
|
191
|
+
for (const user of [...users])
|
|
192
|
+
user(kind);
|
|
193
|
+
};
|
|
179
194
|
let spoken;
|
|
180
195
|
// The auto-turn: whether it is on, the frame that will advance it, the time of the last one, and whether it has moved the view since the page was last told.
|
|
181
196
|
let playing = false;
|
|
@@ -273,7 +288,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
273
288
|
model = decorate === undefined ? plain : decorate(plain);
|
|
274
289
|
ceiling = ceilingFor(model, stage.clientWidth || 960, stage.clientHeight || 600, shown.zoom);
|
|
275
290
|
raster = rasterFor(model);
|
|
276
|
-
const layout = layoutOf(model, shown, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens, false, raster);
|
|
291
|
+
const layout = layoutOf(model, aim === undefined ? shown : { ...shown, zoom: shown.zoom * aim.zoom }, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens, false, raster, focusOf(aim));
|
|
277
292
|
const world = root.querySelector(".ky-turn-world");
|
|
278
293
|
world.innerHTML = worldMarkup(model, layout, raster);
|
|
279
294
|
// The look the floor is painted in (and the theme) are attributes of the world.
|
|
@@ -342,10 +357,11 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
342
357
|
settling = next === ceiling ? undefined : win.setTimeout(settleRaster, next > ceiling ? 160 : 1500);
|
|
343
358
|
};
|
|
344
359
|
/** Put a view on the screen. */
|
|
345
|
-
const paint = (
|
|
360
|
+
const paint = (asked) => {
|
|
346
361
|
const width = stage.clientWidth || 960;
|
|
347
362
|
const height = stage.clientHeight || 600;
|
|
348
|
-
const
|
|
363
|
+
const now = aim === undefined ? asked : { ...asked, zoom: asked.zoom * aim.zoom };
|
|
364
|
+
const layout = layoutOf(model, now, width, height, settings.lens, true, raster, focusOf(aim));
|
|
349
365
|
outside?.zoom(now.zoom);
|
|
350
366
|
const culled = layout.culled;
|
|
351
367
|
elements.forEach((one, i) => {
|
|
@@ -389,11 +405,16 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
389
405
|
};
|
|
390
406
|
/** Go to a view: by a glide of `ms` milliseconds, or at once. */
|
|
391
407
|
const show = (to, ms) => {
|
|
408
|
+
// A camera that follows the play and is asked to let go glides back with the view (and goes on doing so when another glide takes over).
|
|
409
|
+
const aiming = aimTo !== undefined ? { from: aim, to: aimTo.to } : flight?.aim === undefined ? undefined : { from: aim, to: flight.aim.to };
|
|
410
|
+
aimTo = undefined;
|
|
392
411
|
stop();
|
|
393
412
|
view = to;
|
|
394
413
|
label();
|
|
395
414
|
if (ms <= 0 || calm() || win === null) {
|
|
396
415
|
shown = to;
|
|
416
|
+
if (aiming !== undefined)
|
|
417
|
+
aim = aiming.to;
|
|
397
418
|
paint(shown);
|
|
398
419
|
return;
|
|
399
420
|
}
|
|
@@ -404,13 +425,15 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
404
425
|
const t = Math.min(1, (stamp - flight.start) / flight.ms);
|
|
405
426
|
const eased = 1 - Math.pow(1 - t, 3);
|
|
406
427
|
shown = t >= 1 ? flight.to : between(flight.from, flight.to, eased);
|
|
428
|
+
if (flight.aim !== undefined)
|
|
429
|
+
aim = t >= 1 ? flight.aim.to : aimBetween(flight.aim.from, flight.aim.to, eased);
|
|
407
430
|
paint(shown);
|
|
408
431
|
if (t < 1)
|
|
409
432
|
flight.frame = win.requestAnimationFrame(run);
|
|
410
433
|
else
|
|
411
434
|
flight = undefined;
|
|
412
435
|
};
|
|
413
|
-
flight = { from, to, start: win.performance.now(), ms, frame: 0 };
|
|
436
|
+
flight = { from, to, start: win.performance.now(), ms, frame: 0, aim: aiming };
|
|
414
437
|
flight.frame = win.requestAnimationFrame(run);
|
|
415
438
|
};
|
|
416
439
|
const say = (text, wait = 0) => {
|
|
@@ -543,6 +566,12 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
543
566
|
if (target === null || target === undefined || !root.contains(target))
|
|
544
567
|
return;
|
|
545
568
|
const name = target.dataset.preset;
|
|
569
|
+
const act = target.dataset.act ?? "";
|
|
570
|
+
if (name !== undefined || act === "reset")
|
|
571
|
+
took("preset");
|
|
572
|
+
// (The replay's own buttons are in the view too: only the view's turn it.)
|
|
573
|
+
else if (act !== "" && act !== "play" && act !== "reverse")
|
|
574
|
+
took("turn");
|
|
546
575
|
if (name !== undefined)
|
|
547
576
|
return toPreset(name);
|
|
548
577
|
const big = event.shiftKey === true;
|
|
@@ -569,6 +598,8 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
569
598
|
const yaw = big ? STEP.yawBig : STEP.yaw;
|
|
570
599
|
const pitch = big ? STEP.pitchBig : STEP.pitch;
|
|
571
600
|
event.preventDefault();
|
|
601
|
+
if (event.key !== " ")
|
|
602
|
+
took(/^[0-4]$|^Home$/.test(event.key) ? "preset" : "turn");
|
|
572
603
|
switch (event.key) {
|
|
573
604
|
// A key takes the surface toward its side as a drag toward that side does: the side nearest the eye goes to the right on ArrowRight, and ArrowUp tips it as a drag up does.
|
|
574
605
|
case "ArrowLeft": return go(nudge(view, "left", yaw), 120);
|
|
@@ -629,9 +660,13 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
629
660
|
}
|
|
630
661
|
else {
|
|
631
662
|
view = drag(view, here.x - before.x, here.y - before.y, tilts(event.pointerType));
|
|
632
|
-
if (Math.abs(here.x - before.x) + Math.abs(here.y - before.y) > 0)
|
|
663
|
+
if (Math.abs(here.x - before.x) + Math.abs(here.y - before.y) > 0 && !moved) {
|
|
633
664
|
moved = true;
|
|
665
|
+
took("turn");
|
|
666
|
+
}
|
|
634
667
|
}
|
|
668
|
+
if (pinch !== undefined)
|
|
669
|
+
took("turn");
|
|
635
670
|
shown = view;
|
|
636
671
|
paint(shown);
|
|
637
672
|
};
|
|
@@ -655,7 +690,11 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
655
690
|
stage.addEventListener("pointermove", onMove);
|
|
656
691
|
stage.addEventListener("pointerup", onUp);
|
|
657
692
|
stage.addEventListener("pointercancel", onUp);
|
|
658
|
-
|
|
693
|
+
const onDouble = () => {
|
|
694
|
+
took("preset");
|
|
695
|
+
toOpening();
|
|
696
|
+
};
|
|
697
|
+
stage.addEventListener("dblclick", onDouble);
|
|
659
698
|
// ---- The wheel over the view zooms it, as a map does; with the pointer anywhere else the page scrolls. Ctrl or Command and the wheel (a pinch on a trackpad) zoom too. Shift and the
|
|
660
699
|
// wheel, or a sideways swipe, turn it. Tipping is a drag, the keys and the buttons. A wheel that starts while the zoom is already at its end that way goes on to scroll the page, so the page is
|
|
661
700
|
// never trapped; one that reaches the end part-way through stops there rather than flinging the page. ----
|
|
@@ -667,6 +706,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
667
706
|
if (!pinch && (event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY))) {
|
|
668
707
|
event.preventDefault();
|
|
669
708
|
wheelTaken = now;
|
|
709
|
+
took("turn");
|
|
670
710
|
grab();
|
|
671
711
|
view = clampView({ ...view, yaw: view.yaw + (event.deltaX || event.deltaY) * 0.25 });
|
|
672
712
|
}
|
|
@@ -680,6 +720,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
680
720
|
wheelTaken = now;
|
|
681
721
|
if (next.zoom === view.zoom)
|
|
682
722
|
return;
|
|
723
|
+
took("turn");
|
|
683
724
|
grab();
|
|
684
725
|
view = next;
|
|
685
726
|
}
|
|
@@ -721,6 +762,21 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
721
762
|
if (alive)
|
|
722
763
|
paint(shown);
|
|
723
764
|
},
|
|
765
|
+
aim(next, glide = false) {
|
|
766
|
+
if (!alive)
|
|
767
|
+
return;
|
|
768
|
+
if (glide) {
|
|
769
|
+
aimTo = { to: next };
|
|
770
|
+
// (A glide of the view that is about to start carries it; otherwise it glides alone.)
|
|
771
|
+
show(view, 560);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
aim = next;
|
|
775
|
+
},
|
|
776
|
+
onUser(listener) {
|
|
777
|
+
users.add(listener);
|
|
778
|
+
return () => void users.delete(listener);
|
|
779
|
+
},
|
|
724
780
|
requestFrame,
|
|
725
781
|
cancelFrame,
|
|
726
782
|
});
|
|
@@ -864,6 +920,7 @@ export function mountTurn(host, options, settings, build, replayable = false, le
|
|
|
864
920
|
probe?.stop();
|
|
865
921
|
frames.clear();
|
|
866
922
|
seen?.disconnect();
|
|
923
|
+
users.clear();
|
|
867
924
|
doc.removeEventListener("visibilitychange", onVisible);
|
|
868
925
|
clearTimeout(spoken);
|
|
869
926
|
observer?.disconnect();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReplayController } from "./replay.control.ts";
|
|
2
|
+
import { type FollowShot } from "./replay.follow.ts";
|
|
3
|
+
import type { ReplayChange, ReplayLook, ReplayTimeline } from "./replay.types.ts";
|
|
4
|
+
import type { Aim } from "./turn.dom.ts";
|
|
5
|
+
import type { TurnModel } from "./turn.model.ts";
|
|
6
|
+
import type { TurnInternals } from "./turn.replay.dom.ts";
|
|
7
|
+
import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
8
|
+
/**
|
|
9
|
+
* What a 3D view looks at for a shot: the shot's middle as a point of the floor in the scene's pixels from its middle, and the zoom on top of the view's. The scene at a zoom of 1 holds more
|
|
10
|
+
* than the surface (the boards, the stands): the zoom is worked out against how much more, so that the closest shot frames the same share of the surface as a flat drawing's.
|
|
11
|
+
*/
|
|
12
|
+
export declare function aimOf(shot: FollowShot, model: Pick<TurnModel, "scale" | "centre" | "box">, surface: {
|
|
13
|
+
w: number;
|
|
14
|
+
h: number;
|
|
15
|
+
}): Aim;
|
|
16
|
+
/** What `turnFollow` gives back: the paint the replay calls on each change of its clock, and `destroy`. */
|
|
17
|
+
export interface TurnFollow {
|
|
18
|
+
paint(change: ReplayChange): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}
|
|
21
|
+
/** The camera of a replay on a 3D view. `floor` is the options the floor is drawn with (the replay's), or `undefined` before it is laid. */
|
|
22
|
+
export declare function turnFollow(view: TurnInternals, controller: () => ReplayController, timeline: ReplayTimeline, look: ReplayLook, floor: () => SurfaceOptions | ScoreOptions | undefined): TurnFollow;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Following the play on a 3D view: the shot of replay.follow.ts turned into what the view looks at (a point of the floor, and a zoom on top of the view's own), so that the auto-turn, the
|
|
2
|
+
// tip and every viewpoint go on as they were while the camera follows. The reader's own turn, tip or zoom holds it until Resume; a viewpoint or Reset also takes it back to the whole surface,
|
|
3
|
+
// in the same glide. It runs in the replay's own paint, inside the view's one animation frame, and never asks for a frame of its own.
|
|
4
|
+
import { outlineOf } from "./outline.js";
|
|
5
|
+
import { followRunner, zoomFor } from "./replay.follow.js";
|
|
6
|
+
import { replayPlacer, replaySnapshot } from "./replay.js";
|
|
7
|
+
/**
|
|
8
|
+
* What a 3D view looks at for a shot: the shot's middle as a point of the floor in the scene's pixels from its middle, and the zoom on top of the view's. The scene at a zoom of 1 holds more
|
|
9
|
+
* than the surface (the boards, the stands): the zoom is worked out against how much more, so that the closest shot frames the same share of the surface as a flat drawing's.
|
|
10
|
+
*/
|
|
11
|
+
export function aimOf(shot, model, surface) {
|
|
12
|
+
const s = model.scale;
|
|
13
|
+
const room = Math.hypot(model.box.x1 - model.box.x0, model.box.y1 - model.box.y0) / Math.max(1e-9, Math.hypot(surface.w * s, surface.h * s));
|
|
14
|
+
return { focus: [(shot.x - model.centre[0]) * s, (shot.y - model.centre[1]) * s], zoom: zoomFor(shot.zoom, Math.min(3, room)) };
|
|
15
|
+
}
|
|
16
|
+
/** The camera of a replay on a 3D view. `floor` is the options the floor is drawn with (the replay's), or `undefined` before it is laid. */
|
|
17
|
+
export function turnFollow(view, controller, timeline, look, floor) {
|
|
18
|
+
const surface = () => outlineOf((floor() ?? view.model().options)).box;
|
|
19
|
+
const runner = followRunner({
|
|
20
|
+
timeline,
|
|
21
|
+
now: () => view.doc.defaultView?.performance.now() ?? 0,
|
|
22
|
+
at(t, stop) {
|
|
23
|
+
const place = replayPlacer(floor() ?? view.model().options, timeline);
|
|
24
|
+
const objects = replaySnapshot(timeline, t, look).objects;
|
|
25
|
+
const placed = place(objects.map((o) => [o.x, o.y]));
|
|
26
|
+
return { box: surface(), objects: objects.map((o, i) => ({ team: o.team, x: placed[i][0], y: placed[i][1] })), stop: stop === undefined ? undefined : place([[stop[0], stop[1]]])[0] };
|
|
27
|
+
},
|
|
28
|
+
show: (shot) => view.aim(shot === undefined ? undefined : aimOf(shot, view.model(), surface())),
|
|
29
|
+
});
|
|
30
|
+
const stop = view.onUser((kind) => {
|
|
31
|
+
if (controller().state().follow === "off")
|
|
32
|
+
return;
|
|
33
|
+
controller().holdFollow(true);
|
|
34
|
+
if (kind === "preset") {
|
|
35
|
+
runner.reset();
|
|
36
|
+
view.aim(undefined, true);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
paint: (change) => runner.paint(change),
|
|
41
|
+
destroy() {
|
|
42
|
+
stop();
|
|
43
|
+
view.aim(undefined);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/turn.markup.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface Layout {
|
|
|
33
33
|
* which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs. `raster` is the share of its size an element is made at (`rasterOf`): each element's matrix widens it back, so that the
|
|
34
34
|
* picture is the same and the layers hold `raster` squared as many pixels.
|
|
35
35
|
*/
|
|
36
|
-
export declare function layoutOf(model: TurnModel, view: TurnView, width: number, height: number, lens?: number, cull?: boolean, raster?: Raster): Layout;
|
|
36
|
+
export declare function layoutOf(model: TurnModel, view: TurnView, width: number, height: number, lens?: number, cull?: boolean, raster?: Raster, focus?: readonly [number, number] | readonly [number, number, number]): Layout;
|
|
37
37
|
/** One plane as an element: its box in pixels and the SVG drawn on it in feet. */
|
|
38
38
|
export declare function planeMarkup(p: Plane, scale: number, transform: string, stack: number, fade?: number, raster?: number): string;
|
|
39
39
|
/**
|
package/dist/turn.markup.js
CHANGED
|
@@ -56,6 +56,8 @@ export function groupRanks(groups, yaw) {
|
|
|
56
56
|
return rank;
|
|
57
57
|
}
|
|
58
58
|
const RAD = Math.PI / 180;
|
|
59
|
+
/** The things of a venue's bowl: they stand on the ground round the surface, never under a raised floor. */
|
|
60
|
+
const BOWL = new Set(["stands", "wall-seats", "gallery", "roof", "ribbon-boards", "hung-board"]);
|
|
59
61
|
const prepared = new WeakMap();
|
|
60
62
|
function prepare(model) {
|
|
61
63
|
const known = prepared.get(model.planes);
|
|
@@ -79,7 +81,8 @@ function prepare(model) {
|
|
|
79
81
|
const zs = [p.o[2], p.o[2] + p.u[2], p.o[2] + p.v[2], p.o[2] + p.u[2] + p.v[2]];
|
|
80
82
|
mid.push([(p.o[0] + (p.u[0] + p.v[0]) / 2 - centre[0]) * s, (p.o[1] + (p.u[1] + p.v[1]) / 2 - centre[1]) * s, (p.o[2] + (p.u[2] + p.v[2]) / 2) * s, Math.min(...zs) * s, Math.max(...zs) * s]);
|
|
81
83
|
}
|
|
82
|
-
const
|
|
84
|
+
const ofBowl = new Set(model.uprights.items.filter((item) => BOWL.has(item.kind)).flatMap((item) => item.planes));
|
|
85
|
+
const made = { planes: model.planes, scale: s, centre, base, mid, bowl: model.planes.map((p) => ofBowl.has(p)) };
|
|
83
86
|
prepared.set(model.planes, made);
|
|
84
87
|
return made;
|
|
85
88
|
}
|
|
@@ -93,10 +96,10 @@ const turnedAway = (m, side) => side * (m[0] * (m[5] * m[15] - m[13] * m[7]) - m
|
|
|
93
96
|
* which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs. `raster` is the share of its size an element is made at (`rasterOf`): each element's matrix widens it back, so that the
|
|
94
97
|
* picture is the same and the layers hold `raster` squared as many pixels.
|
|
95
98
|
*/
|
|
96
|
-
export function layoutOf(model, view, width, height, lens, cull = false, raster = 1) {
|
|
99
|
+
export function layoutOf(model, view, width, height, lens, cull = false, raster = 1, focus) {
|
|
97
100
|
const distance = eyeDistance(width, height, lens ?? model.lens);
|
|
98
101
|
const at = fit(model.box, { ...view, zoom: 1 }, width, height, distance);
|
|
99
|
-
const camera = cameraMatrix(view, at, distance);
|
|
102
|
+
const camera = cameraMatrix(view, at, distance, focus);
|
|
100
103
|
const s = model.scale;
|
|
101
104
|
const centre = model.centre;
|
|
102
105
|
const known = prepare(model);
|
|
@@ -159,6 +162,14 @@ export function layoutOf(model, view, width, height, lens, cull = false, raster
|
|
|
159
162
|
order.length = 0;
|
|
160
163
|
for (let i = 0, j = 0; i < own.length || j < apart.length;)
|
|
161
164
|
order.push(j >= apart.length || (i < own.length && own[i].depth <= apart[j].depth) ? own[i++] : apart[j++]);
|
|
165
|
+
// A raised floor (a table's top) carries the ground the bowl stands on, so it is sorted by its middle among the surface's own things only: the parts of the bowl that are farther than its middle
|
|
166
|
+
// come after it, still in their own order, or the floor would be drawn over the far stands.
|
|
167
|
+
if (raised) {
|
|
168
|
+
const at = order.findIndex((one) => one.i === 0);
|
|
169
|
+
const far = order.slice(0, at).filter((one) => known.bowl[one.i - 1]);
|
|
170
|
+
if (far.length > 0)
|
|
171
|
+
order.splice(0, at + 1, ...order.slice(0, at).filter((one) => !known.bowl[one.i - 1]), order[at], ...far);
|
|
172
|
+
}
|
|
162
173
|
const stack = new Array(count + 1).fill(0);
|
|
163
174
|
stack[0] = 1;
|
|
164
175
|
order.forEach((one, rank) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KyogijoReplay } from "./mount.ts";
|
|
2
|
+
import { type Aim } from "./turn.dom.ts";
|
|
2
3
|
import type { ReplaySettings, ReplayTimeline } from "./replay.types.ts";
|
|
3
4
|
import type { TurnModel } from "./turn.model.ts";
|
|
4
5
|
import { type TurnReplayLook } from "./turn.replay.ts";
|
|
@@ -13,6 +14,10 @@ export interface TurnInternals {
|
|
|
13
14
|
setDecorate(decorate: ((model: TurnModel) => TurnModel) | undefined, after?: () => void): void;
|
|
14
15
|
/** Paint the view as it is now: the planes whose place changed are written, the others are not touched. */
|
|
15
16
|
repaint(): void;
|
|
17
|
+
/** Look at a point of the floor with a zoom on top of the view's (a camera following the play), at once or in a glide; `undefined` gives the view back. Painted at the next `repaint`. */
|
|
18
|
+
aim(next: Aim | undefined, glide?: boolean): void;
|
|
19
|
+
/** Be told when the reader turns, tips or zooms the view (`turn`) or presses a viewpoint or Reset (`preset`). */
|
|
20
|
+
onUser(listener: (kind: "preset" | "turn") => void): () => void;
|
|
16
21
|
/** The view's own frame requests, which the auto-turn shares: one animation frame a frame, whatever runs. */
|
|
17
22
|
requestFrame(callback: FrameRequestCallback): number;
|
|
18
23
|
cancelFrame(id: number): void;
|
package/dist/turn.replay.dom.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createReplayView, replayDriver, replayPainter } from "./replay.dom.js";
|
|
|
2
2
|
import { useTurnReplay } from "./turn.dom.js";
|
|
3
3
|
import { drawFrameOf, replayPlacer, replaySnapshot } from "./replay.js";
|
|
4
4
|
import { boardFollower } from "./turn.replay.board.js";
|
|
5
|
+
import { turnFollow } from "./turn.follow.js";
|
|
5
6
|
import { keepOnFloor, moveDisc, replayTurnModel } from "./turn.replay.js";
|
|
6
7
|
/** Attach a replay to a 3D view. The controller it gives back has `destroy`, which takes the replay away and leaves the view as it was. */
|
|
7
8
|
export function attachTurnReplay(view, timeline, settings) {
|
|
@@ -57,6 +58,8 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
};
|
|
61
|
+
// The camera that follows the play.
|
|
62
|
+
const camera = turnFollow(view, () => driver.controller, timeline, look, () => laid?.floorOptions);
|
|
60
63
|
const wrap = doc.createElement("div");
|
|
61
64
|
wrap.className = "ky-replay ky-turn-replay";
|
|
62
65
|
const driver = replayDriver(doc, timeline, settings, {
|
|
@@ -65,6 +68,8 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
65
68
|
place(t);
|
|
66
69
|
if (look.board !== false)
|
|
67
70
|
board.follow(t, view.model(), change);
|
|
71
|
+
if (change !== undefined)
|
|
72
|
+
camera.paint(change);
|
|
68
73
|
view.repaint();
|
|
69
74
|
},
|
|
70
75
|
language,
|
|
@@ -119,12 +124,15 @@ export function attachTurnReplay(view, timeline, settings) {
|
|
|
119
124
|
laid = replayTurnModel(model, timeline, driver.controller.state().t, look);
|
|
120
125
|
return laid.model;
|
|
121
126
|
}, after);
|
|
127
|
+
camera.paint({ state: driver.controller.state(), reason: "follow", crossed: [] });
|
|
128
|
+
view.repaint();
|
|
122
129
|
if (settings.autoplay === true && driver.controller.state().motion === "smooth" && doc.visibilityState !== "hidden")
|
|
123
130
|
driver.controller.play();
|
|
124
131
|
return {
|
|
125
132
|
...driver.controller,
|
|
126
133
|
destroy() {
|
|
127
134
|
driver.destroy();
|
|
135
|
+
camera.destroy();
|
|
128
136
|
root.removeEventListener("keydown", onStageKey, true);
|
|
129
137
|
if (described === null)
|
|
130
138
|
stage.removeAttribute("aria-describedby");
|
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.60.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.60.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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",
|