@johnmorrisdotca/kyogijo-stadiums 0.36.0 → 0.38.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 +17 -0
- package/README.md +1 -1
- package/dist/crowd.d.ts +5 -1
- package/dist/crowd.draw.js +33 -5
- package/dist/crowd.js +2 -0
- package/dist/district.turn.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/multi.draw.js +2 -1
- package/dist/multi.turn.js +3 -2
- package/dist/multi.types.d.ts +3 -1
- package/dist/strings.d.ts +1 -1
- package/dist/strings.js +2 -2
- package/dist/turn-entry.d.ts +1 -0
- package/dist/turn.dom.js +5 -0
- package/dist/turn.kit.d.ts +2 -2
- package/dist/turn.markup.js +1 -1
- package/dist/turn.model.d.ts +9 -1
- package/dist/turn.model.js +8 -1
- package/dist/turn.style.js +13 -0
- package/dist/types.d.ts +33 -0
- package/dist/uprights.js +2 -0
- package/dist/venue.boards.d.ts +111 -0
- package/dist/venue.boards.js +273 -0
- package/dist/venue.d.ts +13 -1
- package/dist/venue.js +49 -13
- package/dist/venue.lighting.d.ts +18 -0
- package/dist/venue.lighting.js +26 -0
- 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,23 @@ All notable changes to this project are written here. The format follows
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.38.0] - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Night lighting** (`lighting: "night"`, [night lighting](./docs/venues.md#night-lighting), `src/venue.lighting.ts`): a venue lit as at night. The house goes down (a veil over the seats, the venue's colours set dark in the light theme and in the dark one), the playing surface stays lit with a pool of light round it that is clear over the markings, small lights show along the aisles and stairs, the exits (every fourth aisle, the vomitories) are green, the ribbon boards and the scoreboard of [the LED boards](./docs/venues.md#led-boards) glow with a halo under every panel, and the windows, the coping and the lanes of the concourse are softly lit. `venue: { lighting }` in the 3D view, `crowd: { lighting }` in the plan (and read by the 3D view where `venue.lighting` says nothing), `stadium: { lighting }` round a tournament hall; `"night"`, `true` and `{ preset: "night" }` are the same and `false` is none. It is the base later lightings are added to: the option is a preset name, and `TurnModel.lighting` and the view's `data-lighting` say which. Off by default, and a drawing without it is exactly what it was. Nothing is worked out again as the view turns: the lights are custom properties (`--ky-night-aisle`, `--ky-night-exit`, `--ky-night-rim`) and a few static shapes, one added plane (`night-pool`) to a venue; `turn.update(options, { venue: { lighting } })` changes it while the view turns. The lights are at least seven to one against the seats in both themes (a test holds it). New exports: `LightingOptions` and `LightingPreset`. About 3 KB of source in the one new module.
|
|
15
|
+
- **A Night switch on the demo** (kept in the address as `night=1`): a button in the bar over the venues page's view and the home page's featured arena, which lights the plan and the 3D view and, in 3D, changes it without stopping or sending back the turn; a checkbox on the stadiums, tournament layouts and district pages.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **The plan's ribbon boards are outside the crowd's group** (`<g class="ky-cr-ribbon">` follows `<g class="ky-crowd">` and no longer sits inside it), so that the crowd's dimming and the night veil do not reach them.
|
|
20
|
+
## [0.37.0] - 2026-10-20
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **LED boards** (`boards`, [the LED boards](./docs/venues.md#led-boards), `src/venue.boards.ts`): the ribbon boards along the fascia between the seating levels, and a scoreboard hung over the middle of the playing surface, for every real venue (the thirty-two arenas of the NHL, BC Place, Lumen Field, Korakuen Hall and the ballparks), for the usual stands, in a district, and, as `stadium: { boards }`, round a tournament hall. `venue: { boards: true | { ribbon, scoreboard, colors, pattern, score } }` in the 3D view, `crowd: { boards }` in the plan (and read by the 3D view where `venue.boards` says nothing); off by default, and a drawing without it is exactly what it was. The content is generic and ours: panels of colour in `bands`, `blocks`, `chevrons` or `waves`, and a plain score in seven-segment digits (paths, no font); the colours are the asker's, else the crowd's `home` and `away`, else amber, blue and white (any other string than a hex, `rgb()`, `hsl()` or a name is ignored). In 3D a ribbon is the top 3.8 feet of the suite level's wall, a plane of its own (the walk below it is the walk less that strip; a venue with one level has its ribbon on the front of that level), and the scoreboard is four faces with the score on each, a ring of display under them, a cap and a strut, hung over the surface by a fifth of its longer side and sized by it (an arena's is about 28 by 19 feet). A ballpark has no scoreboard over the field, a hall has the layout's own (only ribbons there), and a score drawing's own scoreboard replaces the venue's. Every lit shape carries the class `u-led` (`ky-cr-led` in the plan) over a dark housing, for a lighting layer to make glow; the planes are the kinds `ribbon` and `hung-board` (uprights `ribbon-boards` and `hung-board`, said in words in English and Japanese). `turn.update(options, { venue: { boards } })` changes them while the view turns. New exports: `LedBoardOptions` and `LedBoardPattern`. About 17 KB of source in the one new module (`dist/venue.boards.js`), a ribbon of a 32-arena venue being a few dozen planes.
|
|
25
|
+
- **An LED boards switch on the demo** (kept in the address as `boards=1`): a button in the bar over the venues page's view and the home page's featured arena, which draws it in the plan and in 3D and, in 3D, changes it without stopping or sending back the turn; a checkbox on the stadiums, tournament layouts and district pages.
|
|
26
|
+
|
|
10
27
|
## [0.36.0] - 2026-10-20
|
|
11
28
|
|
|
12
29
|
### 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)). 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, 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, building, markings and labels switched on the view while it turns.
|
|
585
585
|
|
|
586
586
|
```ts
|
|
587
587
|
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
|
package/dist/crowd.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CrowdShape, Side, Sport, SurfaceOptions } from "./types.ts";
|
|
1
|
+
import type { CrowdShape, LedBoardOptions, LightingOptions, Side, Sport, SurfaceOptions } from "./types.ts";
|
|
2
2
|
export type Pt = [number, number];
|
|
3
3
|
export type Box = {
|
|
4
4
|
x: number;
|
|
@@ -188,6 +188,10 @@ export interface Crowd {
|
|
|
188
188
|
/** Whether the seats are drawn (a real venue's building is, without them, when this is false). */
|
|
189
189
|
stands: boolean;
|
|
190
190
|
detail: "auto" | "fine" | "plain";
|
|
191
|
+
/** The LED boards as asked (`resolveBoards` reads them), or `undefined` for none. */
|
|
192
|
+
boards: boolean | LedBoardOptions | undefined;
|
|
193
|
+
/** The lighting as asked (`resolveLighting` reads it), or `undefined`. */
|
|
194
|
+
lighting: LightingOptions | undefined;
|
|
191
195
|
}
|
|
192
196
|
/** The crowd the options ask for, or `undefined` for none. */
|
|
193
197
|
export declare function resolveCrowd(value: SurfaceOptions["crowd"]): Crowd | undefined;
|
package/dist/crowd.draw.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// All of it is patterns, paths and rectangles, which every renderer of the package reads. The tiles are made from a seed with a generator of our own, so the same options give
|
|
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
|
+
import { hangsOver, planRibbons, planScoreboard, resolveBoards } from "./venue.boards.js";
|
|
7
|
+
import { POOL, resolveLighting } from "./venue.lighting.js";
|
|
6
8
|
import { NORTH_AT, generator, outlinePath } from "./crowd.js";
|
|
7
9
|
/** The width and the height of a tile of the crowd, in feet, and the seats' pitch. */
|
|
8
10
|
const TILE = 14.4;
|
|
@@ -67,11 +69,11 @@ export function patterns(id, crowd, density, tifo, neutral = false, rows = false
|
|
|
67
69
|
}
|
|
68
70
|
const path = (ring) => [ring.region, ...(ring.more ?? [])].map((o) => outlinePath(o)).join("") + (ring.hole === null ? "" : outlinePath(ring.hole, true));
|
|
69
71
|
/** The aisles of a band as one path: the lines the stands data gives, every fourth a vomitory, drawn wider. */
|
|
70
|
-
function aisles(ring) {
|
|
72
|
+
function aisles(ring, night = false) {
|
|
71
73
|
const line = ([[x0, y0], [x1, y1]]) => `M${Math.round(x0)} ${Math.round(y0)}L${Math.round(x1)} ${Math.round(y1)}`;
|
|
72
74
|
const plain = ring.aisles.filter((_, i) => i % 4 !== 0).map(line).join("");
|
|
73
75
|
const vomitories = ring.aisles.filter((_, i) => i % 4 === 0).map(line).join("");
|
|
74
|
-
return (plain === "" ? "" : el("path", { class: "ky-cr-aisle", d: plain, "stroke-width": 1.2 })) + (vomitories === "" ? "" : el("path", { class: "ky-cr-aisle", d: vomitories, "stroke-width": 2.6 }));
|
|
76
|
+
return (plain === "" ? "" : el("path", { class: "ky-cr-aisle", d: plain, "stroke-width": 1.2 })) + (vomitories === "" ? "" : el("path", { class: night ? "ky-cr-aisle ky-cr-vom" : "ky-cr-aisle", d: vomitories, "stroke-width": 2.6 }));
|
|
75
77
|
}
|
|
76
78
|
/** The pitch of the row lines, in feet: every second row of seats. */
|
|
77
79
|
const ROW = 5;
|
|
@@ -193,9 +195,14 @@ export function drawCrowd(stands, o) {
|
|
|
193
195
|
}
|
|
194
196
|
for (const s of stands.sectors)
|
|
195
197
|
body += sector(s, id, fine, density);
|
|
198
|
+
// 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)
|
|
201
|
+
for (const ring of stands.rings)
|
|
202
|
+
body += el("path", { class: "ky-cr-veil", d: path(ring) });
|
|
196
203
|
if (fine)
|
|
197
204
|
for (const ring of stands.rings)
|
|
198
|
-
body += stands.gallery === true ? "" : rows(ring) + aisles(ring);
|
|
205
|
+
body += stands.gallery === true ? "" : rows(ring) + aisles(ring, night);
|
|
199
206
|
// 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.
|
|
200
207
|
// A gallery's railing: a line along its front edge, over the seats.
|
|
201
208
|
if (stands.gallery === true)
|
|
@@ -210,13 +217,26 @@ export function drawCrowd(stands, o) {
|
|
|
210
217
|
body += el("path", { d: outlinePath(ring.region), fill: "none", stroke: "#000", "stroke-opacity": 0.16, "stroke-width": 18 });
|
|
211
218
|
}
|
|
212
219
|
}
|
|
220
|
+
// 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);
|
|
222
|
+
const lit = boards?.ribbon === true ? planRibbons(stands, boards) : "";
|
|
223
|
+
if (boards?.scoreboard === true && hangsOver(stands.sport))
|
|
224
|
+
over += planScoreboard(stands.box, boards);
|
|
225
|
+
// The surface is lit: a pool of light round it, clear over the markings.
|
|
226
|
+
let pool = "";
|
|
227
|
+
if (night) {
|
|
228
|
+
const b = stands.box;
|
|
229
|
+
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(""));
|
|
231
|
+
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
|
+
}
|
|
213
233
|
for (const [x0, y0, x1, y1] of stands.covers)
|
|
214
234
|
body += el("rect", { class: o.ground, x: n1(x0), y: n1(y0), width: n1(x1 - x0), height: n1(y1 - y0) });
|
|
215
235
|
for (const g of stands.gaps) {
|
|
216
236
|
const [x0, y0, x1, y1] = g.box;
|
|
217
237
|
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 });
|
|
218
238
|
}
|
|
219
|
-
return { defs: (fine && (stands.gallery !== true || density > 0) ? patterns(id, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.gallery === true || stands.real !== undefined) : ""), markup: el("g", { class: "ky-crowd" }, body), over };
|
|
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 };
|
|
220
240
|
}
|
|
221
241
|
/** A block of visitors or a card display, a polygon over the band it is in. */
|
|
222
242
|
function sector(s, id, fine, density) {
|
|
@@ -255,4 +275,12 @@ export const KYOGIJO_CROWD_STYLE = `
|
|
|
255
275
|
.kyogijo .ky-cr-north-n{fill:none;stroke:var(--ky-cr-rim);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
|
|
256
276
|
.kyogijo .ky-cr-plaza{fill:var(--ky-apron);stroke:var(--ky-apron);stroke-width:8;stroke-linejoin:round}
|
|
257
277
|
.kyogijo .ky-cr-ez{fill:var(--ky-line);fill-opacity:.16}
|
|
258
|
-
.kyogijo .ky-cr-yard{stroke:var(--ky-line);stroke-opacity:.5;fill:none}
|
|
278
|
+
.kyogijo .ky-cr-yard{stroke:var(--ky-line);stroke-opacity:.5;fill:none}
|
|
279
|
+
.kyogijo .ky-night{--ky-cr-bed:#11151a;--ky-cr-empty:#2a313a;--ky-cr-gap:#020304;--ky-cr-conc:#171c22;--ky-cr-rim:#07090c}
|
|
280
|
+
.kyogijo .ky-cr-veil{fill:#04060a;fill-opacity:.6}
|
|
281
|
+
.kyogijo .ky-night .ky-cr-aisle{stroke:#ffd9a3;stroke-opacity:.95;stroke-dasharray:1.1 3.4;stroke-linecap:round}
|
|
282
|
+
.kyogijo .ky-night .ky-cr-vom{stroke:#7dffb0;stroke-opacity:.6;stroke-dasharray:none}
|
|
283
|
+
.kyogijo .ky-night .ky-cr-shell{fill:#171c22}
|
|
284
|
+
.kyogijo .ky-cr-led-bed{fill:#0b0f14}
|
|
285
|
+
.kyogijo .ky-cr-led-line{fill:none;stroke:#0b0f14;stroke-linejoin:round}
|
|
286
|
+
.kyogijo .ky-cr-led{stroke-linecap:butt}`.trim();
|
package/dist/crowd.js
CHANGED
|
@@ -34,6 +34,8 @@ export function resolveCrowd(value) {
|
|
|
34
34
|
real: o.real === true,
|
|
35
35
|
stands: o.stands !== false,
|
|
36
36
|
detail: o.detail === "fine" || o.detail === "plain" ? o.detail : "auto",
|
|
37
|
+
boards: o.boards === true || (typeof o.boards === "object" && o.boards !== null) ? o.boards : undefined,
|
|
38
|
+
lighting: o.lighting,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
/** How full the stands are: a density, else attendance over capacity (the ground's, else the model's seats), else 0.85. */
|
package/dist/district.turn.js
CHANGED
|
@@ -7,7 +7,7 @@ import { n, plane } from "./turn.kit.js";
|
|
|
7
7
|
import { TURN_PRESETS, wrap } from "./turn.camera.js";
|
|
8
8
|
import { mountTurn } from "./turn.dom.js";
|
|
9
9
|
import { turnMarkupOf } from "./turn.markup.js";
|
|
10
|
-
import { buildTurn, openingView, sceneBox, WORLD_PX } from "./turn.model.js";
|
|
10
|
+
import { buildTurn, lightingOf, openingView, sceneBox, WORLD_PX } from "./turn.model.js";
|
|
11
11
|
import { planesOf } from "./uprights.js";
|
|
12
12
|
import { FEET_PER_METRE } from "./geometry.js";
|
|
13
13
|
import { hullOf } from "./venues.geo.js";
|
|
@@ -124,6 +124,7 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
124
124
|
defs: defs.join(""),
|
|
125
125
|
crowdStyle,
|
|
126
126
|
venue: true,
|
|
127
|
+
...lightingOf(typeof settings.venue === "object" ? settings.venue : {}, undefined),
|
|
127
128
|
look: undefined,
|
|
128
129
|
planes,
|
|
129
130
|
box,
|
package/dist/index.d.ts
CHANGED
|
@@ -76,4 +76,4 @@ export type { SoccerPitchClass, SoccerPitchId, SoccerSize, SoccerSpec } from "./
|
|
|
76
76
|
export { SOCCER_PROVIDER_IDS, SOCCER_PROVIDERS, frameMatrix, rescale, soccerFrame, soccerLandmarks } from "./surfaces/soccer-providers.ts";
|
|
77
77
|
export type { Rescaler, SoccerFrame, SoccerProvider, SoccerProviderId } from "./surfaces/soccer-providers.ts";
|
|
78
78
|
export { VERSION } from "./version.ts";
|
|
79
|
-
export type { AxesOptions, Box, Colors, CourtStyle, CrowdOptions, CrowdShape, Detail, DirectionOptions, BudoSport, FieldSport, FieldStyle, Fence, GoalInfo, GoalStyle, HardwoodOptions, Language, NetStyle, LineDash, LineWidth, LookName, Orientation, PoolSport, PoolStyle, QuirkName, QuirkOptions, RangeBox, PageantOptions, PageantStyle, RangeName, RangeNamed, ScoreOptions, Side, Size, Sport, StatusWord, StripePattern, SurfaceOptions, Team, TeamMark, Theme, WallOptions, } from "./types.ts";
|
|
79
|
+
export type { AxesOptions, Box, Colors, CourtStyle, CrowdOptions, LedBoardOptions, LedBoardPattern, LightingOptions, LightingPreset, CrowdShape, Detail, DirectionOptions, BudoSport, FieldSport, FieldStyle, Fence, GoalInfo, GoalStyle, HardwoodOptions, Language, NetStyle, LineDash, LineWidth, LookName, Orientation, PoolSport, PoolStyle, QuirkName, QuirkOptions, RangeBox, PageantOptions, PageantStyle, RangeName, RangeNamed, ScoreOptions, Side, Size, Sport, StatusWord, StripePattern, SurfaceOptions, Team, TeamMark, Theme, WallOptions, } from "./types.ts";
|
package/dist/multi.draw.js
CHANGED
|
@@ -15,6 +15,7 @@ import { outlineOf } from "./outline.js";
|
|
|
15
15
|
import { fill } from "./strings.js";
|
|
16
16
|
import { textWidth } from "./text.js";
|
|
17
17
|
import { uprightsFor } from "./uprights.js";
|
|
18
|
+
import { hallBoards } from "./venue.boards.js";
|
|
18
19
|
import { VERSION } from "./version.js";
|
|
19
20
|
const metres = (ft) => ft / FEET_PER_METRE;
|
|
20
21
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
@@ -267,7 +268,7 @@ export function multiDrawing(options, raise, uprights = true) {
|
|
|
267
268
|
}
|
|
268
269
|
if (stadium !== undefined) {
|
|
269
270
|
const standsLike = venue?.standsLike ?? spec.sport;
|
|
270
|
-
const crowd = resolveCrowd({ gaps: false, awayEnd: "none", roof: stadium.roof, shape: stadium.shape, density: stadium.density, detail: stadium.detail === "plain" ? "plain" : "fine" });
|
|
271
|
+
const crowd = resolveCrowd({ gaps: false, awayEnd: "none", roof: stadium.roof, shape: stadium.shape, density: stadium.density, detail: stadium.detail === "plain" ? "plain" : "fine", boards: hallBoards(stadium.boards), lighting: stadium.lighting });
|
|
271
272
|
const stands = layoutOf({ sport: standsLike, box: wide, crowd, awaySide: "right", arena: venue?.shape === "octagon" });
|
|
272
273
|
const wall = venue?.shape === "octagon" ? el("path", { class: "ky-mu-wall", d: octagonD(hall.w / 2, hall.w / 2, hall.w / 2) }) : el("rect", { class: "ky-mu-wall", x: 0, y: 0, width: num(hall.w), height: num(hall.h), rx: 0.5 });
|
|
273
274
|
if (raise)
|
package/dist/multi.turn.js
CHANGED
|
@@ -10,7 +10,7 @@ import { TURN_PRESETS, NET_PRESETS, wrap } from "./turn.camera.js";
|
|
|
10
10
|
import { box, flat, n, plane, rect, shy, thinnest, wall } from "./turn.kit.js";
|
|
11
11
|
import { mountTurn } from "./turn.dom.js";
|
|
12
12
|
import { turnMarkupOf } from "./turn.markup.js";
|
|
13
|
-
import { openingView, sceneBox } from "./turn.model.js";
|
|
13
|
+
import { lightingOf, openingView, sceneBox } from "./turn.model.js";
|
|
14
14
|
import { describeUprights, planesOf, uprightsFor } from "./uprights.js";
|
|
15
15
|
import { standsVenueOf } from "./venue.js";
|
|
16
16
|
import { isNetSport } from "./court.js";
|
|
@@ -177,7 +177,7 @@ export function buildMultiTurn(options, settings = {}) {
|
|
|
177
177
|
let crowdStyle = "";
|
|
178
178
|
if (drawn.stands !== undefined && keep("stands")) {
|
|
179
179
|
const roof = typeof settings.venue === "object" && settings.venue.roof !== undefined ? settings.venue.roof : typeof options.stadium === "object" && options.stadium.roof === true;
|
|
180
|
-
const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { fill: true, ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id }, 0);
|
|
180
|
+
const venue = standsVenueOf(drawn.stands.stands, drawn.stands.crowd, { sport: drawn.stands.standsLike }, { fill: true, ...(typeof settings.venue === "object" ? settings.venue : {}), roof, id, noCentre: true }, 0);
|
|
181
181
|
items.push(...venue.items.filter((item) => item.kind !== "roof" || roof));
|
|
182
182
|
defs = venue.defs;
|
|
183
183
|
crowdStyle = "--ky-cr-home:#8b94a3;--ky-cr-away:#8b94a3";
|
|
@@ -207,6 +207,7 @@ export function buildMultiTurn(options, settings = {}) {
|
|
|
207
207
|
defs,
|
|
208
208
|
crowdStyle,
|
|
209
209
|
venue: drawn.stands !== undefined,
|
|
210
|
+
...(drawn.stands === undefined ? {} : lightingOf({ lighting: typeof settings.venue === "object" && settings.venue.lighting !== undefined ? settings.venue.lighting : drawn.stands.crowd.lighting }, undefined)),
|
|
210
211
|
look: { sport: spec.sport, look: look ?? "plain" },
|
|
211
212
|
planes,
|
|
212
213
|
box: box3,
|
package/dist/multi.types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Sport, SurfaceOptions } from "./types.ts";
|
|
1
|
+
import type { LedBoardOptions, LightingOptions, Sport, SurfaceOptions } from "./types.ts";
|
|
2
2
|
/** The unit a layout's lengths are written in: the one its rulebook prints. */
|
|
3
3
|
export type MultiUnit = "m" | "ft";
|
|
4
4
|
/** A length along the layout (`x`, left to right) and across it (`y`, top to bottom). */
|
|
@@ -247,6 +247,8 @@ export interface MultiOptions {
|
|
|
247
247
|
roof?: boolean;
|
|
248
248
|
shape?: "bowl" | "sides";
|
|
249
249
|
detail?: "fine" | "plain";
|
|
250
|
+
boards?: boolean | LedBoardOptions;
|
|
251
|
+
lighting?: LightingOptions;
|
|
250
252
|
};
|
|
251
253
|
/** The empty places for the event's words and marks, and the boards: on by default; `false` draws neither. */
|
|
252
254
|
slots?: boolean;
|
package/dist/strings.d.ts
CHANGED
|
@@ -1046,7 +1046,7 @@ export interface KyogijoWords {
|
|
|
1046
1046
|
item: string;
|
|
1047
1047
|
itemSag: string;
|
|
1048
1048
|
/** The names of what stands on the surface, by kind. */
|
|
1049
|
-
parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts", string>;
|
|
1049
|
+
parts: Record<"scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts" | "ribbon-boards" | "hung-board", string>;
|
|
1050
1050
|
};
|
|
1051
1051
|
/** The names of the zones that `zoneAt` answers with (rules.ts), by id. */
|
|
1052
1052
|
rules: Record<RuleId, string>;
|
package/dist/strings.js
CHANGED
|
@@ -455,7 +455,7 @@ export const KYOGIJO_STRINGS = {
|
|
|
455
455
|
announce: "{name} view", announceAngle: "Turned to {yaw} degrees, seen from {pitch} degrees above the floor.",
|
|
456
456
|
flat: "This browser cannot draw the 3D view, so the flat drawing is shown.",
|
|
457
457
|
standing: "Standing on it, in the 3D view: {list}.", item: "{name} {h}", itemSag: "{name} {h} at the posts, {low} at the middle",
|
|
458
|
-
parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness", surrounds: "surrounds", officials: "umpires' chairs and score tables", judges: "name cards on the judges' table", deck: "platform", "cage-fence": "fence", "cage-posts": "padded posts", "ring-ropes": "ropes", "ring-posts": "corner posts" },
|
|
458
|
+
parts: { scoreboard: "scoreboard (lower edge)", net: "net", antennae: "antennae", goalposts: "goal posts", netposts: "net posts", hoop: "baskets", goal: "goals", boards: "boards and glass", stumps: "stumps", pylons: "pylons", flags: "corner flags", fence: "fence", backstop: "backstop", "foul-poles": "foul poles", "table-legs": "table top", walls: "walls", "starting-blocks": "starting blocks", "backstroke-flags": "backstroke flags", "false-start-rope": "false start rope", springboards: "springboards", platforms: "platforms", "judges-stand": "judges' stand", hurdles: "hurdles", steeplechase: "steeplechase barriers", "high-jump": "high jump uprights and bar", "pole-vault": "pole vault uprights and bar", cage: "discus and hammer cage", padding: "padding", "rink-boards": "boards", mound: "clay mound", azuchi: "azuchi", targets: "targets", stands: "stands", "wall-seats": "seats on top of the wall", roof: "roof", gallery: "gallery", dais: "dais", banner: "banner", shrine: "shrine", "mat-edge": "mat thickness", surrounds: "surrounds", officials: "umpires' chairs and score tables", judges: "name cards on the judges' table", deck: "platform", "cage-fence": "fence", "cage-posts": "padded posts", "ring-ropes": "ropes", "ring-posts": "corner posts", "ribbon-boards": "LED ribbon boards", "hung-board": "centre-hung scoreboard" },
|
|
459
459
|
},
|
|
460
460
|
rules: {
|
|
461
461
|
"defensive-zone": "Defensive zone",
|
|
@@ -1014,7 +1014,7 @@ export const KYOGIJO_STRINGS = {
|
|
|
1014
1014
|
announce: "{name}の視点", announceAngle: "向きは{yaw}度、見下ろす角度は{pitch}度です。",
|
|
1015
1015
|
flat: "このブラウザーでは3D表示ができないため、平面の図を表示しています。",
|
|
1016
1016
|
standing: "3D表示で立って見えるもの:{list}。", item: "{name} {h}", itemSag: "{name} 支柱の位置で{h}(中央は{low})",
|
|
1017
|
-
parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み", surrounds: "競技エリアのフェンス", officials: "審判席と記録台", judges: "審判員席の名札", deck: "台", "cage-fence": "金網", "cage-posts": "パッド付き支柱", "ring-ropes": "ロープ", "ring-posts": "コーナーポスト" },
|
|
1017
|
+
parts: { scoreboard: "スコアボードの下端", net: "ネット", antennae: "アンテナ", goalposts: "ゴールポスト", netposts: "支柱", hoop: "バスケットゴール", goal: "ゴール", boards: "ボードとガラス", stumps: "ウィケットのスタンプ", pylons: "パイロン", flags: "コーナーフラッグ", fence: "外野フェンス", backstop: "バックネット", "foul-poles": "ファウルポール", "table-legs": "天板", walls: "壁", "starting-blocks": "スタート台", "backstroke-flags": "背泳ぎ用フラッグ", "false-start-rope": "フォルススタートロープ", springboards: "飛板", platforms: "固定台", "judges-stand": "ジャッジ台", hurdles: "ハードル", steeplechase: "障害物", "high-jump": "走高跳の支柱とバー", "pole-vault": "棒高跳の支柱とバー", cage: "円盤投・ハンマー投の囲い", padding: "防護マット", "rink-boards": "フェンス", mound: "土の台", azuchi: "安土", targets: "的", stands: "スタンド", "wall-seats": "壁の上の観客席", roof: "屋根", gallery: "見学席", dais: "上座の台", banner: "垂れ幕", shrine: "神棚", "mat-edge": "畳の厚み", surrounds: "競技エリアのフェンス", officials: "審判席と記録台", judges: "審判員席の名札", deck: "台", "cage-fence": "金網", "cage-posts": "パッド付き支柱", "ring-ropes": "ロープ", "ring-posts": "コーナーポスト", "ribbon-boards": "LEDリボンビジョン", "hung-board": "センタービジョン(吊り下げ式スコアボード)" },
|
|
1018
1018
|
},
|
|
1019
1019
|
rules: {
|
|
1020
1020
|
"defensive-zone": "ディフェンディングゾーン",
|
package/dist/turn-entry.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export { describeUprights, heightUnit, planesOf, uprightsFor } from "./uprights.
|
|
|
13
13
|
export type { UprightOptions, Uprights } from "./uprights.ts";
|
|
14
14
|
export { ROOF } from "./venue.ts";
|
|
15
15
|
export type { Venue, VenueBuild, VenueSettings } from "./venue.ts";
|
|
16
|
+
export type { LedBoardOptions, LedBoardPattern, LightingOptions, LightingPreset } from "./types.ts";
|
|
16
17
|
export type { Plane, PartKind, Upright, UprightKind, V3 } from "./turn.kit.ts";
|
package/dist/turn.dom.js
CHANGED
|
@@ -483,6 +483,11 @@ export function mountTurn(host, options, settings, build) {
|
|
|
483
483
|
stage.insertAdjacentHTML("beforeend", turnBoardMarkup(model.board));
|
|
484
484
|
stage.setAttribute("aria-label", model.title);
|
|
485
485
|
root.querySelector(".ky-turn-help").textContent = `${turnWords(model).keys}${model.standing === "" ? "" : ` ${model.standing}`}`;
|
|
486
|
+
// The lighting is an attribute of the root, which the stylesheet reads: it changes with the scene, the view's turn is not touched.
|
|
487
|
+
if (model.lighting === undefined)
|
|
488
|
+
root.removeAttribute("data-lighting");
|
|
489
|
+
else
|
|
490
|
+
root.setAttribute("data-lighting", model.lighting);
|
|
486
491
|
const theme = model.options.theme;
|
|
487
492
|
if (theme === "light" || theme === "dark")
|
|
488
493
|
root.dataset.theme = theme;
|
package/dist/turn.kit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** A point or a direction: x, y, z in feet. */
|
|
2
2
|
export type V3 = readonly [number, number, number];
|
|
3
3
|
/** What a plane is a part of: it is the class of its element (`ky-up-<kind>`) and what a test finds it by. */
|
|
4
|
-
export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign" | "apron" | "bench" | "floor";
|
|
4
|
+
export type PartKind = "post" | "net" | "tape" | "antenna" | "board" | "glass" | "rim" | "stanchion" | "frame" | "goal" | "flag" | "stump" | "leg" | "table" | "pylon" | "wall" | "screen" | "pole" | "ground" | "target" | "seats" | "concourse" | "fascia" | "roof" | "banner" | "platform" | "rail" | "matedge" | "shrine" | "surround" | "chair" | "desk" | "card" | "sign" | "apron" | "bench" | "floor" | "ribbon" | "hung-board";
|
|
5
5
|
/** One flat rectangle. Its own x runs along `u` (its whole length) and its own y along `v`; `o` is the corner both start from. `svg` is what is drawn on it, in feet along `u` and `v`. */
|
|
6
6
|
export interface Plane {
|
|
7
7
|
/** A name that says what it is and where: `net`, `post-left`, `goal-right-back`. */
|
|
@@ -42,7 +42,7 @@ export interface Plane {
|
|
|
42
42
|
group?: number;
|
|
43
43
|
}
|
|
44
44
|
/** What stands on the surface as a thing a reader would name: a net, a basket, a goal, a pair of posts. */
|
|
45
|
-
export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts";
|
|
45
|
+
export type UprightKind = "scoreboard" | "net" | "antennae" | "goalposts" | "netposts" | "hoop" | "goal" | "boards" | "stumps" | "pylons" | "flags" | "fence" | "backstop" | "foul-poles" | "table-legs" | "walls" | "starting-blocks" | "backstroke-flags" | "false-start-rope" | "springboards" | "platforms" | "judges-stand" | "hurdles" | "steeplechase" | "high-jump" | "pole-vault" | "cage" | "padding" | "rink-boards" | "mound" | "azuchi" | "targets" | "stands" | "wall-seats" | "roof" | "gallery" | "dais" | "banner" | "shrine" | "mat-edge" | "surrounds" | "officials" | "judges" | "deck" | "cage-fence" | "cage-posts" | "ring-ropes" | "ring-posts" | "ribbon-boards" | "hung-board";
|
|
46
46
|
/** A thing that stands on the surface: where, how high, and the planes it is made of. */
|
|
47
47
|
export interface Upright {
|
|
48
48
|
id: string;
|
package/dist/turn.markup.js
CHANGED
|
@@ -154,7 +154,7 @@ export function turnMarkupOf(model, settings = {}) {
|
|
|
154
154
|
const sheet = model.options.style === "none" ? "" : `<style>${KYOGIJO_TURN_STYLE}</style>`;
|
|
155
155
|
const theme = model.options.theme;
|
|
156
156
|
const sport = model.options.sport;
|
|
157
|
-
return (`<div class="ky-turn" id="${esc(model.id)}" data-sport="${esc(String(sport))}" data-mode="3d" data-touch="${settings.touch === "turn" ? "turn" : "scroll"}" data-preset="${presetOf(view, model.presets ?? sport) ?? "custom"}"${theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : ""} lang="${model.language}">` +
|
|
157
|
+
return (`<div class="ky-turn" id="${esc(model.id)}" data-sport="${esc(String(sport))}" data-mode="3d" data-touch="${settings.touch === "turn" ? "turn" : "scroll"}" data-preset="${presetOf(view, model.presets ?? sport) ?? "custom"}"${model.lighting === undefined ? "" : ` data-lighting="${model.lighting}"`}${theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : ""} lang="${model.language}">` +
|
|
158
158
|
sheet +
|
|
159
159
|
`<div class="ky-turn-view" tabindex="0" role="group" aria-roledescription="${esc(words.region)}" aria-label="${esc(model.title)}" aria-describedby="${esc(model.id)}-help" aria-keyshortcuts="ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1 2 3 4 Home Space"><div ${worldAttributes(model)}>${worldMarkup(model, layout)}</div>${model.board === undefined ? "" : turnBoardMarkup(model.board)}</div>` +
|
|
160
160
|
`<p class="ky-turn-help" id="${esc(model.id)}-help">${esc(keys)}</p>` +
|
package/dist/turn.model.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type UprightOptions, type Uprights } from "./uprights.ts";
|
|
|
5
5
|
import { type ResolvedBoard } from "./overlay.ts";
|
|
6
6
|
import type { VenueSettings } from "./venue.ts";
|
|
7
7
|
import type { Box3 } from "./turn.camera.ts";
|
|
8
|
-
import type { Language, ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
8
|
+
import type { Language, LightingOptions, LightingPreset, ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
9
9
|
/** What can be asked of a 3D view. */
|
|
10
10
|
export interface TurnSettings {
|
|
11
11
|
/** The viewpoint it opens on: `overhead`, `broadcast`, `end` or `corner` (the default). */
|
|
@@ -85,6 +85,8 @@ export interface TurnModel {
|
|
|
85
85
|
crowdStyle: string;
|
|
86
86
|
/** Whether the venue is raised. */
|
|
87
87
|
venue: boolean;
|
|
88
|
+
/** The lighting of the venue (`data-lighting` on the view's root, which the stylesheet reads), or `undefined`. */
|
|
89
|
+
lighting?: LightingPreset;
|
|
88
90
|
/** The sport and look the floor is painted in, when it has one (a budō surface's mats): the planes of its room are painted in the same look. */
|
|
89
91
|
look: {
|
|
90
92
|
sport: string;
|
|
@@ -111,6 +113,12 @@ export interface TurnModel {
|
|
|
111
113
|
/** The board the options ask for (`board`), laid over the view: a layer of its own, never a plane of the scene. */
|
|
112
114
|
board?: ResolvedBoard;
|
|
113
115
|
}
|
|
116
|
+
/** 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. */
|
|
117
|
+
export declare const lightingOf: (venue: {
|
|
118
|
+
lighting?: LightingOptions;
|
|
119
|
+
} | undefined, crowd: SurfaceOptions["crowd"]) => {
|
|
120
|
+
lighting?: LightingPreset;
|
|
121
|
+
};
|
|
114
122
|
/** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
|
|
115
123
|
export declare function floorOptions(options: SurfaceOptions | ScoreOptions): SurfaceOptions | ScoreOptions;
|
|
116
124
|
/** The view a set of settings opens on. */
|
package/dist/turn.model.js
CHANGED
|
@@ -9,8 +9,14 @@ import { TURN_DEFAULT, clampView, presetView } from "./turn.camera.js";
|
|
|
9
9
|
import { describeUprights, planesOf, uprightsFor } from "./uprights.js";
|
|
10
10
|
import { resolveCrowd } from "./crowd.js";
|
|
11
11
|
import { resolveBoard } from "./overlay.js";
|
|
12
|
+
import { resolveLighting } from "./venue.lighting.js";
|
|
12
13
|
/** The pixels in one foot of the scene at a zoom of 1: the floor is drawn this big, and scaled down to fit. A larger floor is sharper. */
|
|
13
14
|
export const WORLD_PX = 1400;
|
|
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
|
+
export const lightingOf = (venue, crowd) => {
|
|
17
|
+
const lighting = venue === undefined ? undefined : resolveLighting(venue.lighting !== undefined ? venue.lighting : resolveCrowd(crowd)?.lighting);
|
|
18
|
+
return lighting === undefined ? {} : { lighting };
|
|
19
|
+
};
|
|
14
20
|
/** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
|
|
15
21
|
export function floorOptions(options) {
|
|
16
22
|
const rest = { ...options, orientation: "landscape" };
|
|
@@ -23,7 +29,7 @@ export function floorOptions(options) {
|
|
|
23
29
|
}
|
|
24
30
|
const VIEWBOX = /viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"/;
|
|
25
31
|
/** The kinds of thing the venue makes. */
|
|
26
|
-
const VENUE_KINDS = ["stands", "wall-seats", "roof", "gallery", "dais", "banner", "shrine", "mat-edge", "walls"];
|
|
32
|
+
const VENUE_KINDS = ["stands", "wall-seats", "roof", "gallery", "dais", "banner", "shrine", "mat-edge", "walls", "ribbon-boards", "hung-board"];
|
|
27
33
|
/** The sport and look a floor drawing says it is painted in (`data-sport` and `data-look` on its root), for the planes round it to be painted in the same. */
|
|
28
34
|
function lookOf(floor) {
|
|
29
35
|
const found = /^<svg[^>]*\sdata-sport="([\w-]+)" data-look="([\w-]+)"/.exec(floor);
|
|
@@ -122,6 +128,7 @@ export function buildTurn(options, settings = {}) {
|
|
|
122
128
|
// The colours the crowd has in the flat drawing (which the floor no longer carries) are read from a small drawing of the same options.
|
|
123
129
|
crowdStyle: uprights.defs === undefined ? "" : crowdVars(drawKyogijo({ ...asked, size: "badge", style: "none" })),
|
|
124
130
|
venue: venue !== undefined,
|
|
131
|
+
...lightingOf(venue, asked.crowd),
|
|
125
132
|
look: lookOf(floor),
|
|
126
133
|
planes,
|
|
127
134
|
box,
|
package/dist/turn.style.js
CHANGED
|
@@ -86,6 +86,19 @@ export const KYOGIJO_TURN_STYLE = `
|
|
|
86
86
|
.ky-turn .u-penb-side{fill:color-mix(in srgb,var(--ky-up-glass,#bcd2e0) 80%,#5d7a8f);filter:brightness(.72)}
|
|
87
87
|
.ky-turn .u-teamb{fill:color-mix(in srgb,var(--ky-up-wood,#8a6a47) 80%,#2a2f36)}
|
|
88
88
|
.ky-turn .u-teamb-side{fill:color-mix(in srgb,var(--ky-up-wood,#8a6a47) 80%,#2a2f36);filter:brightness(.7)}
|
|
89
|
+
/* LED boards: the housing and the screen are dark; the lit parts (class u-led) carry their own colours, and are what a lighting layer makes glow. */
|
|
90
|
+
.ky-turn .u-led-bed{fill:#0b0f14}
|
|
91
|
+
.ky-turn .u-led-screen{fill:#06121d}
|
|
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}
|
|
94
|
+
.ky-turn .u-veil{fill:#04060a;fill-opacity:.62}
|
|
95
|
+
.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
|
+
.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}
|
|
89
102
|
.ky-turn .u-back{fill:color-mix(in srgb,var(--ky-up-fascia) 66%,var(--ky-cr-bed))}
|
|
90
103
|
.ky-turn .u-backrib{stroke:var(--ky-cr-bed);stroke-width:.7;stroke-opacity:.7}
|
|
91
104
|
.ky-turn .u-coping{fill:color-mix(in srgb,var(--ky-up-fascia) 70%,var(--ky-cr-conc))}
|
package/dist/types.d.ts
CHANGED
|
@@ -274,6 +274,32 @@ export type SheetShow = "sheet" | "half" | "house" | "folded";
|
|
|
274
274
|
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
|
+
/**
|
|
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.
|
|
280
|
+
*/
|
|
281
|
+
export type LightingPreset = "night";
|
|
282
|
+
export type LightingOptions = LightingPreset | boolean | {
|
|
283
|
+
preset?: LightingPreset;
|
|
284
|
+
};
|
|
285
|
+
/** 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
|
+
export type LedBoardPattern = "bands" | "blocks" | "chevrons" | "waves";
|
|
287
|
+
/**
|
|
288
|
+
* The LED boards of a venue (`crowd.boards`, or `venue.boards` of the 3D view): the ribbon boards along the fascia between the seating levels, and a scoreboard hung over the playing surface. The content is generic: bands of colour,
|
|
289
|
+
* abstract patterns and a plain score, never a logo, a sponsor or text from a real board. `true` is both, in the package's own colours. Off by default.
|
|
290
|
+
*/
|
|
291
|
+
export interface LedBoardOptions {
|
|
292
|
+
/** The ribbon boards along the fascia between the levels (the front of the single level of a hall that has one). On by default. */
|
|
293
|
+
ribbon?: boolean;
|
|
294
|
+
/** The scoreboard hung over the middle of the playing surface, four sided, with a ring of display under it. On by default, and left out of a ballpark (whose scoreboard stands behind the outfield). */
|
|
295
|
+
scoreboard?: boolean;
|
|
296
|
+
/** One to three colours of the lights, as CSS colours (`#e03c31`, `rgb(...)`, a name). The crowd's `home` and `away` colours when left out and the crowd has them, else amber, blue and white. */
|
|
297
|
+
colors?: readonly string[];
|
|
298
|
+
/** What the ribbon boards show. `bands` by default. */
|
|
299
|
+
pattern?: LedBoardPattern;
|
|
300
|
+
/** The plain score on the scoreboard: home, then away, whole numbers 0 to 99. 0 and 0 by default. */
|
|
301
|
+
score?: readonly [number, number];
|
|
302
|
+
}
|
|
277
303
|
/**
|
|
278
304
|
* The stands and the crowd round a surface (`crowd`). Every part is optional. `true` is the sport's usual venue, 85 percent full, with an away end, drawn in detail.
|
|
279
305
|
* See `docs/crowd.md`.
|
|
@@ -324,6 +350,13 @@ export interface CrowdOptions {
|
|
|
324
350
|
wallSeats?: boolean;
|
|
325
351
|
/** How much is drawn: `fine` (the crowd as heads and shoulders, empty seats, aisles), `plain` (blocks of colour), or `auto` (`plain` for `detail: "simple"` or a `width` under 480 pixels). */
|
|
326
352
|
detail?: "auto" | "fine" | "plain";
|
|
353
|
+
/**
|
|
354
|
+
* LED boards: ribbon boards along the front of the upper tiers (drawn as a lit line in the flat plan, raised on the fascia of the suite level in the 3D view, where `venue.boards` says the same and wins) and a scoreboard over the
|
|
355
|
+
* middle of the surface. `true` for both in the package's colours, or `{ ribbon, scoreboard, colors, pattern, score }`. Generic content only. Off by default. See `docs/boards.md`.
|
|
356
|
+
*/
|
|
357
|
+
boards?: boolean | LedBoardOptions;
|
|
358
|
+
/** The lighting of the venue: `"night"` dims the house and lights the surface, the aisles, the boards and the exits. Off by default. See `docs/venues.md`. */
|
|
359
|
+
lighting?: LightingOptions;
|
|
327
360
|
}
|
|
328
361
|
/**
|
|
329
362
|
* The parts of a surface a drawing can be cropped to (`range`). Football: `red-zone`, `between-the-20s`, `end-zone`, `field-of-play`, `own-half`, `opponent-half`.
|
package/dist/uprights.js
CHANGED
|
@@ -141,6 +141,8 @@ export function uprightsFor(o, settings = {}) {
|
|
|
141
141
|
// The score, hung up: the badge on a board that turns to face the eye.
|
|
142
142
|
const scored = o.home !== undefined && o.away !== undefined && o.size !== "badge";
|
|
143
143
|
if (scored && settings.score !== "floor" && area !== undefined) {
|
|
144
|
+
// The score has a board of its own: the venue's hung scoreboard does not stand beside it.
|
|
145
|
+
items = items.filter((item) => item.kind !== "hung-board");
|
|
144
146
|
const board = scoreboard(drawKyogijo({ ...o, size: "badge", style: "none", id: `${drawingId(o)}-board` }), sport, area, thin);
|
|
145
147
|
if (board !== undefined)
|
|
146
148
|
items.push(board);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type CrowdStands, type Pt, type StandRing } from "./crowd.ts";
|
|
2
|
+
import { type Plane } from "./turn.kit.ts";
|
|
3
|
+
import type { LedBoardOptions, LedBoardPattern } from "./types.ts";
|
|
4
|
+
/** The boards a venue has, with every part's value. */
|
|
5
|
+
export interface Boards {
|
|
6
|
+
ribbon: boolean;
|
|
7
|
+
scoreboard: boolean;
|
|
8
|
+
/** Three colours of light (a ribbon's bands and chevrons take them in turn). */
|
|
9
|
+
colors: [string, string, string];
|
|
10
|
+
pattern: LedBoardPattern;
|
|
11
|
+
score: [number, number];
|
|
12
|
+
/** Each lit panel has a halo under it (the venue is lit as at night). */
|
|
13
|
+
glow: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** The package's own colours of light: amber, blue and a white with a little blue in it. */
|
|
16
|
+
export declare const BOARD_COLORS: readonly [string, string, string];
|
|
17
|
+
export declare const BOARD_PATTERNS: readonly LedBoardPattern[];
|
|
18
|
+
/** The sizes of the boards, in feet (ours: the usual ones, no plan of a real board). */
|
|
19
|
+
export declare const BOARD: {
|
|
20
|
+
/** How tall a ribbon is, and how long one of its panels. */
|
|
21
|
+
readonly ribbon: 3.8;
|
|
22
|
+
readonly panel: 9;
|
|
23
|
+
/** The scoreboard's share of the surface's length and width, its least and most, and how far its lower edge hangs over the surface (a share of the surface's longer side, least and most). */
|
|
24
|
+
readonly share: {
|
|
25
|
+
readonly w: 0.14;
|
|
26
|
+
readonly d: 0.22;
|
|
27
|
+
};
|
|
28
|
+
readonly wide: readonly [18, 52];
|
|
29
|
+
readonly deep: readonly [12, 30];
|
|
30
|
+
readonly hang: {
|
|
31
|
+
readonly share: 0.2;
|
|
32
|
+
readonly least: 28;
|
|
33
|
+
readonly most: 70;
|
|
34
|
+
};
|
|
35
|
+
/** The ring of display under the main board, its inset on each side, and the strut it hangs from. */
|
|
36
|
+
readonly ring: {
|
|
37
|
+
readonly rise: readonly [2.5, 4.5];
|
|
38
|
+
readonly inset: 0.06;
|
|
39
|
+
};
|
|
40
|
+
readonly strut: 9;
|
|
41
|
+
};
|
|
42
|
+
export declare const safeColor: (value: unknown) => string | undefined;
|
|
43
|
+
/** The boards a `boards` option asks for (the crowd's `home` and `away` are the colours when it names none), or `undefined` for none. */
|
|
44
|
+
export declare function resolveBoards(ask: boolean | LedBoardOptions | undefined | null, team?: {
|
|
45
|
+
home?: string;
|
|
46
|
+
away?: string;
|
|
47
|
+
}, glow?: boolean): Boards | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* A ribbon on a quad `[a, b, c, d]` of a plane (a and b along its lower edge, d and c along its upper): a dark housing with panels of light in it, in the pattern asked. `run` is how far along the ribbon this quad starts, in feet,
|
|
50
|
+
* so that the panels carry on in the colour they were from one quad to the next. A quad that is not a quad is the housing alone.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ledStrip(corners: readonly Pt[], look: Pick<Boards, "colors" | "pattern"> & {
|
|
53
|
+
glow?: boolean;
|
|
54
|
+
}, run?: number): string;
|
|
55
|
+
/** The ribbon on a wall, `length` feet long and `height` feet high: the same strip, on a rectangle (its lower edge is the wall's lower edge). */
|
|
56
|
+
export declare const ledWall: (length: number, height: number, look: Pick<Boards, "colors" | "pattern">, run?: number) => string;
|
|
57
|
+
/** A walk between two tiers: the tier below it, the one above and the walk's own outlines. */
|
|
58
|
+
export interface TierLink {
|
|
59
|
+
c: CrowdStands["concourses"][number];
|
|
60
|
+
before: StandRing;
|
|
61
|
+
after: StandRing;
|
|
62
|
+
}
|
|
63
|
+
/** The walks between the tiers of a stands model, each with the tier it leaves and the tier it leads to. */
|
|
64
|
+
export declare function tierLinks(stands: CrowdStands): TierLink[];
|
|
65
|
+
/** The share of a plane of `rise` feet over a slope `g` (rise per foot across) that a ribbon of `BOARD.ribbon` feet takes at its top; 0 for a plane too flat to be a facade. */
|
|
66
|
+
export declare function ribbonShare(rise: number, g: number): number;
|
|
67
|
+
/** The size of the scoreboard over a surface of `w` by `h` feet (its long side along the surface's), and how high its lower edge hangs. */
|
|
68
|
+
export declare function boardSize(w: number, h: number): {
|
|
69
|
+
w: number;
|
|
70
|
+
d: number;
|
|
71
|
+
hang: number;
|
|
72
|
+
};
|
|
73
|
+
/** Whether a sport has a scoreboard hung over its surface: a ballpark's stands behind the outfield, so it has none here. */
|
|
74
|
+
export declare const hangsOver: (sport: string | undefined) => boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The scoreboard hung over the middle of a surface: a four sided board with its score on every face, a ring of display under it, a cap on top and a strut up to the rafters. `box` is the surface, `lift` how high the surface stands.
|
|
77
|
+
* Its faces look outwards, each the right way round for an eye outside it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function centreBoard(id: string, box: {
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
w: number;
|
|
83
|
+
h: number;
|
|
84
|
+
}, lift: number, look: Boards): Plane[];
|
|
85
|
+
/** The size of the scoreboard in the flat plan, from the surface's box. */
|
|
86
|
+
export declare const planBoard: (box: {
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
w: number;
|
|
90
|
+
h: number;
|
|
91
|
+
}) => {
|
|
92
|
+
x: number;
|
|
93
|
+
y: number;
|
|
94
|
+
w: number;
|
|
95
|
+
d: number;
|
|
96
|
+
};
|
|
97
|
+
/** The ribbon boards in the flat plan: a lit line along the front of every tier that has a walk below it (the front of the tiers there is no walk to, where the venue has one level), panels of the colours in turn. */
|
|
98
|
+
export declare function planRibbons(stands: CrowdStands, look: Boards): string;
|
|
99
|
+
/** The scoreboard in the flat plan, as the board hangs over the surface seen from above: its dark cap with a line of light round it and its four edges' colours. */
|
|
100
|
+
export declare function planScoreboard(box: {
|
|
101
|
+
x: number;
|
|
102
|
+
y: number;
|
|
103
|
+
w: number;
|
|
104
|
+
h: number;
|
|
105
|
+
}, look: Boards): string;
|
|
106
|
+
/** How tall the ribbon on the front of a tier is, for a front `top` feet high: the usual ribbon, or less where the front is low. */
|
|
107
|
+
export declare const ribbonHigh: (top: number) => number;
|
|
108
|
+
/** The ribbon on the top of a wall that stands over the line from `a` to `b` and is `top` feet high (the front of a tier): a wall of its own, as tall as `ribbonHigh` says. `run` is how far along the ribbon the wall starts. */
|
|
109
|
+
export declare const fasciaRibbon: (id: string, a: Pt, b: Pt, top: number, look: Boards, run: number) => Plane;
|
|
110
|
+
/** A hall's boards as its stadium asks for them: the layout has scoreboards of its own, so the hall has the ribbons and no scoreboard hung over its middle. */
|
|
111
|
+
export declare const hallBoards: (ask: boolean | LedBoardOptions | undefined) => boolean | LedBoardOptions | undefined;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// The LED boards of a venue: the ribbon boards along the fascia between the seating levels, and the scoreboard hung over the middle of the playing surface, in the flat plan and in 3D. The content is generic and ours: panels
|
|
2
|
+
// of colour in bands, blocks, chevrons or waves, and a plain score in seven-segment digits (paths, so that no font is needed): never a logo, a sponsor or a line of text from a real board. The colours are the asker's, else
|
|
3
|
+
// the crowd's home and away colours, else the package's own amber, blue and white. Every lit shape carries the class `u-led` (`ky-cr-led` in the plan), so that a lighting layer can make it glow. Pure: data in, planes and markup out.
|
|
4
|
+
import { outlinePath } from "./crowd.js";
|
|
5
|
+
import { el } from "./format.js";
|
|
6
|
+
import { bar, flat, n, plane, wall } from "./turn.kit.js";
|
|
7
|
+
/** The package's own colours of light: amber, blue and a white with a little blue in it. */
|
|
8
|
+
export const BOARD_COLORS = ["#ffb020", "#2bc4ff", "#f4f7ff"];
|
|
9
|
+
export const BOARD_PATTERNS = ["bands", "blocks", "chevrons", "waves"];
|
|
10
|
+
/** The sizes of the boards, in feet (ours: the usual ones, no plan of a real board). */
|
|
11
|
+
export const BOARD = {
|
|
12
|
+
/** How tall a ribbon is, and how long one of its panels. */
|
|
13
|
+
ribbon: 3.8,
|
|
14
|
+
panel: 9,
|
|
15
|
+
/** The scoreboard's share of the surface's length and width, its least and most, and how far its lower edge hangs over the surface (a share of the surface's longer side, least and most). */
|
|
16
|
+
share: { w: 0.14, d: 0.22 },
|
|
17
|
+
wide: [18, 52],
|
|
18
|
+
deep: [12, 30],
|
|
19
|
+
hang: { share: 0.2, least: 28, most: 70 },
|
|
20
|
+
/** The ring of display under the main board, its inset on each side, and the strut it hangs from. */
|
|
21
|
+
ring: { rise: [2.5, 4.5], inset: 0.06 },
|
|
22
|
+
strut: 9,
|
|
23
|
+
};
|
|
24
|
+
/** A CSS colour that is safe to write into an attribute: a hex, `rgb()`, `hsl()` or a plain name. */
|
|
25
|
+
const COLOR = /^(#[0-9a-f]{3,8}|(?:rgb|hsl)a?\([\d\s.,%/-]{3,40}\)|[a-z]{3,24})$/i;
|
|
26
|
+
export const safeColor = (value) => (typeof value === "string" && COLOR.test(value.trim()) ? value.trim() : undefined);
|
|
27
|
+
/** The boards a `boards` option asks for (the crowd's `home` and `away` are the colours when it names none), or `undefined` for none. */
|
|
28
|
+
export function resolveBoards(ask, team = {}, glow = false) {
|
|
29
|
+
if (ask === undefined || ask === null || ask === false)
|
|
30
|
+
return undefined;
|
|
31
|
+
const o = ask === true ? {} : ask;
|
|
32
|
+
const given = (Array.isArray(o.colors) ? o.colors : [team.home, team.away]).map(safeColor).filter((c) => c !== undefined).slice(0, 3);
|
|
33
|
+
const colors = [0, 1, 2].map((i) => given[i] ?? (given.length > 0 ? (i === 2 ? given[0] : BOARD_COLORS[2]) : BOARD_COLORS[i]));
|
|
34
|
+
const whole = (v) => (typeof v === "number" && Number.isFinite(v) ? Math.min(99, Math.max(0, Math.trunc(v))) : 0);
|
|
35
|
+
const boards = {
|
|
36
|
+
ribbon: o.ribbon !== false,
|
|
37
|
+
scoreboard: o.scoreboard !== false,
|
|
38
|
+
colors,
|
|
39
|
+
pattern: BOARD_PATTERNS.includes(o.pattern) ? o.pattern : "bands",
|
|
40
|
+
score: [whole(o.score?.[0]), whole(o.score?.[1])],
|
|
41
|
+
glow,
|
|
42
|
+
};
|
|
43
|
+
return boards.ribbon || boards.scoreboard ? boards : undefined;
|
|
44
|
+
}
|
|
45
|
+
const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
46
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
47
|
+
const at = (p) => `${n(p[0])} ${n(p[1])}`;
|
|
48
|
+
const loop = (pts) => `M${pts.map(at).join("L")}Z`;
|
|
49
|
+
/**
|
|
50
|
+
* A ribbon on a quad `[a, b, c, d]` of a plane (a and b along its lower edge, d and c along its upper): a dark housing with panels of light in it, in the pattern asked. `run` is how far along the ribbon this quad starts, in feet,
|
|
51
|
+
* so that the panels carry on in the colour they were from one quad to the next. A quad that is not a quad is the housing alone.
|
|
52
|
+
*/
|
|
53
|
+
export function ledStrip(corners, look, run = 0) {
|
|
54
|
+
const bed = `<path class="u-led-bed" d="${loop(corners)}"/>`;
|
|
55
|
+
if (corners.length !== 4)
|
|
56
|
+
return bed;
|
|
57
|
+
const [a, b, c, d] = corners;
|
|
58
|
+
const across = (s, t) => lerp(lerp(a, b, s), lerp(d, c, s), t);
|
|
59
|
+
const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
60
|
+
const panels = Math.max(1, Math.round(long / BOARD.panel));
|
|
61
|
+
const first = Math.floor(run / BOARD.panel);
|
|
62
|
+
const lit = new Map();
|
|
63
|
+
const paint = (color, shape) => {
|
|
64
|
+
const list = lit.get(color) ?? [];
|
|
65
|
+
list.push(shape);
|
|
66
|
+
lit.set(color, list);
|
|
67
|
+
};
|
|
68
|
+
const [lo, hi] = [0.14, 0.86];
|
|
69
|
+
const gap = Math.min(0.06, 0.4 / panels);
|
|
70
|
+
if (look.pattern === "waves") {
|
|
71
|
+
// Three waves along the whole quad, one in each colour, a little apart.
|
|
72
|
+
const steps = Math.max(8, Math.round(long / 1.2));
|
|
73
|
+
look.colors.forEach((color, w) => {
|
|
74
|
+
const pts = Array.from({ length: steps + 1 }, (_, k) => {
|
|
75
|
+
const s = k / steps;
|
|
76
|
+
return across(s, 0.5 + 0.3 * Math.sin(((run + s * long) / BOARD.panel) * Math.PI * 1.2 + (w * 2 * Math.PI) / 3));
|
|
77
|
+
});
|
|
78
|
+
paint(color, `M${pts.map(at).join("L")}`);
|
|
79
|
+
});
|
|
80
|
+
const waves = [...lit].map(([color, shapes]) => (look.glow === true ? `<path class="u-led-glow" fill="none" stroke="${color}" stroke-opacity=".3" stroke-width="${n(BOARD.ribbon * 0.7)}" stroke-linecap="round" d="${shapes.join("")}"/>` : "") + `<path class="u-led" fill="none" stroke="${color}" stroke-width="${n(BOARD.ribbon * 0.2)}" stroke-linecap="round" stroke-linejoin="round" d="${shapes.join("")}"/>`);
|
|
81
|
+
return bed + waves.join("");
|
|
82
|
+
}
|
|
83
|
+
for (let i = 0; i < panels; i += 1) {
|
|
84
|
+
const k = first + i;
|
|
85
|
+
const [s0, s1] = [i / panels + gap / 2, (i + 1) / panels - gap / 2];
|
|
86
|
+
const mine = look.colors[k % 3];
|
|
87
|
+
const other = look.colors[(k + 1) % 3];
|
|
88
|
+
if (look.pattern === "blocks") {
|
|
89
|
+
paint(mine, loop([across(s0, lo), across(s1, lo), across(s1, hi), across(s0, hi)]));
|
|
90
|
+
}
|
|
91
|
+
else if (look.pattern === "chevrons") {
|
|
92
|
+
// An arrow along the ribbon: a thick chevron pointing the way the run goes, in this panel's colour.
|
|
93
|
+
const w = s1 - s0;
|
|
94
|
+
const cell = (x, t) => across(s0 + x * w, lo + t * (hi - lo));
|
|
95
|
+
paint(mine, loop([cell(0, 0), cell(0.55, 0), cell(1, 0.5), cell(0.55, 1), cell(0, 1), cell(0.45, 0.5)]));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const mid = (lo + hi) / 2;
|
|
99
|
+
paint(mine, loop([across(s0, lo), across(s1, lo), across(s1, mid - 0.03), across(s0, mid - 0.03)]));
|
|
100
|
+
paint(other, loop([across(s0, mid + 0.03), across(s1, mid + 0.03), across(s1, hi), across(s0, hi)]));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// A halo under each colour's panels: the same shapes, stroked wide and faint, so that the light seems to spill over the housing and the wall beside it.
|
|
104
|
+
const halo = (color, shapes) => (look.glow === true ? `<path class="u-led-glow" fill="${color}" fill-opacity=".3" stroke="${color}" stroke-opacity=".3" stroke-width="${n(BOARD.ribbon * 0.6)}" stroke-linejoin="round" d="${shapes.join("")}"/>` : "");
|
|
105
|
+
return bed + [...lit].map(([color, shapes]) => `${halo(color, shapes)}<path class="u-led" fill="${color}" d="${shapes.join("")}"/>`).join("");
|
|
106
|
+
}
|
|
107
|
+
/** The ribbon on a wall, `length` feet long and `height` feet high: the same strip, on a rectangle (its lower edge is the wall's lower edge). */
|
|
108
|
+
export const ledWall = (length, height, look, run = 0) => ledStrip([[0, height], [length, height], [length, 0], [0, 0]], look, run);
|
|
109
|
+
/** The walks between the tiers of a stands model, each with the tier it leaves and the tier it leads to. */
|
|
110
|
+
export function tierLinks(stands) {
|
|
111
|
+
const links = [];
|
|
112
|
+
stands.concourses.forEach((c, ci) => {
|
|
113
|
+
let at = stands.rings.findIndex((r) => r.hole === c.region);
|
|
114
|
+
if (at < 1)
|
|
115
|
+
at = Math.min(ci + 1, stands.rings.length - 1);
|
|
116
|
+
const [before, after] = [stands.rings[at - 1], stands.rings[at]];
|
|
117
|
+
if (before !== undefined && after !== undefined)
|
|
118
|
+
links.push({ c, before, after });
|
|
119
|
+
});
|
|
120
|
+
return links;
|
|
121
|
+
}
|
|
122
|
+
/** The share of a plane of `rise` feet over a slope `g` (rise per foot across) that a ribbon of `BOARD.ribbon` feet takes at its top; 0 for a plane too flat to be a facade. */
|
|
123
|
+
export function ribbonShare(rise, g) {
|
|
124
|
+
if (g < 0.25 || rise < BOARD.ribbon)
|
|
125
|
+
return 0;
|
|
126
|
+
const slant = (rise / g) * Math.hypot(1, g);
|
|
127
|
+
return clamp(BOARD.ribbon / slant, 0.06, 0.4);
|
|
128
|
+
}
|
|
129
|
+
// ---- The scoreboard. ----
|
|
130
|
+
/** The size of the scoreboard over a surface of `w` by `h` feet (its long side along the surface's), and how high its lower edge hangs. */
|
|
131
|
+
export function boardSize(w, h) {
|
|
132
|
+
const pick = (share, [lo, hi], side) => clamp(side * share, Math.min(lo, side * 0.6), hi);
|
|
133
|
+
return { w: pick(BOARD.share.w, BOARD.wide, w), d: pick(BOARD.share.d, BOARD.deep, h), hang: clamp(Math.max(w, h) * BOARD.hang.share, BOARD.hang.least, BOARD.hang.most) };
|
|
134
|
+
}
|
|
135
|
+
/** Whether a sport has a scoreboard hung over its surface: a ballpark's stands behind the outfield, so it has none here. */
|
|
136
|
+
export const hangsOver = (sport) => sport !== "mlb";
|
|
137
|
+
/** The segments of a seven-segment digit, a to g, that are lit for each digit. */
|
|
138
|
+
const SEGMENTS = ["abcdef", "bc", "abged", "abgcd", "fgbc", "afgcd", "afgedc", "abc", "abcdefg", "abcdfg"];
|
|
139
|
+
/** A number of one or two digits in seven segments, `high` feet tall, from (x, y) at its top left: the path and the width it takes. */
|
|
140
|
+
function digits(value, x, y, high) {
|
|
141
|
+
const text = String(Math.min(99, Math.max(0, Math.trunc(value))));
|
|
142
|
+
const w = high * 0.56;
|
|
143
|
+
const t = high * 0.13;
|
|
144
|
+
const half = high / 2;
|
|
145
|
+
const rects = {
|
|
146
|
+
a: [t * 0.6, 0, w - t * 1.2, t],
|
|
147
|
+
b: [w - t, t * 0.6, t, half - t * 0.9],
|
|
148
|
+
c: [w - t, half + t * 0.3, t, half - t * 0.9],
|
|
149
|
+
d: [t * 0.6, high - t, w - t * 1.2, t],
|
|
150
|
+
e: [0, half + t * 0.3, t, half - t * 0.9],
|
|
151
|
+
f: [0, t * 0.6, t, half - t * 0.9],
|
|
152
|
+
g: [t * 0.6, half - t / 2, w - t * 1.2, t],
|
|
153
|
+
};
|
|
154
|
+
let d = "";
|
|
155
|
+
[...text].forEach((ch, i) => {
|
|
156
|
+
for (const s of SEGMENTS[Number(ch)]) {
|
|
157
|
+
const [rx, ry, rw, rh] = rects[s];
|
|
158
|
+
d += `M${n(x + i * (w + t) + rx)} ${n(y + ry)}h${n(rw)}v${n(rh)}h${n(-rw)}z`;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return { d, width: text.length * w + (text.length - 1) * t };
|
|
162
|
+
}
|
|
163
|
+
/** The face of the main board, `w` by `h` feet: a dark screen with a band of light along its top and bottom, a block of colour at each end, and the score between them. */
|
|
164
|
+
function boardFace(w, h, look) {
|
|
165
|
+
const [c0, c1, c2] = look.colors;
|
|
166
|
+
const edge = Math.min(0.5, h * 0.06);
|
|
167
|
+
const band = h * 0.1;
|
|
168
|
+
const halo = look.glow ? `<rect class="u-led-glow" x="${n(-0.8)}" y="${n(-0.8)}" width="${n(w + 1.6)}" height="${n(h + 1.6)}" rx="1" fill="${c2}" fill-opacity=".2"/>` : "";
|
|
169
|
+
let svg = `${halo}<rect class="u-led-bed" width="${n(w)}" height="${n(h)}"/><rect class="u-led-screen" x="${n(edge)}" y="${n(edge)}" width="${n(w - 2 * edge)}" height="${n(h - 2 * edge)}"/>`;
|
|
170
|
+
svg += `<path class="u-led" fill="${c2}" d="M${n(edge)} ${n(edge)}h${n(w - 2 * edge)}v${n(band)}h${n(-(w - 2 * edge))}zM${n(edge)} ${n(h - edge - band)}h${n(w - 2 * edge)}v${n(band)}h${n(-(w - 2 * edge))}z"/>`;
|
|
171
|
+
const block = Math.min(w * 0.2, h * 0.5);
|
|
172
|
+
const [yb, hb] = [h * 0.28, h * 0.44];
|
|
173
|
+
svg += `<path class="u-led" fill="${c0}" d="M${n(edge * 2)} ${n(yb)}h${n(block)}v${n(hb)}h${n(-block)}z"/><path class="u-led" fill="${c1}" d="M${n(w - edge * 2 - block)} ${n(yb)}h${n(block)}v${n(hb)}h${n(-block)}z"/>`;
|
|
174
|
+
// The score: two numbers and a dash, as large as the face between the blocks lets them be.
|
|
175
|
+
const room = w - 2 * (block + edge * 4);
|
|
176
|
+
const digitCount = String(look.score[0]).length + String(look.score[1]).length;
|
|
177
|
+
const dash = 0.4;
|
|
178
|
+
const high = Math.min(h * 0.5, room / (digitCount * 0.56 + (digitCount - 2) * 0.13 + dash + 0.5));
|
|
179
|
+
const [home, away] = [digits(look.score[0], 0, 0, high), digits(look.score[1], 0, 0, high)];
|
|
180
|
+
const gap = high * 0.3;
|
|
181
|
+
const total = home.width + gap + high * dash + gap + away.width;
|
|
182
|
+
const x0 = (w - total) / 2;
|
|
183
|
+
const y0 = (h - high) / 2;
|
|
184
|
+
const [h0, a0] = [digits(look.score[0], x0, y0, high), digits(look.score[1], x0 + home.width + gap * 2 + high * dash, y0, high)];
|
|
185
|
+
svg += `<path class="u-led" fill="${c2}" d="${h0.d}${a0.d}M${n(x0 + home.width + gap)} ${n(y0 + high / 2 - high * 0.065)}h${n(high * dash)}v${n(high * 0.13)}h${n(-high * dash)}z"/>`;
|
|
186
|
+
return svg;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The scoreboard hung over the middle of a surface: a four sided board with its score on every face, a ring of display under it, a cap on top and a strut up to the rafters. `box` is the surface, `lift` how high the surface stands.
|
|
190
|
+
* Its faces look outwards, each the right way round for an eye outside it.
|
|
191
|
+
*/
|
|
192
|
+
export function centreBoard(id, box, lift, look) {
|
|
193
|
+
const size = boardSize(box.w, box.h);
|
|
194
|
+
const [cx, cy] = [box.x + box.w / 2, box.y + box.h / 2];
|
|
195
|
+
const [x0, x1, y0, y1] = [cx - size.w / 2, cx + size.w / 2, cy - size.d / 2, cy + size.d / 2];
|
|
196
|
+
const ring = clamp(size.w * 0.16, BOARD.ring.rise[0], BOARD.ring.rise[1]);
|
|
197
|
+
const body = clamp(size.w * 0.45, 8, 16);
|
|
198
|
+
const [z0, z1, z2] = [lift + size.hang, lift + size.hang + ring, lift + size.hang + ring + body];
|
|
199
|
+
const ins = size.w * BOARD.ring.inset;
|
|
200
|
+
const insD = size.d * BOARD.ring.inset;
|
|
201
|
+
// The faces, each from the left of an eye outside it to its right: +y, -y, +x, -x.
|
|
202
|
+
const faces = [
|
|
203
|
+
{ name: "south", from: [x0, y1], to: [x1, y1] },
|
|
204
|
+
{ name: "north", from: [x1, y0], to: [x0, y0] },
|
|
205
|
+
{ name: "east", from: [x1, y1], to: [x1, y0] },
|
|
206
|
+
{ name: "west", from: [x0, y0], to: [x0, y1] },
|
|
207
|
+
];
|
|
208
|
+
const planes = [];
|
|
209
|
+
let run = 0;
|
|
210
|
+
for (const f of faces) {
|
|
211
|
+
const long = Math.hypot(f.to[0] - f.from[0], f.to[1] - f.from[1]);
|
|
212
|
+
planes.push(plane(`${id}-${f.name}`, "hung-board", [f.from[0], f.from[1], z2], [f.to[0] - f.from[0], f.to[1] - f.from[1], 0], [0, 0, z1 - z2], boardFace(long, body, look)));
|
|
213
|
+
// The ring of display under it, set in a little on every side, in the pattern the ribbons have.
|
|
214
|
+
const grow = (p, q) => {
|
|
215
|
+
const [dx, dy] = [(q[0] - p[0]) / long, (q[1] - p[1]) / long];
|
|
216
|
+
const cut = f.name === "south" || f.name === "north" ? ins : insD;
|
|
217
|
+
return [[p[0] + dx * cut, p[1] + dy * cut], [q[0] - dx * cut, q[1] - dy * cut]];
|
|
218
|
+
};
|
|
219
|
+
const [r0, r1] = grow(f.from, f.to);
|
|
220
|
+
const span = Math.hypot(r1[0] - r0[0], r1[1] - r0[1]);
|
|
221
|
+
planes.push(plane(`${id}-${f.name}-ring`, "hung-board", [r0[0], r0[1], z1], [r1[0] - r0[0], r1[1] - r0[1], 0], [0, 0, z0 - z1], ledWall(span, ring, look, run)));
|
|
222
|
+
run += long;
|
|
223
|
+
}
|
|
224
|
+
// The cap, a dark top with a thin line of light round its edge, and the strut the board hangs from.
|
|
225
|
+
const [w, d] = [x1 - x0, y1 - y0];
|
|
226
|
+
const rim = Math.min(0.5, d * 0.05);
|
|
227
|
+
const capSvg = `<rect class="u-led-bed" width="${n(w)}" height="${n(d)}"/><path class="u-led" fill="none" stroke="${look.colors[0]}" stroke-width="${n(rim)}" d="M${n(rim * 2)} ${n(rim * 2)}h${n(w - rim * 4)}v${n(d - rim * 4)}h${n(-(w - rim * 4))}z"/>`;
|
|
228
|
+
planes.push(flat(`${id}-cap`, "hung-board", x0, y0, x1, y1, z2, capSvg));
|
|
229
|
+
planes.push(...bar(`${id}-strut`, "frame", [cx, cy, z2], [cx, cy, z2 + BOARD.strut], 0.9, "u-led-bed"));
|
|
230
|
+
return planes;
|
|
231
|
+
}
|
|
232
|
+
// ---- The flat plan. ----
|
|
233
|
+
/** The size of the scoreboard in the flat plan, from the surface's box. */
|
|
234
|
+
export const planBoard = (box) => {
|
|
235
|
+
const s = boardSize(box.w, box.h);
|
|
236
|
+
return { x: box.x + (box.w - s.w) / 2, y: box.y + (box.h - s.d) / 2, w: s.w, d: s.d };
|
|
237
|
+
};
|
|
238
|
+
/** The ribbon boards in the flat plan: a lit line along the front of every tier that has a walk below it (the front of the tiers there is no walk to, where the venue has one level), panels of the colours in turn. */
|
|
239
|
+
export function planRibbons(stands, look) {
|
|
240
|
+
const links = tierLinks(stands);
|
|
241
|
+
const fronts = links.length > 0 ? links.map((l) => l.after) : stands.rings.filter((r) => r.tier !== "wall" && r.tier !== "bleachers").slice(0, 1);
|
|
242
|
+
const paths = fronts.flatMap((ring) => (ring.hole === null ? [] : [outlinePath(ring.hole)])).join("");
|
|
243
|
+
if (paths === "")
|
|
244
|
+
return "";
|
|
245
|
+
const cell = BOARD.panel;
|
|
246
|
+
const lit = look.colors.map((color, k) => el("path", { class: "ky-cr-led", d: paths, fill: "none", stroke: color, "stroke-width": 2.2, "stroke-dasharray": `${cell} ${cell * 2}`, "stroke-dashoffset": -cell * k }));
|
|
247
|
+
const glow = look.glow ? look.colors.map((color, k) => el("path", { class: "ky-cr-led-glow", d: paths, fill: "none", stroke: color, "stroke-opacity": 0.3, "stroke-width": 7, "stroke-dasharray": `${cell} ${cell * 2}`, "stroke-dashoffset": -cell * k })).join("") : "";
|
|
248
|
+
return el("g", { class: "ky-cr-ribbon" }, el("path", { class: "ky-cr-led-line", d: paths, "stroke-width": 3.4 }) + glow + lit.join(""));
|
|
249
|
+
}
|
|
250
|
+
/** The scoreboard in the flat plan, as the board hangs over the surface seen from above: its dark cap with a line of light round it and its four edges' colours. */
|
|
251
|
+
export function planScoreboard(box, look) {
|
|
252
|
+
const b = planBoard(box);
|
|
253
|
+
const [c0, c1, c2] = look.colors;
|
|
254
|
+
const rim = b.d * 0.07;
|
|
255
|
+
const inner = { x: b.x + rim * 2, y: b.y + rim * 2, w: b.w - rim * 4, d: b.d - rim * 4 };
|
|
256
|
+
const rect = (cls, r, extra = {}) => el("rect", { class: cls, x: n(r.x), y: n(r.y), width: n(r.w), height: n(r.d), rx: n(rim), ...extra });
|
|
257
|
+
const edge = (x, y, w, h, color) => el("rect", { class: "ky-cr-led", x: n(x), y: n(y), width: n(w), height: n(h), fill: color });
|
|
258
|
+
return el("g", { class: "ky-cr-board" }, (look.glow ? rect("ky-cr-led-glow", { x: b.x - rim * 2, y: b.y - rim * 2, w: b.w + rim * 4, d: b.d + rim * 4 }, { fill: c2, "fill-opacity": 0.22 }) : "") +
|
|
259
|
+
rect("ky-cr-led-bed", b) +
|
|
260
|
+
edge(inner.x, inner.y, inner.w, rim, c0) +
|
|
261
|
+
edge(inner.x, inner.y + inner.d - rim, inner.w, rim, c1) +
|
|
262
|
+
edge(inner.x, inner.y, rim, inner.d, c2) +
|
|
263
|
+
edge(inner.x + inner.w - rim, inner.y, rim, inner.d, c2));
|
|
264
|
+
}
|
|
265
|
+
/** How tall the ribbon on the front of a tier is, for a front `top` feet high: the usual ribbon, or less where the front is low. */
|
|
266
|
+
export const ribbonHigh = (top) => Math.min(BOARD.ribbon, top * 0.6);
|
|
267
|
+
/** The ribbon on the top of a wall that stands over the line from `a` to `b` and is `top` feet high (the front of a tier): a wall of its own, as tall as `ribbonHigh` says. `run` is how far along the ribbon the wall starts. */
|
|
268
|
+
export const fasciaRibbon = (id, a, b, top, look, run) => {
|
|
269
|
+
const high = ribbonHigh(top);
|
|
270
|
+
return wall(id, "ribbon", a[0], a[1], b[0], b[1], top - high, top, ledWall(Math.hypot(b[0] - a[0], b[1] - a[1]), high, look, run));
|
|
271
|
+
};
|
|
272
|
+
/** A hall's boards as its stadium asks for them: the layout has scoreboards of its own, so the hall has the ribbons and no scoreboard hung over its middle. */
|
|
273
|
+
export const hallBoards = (ask) => (ask === undefined || ask === false ? undefined : { ...(ask === true ? {} : ask), scoreboard: false });
|
package/dist/venue.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Crowd, type CrowdStands } from "./crowd.ts";
|
|
2
2
|
import { type Cut } from "./venue.geom.ts";
|
|
3
3
|
import { type Upright } from "./turn.kit.ts";
|
|
4
|
-
import type { SurfaceOptions } from "./types.ts";
|
|
4
|
+
import type { LedBoardOptions, LightingOptions, SurfaceOptions } from "./types.ts";
|
|
5
5
|
/** What can be asked of the venue. */
|
|
6
6
|
export interface VenueSettings {
|
|
7
7
|
/** Roofs and canopies over the top tier of the stands: a canopy over the back of the top tier in the open, a lid over a hall. They give way to the eye as the walls do. Off by default. */
|
|
@@ -12,12 +12,24 @@ export interface VenueSettings {
|
|
|
12
12
|
walls?: boolean;
|
|
13
13
|
/** Fill the empty places of the stands (the apron round the surface, the back of each bowl): on for a real venue's, off for the usual stands (where it is a tournament layout's hall that asks for it). */
|
|
14
14
|
fill?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The LED boards: ribbon boards along the fascia between the seating levels (on the front of the one level a venue has, where there is no walk) and a scoreboard hung over the middle of the playing surface. `true` for both in the package's
|
|
17
|
+
* colours, or `{ ribbon, scoreboard, colors, pattern, score }`. Generic content, never a logo, a sponsor or a real board's text. Off by default; it wins over the crowd's own `boards`, and `false` turns that one off. See `docs/boards.md`.
|
|
18
|
+
*/
|
|
19
|
+
boards?: boolean | LedBoardOptions;
|
|
20
|
+
/**
|
|
21
|
+
* The lighting: `"night"` (or `true`, or `{ preset: "night" }`) dims the seats, lights the surface with a pool of light round it, shows small lights along the aisles and the exits, makes the boards glow and lights the rim of the concourse.
|
|
22
|
+
* Off by default; it wins over the crowd's own `lighting`, and `false` turns that one off. See `docs/venues.md`.
|
|
23
|
+
*/
|
|
24
|
+
lighting?: LightingOptions;
|
|
15
25
|
/** How finely the curves are cut: `low` a facet to each corner, `auto` (the default) three, `high` six; an oval is 12, 28 or 56 facets round. */
|
|
16
26
|
detail?: Cut;
|
|
17
27
|
}
|
|
18
28
|
/** The settings a venue is built from: the asker's, and the prefix of the ids of what it draws, which the view gives. */
|
|
19
29
|
export interface VenueBuild extends VenueSettings {
|
|
20
30
|
id: string;
|
|
31
|
+
/** Leave the scoreboard hung over the middle out (a tournament hall has scoreboards of its own). */
|
|
32
|
+
noCentre?: boolean;
|
|
21
33
|
}
|
|
22
34
|
/** What the venue is made of: the things that stand, and the patterns they are drawn with (the contents of a `defs`). */
|
|
23
35
|
export interface Venue {
|
package/dist/venue.js
CHANGED
|
@@ -7,8 +7,10 @@ import { patterns } from "./crowd.draw.js";
|
|
|
7
7
|
import { crowdStands } from "./draw.js";
|
|
8
8
|
import { groundById } from "./grounds.js";
|
|
9
9
|
import { ARC_STEPS, clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
|
|
10
|
-
import { apronPlanes, backEdges, backWall, concourseFloor, rimAt, suiteWall } from "./venue.fill.js";
|
|
10
|
+
import { APRON_RISE, apronPlanes, backEdges, backWall, concourseFloor, rimAt, suiteWall } from "./venue.fill.js";
|
|
11
11
|
import { firstHit, realBuilding } from "./venue.real.js";
|
|
12
|
+
import { lightPool, resolveLighting } from "./venue.lighting.js";
|
|
13
|
+
import { centreBoard, fasciaRibbon, hangsOver, ledStrip, resolveBoards, ribbonHigh, ribbonShare, tierLinks } from "./venue.boards.js";
|
|
12
14
|
import { n, shy, wall } from "./turn.kit.js";
|
|
13
15
|
export const NO_VENUE = { items: [], defs: "", reach: 0 };
|
|
14
16
|
/** The sports played under a roof: their stands have a lid and not a canopy. */
|
|
@@ -72,6 +74,8 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
|
|
|
72
74
|
else
|
|
73
75
|
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}"/>`;
|
|
74
76
|
}
|
|
77
|
+
if (night)
|
|
78
|
+
svg += `<polygon class="u-veil" points="${pointList(corners)}"/>`;
|
|
75
79
|
if (fine) {
|
|
76
80
|
let plain = "";
|
|
77
81
|
let vomitory = "";
|
|
@@ -89,19 +93,20 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
|
|
|
89
93
|
if (plain !== "")
|
|
90
94
|
svg += `<path class="ky-cr-aisle" fill="none" stroke-width="1.1" d="${plain}"/>`;
|
|
91
95
|
if (vomitory !== "")
|
|
92
|
-
svg += `<path class="ky-cr-aisle" fill="none" stroke-width="2.4" d="${vomitory}"/>`;
|
|
96
|
+
svg += `<path class="${night ? "ky-cr-aisle ky-cr-vom" : "ky-cr-aisle"}" fill="none" stroke-width="2.4" d="${vomitory}"/>`;
|
|
93
97
|
}
|
|
94
98
|
return svg;
|
|
95
99
|
};
|
|
96
100
|
// The tiers. A ring that no concourse leads to is the first of its chain: a low wall stands under its first row.
|
|
97
101
|
const chained = new Set();
|
|
98
|
-
const links = stands
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const links = tierLinks(stands);
|
|
103
|
+
for (const l of links)
|
|
104
|
+
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);
|
|
107
|
+
const ribbons = [];
|
|
108
|
+
/** How far along the ribbon of one level the next piece of it starts, in feet, so that its panels carry on. */
|
|
109
|
+
let run = 0;
|
|
105
110
|
for (const ring of stands.rings) {
|
|
106
111
|
const strips = stripsOf(ring.region, ring.hole, ring.more ?? [], cut);
|
|
107
112
|
const into = ring.tier === "wall" ? wallSeats : seatPlanes;
|
|
@@ -124,24 +129,45 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
|
|
|
124
129
|
if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 0.5 || inCover([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]))
|
|
125
130
|
continue;
|
|
126
131
|
const h = lift + ring.height[0];
|
|
127
|
-
const
|
|
132
|
+
const id = `${ring.id}-front-${si === 0 ? "" : `s${si}-`}${i}`;
|
|
133
|
+
const long = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
134
|
+
// A venue with one level has its ribbon on the front of that level: the front is the wall below the ribbon, and the ribbon a wall of its own over it.
|
|
135
|
+
const ribbon = boards?.ribbon === true && links.length === 0 && ring.height[0] >= 5 ? ribbonHigh(h) : 0;
|
|
136
|
+
const w = wall(id, "fascia", a[0], a[1], b[0], b[1], 0, h - ribbon, `<rect class="u-fascia" width="${n(long)}" height="${n(h - ribbon)}"/>`);
|
|
128
137
|
seatPlanes.push(shy(w, centre));
|
|
138
|
+
if (ribbon > 0 && boards !== undefined) {
|
|
139
|
+
ribbons.push(shy(fasciaRibbon(`${id}-ribbon`, a, b, h, boards, run), centre));
|
|
140
|
+
run += long;
|
|
141
|
+
}
|
|
129
142
|
}
|
|
130
143
|
});
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
// The walks between the tiers: a plane from the outer edge of one tier up to the first row of the next.
|
|
134
147
|
for (const { c, before, after } of links) {
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
run = 0;
|
|
149
|
+
const rise = after.height[0] - before.height[1];
|
|
137
150
|
for (const q of quadsOf(stripsOf(c.region, c.hole, [], cut))) {
|
|
138
151
|
const slope = slopeOf(q, lift + before.height[1], lift + after.height[0]);
|
|
139
152
|
if (slope === undefined)
|
|
140
153
|
continue;
|
|
154
|
+
const share = boards?.ribbon === true ? ribbonShare(rise, Math.hypot(slope.slope.gx, slope.slope.gy)) : 0;
|
|
141
155
|
for (const piece of pieces(tidy(q))) {
|
|
142
156
|
count += 1;
|
|
157
|
+
// A ribbon board is the top of the walk, a plane of its own over the same slope: the walk below it is the walk less that strip.
|
|
158
|
+
let walk = piece;
|
|
159
|
+
if (share > 0 && boards !== undefined && piece.length === 4) {
|
|
160
|
+
const [a, b, c2, d] = piece;
|
|
161
|
+
const cut1 = (p, q2) => [p[0] + (q2[0] - p[0]) * (1 - share), p[1] + (q2[1] - p[1]) * (1 - share)];
|
|
162
|
+
const [e, f] = [cut1(a, d), cut1(b, c2)];
|
|
163
|
+
walk = [a, b, f, e];
|
|
164
|
+
const rib = raked(`ribbon-${count}`, "ribbon", [e, f, c2, d], slope.slope, slope.along, (corners) => ledStrip(corners, boards, run));
|
|
165
|
+
if (rib !== undefined)
|
|
166
|
+
ribbons.push({ ...rib, shy: slope.out });
|
|
167
|
+
run += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
168
|
+
}
|
|
143
169
|
// Where the empty places are filled, the walk between two tiers is the suite level: a facing wall with a band of windows, not a bare shelf.
|
|
144
|
-
const p = raked(`walk-${count}`, "concourse",
|
|
170
|
+
const p = raked(`walk-${count}`, "concourse", walk, slope.slope, slope.along, (corners) => (filled ? suiteWall(corners) : `<polygon class="u-conc" points="${pointList(corners)}"/>`));
|
|
145
171
|
if (p !== undefined)
|
|
146
172
|
seatPlanes.push({ ...p, shy: slope.out });
|
|
147
173
|
}
|
|
@@ -165,9 +191,19 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
|
|
|
165
191
|
});
|
|
166
192
|
}
|
|
167
193
|
}
|
|
194
|
+
// The surface is lit: a pool of light on the floor round it, under the seats.
|
|
195
|
+
if (night)
|
|
196
|
+
seatPlanes.push(lightPool("night-pool", stands.box, lift + APRON_RISE + 0.03));
|
|
168
197
|
const items = [];
|
|
169
198
|
if (seatPlanes.length > 0)
|
|
170
199
|
items.push({ id: "stands", kind: "stands", at: centre, height: topOf(seatPlanes), planes: seatPlanes });
|
|
200
|
+
if (ribbons.length > 0)
|
|
201
|
+
items.push({ id: "ribbon-boards", kind: "ribbon-boards", at: centre, height: topOf(ribbons), planes: ribbons });
|
|
202
|
+
// The scoreboard hung over the middle of the surface (a ballpark has none, and a hall's own scoreboards stand in its place).
|
|
203
|
+
if (boards?.scoreboard === true && v.noCentre !== true && hangsOver(String(o.sport))) {
|
|
204
|
+
const planes = centreBoard("centre-board", stands.box, lift, boards);
|
|
205
|
+
items.push({ id: "centre-board", kind: "hung-board", at: centre, height: topOf(planes), planes });
|
|
206
|
+
}
|
|
171
207
|
if (wallSeats.length > 0)
|
|
172
208
|
items.push({ id: "wall-seats", kind: "wall-seats", at: centre, height: topOf(wallSeats), planes: wallSeats });
|
|
173
209
|
// The roof: a canopy over the back of the top tier, or a lid over it in a hall. A real venue has its own: the walls of its building and a roof of its kind over them.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Plane } from "./turn.kit.ts";
|
|
2
|
+
import type { LightingOptions, LightingPreset } from "./types.ts";
|
|
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;
|
|
6
|
+
/** 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
|
+
export declare const POOL: {
|
|
8
|
+
readonly reach: 20;
|
|
9
|
+
readonly edge: 0.7;
|
|
10
|
+
readonly strength: 0.34;
|
|
11
|
+
};
|
|
12
|
+
/** 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. */
|
|
13
|
+
export declare function lightPool(id: string, box: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
w: number;
|
|
17
|
+
h: number;
|
|
18
|
+
}, z: number): Plane;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The lighting of a venue. `night` is the first preset: the house goes down, the playing surface stays lit, small lights show along the aisles and stairs, the ribbon boards and the scoreboard glow and the exits and the concourse rim are
|
|
2
|
+
// softly lit. It is made of what the venue already has: the colours are custom properties a stylesheet sets on the 3D view's root (`data-lighting="night"`, see turn.style.ts) and on the plan's crowd (`ky-night`), a veil over each
|
|
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
|
+
// Pure: data in, planes out.
|
|
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). */
|
|
8
|
+
export function resolveLighting(ask) {
|
|
9
|
+
if (ask === undefined || ask === null || ask === false)
|
|
10
|
+
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
|
+
}
|
|
16
|
+
/** 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
|
+
export const POOL = { reach: 20, edge: 0.7, strength: 0.34 };
|
|
18
|
+
/** 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) {
|
|
20
|
+
const [w, h] = [box.w + 2 * POOL.reach, box.h + 2 * POOL.reach];
|
|
21
|
+
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>` +
|
|
24
|
+
`<rect class="u-pool" width="${n(w)}" height="${n(h)}" fill="url(#${gid})"/>`;
|
|
25
|
+
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
|
+
}
|
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.
|
|
2
|
+
export declare const VERSION = "0.38.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.
|
|
2
|
+
export const VERSION = "0.38.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|