@johnmorrisdotca/kyogijo-stadiums 0.28.0 → 0.31.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 +45 -0
- package/NOTICE.md +17 -5
- package/README.md +80 -107
- package/dist/crowd.d.ts +20 -2
- package/dist/crowd.park.d.ts +6 -0
- package/dist/crowd.park.js +3 -3
- package/dist/district.d.ts +3 -3
- package/dist/district.js +14 -13
- package/dist/draw.js +25 -10
- package/dist/flythrough-entry.d.ts +1 -0
- package/dist/flythrough-entry.js +1 -0
- package/dist/flythrough.dom.js +52 -6
- package/dist/flythrough.game.d.ts +36 -0
- package/dist/flythrough.game.js +145 -0
- package/dist/flythrough.markup.js +1 -0
- package/dist/flythrough.path.d.ts +6 -4
- package/dist/flythrough.path.js +26 -22
- package/dist/flythrough.style.d.ts +1 -1
- package/dist/flythrough.style.js +3 -0
- package/dist/flythrough.words.d.ts +2 -0
- package/dist/flythrough.words.js +6 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/stadiums-entry.d.ts +2 -2
- package/dist/stadiums-entry.js +2 -2
- package/dist/stadiums.d.ts +109 -28
- package/dist/stadiums.data.js +25 -0
- package/dist/stadiums.js +159 -30
- package/dist/stadiums.osm.data.js +114 -0
- package/dist/strings.d.ts +4 -0
- package/dist/strings.js +2 -2
- package/dist/turn.dom.d.ts +8 -2
- package/dist/turn.dom.js +10 -4
- package/dist/venue.d.ts +2 -0
- package/dist/venue.js +6 -5
- package/dist/venue.real.d.ts +7 -2
- package/dist/venue.real.js +124 -10
- package/dist/venues-entry.d.ts +2 -2
- package/dist/venues-entry.js +1 -1
- package/dist/venues.d.ts +14 -3
- package/dist/venues.data.js +72 -0
- package/dist/venues.geo.d.ts +22 -0
- package/dist/venues.geo.js +28 -0
- package/dist/venues.js +9 -1
- package/dist/venues.osm.data.js +366 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
package/dist/draw.js
CHANGED
|
@@ -11,7 +11,7 @@ import { FOOTBALL } from "./geometry.js";
|
|
|
11
11
|
import { renderLayers } from "./layers.js";
|
|
12
12
|
import { arenaById, arenaCourtIn, arenaNameIn, arenaOfficialName, arenaRinkIn } from "./arenas.js";
|
|
13
13
|
import { venueById, venueBowl } from "./venues.js";
|
|
14
|
-
import {
|
|
14
|
+
import { planPark, stadiumBowl, stadiumById, stadiumConfigOf, stadiumKind, stadiumNames, stadiumSeats } from "./stadiums.js";
|
|
15
15
|
import { groundById, groundNameIn, groundOfficialName } from "./grounds.js";
|
|
16
16
|
import { BOARD, courtFor, hasSetsBoard, isNetLeague, isNetSport } from "./court.js";
|
|
17
17
|
import { cricketBoardMarkup } from "./cricket.board.js";
|
|
@@ -419,6 +419,17 @@ function parkFence(o) {
|
|
|
419
419
|
}
|
|
420
420
|
return { points: fenceFor(parkById(o.park), feetFence(o.fence, unit), o.year), backstop: MLB_DIAMOND.backstop };
|
|
421
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* The real stadium (and the configuration) the options draw, where `crowd: { real: true }` asks for one: a football field (`cfl`, `nfl`) or a soccer pitch in the stadium named by `stadium` or by the `ground`, a baseball field in
|
|
424
|
+
* the park named by `park`; none for a league of the caller's own, a stadium that has no such configuration, or a sport with none.
|
|
425
|
+
*/
|
|
426
|
+
function realStadiumOf(o, sport, real) {
|
|
427
|
+
const config = real ? stadiumConfigOf(sport) : undefined;
|
|
428
|
+
if (config === undefined || chooseLeague(o).league !== undefined)
|
|
429
|
+
return undefined;
|
|
430
|
+
const stadium = stadiumById(config === "mlb" ? o.park : o.stadium ?? o.ground);
|
|
431
|
+
return stadium !== undefined && stadium.configs[config] !== undefined && (stadiumKind(stadium) === "park") === (config === "mlb") ? { stadium, config } : undefined;
|
|
432
|
+
}
|
|
422
433
|
/** The stands round a surface, laid out from its box (or, for a ballpark, from its fence). */
|
|
423
434
|
function standsFor(o, surface, crowd) {
|
|
424
435
|
const { sport } = chooseLeague(o);
|
|
@@ -428,7 +439,12 @@ function standsFor(o, surface, crowd) {
|
|
|
428
439
|
// The walls that have seats on top of them: the ones the package knows (Fenway Park's Green Monster), or, asked for, every wall of 25 feet or more.
|
|
429
440
|
const park = parkById(o.park);
|
|
430
441
|
const walls = park === undefined || crowd.wallSeats === false ? [] : parkWallsIn(park, o.year).filter((w) => (crowd.wallSeats === true ? w.height_ft >= 25 : w.label !== undefined && WALL_SEATS[park.id] === w.label));
|
|
431
|
-
|
|
442
|
+
const input = { fence: fenceOutline(points), backstop, crowd, seatsOnWalls: walls.map((w) => ({ from: w.from, to: w.to, height: w.height_ft })) };
|
|
443
|
+
// A real ballpark: the building it stands in, the way home plate faces and a roof, with its stands as deep as its seats need (no visitors' block: no source places one).
|
|
444
|
+
const real = realStadiumOf(o, sport, crowd.real);
|
|
445
|
+
if (real !== undefined)
|
|
446
|
+
return planPark(real.stadium, { ...input, crowd: crowd.awayEnd === "auto" ? { ...crowd, awayEnd: "none" } : crowd }).stands;
|
|
447
|
+
return parkLayout(input);
|
|
432
448
|
}
|
|
433
449
|
// An arena's real venue: the building it stands in and a bowl fitted to it, where the package has one for the arena and no league of the caller's own sets the surface.
|
|
434
450
|
const box = surface.whole ?? surface.box ?? { x: 0, y: 0, w: 100, h: 100 };
|
|
@@ -437,10 +453,9 @@ function standsFor(o, surface, crowd) {
|
|
|
437
453
|
if (venue !== undefined)
|
|
438
454
|
return crowdLayout({ sport, box, crowd: crowd.awayEnd === "auto" ? { ...crowd, awayEnd: "none" } : crowd, awaySide, real: venueBowl(venue, box) });
|
|
439
455
|
// A football field or a soccer pitch in a real stadium: its building and a bowl of the configuration's seats.
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return crowdLayout({ sport, box, crowd: crowd.awayEnd === "auto" ? { ...crowd, awayEnd: "none" } : crowd, awaySide, real: stadiumBowl(stadium, config, box) });
|
|
456
|
+
const real = realStadiumOf(o, sport, crowd.real);
|
|
457
|
+
if (real !== undefined)
|
|
458
|
+
return crowdLayout({ sport, box, crowd: crowd.awayEnd === "auto" ? { ...crowd, awayEnd: "none" } : crowd, awaySide, real: stadiumBowl(real.stadium, real.config, box) });
|
|
444
459
|
// A dojo's crowd sits in its gallery, from the room's balcony data (in feet here); the arenas (the Budokan, the Kokugikan) keep their bowls.
|
|
445
460
|
const hall = sport === "dojo" ? dojoGallery(o) : undefined;
|
|
446
461
|
const gallery = hall === undefined ? undefined : { offset: hall.offset / 0.3048, depth: hall.depth / 0.3048, height: [hall.height[0] / 0.3048, hall.height[1] / 0.3048], rows: hall.rows, aisles: hall.aisles };
|
|
@@ -733,10 +748,10 @@ export function describe(o) {
|
|
|
733
748
|
const realCrowd = o.crowd === undefined || o.crowd === false ? undefined : resolveCrowd(o.crowd);
|
|
734
749
|
const venue = sport === "nhl" && league === undefined && realCrowd?.real === true ? venueById(o.arena) : undefined;
|
|
735
750
|
const realLine = venue === undefined || arenaName === undefined ? "" : `${gap}${fill(words.said.real, { arena: arenaName, seats: String(venue.seats.hockey), tiers: String(venue.tiers.count) })}${gap}${words.said[venue.osm.axis.basis === "mapped" ? "realMapped" : venue.osm.axis.basis === "building" ? "realBuilding" : "realNone"]}`;
|
|
736
|
-
const
|
|
737
|
-
const
|
|
738
|
-
const stadiumLine =
|
|
739
|
-
const parkLine = (parkName === undefined ? `${groundLine}${halfLine}${arenaLine}${realLine}${stadiumLine}` : `${gap}${fill(words.said.park, { park: parkName })}${officially(parkOfficial)}${ballpark === undefined ? "" : ballparkSentences(ballpark).map((line) => `${gap}${line}`).join("")}`) + (special === undefined ? "" : `${gap}${special.text}`);
|
|
751
|
+
const real = realStadiumOf(o, sport, realCrowd?.real === true);
|
|
752
|
+
const stadiumKey = (basis) => (basis === "opening" ? "stadiumOpening" : basis === "pitch" ? "stadiumPitch" : basis === "infield" ? "stadiumInfield" : basis === "building" ? "stadiumBuilding" : "stadiumNone");
|
|
753
|
+
const stadiumLine = real === undefined ? "" : `${gap}${fill(real.config === "mlb" ? words.said.stadiumPark : words.said.stadium, { stadium: stadiumNames(real.stadium)[language], seats: String(stadiumSeats(real.stadium, real.config)), tiers: String(real.stadium.tiers.count) })}${gap}${words.said[stadiumKey(real.stadium.osm.axis.basis)]}`;
|
|
754
|
+
const parkLine = (parkName === undefined ? `${groundLine}${halfLine}${arenaLine}${realLine}${stadiumLine}` : `${gap}${fill(words.said.park, { park: parkName })}${officially(parkOfficial)}${ballpark === undefined ? "" : ballparkSentences(ballpark).map((line) => `${gap}${line}`).join("")}${stadiumLine}`) + (special === undefined ? "" : `${gap}${special.text}`);
|
|
740
755
|
// A drawing of a past season says which: the markings are those of that season.
|
|
741
756
|
const dated = typeof o.year === "number" && Number.isFinite(o.year);
|
|
742
757
|
const eraLine = dated && league !== undefined
|
|
@@ -3,6 +3,7 @@ export type { Flythrough, FlythroughOptions, FlythroughSettings, FlythroughStart
|
|
|
3
3
|
export { CHANNELS, PACE, RAMP, STAGE_IDS, cameraAt, flightPlan, foldTime, glide, glideSeconds, mainView, mapBox, mapFrame, monotoneSlopes, nextRate, stageAt, startOf, stagesOf } from "./flythrough.path.ts";
|
|
4
4
|
export type { Camera, Channel, Flight, FlightInput, Keyframe, MapFrame, StageId } from "./flythrough.path.ts";
|
|
5
5
|
export { flythroughMarkup, graticuleMarkup, mapMarkup } from "./flythrough.markup.ts";
|
|
6
|
+
export { ILLUSTRATIVE_PLAY, PLAY_LOOK, PLAY_PERIOD, PLAY_SECONDS, PLAY_STILL, playAlpha, playMarkup, playMoment } from "./flythrough.game.ts";
|
|
6
7
|
export { FLYTHROUGH_STYLE } from "./flythrough.style.ts";
|
|
7
8
|
export { FLYTHROUGH_WORDS } from "./flythrough.words.ts";
|
|
8
9
|
export type { FlythroughWords } from "./flythrough.words.ts";
|
package/dist/flythrough-entry.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
export { mountFlythrough } from "./flythrough.dom.js";
|
|
3
3
|
export { CHANNELS, PACE, RAMP, STAGE_IDS, cameraAt, flightPlan, foldTime, glide, glideSeconds, mainView, mapBox, mapFrame, monotoneSlopes, nextRate, stageAt, startOf, stagesOf } from "./flythrough.path.js";
|
|
4
4
|
export { flythroughMarkup, graticuleMarkup, mapMarkup } from "./flythrough.markup.js";
|
|
5
|
+
export { ILLUSTRATIVE_PLAY, PLAY_LOOK, PLAY_PERIOD, PLAY_SECONDS, PLAY_STILL, playAlpha, playMarkup, playMoment } from "./flythrough.game.js";
|
|
5
6
|
export { FLYTHROUGH_STYLE } from "./flythrough.style.js";
|
|
6
7
|
export { FLYTHROUGH_WORDS } from "./flythrough.words.js";
|
package/dist/flythrough.dom.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { arenaById, arenaNameIn } from "./arenas.js";
|
|
6
6
|
import { STAGE_IDS, cameraAt, coverSide, flightPlan, foldTime, glide, glideSeconds, lengthWords, mainView, mapBox, mapFrame, nextRate, niceLength, stageAt, startOf, stagesOf, } from "./flythrough.path.js";
|
|
7
7
|
import { flythroughMarkup, graticuleMarkup, gridPath, mapMarkup, stageWords } from "./flythrough.markup.js";
|
|
8
|
+
import { PLAY_STILL, playAlpha, playMarkup, playMoment } from "./flythrough.game.js";
|
|
8
9
|
import { FLYTHROUGH_WORDS, say } from "./flythrough.words.js";
|
|
9
10
|
import { LENS } from "./turn.camera.js";
|
|
10
11
|
import { mountKyogijoTurn } from "./turn.dom.js";
|
|
@@ -84,6 +85,7 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
84
85
|
const scaleBar = root.querySelector(".kf-scale-bar");
|
|
85
86
|
const scaleText = root.querySelector(".kf-scale-text");
|
|
86
87
|
const caption = root.querySelector(".kf-caption");
|
|
88
|
+
const badge = root.querySelector(".kf-badge");
|
|
87
89
|
const live = root.querySelector(".kf-live");
|
|
88
90
|
const media = win?.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
89
91
|
const calm = () => settings.motion === "none" || (settings.motion !== "smooth" && media?.matches === true);
|
|
@@ -92,8 +94,25 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
92
94
|
let turn;
|
|
93
95
|
// The settings object is kept: the view reads its `lens` at every paint, and the path shortens the lens as the camera comes into the bowl.
|
|
94
96
|
const turnSettings = { preset: "overhead", controls: false, venue: { roof: true }, motion: "none", id: `${id}-turn`, lens: LENS };
|
|
95
|
-
if (stageList.some((one) => one === "arena" || one === "bowl" || one === "rink"))
|
|
97
|
+
if (stageList.some((one) => one === "arena" || one === "bowl" || one === "rink" || one === "game"))
|
|
96
98
|
turn = mountKyogijoTurn(turnHost, drawing, turnSettings);
|
|
99
|
+
// ---- The game: a plane that lies on the floor of the 3D arena, with the play's dots drawn on it by the replay's own layers. It takes the floor's own transform after every paint, and stands in the same layer of the stack
|
|
100
|
+
// as the floor, above it and under everything that stands up (the glass, the boards, the stands), so that what is in front of the ice hides the play as it hides the ice. ----
|
|
101
|
+
let gameEl;
|
|
102
|
+
let gameLayers = null;
|
|
103
|
+
let floorEl = null;
|
|
104
|
+
if (turn?.mode === "3d" && stageList.includes("game")) {
|
|
105
|
+
const world = turnHost.querySelector(".ky-turn-world");
|
|
106
|
+
floorEl = world?.querySelector(".ky-turn-floor") ?? null;
|
|
107
|
+
const box = turn.svg()?.getAttribute("viewBox");
|
|
108
|
+
if (world !== null && floorEl !== null && box !== null && box !== undefined) {
|
|
109
|
+
gameEl = doc.createElement("div");
|
|
110
|
+
gameEl.className = "kf-game";
|
|
111
|
+
gameEl.innerHTML = `<svg class="kyogijo ky-nhl" data-sport="nhl" xmlns="http://www.w3.org/2000/svg" viewBox="${box}" aria-hidden="true" focusable="false"${options.theme === undefined ? "" : ` data-theme="${options.theme}"`}><g class="kf-game-layers"></g></svg>`;
|
|
112
|
+
floorEl.after(gameEl);
|
|
113
|
+
gameLayers = gameEl.querySelector("g.kf-game-layers");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
97
116
|
// ---- The site plan: the building's outline and the rink, in the drawing's own feet; the sheet under it is the floor's. ----
|
|
98
117
|
const plan = planVenue(venue);
|
|
99
118
|
const rink = venueRink(venue);
|
|
@@ -169,8 +188,8 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
169
188
|
el.style.setProperty(prop, value);
|
|
170
189
|
};
|
|
171
190
|
const num = (value, places = 2) => String(Math.round(value * 10 ** places) / 10 ** places);
|
|
172
|
-
/** Put a camera on the screen. */
|
|
173
|
-
const render = (cam) => {
|
|
191
|
+
/** Put a camera on the screen; `clockAt` is the seconds the clock has run, from which the play's moment is worked out. */
|
|
192
|
+
const render = (cam, clockAt) => {
|
|
174
193
|
current = cam;
|
|
175
194
|
const W = width;
|
|
176
195
|
const H = height;
|
|
@@ -217,6 +236,27 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
217
236
|
turn.set({ yaw: cam.yaw, pitch: cam.pitch, zoom: Math.exp(cam.logZoom) }, false);
|
|
218
237
|
}
|
|
219
238
|
}
|
|
239
|
+
// The play on the ice, drawn again a thirtieth of a second at a time, and the label that says it is made up.
|
|
240
|
+
const gameAt = startOf(flight, "game");
|
|
241
|
+
const gameShown = cam.game > 0.003 && live3d && gameEl !== undefined && floorEl !== null && gameAt !== undefined;
|
|
242
|
+
if (gameEl !== undefined)
|
|
243
|
+
put("game", gameEl, "visibility", gameShown ? "visible" : "hidden");
|
|
244
|
+
put("badge", badge, "visibility", gameShown ? "visible" : "hidden");
|
|
245
|
+
if (gameShown && gameEl !== undefined && floorEl !== null && gameAt !== undefined) {
|
|
246
|
+
const moment = playMoment(clockAt - gameAt);
|
|
247
|
+
const alpha = cam.game * playAlpha(moment);
|
|
248
|
+
put("game", gameEl, "opacity", num(alpha, 3));
|
|
249
|
+
put("badge", badge, "opacity", num(alpha, 3));
|
|
250
|
+
put("game", gameEl, "width", floorEl.style.width);
|
|
251
|
+
put("game", gameEl, "height", floorEl.style.height);
|
|
252
|
+
// The 3D view has just been painted: the plane takes the floor's place.
|
|
253
|
+
gameEl.style.transform = floorEl.style.transform;
|
|
254
|
+
const frame = String(Math.round(moment * 30));
|
|
255
|
+
if (written["game:frame"] !== frame && gameLayers !== null) {
|
|
256
|
+
written["game:frame"] = frame;
|
|
257
|
+
gameLayers.innerHTML = playMarkup(moment, { id: `${id}-play`, language });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
220
260
|
// The pin, the scale bar (true on the map and the plan, not in perspective) and the stage's words.
|
|
221
261
|
put("pin", pin, "visibility", mapOn ? "visible" : "hidden");
|
|
222
262
|
put("pin", pin, "opacity", num(cam.map, 3));
|
|
@@ -253,6 +293,8 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
253
293
|
};
|
|
254
294
|
// ---- The clock. ----
|
|
255
295
|
let time = 0;
|
|
296
|
+
/** The seconds the clock has run, not folded back by the turn's period as `time` is: the play's moment is worked out from it, so that the play does not jump when the turn comes round. */
|
|
297
|
+
let clock = 0;
|
|
256
298
|
let rate = 0;
|
|
257
299
|
let playing = false;
|
|
258
300
|
let journey;
|
|
@@ -276,7 +318,7 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
276
318
|
const show = (at, announce = true) => {
|
|
277
319
|
time = foldTime(flight, at);
|
|
278
320
|
const cam = cameraAt(flight, time);
|
|
279
|
-
render(cam);
|
|
321
|
+
render(cam, clock);
|
|
280
322
|
enter(stageAt(flight, time), announce);
|
|
281
323
|
};
|
|
282
324
|
const stopLoop = () => {
|
|
@@ -300,12 +342,14 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
300
342
|
journey.elapsed += dt;
|
|
301
343
|
const done = journey.elapsed >= journey.seconds;
|
|
302
344
|
time = done ? journey.to : glide(journey.from, journey.to, journey.elapsed, journey.seconds);
|
|
345
|
+
clock = time;
|
|
303
346
|
if (done)
|
|
304
347
|
journey = undefined;
|
|
305
348
|
}
|
|
306
349
|
else {
|
|
307
350
|
rate = nextRate(rate, playing, dt);
|
|
308
351
|
time += rate * dt * speed;
|
|
352
|
+
clock += rate * dt * speed;
|
|
309
353
|
}
|
|
310
354
|
show(time, true);
|
|
311
355
|
if (busy())
|
|
@@ -334,7 +378,8 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
334
378
|
};
|
|
335
379
|
const stillStage = (to, announce = true) => {
|
|
336
380
|
time = startOf(flight, to) ?? 0;
|
|
337
|
-
|
|
381
|
+
clock = time;
|
|
382
|
+
render(flight.rest[to], to === "game" ? time + PLAY_STILL : time);
|
|
338
383
|
enter(to, announce);
|
|
339
384
|
};
|
|
340
385
|
const scheduleStill = () => {
|
|
@@ -508,7 +553,8 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
508
553
|
stopLoop();
|
|
509
554
|
journey = undefined;
|
|
510
555
|
rate = 0;
|
|
511
|
-
|
|
556
|
+
clock = Math.max(0, seconds);
|
|
557
|
+
show(clock, false);
|
|
512
558
|
},
|
|
513
559
|
destroy() {
|
|
514
560
|
if (!alive)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReplayLook, ReplayTimeline } from "./replay.types.ts";
|
|
2
|
+
import type { Language } from "./types.ts";
|
|
3
|
+
/** A place on the rink, in feet from its top left corner as the drawing has it wide: along the rink (0 to 200) and across it (0 to 85). */
|
|
4
|
+
type P = readonly [number, number];
|
|
5
|
+
/** A skater's way: where he is at a time (seconds), from one waypoint to the next. */
|
|
6
|
+
type Way = readonly (readonly [number, number, number])[];
|
|
7
|
+
/** How long the play runs, and how long it is held at the end (the goal's marks still on the ice) before it starts again. */
|
|
8
|
+
export declare const PLAY_SECONDS = 10;
|
|
9
|
+
export declare const PLAY_HOLD = 1.5;
|
|
10
|
+
/** The seconds of the whole run, the play and the hold: what the loop's period is. */
|
|
11
|
+
export declare const PLAY_PERIOD: number;
|
|
12
|
+
/** The moment a still of the play (for a reader who prefers reduced motion) is taken at: the shot has just been taken. */
|
|
13
|
+
export declare const PLAY_STILL = 6.4;
|
|
14
|
+
/** The seconds the dots fade in and out at the ends of a run, so that the play does not jump back to its start in view. */
|
|
15
|
+
export declare const PLAY_FADE = 0.5;
|
|
16
|
+
/** A point of a way at a time: a cubic through the waypoints with the slope between the neighbours at each, so that a skater never stops or turns on a corner. */
|
|
17
|
+
export declare function at(way: Way, t: number): P;
|
|
18
|
+
/** The puck at a time. */
|
|
19
|
+
export declare function puckAt(t: number): P;
|
|
20
|
+
/** The play, as a replay's timeline in the surface's own feet. It is made once. */
|
|
21
|
+
export declare const ILLUSTRATIVE_PLAY: ReplayTimeline;
|
|
22
|
+
/** How the play is drawn: a short trail behind the puck, the dots large enough to see from the stands, events that stay a moment. */
|
|
23
|
+
export declare const PLAY_LOOK: ReplayLook;
|
|
24
|
+
/** Where a moment of a run is in the play, looping: `seconds` after the run began, brought into one period. */
|
|
25
|
+
export declare const playMoment: (seconds: number) => number;
|
|
26
|
+
/** How much of the play shows at a moment of a run: it fades in at the start and out at the end of each run. 0 to 1. */
|
|
27
|
+
export declare function playAlpha(moment: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* The play at a moment of a run, as the layers' SVG (a `g.ky-layers` in the drawing's own feet, to go inside an `svg.kyogijo` with the floor's viewBox): the dots with their numbers, the puck and its trail, and the events that are on
|
|
30
|
+
* the ice at that time, drawn by the replay's own layers. The same moment always gives the same string.
|
|
31
|
+
*/
|
|
32
|
+
export declare function playMarkup(moment: number, options: {
|
|
33
|
+
id: string;
|
|
34
|
+
language?: Language;
|
|
35
|
+
}): string;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The game of the fly-through: a short play on the rink, made up for the picture and said to be: five skaters and a goalie a side, a breakout, two passes, a rush and a goal, as a replay's timeline
|
|
2
|
+
// (`ReplayTimeline` of `replay.types.ts`, the format the package already animates), so that the dots, their numbers, the puck, its trail and the events are drawn by the replay's own layers. It is NOT a real game's tracking data
|
|
3
|
+
// and nothing in it is a real player, a real team or a real play: the shirt numbers are only numbers. Pure and deterministic: the same moment always gives the same frame.
|
|
4
|
+
import { renderLayers } from "./layers.js";
|
|
5
|
+
import { replayLayers } from "./replay.js";
|
|
6
|
+
/** How long the play runs, and how long it is held at the end (the goal's marks still on the ice) before it starts again. */
|
|
7
|
+
export const PLAY_SECONDS = 10;
|
|
8
|
+
export const PLAY_HOLD = 1.5;
|
|
9
|
+
/** The seconds of the whole run, the play and the hold: what the loop's period is. */
|
|
10
|
+
export const PLAY_PERIOD = PLAY_SECONDS + PLAY_HOLD;
|
|
11
|
+
/** The moment a still of the play (for a reader who prefers reduced motion) is taken at: the shot has just been taken. */
|
|
12
|
+
export const PLAY_STILL = 6.4;
|
|
13
|
+
/** The seconds the dots fade in and out at the ends of a run, so that the play does not jump back to its start in view. */
|
|
14
|
+
export const PLAY_FADE = 0.5;
|
|
15
|
+
/** The home skaters, as waypoints `[seconds, x, y]`: the home team attacks to the right. */
|
|
16
|
+
const HOME = {
|
|
17
|
+
h4: { number: 4, way: [[0, 24, 26], [1.2, 30, 30], [3.5, 60, 24], [6, 110, 30], [8, 130, 35], [10, 135, 38]] },
|
|
18
|
+
h6: { number: 6, way: [[0, 24, 58], [1.5, 40, 60], [3.5, 75, 58], [6, 120, 55], [8, 140, 50], [10, 145, 48]] },
|
|
19
|
+
h17: { number: 17, way: [[0, 45, 12], [1.2, 48, 12], [2.6, 85, 14], [4, 130, 12], [5.4, 165, 18], [6.8, 176, 28], [10, 170, 30]] },
|
|
20
|
+
h9: { number: 9, way: [[0, 60, 40], [2.4, 80, 40], [3.6, 105, 36], [5, 150, 42], [6.2, 166, 42], [7, 180, 44], [10, 178, 46]] },
|
|
21
|
+
h22: { number: 22, way: [[0, 55, 70], [2.6, 90, 70], [4.2, 135, 64], [5.6, 165, 60], [6.4, 178, 50], [10, 176, 52]] },
|
|
22
|
+
};
|
|
23
|
+
/** The away skaters, who fall back into their own end as the rush comes. */
|
|
24
|
+
const AWAY = {
|
|
25
|
+
a11: { number: 11, way: [[0, 95, 40], [2, 110, 42], [4, 150, 44], [6, 170, 43], [10, 180, 44]] },
|
|
26
|
+
a14: { number: 14, way: [[0, 100, 20], [3, 125, 22], [5, 160, 24], [6.5, 175, 26], [10, 178, 30]] },
|
|
27
|
+
a27: { number: 27, way: [[0, 98, 65], [3, 128, 62], [5, 158, 62], [6.5, 176, 62], [10, 178, 60]] },
|
|
28
|
+
a5: { number: 5, way: [[0, 150, 30], [4, 160, 30], [6, 172, 34], [10, 180, 32]] },
|
|
29
|
+
a8: { number: 8, way: [[0, 150, 55], [4, 162, 54], [6, 172, 52], [10, 180, 54]] },
|
|
30
|
+
};
|
|
31
|
+
/** The goalies: the home one stays on his line, the away one moves across his crease as the shot comes. */
|
|
32
|
+
const HOME_GOALIE = [11, 42.5];
|
|
33
|
+
const AWAY_GOAL = [189, 42.5];
|
|
34
|
+
/** A point of a way at a time: a cubic through the waypoints with the slope between the neighbours at each, so that a skater never stops or turns on a corner. */
|
|
35
|
+
export function at(way, t) {
|
|
36
|
+
const last = way.length - 1;
|
|
37
|
+
if (t <= way[0][0])
|
|
38
|
+
return [way[0][1], way[0][2]];
|
|
39
|
+
if (t >= way[last][0])
|
|
40
|
+
return [way[last][1], way[last][2]];
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < last - 1 && t >= way[i + 1][0])
|
|
43
|
+
i += 1;
|
|
44
|
+
const [t0, x0, y0] = way[i];
|
|
45
|
+
const [t1, x1, y1] = way[i + 1];
|
|
46
|
+
const slope = (k) => {
|
|
47
|
+
const a = way[Math.max(0, k - 1)];
|
|
48
|
+
const b = way[Math.min(last, k + 1)];
|
|
49
|
+
const span = b[0] - a[0];
|
|
50
|
+
return span === 0 ? [0, 0] : [(b[1] - a[1]) / span, (b[2] - a[2]) / span];
|
|
51
|
+
};
|
|
52
|
+
const [m0, m1] = [slope(i), slope(i + 1)];
|
|
53
|
+
const h = t1 - t0;
|
|
54
|
+
const s = (t - t0) / h;
|
|
55
|
+
const [s2, s3] = [s * s, s * s * s];
|
|
56
|
+
const [h00, h10, h01, h11] = [2 * s3 - 3 * s2 + 1, s3 - 2 * s2 + s, -2 * s3 + 3 * s2, s3 - s2];
|
|
57
|
+
return [h00 * x0 + h10 * h * m0[0] + h01 * x1 + h11 * h * m1[0], h00 * y0 + h10 * h * m0[1] + h01 * y1 + h11 * h * m1[1]];
|
|
58
|
+
}
|
|
59
|
+
const lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
|
|
60
|
+
const round = (n) => Math.round(n * 100) / 100;
|
|
61
|
+
const LEGS = [
|
|
62
|
+
{ from: 0, to: 1.2, carrier: "h4" },
|
|
63
|
+
{ from: 1.2, to: 1.8, carrier: null },
|
|
64
|
+
{ from: 1.8, to: 2.6, carrier: "h17" },
|
|
65
|
+
{ from: 2.6, to: 3.6, carrier: null },
|
|
66
|
+
{ from: 3.6, to: 6.2, carrier: "h9" },
|
|
67
|
+
{ from: 6.2, to: 6.55, carrier: null },
|
|
68
|
+
{ from: 6.55, to: PLAY_SECONDS, carrier: "net" },
|
|
69
|
+
];
|
|
70
|
+
const WAYS = Object.fromEntries([...Object.entries(HOME), ...Object.entries(AWAY)].map(([id, one]) => [id, one.way]));
|
|
71
|
+
const where = (id, t) => (id === "net" ? AWAY_GOAL : at(WAYS[id], t));
|
|
72
|
+
/** The puck at a time. */
|
|
73
|
+
export function puckAt(t) {
|
|
74
|
+
const time = Math.min(Math.max(t, 0), PLAY_SECONDS);
|
|
75
|
+
const index = LEGS.findIndex((leg) => time <= leg.to);
|
|
76
|
+
const leg = LEGS[index < 0 ? LEGS.length - 1 : index];
|
|
77
|
+
const carried = (id, when) => {
|
|
78
|
+
if (id === "net")
|
|
79
|
+
return AWAY_GOAL;
|
|
80
|
+
const [x, y] = where(id, when);
|
|
81
|
+
return [x + 2.5, y];
|
|
82
|
+
};
|
|
83
|
+
if (leg.carrier !== null)
|
|
84
|
+
return carried(leg.carrier, time);
|
|
85
|
+
// A pass or a shot goes from the carrier before it, at its start, to the carrier after it, at its end.
|
|
86
|
+
const [before, after] = [LEGS[LEGS.indexOf(leg) - 1], LEGS[LEGS.indexOf(leg) + 1]];
|
|
87
|
+
return lerp(carried(before.carrier, leg.from), after.carrier === "net" ? AWAY_GOAL : carried(after.carrier, leg.to), (time - leg.from) / (leg.to - leg.from));
|
|
88
|
+
}
|
|
89
|
+
/** The away goalie's place: across his crease a little as the shot is taken, and back. */
|
|
90
|
+
function awayGoalie(t) {
|
|
91
|
+
const bump = Math.max(0, 1 - Math.abs(t - 6.45) / 0.6);
|
|
92
|
+
return [AWAY_GOAL[0] - 1, AWAY_GOAL[1] - 3 * bump];
|
|
93
|
+
}
|
|
94
|
+
/** Who has the puck at a time, as a replay says it: a skater's id, or `null` while it is passed or shot. */
|
|
95
|
+
const possessionAt = (t) => {
|
|
96
|
+
const leg = LEGS.find((one) => t >= one.from && t < one.to);
|
|
97
|
+
return leg === undefined || leg.carrier === null || leg.carrier === "net" ? null : leg.carrier;
|
|
98
|
+
};
|
|
99
|
+
/** The frames of the play, a tenth of a second apart. */
|
|
100
|
+
function frames() {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (let step = 0; step <= PLAY_SECONDS * 10; step += 1) {
|
|
103
|
+
const t = step / 10;
|
|
104
|
+
const objects = [];
|
|
105
|
+
for (const [id, one] of Object.entries(HOME))
|
|
106
|
+
objects.push({ id, team: "home", number: one.number, x: round(at(one.way, t)[0]), y: round(at(one.way, t)[1]) });
|
|
107
|
+
objects.push({ id: "h30", team: "home", number: 30, x: HOME_GOALIE[0], y: HOME_GOALIE[1] });
|
|
108
|
+
for (const [id, one] of Object.entries(AWAY))
|
|
109
|
+
objects.push({ id, team: "away", number: one.number, x: round(at(one.way, t)[0]), y: round(at(one.way, t)[1]) });
|
|
110
|
+
const goalie = awayGoalie(t);
|
|
111
|
+
objects.push({ id: "a35", team: "away", number: 35, x: round(goalie[0]), y: round(goalie[1]) });
|
|
112
|
+
const puck = puckAt(t);
|
|
113
|
+
objects.push({ id: "puck", team: "puck", x: round(puck[0]), y: round(puck[1]) });
|
|
114
|
+
const holder = possessionAt(t);
|
|
115
|
+
out.push({ t, objects, possession: holder });
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const EVENTS = [
|
|
120
|
+
{ t: 1.2, kind: "pass", team: "home", by: "h4", to: "h17", until: 1.8 },
|
|
121
|
+
{ t: 2.6, kind: "pass", team: "home", by: "h17", to: "h9", until: 3.6 },
|
|
122
|
+
{ t: 6.2, kind: "shot", team: "home", by: "h9", x2: AWAY_GOAL[0], y2: AWAY_GOAL[1], until: 6.55 },
|
|
123
|
+
{ t: 6.55, kind: "goal", team: "home", by: "h9", x: AWAY_GOAL[0], y: AWAY_GOAL[1] },
|
|
124
|
+
];
|
|
125
|
+
/** The play, as a replay's timeline in the surface's own feet. It is made once. */
|
|
126
|
+
export const ILLUSTRATIVE_PLAY = Object.freeze({ frames: frames(), events: EVENTS });
|
|
127
|
+
/** How the play is drawn: a short trail behind the puck, the dots large enough to see from the stands, events that stay a moment. */
|
|
128
|
+
export const PLAY_LOOK = Object.freeze({ trail: 1.2, hold: 1.5, scale: 1.7, numbers: true });
|
|
129
|
+
/** Where a moment of a run is in the play, looping: `seconds` after the run began, brought into one period. */
|
|
130
|
+
export const playMoment = (seconds) => ((seconds % PLAY_PERIOD) + PLAY_PERIOD) % PLAY_PERIOD;
|
|
131
|
+
/** How much of the play shows at a moment of a run: it fades in at the start and out at the end of each run. 0 to 1. */
|
|
132
|
+
export function playAlpha(moment) {
|
|
133
|
+
const m = Math.min(Math.max(moment, 0), PLAY_PERIOD);
|
|
134
|
+
return Math.min(1, m / PLAY_FADE, (PLAY_PERIOD - m) / PLAY_FADE);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The play at a moment of a run, as the layers' SVG (a `g.ky-layers` in the drawing's own feet, to go inside an `svg.kyogijo` with the floor's viewBox): the dots with their numbers, the puck and its trail, and the events that are on
|
|
138
|
+
* the ice at that time, drawn by the replay's own layers. The same moment always gives the same string.
|
|
139
|
+
*/
|
|
140
|
+
export function playMarkup(moment, options) {
|
|
141
|
+
const t = Math.min(playMoment(moment), PLAY_SECONDS);
|
|
142
|
+
const language = options.language === "ja" ? "ja" : "en";
|
|
143
|
+
const surface = { sport: "nhl", id: options.id, language };
|
|
144
|
+
return renderLayers(surface, replayLayers(ILLUSTRATIVE_PLAY, t, PLAY_LOOK, language)).svg;
|
|
145
|
+
}
|
|
@@ -75,6 +75,7 @@ export function flythroughMarkup(input) {
|
|
|
75
75
|
`<div class="kf-layer kf-3d" inert></div>` +
|
|
76
76
|
`<div class="kf-pin" aria-hidden="true"></div>` +
|
|
77
77
|
`<div class="kf-scale" aria-hidden="true"><span class="kf-scale-bar"></span><span class="kf-scale-text"></span></div>` +
|
|
78
|
+
`<div class="kf-badge" aria-hidden="true">${esc(words.illustrative)}</div>` +
|
|
78
79
|
`<div class="kf-caption" aria-hidden="true"><span class="kf-title">${esc(intro.name)}</span><span class="kf-text">${esc(intro.text)}</span></div>` +
|
|
79
80
|
`</div>` +
|
|
80
81
|
`<p class="kf-help" id="${esc(input.id)}-help">${esc(words.keys)}</p>` +
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/** The stages of the fly-through, in order. */
|
|
2
|
-
export type StageId = "map" | "city" | "arena" | "bowl" | "rink";
|
|
2
|
+
export type StageId = "map" | "city" | "arena" | "bowl" | "rink" | "game";
|
|
3
3
|
/** The stages that exist, in the order of the path. */
|
|
4
4
|
export declare const STAGE_IDS: readonly StageId[];
|
|
5
5
|
/** The channels of the camera. */
|
|
6
|
-
export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
|
|
6
|
+
export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof", "game"];
|
|
7
7
|
export type Channel = (typeof CHANNELS)[number];
|
|
8
8
|
/**
|
|
9
9
|
* Where the camera is. `logSpan` is the natural logarithm of the metres the container's width shows (so that a zoom is a straight line); `ax` and `ay` are where the arena stands on the screen, as a
|
|
10
10
|
* share of the width and of the height from the middle; `north` is how far the map and the site plan are turned, in degrees clockwise (the rink's long side lies along the page when it reaches the
|
|
11
11
|
* venue's); `map`, `site` and `arena` are how much of the map, of the site plan and of the 3D arena shows (0 to 1); `yaw`, `pitch` and `logZoom` are the 3D view's turn and tip in degrees and the natural logarithm of its zoom;
|
|
12
|
-
* `logLens` is the natural logarithm of the eye's lens, as a multiple of the stage's longer side (a long lens looks from far outside, a short one from inside the bowl)
|
|
12
|
+
* `logLens` is the natural logarithm of the eye's lens, as a multiple of the stage's longer side (a long lens looks from far outside, a short one from inside the bowl), `roof` is how much of the building's roof shows and `game` how much of the
|
|
13
|
+
* play on the ice shows (each 0 to 1).
|
|
13
14
|
*/
|
|
14
15
|
export type Camera = Record<Channel, number>;
|
|
15
16
|
/** What the path needs to know of the page and the venue. */
|
|
@@ -92,7 +93,7 @@ export declare const stagesOf: (flight: Flight) => StageId[];
|
|
|
92
93
|
export declare const turnPeriod: (flight: Flight) => number;
|
|
93
94
|
/** A moment after the end brought back by whole turns of the arena to one near the end: the same view, and a time that does not grow without end. */
|
|
94
95
|
export declare function foldTime(flight: Flight, time: number): number;
|
|
95
|
-
/** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long
|
|
96
|
+
/** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long the rink turns bare before the play comes onto it, and `fade` how long the play takes to show. */
|
|
96
97
|
export declare const PACE: {
|
|
97
98
|
readonly dwell: 1.6;
|
|
98
99
|
readonly step: 1.25;
|
|
@@ -105,6 +106,7 @@ export declare const PACE: {
|
|
|
105
106
|
readonly over: 2.2;
|
|
106
107
|
readonly settle: 2.2;
|
|
107
108
|
readonly stay: 2;
|
|
109
|
+
readonly fade: 0.6;
|
|
108
110
|
};
|
|
109
111
|
/**
|
|
110
112
|
* The path from the map to the rink, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
|
package/dist/flythrough.path.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// Node, tested frame by frame, and handed to the mount, which draws it.
|
|
6
6
|
import { LENS, LONG_PRESETS, presetView } from "./turn.camera.js";
|
|
7
7
|
/** The stages that exist, in the order of the path. */
|
|
8
|
-
export const STAGE_IDS = ["map", "city", "arena", "bowl", "rink"];
|
|
8
|
+
export const STAGE_IDS = ["map", "city", "arena", "bowl", "rink", "game"];
|
|
9
9
|
/** The channels of the camera. */
|
|
10
|
-
export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
|
|
10
|
+
export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof", "game"];
|
|
11
11
|
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
12
12
|
const lerp = (a, b, t) => a + (b - a) * t;
|
|
13
13
|
/** Zero to one with a zero slope at both ends. */
|
|
@@ -53,34 +53,33 @@ function hermite(times, values, slopes, time) {
|
|
|
53
53
|
const s3 = s2 * s;
|
|
54
54
|
return (2 * s3 - 3 * s2 + 1) * values[i] + (s3 - 2 * s2 + s) * h * slopes[i] + (-2 * s3 + 3 * s2) * values[i + 1] + (s3 - s2) * h * slopes[i + 1];
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const known = SLOPES.get(keys);
|
|
56
|
+
const TABLES = new WeakMap();
|
|
57
|
+
function tableOf(keys) {
|
|
58
|
+
const known = TABLES.get(keys);
|
|
60
59
|
if (known !== undefined)
|
|
61
60
|
return known;
|
|
62
61
|
const times = keys.map((key) => key.t);
|
|
63
|
-
const
|
|
62
|
+
const values = {};
|
|
63
|
+
const slopes = {};
|
|
64
64
|
for (const channel of CHANNELS) {
|
|
65
|
-
|
|
65
|
+
values[channel] = keys.map((key) => key.camera[channel]);
|
|
66
|
+
slopes[channel] = monotoneSlopes(times, values[channel]);
|
|
66
67
|
keys.forEach((key, i) => {
|
|
67
68
|
const own = key.slope?.[channel];
|
|
68
69
|
if (own !== undefined)
|
|
69
|
-
slopes[i] = own;
|
|
70
|
+
slopes[channel][i] = own;
|
|
70
71
|
});
|
|
71
|
-
made[channel] = slopes;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
const made = { times, values, slopes };
|
|
74
|
+
TABLES.set(keys, made);
|
|
74
75
|
return made;
|
|
75
76
|
}
|
|
76
77
|
/** The camera at a moment of a path. After the last keyframe the arena goes on turning and nothing else moves. Before the first, the first. */
|
|
77
78
|
export function cameraAt(flight, time) {
|
|
78
|
-
const {
|
|
79
|
-
const slopes = slopesOf(keys);
|
|
80
|
-
const times = keys.map((key) => key.t);
|
|
79
|
+
const { times, values, slopes } = tableOf(flight.keys);
|
|
81
80
|
const out = {};
|
|
82
81
|
for (const channel of CHANNELS)
|
|
83
|
-
out[channel] = hermite(times,
|
|
82
|
+
out[channel] = hermite(times, values[channel], slopes[channel], time);
|
|
84
83
|
if (time > flight.end)
|
|
85
84
|
out.yaw += flight.tail * (time - flight.end);
|
|
86
85
|
return out;
|
|
@@ -107,9 +106,9 @@ export function foldTime(flight, time) {
|
|
|
107
106
|
return flight.end + ((time - flight.end) % period);
|
|
108
107
|
}
|
|
109
108
|
/** The camera's defaults: the map, from far above, with nothing of the arena showing. */
|
|
110
|
-
const BASE = { logSpan: 0, ax: 0, ay: 0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0, logLens: Math.log(LENS), roof: 1 };
|
|
111
|
-
/** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long
|
|
112
|
-
export const PACE = { dwell: 1.6, step: 1.25, cityDwell: 0.9, approach: 1.6, rise: 0.8, hand: 0.7, tilt: 3.2, hold: 3.5, over: 2.2, settle: 2.2, stay: 2 };
|
|
109
|
+
const BASE = { logSpan: 0, ax: 0, ay: 0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0, logLens: Math.log(LENS), roof: 1, game: 0 };
|
|
110
|
+
/** How a path is paced: the seconds of each part. `hold` is how long the arena turns, whole, before the camera goes in; `over` and `settle` the two parts of the way into the bowl; `stay` how long the rink turns bare before the play comes onto it, and `fade` how long the play takes to show. */
|
|
111
|
+
export const PACE = { dwell: 1.6, step: 1.25, cityDwell: 0.9, approach: 1.6, rise: 0.8, hand: 0.7, tilt: 3.2, hold: 3.5, over: 2.2, settle: 2.2, stay: 2, fade: 0.6 };
|
|
113
112
|
/**
|
|
114
113
|
* The path from the map to the rink, for the stages asked (all by default): the map holds a moment, then the camera falls to the city in four even steps of the logarithm of the distance (the
|
|
115
114
|
* map turning as it comes to the venue's own orientation and giving way to the site plan), holds there, comes in until the plan is as large as the 3D arena seen from above, the arena rises out of it,
|
|
@@ -148,7 +147,7 @@ export function flightPlan(input) {
|
|
|
148
147
|
// Where the turn has got to at each key of the arena, the bowl and the rink: the speed changes from `turn` to `rinkTurn` along the way, and each yaw is the one the two speeds either side of it come to.
|
|
149
148
|
const yaws = [];
|
|
150
149
|
let yaw = 0;
|
|
151
|
-
for (const [from, to, seconds] of [[0, turn, PACE.tilt], [turn, turn, PACE.hold], [turn, (turn + rinkTurn) / 2, PACE.over], [(turn + rinkTurn) / 2, rinkTurn, PACE.settle], [rinkTurn, rinkTurn, PACE.stay]]) {
|
|
150
|
+
for (const [from, to, seconds] of [[0, turn, PACE.tilt], [turn, turn, PACE.hold], [turn, (turn + rinkTurn) / 2, PACE.over], [(turn + rinkTurn) / 2, rinkTurn, PACE.settle], [rinkTurn, rinkTurn, PACE.stay], [rinkTurn, rinkTurn, PACE.fade]]) {
|
|
152
151
|
yaw -= ((from + to) / 2) * seconds;
|
|
153
152
|
yaws.push(yaw);
|
|
154
153
|
}
|
|
@@ -171,9 +170,13 @@ export function flightPlan(input) {
|
|
|
171
170
|
},
|
|
172
171
|
{
|
|
173
172
|
id: "rink",
|
|
173
|
+
keys: [{ dt: PACE.settle, camera: { ...near, arena: 1, site: 0, yaw: yaws[3], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 0 }, slope: { yaw: -rinkTurn } }],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "game",
|
|
174
177
|
keys: [
|
|
175
|
-
{ dt: PACE.
|
|
176
|
-
{ dt: PACE.
|
|
178
|
+
{ dt: PACE.stay, camera: { ...near, arena: 1, site: 0, yaw: yaws[4], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 0 }, slope: { yaw: -rinkTurn } },
|
|
179
|
+
{ dt: PACE.fade, camera: { ...near, arena: 1, site: 0, yaw: yaws[5], pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 1 }, slope: { yaw: -rinkTurn } },
|
|
177
180
|
],
|
|
178
181
|
},
|
|
179
182
|
];
|
|
@@ -208,7 +211,8 @@ export function flightPlan(input) {
|
|
|
208
211
|
city: { ...BASE, ...arrive },
|
|
209
212
|
arena: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: view.pitch, logZoom: 0 },
|
|
210
213
|
bowl: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: lerp(view.pitch, seat.pitch, 0.5), logZoom: ln(seat.zoom) * 0.55, logLens: lerp(ln(LENS), ln(seat.lens), 0.6), roof: 0 },
|
|
211
|
-
rink: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0 },
|
|
214
|
+
rink: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 0 },
|
|
215
|
+
game: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: seat.pitch, logZoom: ln(seat.zoom), logLens: ln(seat.lens), roof: 0, game: 1 },
|
|
212
216
|
};
|
|
213
217
|
return {
|
|
214
218
|
keys,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The fly-through's stylesheet. */
|
|
2
|
-
export declare const FLYTHROUGH_STYLE = ".kf{--kf-bg:#ffffff;--kf-ink:#10151b;--kf-muted:#434b54;--kf-panel:rgba(255,255,255,.9);--kf-line:#b9b3a3;--kf-accent:#1f5fa8;--kf-pin:#c0392b;--kf-grid:rgba(60,70,80,.2);--kf-sea:#cfe3ee;--kf-button:#ffffff;--kf-button-on:#10151b;--kf-button-on-ink:#ffffff;--kf-land:#e9e1c8;--kf-coast:#5d5a50;color:var(--kf-ink);font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif;color-scheme:light dark}\n@media (prefers-color-scheme:dark){.kf:not([data-theme=light]){--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3}}\n.kf[data-theme=dark]{--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3;color-scheme:dark}\n.kf[data-theme=light]{color-scheme:light}\n.kf *{box-sizing:border-box}\n.kf [hidden]{display:none!important}\n.kf-stage{position:relative;isolation:isolate;width:100%;aspect-ratio:16/10;min-height:260px;overflow:hidden;background:var(--kf-bg);border:1px solid var(--kf-line);border-radius:12px;outline-offset:2px;user-select:none;-webkit-user-select:none}\n.kf-stage:focus-visible{outline:3px solid var(--kf-accent)}\n@media (max-width:560px){.kf-stage{aspect-ratio:4/5}}\n.kf-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.kf-map{overflow:hidden}\n.kf-map-box{position:absolute;transform-origin:50% 50%;background:var(--kf-sea)}\n.kf-map-box svg{display:block;width:100%;height:100%}\n.kf-map-box .chizu{--cz-sea:var(--kf-sea);--cz-land:var(--kf-land);--cz-line:var(--kf-coast)}\n.kf-site{overflow:hidden}\n.kf-grid{fill:none;stroke:var(--kf-grid);stroke-width:1}\n.kf-rink{fill:var(--kf-bg);fill-opacity:.9;stroke:var(--kf-muted);stroke-width:.6;vector-effect:non-scaling-stroke}\n.kf-3d{overflow:hidden}\n.kf-3d .ky-turn{height:100%}\n.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}\n.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}\n.kf-3d .ky-up-roof{filter:opacity(var(--kf-roof,1))}\n.kf-3d[data-roof=\"0\"] .ky-up-roof{visibility:hidden}\n.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}\n.kf-pin::before{content:\"\";position:absolute;left:-7px;top:-7px;width:14px;height:14px;border-radius:50%;background:var(--kf-pin);border:2px solid var(--kf-bg);box-shadow:0 0 0 2px var(--kf-pin)}\n.kf-scale{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:2px;align-items:flex-start;font-size:.8rem;color:var(--kf-ink);background:var(--kf-panel);padding:4px 8px;border-radius:8px;pointer-events:none}\n.kf-scale-bar{display:block;height:6px;border:2px solid var(--kf-ink);border-top:0}\n.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}\n.kf-title{display:block;font-weight:800}\n.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}\n.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}\n.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}\n.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}\n.kf button[aria-pressed=true],.kf button[aria-current=step]{background:var(--kf-button-on);color:var(--kf-button-on-ink)}\n.kf button:focus-visible{outline:3px solid var(--kf-accent);outline-offset:2px}\n.kf-help,.kf-credit,.kf-still{margin:8px 0 0;font-size:.85rem;color:var(--kf-muted)}\n.kf-live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";
|
|
2
|
+
export declare const FLYTHROUGH_STYLE = ".kf{--kf-bg:#ffffff;--kf-ink:#10151b;--kf-muted:#434b54;--kf-panel:rgba(255,255,255,.9);--kf-line:#b9b3a3;--kf-accent:#1f5fa8;--kf-pin:#c0392b;--kf-grid:rgba(60,70,80,.2);--kf-sea:#cfe3ee;--kf-button:#ffffff;--kf-button-on:#10151b;--kf-button-on-ink:#ffffff;--kf-land:#e9e1c8;--kf-coast:#5d5a50;color:var(--kf-ink);font:16px/1.45 system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif;color-scheme:light dark}\n@media (prefers-color-scheme:dark){.kf:not([data-theme=light]){--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3}}\n.kf[data-theme=dark]{--kf-bg:#18212a;--kf-ink:#f6f3ea;--kf-muted:#bcc3cb;--kf-panel:rgba(24,33,42,.9);--kf-line:#475463;--kf-accent:#7fb2ee;--kf-pin:#ff7a6b;--kf-grid:rgba(200,210,220,.22);--kf-sea:#14232f;--kf-button:#18212a;--kf-button-on:#f6f3ea;--kf-button-on-ink:#0d1218;--kf-land:#2f3b44;--kf-coast:#8a97a3;color-scheme:dark}\n.kf[data-theme=light]{color-scheme:light}\n.kf *{box-sizing:border-box}\n.kf [hidden]{display:none!important}\n.kf-stage{position:relative;isolation:isolate;width:100%;aspect-ratio:16/10;min-height:260px;overflow:hidden;background:var(--kf-bg);border:1px solid var(--kf-line);border-radius:12px;outline-offset:2px;user-select:none;-webkit-user-select:none}\n.kf-stage:focus-visible{outline:3px solid var(--kf-accent)}\n@media (max-width:560px){.kf-stage{aspect-ratio:4/5}}\n.kf-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.kf-map{overflow:hidden}\n.kf-map-box{position:absolute;transform-origin:50% 50%;background:var(--kf-sea)}\n.kf-map-box svg{display:block;width:100%;height:100%}\n.kf-map-box .chizu{--cz-sea:var(--kf-sea);--cz-land:var(--kf-land);--cz-line:var(--kf-coast)}\n.kf-site{overflow:hidden}\n.kf-grid{fill:none;stroke:var(--kf-grid);stroke-width:1}\n.kf-rink{fill:var(--kf-bg);fill-opacity:.9;stroke:var(--kf-muted);stroke-width:.6;vector-effect:non-scaling-stroke}\n.kf-3d{overflow:hidden}\n.kf-3d .ky-turn{height:100%}\n.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}\n.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}\n.kf-3d .ky-up-roof{filter:opacity(var(--kf-roof,1))}\n.kf-3d[data-roof=\"0\"] .ky-up-roof{visibility:hidden}\n.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}\n.kf-pin::before{content:\"\";position:absolute;left:-7px;top:-7px;width:14px;height:14px;border-radius:50%;background:var(--kf-pin);border:2px solid var(--kf-bg);box-shadow:0 0 0 2px var(--kf-pin)}\n.kf-scale{position:absolute;left:12px;bottom:12px;display:flex;flex-direction:column;gap:2px;align-items:flex-start;font-size:.8rem;color:var(--kf-ink);background:var(--kf-panel);padding:4px 8px;border-radius:8px;pointer-events:none}\n.kf-scale-bar{display:block;height:6px;border:2px solid var(--kf-ink);border-top:0}\n.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}\n.kf-title{display:block;font-weight:800}\n.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}\n.kf-badge{position:absolute;left:12px;top:12px;padding:3px 10px;border-radius:999px;background:var(--kf-panel);border:1px solid var(--kf-line);font-size:.8rem;font-weight:700;color:var(--kf-ink);pointer-events:none;opacity:0;visibility:hidden}\n.kf-game{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none;z-index:1}\n.kf-game svg{display:block;width:100%;height:100%;overflow:visible}\n.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}\n.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}\n.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}\n.kf button[aria-pressed=true],.kf button[aria-current=step]{background:var(--kf-button-on);color:var(--kf-button-on-ink)}\n.kf button:focus-visible{outline:3px solid var(--kf-accent);outline-offset:2px}\n.kf-help,.kf-credit,.kf-still{margin:8px 0 0;font-size:.85rem;color:var(--kf-muted)}\n.kf-live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}";
|
package/dist/flythrough.style.js
CHANGED
|
@@ -33,6 +33,9 @@ export const FLYTHROUGH_STYLE = `.kf{${LIGHT};color:var(--kf-ink);font:16px/1.45
|
|
|
33
33
|
.kf-caption{position:absolute;right:12px;top:12px;max-width:min(26rem,calc(100% - 24px));padding:8px 12px;border-radius:10px;background:var(--kf-panel);color:var(--kf-ink);pointer-events:none}
|
|
34
34
|
.kf-title{display:block;font-weight:800}
|
|
35
35
|
.kf-text{display:block;font-size:.92rem;color:var(--kf-muted)}
|
|
36
|
+
.kf-badge{position:absolute;left:12px;top:12px;padding:3px 10px;border-radius:999px;background:var(--kf-panel);border:1px solid var(--kf-line);font-size:.8rem;font-weight:700;color:var(--kf-ink);pointer-events:none;opacity:0;visibility:hidden}
|
|
37
|
+
.kf-game{position:absolute;left:0;top:0;transform-origin:0 0;pointer-events:none;z-index:1}
|
|
38
|
+
.kf-game svg{display:block;width:100%;height:100%;overflow:visible}
|
|
36
39
|
.kf-controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:10px 0 0}
|
|
37
40
|
.kf-group{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}
|
|
38
41
|
.kf button{font:inherit;font-size:1rem;font-weight:700;min-height:44px;min-width:44px;padding:0 14px;border-radius:12px;border:2px solid var(--kf-ink);background:var(--kf-button);color:var(--kf-ink);cursor:pointer}
|
|
@@ -25,6 +25,8 @@ export interface FlythroughWords {
|
|
|
25
25
|
/** The credit of the map and of the building's outline, drawn with the picture. */
|
|
26
26
|
creditMap: string;
|
|
27
27
|
creditPlain: string;
|
|
28
|
+
/** The label on the picture while the play is on the ice: it is made up for the picture. */
|
|
29
|
+
illustrative: string;
|
|
28
30
|
/** The sentence on a still stage, for a reader who prefers reduced motion. */
|
|
29
31
|
still: string;
|
|
30
32
|
}
|