@johnmorrisdotca/kyogijo-stadiums 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ All notable changes to this project are written here. The format follows
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.39.0] - 2026-10-21
11
+
12
+ ### Added
13
+
14
+ - **Lights down** (`lighting: "lightsDown"`, [lights down](./docs/venues.md#lights-down)): a show's house on the same `lighting` option as the night. The house is nearly black; each stair aisle is lit from above as a beam, drawn in the plane of the seats (so it follows the tier's rake) as a wide faint stroke, a narrower one and a bright core, which is the soft falloff on the steps either side, with two thin lit rails beside it (a vomitory has a broader beam) and the front of every step edged with a thin line of light; the boards glow as at night. In the plan the same, as `ky-cr-beam` and `ky-cr-edge` under a heavier veil. 3D, plan, a district, a tournament hall and the usual stands all take it.
15
+ - **A wash of colour of light on the surface** (`lighting: { preset, wash }`): one flat plane over the playing surface in 3D (`light-wash`, with a rink's rounded corners) and a rectangle in the plan (`ky-cr-wash`), a radial gradient brighter over the middle (`WASH`: 0.46 to 0.26, never so much that the markings under it are lost, and a test holds it), with the night's pool of light taking the colour as well. `aisles` is the colour of the beams. Both take a CSS colour (anything else is ignored) or a name from `LIGHT_COLORS`: `white` (the beams' default), `amber`, `purple`, `magenta`, `blue`, `cyan`. `resolveLighting` now answers `{ preset, aisles, wash }`; `LightingPreset` is `"night" | "lightsDown"` and `LightingOptions` takes `{ preset, aisles, wash }`. `TurnModel.lighting` and the view's `data-lighting` say the preset (`"lightsDown"` is darker than `"night"` in the stylesheet). Off by default, and a drawing without lighting is exactly what it was; the presets, the chase and the team colours of the show-lighting plan are to come, on the same option. The beams, edges and wash are static shapes (colours baked in): nothing is worked out as the view turns.
16
+ - **A Lights down button and a wash choice on the demo** (kept in the address as `lights=1` and `wash=purple`, `magenta`, `blue` or `white`): in the bar over the venues page's view and the home page's featured arena (Lights down and Night turn each other off; the beams take a colour that goes with the wash), and as a checkbox and a choice on the stadiums, tournament layouts and district pages. In 3D they change the lighting while the view turns, without stopping it or sending it back.
17
+
10
18
  ## [0.38.0] - 2026-10-21
11
19
 
12
20
  ### Added
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, 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, night, lights down, building, markings and labels switched on the view while it turns.
585
585
 
586
586
  ```ts
587
587
  import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
@@ -4,7 +4,8 @@
4
4
  // the same crowd. Written for this package; no photograph or other drawing was copied.
5
5
  import { el, num } from "./format.js";
6
6
  import { hangsOver, planRibbons, planScoreboard, resolveBoards } from "./venue.boards.js";
7
- import { POOL, resolveLighting } from "./venue.lighting.js";
7
+ import { NHL } from "./geometry.js";
8
+ import { POOL, WASH, beamSvg, resolveLighting } from "./venue.lighting.js";
8
9
  import { NORTH_AT, generator, outlinePath } from "./crowd.js";
9
10
  /** The width and the height of a tile of the crowd, in feet, and the seats' pitch. */
10
11
  const TILE = 14.4;
@@ -196,13 +197,25 @@ export function drawCrowd(stands, o) {
196
197
  for (const s of stands.sectors)
197
198
  body += sector(s, id, fine, density);
198
199
  // The house is down: a dark veil over the seats, under the aisles' lights.
199
- const night = resolveLighting(crowd.lighting) === "night" && stands.gallery !== true;
200
- if (night)
200
+ const light = stands.gallery === true ? undefined : resolveLighting(crowd.lighting);
201
+ const night = light?.preset === "night";
202
+ const down = light?.preset === "lightsDown";
203
+ if (light !== undefined)
201
204
  for (const ring of stands.rings)
202
- body += el("path", { class: "ky-cr-veil", d: path(ring) });
203
- if (fine)
204
- for (const ring of stands.rings)
205
- body += stands.gallery === true ? "" : rows(ring) + aisles(ring, night);
205
+ body += el("path", { class: down ? "ky-cr-veil ky-cr-veil-down" : "ky-cr-veil", d: path(ring) });
206
+ if (fine) {
207
+ for (const ring of stands.rings) {
208
+ if (stands.gallery === true)
209
+ continue;
210
+ if (!down || light === undefined)
211
+ body += rows(ring) + aisles(ring, night);
212
+ else {
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)));
216
+ }
217
+ }
218
+ }
206
219
  // Depth: the front wall of a tier throws a shade on its first rows (and the ground in front of it), the roof's edge on the last rows of the upper tier.
207
220
  // A gallery's railing: a line along its front edge, over the seats.
208
221
  if (stands.gallery === true)
@@ -218,16 +231,21 @@ export function drawCrowd(stands, o) {
218
231
  }
219
232
  }
220
233
  // The LED boards: a lit line along the front of the upper tiers, and the scoreboard over the middle of the surface (over it, in `over`).
221
- const boards = resolveBoards(crowd.boards, { home: crowd.home, away: crowd.away }, night);
234
+ const boards = resolveBoards(crowd.boards, { home: crowd.home, away: crowd.away }, light !== undefined);
222
235
  const lit = boards?.ribbon === true ? planRibbons(stands, boards) : "";
223
236
  if (boards?.scoreboard === true && hangsOver(stands.sport))
224
237
  over += planScoreboard(stands.box, boards);
225
238
  // The surface is lit: a pool of light round it, clear over the markings.
226
239
  let pool = "";
240
+ if (light?.wash !== undefined) {
241
+ const b = stands.box;
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
+ 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
+ }
227
245
  if (night) {
228
246
  const b = stands.box;
229
247
  const [w, h] = [b.w + 2 * POOL.reach, b.h + 2 * POOL.reach];
230
- 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(""));
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(""));
231
249
  over += el("rect", { class: "ky-cr-pool", x: n1(b.x - POOL.reach), y: n1(b.y - POOL.reach), width: n1(w), height: n1(h), fill: `url(#${id}-pool)` });
232
250
  }
233
251
  for (const [x0, y0, x1, y1] of stands.covers)
@@ -236,7 +254,7 @@ export function drawCrowd(stands, o) {
236
254
  const [x0, y0, x1, y1] = g.box;
237
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 });
238
256
  }
239
- 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: 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: down ? "ky-crowd ky-night ky-down" : night ? "ky-crowd ky-night" : "ky-crowd" }, body) + lit, over };
240
258
  }
241
259
  /** A block of visitors or a card display, a polygon over the band it is in. */
242
260
  function sector(s, id, fine, density) {
@@ -278,6 +296,8 @@ export const KYOGIJO_CROWD_STYLE = `
278
296
  .kyogijo .ky-cr-yard{stroke:var(--ky-line);stroke-opacity:.5;fill:none}
279
297
  .kyogijo .ky-night{--ky-cr-bed:#11151a;--ky-cr-empty:#2a313a;--ky-cr-gap:#020304;--ky-cr-conc:#171c22;--ky-cr-rim:#07090c}
280
298
  .kyogijo .ky-cr-veil{fill:#04060a;fill-opacity:.6}
299
+ .kyogijo .ky-cr-veil-down{fill-opacity:.8}
300
+ .kyogijo .ky-cr-beam,.kyogijo .ky-cr-edge{stroke-linecap:butt}
281
301
  .kyogijo .ky-night .ky-cr-aisle{stroke:#ffd9a3;stroke-opacity:.95;stroke-dasharray:1.1 3.4;stroke-linecap:round}
282
302
  .kyogijo .ky-night .ky-cr-vom{stroke:#7dffb0;stroke-opacity:.6;stroke-dasharray:none}
283
303
  .kyogijo .ky-night .ky-cr-shell{fill:#171c22}
@@ -14,7 +14,7 @@ import { resolveLighting } from "./venue.lighting.js";
14
14
  export const WORLD_PX = 1400;
15
15
  /** The lighting a venue is built with, as a model's field: the venue's own setting, else the crowd's, and none where there is no venue. */
16
16
  export const lightingOf = (venue, crowd) => {
17
- const lighting = venue === undefined ? undefined : resolveLighting(venue.lighting !== undefined ? venue.lighting : resolveCrowd(crowd)?.lighting);
17
+ const lighting = venue === undefined ? undefined : resolveLighting(venue.lighting !== undefined ? venue.lighting : resolveCrowd(crowd)?.lighting)?.preset;
18
18
  return lighting === undefined ? {} : { lighting };
19
19
  };
20
20
  /** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
@@ -90,15 +90,18 @@ export const KYOGIJO_TURN_STYLE = `
90
90
  .ky-turn .u-led-bed{fill:#0b0f14}
91
91
  .ky-turn .u-led-screen{fill:#06121d}
92
92
  /* Night: the house is down. The venue's own colours go dark in either theme (the page's theme is the page's), the seats under a veil, small lights show along the aisles, the exits are green, and the windows, the coping and the lanes of the concourse are softly lit. */
93
- .ky-turn[data-lighting=night]{--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}
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
+ .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}
95
97
  .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}
96
98
  .ky-turn[data-lighting=night] .ky-cr-vom{stroke:var(--ky-night-exit);stroke-opacity:.6;stroke-dasharray:none}
97
- .ky-turn[data-lighting=night] .u-suite-win{fill:color-mix(in srgb,var(--ky-night-aisle) 50%,#2a2218)}
98
- .ky-turn[data-lighting=night] .u-coping{fill:color-mix(in srgb,var(--ky-night-rim) 55%,var(--ky-up-fascia))}
99
- .ky-turn[data-lighting=night] .u-suite-ledge{fill:color-mix(in srgb,var(--ky-night-rim) 45%,var(--ky-up-fascia))}
100
- .ky-turn[data-lighting=night] .u-clane{stroke:var(--ky-night-rim);stroke-opacity:.4}
101
- .ky-turn[data-lighting=night] .u-apron{fill:#30363e}
99
+ .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)}
101
+ .ky-turn[data-lighting] .u-coping{fill:color-mix(in srgb,var(--ky-night-rim) 55%,var(--ky-up-fascia))}
102
+ .ky-turn[data-lighting] .u-suite-ledge{fill:color-mix(in srgb,var(--ky-night-rim) 45%,var(--ky-up-fascia))}
103
+ .ky-turn[data-lighting] .u-clane{stroke:var(--ky-night-rim);stroke-opacity:.4}
104
+ .ky-turn[data-lighting] .u-apron{fill:#30363e}
102
105
  .ky-turn .u-back{fill:color-mix(in srgb,var(--ky-up-fascia) 66%,var(--ky-cr-bed))}
103
106
  .ky-turn .u-backrib{stroke:var(--ky-cr-bed);stroke-width:.7;stroke-opacity:.7}
104
107
  .ky-turn .u-coping{fill:color-mix(in srgb,var(--ky-up-fascia) 70%,var(--ky-cr-conc))}
package/dist/types.d.ts CHANGED
@@ -275,12 +275,15 @@ 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. The named presets are the base other lightings are added to; `false` is no lighting. Off by default.
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.
280
281
  */
281
- export type LightingPreset = "night";
282
+ export type LightingPreset = "night" | "lightsDown";
282
283
  export type LightingOptions = LightingPreset | boolean | {
283
284
  preset?: LightingPreset;
285
+ aisles?: string;
286
+ wash?: string;
284
287
  };
285
288
  /** The patterns a ribbon board shows: `bands` (two bands of colour, in panels), `blocks` (a block of colour to a panel), `chevrons` (arrows running along it) or `waves`. */
286
289
  export type LedBoardPattern = "bands" | "blocks" | "chevrons" | "waves";
package/dist/venue.js CHANGED
@@ -5,11 +5,12 @@
5
5
  import { densityOf, resolveCrowd } from "./crowd.js";
6
6
  import { patterns } from "./crowd.draw.js";
7
7
  import { crowdStands } from "./draw.js";
8
+ import { NHL } from "./geometry.js";
8
9
  import { groundById } from "./grounds.js";
9
10
  import { ARC_STEPS, clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
10
11
  import { APRON_RISE, apronPlanes, backEdges, backWall, concourseFloor, rimAt, suiteWall } from "./venue.fill.js";
11
12
  import { firstHit, realBuilding } from "./venue.real.js";
12
- import { lightPool, resolveLighting } from "./venue.lighting.js";
13
+ import { beamSvg, edgeSvg, lightPool, lightWash, resolveLighting } from "./venue.lighting.js";
13
14
  import { centreBoard, fasciaRibbon, hangsOver, ledStrip, resolveBoards, ribbonHigh, ribbonShare, tierLinks } from "./venue.boards.js";
14
15
  import { n, shy, wall } from "./turn.kit.js";
15
16
  export const NO_VENUE = { items: [], defs: "", reach: 0 };
@@ -74,9 +75,22 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
74
75
  else
75
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}"/>`;
76
77
  }
77
- if (night)
78
- svg += `<polygon class="u-veil" points="${pointList(corners)}"/>`;
79
- if (fine) {
78
+ if (light !== undefined)
79
+ svg += `<polygon class="${down ? "u-veil u-veil-down" : "u-veil"}" points="${pointList(corners)}"/>`;
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
+ if (down && light !== undefined) {
82
+ if (corners.length === 4)
83
+ svg += edgeSvg(corners[0], corners[1], light.aisles, "u-edge");
84
+ if (fine) {
85
+ ring.aisles.forEach(([a, b], i) => {
86
+ const seg = clipSegment(a, b, piece);
87
+ if (seg === undefined || Math.hypot(seg[1][0] - seg[0][0], seg[1][1] - seg[0][1]) < 0.5)
88
+ return;
89
+ svg += beamSvg(local(seg[0]), local(seg[1]), light.aisles, "u-beam", i % 4 === 0);
90
+ });
91
+ }
92
+ }
93
+ else if (fine) {
80
94
  let plain = "";
81
95
  let vomitory = "";
82
96
  ring.aisles.forEach(([a, b], i) => {
@@ -102,8 +116,10 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
102
116
  const links = tierLinks(stands);
103
117
  for (const l of links)
104
118
  chained.add(l.after);
105
- const night = resolveLighting(v.lighting !== undefined ? v.lighting : crowd.lighting) === "night";
106
- const boards = resolveBoards(v.boards !== undefined ? v.boards : crowd.boards, { home: crowd.home, away: crowd.away }, night);
119
+ const light = resolveLighting(v.lighting !== undefined ? v.lighting : crowd.lighting);
120
+ const night = light?.preset === "night";
121
+ const down = light?.preset === "lightsDown";
122
+ const boards = resolveBoards(v.boards !== undefined ? v.boards : crowd.boards, { home: crowd.home, away: crowd.away }, light !== undefined);
107
123
  const ribbons = [];
108
124
  /** How far along the ribbon of one level the next piece of it starts, in feet, so that its panels carry on. */
109
125
  let run = 0;
@@ -193,7 +209,10 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
193
209
  }
194
210
  // The surface is lit: a pool of light on the floor round it, under the seats.
195
211
  if (night)
196
- seatPlanes.push(lightPool("night-pool", stands.box, lift + APRON_RISE + 0.03));
212
+ seatPlanes.push(lightPool("night-pool", stands.box, lift + APRON_RISE + 0.03, light?.wash));
213
+ // A colour of light on the surface (a show's wash), over the markings, which stay visible through it.
214
+ if (light?.wash !== undefined)
215
+ seatPlanes.push(lightWash("light-wash", stands.box, lift + APRON_RISE + 0.06, light.wash, stands.sport === "nhl" ? NHL.cornerRadius : 0));
197
216
  const items = [];
198
217
  if (seatPlanes.length > 0)
199
218
  items.push({ id: "stands", kind: "stands", at: centre, height: topOf(seatPlanes), planes: seatPlanes });
@@ -1,8 +1,23 @@
1
1
  import { type Plane } from "./turn.kit.ts";
2
2
  import type { LightingOptions, LightingPreset } from "./types.ts";
3
3
  export declare const LIGHTING_PRESETS: readonly LightingPreset[];
4
- /** The lighting an option asks for: a named preset, `true` for the first (night), `{ preset }`, or `undefined` for none (false, nothing, an unknown name). */
5
- export declare function resolveLighting(ask: LightingOptions | undefined | null): LightingPreset | undefined;
4
+ /** The named colours of light: `wash` and `aisles` take these by name (ours, a little saturated, for a light seen on dark). */
5
+ export declare const LIGHT_COLORS: {
6
+ readonly white: "#fff6e6";
7
+ readonly amber: "#ffb830";
8
+ readonly purple: "#9b4dff";
9
+ readonly magenta: "#ff2bd6";
10
+ readonly blue: "#2b7bff";
11
+ readonly cyan: "#27d6ff";
12
+ };
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). */
14
+ export interface Lighting {
15
+ preset: LightingPreset;
16
+ aisles: string;
17
+ wash: string | undefined;
18
+ }
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;
6
21
  /** 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. */
7
22
  export declare const POOL: {
8
23
  readonly reach: 20;
@@ -15,4 +30,27 @@ export declare function lightPool(id: string, box: {
15
30
  y: number;
16
31
  w: number;
17
32
  h: number;
18
- }, z: number): Plane;
33
+ }, z: number, color?: string): Plane;
34
+ /** The beam of a stair aisle, lit from above: the aisle's line drawn three times, wide and faint, narrower, and a bright core, so that the light falls off over the steps either side; a vomitory (`wide`) is a broader beam. Two thin lines beside it are the rails. Feet along the plane. */
35
+ export declare const BEAM: {
36
+ readonly widths: readonly [7, 3.8, 1.5];
37
+ readonly opacity: readonly [0.1, 0.22, 0.95];
38
+ readonly rail: 2.3;
39
+ readonly railWidth: 0.35;
40
+ readonly railOpacity: 0.6;
41
+ readonly wide: 1.6;
42
+ };
43
+ export declare function beamSvg(from: readonly [number, number], to: readonly [number, number], color: string, cls: string, wide?: boolean): string;
44
+ /** The lit edge of a step: the front of a piece of a tier, from `from` to `to`, a thin bright line. */
45
+ export declare const edgeSvg: (from: readonly [number, number], to: readonly [number, number], color: string, cls: string) => string;
46
+ /** The wash of light on the playing surface: a flat plane over the surface's box, a colour that is brighter over the middle and weaker at the edge, never so strong that the markings under it are lost (`WASH` says how much). */
47
+ export declare const WASH: {
48
+ readonly middle: 0.46;
49
+ readonly edge: 0.26;
50
+ };
51
+ export declare function lightWash(id: string, box: {
52
+ x: number;
53
+ y: number;
54
+ w: number;
55
+ h: number;
56
+ }, z: number, color: string, round?: number): Plane;
@@ -3,24 +3,51 @@
3
3
  // plane of seats, a pool of light on the floor round the surface and a halo under each lit board panel; nothing is worked out again as the view turns. Later presets (a show's colours and chases) are added to the same option.
4
4
  // Pure: data in, planes out.
5
5
  import { flat, n } from "./turn.kit.js";
6
- export const LIGHTING_PRESETS = ["night"];
7
- /** The lighting an option asks for: a named preset, `true` for the first (night), `{ preset }`, or `undefined` for none (false, nothing, an unknown name). */
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). */
9
+ export const LIGHT_COLORS = { white: "#fff6e6", amber: "#ffb830", purple: "#9b4dff", magenta: "#ff2bd6", blue: "#2b7bff", cyan: "#27d6ff" };
10
+ 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). */
8
12
  export function resolveLighting(ask) {
9
13
  if (ask === undefined || ask === null || ask === false)
10
14
  return undefined;
11
- if (ask === true)
12
- return "night";
13
- const name = typeof ask === "string" ? ask : ask.preset ?? "night";
14
- return LIGHTING_PRESETS.includes(name) ? name : undefined;
15
+ const o = typeof ask === "object" ? ask : {};
16
+ const name = typeof ask === "string" ? ask : ask === true ? "night" : o.preset ?? "night";
17
+ if (!LIGHTING_PRESETS.includes(name))
18
+ return undefined;
19
+ return { preset: name, aisles: colorOf(o.aisles) ?? LIGHT_COLORS.white, wash: colorOf(o.wash) };
15
20
  }
16
21
  /** 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. */
17
22
  export const POOL = { reach: 20, edge: 0.7, strength: 0.34 };
18
23
  /** The pool of light on the floor round the playing surface: a flat plane under the seats with a radial gradient that is clear over the middle (the markings stay as they are) and glows round its edge. */
19
- export function lightPool(id, box, z) {
24
+ export function lightPool(id, box, z, color = "#fff4dc") {
20
25
  const [w, h] = [box.w + 2 * POOL.reach, box.h + 2 * POOL.reach];
21
26
  const gid = `${id}-g`;
22
- const svg = `<defs><radialGradient id="${gid}" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#fff4dc" stop-opacity="0"/><stop offset="${POOL.edge - 0.12}" stop-color="#fff4dc" stop-opacity="0"/>` +
23
- `<stop offset="${POOL.edge}" stop-color="#fff4dc" stop-opacity="${POOL.strength}"/><stop offset="1" stop-color="#fff4dc" stop-opacity="0"/></radialGradient></defs>` +
27
+ const svg = `<defs><radialGradient id="${gid}" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="${color}" stop-opacity="0"/><stop offset="${POOL.edge - 0.12}" stop-color="${color}" stop-opacity="0"/>` +
28
+ `<stop offset="${POOL.edge}" stop-color="${color}" stop-opacity="${POOL.strength}"/><stop offset="1" stop-color="${color}" stop-opacity="0"/></radialGradient></defs>` +
24
29
  `<rect class="u-pool" width="${n(w)}" height="${n(h)}" fill="url(#${gid})"/>`;
25
30
  return flat(id, "apron", box.x - POOL.reach, box.y - POOL.reach, box.x + box.w + POOL.reach, box.y + box.h + POOL.reach, z, svg);
26
31
  }
32
+ /** The beam of a stair aisle, lit from above: the aisle's line drawn three times, wide and faint, narrower, and a bright core, so that the light falls off over the steps either side; a vomitory (`wide`) is a broader beam. Two thin lines beside it are the rails. Feet along the plane. */
33
+ export const BEAM = { widths: [7, 3.8, 1.5], opacity: [0.1, 0.22, 0.95], rail: 2.3, railWidth: 0.35, railOpacity: 0.6, wide: 1.6 };
34
+ export function beamSvg(from, to, color, cls, wide = false) {
35
+ const d = `M${n(from[0])} ${n(from[1])}L${n(to[0])} ${n(to[1])}`;
36
+ const k = wide ? BEAM.wide : 1;
37
+ const long = Math.hypot(to[0] - from[0], to[1] - from[1]) || 1;
38
+ const [nx, ny] = [-(to[1] - from[1]) / long, (to[0] - from[0]) / long];
39
+ const off = BEAM.rail * k;
40
+ const rail = (s) => `M${n(from[0] + nx * off * s)} ${n(from[1] + ny * off * s)}L${n(to[0] + nx * off * s)} ${n(to[1] + ny * off * s)}`;
41
+ const layers = BEAM.widths.map((w, i) => `<path class="${cls}" fill="none" stroke="${color}" stroke-opacity="${BEAM.opacity[i]}" stroke-width="${n(w * k)}" d="${d}"/>`).join("");
42
+ return `${layers}<path class="${cls}" fill="none" stroke="${color}" stroke-opacity="${BEAM.railOpacity}" stroke-width="${BEAM.railWidth}" d="${rail(1)}${rail(-1)}"/>`;
43
+ }
44
+ /** The lit edge of a step: the front of a piece of a tier, from `from` to `to`, a thin bright line. */
45
+ export const edgeSvg = (from, to, color, cls) => `<path class="${cls}" fill="none" stroke="${color}" stroke-opacity=".7" stroke-width=".4" d="M${n(from[0])} ${n(from[1])}L${n(to[0])} ${n(to[1])}"/>`;
46
+ /** The wash of light on the playing surface: a flat plane over the surface's box, a colour that is brighter over the middle and weaker at the edge, never so strong that the markings under it are lost (`WASH` says how much). */
47
+ export const WASH = { middle: 0.46, edge: 0.26 };
48
+ export function lightWash(id, box, z, color, round = 0) {
49
+ const gid = `${id}-g`;
50
+ const svg = `<defs><radialGradient id="${gid}" cx=".5" cy=".5" r=".72"><stop offset="0" stop-color="${color}" stop-opacity="${WASH.middle}"/><stop offset="1" stop-color="${color}" stop-opacity="${WASH.edge}"/></radialGradient></defs>` +
51
+ `<rect class="u-wash" width="${n(box.w)}" height="${n(box.h)}" rx="${n(Math.min(round, box.w / 2, box.h / 2))}" fill="url(#${gid})"/>`;
52
+ return flat(id, "apron", box.x, box.y, box.x + box.w, box.y + box.h, z, svg);
53
+ }
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.38.0";
2
+ export declare const VERSION = "0.39.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The package's version: the same as package.json's, and a test holds them to each other. */
2
- export const VERSION = "0.38.0";
2
+ export const VERSION = "0.39.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kyogijo-stadiums",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Kyogijo Stadiums 競技場, 40 sports' surfaces as SVG, drawn to scale from the rulebooks: NFL, CFL and IFAF football fields, an NHL rink, an NBA court, MLB ballparks, soccer pitches, curling sheets, athletics stadiums, speed skating, short track and figure skating rinks, swimming pools, water polo fields, diving wells, artistic swimming areas, and the courts and fields of tennis, volleyball (indoor, beach and sitting), badminton, pickleball, table tennis, padel, squash, racquetball, sepak takraw, teqball, roundnet, netball, lacrosse, rugby, Australian football, Gaelic games, cricket, field hockey and handball, and the budo of judo, karate, kendo, sumo and kyudo with a dojo of tatami laid as real units, the MMA cage, with each team's name and logo on its side and the score in the middle, stands and a crowd, a pageant of flags, and a 3D view. Light and dark, accessible, a compact badge for lists, a helper for React and a custom element. Zero dependencies.",
5
5
  "keywords": [
6
6
  "sports",