@johnmorrisdotca/kyogijo-stadiums 0.23.0 → 0.25.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/dist/draw.js CHANGED
@@ -10,6 +10,7 @@ import { ERA_SPORTS, footballIn, rulesFor, seasonLabel } from "./eras.js";
10
10
  import { FOOTBALL } from "./geometry.js";
11
11
  import { renderLayers } from "./layers.js";
12
12
  import { arenaById, arenaCourtIn, arenaNameIn, arenaOfficialName, arenaRinkIn } from "./arenas.js";
13
+ import { venueById, venueBowl } from "./venues.js";
13
14
  import { groundById, groundNameIn, groundOfficialName } from "./grounds.js";
14
15
  import { BOARD, courtFor, hasSetsBoard, isNetLeague, isNetSport } from "./court.js";
15
16
  import { cricketBoardMarkup } from "./cricket.board.js";
@@ -55,6 +56,7 @@ import { KYOGIJO_STRINGS, fill } from "./strings.js";
55
56
  import { courtStyle } from "./courts.style.js";
56
57
  import { annotate, annotated, wideView } from "./annotate.js";
57
58
  import { KYOGIJO_BACKGROUND_STYLE, laidOn } from "./background.js";
59
+ import { overlayMarkup, placeBoard, resolveBoard } from "./overlay.js";
58
60
  import { KYOGIJO_HARDWOOD_STYLE, resolveHardwood } from "./hardwood.js";
59
61
  import { WALL_SEATS, densityOf, layoutOf as crowdLayout, resolveCrowd } from "./crowd.js";
60
62
  import { drawCrowd, KYOGIJO_CROWD_STYLE } from "./crowd.draw.js";
@@ -427,10 +429,16 @@ function standsFor(o, surface, crowd) {
427
429
  const walls = park === undefined || crowd.wallSeats === false ? [] : parkWallsIn(park, o.year).filter((w) => (crowd.wallSeats === true ? w.height_ft >= 25 : w.label !== undefined && WALL_SEATS[park.id] === w.label));
428
430
  return parkLayout({ fence: fenceOutline(points), backstop, crowd, seatsOnWalls: walls.map((w) => ({ from: w.from, to: w.to, height: w.height_ft })) });
429
431
  }
432
+ // An arena's real venue: the building it stands in and a bowl fitted to it, where the package has one for the arena and no league of the caller's own sets the surface.
433
+ const box = surface.whole ?? surface.box ?? { x: 0, y: 0, w: 100, h: 100 };
434
+ const venue = crowd.real && sport === "nhl" && chooseLeague(o).league === undefined ? venueById(o.arena) : undefined;
435
+ // (No visitors' block: no source says where a real arena puts them.)
436
+ if (venue !== undefined)
437
+ return crowdLayout({ sport, box, crowd: crowd.awayEnd === "auto" ? { ...crowd, awayEnd: "none" } : crowd, awaySide, real: venueBowl(venue, box) });
430
438
  // A dojo's crowd sits in its gallery, from the room's balcony data (in feet here); the arenas (the Budokan, the Kokugikan) keep their bowls.
431
439
  const hall = sport === "dojo" ? dojoGallery(o) : undefined;
432
440
  const gallery = hall === undefined ? undefined : { offset: hall.offset / 0.3048, depth: hall.depth / 0.3048, height: [hall.height[0] / 0.3048, hall.height[1] / 0.3048], rows: hall.rows, aisles: hall.aisles };
433
- return crowdLayout({ sport, box: surface.whole ?? surface.box ?? { x: 0, y: 0, w: 100, h: 100 }, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }) });
441
+ return crowdLayout({ sport, box, crowd, awaySide, ground: o.ground, arena: o.budo?.hall === "budokan", ...(gallery === undefined ? {} : { gallery }) });
434
442
  }
435
443
  /** True when the drawing is asked to be small: `detail: "simple"` or a `width` under 480 pixels. */
436
444
  const smallDrawing = (o) => {
@@ -715,7 +723,11 @@ export function describe(o) {
715
723
  const special = specialText(o, language, surfaceName);
716
724
  // What the arena says about the size: that it is the one its sources publish, or that its rink is said to have been shorter and no size is given.
717
725
  const arenaLine = arenaName === undefined ? "" : `${arenaSize !== undefined ? `${gap}${fill(words.said.arenaSize, { arena: arenaName })}` : sport === "nhl" && arena?.rinkUnknown === true ? `${gap}${fill(words.said.arenaUnknown, { arena: arenaName })}` : ""}${officially(arena === undefined ? undefined : arenaOfficialName(arena, language, year))}`;
718
- const parkLine = (parkName === undefined ? `${groundLine}${halfLine}${arenaLine}` : `${gap}${fill(words.said.park, { park: parkName })}${officially(parkOfficial)}${ballpark === undefined ? "" : ballparkSentences(ballpark).map((line) => `${gap}${line}`).join("")}`) + (special === undefined ? "" : `${gap}${special.text}`);
726
+ // The stands of a real venue say what they are: a plausible bowl, and how the rink lies in the building.
727
+ const realCrowd = o.crowd === undefined || o.crowd === false ? undefined : resolveCrowd(o.crowd);
728
+ const venue = sport === "nhl" && league === undefined && realCrowd?.real === true ? venueById(o.arena) : undefined;
729
+ const realLine = venue === undefined || arenaName === undefined ? "" : `${gap}${fill(words.said.real, { arena: arenaName, seats: String(venue.seats.hockey), tiers: String(venue.tiers.count) })}${gap}${words.said[venue.osm.axis.basis === "mapped" ? "realMapped" : venue.osm.axis.basis === "building" ? "realBuilding" : "realNone"]}`;
730
+ const parkLine = (parkName === undefined ? `${groundLine}${halfLine}${arenaLine}${realLine}` : `${gap}${fill(words.said.park, { park: parkName })}${officially(parkOfficial)}${ballpark === undefined ? "" : ballparkSentences(ballpark).map((line) => `${gap}${line}`).join("")}`) + (special === undefined ? "" : `${gap}${special.text}`);
719
731
  // A drawing of a past season says which: the markings are those of that season.
720
732
  const dated = typeof o.year === "number" && Number.isFinite(o.year);
721
733
  const eraLine = dated && league !== undefined
@@ -901,6 +913,8 @@ function drawnKyogijo(o) {
901
913
  let defs = "";
902
914
  let grounded = false;
903
915
  let foldedSheet = false;
916
+ let boarded;
917
+ const board = sized === "badge" ? undefined : resolveBoard(o.board);
904
918
  let pg;
905
919
  if (isScore(o) && left !== undefined && right !== undefined && sized === "badge") {
906
920
  const badge = badgeMarkup(left, right, { sport, status, parts, id, situation: situation === undefined ? undefined : badgeSituation(situation, language) });
@@ -1065,6 +1079,14 @@ function drawnKyogijo(o) {
1065
1079
  body = group;
1066
1080
  viewBox = `${num(v.x)} ${num(v.y)} ${num(v.w)} ${num(v.h)}`;
1067
1081
  }
1082
+ // A board over the surface, in the drawing's own units, over everything else (a strip's score stays clear of it, under the surface).
1083
+ if (board !== undefined) {
1084
+ const laid = overlayMarkup(board, placeBoard(board, v));
1085
+ if (laid !== "") {
1086
+ body += laid;
1087
+ boarded = typeof board.position === "object" ? "custom" : board.position;
1088
+ }
1089
+ }
1068
1090
  }
1069
1091
  const root = el("svg", {
1070
1092
  xmlns: "http://www.w3.org/2000/svg",
@@ -1084,6 +1106,7 @@ function drawnKyogijo(o) {
1084
1106
  "data-track": sport === "athletics" ? o.athletics?.track : undefined,
1085
1107
  "data-winner": isScore(o) ? winner : undefined,
1086
1108
  "data-pageant": pg === undefined ? undefined : pageant?.style,
1109
+ "data-board": boarded,
1087
1110
  "data-version": VERSION,
1088
1111
  width: safeLength(o.width),
1089
1112
  height: safeLength(o.height),
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export { OUTCOMES, drawLayers, layerTable } from "./layers.ts";
10
10
  export { RANGES } from "./ranges.names.ts";
11
11
  export { BACKGROUND_OPACITY, BACKGROUND_PATTERNS, KYOGIJO_BACKGROUND_STYLE } from "./background.ts";
12
12
  export type { Background, BackgroundFit, BackgroundOptions, BackgroundPattern } from "./background.ts";
13
+ export { BOARD_CLEAR, BOARD_POSITIONS, placeBoard } from "./overlay.ts";
14
+ export type { BoardBox, BoardOptions, BoardPoint, BoardPosition } from "./overlay.ts";
13
15
  export { FIELD_SPORTS, fieldPlan, isFieldSport } from "./fields.ts";
14
16
  export { POOL_SPORTS, isPoolSport, poolPlan, swimPlan, venueLayout } from "./pool.ts";
15
17
  export type { ArtisticPlan, Board, DivePlan, LanePlan, PoloPlan, PoolPlan, Rope, RopeColour, SwimPlan, VenueItem, VenueLayout } from "./pool.ts";
@@ -61,6 +63,8 @@ export { ERA_CURRENT, ERA_SOURCES, ERA_SPORTS, eraChanges, eraRange, eraStates,
61
63
  export type { EraChange, EraRules, EraRulesOf, EraSource, EraSpan, EraSport, FootballRules, MlbRules, NbaRules, NhlRules } from "./eras.ts";
62
64
  export { ARENA_IDS, ARENAS, arenaById, arenaByName, arenaByWikidata, arenaCourtIn, arenaNameIn, arenaOfficialName, arenaRinkIn, arenaSourceUrl, arenasByName } from "./arenas.ts";
63
65
  export type { Arena, ArenaCapacity, ArenaData, ArenaName, ArenaSize, ArenaSource } from "./arenas.ts";
66
+ export { VENUE_ATTRIBUTION, VENUE_IDS, VENUES, arenaOfVenue, bowlInside, planVenue, venueBowl, venueById, venueByWikidata, venueOrientation, venueRink, venuesIn } from "./venues.ts";
67
+ export type { RealVenue, RealVenueData, VenueBasis, VenueDivision, VenueOrientation, VenueOsm, VenuePlan, VenueRoof } from "./venues.ts";
64
68
  export { GROUND_IDS, GROUNDS, groundById, groundByName, groundByWikidata, groundNameIn, groundOfficialName, groundSourceUrl, groundsByName, groundsHosting } from "./grounds.ts";
65
69
  export type { Ground, GroundCompetition, GroundData, GroundName, GroundSource, GroundTournament } from "./grounds.ts";
66
70
  export { AFL_GROUNDS, FAMILY_OF, LEAGUES, LEAGUE_SOURCES, VARIANTS, chooseLeague, hasHistory, leagueAt, leagueById, leagueChanges, leagueFigures, leagueIds, leagueSeason, leagueSize } from "./leagues.ts";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { useFlags } from "./flags-source.js";
8
8
  export { OUTCOMES, drawLayers, layerTable } from "./layers.js";
9
9
  export { RANGES } from "./ranges.names.js";
10
10
  export { BACKGROUND_OPACITY, BACKGROUND_PATTERNS, KYOGIJO_BACKGROUND_STYLE } from "./background.js";
11
+ export { BOARD_CLEAR, BOARD_POSITIONS, placeBoard } from "./overlay.js";
11
12
  export { FIELD_SPORTS, fieldPlan, isFieldSport } from "./fields.js";
12
13
  export { POOL_SPORTS, isPoolSport, poolPlan, swimPlan, venueLayout } from "./pool.js";
13
14
  export { formatTime, parseTime } from "./pool.race.js";
@@ -38,6 +39,7 @@ export { PARKS, PARK_IDS, fenceFor, parkById, parkByName, parkByWikidata, parkNa
38
39
  export { officialName, plainName } from "./names.js";
39
40
  export { ERA_CURRENT, ERA_SOURCES, ERA_SPORTS, eraChanges, eraRange, eraStates, footballIn, rulesFor, seasonLabel } from "./eras.js";
40
41
  export { ARENA_IDS, ARENAS, arenaById, arenaByName, arenaByWikidata, arenaCourtIn, arenaNameIn, arenaOfficialName, arenaRinkIn, arenaSourceUrl, arenasByName } from "./arenas.js";
42
+ export { VENUE_ATTRIBUTION, VENUE_IDS, VENUES, arenaOfVenue, bowlInside, planVenue, venueBowl, venueById, venueByWikidata, venueOrientation, venueRink, venuesIn } from "./venues.js";
41
43
  export { GROUND_IDS, GROUNDS, groundById, groundByName, groundByWikidata, groundNameIn, groundOfficialName, groundSourceUrl, groundsByName, groundsHosting } from "./grounds.js";
42
44
  export { AFL_GROUNDS, FAMILY_OF, LEAGUES, LEAGUE_SOURCES, VARIANTS, chooseLeague, hasHistory, leagueAt, leagueById, leagueChanges, leagueFigures, leagueIds, leagueSeason, leagueSize } from "./leagues.js";
43
45
  export { SURFACE_UNITS, UNITS, convertLength, isUnit } from "./units.js";
@@ -0,0 +1,73 @@
1
+ /** Where a board goes. `top` and `bottom` are centred on the drawing's top and bottom edge, `side` is at the right edge, centred, `left` the same at the left, `corner` is the top right corner, and the four corners by their names. */
2
+ export type BoardPosition = "top" | "bottom" | "side" | "left" | "corner" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
3
+ /** A place of your own: the board's top left corner (or, with `anchor: "center"`, its middle) at this share of the drawing's width (`x`) and height (`y`), 0 to 1. The board may then cover the middle of the surface: you asked for it. */
4
+ export interface BoardPoint {
5
+ x: number;
6
+ y: number;
7
+ anchor?: "start" | "center";
8
+ }
9
+ /** A board over a drawing. */
10
+ export interface BoardOptions {
11
+ /** The board, as SVG text (`<svg …>…</svg>`): the string `resultsBoard`, `boutBoard` or `broadcastMedalTable` of Taisen Brackets gives. It is written as an image, so nothing in it can run and its styles stay in it. Anything else is ignored. */
12
+ svg: string;
13
+ /** The board's own width and height, the numbers its maker gives (`resultsBoardSize`, `boutBoardSize`, `broadcastMedalSize`): they give its shape. Left out, the `viewBox` of the SVG is read. A board with no size is ignored. */
14
+ width?: number;
15
+ height?: number;
16
+ /** Where it goes: a name (`BOARD_POSITIONS`) or a point of your own. `top` by default. */
17
+ position?: BoardPosition | BoardPoint;
18
+ /** How opaque it is, 0 to 1. 1 by default. */
19
+ opacity?: number;
20
+ /** Its width as a share of the drawing's width, 0.1 to 1. By default 0.5 at the top and the bottom, 0.3 at a side and 0.36 in a corner, and 0.4 at a point; a board is never made larger than the room that keeps the middle of the surface clear. */
21
+ share?: number;
22
+ /** The gap between the board and the edge of the drawing, as a share of the drawing's width, 0 to 0.2. 0.02 by default. */
23
+ margin?: number;
24
+ /** Let a named position cover the middle of the surface, so that the board may be as large as `share` says (it is still kept inside the drawing). Off by default. */
25
+ cover?: boolean;
26
+ /** What the board says, in a sentence, for a reader who cannot see it: its title and who leads, say. Without it the board is hidden from assistive technology (it is a picture of what the page says elsewhere). */
27
+ label?: string;
28
+ }
29
+ /** A board that has been read: sizes and places that can be used, and the picture as an address. */
30
+ export interface ResolvedBoard {
31
+ href: string;
32
+ width: number;
33
+ height: number;
34
+ position: BoardPosition | {
35
+ x: number;
36
+ y: number;
37
+ anchor: "start" | "center";
38
+ };
39
+ opacity: number;
40
+ share: number;
41
+ margin: number;
42
+ cover: boolean;
43
+ label: string | undefined;
44
+ }
45
+ /** A box: a drawing's viewBox, or the place a board takes in it. */
46
+ export interface BoardBox {
47
+ x: number;
48
+ y: number;
49
+ w: number;
50
+ h: number;
51
+ }
52
+ /** The positions, in the order a menu lists them. */
53
+ export declare const BOARD_POSITIONS: readonly BoardPosition[];
54
+ /** The share of a drawing's width and of its height that is kept clear round its middle: a board placed by name does not reach into it, so the middle of the playing area, where the score's panel stands, is never covered. */
55
+ export declare const BOARD_CLEAR = 0.34;
56
+ /** The shape of the box a 3D view is placed for (16 by 10, the view's own) and the units of it: a board in 3D is placed for this box and keeps its place, as a share of the box, when the box is another shape. */
57
+ export declare const BOARD_TURN_BOX: BoardBox;
58
+ /** The board an option asks for, read: a bad one is `undefined` and the drawing is as it was without it, as a bad `background` image is. */
59
+ export declare function resolveBoard(value: unknown): ResolvedBoard | undefined;
60
+ /**
61
+ * The place a board takes in a box. A named position keeps to its edge and takes the share asked for, unless that would reach into the middle (`BOARD_CLEAR` of the box's width and height, centred): then the board is made smaller, down
62
+ * to nothing in a box too small to hold one, and never moved. `cover` lifts that, and a point of your own is never held to it. The board keeps its own shape, and stays inside the box.
63
+ */
64
+ export declare function placeBoard(board: ResolvedBoard, view: BoardBox): BoardBox;
65
+ /** The board as markup in a drawing: an image in the box `placeBoard` gave, in the drawing's own units, over everything else. Nothing for a box with no room. */
66
+ export declare function overlayMarkup(board: ResolvedBoard, box: BoardBox): string;
67
+ /**
68
+ * The 3D view's board: a layer over the whole view (not in the scene, so it never turns, tips or zooms with it), and in it the picture at its size as a share of the view's width, its own shape kept (`height:auto`, `object-fit:contain`),
69
+ * placed by flex alignment on the view's edges. Its largest height is what the box `BOARD_TURN_BOX` gives it, so that on a wider view than that it still does not reach into the middle. It never takes the pointer: a drag on it turns the view.
70
+ */
71
+ export declare function turnBoardMarkup(board: ResolvedBoard): string;
72
+ /** The rules of the 3D view's board layer, written into `KYOGIJO_TURN_STYLE`. */
73
+ export declare const KYOGIJO_TURN_BOARD_STYLE: string;
@@ -0,0 +1,182 @@
1
+ // A board laid over a drawing: a results board, a bout presentation, a medal table, drawn by anything else as an SVG (Taisen Brackets', say) and placed here, over the surface, in 2D (`board` in the drawing's
2
+ // options) and in 3D (`board` in the options of the 3D view, where it is a screen-space overlay over the view and never a plane that turns). The package takes the board as a string and draws no board of its own and
3
+ // reads nothing from the one it is given but its size: it is written as an image, which cannot run scripts, scaled evenly (its own shape is kept, so no letter is ever stretched), and placed by a named position
4
+ // that keeps the middle of the surface clear, or by a point of the caller's choosing. Pure: the same options always give the same string.
5
+ import { safePicture } from "./colour.js";
6
+ import { el, esc, num } from "./format.js";
7
+ /** The positions, in the order a menu lists them. */
8
+ export const BOARD_POSITIONS = ["top", "bottom", "side", "left", "corner", "top-left", "top-right", "bottom-left", "bottom-right"];
9
+ /** The share of a drawing's width and of its height that is kept clear round its middle: a board placed by name does not reach into it, so the middle of the playing area, where the score's panel stands, is never covered. */
10
+ export const BOARD_CLEAR = 0.34;
11
+ /** The shape of the box a 3D view is placed for (16 by 10, the view's own) and the units of it: a board in 3D is placed for this box and keeps its place, as a share of the box, when the box is another shape. */
12
+ export const BOARD_TURN_BOX = { x: 0, y: 0, w: 1600, h: 1000 };
13
+ const MAX_SIZE = 100000;
14
+ const isPosition = (value) => typeof value === "string" && BOARD_POSITIONS.includes(value);
15
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
16
+ const between = (value, low, high) => Math.min(high, Math.max(low, value));
17
+ /** The width and height of an SVG document's root, from its `viewBox`. */
18
+ function viewBoxOf(svg) {
19
+ const root = /^\s*(?:<\?xml[^>]*\?>\s*)?(?:<!--[\s\S]*?-->\s*)*<svg\b[^>]*>/i.exec(svg)?.[0];
20
+ const box = root === undefined ? undefined : /\sviewBox\s*=\s*"\s*(-?[\d.]+)[\s,]+(-?[\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*"/i.exec(root);
21
+ if (box === undefined || box === null)
22
+ return undefined;
23
+ const [width, height] = [Number(box[3]), Number(box[4])];
24
+ return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 ? { width, height } : undefined;
25
+ }
26
+ const defaultShare = (position) => {
27
+ if (typeof position === "object")
28
+ return 0.4;
29
+ if (position === "top" || position === "bottom")
30
+ return 0.5;
31
+ if (position === "side" || position === "left")
32
+ return 0.3;
33
+ return 0.36;
34
+ };
35
+ /** The board an option asks for, read: a bad one is `undefined` and the drawing is as it was without it, as a bad `background` image is. */
36
+ export function resolveBoard(value) {
37
+ if (typeof value !== "object" || value === null)
38
+ return undefined;
39
+ const o = value;
40
+ if (typeof o.svg !== "string" || !/^\s*(?:<\?xml|<svg)\b/i.test(o.svg))
41
+ return undefined;
42
+ const href = safePicture(o.svg);
43
+ if (href === undefined)
44
+ return undefined;
45
+ const own = viewBoxOf(o.svg);
46
+ const width = finite(o.width) && o.width > 0 ? o.width : own?.width;
47
+ const height = finite(o.height) && o.height > 0 ? o.height : own?.height;
48
+ if (width === undefined || height === undefined || width > MAX_SIZE || height > MAX_SIZE)
49
+ return undefined;
50
+ let position = "top";
51
+ const asked = o.position;
52
+ if (isPosition(asked))
53
+ position = asked;
54
+ else if (typeof asked === "object" && asked !== null && finite(asked.x) && finite(asked.y)) {
55
+ const point = asked;
56
+ position = { x: between(point.x, 0, 1), y: between(point.y, 0, 1), anchor: point.anchor === "center" ? "center" : "start" };
57
+ }
58
+ const label = typeof o.label === "string" && o.label.trim() !== "" ? o.label.trim().slice(0, 300) : undefined;
59
+ return {
60
+ href,
61
+ width,
62
+ height,
63
+ position,
64
+ opacity: finite(o.opacity) ? between(o.opacity, 0, 1) : 1,
65
+ share: finite(o.share) ? between(o.share, 0.1, 1) : defaultShare(position),
66
+ margin: finite(o.margin) ? between(o.margin, 0, 0.2) : 0.02,
67
+ cover: o.cover === true,
68
+ label,
69
+ };
70
+ }
71
+ /** Where a named position stands: which edge it keeps to on each axis. */
72
+ function edges(position) {
73
+ switch (position) {
74
+ case "top":
75
+ return { v: "top", h: "centre" };
76
+ case "bottom":
77
+ return { v: "bottom", h: "centre" };
78
+ case "side":
79
+ return { v: "middle", h: "right" };
80
+ case "left":
81
+ return { v: "middle", h: "left" };
82
+ case "corner":
83
+ case "top-right":
84
+ return { v: "top", h: "right" };
85
+ case "top-left":
86
+ return { v: "top", h: "left" };
87
+ case "bottom-left":
88
+ return { v: "bottom", h: "left" };
89
+ case "bottom-right":
90
+ return { v: "bottom", h: "right" };
91
+ }
92
+ }
93
+ /**
94
+ * The place a board takes in a box. A named position keeps to its edge and takes the share asked for, unless that would reach into the middle (`BOARD_CLEAR` of the box's width and height, centred): then the board is made smaller, down
95
+ * to nothing in a box too small to hold one, and never moved. `cover` lifts that, and a point of your own is never held to it. The board keeps its own shape, and stays inside the box.
96
+ */
97
+ export function placeBoard(board, view) {
98
+ const ratio = board.width / board.height;
99
+ const gap = board.margin * view.w;
100
+ const inner = { x0: view.x + gap, y0: view.y + gap, x1: view.x + view.w - gap, y1: view.y + view.h - gap };
101
+ const innerW = Math.max(0, inner.x1 - inner.x0);
102
+ const innerH = Math.max(0, inner.y1 - inner.y0);
103
+ const wanted = board.share * view.w;
104
+ const point = board.position;
105
+ if (typeof point === "object") {
106
+ const w = Math.max(0, Math.min(wanted, view.w, view.h * ratio));
107
+ const h = w / ratio;
108
+ const x = view.x + point.x * view.w - (point.anchor === "center" ? w / 2 : 0);
109
+ const y = view.y + point.y * view.h - (point.anchor === "center" ? h / 2 : 0);
110
+ return { x: between(x, view.x, view.x + view.w - w), y: between(y, view.y, view.y + view.h - h), w, h };
111
+ }
112
+ const { v, h: side } = edges(point);
113
+ const clear = { x0: view.x + view.w * (0.5 - BOARD_CLEAR / 2), x1: view.x + view.w * (0.5 + BOARD_CLEAR / 2), y0: view.y + view.h * (0.5 - BOARD_CLEAR / 2), y1: view.y + view.h * (0.5 + BOARD_CLEAR / 2) };
114
+ let room = Math.min(innerW, innerH * ratio);
115
+ if (!board.cover) {
116
+ // Beside the middle (to its right, or to its left) and above or below it: how wide the board may be in each.
117
+ const beside = side === "right" ? inner.x1 - (clear.x1 + gap) : side === "left" ? clear.x0 - gap - inner.x0 : 0;
118
+ const above = v === "top" ? clear.y0 - gap - inner.y0 : v === "bottom" ? inner.y1 - (clear.y1 + gap) : 0;
119
+ if (v === "middle")
120
+ room = Math.min(beside, innerH * ratio);
121
+ else if (side === "centre")
122
+ room = Math.min(above * ratio, innerW);
123
+ else
124
+ room = Math.max(Math.min(beside, innerH * ratio), Math.min(above * ratio, innerW));
125
+ }
126
+ const w = Math.max(0, Math.min(wanted, room));
127
+ const h = w / ratio;
128
+ const x = side === "left" ? inner.x0 : side === "right" ? inner.x1 - w : view.x + (view.w - w) / 2;
129
+ const y = v === "top" ? inner.y0 : v === "bottom" ? inner.y1 - h : view.y + (view.h - h) / 2;
130
+ return { x, y, w, h };
131
+ }
132
+ /** The name a position goes by in the markup. */
133
+ const positionName = (board) => (typeof board.position === "object" ? "custom" : board.position);
134
+ /** The board as markup in a drawing: an image in the box `placeBoard` gave, in the drawing's own units, over everything else. Nothing for a box with no room. */
135
+ export function overlayMarkup(board, box) {
136
+ if (!(box.w > 0) || !(box.h > 0))
137
+ return "";
138
+ return el("g", {
139
+ class: "ky-board",
140
+ "data-position": positionName(board),
141
+ opacity: board.opacity < 1 ? board.opacity : undefined,
142
+ "pointer-events": "none",
143
+ role: board.label === undefined ? undefined : "img",
144
+ "aria-label": board.label,
145
+ "aria-hidden": board.label === undefined ? "true" : undefined,
146
+ }, el("image", { class: "ky-board-img", href: board.href, x: box.x, y: box.y, width: box.w, height: box.h, preserveAspectRatio: "xMidYMid meet" }));
147
+ }
148
+ /**
149
+ * The 3D view's board: a layer over the whole view (not in the scene, so it never turns, tips or zooms with it), and in it the picture at its size as a share of the view's width, its own shape kept (`height:auto`, `object-fit:contain`),
150
+ * placed by flex alignment on the view's edges. Its largest height is what the box `BOARD_TURN_BOX` gives it, so that on a wider view than that it still does not reach into the middle. It never takes the pointer: a drag on it turns the view.
151
+ */
152
+ export function turnBoardMarkup(board) {
153
+ const box = placeBoard(board, BOARD_TURN_BOX);
154
+ if (!(box.w > 0))
155
+ return "";
156
+ const percent = (n) => `${num(Math.round(n * 1000) / 10)}%`;
157
+ const style = [`width:${percent(box.w / BOARD_TURN_BOX.w)}`, `max-height:${percent(box.h / BOARD_TURN_BOX.h)}`];
158
+ if (typeof board.position === "object") {
159
+ style.push("position:absolute", `left:${percent(board.position.x)}`, `top:${percent(board.position.y)}`, "margin:0");
160
+ if (board.position.anchor === "center")
161
+ style.push("translate:-50% -50%");
162
+ }
163
+ else
164
+ style.push(`margin:${percent(board.margin)}`);
165
+ if (board.opacity < 1)
166
+ style.push(`opacity:${num(board.opacity)}`);
167
+ const attributes = `class="ky-turn-board" data-position="${positionName(board)}"${board.label === undefined ? ' aria-hidden="true"' : ` role="img" aria-label="${esc(board.label)}"`}`;
168
+ return `<div ${attributes}><img class="ky-turn-board-img" src="${esc(board.href)}" alt="" width="${num(board.width)}" height="${num(board.height)}" style="${style.join(";")}"></div>`;
169
+ }
170
+ /** The rules of the 3D view's board layer, written into `KYOGIJO_TURN_STYLE`. */
171
+ export const KYOGIJO_TURN_BOARD_STYLE = `
172
+ .ky-turn-board{position:absolute;inset:0;z-index:var(--ky-turn-board-z,9999);display:flex;pointer-events:none;overflow:hidden}
173
+ .ky-turn-board-img{display:block;height:auto;object-fit:contain;flex:none;min-width:0}
174
+ .ky-turn-board[data-position=top]{justify-content:center;align-items:flex-start}
175
+ .ky-turn-board[data-position=bottom]{justify-content:center;align-items:flex-end}
176
+ .ky-turn-board[data-position=side]{justify-content:flex-end;align-items:center}
177
+ .ky-turn-board[data-position=left]{justify-content:flex-start;align-items:center}
178
+ .ky-turn-board[data-position=corner],.ky-turn-board[data-position=top-right]{justify-content:flex-end;align-items:flex-start}
179
+ .ky-turn-board[data-position=top-left]{justify-content:flex-start;align-items:flex-start}
180
+ .ky-turn-board[data-position=bottom-left]{justify-content:flex-start;align-items:flex-end}
181
+ .ky-turn-board[data-position=bottom-right]{justify-content:flex-end;align-items:flex-end}
182
+ .ky-turn-board[data-position=custom]{display:block}`.trim();
package/dist/strings.d.ts CHANGED
@@ -504,6 +504,12 @@ export interface KyogijoWords {
504
504
  arenaSize: string;
505
505
  /** An arena whose rink is said to have been shorter than the NHL's, with no size given: {arena}. */
506
506
  arenaUnknown: string;
507
+ /** The stands of an arena's real venue: {seats} seats in {tiers} tiers, fitted inside the outline of {arena}'s building from OpenStreetMap. */
508
+ real: string;
509
+ /** How the rink lies in it: along the building's long axis (an inference), as OpenStreetMap maps the ice, or not known. */
510
+ realBuilding: string;
511
+ realMapped: string;
512
+ realNone: string;
507
513
  /** A game decided on penalties: {name}, and the shoot-out score {a}-{b}, winner first. */
508
514
  wonPens: string;
509
515
  /** The shoot-out, as it is written beside a status: {a} and {b}, in the order of the sides on the drawing. */
package/dist/strings.js CHANGED
@@ -199,7 +199,7 @@ export const KYOGIJO_STRINGS = {
199
199
  },
200
200
  },
201
201
  scoreTitle: "{away} {awayScore}, {home} {homeScore}",
202
- said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
202
+ said: { home: "home", away: "away", won: "{name} won.", tied: "The game is level.", park: "The outfield fence follows the figures published for {park}.", officially: "Its official name is {name}.", ground: "The pitch follows the size published for {ground}.", groundDefault: "No pitch size is published for {ground}, so the pitch has the international size of 105 by 68 metres.", arenaRink: "{arena}, hockey rink", arenaCourt: "{arena}, basketball court", arenaRinkAbout: "an ice hockey rink, {length} by {width} feet ({lengthM} by {widthM} metres) with rounded corners, with its red and blue lines, face-off circles, creases and goal nets", arenaCourtAbout: "a basketball court, {length} by {width} feet ({lengthM} by {widthM} metres), with its keys, three-point lines, free-throw circles and the centre circle", arenaSize: "The size is the one published for {arena}.", arenaUnknown: "The article says the rink of {arena} was shorter than the NHL's but gives no size, so the NHL's is drawn.", real: "The stands are a plausible bowl of {seats} seats in {tiers} tiers, fitted inside the outline of the building of {arena} from OpenStreetMap ((c) OpenStreetMap contributors); it is not the arena's plan.", realBuilding: "The rink lies along the building's long axis, which is an inference, and the arrow points north.", realMapped: "The rink lies as OpenStreetMap maps the ice, and the arrow points north.", realNone: "The rink's orientation is not known, so it is drawn east-west with north at the top.", era: "The markings are those of the {season} season.", hammer: "{name} has the hammer.", ends: "By end, {name}: {list}.", total: "Total", wonPens: "{name} won {a}-{b} on penalties.", pens: "{a}-{b} pens", on: "Drawn as", sep: ", ", stop: ".", colon: ": ", gap: " " },
203
203
  budo: {
204
204
  sides: { north: "north", south: "south", east: "east", west: "west" },
205
205
  judo: "a judo contest area of {contest} by {contest} metres in a safety area {safety} metres wide, laid with tatami of {tatami}",
@@ -758,7 +758,7 @@ export const KYOGIJO_STRINGS = {
758
758
  },
759
759
  },
760
760
  scoreTitle: "{away} {awayScore}、{home} {homeScore}",
761
- said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
761
+ said: { home: "ホーム", away: "アウェイ", won: "{name}の勝ちです。", tied: "同点です。", park: "外野フェンスは、{park}の公表されている距離をもとに描かれています。", officially: "正式名称は{name}です。", ground: "ピッチは、{ground}の公表されている広さをもとに描かれています。", groundDefault: "{ground}のピッチの広さは公表されていないため、国際標準の105×68mで描かれています。", arenaRink: "{arena}のアイスホッケーリンク", arenaCourt: "{arena}のバスケットボールコート", arenaRinkAbout: "アイスホッケーのリンク。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で角が丸く、レッドライン、ブルーライン、フェイスオフサークル、クリース、ゴールがあります", arenaCourtAbout: "バスケットボールのコート。{length}フィート(約{lengthM}m)×{width}フィート(約{widthM}m)で、ペイントエリア、スリーポイントライン、フリースローサークル、センターサークルがあります", arenaSize: "広さは、{arena}の公表されている数値をもとに描かれています。", arenaUnknown: "{arena}のリンクはNHLの規格より短かったとされていますが、広さが示されていないため、NHLの規格で描かれています。", real: "スタンドは、OpenStreetMapの建物の輪郭((c) OpenStreetMap contributors)の内側に収まるように、{arena}の{seats}席・{tiers}層を推定して描いたもので、アリーナの設計図ではありません。", realBuilding: "リンクは建物の長い軸に沿って置いており(推定)、矢印は北を指します。", realMapped: "リンクの向きはOpenStreetMapに記録された氷面のとおりで、矢印は北を指します。", realNone: "リンクの向きは分かっていないため、東西に置き、北を上にしています。", era: "ラインやマークは{season}シーズンのものです。", hammer: "{name}がハンマー(最後の1投)を持っています。", ends: "{name}のエンドごとの得点:{list}。", total: "合計", wonPens: "{name}がPK戦を{a}-{b}で制しました。", pens: "PK戦 {a}-{b}", on: "図の内容", sep: "、", stop: "。", colon: ":", gap: "" },
762
762
  budo: {
763
763
  sides: { north: "北", south: "南", east: "東", west: "西" },
764
764
  judo: "柔道の試合場。試合場内は{contest}m四方、幅{safety}mの安全地帯があり、{tatami}の畳を敷いています",
package/dist/turn.dom.js CHANGED
@@ -8,6 +8,7 @@ import { buildTurn, floorOptions, openingView, turnWords } from "./turn.model.js
8
8
  import { controlsMarkup, layoutOf, turnMarkupOf, worldMarkup } from "./turn.markup.js";
9
9
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
10
10
  import { esc } from "./format.js";
11
+ import { turnBoardMarkup } from "./overlay.js";
11
12
  /** True where the page can draw a plane in 3D by one `matrix3d`: that is, nearly everywhere with a stylesheet. */
12
13
  export function turnSupported(win = typeof window === "undefined" ? undefined : window) {
13
14
  const css = win?.CSS;
@@ -47,7 +48,7 @@ export function mountTurn(host, options, settings, build) {
47
48
  if (!supported) {
48
49
  // The flat drawing, with a sentence that says why it does not turn when the browser is the reason.
49
50
  const flat = (next) => {
50
- host.innerHTML = `<div class="ky-turn" data-mode="flat"><style>${KYOGIJO_TURN_STYLE}</style>${model.flat ?? drawKyogijo(floorOptions(next))}${model.turnable ? `<p class="ky-turn-flat">${esc(turnWords(model).flat)}</p>` : ""}</div>`;
51
+ host.innerHTML = `<div class="ky-turn" data-mode="flat"><style>${KYOGIJO_TURN_STYLE}</style>${model.flat ?? drawKyogijo({ ...floorOptions(next), board: next.board })}${model.turnable ? `<p class="ky-turn-flat">${esc(turnWords(model).flat)}</p>` : ""}</div>`;
51
52
  };
52
53
  flat(options);
53
54
  return {
@@ -456,6 +457,10 @@ export function mountTurn(host, options, settings, build) {
456
457
  world.setAttribute("data-theme", forced);
457
458
  }
458
459
  gather();
460
+ // The board is a layer of its own over the view, after the scene: it is written again, and the view's turn is not touched.
461
+ stage.querySelector(".ky-turn-board")?.remove();
462
+ if (model.board !== undefined)
463
+ stage.insertAdjacentHTML("beforeend", turnBoardMarkup(model.board));
459
464
  stage.setAttribute("aria-label", model.title);
460
465
  root.querySelector(".ky-turn-help").textContent = `${turnWords(model).keys}${model.standing === "" ? "" : ` ${model.standing}`}`;
461
466
  const theme = model.options.theme;
@@ -7,6 +7,7 @@ import { cross, facing, fadeOf, n, size, unit } from "./turn.kit.js";
7
7
  import { buildTurn, turnWords } from "./turn.model.js";
8
8
  import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
9
9
  import { esc } from "./format.js";
10
+ import { turnBoardMarkup } from "./overlay.js";
10
11
  const px = (value) => `${Math.round(value * 1000) / 1000}`;
11
12
  /**
12
13
  * The matrix that stands a plane in the scene, before the camera: its x along `u`, its y along `v`, its corner `o`, in pixels from the middle of the scene
@@ -117,7 +118,7 @@ export function turnMarkupOf(model, settings = {}) {
117
118
  const sport = model.options.sport;
118
119
  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}">` +
119
120
  sheet +
120
- `<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></div>` +
121
+ `<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>` +
121
122
  `<p class="ky-turn-help" id="${esc(model.id)}-help">${esc(keys)}</p>` +
122
123
  (settings.controls === false ? "" : controlsMarkup(model, view)) +
123
124
  `<div class="ky-turn-live" role="status" aria-live="polite"></div>` +
@@ -2,6 +2,7 @@ import { KYOGIJO_STRINGS } from "./strings.ts";
2
2
  import { type PresetTable, type TurnPreset, type TurnView } from "./turn.camera.ts";
3
3
  import type { Plane } from "./turn.kit.ts";
4
4
  import { type UprightOptions, type Uprights } from "./uprights.ts";
5
+ import { type ResolvedBoard } from "./overlay.ts";
5
6
  import type { VenueSettings } from "./venue.ts";
6
7
  import type { Box3 } from "./turn.camera.ts";
7
8
  import type { Language, ScoreOptions, SurfaceOptions } from "./types.ts";
@@ -99,6 +100,8 @@ export interface TurnModel {
99
100
  lens?: number;
100
101
  /** The flat drawing to show where the 3D view cannot be drawn, when it is not `drawKyogijo`'s of `options` (a tournament layout's). */
101
102
  flat?: string;
103
+ /** The board the options ask for (`board`), laid over the view: a layer of its own, never a plane of the scene. */
104
+ board?: ResolvedBoard;
102
105
  }
103
106
  /** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
104
107
  export declare function floorOptions(options: SurfaceOptions | ScoreOptions): SurfaceOptions | ScoreOptions;
@@ -8,6 +8,7 @@ import { KYOGIJO_STRINGS } from "./strings.js";
8
8
  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
+ import { resolveBoard } from "./overlay.js";
11
12
  /** 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. */
12
13
  export const WORLD_PX = 1400;
13
14
  /** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
@@ -16,6 +17,8 @@ export function floorOptions(options) {
16
17
  delete rest.range;
17
18
  delete rest.half;
18
19
  delete rest.aspect;
20
+ // A board is a layer over the 3D view, not a picture on the floor.
21
+ delete rest.board;
19
22
  return rest;
20
23
  }
21
24
  const VIEWBOX = /viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"/;
@@ -86,7 +89,9 @@ export function buildTurn(options, settings = {}) {
86
89
  const reach = uprights.reach;
87
90
  const margin = reach === undefined ? undefined : convertLength(reach, "ft", lengthUnit(asked));
88
91
  const own = asked.margin;
89
- const base = reach !== undefined && crowdOpt !== undefined && crowdOpt !== false ? { ...asked, crowd: undefined, margin: Math.max(margin ?? 0, typeof own === "number" && Number.isFinite(own) ? own : 0) } : asked;
92
+ // (A real venue's building stays on the floor, with the arrow that says where north is: the crowd drawn without its seats.)
93
+ const keep = resolveCrowd(crowdOpt)?.real === true ? { real: true, stands: false } : undefined;
94
+ const base = reach !== undefined && crowdOpt !== undefined && crowdOpt !== false ? { ...asked, crowd: keep, margin: Math.max(margin ?? 0, typeof own === "number" && Number.isFinite(own) ? own : 0) } : asked;
90
95
  const o = boarded ? { ...base, plate: false, size: asked.size === "compact" ? "full" : asked.size } : base;
91
96
  const floor = settings.floor === undefined ? drawKyogijo(o) : settings.floor(o);
92
97
  const found = VIEWBOX.exec(floor);
@@ -123,6 +128,7 @@ export function buildTurn(options, settings = {}) {
123
128
  centre,
124
129
  view: openingView(settings, o.sport),
125
130
  turnable: !badge,
131
+ board: badge ? undefined : resolveBoard(options.board),
126
132
  };
127
133
  }
128
134
  /** The words of the 3D view in the model's language. */
@@ -1,4 +1,5 @@
1
1
  import { JUDGING_DARK, JUDGING_LIGHT } from "./judging.style.js";
2
+ import { KYOGIJO_TURN_BOARD_STYLE } from "./overlay.js";
2
3
  /**
3
4
  * The 3D view's stylesheet: the colours of what stands on the surface, in light and dark, and the rules that lay the scene out. Like the drawing's own, the
4
5
  * defaults are in `:where()`, which has no weight, so a page's own rule always wins; the colours are custom properties (`--ky-up-*`) a page can set.
@@ -65,6 +66,7 @@ export const KYOGIJO_TURN_STYLE = `
65
66
  .ky-turn .u-th{fill:var(--ky-cr-home)}
66
67
  .ky-turn .u-conc{fill:var(--ky-cr-conc)}
67
68
  .ky-turn .u-fascia{fill:var(--ky-up-fascia)}
69
+ .ky-turn .u-shell{fill:var(--ky-up-fascia);stroke:var(--ky-up-roofline);stroke-width:.6}
68
70
  .ky-turn .u-roof{fill:var(--ky-up-roof);stroke:var(--ky-up-roofline);stroke-width:.8;stroke-linejoin:round}
69
71
  .ky-turn .ky-up-seats svg{opacity:var(--ky-cr-dim)}
70
72
  .ky-turn .ky-cr-seat{fill:none;stroke:var(--ky-cr-empty);stroke-width:1.3;stroke-opacity:.65}
@@ -136,4 +138,5 @@ export const KYOGIJO_TURN_STYLE = `
136
138
  .ky-turn-controls button:disabled{opacity:.4;cursor:default}
137
139
  .ky-turn-help,.ky-turn-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
138
140
  .ky-turn-flat{margin:8px 0 0;font-size:.85rem}
141
+ ${KYOGIJO_TURN_BOARD_STYLE}
139
142
  `.trim();
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Background } from "./background.ts";
2
+ import type { BoardOptions } from "./overlay.ts";
2
3
  import type { EmblemRef, EmblemRenderer } from "./emblems-source.ts";
3
4
  import type { FlagRenderer } from "./flags-source.ts";
4
5
  import type { Layer } from "./layers.types.ts";
@@ -283,6 +284,14 @@ export interface CrowdOptions {
283
284
  * ballpark's outfield with a gap at the batter's eye) or `grandstand` (a ballpark's infield stands only). Each sport has its usual one.
284
285
  */
285
286
  shape?: CrowdShape;
287
+ /**
288
+ * The stands of the arena's real venue, for an NHL `arena` the package has one of (`VENUES`, `@johnmorrisdotca/kyogijo-stadiums/venues`): the outline of its building from OpenStreetMap, the rink lying along
289
+ * the building as it does in the world (a small arrow says where north is), and a bowl of the number of tiers and the seats the sources give, fitted inside the building, under its kind of roof. An arena with no venue
290
+ * is drawn with the usual stands. Off by default. The drawing needs the credit `VENUE_ATTRIBUTION`. See `docs/venues.md`.
291
+ */
292
+ real?: boolean;
293
+ /** `false` draws no seats: of a real venue (`real`) only its building and the arrow that says where north is, which is what the floor of the 3D view needs under the stands it raises. On by default. */
294
+ stands?: boolean;
286
295
  /** The end of a `horseshoe` that is open: `right` (the usual) or `left`. */
287
296
  open?: "left" | "right";
288
297
  /**
@@ -699,6 +708,12 @@ export interface SurfaceOptions {
699
708
  * Unset, the drawing is byte for byte as before. See `BackgroundOptions`.
700
709
  */
701
710
  background?: Background;
711
+ /**
712
+ * A board laid over the drawing: a results board, a bout presentation or a medal table that another package drew (Taisen Brackets', say), taken as an SVG string with its width and height, and placed at the top, a side, a corner or a point
713
+ * of your own, kept off the middle of the surface unless you say otherwise. It is written as an image, never changed and never stretched, and in the 3D view it is a layer over the view that does not turn with it. A compact strip and a
714
+ * badge take none. Unset, the drawing is byte for byte as before. See `BoardOptions` and `docs/boards.md`.
715
+ */
716
+ board?: BoardOptions;
702
717
  /**
703
718
  * The pageant: each team's flag or logo laid large over its half (a `carpet`) or on a round disc (a `stage`), with an emblem in the centre. `"carpet"`, `"stage"`, `true` (a carpet) or `PageantOptions`.
704
719
  * Off by default; a score drawing only. See `PageantOptions`.
package/dist/venue.js CHANGED
@@ -6,7 +6,8 @@ import { densityOf, resolveCrowd } from "./crowd.js";
6
6
  import { patterns } from "./crowd.draw.js";
7
7
  import { crowdStands } from "./draw.js";
8
8
  import { groundById } from "./grounds.js";
9
- import { clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
9
+ import { ARC_STEPS, clipAround, clipConvex, clipSegment, areaOf, pointList, raked, slopeOf, stripsOf, tidy } from "./venue.geom.js";
10
+ import { realBuilding } from "./venue.real.js";
10
11
  import { n, shy, wall } from "./turn.kit.js";
11
12
  export const NO_VENUE = { items: [], defs: "", reach: 0 };
12
13
  /** The sports played under a roof: their stands have a lid and not a canopy. */
@@ -45,7 +46,7 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
45
46
  const density = densityOf(crowd, stands.seats, groundById(o.ground)?.capacity);
46
47
  const tifo = { home: stands.sectors.some((s) => s.kind === "tifo-home"), away: stands.sectors.some((s) => s.kind === "tifo-away") };
47
48
  const pid = `${v.id}-v`;
48
- const defs = fine ? patterns(pid, crowd, density, tifo.home || tifo.away ? tifo : undefined, false, true) : "";
49
+ const defs = fine ? patterns(pid, crowd, density, tifo.home || tifo.away ? tifo : undefined, stands.real !== undefined, true) : "";
49
50
  const centre = [stands.box.x + stands.box.w / 2, stands.box.y + stands.box.h / 2];
50
51
  const covers = stands.covers;
51
52
  const inCover = (p) => covers.some(([x0, y0, x1, y1]) => p[0] >= x0 && p[0] <= x1 && p[1] >= y0 && p[1] <= y1);
@@ -148,8 +149,10 @@ export function standsVenueOf(stands, crowd, o, v, lift) {
148
149
  items.push({ id: "stands", kind: "stands", at: centre, height: topOf(seatPlanes), planes: seatPlanes });
149
150
  if (wallSeats.length > 0)
150
151
  items.push({ id: "wall-seats", kind: "wall-seats", at: centre, height: topOf(wallSeats), planes: wallSeats });
151
- // The roof: a canopy over the back of the top tier, or a lid over it in a hall.
152
- const top = v.roof === true && crowd.roof ? topRing(stands) : undefined;
152
+ // 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.
153
+ if (stands.real !== undefined && v.roof === true)
154
+ items.push(...realBuilding(stands.real, topRing(stands), centre, lift, ARC_STEPS[cut].oval * 2));
155
+ const top = v.roof === true && crowd.roof && stands.real === undefined ? topRing(stands) : undefined;
153
156
  if (top !== undefined) {
154
157
  const lid = INDOOR.includes(String(o.sport));
155
158
  const reach = lid ? 1 : ROOF.canopyReach;