@johnmorrisdotca/kyogijo-stadiums 0.31.0 → 0.33.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/dist/stadiums.js CHANGED
@@ -1,4 +1,4 @@
1
- // The real stadiums: for a stadium that stages football and soccer, or a ballpark, the outline of its building and the way its field lies in it (from OpenStreetMap, ODbL 1.0), and a bowl of the seats of each configuration,
1
+ // The real stadiums: for a stadium that stages football and soccer, a ballpark, or a hall that stages boxing, the outline of its building and the way its field lies in it (from OpenStreetMap, ODbL 1.0), and a bowl of the seats of each configuration,
2
2
  // the number of bowls and the kind of roof the sources give (Wikidata, CC0, and the English Wikipedia article each fact cites), fitted inside the outline. It is the stadium's counterpart of `venues.ts`, for a surface that is a
3
3
  // field and not a rink: level one is the building, level two a plausible bowl, and it stops there (no team's, ticket seller's or federation's seat map is read or copied, no section or row is named, and no mark of a club, a
4
4
  // league or a tournament is drawn). The drawing is `drawKyogijo({ sport: "cfl", stadium, crowd: { real: true } })` (or `sport: "soccer"` with a `ground` or a `stadium`, `sport: "mlb"` with a `park`) and, in 3D, the same options
@@ -26,15 +26,20 @@ export const stadiumByWikidata = (wikidata) => Object.values(STADIUMS).find((s)
26
26
  export const stadiumKind = (stadium) => stadium.kind ?? "stadium";
27
27
  /** The configurations a stadium has, in the order of its data. */
28
28
  export const stadiumConfigs = (stadium) => Object.keys(stadium.configs);
29
- /** The ground a stadium is (its name, place and pitch). Throws for a ballpark, which has a park. */
29
+ /** The ground a stadium is (its name, place and pitch). Throws for a ballpark, which has a park, and for a hall, which has none. */
30
30
  export const groundOfStadium = (stadium) => {
31
31
  const ground = groundById(stadium.ground);
32
32
  if (ground === undefined)
33
33
  throw new Error(`${stadium.id} has no ground ${String(stadium.ground)}`);
34
34
  return ground;
35
35
  };
36
- /** The names a stadium goes by in both languages, of its ground or its park. */
36
+ /** The names a stadium goes by in both languages, of its ground or its park (a hall's own). */
37
37
  export function stadiumNames(stadium) {
38
+ if (stadiumKind(stadium) === "hall") {
39
+ if (stadium.names === undefined)
40
+ throw new Error(`${stadium.id} is a hall with no names`);
41
+ return { en: stadium.names.en, ja: stadium.names.ja };
42
+ }
38
43
  if (stadiumKind(stadium) === "park") {
39
44
  const park = PARKS[stadium.park ?? ""];
40
45
  if (park === undefined)
@@ -50,13 +55,15 @@ const lonLat = (ring) => ring.map(([lon, lat]) => [lon, lat]);
50
55
  export const stadiumFrameHeading = (stadium) => (stadiumKind(stadium) === "park" ? (stadium.osm.axis.heading + 90) % 360 : stadium.osm.axis.heading);
51
56
  /** How the field lies in a stadium's building (see `StadiumOsm.axis`). */
52
57
  export const stadiumOrientation = (stadium) => ({ heading: stadium.osm.axis.heading, basis: stadium.osm.axis.basis, north: round(northOn(stadiumFrameHeading(stadium))) });
53
- /** The configuration a sport is: `cfl`, `nfl`, `soccer` or `mlb`, or `undefined` for a sport a stadium has no configuration for. */
54
- export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "nfl" || sport === "soccer" || sport === "mlb" ? sport : undefined);
58
+ /** The configuration a sport is: `cfl`, `nfl`, `soccer`, `mlb` or `boxing`, or `undefined` for a sport a stadium has no configuration for. */
59
+ export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "nfl" || sport === "soccer" || sport === "mlb" || sport === "boxing" ? sport : undefined);
55
60
  /** The seats of a stadium for a configuration (0 for one it has none of). */
56
61
  export const stadiumSeats = (stadium, config) => stadium.configs[config]?.seats ?? 0;
57
62
  // ---- The bowl. ----
58
63
  /** How far the first row is from a football field's footprint (the league's field and its end zones) along a side and at an end, and the corner's radius: the package's own gridiron profile (`crowd.ts`), so that a real stadium's first row is the usual one. */
59
64
  const FIRST_ROW = { cfl: { side: 42, end: 28, corner: 60 }, nfl: { side: 40, end: 26, corner: 60 } };
65
+ /** How far a hall's first row is from the box of the ring, on every side, in feet (ours: the seats at the ringside are a few feet from the ropes). */
66
+ export const HALL_FIRST_ROW = 8;
60
67
  /** The height of the first row above the field, in feet, the rise of a bowl's rows by the number of bowls (what the lowest, a middle and the upper rise, at most), and the rise of the walk between two bowls (a suite level): ours, no source gives them. */
61
68
  export const STADIUM_FIRST_HEIGHT = 5;
62
69
  export const STADIUM_RISES = { 1: [60], 2: [38, 62], 3: [32, 26, 56] };
@@ -102,7 +109,7 @@ const PLANS = new WeakMap();
102
109
  * ```
103
110
  */
104
111
  export function planStadium(stadium, config, box) {
105
- if (stadiumKind(stadium) !== "stadium")
112
+ if (stadiumKind(stadium) === "park")
106
113
  throw new Error(`${stadium.id} is a ballpark: planPark plans it`);
107
114
  if (stadium.configs[config] === undefined)
108
115
  throw new Error(`${stadium.id} has no configuration ${config}`);
@@ -133,7 +140,10 @@ function plan(stadium, config, rink) {
133
140
  const gridiron = FOOTBALL[kind];
134
141
  const row = FIRST_ROW[kind];
135
142
  const length = gridiron.length + 2 * gridiron.endZone;
136
- const first = { kind: "rrect", x0: at[0] - length / 2 - row.end, y0: at[1] - gridiron.width / 2 - row.side, x1: at[0] + length / 2 + row.end, y1: at[1] + gridiron.width / 2 + row.side, r: row.corner };
143
+ // A hall's first row stands a little way from the ring's box on every side; a stadium's round a football field's footprint.
144
+ const first = stadiumKind(stadium) === "hall"
145
+ ? { kind: "rrect", x0: rink.x - HALL_FIRST_ROW, y0: rink.y - HALL_FIRST_ROW, x1: rink.x + rink.w + HALL_FIRST_ROW, y1: rink.y + rink.h + HALL_FIRST_ROW, r: HALL_FIRST_ROW }
146
+ : { kind: "rrect", x0: at[0] - length / 2 - row.end, y0: at[1] - gridiron.width / 2 - row.side, x1: at[0] + length / 2 + row.end, y1: at[1] + gridiron.width / 2 + row.side, r: row.corner };
137
147
  const shapedAs = (long, s) => ({ kind: "rrect", x0: at[0] - long * s, y0: at[1] - s, x1: at[0] + long * s, y1: at[1] + s, r: CORNER * s });
138
148
  const largest = (long) => {
139
149
  let [lo, hi] = [60, Math.max(fb.x1 - fb.x0, fb.y1 - fb.y0)];
@@ -216,6 +226,14 @@ function plan(stadium, config, rink) {
216
226
  const missing = seatsTotal - rings.reduce((sum, ring) => sum + ring.seats, 0);
217
227
  if (!tight && Math.abs(missing) <= 3)
218
228
  rings[rings.length - 1].seats += missing;
229
+ // Where a source gives the seats of the lowest bowl, that bowl holds them to the seat (the rounding of its area leaves it one or two off) and the last bowl the rest.
230
+ if (!tight && stadium.tiers.lowerSeats !== undefined && rings.length > 1) {
231
+ const off = stadium.tiers.lowerSeats - rings[0].seats;
232
+ if (off !== 0 && Math.abs(off) <= 3) {
233
+ rings[0].seats += off;
234
+ rings[rings.length - 1].seats -= off;
235
+ }
236
+ }
219
237
  const total = rings.reduce((sum, ring) => sum + ring.seats, 0);
220
238
  // A configuration that opens fewer seats than the bowl holds opens them from the lowest bowl up: a bowl's people are its open share of them, and a closed one is empty.
221
239
  if (open < seatsTotal) {
@@ -260,6 +278,8 @@ export function stadiumBowlInside(plan) {
260
278
  const PARK_PLANS = new WeakMap();
261
279
  /** The share of a ballpark's building the closed roof covers, and the share of its width along the east side the open roof's sections rest over (ours: the article says where they rest, not how much). */
262
280
  const PARK_COVER = 0.92;
281
+ /** How far an open-air ballpark's walls stand over its top row, in feet (ours). */
282
+ const OPEN_WALL_OVER = 6;
263
283
  const PARK_PARKED = 0.3;
264
284
  /**
265
285
  * 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
@@ -284,17 +304,6 @@ function park(stadium, input) {
284
304
  const bands = stadium.tiers.tierSeats ?? { lower: seats * 0.45, upper: seats * 0.47, bleachers: seats * 0.08 };
285
305
  const sum = bands.lower + bands.upper + bands.bleachers;
286
306
  const wanted = { lower: (bands.lower / sum) * seats, upper: (bands.upper / sum) * seats, bleachers: (bands.bleachers / sum) * seats };
287
- // 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).
288
- const depths = { lower: 60, upper: 56, bleachers: 54 };
289
- let stands = parkLayout({ ...input, depths });
290
- for (let i = 0; i < 12; i += 1) {
291
- const held = (id) => stands.rings.find((r) => r.id === id)?.seats ?? 0;
292
- const next = { lower: depths.lower * (wanted.lower / Math.max(1, held("lower"))), upper: depths.upper * (wanted.upper / Math.max(1, held("upper"))), bleachers: depths.bleachers * (wanted.bleachers / Math.max(1, held("bleachers"))) };
293
- for (const k of ["lower", "upper", "bleachers"])
294
- depths[k] = Math.min(160, Math.max(20, next[k]));
295
- stands = parkLayout({ ...input, depths });
296
- }
297
- const total = stands.rings.reduce((n, r) => n + r.seats, 0);
298
307
  const orientation = stadiumOrientation(stadium);
299
308
  const frameHeading = stadiumFrameHeading(stadium);
300
309
  // The building in the park's frame, in feet: home plate at the origin.
@@ -307,27 +316,63 @@ function park(stadium, input) {
307
316
  };
308
317
  const footprint = world.map(frameOf);
309
318
  const fb = boxOf(footprint);
319
+ // 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
320
+ // 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.
321
+ let depths = { lower: 60, upper: 56, bleachers: 54 };
322
+ let stands = parkLayout({ ...input, depths });
323
+ let factor = 1;
324
+ for (const f of [1, ...SEAT_AREAS.map((area) => 4.6 / area)]) {
325
+ factor = f;
326
+ depths = { lower: 60, upper: 56, bleachers: 54 };
327
+ stands = parkLayout({ ...input, depths });
328
+ for (let i = 0; i < 12; i += 1) {
329
+ const held = (id) => (stands.rings.find((r) => r.id === id)?.seats ?? 0) * f;
330
+ const next = { lower: depths.lower * (wanted.lower / Math.max(1, held("lower"))), upper: depths.upper * (wanted.upper / Math.max(1, held("upper"))), bleachers: depths.bleachers * (wanted.bleachers / Math.max(1, held("bleachers"))) };
331
+ for (const k of ["lower", "upper", "bleachers"])
332
+ depths[k] = Math.min(160, Math.max(20, next[k]));
333
+ stands = parkLayout({ ...input, depths });
334
+ }
335
+ if (stands.rings.every((r) => [r.region, ...(r.more ?? [])].every((o) => o.kind !== "poly" || o.points.every((p) => inside(footprint, p)))))
336
+ break;
337
+ }
338
+ if (factor !== 1) {
339
+ for (const ring of stands.rings)
340
+ ring.seats = Math.round(ring.seats * factor);
341
+ stands.seats = stands.rings.reduce((n, r) => n + r.seats, 0);
342
+ }
343
+ const total = stands.rings.reduce((n, r) => n + r.seats, 0);
310
344
  const box = stands.box;
311
345
  const c = [box.x + box.w / 2, box.y + box.h / 2];
312
- const cover = footprint.map((p) => [c[0] + (p[0] - c[0]) * PARK_COVER, c[1] + (p[1] - c[1]) * PARK_COVER]);
313
- // Where the sections rest: the east side of the building, over the railway there (the article says so): the strip of the outline east of a share of its width.
314
- const [wx0, wx1] = [Math.min(...world.map((p) => p[0])), Math.max(...world.map((p) => p[0]))];
315
- const [wy0, wy1] = [Math.min(...world.map((p) => p[1])), Math.max(...world.map((p) => p[1]))];
316
- const strip = clipConvex(world, [[wx1 - (wx1 - wx0) * PARK_PARKED, wy0 - 1], [wx1 + 1, wy0 - 1], [wx1 + 1, wy1 + 1], [wx1 - (wx1 - wx0) * PARK_PARKED, wy1 + 1]]);
317
- const parked = strip.map(frameOf);
318
346
  const north = orientation.basis === "none" ? undefined : orientation.north;
319
347
  const room = north === undefined ? 0 : NORTH_ROOM;
320
- const wallFeet = round((stadium.osm.height ?? 20) * FEET_PER_METRE);
321
348
  const hall = { kind: "rrect", x0: fb.x0 - 30, y0: fb.y0 - 30, x1: fb.x1 + 30, y1: fb.y1 + 30, r: 0 };
322
- const shell = {
323
- footprint,
324
- hall,
325
- ...(north === undefined ? {} : { north }),
326
- roof: "panels",
327
- edge: wallFeet,
328
- wall: wallFeet,
329
- panels: { cover, bottom: stadium.roof.bottom ?? 217, top: stadium.roof.top ?? 269, parked },
330
- };
349
+ const top = Math.max(...stands.rings.map((r) => r.height[1]));
350
+ const ivy = stadium.looks?.ivyWalls === undefined ? {} : { ivy: true };
351
+ let shell;
352
+ if (stadium.roof.kind === "dome") {
353
+ // An air-supported roof: the membrane over the outline OpenStreetMap maps for it, standing at the height of its rim (the building's height less the roof's own rise) and rising to the building's height.
354
+ const mapped = stadium.osm.roof;
355
+ if (mapped?.ring === undefined || mapped.height === undefined || mapped.rise === undefined)
356
+ throw new Error(`${stadium.id} has a dome roof and no mapped roof`);
357
+ const rim = metresFrom(origin, lonLat(mapped.ring)).map(frameOf);
358
+ const low = round((mapped.height - mapped.rise) * FEET_PER_METRE);
359
+ shell = { footprint, hall, ...(north === undefined ? {} : { north }), roof: "dome", edge: low, wall: low, dome: { rim, low, crown: round(mapped.height * FEET_PER_METRE) }, ...ivy };
360
+ }
361
+ else if (stadium.roof.kind === "open") {
362
+ // No roof: the walls stand a little over the top row.
363
+ const wall = round(Math.max(top + OPEN_WALL_OVER, (stadium.osm.height ?? 0) * FEET_PER_METRE));
364
+ shell = { footprint, hall, ...(north === undefined ? {} : { north }), roof: "open", edge: wall, wall, ...ivy };
365
+ }
366
+ else {
367
+ const cover = footprint.map((p) => [c[0] + (p[0] - c[0]) * PARK_COVER, c[1] + (p[1] - c[1]) * PARK_COVER]);
368
+ // Where the sections rest: the east side of the building, over the railway there (the article says so): the strip of the outline east of a share of its width.
369
+ const [wx0, wx1] = [Math.min(...world.map((p) => p[0])), Math.max(...world.map((p) => p[0]))];
370
+ const [wy0, wy1] = [Math.min(...world.map((p) => p[1])), Math.max(...world.map((p) => p[1]))];
371
+ const strip = clipConvex(world, [[wx1 - (wx1 - wx0) * PARK_PARKED, wy0 - 1], [wx1 + 1, wy0 - 1], [wx1 + 1, wy1 + 1], [wx1 - (wx1 - wx0) * PARK_PARKED, wy1 + 1]]);
372
+ const parked = strip.map(frameOf);
373
+ const wallFeet = round((stadium.osm.height ?? 20) * FEET_PER_METRE);
374
+ shell = { footprint, hall, ...(north === undefined ? {} : { north }), roof: "panels", edge: wallFeet, wall: wallFeet, panels: { cover, bottom: stadium.roof.bottom ?? 217, top: stadium.roof.top ?? 269, parked }, ...ivy };
375
+ }
331
376
  const reach = {
332
377
  left: Math.max(stands.reach.left, box.x - fb.x0),
333
378
  right: Math.max(stands.reach.right, fb.x1 - box.x - box.w),
@@ -335,5 +380,5 @@ function park(stadium, input) {
335
380
  bottom: Math.max(stands.reach.bottom, fb.y1 - box.y - box.h),
336
381
  };
337
382
  const real = { ...stands, real: shell, reach };
338
- return { stadium, config, orientation, footprint, stands: real, depths: { lower: round(depths.lower), upper: round(depths.upper), bleachers: round(depths.bleachers) }, seats: stands.rings.map((r) => ({ tier: r.id, seats: r.seats })), total, fit: Math.abs(total - seats) / seats <= 0.02 ? "fits" : "tight" };
383
+ return { stadium, config, orientation, footprint, stands: real, depths: { lower: round(depths.lower), upper: round(depths.upper), bleachers: round(depths.bleachers) }, seats: stands.rings.map((r) => ({ tier: r.id, seats: r.seats })), total, seatArea: round(4.6 / factor, 1), fit: Math.abs(total - seats) / seats <= 0.02 ? "fits" : "tight" };
339
384
  }
@@ -169,4 +169,104 @@ export const STADIUM_OSM = {
169
169
  [-122.333251, 47.592271],
170
170
  ],
171
171
  },
172
+ "tokyo-dome": {
173
+ "source": "way/20028328",
174
+ "read": "2026-10-20",
175
+ "base": "2026-10-21T00:17:36Z",
176
+ "licence": "ODbL-1.0",
177
+ "attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
178
+ "axis": { "heading": 55, "basis": "none", "strength": 1.2, "elongation": 1.13, "centre": [139.751525, 35.705268] },
179
+ "roof": {
180
+ "source": "way/577764996",
181
+ "height": 56.19,
182
+ "rise": 30,
183
+ "ring": [
184
+ [139.752871, 35.706144],
185
+ [139.752932, 35.706031],
186
+ [139.752976, 35.705847],
187
+ [139.752975, 35.705675],
188
+ [139.752921, 35.705283],
189
+ [139.75286, 35.705117],
190
+ [139.752782, 35.704983],
191
+ [139.75269, 35.70489],
192
+ [139.752569, 35.70481],
193
+ [139.752398, 35.70474],
194
+ [139.75215, 35.704703],
195
+ [139.75182, 35.704713],
196
+ [139.75155, 35.704741],
197
+ [139.751333, 35.704784],
198
+ [139.75115, 35.704846],
199
+ [139.751022, 35.704923],
200
+ [139.750934, 35.705001],
201
+ [139.750846, 35.705134],
202
+ [139.750789, 35.70533],
203
+ [139.75078, 35.705468],
204
+ [139.750809, 35.705726],
205
+ [139.750861, 35.705993],
206
+ [139.750925, 35.706155],
207
+ [139.751, 35.70625],
208
+ [139.751107, 35.706344],
209
+ [139.751219, 35.706412],
210
+ [139.75136, 35.706461],
211
+ [139.751524, 35.70649],
212
+ [139.751747, 35.706495],
213
+ [139.752179, 35.706462],
214
+ [139.752393, 35.706426],
215
+ [139.752659, 35.706339],
216
+ [139.752775, 35.706248],
217
+ ],
218
+ },
219
+ "ring": [
220
+ [139.75136, 35.706461],
221
+ [139.751524, 35.70649],
222
+ [139.751747, 35.706495],
223
+ [139.752179, 35.706462],
224
+ [139.752393, 35.706426],
225
+ [139.752659, 35.706339],
226
+ [139.752775, 35.706248],
227
+ [139.752871, 35.706144],
228
+ [139.752916, 35.706158],
229
+ [139.753075, 35.706065],
230
+ [139.753143, 35.705858],
231
+ [139.753151, 35.705673],
232
+ [139.753126, 35.705252],
233
+ [139.753079, 35.705055],
234
+ [139.752993, 35.704894],
235
+ [139.752866, 35.704753],
236
+ [139.752696, 35.704642],
237
+ [139.752467, 35.704559],
238
+ [139.752289, 35.704533],
239
+ [139.752261, 35.704562],
240
+ [139.751532, 35.704648],
241
+ [139.751445, 35.70461],
242
+ [139.751372, 35.704602],
243
+ [139.751286, 35.704665],
244
+ [139.751075, 35.704741],
245
+ [139.750998, 35.70489],
246
+ [139.751022, 35.704923],
247
+ [139.750934, 35.705001],
248
+ [139.750846, 35.705134],
249
+ [139.750789, 35.70533],
250
+ [139.75078, 35.705468],
251
+ [139.750861, 35.705993],
252
+ [139.750925, 35.706155],
253
+ [139.751, 35.70625],
254
+ [139.751107, 35.706344],
255
+ [139.751219, 35.706412],
256
+ ],
257
+ },
258
+ "korakuen-hall": {
259
+ "source": "way/45977418",
260
+ "read": "2026-10-20",
261
+ "base": "2026-10-21T00:17:36Z",
262
+ "licence": "ODbL-1.0",
263
+ "attribution": "(c) OpenStreetMap contributors, ODbL 1.0, openstreetmap.org/copyright",
264
+ "axis": { "heading": 6, "basis": "building", "strength": 3.38, "elongation": 1.82, "centre": [139.752058, 35.704256] },
265
+ "ring": [
266
+ [139.751895, 35.704557],
267
+ [139.751831, 35.703995],
268
+ [139.752206, 35.703963],
269
+ [139.752282, 35.704525],
270
+ ],
271
+ },
172
272
  };
package/dist/strings.d.ts CHANGED
@@ -520,6 +520,10 @@ export interface KyogijoWords {
520
520
  stadiumPark: string;
521
521
  stadiumPitch: string;
522
522
  stadiumInfield: string;
523
+ stadiumHall: string;
524
+ stadiumRingBuilding: string;
525
+ stadiumPitchPark: string;
526
+ stadiumNonePark: string;
523
527
  /** A district: a plan of {count} real venues at their real positions, {mode}; and what `mode` says for the two kinds of plan, and the word before the credit. */
524
528
  district: string;
525
529
  districtNeighbours: string;
package/dist/strings.js CHANGED
@@ -199,7 +199,7 @@ export const KYOGIJO_STRINGS = {
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.", 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:", 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.", 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:", 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}",
@@ -758,7 +758,7 @@ export const KYOGIJO_STRINGS = {
758
758
  },
759
759
  },
760
760
  scoreTitle: "{away} {awayScore}、{home} {homeScore}",
761
- 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: "フィールドの向きは分かっていないため、東西に置き、北を上にしています。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
761
+ 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: "リングは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", stadiumPitchPark: "ホームプレートの向きは、OpenStreetMapに記録されたグラウンドの芝生を、公表されているフェンスの形に合わせて求めており、矢印は北を指します。", stadiumNonePark: "ホームプレートの向きは分かっていないため、建物に収まる向きでグラウンドを描いており、北を示す矢印はありません。", stadiumPark: "{stadium}の建物の輪郭(OpenStreetMap、(c) OpenStreetMap contributors)の内側に、{seats}席のスタンドを推定して描いています。球場の設計図ではありません。", stadiumPitch: "フィールドの向きはOpenStreetMapに記録されたピッチのとおりで、矢印は北を指します。", stadiumInfield: "ホームプレートの向きは、OpenStreetMapに記録された内野と建物の輪郭から推定しており、矢印は北を指します。", district: "OpenStreetMapの建物の輪郭をもとに、{count}か所の実在の会場を実際の位置と向きのとおりに並べた配置図です。{mode}。", districtNeighbours: "建物のすぐ近くまで寄せて描いています", districtCity: "実際の距離を保ち、1km方眼と縮尺を添えています", districtCredit: "出典:", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
762
762
  budo: {
763
763
  sides: { north: "北", south: "南", east: "東", west: "西" },
764
764
  judo: "柔道の試合場。試合場内は{contest}m四方、幅{safety}mの安全地帯があり、{tatami}の畳を敷いています",
@@ -107,7 +107,8 @@ export function baseball(points, o, era, diamond = MLB_DIAMOND) {
107
107
  base += el("path", { class: ft(o, "infield-dirt", "ky-dirt"), d: `M0 0L${num(ua)} ${num(-ua)}A${num(skin)} ${num(skin)} 0 0 0 ${num(-ua)} ${num(-ua)}Z` });
108
108
  const centreV = d.base * SQ;
109
109
  const half = centreV - 4.5 * Math.SQRT2;
110
- base += el("path", { class: ft(o, "infield-grass", "ky-infield"), d: `M0 ${num(-(centreV + half))}L${num(half)} ${num(-centreV)}L0 ${num(-(centreV - half))}L${num(-half)} ${num(-centreV)}Z` });
110
+ if (o.dirtInfield !== true)
111
+ base += el("path", { class: ft(o, "infield-grass", "ky-infield"), d: `M0 ${num(-(centreV + half))}L${num(half)} ${num(-centreV)}L0 ${num(-(centreV - half))}L${num(-half)} ${num(-centreV)}Z` });
111
112
  const cuts = [
112
113
  [0, 0, MLB.homeCircleRadius],
113
114
  [centreV, centreV, MLB.baseCircleRadius],
@@ -203,6 +203,8 @@ export interface BuildOptions {
203
203
  landmarks?: boolean;
204
204
  /** For baseball: what the park's article adds to the plain field (the warning track's width, the walls, the quirks). */
205
205
  ballpark?: Ballpark;
206
+ /** For baseball: the infield is all dirt, with no grass inside the bases (a real ballpark's, where its sources say so: Koshien Stadium's). */
207
+ dirtInfield?: boolean;
206
208
  }
207
209
  /**
208
210
  * The class of an element that is a named feature of the surface: `cls` and, for each feature name that has a colour of its own, `ky-f-<name>`.
@@ -5,9 +5,9 @@ import { KYOGIJO_TURN_BOARD_STYLE } from "./overlay.js";
5
5
  * defaults are in `:where()`, which has no weight, so a page's own rule always wins; the colours are custom properties (`--ky-up-*`) a page can set.
6
6
  */
7
7
  export const KYOGIJO_TURN_STYLE = `
8
- :where(.ky-turn){--ky-left:#2f6db0;--ky-right:#b5452c;--ky-cr-home:#b5452c;--ky-cr-away:#2f6db0;--ky-cr-bed:#4a525d;--ky-cr-empty:#7d8794;--ky-cr-gap:#14181d;--ky-cr-conc:#8c929b;--ky-cr-dim:1;--ky-up-roof:rgba(214,218,212,.6);--ky-up-roofline:rgba(40,46,42,.55);--ky-up-fascia:#9aa09a;--ky-up-door:#8a6a46;--ky-up-window:rgba(150,200,240,.55);--ky-up-sash:#f2f0e6;--ky-up-banner:#26211d;--ky-up-bannerline:#c8b88a;--ky-up-room:#efe8d6;--ky-up-roomback:#d6cdb9;--ky-up-ceiling:#dedcd5;--ky-up-railw:#fbfaf6;--ky-up-wainline:#6b4a2a;--ky-up-riser:#3f342c;--ky-up-slab:#d8cfba;--ky-up-slabedge:#c2b69c;--ky-up-seatc:#46342a;--ky-up-seatb:#2d211b;--ky-up-dais:#a0683a;--ky-up-dais-side:#6a4326;--ky-up-shrine:#c9b48f;--ky-up-shrine-side:#8f7a55;--ky-up-gal:#4d4037;--ky-up-aisle:#dcc196;--ky-up-panel:rgba(95,58,22,.3);--ky-up-alcove:#b58a52;--ky-up-purple:#5b2a6e;--ky-up-clock:#ffffff;--ky-up-rail:#3a2c1f;--ky-up-matedge:#2a2a26;--ky-up-sbframe:#1a1f1c;--ky-up-sbedge:#5b645e;--ky-up-sbline:#59615b;--ky-up-pole:#e9e7de;--ky-up-white:#f6f4ec;--ky-up-yellow:#f2c230;--ky-up-red:#c8372d;--ky-up-orange:#f26a1b;--ky-up-wood:#e3d4b4;--ky-up-leg:#474e49;--ky-up-pad:#2f6db0;--ky-up-cagepad:#2a2d32;--ky-up-table:#18243f;--ky-up-foot:rgba(0,0,0,.25);--ky-up-ground:#d9d5c7;--ky-up-wall:#2f4a36;--ky-up-clay:#a8703f;--ky-up-cap:#1d2a22;--ky-up-board:#ece9df;--ky-up-glass:rgba(138,190,232,.26);--ky-up-backboard:rgba(255,255,255,.88);--ky-up-outline:#f2f0e6;--ky-up-rim:#e8591a;--ky-up-mesh:rgba(24,30,27,.7);--ky-up-netfill:rgba(255,255,255,.2);--ky-up-tape:#fbfaf4;--ky-turn-bg:transparent;--ky-turn-ink:#1f2320;--ky-turn-rule:rgba(31,35,32,.22);--ky-turn-chip:rgba(255,255,255,.86);--ky-turn-chip-on:#1f2320;--ky-turn-chip-on-ink:#fff;${JUDGING_LIGHT}}
9
- @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-turn:not([data-theme=light])){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK}}}
10
- :where(html[data-theme=dark] .ky-turn,.ky-turn[data-theme=dark]){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK}}
8
+ :where(.ky-turn){--ky-left:#2f6db0;--ky-right:#b5452c;--ky-cr-home:#b5452c;--ky-cr-away:#2f6db0;--ky-cr-bed:#4a525d;--ky-cr-empty:#7d8794;--ky-cr-gap:#14181d;--ky-cr-conc:#8c929b;--ky-cr-dim:1;--ky-up-roof:rgba(214,218,212,.6);--ky-up-roofline:rgba(40,46,42,.55);--ky-up-fascia:#9aa09a;--ky-up-ivy:#5b8a45;--ky-up-door:#8a6a46;--ky-up-window:rgba(150,200,240,.55);--ky-up-sash:#f2f0e6;--ky-up-banner:#26211d;--ky-up-bannerline:#c8b88a;--ky-up-room:#efe8d6;--ky-up-roomback:#d6cdb9;--ky-up-ceiling:#dedcd5;--ky-up-railw:#fbfaf6;--ky-up-wainline:#6b4a2a;--ky-up-riser:#3f342c;--ky-up-slab:#d8cfba;--ky-up-slabedge:#c2b69c;--ky-up-seatc:#46342a;--ky-up-seatb:#2d211b;--ky-up-dais:#a0683a;--ky-up-dais-side:#6a4326;--ky-up-shrine:#c9b48f;--ky-up-shrine-side:#8f7a55;--ky-up-gal:#4d4037;--ky-up-aisle:#dcc196;--ky-up-panel:rgba(95,58,22,.3);--ky-up-alcove:#b58a52;--ky-up-purple:#5b2a6e;--ky-up-clock:#ffffff;--ky-up-rail:#3a2c1f;--ky-up-matedge:#2a2a26;--ky-up-sbframe:#1a1f1c;--ky-up-sbedge:#5b645e;--ky-up-sbline:#59615b;--ky-up-pole:#e9e7de;--ky-up-white:#f6f4ec;--ky-up-yellow:#f2c230;--ky-up-red:#c8372d;--ky-up-orange:#f26a1b;--ky-up-wood:#e3d4b4;--ky-up-leg:#474e49;--ky-up-pad:#2f6db0;--ky-up-cagepad:#2a2d32;--ky-up-table:#18243f;--ky-up-foot:rgba(0,0,0,.25);--ky-up-ground:#d9d5c7;--ky-up-wall:#2f4a36;--ky-up-clay:#a8703f;--ky-up-cap:#1d2a22;--ky-up-board:#ece9df;--ky-up-glass:rgba(138,190,232,.26);--ky-up-backboard:rgba(255,255,255,.88);--ky-up-outline:#f2f0e6;--ky-up-rim:#e8591a;--ky-up-mesh:rgba(24,30,27,.7);--ky-up-netfill:rgba(255,255,255,.2);--ky-up-tape:#fbfaf4;--ky-turn-bg:transparent;--ky-turn-ink:#1f2320;--ky-turn-rule:rgba(31,35,32,.22);--ky-turn-chip:rgba(255,255,255,.86);--ky-turn-chip-on:#1f2320;--ky-turn-chip-on-ink:#fff;${JUDGING_LIGHT}}
9
+ @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-turn:not([data-theme=light])){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-ivy:#3d6633;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK}}}
10
+ :where(html[data-theme=dark] .ky-turn,.ky-turn[data-theme=dark]){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-ivy:#3d6633;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-cagepad:#8f969f;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320;${JUDGING_DARK}}
11
11
  .ky-turn{position:relative;display:block;width:100%;color:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);background:var(--ky-turn-bg)}
12
12
  .ky-turn-view{position:relative;isolation:isolate;width:100%;aspect-ratio:var(--ky-turn-ratio,16/10);min-height:260px;overflow:hidden;touch-action:pan-y;cursor:grab;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;border-radius:12px;outline-offset:2px}
13
13
  .ky-turn[data-touch=turn] .ky-turn-view{touch-action:none}
@@ -67,6 +67,7 @@ export const KYOGIJO_TURN_STYLE = `
67
67
  .ky-turn .u-conc{fill:var(--ky-cr-conc)}
68
68
  .ky-turn .u-fascia{fill:var(--ky-up-fascia)}
69
69
  .ky-turn .u-shell{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
70
+ .ky-turn .u-ivy{fill:var(--ky-up-ivy)}
70
71
  .ky-turn .u-roof{fill:var(--ky-up-roof);stroke:var(--ky-up-roofline);stroke-width:.8;stroke-linejoin:round}
71
72
  .ky-turn .u-signplate{fill:var(--ky-turn-bg);fill-opacity:.9;stroke:var(--ky-turn-rule);stroke-width:.5}
72
73
  .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}
package/dist/types.d.ts CHANGED
@@ -685,7 +685,7 @@ export interface SurfaceOptions {
685
685
  pitch?: SoccerPitchId | SoccerSize;
686
686
  /** The soccer ground to draw: a key of `GROUNDS`. The pitch has the size its article publishes (the international 105 by 68 m where none is), and the drawing carries the ground's name for `year`. A `pitch` of your own wins over the ground's size. */
687
687
  ground?: string;
688
- /** The stadium to draw a football field (`sport: "cfl"`) or a soccer pitch in, for `crowd: { real: true }`: a key of `STADIUMS` (`bc-place`). A soccer drawing with a `ground` that is a stadium of `STADIUMS` needs no other. */
688
+ /** The stadium to draw a football field (`sport: "cfl"`), a soccer pitch or a boxing ring (`sport: "boxing"`, in a hall) in, for `crowd: { real: true }`: a key of `STADIUMS` (`bc-place`, `korakuen-hall`). A soccer drawing with a `ground` that is a stadium of `STADIUMS` needs no other. */
689
689
  stadium?: string;
690
690
  /** The arena to draw an NHL rink or an NBA court of: a key of `ARENAS`. The rink or the court has the size its sources publish where that differs from the league's (the league's where none does), the markings are those of `year`, and the drawing carries the arena's name for `year`. A `league` of your own wins over the arena's size. */
691
691
  arena?: string;
@@ -17,6 +17,8 @@ export declare function realBuilding(real: RealShell, top: StandRing | undefined
17
17
  }): Items;
18
18
  /** 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). */
19
19
  export declare const CANOPY_NOTE = "low over the field, high over the back of the stands";
20
+ /** 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). */
21
+ export declare const DOME_RINGS = 4;
20
22
  /** 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). */
21
23
  export declare const POD: {
22
24
  readonly share: 0.24;
@@ -1,7 +1,8 @@
1
1
  // The building of a real venue, raised: its walls, standing on the outline OpenStreetMap gives, and its roof over the top tier, a lid that rises towards the middle, a saddle that rises at the two ends and falls at the two
2
2
  // sides, a cable-supported roof that rises to an opening over the surface which a retractable fabric closes (BC Place's, with the fabric gathered into its pod when `roofOpen` is asked), canopies over some of the
3
- // stands that leave the field open (Lumen Field's), or retractable panels high over a ballpark (T-Mobile Park's, resting over its east side when `roofOpen` is asked). All are asked for with the roof
4
- // (`venue: { roof: true }`) and give way to the eye as the other walls do. Pure: data in, planes out.
3
+ // stands that leave the field open (Lumen Field's), retractable panels high over a ballpark (T-Mobile Park's, resting over its east side when `roofOpen` is asked), an air-supported dome (Tokyo Dome's: a smooth lens of
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
+ // Pure: data in, planes out.
5
6
  import { rayAt } from "./crowd.js";
6
7
  import { n, shy, wall } from "./turn.kit.js";
7
8
  import { pointList, raked } from "./venue.geom.js";
@@ -72,7 +73,7 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
72
73
  if (long < 1)
73
74
  return;
74
75
  const high = lift + (real.wall ?? roofAt(real, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]));
75
- planes.push({ ...shy(wall(`shell-${i}`, "wall", a[0], a[1], b[0], b[1], 0, high, `<rect class="u-shell" width="${n(long)}" height="${n(high)}"/>`), centre), fadeTo: 0.02 });
76
+ 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" : "u-shell"}" width="${n(long)}" height="${n(high)}"/>`), centre), fadeTo: 0.02 });
76
77
  });
77
78
  if (parts.walls && planes.length > 0)
78
79
  items.push({ id: "shell", kind: "walls", at: centre, height: Math.max(...planes.map((p) => p.o[2])), planes });
@@ -82,6 +83,15 @@ export function realBuilding(real, top, centre, lift, steps, open = false, parts
82
83
  items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(planes), planes });
83
84
  return items;
84
85
  }
86
+ if (parts.roof && real.roof === "dome" && real.dome !== undefined) {
87
+ const planes = domeRoof(real.dome, lift, steps);
88
+ if (planes.length > 0)
89
+ items.push({ id: "roof", kind: "roof", at: centre, height: heightOf(planes), planes });
90
+ return items;
91
+ }
92
+ // An open-air ballpark has walls and no roof.
93
+ if (real.roof === "open")
94
+ return items;
85
95
  if (parts.roof && real.roof === "panels" && real.panels !== undefined) {
86
96
  const planes = panelsRoof(real.panels, c, lift, steps, open);
87
97
  if (planes.length > 0)
@@ -187,6 +197,46 @@ function canopyRoof(canopies, c, lift, steps) {
187
197
  });
188
198
  return planes;
189
199
  }
200
+ /** 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). */
201
+ export const DOME_RINGS = 4;
202
+ /**
203
+ * An air-supported roof: the membrane over the rim of the walls, a lens that stands `low` over the surface at the rim and rises to the `crown` over the middle (a shallow cap: the height falls with the square of the way
204
+ * out to the rim), in rings of pieces between rays from the middle of the rim. Its pieces are drawn as the roof is (a pale skin with a thin line), so that the net of wires it is made on shows.
205
+ */
206
+ function domeRoof(dome, lift, steps) {
207
+ const planes = [];
208
+ const mid = [dome.rim.reduce((s, p) => s + p[0], 0) / dome.rim.length, dome.rim.reduce((s, p) => s + p[1], 0) / dome.rim.length];
209
+ const rim = Array.from({ length: steps }, (_, k) => firstHit(dome.rim, mid, (2 * Math.PI * k) / steps) ?? mid);
210
+ // The rings' share of the way out (1 is the rim), the outermost first, and the height there.
211
+ const shares = Array.from({ length: DOME_RINGS + 1 }, (_, j) => 1 - (j * 0.85) / DOME_RINGS);
212
+ // (The innermost ring is the crown itself, the flat cap's height, so that the roof reaches the height OpenStreetMap maps.)
213
+ const at = (share) => lift + (share === shares[DOME_RINGS] ? dome.crown : dome.low + (dome.crown - dome.low) * (1 - share * share));
214
+ const ring = (share) => rim.map((p) => [mid[0] + (p[0] - mid[0]) * share, mid[1] + (p[1] - mid[1]) * share]);
215
+ for (let j = 0; j < DOME_RINGS; j += 1) {
216
+ const [outer, inner] = [ring(shares[j]), ring(shares[j + 1])];
217
+ const [zo, zi] = [at(shares[j]), at(shares[j + 1])];
218
+ for (let k = 0; k < steps; k += 1) {
219
+ const [a, b] = [outer[k], outer[(k + 1) % steps]];
220
+ const [ai, bi] = [inner[k], inner[(k + 1) % steps]];
221
+ const [ex, ey] = [b[0] - a[0], b[1] - a[1]];
222
+ const el = Math.hypot(ex, ey);
223
+ if (el < 1e-6)
224
+ continue;
225
+ const slope = slopeThrough([[ai[0], ai[1], zi], [bi[0], bi[1], zi], [b[0], b[1], zo], [a[0], a[1], zo]]);
226
+ const centre = [(a[0] + b[0] + ai[0] + bi[0]) / 4, (a[1] + b[1] + ai[1] + bi[1]) / 4];
227
+ const away = Math.hypot(centre[0] - mid[0], centre[1] - mid[1]) || 1;
228
+ const plane = raked(`roof-${j}-${k}`, "roof", [ai, bi, b, a], slope, [ex / el, ey / el], (corners) => `<polygon class="u-roof" points="${pointList(corners)}"/>`);
229
+ if (plane !== undefined)
230
+ planes.push({ ...plane, shy: [(centre[0] - mid[0]) / away, (centre[1] - mid[1]) / away], fadeTo: 0.2 });
231
+ }
232
+ }
233
+ // The crown: a flat cap over the middle.
234
+ const top = ring(shares[DOME_RINGS]);
235
+ const cap = raked("roof-crown", "roof", top, { x0: mid[0], y0: mid[1], z0: at(shares[DOME_RINGS]), gx: 0, gy: 0 }, [1, 0], (corners) => `<polygon class="u-roof" points="${pointList(corners)}"/>`);
236
+ if (cap !== undefined)
237
+ planes.push({ ...cap, fadeTo: 0.2 });
238
+ return planes;
239
+ }
190
240
  /** The share of a retractable roof's cover that the closed roof's flat crown takes (the sections meet in the middle at the crown's height). */
191
241
  const PANEL_CROWN = 0.3;
192
242
  /** How thick a parked section is, in feet (ours). */
package/dist/venues.d.ts CHANGED
@@ -28,8 +28,8 @@ export interface RealVenueData {
28
28
  /** The seats for ice hockey: Wikidata's where its item gives a figure for ice hockey (`wikidata`), else the article's (`wikipedia`). */
29
29
  hockey: number;
30
30
  source: "wikidata" | "wikipedia";
31
- /** Wikidata's own capacity (P1083), kept beside it: it is often for another event, or an older year. */
32
- wikidata: number;
31
+ /** Wikidata's own capacity (P1083), kept beside it: it is often for another event, or an older year; `null` where the item gives none (the Little Caesars Arena's, the UBS Arena's). */
32
+ wikidata: number | null;
33
33
  /** What was read, in a few words of ours. */
34
34
  read: string;
35
35
  };