@johnmorrisdotca/kyogijo-stadiums 0.40.2 → 0.43.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 +56 -0
- package/NOTICE.md +17 -0
- package/README.md +1 -3
- package/dist/context.d.ts +141 -0
- package/dist/context.js +290 -0
- package/dist/crowd.d.ts +9 -0
- package/dist/district-entry.d.ts +14 -0
- package/dist/district-entry.js +9 -0
- package/dist/district.d.ts +54 -1
- package/dist/district.dom.d.ts +31 -0
- package/dist/district.dom.js +52 -0
- package/dist/district.js +71 -11
- package/dist/district.turn.js +30 -6
- package/dist/gates.d.ts +56 -0
- package/dist/gates.js +95 -0
- package/dist/halls.d.ts +2 -0
- package/dist/halls.js +1 -1
- package/dist/kodokan.d.ts +28 -0
- package/dist/kodokan.data.d.ts +18 -0
- package/dist/kodokan.data.js +18 -0
- package/dist/kodokan.js +34 -0
- package/dist/osm-entry.d.ts +2 -0
- package/dist/osm-entry.js +2 -0
- package/dist/osm.context.data.d.ts +10 -0
- package/dist/osm.context.data.js +1107 -0
- package/dist/outside.d.ts +43 -0
- package/dist/outside.dom.d.ts +44 -0
- package/dist/outside.dom.js +203 -0
- package/dist/outside.js +227 -0
- package/dist/outside.paint.d.ts +57 -0
- package/dist/outside.paint.js +88 -0
- package/dist/plateau-entry.d.ts +3 -0
- package/dist/plateau-entry.js +3 -0
- package/dist/plateau.credit.d.ts +5 -0
- package/dist/plateau.credit.js +7 -0
- package/dist/plateau.data.d.ts +7 -0
- package/dist/plateau.data.js +757 -0
- package/dist/stadiums.d.ts +14 -0
- package/dist/stadiums.data.js +2 -1
- package/dist/stadiums.js +2 -0
- package/dist/strings.d.ts +3 -1
- package/dist/strings.js +4 -4
- package/dist/turn.dom.d.ts +10 -0
- package/dist/turn.dom.js +21 -0
- package/dist/turn.kit.d.ts +10 -3
- package/dist/turn.kit.js +7 -0
- package/dist/turn.markup.js +7 -2
- package/dist/turn.model.d.ts +3 -0
- package/dist/turn.style.js +14 -3
- package/dist/venue.real.d.ts +22 -1
- package/dist/venue.real.js +76 -7
- package/dist/venues.d.ts +9 -1
- package/dist/venues.data.js +2 -1
- package/dist/venues.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +56 -0
- package/dist/walk.data.d.ts +4 -0
- package/dist/walk.data.js +1295 -0
- package/dist/walk.js +120 -0
- package/package.json +17 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type DistrictPlan } from "./district.ts";
|
|
2
|
+
import { OUTSIDE_LEVELS, outsideCount, type OutsideCrowd, type OutsideLevel } from "./outside.paint.ts";
|
|
3
|
+
import type { Pt } from "./crowd.ts";
|
|
4
|
+
export { OUTSIDE_LEVELS, outsideCount };
|
|
5
|
+
export type { OutsideCrowd, OutsideLevel };
|
|
6
|
+
/** What can be asked of the crowd outside a district (`DistrictOptions.outside`). Off by default. */
|
|
7
|
+
export interface OutsideOptions {
|
|
8
|
+
/** The share of the venues' seats standing outside, in percent: 0 (the default, nobody), 25, 50, 75 or 100. */
|
|
9
|
+
level?: OutsideLevel;
|
|
10
|
+
}
|
|
11
|
+
/** The most people to a square metre of walkable ground in the list at 100 percent: a crowd standing close, short of a crush. */
|
|
12
|
+
export declare const OUTSIDE_DENSITY = 3;
|
|
13
|
+
/** The distance from a venue's building at which the crowd has thinned to half what it is at the wall, in metres. */
|
|
14
|
+
export declare const OUTSIDE_REACH = 60;
|
|
15
|
+
/** Whether a number is one of the levels. */
|
|
16
|
+
export declare const isOutsideLevel: (level: unknown) => level is OutsideLevel;
|
|
17
|
+
/** The level an options object asks for: 0 when it asks for none. Throws for a level that is not one of `OUTSIDE_LEVELS`. */
|
|
18
|
+
export declare function outsideLevelOf(options: {
|
|
19
|
+
outside?: OutsideOptions;
|
|
20
|
+
} | undefined): OutsideLevel;
|
|
21
|
+
/** A small, fast generator of numbers in [0, 1) from a whole seed (mulberry32): only integer arithmetic, so it gives the same numbers in every engine. */
|
|
22
|
+
export declare function seeded(seed: number): () => number;
|
|
23
|
+
/** The mask a plan's venues stand on, if there is one: the first mask that names one of its venues. */
|
|
24
|
+
export declare const maskIdOf: (plan: Pick<DistrictPlan, "members">) => string | undefined;
|
|
25
|
+
/** Whether a plan has a crowd to show: some venue of it stands on a mask. */
|
|
26
|
+
export declare const hasOutside: (plan: Pick<DistrictPlan, "members">) => boolean;
|
|
27
|
+
/** The ids of the venues there is a walkable mask round. */
|
|
28
|
+
export declare const OUTSIDE_VENUES: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* The crowd outside a plan's venues, in the plan's frame: the people of every venue of the plan that stands on a mask (each with its seats as the plan has them), worked out once and kept. `undefined` where no venue of the plan
|
|
31
|
+
* has a mask. The crowd is the same list whatever level is shown: `outsideCount` says how many of its first people stand at a level.
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { planDistrict } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
35
|
+
* import { outsideCount, outsideCrowd } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
36
|
+
* const crowd = outsideCrowd(planDistrict({ venues: ["bc-place", "rogers-arena"] }))!;
|
|
37
|
+
* console.log(crowd.venues.map((v) => `${v.key} ${v.placed}`).join(", "), outsideCount(crowd, 50));
|
|
38
|
+
* // bc-place:cfl 54500, rogers-arena 18871 36686
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function outsideCrowd(plan: DistrictPlan): OutsideCrowd | undefined;
|
|
42
|
+
/** Whether a point of the plan's frame (feet) is on the mask's walkable ground: the inverse of what `outsideCrowd` does to a person, for a test to hold the crowd to the mask. */
|
|
43
|
+
export declare function standsOnWalkable(plan: DistrictPlan, maskId: string, [fx, fy]: Pt): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type OutsideCrowd, type OutsideLevel, type OutsideStats } from "./outside.paint.ts";
|
|
2
|
+
/** What a view tells its crowd layer. `crowd` makes (or fetches) the crowd and is called only when one is to be shown. */
|
|
3
|
+
export interface OutsideSpec {
|
|
4
|
+
level: OutsideLevel;
|
|
5
|
+
crowd: () => OutsideCrowd | undefined;
|
|
6
|
+
/** The plan's view box in feet: what the canvas covers. */
|
|
7
|
+
viewBox: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
w: number;
|
|
11
|
+
h: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** What a mounted crowd layer lets a page do and read. */
|
|
15
|
+
export interface OutsideLayer {
|
|
16
|
+
/** The level shown. */
|
|
17
|
+
readonly level: OutsideLevel;
|
|
18
|
+
setLevel(level: OutsideLevel): void;
|
|
19
|
+
/** 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
|
+
zoom(zoom: number): void;
|
|
21
|
+
/** What the last paint cost, in milliseconds, and drew: `undefined` before the first. */
|
|
22
|
+
readonly last: {
|
|
23
|
+
ms: number;
|
|
24
|
+
pixels: number;
|
|
25
|
+
stats: OutsideStats;
|
|
26
|
+
} | undefined;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
/** The zoom above which the 3D view paints the crowd finer. */
|
|
30
|
+
export declare const NEAR = 1.5;
|
|
31
|
+
/** The widest a canvas is made, in pixels: a district seen whole at 1.4 times the floor's own size, near at 2.4 times, never past this. */
|
|
32
|
+
export declare const MAX_WIDE = 3072;
|
|
33
|
+
/** The colour the crowd is painted in: the page's `--ky-dist-people` (as the district's drawing sets it, light and dark) read as red, green and blue. */
|
|
34
|
+
export declare function inkOf(from: Element): [number, number, number];
|
|
35
|
+
/**
|
|
36
|
+
* The crowd on the floor of a 3D view: a canvas as the last child of the floor element (which the view's matrices already move), as wide in pixels as the floor is, 1.4 times (2.4 times when the view is near; `layer.zoom`).
|
|
37
|
+
* `floor` is the `.ky-turn-floor` element. It is painted once now and again only when something it shows changes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function attachOutside3d(floor: HTMLElement, spec: OutsideSpec, prefetchCrowd?: boolean): OutsideLayer;
|
|
40
|
+
/**
|
|
41
|
+
* The crowd over a 2D plan: a canvas in `wrap` (a positioned element the plan's SVG is the child of), laid exactly over the box the SVG draws its view in (it keeps its shape inside a box of another, as `meet` does), as many pixels
|
|
42
|
+
* as the box has device pixels, and made again when the box changes size.
|
|
43
|
+
*/
|
|
44
|
+
export declare function attachOutsidePlan(wrap: HTMLElement, svg: SVGSVGElement, spec: OutsideSpec, prefetchCrowd?: boolean): OutsideLayer;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// The crowd outside on a page: one canvas per view, painted from the typed arrays of `outside.paint.ts` with a single `putImageData`. In the 3D view the canvas is a child of the floor, so the floor's own `matrix3d` carries it
|
|
2
|
+
// round with the turn and no frame of the turn spends anything on it; in the 2D plan it lies over the drawing's own box. It is painted when its level, its size, its zoom's detail or its colour changes, and never because a view
|
|
3
|
+
// turned or a pointer moved: nothing here animates, nothing is polled, and a reader who prefers reduced motion sees the same. The crowd (a quarter of a second of work, once) is asked for only when a level above 0 is shown or,
|
|
4
|
+
// when a page asked for one, in an idle moment after the mount, so that the first press of a level is not slow.
|
|
5
|
+
import { frameOf, rasterOutside } from "./outside.paint.js";
|
|
6
|
+
/** The zoom above which the 3D view paints the crowd finer. */
|
|
7
|
+
export const NEAR = 1.5;
|
|
8
|
+
/** The widest a canvas is made, in pixels: a district seen whole at 1.4 times the floor's own size, near at 2.4 times, never past this. */
|
|
9
|
+
export const MAX_WIDE = 3072;
|
|
10
|
+
/** The colour the crowd is painted in: the page's `--ky-dist-people` (as the district's drawing sets it, light and dark) read as red, green and blue. */
|
|
11
|
+
export function inkOf(from) {
|
|
12
|
+
const doc = from.ownerDocument;
|
|
13
|
+
const value = doc.defaultView?.getComputedStyle(from).getPropertyValue("--ky-dist-people").trim() || "#c2410c";
|
|
14
|
+
const probe = doc.createElement("canvas");
|
|
15
|
+
probe.width = probe.height = 1;
|
|
16
|
+
const g = probe.getContext("2d", { willReadFrequently: true });
|
|
17
|
+
if (g === null)
|
|
18
|
+
return [194, 65, 12];
|
|
19
|
+
g.fillStyle = "#c2410c";
|
|
20
|
+
g.fillStyle = value;
|
|
21
|
+
g.fillRect(0, 0, 1, 1);
|
|
22
|
+
const d = g.getImageData(0, 0, 1, 1).data;
|
|
23
|
+
return [d[0], d[1], d[2]];
|
|
24
|
+
}
|
|
25
|
+
/** Call `change` when the page's theme may have changed (its `data-theme`, its class, or the system's colour scheme); gives the function that stops. */
|
|
26
|
+
function watchTheme(doc, change) {
|
|
27
|
+
const media = doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)");
|
|
28
|
+
media?.addEventListener?.("change", change);
|
|
29
|
+
const seen = typeof MutationObserver === "function" ? new MutationObserver(change) : undefined;
|
|
30
|
+
seen?.observe(doc.documentElement, { attributes: true, attributeFilter: ["data-theme", "class"] });
|
|
31
|
+
return () => {
|
|
32
|
+
media?.removeEventListener?.("change", change);
|
|
33
|
+
seen?.disconnect();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The shared body of both layers: a canvas, what to paint on it and where. */
|
|
37
|
+
function layerOf(canvas, spec, colourFrom, size) {
|
|
38
|
+
const doc = canvas.ownerDocument;
|
|
39
|
+
let level = spec.level;
|
|
40
|
+
let ink = inkOf(colourFrom());
|
|
41
|
+
let drawn = "";
|
|
42
|
+
let alive = true;
|
|
43
|
+
let last;
|
|
44
|
+
const layer = {
|
|
45
|
+
zoomTier: 0,
|
|
46
|
+
get level() {
|
|
47
|
+
return level;
|
|
48
|
+
},
|
|
49
|
+
get last() {
|
|
50
|
+
return last;
|
|
51
|
+
},
|
|
52
|
+
setLevel(next) {
|
|
53
|
+
if (next === level)
|
|
54
|
+
return;
|
|
55
|
+
level = next;
|
|
56
|
+
layer.repaint();
|
|
57
|
+
},
|
|
58
|
+
zoom(zoom) {
|
|
59
|
+
const tier = zoom > NEAR ? 1 : 0;
|
|
60
|
+
if (tier === layer.zoomTier)
|
|
61
|
+
return;
|
|
62
|
+
layer.zoomTier = tier;
|
|
63
|
+
layer.repaint();
|
|
64
|
+
},
|
|
65
|
+
repaint(force = false) {
|
|
66
|
+
if (!alive)
|
|
67
|
+
return;
|
|
68
|
+
const width = Math.min(MAX_WIDE, Math.max(1, Math.round(size())));
|
|
69
|
+
if (level === 0) {
|
|
70
|
+
// Nobody: the canvas is made one pixel, so that it holds no memory.
|
|
71
|
+
if (canvas.width !== 1 || canvas.height !== 1) {
|
|
72
|
+
canvas.width = 1;
|
|
73
|
+
canvas.height = 1;
|
|
74
|
+
}
|
|
75
|
+
canvas.hidden = true;
|
|
76
|
+
drawn = "";
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const crowd = spec.crowd();
|
|
80
|
+
if (crowd === undefined)
|
|
81
|
+
return;
|
|
82
|
+
canvas.hidden = false;
|
|
83
|
+
const frame = frameOf(spec.viewBox, width);
|
|
84
|
+
const key = `${level}|${frame.width}x${frame.height}|${ink.join(",")}`;
|
|
85
|
+
if (!force && key === drawn)
|
|
86
|
+
return;
|
|
87
|
+
const started = performance.now();
|
|
88
|
+
const { rgba, stats } = rasterOutside(crowd, level, frame, ink);
|
|
89
|
+
if (canvas.width !== frame.width || canvas.height !== frame.height) {
|
|
90
|
+
canvas.width = frame.width;
|
|
91
|
+
canvas.height = frame.height;
|
|
92
|
+
}
|
|
93
|
+
const g = canvas.getContext("2d");
|
|
94
|
+
if (g === null)
|
|
95
|
+
return;
|
|
96
|
+
g.putImageData(new ImageData(rgba, frame.width, frame.height), 0, 0);
|
|
97
|
+
drawn = key;
|
|
98
|
+
last = { ms: performance.now() - started, pixels: frame.width * frame.height, stats };
|
|
99
|
+
},
|
|
100
|
+
destroy() {
|
|
101
|
+
if (!alive)
|
|
102
|
+
return;
|
|
103
|
+
alive = false;
|
|
104
|
+
stopTheme();
|
|
105
|
+
canvas.remove();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const stopTheme = watchTheme(doc, () => {
|
|
109
|
+
const next = inkOf(colourFrom());
|
|
110
|
+
if (next.join() !== ink.join()) {
|
|
111
|
+
ink = next;
|
|
112
|
+
layer.repaint();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return layer;
|
|
116
|
+
}
|
|
117
|
+
/** Ask for the crowd in an idle moment, so that the first press of a level has nothing to wait for. */
|
|
118
|
+
function prefetch(win, crowd) {
|
|
119
|
+
const idle = win?.requestIdleCallback;
|
|
120
|
+
if (typeof idle === "function")
|
|
121
|
+
idle.call(win, () => void crowd(), { timeout: 2000 });
|
|
122
|
+
else
|
|
123
|
+
win?.setTimeout(() => void crowd(), 300);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The crowd on the floor of a 3D view: a canvas as the last child of the floor element (which the view's matrices already move), as wide in pixels as the floor is, 1.4 times (2.4 times when the view is near; `layer.zoom`).
|
|
127
|
+
* `floor` is the `.ky-turn-floor` element. It is painted once now and again only when something it shows changes.
|
|
128
|
+
*/
|
|
129
|
+
export function attachOutside3d(floor, spec, prefetchCrowd = true) {
|
|
130
|
+
const doc = floor.ownerDocument;
|
|
131
|
+
const canvas = doc.createElement("canvas");
|
|
132
|
+
canvas.className = "ky-turn-outside";
|
|
133
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
134
|
+
canvas.setAttribute("data-outside", String(spec.level));
|
|
135
|
+
canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none";
|
|
136
|
+
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));
|
|
138
|
+
const set = layer.setLevel;
|
|
139
|
+
layer.setLevel = (next) => {
|
|
140
|
+
canvas.setAttribute("data-outside", String(next));
|
|
141
|
+
set(next);
|
|
142
|
+
};
|
|
143
|
+
layer.repaint();
|
|
144
|
+
if (prefetchCrowd)
|
|
145
|
+
prefetch(doc.defaultView, spec.crowd);
|
|
146
|
+
return layer;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The crowd over a 2D plan: a canvas in `wrap` (a positioned element the plan's SVG is the child of), laid exactly over the box the SVG draws its view in (it keeps its shape inside a box of another, as `meet` does), as many pixels
|
|
150
|
+
* as the box has device pixels, and made again when the box changes size.
|
|
151
|
+
*/
|
|
152
|
+
export function attachOutsidePlan(wrap, svg, spec, prefetchCrowd = true) {
|
|
153
|
+
const doc = wrap.ownerDocument;
|
|
154
|
+
const win = doc.defaultView;
|
|
155
|
+
const canvas = doc.createElement("canvas");
|
|
156
|
+
canvas.className = "ky-district-outside";
|
|
157
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
158
|
+
canvas.setAttribute("data-outside", String(spec.level));
|
|
159
|
+
canvas.style.cssText = "position:absolute;pointer-events:none";
|
|
160
|
+
wrap.append(canvas);
|
|
161
|
+
/** The box the drawing's view fills inside the SVG's own box, in CSS pixels, relative to the wrap. */
|
|
162
|
+
const place = () => {
|
|
163
|
+
const box = svg.getBoundingClientRect();
|
|
164
|
+
const home = wrap.getBoundingClientRect();
|
|
165
|
+
const k = Math.min(box.width / spec.viewBox.w, box.height / spec.viewBox.h);
|
|
166
|
+
const [w, h] = [spec.viewBox.w * k, spec.viewBox.h * k];
|
|
167
|
+
canvas.style.left = `${box.left - home.left + (box.width - w) / 2}px`;
|
|
168
|
+
canvas.style.top = `${box.top - home.top + (box.height - h) / 2}px`;
|
|
169
|
+
canvas.style.width = `${w}px`;
|
|
170
|
+
canvas.style.height = `${h}px`;
|
|
171
|
+
return { w };
|
|
172
|
+
};
|
|
173
|
+
const layer = layerOf(canvas, spec, () => svg, () => place().w * Math.min(3, win?.devicePixelRatio ?? 1));
|
|
174
|
+
const set = layer.setLevel;
|
|
175
|
+
layer.setLevel = (next) => {
|
|
176
|
+
canvas.setAttribute("data-outside", String(next));
|
|
177
|
+
set(next);
|
|
178
|
+
};
|
|
179
|
+
let frame = 0;
|
|
180
|
+
let observer;
|
|
181
|
+
if (typeof ResizeObserver === "function") {
|
|
182
|
+
observer = new ResizeObserver(() => {
|
|
183
|
+
if (frame !== 0 || win === null)
|
|
184
|
+
return;
|
|
185
|
+
frame = win.requestAnimationFrame(() => {
|
|
186
|
+
frame = 0;
|
|
187
|
+
layer.repaint();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
observer.observe(svg);
|
|
191
|
+
}
|
|
192
|
+
const destroy = layer.destroy;
|
|
193
|
+
layer.destroy = () => {
|
|
194
|
+
observer?.disconnect();
|
|
195
|
+
if (frame !== 0)
|
|
196
|
+
win?.cancelAnimationFrame(frame);
|
|
197
|
+
destroy();
|
|
198
|
+
};
|
|
199
|
+
layer.repaint();
|
|
200
|
+
if (prefetchCrowd)
|
|
201
|
+
prefetch(win, spec.crowd);
|
|
202
|
+
return layer;
|
|
203
|
+
}
|
package/dist/outside.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// The crowd outside a district's venues: people standing where people may stand (`walk.ts`: sidewalks, plazas, parks, platforms and the venues' own ground, never a carriageway, a railway, water or a building), at 0, 25, 50, 75
|
|
2
|
+
// or 100 percent of the venues' seats. One ordered list of people: a level is a prefix of it, so that moving the level up only adds people and moving it down only takes some away, and the same options always give the same
|
|
3
|
+
// list (a seeded generator, integer arithmetic and no clock). The crowd is thickest by the venues and thins with distance, never more than `OUTSIDE_DENSITY` people to a square metre. Pure: a plan in, typed arrays out; `outside.paint.ts`
|
|
4
|
+
// turns them into pixels and `outside.dom.ts` puts the pixels on a page. Roads are never drawn: the crowd's shape is what shows them.
|
|
5
|
+
import { districtMember, turnBy } from "./district.js";
|
|
6
|
+
import { FEET_PER_METRE } from "./geometry.js";
|
|
7
|
+
import { metresFrom } from "./venues.geo.js";
|
|
8
|
+
import { WALK_DATA } from "./walk.data.js";
|
|
9
|
+
import { WALK_IDS, walkIdFor, walkMask } from "./walk.js";
|
|
10
|
+
import { OUTSIDE_LEVELS, outsideCount } from "./outside.paint.js";
|
|
11
|
+
export { OUTSIDE_LEVELS, outsideCount };
|
|
12
|
+
/** The most people to a square metre of walkable ground in the list at 100 percent: a crowd standing close, short of a crush. */
|
|
13
|
+
export const OUTSIDE_DENSITY = 3;
|
|
14
|
+
/** The distance from a venue's building at which the crowd has thinned to half what it is at the wall, in metres. */
|
|
15
|
+
export const OUTSIDE_REACH = 60;
|
|
16
|
+
/** Whether a number is one of the levels. */
|
|
17
|
+
export const isOutsideLevel = (level) => OUTSIDE_LEVELS.includes(level);
|
|
18
|
+
/** The level an options object asks for: 0 when it asks for none. Throws for a level that is not one of `OUTSIDE_LEVELS`. */
|
|
19
|
+
export function outsideLevelOf(options) {
|
|
20
|
+
const level = options?.outside?.level;
|
|
21
|
+
if (level === undefined)
|
|
22
|
+
return 0;
|
|
23
|
+
if (!isOutsideLevel(level))
|
|
24
|
+
throw new Error(`outside level ${String(level)}: the levels are ${OUTSIDE_LEVELS.join(", ")}`);
|
|
25
|
+
return level;
|
|
26
|
+
}
|
|
27
|
+
/** A small, fast generator of numbers in [0, 1) from a whole seed (mulberry32): only integer arithmetic, so it gives the same numbers in every engine. */
|
|
28
|
+
export function seeded(seed) {
|
|
29
|
+
let a = seed >>> 0;
|
|
30
|
+
return () => {
|
|
31
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
32
|
+
let t = a;
|
|
33
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
34
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
35
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const seedOf = (text) => {
|
|
39
|
+
let h = 0x811c9dc5;
|
|
40
|
+
for (let i = 0; i < text.length; i += 1)
|
|
41
|
+
h = Math.imul(h ^ text.charCodeAt(i), 0x01000193);
|
|
42
|
+
return h >>> 0;
|
|
43
|
+
};
|
|
44
|
+
/** The distance from a point to a ring's outline, in metres. */
|
|
45
|
+
function edgeDistance(ring, x, y) {
|
|
46
|
+
let best = Infinity;
|
|
47
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
|
|
48
|
+
const [ax, ay] = ring[j];
|
|
49
|
+
const [dx, dy] = [ring[i][0] - ax, ring[i][1] - ay];
|
|
50
|
+
const len2 = dx * dx + dy * dy;
|
|
51
|
+
const t = len2 < 1e-12 ? 0 : Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / len2));
|
|
52
|
+
const [ex, ey] = [x - ax - t * dx, y - ay - t * dy];
|
|
53
|
+
best = Math.min(best, ex * ex + ey * ey);
|
|
54
|
+
}
|
|
55
|
+
return Math.sqrt(best);
|
|
56
|
+
}
|
|
57
|
+
/** The side of a block of cells a crowd's weight is worked out for, in cells. */
|
|
58
|
+
const BLOCK = 8;
|
|
59
|
+
const populations = new Map();
|
|
60
|
+
/**
|
|
61
|
+
* The people of every venue on a mask: for each venue in turn, its seats' worth, placed on the cells nobody has filled, thick by its building and thinning with the distance (a weight 1 / (1 + (d / reach)^2), scaled until the
|
|
62
|
+
* seats are placed without a cell going past the density), then every venue's people mixed in one list so that any prefix is a random share of each. A venue the ground cannot hold has `placed` below `seats`.
|
|
63
|
+
*/
|
|
64
|
+
function populate(mask, wanted, id) {
|
|
65
|
+
const { cell, width, height, cells } = mask;
|
|
66
|
+
const perCell = OUTSIDE_DENSITY * cell * cell;
|
|
67
|
+
const room = new Float32Array(width * height);
|
|
68
|
+
const standing = [];
|
|
69
|
+
for (let i = 0; i < cells.length; i += 1)
|
|
70
|
+
if (cells[i] === 1) {
|
|
71
|
+
room[i] = perCell;
|
|
72
|
+
standing.push(i);
|
|
73
|
+
}
|
|
74
|
+
const bw = Math.ceil(width / BLOCK);
|
|
75
|
+
// The blocks with any ground to stand on: the only ones a weight is worked out for.
|
|
76
|
+
const used = new Uint8Array(bw * Math.ceil(height / BLOCK));
|
|
77
|
+
for (const i of standing)
|
|
78
|
+
used[Math.floor(Math.floor(i / width) / BLOCK) * bw + Math.floor((i % width) / BLOCK)] = 1;
|
|
79
|
+
const lists = [];
|
|
80
|
+
const venues = [];
|
|
81
|
+
for (const venue of wanted) {
|
|
82
|
+
// The weight of each block: a function of the distance of its middle from the venue's building.
|
|
83
|
+
const weight = new Float32Array(bw * Math.ceil(height / BLOCK));
|
|
84
|
+
for (let br = 0; br < Math.ceil(height / BLOCK); br += 1) {
|
|
85
|
+
for (let bc = 0; bc < bw; bc += 1) {
|
|
86
|
+
if (!used[br * bw + bc])
|
|
87
|
+
continue;
|
|
88
|
+
const d = edgeDistance(venue.ring, mask.x0 + (bc + 0.5) * BLOCK * cell, mask.y0 + (br + 0.5) * BLOCK * cell);
|
|
89
|
+
weight[br * bw + bc] = 1 / (1 + (d / OUTSIDE_REACH) ** 2);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const w = new Float32Array(standing.length);
|
|
93
|
+
standing.forEach((i, k) => {
|
|
94
|
+
w[k] = weight[Math.floor(Math.floor(i / width) / BLOCK) * bw + Math.floor((i % width) / BLOCK)];
|
|
95
|
+
});
|
|
96
|
+
// The scale that places the seats (and a little over, which the shuffle's cut takes off again): found by bisection on the sum of what each cell takes.
|
|
97
|
+
const target = venue.seats * 1.02;
|
|
98
|
+
const taken = (lambda) => {
|
|
99
|
+
let sum = 0;
|
|
100
|
+
for (let k = 0; k < standing.length; k += 1)
|
|
101
|
+
sum += Math.min(room[standing[k]], lambda * w[k]);
|
|
102
|
+
return sum;
|
|
103
|
+
};
|
|
104
|
+
let [lo, hi] = [0, 200];
|
|
105
|
+
if (taken(hi) < target)
|
|
106
|
+
lo = hi;
|
|
107
|
+
else
|
|
108
|
+
for (let n = 0; n < 24; n += 1) {
|
|
109
|
+
const mid = (lo + hi) / 2;
|
|
110
|
+
if (taken(mid) < target)
|
|
111
|
+
lo = mid;
|
|
112
|
+
else
|
|
113
|
+
hi = mid;
|
|
114
|
+
}
|
|
115
|
+
const lambda = taken(hi) < target ? hi : (lo + hi) / 2;
|
|
116
|
+
const random = seeded(seedOf(`${id}|${venue.key}`));
|
|
117
|
+
const px = [];
|
|
118
|
+
const py = [];
|
|
119
|
+
for (let k = 0; k < standing.length; k += 1) {
|
|
120
|
+
const i = standing[k];
|
|
121
|
+
const want = Math.min(room[i], lambda * w[k]);
|
|
122
|
+
const n = Math.floor(want + random());
|
|
123
|
+
if (n === 0)
|
|
124
|
+
continue;
|
|
125
|
+
room[i] = Math.max(0, room[i] - n);
|
|
126
|
+
const [col, row] = [i % width, Math.floor(i / width)];
|
|
127
|
+
for (let m = 0; m < n; m += 1) {
|
|
128
|
+
// Inside the middle of the cell, so that a person is never on its edge.
|
|
129
|
+
px.push(mask.x0 + (col + 0.15 + 0.7 * random()) * cell);
|
|
130
|
+
py.push(mask.y0 + (row + 0.15 + 0.7 * random()) * cell);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// A random order, so that any prefix is a random share of the venue's people; the cut at the seats takes off the few over.
|
|
134
|
+
for (let i = px.length - 1; i > 0; i -= 1) {
|
|
135
|
+
const j = Math.floor(random() * (i + 1));
|
|
136
|
+
[px[i], px[j]] = [px[j], px[i]];
|
|
137
|
+
[py[i], py[j]] = [py[j], py[i]];
|
|
138
|
+
}
|
|
139
|
+
const placed = Math.min(venue.seats, px.length);
|
|
140
|
+
lists.push({ x: px.slice(0, placed), y: py.slice(0, placed) });
|
|
141
|
+
venues.push({ key: venue.key, seats: venue.seats, placed });
|
|
142
|
+
}
|
|
143
|
+
// The venues mixed in one list: the next person is the venue's whose share is the smallest so far.
|
|
144
|
+
const total = lists.reduce((s, l) => s + l.x.length, 0);
|
|
145
|
+
const x = new Float32Array(total);
|
|
146
|
+
const y = new Float32Array(total);
|
|
147
|
+
const venue = new Uint8Array(total);
|
|
148
|
+
const given = new Array(lists.length).fill(0);
|
|
149
|
+
for (let k = 0; k < total; k += 1) {
|
|
150
|
+
let pick = -1;
|
|
151
|
+
lists.forEach((l, v) => {
|
|
152
|
+
if (given[v] >= l.x.length)
|
|
153
|
+
return;
|
|
154
|
+
if (pick < 0 || given[v] * lists[pick].x.length < given[pick] * l.x.length)
|
|
155
|
+
pick = v;
|
|
156
|
+
});
|
|
157
|
+
x[k] = lists[pick].x[given[pick]];
|
|
158
|
+
y[k] = lists[pick].y[given[pick]];
|
|
159
|
+
venue[k] = pick;
|
|
160
|
+
given[pick] += 1;
|
|
161
|
+
}
|
|
162
|
+
return { venues, x, y, venue, count: total };
|
|
163
|
+
}
|
|
164
|
+
/** The mask a plan's venues stand on, if there is one: the first mask that names one of its venues. */
|
|
165
|
+
export const maskIdOf = (plan) => walkIdFor(plan.members.map((m) => m.member.id));
|
|
166
|
+
/** Whether a plan has a crowd to show: some venue of it stands on a mask. */
|
|
167
|
+
export const hasOutside = (plan) => maskIdOf(plan) !== undefined;
|
|
168
|
+
/** The ids of the venues there is a walkable mask round. */
|
|
169
|
+
export const OUTSIDE_VENUES = Object.freeze([...new Set(WALK_IDS.flatMap((id) => WALK_DATA[id].venues.map((key) => key.split(":")[0])))]);
|
|
170
|
+
const placedCache = new WeakMap();
|
|
171
|
+
/**
|
|
172
|
+
* The crowd outside a plan's venues, in the plan's frame: the people of every venue of the plan that stands on a mask (each with its seats as the plan has them), worked out once and kept. `undefined` where no venue of the plan
|
|
173
|
+
* has a mask. The crowd is the same list whatever level is shown: `outsideCount` says how many of its first people stand at a level.
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* import { planDistrict } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
177
|
+
* import { outsideCount, outsideCrowd } from "@johnmorrisdotca/kyogijo-stadiums/district";
|
|
178
|
+
* const crowd = outsideCrowd(planDistrict({ venues: ["bc-place", "rogers-arena"] }))!;
|
|
179
|
+
* console.log(crowd.venues.map((v) => `${v.key} ${v.placed}`).join(", "), outsideCount(crowd, 50));
|
|
180
|
+
* // bc-place:cfl 54500, rogers-arena 18871 36686
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function outsideCrowd(plan) {
|
|
184
|
+
const cached = placedCache.get(plan);
|
|
185
|
+
if (cached !== undefined)
|
|
186
|
+
return cached ?? undefined;
|
|
187
|
+
const maskId = maskIdOf(plan);
|
|
188
|
+
if (maskId === undefined) {
|
|
189
|
+
placedCache.set(plan, null);
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const data = WALK_DATA[maskId];
|
|
193
|
+
const ids = data.venues.map((key) => key.split(":")[0]);
|
|
194
|
+
const present = ids.map((id) => plan.members.find((m) => m.member.id === id)).filter((m) => m !== undefined);
|
|
195
|
+
const extras = { buildings: data.venues.map((key) => metresFrom(data.origin, districtMember(key).ring)), apron: 5 };
|
|
196
|
+
const mask = walkMask(maskId, extras);
|
|
197
|
+
const wanted = present.map((m) => ({ key: m.member.key, seats: m.member.seats, ring: extras.buildings[ids.indexOf(m.member.id)] }));
|
|
198
|
+
const kept = `${maskId}|${wanted.map((w) => `${w.key}:${w.seats}`).join(",")}`;
|
|
199
|
+
let population = populations.get(kept);
|
|
200
|
+
if (population === undefined) {
|
|
201
|
+
population = populate(mask, wanted, maskId);
|
|
202
|
+
populations.set(kept, population);
|
|
203
|
+
}
|
|
204
|
+
// Into the plan's frame: the mask's metres from its origin, to the plan's origin, to feet with the page's y going down, turned with the map.
|
|
205
|
+
const [shiftX, shiftY] = metresFrom(plan.origin, [data.origin])[0];
|
|
206
|
+
const x = new Float32Array(population.count);
|
|
207
|
+
const y = new Float32Array(population.count);
|
|
208
|
+
for (let i = 0; i < population.count; i += 1) {
|
|
209
|
+
const [px, py] = turnBy([(shiftX + population.x[i]) * FEET_PER_METRE, -(shiftY + population.y[i]) * FEET_PER_METRE], plan.turn);
|
|
210
|
+
x[i] = px;
|
|
211
|
+
y[i] = py;
|
|
212
|
+
}
|
|
213
|
+
const crowd = { mask: maskId, venues: population.venues, x, y, venue: population.venue, count: population.count };
|
|
214
|
+
placedCache.set(plan, crowd);
|
|
215
|
+
return crowd;
|
|
216
|
+
}
|
|
217
|
+
/** Whether a point of the plan's frame (feet) is on the mask's walkable ground: the inverse of what `outsideCrowd` does to a person, for a test to hold the crowd to the mask. */
|
|
218
|
+
export function standsOnWalkable(plan, maskId, [fx, fy]) {
|
|
219
|
+
const data = WALK_DATA[maskId];
|
|
220
|
+
const [shiftX, shiftY] = metresFrom(plan.origin, [data.origin])[0];
|
|
221
|
+
const [ux, uy] = turnBy([fx, fy], -plan.turn);
|
|
222
|
+
const mask = walkMask(maskId, { buildings: data.venues.map((key) => metresFrom(data.origin, districtMember(key).ring)), apron: 5 });
|
|
223
|
+
const [mx, my] = [ux / FEET_PER_METRE - shiftX, -uy / FEET_PER_METRE - shiftY];
|
|
224
|
+
const col = Math.floor((mx - mask.x0) / mask.cell);
|
|
225
|
+
const row = Math.floor((my - mask.y0) / mask.cell);
|
|
226
|
+
return col >= 0 && row >= 0 && col < mask.width && row < mask.height && mask.cells[row * mask.width + col] === 1;
|
|
227
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The crowds outside there are: a share of the venues' seats, in percent. */
|
|
2
|
+
export declare const OUTSIDE_LEVELS: readonly [0, 25, 50, 75, 100];
|
|
3
|
+
export type OutsideLevel = (typeof OUTSIDE_LEVELS)[number];
|
|
4
|
+
/** The people of one district, in the plan's frame: where each stands (feet, as the plan's drawing has them), which venue's crowd it is, and how many the venues' seats asked for and the walkable ground could hold. */
|
|
5
|
+
export interface OutsideCrowd {
|
|
6
|
+
/** The mask the crowd stands on. */
|
|
7
|
+
mask: string;
|
|
8
|
+
venues: {
|
|
9
|
+
key: string;
|
|
10
|
+
seats: number;
|
|
11
|
+
placed: number;
|
|
12
|
+
}[];
|
|
13
|
+
/** The people, in order: venues interleaved, so that a prefix holds each venue's share. */
|
|
14
|
+
x: Float32Array;
|
|
15
|
+
y: Float32Array;
|
|
16
|
+
/** The index in `venues` of each person's venue. */
|
|
17
|
+
venue: Uint8Array;
|
|
18
|
+
/** How many people the list holds. */
|
|
19
|
+
count: number;
|
|
20
|
+
}
|
|
21
|
+
/** How many people of the list stand outside at a level: the list is ordered, so these are its first. */
|
|
22
|
+
export declare const outsideCount: (crowd: Pick<OutsideCrowd, "count">, level: OutsideLevel) => number;
|
|
23
|
+
/** A person's width in metres. */
|
|
24
|
+
export declare const PERSON_METRES = 0.55;
|
|
25
|
+
/** Where the crowd's pixels lie: a buffer `width` by `height`, with `(x0, y0)` the plan's feet at its top left corner and `scale` its pixels to a foot. */
|
|
26
|
+
export interface OutsideFrame {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
x0: number;
|
|
30
|
+
y0: number;
|
|
31
|
+
scale: number;
|
|
32
|
+
}
|
|
33
|
+
/** A person is drawn as a disc where they are wider than this many pixels, and as one pixel of density where they are not. */
|
|
34
|
+
export declare const DOTS_FROM = 1.2;
|
|
35
|
+
/** What painting a level cost and drew: for the docs and the tests to quote. */
|
|
36
|
+
export interface OutsideStats {
|
|
37
|
+
people: number;
|
|
38
|
+
/** `dots` when a person is wider than a pixel and a fifth, else `density`. */
|
|
39
|
+
mode: "dots" | "density";
|
|
40
|
+
/** Pixels stamped, counting each time a person lands on one. */
|
|
41
|
+
stamps: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The people of a crowd at a level (its first `outsideCount`) as RGBA bytes in a frame, in one colour with the alpha that says how many stand on each pixel. `ink` is `[red, green, blue]`.
|
|
45
|
+
* It gives the bytes and what it drew.
|
|
46
|
+
*/
|
|
47
|
+
export declare function rasterOutside(crowd: OutsideCrowd, level: OutsideLevel, frame: OutsideFrame, ink: readonly [number, number, number]): {
|
|
48
|
+
rgba: Uint8ClampedArray;
|
|
49
|
+
stats: OutsideStats;
|
|
50
|
+
};
|
|
51
|
+
/** The frame of a buffer that covers a view box of the plan (feet) in `width` pixels, as high as the box's shape makes it. */
|
|
52
|
+
export declare function frameOf(view: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
w: number;
|
|
56
|
+
h: number;
|
|
57
|
+
}, width: number): OutsideFrame;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The crowd outside as pixels: every person of a level's prefix stamped into one buffer, as a typed array and a loop, never an element, a path or a call to the canvas per person. Pure (no page, no canvas): a crowd and a frame in,
|
|
2
|
+
// RGBA bytes out, so that the same bytes come out in Node and in a browser, and a test can hold them to a budget. A person is 0.55 metres across; where that is under a pixel and a fifth (a district seen whole) a person is one
|
|
3
|
+
// pixel and the crowd reads as density, where it is more (a venue seen near) each is a small disc. Nothing animates: a crowd is painted when its level, its size or its colour changes, not when the view turns.
|
|
4
|
+
import { FEET_PER_METRE } from "./geometry.js";
|
|
5
|
+
/** The crowds outside there are: a share of the venues' seats, in percent. */
|
|
6
|
+
export const OUTSIDE_LEVELS = [0, 25, 50, 75, 100];
|
|
7
|
+
/** How many people of the list stand outside at a level: the list is ordered, so these are its first. */
|
|
8
|
+
export const outsideCount = (crowd, level) => Math.round((crowd.count * level) / 100);
|
|
9
|
+
/** A person's width in metres. */
|
|
10
|
+
export const PERSON_METRES = 0.55;
|
|
11
|
+
/** A person is drawn as a disc where they are wider than this many pixels, and as one pixel of density where they are not. */
|
|
12
|
+
export const DOTS_FROM = 1.2;
|
|
13
|
+
/** The disc a person is, as offsets from its pixel, by radius in pixels (0 is the pixel itself). */
|
|
14
|
+
const DISCS = new Map();
|
|
15
|
+
function discOf(radius) {
|
|
16
|
+
const kept = DISCS.get(radius);
|
|
17
|
+
if (kept !== undefined)
|
|
18
|
+
return kept;
|
|
19
|
+
const offsets = [];
|
|
20
|
+
for (let dy = -radius; dy <= radius; dy += 1)
|
|
21
|
+
for (let dx = -radius; dx <= radius; dx += 1)
|
|
22
|
+
if (dx * dx + dy * dy <= radius * radius + radius * 0.5)
|
|
23
|
+
offsets.push(dx, dy);
|
|
24
|
+
const disc = Int16Array.from(offsets);
|
|
25
|
+
DISCS.set(radius, disc);
|
|
26
|
+
return disc;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The people of a crowd at a level (its first `outsideCount`) as RGBA bytes in a frame, in one colour with the alpha that says how many stand on each pixel. `ink` is `[red, green, blue]`.
|
|
30
|
+
* It gives the bytes and what it drew.
|
|
31
|
+
*/
|
|
32
|
+
export function rasterOutside(crowd, level, frame, ink) {
|
|
33
|
+
const { width, height, x0, y0, scale } = frame;
|
|
34
|
+
const people = outsideCount(crowd, level);
|
|
35
|
+
const rgba = new Uint8ClampedArray(width * height * 4);
|
|
36
|
+
const metresPerPixel = 1 / (scale * FEET_PER_METRE);
|
|
37
|
+
const across = PERSON_METRES / metresPerPixel;
|
|
38
|
+
const dots = across > DOTS_FROM;
|
|
39
|
+
const radius = dots ? Math.min(8, Math.max(1, Math.round(across / 2))) : 0;
|
|
40
|
+
const disc = discOf(radius);
|
|
41
|
+
const cover = new Uint8Array(width * height);
|
|
42
|
+
let stamps = 0;
|
|
43
|
+
for (let i = 0; i < people; i += 1) {
|
|
44
|
+
const px = Math.floor((crowd.x[i] - x0) * scale);
|
|
45
|
+
const py = Math.floor((crowd.y[i] - y0) * scale);
|
|
46
|
+
if (radius === 0) {
|
|
47
|
+
if (px >= 0 && py >= 0 && px < width && py < height) {
|
|
48
|
+
const at = py * width + px;
|
|
49
|
+
if (cover[at] < 255)
|
|
50
|
+
cover[at] += 1;
|
|
51
|
+
stamps += 1;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (let k = 0; k < disc.length; k += 2) {
|
|
56
|
+
const [sx, sy] = [px + disc[k], py + disc[k + 1]];
|
|
57
|
+
if (sx < 0 || sy < 0 || sx >= width || sy >= height)
|
|
58
|
+
continue;
|
|
59
|
+
const at = sy * width + sx;
|
|
60
|
+
if (cover[at] < 255)
|
|
61
|
+
cover[at] += 1;
|
|
62
|
+
stamps += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// The alpha of a pixel with n people on it: one person covers `first` of it, and each more covers the rest by that share again, so that a dense crowd saturates and a sparse one stays light.
|
|
66
|
+
const area = metresPerPixel * metresPerPixel;
|
|
67
|
+
const first = dots ? 0.82 : Math.min(0.7, Math.max(0.16, (Math.PI / 4) * PERSON_METRES * PERSON_METRES / area));
|
|
68
|
+
const alpha = new Uint8Array(256);
|
|
69
|
+
for (let n = 1; n < 256; n += 1)
|
|
70
|
+
alpha[n] = Math.round(255 * (1 - (1 - first) ** n));
|
|
71
|
+
const [r, g, b] = ink;
|
|
72
|
+
for (let at = 0; at < cover.length; at += 1) {
|
|
73
|
+
const n = cover[at];
|
|
74
|
+
if (n === 0)
|
|
75
|
+
continue;
|
|
76
|
+
const o = at * 4;
|
|
77
|
+
rgba[o] = r;
|
|
78
|
+
rgba[o + 1] = g;
|
|
79
|
+
rgba[o + 2] = b;
|
|
80
|
+
rgba[o + 3] = alpha[n];
|
|
81
|
+
}
|
|
82
|
+
return { rgba, stats: { people, mode: dots ? "dots" : "density", stamps } };
|
|
83
|
+
}
|
|
84
|
+
/** The frame of a buffer that covers a view box of the plan (feet) in `width` pixels, as high as the box's shape makes it. */
|
|
85
|
+
export function frameOf(view, width) {
|
|
86
|
+
const scale = width / view.w;
|
|
87
|
+
return { width: Math.round(width), height: Math.max(1, Math.round(view.h * scale)), x0: view.x, y0: view.y, scale };
|
|
88
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { PLATEAU_ATTRIBUTION, PLATEAU_AREAS, TOKYO_DOME_CITY } from "./plateau.data.ts";
|
|
2
|
+
export { CONTEXT_BLOCKS, CONTEXT_LANDMARK, CONTEXT_LANDMARK_REACH, CONTEXT_REACH, CONTEXT_TONES, decodeBuilding } from "./context.ts";
|
|
3
|
+
export type { ContextArea } from "./context.ts";
|