@johnmorrisdotca/kyogijo-stadiums 0.40.2 → 0.43.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 +56 -0
- package/NOTICE.md +17 -0
- package/README.md +1 -3
- package/dist/context.d.ts +141 -0
- package/dist/context.js +290 -0
- package/dist/crowd.d.ts +9 -0
- package/dist/district-entry.d.ts +14 -0
- package/dist/district-entry.js +9 -0
- package/dist/district.d.ts +54 -1
- package/dist/district.dom.d.ts +31 -0
- package/dist/district.dom.js +52 -0
- package/dist/district.js +71 -11
- package/dist/district.turn.js +30 -6
- package/dist/gates.d.ts +56 -0
- package/dist/gates.js +95 -0
- package/dist/halls.d.ts +2 -0
- package/dist/halls.js +1 -1
- package/dist/kodokan.d.ts +28 -0
- package/dist/kodokan.data.d.ts +18 -0
- package/dist/kodokan.data.js +18 -0
- package/dist/kodokan.js +34 -0
- package/dist/osm-entry.d.ts +2 -0
- package/dist/osm-entry.js +2 -0
- package/dist/osm.context.data.d.ts +10 -0
- package/dist/osm.context.data.js +1107 -0
- package/dist/outside.d.ts +43 -0
- package/dist/outside.dom.d.ts +44 -0
- package/dist/outside.dom.js +203 -0
- package/dist/outside.js +227 -0
- package/dist/outside.paint.d.ts +57 -0
- package/dist/outside.paint.js +88 -0
- package/dist/plateau-entry.d.ts +3 -0
- package/dist/plateau-entry.js +3 -0
- package/dist/plateau.credit.d.ts +5 -0
- package/dist/plateau.credit.js +7 -0
- package/dist/plateau.data.d.ts +7 -0
- package/dist/plateau.data.js +757 -0
- package/dist/stadiums.d.ts +14 -0
- package/dist/stadiums.data.js +2 -1
- package/dist/stadiums.js +2 -0
- package/dist/strings.d.ts +3 -1
- package/dist/strings.js +4 -4
- package/dist/turn.dom.d.ts +10 -0
- package/dist/turn.dom.js +21 -0
- package/dist/turn.kit.d.ts +10 -3
- package/dist/turn.kit.js +7 -0
- package/dist/turn.markup.js +7 -2
- package/dist/turn.model.d.ts +3 -0
- package/dist/turn.style.js +14 -3
- package/dist/venue.real.d.ts +22 -1
- package/dist/venue.real.js +76 -7
- package/dist/venues.d.ts +9 -1
- package/dist/venues.data.js +2 -1
- package/dist/venues.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +56 -0
- package/dist/walk.data.d.ts +4 -0
- package/dist/walk.data.js +1295 -0
- package/dist/walk.js +120 -0
- package/package.json +17 -1
package/dist/district.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type ContextPlan, type ContextArea } from "./context.ts";
|
|
1
2
|
import { type StadiumConfigId } from "./stadiums.ts";
|
|
3
|
+
import type { OutsideOptions } from "./outside.ts";
|
|
2
4
|
import type { Pt } from "./crowd.ts";
|
|
3
5
|
import type { Language, SurfaceOptions } from "./types.ts";
|
|
4
6
|
/** How a district is fitted: `neighbours` tight round venues that stand close together, `city` at the real distances between venues that are kilometres apart. */
|
|
@@ -27,6 +29,23 @@ export interface DistrictOptions {
|
|
|
27
29
|
style?: "none";
|
|
28
30
|
/** Draw every venue's stands in 2D (true, the default) or its building only. */
|
|
29
31
|
stands?: boolean;
|
|
32
|
+
/** Mark each venue's gates on the plan (`venueGates`: a door's bar on the building's outline and a dashed stub the way a queue runs). Off when left out. */
|
|
33
|
+
gates?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The crowd outside the venues (`outside.ts`): `{ level: 25 }` puts a quarter of the seats' worth of people on the sidewalks, plazas and grounds round them, in the mounted views (`mountDistrict`, `mountDistrictTurn`). Off when left out. Asking
|
|
36
|
+
* for it at all, even at level 0, makes room round the buildings for the crowd (`OUTSIDE_ROOM`), so that the plan does not move when the level does; only a district with a venue that has a walkable mask (`OUTSIDE_VENUES`) is changed.
|
|
37
|
+
*/
|
|
38
|
+
outside?: OutsideOptions;
|
|
39
|
+
/**
|
|
40
|
+
* The buildings round the venues, as areas of data a page passes in (`TOKYO_DOME_CITY` of `@johnmorrisdotca/kyogijo-stadiums/plateau`): low in detail, flat on the floor shaded by height and, for the biggest and the tallest, standing
|
|
41
|
+
* in the 3D view. The frame is widened to take in the streets round the venues. None (the default) draws the district as it always was, and carries no data. The credit of each area goes into the description.
|
|
42
|
+
*/
|
|
43
|
+
context?: readonly ContextArea[];
|
|
44
|
+
/**
|
|
45
|
+
* The building a raised venue stands in (the Kodokan's main dojo, on the 7th floor): `xray` (the default) draws it with its walls giving way to the eye as it looks in, `solid` draws it whole, `none` leaves it out, and the dojo floats on its floor
|
|
46
|
+
* alone. Only a venue that stands in a building (`DistrictMember.shell`) is changed.
|
|
47
|
+
*/
|
|
48
|
+
building?: "xray" | "solid" | "none";
|
|
30
49
|
}
|
|
31
50
|
/** A venue of a district as the district needs it: what it is called, where it stands, how it lies, and how it is drawn. */
|
|
32
51
|
export interface DistrictMember {
|
|
@@ -51,6 +70,18 @@ export interface DistrictMember {
|
|
|
51
70
|
seats: number;
|
|
52
71
|
/** The options the venue is drawn from. */
|
|
53
72
|
options: SurfaceOptions;
|
|
73
|
+
/** How high the venue's floor stands above the ground, in feet: the Kodokan's main dojo is on the 7th floor of its building, drawn in the air on its real floor. Missing: on the ground. */
|
|
74
|
+
elevation?: number;
|
|
75
|
+
/** The building the venue stands in, when the district draws it (`building`): its outline is `ring`, and its height in feet and storeys are these. */
|
|
76
|
+
shell?: {
|
|
77
|
+
height: number;
|
|
78
|
+
storeys: number;
|
|
79
|
+
};
|
|
80
|
+
/** The credit of data this venue is made from that is not OpenStreetMap's (the Kodokan's building: Project PLATEAU). */
|
|
81
|
+
attribution?: {
|
|
82
|
+
en: string;
|
|
83
|
+
ja: string;
|
|
84
|
+
};
|
|
54
85
|
}
|
|
55
86
|
/** Every key a district can be asked for: the arenas of `VENUES` and the stadiums of `STADIUMS`. */
|
|
56
87
|
export declare const DISTRICT_IDS: readonly string[];
|
|
@@ -67,6 +98,8 @@ export declare const isDistrictId: (id: string) => boolean;
|
|
|
67
98
|
* ```
|
|
68
99
|
*/
|
|
69
100
|
export declare function districtMember(input: DistrictVenueInput, language?: Language): DistrictMember;
|
|
101
|
+
/** Turn a point of the map (east, down the page) by `degrees` clockwise on the page. */
|
|
102
|
+
export declare const turnBy: ([x, y]: Pt, degrees: number) => Pt;
|
|
70
103
|
/** A venue placed in the district: where its surface's middle is, how far its drawing is turned, and where its building stands, all in feet in the district's frame (x to the right, y down the page). */
|
|
71
104
|
export interface DistrictPlaced {
|
|
72
105
|
member: DistrictMember;
|
|
@@ -79,6 +112,18 @@ export interface DistrictPlaced {
|
|
|
79
112
|
/** Its convex hull, which the depth order of the 3D view separates buildings by. */
|
|
80
113
|
hull: Pt[];
|
|
81
114
|
}
|
|
115
|
+
/** A gate placed in a district: where it is on its venue's outline in the plan's frame, in feet, and the way it faces on the page, in degrees clockwise from the top. */
|
|
116
|
+
export interface DistrictGate {
|
|
117
|
+
key: string;
|
|
118
|
+
venue: string;
|
|
119
|
+
name: {
|
|
120
|
+
en: string;
|
|
121
|
+
ja: string;
|
|
122
|
+
};
|
|
123
|
+
at: Pt;
|
|
124
|
+
angle: number;
|
|
125
|
+
basis: "documented" | "plausible";
|
|
126
|
+
}
|
|
82
127
|
/** What a district is: its venues placed, the box that fits them, and how the map lies. */
|
|
83
128
|
export interface DistrictPlan {
|
|
84
129
|
options: DistrictOptions;
|
|
@@ -88,6 +133,10 @@ export interface DistrictPlan {
|
|
|
88
133
|
/** The way north points on the page, degrees clockwise from the top. */
|
|
89
134
|
north: number;
|
|
90
135
|
members: DistrictPlaced[];
|
|
136
|
+
/** The buildings round the venues that the options' `context` brings, placed in the frame; `undefined` without it. */
|
|
137
|
+
context: ContextPlan | undefined;
|
|
138
|
+
/** The gates of the venues, where the options ask for them: each on its building's outline in the plan's frame (feet), and the way it faces on the page. Empty otherwise. */
|
|
139
|
+
gates: DistrictGate[];
|
|
91
140
|
/** The frame the plan is drawn in, in feet: the venues' buildings with the margin the mode gives them. */
|
|
92
141
|
view: {
|
|
93
142
|
x: number;
|
|
@@ -101,9 +150,13 @@ export interface DistrictPlan {
|
|
|
101
150
|
b: string;
|
|
102
151
|
metres: number;
|
|
103
152
|
}[];
|
|
153
|
+
/** The `[longitude, latitude]` the plan's metres are measured from: the middle of the venues' surfaces. */
|
|
154
|
+
origin: readonly [number, number];
|
|
104
155
|
}
|
|
105
156
|
/** The margin round the buildings, as a share of the longer side of the district: tight for neighbours, wide for a city's grid and names. */
|
|
106
157
|
export declare const DISTRICT_MARGIN: Readonly<Record<DistrictMode, number>>;
|
|
158
|
+
/** The ground kept round the buildings for the crowd outside, in metres, when a district asks for one. */
|
|
159
|
+
export declare const OUTSIDE_ROOM = 160;
|
|
107
160
|
/**
|
|
108
161
|
* The plan of a district: each venue's surface placed where it is on the ground (the distance and the direction between middles come from the coordinates of OpenStreetMap's outlines, to a metre or so) and turned so
|
|
109
162
|
* that its long side points where it does in the world, and the frame that fits them. A venue whose orientation is not known (an arena the building gives no axis for) is placed with its rink east-west, as it is drawn alone.
|
|
@@ -117,7 +170,7 @@ export declare const DISTRICT_MARGIN: Readonly<Record<DistrictMode, number>>;
|
|
|
117
170
|
*/
|
|
118
171
|
export declare function planDistrict(options: DistrictOptions): DistrictPlan;
|
|
119
172
|
/** The colours and the type of a district's drawing, light and dark. Defaults sit in `:where()`, so a page's own rule wins. */
|
|
120
|
-
export declare const KYOGIJO_DISTRICT_STYLE = "\n:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9)}\n@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85)}}\n:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85)}\n.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}\n.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}\n.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}\n.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}";
|
|
173
|
+
export declare const KYOGIJO_DISTRICT_STYLE = "\n:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8}\n@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}}\n:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}\n.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}\n.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}\n.ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}\n.ky-district .ky-dist-b0{fill:var(--ky-dist-b0)}\n.ky-district .ky-dist-b1{fill:var(--ky-dist-b1)}\n.ky-district .ky-dist-b2{fill:var(--ky-dist-b2)}\n.ky-district .ky-dist-b3{fill:var(--ky-dist-b3)}\n.ky-district .ky-dist-street{stroke-linejoin:round;stroke-linecap:butt}\n.ky-district .ky-dist-street-0{stroke:var(--ky-dist-road)}\n.ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}\n.ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}\n.ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}\n.ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}\n.ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}\n.ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}\n.ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}\n.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}\n.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,\"Segoe UI\",\"Hiragino Sans\",\"Noto Sans JP\",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}";
|
|
121
174
|
/**
|
|
122
175
|
* A venue's drawing as a part of the district's: its root made a nested `svg` that keeps the class, the sport and the colours the drawing's stylesheet reads, its box laid in its own frame (so that the group's turn
|
|
123
176
|
* and shift put it in place), and its title, description, apron, arrow and stylesheet taken out (the stylesheet is given back in `styles`).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type DistrictOptions } from "./district.ts";
|
|
2
|
+
import type { OutsideLevel } from "./outside.paint.ts";
|
|
3
|
+
/** What `mountDistrict` gives back. */
|
|
4
|
+
export interface KyogijoDistrict {
|
|
5
|
+
readonly mode: "2d";
|
|
6
|
+
/** Draw again with other options (venues, fit, theme, level): the container's contents are replaced. */
|
|
7
|
+
update(options: DistrictOptions): void;
|
|
8
|
+
/** Show another level of the crowd outside at once, without drawing the plan again. A district with no crowd does nothing. */
|
|
9
|
+
setOutside(level: OutsideLevel): void;
|
|
10
|
+
/** What the crowd's last paint cost, in milliseconds, how many people it drew and how, or `undefined` where there is no crowd or it has not been drawn. */
|
|
11
|
+
outsideCost(): {
|
|
12
|
+
ms: number;
|
|
13
|
+
people: number;
|
|
14
|
+
mode: string;
|
|
15
|
+
pixels: number;
|
|
16
|
+
} | undefined;
|
|
17
|
+
/** The plan's SVG, or `null` after `destroy`. */
|
|
18
|
+
svg(): SVGSVGElement | null;
|
|
19
|
+
/** Empty the container and stop listening. */
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Mount a district's plan in a container, with the crowd outside over it when `options.outside` asks for one.
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts no-run
|
|
26
|
+
* import { mountDistrict } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
27
|
+
* const plan = mountDistrict(document.querySelector("#city")!, { venues: ["bc-place", "rogers-arena"], outside: { level: 50 } });
|
|
28
|
+
* plan.setOutside(100);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function mountDistrict(host: HTMLElement, options: DistrictOptions): KyogijoDistrict;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// A district's 2D plan in a page, with the crowd outside over it: `mountDistrict` writes `drawDistrict`'s SVG into a container and, where the district asks for a crowd (`DistrictOptions.outside`) and has a mask to put it on,
|
|
2
|
+
// lays one canvas over the drawing (`outside.dom.ts`). The plan is still: nothing animates, and the crowd is painted when its level or the box's size changes. Without a crowd it is `drawDistrict` written into the container.
|
|
3
|
+
import { drawDistrict, planDistrict } from "./district.js";
|
|
4
|
+
import { hasOutside, outsideCrowd, outsideLevelOf } from "./outside.js";
|
|
5
|
+
import { attachOutsidePlan } from "./outside.dom.js";
|
|
6
|
+
/**
|
|
7
|
+
* Mount a district's plan in a container, with the crowd outside over it when `options.outside` asks for one.
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts no-run
|
|
10
|
+
* import { mountDistrict } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
11
|
+
* const plan = mountDistrict(document.querySelector("#city")!, { venues: ["bc-place", "rogers-arena"], outside: { level: 50 } });
|
|
12
|
+
* plan.setOutside(100);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function mountDistrict(host, options) {
|
|
16
|
+
let layer;
|
|
17
|
+
let alive = true;
|
|
18
|
+
const draw = (next) => {
|
|
19
|
+
layer?.destroy();
|
|
20
|
+
layer = undefined;
|
|
21
|
+
const plan = planDistrict(next);
|
|
22
|
+
host.innerHTML = `<div class="ky-district-view" style="position:relative">${drawDistrict(next, plan)}</div>`;
|
|
23
|
+
const wrap = host.firstElementChild;
|
|
24
|
+
const svg = wrap.querySelector("svg.ky-district");
|
|
25
|
+
if (next.outside !== undefined && svg !== null && hasOutside(plan))
|
|
26
|
+
layer = attachOutsidePlan(wrap, svg, { level: outsideLevelOf(next), crowd: () => outsideCrowd(plan), viewBox: plan.view });
|
|
27
|
+
};
|
|
28
|
+
draw(options);
|
|
29
|
+
return {
|
|
30
|
+
mode: "2d",
|
|
31
|
+
update(next) {
|
|
32
|
+
if (alive)
|
|
33
|
+
draw(next);
|
|
34
|
+
},
|
|
35
|
+
setOutside(level) {
|
|
36
|
+
if (alive)
|
|
37
|
+
layer?.setLevel(level);
|
|
38
|
+
},
|
|
39
|
+
outsideCost() {
|
|
40
|
+
const last = layer?.last;
|
|
41
|
+
return last === undefined ? undefined : { ms: last.ms, people: last.stats.people, mode: last.stats.mode, pixels: last.pixels };
|
|
42
|
+
},
|
|
43
|
+
svg: () => (alive ? host.querySelector("svg.ky-district") : null),
|
|
44
|
+
destroy() {
|
|
45
|
+
if (!alive)
|
|
46
|
+
return;
|
|
47
|
+
alive = false;
|
|
48
|
+
layer?.destroy();
|
|
49
|
+
host.replaceChildren();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/district.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// apart, with a grid, a scale and a name beside each. Nothing is drawn but what the venues are drawn from: no street, no park and no mark of a team, a league or a tournament; the credit
|
|
5
5
|
// `VENUE_ATTRIBUTION` goes with it. Pure: options in, a plan, and markup, out; `district.turn.ts` raises it in 3D.
|
|
6
6
|
import { arenaById, arenaNameIn } from "./arenas.js";
|
|
7
|
+
import { kodokanMember } from "./kodokan.js";
|
|
8
|
+
import { contextFloor, contextFrame, placeContext, toneOf } from "./context.js";
|
|
7
9
|
import { crowdStands, drawKyogijo } from "./draw.js";
|
|
8
10
|
import { FEET_PER_METRE } from "./geometry.js";
|
|
9
11
|
import { KYOGIJO_STRINGS } from "./strings.js";
|
|
@@ -11,6 +13,8 @@ import { esc, hash } from "./format.js";
|
|
|
11
13
|
import { STADIUM_IDS, stadiumById, stadiumConfigs, stadiumFrameHeading, stadiumKind, stadiumNames, stadiumSeats } from "./stadiums.js";
|
|
12
14
|
import { VENUE_ATTRIBUTION, VENUE_IDS, venueById } from "./venues.js";
|
|
13
15
|
import { hullOf, metresFrom, middleOf, smallestBox } from "./venues.geo.js";
|
|
16
|
+
import { venueGates } from "./gates.js";
|
|
17
|
+
import { walkIdFor } from "./walk.js";
|
|
14
18
|
export const DISTRICT_MODES = ["neighbours", "city"];
|
|
15
19
|
const venueMember = (venue, language) => {
|
|
16
20
|
const arena = arenaById(venue.id);
|
|
@@ -46,7 +50,7 @@ const stadiumMember = (stadium, config, language) => {
|
|
|
46
50
|
};
|
|
47
51
|
};
|
|
48
52
|
/** Every key a district can be asked for: the arenas of `VENUES` and the stadiums of `STADIUMS`. */
|
|
49
|
-
export const DISTRICT_IDS = Object.freeze([...VENUE_IDS, ...STADIUM_IDS]);
|
|
53
|
+
export const DISTRICT_IDS = Object.freeze([...VENUE_IDS, ...STADIUM_IDS, "kodokan"]);
|
|
50
54
|
/** Whether a key is a venue a district can place. */
|
|
51
55
|
export const isDistrictId = (id) => DISTRICT_IDS.includes(id);
|
|
52
56
|
/**
|
|
@@ -74,14 +78,22 @@ export function districtMember(input, language = "en") {
|
|
|
74
78
|
throw new Error(`${id} has no configuration ${String(named)}: ${stadiumConfigs(stadium).join(", ")}`);
|
|
75
79
|
return stadiumMember(stadium, config, language);
|
|
76
80
|
}
|
|
81
|
+
// The Kodokan: a hall of the budō, its main dojo in the air in its building (`kodokan.ts`).
|
|
82
|
+
if (id === "kodokan") {
|
|
83
|
+
if (named !== undefined)
|
|
84
|
+
throw new Error(`kodokan has one configuration, not ${named}`);
|
|
85
|
+
return kodokanMember(language);
|
|
86
|
+
}
|
|
77
87
|
throw new Error(`no venue ${String(id)}: the venues are ${DISTRICT_IDS.join(", ")}`);
|
|
78
88
|
}
|
|
79
89
|
// ---- The plan. ----
|
|
80
90
|
const RAD = Math.PI / 180;
|
|
81
91
|
/** Turn a point of the map (east, down the page) by `degrees` clockwise on the page. */
|
|
82
|
-
const turnBy = ([x, y], degrees) => [x * Math.cos(degrees * RAD) - y * Math.sin(degrees * RAD), x * Math.sin(degrees * RAD) + y * Math.cos(degrees * RAD)];
|
|
92
|
+
export const turnBy = ([x, y], degrees) => [x * Math.cos(degrees * RAD) - y * Math.sin(degrees * RAD), x * Math.sin(degrees * RAD) + y * Math.cos(degrees * RAD)];
|
|
83
93
|
/** The margin round the buildings, as a share of the longer side of the district: tight for neighbours, wide for a city's grid and names. */
|
|
84
94
|
export const DISTRICT_MARGIN = { neighbours: 0.09, city: 0.12 };
|
|
95
|
+
/** The ground kept round the buildings for the crowd outside, in metres, when a district asks for one. */
|
|
96
|
+
export const OUTSIDE_ROOM = 160;
|
|
85
97
|
const EARTH = 6371008.8;
|
|
86
98
|
const haversine = (a, b) => {
|
|
87
99
|
const [p, q] = [a[1] * RAD, b[1] * RAD];
|
|
@@ -131,20 +143,43 @@ export function planDistrict(options) {
|
|
|
131
143
|
const xs = placed.flatMap((p) => p.footprint.map((q) => q[0]));
|
|
132
144
|
const ys = placed.flatMap((p) => p.footprint.map((q) => q[1]));
|
|
133
145
|
const [x0, x1, y0, y1] = [Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys)];
|
|
134
|
-
const
|
|
135
|
-
|
|
146
|
+
const context = options.context !== undefined && options.context.length > 0;
|
|
147
|
+
// A district that asks for a crowd outside (and has a mask to put it on) keeps room round its buildings for it, whatever the level, so that the plan stays where it is as the level changes.
|
|
148
|
+
const room = options.outside !== undefined && walkIdFor(members.map((m) => m.id)) !== undefined ? OUTSIDE_ROOM * FEET_PER_METRE : 0;
|
|
149
|
+
const margin = Math.max(Math.max(x1 - x0, y1 - y0) * DISTRICT_MARGIN[mode], room);
|
|
150
|
+
// With context the frame takes in the streets round the buildings (and any landmark near them) as well.
|
|
151
|
+
const round = { x0: x0 - margin, y0: y0 - margin, x1: x1 + margin, y1: y1 + margin };
|
|
152
|
+
const wide = context ? contextFrame(options.context, origin, turn, { x0, y0, x1, y1 }) : round;
|
|
153
|
+
const framed = { x0: Math.min(round.x0, wide.x0), y0: Math.min(round.y0, wide.y0), x1: Math.max(round.x1, wide.x1), y1: Math.max(round.y1, wide.y1) };
|
|
154
|
+
const view = { x: framed.x0, y: framed.y0, w: framed.x1 - framed.x0, h: framed.y1 - framed.y0 };
|
|
136
155
|
const between = [];
|
|
137
156
|
members.forEach((a, i) => members.slice(i + 1).forEach((b) => between.push({ a: a.key, b: b.key, metres: Math.round(haversine(a.centre, b.centre) * 10) / 10 })));
|
|
138
|
-
|
|
157
|
+
const around = context ? placeContext(options.context, origin, turn, view, placed.map((p) => ({ footprint: p.footprint, shell: p.member.shell !== undefined }))) : undefined;
|
|
158
|
+
const gates = options.gates !== true ? [] : placed.flatMap((p) => venueGates(p.member.id).map((g) => ({ key: g.key, venue: p.member.key, name: g.name, at: turnBy(ground([g.at])[0], turn), angle: ((g.bearing + turn) % 360 + 360) % 360, basis: g.basis })));
|
|
159
|
+
return { options, mode, turn, north: turn, members: placed, origin, context: around, gates, view, between };
|
|
139
160
|
}
|
|
140
161
|
// ---- The drawing. ----
|
|
141
162
|
/** The colours and the type of a district's drawing, light and dark. Defaults sit in `:where()`, so a page's own rule wins. */
|
|
142
163
|
export const KYOGIJO_DISTRICT_STYLE = `
|
|
143
|
-
:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9)}
|
|
144
|
-
@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85)}}
|
|
145
|
-
:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85)}
|
|
164
|
+
:where(.ky-district){--ky-dist-ground:#e1e4da;--ky-dist-grid:rgba(40,46,42,.16);--ky-dist-ink:#1f2320;--ky-dist-halo:rgba(255,255,255,.9);--ky-dist-people:#c2410c;--ky-dist-b0:#d1d4cb;--ky-dist-b1:#c3c7be;--ky-dist-b2:#b2b7ae;--ky-dist-b3:#9ea49b;--ky-dist-bline:rgba(40,46,42,.28);--ky-dist-road:#cbcfc4;--ky-dist-deck:#bdc2b7;--ky-dist-walk:#eef0e8;--ky-dist-rail:#8b9288;--ky-dist-gate:#1f5fa8}
|
|
165
|
+
@media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-district:not([data-theme=light])){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}}
|
|
166
|
+
:where(html[data-theme=dark] .ky-district,.ky-district[data-theme=dark]){--ky-dist-ground:#1b2025;--ky-dist-grid:rgba(220,226,220,.16);--ky-dist-ink:#e8ece8;--ky-dist-halo:rgba(14,18,16,.85);--ky-dist-people:#fbbf24;--ky-dist-b0:#262c31;--ky-dist-b1:#2e353b;--ky-dist-b2:#394148;--ky-dist-b3:#474f57;--ky-dist-bline:rgba(220,226,220,.22);--ky-dist-road:#2b3239;--ky-dist-deck:#353d45;--ky-dist-walk:#242b31;--ky-dist-rail:#5b646c;--ky-dist-gate:#7fb2ee}
|
|
146
167
|
.ky-district .ky-dist-ground{fill:var(--ky-dist-ground)}
|
|
147
168
|
.ky-district .ky-dist-grid{fill:none;stroke:var(--ky-dist-grid);stroke-width:1}
|
|
169
|
+
.ky-district .ky-dist-block{stroke:var(--ky-dist-bline);stroke-width:.5;stroke-linejoin:round}
|
|
170
|
+
.ky-district .ky-dist-b0{fill:var(--ky-dist-b0)}
|
|
171
|
+
.ky-district .ky-dist-b1{fill:var(--ky-dist-b1)}
|
|
172
|
+
.ky-district .ky-dist-b2{fill:var(--ky-dist-b2)}
|
|
173
|
+
.ky-district .ky-dist-b3{fill:var(--ky-dist-b3)}
|
|
174
|
+
.ky-district .ky-dist-street{stroke-linejoin:round;stroke-linecap:butt}
|
|
175
|
+
.ky-district .ky-dist-street-0{stroke:var(--ky-dist-road)}
|
|
176
|
+
.ky-district .ky-dist-street-1{stroke:var(--ky-dist-deck)}
|
|
177
|
+
.ky-district .ky-dist-street-2{stroke:var(--ky-dist-rail);stroke-dasharray:6 3}
|
|
178
|
+
.ky-district .ky-dist-street-3{stroke:var(--ky-dist-walk)}
|
|
179
|
+
.ky-district .ky-dist-gate rect{fill:var(--ky-dist-gate);stroke:var(--ky-dist-gate);stroke-width:2.4}
|
|
180
|
+
.ky-district .ky-dist-gate-plausible rect{fill:var(--ky-dist-ground);fill-opacity:.85}
|
|
181
|
+
.ky-district .ky-dist-gate-queue{fill:none;stroke:var(--ky-dist-gate);stroke-width:3;stroke-dasharray:9 7}
|
|
182
|
+
.ky-district .ky-dist-ghost{fill:var(--ky-dist-b1);fill-opacity:.35}
|
|
148
183
|
.ky-district .ky-dist-ink{fill:none;stroke:var(--ky-dist-ink);stroke-linecap:round;stroke-linejoin:round}
|
|
149
184
|
.ky-district .ky-dist-text{fill:var(--ky-dist-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);font-weight:600;paint-order:stroke;stroke:var(--ky-dist-halo);stroke-linejoin:round}`;
|
|
150
185
|
const ROOT = /^<svg([^>]*)>/;
|
|
@@ -191,7 +226,7 @@ export function scaleOf(view) {
|
|
|
191
226
|
/** The distance a scale says, as it is read: metres under a kilometre, else kilometres. */
|
|
192
227
|
export const metresText = (metres) => (metres >= 1000 ? `${metres / 1000} km` : `${metres} m`);
|
|
193
228
|
/** The id of a district's drawing: the option's, or a hash of the options. */
|
|
194
|
-
export const districtId = (options) => (typeof options.id === "string" && options.id !== "" ? options.id : `ky-district-${hash(JSON.stringify([options.venues, options.mode, options.turn, options.language, options.theme, options.stands]))}`);
|
|
229
|
+
export const districtId = (options) => (typeof options.id === "string" && options.id !== "" ? options.id : `ky-district-${hash(JSON.stringify([options.venues, options.mode, options.turn, options.language, options.theme, options.stands, ...(options.context === undefined || options.context.length === 0 ? [] : [options.context.map((a) => a.key)]), ...(options.building === undefined ? [] : [options.building])]))}`);
|
|
195
230
|
/** The title and the description of a district, in its language: what is drawn, where each venue stands from the first, how far apart they are, and the credit. */
|
|
196
231
|
export function describeDistrict(options, plan = planDistrict(options)) {
|
|
197
232
|
const language = options.language === "ja" ? "ja" : "en";
|
|
@@ -204,7 +239,12 @@ export function describeDistrict(options, plan = planDistrict(options)) {
|
|
|
204
239
|
const nameOf = (key) => plan.members.find((p) => p.member.key === key).member.name[language];
|
|
205
240
|
const apart = first === undefined ? "" : language === "ja" ? `${gap}${nameOf(first.a)}と${nameOf(first.b)}の中心は約${Math.round(first.metres)}m離れています。` : `${gap}The middles of ${nameOf(first.a)} and ${nameOf(first.b)} are about ${Math.round(first.metres)} metres apart.`;
|
|
206
241
|
const text = fillWords(words.district, { count: String(plan.members.length), mode: words[plan.mode === "city" ? "districtCity" : "districtNeighbours"] });
|
|
207
|
-
|
|
242
|
+
const level = options.outside?.level ?? 0;
|
|
243
|
+
const standing = level > 0 ? Math.round(plan.members.filter((p) => walkIdFor([p.member.id]) !== undefined).reduce((sum, p) => sum + p.member.seats, 0) * level / 100) : 0;
|
|
244
|
+
const crowd = standing > 0 ? `${gap}${fillWords(words.districtOutside, { percent: String(level), people: String(standing).replace(/\B(?=(\d{3})+(?!\d))/g, ",") })}` : "";
|
|
245
|
+
const credits = [...new Set([...(plan.context?.attribution[language] ?? []), ...plan.members.flatMap((p) => (p.member.attribution === undefined ? [] : [p.member.attribution[language]]))])];
|
|
246
|
+
const credit = credits.length === 0 ? "" : `${gap}${credits.join(language === "ja" ? "。" : "; ")}${language === "ja" ? "。" : "."}`;
|
|
247
|
+
return { title, description: `${text}${apart}${crowd}${gap}${words.districtCredit}${gap}${VENUE_ATTRIBUTION}${language === "ja" ? "。" : "."}${credit}` };
|
|
208
248
|
}
|
|
209
249
|
const fillWords = (text, values) => text.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? "");
|
|
210
250
|
/** How a venue is drawn for a district: its own options with the district's language, theme and ids, in 2D with its stands or as the floor of the 3D view (its building only). */
|
|
@@ -253,6 +293,18 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
|
|
|
253
293
|
parts.push(`<rect class="ky-dist-ground" x="${num(view.x)}" y="${num(view.y)}" width="${num(view.w)}" height="${num(view.h)}"/>`);
|
|
254
294
|
parts.push(`<clipPath id="${id}-clip"><rect x="${num(view.x)}" y="${num(view.y)}" width="${num(view.w)}" height="${num(view.h)}"/></clipPath>`);
|
|
255
295
|
parts.push(`<path class="ky-dist-grid" clip-path="url(#${id}-clip)" d="${grid.join("")}"/>`);
|
|
296
|
+
if (plan.context !== undefined)
|
|
297
|
+
parts.push(`<g clip-path="url(#${id}-clip)">${contextFloor(plan.context)}</g>`);
|
|
298
|
+
// The building a raised venue stands in, flat under it: pale as a ghost where it is see-through, in its tone of height where it is solid.
|
|
299
|
+
if (options.building !== "none") {
|
|
300
|
+
for (const placed of plan.members) {
|
|
301
|
+
const shell = placed.member.shell;
|
|
302
|
+
if (shell === undefined)
|
|
303
|
+
continue;
|
|
304
|
+
const tone = options.building === "solid" ? `ky-dist-b${toneOf(shell.height)}` : "ky-dist-ghost";
|
|
305
|
+
parts.push(`<path class="ky-dist-block ${tone}" data-shell="${esc(placed.member.key)}" clip-path="url(#${id}-clip)" d="M${placed.footprint.map(([x, y]) => `${num(x)} ${num(y)}`).join("L")}Z"/>`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
256
308
|
const styles = [];
|
|
257
309
|
plan.members.forEach((placed, i) => {
|
|
258
310
|
const svg = drawing === undefined ? drawMember(placed.member, options, i, id) : drawing(placed.member, i);
|
|
@@ -260,9 +312,17 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
|
|
|
260
312
|
for (const block of own)
|
|
261
313
|
if (!styles.includes(block))
|
|
262
314
|
styles.push(block);
|
|
315
|
+
// The 3D view draws a raised floor in the air itself: the ground under it is left bare.
|
|
316
|
+
if (drawing !== undefined && (placed.member.elevation ?? 0) > 0)
|
|
317
|
+
return;
|
|
263
318
|
const centre = surfaceCentre(placed.member.options);
|
|
264
319
|
parts.push(`<g class="ky-dist-venue" data-venue="${esc(placed.member.key)}" transform="translate(${num(placed.at[0])} ${num(placed.at[1])}) rotate(${num(placed.angle)}) translate(${num(-centre[0])} ${num(-centre[1])})">${markup}</g>`);
|
|
265
320
|
});
|
|
321
|
+
if (plan.gates.length > 0) {
|
|
322
|
+
// A door's bar on the outline, and a dashed stub out from it the way a queue would run; a gate a source names is filled, one that is ours is drawn hollow.
|
|
323
|
+
const bars = plan.gates.map((g) => `<g class="ky-dist-gate ky-dist-gate-${g.basis}" data-gate="${esc(g.key)}" transform="translate(${num(g.at[0])} ${num(g.at[1])}) rotate(${num(g.angle)})"><path class="ky-dist-gate-queue" d="M0 -9V-90"/><rect x="-24" y="-5" width="48" height="10" rx="3.5"/></g>`);
|
|
324
|
+
parts.push(`<g class="ky-dist-gates" aria-hidden="true">${bars.join("")}</g>`);
|
|
325
|
+
}
|
|
266
326
|
if (labels) {
|
|
267
327
|
const size = diag * (plan.mode === "city" ? 0.018 : 0.026);
|
|
268
328
|
plan.members.forEach((placed) => {
|
|
@@ -286,7 +346,7 @@ export function drawDistrict(options, plan = planDistrict(options), drawing) {
|
|
|
286
346
|
parts.push(`<g class="ky-dist-scale" aria-hidden="true"><path class="ky-dist-ink" stroke-width="${num(diag * 0.004)}" d="M${num(sx)} ${num(sy - diag * 0.01)}V${num(sy)}H${num(sx + bar)}V${num(sy - diag * 0.01)}"/><text class="ky-dist-text" x="${num(sx + bar / 2)}" y="${num(sy - diag * 0.018)}" text-anchor="middle" font-size="${num(diag * 0.022)}" stroke-width="${num(diag * 0.005)}">${metresText(metres)}</text></g>`);
|
|
287
347
|
const sheet = options.style === "none" ? "" : `<style>${KYOGIJO_DISTRICT_STYLE}</style>${styles.join("")}`;
|
|
288
348
|
const theme = options.theme === "light" || options.theme === "dark" ? ` data-theme="${options.theme}"` : "";
|
|
289
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" class="kyogijo ky-district" viewBox="${num(view.x)} ${num(view.y)} ${num(view.w)} ${num(view.h)}" role="img" aria-labelledby="${id}-t ${id}-d" data-sport="district" data-mode="${plan.mode}"${theme} lang="${language}"><title id="${id}-t">${esc(title)}</title><desc id="${id}-d">${esc(description)}</desc>${sheet}${parts.join("")}</svg>`;
|
|
349
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" class="kyogijo ky-district" viewBox="${num(view.x)} ${num(view.y)} ${num(view.w)} ${num(view.h)}" role="img" aria-labelledby="${id}-t ${id}-d" data-sport="district" data-mode="${plan.mode}"${(options.outside?.level ?? 0) > 0 ? ` data-outside="${options.outside?.level}"` : ""}${theme} lang="${language}"><title id="${id}-t">${esc(title)}</title><desc id="${id}-d">${esc(description)}</desc>${sheet}${parts.join("")}</svg>`;
|
|
290
350
|
}
|
|
291
351
|
/** The venues a district was asked for that the package has no key for: none when it can be drawn. */
|
|
292
352
|
export const unknownVenues = (venues) => venues.map((v) => (typeof v === "string" ? v.split(":")[0] : v.id)).filter((id) => !isDistrictId(id));
|
package/dist/district.turn.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// (`buildTurn`: the stands, the walls and the roof, the goals and posts of its field), its planes are turned about the middle of its surface and moved to where the plan puts it, and the floor is the district's flat
|
|
3
3
|
// plan drawn with each venue's building only. The model it makes is a `TurnModel`, so the camera, the controls, the pointer, the auto-turn and the steady fit of every other 3D view are this one's; its
|
|
4
4
|
// buildings are stacked building by building (`Plane.group`), the far one first, so that a roof never shows through a nearer building.
|
|
5
|
-
import { describeDistrict, districtId, drawDistrict, drawMember, planDistrict, surfaceCentre } from "./district.js";
|
|
6
|
-
import {
|
|
5
|
+
import { describeDistrict, districtId, drawDistrict, drawMember, nestedDrawing, planDistrict, surfaceCentre } from "./district.js";
|
|
6
|
+
import { blockPlanes, contextUprights, shellBlock } from "./context.js";
|
|
7
|
+
import { APART, n, plane } from "./turn.kit.js";
|
|
7
8
|
import { TURN_PRESETS, wrap } from "./turn.camera.js";
|
|
8
9
|
import { mountTurn } from "./turn.dom.js";
|
|
9
10
|
import { turnMarkupOf } from "./turn.markup.js";
|
|
@@ -11,6 +12,7 @@ import { buildTurn, lightingOf, openingView, sceneBox, WORLD_PX } from "./turn.m
|
|
|
11
12
|
import { planesOf } from "./uprights.js";
|
|
12
13
|
import { FEET_PER_METRE } from "./geometry.js";
|
|
13
14
|
import { hullOf } from "./venues.geo.js";
|
|
15
|
+
import { hasOutside, outsideCrowd, outsideLevelOf } from "./outside.js";
|
|
14
16
|
/** The four viewpoints of a district, turned with the map: the main camera looks from the south over the district (north at the top of the page), the end-on view from the west and the low corner from the south-west. */
|
|
15
17
|
export function districtPresets(plan) {
|
|
16
18
|
const turn = plan.turn;
|
|
@@ -27,7 +29,7 @@ const rot = ([x, y], degrees) => {
|
|
|
27
29
|
return [x * Math.cos(a) - y * Math.sin(a), x * Math.sin(a) + y * Math.cos(a)];
|
|
28
30
|
};
|
|
29
31
|
/** A plane of a venue's own frame (its surface's middle at `centre`) put in the district's: turned by the venue's angle about that middle and moved to where the plan puts it. */
|
|
30
|
-
function placeInDistrict(p, placed, centre, prefix, group) {
|
|
32
|
+
function placeInDistrict(p, placed, centre, prefix, group, lift = 0) {
|
|
31
33
|
const at = (x, y) => {
|
|
32
34
|
const [rx, ry] = rot([x - centre[0], y - centre[1]], placed.angle);
|
|
33
35
|
return [rx + placed.at[0], ry + placed.at[1]];
|
|
@@ -36,7 +38,7 @@ function placeInDistrict(p, placed, centre, prefix, group) {
|
|
|
36
38
|
const [ox, oy] = at(p.o[0], p.o[1]);
|
|
37
39
|
const [ux, uy] = dir(p.u[0], p.u[1]);
|
|
38
40
|
const [vx, vy] = dir(p.v[0], p.v[1]);
|
|
39
|
-
const moved = { ...p, id: `${prefix}${p.id}`, o: [ox, oy, p.o[2]], u: [ux, uy, p.u[2]], v: [vx, vy, p.v[2]], group };
|
|
41
|
+
const moved = { ...p, id: `${prefix}${p.id}`, o: [ox, oy, p.o[2] + lift], u: [ux, uy, p.u[2]], v: [vx, vy, p.v[2]], group };
|
|
40
42
|
if (p.shy !== undefined)
|
|
41
43
|
moved.shy = dir(p.shy[0], p.shy[1]);
|
|
42
44
|
if (p.crop === true) {
|
|
@@ -77,19 +79,39 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
77
79
|
const model = buildTurn({ ...member.options, language, ...(options.theme === undefined ? {} : { theme: options.theme }), id: `${base}-m${i}` }, { ...settings, id: memberId, venue: { roof: true, ...(typeof settings.venue === "object" ? settings.venue : {}) }, preset: undefined, view: undefined });
|
|
78
80
|
const centre = surfaceCentre(member.options);
|
|
79
81
|
const prefix = `m${i}-`;
|
|
82
|
+
// A venue on an upper floor (the Kodokan's main dojo) stands that high over the ground, on a floor drawn in the air; the ground under it is left bare.
|
|
83
|
+
const lift = placed.member.elevation ?? 0;
|
|
80
84
|
for (const item of model.uprights.items) {
|
|
81
|
-
const planes = item.planes.filter((p) => p.kind !== "ground").map((p) => placeInDistrict(p, placed, centre, prefix, i));
|
|
85
|
+
const planes = item.planes.filter((p) => p.kind !== "ground").map((p) => placeInDistrict(p, placed, centre, prefix, i, lift));
|
|
82
86
|
if (planes.length > 0) {
|
|
83
87
|
const [ax, ay] = item.at;
|
|
84
88
|
const [rx, ry] = rot([ax - centre[0], ay - centre[1]], placed.angle);
|
|
85
|
-
items.push({ ...item, id: `${prefix}${item.id}`, at: [rx + placed.at[0], ry + placed.at[1]], planes });
|
|
89
|
+
items.push({ ...item, id: `${prefix}${item.id}`, at: [rx + placed.at[0], ry + placed.at[1]], height: item.height + lift, planes });
|
|
86
90
|
}
|
|
87
91
|
}
|
|
92
|
+
if (lift > 0) {
|
|
93
|
+
const { markup, box, styles } = nestedDrawing(drawMember(member, { ...options, id: base }, i, base, { floor: true }));
|
|
94
|
+
for (const block of styles)
|
|
95
|
+
if (!defs.includes(block))
|
|
96
|
+
defs.push(block);
|
|
97
|
+
const floor = plane("floor", "floor", [box.x, box.y, 0], [box.w, 0, 0], [0, box.h, 0], `<g transform="translate(${n(-box.x)} ${n(-box.y)})">${markup}</g>`);
|
|
98
|
+
items.push({ id: `${prefix}floor`, kind: "deck", at: placed.at, height: lift, planes: [placeInDistrict(floor, placed, centre, prefix, i, lift)] });
|
|
99
|
+
}
|
|
100
|
+
// The building it stands in: see-through (its walls give way to the eye) or solid, in the venue's group so that it is stacked with it.
|
|
101
|
+
if (member.shell !== undefined && options.building !== "none") {
|
|
102
|
+
const block = shellBlock(member.key, placed.footprint, member.shell.height, options.building === "solid" ? undefined : placed.at);
|
|
103
|
+
const planes = blockPlanes(block, `${prefix}shell-`).map((p) => ({ ...p, group: i }));
|
|
104
|
+
items.push({ id: `${prefix}shell`, kind: "blocks", at: placed.at, height: member.shell.height, planes });
|
|
105
|
+
}
|
|
88
106
|
if (model.defs !== "")
|
|
89
107
|
defs.push(model.defs);
|
|
90
108
|
if (crowdStyle === "" && model.crowdStyle !== "")
|
|
91
109
|
crowdStyle = model.crowdStyle;
|
|
92
110
|
});
|
|
111
|
+
// The buildings round the venues stand apart from them: each block's planes are of no venue's group.
|
|
112
|
+
if (plan.context !== undefined)
|
|
113
|
+
for (const item of contextUprights(plan.context))
|
|
114
|
+
items.push({ ...item, planes: item.planes.map((p) => ({ ...p, group: APART })) });
|
|
93
115
|
if (labels) {
|
|
94
116
|
const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.016 : 0.022);
|
|
95
117
|
plan.members.forEach((placed, i) => {
|
|
@@ -136,6 +158,8 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
136
158
|
lens: 12,
|
|
137
159
|
flat: drawDistrict(options, plan),
|
|
138
160
|
groups: plan.members.map((p) => ({ hull: hullOf(p.footprint) })),
|
|
161
|
+
// The crowd outside, when the district asks for one and has a mask to put it on: the mount paints it on a canvas on the floor.
|
|
162
|
+
...(options.outside !== undefined && hasOutside(plan) ? { outside: { level: outsideLevelOf(options), crowd: () => outsideCrowd(plan), viewBox: { x: view.x, y: view.y, w: view.w, h: view.h } } } : {}),
|
|
139
163
|
};
|
|
140
164
|
}
|
|
141
165
|
/**
|
package/dist/gates.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type LonLat } from "./venues.geo.ts";
|
|
2
|
+
/** How a gate is known: `documented` is what a source cited with it says; `plausible` is ours. */
|
|
3
|
+
export type GateBasis = "documented" | "plausible";
|
|
4
|
+
/** What is said of a venue's gate: the way it faces, and what is read from where. */
|
|
5
|
+
export interface GateFact {
|
|
6
|
+
/** A key that is the same on every run: `south`, `south-west-corner`. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** The bearing of the gate's outward normal, degrees clockwise from north, 0 to under 360. */
|
|
9
|
+
bearing: number;
|
|
10
|
+
/** The name where a source gives one (`Gate G`); a plain description of where it is otherwise. */
|
|
11
|
+
name?: {
|
|
12
|
+
en: string;
|
|
13
|
+
ja: string;
|
|
14
|
+
};
|
|
15
|
+
basis: GateBasis;
|
|
16
|
+
read: string;
|
|
17
|
+
}
|
|
18
|
+
/** A gate, placed on a venue's outline. */
|
|
19
|
+
export interface Gate {
|
|
20
|
+
/** `bc-place:south`: the venue's id and the gate's. */
|
|
21
|
+
key: string;
|
|
22
|
+
venue: string;
|
|
23
|
+
id: string;
|
|
24
|
+
name: {
|
|
25
|
+
en: string;
|
|
26
|
+
ja: string;
|
|
27
|
+
};
|
|
28
|
+
/** Where it stands on the outline, `[longitude, latitude]`. */
|
|
29
|
+
at: LonLat;
|
|
30
|
+
/** The bearing of its outward normal, degrees clockwise from north: the way a queue at it runs. */
|
|
31
|
+
bearing: number;
|
|
32
|
+
basis: GateBasis;
|
|
33
|
+
read: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The facts of each venue's gates. BC Place: the English Wikipedia article (revision 1372065649) says a tear in the old roof was "close to Gate G on the south side where the roof meets the top of the concrete bowl", so there is a
|
|
37
|
+
* Gate G on the south side (documented); the other seven are ours, one to each side of the building, on the street sides the brief names (Pacific Boulevard to the south, Robson and Beatty Streets). Rogers Arena: the brief's corner
|
|
38
|
+
* entrance plazas on the Pat Quinn Way side, the south front and the two south corners, and one at the north-west, all ours.
|
|
39
|
+
*/
|
|
40
|
+
export declare const GATE_FACTS: Readonly<Record<string, readonly GateFact[]>>;
|
|
41
|
+
/** The ids of the venues that have gates. */
|
|
42
|
+
export declare const GATE_VENUES: readonly string[];
|
|
43
|
+
/** The name of a gate in a language: the source's where there is one, else its side (`South-west gate`). */
|
|
44
|
+
export declare function gateName(fact: GateFact, language: "en" | "ja"): string;
|
|
45
|
+
/**
|
|
46
|
+
* The gates of a venue (`bc-place`, `rogers-arena`), each on the building's outline where a ray from the middle of the surface at its bearing meets it; empty for a venue with none. A gate whose ray misses the outline is left out
|
|
47
|
+
* (none does for the venues here: a test holds it).
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { venueGates } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
51
|
+
* const gates = venueGates("bc-place");
|
|
52
|
+
* console.log(gates.length, gates.filter((g) => g.basis === "documented").map((g) => g.name.en).join(", "));
|
|
53
|
+
* // 8 Gate G
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function venueGates(id: string): Gate[];
|
package/dist/gates.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The gates of a real venue: where the crowd goes in. A gate is a point on the building's outline (OpenStreetMap's, ODbL 1.0) with the way it faces (the bearing of its outward normal, degrees clockwise from north), a name in English
|
|
2
|
+
// and Japanese, and how it is known: `documented` where a source cited with it says it, `plausible` where it is ours (a gate on each street side, put where a ray from the middle of the surface at the bearing meets the outline).
|
|
3
|
+
// The data is the gates' list of facts (`GATE_FACTS`, with what each cites); the positions are worked out from the outline, so a venue whose outline changes keeps its gates on it. This is the data the crowd's timelapse will use
|
|
4
|
+
// for queues and entrances: where people stand in a line, which way the line runs (away from the gate along its bearing) and where the walkable ground starts. Pure: ids in, gates out.
|
|
5
|
+
import { stadiumById } from "./stadiums.js";
|
|
6
|
+
import { venueById } from "./venues.js";
|
|
7
|
+
import { lonLatFrom, metresFrom } from "./venues.geo.js";
|
|
8
|
+
const WIND = [
|
|
9
|
+
{ en: "north", ja: "北" },
|
|
10
|
+
{ en: "north-east", ja: "北東" },
|
|
11
|
+
{ en: "east", ja: "東" },
|
|
12
|
+
{ en: "south-east", ja: "南東" },
|
|
13
|
+
{ en: "south", ja: "南" },
|
|
14
|
+
{ en: "south-west", ja: "南西" },
|
|
15
|
+
{ en: "west", ja: "西" },
|
|
16
|
+
{ en: "north-west", ja: "北西" },
|
|
17
|
+
];
|
|
18
|
+
const ours = (reason) => `ours: ${reason}`;
|
|
19
|
+
/** A gate on each of the eight sides of a building: ours, the sources say that a stadium has gates and not where each is. */
|
|
20
|
+
const SIDES = (venue) => WIND.map((_, i) => ({ id: WIND[i].en, bearing: i * 45, basis: "plausible", read: ours(`one gate on each of the eight sides of ${venue}'s building: the sources say that it has gates round the bowl and not where each is`) }));
|
|
21
|
+
/**
|
|
22
|
+
* The facts of each venue's gates. BC Place: the English Wikipedia article (revision 1372065649) says a tear in the old roof was "close to Gate G on the south side where the roof meets the top of the concrete bowl", so there is a
|
|
23
|
+
* Gate G on the south side (documented); the other seven are ours, one to each side of the building, on the street sides the brief names (Pacific Boulevard to the south, Robson and Beatty Streets). Rogers Arena: the brief's corner
|
|
24
|
+
* entrance plazas on the Pat Quinn Way side, the south front and the two south corners, and one at the north-west, all ours.
|
|
25
|
+
*/
|
|
26
|
+
export const GATE_FACTS = Object.freeze({
|
|
27
|
+
"bc-place": SIDES("BC Place").map((g) => (g.id === "south" ? { ...g, name: { en: "Gate G", ja: "ゲートG" }, basis: "documented", read: "the English Wikipedia article on BC Place, revision 1372065649: a tear in the old roof 'close to Gate G on the south side'; where on the south side is ours" } : g)),
|
|
28
|
+
"rogers-arena": [
|
|
29
|
+
{ id: "south", bearing: 180, basis: "plausible", read: ours("the main front of the building faces Pat Quinn Way to the south, as the brief says") },
|
|
30
|
+
{ id: "south-west", bearing: 225, basis: "plausible", read: ours("a corner entrance plaza on the Pat Quinn Way side, as the brief says") },
|
|
31
|
+
{ id: "south-east", bearing: 135, basis: "plausible", read: ours("a corner entrance plaza on the Pat Quinn Way side, as the brief says") },
|
|
32
|
+
{ id: "north-west", bearing: 315, basis: "plausible", read: ours("a gate on the side of the Georgia Viaduct and the towers, one of those the brief asks for at the corners") },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
/** The ids of the venues that have gates. */
|
|
36
|
+
export const GATE_VENUES = Object.freeze(Object.keys(GATE_FACTS));
|
|
37
|
+
/** The point where a ray from the origin at a compass bearing first meets a ring (both in metres east and north of the origin), or `undefined`. */
|
|
38
|
+
function hit(ring, bearing) {
|
|
39
|
+
const a = (bearing * Math.PI) / 180;
|
|
40
|
+
const [dx, dy] = [Math.sin(a), Math.cos(a)];
|
|
41
|
+
let best;
|
|
42
|
+
ring.forEach(([ax, ay], i) => {
|
|
43
|
+
const [bx, by] = ring[(i + 1) % ring.length];
|
|
44
|
+
const [ex, ey] = [bx - ax, by - ay];
|
|
45
|
+
const den = dx * ey - dy * ex;
|
|
46
|
+
if (Math.abs(den) < 1e-12)
|
|
47
|
+
return;
|
|
48
|
+
const t = (ax * ey - ay * ex) / den;
|
|
49
|
+
const u = (ax * dy - ay * dx) / den;
|
|
50
|
+
if (t > 1e-6 && u >= 0 && u <= 1 && (best === undefined || t < best))
|
|
51
|
+
best = t;
|
|
52
|
+
});
|
|
53
|
+
return best === undefined ? undefined : [dx * best, dy * best];
|
|
54
|
+
}
|
|
55
|
+
/** The outline and the middle of a venue's surface that its gates are placed from, `[longitude, latitude]`: an arena's, or a stadium's. */
|
|
56
|
+
function outlineOf(id) {
|
|
57
|
+
const venue = venueById(id);
|
|
58
|
+
if (venue !== undefined)
|
|
59
|
+
return { ring: venue.osm.ring, centre: venue.osm.axis.centre };
|
|
60
|
+
const stadium = stadiumById(id);
|
|
61
|
+
return stadium === undefined ? undefined : { ring: stadium.osm.ring, centre: stadium.osm.axis.centre };
|
|
62
|
+
}
|
|
63
|
+
/** The name of a gate in a language: the source's where there is one, else its side (`South-west gate`). */
|
|
64
|
+
export function gateName(fact, language) {
|
|
65
|
+
if (fact.name !== undefined)
|
|
66
|
+
return fact.name[language];
|
|
67
|
+
const side = WIND.find((w) => w.en === fact.id.replace(/-corner$/, ""));
|
|
68
|
+
return language === "ja" ? `${side?.ja ?? fact.id}ゲート` : `${(side?.en ?? fact.id).replace(/^./, (c) => c.toUpperCase())} gate`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The gates of a venue (`bc-place`, `rogers-arena`), each on the building's outline where a ray from the middle of the surface at its bearing meets it; empty for a venue with none. A gate whose ray misses the outline is left out
|
|
72
|
+
* (none does for the venues here: a test holds it).
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { venueGates } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
76
|
+
* const gates = venueGates("bc-place");
|
|
77
|
+
* console.log(gates.length, gates.filter((g) => g.basis === "documented").map((g) => g.name.en).join(", "));
|
|
78
|
+
* // 8 Gate G
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function venueGates(id) {
|
|
82
|
+
const facts = GATE_FACTS[id];
|
|
83
|
+
const outline = outlineOf(id);
|
|
84
|
+
if (facts === undefined || outline === undefined)
|
|
85
|
+
return [];
|
|
86
|
+
const ring = metresFrom(outline.centre, outline.ring);
|
|
87
|
+
const gates = [];
|
|
88
|
+
for (const fact of facts) {
|
|
89
|
+
const p = hit(ring, fact.bearing);
|
|
90
|
+
if (p === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
gates.push({ key: `${id}:${fact.id}`, venue: id, id: fact.id, name: { en: gateName(fact, "en"), ja: gateName(fact, "ja") }, at: lonLatFrom(outline.centre, p).map((x) => Math.round(x * 1e6) / 1e6), bearing: fact.bearing, basis: fact.basis, read: fact.read });
|
|
93
|
+
}
|
|
94
|
+
return gates;
|
|
95
|
+
}
|
package/dist/halls.d.ts
CHANGED
|
@@ -61,5 +61,7 @@ export interface Hall {
|
|
|
61
61
|
ja: string;
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
/** The Kodokan's main dojo as drawn: a square floor of 420 jō's area, four contest areas, the seat of honour, the banner, the wood and the gallery, in the two looks the owner's photographs show. The years of both are unknown. */
|
|
65
|
+
export declare const KODOKAN_MAIN: DojoSpec;
|
|
64
66
|
/** The halls, in the order of their surfaces' fame: the Kodokan, the Budokan, the Kokugikan, the Butokuden and the Budokwai. */
|
|
65
67
|
export declare const HALLS: readonly Hall[];
|