@johnmorrisdotca/kyogijo-stadiums 0.7.0 → 0.8.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.
@@ -2,9 +2,12 @@
2
2
  // drawing's own options, in Node or in a page; `turn.markup.ts` writes it as markup and `turn.dom.ts` makes it turn.
3
3
  import { describe, drawKyogijo } from "./draw.js";
4
4
  import { hash } from "./format.js";
5
+ import { lengthUnit } from "./common.js";
6
+ import { convertLength } from "./units.js";
5
7
  import { KYOGIJO_STRINGS } from "./strings.js";
6
8
  import { TURN_DEFAULT, clampView, presetView } from "./turn.camera.js";
7
9
  import { describeUprights, planesOf, uprightsFor } from "./uprights.js";
10
+ import { resolveCrowd } from "./crowd.js";
8
11
  /** 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. */
9
12
  export const WORLD_PX = 1400;
10
13
  /** The options with what makes the floor of a 3D view: the whole surface, lying wide, at its full size. */
@@ -16,6 +19,18 @@ export function floorOptions(options) {
16
19
  return rest;
17
20
  }
18
21
  const VIEWBOX = /viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"/;
22
+ /** The kinds of thing the venue makes. */
23
+ const VENUE_KINDS = ["stands", "wall-seats", "roof", "gallery", "dais", "banner", "shrine", "mat-edge", "walls"];
24
+ /** 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. */
25
+ function lookOf(floor) {
26
+ const found = /^<svg[^>]*\sdata-sport="([\w-]+)" data-look="([\w-]+)"/.exec(floor);
27
+ return found === null ? undefined : { sport: found[1], look: found[2] };
28
+ }
29
+ /** The crowd's colours and the sides' that the floor drawing sets on its root, for the venue's patterns (which stand outside that SVG) to read. */
30
+ function crowdVars(floor) {
31
+ const style = /^<svg[^>]*\sstyle="([^"]*)"/.exec(floor)?.[1];
32
+ return style === undefined ? "" : style.split(";").filter((rule) => /^--ky-(cr-home|cr-away|left|right):/.test(rule)).join(";");
33
+ }
19
34
  /** The view a set of settings opens on. */
20
35
  export const openingView = (settings, sport) => clampView({ ...(presetView(settings.preset, sport) ?? presetView(TURN_DEFAULT, sport)), ...settings.view });
21
36
  /** Work out a 3D view from the options of a drawing. */
@@ -25,10 +40,22 @@ export function buildTurn(options, settings = {}) {
25
40
  const wanted = settings.uprights;
26
41
  const badge = asked.size === "badge";
27
42
  const upOptions = wanted === false || wanted === true || wanted === undefined ? {} : wanted;
28
- const uprights = wanted === false || badge ? { items: [], lift: 0, thin: 0.2 } : uprightsFor(asked, { ...upOptions, score: settings.score ?? upOptions.score });
43
+ const id = settings.id ?? (typeof options.id === "string" && options.id !== "" ? `${options.id}-turn` : `ky-turn-${hash(JSON.stringify([options, settings.uprights, settings.preset, settings.venue]))}`);
44
+ // The venue is on when the drawing has a crowd or is a dojo (and when `uprights` has not asked for the floor alone), unless it is turned off; `venue: true` raises it whatever else is asked.
45
+ const asks = settings.venue;
46
+ const venueOn = asks === false || badge ? false : asks === undefined ? wanted !== false && (resolveCrowd(asked.crowd) !== undefined || asked.sport === "dojo") : true;
47
+ const venue = venueOn ? { ...(typeof asks === "object" ? asks : {}), id } : undefined;
48
+ const score = settings.score ?? upOptions.score;
49
+ const uprights = badge ? { items: [], lift: 0, thin: 0.2 } : wanted === false ? (venue === undefined ? { items: [], lift: 0, thin: 0.2 } : uprightsFor(asked, { only: VENUE_KINDS, venue })) : uprightsFor(asked, { ...upOptions, score, venue });
29
50
  // The score hung up as a scoreboard is not on the floor too.
30
51
  const boarded = uprights.items.some((item) => item.kind === "scoreboard");
31
- const o = boarded ? { ...asked, plate: false, size: asked.size === "compact" ? "full" : asked.size } : asked;
52
+ // The stands are raised, so the floor draws no crowd: it is the surface with ground enough round it for the stands to stand on.
53
+ const crowdOpt = asked.crowd;
54
+ const reach = uprights.reach;
55
+ const margin = reach === undefined ? undefined : convertLength(reach, "ft", lengthUnit(asked));
56
+ const own = asked.margin;
57
+ 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;
58
+ const o = boarded ? { ...base, plate: false, size: asked.size === "compact" ? "full" : asked.size } : base;
32
59
  const floor = settings.floor === undefined ? drawKyogijo(o) : settings.floor(o);
33
60
  const found = VIEWBOX.exec(floor);
34
61
  if (found === null)
@@ -68,7 +95,6 @@ export function buildTurn(options, settings = {}) {
68
95
  const box = { x0: (x0 - centre[0]) * scale, x1: (x1 - centre[0]) * scale, y0: (y0 - centre[1]) * scale, y1: (y1 - centre[1]) * scale, z0: 0, z1: top * scale };
69
96
  const { title, description } = describe(o);
70
97
  const standing = describeUprights(o, uprights, language);
71
- const id = settings.id ?? (typeof options.id === "string" && options.id !== "" ? `${options.id}-turn` : `ky-turn-${hash(JSON.stringify([options, settings.uprights, settings.preset]))}`);
72
98
  return {
73
99
  id,
74
100
  language,
@@ -80,6 +106,11 @@ export function buildTurn(options, settings = {}) {
80
106
  viewBox: { x: vx, y: vy, w: vw, h: vh },
81
107
  scale,
82
108
  uprights,
109
+ defs: uprights.defs ?? "",
110
+ // 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.
111
+ crowdStyle: uprights.defs === undefined ? "" : crowdVars(drawKyogijo({ ...asked, size: "badge", style: "none" })),
112
+ venue: venue !== undefined,
113
+ look: lookOf(floor),
83
114
  planes,
84
115
  box,
85
116
  centre,
@@ -3,9 +3,9 @@
3
3
  * 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.
4
4
  */
5
5
  export const KYOGIJO_TURN_STYLE = `
6
- :where(.ky-turn){--ky-up-sbframe:#1a1f1c;--ky-up-sbedge:#5b645e;--ky-up-sbline:#59615b;--ky-up-pole:#e9e7de;--ky-up-white:#f6f4ec;--ky-up-yellow:#f2c230;--ky-up-red:#c8372d;--ky-up-orange:#f26a1b;--ky-up-wood:#e3d4b4;--ky-up-leg:#474e49;--ky-up-pad:#2f6db0;--ky-up-table:#18243f;--ky-up-foot:rgba(0,0,0,.25);--ky-up-ground:#d9d5c7;--ky-up-wall:#2f4a36;--ky-up-clay:#a8703f;--ky-up-cap:#1d2a22;--ky-up-board:#ece9df;--ky-up-glass:rgba(138,190,232,.26);--ky-up-backboard:rgba(255,255,255,.88);--ky-up-outline:#f2f0e6;--ky-up-rim:#e8591a;--ky-up-mesh:rgba(24,30,27,.7);--ky-up-netfill:rgba(255,255,255,.2);--ky-up-tape:#fbfaf4;--ky-turn-bg:transparent;--ky-turn-ink:#1f2320;--ky-turn-rule:rgba(31,35,32,.22);--ky-turn-chip:rgba(255,255,255,.86);--ky-turn-chip-on:#1f2320;--ky-turn-chip-on-ink:#fff}
7
- @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-turn:not([data-theme=light])){--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320}}
8
- :where(html[data-theme=dark] .ky-turn,.ky-turn[data-theme=dark]){--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320}
6
+ :where(.ky-turn){--ky-left:#2f6db0;--ky-right:#b5452c;--ky-cr-home:#b5452c;--ky-cr-away:#2f6db0;--ky-cr-bed:#4a525d;--ky-cr-empty:#7d8794;--ky-cr-gap:#14181d;--ky-cr-conc:#8c929b;--ky-cr-dim:1;--ky-up-roof:rgba(214,218,212,.6);--ky-up-roofline:rgba(40,46,42,.55);--ky-up-fascia:#9aa09a;--ky-up-door:#8a6a46;--ky-up-window:rgba(150,200,240,.55);--ky-up-sash:#f2f0e6;--ky-up-banner:#26211d;--ky-up-bannerline:#c8b88a;--ky-up-room:#efe8d6;--ky-up-roomback:#d6cdb9;--ky-up-ceiling:#dedcd5;--ky-up-railw:#fbfaf6;--ky-up-wainline:#6b4a2a;--ky-up-riser:#3f342c;--ky-up-slab:#d8cfba;--ky-up-slabedge:#c2b69c;--ky-up-seatc:#46342a;--ky-up-seatb:#2d211b;--ky-up-dais:#a0683a;--ky-up-dais-side:#6a4326;--ky-up-shrine:#c9b48f;--ky-up-shrine-side:#8f7a55;--ky-up-gal:#4d4037;--ky-up-aisle:#dcc196;--ky-up-panel:rgba(95,58,22,.3);--ky-up-alcove:#b58a52;--ky-up-purple:#5b2a6e;--ky-up-clock:#ffffff;--ky-up-rail:#3a2c1f;--ky-up-matedge:#2a2a26;--ky-up-sbframe:#1a1f1c;--ky-up-sbedge:#5b645e;--ky-up-sbline:#59615b;--ky-up-pole:#e9e7de;--ky-up-white:#f6f4ec;--ky-up-yellow:#f2c230;--ky-up-red:#c8372d;--ky-up-orange:#f26a1b;--ky-up-wood:#e3d4b4;--ky-up-leg:#474e49;--ky-up-pad:#2f6db0;--ky-up-table:#18243f;--ky-up-foot:rgba(0,0,0,.25);--ky-up-ground:#d9d5c7;--ky-up-wall:#2f4a36;--ky-up-clay:#a8703f;--ky-up-cap:#1d2a22;--ky-up-board:#ece9df;--ky-up-glass:rgba(138,190,232,.26);--ky-up-backboard:rgba(255,255,255,.88);--ky-up-outline:#f2f0e6;--ky-up-rim:#e8591a;--ky-up-mesh:rgba(24,30,27,.7);--ky-up-netfill:rgba(255,255,255,.2);--ky-up-tape:#fbfaf4;--ky-turn-bg:transparent;--ky-turn-ink:#1f2320;--ky-turn-rule:rgba(31,35,32,.22);--ky-turn-chip:rgba(255,255,255,.86);--ky-turn-chip-on:#1f2320;--ky-turn-chip-on-ink:#fff}
7
+ @media (prefers-color-scheme:dark){:where(html:not([data-theme=light]) .ky-turn:not([data-theme=light])){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320}}
8
+ :where(html[data-theme=dark] .ky-turn,.ky-turn[data-theme=dark]){--ky-cr-bed:#242a32;--ky-cr-empty:#4a535f;--ky-cr-gap:#050709;--ky-cr-conc:#2a3038;--ky-cr-dim:.82;--ky-up-roof:rgba(60,66,62,.6);--ky-up-roofline:rgba(200,206,200,.5);--ky-up-fascia:#505650;--ky-up-door:#6a4f33;--ky-up-gal:#2d2621;--ky-up-aisle:#8f7752;--ky-up-alcove:#7d5c36;--ky-up-purple:#43204f;--ky-up-clock:#cfcbc0;--ky-up-room:#6d675a;--ky-up-roomback:#58534a;--ky-up-ceiling:#8c8a82;--ky-up-railw:#d9d6cc;--ky-up-riser:#251f1b;--ky-up-slab:#6b6455;--ky-up-slabedge:#585143;--ky-up-seatc:#2f231c;--ky-up-seatb:#1c1411;--ky-up-dais:#7d5230;--ky-up-dais-side:#52361f;--ky-up-sbframe:#0c0f0d;--ky-up-sbedge:#7a847d;--ky-up-sbline:#8a928c;--ky-up-pole:#d6d4ca;--ky-up-white:#e8e6dc;--ky-up-wood:#a89873;--ky-up-leg:#8a928c;--ky-up-ground:#242823;--ky-up-wall:#4d6b55;--ky-up-clay:#7d5231;--ky-up-cap:#c9d2cc;--ky-up-board:#8d928b;--ky-up-glass:rgba(150,200,240,.2);--ky-up-backboard:rgba(255,255,255,.34);--ky-up-outline:#e7e4d8;--ky-up-mesh:rgba(236,236,226,.62);--ky-up-netfill:rgba(255,255,255,.08);--ky-up-tape:#efede2;--ky-turn-ink:#ece8dc;--ky-turn-rule:rgba(236,232,220,.28);--ky-turn-chip:rgba(24,28,25,.82);--ky-turn-chip-on:#ece8dc;--ky-turn-chip-on-ink:#1f2320}
9
9
  .ky-turn{position:relative;display:block;width:100%;color:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);background:var(--ky-turn-bg)}
10
10
  .ky-turn-view{position:relative;isolation:isolate;width:100%;aspect-ratio:var(--ky-turn-ratio,16/10);min-height:260px;overflow:hidden;touch-action:pan-y;cursor:grab;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;border-radius:12px;outline-offset:2px}
11
11
  .ky-turn[data-touch=turn] .ky-turn-view{touch-action:none}
@@ -44,11 +44,65 @@ export const KYOGIJO_TURN_STYLE = `
44
44
  .ky-turn .u-flag{fill:var(--ky-up-orange)}
45
45
  .ky-turn .u-frame-white{stroke:var(--ky-up-white)}
46
46
  .ky-turn .u-frame-red{stroke:var(--ky-up-red)}
47
+ .ky-turn-defs{position:absolute;width:0;height:0;overflow:hidden}
48
+ .ky-turn .u-bed{fill:var(--ky-cr-bed)}
49
+ .ky-turn .u-blk{fill:var(--ky-cr-home)}
50
+ .ky-turn .u-blk-a,.ky-turn .u-ta{fill:var(--ky-cr-away)}
51
+ .ky-turn .u-th{fill:var(--ky-cr-home)}
52
+ .ky-turn .u-conc{fill:var(--ky-cr-conc)}
53
+ .ky-turn .u-fascia{fill:var(--ky-up-fascia)}
54
+ .ky-turn .u-roof{fill:var(--ky-up-roof);stroke:var(--ky-up-roofline);stroke-width:.8;stroke-linejoin:round}
55
+ .ky-turn .ky-up-seats svg{opacity:var(--ky-cr-dim)}
56
+ .ky-turn .ky-cr-seat{fill:none;stroke:var(--ky-cr-empty);stroke-width:1.3;stroke-opacity:.65}
57
+ .ky-turn .ky-cr-h{stroke:var(--ky-cr-home)}
58
+ .ky-turn .ky-cr-a{stroke:var(--ky-cr-away)}
59
+ .ky-turn .ky-cr-h-d{stroke:var(--ky-cr-home);stroke-opacity:.6}
60
+ .ky-turn .ky-cr-a-d{stroke:var(--ky-cr-away);stroke-opacity:.6}
61
+ .ky-turn .ky-cr-th{fill:var(--ky-cr-home)}
62
+ .ky-turn .ky-cr-ta{fill:var(--ky-cr-away)}
63
+ .ky-turn .ky-cr-tw{fill:#f2f0ea}
64
+ .ky-turn .ky-cr-aisle{fill:none;stroke:var(--ky-cr-gap);stroke-opacity:.8}
65
+ .ky-turn .ky-cr-row{fill:none;stroke:var(--ky-cr-gap);stroke-opacity:.5;stroke-width:.5}
66
+ .ky-turn .ky-bud-gseat{stroke:var(--ky-up-rail);stroke-opacity:.3}
67
+ .ky-turn .u-door{fill:var(--ky-up-door)}
68
+ .ky-turn .u-window{fill:var(--ky-up-window)}
69
+ .ky-turn .u-sash{stroke:var(--ky-up-sash);stroke-width:.25}
70
+ .ky-turn .u-banner{fill:var(--ky-up-banner)}
71
+ .ky-turn .u-bannerline{stroke:var(--ky-up-bannerline);stroke-width:.18}
72
+ .ky-turn .u-room{fill:var(--ky-up-room)}
73
+ .ky-turn .u-room.u-room-back{fill:var(--ky-up-roomback)}
74
+ .ky-turn .u-ceiling{fill:var(--ky-up-ceiling);fill-opacity:.9}
75
+ .ky-turn .u-wainscot{fill:var(--ky-bud-floor,#b98f5c)}
76
+ .ky-turn .u-wainline{stroke:var(--ky-up-wainline);stroke-width:.12}
77
+ .ky-turn .u-gbed.u-riser{fill:var(--ky-up-riser)}
78
+ .ky-turn .u-aisle{fill:var(--ky-up-aisle)}
79
+ .ky-turn .u-panel{stroke:var(--ky-up-panel);stroke-width:.1}
80
+ .ky-turn .u-alcove{fill:var(--ky-up-alcove)}
81
+ .ky-turn .u-frame{stroke:#f6f3ea;stroke-width:.3}
82
+ .ky-turn .u-shrinef{fill:#2b2018;stroke:#f6f3ea;stroke-width:.25}
83
+ .ky-turn .u-scroll{fill:#efe6cf;stroke:#8a6f45;stroke-width:.08}
84
+ .ky-turn .u-purple{fill:var(--ky-up-purple)}
85
+ .ky-turn .u-clockface{fill:var(--ky-up-clock);stroke:#3f3c36;stroke-width:.35}
86
+ .ky-turn .u-clockhands{stroke:#2a2a28;stroke-width:.3;stroke-linecap:round}
87
+ .ky-turn .u-light{fill:#fffdf3;stroke:#b9b7b0;stroke-width:.08}
88
+ .ky-turn .u-dais{fill:var(--ky-up-dais);stroke:#f2e6c8;stroke-width:.12}
89
+ .ky-turn .u-dais-side{fill:var(--ky-up-dais-side);stroke:#f2e6c8;stroke-width:.08}
90
+ .ky-turn .u-shrine{fill:var(--ky-up-shrine)}
91
+ .ky-turn .u-shrine-side{fill:var(--ky-up-shrine-side)}
92
+ .ky-turn .u-gbed{fill:var(--ky-up-gal)}
93
+ .ky-turn .u-slab{fill:var(--ky-up-slab)}
94
+ .ky-turn .u-slabedge{fill:var(--ky-up-slabedge)}
95
+ .ky-turn .u-seatcush{fill:var(--ky-up-seatc)}
96
+ .ky-turn .u-seatback{fill:var(--ky-up-seatb)}
97
+ .ky-turn .u-railline{stroke:var(--ky-up-railw);stroke-width:.3;stroke-linecap:square}
98
+ .ky-turn .u-matedge{fill:var(--ky-up-matedge)}
47
99
  .ky-turn-controls{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;align-items:center}
48
100
  .ky-turn-group{display:flex;flex-wrap:wrap;gap:6px}
49
101
  .ky-turn-controls button{font:inherit;font-size:.9rem;font-weight:600;min-width:44px;min-height:44px;padding:0 12px;border-radius:999px;border:1px solid var(--ky-turn-rule);background:var(--ky-turn-chip);color:var(--ky-turn-ink);cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
50
102
  .ky-turn-controls button[aria-pressed=true]{background:var(--ky-turn-chip-on);color:var(--ky-turn-chip-on-ink);border-color:var(--ky-turn-chip-on)}
51
- .ky-turn-controls button[data-act]:not([data-act=reset]){font-size:1.2rem}
103
+ .ky-turn-controls button[data-act]:not([data-act=reset]):not([data-act=play]){font-size:1.2rem}
104
+ .ky-turn-controls button[data-act=play]{gap:.45em;padding:0 16px}
105
+ .ky-turn-controls button[data-act=play] [data-glyph]{font-size:.8em;letter-spacing:-.1em}
52
106
  .ky-turn-controls button:focus-visible{outline:3px solid var(--ky-turn-ink);outline-offset:2px}
53
107
  .ky-turn-controls button:disabled{opacity:.4;cursor:default}
54
108
  .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}
@@ -1,5 +1,6 @@
1
1
  import { type Plane, type Upright, type UprightKind } from "./turn.kit.ts";
2
2
  import { type Unit } from "./units.ts";
3
+ import { type VenueBuild } from "./venue.ts";
3
4
  import type { Language, SurfaceOptions } from "./types.ts";
4
5
  /** What can be asked of the uprights. */
5
6
  export interface UprightOptions {
@@ -11,6 +12,8 @@ export interface UprightOptions {
11
12
  boards?: boolean;
12
13
  /** Only these kinds of thing; every kind the surface has when missing. */
13
14
  only?: readonly UprightKind[];
15
+ /** The venue round the surface: the stands of its crowd raised, their roof if asked, a dojo's walls with their doors and windows, its dais, banner, balconies and the edge of its mats. Off here when missing: `buildTurn` turns it on for a drawing with a crowd or a dojo. `id` is the prefix of the ids of the patterns it uses. */
16
+ venue?: VenueBuild;
14
17
  }
15
18
  /** What stands on a surface. */
16
19
  export interface Uprights {
@@ -26,6 +29,10 @@ export interface Uprights {
26
29
  x1: number;
27
30
  y1: number;
28
31
  };
32
+ /** The patterns the venue's planes are drawn with (the contents of an SVG `defs`), when it has any. */
33
+ defs?: string;
34
+ /** How far the venue reaches beyond the surface, in feet, when it has one: the ground a floor needs for it to stand on. */
35
+ reach?: number;
29
36
  }
30
37
  /** All the planes of all the things. */
31
38
  export declare const planesOf: (u: Pick<Uprights, "items">) => Plane[];
package/dist/uprights.js CHANGED
@@ -27,6 +27,8 @@ import { isBudoSport } from "./budo/plan.js";
27
27
  import { courtUprights, rinkUprights } from "./uprights.rinks.js";
28
28
  import { convertLength } from "./units.js";
29
29
  import { sheetFor } from "./sheet.js";
30
+ import { roomVenue } from "./venue.room.js";
31
+ import { NO_VENUE, standsVenue } from "./venue.js";
30
32
  /** All the planes of all the things. */
31
33
  export const planesOf = (u) => u.items.flatMap((item) => item.planes);
32
34
  /** The unit each sport's rulebook gives its heights in, when the options name none. */
@@ -122,6 +124,18 @@ export function uprightsFor(o, settings = {}) {
122
124
  thin = thinnest(sheet.length, sheet.width);
123
125
  area = { x0: 0, y0: 0, x1: sheet.length, y1: sheet.width };
124
126
  }
127
+ // The venue: the stands and the room, raised. A dojo's own walls are the room's, with their doors and windows.
128
+ let defs = "";
129
+ let reach = 0;
130
+ if (settings.venue !== undefined) {
131
+ const stands = standsVenue(o, settings.venue, lift);
132
+ const room = isBudoSport(sport) ? roomVenue({ ...o, sport }, settings.venue, lift) : NO_VENUE;
133
+ if (room.items.some((item) => item.kind === "walls"))
134
+ items = items.filter((item) => item.kind !== "walls");
135
+ items.push(...stands.items, ...room.items);
136
+ defs = stands.defs + room.defs;
137
+ reach = Math.max(stands.reach, room.reach);
138
+ }
125
139
  // The score, hung up: the badge on a board that turns to face the eye.
126
140
  const scored = o.home !== undefined && o.away !== undefined && o.size !== "badge";
127
141
  if (scored && settings.score !== "floor" && area !== undefined) {
@@ -131,7 +145,7 @@ export function uprightsFor(o, settings = {}) {
131
145
  }
132
146
  if (settings.only !== undefined)
133
147
  items = items.filter((item) => settings.only.includes(item.kind));
134
- return { items, lift, thin, area };
148
+ return { items, lift, thin, area, ...(defs === "" ? {} : { defs }), ...(reach === 0 ? {} : { reach }) };
135
149
  }
136
150
  /** A length in feet as a rulebook would print it in `unit`: metres to the centimetre, feet to a tenth. */
137
151
  function said(feet, unit, language) {
@@ -0,0 +1,31 @@
1
+ import { type Cut } from "./venue.geom.ts";
2
+ import { type Upright } from "./turn.kit.ts";
3
+ import type { SurfaceOptions } from "./types.ts";
4
+ /** What can be asked of the venue. */
5
+ export interface VenueSettings {
6
+ /** 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. */
7
+ roof?: boolean;
8
+ /** 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. */
9
+ detail?: Cut;
10
+ }
11
+ /** The settings a venue is built from: the asker's, and the prefix of the ids of what it draws, which the view gives. */
12
+ export interface VenueBuild extends VenueSettings {
13
+ id: string;
14
+ }
15
+ /** What the venue is made of: the things that stand, and the patterns they are drawn with (the contents of a `defs`). */
16
+ export interface Venue {
17
+ items: Upright[];
18
+ defs: string;
19
+ /** How far the venue reaches beyond the surface's box on its widest side, in feet: the ground the floor needs for it to stand on. 0 for none. */
20
+ reach: number;
21
+ }
22
+ export declare const NO_VENUE: Venue;
23
+ /** How far the roof stands over the top row at its edge, in feet (ours: no rule gives it), and how far it reaches over the seats. */
24
+ export declare const ROOF: {
25
+ readonly clearance: 26;
26
+ readonly canopyReach: 0.62;
27
+ readonly canopyDrop: 6;
28
+ readonly lidRise: 26;
29
+ };
30
+ /** The stands of a surface as planes: `undefined`-free, an empty venue when the options have no crowd (or the crowd is a hall's gallery, which `venue.room.ts` raises). */
31
+ export declare function standsVenue(o: SurfaceOptions, v: VenueBuild, lift: number): Venue;
@@ -0,0 +1,53 @@
1
+ import type { Outline, Pt } from "./crowd.ts";
2
+ import { type Plane } from "./turn.kit.ts";
3
+ /** How finely the curves of a ring are cut: facets to a quarter of a rounded corner, or round an oval. */
4
+ export type Cut = "low" | "auto" | "high";
5
+ /** A band of the stands as two polylines of one length, the inner edge and the outer: quad `i` is `inner[i]`, `inner[i + 1]`, `outer[i + 1]`, `outer[i]`. */
6
+ export interface Strip {
7
+ inner: Pt[];
8
+ outer: Pt[];
9
+ }
10
+ /** Facets to a quarter circle of a rounded rectangle, and to a whole oval, at each cut. */
11
+ export declare const ARC_STEPS: Readonly<Record<Cut, {
12
+ corner: number;
13
+ oval: number;
14
+ poly: number;
15
+ }>>;
16
+ /** The strips of one tier of the stands: its band between `hole` and `region`, with the pieces that are apart from it. */
17
+ export declare function stripsOf(region: Outline, hole: Outline | null, more: readonly Outline[], cut: Cut): Strip[];
18
+ /** The strip a ring's concourse is: from one outline to another. */
19
+ export declare const stripBetween: (inner: Outline, outer: Outline, cut: Cut) => Strip[];
20
+ /** A polygon without its repeated neighbours (and its closing point, if given). */
21
+ export declare function tidy(points: readonly Pt[]): Pt[];
22
+ /** The area of a polygon, signed: positive going clockwise on a page with y down. */
23
+ export declare function areaOf(points: readonly Pt[]): number;
24
+ /** A polygon cut to the inside of a convex polygon (either way round). */
25
+ export declare function clipConvex(subject: readonly Pt[], clip: readonly Pt[]): Pt[];
26
+ /** A polygon with a box `[x0, y0, x1, y1]` taken out of it: the pieces left (left of it, right of it, and above and below it between), which are convex when the polygon is. */
27
+ export declare function clipOutside(poly: readonly Pt[], box: readonly [number, number, number, number]): Pt[][];
28
+ /** A polygon with every box taken out of it. */
29
+ export declare function clipAround(poly: readonly Pt[], boxes: readonly (readonly [number, number, number, number])[]): Pt[][];
30
+ /** A segment cut to the inside of a convex polygon (Cyrus and Beck), or `undefined` when none of it is. */
31
+ export declare function clipSegment(a: Pt, b: Pt, clip: readonly Pt[]): [Pt, Pt] | undefined;
32
+ /** How the height of a raked plane changes with place: `z = z0 + gx (x - x0) + gy (y - y0)`. */
33
+ export interface Slope {
34
+ x0: number;
35
+ y0: number;
36
+ z0: number;
37
+ gx: number;
38
+ gy: number;
39
+ }
40
+ export declare const heightAt: (s: Slope, x: number, y: number) => number;
41
+ /** The slope of a quad of a band: the height `h0` along its inner edge, `h1` along its outer, rising across it. `undefined` for a quad with no width. */
42
+ export declare function slopeOf(q: readonly [Pt, Pt, Pt, Pt], h0: number, h1: number): {
43
+ slope: Slope;
44
+ out: Pt;
45
+ along: Pt;
46
+ } | undefined;
47
+ /**
48
+ * A planar polygon as one plane: its corner and its two sides are in the polygon's own plane, its `u` along the horizontal direction `along` (so that a texture's rows run along a band), its `v` across.
49
+ * `paint` is given the polygon's corners in the plane's own feet (and a function that puts any plan point there) and answers the SVG drawn on it.
50
+ */
51
+ export declare function raked(id: string, kind: Plane["kind"], poly: readonly Pt[], slope: Slope, along: Pt, paint: (corners: Pt[], local: (p: Pt) => Pt) => string): Plane | undefined;
52
+ /** Points as the `points` attribute of a polygon. */
53
+ export declare const pointList: (pts: readonly Pt[]) => string;
@@ -0,0 +1,225 @@
1
+ import { n, plane } from "./turn.kit.js";
2
+ /** Facets to a quarter circle of a rounded rectangle, and to a whole oval, at each cut. */
3
+ export const ARC_STEPS = { low: { corner: 1, oval: 12, poly: 12 }, auto: { corner: 3, oval: 28, poly: 28 }, high: { corner: 6, oval: 56, poly: 56 } };
4
+ /** A rounded rectangle as a closed loop of points: `steps + 1` on each of its four corners, going clockwise as the page shows it from the left end of the top. */
5
+ function rrectLoop(o, steps) {
6
+ const r = Math.min(o.r, (o.x1 - o.x0) / 2, (o.y1 - o.y0) / 2);
7
+ const corners = [
8
+ [o.x0 + r, o.y0 + r, 180],
9
+ [o.x1 - r, o.y0 + r, 270],
10
+ [o.x1 - r, o.y1 - r, 0],
11
+ [o.x0 + r, o.y1 - r, 90],
12
+ ];
13
+ const out = [];
14
+ for (const [cx, cy, a0] of corners) {
15
+ for (let i = 0; i <= steps; i += 1) {
16
+ const a = ((a0 + (90 * i) / steps) * Math.PI) / 180;
17
+ out.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
18
+ }
19
+ }
20
+ out.push(out[0]);
21
+ return out;
22
+ }
23
+ /** An oval as a closed loop of `count` points and the first again. */
24
+ function ovalLoop(o, count) {
25
+ const out = [];
26
+ for (let i = 0; i <= count; i += 1) {
27
+ const a = (2 * Math.PI * (i % count)) / count;
28
+ out.push([o.cx + o.rx * Math.cos(a), o.cy + o.ry * Math.sin(a)]);
29
+ }
30
+ return out;
31
+ }
32
+ /** The points of a polygon made by `band()`: the first half is the inner edge, the second the outer edge back again. Cut to at most `most` quads. */
33
+ function polyStrip(points, most) {
34
+ const m = points.length / 2;
35
+ const inner = points.slice(0, m);
36
+ const outer = points.slice(m).reverse();
37
+ const every = Math.max(1, Math.ceil((m - 1) / most));
38
+ const keep = [];
39
+ for (let i = 0; i < m - 1; i += every)
40
+ keep.push(i);
41
+ keep.push(m - 1);
42
+ return { inner: keep.map((i) => inner[i]), outer: keep.map((i) => outer[i]) };
43
+ }
44
+ /** The strips of one tier of the stands: its band between `hole` and `region`, with the pieces that are apart from it. */
45
+ export function stripsOf(region, hole, more, cut) {
46
+ const steps = ARC_STEPS[cut];
47
+ if (region.kind === "poly")
48
+ return [region, ...more].flatMap((piece) => (piece.kind === "poly" && piece.points.length >= 4 ? [polyStrip(piece.points, steps.poly)] : []));
49
+ if (hole === null)
50
+ return [];
51
+ if (region.kind === "rrect" && hole.kind === "rrect")
52
+ return [{ inner: rrectLoop(hole, steps.corner), outer: rrectLoop(region, steps.corner) }];
53
+ if (region.kind === "ellipse" && hole.kind === "ellipse")
54
+ return [{ inner: ovalLoop(hole, steps.oval), outer: ovalLoop(region, steps.oval) }];
55
+ return [];
56
+ }
57
+ /** The strip a ring's concourse is: from one outline to another. */
58
+ export const stripBetween = (inner, outer, cut) => stripsOf(outer, inner, [], cut);
59
+ // ---- Polygons. ----
60
+ /** A polygon without its repeated neighbours (and its closing point, if given). */
61
+ export function tidy(points) {
62
+ const out = [];
63
+ for (const p of points) {
64
+ const last = out[out.length - 1];
65
+ if (last === undefined || Math.hypot(p[0] - last[0], p[1] - last[1]) > 1e-6)
66
+ out.push([p[0], p[1]]);
67
+ }
68
+ while (out.length > 1 && Math.hypot(out[0][0] - out[out.length - 1][0], out[0][1] - out[out.length - 1][1]) <= 1e-6)
69
+ out.pop();
70
+ return out;
71
+ }
72
+ /** The area of a polygon, signed: positive going clockwise on a page with y down. */
73
+ export function areaOf(points) {
74
+ let s = 0;
75
+ points.forEach(([x, y], i) => {
76
+ const [x2, y2] = points[(i + 1) % points.length];
77
+ s += x * y2 - x2 * y;
78
+ });
79
+ return s / 2;
80
+ }
81
+ /** A polygon cut to one side of a line `a x + b y <= c` (Sutherland and Hodgman). */
82
+ function halfPlane(poly, a, b, c) {
83
+ const out = [];
84
+ poly.forEach((p, i) => {
85
+ const q = poly[(i + 1) % poly.length];
86
+ const dp = a * p[0] + b * p[1] - c;
87
+ const dq = a * q[0] + b * q[1] - c;
88
+ if (dp <= 0)
89
+ out.push(p);
90
+ if ((dp < 0 && dq > 0) || (dp > 0 && dq < 0)) {
91
+ const t = dp / (dp - dq);
92
+ out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]);
93
+ }
94
+ });
95
+ return out;
96
+ }
97
+ /** A polygon cut to the inside of a convex polygon (either way round). */
98
+ export function clipConvex(subject, clip) {
99
+ const sign = areaOf(clip) >= 0 ? 1 : -1;
100
+ let out = [...subject];
101
+ clip.forEach((p, i) => {
102
+ if (out.length === 0)
103
+ return;
104
+ const q = clip[(i + 1) % clip.length];
105
+ // The inside is to the right of the edge on a page with y down for a clockwise polygon: the half-plane keeps points where the cross product has the polygon's sign.
106
+ const ex = q[0] - p[0];
107
+ const ey = q[1] - p[1];
108
+ out = halfPlane(out, sign * ey, -sign * ex, sign * (ey * p[0] - ex * p[1]));
109
+ });
110
+ return out;
111
+ }
112
+ /** A polygon with a box `[x0, y0, x1, y1]` taken out of it: the pieces left (left of it, right of it, and above and below it between), which are convex when the polygon is. */
113
+ export function clipOutside(poly, box) {
114
+ const [x0, y0, x1, y1] = box;
115
+ const middle = halfPlane(halfPlane(poly, -1, 0, -x0), 1, 0, x1);
116
+ const pieces = [halfPlane(poly, 1, 0, x0), halfPlane(poly, -1, 0, -x1), halfPlane(middle, 0, 1, y0), halfPlane(middle, 0, -1, -y1)];
117
+ return pieces.map((p) => tidy(p)).filter((p) => p.length >= 3 && Math.abs(areaOf(p)) > 0.05);
118
+ }
119
+ /** A polygon with every box taken out of it. */
120
+ export function clipAround(poly, boxes) {
121
+ let pieces = [tidy(poly)];
122
+ for (const box of boxes)
123
+ pieces = pieces.flatMap((p) => clipOutside(p, box));
124
+ return pieces;
125
+ }
126
+ /** A segment cut to the inside of a convex polygon (Cyrus and Beck), or `undefined` when none of it is. */
127
+ export function clipSegment(a, b, clip) {
128
+ const sign = areaOf(clip) >= 0 ? 1 : -1;
129
+ let t0 = 0;
130
+ let t1 = 1;
131
+ const dx = b[0] - a[0];
132
+ const dy = b[1] - a[1];
133
+ for (let i = 0; i < clip.length; i += 1) {
134
+ const p = clip[i];
135
+ const q = clip[(i + 1) % clip.length];
136
+ const ex = q[0] - p[0];
137
+ const ey = q[1] - p[1];
138
+ // The inside has `sign * (ex * (y - p.y) - ey * (x - p.x)) >= 0`.
139
+ const num = sign * (ex * (a[1] - p[1]) - ey * (a[0] - p[0]));
140
+ const den = sign * (ex * dy - ey * dx);
141
+ if (Math.abs(den) < 1e-12) {
142
+ if (num < -1e-9)
143
+ return undefined;
144
+ continue;
145
+ }
146
+ const t = -num / den;
147
+ if (den > 0)
148
+ t0 = Math.max(t0, t);
149
+ else
150
+ t1 = Math.min(t1, t);
151
+ if (t0 > t1)
152
+ return undefined;
153
+ }
154
+ return [[a[0] + dx * t0, a[1] + dy * t0], [a[0] + dx * t1, a[1] + dy * t1]];
155
+ }
156
+ export const heightAt = (s, x, y) => s.z0 + s.gx * (x - s.x0) + s.gy * (y - s.y0);
157
+ /** The slope of a quad of a band: the height `h0` along its inner edge, `h1` along its outer, rising across it. `undefined` for a quad with no width. */
158
+ export function slopeOf(q, h0, h1) {
159
+ const [i0, i1, o1, o0] = q;
160
+ // The edge that runs along the band: the inner, or the outer where the inner has shrunk to a point.
161
+ const longIn = Math.hypot(i1[0] - i0[0], i1[1] - i0[1]);
162
+ const longOut = Math.hypot(o1[0] - o0[0], o1[1] - o0[1]);
163
+ if (longIn < 1e-6 && longOut < 1e-6)
164
+ return undefined;
165
+ const [from, to, far] = longIn >= longOut ? [i0, i1, o0] : [o0, o1, i0];
166
+ const ex = (to[0] - from[0]) / Math.hypot(to[0] - from[0], to[1] - from[1]);
167
+ const ey = (to[1] - from[1]) / Math.hypot(to[0] - from[0], to[1] - from[1]);
168
+ // The way across the band, away from the inner edge.
169
+ let nx = -ey;
170
+ let ny = ex;
171
+ const mid = (a, b) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
172
+ const mi = mid(i0, i1);
173
+ const mo = mid(o0, o1);
174
+ if (nx * (mo[0] - mi[0]) + ny * (mo[1] - mi[1]) < 0) {
175
+ nx = -nx;
176
+ ny = -ny;
177
+ }
178
+ const across = Math.abs(nx * (far[0] - from[0]) + ny * (far[1] - from[1]));
179
+ if (across < 1e-6)
180
+ return undefined;
181
+ // `from` is on the inner edge when it came from there, else on the outer one.
182
+ const z0 = longIn >= longOut ? h0 : h1;
183
+ const slope = { x0: from[0], y0: from[1], z0, gx: (nx * (h1 - h0)) / across, gy: (ny * (h1 - h0)) / across };
184
+ return { slope, out: [nx, ny], along: [ex, ey] };
185
+ }
186
+ /**
187
+ * A planar polygon as one plane: its corner and its two sides are in the polygon's own plane, its `u` along the horizontal direction `along` (so that a texture's rows run along a band), its `v` across.
188
+ * `paint` is given the polygon's corners in the plane's own feet (and a function that puts any plan point there) and answers the SVG drawn on it.
189
+ */
190
+ export function raked(id, kind, poly, slope, along, paint) {
191
+ if (poly.length < 3)
192
+ return undefined;
193
+ const at = (p) => [p[0], p[1], heightAt(slope, p[0], p[1])];
194
+ const ud = [along[0], along[1], slope.gx * along[0] + slope.gy * along[1]];
195
+ const ul = Math.hypot(...ud);
196
+ const uh = [ud[0] / ul, ud[1] / ul, ud[2] / ul];
197
+ // The way up the slope: perpendicular to `u` in the polygon's plane.
198
+ const across = [-along[1], along[0], slope.gx * -along[1] + slope.gy * along[0]];
199
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
200
+ const va = dot(across, uh);
201
+ const vperp = [across[0] - uh[0] * va, across[1] - uh[1] * va, across[2] - uh[2] * va];
202
+ const vl = Math.hypot(...vperp);
203
+ if (vl < 1e-9)
204
+ return undefined;
205
+ const vh = [vperp[0] / vl, vperp[1] / vl, vperp[2] / vl];
206
+ const base = at(poly[0]);
207
+ const loc = (p) => {
208
+ const q = at(p);
209
+ const d = [q[0] - base[0], q[1] - base[1], q[2] - base[2]];
210
+ return [dot(d, uh), dot(d, vh)];
211
+ };
212
+ const pts = poly.map(loc);
213
+ const a0 = Math.min(...pts.map((p) => p[0]));
214
+ const a1 = Math.max(...pts.map((p) => p[0]));
215
+ const b0 = Math.min(...pts.map((p) => p[1]));
216
+ const b1 = Math.max(...pts.map((p) => p[1]));
217
+ const o = [base[0] + uh[0] * a0 + vh[0] * b0, base[1] + uh[1] * a0 + vh[1] * b0, base[2] + uh[2] * a0 + vh[2] * b0];
218
+ const shifted = (p) => {
219
+ const [a, b] = loc(p);
220
+ return [a - a0, b - b0];
221
+ };
222
+ return plane(id, kind, o, [uh[0] * (a1 - a0), uh[1] * (a1 - a0), uh[2] * (a1 - a0)], [vh[0] * (b1 - b0), vh[1] * (b1 - b0), vh[2] * (b1 - b0)], paint(pts.map(([a, b]) => [a - a0, b - b0]), shifted));
223
+ }
224
+ /** Points as the `points` attribute of a polygon. */
225
+ export const pointList = (pts) => pts.map(([x, y]) => `${n(x)},${n(y)}`).join(" ");