@johnmorrisdotca/kyogijo-stadiums 0.45.3 → 0.47.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 +43 -0
- package/NOTICE.md +9 -2
- package/dist/areas.d.ts +4 -0
- package/dist/areas.js +33 -1
- package/dist/cityctx.belmont-park.data.d.ts +3 -0
- package/dist/cityctx.belmont-park.data.js +1802 -0
- package/dist/cityctx.data.d.ts +10 -1
- package/dist/cityctx.data.js +11 -2
- package/dist/cityctx.js +19 -1
- package/dist/cityctx.meadowlands.data.d.ts +3 -0
- package/dist/cityctx.meadowlands.data.js +820 -0
- package/dist/cityctx.montreal-city.data.d.ts +3 -0
- package/dist/cityctx.montreal-city.data.js +2899 -0
- package/dist/cityctx.montreal-downtown.data.d.ts +3 -0
- package/dist/cityctx.montreal-downtown.data.js +3004 -0
- package/dist/cityctx.montreal-olympic.data.d.ts +3 -0
- package/dist/cityctx.montreal-olympic.data.js +2562 -0
- package/dist/cityctx.nyc-bronx.data.d.ts +3 -0
- package/dist/cityctx.nyc-bronx.data.js +1671 -0
- package/dist/cityctx.nyc-brooklyn.data.d.ts +3 -0
- package/dist/cityctx.nyc-brooklyn.data.js +3485 -0
- package/dist/cityctx.nyc-flushing.data.d.ts +3 -0
- package/dist/cityctx.nyc-flushing.data.js +865 -0
- package/dist/cityctx.nyc-midtown.data.d.ts +3 -0
- package/dist/cityctx.nyc-midtown.data.js +2438 -0
- package/dist/crowd.d.ts +26 -3
- package/dist/district.js +1 -1
- package/dist/draw.js +2 -2
- package/dist/flythrough.dom.d.ts +2 -0
- package/dist/flythrough.dom.js +2 -2
- package/dist/stadiums.d.ts +51 -7
- package/dist/stadiums.data.js +60 -0
- package/dist/stadiums.js +40 -6
- package/dist/stadiums.osm.data.js +224 -2
- package/dist/strings.core.d.ts +2 -0
- package/dist/strings.en.js +1 -1
- package/dist/strings.ja.js +1 -1
- package/dist/turn.kit.d.ts +14 -1
- package/dist/turn.kit.js +19 -0
- package/dist/turn.style.js +20 -3
- package/dist/venue.js +3 -1
- package/dist/venue.real.d.ts +45 -1
- package/dist/venue.real.js +245 -21
- package/dist/venue.ribbed.d.ts +18 -0
- package/dist/venue.ribbed.js +199 -0
- package/dist/venues/american-airlines-center.data.js +1 -1
- package/dist/venues.d.ts +2 -2
- package/dist/venues.data.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/venue.real.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
// membrane from the rim of the walls to a crown, which never opens), or no roof at all (Koshien Stadium's). All are asked for with the roof (`venue: { roof: true }`) and give way to the eye as the other walls do.
|
|
5
5
|
// Pure: data in, planes out.
|
|
6
6
|
import { rayAt } from "./crowd.js";
|
|
7
|
-
import { n, shy, wall } from "./turn.kit.js";
|
|
7
|
+
import { bar, box, n, pole, shy, wall } from "./turn.kit.js";
|
|
8
8
|
import { pointList, raked } from "./venue.geom.js";
|
|
9
|
+
import { ribbedRoof, ribbedWalls } from "./venue.ribbed.js";
|
|
10
|
+
import { edgeDistance, inside } from "./venues.geo.js";
|
|
9
11
|
/** How far a lid rises from its edge towards the middle, and how far a saddle's high ends stand over its low sides, in feet (ours: the sources give a roof's kind, not its height). */
|
|
10
12
|
export const LID_RISE = 22;
|
|
11
13
|
export const SADDLE_RISE = 30;
|
|
14
|
+
/** How far a barrel vault's crown stands over its springing at the long sides, in feet (ours: the article says the roof looks like a Quonset hut, not how high it rises). */
|
|
15
|
+
export const BARREL_RISE = 34;
|
|
12
16
|
/** The saddle's share of its rise at a point (0 at the middle of a side, 1 at the middle of an end): half a saddle's way from one to the other, as a hyperbolic paraboloid (u squared less v squared) lies over the building's box. */
|
|
13
17
|
export function saddleAt(real, [x, y]) {
|
|
14
18
|
const xs = real.footprint.map((p) => p[0]);
|
|
@@ -18,8 +22,111 @@ export function saddleAt(real, [x, y]) {
|
|
|
18
22
|
const s = ((x - cx) / hx) ** 2 - ((y - cy) / hy) ** 2;
|
|
19
23
|
return (1 + Math.max(-1, Math.min(1, s))) / 2;
|
|
20
24
|
}
|
|
25
|
+
/** The barrel vault's share of its rise at a point (0 at the springing along each long side, 1 over the middle line): a round arch over the building's short axis, the quarter of a circle's rise at each step in from the side. */
|
|
26
|
+
export function barrelAt(real, [, y]) {
|
|
27
|
+
const ys = real.footprint.map((p) => p[1]);
|
|
28
|
+
const [cy, hy] = [(Math.min(...ys) + Math.max(...ys)) / 2, (Math.max(...ys) - Math.min(...ys)) / 2];
|
|
29
|
+
const v = Math.min(1, Math.abs((y - cy) / hy));
|
|
30
|
+
return Math.sqrt(1 - v * v);
|
|
31
|
+
}
|
|
21
32
|
/** The height of the roof over a point of the building's edge, above the surface, in feet. */
|
|
22
|
-
export const roofAt = (real, p) => real.edge + (real.roof === "saddle" ? SADDLE_RISE * saddleAt(real, p) : 0);
|
|
33
|
+
export const roofAt = (real, p) => real.edge + (real.roof === "saddle" ? SADDLE_RISE * saddleAt(real, p) : real.roof === "barrel" ? BARREL_RISE * barrelAt(real, p) : 0);
|
|
34
|
+
/** How near a wall must stand to a canopy's outline to be the wall under it, in feet. */
|
|
35
|
+
const CANOPY_REACH = 50;
|
|
36
|
+
/** The height of the wall of a building under canopies at a point of its rim: where a canopy is over it, the canopy's back edge, which the wall carries up to (the outside of Lumen Field's roofs is one curved face to the ground); elsewhere the rim's own height. */
|
|
37
|
+
export function wallAt(real, p) {
|
|
38
|
+
if (real.roof !== "canopy" || real.canopies === undefined)
|
|
39
|
+
return roofAt(real, p);
|
|
40
|
+
let top = roofAt(real, p);
|
|
41
|
+
for (const canopy of real.canopies)
|
|
42
|
+
if (inside(canopy.ring, p) || edgeDistance(canopy.ring, p) < CANOPY_REACH)
|
|
43
|
+
top = Math.max(top, canopy.low + canopy.rise);
|
|
44
|
+
return top;
|
|
45
|
+
}
|
|
46
|
+
/** How many rings the leaning tower's shaft is cut into, and the observatory on its top: its length along the lean, its width and its height, in feet (ours: the sources give the tower's height and its inclination, not these). */
|
|
47
|
+
const TOWER_RINGS = 12;
|
|
48
|
+
const TOWER_CAP = { long: 84, wide: 72, high: 34 };
|
|
49
|
+
/** How far the shaft's back runs out beyond its foot, as a share of the run from the foot to the top (the back sweeps down to the ground in a long curve), how far its front stands over the top, and how far its back stands behind it, in feet (ours). */
|
|
50
|
+
const TOWER_TAIL = 0.45;
|
|
51
|
+
const TOWER_PRAW = { front: 18, back: 58 };
|
|
52
|
+
/**
|
|
53
|
+
* A tower that leans and curves: a shaft of horizontal sections, a square of rising width at its foot narrowing to its top, that leans over the building. Its foot is at the building's rim (where the mapped axis crosses it, `rim` feet
|
|
54
|
+
* from the top along the axis), its front, the steep face that looks at the building, runs from there to the top, and its back, the face that looks away and up, sweeps down from the top in a long curve that flattens to the ground
|
|
55
|
+
* far behind the foot. On the top stands the observatory, a dark block with a band of glass, and, with `cables`, thin lines run from it to the roof. Every face of a ring is a trapezoid on a plane through its corners, so the faces
|
|
56
|
+
* meet along their edges. The foot, the top and the height are the sources' (the map's funicular line and the article's inclination); the curve, the widths, the slits and the observatory are ours.
|
|
57
|
+
*/
|
|
58
|
+
export function towerPlanes(tower, z, cables = [], rim) {
|
|
59
|
+
const [bx, by] = tower.base;
|
|
60
|
+
const [tx, ty] = tower.top;
|
|
61
|
+
const run = Math.hypot(tx - bx, ty - by) || 1;
|
|
62
|
+
const d = [(tx - bx) / run, (ty - by) / run];
|
|
63
|
+
const a = [-d[1], d[0]];
|
|
64
|
+
const [w0, w1] = tower.width;
|
|
65
|
+
const H = tower.height - TOWER_CAP.high;
|
|
66
|
+
// Along the lean (from the foot towards the top) and across it: a point of the shaft at a place `p` along, `q` across and `h` up.
|
|
67
|
+
const at = (p, q, h) => [bx + d[0] * p + a[0] * q, by + d[1] * p + a[1] * q, z + h];
|
|
68
|
+
const front0 = Math.max(0, Math.min(run * 0.9, run - (rim ?? run * 0.5)));
|
|
69
|
+
const frontAt = (h) => front0 + (run + TOWER_PRAW.front - front0) * (h / H) ** 0.92;
|
|
70
|
+
// The back: a quadratic curve from the tail on the ground, flat there, to the top, steep there; its rings are equal steps along the curve.
|
|
71
|
+
const [p0, p1, p2] = [[-TOWER_TAIL * run, 0], [run * 0.34, H * 0.15], [run - TOWER_PRAW.back, H]];
|
|
72
|
+
const curve = (t) => [(1 - t) ** 2 * p0[0] + 2 * t * (1 - t) * p1[0] + t * t * p2[0], (1 - t) ** 2 * p0[1] + 2 * t * (1 - t) * p1[1] + t * t * p2[1]];
|
|
73
|
+
const wide = (h) => w1 + (w0 - w1) * (1 - h / H) ** 1.15;
|
|
74
|
+
const rings = Array.from({ length: TOWER_RINGS + 1 }, (_, i) => {
|
|
75
|
+
const [back, h] = curve(i / TOWER_RINGS);
|
|
76
|
+
return { h, back, front: frameFront(frontAt(h), back), w: wide(h) };
|
|
77
|
+
});
|
|
78
|
+
const planes = [];
|
|
79
|
+
const face = (id, o, up, along, r, svg) => {
|
|
80
|
+
const [lu, lv] = [Math.hypot(...up), Math.hypot(...along)];
|
|
81
|
+
// (A wall, not a mast: the lower tiers leave out the small parts, and a tower 165 metres high is not one.)
|
|
82
|
+
planes.push({ id, kind: "wall", o, u: up, v: along, svg: `<polygon class="u-tower" points="0 0 ${n(lu)} 0 ${n(lu)} ${n(lv * r)} 0 ${n(lv)}"/>${svg(lu)}` });
|
|
83
|
+
};
|
|
84
|
+
const sub = (p, q) => [p[0] - q[0], p[1] - q[1], p[2] - q[2]];
|
|
85
|
+
const scale = (p, k) => [p[0] * k, p[1] * k, p[2] * k];
|
|
86
|
+
for (let i = 0; i < TOWER_RINGS; i += 1) {
|
|
87
|
+
const [r0, r1] = [rings[i], rings[i + 1]];
|
|
88
|
+
const wr = r1.w / r0.w;
|
|
89
|
+
const lr = (r1.front - r1.back) / (r0.front - r0.back);
|
|
90
|
+
// The front and the back: across the shaft, from the left corner of the ring to the right.
|
|
91
|
+
for (const [name, k0, k1] of [["f", "front", "front"], ["b", "back", "back"]]) {
|
|
92
|
+
const o = at(r0[k0], -r0.w / 2, r0.h);
|
|
93
|
+
const top = at(r1[k1], -r1.w / 2, r1.h);
|
|
94
|
+
// The sloped back carries the two long dark slits down it, a few rings clear of the foot and of the top.
|
|
95
|
+
const slit = (lu, f0, f1) => `<polygon class="u-tower-slit" points="0 ${n(f0 * r0.w)} ${n(lu)} ${n(f0 * r1.w)} ${n(lu)} ${n(f1 * r1.w)} 0 ${n(f1 * r0.w)}"/>`;
|
|
96
|
+
const slits = name === "b" && i >= 2 && i < TOWER_RINGS - 1 ? (lu) => `${slit(lu, 0.24, 0.31)}${slit(lu, 0.62, 0.69)}` : () => "";
|
|
97
|
+
face(`tower-${i}-${name}`, o, sub(top, o), scale([a[0], a[1], 0], r0.w), wr, slits);
|
|
98
|
+
}
|
|
99
|
+
// The two flanks: along the lean, from the front to the back.
|
|
100
|
+
for (const [name, side] of [["l", -1], ["r", 1]]) {
|
|
101
|
+
const o = at(r0.front, (side * r0.w) / 2, r0.h);
|
|
102
|
+
const top = at(r1.front, (side * r1.w) / 2, r1.h);
|
|
103
|
+
face(`tower-${i}-${name}`, o, sub(top, o), scale([-d[0], -d[1], 0], r0.front - r0.back), lr, () => "");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// The observatory on the top: a block a little wider than the shaft there, its walls dark, with a band of glass round it, and a flat roof.
|
|
107
|
+
const top = rings[TOWER_RINGS];
|
|
108
|
+
const centre = (top.front + top.back) / 2;
|
|
109
|
+
const [hl, hw] = [TOWER_CAP.long / 2, TOWER_CAP.wide / 2];
|
|
110
|
+
const corners = [[hl, -hw], [hl, hw], [-hl, hw], [-hl, -hw]];
|
|
111
|
+
for (let k = 0; k < 4; k += 1) {
|
|
112
|
+
const [c0, c1] = [corners[k], corners[(k + 1) % 4]];
|
|
113
|
+
const o = at(centre + c0[0], c0[1], top.h);
|
|
114
|
+
const along = sub(at(centre + c1[0], c1[1], top.h), o);
|
|
115
|
+
const lv = Math.hypot(...along);
|
|
116
|
+
planes.push({ id: `tower-cap-${k}`, kind: "wall", o, u: [0, 0, TOWER_CAP.high], v: along, svg: `<rect class="u-tower-cap" width="${n(TOWER_CAP.high)}" height="${n(lv)}"/><rect class="u-tower-glass" x="${n(TOWER_CAP.high * 0.3)}" width="${n(TOWER_CAP.high * 0.45)}" height="${n(lv)}"/>` });
|
|
117
|
+
}
|
|
118
|
+
{
|
|
119
|
+
const o = at(centre + corners[0][0], corners[0][1], top.h + TOWER_CAP.high);
|
|
120
|
+
const [c1, c3] = [at(centre + corners[1][0], corners[1][1], top.h + TOWER_CAP.high), at(centre + corners[3][0], corners[3][1], top.h + TOWER_CAP.high)];
|
|
121
|
+
planes.push({ id: "tower-cap-top", kind: "wall", o, u: sub(c1, o), v: sub(c3, o), svg: `<rect class="u-tower-cap" width="${n(TOWER_CAP.wide)}" height="${n(TOWER_CAP.long)}"/>` });
|
|
122
|
+
}
|
|
123
|
+
// The cables that held the roof, from the observatory to the roof: thin lines.
|
|
124
|
+
const from = at(centre, 0, top.h + TOWER_CAP.high * 0.4);
|
|
125
|
+
cables.forEach((to, i) => planes.push(...bar(`tower-cable-${i}`, "mast", from, to, 1.2, "u-tower-cable")));
|
|
126
|
+
return planes;
|
|
127
|
+
}
|
|
128
|
+
/** The front's place along the lean at a height, kept in front of the back (a shaft is never turned inside out where the back's curve comes close to the front's line). */
|
|
129
|
+
const frameFront = (front, back) => Math.max(front, back + 40);
|
|
23
130
|
/** The point where a ray from `from` first meets a polygon, or `undefined`. */
|
|
24
131
|
export function firstHit(poly, from, angle) {
|
|
25
132
|
const [dx, dy] = [Math.cos(angle), Math.sin(angle)];
|
|
@@ -102,16 +209,29 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
|
|
|
102
209
|
const c = [centre[0], centre[1]];
|
|
103
210
|
const rim = Array.from({ length: steps }, (_, k) => rimPoint(real, c, (2 * Math.PI * k) / steps));
|
|
104
211
|
const planes = [];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
212
|
+
// A ribbed shell (the Olympic Stadium's) is its own: ribs and glazed bays in place of a drum of walls.
|
|
213
|
+
const ribbed = real.roof === "dome" && real.dome?.ribs !== undefined;
|
|
214
|
+
if (ribbed && parts.walls) {
|
|
215
|
+
const mid = [real.dome.rim.reduce((t, p) => t + p[0], 0) / real.dome.rim.length, real.dome.rim.reduce((t, p) => t + p[1], 0) / real.dome.rim.length];
|
|
216
|
+
const shell = ribbedWalls(real, mid, lift, steps);
|
|
217
|
+
items.push({ id: "shell", kind: "walls", at: centre, height: heightOf(shell), planes: shell });
|
|
218
|
+
}
|
|
219
|
+
if (!ribbed)
|
|
220
|
+
rim.forEach((a, i) => {
|
|
221
|
+
const b = rim[(i + 1) % steps];
|
|
222
|
+
const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
223
|
+
if (long < 1)
|
|
224
|
+
return;
|
|
225
|
+
const high = lift + (real.wall ?? wallAt(real, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]));
|
|
226
|
+
planes.push({ ...shy(wall(`shell-${i}`, "wall", a[0], a[1], b[0], b[1], 0, high, `<rect class="${real.ivy === true ? "u-shell u-ivy" : real.brick === true ? "u-shell u-brick" : "u-shell"}" width="${n(long)}" height="${n(high)}"/>${real.podium === undefined ? "" : podiumBands(long, high, real.podium)}`), centre), fadeTo: 0.02 });
|
|
227
|
+
});
|
|
113
228
|
if (parts.walls && planes.length > 0)
|
|
114
229
|
items.push({ id: "shell", kind: "walls", at: centre, height: Math.max(...planes.map((p) => p.o[2])), planes });
|
|
230
|
+
if (parts.walls && real.towers !== undefined) {
|
|
231
|
+
const towers = real.towers.flatMap((tower, i) => endTowerPlanes(tower, i, lift, c));
|
|
232
|
+
if (towers.length > 0)
|
|
233
|
+
items.push({ id: "endtower", kind: "walls", at: centre, height: Math.max(...real.towers.map((t) => t.height)) + lift, planes: towers });
|
|
234
|
+
}
|
|
115
235
|
if (parts.roof && real.roof === "canopy" && real.canopies !== undefined) {
|
|
116
236
|
const planes = canopyRoof(real.canopies, c, lift, steps);
|
|
117
237
|
if (planes.length > 0)
|
|
@@ -119,7 +239,17 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
|
|
|
119
239
|
return items;
|
|
120
240
|
}
|
|
121
241
|
if (parts.roof && real.roof === "dome" && real.dome !== undefined) {
|
|
122
|
-
|
|
242
|
+
// The cables run from the tower in a fan across the roof, from the crown out to either side of it, down to the ring.
|
|
243
|
+
const rim = real.dome.rim;
|
|
244
|
+
const mid = [rim.reduce((t, p) => t + p[0], 0) / rim.length, rim.reduce((t, p) => t + p[1], 0) / rim.length];
|
|
245
|
+
const reach = Math.max(...rim.map((p) => Math.hypot(p[0] - mid[0], p[1] - mid[1]))) / 2.4;
|
|
246
|
+
const across = real.tower === undefined ? [0, 0] : [-(real.tower.top[1] - real.tower.base[1]), real.tower.top[0] - real.tower.base[0]];
|
|
247
|
+
const long = Math.hypot(across[0], across[1]) || 1;
|
|
248
|
+
const cables = [-1, -0.5, 0, 0.5, 1].map((f) => [mid[0] + (across[0] / long) * reach * f, mid[1] + (across[1] / long) * reach * f, lift + real.dome.crown - (real.dome.crown - real.dome.low) * 0.9 * Math.abs(f) ** 1.6]);
|
|
249
|
+
// Where the tower's axis meets the building's rim, seen from the top: how far the foot's front is from the top along the axis.
|
|
250
|
+
const hit = real.tower === undefined ? undefined : firstHit(real.footprint, real.tower.top, Math.atan2(real.tower.base[1] - real.tower.top[1], real.tower.base[0] - real.tower.top[0]));
|
|
251
|
+
const rimRun = real.tower === undefined || hit === undefined ? undefined : Math.hypot(hit[0] - real.tower.top[0], hit[1] - real.tower.top[1]);
|
|
252
|
+
const planes = [...(real.dome.ribs === undefined ? domeRoof(real.dome, lift, steps) : ribbedRoof(real, mid, lift, steps)), ...(real.tower === undefined ? [] : towerPlanes(real.tower, lift, cables, rimRun))];
|
|
123
253
|
if (planes.length > 0)
|
|
124
254
|
items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(planes), planes });
|
|
125
255
|
return items;
|
|
@@ -142,7 +272,7 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
|
|
|
142
272
|
const angle = (2 * Math.PI * k) / steps;
|
|
143
273
|
return crown === undefined ? rayAt(top.hole, c[0], c[1], angle) : firstHit(crown.opening, c, angle) ?? rayAt(top.hole, c[0], c[1], angle);
|
|
144
274
|
});
|
|
145
|
-
const rise = real.roof === "saddle" || real.roof === "cable" ? 0 : LID_RISE;
|
|
275
|
+
const rise = real.roof === "saddle" || real.roof === "barrel" || real.roof === "cable" ? 0 : LID_RISE;
|
|
146
276
|
const zOf = (p, inward) => lift + roofAt(real, p) + (inward ? (crown === undefined ? rise : crown.rise) : 0);
|
|
147
277
|
for (let k = 0; k < steps; k += 1) {
|
|
148
278
|
const [a, b] = [rim[k], rim[(k + 1) % steps]];
|
|
@@ -228,7 +358,7 @@ function span(poly, from, angle) {
|
|
|
228
358
|
* A piece of roof between two rays: the quad `[inner a, inner b, outer b, outer a]` at the heights given, as a plane that gives way to the eye. A quad whose edges are not parallel is not flat at those heights, so
|
|
229
359
|
* with `exact` it is two triangles, each a plane through its corners.
|
|
230
360
|
*/
|
|
231
|
-
function piece(id, quad, heights, c, cables, exact = false) {
|
|
361
|
+
function piece(id, quad, heights, c, cables, exact = false, cls = "u-roof") {
|
|
232
362
|
const [ai, bi, b, a] = quad;
|
|
233
363
|
const [ex, ey] = [b[0] - a[0], b[1] - a[1]];
|
|
234
364
|
const el = Math.hypot(ex, ey);
|
|
@@ -238,7 +368,7 @@ function piece(id, quad, heights, c, cables, exact = false) {
|
|
|
238
368
|
const slope = slopeThrough(poly.map((p, i) => [p[0], p[1], hs[i]]));
|
|
239
369
|
const mid = [poly.reduce((s, p) => s + p[0], 0) / poly.length, poly.reduce((s, p) => s + p[1], 0) / poly.length];
|
|
240
370
|
const away = Math.hypot(mid[0] - c[0], mid[1] - c[1]) || 1;
|
|
241
|
-
const paint = (corners) => `<polygon class="
|
|
371
|
+
const paint = (corners) => `<polygon class="${cls}" points="${pointList(corners)}"/>${cable ? `<path class="u-cable" d="M${n(corners[0][0])} ${n(corners[0][1])}L${n(corners[3][0])} ${n(corners[3][1])}"/>` : ""}`;
|
|
242
372
|
const plane = raked(name, "roof", poly, slope, [ex / el, ey / el], paint);
|
|
243
373
|
return plane === undefined ? [] : [{ ...plane, shy: [(mid[0] - c[0]) / away, (mid[1] - c[1]) / away] }];
|
|
244
374
|
};
|
|
@@ -248,24 +378,69 @@ function piece(id, quad, heights, c, cables, exact = false) {
|
|
|
248
378
|
}
|
|
249
379
|
/** How far a canopy's low edge, the field's side of it, stands under its high edge: the OpenStreetMap roof's `roof:height` is the rise, and the canopy rises to the back of the stands (ours: the sources do not say which way it slopes). */
|
|
250
380
|
export const CANOPY_NOTE = "low over the field, high over the back of the stands";
|
|
381
|
+
/** How many strips a canopy is cut into from the field's side to the back, so that a curved roof is a curve (ours: the drawing's pieces are flat). */
|
|
382
|
+
export const CANOPY_STRIPS = 3;
|
|
383
|
+
/** From this many rays round the middle on, a canopy is cut by every other one. */
|
|
384
|
+
export const CANOPY_FINE = 48;
|
|
385
|
+
/** The share of a canopy's rise at a share of the way from the field's side to the back: a straight slope, or for OpenStreetMap's `roof:shape=round` a convex arc, a quarter of a circle, steep at the field's edge and levelling over the back of the stands. */
|
|
386
|
+
export function canopyRiseAt(shape, t) {
|
|
387
|
+
return shape === "round" ? Math.sin((Math.max(0, Math.min(1, t)) * Math.PI) / 2) : t;
|
|
388
|
+
}
|
|
389
|
+
const between = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
251
390
|
/**
|
|
252
|
-
* Canopies over some of the stands (Lumen Field's two, one each side of the field): each OpenStreetMap roof outline is cut by the rays from the middle of the field into the strips between them,
|
|
253
|
-
* the field low
|
|
391
|
+
* Canopies over some of the stands (Lumen Field's two, one each side of the field): each OpenStreetMap roof outline is cut by the rays from the middle of the field into the strips between them, and each strip across, from
|
|
392
|
+
* the edge towards the field, low, to the edge towards the back of the stands, `rise` higher, into `CANOPY_STRIPS` pieces that follow the roof's shape: a straight slope, or a round arc where OpenStreetMap says `round`.
|
|
254
393
|
*/
|
|
255
394
|
function canopyRoof(canopies, c, lift, steps) {
|
|
256
395
|
const planes = [];
|
|
257
396
|
canopies.forEach((canopy, ci) => {
|
|
258
397
|
const spans = Array.from({ length: steps }, (_, k) => span(canopy.ring, c, (2 * Math.PI * k) / steps));
|
|
259
|
-
|
|
260
|
-
|
|
398
|
+
// Every other ray of a fine cut: a canopy is long and gently curved, and each ray is a piece across its three strips (the plane budget of a district counts them).
|
|
399
|
+
const stride = steps >= CANOPY_FINE ? 2 : 1;
|
|
400
|
+
for (let k = 0; k < steps; k += stride) {
|
|
401
|
+
const [s, t] = [spans[k], spans[(k + stride) % steps]];
|
|
261
402
|
if (s === undefined || t === undefined)
|
|
262
403
|
continue;
|
|
263
|
-
|
|
264
|
-
|
|
404
|
+
for (let j = 0; j < CANOPY_STRIPS; j += 1) {
|
|
405
|
+
const [u0, u1] = [j / CANOPY_STRIPS, (j + 1) / CANOPY_STRIPS];
|
|
406
|
+
const [z0, z1] = [lift + canopy.low + canopy.rise * canopyRiseAt(canopy.shape, u0), lift + canopy.low + canopy.rise * canopyRiseAt(canopy.shape, u1)];
|
|
407
|
+
planes.push(...piece(`roof-c${ci}-${k}-${j}`, [between(s[0], s[1], u0), between(t[0], t[1], u0), between(t[0], t[1], u1), between(s[0], s[1], u1)], [z0, z0, z1, z1], c, false, false, "u-roof u-canopy"));
|
|
408
|
+
}
|
|
265
409
|
}
|
|
266
410
|
});
|
|
267
411
|
return planes;
|
|
268
412
|
}
|
|
413
|
+
/** How wide the scoreboard on a tower's face is and how tall, as shares of the face, and how far it stands out from it, in feet (ours: the article says the scoreboard is vertical, not how large). */
|
|
414
|
+
export const TOWER_BOARD = { wide: 0.62, tall: 0.46, from: 0.55, out: 0.4 };
|
|
415
|
+
/**
|
|
416
|
+
* A tower standing apart from the bowl: a box on its outline, four faces and a flat top, that gives way to the eye as a wall does (a tall tower between the eye and the field is cut away, whole from above), with the scoreboard
|
|
417
|
+
* on the face towards the field, a dark panel high on it and divided into its rows of lamps.
|
|
418
|
+
*/
|
|
419
|
+
export function endTowerPlanes(tower, index, lift, centre) {
|
|
420
|
+
const xs = tower.ring.map((p) => p[0]);
|
|
421
|
+
const ys = tower.ring.map((p) => p[1]);
|
|
422
|
+
const [x0, x1, y0, y1] = [Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys)];
|
|
423
|
+
const top = lift + tower.height;
|
|
424
|
+
const parts = box(`endtower-${index}`, "wall", { x0, y0, x1, y1 }, top, "u-endtower");
|
|
425
|
+
const planes = parts.map((p) => (p.id.endsWith("-top") ? p : { ...shy(p, centre), fadeTo: 0.25 }));
|
|
426
|
+
// The scoreboard: on the face that looks at the field, a little proud of it.
|
|
427
|
+
const [fx, fy] = tower.face;
|
|
428
|
+
const [w, h] = fx !== 0 ? [y1 - y0, top] : [x1 - x0, top];
|
|
429
|
+
const [bw, bh] = [w * TOWER_BOARD.wide, h * TOWER_BOARD.tall];
|
|
430
|
+
const zTop = top * TOWER_BOARD.from + bh;
|
|
431
|
+
const svg = `<rect class="u-endtower-board" width="${n(bw)}" height="${n(bh)}"/><path class="u-endtower-rows" d="${Array.from({ length: 9 }, (_, k) => `M0 ${n(((k + 1) * bh) / 10)}H${n(bw)}`).join("")}"/>`;
|
|
432
|
+
if (fx !== 0) {
|
|
433
|
+
const x = fx > 0 ? x1 + TOWER_BOARD.out : x0 - TOWER_BOARD.out;
|
|
434
|
+
const mid = (y0 + y1) / 2;
|
|
435
|
+
planes.push({ ...shy(wall(`endtower-${index}-board`, "wall", x, mid - bw / 2, x, mid + bw / 2, zTop - bh, zTop, svg), centre), fadeTo: 0.25 });
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const y = fy > 0 ? y1 + TOWER_BOARD.out : y0 - TOWER_BOARD.out;
|
|
439
|
+
const mid = (x0 + x1) / 2;
|
|
440
|
+
planes.push({ ...shy(wall(`endtower-${index}-board`, "wall", mid - bw / 2, y, mid + bw / 2, y, zTop - bh, zTop, svg), centre), fadeTo: 0.25 });
|
|
441
|
+
}
|
|
442
|
+
return planes;
|
|
443
|
+
}
|
|
269
444
|
/** How many rings of pieces an air-supported roof is drawn in, from the rim in to the crown (ours: the roof is smooth, the drawing is faceted). */
|
|
270
445
|
export const DOME_RINGS = 4;
|
|
271
446
|
/**
|
|
@@ -315,7 +490,7 @@ const PANEL_THICK = 7;
|
|
|
315
490
|
* building they park on, as slabs at the rim's height.
|
|
316
491
|
*/
|
|
317
492
|
function panelsRoof(panels, c, lift, steps, open) {
|
|
318
|
-
const planes = [];
|
|
493
|
+
const planes = [...panelRails(panels, c, lift)];
|
|
319
494
|
if (!open) {
|
|
320
495
|
const rim = Array.from({ length: steps }, (_, k) => firstHit(panels.cover, c, (2 * Math.PI * k) / steps) ?? c);
|
|
321
496
|
const inner = rim.map((p) => [c[0] + (p[0] - c[0]) * PANEL_CROWN, c[1] + (p[1] - c[1]) * PANEL_CROWN]);
|
|
@@ -344,6 +519,55 @@ function panelsRoof(panels, c, lift, steps, open) {
|
|
|
344
519
|
});
|
|
345
520
|
return planes;
|
|
346
521
|
}
|
|
522
|
+
/** How wide a rail girder is, how thick, how wide a column under it is, and how many columns stand under each rail, in feet (ours: the article says the roof runs on rails on the north and the south side, not what holds them up). */
|
|
523
|
+
export const RAIL = { wide: 12, thick: 16, column: 15, columns: 4, inset: 24 };
|
|
524
|
+
/** A prism over a four-cornered outline, from `z0` to `z1`: its sides and its top, in the roof's pod tone (a dark steel). */
|
|
525
|
+
function prism(id, ring, z0, z1, c) {
|
|
526
|
+
const planes = [];
|
|
527
|
+
ring.forEach((a, i) => {
|
|
528
|
+
const b = ring[(i + 1) % ring.length];
|
|
529
|
+
const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
530
|
+
if (long < 0.5)
|
|
531
|
+
return;
|
|
532
|
+
planes.push({ ...shy(wall(`${id}-${i}`, "roof", a[0], a[1], b[0], b[1], z0, z1, `<rect class="u-rail" width="${n(long)}" height="${n(z1 - z0)}"/>`), c), fadeTo: 0.3 });
|
|
533
|
+
});
|
|
534
|
+
const top = raked(`${id}-top`, "roof", [...ring], { x0: ring[0][0], y0: ring[0][1], z0: z1, gx: 0, gy: 0 }, [1, 0], (corners) => `<polygon class="u-rail" points="${pointList(corners)}"/>`);
|
|
535
|
+
if (top !== undefined)
|
|
536
|
+
planes.push(top);
|
|
537
|
+
return planes;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The rails a retractable ballpark roof runs on, along its north and south sides (the article: 128 wheels on rails there), as two girders at the roof's lowest height, each on a row of columns from the ground, and running
|
|
541
|
+
* on over the strip where the sections rest. The rails run the way the sections park, from the cover's middle to where they rest; a girder lies a little inside each side of the cover.
|
|
542
|
+
*/
|
|
543
|
+
export function panelRails(panels, c, lift) {
|
|
544
|
+
const mean = (ring) => [ring.reduce((s, p) => s + p[0], 0) / ring.length, ring.reduce((s, p) => s + p[1], 0) / ring.length];
|
|
545
|
+
const park = mean(panels.parked);
|
|
546
|
+
const length = Math.hypot(park[0] - c[0], park[1] - c[1]);
|
|
547
|
+
if (length < 1)
|
|
548
|
+
return [];
|
|
549
|
+
const e = [(park[0] - c[0]) / length, (park[1] - c[1]) / length];
|
|
550
|
+
const m = [-e[1], e[0]];
|
|
551
|
+
const along = (p) => (p[0] - c[0]) * e[0] + (p[1] - c[1]) * e[1];
|
|
552
|
+
const across = (p) => (p[0] - c[0]) * m[0] + (p[1] - c[1]) * m[1];
|
|
553
|
+
const [a0, a1] = [Math.min(...panels.cover.map(along)), Math.max(...panels.parked.map(along), ...panels.cover.map(along))];
|
|
554
|
+
const [m0, m1] = [Math.min(...panels.cover.map(across)) + RAIL.inset, Math.max(...panels.cover.map(across)) - RAIL.inset];
|
|
555
|
+
const at = (u, v) => [c[0] + e[0] * u + m[0] * v, c[1] + e[1] * u + m[1] * v];
|
|
556
|
+
const planes = [];
|
|
557
|
+
const z = lift + panels.bottom;
|
|
558
|
+
[m0, m1].forEach((v, side) => {
|
|
559
|
+
const w = RAIL.wide / 2;
|
|
560
|
+
planes.push(...prism(`rail-${side}`, [at(a0, v - w), at(a1, v - w), at(a1, v + w), at(a0, v + w)], z - RAIL.thick, z, c));
|
|
561
|
+
for (let k = 0; k < RAIL.columns; k += 1) {
|
|
562
|
+
const u = a0 + ((k + 0.5) * (a1 - a0)) / RAIL.columns;
|
|
563
|
+
const h = RAIL.column / 2;
|
|
564
|
+
// A column is two strips crossed, as a post is (`pole`): seen from every side with two planes and not five.
|
|
565
|
+
const [x, y] = at(u, v);
|
|
566
|
+
planes.push(...pole(`rail-${side}-leg-${k}`, "roof", x, y, lift, z - RAIL.thick, h * 2, "u-rail"));
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return planes;
|
|
570
|
+
}
|
|
347
571
|
/** The pod in the middle of a cable roof's opening, as a share of the opening's size, and how thick it is, in feet (ours: the article says the fabric gathers into a pod above the video board, not how large it is). */
|
|
348
572
|
export const POD = { share: 0.24, thick: 9 };
|
|
349
573
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Pt, RealShell } from "./crowd.ts";
|
|
2
|
+
import { type Plane } from "./turn.kit.ts";
|
|
3
|
+
/** The rib's outer face's share of the way to the middle at a share `t` of the ring's height: a wing that stands out at the foot and sweeps in as it rises. */
|
|
4
|
+
export declare const ribShare: (t: number) => number;
|
|
5
|
+
/** `count` points equally spaced along a closed outline, the first at the point nearest `near` (the tower's side, so that the ribs there begin at the tower). */
|
|
6
|
+
export declare function spaced(outline: readonly Pt[], count: number, near?: Pt): Pt[];
|
|
7
|
+
/** The height of the roof panels over a point that stands the share `s` of the way from the middle to the outline (above the surface, in feet), for a ring at `ring` feet: the panels' surface, which the stands under them must stay below. 0 where the point is outside the wall's sweep. */
|
|
8
|
+
export declare function ribbedSurfaceAt(s: number, ring: number): number;
|
|
9
|
+
/**
|
|
10
|
+
* The shell's walls: the podium and the glazed stack of each bay (one plane), and the ribs between them that rise from the ground and sweep in to the ring. Each rib is its profile on a plane each side of its middle (a curved wing) and its outer
|
|
11
|
+
* face, one trapezoid, which is a small part (`fascia`) that the lower tiers leave out, as they leave out the ring's lip.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ribbedWalls(real: RealShell, c: Pt, lift: number, steps: number): Plane[];
|
|
14
|
+
/**
|
|
15
|
+
* The shell's roof: over each bay two pieces of the white roof panels, rising from the glazed stack to the ring, the ring's lip, and over the ring the membrane in two rings and a flat crown, its pieces crossed by the line of the
|
|
16
|
+
* cable net it is stretched on.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ribbedRoof(real: RealShell, c: Pt, lift: number, steps: number): Plane[];
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { n, plane } from "./turn.kit.js";
|
|
2
|
+
/** How far the ring's outer edge stands in from the outline, as a share of the way to the middle (ours: the sources give the roof's kind and its height, not its profile; the profile is drawn from photographs). */
|
|
3
|
+
const RING = 0.775;
|
|
4
|
+
/** How wide a rib is at its foot and at its top (the width of its outer face), how deep (how far it stands proud of the wall between two ribs) at its foot and at its top, in feet (ours). */
|
|
5
|
+
const RIB = { wide: [48, 24], deep: [34, 14], flare: 0.225, bend: 2.63, side: 12 };
|
|
6
|
+
/** How much of a wall is left where it stands between the eye and the field: the glazed bays give way almost entirely, the ribs stay as a ghost, so that the shell is still known from the near side (ours). */
|
|
7
|
+
const BAY_FADE = 0.12;
|
|
8
|
+
const RIB_FADE = 0.3;
|
|
9
|
+
/** How far the bays are set back from the ribs' outer face at the foot and at the ring, as a share of the way to the middle (ours). */
|
|
10
|
+
const SET_BACK = [0.075, 0.028];
|
|
11
|
+
/** The height of the podium, of the glazed stack's top and of the ring's top, as shares of the ring's height, and of the ring's lip over it (ours, from photographs). */
|
|
12
|
+
const LEVELS = { podium: 0.13, glass: 0.46, ring: 1, lip: 1.05 };
|
|
13
|
+
/** The membrane's ring, from the ring in to the crown's edge, as shares of the way to the middle (ours: one ring of long pieces, a shallow cone, to keep the planes few). */
|
|
14
|
+
const MEMBRANE = [RING, 0.16];
|
|
15
|
+
const scaled = (c, p, s) => [c[0] + (p[0] - c[0]) * s, c[1] + (p[1] - c[1]) * s];
|
|
16
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
17
|
+
/** The rib's outer face's share of the way to the middle at a share `t` of the ring's height: a wing that stands out at the foot and sweeps in as it rises. */
|
|
18
|
+
export const ribShare = (t) => 1 - RIB.flare * Math.min(1, Math.max(0, t)) ** RIB.bend;
|
|
19
|
+
/** `count` points equally spaced along a closed outline, the first at the point nearest `near` (the tower's side, so that the ribs there begin at the tower). */
|
|
20
|
+
export function spaced(outline, count, near) {
|
|
21
|
+
const edges = outline.map((p, i) => [p, outline[(i + 1) % outline.length]]);
|
|
22
|
+
const lengths = edges.map(([a, b]) => Math.hypot(b[0] - a[0], b[1] - a[1]));
|
|
23
|
+
const total = lengths.reduce((s, l) => s + l, 0);
|
|
24
|
+
const at = (distance) => {
|
|
25
|
+
let d = ((distance % total) + total) % total;
|
|
26
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
27
|
+
if (d <= lengths[i] || i === edges.length - 1) {
|
|
28
|
+
const t = lengths[i] === 0 ? 0 : Math.min(1, d / lengths[i]);
|
|
29
|
+
return [lerp(edges[i][0][0], edges[i][1][0], t), lerp(edges[i][0][1], edges[i][1][1], t)];
|
|
30
|
+
}
|
|
31
|
+
d -= lengths[i];
|
|
32
|
+
}
|
|
33
|
+
return outline[0];
|
|
34
|
+
};
|
|
35
|
+
let start = 0;
|
|
36
|
+
if (near !== undefined) {
|
|
37
|
+
let best = Infinity;
|
|
38
|
+
let run = 0;
|
|
39
|
+
edges.forEach(([a], i) => {
|
|
40
|
+
const gap = Math.hypot(a[0] - near[0], a[1] - near[1]);
|
|
41
|
+
if (gap < best)
|
|
42
|
+
[best, start] = [gap, run];
|
|
43
|
+
run += lengths[i];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return Array.from({ length: count }, (_, k) => at(start + (total * k) / count));
|
|
47
|
+
}
|
|
48
|
+
function cutOf(real, c, steps) {
|
|
49
|
+
const dome = real.dome;
|
|
50
|
+
const count = steps >= 56 ? dome.ribs.count : Math.round(dome.ribs.count / 2);
|
|
51
|
+
// The tower stands on the side its axis comes from: the ribs begin there.
|
|
52
|
+
return { points: spaced(real.footprint, count, real.tower?.base), c, ring: dome.low, crown: dome.crown };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One trapezoid of the shell: between the outline's points `a` and `b`, from the share `s0` of the way to the middle at the height `z0` to the share `s1` at `z1`, drawn in from the rib at each end by `m0` feet at its foot and `m1` at its top
|
|
56
|
+
* (the two chords stay parallel, so it is flat). `paint` draws on it, given its length up the slope, its width at the foot and its top's share of that.
|
|
57
|
+
*/
|
|
58
|
+
function band(id, kind, c, a, b, [s0, z0], [s1, z1], paint, [m0, m1] = [0, 0]) {
|
|
59
|
+
const long = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
|
|
60
|
+
const e = [(b[0] - a[0]) / long, (b[1] - a[1]) / long];
|
|
61
|
+
const [a0, a1] = [scaled(c, a, s0), scaled(c, a, s1)];
|
|
62
|
+
const o = [a0[0] + e[0] * m0, a0[1] + e[1] * m0, z0];
|
|
63
|
+
const top = [a1[0] + e[0] * m1, a1[1] + e[1] * m1, z1];
|
|
64
|
+
const [w0, w1] = [Math.max(1, long * s0 - 2 * m0), Math.max(1, long * s1 - 2 * m1)];
|
|
65
|
+
const v = [e[0] * w0, e[1] * w0, 0];
|
|
66
|
+
const u = [top[0] - o[0], top[1] - o[1], z1 - z0];
|
|
67
|
+
return plane(id, kind, o, u, v, paint(Math.hypot(...u), w0, w1 / w0));
|
|
68
|
+
}
|
|
69
|
+
/** How far a piece between two ribs stands back from the rib's middle, in feet: the rib's side planes stand `RIB.side` out from it, and the piece meets them. */
|
|
70
|
+
const ribMargin = () => RIB.side + 0.4;
|
|
71
|
+
const shape = (cls) => (up, wide, r) => `<polygon class="${cls}" points="0 0 ${n(up)} 0 ${n(up)} ${n(wide * r)} 0 ${n(wide)}"/>`;
|
|
72
|
+
/** The bay's facade: the podium at its foot (a low dark band of openings, up to the share `low` of the plane's height) and over it the glazed stack, three bands of glass between thin ledges. */
|
|
73
|
+
const facade = (low) => (up, wide, r) => {
|
|
74
|
+
const edge = (x) => wide * (1 - (1 - r) * (x / up));
|
|
75
|
+
const band = (f0, f1, m) => {
|
|
76
|
+
const [x0, x1] = [f0 * up, f1 * up];
|
|
77
|
+
return `<polygon class="u-glass" points="${n(x0)} ${n(m * edge(x0))} ${n(x1)} ${n(m * edge(x1))} ${n(x1)} ${n((1 - m) * edge(x1))} ${n(x0)} ${n((1 - m) * edge(x0))}"/>`;
|
|
78
|
+
};
|
|
79
|
+
const stack = [[0.06, 0.30], [0.38, 0.62], [0.70, 0.94]].map(([f0, f1]) => band(low + (1 - low) * f0, low + (1 - low) * f1, 0.03));
|
|
80
|
+
return `${shape("u-bay")(up, wide, r)}${band(0.35 * low, 0.8 * low, 0.04)}${stack.join("")}`;
|
|
81
|
+
};
|
|
82
|
+
/** The share of the way to the middle of the wall between two ribs at a share `t` of the ring's height: the ribs' outer face, set back. */
|
|
83
|
+
const bayShare = (t) => ribShare(t) - lerp(SET_BACK[0], SET_BACK[1], Math.min(1, t));
|
|
84
|
+
/** The height of the roof panels over a point that stands the share `s` of the way from the middle to the outline (above the surface, in feet), for a ring at `ring` feet: the panels' surface, which the stands under them must stay below. 0 where the point is outside the wall's sweep. */
|
|
85
|
+
export function ribbedSurfaceAt(s, ring) {
|
|
86
|
+
let [lo, hi] = [0, 1];
|
|
87
|
+
for (let i = 0; i < 40; i += 1) {
|
|
88
|
+
const mid = (lo + hi) / 2;
|
|
89
|
+
if (bayShare(mid) > s)
|
|
90
|
+
lo = mid;
|
|
91
|
+
else
|
|
92
|
+
hi = mid;
|
|
93
|
+
}
|
|
94
|
+
return ring * LEVELS.lip * lo;
|
|
95
|
+
}
|
|
96
|
+
/** The outward unit direction at a point of the outline, for the planes that give way to the eye. */
|
|
97
|
+
const outward = (c, p) => {
|
|
98
|
+
const d = Math.hypot(p[0] - c[0], p[1] - c[1]) || 1;
|
|
99
|
+
return [(p[0] - c[0]) / d, (p[1] - c[1]) / d];
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The shell's walls: the podium and the glazed stack of each bay (one plane), and the ribs between them that rise from the ground and sweep in to the ring. Each rib is its profile on a plane each side of its middle (a curved wing) and its outer
|
|
103
|
+
* face, one trapezoid, which is a small part (`fascia`) that the lower tiers leave out, as they leave out the ring's lip.
|
|
104
|
+
*/
|
|
105
|
+
export function ribbedWalls(real, c, lift, steps) {
|
|
106
|
+
const cut = cutOf(real, c, steps);
|
|
107
|
+
const planes = [];
|
|
108
|
+
const Z = cut.ring;
|
|
109
|
+
const { points } = cut;
|
|
110
|
+
const t1 = LEVELS.glass;
|
|
111
|
+
const t0 = LEVELS.podium;
|
|
112
|
+
points.forEach((a, k) => {
|
|
113
|
+
const b = points[(k + 1) % points.length];
|
|
114
|
+
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
115
|
+
const shy = outward(c, mid);
|
|
116
|
+
const [sp0, sg] = [bayShare(0), bayShare(t1 / LEVELS.lip)];
|
|
117
|
+
planes.push({ ...band(`bay-${k}`, "wall", c, a, b, [sp0, lift], [sg, lift + Z * t1], facade(t0 / t1), [ribMargin(), ribMargin()]), shy, fadeTo: BAY_FADE });
|
|
118
|
+
planes.push(...rib(`rib-${k}`, c, a, Z, lift, shy));
|
|
119
|
+
});
|
|
120
|
+
return planes;
|
|
121
|
+
}
|
|
122
|
+
/** One rib at the outline's point `a`: its curved profile on the plane over its radius, and its outer face in two pieces. */
|
|
123
|
+
function rib(id, c, a, Z, lift, shy) {
|
|
124
|
+
const lip = LEVELS.lip;
|
|
125
|
+
const R = Math.hypot(a[0] - c[0], a[1] - c[1]) || 1;
|
|
126
|
+
const [dx, dy] = [(a[0] - c[0]) / R, (a[1] - c[1]) / R];
|
|
127
|
+
// The plane stands over the radius from the ring's share to the foot's, its x running outwards and its y down from the top.
|
|
128
|
+
const [s0, top] = [RING - 0.02, Z * lip];
|
|
129
|
+
const o = [c[0] + dx * R * s0, c[1] + dy * R * s0, lift + top];
|
|
130
|
+
const wide = R * (1 - s0);
|
|
131
|
+
const steps = 8;
|
|
132
|
+
const t = (i) => i / steps;
|
|
133
|
+
const out = Array.from({ length: steps + 1 }, (_, i) => [R * (ribShare(t(i)) - s0), top - Z * lip * t(i)]);
|
|
134
|
+
const inner = out.map(([x, y], i) => [x - lerp(RIB.deep[0], RIB.deep[1], t(i)), y]);
|
|
135
|
+
const outline = [...out, ...inner.reverse()];
|
|
136
|
+
// The two sides of the rib: the same curved profile on a plane each side of its middle, so that the rib is closed from every side and the pieces between ribs meet it.
|
|
137
|
+
const [tx, ty] = [-dy, dx];
|
|
138
|
+
const outlineText = outline.map(([x, y]) => `${n(x)},${n(y)}`).join(" ");
|
|
139
|
+
const planes = [-1, 1].map((side) => {
|
|
140
|
+
const at = [o[0] + tx * RIB.side * side, o[1] + ty * RIB.side * side, o[2]];
|
|
141
|
+
return { ...plane(`${id}${side < 0 ? "-l" : "-r"}`, "wall", at, [dx * wide, dy * wide, 0], [0, 0, -top], `<polygon class="u-rib" points="${outlineText}"/>`), shy, fadeTo: RIB_FADE };
|
|
142
|
+
});
|
|
143
|
+
// The outer face: one trapezoid along the wing's outer edge, from its foot to the ring, as wide as the rib is at each end.
|
|
144
|
+
const at = (tt, side) => {
|
|
145
|
+
const half = lerp(RIB.wide[0], RIB.wide[1], tt) / 2;
|
|
146
|
+
const share = ribShare(tt);
|
|
147
|
+
return [c[0] + dx * R * share + tx * half * side, c[1] + dy * R * share + ty * half * side, lift + Z * lip * tt];
|
|
148
|
+
};
|
|
149
|
+
{
|
|
150
|
+
const [p0, p1, p2] = [at(0, -1), at(1, -1), at(0, 1)];
|
|
151
|
+
const [u, v] = [[p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]], [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]]];
|
|
152
|
+
const [lu, lv] = [Math.hypot(...u), Math.hypot(...v)];
|
|
153
|
+
const r = RIB.wide[1] / RIB.wide[0];
|
|
154
|
+
planes.push({ ...plane(`${id}-f`, "fascia", p0, u, v, `<polygon class="u-rib" points="0 0 ${n(lu)} 0 ${n(lu)} ${n(lv * r)} 0 ${n(lv)}"/>`), shy, fadeTo: RIB_FADE });
|
|
155
|
+
}
|
|
156
|
+
return planes;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The shell's roof: over each bay two pieces of the white roof panels, rising from the glazed stack to the ring, the ring's lip, and over the ring the membrane in two rings and a flat crown, its pieces crossed by the line of the
|
|
160
|
+
* cable net it is stretched on.
|
|
161
|
+
*/
|
|
162
|
+
export function ribbedRoof(real, c, lift, steps) {
|
|
163
|
+
const cut = cutOf(real, c, steps);
|
|
164
|
+
const planes = [];
|
|
165
|
+
const { points, ring: Z, crown } = cut;
|
|
166
|
+
const lip = LEVELS.lip;
|
|
167
|
+
const roofAt = (s) => lift + Z * lip + (crown - Z * lip) * (1 - (s / RING) ** 2);
|
|
168
|
+
const across = (id, a, b, from, to, cls, net, margin, kind = "roof") => {
|
|
169
|
+
const paint = (up, wide, r) => `${shape(cls)(up, wide, r)}${net ? `<path class="u-cable" d="M0 0L${n(up)} ${n(wide * r)}"/>` : ""}`;
|
|
170
|
+
const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
171
|
+
return { ...band(id, kind, c, a, b, from, to, paint, margin), shy: outward(c, mid), fadeTo: 0.2 };
|
|
172
|
+
};
|
|
173
|
+
points.forEach((a, k) => {
|
|
174
|
+
const b = points[(k + 1) % points.length];
|
|
175
|
+
const s = (t) => bayShare(t / lip);
|
|
176
|
+
// The roof panels climb from the glazed stack's top to the ring's outer edge in two pieces; the ring's lip stands over it.
|
|
177
|
+
const mids = [[s(LEVELS.glass), lift + Z * LEVELS.glass], [s(LEVELS.ring), lift + Z * LEVELS.ring], [RING, lift + Z * lip]];
|
|
178
|
+
for (let j = 0; j < 2; j += 1)
|
|
179
|
+
planes.push(across(`roof-p${j}-${k}`, a, b, mids[j], mids[j + 1], j === 1 ? "u-rib" : "u-dome", false, [ribMargin(), ribMargin()], j === 1 ? "fascia" : "roof"));
|
|
180
|
+
// The membrane: rings in from the ring to the crown.
|
|
181
|
+
for (let j = 0; j < MEMBRANE.length - 1; j += 1)
|
|
182
|
+
planes.push(across(`roof-${j}-${k}`, a, b, [MEMBRANE[j], roofAt(MEMBRANE[j])], [MEMBRANE[j + 1], roofAt(MEMBRANE[j + 1])], "u-dome", j === 0, [0, 0]));
|
|
183
|
+
});
|
|
184
|
+
// The crown: a flat cap over the middle.
|
|
185
|
+
const top = points.map((p) => scaled(c, p, MEMBRANE[MEMBRANE.length - 1]));
|
|
186
|
+
const cap = capOver(top, lift + crown);
|
|
187
|
+
if (cap !== undefined)
|
|
188
|
+
planes.push(cap);
|
|
189
|
+
return planes;
|
|
190
|
+
}
|
|
191
|
+
/** A flat polygon at the height `z`, as one plane over its bounding box (the crown). */
|
|
192
|
+
function capOver(poly, z) {
|
|
193
|
+
if (poly.length < 3)
|
|
194
|
+
return undefined;
|
|
195
|
+
const [x0, x1] = [Math.min(...poly.map((p) => p[0])), Math.max(...poly.map((p) => p[0]))];
|
|
196
|
+
const [y0, y1] = [Math.min(...poly.map((p) => p[1])), Math.max(...poly.map((p) => p[1]))];
|
|
197
|
+
const svg = `<polygon class="u-dome" points="${poly.map(([x, y]) => `${n(x - x0)},${n(y - y0)}`).join(" ")}"/>`;
|
|
198
|
+
return { ...plane("roof-crown", "roof", [x0, y0, z], [x1 - x0, 0, 0], [0, y1 - y0, 0], svg), fadeTo: 0.2 };
|
|
199
|
+
}
|
|
@@ -5,5 +5,5 @@ export const VENUE = {
|
|
|
5
5
|
"en": { "title": "American Airlines Center", "revision": 1379833787 },
|
|
6
6
|
"seats": { "hockey": 18532, "source": "wikipedia", "wikidata": 19200, "read": "the infobox's figure for ice hockey, 18,532 (19,323 with standing room); Wikidata's one capacity (19,200) is the basketball one" },
|
|
7
7
|
"tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of retractable seating, which the bowl does not draw" },
|
|
8
|
-
"roof": { "kind": "
|
|
8
|
+
"roof": { "kind": "barrel", "basis": "documented", "read": "the article says the roof looks like a Quonset hut, which is a barrel vault: drawn as a round arch over the rink's short axis, highest over its middle line and falling to the walls at the two long sides; the height of the crown (34 feet over the springing) is ours (the article gives none)" },
|
|
9
9
|
};
|
package/dist/venues.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { type Box, type Pt, type RealBowl } from "./crowd.ts";
|
|
|
4
4
|
export declare const VENUE_ATTRIBUTION = "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright";
|
|
5
5
|
/** How a fact is known: `documented` is what a source cited with it says; `plausible` is ours, for a bowl no source describes. */
|
|
6
6
|
export type VenueBasis = "documented" | "plausible";
|
|
7
|
-
/** The kinds of roof the venues have: `fixed` is a lid over the bowl, `saddle` a roof that rises at the two ends and falls at the two sides (the Scotiabank Saddledome's). */
|
|
8
|
-
export type VenueRoof = "fixed" | "saddle";
|
|
7
|
+
/** The kinds of roof the venues have: `fixed` is a lid over the bowl, `saddle` a roof that rises at the two ends and falls at the two sides (the Scotiabank Saddledome's), `barrel` a barrel vault along the rink, a round arch over its short axis (the American Airlines Center's). */
|
|
8
|
+
export type VenueRoof = "fixed" | "saddle" | "barrel";
|
|
9
9
|
/** The NHL's divisions, in the order of the league's own listing: the Western Conference's two, then the Eastern's. A division none of whose arenas has a venue yet has no venues in `venuesIn`. */
|
|
10
10
|
export declare const VENUE_DIVISIONS: readonly ["pacific", "central", "atlantic", "metropolitan"];
|
|
11
11
|
export type VenueDivision = (typeof VENUE_DIVISIONS)[number];
|
package/dist/venues.data.js
CHANGED
|
@@ -97,7 +97,7 @@ export const VENUE_DATA = [
|
|
|
97
97
|
"en": { "title": "American Airlines Center", "revision": 1379833787 },
|
|
98
98
|
"seats": { "hockey": 18532, "source": "wikipedia", "wikidata": 19200, "read": "the infobox's figure for ice hockey, 18,532 (19,323 with standing room); Wikidata's one capacity (19,200) is the basketball one" },
|
|
99
99
|
"tiers": { "count": 2, "basis": "plausible", "read": "no source says how many seating levels there are: two bowls are drawn; the article speaks of retractable seating, which the bowl does not draw" },
|
|
100
|
-
"roof": { "kind": "
|
|
100
|
+
"roof": { "kind": "barrel", "basis": "documented", "read": "the article says the roof looks like a Quonset hut, which is a barrel vault: drawn as a round arch over the rink's short axis, highest over its middle line and falling to the walls at the two long sides; the height of the crown (34 feet over the springing) is ours (the article gives none)" },
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"id": "grand-casino-arena",
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version: the same as package.json's, and a test holds them to each other. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.47.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The package's version: the same as package.json's, and a test holds them to each other. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.47.0";
|