@johnmorrisdotca/kyogijo-stadiums 0.46.0 → 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 +15 -0
- package/dist/crowd.d.ts +4 -1
- package/dist/draw.js +1 -1
- package/dist/stadiums.d.ts +16 -3
- package/dist/stadiums.data.js +1 -1
- package/dist/stadiums.js +12 -4
- package/dist/stadiums.osm.data.js +1 -1
- package/dist/strings.core.d.ts +1 -0
- package/dist/strings.en.js +1 -1
- package/dist/strings.ja.js +1 -1
- package/dist/turn.style.js +7 -3
- package/dist/venue.real.d.ts +5 -5
- package/dist/venue.real.js +82 -68
- package/dist/venue.ribbed.d.ts +18 -0
- package/dist/venue.ribbed.js +199 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ All notable changes to this project are written here. The format follows
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.47.0] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The Olympic Stadium's shell is the building's: 38 ribs, glazed bays and a low white roof (SPX-811).** In place of a plain tall drum under a low lens, the shell is 38 concrete consoles (34 anchored to the ground, 4 fixed to the tower: the French article) round the building's outline, each a curved wing that stands out at its foot and sweeps in to a ring, a glazed bay between each pair (a podium and three bands of glass), white roof panels climbing from the glazing to the ring, and a membrane closing the ring in a low cone, its crown the 52 metres over the field the English article gives. The stands are fitted inside the shell's sweep (`RIBBED_STANDS`), so no row stands out through the roof; the ring's height, the ribs' flare and the bays' profile are ours, from Commons photographs (`src/venue.ribbed.ts`, `RealShell.dome.ribs`, `StadiumRoof` data `ribs` and `top`). Compared with Commons photographs from the plaza, across the river, from Mount Royal and from below the tower (sheets in the backups folder, `2026-10-30-olympic-shell`). Left as it is, honestly: the real ribs are bigger and more curved, the real membrane is pleated and the real tower has about forty cables where the drawing has five.
|
|
15
|
+
- **The Montreal Tower's foot is at the rim, it flares and its back curves.** The shaft is twelve horizontal sections between a steep front, from where the mapped axis meets the building's rim to the observatory, and a back that sweeps down from the top in a long curve and flattens along the ground behind the foot; a fan of five cables replaces the three; no plinth. Height, top and axis are the sources' as before.
|
|
16
|
+
- **How the Olympic Stadium's field lies, to the nearest 45 degrees (SPX-811).** Nothing maps the field and no article gives a bearing, but the English Wikipedia's list of Montreal's ballparks names a street at first base (Pierre-De Coubertin), left field (Sherbrooke), third base (Pie-IX) and right field (Viau), which bound the stadium on its north, south, west and east: home plate is in the north-west corner and centre field to the south-east, 135 degrees. The new basis is `streets` (`StadiumOsm.axis.basis`, `orientation` in the facts, `said.stadiumStreets` in English and Japanese); the drawing says it is an inference and draws the arrow to north. The seats still fit (49,756 of 49,757).
|
|
17
|
+
- **Speed checks per device class (SPX-774).** Three Playwright projects, each a job of its own on the public repository's free CI (`device-desk`: Chromium 1280 by 800, ratio 1, held at `high`; `device-mid-phone`: WebKit, 390 by 844, ratio 3, `medium`; `device-slow-android`: Chromium 412 by 915, ratio 2.6, CPU slowed 6 times, `low`) open every district preset (all twelve, the New York and Montreal ones among them) and the venues page's 3D view and hold each to budgets (`e2e/budgets.device.mjs`, `e2e/device.budgets.json`, `e2e/device.lib.mjs`, docs/quality.md): exact counts that no runner changes (layers, those drawn, elements, the memory the layers take at the device's ratio, the biggest layer; budget = measured plus 15 percent) and a ratio of a step of the turn to the end of its frame over a reference scene measured beside it with the same CPU slowing (budget = twice what it measured, at least one more). The tier is pinned in the address, because `auto` steps down on a slow runner and a step changes what is counted. A test checks that every preset of the page has a budget. About 25 to 45 seconds each on a laptop, one to two minutes with the build on the runner; the three run beside the six demo jobs and add no time to the longest of them. `DEVICE_MEASURE=file.jsonl` writes what each page measured, to make the budgets again.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The Olympic Stadium's district view is 1,096 planes at `high` (775 at `medium`, 424 at `low`), down from 1,111 (870, 519), and 146 more elements (4,503 against 4,357). At the lower tiers the ribs' outer faces and the ring's lip are left out as small parts, and at the coarsest cut there are 19 ribs.
|
|
22
|
+
- The package's `dist/` grows by 17.5 KB (5.4 KB gzipped) of script and data: the new shell builder 12.8 KB, the tower and the shell's hook 2 KB, the stadiums' facts 0.4 KB, the rest style and words; no dependency is added.
|
|
23
|
+
- `scripts/readme-pictures.mjs`: the stadiums page's Olympic picture is taken from the end-on viewpoint, so the tower is seen in profile.
|
|
24
|
+
|
|
10
25
|
## [0.46.0] - 2026-10-30
|
|
11
26
|
|
|
12
27
|
### Added
|
package/dist/crowd.d.ts
CHANGED
|
@@ -139,11 +139,14 @@ export interface RealShell {
|
|
|
139
139
|
top: number;
|
|
140
140
|
parked: Pt[];
|
|
141
141
|
};
|
|
142
|
-
/** A `dome` roof: the outline of its rim in the drawing's frame, and the heights of the rim (`low`) and of the crown above the surface, in feet. It is held up by the air inside, so it is a smooth lens, with no struts. */
|
|
142
|
+
/** A `dome` roof: the outline of its rim in the drawing's frame, and the heights of the rim (`low`) and of the crown above the surface, in feet. It is held up by the air inside, so it is a smooth lens, with no struts. With `ribs` (the Olympic Stadium's) the shell is `count` concrete consoles with glazed panels between them and a roof of panels rising to a ring, and `low` is the ring's height. */
|
|
143
143
|
dome?: {
|
|
144
144
|
rim: Pt[];
|
|
145
145
|
low: number;
|
|
146
146
|
crown: number;
|
|
147
|
+
ribs?: {
|
|
148
|
+
count: number;
|
|
149
|
+
};
|
|
147
150
|
};
|
|
148
151
|
/** Draw the walls as brick (a ballpark's facade, T-Mobile Park's: from the brief; a plain drawing choice, a wall in a brick tone, no courses). */
|
|
149
152
|
brick?: boolean;
|
package/dist/draw.js
CHANGED
|
@@ -789,7 +789,7 @@ export function describe(o) {
|
|
|
789
789
|
const venue = sport === "nhl" && league === undefined && realCrowd?.real === true ? venueById(o.arena) : undefined;
|
|
790
790
|
const realLine = venue === undefined || arenaName === undefined ? "" : `${gap}${fill(words.said.real, { arena: arenaName, seats: String(venue.seats.hockey), tiers: String(venue.tiers.count) })}${gap}${words.said[venue.osm.axis.basis === "mapped" ? "realMapped" : venue.osm.axis.basis === "building" ? "realBuilding" : "realNone"]}`;
|
|
791
791
|
const real = realStadiumOf(o, sport, realCrowd?.real === true);
|
|
792
|
-
const stadiumKey = (basis, kind, config) => basis === "opening" ? "stadiumOpening" : basis === "pitch" ? (kind === "park" ? "stadiumPitchPark" : "stadiumPitch") : basis === "infield" ? "stadiumInfield" : basis === "building" ? (kind === "hall" ? (config === "nba" ? "stadiumCourtBuilding" : "stadiumRingBuilding") : "stadiumBuilding") : kind === "park" ? "stadiumNonePark" : "stadiumNone";
|
|
792
|
+
const stadiumKey = (basis, kind, config) => basis === "opening" ? "stadiumOpening" : basis === "pitch" ? (kind === "park" ? "stadiumPitchPark" : "stadiumPitch") : basis === "infield" ? "stadiumInfield" : basis === "streets" ? "stadiumStreets" : basis === "building" ? (kind === "hall" ? (config === "nba" ? "stadiumCourtBuilding" : "stadiumRingBuilding") : "stadiumBuilding") : kind === "park" ? "stadiumNonePark" : "stadiumNone";
|
|
793
793
|
const stadiumLine = real === undefined ? "" : `${gap}${fill(real.config === "mlb" ? words.said.stadiumPark : stadiumKind(real.stadium) === "hall" ? words.said.stadiumHall : words.said.stadium, { stadium: stadiumNames(real.stadium)[language], seats: String(stadiumSeats(real.stadium, real.config)), tiers: String(real.stadium.tiers.count) })}${gap}${words.said[stadiumKey(real.stadium.osm.axis.basis, stadiumKind(real.stadium), real.config)]}`;
|
|
794
794
|
const parkLine = (parkName === undefined ? `${groundLine}${halfLine}${arenaLine}${realLine}${stadiumLine}` : `${gap}${fill(words.said.park, { park: parkName })}${officially(parkOfficial)}${ballpark === undefined ? "" : ballparkSentences(ballpark).map((line) => `${gap}${line}`).join("")}${stadiumLine}`) + (special === undefined ? "" : `${gap}${special.text}`);
|
|
795
795
|
// A drawing of a past season says which: the markings are those of that season.
|
package/dist/stadiums.d.ts
CHANGED
|
@@ -62,6 +62,13 @@ export interface RealStadiumData {
|
|
|
62
62
|
dirtInfield?: string;
|
|
63
63
|
brickWalls?: string;
|
|
64
64
|
};
|
|
65
|
+
/** Which way a ballpark's field lies where no map says but a published list does: the heading from home plate to centre field in degrees clockwise from north, `streets` (the streets the list names at the bases and the outfield), where it was read and what was read. */
|
|
66
|
+
orientation?: {
|
|
67
|
+
heading: number;
|
|
68
|
+
basis: "streets";
|
|
69
|
+
source: string;
|
|
70
|
+
read: string;
|
|
71
|
+
};
|
|
65
72
|
configs: Partial<Record<StadiumConfigId, StadiumConfigData>>;
|
|
66
73
|
/** The seats of the whole bowl where it holds more than a configuration's (Lumen Field's soccer seats are fewer: the upper sections are closed): the bowl is fitted to these and a configuration opens its seats from the lowest bowl up. */
|
|
67
74
|
bowlSeats?: number;
|
|
@@ -124,9 +131,15 @@ export interface RealStadiumData {
|
|
|
124
131
|
};
|
|
125
132
|
/** A canopy's share of the seats it covers, as the article gives it. */
|
|
126
133
|
cover?: number;
|
|
127
|
-
/** A retractable roof's bottom and top above the field, in feet, as the article gives them (an air-supported roof's height is the building's, from OpenStreetMap). */
|
|
134
|
+
/** A retractable roof's bottom and top above the field, in feet, as the article gives them (an air-supported roof's height is the building's, from OpenStreetMap; a fixed roof's `top` is its crown's). */
|
|
128
135
|
bottom?: number;
|
|
129
136
|
top?: number;
|
|
137
|
+
/** A fixed roof on a frame of ribs (the Olympic Stadium's consoles): how many, how many of them are anchored to the ground, and where it is read. */
|
|
138
|
+
ribs?: {
|
|
139
|
+
count: number;
|
|
140
|
+
anchored: number;
|
|
141
|
+
source: string;
|
|
142
|
+
};
|
|
130
143
|
};
|
|
131
144
|
}
|
|
132
145
|
/** What OpenStreetMap gave of a stadium: ODbL 1.0 data, "(c) OpenStreetMap contributors". */
|
|
@@ -177,12 +190,12 @@ export interface StadiumOsm {
|
|
|
177
190
|
* How the field lies, worked out when the data was made: the heading of its long side (degrees clockwise from north, 0 to under 180; for a ballpark, the heading from home plate out to centre field, 0 to under 360), how that
|
|
178
191
|
* is known (`opening`: from the roof's opening, which the article says is the field's size; `pitch`: from the pitches OpenStreetMap maps, and for a ballpark from the grass of the field it maps, fitted to the fence
|
|
179
192
|
* the park publishes (`rms`, in metres, is how far the two lie apart); `infield`: from the mapped infield, with the outline choosing which way is out to centre field; `building`: from the long axis of the
|
|
180
|
-
* building's main hall, an inference; `none`: no direction is known, and the heading is a stand-in (90 for a field with a long side, and for a ballpark the way that keeps its fence and its stands inside the building)),
|
|
193
|
+
* building's main hall, an inference; `streets`: from the streets a published list gives at the bases and the outfield of a ballpark, to the nearest 45 degrees, an inference; `none`: no direction is known, and the heading is a stand-in (90 for a field with a long side, and for a ballpark the way that keeps its fence and its stands inside the building)),
|
|
181
194
|
* how much of a direction the building has (`strength`) and how much longer than wide the opening, the pitch or the hall is (`elongation`), and the middle of the field as `[longitude, latitude]` (home plate for a ballpark).
|
|
182
195
|
*/
|
|
183
196
|
axis: {
|
|
184
197
|
heading: number;
|
|
185
|
-
basis: "opening" | "pitch" | "infield" | "building" | "none";
|
|
198
|
+
basis: "opening" | "pitch" | "infield" | "building" | "streets" | "none";
|
|
186
199
|
strength: number;
|
|
187
200
|
elongation: number;
|
|
188
201
|
centre: [number, number];
|
package/dist/stadiums.data.js
CHANGED
|
@@ -118,7 +118,7 @@ export const STADIUM_DATA = [
|
|
|
118
118
|
"configs": { "mlb": { "sport": "mlb", "seats": 49757, "source": "wikipedia", "read": "the infobox's capacity for baseball, 49,757 (1992 on, after 12,000 seats were taken out and home plate moved closer to the stands in the renovations of 1991); Wikidata's capacity (45,757) is kept beside it" } },
|
|
119
119
|
"wikidataSeats": [45757],
|
|
120
120
|
"tiers": { "count": 2, "basis": "plausible", "read": "no source read says how many seating levels there are: a lower and an upper bowl are drawn, the usual for a stadium of 50,000 seats (ours)" },
|
|
121
|
-
"roof": { "kind": "fixed", "basis": "documented", "read": "the article: the Kevlar roof, which could retract, was removed in May 1998 and a $26 million non-retractable opaque blue roof was installed later that year; a doughnut-shaped permanent ring is the roof's permanent component" },
|
|
121
|
+
"roof": { "kind": "fixed", "basis": "documented", "read": "the article: the Kevlar roof, which could retract, was removed in May 1998 and a $26 million non-retractable opaque blue roof was installed later that year; a doughnut-shaped permanent ring is the roof's permanent component; the roof is only 52 m (170.6 ft) over the field of play (the English article) and the building 284 m by 245 m (the French article), the roof's profile, the glazed bays and the ribs' flare are ours, from photographs", "top": 171, "ribs": { "count": 38, "anchored": 34, "source": "https://fr.wikipedia.org/wiki/Stade_olympique_de_Montr%C3%A9al: the frame is 38 cantilevered consoles, 34 anchored to the ground and 4 fixed to the tower" } },
|
|
122
122
|
"tower": { "height": 165, "unit": "m", "angle": 45, "basis": "documented", "read": "the article: the Montreal Tower, 165 m high, the tallest inclined structure in the world, inclined at 45 degrees and built into the north base of the stadium, from which the cables of the roof were hung; OpenStreetMap maps the building's outline and the same 165 m height, the observatory at its top (a viewpoint) and, as the part of the funicular line that rides on the tower (tagged above ground, its tunnel below), the line of the shaft from its foot to its top: that is 158 m across for 165 m of height, the article's 45 degrees. The slight curve of the shaft and its widths are ours." },
|
|
123
123
|
},
|
|
124
124
|
{
|
package/dist/stadiums.js
CHANGED
|
@@ -339,9 +339,13 @@ const PARK_PLANS = new WeakMap();
|
|
|
339
339
|
const PARK_COVER = 0.92;
|
|
340
340
|
/** How far an open-air ballpark's walls stand over its top row, in feet (ours). */
|
|
341
341
|
const OPEN_WALL_OVER = 6;
|
|
342
|
-
/** How far a fixed roof's crown stands over its rim, in feet, and the width of a leaning tower's shaft at its base and at its top, in feet (ours: the sources give a roof's kind and a tower's height, not these). */
|
|
342
|
+
/** How far a fixed roof's crown stands over its rim, in feet (where no source gives its height), and the width of a leaning tower's shaft at its base and at its top, in feet (ours: the sources give a roof's kind and a tower's height, not these). */
|
|
343
343
|
const FIXED_CROWN = 30;
|
|
344
344
|
const TOWER_WIDTH = [240, 64];
|
|
345
|
+
/** How far in the outline the stands of a ribbed shell are fitted, as a share of the way to its middle: the shell's sweep clears the top row there (ours, from the shell's profile in `venue.ribbed.ts`). */
|
|
346
|
+
const RIBBED_STANDS = 0.87;
|
|
347
|
+
/** How far a ribbed shell's ring stands over the top row, in feet: the roof panels climb to it from the glazing, and the top row's back wall under them stays below the panel there (ours). */
|
|
348
|
+
const RIBBED_CLEARANCE = 36;
|
|
345
349
|
const PARK_PARKED = 0.3;
|
|
346
350
|
/**
|
|
347
351
|
* The plan of a ballpark from what its stands are laid from (`input`: the park's fence, the backstop and the crowd): the usual stands with each band as deep as its seats need, the building's outline turned so that home
|
|
@@ -378,6 +382,10 @@ function park(stadium, input) {
|
|
|
378
382
|
};
|
|
379
383
|
const footprint = world.map(frameOf);
|
|
380
384
|
const fb = boxOf(footprint);
|
|
385
|
+
// A ribbed shell (the Olympic Stadium's) rises over the bowl in a sweep from its outline in to a ring, so the stands must stand inside the shell: they are fitted inside the outline drawn in by `RIBBED_STANDS`.
|
|
386
|
+
const ribbed = stadium.roof.kind === "fixed" && stadium.roof.ribs !== undefined;
|
|
387
|
+
const mean = [footprint.reduce((t, p) => t + p[0], 0) / footprint.length, footprint.reduce((t, p) => t + p[1], 0) / footprint.length];
|
|
388
|
+
const fitted = ribbed ? footprint.map((p) => [mean[0] + (p[0] - mean[0]) * RIBBED_STANDS, mean[1] + (p[1] - mean[1]) * RIBBED_STANDS]) : footprint;
|
|
381
389
|
// Each band is as deep as holds its seats: the usual depths scaled by what the seats of the band are to what the band holds, a few rounds over (a band's seats grow with its depth). The usual depths
|
|
382
390
|
// take a seat to be 4.6 square feet of plan; a building too small for them has a steeper bowl, in which a seat takes less (the factors of `SEAT_AREAS`, as an arena's bowl), until the stands fit inside it.
|
|
383
391
|
let depths = { lower: 60, upper: 56, bleachers: 54 };
|
|
@@ -394,7 +402,7 @@ function park(stadium, input) {
|
|
|
394
402
|
depths[k] = Math.min(160, Math.max(20, next[k]));
|
|
395
403
|
stands = parkLayout({ ...input, depths });
|
|
396
404
|
}
|
|
397
|
-
if (stands.rings.every((r) => [r.region, ...(r.more ?? [])].every((o) => o.kind !== "poly" || o.points.every((p) => inside(
|
|
405
|
+
if (stands.rings.every((r) => [r.region, ...(r.more ?? [])].every((o) => o.kind !== "poly" || o.points.every((p) => inside(fitted, p)))))
|
|
398
406
|
break;
|
|
399
407
|
}
|
|
400
408
|
if (factor !== 1) {
|
|
@@ -423,11 +431,11 @@ function park(stadium, input) {
|
|
|
423
431
|
else if (stadium.roof.kind === "fixed") {
|
|
424
432
|
// A fixed roof over the whole bowl (the Olympic Stadium's): a low lens over the outline, from the walls (a little over the top row) to a crown a little higher over the middle (ours: the sources say the roof is fixed, not what shape it has),
|
|
425
433
|
// and the tower that leans over it where OpenStreetMap maps one.
|
|
426
|
-
const low = round(Math.max(top + STADIUM_CLEARANCE, (stadium.osm.height ?? 0) * FEET_PER_METRE));
|
|
434
|
+
const low = round(Math.max(top + (ribbed ? RIBBED_CLEARANCE : STADIUM_CLEARANCE), (stadium.osm.height ?? 0) * FEET_PER_METRE));
|
|
427
435
|
const rim = footprint.map((p) => [c[0] + (p[0] - c[0]) * 0.98, c[1] + (p[1] - c[1]) * 0.98]);
|
|
428
436
|
const mapped = stadium.osm.tower;
|
|
429
437
|
const tower = mapped === undefined || stadium.tower === undefined ? undefined : { base: frameOf(metresFrom(origin, [mapped.base])[0]), top: frameOf(metresFrom(origin, [mapped.top])[0]), height: round(stadium.tower.height * FEET_PER_METRE), angle: stadium.tower.angle, width: [TOWER_WIDTH[0], TOWER_WIDTH[1]] };
|
|
430
|
-
shell = { footprint, hall, ...(north === undefined ? {} : { north }), roof: "dome", edge: low, wall: low, dome: { rim, low, crown: low + FIXED_CROWN }, ...(tower === undefined ? {} : { tower }), ...ivy };
|
|
438
|
+
shell = { footprint, hall, ...(north === undefined ? {} : { north }), roof: "dome", edge: low, wall: low, dome: { rim, low, crown: stadium.roof.top === undefined ? low + FIXED_CROWN : Math.max(low + 6, stadium.roof.top), ...(stadium.roof.ribs === undefined ? {} : { ribs: { count: stadium.roof.ribs.count } }) }, ...(tower === undefined ? {} : { tower }), ...ivy };
|
|
431
439
|
}
|
|
432
440
|
else if (stadium.roof.kind === "open") {
|
|
433
441
|
// No roof: the walls stand a little over the top row.
|
|
@@ -443,7 +443,7 @@ export const STADIUM_OSM = {
|
|
|
443
443
|
"base": "2026-10-30T15:34:06Z",
|
|
444
444
|
"licence": "ODbL-1.0",
|
|
445
445
|
"attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
|
|
446
|
-
"axis": { "heading":
|
|
446
|
+
"axis": { "heading": 135, "basis": "streets", "strength": 1.21, "elongation": 1.28, "centre": [-73.552063, 45.558116] },
|
|
447
447
|
"tower": { "source": "way/20213158", "height": 165, "base": [-73.552212, 45.55979], "top": [-73.551804, 45.558392], "ring": [
|
|
448
448
|
[-73.551265, 45.559204],
|
|
449
449
|
[-73.55152, 45.559114],
|
package/dist/strings.core.d.ts
CHANGED
package/dist/strings.en.js
CHANGED
|
@@ -199,7 +199,7 @@ export const WORDS_EN = {
|
|
|
199
199
|
},
|
|
200
200
|
},
|
|
201
201
|
scoreTitle: "{away} {awayScore}, {home} {homeScore}",
|
|
202
|
-
said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", stadiumHall: "The stands are a plausible bowl of {seats} seats in {tiers} levels, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the hall's plan.", stadiumRingBuilding: "The ring is set along the building's long axis, which is an inference, and the arrow points north.", stadiumCourtBuilding: "The court is set along the building's long axis, which is an inference, and the arrow points north.", stadiumPitchPark: "The way home plate faces is worked out from the grass of the field OpenStreetMap maps, fitted to the published fence, and the arrow points north.", stadiumNonePark: "The way home plate faces is not known, so the field is drawn the way that fits the building, and no arrow points north.", stadiumPark: "The stands are the usual stands of a ballpark, each band as deep as its seats need, for {seats} seats, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the park's plan.", stadiumPitch: "The field lies as OpenStreetMap maps the pitch, and the arrow points north.", stadiumInfield: "The way home plate faces is worked out from the infield OpenStreetMap maps and the building's outline, and the arrow points north.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", districtOutside: "People outside: {percent} percent of the seats of the venues, {people} people, stand on the sidewalks, plazas and grounds round them, and none on a road.", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
|
|
202
|
+
said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", stadium: "The stands are a plausible bowl of {seats} seats in {tiers} bowls, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the stadium's plan.", stadiumOpening: "The field lies along the opening in the roof, which is the size of the field, and the arrow points north.", stadiumBuilding: "The field lies along the building's long axis, which is an inference, and the arrow points north.", stadiumNone: "The field's orientation is not known, so it is drawn east-west with north at the top.", stadiumHall: "The stands are a plausible bowl of {seats} seats in {tiers} levels, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the hall's plan.", stadiumRingBuilding: "The ring is set along the building's long axis, which is an inference, and the arrow points north.", stadiumCourtBuilding: "The court is set along the building's long axis, which is an inference, and the arrow points north.", stadiumPitchPark: "The way home plate faces is worked out from the grass of the field OpenStreetMap maps, fitted to the published fence, and the arrow points north.", stadiumNonePark: "The way home plate faces is not known, so the field is drawn the way that fits the building, and no arrow points north.", stadiumPark: "The stands are the usual stands of a ballpark, each band as deep as its seats need, for {seats} seats, fitted inside the outline of the building of {stadium} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the park's plan.", stadiumPitch: "The field lies as OpenStreetMap maps the pitch, and the arrow points north.", stadiumInfield: "The way home plate faces is worked out from the infield OpenStreetMap maps and the building's outline, and the arrow points north.", stadiumStreets: "The way home plate faces is worked out from the streets that the English Wikipedia lists at first base, left field, third base and right field, to the nearest 45 degrees, and the arrow points north.", district: "A plan of {count} real venues at their real positions and orientations, from the outlines OpenStreetMap maps; {mode}.", districtNeighbours: "it is fitted close round the buildings", districtCity: "it keeps the real distances, with a grid of kilometres and a scale", districtCredit: "Credit:", districtOutside: "People outside: {percent} percent of the seats of the venues, {people} people, stand on the sidewalks, plazas and grounds round them, and none on a road.", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
|
|
203
203
|
budo: {
|
|
204
204
|
sides: { north: "north", south: "south", east: "east", west: "west" },
|
|
205
205
|
judo: "a judo contest area of {contest} by {contest} metres in a safety area {safety} metres wide, laid with tatami of {tatami}",
|
package/dist/strings.ja.js
CHANGED
|
@@ -224,7 +224,7 @@ export const WORDS_JA = {
|
|
|
224
224
|
},
|
|
225
225
|
},
|
|
226
226
|
scoreTitle: "{away} {awayScore}、{home} {homeScore}",
|
|
227
|
-
said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", stadiumHall: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層の客席を推定して描いています。ホールの設計図ではありません。", stadiumRingBuilding: "リングは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumCourtBuilding: "コートは建物の長軸に沿って置かれており(推定)、矢印は北を指します。", stadiumPitchPark: "ホームプレートの向きは、OpenStreetMapに記録されたグラウンドの芝生を、公表されているフェンスの形に合わせて求めており、矢印は北を指します。", stadiumNonePark: "ホームプレートの向きは分かっていないため、建物に収まる向きでグラウンドを描いており、北を示す矢印はありません。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", districtOutside: "会場の外の人出:会場の全座席数の{percent}%にあたる{people}人が、会場の周りの歩道、広場、敷地に立っています。車道には立っていません。", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
|
|
227
|
+
said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", stadium: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層のスタンドを推定して描いています。スタジアムの設計図ではありません。", stadiumOpening: "フィールドは屋根の開口部(フィールドと同じ大きさ)に沿って置いており、矢印は北を指します。", stadiumBuilding: "フィールドは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumNone: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", stadiumHall: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席・{tiers}層の客席を推定して描いています。ホールの設計図ではありません。", stadiumRingBuilding: "リングは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumCourtBuilding: "コートは建物の長軸に沿って置かれており(推定)、矢印は北を指します。", stadiumPitchPark: "ホームプレートの向きは、OpenStreetMapに記録されたグラウンドの芝生を、公表されているフェンスの形に合わせて求めており、矢印は北を指します。", stadiumNonePark: "ホームプレートの向きは分かっていないため、建物に収まる向きでグラウンドを描いており、北を示す矢印はありません。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", stadiumStreets: "ホームプレートの向きは、英語版ウィキペディアが一塁・左翼・三塁・右翼の位置に挙げている通りから、45度刻みで推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", districtOutside: "会場の外の人出:会場の全座席数の{percent}%にあたる{people}人が、会場の周りの歩道、広場、敷地に立っています。車道には立っていません。", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
|
|
228
228
|
budo: {
|
|
229
229
|
sides: { north: "北", south: "南", east: "東", west: "西" },
|
|
230
230
|
judo: "柔道の試合場。試合場内は{contest}m四方、幅{safety}mの安全地帯があり、{tatami}の畳を敷いています",
|
package/dist/turn.style.js
CHANGED
|
@@ -97,11 +97,15 @@ export const KYOGIJO_TURN_STYLE = `
|
|
|
97
97
|
.ky-turn .u-signplate{fill:var(--ky-turn-bg);fill-opacity:.9;stroke:var(--ky-turn-rule);stroke-width:.5}
|
|
98
98
|
.ky-turn .u-sign{fill:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);font-weight:600}
|
|
99
99
|
.ky-turn .u-mast{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.4}
|
|
100
|
-
.ky-turn .u-tower{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.4}
|
|
100
|
+
.ky-turn .u-tower{fill:color-mix(in srgb,var(--ky-up-fascia) 70%,var(--ky-up-white));stroke:var(--ky-up-roofline);stroke-width:.4}
|
|
101
101
|
.ky-turn .u-tower-slit{fill:var(--ky-up-roofline);fill-opacity:.55}
|
|
102
|
+
.ky-turn .u-rib{fill:color-mix(in srgb,var(--ky-up-fascia) 40%,var(--ky-up-white));stroke:var(--ky-up-roofline);stroke-width:.5}
|
|
103
|
+
.ky-turn .u-bay{fill:color-mix(in srgb,var(--ky-up-fascia) 55%,var(--ky-up-white));stroke:none}
|
|
104
|
+
.ky-turn .u-dome{fill:color-mix(in srgb,var(--ky-up-fascia) 22%,var(--ky-up-white));stroke:var(--ky-up-roofline);stroke-width:.5;stroke-linejoin:round}
|
|
105
|
+
.ky-turn .u-glass{fill:#1b252d;fill-opacity:.8}
|
|
102
106
|
.ky-turn .u-tower-foot{fill:var(--ky-up-roofline);fill-opacity:.8}
|
|
103
|
-
.ky-turn .u-tower-cap{fill:var(--ky-up-roofline);
|
|
104
|
-
.ky-turn .u-tower-glass{fill
|
|
107
|
+
.ky-turn .u-tower-cap{fill:color-mix(in srgb,var(--ky-up-fascia) 60%,var(--ky-up-white));stroke:var(--ky-up-roofline);stroke-width:.4}
|
|
108
|
+
.ky-turn .u-tower-glass{fill:#0b1a24;fill-opacity:.5}
|
|
105
109
|
.ky-turn .u-tower-cable{fill:var(--ky-up-roofline);fill-opacity:.7}
|
|
106
110
|
.ky-turn .u-skylight{fill:var(--ky-up-skylight,#a9c8da);fill-opacity:.9;stroke:var(--ky-up-roofline);stroke-width:.5}
|
|
107
111
|
.ky-turn .u-podium{fill:#000;fill-opacity:.16}
|
package/dist/venue.real.d.ts
CHANGED
|
@@ -14,12 +14,12 @@ export declare const roofAt: (real: RealShell, p: Pt) => number;
|
|
|
14
14
|
/** 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. */
|
|
15
15
|
export declare function wallAt(real: RealShell, p: Pt): number;
|
|
16
16
|
/**
|
|
17
|
-
* A tower that leans and curves: a
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* 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
|
|
18
|
+
* 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
|
|
19
|
+
* 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
|
|
20
|
+
* 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.
|
|
21
21
|
*/
|
|
22
|
-
export declare function towerPlanes(tower: NonNullable<RealShell["tower"]>, z: number, cables?: V3[]): Plane[];
|
|
22
|
+
export declare function towerPlanes(tower: NonNullable<RealShell["tower"]>, z: number, cables?: V3[], rim?: number): Plane[];
|
|
23
23
|
/** The point where a ray from `from` first meets a polygon, or `undefined`. */
|
|
24
24
|
export declare function firstHit(poly: readonly Pt[], from: Pt, angle: number): Pt | undefined;
|
|
25
25
|
/** How tall the glazed concourse band over a podium is, in feet (ours). */
|
package/dist/venue.real.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { rayAt } from "./crowd.js";
|
|
7
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";
|
|
9
10
|
import { edgeDistance, inside } from "./venues.geo.js";
|
|
10
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). */
|
|
11
12
|
export const LID_RISE = 22;
|
|
@@ -42,17 +43,19 @@ export function wallAt(real, p) {
|
|
|
42
43
|
top = Math.max(top, canopy.low + canopy.rise);
|
|
43
44
|
return top;
|
|
44
45
|
}
|
|
45
|
-
/** How many rings the leaning tower's shaft is cut into,
|
|
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). */
|
|
46
47
|
const TOWER_RINGS = 12;
|
|
47
|
-
const
|
|
48
|
-
|
|
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 };
|
|
49
52
|
/**
|
|
50
|
-
* A tower that leans and curves: a
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
54
57
|
*/
|
|
55
|
-
export function towerPlanes(tower, z, cables = []) {
|
|
58
|
+
export function towerPlanes(tower, z, cables = [], rim) {
|
|
56
59
|
const [bx, by] = tower.base;
|
|
57
60
|
const [tx, ty] = tower.top;
|
|
58
61
|
const run = Math.hypot(tx - bx, ty - by) || 1;
|
|
@@ -60,69 +63,70 @@ export function towerPlanes(tower, z, cables = []) {
|
|
|
60
63
|
const a = [-d[1], d[0]];
|
|
61
64
|
const [w0, w1] = tower.width;
|
|
62
65
|
const H = tower.height - TOWER_CAP.high;
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
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
|
+
});
|
|
71
78
|
const planes = [];
|
|
72
|
-
const face = (id, o,
|
|
79
|
+
const face = (id, o, up, along, r, svg) => {
|
|
80
|
+
const [lu, lv] = [Math.hypot(...up), Math.hypot(...along)];
|
|
73
81
|
// (A wall, not a mast: the lower tiers leave out the small parts, and a tower 165 metres high is not one.)
|
|
74
|
-
planes.push({ id, kind: "wall", o, u, v, svg });
|
|
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)}` });
|
|
75
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];
|
|
76
86
|
for (let i = 0; i < TOWER_RINGS; i += 1) {
|
|
77
|
-
const [
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
// The sloped back (the face away from the lean) has the two long dark slits down it, a few rings clear of the foot and of the top.
|
|
89
|
-
const slit = (f0, f1) => `<polygon class="u-tower-slit" points="0 ${n(f0 * lv)} ${n(lu)} ${n(f0 * lv * r)} ${n(lu)} ${n(f1 * lv * r)} 0 ${n(f1 * lv)}"/>`;
|
|
90
|
-
const slits = k === 0 && i >= 2 && i < TOWER_RINGS - 1 ? `${slit(0.24, 0.31)}${slit(0.62, 0.69)}` : "";
|
|
91
|
-
face(`tower-${i}-${k}`, o, u, v, `<polygon class="u-tower" points="0 0 ${n(lu)} 0 ${n(lu)} ${n(lv * r)} 0 ${n(lv)}"/>${slits}`);
|
|
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);
|
|
92
98
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const o = ring([0, 0], 0, wp, k);
|
|
99
|
-
const next = ring([0, 0], 0, wp, (k + 1) % 4);
|
|
100
|
-
const v = [next[0] - o[0], next[1] - o[1], 0];
|
|
101
|
-
face(`tower-foot-${k}`, o, [0, 0, TOWER_PLINTH.high], v, `<rect class="u-tower-foot" width="${n(TOWER_PLINTH.high)}" height="${n(Math.hypot(v[0], v[1]))}"/>`);
|
|
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, () => "");
|
|
102
104
|
}
|
|
103
|
-
const o = ring([0, 0], TOWER_PLINTH.high, wp, 0);
|
|
104
|
-
const [c1, c3] = [ring([0, 0], TOWER_PLINTH.high, wp, 1), ring([0, 0], TOWER_PLINTH.high, wp, 3)];
|
|
105
|
-
face("tower-foot-top", o, [c1[0] - o[0], c1[1] - o[1], 0], [c3[0] - o[0], c3[1] - o[1], 0], `<rect class="u-tower-foot" width="${n(wp)}" height="${n(wp)}"/>`);
|
|
106
105
|
}
|
|
107
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.
|
|
108
|
-
const
|
|
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]];
|
|
109
111
|
for (let k = 0; k < 4; k += 1) {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const lv = Math.hypot(
|
|
114
|
-
|
|
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)}"/>` });
|
|
115
117
|
}
|
|
116
118
|
{
|
|
117
|
-
const o =
|
|
118
|
-
const [c1, c3] = [
|
|
119
|
-
|
|
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)}"/>` });
|
|
120
122
|
}
|
|
121
123
|
// The cables that held the roof, from the observatory to the roof: thin lines.
|
|
122
|
-
const from =
|
|
124
|
+
const from = at(centre, 0, top.h + TOWER_CAP.high * 0.4);
|
|
123
125
|
cables.forEach((to, i) => planes.push(...bar(`tower-cable-${i}`, "mast", from, to, 1.2, "u-tower-cable")));
|
|
124
126
|
return planes;
|
|
125
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);
|
|
126
130
|
/** The point where a ray from `from` first meets a polygon, or `undefined`. */
|
|
127
131
|
export function firstHit(poly, from, angle) {
|
|
128
132
|
const [dx, dy] = [Math.cos(angle), Math.sin(angle)];
|
|
@@ -205,14 +209,22 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
|
|
|
205
209
|
const c = [centre[0], centre[1]];
|
|
206
210
|
const rim = Array.from({ length: steps }, (_, k) => rimPoint(real, c, (2 * Math.PI * k) / steps));
|
|
207
211
|
const planes = [];
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
});
|
|
216
228
|
if (parts.walls && planes.length > 0)
|
|
217
229
|
items.push({ id: "shell", kind: "walls", at: centre, height: Math.max(...planes.map((p) => p.o[2])), planes });
|
|
218
230
|
if (parts.walls && real.towers !== undefined) {
|
|
@@ -227,15 +239,17 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
|
|
|
227
239
|
return items;
|
|
228
240
|
}
|
|
229
241
|
if (parts.roof && real.roof === "dome" && real.dome !== undefined) {
|
|
230
|
-
// The cables run from the tower
|
|
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.
|
|
231
243
|
const rim = real.dome.rim;
|
|
232
244
|
const mid = [rim.reduce((t, p) => t + p[0], 0) / rim.length, rim.reduce((t, p) => t + p[1], 0) / rim.length];
|
|
233
|
-
const reach = Math.max(...rim.map((p) => Math.hypot(p[0] - mid[0], p[1] - mid[1]))) /
|
|
245
|
+
const reach = Math.max(...rim.map((p) => Math.hypot(p[0] - mid[0], p[1] - mid[1]))) / 2.4;
|
|
234
246
|
const across = real.tower === undefined ? [0, 0] : [-(real.tower.top[1] - real.tower.base[1]), real.tower.top[0] - real.tower.base[0]];
|
|
235
247
|
const long = Math.hypot(across[0], across[1]) || 1;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
const
|
|
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))];
|
|
239
253
|
if (planes.length > 0)
|
|
240
254
|
items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(planes), planes });
|
|
241
255
|
return items;
|
|
@@ -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
|
+
}
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Kyogijo Stadiums 競技場, 40 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, the MMA cage, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sports",
|