@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.
Files changed (195) hide show
  1. package/NOTICE.md +20 -0
  2. package/README.md +52 -2
  3. package/docs/package.md +224 -0
  4. package/docs/skyline-geography.md +503 -0
  5. package/lib/models/aon-center.d.ts +4 -0
  6. package/lib/models/aon-center.js +60 -0
  7. package/lib/models/aon-geographic.d.ts +9 -0
  8. package/lib/models/aon-geographic.js +35 -0
  9. package/lib/models/aon-tower.d.ts +23 -0
  10. package/lib/models/aon-tower.js +130 -0
  11. package/lib/models/blue-cross-geographic.d.ts +14 -0
  12. package/lib/models/blue-cross-geographic.js +248 -0
  13. package/lib/models/borg-warner-geographic.d.ts +20 -0
  14. package/lib/models/borg-warner-geographic.js +143 -0
  15. package/lib/models/buckingham-geographic.d.ts +10 -0
  16. package/lib/models/buckingham-geographic.js +121 -0
  17. package/lib/models/building-kit.d.ts +84 -0
  18. package/lib/models/building-kit.js +358 -0
  19. package/lib/models/celebrations.d.ts +39 -0
  20. package/lib/models/celebrations.js +44 -0
  21. package/lib/models/chicago-athletic-association-geographic.d.ts +32 -0
  22. package/lib/models/chicago-athletic-association-geographic.js +173 -0
  23. package/lib/models/crain-communications.d.ts +5 -0
  24. package/lib/models/crain-communications.js +75 -0
  25. package/lib/models/crain-geographic.d.ts +10 -0
  26. package/lib/models/crain-geographic.js +69 -0
  27. package/lib/models/crain-tower.d.ts +32 -0
  28. package/lib/models/crain-tower.js +378 -0
  29. package/lib/models/facade-grid.d.ts +18 -0
  30. package/lib/models/facade-grid.js +176 -0
  31. package/lib/models/gage-geographic.d.ts +19 -0
  32. package/lib/models/gage-geographic.js +154 -0
  33. package/lib/models/heritage-at-millennium-park.d.ts +3 -0
  34. package/lib/models/heritage-at-millennium-park.js +284 -0
  35. package/lib/models/heritage-geographic.d.ts +11 -0
  36. package/lib/models/heritage-geographic.js +208 -0
  37. package/lib/models/hyatt-west-tower-geographic.d.ts +26 -0
  38. package/lib/models/hyatt-west-tower-geographic.js +86 -0
  39. package/lib/models/keith-ascher-geographic.d.ts +29 -0
  40. package/lib/models/keith-ascher-geographic.js +110 -0
  41. package/lib/models/kemper-geographic.d.ts +8 -0
  42. package/lib/models/kemper-geographic.js +153 -0
  43. package/lib/models/kemper.d.ts +2 -0
  44. package/lib/models/kemper.js +78 -0
  45. package/lib/models/lake-view-geographic.d.ts +15 -0
  46. package/lib/models/lake-view-geographic.js +134 -0
  47. package/lib/models/maclean-center-geographic.d.ts +20 -0
  48. package/lib/models/maclean-center-geographic.js +162 -0
  49. package/lib/models/michigan-boulevard-geographic.d.ts +12 -0
  50. package/lib/models/michigan-boulevard-geographic.js +157 -0
  51. package/lib/models/michigan-plaza-south-geographic.d.ts +7 -0
  52. package/lib/models/michigan-plaza-south-geographic.js +84 -0
  53. package/lib/models/michigan-plaza-south.d.ts +2 -0
  54. package/lib/models/michigan-plaza-south.js +68 -0
  55. package/lib/models/millennium-park-plaza-geographic.d.ts +9 -0
  56. package/lib/models/millennium-park-plaza-geographic.js +103 -0
  57. package/lib/models/monroe-geographic.d.ts +19 -0
  58. package/lib/models/monroe-geographic.js +177 -0
  59. package/lib/models/north-michigan-180-geographic.d.ts +14 -0
  60. package/lib/models/north-michigan-180-geographic.js +117 -0
  61. package/lib/models/north-wabash-geographic.d.ts +14 -0
  62. package/lib/models/north-wabash-geographic.js +122 -0
  63. package/lib/models/on-the-park-geographic.d.ts +11 -0
  64. package/lib/models/on-the-park-geographic.js +245 -0
  65. package/lib/models/one-prudential-geographic.d.ts +12 -0
  66. package/lib/models/one-prudential-geographic.js +55 -0
  67. package/lib/models/one-prudential-plaza.d.ts +5 -0
  68. package/lib/models/one-prudential-plaza.js +72 -0
  69. package/lib/models/one-prudential-tower.d.ts +45 -0
  70. package/lib/models/one-prudential-tower.js +246 -0
  71. package/lib/models/peoples-gas-geographic.d.ts +23 -0
  72. package/lib/models/peoples-gas-geographic.js +250 -0
  73. package/lib/models/railway-exchange-geographic.d.ts +18 -0
  74. package/lib/models/railway-exchange-geographic.js +176 -0
  75. package/lib/models/river-plaza-geographic.d.ts +16 -0
  76. package/lib/models/river-plaza-geographic.js +104 -0
  77. package/lib/models/sheraton-grand-geographic.d.ts +24 -0
  78. package/lib/models/sheraton-grand-geographic.js +112 -0
  79. package/lib/models/six-north-michigan-geographic.d.ts +16 -0
  80. package/lib/models/six-north-michigan-geographic.js +180 -0
  81. package/lib/models/skyline-geography-data.d.ts +44 -0
  82. package/lib/models/skyline-geography-data.js +607 -0
  83. package/lib/models/skyline-geography.d.ts +23 -0
  84. package/lib/models/skyline-geography.js +322 -0
  85. package/lib/models/swissotel-geographic.d.ts +24 -0
  86. package/lib/models/swissotel-geographic.js +79 -0
  87. package/lib/models/three-illinois-center-geographic.d.ts +26 -0
  88. package/lib/models/three-illinois-center-geographic.js +86 -0
  89. package/lib/models/trump-geographic.d.ts +14 -0
  90. package/lib/models/trump-geographic.js +51 -0
  91. package/lib/models/trump-international-tower.d.ts +4 -0
  92. package/lib/models/trump-international-tower.js +84 -0
  93. package/lib/models/trump-tower.d.ts +43 -0
  94. package/lib/models/trump-tower.js +229 -0
  95. package/lib/models/two-illinois-center-geographic.d.ts +26 -0
  96. package/lib/models/two-illinois-center-geographic.js +85 -0
  97. package/lib/models/two-prudential-geographic.d.ts +25 -0
  98. package/lib/models/two-prudential-geographic.js +47 -0
  99. package/lib/models/two-prudential-plaza.d.ts +8 -0
  100. package/lib/models/two-prudential-plaza.js +55 -0
  101. package/lib/models/two-prudential-tower.d.ts +53 -0
  102. package/lib/models/two-prudential-tower.js +441 -0
  103. package/lib/models/university-club-geographic.d.ts +24 -0
  104. package/lib/models/university-club-geographic.js +217 -0
  105. package/lib/models/willoughby-tower-geographic.d.ts +17 -0
  106. package/lib/models/willoughby-tower-geographic.js +195 -0
  107. package/lib/models/window-illumination.d.ts +14 -0
  108. package/lib/models/window-illumination.js +64 -0
  109. package/lib/package-assets.d.ts +5 -0
  110. package/lib/package-assets.js +9 -0
  111. package/lib/skyline-package.d.ts +23 -0
  112. package/lib/skyline-package.js +25 -0
  113. package/lib/skyline-scene.d.ts +25 -0
  114. package/lib/skyline-scene.js +86 -0
  115. package/lib/study-types.d.ts +49 -0
  116. package/lib/study-types.js +1 -0
  117. package/lib/vendor/three-r186.d.ts +1 -0
  118. package/lib/vendor/three-r186.js +1 -0
  119. package/package.json +64 -4
  120. package/site/building-detail.html +54 -0
  121. package/site/building-detail.js +84 -0
  122. package/site/building-study.html +58 -0
  123. package/site/building-study.js +3 -0
  124. package/site/index.html +407 -0
  125. package/site/models/aon-center.js +60 -0
  126. package/site/models/aon-geographic.js +35 -0
  127. package/site/models/aon-tower.js +130 -0
  128. package/site/models/blue-cross-geographic.js +248 -0
  129. package/site/models/borg-warner-geographic.js +143 -0
  130. package/site/models/buckingham-geographic.js +121 -0
  131. package/site/models/building-kit.js +358 -0
  132. package/site/models/celebrations.js +44 -0
  133. package/site/models/chicago-athletic-association-geographic.js +173 -0
  134. package/site/models/crain-communications.js +75 -0
  135. package/site/models/crain-geographic.js +69 -0
  136. package/site/models/crain-reference.svg +10 -0
  137. package/site/models/crain-tower.js +378 -0
  138. package/site/models/facade-grid.js +176 -0
  139. package/site/models/gage-geographic.js +154 -0
  140. package/site/models/heritage-at-millennium-park.js +284 -0
  141. package/site/models/heritage-geographic.js +208 -0
  142. package/site/models/hyatt-west-tower-geographic.js +86 -0
  143. package/site/models/keith-ascher-geographic.js +110 -0
  144. package/site/models/kemper-geographic.js +153 -0
  145. package/site/models/kemper.js +78 -0
  146. package/site/models/lake-view-geographic.js +134 -0
  147. package/site/models/maclean-center-geographic.js +162 -0
  148. package/site/models/michigan-boulevard-geographic.js +157 -0
  149. package/site/models/michigan-plaza-south-geographic.js +84 -0
  150. package/site/models/michigan-plaza-south.js +68 -0
  151. package/site/models/millennium-park-plaza-geographic.js +103 -0
  152. package/site/models/monroe-geographic.js +177 -0
  153. package/site/models/north-michigan-180-geographic.js +117 -0
  154. package/site/models/north-wabash-geographic.js +122 -0
  155. package/site/models/on-the-park-geographic.js +245 -0
  156. package/site/models/one-prudential-geographic.js +55 -0
  157. package/site/models/one-prudential-plaza.js +72 -0
  158. package/site/models/one-prudential-tower.js +246 -0
  159. package/site/models/peoples-gas-geographic.js +250 -0
  160. package/site/models/railway-exchange-geographic.js +176 -0
  161. package/site/models/river-plaza-geographic.js +104 -0
  162. package/site/models/sheraton-grand-geographic.js +112 -0
  163. package/site/models/six-north-michigan-geographic.js +180 -0
  164. package/site/models/skyline-geography-data.js +607 -0
  165. package/site/models/skyline-geography.js +322 -0
  166. package/site/models/skyline-panorama.svg +3491 -0
  167. package/site/models/skyline-reference.svg +3090 -0
  168. package/site/models/swissotel-geographic.js +79 -0
  169. package/site/models/three-illinois-center-geographic.js +86 -0
  170. package/site/models/trump-geographic.js +51 -0
  171. package/site/models/trump-international-tower.js +84 -0
  172. package/site/models/trump-tower.js +229 -0
  173. package/site/models/two-illinois-center-geographic.js +85 -0
  174. package/site/models/two-prudential-geographic.js +47 -0
  175. package/site/models/two-prudential-plaza.js +55 -0
  176. package/site/models/two-prudential-tower.js +441 -0
  177. package/site/models/university-club-geographic.js +217 -0
  178. package/site/models/willoughby-tower-geographic.js +195 -0
  179. package/site/models/window-illumination.js +64 -0
  180. package/site/skyline-3d.html +92 -0
  181. package/site/skyline-3d.js +333 -0
  182. package/site/skyline-animated.svg +18795 -0
  183. package/site/skyline-comparison.js +97 -0
  184. package/site/skyline-original-fit.svg +18312 -0
  185. package/site/skyline-scene.js +86 -0
  186. package/site/skyline-study.html +83 -0
  187. package/site/skyline-study.js +109 -0
  188. package/site/skyline-webgl.html +667 -0
  189. package/site/stars.svg +176 -0
  190. package/site/study-loader.js +20 -0
  191. package/site/study-types.js +1 -0
  192. package/site/study-viewer.js +605 -0
  193. package/site/study.css +204 -0
  194. package/site/vendor/THREE-LICENSE.txt +21 -0
  195. package/site/vendor/three-r186.js +4228 -0
@@ -0,0 +1,407 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="robots" content="noindex" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <title>Chicago Skyline — Living Night</title>
9
+ <script>
10
+ if (new URLSearchParams(location.search).get("embed") === "1") {
11
+ document.documentElement.dataset.embedded = "true";
12
+ }
13
+ </script>
14
+ <style>
15
+ :root {
16
+ color-scheme: dark;
17
+ background: #000;
18
+ font-family: Georgia, "Times New Roman", serif;
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ html,
26
+ body,
27
+ main {
28
+ width: 100%;
29
+ height: 100%;
30
+ margin: 0;
31
+ overflow: hidden;
32
+ background: #000;
33
+ }
34
+
35
+ .stars,
36
+ .scene {
37
+ position: fixed;
38
+ inset: 0;
39
+ display: block;
40
+ width: 100%;
41
+ height: 100%;
42
+ border: 0;
43
+ background: transparent;
44
+ }
45
+
46
+ .stars {
47
+ z-index: 0;
48
+ pointer-events: none;
49
+ }
50
+
51
+ .scene {
52
+ z-index: 1;
53
+ }
54
+
55
+ .scene[hidden] {
56
+ display: none;
57
+ }
58
+
59
+ .controls {
60
+ position: fixed;
61
+ z-index: 2;
62
+ top: max(12px, env(safe-area-inset-top));
63
+ left: 50%;
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ align-items: baseline;
67
+ justify-content: center;
68
+ gap: 10px clamp(18px, 2vw, 28px);
69
+ width: max-content;
70
+ max-width: calc(100% - 24px);
71
+ transform: translateX(-50%);
72
+ color: #fff;
73
+ font-size: clamp(18px, 1.85vw, 26px);
74
+ line-height: 1;
75
+ white-space: nowrap;
76
+ }
77
+
78
+ .controls button,
79
+ .controls a {
80
+ appearance: none;
81
+ margin: 0;
82
+ border: 0;
83
+ border-radius: 0;
84
+ padding: 0;
85
+ background: transparent;
86
+ color: inherit;
87
+ font: inherit;
88
+ line-height: inherit;
89
+ text-decoration: underline;
90
+ text-decoration-thickness: 1px;
91
+ text-underline-offset: 3px;
92
+ cursor: pointer;
93
+ opacity: 0.88;
94
+ }
95
+
96
+ html[data-embedded="true"] .controls {
97
+ display: none;
98
+ }
99
+
100
+ .controls button:hover,
101
+ .controls a:hover,
102
+ .controls button:focus-visible,
103
+ .controls a:focus-visible,
104
+ .controls button[aria-pressed="true"] {
105
+ color: #fff;
106
+ opacity: 1;
107
+ text-decoration-thickness: 2px;
108
+ }
109
+
110
+ .controls button:focus-visible,
111
+ .controls a:focus-visible {
112
+ outline: 1px solid #fff;
113
+ outline-offset: 6px;
114
+ }
115
+
116
+ .original-return {
117
+ position: fixed;
118
+ z-index: 2;
119
+ right: max(12px, env(safe-area-inset-right));
120
+ bottom: max(5px, env(safe-area-inset-bottom));
121
+ border: 1px solid #393939;
122
+ border-radius: 2px;
123
+ padding: 5px 8px;
124
+ background: #101010;
125
+ color: #aaa;
126
+ font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
127
+ cursor: pointer;
128
+ }
129
+
130
+ .original-return:hover { color: #fff; border-color: #999; background: #202020; }
131
+ .original-return:focus-visible { outline: 1px solid #fff; outline-offset: 2px; }
132
+
133
+ .motion-status {
134
+ flex-basis: 100%;
135
+ margin: 0;
136
+ color: #999;
137
+ font: 12px/1.5 system-ui, sans-serif;
138
+ text-align: center;
139
+ white-space: normal;
140
+ }
141
+
142
+ .visually-hidden {
143
+ position: absolute;
144
+ width: 1px;
145
+ height: 1px;
146
+ padding: 0;
147
+ margin: -1px;
148
+ overflow: hidden;
149
+ clip: rect(0, 0, 0, 0);
150
+ white-space: nowrap;
151
+ border: 0;
152
+ }
153
+
154
+ @media (max-width: 520px) {
155
+ .controls {
156
+ top: max(16px, env(safe-area-inset-top));
157
+ gap: 10px 18px;
158
+ }
159
+ }
160
+ </style>
161
+ </head>
162
+ <body>
163
+ <main id="stage">
164
+ <iframe
165
+ class="stars"
166
+ id="stars"
167
+ src="stars.svg"
168
+ title="Twinkling night sky"
169
+ aria-hidden="true"
170
+ tabindex="-1"
171
+ ></iframe>
172
+ <iframe
173
+ class="scene"
174
+ id="skyline-3d-scene"
175
+ src="skyline-3d.html"
176
+ title="3D Chicago skyline"
177
+ ></iframe>
178
+ <iframe
179
+ class="scene"
180
+ id="scene"
181
+ title="Animated Chicago skyline at night"
182
+ hidden
183
+ ></iframe>
184
+ <iframe
185
+ class="scene"
186
+ id="webgl-scene"
187
+ data-src="skyline-webgl.html"
188
+ title="WebGL Chicago skyline prototype"
189
+ hidden
190
+ ></iframe>
191
+
192
+ <nav class="controls" aria-label="Skyline controls">
193
+ <button type="button" id="fullscreen" aria-pressed="false">fullscreen</button>
194
+ <button type="button" id="toggle-skyline" aria-pressed="false">show original</button>
195
+ <button type="button" id="toggle-webgl" aria-pressed="false">show webgl</button>
196
+ <button type="button" id="toggle-enhanced" aria-pressed="false">show enhanced</button>
197
+ <a href="building-study.html">3d building</a>
198
+ <a href="skyline-study.html">skyline study</a>
199
+ <button type="button" id="debug-motion" aria-pressed="false" aria-describedby="motion-status">debug motion</button>
200
+ <p class="motion-status" id="motion-status" role="status">Checking motion preference…</p>
201
+ </nav>
202
+
203
+ <button type="button" class="original-return" id="return-skyline-3d" title="Return to the 3D skyline" hidden>3d skyline</button>
204
+ <button type="button" class="original-return" id="embedded-original" title="Compare with the original skyline SVG" hidden>show original</button>
205
+ <p class="visually-hidden" id="status" aria-live="polite"></p>
206
+ </main>
207
+
208
+ <script>
209
+ const fullscreenButton = document.querySelector("#fullscreen");
210
+ const status = document.querySelector("#status");
211
+ const scene = document.querySelector("#scene");
212
+ const webglScene = document.querySelector("#webgl-scene");
213
+ const skyline3dScene = document.querySelector("#skyline-3d-scene");
214
+ const stars = document.querySelector("#stars");
215
+ const debugMotionButton = document.querySelector("#debug-motion");
216
+ const motionStatus = document.querySelector("#motion-status");
217
+ const originalReturn = document.querySelector("#return-skyline-3d");
218
+ const embeddedOriginal = document.querySelector("#embedded-original");
219
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
220
+ // Modes shown in their own page. The 3D skyline loads with the viewer; the others load
221
+ // the first time they are chosen.
222
+ const framedScenes = { webgl: webglScene, "3d": skyline3dScene };
223
+ // The viewer opens on the 3D skyline. Each toggle shows its mode and, pressed again,
224
+ // returns to the 3D skyline.
225
+ const homeMode = "3d";
226
+ const home = { label: "3d skyline", title: "Return to the 3D skyline", status: "3D skyline displayed" };
227
+ const toggles = {
228
+ original: {
229
+ button: document.querySelector("#toggle-skyline"),
230
+ label: "show original",
231
+ title: "Compare with the original skyline SVG",
232
+ status: "Original skyline displayed",
233
+ },
234
+ webgl: {
235
+ button: document.querySelector("#toggle-webgl"),
236
+ label: "show webgl",
237
+ title: "Preview the WebGL skyline prototype",
238
+ status: "WebGL skyline prototype displayed",
239
+ },
240
+ enhanced: {
241
+ button: document.querySelector("#toggle-enhanced"),
242
+ label: "show enhanced",
243
+ title: "Show the enhanced interactive skyline drawing",
244
+ status: "Enhanced interactive skyline displayed",
245
+ },
246
+ };
247
+ let debugMotion = false;
248
+ let skylineMode = homeMode;
249
+ // Keep SVG recovery reachable until the first 3D frame succeeds, even if
250
+ // the scene document or its bootstrap script never initializes.
251
+ let sceneUnavailable = true;
252
+
253
+ function updateSkylineView() {
254
+ const framedScene = framedScenes[skylineMode];
255
+
256
+ if (framedScene && !framedScene.getAttribute("src")) {
257
+ framedScene.src = framedScene.dataset.src;
258
+ }
259
+ for (const [mode, frame] of Object.entries(framedScenes)) {
260
+ frame.hidden = mode !== skylineMode;
261
+ }
262
+ if (!framedScene) {
263
+ const showingOriginal = skylineMode === "original";
264
+ const nextSource = showingOriginal ? "skyline-original-fit.svg" : "skyline-animated.svg";
265
+ if (scene.getAttribute("src") !== nextSource) {
266
+ scene.src = nextSource;
267
+ }
268
+ scene.title = showingOriginal
269
+ ? "Original Chicago skyline"
270
+ : "Animated Chicago skyline at night";
271
+ }
272
+ scene.hidden = Boolean(framedScene);
273
+ originalReturn.hidden = skylineMode !== "original";
274
+ embeddedOriginal.hidden = document.documentElement.dataset.embedded !== "true" || skylineMode !== homeMode || !sceneUnavailable;
275
+
276
+ for (const [mode, toggle] of Object.entries(toggles)) {
277
+ const showing = mode === skylineMode;
278
+ toggle.button.textContent = showing ? home.label : toggle.label;
279
+ toggle.button.setAttribute("aria-pressed", String(showing));
280
+ toggle.button.title = showing ? home.title : toggle.title;
281
+ }
282
+ }
283
+
284
+ function showSkylineMode(mode) {
285
+ skylineMode = mode;
286
+ updateSkylineView();
287
+ status.textContent = mode === homeMode ? home.status : toggles[mode].status;
288
+ }
289
+
290
+ for (const [mode, toggle] of Object.entries(toggles)) {
291
+ toggle.button.addEventListener("click", () => {
292
+ showSkylineMode(skylineMode === mode ? homeMode : mode);
293
+ });
294
+ }
295
+
296
+ // The scene's bottom shortcut uses the viewer's mode switch. Only this scene, on
297
+ // this origin and while it is displayed, can request the original drawing.
298
+ window.addEventListener("message", (event) => {
299
+ if (event.origin !== location.origin || event.source !== skyline3dScene.contentWindow) return;
300
+ if (event.data?.type === "skyline:unavailable" || event.data?.type === "skyline:ready") {
301
+ sceneUnavailable = event.data.type === "skyline:unavailable";
302
+ updateSkylineView();
303
+ return;
304
+ }
305
+ if (event.data?.type !== "skyline:show-original" || skylineMode !== homeMode) return;
306
+ showSkylineMode("original");
307
+ originalReturn.focus();
308
+ });
309
+ originalReturn.addEventListener("click", () => {
310
+ showSkylineMode(homeMode);
311
+ const sceneDocument = skyline3dScene.contentDocument;
312
+ const star = sceneDocument?.querySelector("#menu-toggle");
313
+ const focus = star?.getAttribute("aria-expanded") === "true" ? sceneDocument.querySelector("#show-original") : star;
314
+ const target = !sceneUnavailable && focus?.matches(":enabled") ? focus : null;
315
+ target?.focus();
316
+ if (!target || sceneDocument?.activeElement !== target) {
317
+ (embeddedOriginal.hidden ? toggles.original.button : embeddedOriginal).focus();
318
+ }
319
+ });
320
+ embeddedOriginal.addEventListener("click", () => {
321
+ showSkylineMode("original");
322
+ originalReturn.focus();
323
+ });
324
+
325
+ updateSkylineView();
326
+
327
+ function updateMotionStatus() {
328
+ const preference = reducedMotion.matches ? "reduced" : "allowed";
329
+ const animation = debugMotion
330
+ ? (reducedMotion.matches ? "debug override active" : "debug animation")
331
+ : (reducedMotion.matches ? "stars paused" : "stars twinkling");
332
+ motionStatus.textContent = `System motion: ${preference} · ${animation}`;
333
+ debugMotionButton.textContent = debugMotion ? "stop debug" : "debug motion";
334
+ debugMotionButton.setAttribute("aria-pressed", String(debugMotion));
335
+ debugMotionButton.title = debugMotion
336
+ ? "Return to the system motion setting"
337
+ : "Preview slightly stronger brightness changes at the same gentle pace, even when reduced motion is enabled";
338
+ }
339
+
340
+ debugMotionButton.addEventListener("click", () => {
341
+ debugMotion = !debugMotion;
342
+ // Keep a fragment when stopping so the randomized sky does not reload.
343
+ stars.src = `stars.svg#${debugMotion ? "debug-motion" : ""}`;
344
+ updateMotionStatus();
345
+ });
346
+
347
+ reducedMotion.addEventListener("change", updateMotionStatus);
348
+ updateMotionStatus();
349
+
350
+ async function toggleFullscreen() {
351
+ try {
352
+ if (document.fullscreenElement) {
353
+ await document.exitFullscreen();
354
+ } else {
355
+ await document.documentElement.requestFullscreen();
356
+ }
357
+ } catch {
358
+ status.textContent = "Fullscreen is not available in this browser";
359
+ }
360
+ }
361
+
362
+ fullscreenButton.addEventListener("click", toggleFullscreen);
363
+
364
+ document.addEventListener("fullscreenchange", () => {
365
+ const isFullscreen = Boolean(document.fullscreenElement);
366
+ fullscreenButton.textContent = isFullscreen ? "exit fullscreen" : "fullscreen";
367
+ fullscreenButton.setAttribute("aria-pressed", String(isFullscreen));
368
+ status.textContent = isFullscreen ? "Fullscreen enabled" : "Fullscreen exited";
369
+ });
370
+
371
+ if (!document.fullscreenEnabled) {
372
+ fullscreenButton.hidden = true;
373
+ }
374
+
375
+ function fullscreenShortcut(event) {
376
+ if (event.key.toLowerCase() === "f" && !event.metaKey && !event.ctrlKey && !event.altKey) {
377
+ event.preventDefault();
378
+ toggleFullscreen();
379
+ }
380
+ }
381
+
382
+ document.addEventListener("keydown", fullscreenShortcut);
383
+ // Keys pressed inside a scene, such as on the 3D skyline's canvas, go to its frame's own
384
+ // window, so listen there too. A frame from another origin, as under file://, keeps them.
385
+ function listenInFrame(frame) {
386
+ try {
387
+ frame.contentWindow.addEventListener("keydown", fullscreenShortcut);
388
+ } catch {}
389
+ }
390
+ for (const frame of [skyline3dScene, scene, webglScene]) {
391
+ listenInFrame(frame);
392
+ frame.addEventListener("load", () => listenInFrame(frame));
393
+ }
394
+
395
+ // The controls lie over the top of the 3D skyline's frame, which floats a building's
396
+ // detail below them.
397
+ const controls = document.querySelector(".controls");
398
+ function insetSkyline() {
399
+ try {
400
+ skyline3dScene.contentDocument.documentElement.style.setProperty("--viewer-controls", `${Math.ceil(controls.getBoundingClientRect().bottom)}px`);
401
+ } catch {}
402
+ }
403
+ new ResizeObserver(insetSkyline).observe(controls);
404
+ skyline3dScene.addEventListener("load", insetSkyline);
405
+ </script>
406
+ </body>
407
+ </html>
@@ -0,0 +1,60 @@
1
+ import { aonLevels, buildAonTower } from "./aon-tower.js";
2
+ // Aon Center on a clean version of its plan, in real meters, for the skyline study's
3
+ // original layout, which places this copy in skyline-study.ts. aon-tower.ts builds it, and
4
+ // the geographic model on the mapped outline.
5
+ //
6
+ // The plan is the published 59.15 m square. Each face's main run holds fifteen 10 ft bays,
7
+ // as the photograph and the drawing both count and the building's descriptions give.
8
+ // That leaves each corner a 6.7 m notch: a 45° chamfer, a square step, and a second
9
+ // chamfer, in the mapped outline's proportions. The mapped notches run about 1.5 m deeper;
10
+ // the photograph's bays and corner widths fit these. See docs/aon-reference.md. Units are
11
+ // meters; +x is east and +z is south.
12
+ const half = 59.15 / 2, notch = 6.715, chamfer = 1.9, step = notch - 2 * chamfer;
13
+ // The south-east notch, from the south face's east end to the east face's south end.
14
+ const southEast = [
15
+ [half - notch, half],
16
+ [half - notch + chamfer, half - chamfer],
17
+ [half - notch + chamfer, half - chamfer - step],
18
+ [half - notch + chamfer + step, half - chamfer - step],
19
+ [half, half - notch],
20
+ ];
21
+ // Each corner turns the south-east one a quarter further round.
22
+ const turn = ([x, z], quarters) => (quarters === 0 ? [x, z] : turn([z, -x], quarters - 1));
23
+ const outline = [0, 1, 2, 3].flatMap((quarters) => southEast.map((point) => turn(point, quarters)));
24
+ // The rooftop enclosure, centred as the mapped part is, and the antenna at its centre.
25
+ const enclosure = [[-16, -16], [16, -16], [16, 16], [-16, 16]];
26
+ // The skyline drawing shows the tower from the photograph's treeline up, so its platform
27
+ // datum crosses the tower above the street: 30.6 m, fitted with the placement in
28
+ // skyline-study.ts, which scales, turns, and places this copy. Its y = 0 is that datum,
29
+ // and the drawing's top floor band is the eightieth office floor, under the mechanical ones.
30
+ export const aonSkylineBase = 30.6;
31
+ export function createAonCenterBuilding() {
32
+ return buildAonTower({ name: "Aon Center", id: "layer3", outline, enclosure, mast: [0, 0], base: aonSkylineBase });
33
+ }
34
+ // Features the skyline test projects against the drawing, in this copy's coordinates.
35
+ const at = ([x, z], real) => [x, real - aonSkylineBase, z];
36
+ const bays = 15, bay = (southEast[0][0] - -southEast[0][0]) / bays;
37
+ // The columns stand on the bay lines, the end ones drawn in to stay on their face.
38
+ const columnAt = (i) => Math.min(Math.max(i * bay, 0.65), bays * bay - 0.65);
39
+ const southColumn = (i) => [-(half - notch) + columnAt(i), half];
40
+ const eastColumn = (i) => [half, half - notch - columnAt(i)];
41
+ export const aonFeatures = {
42
+ // The roof edge's ends: the west face's south end, the south face's east end, and the
43
+ // east face's north end, at the top of the cap.
44
+ aonRoofWest: at(turn(southEast[0], 3), aonLevels.roof),
45
+ aonRoofNear: at(southEast[0], aonLevels.roof),
46
+ aonRoofEast: at(turn(southEast[0], 1), aonLevels.roof),
47
+ // A point on the south face's glass, clear of every neighbour, for hover.
48
+ aonFrontFacade: at([-(half - notch) + 7.5 * bay, half + 0.07], 230),
49
+ // The columns, halfway out to their points at mid-height: the fourteen after each face's
50
+ // first, which the drawing's fifteen strips on each face match one for one.
51
+ aonFrontPiers: Array.from({ length: 14 }, (_, i) => { const [x, z] = southColumn(i + 1); return [x, 220 - aonSkylineBase, z + 0.35]; }),
52
+ aonSidePiers: Array.from({ length: 14 }, (_, i) => { const [x, z] = eastColumn(i + 1); return [x + 0.35, 220 - aonSkylineBase, z]; }),
53
+ // The notch's two sides at mid-height: the south face's east end and the east face's
54
+ // south end, between which the drawing paints the corner's stone.
55
+ aonNotchFront: at(southEast[0], 200),
56
+ aonNotchEast: at(southEast[4], 200),
57
+ // Window centres across the south face's middle on six office floors, the drawing's
58
+ // eleventh, twenty-third and every twelfth band on from the top.
59
+ aonFloorRows: [70, 58, 46, 34, 22, 10].map((floor) => at([0, half + 0.07], aonLevels.lobbyTop + (floor - 1) * aonLevels.pitch + (aonLevels.sill + aonLevels.head) / 2)),
60
+ };
@@ -0,0 +1,35 @@
1
+ import { polygonOf } from "./building-kit.js";
2
+ import { aonLevels, buildAonTower } from "./aon-tower.js";
3
+ // Aon Center: the geographic layout's model, on the mapped outline 64388609 with its
4
+ // notched corners, the rooftop enclosure part 284775635, and the published heights. The
5
+ // tower is the shared generator in aon-tower.ts, the same one the original layout's copy
6
+ // uses. Each mapped face carries the source plan's fifteen window bays, distributed
7
+ // over its chord; the mapped notches are deeper, so this spacing is approximate. The antenna stands
8
+ // at the enclosure's centre, reaching the published 362.5 m tip; the map gives no antenna
9
+ // geometry. See docs/aon-reference.md. Units are meters; +x is east, +z is south.
10
+ export const aonGeographicLevels = Object.freeze({
11
+ shaftTop: aonLevels.roof, // OSM shaft part top
12
+ enclosureTop: aonLevels.enclosureTop, // OSM part 284775635
13
+ tip: aonLevels.tip, // published tip
14
+ floors: 83,
15
+ });
16
+ const h = aonGeographicLevels;
17
+ export function createAonGeographicBuilding(record, projectPlan, offset) {
18
+ const enclosurePart = record.parts.find((p) => p.way === 284775635);
19
+ const enclosure = polygonOf(projectPlan(enclosurePart.coordinates));
20
+ // The antenna at the enclosure's area centroid: its traced outline carries an extra
21
+ // vertex on one edge, which would pull a mean of its vertices off centre.
22
+ let twice = 0, cx = 0, cz = 0;
23
+ enclosure.forEach(([x, z], i) => {
24
+ const [x2, z2] = enclosure[(i + 1) % enclosure.length], cross = x * z2 - x2 * z;
25
+ twice += cross;
26
+ cx += (x + x2) * cross;
27
+ cz += (z + z2) * cross;
28
+ });
29
+ const mast = [cx / (3 * twice), cz / (3 * twice)];
30
+ const model = buildAonTower({ name: record.name, id: record.id, outline: polygonOf(projectPlan(record.footprint.coordinates)), enclosure, mast, base: 0 });
31
+ model.building.position.set(offset[0], 0, offset[1]);
32
+ model.building.userData["geography"] = record;
33
+ model.building.userData["geographicDetail"] = { levels: h, floors: h.floors, source: "docs/aon-reference.md" };
34
+ return model;
35
+ }
@@ -0,0 +1,130 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, line } from "./building-kit.js";
3
+ import { chainsOf, gridBox, mitredBox, orient, paintedSlab, planOf, turnAt } from "./facade-grid.js";
4
+ // Heights above the street. The floor pitch and the cap are measured on the photograph the
5
+ // drawing was traced from. The lobby takes what is left below eighty office floors and the
6
+ // two mechanical floors over them, which with the lobby make the published 83; daylight
7
+ // photographs show the slots unchanged over the mechanical floors, so the glass keeps the
8
+ // office floors' pitch up to the cap.
9
+ export const aonLevels = Object.freeze({
10
+ roof: 340,
11
+ capBottom: 338.5,
12
+ lobbyTop: 11.9,
13
+ pitch: 3.87,
14
+ sill: 0.8,
15
+ head: 3.1,
16
+ enclosureTop: 346.3,
17
+ tip: 362.5,
18
+ bay: 3.048, // nominal clean-plan module; mapped spacing follows each face's chord
19
+ baysPerFace: 15, // tenant portal's Suite 1300 plan: sixteen piers, fifteen openings
20
+ faceLength: 20,
21
+ });
22
+ const h = aonLevels;
23
+ const color = (hex) => new THREE.Color(hex);
24
+ const glassTones = [0x262626, 0x2c2c2c, 0x323232, 0x222222].map(color);
25
+ const litGlass = color(0x7c7c7c), dimGlass = color(0x4e4e4e);
26
+ const spandrel = color(0x1b1b1b), granite = color(0xb2b2b2), core = color(0x202020);
27
+ const slat = color(0x9c9c9c), slatGap = color(0x444444), roofing = color(0x4c4c4c);
28
+ function paneColor(row, bay, face) {
29
+ const hash = (Math.imul(row + 17, 0x9e3779b1) ^ Math.imul(bay + 31, 0x85ebca77) ^ Math.imul(face + 7, 0xc2b2ae3d)) >>> 0;
30
+ const value = hash % 101;
31
+ if (value < 3)
32
+ return litGlass;
33
+ if (value < 6)
34
+ return dimGlass;
35
+ return glassTones[value % glassTones.length];
36
+ }
37
+ export function buildAonTower(form) {
38
+ const kit = createBuilder(form.name, form.id);
39
+ const shell = kit.batch("Aon · tube shell", kit.material(0xffffff, { vertexColors: true }));
40
+ const glazing = kit.batch("Aon · window ribbons", kit.material(0xffffff, { vertexColors: true }));
41
+ const piers = kit.batch("Aon · granite piers", kit.material(0xbababa));
42
+ const cap = kit.batch("Aon · granite cap", kit.material(0xc4c4c4));
43
+ const enclosure = kit.batch("Aon · rooftop enclosure", kit.material(0xffffff, { vertexColors: true }));
44
+ const mast = kit.batch("Aon · antenna mast", kit.material(0x6a6a6a));
45
+ const base = form.base;
46
+ const y = (real) => real - base;
47
+ const outline = orient(form.outline), plan = planOf(outline);
48
+ // A traced outline can split one face into nearly collinear pieces, which chainsOf joins
49
+ // within 5°: a chain at least `faceLength` long is a main face, whose facade stands on its
50
+ // chord, and every other run belongs to a notched corner.
51
+ const faces = chainsOf(plan).map(({ runs }) => ({ chord: line(plan[runs[0]].at(0), plan[runs.at(-1)].at(plan[runs.at(-1)].length)), runs })).filter(({ chord }) => chord.length >= h.faceLength);
52
+ const onFace = new Set(faces.flatMap(({ runs }) => runs));
53
+ const grid = (target, run, columns, rows, back, front, paint, sides) => gridBox(kit, target, run, columns, rows, back, front, paint, sides, y);
54
+ // The shaft: dark spandrel walls on the faces, granite on the notched corners, a floor,
55
+ // and the flat roof.
56
+ plan.forEach((run, index) => {
57
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
58
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], y(h.roof), b[1]], [a[0], y(h.roof), a[1]]], [[n[0], 0, n[1]]], onFace.has(index) ? core : granite);
59
+ });
60
+ paintedSlab(kit, shell, outline, 0, false, roofing);
61
+ paintedSlab(kit, shell, outline, y(h.roof), true, roofing);
62
+ // The source plan fixes fifteen openings per face, independent of an OSM trace's
63
+ // corner depth. Distribute them over each face's chord; mapped spacing remains an
64
+ // approximation (docs/aon-reference.md, FID-AON-001). End piers stay on their face.
65
+ const faceBays = (run) => {
66
+ const count = h.baysPerFace, bay = run.length / count;
67
+ return Array.from({ length: count + 1 }, (_, i) => i * bay);
68
+ };
69
+ const pierHalf = 0.65, pierPoint = 0.7;
70
+ const floors = [];
71
+ for (let level = h.lobbyTop; level + h.pitch <= h.capBottom + 1e-6; level += h.pitch)
72
+ floors.push(level);
73
+ faces.forEach(({ chord: run }, face) => {
74
+ const stations = faceBays(run);
75
+ // End piers are inset from the corner. Stop glass at their centres so it
76
+ // cannot peek past the taper of the V and create two extra narrow slots.
77
+ const glassStations = [pierHalf, ...stations.slice(1, -1), run.length - pierHalf];
78
+ // The glass: one ribbon per floor across the face, cut into panes at the columns, which
79
+ // stand in front of the joints. The lobby is a single tall storey of glass.
80
+ const visible = (low) => low >= base;
81
+ if (visible(0.4))
82
+ grid(glazing, run, glassStations, [0.4, h.lobbyTop - 0.9], 0.02, 0.07, (bay) => paneColor(0, bay, face), spandrel);
83
+ floors.forEach((level, row) => {
84
+ if (!visible(level + h.sill))
85
+ return;
86
+ grid(glazing, run, glassStations, [level + h.sill, level + h.head], 0.02, 0.07, (bay) => paneColor(row + 1, bay, face), spandrel);
87
+ });
88
+ // The V-shaped columns: granite prisms from just above grade to the cap, their points
89
+ // outward. They start 0.3 m up so their relief leaves the street outline unchanged.
90
+ for (const station of stations) {
91
+ const s = Math.min(Math.max(station, pierHalf), run.length - pierHalf);
92
+ const [left, right, point] = [run.at(s - pierHalf, 0), run.at(s + pierHalf, 0), run.at(s, pierPoint)];
93
+ const low = y(Math.max(base, 0.3)), high = y(h.capBottom);
94
+ const up = [0, 1, 0], down = [0, -1, 0];
95
+ const p = (q, v) => [q[0], v, q[1]];
96
+ // Outward is to the right of a and then b, as it is for every run of the plan.
97
+ const faceOf = (a, b) => { const d = [b[0] - a[0], b[1] - a[1]], l = Math.hypot(...d); return [-d[1] / l, 0, d[0] / l]; };
98
+ kit.quad(piers, [p(left, low), p(point, low), p(point, high), p(left, high)], [faceOf(left, point)]);
99
+ kit.quad(piers, [p(point, low), p(right, low), p(right, high), p(point, high)], [faceOf(point, right)]);
100
+ kit.quad(piers, [p(right, low), p(left, low), p(left, high), p(right, high)], [[-run.normal(0)[0], 0, -run.normal(0)[1]]]);
101
+ kit.triangle(piers, [p(left, high), p(point, high), p(right, high)], [up, up, up]);
102
+ kit.triangle(piers, [p(left, low), p(right, low), p(point, low)], [down, down, down]);
103
+ }
104
+ });
105
+ // The granite cap: a fascia under the roof edge on every run, standing clear of the
106
+ // columns' points and mitred on the bisector of each corner.
107
+ const capWidth = pierPoint + 0.2;
108
+ if (h.roof > base)
109
+ plan.forEach((run, index) => {
110
+ mitredBox(kit, cap, run, 0, run.length, turnAt(plan[(index + plan.length - 1) % plan.length], run), turnAt(run, plan[(index + 1) % plan.length]), capWidth, h.capBottom, h.roof, y);
111
+ });
112
+ // The rooftop enclosure: louvered walls from the roof to the published top, and a lid.
113
+ const enclosureOutline = orient(form.enclosure);
114
+ planOf(enclosureOutline).forEach((run) => {
115
+ const rows = [];
116
+ for (let v = h.roof; v < h.enclosureTop - 1e-6; v += 0.6)
117
+ rows.push(v);
118
+ rows.push(h.enclosureTop);
119
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
120
+ // Its walls are cells of blades and gaps, so the enclosure closes on its own lid and floor.
121
+ const at = (q, v) => [q[0], y(v), q[1]];
122
+ for (let r = 0; r + 1 < rows.length; r += 1)
123
+ kit.quad(enclosure, [at(a, rows[r]), at(b, rows[r]), at(b, rows[r + 1]), at(a, rows[r + 1])], [[n[0], 0, n[1]]], r % 2 === 0 ? slat : slatGap);
124
+ });
125
+ paintedSlab(kit, enclosure, enclosureOutline, y(h.roof), false, roofing);
126
+ paintedSlab(kit, enclosure, enclosureOutline, y(h.enclosureTop), true, roofing);
127
+ // The antenna: a slim mast from the enclosure to the published tip.
128
+ kit.box(mast, form.mast, [0, 1], 0.3, -0.3, 0.3, y(h.enclosureTop), y(h.tip));
129
+ return kit.finish({ height: y(h.tip), outlines: [shell, cap], opacity: 0.18 });
130
+ }