@johnmorrisdotca/kyogijo-stadiums 0.39.1 → 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,25 @@ 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
+
17
+ ## [0.40.0] - 2026-10-21
18
+
19
+ ### Added
20
+
21
+ - **The named lighting presets** ([the presets](./docs/venues.md#the-lighting-presets)): `lighting: "game" | "intro" | "concert"` join `"night"` and `"lightsDown"`. `game` is white light with the house half lit (a light veil, `u-veil-half`) and a pool of white light round the surface, `intro` the house dark with the home team's colour washed over the surface and the visitors' (the second colour) in the aisle beams, `concert` the house dark with warm amber beams and a purple surface. The presets are a table of constants, `LIGHT_PRESETS` (each preset's `house`, `aisles`, `pool`, and wash and beam colours), with `LIGHTING_PRESETS` (the names) and `LIGHT_COLORS` (the named colours); `LIGHT_PRESETS`, `LIGHTING_PRESETS`, `LIGHT_COLORS` and `resolveLighting` are exported from the main entry and from `/turn` (the 0.39.0 notes named `LIGHT_COLORS` without exporting it), and `WASH` from `/turn`. The `intro`'s team colours are the package's where it has them: the crowd's `home` and `away`, else the colours of a score's two teams, else blue and amber. `lighting: { preset, colors }` passes colours (the first stands for the home team's, the second for the visitors'), and `wash` and `aisles` win over them, for any preset. `resolveLighting(option, team)` now answers `{ preset, house, aisles, pool, beam, wash }`. `TurnModel.lighting` and the view's `data-lighting` say the preset (the stylesheet has a rule for `game` and one for the three dark ones); in the plan the stands' group has `ky-half` or `ky-down`. The `celebration` (colours chasing round the bowl) is to come. Off by default, and a drawing without lighting is exactly what it was.
22
+ - **A Lighting choice on the demo**, in place of the Night and Lights down buttons (kept in the address as `lighting=intro` and so on, with `wash=` for the colour of light over the preset's own; an old address's `night=1` and `lights=1` still work): a choice in the bar over the venues page's view and the home page's featured arena, and on the stadiums, tournament layouts and district pages. The intro is shown with sample team colours (a red and a blue that are nobody's). In 3D a change goes in while the view turns, without stopping it or sending it back.
23
+
24
+ ### Fixed
25
+
26
+ - **A test that raced another** (`src/docs-language.test.js`): it walks the Markdown files from the root, and met the docs examples' scratch folder `.cache` while that test was removing it, so that `pnpm check` failed now and then with an error reading a file that had just gone. `.cache` is skipped.
27
+ - **The demo's selects were 24 pixels high on a phone in WebKit** (`e2e/looks.demo.mjs`, "every control is a finger wide", failing on `webkit-phone` since 0.39.0): the bar's colour choice took the chip class, whose `display: inline-flex` WebKit does not honour on a select. The bar's selects are `display: inline-block`, 44 pixels high, no wider than 12 rem (they had taken the venue choice's 16 rem minimum).
28
+
10
29
  ## [0.39.1] - 2026-10-21
11
30
 
12
31
  ### Fixed
package/README.md CHANGED
@@ -581,7 +581,7 @@ console.log(layout.needs.length, layout.needs.width);
581
581
 
582
582
  ### The real venues
583
583
 
584
- `crowd: { real: true }` draws an NHL arena in its building: the outline from OpenStreetMap, the rink lying along the building where it says so, and a plausible bowl of the seats, tiers and roof the sources give. All thirty-two arenas of the league, division by division ([the real venues](./docs/venues.md)). `boards: true` adds LED ribbon boards and a hung scoreboard, generic content ([LED boards](./docs/venues.md#led-boards)). In 3D the demo's [venues page](https://johnmorrisdotca.github.io/kyogijo-stadiums/venues.html) turns while the pointer is over it, with the stands, walls, roof, LED boards, night, lights down, building, markings and labels switched on the view while it turns.
584
+ `crowd: { real: true }` draws an NHL arena in its building: the outline from OpenStreetMap, the rink lying along the building where it says so, and a plausible bowl of the seats, tiers and roof the sources give. All thirty-two arenas of the league, division by division ([the real venues](./docs/venues.md)). `boards: true` adds LED ribbon boards and a hung scoreboard, generic content ([LED boards](./docs/venues.md#led-boards)). In 3D the demo's [venues page](https://johnmorrisdotca.github.io/kyogijo-stadiums/venues.html) turns while the pointer is over it, with the stands, walls, roof, LED boards, lighting, building, markings and labels switched on the view while it turns.
585
585
 
586
586
  ```ts
587
587
  import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
@@ -11,6 +11,11 @@ export interface DrawInput {
11
11
  density: number;
12
12
  fine: boolean;
13
13
  ground: string;
14
+ /** The teams' colours of the drawing's score, where it has them: what a lighting's team colours are (the crowd's own `home` and `away` come first). */
15
+ team?: {
16
+ home?: string;
17
+ away?: string;
18
+ };
14
19
  }
15
20
  /** The building of a real venue and, where its rink's orientation is known, a small arrow with an N that says where north lies. The N is a path of three strokes, so that no font is needed. */
16
21
  export declare function realMarkup(real: RealShell): string;
@@ -197,12 +197,12 @@ export function drawCrowd(stands, o) {
197
197
  for (const s of stands.sectors)
198
198
  body += sector(s, id, fine, density);
199
199
  // The house is down: a dark veil over the seats, under the aisles' lights.
200
- const light = stands.gallery === true ? undefined : resolveLighting(crowd.lighting);
201
- const night = light?.preset === "night";
202
- const down = light?.preset === "lightsDown";
200
+ const light = stands.gallery === true ? undefined : resolveLighting(crowd.lighting, { home: crowd.home ?? o.team?.home, away: crowd.away ?? o.team?.away });
201
+ const night = light?.aisles === "dots";
202
+ const down = light?.aisles === "beams";
203
203
  if (light !== undefined)
204
204
  for (const ring of stands.rings)
205
- body += el("path", { class: down ? "ky-cr-veil ky-cr-veil-down" : "ky-cr-veil", d: path(ring) });
205
+ body += el("path", { class: light.house === "dark" ? "ky-cr-veil ky-cr-veil-down" : light.house === "half" ? "ky-cr-veil ky-cr-veil-half" : "ky-cr-veil", d: path(ring) });
206
206
  if (fine) {
207
207
  for (const ring of stands.rings) {
208
208
  if (stands.gallery === true)
@@ -211,8 +211,8 @@ export function drawCrowd(stands, o) {
211
211
  body += rows(ring) + aisles(ring, night);
212
212
  else {
213
213
  // The lights down: the front of each tier edged with light, each stair aisle a beam from above with its rails (a vomitory a broader one).
214
- body += rows(ring) + (ring.hole === null ? "" : el("path", { class: "ky-cr-edge", d: outlinePath(ring.hole), fill: "none", stroke: light.aisles, "stroke-opacity": 0.6, "stroke-width": 0.6 }));
215
- ring.aisles.forEach(([[x0, y0], [x1, y1]], i) => (body += beamSvg([x0, y0], [x1, y1], light.aisles, "ky-cr-beam", i % 4 === 0)));
214
+ body += rows(ring) + (ring.hole === null ? "" : el("path", { class: "ky-cr-edge", d: outlinePath(ring.hole), fill: "none", stroke: light.beam, "stroke-opacity": 0.6, "stroke-width": 0.6 }));
215
+ ring.aisles.forEach(([[x0, y0], [x1, y1]], i) => (body += beamSvg([x0, y0], [x1, y1], light.beam, "ky-cr-beam", i % 4 === 0)));
216
216
  }
217
217
  }
218
218
  }
@@ -242,7 +242,7 @@ export function drawCrowd(stands, o) {
242
242
  pool += el("radialGradient", { id: `${id}-wash`, cx: 0.5, cy: 0.5, r: 0.72 }, el("stop", { offset: 0, "stop-color": light.wash, "stop-opacity": WASH.middle }) + el("stop", { offset: 1, "stop-color": light.wash, "stop-opacity": WASH.edge }));
243
243
  over += el("rect", { class: "ky-cr-wash", x: n1(b.x), y: n1(b.y), width: n1(b.w), height: n1(b.h), rx: stands.sport === "nhl" ? NHL.cornerRadius : undefined, fill: `url(#${id}-wash)` });
244
244
  }
245
- if (night) {
245
+ if (light?.pool === true) {
246
246
  const b = stands.box;
247
247
  const [w, h] = [b.w + 2 * POOL.reach, b.h + 2 * POOL.reach];
248
248
  pool += el("radialGradient", { id: `${id}-pool`, cx: 0.5, cy: 0.5, r: 0.5 }, [[0, 0], [POOL.edge - 0.12, 0], [POOL.edge, POOL.strength], [1, 0]].map(([offset, opacity]) => el("stop", { offset, "stop-color": "#fff4dc", "stop-opacity": opacity })).join(""));
@@ -254,7 +254,7 @@ export function drawCrowd(stands, o) {
254
254
  const [x0, y0, x1, y1] = g.box;
255
255
  body += el("rect", { class: `ky-cr-${g.kind}`, x: n1(x0), y: n1(y0), width: n1(x1 - x0), height: n1(y1 - y0), rx: g.kind === "bench" ? 1 : undefined });
256
256
  }
257
- return { defs: (fine && (stands.gallery !== true || density > 0) ? patterns(id, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.gallery === true || stands.real !== undefined) : "") + pool, markup: el("g", { class: down ? "ky-crowd ky-night ky-down" : night ? "ky-crowd ky-night" : "ky-crowd" }, body) + lit, over };
257
+ return { defs: (fine && (stands.gallery !== true || density > 0) ? patterns(id, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.gallery === true || stands.real !== undefined) : "") + pool, markup: el("g", { class: light === undefined ? "ky-crowd" : light.house === "dark" ? "ky-crowd ky-night ky-down" : light.house === "half" ? "ky-crowd ky-night ky-half" : "ky-crowd ky-night" }, body) + lit, over };
258
258
  }
259
259
  /** A block of visitors or a card display, a polygon over the band it is in. */
260
260
  function sector(s, id, fine, density) {
@@ -297,6 +297,8 @@ export const KYOGIJO_CROWD_STYLE = `
297
297
  .kyogijo .ky-night{--ky-cr-bed:#11151a;--ky-cr-empty:#2a313a;--ky-cr-gap:#020304;--ky-cr-conc:#171c22;--ky-cr-rim:#07090c}
298
298
  .kyogijo .ky-cr-veil{fill:#04060a;fill-opacity:.6}
299
299
  .kyogijo .ky-cr-veil-down{fill-opacity:.8}
300
+ .kyogijo .ky-cr-veil-half{fill-opacity:.3}
301
+ .kyogijo .ky-half{--ky-cr-bed:#2a3039;--ky-cr-empty:#4a535f;--ky-cr-gap:#0a0c0f;--ky-cr-conc:#2a3038}
300
302
  .kyogijo .ky-cr-beam,.kyogijo .ky-cr-edge{stroke-linecap:butt}
301
303
  .kyogijo .ky-night .ky-cr-aisle{stroke:#ffd9a3;stroke-opacity:.95;stroke-dasharray:1.1 3.4;stroke-linecap:round}
302
304
  .kyogijo .ky-night .ky-cr-vom{stroke:#7dffb0;stroke-opacity:.6;stroke-dasharray:none}
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";
@@ -61,6 +61,7 @@ import { overlayMarkup, placeBoard, resolveBoard } from "./overlay.js";
61
61
  import { KYOGIJO_HARDWOOD_STYLE, resolveHardwood } from "./hardwood.js";
62
62
  import { WALL_SEATS, densityOf, layoutOf as crowdLayout, resolveCrowd } from "./crowd.js";
63
63
  import { drawCrowd, KYOGIJO_CROWD_STYLE } from "./crowd.draw.js";
64
+ import { teamColors } from "./venue.lighting.js";
64
65
  import { parkLayout } from "./crowd.park.js";
65
66
  import { featureColours, featureStyle } from "./features.js";
66
67
  import { isLook, lookStyle } from "./look.js";
@@ -468,6 +469,13 @@ const smallDrawing = (o) => {
468
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;
469
470
  return o.detail === "simple" || (w !== undefined && w < 480);
470
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
+ }
471
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. */
472
480
  function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
473
481
  let margin = extra.margin;
@@ -482,6 +490,13 @@ function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
482
490
  }
483
491
  const surface = buildWhole(o, portrait, id, score, scored, { ...extra, margin });
484
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
+ }
485
500
  const seats = stands.seats;
486
501
  const ground = groundById(o.ground)?.capacity;
487
502
  const fine = crowd.detail === "fine" || (crowd.detail === "auto" && !smallDrawing(o));
@@ -489,7 +504,7 @@ function buildCrowded(o, portrait, id, score, scored, extra, crowd) {
489
504
  const bg = /class="([^"]*)"/.exec(first)?.[1] ?? "ky-apron";
490
505
  // A gallery is empty unless the options say how full: its seats are the detail.
491
506
  const taken = stands.gallery === true && crowd.density === undefined && crowd.attendance === undefined ? 0 : densityOf(crowd, seats, ground);
492
- const drawn = drawCrowd(stands, { id, crowd, density: taken, fine, ground: bg });
507
+ const drawn = drawCrowd(stands, { id, crowd, density: taken, fine, ground: bg, team: teamColors(o) });
493
508
  surface.base = first + drawn.markup + surface.base.slice(first.length);
494
509
  surface.defs += drawn.defs;
495
510
  surface.lines += drawn.over;
@@ -929,6 +944,9 @@ function drawnKyogijo(o) {
929
944
  const walled = sport === "mlb" && ballparkDrawn(ballparkFor(o));
930
945
  const hardwood = sport === "nba" && o.court?.hardwood !== undefined && o.court.hardwood !== false;
931
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;
932
950
  const style = colourStyle(o, { left: leftTeam, right: rightTeam, home: isScore(o) ? o.home : undefined, away: isScore(o) ? o.away : undefined }, featured);
933
951
  const palette = isLook(o.look) ? o.look : undefined;
934
952
  const budoLook = isMatSport(sport) ? budoLookOf(sport, o.budo?.look ?? (sport === "dojo" ? dojoSpecOf(o.dojo).look : undefined)) : undefined;
@@ -1121,6 +1139,7 @@ function drawnKyogijo(o) {
1121
1139
  role: "img",
1122
1140
  "aria-labelledby": `${titleId} ${descId}`,
1123
1141
  "data-sport": sport,
1142
+ "data-stadium": stadiumOfDrawing,
1124
1143
  "data-league": o.league === undefined || o.league === "" ? (chooseLeague(o).league?.id ?? undefined) : o.league,
1125
1144
  "data-range": isScore(o) && sport === "curling" ? undefined : rangeLabel(o),
1126
1145
  "data-event": o.host !== undefined && typeof o.event === "string" ? safeClass(o.event) : undefined,
@@ -1139,7 +1158,7 @@ function drawnKyogijo(o) {
1139
1158
  part: parts ? "svg" : undefined,
1140
1159
  style: style === "" ? undefined : style,
1141
1160
  }, `<title id="${titleId}">${esc(title)}</title><desc id="${descId}">${esc(description)}</desc>` +
1142
- (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>`) +
1143
1162
  (defs === "" ? "" : `<defs>${defs}</defs>`) +
1144
1163
  body);
1145
1164
  return root;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { HOME_FIRST, HOME_FIRST_LEAGUES, homeSideFor } from "./homeside.ts";
3
3
  export { inkOn, luminance, rgbOf, safeColour, safePicture } from "./colour.ts";
4
4
  export { safeLogo } from "./logo.ts";
5
5
  export { useEmblems } from "./emblems-source.ts";
6
+ export { LIGHTING_PRESETS, LIGHT_COLORS, LIGHT_PRESETS, resolveLighting } from "./venue.lighting.ts";
6
7
  export type { EmblemDrawOptions, EmblemId, EmblemRef, EmblemRenderer, EmblemStyleName } from "./emblems-source.ts";
7
8
  export { useFlags } from "./flags-source.ts";
8
9
  export type { FlagRenderer, FlagRequest, FlagShape } from "./flags-source.ts";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { HOME_FIRST, HOME_FIRST_LEAGUES, homeSideFor } from "./homeside.js";
4
4
  export { inkOn, luminance, rgbOf, safeColour, safePicture } from "./colour.js";
5
5
  export { safeLogo } from "./logo.js";
6
6
  export { useEmblems } from "./emblems-source.js";
7
+ export { LIGHTING_PRESETS, LIGHT_COLORS, LIGHT_PRESETS, resolveLighting } from "./venue.lighting.js";
7
8
  export { useFlags } from "./flags-source.js";
8
9
  export { OUTCOMES, drawLayers, layerTable } from "./layers.js";
9
10
  export { RANGES } from "./ranges.names.js";
@@ -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 },
@@ -12,6 +12,8 @@ export type { Box3, Mat4, Placement, TurnPreset, TurnToward, TurnView } from "./
12
12
  export { describeUprights, heightUnit, planesOf, uprightsFor } from "./uprights.ts";
13
13
  export type { UprightOptions, Uprights } from "./uprights.ts";
14
14
  export { ROOF } from "./venue.ts";
15
+ export { LIGHTING_PRESETS, LIGHT_COLORS, LIGHT_PRESETS, WASH, resolveLighting } from "./venue.lighting.ts";
16
+ export type { House, LightPreset, Lighting } from "./venue.lighting.ts";
15
17
  export type { Venue, VenueBuild, VenueSettings } from "./venue.ts";
16
18
  export type { LedBoardOptions, LedBoardPattern, LightingOptions, LightingPreset } from "./types.ts";
17
19
  export type { Plane, PartKind, Upright, UprightKind, V3 } from "./turn.kit.ts";
@@ -7,3 +7,4 @@ export { KYOGIJO_TURN_STYLE } from "./turn.style.js";
7
7
  export { CURLING_PRESETS, LONG_PRESETS, NET_PRESETS, PITCH_MAX, STEP, TURN_DEFAULT, TURN_PRESETS, TURN_PRESET_NAMES, ZOOM_MAX, ZOOM_MIN, cameraMatrix, clampView, depthOf, drag, eyeDistance, fit, matrix3d, multiply, nudge, presetOf, presetView, presetsFor, project, step } from "./turn.camera.js";
8
8
  export { describeUprights, heightUnit, planesOf, uprightsFor } from "./uprights.js";
9
9
  export { ROOF } from "./venue.js";
10
+ export { LIGHTING_PRESETS, LIGHT_COLORS, LIGHT_PRESETS, WASH, resolveLighting } from "./venue.lighting.js";
@@ -93,11 +93,13 @@ export const KYOGIJO_TURN_STYLE = `
93
93
  .ky-turn[data-lighting]{--ky-cr-bed:#11151a;--ky-cr-empty:#2a313a;--ky-cr-gap:#020304;--ky-cr-conc:#171c22;--ky-cr-dim:1;--ky-up-fascia:#272c29;--ky-up-ground:#1d211e;--ky-night-aisle:#ffd9a3;--ky-night-exit:#7dffb0;--ky-night-rim:#ffe0b0}
94
94
  .ky-turn .u-veil{fill:#04060a;fill-opacity:.62}
95
95
  .ky-turn .u-veil-down{fill-opacity:.8}
96
- .ky-turn[data-lighting=lightsDown]{--ky-cr-bed:#0a0d11;--ky-cr-conc:#101419;--ky-up-fascia:#1b1f1d;--ky-up-ground:#15181a}
96
+ .ky-turn .u-veil-half{fill-opacity:.3}
97
+ .ky-turn:is([data-lighting=lightsDown],[data-lighting=intro],[data-lighting=concert]){--ky-cr-bed:#0a0d11;--ky-cr-conc:#101419;--ky-up-fascia:#1b1f1d;--ky-up-ground:#15181a}
98
+ .ky-turn[data-lighting=game]{--ky-cr-bed:#2a3039;--ky-cr-empty:#4a535f;--ky-cr-gap:#0a0c0f;--ky-cr-conc:#2a3038;--ky-up-fascia:#3b403c;--ky-up-ground:#242823}
97
99
  .ky-turn[data-lighting=night] .ky-cr-aisle{stroke:var(--ky-night-aisle);stroke-opacity:.95;stroke-dasharray:1.1 3.4;stroke-linecap:round}
98
100
  .ky-turn[data-lighting=night] .ky-cr-vom{stroke:var(--ky-night-exit);stroke-opacity:.6;stroke-dasharray:none}
99
101
  .ky-turn[data-lighting] .u-suite-win{fill:color-mix(in srgb,var(--ky-night-aisle) 50%,#2a2218)}
100
- .ky-turn[data-lighting=lightsDown] .u-suite-win{fill:color-mix(in srgb,var(--ky-night-aisle) 10%,#0d1116)}
102
+ .ky-turn:is([data-lighting=lightsDown],[data-lighting=intro],[data-lighting=concert]) .u-suite-win{fill:color-mix(in srgb,var(--ky-night-aisle) 10%,#0d1116)}
101
103
  .ky-turn[data-lighting] .u-coping{fill:color-mix(in srgb,var(--ky-night-rim) 55%,var(--ky-up-fascia))}
102
104
  .ky-turn[data-lighting] .u-suite-ledge{fill:color-mix(in srgb,var(--ky-night-rim) 45%,var(--ky-up-fascia))}
103
105
  .ky-turn[data-lighting] .u-clane{stroke:var(--ky-night-rim);stroke-opacity:.4}
package/dist/types.d.ts CHANGED
@@ -275,13 +275,16 @@ export type SheetEnd = Side | "top" | "bottom";
275
275
  /** The shape of a venue's stands round the playing surface. */
276
276
  export type CrowdShape = "bowl" | "sides" | "horseshoe" | "grandstand";
277
277
  /**
278
- * The lighting of a venue (`crowd.lighting` in the plan, `venue.lighting` in the 3D view). `"night"` (or `true`, or `{ preset: "night" }`) dims the house, lights the playing surface and shows small lights along the aisles and stairs, the ribbon
279
- * boards and the scoreboard glowing, the exits and the concourse rim softly lit. `"lightsDown"` is a show's house: nearly black, each stair aisle lit from above as a beam with a soft falloff on the steps either side, the rails and the front of
280
- * every step edged with light. `wash` colours the playing surface with a light (a CSS colour, or `white`, `amber`, `purple`, `magenta`, `blue`, `cyan`; the markings stay visible) and `aisles` the beams (`lightsDown`). `false` is no lighting. Off by default.
278
+ * The lighting of a venue (`crowd.lighting` in the plan, `venue.lighting` in the 3D view): a named preset (`LIGHT_PRESETS`), `true` (the night), or `{ preset, colors, aisles, wash }`. `"night"` dims the house, lights the playing surface and shows small
279
+ * lights along the aisles and stairs, the ribbon boards and the scoreboard glowing, the exits and the concourse rim softly lit. `"game"` is white light with the house half lit. `"lightsDown"` is a show's house: nearly black, each stair aisle lit from
280
+ * above as a beam with a soft falloff on the steps either side, the rails and the front of every step edged with light. `"intro"` is the same house with the home team's colour washed over the surface and the visitors' in the beams (the teams'
281
+ * colours where the options have them, else blue and amber); `"concert"` has warm amber beams and a purple surface. `colors` are one or two colours that stand for the home and the visitors (before the options' teams); `wash` colours the playing surface
282
+ * with a light (a CSS colour, or `white`, `amber`, `purple`, `magenta`, `blue`, `cyan`; the markings stay visible) and `aisles` the beams, and both win over the preset's. `false` is no lighting. Off by default.
281
283
  */
282
- export type LightingPreset = "night" | "lightsDown";
284
+ export type LightingPreset = "night" | "game" | "lightsDown" | "intro" | "concert";
283
285
  export type LightingOptions = LightingPreset | boolean | {
284
286
  preset?: LightingPreset;
287
+ colors?: readonly string[];
285
288
  aisles?: string;
286
289
  wash?: string;
287
290
  };
package/dist/venue.d.ts CHANGED
@@ -30,6 +30,11 @@ export interface VenueBuild extends VenueSettings {
30
30
  id: string;
31
31
  /** Leave the scoreboard hung over the middle out (a tournament hall has scoreboards of its own). */
32
32
  noCentre?: boolean;
33
+ /** The teams' colours the drawing has (a crowd's `home` and `away`, or a score's teams'): what a lighting's team colours are. */
34
+ team?: {
35
+ home?: string;
36
+ away?: string;
37
+ };
33
38
  }
34
39
  /** What the venue is made of: the things that stand, and the patterns they are drawn with (the contents of a `defs`). */
35
40
  export interface Venue {
package/dist/venue.js CHANGED
@@ -10,7 +10,7 @@ import { groundById } from "./grounds.js";
10
10
  import { ARC_STEPS, clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
11
11
  import { APRON_RISE, apronPlanes, backEdges, backWall, concourseFloor, rimAt, suiteWall } from "./venue.fill.js";
12
12
  import { firstHit, realBuilding } from "./venue.real.js";
13
- import { beamSvg, edgeSvg, lightPool, lightWash, resolveLighting } from "./venue.lighting.js";
13
+ import { beamSvg, edgeSvg, lightPool, lightWash, resolveLighting, teamColors } from "./venue.lighting.js";
14
14
  import { centreBoard, fasciaRibbon, hangsOver, ledStrip, resolveBoards, ribbonHigh, ribbonShare, tierLinks } from "./venue.boards.js";
15
15
  import { n, shy, wall } from "./turn.kit.js";
16
16
  export const NO_VENUE = { items: [], defs: "", reach: 0 };
@@ -38,7 +38,7 @@ export function standsVenue(o, v, lift) {
38
38
  const crowd = resolveCrowd(o.crowd);
39
39
  if (stands === undefined || crowd === undefined || stands.gallery === true)
40
40
  return NO_VENUE;
41
- return standsVenueOf(stands, crowd, { sport: o.sport, detail: o.detail, ground: o.ground }, v, lift);
41
+ return standsVenueOf(stands, crowd, { sport: o.sport, detail: o.detail, ground: o.ground }, { ...v, team: v.team ?? teamColors(o, crowd) }, lift);
42
42
  }
43
43
  /**
44
44
  * The stands the model says (`crowdStands`, or the stands of a tournament layout's hall) as planes: what `standsVenue` makes of a surface's, for stands that were worked out some other way. `o` is the little of the
@@ -76,17 +76,17 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
76
76
  svg += fine ? `<polygon fill="url(#${pid}-cr-${s.kind === "tifo-home" ? "th" : "ta"})" points="${points}"/>` : `<polygon class="${s.kind === "tifo-home" ? "u-th" : "u-ta"}" points="${points}"/>`;
77
77
  }
78
78
  if (light !== undefined)
79
- svg += `<polygon class="${down ? "u-veil u-veil-down" : "u-veil"}" points="${pointList(corners)}"/>`;
79
+ svg += `<polygon class="${light.house === "dark" ? "u-veil u-veil-down" : light.house === "half" ? "u-veil u-veil-half" : "u-veil"}" points="${pointList(corners)}"/>`;
80
80
  // The lights down: the front of each step is edged with light, and each stair aisle is a beam from above with the rails beside it (the aisle lines the stands' dark ones are, are left out).
81
81
  if (down && light !== undefined) {
82
82
  if (corners.length === 4)
83
- svg += edgeSvg(corners[0], corners[1], light.aisles, "u-edge");
83
+ svg += edgeSvg(corners[0], corners[1], light.beam, "u-edge");
84
84
  if (fine) {
85
85
  ring.aisles.forEach(([a, b], i) => {
86
86
  const seg = clipSegment(a, b, piece);
87
87
  if (seg === undefined || Math.hypot(seg[1][0] - seg[0][0], seg[1][1] - seg[0][1]) < 0.5)
88
88
  return;
89
- svg += beamSvg(local(seg[0]), local(seg[1]), light.aisles, "u-beam", i % 4 === 0);
89
+ svg += beamSvg(local(seg[0]), local(seg[1]), light.beam, "u-beam", i % 4 === 0);
90
90
  });
91
91
  }
92
92
  }
@@ -116,9 +116,9 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
116
116
  const links = tierLinks(stands);
117
117
  for (const l of links)
118
118
  chained.add(l.after);
119
- const light = resolveLighting(v.lighting !== undefined ? v.lighting : crowd.lighting);
120
- const night = light?.preset === "night";
121
- const down = light?.preset === "lightsDown";
119
+ const light = resolveLighting(v.lighting !== undefined ? v.lighting : crowd.lighting, v.team ?? teamColors({}, crowd));
120
+ const night = light?.aisles === "dots";
121
+ const down = light?.aisles === "beams";
122
122
  const boards = resolveBoards(v.boards !== undefined ? v.boards : crowd.boards, { home: crowd.home, away: crowd.away }, light !== undefined);
123
123
  const ribbons = [];
124
124
  /** How far along the ribbon of one level the next piece of it starts, in feet, so that its panels carry on. */
@@ -208,7 +208,7 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
208
208
  }
209
209
  }
210
210
  // The surface is lit: a pool of light on the floor round it, under the seats.
211
- if (night)
211
+ if (light?.pool === true)
212
212
  seatPlanes.push(lightPool("night-pool", stands.box, lift + APRON_RISE + 0.03, light?.wash));
213
213
  // A colour of light on the surface (a show's wash), over the markings, which stay visible through it.
214
214
  if (light?.wash !== undefined)
@@ -1,7 +1,6 @@
1
1
  import { type Plane } from "./turn.kit.ts";
2
2
  import type { LightingOptions, LightingPreset } from "./types.ts";
3
- export declare const LIGHTING_PRESETS: readonly LightingPreset[];
4
- /** The named colours of light: `wash` and `aisles` take these by name (ours, a little saturated, for a light seen on dark). */
3
+ /** The named colours of light: `wash`, `aisles` and `colors` take these by name (ours, a little saturated, for a light seen on dark). */
5
4
  export declare const LIGHT_COLORS: {
6
5
  readonly white: "#fff6e6";
7
6
  readonly amber: "#ffb830";
@@ -10,14 +9,57 @@ export declare const LIGHT_COLORS: {
10
9
  readonly blue: "#2b7bff";
11
10
  readonly cyan: "#27d6ff";
12
11
  };
13
- /** The lighting as asked, with every part's value. `aisles` is the colour of the beams and `wash` the surface's colour of light (none for the surface as it is). */
12
+ /** How dark the house is: `half` (a light veil, the game's), `dim` (the night's) or `dark` (a show's). */
13
+ export type House = "half" | "dim" | "dark";
14
+ /**
15
+ * A named lighting. `house` is how dark the stands are; `aisles` how the aisles are lit (`none`, small `dots`, or a `beam` of light from above down each); `pool` whether a pool of light lies on the floor round the surface; `wash` and `beam` are the colours
16
+ * of the light on the surface and of the beams: a colour (a name of `LIGHT_COLORS`), or `home` and `away`, the teams' colours where the options have them (`crowd.home` and `crowd.away`, the colours of a score's teams), else `fallback`'s.
17
+ */
18
+ export interface LightPreset {
19
+ house: House;
20
+ aisles: "none" | "dots" | "beams";
21
+ pool: boolean;
22
+ wash?: keyof typeof LIGHT_COLORS | "home" | "away";
23
+ beam?: keyof typeof LIGHT_COLORS | "home" | "away";
24
+ /** What `home` and `away` are where the options name no team colour. */
25
+ fallback?: {
26
+ home: keyof typeof LIGHT_COLORS;
27
+ away: keyof typeof LIGHT_COLORS;
28
+ };
29
+ }
30
+ /**
31
+ * The named lightings. `night`: the calm dim house with small lights along the aisles. `game`: white light, the house half lit, a pool of light round the surface. `lightsDown`: the house nearly black, a white beam down each aisle. `intro`: the
32
+ * house dark, the home team's colour washed over the surface and the visitors' (the second colour) in the beams. `concert`: warm amber beams and a purple surface. (The `celebration` of the plan, colours chasing round the bowl, is a later one.)
33
+ */
34
+ export declare const LIGHT_PRESETS: Readonly<Record<LightingPreset, LightPreset>>;
35
+ export declare const LIGHTING_PRESETS: readonly LightingPreset[];
36
+ /** The lighting as asked, with every part's value. */
14
37
  export interface Lighting {
15
38
  preset: LightingPreset;
16
- aisles: string;
39
+ house: House;
40
+ aisles: "none" | "dots" | "beams";
41
+ pool: boolean;
42
+ /** The colour of the beams. */
43
+ beam: string;
44
+ /** The surface's colour of light (none for the surface as it is). */
17
45
  wash: string | undefined;
18
46
  }
19
- /** The lighting an option asks for: a named preset, `true` for the first (night), `{ preset, aisles, wash }`, or `undefined` for none (false, nothing, an unknown name). */
20
- export declare function resolveLighting(ask: LightingOptions | undefined | null): Lighting | undefined;
47
+ /** The teams' colours the options have: a score's teams' `color`, over which a crowd's `home` and `away` stand. */
48
+ export declare function teamColors(o: object, crowd?: {
49
+ home?: string;
50
+ away?: string;
51
+ }): {
52
+ home?: string;
53
+ away?: string;
54
+ };
55
+ /**
56
+ * The lighting an option asks for: a preset's name, `true` for the first (night), or `{ preset, colors, aisles, wash }`, or `undefined` for none (false, nothing, an unknown name). A colour comes from, in order: `wash` or `aisles`, `colors` (the first
57
+ * is what the preset's `home` is, the second its `away`), the teams' colours (`team`), the preset's own.
58
+ */
59
+ export declare function resolveLighting(ask: LightingOptions | undefined | null, team?: {
60
+ home?: string;
61
+ away?: string;
62
+ }): Lighting | undefined;
21
63
  /** How far the pool of light round the surface reaches beyond its box, in feet, and the share of the pool that is bright (the surface's own edge). The surface itself stays as drawn: the pool is brightest just outside its markings. */
22
64
  export declare const POOL: {
23
65
  readonly reach: 20;
@@ -4,19 +4,43 @@
4
4
  // Pure: data in, planes out.
5
5
  import { flat, n } from "./turn.kit.js";
6
6
  import { safeColor } from "./venue.boards.js";
7
- export const LIGHTING_PRESETS = ["night", "lightsDown"];
8
- /** The named colours of light: `wash` and `aisles` take these by name (ours, a little saturated, for a light seen on dark). */
7
+ /** The named colours of light: `wash`, `aisles` and `colors` take these by name (ours, a little saturated, for a light seen on dark). */
9
8
  export const LIGHT_COLORS = { white: "#fff6e6", amber: "#ffb830", purple: "#9b4dff", magenta: "#ff2bd6", blue: "#2b7bff", cyan: "#27d6ff" };
9
+ /**
10
+ * The named lightings. `night`: the calm dim house with small lights along the aisles. `game`: white light, the house half lit, a pool of light round the surface. `lightsDown`: the house nearly black, a white beam down each aisle. `intro`: the
11
+ * house dark, the home team's colour washed over the surface and the visitors' (the second colour) in the beams. `concert`: warm amber beams and a purple surface. (The `celebration` of the plan, colours chasing round the bowl, is a later one.)
12
+ */
13
+ export const LIGHT_PRESETS = {
14
+ night: { house: "dim", aisles: "dots", pool: true },
15
+ game: { house: "half", aisles: "none", pool: true },
16
+ lightsDown: { house: "dark", aisles: "beams", pool: false },
17
+ intro: { house: "dark", aisles: "beams", pool: false, wash: "home", beam: "away", fallback: { home: "blue", away: "amber" } },
18
+ concert: { house: "dark", aisles: "beams", pool: false, wash: "purple", beam: "amber" },
19
+ };
20
+ export const LIGHTING_PRESETS = Object.keys(LIGHT_PRESETS);
10
21
  const colorOf = (value) => (typeof value === "string" && Object.hasOwn(LIGHT_COLORS, value) ? LIGHT_COLORS[value] : safeColor(value));
11
- /** The lighting an option asks for: a named preset, `true` for the first (night), `{ preset, aisles, wash }`, or `undefined` for none (false, nothing, an unknown name). */
12
- export function resolveLighting(ask) {
22
+ /** The teams' colours the options have: a score's teams' `color`, over which a crowd's `home` and `away` stand. */
23
+ export function teamColors(o, crowd) {
24
+ const teams = o;
25
+ return { home: safeColor(crowd?.home) ?? safeColor(teams.home?.color), away: safeColor(crowd?.away) ?? safeColor(teams.away?.color) };
26
+ }
27
+ /**
28
+ * The lighting an option asks for: a preset's name, `true` for the first (night), or `{ preset, colors, aisles, wash }`, or `undefined` for none (false, nothing, an unknown name). A colour comes from, in order: `wash` or `aisles`, `colors` (the first
29
+ * is what the preset's `home` is, the second its `away`), the teams' colours (`team`), the preset's own.
30
+ */
31
+ export function resolveLighting(ask, team = {}) {
13
32
  if (ask === undefined || ask === null || ask === false)
14
33
  return undefined;
15
34
  const o = typeof ask === "object" ? ask : {};
16
35
  const name = typeof ask === "string" ? ask : ask === true ? "night" : o.preset ?? "night";
17
36
  if (!LIGHTING_PRESETS.includes(name))
18
37
  return undefined;
19
- return { preset: name, aisles: colorOf(o.aisles) ?? LIGHT_COLORS.white, wash: colorOf(o.wash) };
38
+ const preset = name;
39
+ const base = LIGHT_PRESETS[preset];
40
+ const given = (Array.isArray(o.colors) ? o.colors : []).map(colorOf).filter((c) => c !== undefined);
41
+ const side = (which) => (which === "home" ? given[0] ?? team.home : given[1] ?? team.away) ?? LIGHT_COLORS[base.fallback?.[which] ?? (which === "home" ? "blue" : "amber")];
42
+ const pick = (token) => (token === undefined ? undefined : token === "home" || token === "away" ? side(token) : LIGHT_COLORS[token]);
43
+ return { preset, house: base.house, aisles: base.aisles, pool: base.pool, beam: colorOf(o.aisles) ?? pick(base.beam) ?? LIGHT_COLORS.white, wash: colorOf(o.wash) ?? pick(base.wash) };
20
44
  }
21
45
  /** How far the pool of light round the surface reaches beyond its box, in feet, and the share of the pool that is bright (the surface's own edge). The surface itself stays as drawn: the pool is brightest just outside its markings. */
22
46
  export const POOL = { reach: 20, edge: 0.7, strength: 0.34 };
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.39.1";
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.39.1";
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.39.1",
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",