@johnmorrisdotca/kyogijo-stadiums 0.40.1 → 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 +63 -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 +33 -1
- package/dist/crowd.draw.d.ts +1 -1
- package/dist/crowd.draw.js +16 -2
- package/dist/crowd.js +9 -1
- 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/draw.js +37 -13
- 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 +15 -1
- package/dist/stadiums.data.js +2 -1
- package/dist/stadiums.js +48 -12
- package/dist/strings.d.ts +3 -1
- package/dist/strings.js +4 -4
- package/dist/surfaces/surface.d.ts +5 -0
- 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 +15 -3
- package/dist/uprights.js +9 -1
- package/dist/venue.js +9 -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/crowd.d.ts
CHANGED
|
@@ -48,11 +48,13 @@ export interface StandRing {
|
|
|
48
48
|
seats: number;
|
|
49
49
|
/** The aisles and vomitories across the band, each from a point on its inner edge to its outer: across the sides, fanning round the corners. */
|
|
50
50
|
aisles: [Pt, Pt][];
|
|
51
|
+
/** The band is closed in this configuration (a real stadium's seats a configuration does not open): drawn as empty seats, with no people, in the same place every time. */
|
|
52
|
+
closed?: boolean;
|
|
51
53
|
}
|
|
52
54
|
/** A block of the stands that is not the home fans': the visitors', or a card display. A polygon in the wide frame. */
|
|
53
55
|
export interface StandSector {
|
|
54
56
|
ring: string;
|
|
55
|
-
kind: "away" | "tifo-home" | "tifo-away";
|
|
57
|
+
kind: "away" | "tifo-home" | "tifo-away" | "closed";
|
|
56
58
|
points: Pt[];
|
|
57
59
|
}
|
|
58
60
|
/** A place left open or built on: a tunnel through the lower stands, a bench, the camera platform of the media side. A box in the wide frame. */
|
|
@@ -96,6 +98,11 @@ export interface CrowdStands {
|
|
|
96
98
|
gallery?: boolean;
|
|
97
99
|
/** The stands are a real venue's (see `src/venues.ts`): the outline of its building, the way north lies on the page and the roof. */
|
|
98
100
|
real?: RealShell;
|
|
101
|
+
/**
|
|
102
|
+
* A real stadium of more than one configuration is laid out in its own frame, the venue's, with the middle of its field at the origin whichever field it is: `shift` is where that middle is in the drawing's own
|
|
103
|
+
* frame (the surface's), so that what belongs to the field is drawn there and the building, the bowl and the crowd are the same numbers for every configuration.
|
|
104
|
+
*/
|
|
105
|
+
shift?: [number, number];
|
|
99
106
|
}
|
|
100
107
|
/** The room above a real venue's building that the arrow saying where north is takes, in feet, and how far above the building's box the middle of the arrow stands. */
|
|
101
108
|
export declare const NORTH_ROOM = 60;
|
|
@@ -144,12 +151,23 @@ export interface RealShell {
|
|
|
144
151
|
rise: number;
|
|
145
152
|
opening: Pt[];
|
|
146
153
|
};
|
|
154
|
+
/** The height of the building's podium, a lower band of its walls under the concourse levels, in feet; the walls are drawn in two bands where a venue has one (ours where the sources do not say). */
|
|
155
|
+
podium?: number;
|
|
156
|
+
/** A `fixed` roof closed in the middle by a cap with a long skylight along the surface's long side (Rogers Arena's): ours, from the brief. */
|
|
157
|
+
skylight?: boolean;
|
|
158
|
+
/** A `cable` roof's masts round its rim: how many, and how high they rise over the rim, in feet. */
|
|
159
|
+
masts?: {
|
|
160
|
+
count: number;
|
|
161
|
+
height: number;
|
|
162
|
+
};
|
|
147
163
|
/** The height of the roof's lowest edge above the surface, in feet. */
|
|
148
164
|
edge: number;
|
|
149
165
|
}
|
|
150
166
|
/** The stands of a real venue, worked out from its building (`venueBowl`): what `layoutOf` takes in place of a sport's usual tiers. */
|
|
151
167
|
export interface RealBowl {
|
|
152
168
|
rings: StandRing[];
|
|
169
|
+
/** Blocks of a band a configuration closes (its seats beyond the configuration's own): drawn over the band as empty seats, in the same place every time. */
|
|
170
|
+
closedSectors?: StandSector[];
|
|
153
171
|
/** How far the first row stands from the surface's edge at each side, in feet (the bench side has more). */
|
|
154
172
|
off: {
|
|
155
173
|
left: number;
|
|
@@ -220,8 +238,22 @@ export declare function rrectAt(o: Extract<Outline, {
|
|
|
220
238
|
p: Pt;
|
|
221
239
|
n: Pt;
|
|
222
240
|
};
|
|
241
|
+
/** The aisles a band has, as parameters of its inner outline: one at each end of every straight side (about every 30 feet along it) and one in the middle of every corner. */
|
|
242
|
+
export declare function aisleParameters(hole: Extract<Outline, {
|
|
243
|
+
kind: "rrect";
|
|
244
|
+
}>): {
|
|
245
|
+
us: number[];
|
|
246
|
+
nh: number;
|
|
247
|
+
nv: number;
|
|
248
|
+
};
|
|
223
249
|
/** The aisles of a band as lines from its inner edge to its outer. */
|
|
224
250
|
export declare function aislesOf(region: Outline, hole: Outline | null): [Pt, Pt][];
|
|
251
|
+
/** The polygon of a band between two parameters, cut finely (a thirty-second of a side at a time) so that a block at a corner follows its curve as the band does. */
|
|
252
|
+
export declare function sectorPolygon(region: Extract<Outline, {
|
|
253
|
+
kind: "rrect";
|
|
254
|
+
}>, hole: Extract<Outline, {
|
|
255
|
+
kind: "rrect";
|
|
256
|
+
}>, u0: number, u1: number): Pt[];
|
|
225
257
|
/** What the layout needs of the options and the surface. */
|
|
226
258
|
export interface LayoutInput {
|
|
227
259
|
sport: Sport;
|
package/dist/crowd.draw.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type Crowd, type CrowdStands, type RealShell } from "./crowd.ts";
|
|
|
3
3
|
export declare function patterns(id: string, crowd: Crowd, density: number, tifo: {
|
|
4
4
|
home: boolean;
|
|
5
5
|
away: boolean;
|
|
6
|
-
} | undefined, neutral?: boolean, rows?: boolean): string;
|
|
6
|
+
} | undefined, neutral?: boolean, rows?: boolean, shut?: boolean): string;
|
|
7
7
|
/** How the crowd is drawn: the prefix of ids, the options read, the share of seats taken, the detail, and the class of the ground the stands stand on. */
|
|
8
8
|
export interface DrawInput {
|
|
9
9
|
id: string;
|
package/dist/crowd.draw.js
CHANGED
|
@@ -52,11 +52,14 @@ function tile(seed, density, share, team, neutral = false) {
|
|
|
52
52
|
return el("g", { fill: "none", "stroke-linecap": "round", "stroke-width": 2.3 }, out + headPaths);
|
|
53
53
|
}
|
|
54
54
|
/** The tiles a crowd needs: the home tile, the home tile of the upper tiers (emptier) and the visitors', and the display tiles. */
|
|
55
|
-
export function patterns(id, crowd, density, tifo, neutral = false, rows = false) {
|
|
55
|
+
export function patterns(id, crowd, density, tifo, neutral = false, rows = false, shut = false) {
|
|
56
56
|
// A 3D view's tiles carry the lines of the rows too (a row every third of a tile, along the tile's x), so that a raked plane is textured and not drawn seat by seat.
|
|
57
57
|
const rowLines = rows ? el("path", { class: "ky-cr-row", d: `M0 .3H${TILE}M0 ${TILE / 3 + 0.3}H${TILE}M0 ${(2 * TILE) / 3 + 0.3}H${TILE}`, fill: "none" }) : "";
|
|
58
58
|
const make = (suffix, body, size = TILE) => el("pattern", { id: `${id}-cr-${suffix}`, width: size, height: size, patternUnits: "userSpaceOnUse" }, size === TILE ? body + rowLines : body);
|
|
59
59
|
let out = make("h", tile(crowd.seed * 7 + 1, density, neutral ? 0 : 0.88, "ky-cr-h", neutral)) + make("a", tile(crowd.seed * 7 + 3, Math.min(1, density * 1.04), 0.94, "ky-cr-a"));
|
|
60
|
+
// The seats of a closed band: the same seats, every one empty.
|
|
61
|
+
if (shut)
|
|
62
|
+
out += make("e", tile(crowd.seed * 7 + 5, 0, 0, "ky-cr-h", true));
|
|
60
63
|
if (tifo !== undefined) {
|
|
61
64
|
const stripes = (cls) => el("rect", { class: cls, width: 3, height: 6 }) + el("rect", { class: "ky-cr-tw", x: 3, width: 3, height: 6 });
|
|
62
65
|
const checks = (cls) => el("rect", { class: cls, width: 3, height: 3 }) + el("rect", { class: cls, x: 3, y: 3, width: 3, height: 3 }) + el("rect", { class: "ky-cr-tw", x: 3, width: 3, height: 3 }) + el("rect", { class: "ky-cr-tw", y: 3, width: 3, height: 3 });
|
|
@@ -185,6 +188,11 @@ export function drawCrowd(stands, o) {
|
|
|
185
188
|
// The bed of seats under the tile, so that no seam of the pattern shows light.
|
|
186
189
|
const bed = stands.gallery === true ? "ky-cr-bed ky-bud-gbed" : "ky-cr-bed";
|
|
187
190
|
// A hall's gallery, seen from above, is neat rows of seats along its band (a few people are not drawn: it is subtle).
|
|
191
|
+
if (ring.closed === true) {
|
|
192
|
+
// Closed seats: the bed and the same seats, every one empty, so that the closed part is the same drawing in every configuration that closes it.
|
|
193
|
+
body += el("path", { class: bed, d: path(ring) }) + (fine ? el("path", { class: "ky-cr-shut", d: path(ring), fill: `url(#${id}-cr-e)` }) + el("path", { class: "ky-cr-shut-veil", d: path(ring) }) : el("path", { class: "ky-cr-blk", d: path(ring), "fill-opacity": num(0.28) }));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
188
196
|
if (stands.gallery === true && fine)
|
|
189
197
|
body += el("path", { class: bed, d: path(ring) }) + galleryRows(ring) + (density > 0 ? el("path", { class: "ky-cr-people", d: path(ring), fill: `url(#${id}-cr-h)` }) : "");
|
|
190
198
|
else if (fine)
|
|
@@ -254,10 +262,15 @@ export function drawCrowd(stands, o) {
|
|
|
254
262
|
const [x0, y0, x1, y1] = g.box;
|
|
255
263
|
body += el("rect", { class: `ky-cr-${g.kind}`, x: n1(x0), y: n1(y0), width: n1(x1 - x0), height: n1(y1 - y0), rx: g.kind === "bench" ? 1 : undefined });
|
|
256
264
|
}
|
|
257
|
-
return { defs: (fine && (stands.gallery !== true || density > 0) ? patterns(id, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.gallery === true || stands.real !== undefined) : "") + pool, markup: el("g", { class: light === undefined ? "ky-crowd" : light.house === "dark" ? "ky-crowd ky-night ky-down" : light.house === "half" ? "ky-crowd ky-night ky-half" : "ky-crowd ky-night" }, body) + lit, over };
|
|
265
|
+
return { defs: (fine && (stands.gallery !== true || density > 0) ? patterns(id, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.gallery === true || stands.real !== undefined, false, stands.rings.some((r) => r.closed === true) || stands.sectors.some((s) => s.kind === "closed")) : "") + pool, markup: el("g", { class: light === undefined ? "ky-crowd" : light.house === "dark" ? "ky-crowd ky-night ky-down" : light.house === "half" ? "ky-crowd ky-night ky-half" : "ky-crowd ky-night" }, body) + lit, over };
|
|
258
266
|
}
|
|
259
267
|
/** A block of visitors or a card display, a polygon over the band it is in. */
|
|
260
268
|
function sector(s, id, fine, density) {
|
|
269
|
+
// A block of closed seats: the bed and the same seats, every one empty.
|
|
270
|
+
if (s.kind === "closed") {
|
|
271
|
+
const d = `M${s.points.map(([x, y]) => `${n1(x)} ${n1(y)}`).join("L")}Z`;
|
|
272
|
+
return el("path", { class: "ky-cr-bed", d }) + (fine ? el("path", { class: "ky-cr-shut", d, fill: `url(#${id}-cr-e)` }) + el("path", { class: "ky-cr-shut-veil", d }) : el("path", { class: "ky-cr-blk", d, "fill-opacity": num(0.28) }));
|
|
273
|
+
}
|
|
261
274
|
if (s.kind === "away")
|
|
262
275
|
return fine ? el("path", { class: "ky-cr-people", d: poly(s.points), fill: `url(#${id}-cr-a)` }) : el("path", { class: "ky-cr-blk-a", d: poly(s.points), "fill-opacity": num(0.3 + 0.6 * density) });
|
|
263
276
|
const team = s.kind === "tifo-home" ? "th" : "ta";
|
|
@@ -270,6 +283,7 @@ export const KYOGIJO_CROWD_STYLE = `
|
|
|
270
283
|
:where(html[data-theme=dark] .kyogijo,.kyogijo[data-theme=dark]){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-rim:#07090c;--ky-cr-dim:.82}
|
|
271
284
|
.kyogijo .ky-crowd{opacity:var(--ky-cr-dim)}
|
|
272
285
|
.kyogijo .ky-cr-bed{fill:var(--ky-cr-bed);stroke:var(--ky-cr-rim);stroke-width:1.8;stroke-opacity:.55;stroke-linejoin:round}
|
|
286
|
+
.kyogijo .ky-cr-shut-veil{fill:var(--ky-cr-empty);fill-opacity:.42}
|
|
273
287
|
.kyogijo .ky-cr-seat{fill:none;stroke:var(--ky-cr-empty);stroke-width:1.3;stroke-opacity:.65}
|
|
274
288
|
.kyogijo .ky-cr-h{stroke:var(--ky-cr-home,var(--ky-right))}
|
|
275
289
|
.kyogijo .ky-cr-a{stroke:var(--ky-cr-away,var(--ky-left))}
|
package/dist/crowd.js
CHANGED
|
@@ -274,7 +274,7 @@ export function rrectAt(o, u) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
/** The aisles a band has, as parameters of its inner outline: one at each end of every straight side (about every 30 feet along it) and one in the middle of every corner. */
|
|
277
|
-
function aisleParameters(hole) {
|
|
277
|
+
export function aisleParameters(hole) {
|
|
278
278
|
const r = Math.min(hole.r, (hole.x1 - hole.x0) / 2, (hole.y1 - hole.y0) / 2);
|
|
279
279
|
const nh = Math.max(2, Math.round((hole.x1 - hole.x0 - 2 * r) / 30));
|
|
280
280
|
const nv = Math.max(2, Math.round((hole.y1 - hole.y0 - 2 * r) / 30));
|
|
@@ -304,6 +304,12 @@ export function aislesOf(region, hole) {
|
|
|
304
304
|
function galleryAisles(region, hole, shares) {
|
|
305
305
|
return [0, 2, 4, 6].flatMap((side) => shares.map((f) => [rrectAt(hole, side + f).p, rrectAt(region, side + f).p]));
|
|
306
306
|
}
|
|
307
|
+
/** The polygon of a band between two parameters, cut finely (a thirty-second of a side at a time) so that a block at a corner follows its curve as the band does. */
|
|
308
|
+
export function sectorPolygon(region, hole, u0, u1) {
|
|
309
|
+
const steps = Math.max(2, Math.ceil((u1 - u0) * 32));
|
|
310
|
+
const us = Array.from({ length: steps + 1 }, (_, i) => u0 + ((u1 - u0) * i) / steps);
|
|
311
|
+
return [...us.map((u) => rrectAt(region, u).p), ...us.map((u) => rrectAt(hole, u).p).reverse()];
|
|
312
|
+
}
|
|
307
313
|
/** The polygon of a band between two aisle parameters: along the outer edge, then back along the inner. */
|
|
308
314
|
function sectionBetween(region, hole, u0, u1) {
|
|
309
315
|
const us = [u0];
|
|
@@ -394,6 +400,8 @@ export function layoutOf(input) {
|
|
|
394
400
|
const stands = { sport, shape, box, rings, covers, concourses, sectors: [], gaps: [], reach: reachOf, seats: rings.reduce((n, r) => n + r.seats, 0) };
|
|
395
401
|
if (input.real !== undefined)
|
|
396
402
|
stands.real = input.real.shell;
|
|
403
|
+
if (input.real?.closedSectors !== undefined)
|
|
404
|
+
stands.sectors.push(...input.real.closedSectors);
|
|
397
405
|
if (stadium !== undefined)
|
|
398
406
|
stands.stadium = stadium;
|
|
399
407
|
if (gal !== undefined)
|
package/dist/district-entry.d.ts
CHANGED
|
@@ -2,4 +2,18 @@ export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE,
|
|
|
2
2
|
export type { DistrictMember, DistrictMode, DistrictOptions, DistrictPlaced, DistrictPlan, DistrictVenueInput } from "./district.ts";
|
|
3
3
|
export { buildDistrictTurn, districtPresets, districtTurnMarkup, mountDistrictTurn } from "./district.turn.ts";
|
|
4
4
|
export type { DistrictTurnSettings } from "./district.turn.ts";
|
|
5
|
+
export type { ContextArea } from "./context.ts";
|
|
6
|
+
export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_ROOM_HEIGHT, kodokanMember } from "./kodokan.ts";
|
|
5
7
|
export { VENUE_ATTRIBUTION } from "./venues.ts";
|
|
8
|
+
export { mountDistrict } from "./district.dom.ts";
|
|
9
|
+
export type { KyogijoDistrict } from "./district.dom.ts";
|
|
10
|
+
export { OUTSIDE_DENSITY, OUTSIDE_LEVELS, OUTSIDE_REACH, OUTSIDE_VENUES, hasOutside, isOutsideLevel, outsideCount, outsideCrowd, outsideLevelOf } from "./outside.ts";
|
|
11
|
+
export type { OutsideCrowd, OutsideLevel, OutsideOptions } from "./outside.ts";
|
|
12
|
+
export { PERSON_METRES, frameOf, rasterOutside } from "./outside.paint.ts";
|
|
13
|
+
export type { OutsideFrame, OutsideStats } from "./outside.paint.ts";
|
|
14
|
+
export { OUTSIDE_ROOM } from "./district.ts";
|
|
15
|
+
export { WALK_CELL, WALK_IDS, walkMask, walkable } from "./walk.ts";
|
|
16
|
+
export type { WalkData, WalkMask } from "./walk.ts";
|
|
17
|
+
export { GATE_FACTS, GATE_VENUES, gateName, venueGates } from "./gates.ts";
|
|
18
|
+
export type { Gate, GateBasis, GateFact } from "./gates.ts";
|
|
19
|
+
export type { DistrictGate } from "./district.ts";
|
package/dist/district-entry.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
// A district: several real venues of one city on one plan at their real relative positions and orientations (OpenStreetMap, ODbL 1.0), in 2D and in 3D, tight round neighbours or at the real distances of a city.
|
|
2
2
|
export { DISTRICT_IDS, DISTRICT_MARGIN, DISTRICT_MODES, KYOGIJO_DISTRICT_STYLE, describeDistrict, districtId, districtMember, drawDistrict, isDistrictId, metresText, planDistrict, scaleOf, unknownVenues } from "./district.js";
|
|
3
3
|
export { buildDistrictTurn, districtPresets, districtTurnMarkup, mountDistrictTurn } from "./district.turn.js";
|
|
4
|
+
export { KODOKAN, KODOKAN_DOJO, KODOKAN_FLOOR_HEIGHT, KODOKAN_ROOM_HEIGHT, kodokanMember } from "./kodokan.js";
|
|
4
5
|
export { VENUE_ATTRIBUTION } from "./venues.js";
|
|
6
|
+
export { mountDistrict } from "./district.dom.js";
|
|
7
|
+
// The crowd outside the venues, on ground people may stand on (OpenStreetMap, ODbL 1.0), at 0, 25, 50, 75 or 100 percent of the seats.
|
|
8
|
+
export { OUTSIDE_DENSITY, OUTSIDE_LEVELS, OUTSIDE_REACH, OUTSIDE_VENUES, hasOutside, isOutsideLevel, outsideCount, outsideCrowd, outsideLevelOf } from "./outside.js";
|
|
9
|
+
export { PERSON_METRES, frameOf, rasterOutside } from "./outside.paint.js";
|
|
10
|
+
export { OUTSIDE_ROOM } from "./district.js";
|
|
11
|
+
export { WALK_CELL, WALK_IDS, walkMask, walkable } from "./walk.js";
|
|
12
|
+
// The gates of a venue: where the crowd goes in, on the building's outline, with the way each faces and how it is known.
|
|
13
|
+
export { GATE_FACTS, GATE_VENUES, gateName, venueGates } from "./gates.js";
|
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));
|