@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 +7 -0
- package/dist/draw.js +20 -2
- package/dist/stadiums.d.ts +6 -1
- package/dist/stadiums.js +12 -3
- 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,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;
|
package/dist/stadiums.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|