@a2f0/skyline 0.0.0-stage → 0.1.2
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/NOTICE.md +20 -0
- package/README.md +52 -2
- package/docs/package.md +224 -0
- package/docs/skyline-geography.md +503 -0
- package/lib/models/aon-center.d.ts +4 -0
- package/lib/models/aon-center.js +60 -0
- package/lib/models/aon-geographic.d.ts +9 -0
- package/lib/models/aon-geographic.js +35 -0
- package/lib/models/aon-tower.d.ts +23 -0
- package/lib/models/aon-tower.js +130 -0
- package/lib/models/blue-cross-geographic.d.ts +14 -0
- package/lib/models/blue-cross-geographic.js +248 -0
- package/lib/models/borg-warner-geographic.d.ts +20 -0
- package/lib/models/borg-warner-geographic.js +143 -0
- package/lib/models/buckingham-geographic.d.ts +10 -0
- package/lib/models/buckingham-geographic.js +121 -0
- package/lib/models/building-kit.d.ts +84 -0
- package/lib/models/building-kit.js +358 -0
- package/lib/models/celebrations.d.ts +39 -0
- package/lib/models/celebrations.js +44 -0
- package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
- package/lib/models/chicago-athletic-association-geographic.js +173 -0
- package/lib/models/crain-communications.d.ts +5 -0
- package/lib/models/crain-communications.js +75 -0
- package/lib/models/crain-geographic.d.ts +10 -0
- package/lib/models/crain-geographic.js +69 -0
- package/lib/models/crain-tower.d.ts +32 -0
- package/lib/models/crain-tower.js +378 -0
- package/lib/models/facade-grid.d.ts +18 -0
- package/lib/models/facade-grid.js +176 -0
- package/lib/models/gage-geographic.d.ts +19 -0
- package/lib/models/gage-geographic.js +154 -0
- package/lib/models/heritage-at-millennium-park.d.ts +3 -0
- package/lib/models/heritage-at-millennium-park.js +284 -0
- package/lib/models/heritage-geographic.d.ts +11 -0
- package/lib/models/heritage-geographic.js +208 -0
- package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
- package/lib/models/hyatt-west-tower-geographic.js +86 -0
- package/lib/models/keith-ascher-geographic.d.ts +29 -0
- package/lib/models/keith-ascher-geographic.js +110 -0
- package/lib/models/kemper-geographic.d.ts +8 -0
- package/lib/models/kemper-geographic.js +153 -0
- package/lib/models/kemper.d.ts +2 -0
- package/lib/models/kemper.js +78 -0
- package/lib/models/lake-view-geographic.d.ts +15 -0
- package/lib/models/lake-view-geographic.js +134 -0
- package/lib/models/maclean-center-geographic.d.ts +20 -0
- package/lib/models/maclean-center-geographic.js +162 -0
- package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
- package/lib/models/michigan-boulevard-geographic.js +157 -0
- package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
- package/lib/models/michigan-plaza-south-geographic.js +84 -0
- package/lib/models/michigan-plaza-south.d.ts +2 -0
- package/lib/models/michigan-plaza-south.js +68 -0
- package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
- package/lib/models/millennium-park-plaza-geographic.js +103 -0
- package/lib/models/monroe-geographic.d.ts +19 -0
- package/lib/models/monroe-geographic.js +177 -0
- package/lib/models/north-michigan-180-geographic.d.ts +14 -0
- package/lib/models/north-michigan-180-geographic.js +117 -0
- package/lib/models/north-wabash-geographic.d.ts +14 -0
- package/lib/models/north-wabash-geographic.js +122 -0
- package/lib/models/on-the-park-geographic.d.ts +11 -0
- package/lib/models/on-the-park-geographic.js +245 -0
- package/lib/models/one-prudential-geographic.d.ts +12 -0
- package/lib/models/one-prudential-geographic.js +55 -0
- package/lib/models/one-prudential-plaza.d.ts +5 -0
- package/lib/models/one-prudential-plaza.js +72 -0
- package/lib/models/one-prudential-tower.d.ts +45 -0
- package/lib/models/one-prudential-tower.js +246 -0
- package/lib/models/peoples-gas-geographic.d.ts +23 -0
- package/lib/models/peoples-gas-geographic.js +250 -0
- package/lib/models/railway-exchange-geographic.d.ts +18 -0
- package/lib/models/railway-exchange-geographic.js +176 -0
- package/lib/models/river-plaza-geographic.d.ts +16 -0
- package/lib/models/river-plaza-geographic.js +104 -0
- package/lib/models/sheraton-grand-geographic.d.ts +24 -0
- package/lib/models/sheraton-grand-geographic.js +112 -0
- package/lib/models/six-north-michigan-geographic.d.ts +16 -0
- package/lib/models/six-north-michigan-geographic.js +180 -0
- package/lib/models/skyline-geography-data.d.ts +44 -0
- package/lib/models/skyline-geography-data.js +607 -0
- package/lib/models/skyline-geography.d.ts +23 -0
- package/lib/models/skyline-geography.js +322 -0
- package/lib/models/swissotel-geographic.d.ts +24 -0
- package/lib/models/swissotel-geographic.js +79 -0
- package/lib/models/three-illinois-center-geographic.d.ts +26 -0
- package/lib/models/three-illinois-center-geographic.js +86 -0
- package/lib/models/trump-geographic.d.ts +14 -0
- package/lib/models/trump-geographic.js +51 -0
- package/lib/models/trump-international-tower.d.ts +4 -0
- package/lib/models/trump-international-tower.js +84 -0
- package/lib/models/trump-tower.d.ts +43 -0
- package/lib/models/trump-tower.js +229 -0
- package/lib/models/two-illinois-center-geographic.d.ts +26 -0
- package/lib/models/two-illinois-center-geographic.js +85 -0
- package/lib/models/two-prudential-geographic.d.ts +25 -0
- package/lib/models/two-prudential-geographic.js +47 -0
- package/lib/models/two-prudential-plaza.d.ts +8 -0
- package/lib/models/two-prudential-plaza.js +55 -0
- package/lib/models/two-prudential-tower.d.ts +53 -0
- package/lib/models/two-prudential-tower.js +441 -0
- package/lib/models/university-club-geographic.d.ts +24 -0
- package/lib/models/university-club-geographic.js +217 -0
- package/lib/models/willoughby-tower-geographic.d.ts +17 -0
- package/lib/models/willoughby-tower-geographic.js +195 -0
- package/lib/models/window-illumination.d.ts +14 -0
- package/lib/models/window-illumination.js +64 -0
- package/lib/package-assets.d.ts +5 -0
- package/lib/package-assets.js +9 -0
- package/lib/skyline-package.d.ts +23 -0
- package/lib/skyline-package.js +25 -0
- package/lib/skyline-scene.d.ts +25 -0
- package/lib/skyline-scene.js +86 -0
- package/lib/study-types.d.ts +49 -0
- package/lib/study-types.js +1 -0
- package/lib/vendor/three-r186.d.ts +1 -0
- package/lib/vendor/three-r186.js +1 -0
- package/package.json +64 -4
- package/site/building-detail.html +54 -0
- package/site/building-detail.js +84 -0
- package/site/building-study.html +58 -0
- package/site/building-study.js +3 -0
- package/site/index.html +407 -0
- package/site/models/aon-center.js +60 -0
- package/site/models/aon-geographic.js +35 -0
- package/site/models/aon-tower.js +130 -0
- package/site/models/blue-cross-geographic.js +248 -0
- package/site/models/borg-warner-geographic.js +143 -0
- package/site/models/buckingham-geographic.js +121 -0
- package/site/models/building-kit.js +358 -0
- package/site/models/celebrations.js +44 -0
- package/site/models/chicago-athletic-association-geographic.js +173 -0
- package/site/models/crain-communications.js +75 -0
- package/site/models/crain-geographic.js +69 -0
- package/site/models/crain-reference.svg +10 -0
- package/site/models/crain-tower.js +378 -0
- package/site/models/facade-grid.js +176 -0
- package/site/models/gage-geographic.js +154 -0
- package/site/models/heritage-at-millennium-park.js +284 -0
- package/site/models/heritage-geographic.js +208 -0
- package/site/models/hyatt-west-tower-geographic.js +86 -0
- package/site/models/keith-ascher-geographic.js +110 -0
- package/site/models/kemper-geographic.js +153 -0
- package/site/models/kemper.js +78 -0
- package/site/models/lake-view-geographic.js +134 -0
- package/site/models/maclean-center-geographic.js +162 -0
- package/site/models/michigan-boulevard-geographic.js +157 -0
- package/site/models/michigan-plaza-south-geographic.js +84 -0
- package/site/models/michigan-plaza-south.js +68 -0
- package/site/models/millennium-park-plaza-geographic.js +103 -0
- package/site/models/monroe-geographic.js +177 -0
- package/site/models/north-michigan-180-geographic.js +117 -0
- package/site/models/north-wabash-geographic.js +122 -0
- package/site/models/on-the-park-geographic.js +245 -0
- package/site/models/one-prudential-geographic.js +55 -0
- package/site/models/one-prudential-plaza.js +72 -0
- package/site/models/one-prudential-tower.js +246 -0
- package/site/models/peoples-gas-geographic.js +250 -0
- package/site/models/railway-exchange-geographic.js +176 -0
- package/site/models/river-plaza-geographic.js +104 -0
- package/site/models/sheraton-grand-geographic.js +112 -0
- package/site/models/six-north-michigan-geographic.js +180 -0
- package/site/models/skyline-geography-data.js +607 -0
- package/site/models/skyline-geography.js +322 -0
- package/site/models/skyline-panorama.svg +3491 -0
- package/site/models/skyline-reference.svg +3090 -0
- package/site/models/swissotel-geographic.js +79 -0
- package/site/models/three-illinois-center-geographic.js +86 -0
- package/site/models/trump-geographic.js +51 -0
- package/site/models/trump-international-tower.js +84 -0
- package/site/models/trump-tower.js +229 -0
- package/site/models/two-illinois-center-geographic.js +85 -0
- package/site/models/two-prudential-geographic.js +47 -0
- package/site/models/two-prudential-plaza.js +55 -0
- package/site/models/two-prudential-tower.js +441 -0
- package/site/models/university-club-geographic.js +217 -0
- package/site/models/willoughby-tower-geographic.js +195 -0
- package/site/models/window-illumination.js +64 -0
- package/site/skyline-3d.html +92 -0
- package/site/skyline-3d.js +333 -0
- package/site/skyline-animated.svg +18795 -0
- package/site/skyline-comparison.js +97 -0
- package/site/skyline-original-fit.svg +18312 -0
- package/site/skyline-scene.js +86 -0
- package/site/skyline-study.html +83 -0
- package/site/skyline-study.js +109 -0
- package/site/skyline-webgl.html +667 -0
- package/site/stars.svg +176 -0
- package/site/study-loader.js +20 -0
- package/site/study-types.js +1 -0
- package/site/study-viewer.js +605 -0
- package/site/study.css +204 -0
- package/site/vendor/THREE-LICENSE.txt +21 -0
- package/site/vendor/three-r186.js +4228 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createBuildingStudy } from "./study-viewer.js";
|
|
2
|
+
import { createGeographicSkyline } from "./skyline-scene.js";
|
|
3
|
+
import { createGeographicRoads } from "./models/skyline-geography.js";
|
|
4
|
+
import { celebrations } from "./models/celebrations.js";
|
|
5
|
+
// The skyline viewer's full-screen 3D mode: every mapped building, seen through the
|
|
6
|
+
// drawing's own camera and framed as index.html frames the drawing, so each tower stands
|
|
7
|
+
// where its drawn one does until the camera moves.
|
|
8
|
+
const { models, ground, settings, drawingView, comparisonViews, labels } = createGeographicSkyline();
|
|
9
|
+
// The mapped streets show as grey roadways across the whole platform. The study's
|
|
10
|
+
// centerlines, footprint outlines and grid are annotations; the outlines return with the
|
|
11
|
+
// footprints control.
|
|
12
|
+
ground.group.children.forEach((child) => { child.visible = false; });
|
|
13
|
+
const { platform } = settings;
|
|
14
|
+
const roads = createGeographicRoads({
|
|
15
|
+
x: [platform.x - platform.width / 2, platform.x + platform.width / 2],
|
|
16
|
+
z: [platform.z - platform.depth / 2, platform.z + platform.depth / 2],
|
|
17
|
+
}, 0x2c2c2c);
|
|
18
|
+
const { azimuth } = drawingView;
|
|
19
|
+
const viewer = createBuildingStudy({
|
|
20
|
+
models,
|
|
21
|
+
extras: [ground.group, roads],
|
|
22
|
+
defaultView: "skyline",
|
|
23
|
+
// The skyline study's presets, so reduced motion, which stops dragging, still leaves
|
|
24
|
+
// views to step between, and its ground plan and height comparison, which name each
|
|
25
|
+
// building.
|
|
26
|
+
views: {
|
|
27
|
+
skyline: drawingView,
|
|
28
|
+
quarter: { azimuth: azimuth + 0.3, polar: Math.PI / 2 - 0.28, label: "three-quarter view" },
|
|
29
|
+
side: { azimuth: azimuth + Math.PI / 2, polar: Math.PI / 2 - 0.14, label: "side view" },
|
|
30
|
+
...comparisonViews,
|
|
31
|
+
},
|
|
32
|
+
labels,
|
|
33
|
+
fov: 6,
|
|
34
|
+
...settings,
|
|
35
|
+
platform: { ...settings.platform, color: 0x161616 },
|
|
36
|
+
near: 10,
|
|
37
|
+
far: 10000,
|
|
38
|
+
minimumDistanceRatio: 0.1,
|
|
39
|
+
// Twice the viewer's default reach, so the whole mapped city sits well inside the frame
|
|
40
|
+
// from every view.
|
|
41
|
+
maximumDistanceRatio: 4,
|
|
42
|
+
minimumZoom: 0.25,
|
|
43
|
+
maximumZoom: 24,
|
|
44
|
+
enablePan: true,
|
|
45
|
+
minimumCameraHeight: 1,
|
|
46
|
+
});
|
|
47
|
+
// A toolbar badge applies one celebration across the skyline, using each
|
|
48
|
+
// building's own supported wording. Context menus change only their building.
|
|
49
|
+
const illuminated = models.filter((model) => model.illumination);
|
|
50
|
+
const celebrationStatus = document.querySelector("#celebration-status");
|
|
51
|
+
const celebrationAnnouncement = document.querySelector("#celebration-announcement");
|
|
52
|
+
const celebrationButtons = [...document.querySelectorAll("[data-celebration]")];
|
|
53
|
+
function setCelebration(targets, next) {
|
|
54
|
+
targets.forEach((model) => model.illumination.set(model.illumination.presets.some(({ id }) => id === next) ? next : null));
|
|
55
|
+
for (const button of celebrationButtons) {
|
|
56
|
+
const eligible = illuminated.filter((model) => model.illumination.presets.some(({ id }) => id === button.dataset["celebration"]));
|
|
57
|
+
const active = eligible.filter((model) => model.illumination.active === button.dataset["celebration"]).length;
|
|
58
|
+
button.setAttribute("aria-pressed", active === 0 ? "false" : active === eligible.length ? "true" : "mixed");
|
|
59
|
+
}
|
|
60
|
+
celebrationStatus.hidden = false;
|
|
61
|
+
celebrationStatus.textContent = targets.map((model) => {
|
|
62
|
+
const preset = model.illumination.presets.find(({ id }) => id === model.illumination.active);
|
|
63
|
+
const name = model.building.userData["geography"]?.name ?? model.building.name;
|
|
64
|
+
return preset ? `${preset.lines.join(" ")} · ${name}${preset.adapted ? " · adapted tribute" : ""}` : `Celebratory lights off · ${name}`;
|
|
65
|
+
}).join("; ");
|
|
66
|
+
celebrationAnnouncement.textContent = celebrationStatus.textContent;
|
|
67
|
+
viewer.requestRender();
|
|
68
|
+
}
|
|
69
|
+
for (const button of celebrationButtons) {
|
|
70
|
+
const preset = celebrations.find(({ id }) => id === button.dataset["celebration"]);
|
|
71
|
+
if (!preset)
|
|
72
|
+
throw new Error(`Unknown toolbar celebration: ${button.dataset["celebration"]}`);
|
|
73
|
+
const displays = illuminated.flatMap((model) => {
|
|
74
|
+
const message = model.illumination.presets.find(({ id }) => id === preset.id);
|
|
75
|
+
return message ? [`${message.lines.join(" ")}${message.adapted ? " (adapted tribute)" : ""} · ${model.building.userData["geography"]?.name ?? model.building.name}`] : [];
|
|
76
|
+
});
|
|
77
|
+
button.title = `${preset.label}: ${displays.join("; ")} · click to light all; click again to turn off`;
|
|
78
|
+
button.disabled = displays.length === 0;
|
|
79
|
+
button.addEventListener("click", () => {
|
|
80
|
+
const next = button.getAttribute("aria-pressed") === "true" ? null : preset.id;
|
|
81
|
+
const targets = next === null ? illuminated.filter((model) => model.illumination.active === preset.id) : illuminated;
|
|
82
|
+
setCelebration(targets, next);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// The control bar's ground toggles, each pressed while what it toggles shows.
|
|
86
|
+
for (const [id, object] of [["streets", roads], ["footprints", ground.footprints]]) {
|
|
87
|
+
const button = document.querySelector(`#${id}`);
|
|
88
|
+
button.addEventListener("click", () => {
|
|
89
|
+
object.visible = !object.visible;
|
|
90
|
+
button.setAttribute("aria-pressed", String(object.visible));
|
|
91
|
+
viewer.requestRender();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// The star in the middle of the bar opens and closes the toolbar. Each group unfolds from
|
|
95
|
+
// the star: clipped open from its `--fold` inset while it fades in, one beside the star
|
|
96
|
+
// slides out from it to its edge of the bar, and it folds back the same way before it hides. A toggle mid-way reverses the moving
|
|
97
|
+
// fold from where it is. Closing groups are inert at once, and focus inside them returns to
|
|
98
|
+
// the star first; reduced motion shows and hides them at once. Escape closes the toolbar from
|
|
99
|
+
// inside the bar.
|
|
100
|
+
const bar = document.querySelector(".control-bar");
|
|
101
|
+
const menuToggle = document.querySelector("#menu-toggle");
|
|
102
|
+
// This shortcut belongs to the index viewer; the standalone scene has no drawing to
|
|
103
|
+
// switch to. Returning from the drawing keeps this frame and its camera intact.
|
|
104
|
+
const showOriginal = document.querySelector("#show-original");
|
|
105
|
+
try {
|
|
106
|
+
if (parent !== window && parent.document.querySelector("#skyline-3d-scene")?.contentWindow === window) {
|
|
107
|
+
showOriginal.hidden = false;
|
|
108
|
+
showOriginal.addEventListener("click", () => parent.postMessage({ type: "skyline:show-original" }, location.origin));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* A scene embedded on another origin has no viewer switch. */ }
|
|
112
|
+
const groups = [...bar.querySelectorAll(".button-group")];
|
|
113
|
+
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
114
|
+
// The groups' folds while they move, and which way: toward open, or back toward closed.
|
|
115
|
+
const foldDuration = 300;
|
|
116
|
+
let folds = [];
|
|
117
|
+
let unfolding = false;
|
|
118
|
+
function unfold(group) {
|
|
119
|
+
// A group in the star's row starts against the star; one in a row under it opens in place.
|
|
120
|
+
const star = menuToggle.getBoundingClientRect(), box = group.getBoundingClientRect();
|
|
121
|
+
const beside = box.top < star.bottom && box.bottom > star.top;
|
|
122
|
+
const shift = !beside ? 0 : box.right <= star.left ? star.left - box.right : box.left >= star.right ? star.right - box.left : 0;
|
|
123
|
+
// Open, the clip stands off the group far enough to keep its buttons' focus rings.
|
|
124
|
+
return group.animate([
|
|
125
|
+
{ clipPath: getComputedStyle(group).getPropertyValue("--fold").trim(), opacity: 0, translate: `${shift}px 0` },
|
|
126
|
+
{ clipPath: "inset(-4px)", opacity: 1, translate: "0 0" },
|
|
127
|
+
], { duration: foldDuration, easing: "cubic-bezier(0.2, 0.7, 0.2, 1)", fill: "both" });
|
|
128
|
+
}
|
|
129
|
+
function setMenu(open) {
|
|
130
|
+
menuToggle.setAttribute("aria-expanded", String(open));
|
|
131
|
+
menuToggle.title = open ? "Hide the controls" : "Show the controls";
|
|
132
|
+
if (!open && groups.some((group) => group.contains(document.activeElement)))
|
|
133
|
+
menuToggle.focus();
|
|
134
|
+
groups.forEach((group) => { group.inert = !open; });
|
|
135
|
+
if (open)
|
|
136
|
+
bar.classList.add("open");
|
|
137
|
+
if (reducedMotion.matches) {
|
|
138
|
+
folds.forEach((fold) => fold.cancel());
|
|
139
|
+
folds = [];
|
|
140
|
+
bar.classList.toggle("open", open);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Reverse a fold still moving the other way; otherwise start a fresh one from its end. A
|
|
144
|
+
// fold that has yet to leave its starting end, toggled back before its first frame, would
|
|
145
|
+
// seek to its far end on reverse(), so it settles where it stands instead.
|
|
146
|
+
if (folds.length && folds.every((fold) => fold.playState === "running" || fold.playState === "paused")) {
|
|
147
|
+
if (unfolding !== open) {
|
|
148
|
+
const unmoved = folds.every((fold) => { const time = Number(fold.currentTime ?? 0); return unfolding ? time <= 0 : time >= foldDuration; });
|
|
149
|
+
if (unmoved) {
|
|
150
|
+
folds.forEach((fold) => fold.cancel());
|
|
151
|
+
folds = [];
|
|
152
|
+
unfolding = open;
|
|
153
|
+
bar.classList.toggle("open", open);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
folds.forEach((fold) => fold.reverse());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
folds.forEach((fold) => fold.cancel());
|
|
161
|
+
folds = groups.map(unfold);
|
|
162
|
+
if (!open)
|
|
163
|
+
folds.forEach((fold) => fold.reverse());
|
|
164
|
+
}
|
|
165
|
+
unfolding = open;
|
|
166
|
+
const moving = folds;
|
|
167
|
+
void Promise.allSettled(moving.map((fold) => fold.finished)).then(() => {
|
|
168
|
+
if (folds !== moving || (menuToggle.getAttribute("aria-expanded") === "true") !== open)
|
|
169
|
+
return;
|
|
170
|
+
folds.forEach((fold) => fold.cancel());
|
|
171
|
+
folds = [];
|
|
172
|
+
if (!open)
|
|
173
|
+
bar.classList.remove("open");
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
menuToggle.addEventListener("click", () => setMenu(menuToggle.getAttribute("aria-expanded") !== "true"));
|
|
177
|
+
// A fold measures its slide from the layout it starts in, so a resize settles it at once.
|
|
178
|
+
addEventListener("resize", () => folds.forEach((fold) => fold.finish()));
|
|
179
|
+
bar.addEventListener("keydown", (event) => {
|
|
180
|
+
if (event.key !== "Escape" || menuToggle.getAttribute("aria-expanded") !== "true")
|
|
181
|
+
return;
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
menuToggle.focus();
|
|
184
|
+
setMenu(false);
|
|
185
|
+
});
|
|
186
|
+
// Right-clicking a building, or a long press on it where the browser raises a context menu
|
|
187
|
+
// for one, as Chrome on Android does, opens its context menu at the pointer: the building's
|
|
188
|
+
// name over its detail link and available lighting choices. A right-drag still pans, so the menu opens only for a press that stays put: on
|
|
189
|
+
// its release where the menu event comes with the press, as on macOS. Escape, Tab, a press
|
|
190
|
+
// elsewhere, the wheel, or a resize closes it.
|
|
191
|
+
const canvas = document.querySelector("#building");
|
|
192
|
+
const menu = document.querySelector("#building-menu");
|
|
193
|
+
const menuTitle = document.querySelector("#building-menu-title");
|
|
194
|
+
const detailLink = document.querySelector("#building-detail-link");
|
|
195
|
+
const lightingMenu = document.querySelector("#building-lighting-menu");
|
|
196
|
+
let menuBuilding = "";
|
|
197
|
+
let press = null;
|
|
198
|
+
let pendingMenu = null;
|
|
199
|
+
function openMenu(building, x, y) {
|
|
200
|
+
menuBuilding = building.building.userData["buildingId"];
|
|
201
|
+
menuTitle.textContent = building.building.userData["geography"]?.name ?? building.building.name;
|
|
202
|
+
detailLink.href = `building-detail.html?building=${encodeURIComponent(menuBuilding)}`;
|
|
203
|
+
lightingMenu.replaceChildren();
|
|
204
|
+
lightingMenu.hidden = !building.illumination;
|
|
205
|
+
if (building.illumination) {
|
|
206
|
+
const choices = [
|
|
207
|
+
{ id: null, label: "Lights off", title: "Turn off this building's celebratory lights" },
|
|
208
|
+
...building.illumination.presets.map((preset) => ({
|
|
209
|
+
id: preset.id,
|
|
210
|
+
label: `${preset.label} · ${preset.lines.join(" ")}${preset.adapted ? " (adapted)" : ""}`,
|
|
211
|
+
title: `${preset.label} lighting${preset.adapted ? " · adapted tribute" : ""} · select again to turn off`,
|
|
212
|
+
})),
|
|
213
|
+
];
|
|
214
|
+
for (const choice of choices) {
|
|
215
|
+
const button = document.createElement("button");
|
|
216
|
+
button.type = "button";
|
|
217
|
+
button.setAttribute("role", "menuitemradio");
|
|
218
|
+
button.setAttribute("aria-checked", String(building.illumination.active === choice.id));
|
|
219
|
+
button.tabIndex = -1;
|
|
220
|
+
button.dataset["lighting"] = choice.id ?? "off";
|
|
221
|
+
button.textContent = choice.label;
|
|
222
|
+
button.title = choice.title;
|
|
223
|
+
button.addEventListener("click", () => {
|
|
224
|
+
setCelebration([building], building.illumination.active === choice.id ? null : choice.id);
|
|
225
|
+
closeMenu();
|
|
226
|
+
});
|
|
227
|
+
lightingMenu.append(button);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
menu.hidden = false;
|
|
231
|
+
menu.scrollTop = 0;
|
|
232
|
+
// At the pointer, turned back from the window's right and bottom edges.
|
|
233
|
+
const { width, height } = menu.getBoundingClientRect();
|
|
234
|
+
menu.style.left = `${Math.max(4, x + width > innerWidth - 4 ? x - width : x)}px`;
|
|
235
|
+
menu.style.top = `${Math.max(4, y + height > innerHeight - 4 ? y - height : y)}px`;
|
|
236
|
+
detailLink.focus({ preventScroll: true });
|
|
237
|
+
}
|
|
238
|
+
function closeMenu() {
|
|
239
|
+
pendingMenu = null;
|
|
240
|
+
if (menu.hidden)
|
|
241
|
+
return;
|
|
242
|
+
if (menu.contains(document.activeElement))
|
|
243
|
+
canvas.focus({ preventScroll: true });
|
|
244
|
+
menu.hidden = true;
|
|
245
|
+
}
|
|
246
|
+
window.addEventListener("pointerdown", (event) => { if (!menu.contains(event.target))
|
|
247
|
+
closeMenu(); }, { capture: true });
|
|
248
|
+
canvas.addEventListener("pointerdown", (event) => {
|
|
249
|
+
press = { x: event.clientX, y: event.clientY, mouse: event.pointerType === "mouse", down: true, moved: false };
|
|
250
|
+
});
|
|
251
|
+
window.addEventListener("pointermove", (event) => {
|
|
252
|
+
if (press?.down && Math.hypot(event.clientX - press.x, event.clientY - press.y) > 4)
|
|
253
|
+
press.moved = true;
|
|
254
|
+
});
|
|
255
|
+
window.addEventListener("pointerup", () => {
|
|
256
|
+
if (!press?.down)
|
|
257
|
+
return;
|
|
258
|
+
press.down = false;
|
|
259
|
+
const pending = pendingMenu;
|
|
260
|
+
pendingMenu = null;
|
|
261
|
+
if (pending && !press.moved)
|
|
262
|
+
openMenu(pending.building, pending.x, pending.y);
|
|
263
|
+
});
|
|
264
|
+
window.addEventListener("pointercancel", () => {
|
|
265
|
+
if (press)
|
|
266
|
+
press.down = false;
|
|
267
|
+
pendingMenu = null;
|
|
268
|
+
});
|
|
269
|
+
canvas.addEventListener("contextmenu", (event) => {
|
|
270
|
+
const building = viewer.buildingAt(event.clientX, event.clientY);
|
|
271
|
+
if (!building)
|
|
272
|
+
return closeMenu();
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
if (press?.down && press.mouse)
|
|
275
|
+
pendingMenu = { building, x: event.clientX, y: event.clientY };
|
|
276
|
+
else if (!(press?.mouse && press.moved))
|
|
277
|
+
openMenu(building, event.clientX, event.clientY);
|
|
278
|
+
});
|
|
279
|
+
canvas.addEventListener("wheel", closeMenu, { passive: true });
|
|
280
|
+
window.addEventListener("resize", closeMenu);
|
|
281
|
+
window.addEventListener("blur", closeMenu);
|
|
282
|
+
menu.addEventListener("keydown", (event) => {
|
|
283
|
+
if (event.key === "Escape" || event.key === "Tab") {
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
closeMenu();
|
|
286
|
+
}
|
|
287
|
+
else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
const items = [...menu.querySelectorAll('[role="menuitem"], [role="menuitemradio"]')];
|
|
290
|
+
const index = items.findIndex((item) => item === document.activeElement);
|
|
291
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1
|
|
292
|
+
: (index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
|
|
293
|
+
items[next]?.focus();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
detailLink.addEventListener("click", (event) => {
|
|
297
|
+
closeMenu();
|
|
298
|
+
// A modified click opens the detail's own page, as any link does.
|
|
299
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
300
|
+
return;
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
openDetail(menuBuilding, menuTitle.textContent ?? "");
|
|
303
|
+
});
|
|
304
|
+
// The detail floats in a panel over the skyline, in a frame of its own. Each detail gets a
|
|
305
|
+
// fresh frame, so the page's history stays as it was, and closing the panel removes the frame
|
|
306
|
+
// and its 3D view. The skyline stays live around it, and another building's menu replaces the
|
|
307
|
+
// detail. Escape closes it from anywhere on the page or in the detail, unless an open menu or
|
|
308
|
+
// toolbar takes it first, and focus returns to the skyline.
|
|
309
|
+
const detailPanel = document.querySelector("#building-detail");
|
|
310
|
+
const detailClose = document.querySelector("#detail-close");
|
|
311
|
+
function openDetail(id, name) {
|
|
312
|
+
detailPanel.querySelector("iframe")?.remove();
|
|
313
|
+
const frame = document.createElement("iframe");
|
|
314
|
+
frame.title = `${name} — Building Detail`;
|
|
315
|
+
frame.src = `building-detail.html?building=${encodeURIComponent(id)}`;
|
|
316
|
+
frame.addEventListener("load", () => frame.contentWindow?.addEventListener("keydown", closeDetailOnEscape));
|
|
317
|
+
detailPanel.append(frame);
|
|
318
|
+
if (!detailPanel.open)
|
|
319
|
+
detailPanel.show();
|
|
320
|
+
detailClose.focus();
|
|
321
|
+
}
|
|
322
|
+
function closeDetailOnEscape(event) {
|
|
323
|
+
if (event.key !== "Escape" || event.defaultPrevented || !detailPanel.open)
|
|
324
|
+
return;
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
detailPanel.close();
|
|
327
|
+
}
|
|
328
|
+
window.addEventListener("keydown", closeDetailOnEscape);
|
|
329
|
+
detailPanel.addEventListener("close", () => {
|
|
330
|
+
detailPanel.querySelector("iframe")?.remove();
|
|
331
|
+
canvas.focus({ preventScroll: true });
|
|
332
|
+
});
|
|
333
|
+
detailClose.addEventListener("click", () => detailPanel.close());
|