@johnmorrisdotca/kyogijo-stadiums 0.44.2 → 0.45.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 +47 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.js +17 -0
- package/dist/district.turn.js +61 -6
- package/dist/judging.mount.js +7 -0
- package/dist/outside.dom.d.ts +5 -0
- package/dist/outside.dom.js +13 -5
- package/dist/quality.d.ts +110 -0
- package/dist/quality.js +160 -0
- package/dist/turn-entry.d.ts +2 -0
- package/dist/turn-entry.js +1 -0
- package/dist/turn.dom.d.ts +7 -0
- package/dist/turn.dom.js +186 -20
- package/dist/turn.markup.d.ts +10 -7
- package/dist/turn.markup.js +28 -15
- package/dist/turn.model.d.ts +13 -0
- package/dist/turn.style.js +6 -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,53 @@ All notable changes to this project are written here. The format follows
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.45.0] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`quality`: how much the 3D view of a district draws, so that it turns well on every device.** A setting of the 3D view (`quality: "auto" | "high" | "medium" | "low"`, `auto` by default; `view.quality`, `view.tier`, `view.setQuality(quality)`, `onQualityChange`, and `qualityTuning` for a test or an unusual screen; docs/quality.md). `high` is everything as it always was: the same elements and the same pixels as before (checked against 0.44.2 for every preset, in Chromium to the byte of the screenshot, light and dark, and in WebKit at the same place on the page). `auto` starts on `high`, times the frames the view makes while it turns (not the browser's name, not the device's model) and steps a tier down when the mean interval is over 21 ms (under 48 frames a second) for a window, and a tier up when the mean has been 18.5 ms or less for 5 seconds. A try upward that fails is taken back and the next one waits twice as long (5, 10, 20, 40, 80, 160 seconds, at most 5 minutes), so a device on the edge between two tiers settles on the lower and never flickers; a display that refreshes at 30 Hz is measured once, when idle, and not taken for a slow view. A step draws the world again and **keeps the camera where it is** (the lower tiers are framed as `high` is, and the pointer, the auto-turn and a drag in the middle of being made are not touched).
|
|
15
|
+
- **The tiers.** `medium`: the 24 biggest buildings round a district stand up (130 at `high`), every other footprint stays on the floor; every other tree; a small venue (under 12 percent of the plan) is one block with its height; the small parts of a venue (benches, glass, goals, posts, masts, fascia, rails) and the faint floors of a building are left out, the crowd in the stands is blocks of colour at most 0.6 full; the crowd outside is half as many people on a canvas 0.7 as wide. `low`: no building stands up (footprints only, shaded by height), one tree in four, a venue under 40 percent of the plan is a ring of walls open at the top with its field showing inside, no crowd outside. A district of one venue is never drawn as a block, and the flat 2D plan is not changed by any tier.
|
|
16
|
+
- **The pixel ratio of the layers is capped on a phone or a tablet** (a coarse pointer or a screen of 820 CSS pixels or less on a side, `handheld()`): `auto` makes the layers at a ratio of 2 at most, `medium` 1.5 and `low` 1, and a layer past a budget of device pixels (2 million at `medium`, 1 million at `low` on a handheld) is made smaller still. A layer is made that share as wide and as high and its matrix widens it back, so the picture is the same and it holds the square of the share as many device pixels. **What this does and does not do** is in docs/quality.md: the sizes reported here are the boxes times the ratio squared, which is what the Kodokan's 680 megabytes were counted by; an engine that draws a layer again at the scale its matrix has (Chromium does, and screenshots in both engines do) makes the same pixels from it, and we could not measure the memory an iPhone allocates. What is certain is the tiers' own reduction of layers (the Kodokan's faint floors: 152 planes down to 42 at `medium` and `low`).
|
|
17
|
+
- **The demo's district page has the choice in its own controls over the view**: Auto, High, Medium and Low (finger-wide, in light and dark, in English and Japanese) with a line that says what was asked for and what is drawn now ("Auto: drawn at Medium for now."), `?quality=auto|high|medium|low` in the address (`auto`, the default, is left out), and a press changes the tier without moving the camera or stopping the turn.
|
|
18
|
+
- `scripts/perf-district.mjs --quality high,medium,low,auto` measures each tier of every preset, and for `auto` turns the view until its tier has stood still for four seconds and says the tier it settled on and the steps it took. Layers are now counted in WebKit too.
|
|
19
|
+
- Tests: every tier draws every preset without a page error (unit and demo e2e, three browsers); `high` equals no `quality` (planes, floor, markup); the governor on simulated devices (a fast one, a slow one, one on the edge, a 30 Hz and a 120 Hz display, hitches, pauses); a press and a drag keep the camera; `auto` steps down under a CPU slowed 6 times and back up when it is released (Chromium); a tier held by name is never changed by the frames; each lower tier holds the plane budget of the performance test; the controls are finger-wide and not in the browser's own style.
|
|
20
|
+
|
|
21
|
+
### Measured
|
|
22
|
+
|
|
23
|
+
Frames a second of a turn made of pointer moves, planes drawn and layers, on a Mac Studio (Apple silicon) that was **heavily loaded by other work during every run (load average 27 to 90)**, so the numbers are good for comparing tiers and not for their last digit. "Before" is 0.44.2, built beside this release and measured by the same script; `high` here is the same page as before, so those two columns differ only by the load of the minute.
|
|
24
|
+
|
|
25
|
+
| Preset | Planes: high / medium / low | Chromium, CPU x4, frames a second: before / high / medium / low / auto (tier it settled on) | WebKit phone, ratio 3: before / high / medium / low / auto (tier) |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| Rogers Arena alone | 547 / 442 / 280 | 30 / 30 / 38 / 44 / 52 (low) | 48 / 59 / 59 / 59 / 59 (high) |
|
|
28
|
+
| BC Place and Rogers Arena | 1,309 / 983 / 42 | 14 / 13 / 18 / 60 / 60 (low) | 28 / 63 / 59 / 59 / 59 (high) |
|
|
29
|
+
| BC Place (soccer) and Rogers Arena | 1,305 / 983 / 42 | 15 / 15 / 22 / 60 / 60 (low) | 26 / 59 / 59 / 59 / 63 (high) |
|
|
30
|
+
| Seattle Center | 1,088 / 606 / 220 | 21 / 22 / 33 / 60 / 61 (low) | 10 / 6 / 19 / 59 / 59 (low) |
|
|
31
|
+
| SoDo | 1,346 / 926 / 61 | 15 / 14 / 18 / 60 / 60 (low) | 7 / 4 / 11 / 59 / 59 (low) |
|
|
32
|
+
| Seattle, three venues (city) | 2,175 / 280 / 88 | 9 / 8 / 58 / 60 / 58 (medium) | 17 / 15 / 59 / 59 / 63 (medium) |
|
|
33
|
+
| Tokyo Dome City | 2,049 / 839 / 42 | 10 / 9 / 21 / 60 / 60 (low) | 9 / 8 / 26 / 59 / 59 (low) |
|
|
34
|
+
| Tokyo Dome City and the Kodokan | 2,286 / 900 / 83 | 9 / 8 / 19 / 59 / 60 (low) | 6 / 6 / 31 / 59 / 27 (medium) |
|
|
35
|
+
| The Kodokan alone | 152 / 42 / 42 | 60 / 51 / 60 / 60 / 60 (high) | 24 / 36 / 59 / 59 / 59 (medium) |
|
|
36
|
+
|
|
37
|
+
`auto` settles on 48 frames a second or more on every preset with the CPU slowed 4 times in Chromium (desk and phone window), and on 60 on a desk with nothing slowed, except Tokyo Dome City (48: its `high` takes 20 ms a frame, just under the budget). The one row under 30 is WebKit's Tokyo Dome City and the Kodokan (27, on `medium`, measured at a load average of 60: its `medium` measures 31 on a quieter minute and `low` 59). The planes of `high` are those of 0.44.2, in the same order.
|
|
38
|
+
|
|
39
|
+
| Preset | Chromium desk, nothing slowed: `auto` frames a second (tier) | Chromium phone, ratio 3: layers in megabytes, before / `auto` at its start / `medium` / `low` |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Rogers Arena alone | 60 (high) | 100 / 44 / 17 / 5 |
|
|
42
|
+
| BC Place and Rogers Arena | 60 (high) | 118 / 52 / 22 / 4 |
|
|
43
|
+
| BC Place (soccer) and Rogers Arena | 60 (high) | 118 / 53 / 22 / 4 |
|
|
44
|
+
| Seattle Center | 60 (high) | 98 / 43 / 18 / 5 |
|
|
45
|
+
| SoDo | 60 (high) | 86 / 38 / 18 / 4 |
|
|
46
|
+
| Seattle, three venues (city) | 60 (high) | 50 / 22 / 8 / 4 |
|
|
47
|
+
| Tokyo Dome City | 48 (high) | 76 / 34 / 15 / 4 |
|
|
48
|
+
| Tokyo Dome City and the Kodokan | 60 (high) | 78 / 35 / 14 / 4 |
|
|
49
|
+
| The Kodokan alone | 60 (high) | 713 / 317 / 67 / 32 |
|
|
50
|
+
|
|
51
|
+
(The layer sizes are the page's own boxes times the pixel ratio squared, as before; see what the cap does and does not do above.)
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- The district page's outside-crowd levels are 48 pixels high, as the new quality bar's are (they were 44).
|
|
56
|
+
|
|
10
57
|
## [0.44.2] - 2026-10-29
|
|
11
58
|
|
|
12
59
|
### Changed
|
package/dist/context.d.ts
CHANGED
|
@@ -145,6 +145,8 @@ export declare function placeContext(areas: readonly ContextArea[], origin: LonL
|
|
|
145
145
|
w: number;
|
|
146
146
|
h: number;
|
|
147
147
|
}, venues: readonly ContextVenue[]): ContextPlan;
|
|
148
|
+
/** A share of the trees, spread evenly over the lot: every tree at 1, none at 0, and at 0.5 every other. The same trees every time, so that a view drawn again at a tier keeps the ones it kept. */
|
|
149
|
+
export declare function thinTrees(trees: readonly ContextTree[], share: number): ContextTree[];
|
|
148
150
|
/** The tone (0 low to 3 tall) of a building of `feet` height. */
|
|
149
151
|
export declare const toneOf: (feet: number) => number;
|
|
150
152
|
/** The streets flat on the floor, under the buildings: a path for each kind and width (a foot), widest and lowest kinds first, so that a sidewalk lies over the carriageway beside it and a viaduct's deck over the street under it. */
|
package/dist/context.js
CHANGED
|
@@ -149,6 +149,23 @@ export function placeContext(areas, origin, turn, frame, venues) {
|
|
|
149
149
|
const credits = (language) => [...new Set(layers.map((l) => l.attribution[language]).filter((text) => text !== ""))];
|
|
150
150
|
return { blocks, streets, land, trees, owned, attribution: { en: credits("en"), ja: credits("ja") } };
|
|
151
151
|
}
|
|
152
|
+
/** A share of the trees, spread evenly over the lot: every tree at 1, none at 0, and at 0.5 every other. The same trees every time, so that a view drawn again at a tier keeps the ones it kept. */
|
|
153
|
+
export function thinTrees(trees, share) {
|
|
154
|
+
if (share >= 1)
|
|
155
|
+
return [...trees];
|
|
156
|
+
if (!(share > 0))
|
|
157
|
+
return [];
|
|
158
|
+
const kept = [];
|
|
159
|
+
let owed = 0;
|
|
160
|
+
for (const tree of trees) {
|
|
161
|
+
owed += share;
|
|
162
|
+
if (owed >= 1) {
|
|
163
|
+
owed -= 1;
|
|
164
|
+
kept.push(tree);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return kept;
|
|
168
|
+
}
|
|
152
169
|
/** The tone (0 low to 3 tall) of a building of `feet` height. */
|
|
153
170
|
export const toneOf = (feet) => CONTEXT_TONES.filter((m) => feet / FEET_PER_METRE >= m).length;
|
|
154
171
|
const num = (value) => String(Math.round(value * 100) / 100);
|
package/dist/district.turn.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// plan drawn with each venue's building only. The model it makes is a `TurnModel`, so the camera, the controls, the pointer, the auto-turn and the steady fit of every other 3D view are this one's; its
|
|
4
4
|
// buildings are stacked building by building (`Plane.group`), the far one first, so that a roof never shows through a nearer building.
|
|
5
5
|
import { describeDistrict, districtId, drawDistrict, drawMember, nestedDrawing, planDistrict, surfaceCentre } from "./district.js";
|
|
6
|
-
import { blockPlanes, contextUprights, shellBlock, slabPlanes } from "./context.js";
|
|
6
|
+
import { blockPlanes, contextUprights, shellBlock, slabPlanes, thinTrees } from "./context.js";
|
|
7
|
+
import { TIER_PROFILES, tierOf } from "./quality.js";
|
|
7
8
|
import { dojo } from "./budo/dojo.api.js";
|
|
8
9
|
import { KODOKAN_FLOORS, KODOKAN_GALLERY_LEVEL } from "./kodokan.js";
|
|
9
10
|
import { APART, n, plane } from "./turn.kit.js";
|
|
@@ -54,6 +55,27 @@ function placeInDistrict(p, placed, centre, prefix, group, lift = 0) {
|
|
|
54
55
|
}
|
|
55
56
|
return moved;
|
|
56
57
|
}
|
|
58
|
+
/** The box a district at `high` is framed to, kept for the options it was worked out for, so that a lower tier is framed exactly as `high` is: the camera does not move when the tier changes. */
|
|
59
|
+
const HIGH_BOX = new WeakMap();
|
|
60
|
+
/** The small parts of a venue that the lower tiers leave out: its furniture and fittings, never its stands, walls, floor or roof. */
|
|
61
|
+
const SMALL_PARTS = new Set(["bench", "glass", "post", "goal", "stanchion", "pole", "pylon", "mast", "fascia", "rail", "platform", "apron", "flag", "board", "frame", "rim", "net", "antenna", "tape"]);
|
|
62
|
+
/** How high a venue that is drawn as a block stands, in feet, by what it is: a stand-in for its stands and roof, near what its own model reaches. */
|
|
63
|
+
const BLOCK_HEIGHT = { arena: 100, stadium: 115, park: 95, hall: 60 };
|
|
64
|
+
/** A venue drawn as a proxy is a closed block while it is smaller than this share of the plan's longest side, and a ring of walls open at the top (the field and its stands show inside) above it. */
|
|
65
|
+
const SOLID_BELOW = 0.12;
|
|
66
|
+
/** The planes of a venue drawn as a proxy, in the venue's own group, tinted as a venue and not as the buildings round it: its outline standing up to its height, closed by a roof when it is small, a ring of walls seen from both sides when it is not. */
|
|
67
|
+
function venueBlock(key, kind, ring, prefix, group, open) {
|
|
68
|
+
const height = BLOCK_HEIGHT[kind] ?? 90;
|
|
69
|
+
const block = shellBlock(`venue-${key}`, ring, open ? height * 0.6 : height, undefined);
|
|
70
|
+
const planes = blockPlanes(block, `${prefix}proxy-`).filter((p) => !open || /w\d+$/.test(p.id));
|
|
71
|
+
return planes.map((p) => {
|
|
72
|
+
const tinted = { ...p, svg: p.svg.replace(/u-block-roof/g, "u-proxy-roof").replace(/u-block-([abc])/g, "u-proxy-$1").replace(/u-block /g, "u-proxy "), group };
|
|
73
|
+
// Seen from both sides: the inside of the far wall shows over the field.
|
|
74
|
+
if (open)
|
|
75
|
+
delete tinted.cull;
|
|
76
|
+
return tinted;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
57
79
|
/** A name that faces the eye, standing over a building: its text on a plate. */
|
|
58
80
|
function sign(id, text, at, bottom, size, group) {
|
|
59
81
|
const wide = Math.max(size * 2.2, size * 0.62 * text.length + size * 1.2);
|
|
@@ -70,6 +92,9 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
70
92
|
const plan = planDistrict(options);
|
|
71
93
|
const base = districtId(options);
|
|
72
94
|
const id = settings.id ?? `${base}-turn`;
|
|
95
|
+
// How much is drawn: the tier asked for (a view that chooses by its frames builds it again at each tier).
|
|
96
|
+
const profile = TIER_PROFILES[tierOf(settings.quality)];
|
|
97
|
+
const longest = Math.max(plan.view.w, plan.view.h);
|
|
73
98
|
const labels = options.labels ?? plan.mode === "city";
|
|
74
99
|
const items = [];
|
|
75
100
|
const defs = [];
|
|
@@ -77,12 +102,23 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
77
102
|
plan.members.forEach((placed, i) => {
|
|
78
103
|
const member = placed.member;
|
|
79
104
|
const prefix = `m${i}-`;
|
|
105
|
+
// A small venue, in a tier that draws such as a block: its outline standing up to its height, and nothing of its stands.
|
|
106
|
+
const xs = placed.footprint.map((q) => q[0]);
|
|
107
|
+
const ys = placed.footprint.map((q) => q[1]);
|
|
108
|
+
const plain = member.shell === undefined && member.parts === undefined && member.elevation === undefined && member.plate === undefined && member.slabs === undefined;
|
|
109
|
+
const span = Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys));
|
|
110
|
+
// (A district of one venue never draws it as a block: it is what the view is of.)
|
|
111
|
+
if (plain && plan.members.length > 1 && profile.proxy > 0 && span < profile.proxy * longest) {
|
|
112
|
+
items.push({ id: `${prefix}proxy`, kind: "blocks", at: placed.at, height: BLOCK_HEIGHT[member.kind] ?? 90, planes: venueBlock(member.key, member.kind, placed.footprint, prefix, i, span >= SOLID_BELOW * longest) });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
80
115
|
// A venue may have several surfaces (the rooms of a floor): each is built as its own 3D view is, with the crowd drawn without its seats on its floor and the stands raised, and put where the plan puts it, in the venue's one group.
|
|
81
116
|
placed.parts.forEach((pp, j) => {
|
|
82
117
|
const part = pp.part;
|
|
83
118
|
const tag = j === 0 ? "" : `p${j}`;
|
|
84
119
|
const memberId = `${id}-m${i}${tag}`;
|
|
85
|
-
const
|
|
120
|
+
const crowd = part.options.crowd;
|
|
121
|
+
const model = buildTurn({ ...part.options, ...(profile.simple && typeof crowd === "object" && crowd !== null ? { crowd: { ...crowd, detail: "plain", density: Math.min(crowd.density ?? 0.85, 0.6) } } : {}), language, ...(options.theme === undefined ? {} : { theme: options.theme }), id: `${base}-m${i}${tag}` }, { ...settings, id: memberId, venue: { roof: true, ...(typeof settings.venue === "object" ? settings.venue : {}) }, preset: undefined, view: undefined });
|
|
86
122
|
const centre = surfaceCentre(part.options);
|
|
87
123
|
const pfx = `m${i}${tag}-`;
|
|
88
124
|
// A surface on an upper floor (the Kodokan's dojos) stands that high over the ground, on a floor drawn in the air; the ground under it is left bare.
|
|
@@ -91,7 +127,7 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
91
127
|
const lowest = (p) => Math.min(p.o[2], p.o[2] + p.u[2], p.o[2] + p.v[2], p.o[2] + p.u[2] + p.v[2]);
|
|
92
128
|
const shown = (p) => part.show === undefined || (part.show === "gallery") === (lowest(p) >= KODOKAN_GALLERY_LEVEL - 1e-6);
|
|
93
129
|
for (const item of model.uprights.items) {
|
|
94
|
-
const planes = item.planes.filter((p) => p.kind !== "ground" && shown(p)).map((p) => placeInDistrict(p, pp, centre, pfx, i, lift));
|
|
130
|
+
const planes = item.planes.filter((p) => p.kind !== "ground" && shown(p) && !(profile.simple && SMALL_PARTS.has(p.kind))).map((p) => placeInDistrict(p, pp, centre, pfx, i, lift));
|
|
95
131
|
if (planes.length > 0) {
|
|
96
132
|
const [ax, ay] = item.at;
|
|
97
133
|
const [rx, ry] = rot([ax - centre[0], ay - centre[1]], pp.angle);
|
|
@@ -142,6 +178,9 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
142
178
|
// The great dojo's floor, seen with every floor, has its own slab; and a solid building hides what is in it.
|
|
143
179
|
if (!chosen && ((member.plate === undefined && slab.floor === 7) || options.building === "solid"))
|
|
144
180
|
continue;
|
|
181
|
+
// The faint floors are as big as the building (each is a layer of its own, a megapixel and more): the lower tiers keep the chosen floor's and leave the others out.
|
|
182
|
+
if (!chosen && profile.simple)
|
|
183
|
+
continue;
|
|
145
184
|
const ids = chosen ? { top: `${prefix}plate`, side: `${prefix}plate-s` } : { top: `${prefix}plate-f${slab.floor}-top`, side: `${prefix}plate-f${slab.floor}-s` };
|
|
146
185
|
const planes = slabPlanes(placed.footprint, slab.level, thick, ids, chosen ? "chosen" : "faint").map((p) => ({ ...p, group: i }));
|
|
147
186
|
items.push({ id: ids.top, kind: "deck", at: placed.at, height: slab.level, planes });
|
|
@@ -156,7 +195,7 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
156
195
|
});
|
|
157
196
|
// The buildings round the venues stand apart from them: each block's planes are of no venue's group.
|
|
158
197
|
if (plan.context !== undefined)
|
|
159
|
-
for (const item of contextUprights(plan.context))
|
|
198
|
+
for (const item of contextUprights(plan.context, profile.blocks))
|
|
160
199
|
items.push({ ...item, planes: item.planes.map((p) => ({ ...p, group: APART })) });
|
|
161
200
|
if (labels) {
|
|
162
201
|
const size = Math.max(plan.view.w, plan.view.h) * (plan.mode === "city" ? 0.016 : 0.022);
|
|
@@ -172,9 +211,24 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
172
211
|
const area = { x0: view.x, y0: view.y, x1: view.x + view.w, y1: view.y + view.h };
|
|
173
212
|
const uprights = { items, lift: 0, thin: Math.hypot(view.w, view.h) * 0.0012, area, whole: true, ...(defs.length === 0 ? {} : { defs: defs.join("") }) };
|
|
174
213
|
const planes = planesOf(uprights);
|
|
175
|
-
|
|
214
|
+
// The camera is fitted to what the district is at `high`, whatever tier it is drawn at: a view that changes tier keeps its camera where it is, and the lower tiers stand in the same frame.
|
|
215
|
+
let box = sceneBox(planes, area, 0, centre, scale);
|
|
216
|
+
if (profile.tier !== "high") {
|
|
217
|
+
const key = JSON.stringify(settings.venue ?? null);
|
|
218
|
+
const known = HIGH_BOX.get(options);
|
|
219
|
+
if (known?.key === key)
|
|
220
|
+
box = known.box;
|
|
221
|
+
else {
|
|
222
|
+
box = buildDistrictTurn(options, { ...settings, quality: "high" }).box;
|
|
223
|
+
HIGH_BOX.set(options, { key, box });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else
|
|
227
|
+
HIGH_BOX.set(options, { key: JSON.stringify(settings.venue ?? null), box });
|
|
176
228
|
// (The floor has no names: the 3D view's own signs, which face the eye, carry them.)
|
|
177
|
-
|
|
229
|
+
// (The trees of the tiers that draw fewer are thinned on the floor only: a tree is a dot there.)
|
|
230
|
+
const floorPlan = profile.trees < 1 && plan.context?.trees !== undefined ? { ...plan, context: { ...plan.context, trees: thinTrees(plan.context.trees, profile.trees) } } : plan;
|
|
231
|
+
const floor = drawDistrict({ ...options, id: base, labels: false }, floorPlan, (member, i, part) => drawMember(member, { ...options, id: base }, i, base, { floor: true }, part));
|
|
178
232
|
const { title, description } = describeDistrict(options, plan);
|
|
179
233
|
const presets = districtPresets(plan);
|
|
180
234
|
const surfaceLike = { sport: "soccer", language, ...(options.theme === undefined ? {} : { theme: options.theme }), ...(options.style === undefined ? {} : { style: options.style }) };
|
|
@@ -199,6 +253,7 @@ export function buildDistrictTurn(options, settings = {}) {
|
|
|
199
253
|
centre,
|
|
200
254
|
view: openingView(settings, presets),
|
|
201
255
|
turnable: true,
|
|
256
|
+
tiered: true,
|
|
202
257
|
presets,
|
|
203
258
|
// A scene that turns about its middle with buildings far from it wants a longer lens, so that what swings toward the eye does not swell.
|
|
204
259
|
lens: 12,
|
package/dist/judging.mount.js
CHANGED
|
@@ -62,6 +62,13 @@ export function mountJudgingBroadcast(host, options, settings = {}) {
|
|
|
62
62
|
direction: () => turn.direction(),
|
|
63
63
|
setDirection: (direction) => turn.setDirection(direction),
|
|
64
64
|
togglePlay: () => turn.togglePlay(),
|
|
65
|
+
get quality() {
|
|
66
|
+
return turn.quality;
|
|
67
|
+
},
|
|
68
|
+
get tier() {
|
|
69
|
+
return turn.tier;
|
|
70
|
+
},
|
|
71
|
+
setQuality: (quality) => turn.setQuality(quality),
|
|
65
72
|
svg: () => turn.svg(),
|
|
66
73
|
destroy: () => {
|
|
67
74
|
turn.destroy();
|
package/dist/outside.dom.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface OutsideLayer {
|
|
|
16
16
|
/** The level shown. */
|
|
17
17
|
readonly level: OutsideLevel;
|
|
18
18
|
setLevel(level: OutsideLevel): void;
|
|
19
|
+
/** How much of the crowd a quality tier draws: the share of the people (0 hides the canvas) and the share of the usual width of the canvas. Painted again when it changes. */
|
|
20
|
+
setDetail(detail: {
|
|
21
|
+
people: number;
|
|
22
|
+
width: number;
|
|
23
|
+
}): void;
|
|
19
24
|
/** The 3D view's zoom: a near view is painted finer (its people discs, not density). The paint is redone only when the zoom crosses `NEAR`. */
|
|
20
25
|
zoom(zoom: number): void;
|
|
21
26
|
/** What the last paint cost, in milliseconds, and drew: `undefined` before the first. */
|
package/dist/outside.dom.js
CHANGED
|
@@ -37,6 +37,7 @@ function watchTheme(doc, change) {
|
|
|
37
37
|
function layerOf(canvas, spec, colourFrom, size) {
|
|
38
38
|
const doc = canvas.ownerDocument;
|
|
39
39
|
let level = spec.level;
|
|
40
|
+
let detail = { people: 1, width: 1 };
|
|
40
41
|
let ink = inkOf(colourFrom());
|
|
41
42
|
let drawn = "";
|
|
42
43
|
let alive = true;
|
|
@@ -62,11 +63,18 @@ function layerOf(canvas, spec, colourFrom, size) {
|
|
|
62
63
|
layer.zoomTier = tier;
|
|
63
64
|
layer.repaint();
|
|
64
65
|
},
|
|
66
|
+
setDetail(next) {
|
|
67
|
+
if (next.people === detail.people && next.width === detail.width)
|
|
68
|
+
return;
|
|
69
|
+
detail = next;
|
|
70
|
+
layer.repaint();
|
|
71
|
+
},
|
|
65
72
|
repaint(force = false) {
|
|
66
73
|
if (!alive)
|
|
67
74
|
return;
|
|
68
|
-
const width = Math.min(MAX_WIDE, Math.max(1, Math.round(size())));
|
|
69
|
-
|
|
75
|
+
const width = Math.min(MAX_WIDE, Math.max(1, Math.round(size() * detail.width)));
|
|
76
|
+
const shown = Math.round(level * detail.people);
|
|
77
|
+
if (level === 0 || shown === 0) {
|
|
70
78
|
// Nobody: the canvas is made one pixel, so that it holds no memory.
|
|
71
79
|
if (canvas.width !== 1 || canvas.height !== 1) {
|
|
72
80
|
canvas.width = 1;
|
|
@@ -81,11 +89,11 @@ function layerOf(canvas, spec, colourFrom, size) {
|
|
|
81
89
|
return;
|
|
82
90
|
canvas.hidden = false;
|
|
83
91
|
const frame = frameOf(spec.viewBox, width);
|
|
84
|
-
const key = `${
|
|
92
|
+
const key = `${shown}|${frame.width}x${frame.height}|${ink.join(",")}`;
|
|
85
93
|
if (!force && key === drawn)
|
|
86
94
|
return;
|
|
87
95
|
const started = performance.now();
|
|
88
|
-
const { rgba, stats } = rasterOutside(crowd,
|
|
96
|
+
const { rgba, stats } = rasterOutside(crowd, shown, frame, ink);
|
|
89
97
|
if (canvas.width !== frame.width || canvas.height !== frame.height) {
|
|
90
98
|
canvas.width = frame.width;
|
|
91
99
|
canvas.height = frame.height;
|
|
@@ -134,7 +142,7 @@ export function attachOutside3d(floor, spec, prefetchCrowd = true) {
|
|
|
134
142
|
canvas.setAttribute("data-outside", String(spec.level));
|
|
135
143
|
canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none";
|
|
136
144
|
floor.append(canvas);
|
|
137
|
-
const layer = layerOf(canvas, spec, () => floor.querySelector(".ky-district") ?? floor, () => (parseFloat(floor.style.width) || floor.clientWidth || 1400) * (layer.zoomTier === 1 ? 2.4 : 1.4));
|
|
145
|
+
const layer = layerOf(canvas, spec, () => floor.querySelector(".ky-district") ?? floor, () => ((parseFloat(floor.style.width) || floor.clientWidth || 1400) / (parseFloat(floor.dataset.raster ?? "") || 1)) * (layer.zoomTier === 1 ? 2.4 : 1.4));
|
|
138
146
|
const set = layer.setLevel;
|
|
139
147
|
layer.setLevel = (next) => {
|
|
140
148
|
canvas.setAttribute("data-outside", String(next));
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** The tiers of detail, from the most to the least. */
|
|
2
|
+
export type QualityTier = "high" | "medium" | "low";
|
|
3
|
+
/** What a view is asked for: one tier, or `auto`, which chooses by the frames the view makes. */
|
|
4
|
+
export type Quality = "auto" | QualityTier;
|
|
5
|
+
/** The tiers, highest first: a step down is one place later in this list. */
|
|
6
|
+
export declare const QUALITY_TIERS: readonly QualityTier[];
|
|
7
|
+
/** The values a page can ask for: `auto` and the tiers. */
|
|
8
|
+
export declare const QUALITIES: readonly Quality[];
|
|
9
|
+
/** What a tier draws. */
|
|
10
|
+
export interface TierProfile {
|
|
11
|
+
tier: QualityTier;
|
|
12
|
+
/** The most buildings of a district's context that stand up as blocks in the 3D view (the tallest and widest first); the rest are the floor's flat footprints, which every tier draws. */
|
|
13
|
+
blocks: number;
|
|
14
|
+
/** The share of the context's trees that are drawn, 0 to 1. */
|
|
15
|
+
trees: number;
|
|
16
|
+
/** A venue whose longest side is under this share of the plan's longest side is drawn as its footprint standing up to its height, one block, in place of its stands and roof; 0 draws every venue in full. */
|
|
17
|
+
proxy: number;
|
|
18
|
+
/** The small parts of a venue (benches, glass, posts, goals, the fascia and its fittings) are left out, and the crowd in its stands is drawn as blocks of colour. */
|
|
19
|
+
simple: boolean;
|
|
20
|
+
/** The share of the crowd outside that is drawn, 0 to 1 (0 hides it), and the width of its canvas as a share of the usual. */
|
|
21
|
+
outside: {
|
|
22
|
+
people: number;
|
|
23
|
+
width: number;
|
|
24
|
+
};
|
|
25
|
+
/** The pixel ratio the layers are rastered at, at most: on a phone or a tablet, and elsewhere. `Infinity` leaves the device's own. */
|
|
26
|
+
ratio: {
|
|
27
|
+
handheld: number;
|
|
28
|
+
other: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** The profile of each tier. `high` leaves everything as it always was (the cap on a handheld's pixel ratio is the mode `auto`'s and is made in `rasterOf`). */
|
|
32
|
+
export declare const TIER_PROFILES: Record<QualityTier, TierProfile>;
|
|
33
|
+
/** Whether a value is one of the qualities. */
|
|
34
|
+
export declare const isQuality: (value: unknown) => value is Quality;
|
|
35
|
+
/** A quality from whatever a page or an address gave: the same if it is one, else `fallback`. */
|
|
36
|
+
export declare const qualityOf: (value: unknown, fallback?: Quality) => Quality;
|
|
37
|
+
/** The tier a view with no frames to go by is drawn at: the tier asked for, and `high` for `auto` (a page made on a server, a first draw). */
|
|
38
|
+
export declare const tierOf: (quality: Quality | undefined) => QualityTier;
|
|
39
|
+
/** The tier one step lower or higher than `tier`, or the same at the end. */
|
|
40
|
+
export declare const lower: (tier: QualityTier) => QualityTier;
|
|
41
|
+
export declare const higher: (tier: QualityTier) => QualityTier;
|
|
42
|
+
/** What decides whether the device is a phone or a tablet: it has a coarse pointer, or its screen is small. It is asked of the page's own window, never of the browser's name. */
|
|
43
|
+
export declare function handheld(win: {
|
|
44
|
+
matchMedia?: (query: string) => {
|
|
45
|
+
matches: boolean;
|
|
46
|
+
};
|
|
47
|
+
screen?: {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
};
|
|
51
|
+
innerWidth?: number;
|
|
52
|
+
} | null | undefined): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The share of its size a layer is made at, from the device's pixel ratio `dpr`: a layer is made `share` times as wide and as high in CSS pixels and turned back to its size by its matrix, so that it holds `share` squared as many device
|
|
55
|
+
* pixels, as far as the engine keeps a layer's pixels to its own box (the layers of a district at a ratio of 3 are hundreds of megabytes, which a phone's browser does not always keep). An engine that draws a layer again at the scale its matrix has
|
|
56
|
+
* (Chromium does) makes the same pixels from it, so there it changes nothing, and the sizes this package reports are the boxes times the ratio squared, not what an engine allocates. 1 when the ratio is within the cap. The mode `auto` caps a handheld
|
|
57
|
+
* at 2 even at `high`; a tier asked for by name is exactly that tier's, and `high` asked for by name is the device's own ratio, as it always was.
|
|
58
|
+
*/
|
|
59
|
+
export declare function rasterOf(quality: Quality, tier: QualityTier, dpr: number, isHandheld: boolean): number;
|
|
60
|
+
/**
|
|
61
|
+
* The most device pixels one layer may hold in a tier: a flat floor of a building is a layer as big as the building's box (the Kodokan's fourteen plates are 47 megabytes each at a ratio of 3), so a layer past the budget is made smaller
|
|
62
|
+
* still (see `rasterOf`). `Infinity` at `high`, by name or in `auto`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function layerBudget(tier: QualityTier, isHandheld: boolean): number;
|
|
65
|
+
/**
|
|
66
|
+
* The share of its size each layer is made at, from the sizes the layers would have at full size (their boxes in CSS pixels), the share every layer is made at (`rasterOf`), the device's pixel ratio and the layer budget: a layer is
|
|
67
|
+
* made at the smaller of the share and the one that holds `budget` device pixels, never under a fifth. The floor comes first, then the planes, as `layoutOf` takes them.
|
|
68
|
+
*/
|
|
69
|
+
export declare function rasterShares(sizes: readonly (readonly [number, number])[], share: number, dpr: number, budget: number): number[];
|
|
70
|
+
/** How the governor judges the frames. Times are in milliseconds. */
|
|
71
|
+
export interface GovernorTuning {
|
|
72
|
+
/** The frames in a window it keeps (the oldest are dropped past twice this), and the fewest it judges: a window is also at least `settleMs` long, so a view at 60 frames a second judges 42 frames and one at 6 frames a second judges 8. */
|
|
73
|
+
frames: number;
|
|
74
|
+
minFrames: number;
|
|
75
|
+
settleMs: number;
|
|
76
|
+
/** The first frames after the view is mounted are slow whatever it holds (scripts are still compiling, the page is still being laid out): they are not judged. */
|
|
77
|
+
warmMs: number;
|
|
78
|
+
/** The mean interval between frames above which the view is over budget (21 is below 48 frames a second: a view that misses a refresh in three does not hold 60; it widens on a display that refreshes slower than 60 Hz), and the mean at or under which it is steady (18.5 holds 60 frames a second). */
|
|
79
|
+
budgetMs: number;
|
|
80
|
+
steadyMs: number;
|
|
81
|
+
/** How long the view must stay steady before it tries the tier above, and the most that wait grows to after steps that were taken back. */
|
|
82
|
+
upAfterMs: number;
|
|
83
|
+
maxUpAfterMs: number;
|
|
84
|
+
/** Frames are ignored for this long after a step, while the view is drawn again. */
|
|
85
|
+
quietMs: number;
|
|
86
|
+
/** A gap longer than this between two frames is a pause (a hidden page, a stopped turn), not a slow frame. */
|
|
87
|
+
pauseMs: number;
|
|
88
|
+
}
|
|
89
|
+
export declare const GOVERNOR_TUNING: GovernorTuning;
|
|
90
|
+
/** What the governor does with a frame. */
|
|
91
|
+
export interface Governor {
|
|
92
|
+
/** The tier now. */
|
|
93
|
+
readonly tier: QualityTier;
|
|
94
|
+
/** Give it the time since the last frame and the time now; it gives the new tier when it steps, else `undefined`. */
|
|
95
|
+
frame(interval: number, now: number): QualityTier | undefined;
|
|
96
|
+
/** The interval of the display, in milliseconds, as the page measured it with nothing else going on (16.7 for 60 Hz, 33.3 for a phone in a power-saving mode): a display that refreshes slower than 60 Hz is not taken for a view that is too slow. 16.7 until told. */
|
|
97
|
+
display(interval: number): void;
|
|
98
|
+
/** Forget the frames so far (the turn stopped, the page was hidden). */
|
|
99
|
+
pause(): void;
|
|
100
|
+
/** Put it on a tier without a step (a page chose one). */
|
|
101
|
+
set(tier: QualityTier, now?: number): void;
|
|
102
|
+
/** How long it now waits, steady, before it tries a tier up. */
|
|
103
|
+
readonly wait: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The governor of a view that turns. It keeps a window of frame intervals; when their mean is over budget it steps one tier down, and when the mean has been steady for `wait` it tries one tier up. A try that goes over budget
|
|
107
|
+
* is taken back, and every step down within thirty seconds of a step up doubles the wait (up to `maxUpAfterMs`), so a device on the edge between two tiers settles on the lower one and stays there. A step up that holds (steady
|
|
108
|
+
* for `settleMs` after it) leaves the wait as it was.
|
|
109
|
+
*/
|
|
110
|
+
export declare function createGovernor(start: QualityTier, tuning?: Partial<GovernorTuning>): Governor;
|
package/dist/quality.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// How much a 3D view draws, so that it turns well on every device. A view has three tiers of detail, `high` (everything, as it always was), `medium` and `low`, and a mode, `auto`, that chooses the tier by watching the frames
|
|
2
|
+
// the view really makes while it turns: nothing here looks at the browser's name or the device's model. A tier says what is left out or made smaller (the buildings round a district stand up fewer, the trees and the crowd are thinner, a
|
|
3
|
+
// small or far venue is a block with its height, the layers are rastered at a lower pixel ratio); the governor steps a tier down when the frames run over budget for a while and back up when they have been steady for longer, with a wait
|
|
4
|
+
// after a step up that fails that doubles each time, so that the view never flickers between two tiers. Pure: numbers in, numbers out; `turn.dom.ts` feeds it frames and `district.turn.ts` reads the tier's profile.
|
|
5
|
+
/** The tiers, highest first: a step down is one place later in this list. */
|
|
6
|
+
export const QUALITY_TIERS = ["high", "medium", "low"];
|
|
7
|
+
/** The values a page can ask for: `auto` and the tiers. */
|
|
8
|
+
export const QUALITIES = ["auto", ...QUALITY_TIERS];
|
|
9
|
+
/** The profile of each tier. `high` leaves everything as it always was (the cap on a handheld's pixel ratio is the mode `auto`'s and is made in `rasterOf`). */
|
|
10
|
+
export const TIER_PROFILES = {
|
|
11
|
+
high: { tier: "high", blocks: 130, trees: 1, proxy: 0, simple: false, outside: { people: 1, width: 1 }, ratio: { handheld: Infinity, other: Infinity } },
|
|
12
|
+
medium: { tier: "medium", blocks: 24, trees: 0.5, proxy: 0.12, simple: true, outside: { people: 0.5, width: 0.7 }, ratio: { handheld: 1.5, other: 2 } },
|
|
13
|
+
low: { tier: "low", blocks: 0, trees: 0.25, proxy: 0.4, simple: true, outside: { people: 0, width: 0.5 }, ratio: { handheld: 1, other: 1 } },
|
|
14
|
+
};
|
|
15
|
+
/** Whether a value is one of the qualities. */
|
|
16
|
+
export const isQuality = (value) => typeof value === "string" && QUALITIES.includes(value);
|
|
17
|
+
/** A quality from whatever a page or an address gave: the same if it is one, else `fallback`. */
|
|
18
|
+
export const qualityOf = (value, fallback = "auto") => (isQuality(value) ? value : fallback);
|
|
19
|
+
/** The tier a view with no frames to go by is drawn at: the tier asked for, and `high` for `auto` (a page made on a server, a first draw). */
|
|
20
|
+
export const tierOf = (quality) => (quality === undefined || quality === "auto" ? "high" : quality);
|
|
21
|
+
/** The tier one step lower or higher than `tier`, or the same at the end. */
|
|
22
|
+
export const lower = (tier) => QUALITY_TIERS[Math.min(QUALITY_TIERS.length - 1, QUALITY_TIERS.indexOf(tier) + 1)];
|
|
23
|
+
export const higher = (tier) => QUALITY_TIERS[Math.max(0, QUALITY_TIERS.indexOf(tier) - 1)];
|
|
24
|
+
/** What decides whether the device is a phone or a tablet: it has a coarse pointer, or its screen is small. It is asked of the page's own window, never of the browser's name. */
|
|
25
|
+
export function handheld(win) {
|
|
26
|
+
if (win === null || win === undefined)
|
|
27
|
+
return false;
|
|
28
|
+
if (win.matchMedia?.("(pointer: coarse)").matches === true)
|
|
29
|
+
return true;
|
|
30
|
+
const sides = [win.screen?.width, win.screen?.height, win.innerWidth].filter((n) => typeof n === "number" && n > 0);
|
|
31
|
+
return sides.length > 0 && Math.min(...sides) <= 820;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The share of its size a layer is made at, from the device's pixel ratio `dpr`: a layer is made `share` times as wide and as high in CSS pixels and turned back to its size by its matrix, so that it holds `share` squared as many device
|
|
35
|
+
* pixels, as far as the engine keeps a layer's pixels to its own box (the layers of a district at a ratio of 3 are hundreds of megabytes, which a phone's browser does not always keep). An engine that draws a layer again at the scale its matrix has
|
|
36
|
+
* (Chromium does) makes the same pixels from it, so there it changes nothing, and the sizes this package reports are the boxes times the ratio squared, not what an engine allocates. 1 when the ratio is within the cap. The mode `auto` caps a handheld
|
|
37
|
+
* at 2 even at `high`; a tier asked for by name is exactly that tier's, and `high` asked for by name is the device's own ratio, as it always was.
|
|
38
|
+
*/
|
|
39
|
+
export function rasterOf(quality, tier, dpr, isHandheld) {
|
|
40
|
+
const cap = quality === "auto" && tier === "high" ? (isHandheld ? 2 : Infinity) : TIER_PROFILES[tier].ratio[isHandheld ? "handheld" : "other"];
|
|
41
|
+
if (!(dpr > 0) || dpr <= cap)
|
|
42
|
+
return 1;
|
|
43
|
+
return Math.max(0.25, Math.round((cap / dpr) * 1000) / 1000);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The most device pixels one layer may hold in a tier: a flat floor of a building is a layer as big as the building's box (the Kodokan's fourteen plates are 47 megabytes each at a ratio of 3), so a layer past the budget is made smaller
|
|
47
|
+
* still (see `rasterOf`). `Infinity` at `high`, by name or in `auto`.
|
|
48
|
+
*/
|
|
49
|
+
export function layerBudget(tier, isHandheld) {
|
|
50
|
+
if (tier === "high")
|
|
51
|
+
return Infinity;
|
|
52
|
+
return { medium: isHandheld ? 2e6 : 4e6, low: isHandheld ? 1e6 : 2e6 }[tier];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The share of its size each layer is made at, from the sizes the layers would have at full size (their boxes in CSS pixels), the share every layer is made at (`rasterOf`), the device's pixel ratio and the layer budget: a layer is
|
|
56
|
+
* made at the smaller of the share and the one that holds `budget` device pixels, never under a fifth. The floor comes first, then the planes, as `layoutOf` takes them.
|
|
57
|
+
*/
|
|
58
|
+
export function rasterShares(sizes, share, dpr, budget) {
|
|
59
|
+
const ratio = dpr > 0 ? dpr : 1;
|
|
60
|
+
return sizes.map(([w, h]) => {
|
|
61
|
+
const area = w * h * ratio * ratio;
|
|
62
|
+
const own = area > budget ? Math.sqrt(budget / area) : 1;
|
|
63
|
+
return Math.max(0.2, Math.round(Math.min(share, own) * 1000) / 1000);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export const GOVERNOR_TUNING = { frames: 24, minFrames: 8, settleMs: 700, warmMs: 600, budgetMs: 21, steadyMs: 18.5, upAfterMs: 5000, maxUpAfterMs: 300000, quietMs: 600, pauseMs: 250 };
|
|
67
|
+
/**
|
|
68
|
+
* The governor of a view that turns. It keeps a window of frame intervals; when their mean is over budget it steps one tier down, and when the mean has been steady for `wait` it tries one tier up. A try that goes over budget
|
|
69
|
+
* is taken back, and every step down within thirty seconds of a step up doubles the wait (up to `maxUpAfterMs`), so a device on the edge between two tiers settles on the lower one and stays there. A step up that holds (steady
|
|
70
|
+
* for `settleMs` after it) leaves the wait as it was.
|
|
71
|
+
*/
|
|
72
|
+
export function createGovernor(start, tuning = {}) {
|
|
73
|
+
const t = { ...GOVERNOR_TUNING, ...tuning };
|
|
74
|
+
let tier = start;
|
|
75
|
+
let wait = t.upAfterMs;
|
|
76
|
+
let window = [];
|
|
77
|
+
let since = 0;
|
|
78
|
+
let quietUntil = 0;
|
|
79
|
+
let steadyFrom;
|
|
80
|
+
let lastUp = -Infinity;
|
|
81
|
+
let firstAt;
|
|
82
|
+
// The interval of the display: told by the page, which measures it with nothing else going on (a view that cannot keep up looks, from its frames alone, like a screen that refreshes more slowly).
|
|
83
|
+
let refresh = 16.7;
|
|
84
|
+
const reset = (now) => {
|
|
85
|
+
window = [];
|
|
86
|
+
since = now;
|
|
87
|
+
steadyFrom = undefined;
|
|
88
|
+
};
|
|
89
|
+
const step = (to, now) => {
|
|
90
|
+
tier = to;
|
|
91
|
+
quietUntil = now + t.quietMs;
|
|
92
|
+
reset(now);
|
|
93
|
+
return tier;
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
get tier() {
|
|
97
|
+
return tier;
|
|
98
|
+
},
|
|
99
|
+
get wait() {
|
|
100
|
+
return wait;
|
|
101
|
+
},
|
|
102
|
+
display(interval) {
|
|
103
|
+
if (Number.isFinite(interval))
|
|
104
|
+
refresh = Math.min(34, Math.max(6, interval));
|
|
105
|
+
},
|
|
106
|
+
pause() {
|
|
107
|
+
window = [];
|
|
108
|
+
steadyFrom = undefined;
|
|
109
|
+
},
|
|
110
|
+
set(next, now = 0) {
|
|
111
|
+
tier = next;
|
|
112
|
+
quietUntil = now + t.quietMs;
|
|
113
|
+
reset(now);
|
|
114
|
+
},
|
|
115
|
+
frame(interval, now) {
|
|
116
|
+
if (interval > t.pauseMs || interval <= 0) {
|
|
117
|
+
window = [];
|
|
118
|
+
steadyFrom = undefined;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
firstAt ?? (firstAt = now);
|
|
122
|
+
if (now < quietUntil || now - firstAt < t.warmMs)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (window.length === 0)
|
|
125
|
+
since = now - interval;
|
|
126
|
+
window.push(interval);
|
|
127
|
+
if (window.length > t.frames * 2)
|
|
128
|
+
window.shift();
|
|
129
|
+
if (window.length < t.minFrames || now - since < t.settleMs)
|
|
130
|
+
return undefined;
|
|
131
|
+
const mean = window.reduce((s, x) => s + x, 0) / window.length;
|
|
132
|
+
const slow = refresh / 16.7;
|
|
133
|
+
const budget = Math.max(t.budgetMs, t.budgetMs * slow);
|
|
134
|
+
// Steady is steady on any display: only a view that really holds 60 frames a second tries a tier up.
|
|
135
|
+
const steady = t.steadyMs;
|
|
136
|
+
// Over budget is judged without the slowest frames of the window (a hitch, a script that ran in the middle of a turn is not a slow view); steady is judged with all of them.
|
|
137
|
+
const sorted = [...window].sort((x, y) => x - y).slice(0, window.length - Math.max(1, Math.floor(window.length / 12)));
|
|
138
|
+
const trimmed = sorted.reduce((sum, x) => sum + x, 0) / sorted.length;
|
|
139
|
+
if (trimmed > budget && tier !== "low") {
|
|
140
|
+
// A step down soon after a step up is a try that failed: the next try waits longer.
|
|
141
|
+
if (now - lastUp < 30000)
|
|
142
|
+
wait = Math.min(t.maxUpAfterMs, wait * 2);
|
|
143
|
+
return step(lower(tier), now);
|
|
144
|
+
}
|
|
145
|
+
if (mean <= steady) {
|
|
146
|
+
steadyFrom ?? (steadyFrom = now);
|
|
147
|
+
if (tier !== "high" && now - steadyFrom >= wait) {
|
|
148
|
+
lastUp = now;
|
|
149
|
+
return step(higher(tier), now);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
steadyFrom = undefined;
|
|
154
|
+
// Window turned over: judge the next frames afresh once it is full of them.
|
|
155
|
+
if (window.length >= t.frames * 2)
|
|
156
|
+
window = window.slice(t.frames);
|
|
157
|
+
return undefined;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/turn-entry.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { buildTurn, floorOptions, openingView } from "./turn.model.ts";
|
|
|
4
4
|
export { turnDirection } from "./turn.camera.ts";
|
|
5
5
|
export type { TurnDirection } from "./turn.camera.ts";
|
|
6
6
|
export type { TurnModel, TurnSettings } from "./turn.model.ts";
|
|
7
|
+
export { GOVERNOR_TUNING, QUALITIES, QUALITY_TIERS, TIER_PROFILES, createGovernor, handheld, isQuality, qualityOf, rasterOf, tierOf } from "./quality.ts";
|
|
8
|
+
export type { Governor, GovernorTuning, Quality, QualityTier, TierProfile } from "./quality.ts";
|
|
7
9
|
export { layoutOf, planeMarkup, planeMatrix, turnMarkup, turnMarkupOf } from "./turn.markup.ts";
|
|
8
10
|
export type { Layout } from "./turn.markup.ts";
|
|
9
11
|
export { KYOGIJO_TURN_STYLE } from "./turn.style.ts";
|
package/dist/turn-entry.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { mountKyogijoTurn, mountTurn, turnSupported } from "./turn.dom.js";
|
|
3
3
|
export { buildTurn, floorOptions, openingView } from "./turn.model.js";
|
|
4
4
|
export { turnDirection } from "./turn.camera.js";
|
|
5
|
+
export { GOVERNOR_TUNING, QUALITIES, QUALITY_TIERS, TIER_PROFILES, createGovernor, handheld, isQuality, qualityOf, rasterOf, tierOf } from "./quality.js";
|
|
5
6
|
export { layoutOf, planeMarkup, planeMatrix, turnMarkup, turnMarkupOf } from "./turn.markup.js";
|
|
6
7
|
export { KYOGIJO_TURN_STYLE } from "./turn.style.js";
|
|
7
8
|
export { CURLING_PRESETS, LONG_PRESETS, NET_PRESETS, PITCH_MAX, STEP, TURN_DEFAULT, TURN_PRESETS, TURN_PRESET_NAMES, ZOOM_MAX, ZOOM_MIN, cameraMatrix, clampView, depthOf, drag, eyeDistance, fit, matrix3d, multiply, nudge, presetOf, presetView, presetsFor, project, step } from "./turn.camera.js";
|
package/dist/turn.dom.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type TurnDirection, type TurnPreset, type TurnView } from "./turn.camer
|
|
|
2
2
|
import { type TurnModel, type TurnSettings } from "./turn.model.ts";
|
|
3
3
|
import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
4
4
|
import type { VenueSettings } from "./venue.ts";
|
|
5
|
+
import { type Quality, type QualityTier } from "./quality.ts";
|
|
5
6
|
import type { OutsideLevel } from "./outside.paint.ts";
|
|
6
7
|
/** What `mountKyogijoTurn` gives back. `O` is what `update` takes: the options of a drawing, or (for `mountMultiTurn`) of a tournament layout. */
|
|
7
8
|
export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
|
|
@@ -43,6 +44,12 @@ export interface KyogijoTurn<O = SurfaceOptions | ScoreOptions> {
|
|
|
43
44
|
mode: string;
|
|
44
45
|
pixels: number;
|
|
45
46
|
} | undefined;
|
|
47
|
+
/** What the view was asked to draw: `auto`, or the tier it is held at. */
|
|
48
|
+
readonly quality: Quality;
|
|
49
|
+
/** The tier the view is drawn at now: what `quality` says, and for `auto` the one the frames have led it to (`high` at first). */
|
|
50
|
+
readonly tier: QualityTier;
|
|
51
|
+
/** Change what the view is asked to draw: a tier is taken at once, `auto` goes on from the tier it is at. The camera and the auto-turn are not touched. */
|
|
52
|
+
setQuality(quality: Quality): void;
|
|
46
53
|
/** The flat drawing that is the floor, or `null` after `destroy`. */
|
|
47
54
|
svg(): SVGSVGElement | null;
|
|
48
55
|
/** Stop listening and empty the container. */
|
package/dist/turn.dom.js
CHANGED
|
@@ -10,6 +10,8 @@ import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
|
|
|
10
10
|
import { esc } from "./format.js";
|
|
11
11
|
import { turnBoardMarkup } from "./overlay.js";
|
|
12
12
|
import { attachOutside3d } from "./outside.dom.js";
|
|
13
|
+
import { TIER_PROFILES, createGovernor, handheld, layerBudget, qualityOf, rasterOf, rasterShares, tierOf } from "./quality.js";
|
|
14
|
+
import { size } from "./turn.kit.js";
|
|
13
15
|
/** True where the page can draw a plane in 3D by one `matrix3d`: that is, nearly everywhere with a stylesheet. */
|
|
14
16
|
export function turnSupported(win = typeof window === "undefined" ? undefined : window) {
|
|
15
17
|
const css = win?.CSS;
|
|
@@ -43,7 +45,33 @@ export function mountTurn(host, options, settings, build) {
|
|
|
43
45
|
const win = doc.defaultView;
|
|
44
46
|
/** The venue's settings as they are now: the mounted ones, altered by `update`. */
|
|
45
47
|
let venue = settings.venue;
|
|
46
|
-
|
|
48
|
+
// How much is drawn: what was asked, and the tier it is drawn at (`auto` starts on `high` and is moved by the frames the view makes while it turns).
|
|
49
|
+
let quality = qualityOf(settings.quality);
|
|
50
|
+
let tier = tierOf(quality);
|
|
51
|
+
// (Set again below, once the model is built, for a view that has no tiers.)
|
|
52
|
+
const isHandheld = handheld(win);
|
|
53
|
+
/** The share of its size each layer is made at for a model, now: one number when they all are, one each when the biggest are held to a budget of pixels. */
|
|
54
|
+
const rasterFor = (of) => {
|
|
55
|
+
// (Only a model that has tiers is made smaller: a surface on its own is small, and nothing of it would be left out.)
|
|
56
|
+
if (of.tiered !== true)
|
|
57
|
+
return 1;
|
|
58
|
+
const dpr = win?.devicePixelRatio ?? 1;
|
|
59
|
+
const share = rasterOf(quality, tier, dpr, isHandheld);
|
|
60
|
+
const budget = layerBudget(tier, isHandheld);
|
|
61
|
+
if (share === 1 && budget === Infinity)
|
|
62
|
+
return 1;
|
|
63
|
+
const boxes = [[of.viewBox.w * of.scale, of.viewBox.h * of.scale], ...of.planes.map((p) => { const [w, h] = size(p); return [w * of.scale, h * of.scale]; })];
|
|
64
|
+
return rasterShares(boxes, share, dpr, budget);
|
|
65
|
+
};
|
|
66
|
+
const sameRaster = (a, b) => (typeof a === "number" || typeof b === "number" ? a === b : a.length === b.length && a.every((x, i) => x === b[i]));
|
|
67
|
+
let current = options;
|
|
68
|
+
let model = build(options, { ...settings, quality: tier });
|
|
69
|
+
if (model.tiered !== true)
|
|
70
|
+
tier = "high";
|
|
71
|
+
let raster = rasterFor(model);
|
|
72
|
+
/** The models built at each tier for the options now, so that a step back to a tier is not built again. */
|
|
73
|
+
let made = new Map([[tier, model]]);
|
|
74
|
+
const baseId = model.id;
|
|
47
75
|
/** What names the four viewpoints: the model's own table (a tournament layout has one), else the sport's. */
|
|
48
76
|
const sport = () => model.presets ?? model.options.sport;
|
|
49
77
|
const supported = turnSupported(win ?? undefined) && model.turnable;
|
|
@@ -66,6 +94,9 @@ export function mountTurn(host, options, settings, build) {
|
|
|
66
94
|
direction: () => turnDirection(settings.direction),
|
|
67
95
|
setDirection: () => undefined,
|
|
68
96
|
togglePlay: () => false,
|
|
97
|
+
quality: qualityOf(settings.quality),
|
|
98
|
+
tier: tierOf(settings.quality),
|
|
99
|
+
setQuality: () => undefined,
|
|
69
100
|
update: (next, change) => {
|
|
70
101
|
if (change?.venue !== undefined)
|
|
71
102
|
venue = { ...(typeof venue === "object" ? venue : {}), ...change.venue };
|
|
@@ -79,7 +110,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
79
110
|
},
|
|
80
111
|
};
|
|
81
112
|
}
|
|
82
|
-
host.innerHTML = turnMarkupOf(model, settings);
|
|
113
|
+
host.innerHTML = turnMarkupOf(model, settings, raster);
|
|
83
114
|
const root = host.querySelector(".ky-turn");
|
|
84
115
|
const stage = root.querySelector(".ky-turn-view");
|
|
85
116
|
const live = root.querySelector(".ky-turn-live");
|
|
@@ -119,8 +150,116 @@ export function mountTurn(host, options, settings, build) {
|
|
|
119
150
|
outside?.destroy();
|
|
120
151
|
const floor = stage.querySelector(".ky-turn-floor");
|
|
121
152
|
outside = model.outside === undefined || floor === null ? undefined : attachOutside3d(floor, model.outside);
|
|
153
|
+
outside?.setDetail(TIER_PROFILES[tier].outside);
|
|
122
154
|
};
|
|
123
155
|
crowd();
|
|
156
|
+
root.dataset.quality = quality;
|
|
157
|
+
root.dataset.tier = tier;
|
|
158
|
+
// ---- Quality: in `auto`, the frames the view makes while it turns decide the tier. ----
|
|
159
|
+
let governor = quality === "auto" && win !== null && model.tiered === true ? createGovernor(tier, settings.qualityTuning) : undefined;
|
|
160
|
+
let meterFrame;
|
|
161
|
+
let lastStamp;
|
|
162
|
+
let lastPaint = 0;
|
|
163
|
+
/** The display's own interval, measured once with nothing else going on: an idle loop of frames a moment after the mount (the lower decile of their intervals, so that a busy moment does not count). */
|
|
164
|
+
let probe;
|
|
165
|
+
let displayMs;
|
|
166
|
+
const probeDisplay = () => {
|
|
167
|
+
if (win === null || probe !== undefined || displayMs !== undefined)
|
|
168
|
+
return;
|
|
169
|
+
const found = [];
|
|
170
|
+
let before;
|
|
171
|
+
const step = (stamp) => {
|
|
172
|
+
probe = undefined;
|
|
173
|
+
if (!alive || win === null)
|
|
174
|
+
return;
|
|
175
|
+
if (doc.hidden === true) {
|
|
176
|
+
// Hidden: start over when it can be seen.
|
|
177
|
+
found.length = 0;
|
|
178
|
+
before = undefined;
|
|
179
|
+
probe = win.setTimeout(() => (probe = win.requestAnimationFrame(step)), 400);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (before !== undefined)
|
|
183
|
+
found.push(stamp - before);
|
|
184
|
+
before = stamp;
|
|
185
|
+
if (found.length < 24)
|
|
186
|
+
probe = win.requestAnimationFrame(step);
|
|
187
|
+
else {
|
|
188
|
+
displayMs = [...found].sort((a, b) => a - b)[Math.floor(found.length * 0.1)];
|
|
189
|
+
governor?.display(displayMs);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
probe = win.setTimeout(() => (probe = win.requestAnimationFrame(step)), 350);
|
|
193
|
+
};
|
|
194
|
+
const stopMeter = () => {
|
|
195
|
+
if (meterFrame !== undefined)
|
|
196
|
+
win?.cancelAnimationFrame(meterFrame);
|
|
197
|
+
meterFrame = undefined;
|
|
198
|
+
lastStamp = undefined;
|
|
199
|
+
};
|
|
200
|
+
if (governor !== undefined)
|
|
201
|
+
probeDisplay();
|
|
202
|
+
/** The loop that times the frames: it runs while the view is being painted and for a third of a second after, and does nothing while the view is still. */
|
|
203
|
+
const meterTick = (stamp) => {
|
|
204
|
+
meterFrame = undefined;
|
|
205
|
+
if (!alive || governor === undefined || win === null)
|
|
206
|
+
return;
|
|
207
|
+
if (doc.hidden === true || stamp - lastPaint > 300) {
|
|
208
|
+
lastStamp = undefined;
|
|
209
|
+
governor.pause();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const next = lastStamp === undefined ? undefined : governor.frame(stamp - lastStamp, stamp);
|
|
213
|
+
lastStamp = next === undefined ? stamp : undefined;
|
|
214
|
+
// (A step draws the view again, which asks for the loop itself: only one is ever running.)
|
|
215
|
+
if (next !== undefined)
|
|
216
|
+
retier(next);
|
|
217
|
+
if (meterFrame === undefined)
|
|
218
|
+
meterFrame = win.requestAnimationFrame(meterTick);
|
|
219
|
+
};
|
|
220
|
+
/** The view was painted: the frames are timed from now until it is still. */
|
|
221
|
+
const touched = () => {
|
|
222
|
+
if (governor === undefined || win === null)
|
|
223
|
+
return;
|
|
224
|
+
lastPaint = win.performance.now();
|
|
225
|
+
if (meterFrame === undefined)
|
|
226
|
+
meterFrame = win.requestAnimationFrame(meterTick);
|
|
227
|
+
};
|
|
228
|
+
/** Draw the world again, from the model of the tier and the options now: the camera, the auto-turn and the pointer are not touched (the elements are the only thing replaced). */
|
|
229
|
+
const renderWorld = () => {
|
|
230
|
+
const known = made.get(tier);
|
|
231
|
+
model = known ?? build(current, { ...settings, quality: tier, id: settings.id ?? baseId, venue, preset: undefined, view });
|
|
232
|
+
made.set(tier, model);
|
|
233
|
+
raster = rasterFor(model);
|
|
234
|
+
const layout = layoutOf(model, shown, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens, false, raster);
|
|
235
|
+
const world = root.querySelector(".ky-turn-world");
|
|
236
|
+
world.innerHTML = worldMarkup(model, layout, raster);
|
|
237
|
+
// The look the floor is painted in (and the theme) are attributes of the world.
|
|
238
|
+
for (const name of ["data-sport", "data-look", "data-theme"])
|
|
239
|
+
world.removeAttribute(name);
|
|
240
|
+
world.className = model.look === undefined ? "ky-turn-world" : "ky-turn-world kyogijo";
|
|
241
|
+
if (model.look !== undefined) {
|
|
242
|
+
world.setAttribute("data-sport", model.look.sport);
|
|
243
|
+
world.setAttribute("data-look", model.look.look);
|
|
244
|
+
const forced = model.options.theme;
|
|
245
|
+
if (forced === "light" || forced === "dark")
|
|
246
|
+
world.setAttribute("data-theme", forced);
|
|
247
|
+
}
|
|
248
|
+
gather();
|
|
249
|
+
crowd();
|
|
250
|
+
root.dataset.quality = quality;
|
|
251
|
+
root.dataset.tier = tier;
|
|
252
|
+
};
|
|
253
|
+
/** The governor stepped: draw at the new tier. */
|
|
254
|
+
const retier = (next) => {
|
|
255
|
+
if (next === tier)
|
|
256
|
+
return;
|
|
257
|
+
tier = next;
|
|
258
|
+
renderWorld();
|
|
259
|
+
paint(shown);
|
|
260
|
+
label();
|
|
261
|
+
settings.onQualityChange?.(tier, quality);
|
|
262
|
+
};
|
|
124
263
|
const buttons = () => root.querySelectorAll("button[data-preset]");
|
|
125
264
|
/** The Play button says what it is and what it does: pressed while the view turns. */
|
|
126
265
|
const syncPlay = () => {
|
|
@@ -138,7 +277,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
138
277
|
const paint = (now) => {
|
|
139
278
|
const width = stage.clientWidth || 960;
|
|
140
279
|
const height = stage.clientHeight || 600;
|
|
141
|
-
const layout = layoutOf(model, now, width, height, settings.lens, true);
|
|
280
|
+
const layout = layoutOf(model, now, width, height, settings.lens, true, raster);
|
|
142
281
|
outside?.zoom(now.zoom);
|
|
143
282
|
const culled = layout.culled;
|
|
144
283
|
elements.forEach((one, i) => {
|
|
@@ -159,6 +298,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
159
298
|
if (faded[i] !== o)
|
|
160
299
|
one.style.opacity = faded[i] = o;
|
|
161
300
|
});
|
|
301
|
+
touched();
|
|
162
302
|
};
|
|
163
303
|
const label = () => {
|
|
164
304
|
const here = presetOf(view, sport());
|
|
@@ -481,25 +621,11 @@ export function mountTurn(host, options, settings, build) {
|
|
|
481
621
|
return;
|
|
482
622
|
if (change?.venue !== undefined)
|
|
483
623
|
venue = { ...(typeof venue === "object" ? venue : {}), ...change.venue };
|
|
484
|
-
|
|
624
|
+
current = next;
|
|
625
|
+
made = new Map();
|
|
485
626
|
view = clampView(view);
|
|
627
|
+
renderWorld();
|
|
486
628
|
opening = openingView(settings, sport());
|
|
487
|
-
const layout = layoutOf(model, shown, stage.clientWidth || 960, stage.clientHeight || 600, settings.lens);
|
|
488
|
-
const world = root.querySelector(".ky-turn-world");
|
|
489
|
-
world.innerHTML = worldMarkup(model, layout);
|
|
490
|
-
// The look the floor is painted in (and the theme) are attributes of the world.
|
|
491
|
-
for (const name of ["data-sport", "data-look", "data-theme"])
|
|
492
|
-
world.removeAttribute(name);
|
|
493
|
-
world.className = model.look === undefined ? "ky-turn-world" : "ky-turn-world kyogijo";
|
|
494
|
-
if (model.look !== undefined) {
|
|
495
|
-
world.setAttribute("data-sport", model.look.sport);
|
|
496
|
-
world.setAttribute("data-look", model.look.look);
|
|
497
|
-
const forced = model.options.theme;
|
|
498
|
-
if (forced === "light" || forced === "dark")
|
|
499
|
-
world.setAttribute("data-theme", forced);
|
|
500
|
-
}
|
|
501
|
-
gather();
|
|
502
|
-
crowd();
|
|
503
629
|
// 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.
|
|
504
630
|
stage.querySelector(".ky-turn-board")?.remove();
|
|
505
631
|
if (model.board !== undefined)
|
|
@@ -525,6 +651,45 @@ export function mountTurn(host, options, settings, build) {
|
|
|
525
651
|
get playing() {
|
|
526
652
|
return playing;
|
|
527
653
|
},
|
|
654
|
+
get quality() {
|
|
655
|
+
return quality;
|
|
656
|
+
},
|
|
657
|
+
get tier() {
|
|
658
|
+
return tier;
|
|
659
|
+
},
|
|
660
|
+
setQuality(asked) {
|
|
661
|
+
if (!alive)
|
|
662
|
+
return;
|
|
663
|
+
quality = qualityOf(asked, quality);
|
|
664
|
+
const was = tier;
|
|
665
|
+
if (model.tiered !== true) {
|
|
666
|
+
// A view that has no tiers draws as it does whatever is asked: the question is kept, and answered `high`.
|
|
667
|
+
governor = undefined;
|
|
668
|
+
stopMeter();
|
|
669
|
+
tier = "high";
|
|
670
|
+
}
|
|
671
|
+
else if (quality === "auto") {
|
|
672
|
+
stopMeter();
|
|
673
|
+
governor = win === null ? undefined : createGovernor(tier, settings.qualityTuning);
|
|
674
|
+
if (displayMs !== undefined)
|
|
675
|
+
governor?.display(displayMs);
|
|
676
|
+
else
|
|
677
|
+
probeDisplay();
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
governor = undefined;
|
|
681
|
+
stopMeter();
|
|
682
|
+
tier = quality;
|
|
683
|
+
}
|
|
684
|
+
root.dataset.quality = quality;
|
|
685
|
+
// The tier, or the share its layers are made at, has changed: the world is drawn again (the shares are worked out for the model of the tier, in `renderWorld`).
|
|
686
|
+
if (tier !== was || !sameRaster(rasterFor(model), raster)) {
|
|
687
|
+
renderWorld();
|
|
688
|
+
paint(shown);
|
|
689
|
+
label();
|
|
690
|
+
}
|
|
691
|
+
settings.onQualityChange?.(tier, quality);
|
|
692
|
+
},
|
|
528
693
|
play() {
|
|
529
694
|
setPlaying(true);
|
|
530
695
|
return playing;
|
|
@@ -557,6 +722,7 @@ export function mountTurn(host, options, settings, build) {
|
|
|
557
722
|
outside?.destroy();
|
|
558
723
|
stop();
|
|
559
724
|
stopTick();
|
|
725
|
+
stopMeter();
|
|
560
726
|
seen?.disconnect();
|
|
561
727
|
doc.removeEventListener("visibilitychange", onVisible);
|
|
562
728
|
clearTimeout(spoken);
|
package/dist/turn.markup.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type Placement, type TurnDirection, type TurnView } from "./turn.camera
|
|
|
2
2
|
import { type Plane } from "./turn.kit.ts";
|
|
3
3
|
import { type TurnModel, type TurnSettings } from "./turn.model.ts";
|
|
4
4
|
import type { ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
5
|
+
/** The share of its size each layer is made at: one number for all, or one for each (the floor first, then the planes in the model's order). */
|
|
6
|
+
export type Raster = number | readonly number[];
|
|
5
7
|
/**
|
|
6
8
|
* 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
|
|
7
9
|
* (`centre`, in feet).
|
|
@@ -28,18 +30,19 @@ export interface Layout {
|
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* Work a view out in a container `width` by `height` pixels. With `cull`, the faces of a closed solid that are turned away from the eye (`Plane.cull`) are left out: they have no transform and no place in the stack, and `culled` says
|
|
31
|
-
* which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs.
|
|
33
|
+
* which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs. `raster` is the share of its size an element is made at (`rasterOf`): each element's matrix widens it back, so that the
|
|
34
|
+
* picture is the same and the layers hold `raster` squared as many pixels.
|
|
32
35
|
*/
|
|
33
|
-
export declare function layoutOf(model: TurnModel, view: TurnView, width: number, height: number, lens?: number, cull?: boolean): Layout;
|
|
36
|
+
export declare function layoutOf(model: TurnModel, view: TurnView, width: number, height: number, lens?: number, cull?: boolean, raster?: Raster): Layout;
|
|
34
37
|
/** One plane as an element: its box in pixels and the SVG drawn on it in feet. */
|
|
35
|
-
export declare function planeMarkup(p: Plane, scale: number, transform: string, stack: number, fade?: number): string;
|
|
38
|
+
export declare function planeMarkup(p: Plane, scale: number, transform: string, stack: number, fade?: number, raster?: number): string;
|
|
36
39
|
/**
|
|
37
40
|
* A plane that is the floor's own drawing seen through its rectangle (`Plane.crop`): the floor's SVG again, its viewBox cut down to the plane's rectangle, in the plane's own box. It is a picture, not a second
|
|
38
41
|
* description: it carries no role, title or description of its own.
|
|
39
42
|
*/
|
|
40
|
-
export declare function cropMarkup(model: TurnModel, p: Plane, transform: string, stack: number, fade?: number): string;
|
|
43
|
+
export declare function cropMarkup(model: TurnModel, p: Plane, transform: string, stack: number, fade?: number, raster?: number): string;
|
|
41
44
|
/** The floor as an element: the flat drawing, in the pixels of the scene. */
|
|
42
|
-
export declare function floorMarkup(model: TurnModel, transform: string, stack: number): string;
|
|
45
|
+
export declare function floorMarkup(model: TurnModel, transform: string, stack: number, raster?: number): string;
|
|
43
46
|
/** The patterns the venue's planes are drawn with, once, in a box of no size: every plane's SVG reads them by id, and the crowd's colours are on this element. */
|
|
44
47
|
export declare function defsMarkup(model: TurnModel): string;
|
|
45
48
|
/**
|
|
@@ -48,7 +51,7 @@ export declare function defsMarkup(model: TurnModel): string;
|
|
|
48
51
|
*/
|
|
49
52
|
export declare function worldAttributes(model: TurnModel): string;
|
|
50
53
|
/** The floor and the planes of a model, laid out. */
|
|
51
|
-
export declare function worldMarkup(model: TurnModel, layout: Layout): string;
|
|
54
|
+
export declare function worldMarkup(model: TurnModel, layout: Layout, raster?: Raster): string;
|
|
52
55
|
/** The buttons under the view. */
|
|
53
56
|
export declare function controlsMarkup(model: TurnModel, view: TurnView, playing?: boolean, direction?: TurnDirection): string;
|
|
54
57
|
/** The size, in pixels, the first view is worked out for. */
|
|
@@ -57,7 +60,7 @@ export declare const sizeOf: (settings: TurnSettings) => {
|
|
|
57
60
|
height: number;
|
|
58
61
|
};
|
|
59
62
|
/** The 3D view of a model as markup, laid out for `settings.size`; a page's script then makes it turn. */
|
|
60
|
-
export declare function turnMarkupOf(model: TurnModel, settings?: TurnSettings): string;
|
|
63
|
+
export declare function turnMarkupOf(model: TurnModel, settings?: TurnSettings, raster?: Raster): string;
|
|
61
64
|
/**
|
|
62
65
|
* A surface as a 3D view, as markup: the flat drawing for a floor, what stands on it as planes, and the controls under it. Pure, so it can be made on a
|
|
63
66
|
* server; it is a still picture until `mountKyogijoTurn` (or the element) makes it turn in a page.
|
package/dist/turn.markup.js
CHANGED
|
@@ -9,6 +9,8 @@ import { KYOGIJO_TURN_STYLE } from "./turn.style.js";
|
|
|
9
9
|
import { esc } from "./format.js";
|
|
10
10
|
import { turnBoardMarkup } from "./overlay.js";
|
|
11
11
|
const px = (value) => `${Math.round(value * 1000) / 1000}`;
|
|
12
|
+
/** The share of the layer at `index` (0 the floor, then the planes). */
|
|
13
|
+
const shareOf = (raster, index) => (typeof raster === "number" ? raster : (raster[index] ?? 1));
|
|
12
14
|
/**
|
|
13
15
|
* 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
|
|
14
16
|
* (`centre`, in feet).
|
|
@@ -88,9 +90,10 @@ function prepare(model) {
|
|
|
88
90
|
const turnedAway = (m, side) => side * (m[0] * (m[5] * m[15] - m[13] * m[7]) - m[4] * (m[1] * m[15] - m[13] * m[3]) + m[12] * (m[1] * m[7] - m[5] * m[3])) <= 0;
|
|
89
91
|
/**
|
|
90
92
|
* Work a view out in a container `width` by `height` pixels. With `cull`, the faces of a closed solid that are turned away from the eye (`Plane.cull`) are left out: they have no transform and no place in the stack, and `culled` says
|
|
91
|
-
* which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs.
|
|
93
|
+
* which, so that a page need not write them at all; without it every plane is laid out, as a still picture needs. `raster` is the share of its size an element is made at (`rasterOf`): each element's matrix widens it back, so that the
|
|
94
|
+
* picture is the same and the layers hold `raster` squared as many pixels.
|
|
92
95
|
*/
|
|
93
|
-
export function layoutOf(model, view, width, height, lens, cull = false) {
|
|
96
|
+
export function layoutOf(model, view, width, height, lens, cull = false, raster = 1) {
|
|
94
97
|
const distance = eyeDistance(width, height, lens ?? model.lens);
|
|
95
98
|
const at = fit(model.box, { ...view, zoom: 1 }, width, height, distance);
|
|
96
99
|
const camera = cameraMatrix(view, at, distance);
|
|
@@ -99,15 +102,24 @@ export function layoutOf(model, view, width, height, lens, cull = false) {
|
|
|
99
102
|
const known = prepare(model);
|
|
100
103
|
const floor = { id: "floor", kind: "ground", o: [model.viewBox.x, model.viewBox.y, model.uprights.lift], u: [model.viewBox.w, 0, 0], v: [0, model.viewBox.h, 0], svg: "" };
|
|
101
104
|
const count = model.planes.length;
|
|
105
|
+
const widen = (m, index) => {
|
|
106
|
+
const share = shareOf(raster, index);
|
|
107
|
+
if (share === 1)
|
|
108
|
+
return m;
|
|
109
|
+
const back = 1 / share;
|
|
110
|
+
for (let i = 0; i < 8; i += 1)
|
|
111
|
+
m[i] *= back;
|
|
112
|
+
return m;
|
|
113
|
+
};
|
|
102
114
|
const transforms = new Array(count + 1);
|
|
103
115
|
const culled = cull ? new Array(count + 1).fill(false) : undefined;
|
|
104
116
|
// A billboard faces the eye at this view.
|
|
105
117
|
const planes = new Array(count);
|
|
106
118
|
for (let i = 0; i < count; i += 1)
|
|
107
119
|
planes[i] = facing(model.planes[i], view.yaw, view.pitch);
|
|
108
|
-
transforms[0] = matrix3d(multiply(camera, planeMatrix(floor, s, centre)));
|
|
120
|
+
transforms[0] = matrix3d(widen(multiply(camera, planeMatrix(floor, s, centre)), 0));
|
|
109
121
|
for (let i = 0; i < count; i += 1) {
|
|
110
|
-
const m = multiply(camera, known.base[i] ?? planeMatrix(planes[i], s, centre));
|
|
122
|
+
const m = widen(multiply(camera, known.base[i] ?? planeMatrix(planes[i], s, centre)), i + 1);
|
|
111
123
|
const side = planes[i].cull;
|
|
112
124
|
if (culled !== undefined && side !== undefined && turnedAway(m, side)) {
|
|
113
125
|
culled[i + 1] = true;
|
|
@@ -159,15 +171,16 @@ export function layoutOf(model, view, width, height, lens, cull = false) {
|
|
|
159
171
|
return { at, transforms, stack, fade, ...(culled === undefined ? {} : { culled }) };
|
|
160
172
|
}
|
|
161
173
|
/** One plane as an element: its box in pixels and the SVG drawn on it in feet. */
|
|
162
|
-
export function planeMarkup(p, scale, transform, stack, fade = 1) {
|
|
174
|
+
export function planeMarkup(p, scale, transform, stack, fade = 1, raster = 1) {
|
|
163
175
|
const [w, h] = size(p);
|
|
164
|
-
|
|
176
|
+
const [bw, bh] = [px(w * scale * raster), px(h * scale * raster)];
|
|
177
|
+
return `<div class="ky-up ky-up-${p.kind}" data-part="${esc(p.id)}" style="width:${bw}px;height:${bh}px;transform:${transform};z-index:${stack}${fade < 1 ? `;opacity:${n(fade)}` : ""}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(w)} ${n(h)}" width="${bw}" height="${bh}" aria-hidden="true" focusable="false">${p.svg}</svg></div>`;
|
|
165
178
|
}
|
|
166
179
|
/**
|
|
167
180
|
* A plane that is the floor's own drawing seen through its rectangle (`Plane.crop`): the floor's SVG again, its viewBox cut down to the plane's rectangle, in the plane's own box. It is a picture, not a second
|
|
168
181
|
* description: it carries no role, title or description of its own.
|
|
169
182
|
*/
|
|
170
|
-
export function cropMarkup(model, p, transform, stack, fade = 1) {
|
|
183
|
+
export function cropMarkup(model, p, transform, stack, fade = 1, raster = 1) {
|
|
171
184
|
const [w, h] = size(p);
|
|
172
185
|
const box = `${px(p.o[0])} ${px(p.o[1])} ${px(w)} ${px(h)}`;
|
|
173
186
|
const picture = model.floor
|
|
@@ -177,12 +190,12 @@ export function cropMarkup(model, p, transform, stack, fade = 1) {
|
|
|
177
190
|
.replace(/^<svg([^>]*?)\srole="img"/, '<svg$1 aria-hidden="true"')
|
|
178
191
|
.replace(/^<svg([^>]*?)\saria-labelledby="[^"]*"/, "<svg$1")
|
|
179
192
|
.replace(/<title\b[^>]*>[\s\S]*?<\/title>|<desc\b[^>]*>[\s\S]*?<\/desc>/g, "");
|
|
180
|
-
return `<div class="ky-up ky-up-${p.kind}" data-part="${esc(p.id)}" data-crop="floor" style="width:${px(w * model.scale)}px;height:${px(h * model.scale)}px;transform:${transform};z-index:${stack}${fade < 1 ? `;opacity:${n(fade)}` : ""}">${picture}</div>`;
|
|
193
|
+
return `<div class="ky-up ky-up-${p.kind}" data-part="${esc(p.id)}" data-crop="floor" style="width:${px(w * model.scale * raster)}px;height:${px(h * model.scale * raster)}px;transform:${transform};z-index:${stack}${fade < 1 ? `;opacity:${n(fade)}` : ""}">${picture}</div>`;
|
|
181
194
|
}
|
|
182
195
|
/** The floor as an element: the flat drawing, in the pixels of the scene. */
|
|
183
|
-
export function floorMarkup(model, transform, stack) {
|
|
196
|
+
export function floorMarkup(model, transform, stack, raster = 1) {
|
|
184
197
|
const vb = model.viewBox;
|
|
185
|
-
return `<div class="ky-turn-floor" style="width:${px(vb.w * model.scale)}px;height:${px(vb.h * model.scale)}px;transform:${transform};z-index:${stack}">${model.floor}</div>`;
|
|
198
|
+
return `<div class="ky-turn-floor"${raster === 1 ? "" : ` data-raster="${raster}"`} style="width:${px(vb.w * model.scale * raster)}px;height:${px(vb.h * model.scale * raster)}px;transform:${transform};z-index:${stack}">${model.floor}</div>`;
|
|
186
199
|
}
|
|
187
200
|
/** The patterns the venue's planes are drawn with, once, in a box of no size: every plane's SVG reads them by id, and the crowd's colours are on this element. */
|
|
188
201
|
export function defsMarkup(model) {
|
|
@@ -197,8 +210,8 @@ export function worldAttributes(model) {
|
|
|
197
210
|
return model.look === undefined ? 'class="ky-turn-world"' : `class="ky-turn-world kyogijo" data-sport="${esc(model.look.sport)}" data-look="${esc(model.look.look)}"${theme === "light" || theme === "dark" ? ` data-theme="${theme}"` : ""}`;
|
|
198
211
|
}
|
|
199
212
|
/** The floor and the planes of a model, laid out. */
|
|
200
|
-
export function worldMarkup(model, layout) {
|
|
201
|
-
return defsMarkup(model) + floorMarkup(model, layout.transforms[0], layout.stack[0]) + model.planes.map((p, i) => (p.crop === true ? cropMarkup(model, p, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1]) : planeMarkup(p, model.scale, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1]))).join("");
|
|
213
|
+
export function worldMarkup(model, layout, raster = 1) {
|
|
214
|
+
return defsMarkup(model) + floorMarkup(model, layout.transforms[0], layout.stack[0], shareOf(raster, 0)) + model.planes.map((p, i) => (p.crop === true ? cropMarkup(model, p, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1], shareOf(raster, i + 1)) : planeMarkup(p, model.scale, layout.transforms[i + 1], layout.stack[i + 1], layout.fade[i + 1], shareOf(raster, i + 1)))).join("");
|
|
202
215
|
}
|
|
203
216
|
/** The buttons under the view. */
|
|
204
217
|
export function controlsMarkup(model, view, playing = false, direction = "counterclockwise") {
|
|
@@ -223,18 +236,18 @@ export function controlsMarkup(model, view, playing = false, direction = "counte
|
|
|
223
236
|
/** The size, in pixels, the first view is worked out for. */
|
|
224
237
|
export const sizeOf = (settings) => settings.size ?? { width: 960, height: 600 };
|
|
225
238
|
/** The 3D view of a model as markup, laid out for `settings.size`; a page's script then makes it turn. */
|
|
226
|
-
export function turnMarkupOf(model, settings = {}) {
|
|
239
|
+
export function turnMarkupOf(model, settings = {}, raster = 1) {
|
|
227
240
|
const words = turnWords(model);
|
|
228
241
|
const { width, height } = sizeOf(settings);
|
|
229
242
|
const view = model.view;
|
|
230
|
-
const layout = layoutOf(model, view, width, height, settings.lens);
|
|
243
|
+
const layout = layoutOf(model, view, width, height, settings.lens, false, raster);
|
|
231
244
|
const keys = `${words.keys}${model.standing === "" ? "" : ` ${model.standing}`}`;
|
|
232
245
|
const sheet = model.options.style === "none" ? "" : `<style>${KYOGIJO_TURN_STYLE}</style>`;
|
|
233
246
|
const theme = model.options.theme;
|
|
234
247
|
const sport = model.options.sport;
|
|
235
248
|
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}">` +
|
|
236
249
|
sheet +
|
|
237
|
-
`<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>` +
|
|
250
|
+
`<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, raster)}</div>${model.board === undefined ? "" : turnBoardMarkup(model.board)}</div>` +
|
|
238
251
|
`<p class="ky-turn-help" id="${esc(model.id)}-help">${esc(keys)}</p>` +
|
|
239
252
|
(settings.controls === false ? "" : controlsMarkup(model, view, false, turnDirection(settings.direction))) +
|
|
240
253
|
`<div class="ky-turn-live" role="status" aria-live="polite"></div>` +
|
package/dist/turn.model.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type ResolvedBoard } from "./overlay.ts";
|
|
|
6
6
|
import type { VenueSettings } from "./venue.ts";
|
|
7
7
|
import type { Box3 } from "./turn.camera.ts";
|
|
8
8
|
import type { OutsideSpec } from "./outside.dom.ts";
|
|
9
|
+
import type { GovernorTuning, Quality, QualityTier } from "./quality.ts";
|
|
9
10
|
import type { Language, LightingOptions, LightingPreset, ScoreOptions, SurfaceOptions } from "./types.ts";
|
|
10
11
|
/** What can be asked of a 3D view. */
|
|
11
12
|
export interface TurnSettings {
|
|
@@ -53,6 +54,16 @@ export interface TurnSettings {
|
|
|
53
54
|
onDirectionChange?: (direction: TurnDirection) => void;
|
|
54
55
|
/** Called with `true` when the auto-turn starts and `false` when it stops, whatever stopped it. */
|
|
55
56
|
onPlayChange?: (playing: boolean) => void;
|
|
57
|
+
/**
|
|
58
|
+
* How much the view draws, so that it turns well on every device: `"auto"` (the default) starts on `high` and watches the frames the view makes while it turns, stepping a tier down when they run over budget for a while and back up
|
|
59
|
+
* when they have been steady for longer; `"high"` is everything as it always was; `"medium"` and `"low"` draw less (the buildings round a district stand up fewer, the trees and the crowd are thinner, a small venue is a block, and the layers
|
|
60
|
+
* are rastered at a lower pixel ratio). A tier asked for by name is kept; `setQuality` changes it at once, and the camera and the auto-turn go on. See `docs/quality.md`.
|
|
61
|
+
*/
|
|
62
|
+
quality?: Quality;
|
|
63
|
+
/** How the frames are judged in `auto` (see `GovernorTuning`): for a test or a screen that is unusual; the defaults suit a 60 Hz display. */
|
|
64
|
+
qualityTuning?: Partial<GovernorTuning>;
|
|
65
|
+
/** Called with the tier the view is drawn at when it is first mounted and each time `auto` or `setQuality` changes it. */
|
|
66
|
+
onQualityChange?: (tier: QualityTier, quality: Quality) => void;
|
|
56
67
|
/** Called after the view has changed by a key, a button or the pointer, with the view and the viewpoint it is (`undefined` when it has been turned by hand). */
|
|
57
68
|
onChange?: (view: TurnView, preset: TurnPreset | undefined) => void;
|
|
58
69
|
}
|
|
@@ -113,6 +124,8 @@ export interface TurnModel {
|
|
|
113
124
|
}[];
|
|
114
125
|
/** The board the options ask for (`board`), laid over the view: a layer of its own, never a plane of the scene. */
|
|
115
126
|
board?: ResolvedBoard;
|
|
127
|
+
/** Whether the model has tiers: a district's does (`quality`), a single surface's does not, and the view does not time its frames or make its layers smaller for one that has none. */
|
|
128
|
+
tiered?: boolean;
|
|
116
129
|
/** The crowd outside the venues of a district, asked for (`DistrictOptions.outside`): a canvas on the floor that the mount paints (`outside.dom.ts`), never a plane of the scene. */
|
|
117
130
|
outside?: OutsideSpec;
|
|
118
131
|
}
|
package/dist/turn.style.js
CHANGED
|
@@ -80,6 +80,12 @@ export const KYOGIJO_TURN_STYLE = `
|
|
|
80
80
|
.ky-turn .u-slab-top{fill:var(--ky-up-blk-roof);stroke:var(--ky-up-blk-line);stroke-width:.5}
|
|
81
81
|
.ky-turn .u-floorplate{fill:var(--ky-up-plate);stroke:var(--ky-up-plate-line);stroke-width:1.2;stroke-linejoin:round}
|
|
82
82
|
.ky-turn .u-block-roof{fill:var(--ky-up-blk-roof);stroke:var(--ky-up-blk-line);stroke-width:.5;stroke-linejoin:round}
|
|
83
|
+
/* A venue drawn as a proxy in the lower tiers (district.turn.ts): the tones of the buildings round it, darker and bluer, as a stadium's concrete and roof read against a city's. */
|
|
84
|
+
.ky-turn .u-proxy{stroke:var(--ky-up-blk-line);stroke-width:.6}
|
|
85
|
+
.ky-turn .u-proxy-a{fill:color-mix(in srgb,var(--ky-up-blk-a) 52%,#4d6fa0)}
|
|
86
|
+
.ky-turn .u-proxy-b{fill:color-mix(in srgb,var(--ky-up-blk-b) 52%,#3f5c88)}
|
|
87
|
+
.ky-turn .u-proxy-c{fill:color-mix(in srgb,var(--ky-up-blk-c) 52%,#324b72)}
|
|
88
|
+
.ky-turn .u-proxy-roof{fill:color-mix(in srgb,var(--ky-up-blk-roof) 60%,#5f7fae);stroke:var(--ky-up-blk-line);stroke-width:.6;stroke-linejoin:round}
|
|
83
89
|
.ky-turn .u-roof{fill:var(--ky-up-roof);stroke:var(--ky-up-roofline);stroke-width:.8;stroke-linejoin:round}
|
|
84
90
|
.ky-turn .u-signplate{fill:var(--ky-turn-bg);fill-opacity:.9;stroke:var(--ky-turn-rule);stroke-width:.5}
|
|
85
91
|
.ky-turn .u-sign{fill:var(--ky-turn-ink);font-family:var(--ky-font,system-ui,-apple-system,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif);font-weight:600}
|
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.45.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.45.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kyogijo-stadiums",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.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",
|