@johnmorrisdotca/kyogijo-stadiums 0.27.0 → 0.28.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 +8 -0
- package/dist/district.d.ts +1 -1
- package/dist/district.js +1 -1
- package/dist/flythrough.dom.js +15 -5
- package/dist/flythrough.markup.d.ts +5 -0
- package/dist/flythrough.markup.js +1 -1
- package/dist/flythrough.path.d.ts +21 -8
- package/dist/flythrough.path.js +36 -11
- package/dist/flythrough.style.d.ts +1 -1
- package/dist/flythrough.style.js +2 -0
- package/dist/flythrough.words.d.ts +2 -2
- package/dist/flythrough.words.js +6 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ All notable changes to this project are written here. The format follows
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.28.0] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The fly-through goes into the bowl** (`bowl` and `rink`, two stages after `arena`): the roof lifts away, and the camera comes in over the stands, tipping lower and nearer and through a shorter lens, to a seat's viewpoint inside the bowl, from where the rink turns. The 3D arena's own `lens` setting does it (a long lens from outside, about 1 from the seat, so that the eye is inside the building and the bowl has a real perspective), and the roof is dissolved with a CSS `filter: opacity()`, so that it composes with the 3D view's own cutting away of the walls. The seat is a place and a height of our own choosing (a pitch, a zoom and a lens, `seat` of `flightPlan`), said to be so, never a real seat; the bowl's words give the venue's seats and tiers.
|
|
15
|
+
- **The camera has two more channels** (`logLens`, `roof`), still one path with no jumps: the turn slows from the arena's 12 degrees a second to the rink's 8 and never stops or reverses; the lens, the roof, the zoom and the tip change in one direction only on the way in. `flightPlan` takes `seat` and `rinkTurn`; `PACE` has the seconds of each part; the path now ends on the rink turning.
|
|
16
|
+
- Tests for the way in (nothing reverses, the seat is reached where the rink stage starts, the stage stills show the roof lifted and the seat's view) over fourteen lists of stages, and in a real browser the four corners of the rink are followed on the screen at 30 frames a second, from outside and from the seat: each moves a little a frame and none falls behind the eye.
|
|
17
|
+
|
|
10
18
|
## [0.27.0] - 2026-10-20
|
|
11
19
|
|
|
12
20
|
### Added
|
package/dist/district.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export declare const DISTRICT_MARGIN: Readonly<Record<DistrictMode, number>>;
|
|
|
112
112
|
* import { planDistrict } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
113
113
|
* const plan = planDistrict({ venues: ["bc-place", "rogers-arena"] });
|
|
114
114
|
* console.log(plan.mode, plan.members.length, Math.round(plan.between[0].metres));
|
|
115
|
-
* // neighbours 2
|
|
115
|
+
* // neighbours 2 380
|
|
116
116
|
* ```
|
|
117
117
|
*/
|
|
118
118
|
export declare function planDistrict(options: DistrictOptions): DistrictPlan;
|
package/dist/district.js
CHANGED
|
@@ -97,7 +97,7 @@ const haversine = (a, b) => {
|
|
|
97
97
|
* import { planDistrict } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
98
98
|
* const plan = planDistrict({ venues: ["bc-place", "rogers-arena"] });
|
|
99
99
|
* console.log(plan.mode, plan.members.length, Math.round(plan.between[0].metres));
|
|
100
|
-
* // neighbours 2
|
|
100
|
+
* // neighbours 2 380
|
|
101
101
|
* ```
|
|
102
102
|
*/
|
|
103
103
|
export function planDistrict(options) {
|
package/dist/flythrough.dom.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
import { FLYTHROUGH_WORDS, say } from "./flythrough.words.js";
|
|
9
|
+
import { LENS } from "./turn.camera.js";
|
|
9
10
|
import { mountKyogijoTurn } from "./turn.dom.js";
|
|
10
11
|
import { arenaOfVenue, planVenue, venueById, venueOrientation, venueRink } from "./venues.js";
|
|
11
12
|
/**
|
|
@@ -43,7 +44,7 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
43
44
|
}
|
|
44
45
|
})();
|
|
45
46
|
const place = `${arena.city}, ${region}`;
|
|
46
|
-
const values = { arena: name, place };
|
|
47
|
+
const values = { arena: name, place, seats: venue.seats.hockey.toLocaleString(language), tiers: venue.tiers.count };
|
|
47
48
|
const wanted = (options.stages ?? STAGE_IDS).filter((stage, i, all) => STAGE_IDS.includes(stage) && all.indexOf(stage) === i);
|
|
48
49
|
const stageList = STAGE_IDS.filter((stage) => wanted.includes(stage));
|
|
49
50
|
if (stageList.length === 0)
|
|
@@ -68,7 +69,7 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
68
69
|
frame = { at: [0, 0], metresX: 1, metresY: 1 };
|
|
69
70
|
}
|
|
70
71
|
const mapFrameNow = frame;
|
|
71
|
-
host.innerHTML = flythroughMarkup({ id, language, theme: options.theme, arena: name, place, stages: stageList, map: mapSvg, mapCredit: chizu, controls: settings.controls });
|
|
72
|
+
host.innerHTML = flythroughMarkup({ id, language, theme: options.theme, arena: name, place, stages: stageList, map: mapSvg, mapCredit: chizu, controls: settings.controls, seats: values.seats, tiers: values.tiers });
|
|
72
73
|
const root = host.querySelector(".kf");
|
|
73
74
|
const stage = root.querySelector(".kf-stage");
|
|
74
75
|
const mapLayer = root.querySelector(".kf-map");
|
|
@@ -89,8 +90,10 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
89
90
|
// ---- The 3D arena, from above to begin with: the view the path hands over at. ----
|
|
90
91
|
const drawing = { sport: "nhl", arena: venue.id, crowd: { real: true }, language, ...(options.theme === undefined ? {} : { theme: options.theme }) };
|
|
91
92
|
let turn;
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
// 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
|
+
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"))
|
|
96
|
+
turn = mountKyogijoTurn(turnHost, drawing, turnSettings);
|
|
94
97
|
// ---- The site plan: the building's outline and the rink, in the drawing's own feet; the sheet under it is the floor's. ----
|
|
95
98
|
const plan = planVenue(venue);
|
|
96
99
|
const rink = venueRink(venue);
|
|
@@ -126,6 +129,9 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
126
129
|
if (turn !== undefined && floor !== null) {
|
|
127
130
|
// Measure the arena seen from straight above, as the 3D view lays it out in this container: where the rink stands and how big a foot is.
|
|
128
131
|
const was = turn.view();
|
|
132
|
+
const lens = turnSettings.lens;
|
|
133
|
+
// The hand-over is seen from straight above through the usual long lens, whatever the lens is now.
|
|
134
|
+
turnSettings.lens = LENS;
|
|
129
135
|
turn.set({ yaw: 0, pitch: 0, zoom: 1 }, false);
|
|
130
136
|
const box = floor.getBoundingClientRect();
|
|
131
137
|
// The stage's own area, inside its border: the layers are laid out in it.
|
|
@@ -141,6 +147,7 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
141
147
|
sheet = { x: vb.x, y: vb.y, w: vb.width, h: vb.height };
|
|
142
148
|
drawPlan();
|
|
143
149
|
}
|
|
150
|
+
turnSettings.lens = lens;
|
|
144
151
|
turn.set(was, false);
|
|
145
152
|
}
|
|
146
153
|
const startSpan = start?.span ?? 9000000;
|
|
@@ -201,9 +208,12 @@ export function mountFlythrough(host, options, settings = {}) {
|
|
|
201
208
|
put("3d", turnHost, "visibility", live3d ? "visible" : "hidden");
|
|
202
209
|
if (live3d && turn !== undefined) {
|
|
203
210
|
put("3d", turnHost, "opacity", num(cam.arena, 3));
|
|
204
|
-
|
|
211
|
+
put("3d", turnHost, "--kf-roof", num(cam.roof, 3));
|
|
212
|
+
put("3d", turnHost, "@data-roof", cam.roof > 0.004 ? "1" : "0");
|
|
213
|
+
const view = `${num(cam.yaw, 3)}|${num(cam.pitch, 3)}|${num(cam.logZoom, 4)}|${num(cam.logLens, 4)}`;
|
|
205
214
|
if (written["3d:view"] !== view) {
|
|
206
215
|
written["3d:view"] = view;
|
|
216
|
+
turnSettings.lens = Math.exp(cam.logLens);
|
|
207
217
|
turn.set({ yaw: cam.yaw, pitch: cam.pitch, zoom: Math.exp(cam.logZoom) }, false);
|
|
208
218
|
}
|
|
209
219
|
}
|
|
@@ -25,6 +25,9 @@ export interface MarkupInput {
|
|
|
25
25
|
/** The arena's name, and where it is (`Anaheim, United States`), in the page's language. */
|
|
26
26
|
arena: string;
|
|
27
27
|
place: string;
|
|
28
|
+
/** The venue's seats for ice hockey and its seating tiers, in the words of the bowl's stage (`{seats}`, `{tiers}`). */
|
|
29
|
+
seats?: number | string;
|
|
30
|
+
tiers?: number | string;
|
|
28
31
|
/** The stages flown, in order. */
|
|
29
32
|
stages: readonly StageId[];
|
|
30
33
|
/** The drawn map (already through `mapMarkup`), and whether it is chizu's (so that the credit names it). */
|
|
@@ -38,6 +41,8 @@ export interface MarkupInput {
|
|
|
38
41
|
export declare const stageWords: (language: Language, stage: StageId, values: {
|
|
39
42
|
arena: string;
|
|
40
43
|
place: string;
|
|
44
|
+
seats?: number | string;
|
|
45
|
+
tiers?: number | string;
|
|
41
46
|
}) => {
|
|
42
47
|
name: string;
|
|
43
48
|
text: string;
|
|
@@ -63,7 +63,7 @@ export function controlsMarkup(words, stages, current, playing) {
|
|
|
63
63
|
/** The whole fly-through as markup. */
|
|
64
64
|
export function flythroughMarkup(input) {
|
|
65
65
|
const words = FLYTHROUGH_WORDS[input.language];
|
|
66
|
-
const values = { arena: input.arena, place: input.place };
|
|
66
|
+
const values = { arena: input.arena, place: input.place, seats: input.seats, tiers: input.tiers };
|
|
67
67
|
const first = input.stages[0];
|
|
68
68
|
const intro = stageWords(input.language, first, values);
|
|
69
69
|
const label = say(words.title, values);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/** The stages of the fly-through, in order. */
|
|
2
|
-
export type StageId = "map" | "city" | "arena";
|
|
2
|
+
export type StageId = "map" | "city" | "arena" | "bowl" | "rink";
|
|
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"];
|
|
6
|
+
export declare const CHANNELS: readonly ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
|
|
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
|
-
* 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
|
|
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) and `roof` is how much of the building's roof shows (0 to 1).
|
|
12
13
|
*/
|
|
13
14
|
export type Camera = Record<Channel, number>;
|
|
14
15
|
/** What the path needs to know of the page and the venue. */
|
|
@@ -33,6 +34,14 @@ export interface FlightInput {
|
|
|
33
34
|
};
|
|
34
35
|
/** Degrees a second the arena turns at, once it is turning: 12 by default. */
|
|
35
36
|
turn?: number;
|
|
37
|
+
/** The seat's viewpoint the camera settles at in the bowl: the tip of the 3D view in degrees, its zoom and the eye's lens. 77, 2.8 and 1 by default. */
|
|
38
|
+
seat?: {
|
|
39
|
+
pitch: number;
|
|
40
|
+
zoom: number;
|
|
41
|
+
lens: number;
|
|
42
|
+
};
|
|
43
|
+
/** Degrees a second the rink turns at, from the seat: 8 by default. */
|
|
44
|
+
rinkTurn?: number;
|
|
36
45
|
}
|
|
37
46
|
/** A keyframe: the camera at a moment, and where it must be going where that is not what the neighbours say. */
|
|
38
47
|
export interface Keyframe {
|
|
@@ -51,11 +60,11 @@ export interface Flight {
|
|
|
51
60
|
}[];
|
|
52
61
|
/** The time of the last keyframe. */
|
|
53
62
|
end: number;
|
|
54
|
-
/** After `end`, the
|
|
63
|
+
/** After `end`, the view goes on turning at this many degrees a second (negative: the near side goes to the right), and nothing else moves: the arena's turn, or the rink's from the seat. */
|
|
55
64
|
tail: number;
|
|
56
65
|
/** The camera of each stage at rest: what a reader who prefers reduced motion is shown (one layer, nothing half-faded). */
|
|
57
66
|
rest: Readonly<Record<StageId, Camera>>;
|
|
58
|
-
input: Required<Pick<FlightInput, "north" | "siteSpan" | "startSpan" | "citySpan" | "turn">> & {
|
|
67
|
+
input: Required<Pick<FlightInput, "north" | "siteSpan" | "startSpan" | "citySpan" | "turn" | "rinkTurn" | "seat">> & {
|
|
59
68
|
anchorStart: readonly [number, number];
|
|
60
69
|
anchorEnd: readonly [number, number];
|
|
61
70
|
view: {
|
|
@@ -83,7 +92,7 @@ export declare const stagesOf: (flight: Flight) => StageId[];
|
|
|
83
92
|
export declare const turnPeriod: (flight: Flight) => number;
|
|
84
93
|
/** 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. */
|
|
85
94
|
export declare function foldTime(flight: Flight, time: number): number;
|
|
86
|
-
/** How a path is paced: the seconds of each part. */
|
|
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 a still run of the rink's turn is kept before the end. */
|
|
87
96
|
export declare const PACE: {
|
|
88
97
|
readonly dwell: 1.6;
|
|
89
98
|
readonly step: 1.25;
|
|
@@ -92,11 +101,15 @@ export declare const PACE: {
|
|
|
92
101
|
readonly rise: 0.8;
|
|
93
102
|
readonly hand: 0.7;
|
|
94
103
|
readonly tilt: 3.2;
|
|
104
|
+
readonly hold: 3.5;
|
|
105
|
+
readonly over: 2.2;
|
|
106
|
+
readonly settle: 2.2;
|
|
107
|
+
readonly stay: 2;
|
|
95
108
|
};
|
|
96
109
|
/**
|
|
97
|
-
* The path from the map to the
|
|
110
|
+
* 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
|
|
98
111
|
* 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,
|
|
99
|
-
* tips to the sport's main camera and turns.
|
|
112
|
+
* tips to the sport's main camera and turns; then the roof lifts away and the camera comes in over the stands, the eye's lens shortening as it goes, and settles at a seat's viewpoint, from where the rink turns.
|
|
100
113
|
*/
|
|
101
114
|
export declare function flightPlan(input: FlightInput): Flight;
|
|
102
115
|
/** The 3D view the arena settles in, as the sport's main camera: its yaw and pitch. */
|
package/dist/flythrough.path.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// keyframes in time, and every channel runs through them as a monotone cubic (Fritsch and Carlson), so that it is smooth where it passes a keyframe (the speed does not jump),
|
|
4
4
|
// never overshoots between two (a layer's opacity stays between 0 and 1), and is still at its first keyframe. Nothing here touches a page: the same path can be worked out in
|
|
5
5
|
// Node, tested frame by frame, and handed to the mount, which draws it.
|
|
6
|
-
import { LONG_PRESETS, presetView } from "./turn.camera.js";
|
|
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"];
|
|
8
|
+
export const STAGE_IDS = ["map", "city", "arena", "bowl", "rink"];
|
|
9
9
|
/** The channels of the camera. */
|
|
10
|
-
export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom"];
|
|
10
|
+
export const CHANNELS = ["logSpan", "ax", "ay", "north", "map", "site", "arena", "yaw", "pitch", "logZoom", "logLens", "roof"];
|
|
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. */
|
|
@@ -107,13 +107,13 @@ export function foldTime(flight, time) {
|
|
|
107
107
|
return flight.end + ((time - flight.end) % period);
|
|
108
108
|
}
|
|
109
109
|
/** 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 };
|
|
111
|
-
/** How a path is paced: the seconds of each part. */
|
|
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 };
|
|
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 a still run of the rink's turn is kept before the end. */
|
|
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 };
|
|
113
113
|
/**
|
|
114
|
-
* The path from the map to the
|
|
114
|
+
* 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
115
|
* 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,
|
|
116
|
-
* tips to the sport's main camera and turns.
|
|
116
|
+
* tips to the sport's main camera and turns; then the roof lifts away and the camera comes in over the stands, the eye's lens shortening as it goes, and settles at a seat's viewpoint, from where the rink turns.
|
|
117
117
|
*/
|
|
118
118
|
export function flightPlan(input) {
|
|
119
119
|
const startSpan = input.startSpan ?? 9000000;
|
|
@@ -122,6 +122,8 @@ export function flightPlan(input) {
|
|
|
122
122
|
const [ax1, ay1] = input.anchorEnd ?? [0, 0];
|
|
123
123
|
const north = wrap180(input.north);
|
|
124
124
|
const turn = input.turn ?? 12;
|
|
125
|
+
const rinkTurn = input.rinkTurn ?? 8;
|
|
126
|
+
const seat = input.seat ?? { pitch: 77, zoom: 2.8, lens: 1 };
|
|
125
127
|
const view = input.view ?? { yaw: LONG_PRESETS.broadcast.yaw, pitch: LONG_PRESETS.broadcast.pitch };
|
|
126
128
|
const ln = Math.log;
|
|
127
129
|
const L0 = ln(startSpan);
|
|
@@ -143,6 +145,13 @@ export function flightPlan(input) {
|
|
|
143
145
|
const start = { logSpan: L0, ax: ax0, ay: ay0, north: 0, map: 1, site: 0, arena: 0, yaw: 0, pitch: 0, logZoom: 0 };
|
|
144
146
|
const arrive = { ...fall(4), logSpan: L1, ax: ax1, ay: ay1, north, map: 0, site: 1 };
|
|
145
147
|
const near = { ...arrive, logSpan: L2 };
|
|
148
|
+
// 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
|
+
const yaws = [];
|
|
150
|
+
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]]) {
|
|
152
|
+
yaw -= ((from + to) / 2) * seconds;
|
|
153
|
+
yaws.push(yaw);
|
|
154
|
+
}
|
|
146
155
|
const blocks = [
|
|
147
156
|
{ id: "map", keys: [{ dt: 0, camera: start }, { dt: PACE.dwell, camera: start }, { dt: PACE.step, camera: fall(1) }, { dt: PACE.step, camera: fall(2) }, { dt: PACE.step, camera: fall(3) }] },
|
|
148
157
|
{ id: "city", keys: [{ dt: PACE.step, camera: arrive }, { dt: PACE.cityDwell, camera: arrive }, { dt: PACE.approach, camera: near }, { dt: PACE.rise, camera: { ...near, arena: 1 } }] },
|
|
@@ -150,7 +159,21 @@ export function flightPlan(input) {
|
|
|
150
159
|
id: "arena",
|
|
151
160
|
keys: [
|
|
152
161
|
{ dt: PACE.hand, camera: { ...near, arena: 1, site: 0, yaw: 0, pitch: 0 }, slope: { yaw: 0 } },
|
|
153
|
-
{ dt: PACE.tilt, camera: { ...near, arena: 1, site: 0, yaw:
|
|
162
|
+
{ dt: PACE.tilt, camera: { ...near, arena: 1, site: 0, yaw: yaws[0], pitch: view.pitch }, slope: { yaw: -turn } },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "bowl",
|
|
167
|
+
keys: [
|
|
168
|
+
{ dt: PACE.hold, camera: { ...near, arena: 1, site: 0, yaw: yaws[1], pitch: view.pitch }, slope: { yaw: -turn } },
|
|
169
|
+
{ dt: PACE.over, camera: { ...near, arena: 1, site: 0, yaw: yaws[2], 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.3 }, slope: { yaw: -(turn + rinkTurn) / 2 } },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "rink",
|
|
174
|
+
keys: [
|
|
175
|
+
{ 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 }, slope: { yaw: -rinkTurn } },
|
|
176
|
+
{ 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 }, slope: { yaw: -rinkTurn } },
|
|
154
177
|
],
|
|
155
178
|
},
|
|
156
179
|
];
|
|
@@ -184,14 +207,16 @@ export function flightPlan(input) {
|
|
|
184
207
|
map: { ...BASE, ...start },
|
|
185
208
|
city: { ...BASE, ...arrive },
|
|
186
209
|
arena: { ...BASE, ...near, map: 0, site: 0, arena: 1, yaw: view.yaw, pitch: view.pitch, logZoom: 0 },
|
|
210
|
+
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 },
|
|
187
212
|
};
|
|
188
213
|
return {
|
|
189
214
|
keys,
|
|
190
215
|
stages,
|
|
191
216
|
end: time,
|
|
192
|
-
tail: flown.
|
|
217
|
+
tail: flown[flown.length - 1].keys[flown[flown.length - 1].keys.length - 1].slope?.yaw ?? 0,
|
|
193
218
|
rest,
|
|
194
|
-
input: { north, siteSpan: input.siteSpan, startSpan, citySpan, turn, anchorStart: [ax0, ay0], anchorEnd: [ax1, ay1], view },
|
|
219
|
+
input: { north, siteSpan: input.siteSpan, startSpan, citySpan, turn, rinkTurn, seat, anchorStart: [ax0, ay0], anchorEnd: [ax1, ay1], view },
|
|
195
220
|
};
|
|
196
221
|
}
|
|
197
222
|
/** The 3D view the arena settles in, as the sport's main camera: its yaw and pitch. */
|
|
@@ -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-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-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
|
@@ -24,6 +24,8 @@ export const FLYTHROUGH_STYLE = `.kf{${LIGHT};color:var(--kf-ink);font:16px/1.45
|
|
|
24
24
|
.kf-3d .ky-turn{height:100%}
|
|
25
25
|
.kf-3d .ky-turn-view{aspect-ratio:auto;height:100%;min-height:0;border-radius:0;cursor:default;touch-action:auto}
|
|
26
26
|
.kf-3d .ky-turn-help,.kf-3d .ky-turn-live{display:none}
|
|
27
|
+
.kf-3d .ky-up-roof{filter:opacity(var(--kf-roof,1))}
|
|
28
|
+
.kf-3d[data-roof="0"] .ky-up-roof{visibility:hidden}
|
|
27
29
|
.kf-pin{position:absolute;left:0;top:0;width:0;height:0;pointer-events:none}
|
|
28
30
|
.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)}
|
|
29
31
|
.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}
|
|
@@ -8,7 +8,7 @@ export interface FlythroughWords {
|
|
|
8
8
|
region: string;
|
|
9
9
|
/** What a screen reader is told about the whole fly-through: `{arena}`, `{place}`. */
|
|
10
10
|
about: string;
|
|
11
|
-
/** The stages: their names and what is said of each (`{arena}`, `{place}`). */
|
|
11
|
+
/** The stages: their names and what is said of each (`{arena}`, `{place}`, `{seats}`, `{tiers}`). */
|
|
12
12
|
stage: Record<StageId, {
|
|
13
13
|
name: string;
|
|
14
14
|
text: string;
|
|
@@ -30,4 +30,4 @@ export interface FlythroughWords {
|
|
|
30
30
|
}
|
|
31
31
|
export declare const FLYTHROUGH_WORDS: Record<Language, FlythroughWords>;
|
|
32
32
|
/** A sentence with its `{name}` slots filled in. */
|
|
33
|
-
export declare const say: (text: string, values: Record<string, string | number>) => string;
|
|
33
|
+
export declare const say: (text: string, values: Record<string, string | number | undefined>) => string;
|
package/dist/flythrough.words.js
CHANGED
|
@@ -2,11 +2,13 @@ export const FLYTHROUGH_WORDS = {
|
|
|
2
2
|
en: {
|
|
3
3
|
title: "Fly-through: {arena}",
|
|
4
4
|
region: "fly-through",
|
|
5
|
-
about: "A fly-through from a map to {arena} in {place}: the map, the city
|
|
5
|
+
about: "A fly-through from a map to {arena} in {place}: the map, the city, the arena in 3D, the way in over the stands and the rink seen from a seat. Play, pause, step back or forward and choose a stage with the buttons or the keys.",
|
|
6
6
|
stage: {
|
|
7
7
|
map: { name: "Map", text: "{arena} stands in {place}." },
|
|
8
8
|
city: { name: "City", text: "The building of {arena} from above, as OpenStreetMap maps it." },
|
|
9
9
|
arena: { name: "Arena", text: "{arena} in 3D, turning." },
|
|
10
|
+
bowl: { name: "Bowl", text: "In over the stands, with the roof lifted away: a plausible bowl of {seats} seats in {tiers} tiers, not a seat map." },
|
|
11
|
+
rink: { name: "Rink", text: "A view across the ice from the stands, turning; the place and the height of the seat are ours, not a real seat's." },
|
|
10
12
|
},
|
|
11
13
|
announce: "Stage {n} of {total}: {name}. {text}",
|
|
12
14
|
play: "Play",
|
|
@@ -22,11 +24,13 @@ export const FLYTHROUGH_WORDS = {
|
|
|
22
24
|
ja: {
|
|
23
25
|
title: "フライスルー:{arena}",
|
|
24
26
|
region: "フライスルー",
|
|
25
|
-
about: "地図から{place}の{arena}まで進むフライスルーです。地図、都市、3D
|
|
27
|
+
about: "地図から{place}の{arena}まで進むフライスルーです。地図、都市、3Dのアリーナ、スタンドの上空から中へ入る場面、客席から見たリンクの順に見られます。ボタンまたはキーで、再生・一時停止、前後の場面への移動、場面の選択ができます。",
|
|
26
28
|
stage: {
|
|
27
29
|
map: { name: "地図", text: "{arena}は{place}にあります。" },
|
|
28
30
|
city: { name: "都市", text: "{arena}の建物を真上から見たところです(OpenStreetMapの記録による)。" },
|
|
29
31
|
arena: { name: "アリーナ", text: "{arena}を3Dで回転させて表示しています。" },
|
|
32
|
+
bowl: { name: "客席", text: "屋根を取り払い、スタンドの上空から中へ入ります。{seats}席・{tiers}層の推定のスタンドで、実際の座席表ではありません。" },
|
|
33
|
+
rink: { name: "リンク", text: "スタンドからリンクを見渡した眺めで、ゆっくり回転します。視点の位置と高さは仮のもので、実在の座席ではありません。" },
|
|
30
34
|
},
|
|
31
35
|
announce: "場面{n}/{total}:{name}。{text}",
|
|
32
36
|
play: "再生",
|
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.28.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.28.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|