@johnmorrisdotca/kyogijo-stadiums 0.40.0 → 0.40.1

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 CHANGED
@@ -7,6 +7,13 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.40.1] - 2026-10-21
11
+
12
+ ### Fixed
13
+
14
+ - **Switching a stadium between football and soccer reshaped the whole drawing** (BC Place, Lumen Field): the view was fitted to the field and the bowl was rebuilt from the configuration's seats, so the building's outline, the scale, the frame and the tint of the ground all changed. Now only the field changes. The view is the building's with a little ground round it, about the middle of the field (the same outline and scale for every configuration, in 2D and in 3D), the bowl is the building's and holds the most seats any configuration has (BC Place's soccer 52,497 opens from the lowest bowl up and draws the other 2,003 seats empty, as Lumen Field's does), and the ground round the building is the football field's beige whichever field lies in it (`data-stadium` on the drawing's root; the dark ground is one for every sport). `planStadium(...).total` for BC Place's soccer is now 54,500, the bowl's, with `open` the configuration's 52,497.
15
+ - **The stadiums page's buttons were the browser's own** (the venue buttons) and its field choice differed from the family's: the venues are chips and the field a segmented choice, as on the venues and home pages, 48 pixels high, with a pressed state, in light and dark, and on a phone the chips scroll sideways. A new demo test (`e2e/controls.demo.mjs`) fails on any visible button or select of any demo page that is left in the browser's own style, in light and dark.
16
+
10
17
  ## [0.40.0] - 2026-10-21
11
18
 
12
19
  ### Added
package/dist/draw.js CHANGED
@@ -11,7 +11,7 @@ import { FOOTBALL } from "./geometry.js";
11
11
  import { renderLayers } from "./layers.js";
12
12
  import { arenaById, arenaCourtIn, arenaNameIn, arenaOfficialName, arenaRinkIn } from "./arenas.js";
13
13
  import { venueById, venueBowl } from "./venues.js";
14
- import { planPark, stadiumBowl, stadiumById, stadiumConfigOf, stadiumKind, stadiumNames, stadiumSeats } from "./stadiums.js";
14
+ import { KYOGIJO_STADIUM_STYLE, planPark, stadiumBowl, stadiumById, stadiumConfigOf, stadiumConfigs, stadiumKind, stadiumNames, stadiumSeats } from "./stadiums.js";
15
15
  import { groundById, groundNameIn, groundOfficialName } from "./grounds.js";
16
16
  import { BOARD, courtFor, hasSetsBoard, isNetLeague, isNetSport } from "./court.js";
17
17
  import { cricketBoardMarkup } from "./cricket.board.js";
@@ -469,6 +469,13 @@ const smallDrawing = (o) => {
469
469
  const w = typeof o.width === "number" ? o.width : typeof o.width === "string" && /^\s*\d+(?:\.\d+)?(?:px)?\s*$/.test(o.width) ? parseFloat(o.width) : undefined;
470
470
  return o.detail === "simple" || (w !== undefined && w < 480);
471
471
  };
472
+ /** The ground, in feet, a stadium's frame holds beyond its building. */
473
+ const VENUE_PAD = 6;
474
+ /** Whether the options draw a real stadium of more than one configuration (a football field and a soccer pitch in one building): its frame is the building's, not the field's. */
475
+ function stadiumHasConfigs(o, crowd) {
476
+ const real = realStadiumOf(o, chooseLeague(o).sport, crowd.real);
477
+ return real !== undefined && stadiumKind(real.stadium) === "stadium" && stadiumConfigs(real.stadium).length > 1;
478
+ }
472
479
  /** A surface with the stands and the crowd round it: the ground is made as wide as the stands need, then the stands are laid on it under the surface. */
473
480
  function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
474
481
  let margin = extra.margin;
@@ -483,6 +490,13 @@ function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
483
490
  }
484
491
  const surface = buildWhole(o, portrait, id, score, scored, { ...extra, margin });
485
492
  const stands = standsFor(o, surface, crowd);
493
+ // A stadium that stages more than one field is framed by its building, once, whichever field is drawn: the view is the building's with a small margin, about the middle of the field.
494
+ const field = stands.box;
495
+ if (surface.turn === undefined && o.range === undefined && stadiumHasConfigs(o, crowd)) {
496
+ const [cx, cy] = [field.x + field.w / 2, field.y + field.h / 2];
497
+ const [left, right, top, bottom] = [field.w / 2 + stands.reach.left, field.w / 2 + stands.reach.right, field.h / 2 + stands.reach.top, field.h / 2 + stands.reach.bottom].map((d) => d + VENUE_PAD);
498
+ surface.view = { x: cx - left, y: cy - top, w: left + right, h: top + bottom };
499
+ }
486
500
  const seats = stands.seats;
487
501
  const ground = groundById(o.ground)?.capacity;
488
502
  const fine = crowd.detail === "fine" || (crowd.detail === "auto" && !smallDrawing(o));
@@ -930,6 +944,9 @@ function drawnKyogijo(o) {
930
944
  const walled = sport === "mlb" && ballparkDrawn(ballparkFor(o));
931
945
  const hardwood = sport === "nba" && o.court?.hardwood !== undefined && o.court.hardwood !== false;
932
946
  const crowded = crowdOf(o) !== undefined;
947
+ // The stadium a drawing is in, where it has more than one configuration: its ground is its own, whichever field lies in it (the stylesheet reads it).
948
+ const crowdHere = crowdOf(o);
949
+ const stadiumOfDrawing = crowdHere !== undefined && stadiumHasConfigs(o, crowdHere) ? realStadiumOf(o, sport, crowdHere.real)?.stadium.id : undefined;
933
950
  const style = colourStyle(o, { left: leftTeam, right: rightTeam, home: isScore(o) ? o.home : undefined, away: isScore(o) ? o.away : undefined }, featured);
934
951
  const palette = isLook(o.look) ? o.look : undefined;
935
952
  const budoLook = isMatSport(sport) ? budoLookOf(sport, o.budo?.look ?? (sport === "dojo" ? dojoSpecOf(o.dojo).look : undefined)) : undefined;
@@ -1122,6 +1139,7 @@ function drawnKyogijo(o) {
1122
1139
  role: "img",
1123
1140
  "aria-labelledby": `${titleId} ${descId}`,
1124
1141
  "data-sport": sport,
1142
+ "data-stadium": stadiumOfDrawing,
1125
1143
  "data-league": o.league === undefined || o.league === "" ? (chooseLeague(o).league?.id ?? undefined) : o.league,
1126
1144
  "data-range": isScore(o) && sport === "curling" ? undefined : rangeLabel(o),
1127
1145
  "data-event": o.host !== undefined && typeof o.event === "string" ? safeClass(o.event) : undefined,
@@ -1140,7 +1158,7 @@ function drawnKyogijo(o) {
1140
1158
  part: parts ? "svg" : undefined,
1141
1159
  style: style === "" ? undefined : style,
1142
1160
  }, `<title id="${titleId}">${esc(title)}</title><desc id="${descId}">${esc(description)}</desc>` +
1143
- (o.style === "none" ? "" : `<style>${KYOGIJO_BASE_STYLE}${left?.round === true || right?.round === true || (pg !== undefined && pageant?.style === "stage") || (pg !== undefined && pageant?.emblem?.shape !== "plain" && pageant?.emblem !== undefined) ? `\n${KYOGIJO_MARK_STYLE}` : ""}${pg === undefined ? "" : `\n${KYOGIJO_PAGEANT_STYLE}${pageant?.animate === undefined || pageant.animate === "none" ? "" : `\n${KYOGIJO_PAGEANT_MOTION}`}`}${sport === "curling" ? `\n${KYOGIJO_CURLING_STYLE}` : ""}${foldedSheet ? `\n${KYOGIJO_FOLD_STYLE}` : ""}${isPoolSport(sport) ? `\n${o.pool?.venue !== undefined ? KYOGIJO_POOL_STYLE : poolStyle({ sport, ...commonOf(o.pool ?? {}), race: o.pool?.race !== undefined, section: o.pool?.section !== undefined && o.pool.section !== false })}` : ""}${plan === undefined ? "" : `\n${courtStyle(plan.sport, plan.look)}`}${fieldsStyle(sport) === "" ? "" : `\n${fieldsStyle(sport)}`}${athleticsStyle(sport) === "" ? "" : `\n${athleticsStyle(sport)}`}${skatingStyle(sport) === "" ? "" : `\n${skatingStyle(sport)}`}${budoLook === undefined ? "" : `\n${budoStyle(sport, budoLook)}`}${palette === undefined ? "" : `\n${lookStyle(palette)}`}${athleticsTrackRules(sport, o.athletics?.track) === "" ? "" : `\n${athleticsTrackRules(sport, o.athletics?.track)}`}${walled ? `\n${KYOGIJO_WALLS_STYLE}` : ""}${hardwood ? `\n${KYOGIJO_HARDWOOD_STYLE}` : ""}${features === undefined ? "" : `\n${featureStyle(sport, features)}`}${notes === undefined || notes.svg === "" ? "" : `\n${KYOGIJO_NOTE_STYLE}`}${grounded ? `\n${KYOGIJO_BACKGROUND_STYLE}` : ""}${crowded ? `\n${KYOGIJO_CROWD_STYLE}` : ""}${crowded && sport === "dojo" ? KYOGIJO_BUDO_GALLERY_STYLE : ""}${situation === undefined ? "" : `\n${KYOGIJO_SITUATION_STYLE}`}${o.host === undefined ? "" : `\n${specialStyle(hostSport(o.host))}`}</style>`) +
1161
+ (o.style === "none" ? "" : `<style>${KYOGIJO_BASE_STYLE}${left?.round === true || right?.round === true || (pg !== undefined && pageant?.style === "stage") || (pg !== undefined && pageant?.emblem?.shape !== "plain" && pageant?.emblem !== undefined) ? `\n${KYOGIJO_MARK_STYLE}` : ""}${pg === undefined ? "" : `\n${KYOGIJO_PAGEANT_STYLE}${pageant?.animate === undefined || pageant.animate === "none" ? "" : `\n${KYOGIJO_PAGEANT_MOTION}`}`}${sport === "curling" ? `\n${KYOGIJO_CURLING_STYLE}` : ""}${foldedSheet ? `\n${KYOGIJO_FOLD_STYLE}` : ""}${isPoolSport(sport) ? `\n${o.pool?.venue !== undefined ? KYOGIJO_POOL_STYLE : poolStyle({ sport, ...commonOf(o.pool ?? {}), race: o.pool?.race !== undefined, section: o.pool?.section !== undefined && o.pool.section !== false })}` : ""}${plan === undefined ? "" : `\n${courtStyle(plan.sport, plan.look)}`}${fieldsStyle(sport) === "" ? "" : `\n${fieldsStyle(sport)}`}${athleticsStyle(sport) === "" ? "" : `\n${athleticsStyle(sport)}`}${skatingStyle(sport) === "" ? "" : `\n${skatingStyle(sport)}`}${budoLook === undefined ? "" : `\n${budoStyle(sport, budoLook)}`}${palette === undefined ? "" : `\n${lookStyle(palette)}`}${athleticsTrackRules(sport, o.athletics?.track) === "" ? "" : `\n${athleticsTrackRules(sport, o.athletics?.track)}`}${walled ? `\n${KYOGIJO_WALLS_STYLE}` : ""}${hardwood ? `\n${KYOGIJO_HARDWOOD_STYLE}` : ""}${features === undefined ? "" : `\n${featureStyle(sport, features)}`}${notes === undefined || notes.svg === "" ? "" : `\n${KYOGIJO_NOTE_STYLE}`}${grounded ? `\n${KYOGIJO_BACKGROUND_STYLE}` : ""}${stadiumOfDrawing === undefined ? "" : `\n${KYOGIJO_STADIUM_STYLE}`}${crowded ? `\n${KYOGIJO_CROWD_STYLE}` : ""}${crowded && sport === "dojo" ? KYOGIJO_BUDO_GALLERY_STYLE : ""}${situation === undefined ? "" : `\n${KYOGIJO_SITUATION_STYLE}`}${o.host === undefined ? "" : `\n${specialStyle(hostSport(o.host))}`}</style>`) +
1144
1162
  (defs === "" ? "" : `<defs>${defs}</defs>`) +
1145
1163
  body);
1146
1164
  return root;
@@ -181,6 +181,11 @@ export declare const stadiumOrientation: (stadium: RealStadium) => StadiumOrient
181
181
  export declare const stadiumConfigOf: (sport: string | undefined) => StadiumConfigId | undefined;
182
182
  /** The seats of a stadium for a configuration (0 for one it has none of). */
183
183
  export declare const stadiumSeats: (stadium: RealStadium, config: StadiumConfigId) => number;
184
+ /**
185
+ * The stylesheet of a stadium that stages more than one field (`data-stadium` on the drawing's root): the ground round the building is its own, the football field's, whichever field lies in it, so that a soccer pitch does not tint it
186
+ * green. Light only: the dark ground is one for every sport.
187
+ */
188
+ export declare const KYOGIJO_STADIUM_STYLE: string;
184
189
  /** 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). */
185
190
  export declare const HALL_FIRST_ROW = 8;
186
191
  /** 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. */
@@ -225,7 +230,7 @@ export interface StadiumPlan {
225
230
  * import { planStadium, stadiumById } from "@johnmorrisdotca/kyogijo-stadiums/stadiums";
226
231
  * const plan = planStadium(stadiumById("bc-place")!, "soccer", { x: 0, y: 0, w: 344.5, h: 223.1 });
227
232
  * console.log(plan.orientation.basis, plan.seats.map((t) => `${t.tier} ${t.seats}`).join(", "), plan.total);
228
- * // opening lower 27695, upper 24802 52497
233
+ * // opening lower 27695, upper 26805 54500
229
234
  * ```
230
235
  */
231
236
  export declare function planStadium(stadium: RealStadium, config: StadiumConfigId, box: Box): StadiumPlan;
package/dist/stadiums.js CHANGED
@@ -59,6 +59,12 @@ export const stadiumOrientation = (stadium) => ({ heading: stadium.osm.axis.head
59
59
  export const stadiumConfigOf = (sport) => (sport === "cfl" || sport === "nfl" || sport === "soccer" || sport === "mlb" || sport === "boxing" ? sport : undefined);
60
60
  /** The seats of a stadium for a configuration (0 for one it has none of). */
61
61
  export const stadiumSeats = (stadium, config) => stadium.configs[config]?.seats ?? 0;
62
+ /**
63
+ * The stylesheet of a stadium that stages more than one field (`data-stadium` on the drawing's root): the ground round the building is its own, the football field's, whichever field lies in it, so that a soccer pitch does not tint it
64
+ * green. Light only: the dark ground is one for every sport.
65
+ */
66
+ export const KYOGIJO_STADIUM_STYLE = "@media (prefers-color-scheme:light){:where(html:not([data-theme=dark]) .kyogijo[data-sport=soccer][data-stadium]:not([data-theme=dark])){--ky-apron:#dcd8c9}}\n" +
67
+ ":where(html[data-theme=light] .kyogijo[data-sport=soccer][data-stadium],.kyogijo[data-theme=light][data-sport=soccer][data-stadium]){--ky-apron:#dcd8c9}";
62
68
  // ---- The bowl. ----
63
69
  /** 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. */
64
70
  const FIRST_ROW = { cfl: { side: 42, end: 28, corner: 60 }, nfl: { side: 40, end: 26, corner: 60 } };
@@ -105,7 +111,7 @@ const PLANS = new WeakMap();
105
111
  * import { planStadium, stadiumById } from "@johnmorrisdotca/kyogijo-stadiums/stadiums";
106
112
  * const plan = planStadium(stadiumById("bc-place")!, "soccer", { x: 0, y: 0, w: 344.5, h: 223.1 });
107
113
  * console.log(plan.orientation.basis, plan.seats.map((t) => `${t.tier} ${t.seats}`).join(", "), plan.total);
108
- * // opening lower 27695, upper 24802 52497
114
+ * // opening lower 27695, upper 26805 54500
109
115
  * ```
110
116
  */
111
117
  export function planStadium(stadium, config, box) {
@@ -133,6 +139,8 @@ function plan(stadium, config, rink) {
133
139
  const place = (ring) => ring.map(([x, y]) => [round((x - middle[0]) * FEET_PER_METRE + at[0], 2), round((y - middle[1]) * FEET_PER_METRE + at[1], 2)]);
134
140
  const footprint = place(outline);
135
141
  const fb = boxOf(footprint);
142
+ // How far the building reaches from the middle of the field on each side, worked out from the outline as it is (not as placed and rounded): one answer for every configuration, which is the stadium's frame.
143
+ const around = boxOf(outline.map(([x, y]) => [(x - middle[0]) * FEET_PER_METRE, (y - middle[1]) * FEET_PER_METRE]));
136
144
  const openingRing = stadium.osm.roof?.opening?.ring;
137
145
  const opening = openingRing === undefined ? undefined : place(turned(openingRing));
138
146
  // The first row round a football field's footprint, and the largest bowl-shaped outline that fits inside the building round the middle of the field.
@@ -163,7 +171,8 @@ function plan(stadium, config, rink) {
163
171
  const depth = (first.x0 - outer.x0) + (first.y0 - outer.y0) + (outer.x1 - first.x1) + (outer.y1 - first.y1);
164
172
  const gap = GAP / Math.max(20, depth / 4);
165
173
  const open = stadium.configs[config]?.seats ?? 0;
166
- const seatsTotal = stadium.bowlSeats ?? open;
174
+ // The bowl is the building's, the same for every configuration: it holds the most seats any of them opens (or the seats the data gives the bowl), and a configuration that opens fewer closes the rest.
175
+ const seatsTotal = stadium.bowlSeats ?? Math.max(...Object.values(stadium.configs).map((c) => c?.seats ?? 0));
167
176
  const count = Math.min(3, stadium.tiers.count);
168
177
  const lower = stadium.tiers.lowerSeats === undefined ? (count === 1 ? 1 : count === 2 ? LOWER_SHARE : 0.36) : stadium.tiers.lowerSeats / seatsTotal;
169
178
  const shares = count === 1 ? [1] : count === 2 ? [lower, 1 - lower] : [lower, (1 - lower) * 0.36, (1 - lower) * 0.64];
@@ -256,7 +265,7 @@ function plan(stadium, config, rink) {
256
265
  const bowl = {
257
266
  rings,
258
267
  off: { left: rink.x - first.x0, right: first.x1 - rink.x - rink.w, top: rink.y - first.y0, bottom: first.y1 - rink.y - rink.h },
259
- reach: { left: Math.max(0, rink.x - fb.x0), right: Math.max(0, fb.x1 - rink.x - rink.w), top: Math.max(0, rink.y - fb.y0) + room, bottom: Math.max(0, fb.y1 - rink.y - rink.h) },
268
+ reach: { left: Math.max(0, -rink.w / 2 - around.x0), right: Math.max(0, around.x1 - rink.w / 2), top: Math.max(0, -rink.h / 2 - around.y0) + room, bottom: Math.max(0, around.y1 - rink.h / 2) },
260
269
  shell: {
261
270
  footprint,
262
271
  hall: { ...outer, x0: outer.x0 - HALL, y0: outer.y0 - HALL, x1: outer.x1 + HALL, y1: outer.y1 + HALL, r: outer.r + HALL },
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.40.0";
2
+ export declare const VERSION = "0.40.1";
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.40.0";
2
+ export const VERSION = "0.40.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.40.0",
3
+ "version": "0.40.1",
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",