@a2f0/skyline 0.0.0-stage → 0.1.1

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 +222 -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,667 @@
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
+ <title>WebGL Chicago Skyline Prototype</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: dark;
11
+ background: transparent;
12
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
13
+ }
14
+
15
+ html,
16
+ body,
17
+ canvas {
18
+ width: 100%;
19
+ height: 100%;
20
+ margin: 0;
21
+ overflow: hidden;
22
+ background: transparent;
23
+ }
24
+
25
+ canvas {
26
+ display: block;
27
+ cursor: crosshair;
28
+ }
29
+
30
+ .loading,
31
+ .badge,
32
+ .tooltip {
33
+ position: fixed;
34
+ z-index: 2;
35
+ pointer-events: none;
36
+ }
37
+
38
+ .loading {
39
+ left: 50%;
40
+ bottom: 24px;
41
+ transform: translateX(-50%);
42
+ color: #a8a8a8;
43
+ font-size: 11px;
44
+ letter-spacing: 0.04em;
45
+ text-align: center;
46
+ }
47
+
48
+ .loading.error {
49
+ max-width: min(520px, calc(100% - 32px));
50
+ color: #fff;
51
+ line-height: 1.5;
52
+ }
53
+
54
+ .badge {
55
+ right: 12px;
56
+ bottom: 10px;
57
+ color: #777;
58
+ font-size: 10px;
59
+ letter-spacing: 0.08em;
60
+ text-transform: uppercase;
61
+ }
62
+
63
+ .tooltip {
64
+ border: 1px solid #3a3a3a;
65
+ border-radius: 3px;
66
+ padding: 4px 7px;
67
+ background: #000;
68
+ color: #fff;
69
+ font: 600 12px/1.15 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
70
+ white-space: nowrap;
71
+ }
72
+
73
+ [hidden] {
74
+ display: none !important;
75
+ }
76
+ </style>
77
+ </head>
78
+ <body>
79
+ <canvas id="skyline" role="img" aria-label="Interactive WebGL rendering of the Chicago skyline"></canvas>
80
+ <p class="loading" id="loading" role="status">preparing WebGL building layers…</p>
81
+ <div class="tooltip" id="tooltip" aria-hidden="true" hidden></div>
82
+ <div class="badge" id="badge" hidden>WebGL · 0 layers</div>
83
+
84
+ <script>
85
+ (() => {
86
+ "use strict";
87
+
88
+ const SOURCE_URL = "skyline-animated.svg";
89
+ const VIEWBOX = { x: 0, y: 0, width: 8501.0986, height: 2782.0373 };
90
+ const SVG_NS = "http://www.w3.org/2000/svg";
91
+ const XLINK_NS = "http://www.w3.org/1999/xlink";
92
+ const canvas = document.querySelector("#skyline");
93
+ const loading = document.querySelector("#loading");
94
+ const tooltip = document.querySelector("#tooltip");
95
+ const badge = document.querySelector("#badge");
96
+ const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
97
+ const gl = canvas.getContext("webgl2", {
98
+ alpha: true,
99
+ antialias: true,
100
+ premultipliedAlpha: false,
101
+ });
102
+
103
+ if (!gl) {
104
+ fail("WebGL 2 is not available in this browser.");
105
+ return;
106
+ }
107
+
108
+ const vertexSource = `#version 300 es
109
+ in vec2 a_corner;
110
+ out vec2 v_uv;
111
+ uniform vec4 u_bounds;
112
+ uniform vec2 u_viewport;
113
+ uniform vec2 u_origin;
114
+ uniform vec2 u_shift;
115
+ uniform float u_scale;
116
+
117
+ void main() {
118
+ vec2 svgPosition = u_bounds.xy + a_corner * u_bounds.zw;
119
+ vec2 pixelPosition = u_origin + svgPosition * u_scale + u_shift;
120
+ vec2 clipPosition = vec2(
121
+ pixelPosition.x / u_viewport.x * 2.0 - 1.0,
122
+ 1.0 - pixelPosition.y / u_viewport.y * 2.0
123
+ );
124
+ gl_Position = vec4(clipPosition, 0.0, 1.0);
125
+ v_uv = vec2(a_corner.x, 1.0 - a_corner.y);
126
+ }
127
+ `;
128
+
129
+ const fragmentSource = `#version 300 es
130
+ precision highp float;
131
+ in vec2 v_uv;
132
+ out vec4 outColor;
133
+ uniform sampler2D u_texture;
134
+ uniform vec2 u_texel;
135
+ uniform vec3 u_pickColor;
136
+ uniform float u_selected;
137
+ uniform float u_time;
138
+ uniform int u_pickPass;
139
+ uniform int u_flatPass;
140
+
141
+ void main() {
142
+ vec4 texel = texture(u_texture, v_uv);
143
+ if (u_pickPass == 1) {
144
+ if (texel.a < 0.12) {
145
+ discard;
146
+ }
147
+ outColor = vec4(u_pickColor, 1.0);
148
+ return;
149
+ }
150
+
151
+ if (u_flatPass == 1) {
152
+ if (texel.a < 0.015) {
153
+ discard;
154
+ }
155
+ outColor = texel;
156
+ return;
157
+ }
158
+
159
+ float nearbyAlpha = texel.a;
160
+ if (u_selected > 0.5) {
161
+ vec2 radius = u_texel * 5.0;
162
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv + vec2(radius.x, 0.0)).a);
163
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv - vec2(radius.x, 0.0)).a);
164
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv + vec2(0.0, radius.y)).a);
165
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv - vec2(0.0, radius.y)).a);
166
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv + radius).a);
167
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv - radius).a);
168
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv + vec2(radius.x, -radius.y)).a);
169
+ nearbyAlpha = max(nearbyAlpha, texture(u_texture, v_uv + vec2(-radius.x, radius.y)).a);
170
+ }
171
+ if (nearbyAlpha < 0.015) {
172
+ discard;
173
+ }
174
+
175
+ float facadeLight = mix(0.955, 1.035, v_uv.x);
176
+ vec3 shaded = texel.rgb * facadeLight;
177
+ float outputAlpha = texel.a;
178
+
179
+ if (u_selected > 0.5) {
180
+ float halo = max(0.0, nearbyAlpha - texel.a) * 0.72;
181
+ float pulse = 1.15 + 0.025 * sin(u_time * 0.003);
182
+ vec3 illuminated = min(vec3(1.0), shaded * pulse + 0.02);
183
+ shaded = texel.a > 0.025 ? illuminated : vec3(0.76);
184
+ outputAlpha = max(texel.a, halo);
185
+ }
186
+
187
+ outColor = vec4(shaded, outputAlpha);
188
+ }
189
+ `;
190
+
191
+ let program;
192
+ let vao;
193
+ let uniforms;
194
+ let buildingSprites = [];
195
+ let terrainSprite = null;
196
+ let pickFramebuffer = null;
197
+ let pickTexture = null;
198
+ let selectedIndex = -1;
199
+ let pickPending = false;
200
+ let ready = false;
201
+ let pointer = { x: 0, y: 0, inside: false, parallax: 0 };
202
+ let projection = { scale: 1, originX: 0, originY: 0 };
203
+
204
+ function fail(message, error) {
205
+ if (error) {
206
+ console.error(error);
207
+ }
208
+ loading.textContent = location.protocol === "file:"
209
+ ? `${message} Run a local server to use the WebGL view (for example: python3 -m http.server).`
210
+ : message;
211
+ loading.classList.add("error");
212
+ loading.hidden = false;
213
+ badge.hidden = true;
214
+ }
215
+
216
+ function compileShader(type, source) {
217
+ const shader = gl.createShader(type);
218
+ gl.shaderSource(shader, source);
219
+ gl.compileShader(shader);
220
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
221
+ const message = gl.getShaderInfoLog(shader);
222
+ gl.deleteShader(shader);
223
+ throw new Error(message);
224
+ }
225
+ return shader;
226
+ }
227
+
228
+ function createProgram() {
229
+ const vertexShader = compileShader(gl.VERTEX_SHADER, vertexSource);
230
+ const fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSource);
231
+ const nextProgram = gl.createProgram();
232
+ gl.attachShader(nextProgram, vertexShader);
233
+ gl.attachShader(nextProgram, fragmentShader);
234
+ gl.linkProgram(nextProgram);
235
+ gl.deleteShader(vertexShader);
236
+ gl.deleteShader(fragmentShader);
237
+ if (!gl.getProgramParameter(nextProgram, gl.LINK_STATUS)) {
238
+ throw new Error(gl.getProgramInfoLog(nextProgram));
239
+ }
240
+ return nextProgram;
241
+ }
242
+
243
+ function setupGeometry() {
244
+ vao = gl.createVertexArray();
245
+ const buffer = gl.createBuffer();
246
+ gl.bindVertexArray(vao);
247
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
248
+ gl.bufferData(
249
+ gl.ARRAY_BUFFER,
250
+ new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
251
+ gl.STATIC_DRAW,
252
+ );
253
+ const corner = gl.getAttribLocation(program, "a_corner");
254
+ gl.enableVertexAttribArray(corner);
255
+ gl.vertexAttribPointer(corner, 2, gl.FLOAT, false, 0, 0);
256
+ gl.bindVertexArray(null);
257
+ }
258
+
259
+ function cacheUniforms() {
260
+ uniforms = Object.fromEntries([
261
+ "u_bounds",
262
+ "u_viewport",
263
+ "u_origin",
264
+ "u_shift",
265
+ "u_scale",
266
+ "u_texture",
267
+ "u_texel",
268
+ "u_pickColor",
269
+ "u_selected",
270
+ "u_time",
271
+ "u_pickPass",
272
+ "u_flatPass",
273
+ ].map((name) => [name, gl.getUniformLocation(program, name)]));
274
+ }
275
+
276
+ function resizeCanvas() {
277
+ const pixelRatio = Math.min(devicePixelRatio || 1, 2);
278
+ const width = Math.max(1, Math.round(innerWidth * pixelRatio));
279
+ const height = Math.max(1, Math.round(innerHeight * pixelRatio));
280
+ if (canvas.width === width && canvas.height === height) {
281
+ return false;
282
+ }
283
+ canvas.width = width;
284
+ canvas.height = height;
285
+ projection.scale = Math.min(width / VIEWBOX.width, height / VIEWBOX.height);
286
+ projection.originX = (width - VIEWBOX.width * projection.scale) / 2;
287
+ projection.originY = height - VIEWBOX.height * projection.scale;
288
+ createPickTarget();
289
+ return true;
290
+ }
291
+
292
+ function createPickTarget() {
293
+ if (pickFramebuffer) {
294
+ gl.deleteFramebuffer(pickFramebuffer);
295
+ gl.deleteTexture(pickTexture);
296
+ }
297
+ pickTexture = gl.createTexture();
298
+ gl.bindTexture(gl.TEXTURE_2D, pickTexture);
299
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
300
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
301
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
302
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
303
+ gl.texImage2D(
304
+ gl.TEXTURE_2D,
305
+ 0,
306
+ gl.RGBA,
307
+ canvas.width,
308
+ canvas.height,
309
+ 0,
310
+ gl.RGBA,
311
+ gl.UNSIGNED_BYTE,
312
+ null,
313
+ );
314
+ pickFramebuffer = gl.createFramebuffer();
315
+ gl.bindFramebuffer(gl.FRAMEBUFFER, pickFramebuffer);
316
+ gl.framebufferTexture2D(
317
+ gl.FRAMEBUFFER,
318
+ gl.COLOR_ATTACHMENT0,
319
+ gl.TEXTURE_2D,
320
+ pickTexture,
321
+ 0,
322
+ );
323
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
324
+ }
325
+
326
+ function transformedBounds(element, sourceRoot) {
327
+ const bounds = element.getBBox();
328
+ const rootMatrix = sourceRoot.getScreenCTM();
329
+ const elementMatrix = element.getScreenCTM();
330
+ const matrix = rootMatrix.inverse().multiply(elementMatrix);
331
+ const points = [
332
+ new DOMPoint(bounds.x, bounds.y),
333
+ new DOMPoint(bounds.x + bounds.width, bounds.y),
334
+ new DOMPoint(bounds.x, bounds.y + bounds.height),
335
+ new DOMPoint(bounds.x + bounds.width, bounds.y + bounds.height),
336
+ ].map((point) => point.matrixTransform(matrix));
337
+ const xs = points.map((point) => point.x);
338
+ const ys = points.map((point) => point.y);
339
+ return {
340
+ x: Math.min(...xs),
341
+ y: Math.min(...ys),
342
+ width: Math.max(...xs) - Math.min(...xs),
343
+ height: Math.max(...ys) - Math.min(...ys),
344
+ };
345
+ }
346
+
347
+ function parentMatrix(element, sourceRoot) {
348
+ const rootMatrix = sourceRoot.getScreenCTM();
349
+ const parentScreenMatrix = element.parentElement.getScreenCTM();
350
+ return rootMatrix.inverse().multiply(parentScreenMatrix);
351
+ }
352
+
353
+ function matrixAttribute(matrix) {
354
+ return `matrix(${matrix.a} ${matrix.b} ${matrix.c} ${matrix.d} ${matrix.e} ${matrix.f})`;
355
+ }
356
+
357
+ function spriteDocument(descriptor, drawBounds, width, height) {
358
+ return `<?xml version="1.0" encoding="UTF-8"?>
359
+ <svg xmlns="${SVG_NS}" xmlns:xlink="${XLINK_NS}"
360
+ viewBox="${drawBounds.x} ${drawBounds.y} ${drawBounds.width} ${drawBounds.height}"
361
+ width="${width}" height="${height}">
362
+ ${descriptor.definitions}
363
+ <g transform="${descriptor.parentTransform}">${descriptor.content}</g>
364
+ </svg>`;
365
+ }
366
+
367
+ function loadImage(markup) {
368
+ return new Promise((resolve, reject) => {
369
+ const blob = new Blob([markup], { type: "image/svg+xml" });
370
+ const url = URL.createObjectURL(blob);
371
+ const image = new Image();
372
+ image.onload = () => {
373
+ URL.revokeObjectURL(url);
374
+ resolve(image);
375
+ };
376
+ image.onerror = () => {
377
+ URL.revokeObjectURL(url);
378
+ reject(new Error("A WebGL building texture could not be rasterized."));
379
+ };
380
+ image.src = url;
381
+ });
382
+ }
383
+
384
+ async function rasterize(descriptor) {
385
+ const pixelRatio = Math.min(devicePixelRatio || 1, 2);
386
+ const paddingPixels = descriptor.pickable ? 30 * pixelRatio : 2 * pixelRatio;
387
+ const padding = paddingPixels / projection.scale;
388
+ const drawBounds = {
389
+ x: descriptor.bounds.x - padding,
390
+ y: descriptor.bounds.y - padding,
391
+ width: descriptor.bounds.width + padding * 2,
392
+ height: descriptor.bounds.height + padding * 2,
393
+ };
394
+ const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
395
+ let rasterScale = projection.scale;
396
+ rasterScale = Math.min(
397
+ rasterScale,
398
+ maxTextureSize / drawBounds.width,
399
+ maxTextureSize / drawBounds.height,
400
+ );
401
+ const width = Math.max(2, Math.ceil(drawBounds.width * rasterScale));
402
+ const height = Math.max(2, Math.ceil(drawBounds.height * rasterScale));
403
+ const image = await loadImage(spriteDocument(descriptor, drawBounds, width, height));
404
+ const texture = gl.createTexture();
405
+ gl.bindTexture(gl.TEXTURE_2D, texture);
406
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
407
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
408
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
409
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
410
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
411
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
412
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
413
+ return {
414
+ ...descriptor,
415
+ drawBounds,
416
+ texture,
417
+ texelX: 1 / width,
418
+ texelY: 1 / height,
419
+ };
420
+ }
421
+
422
+ async function prepareSprites() {
423
+ const response = await fetch(SOURCE_URL);
424
+ if (!response.ok) {
425
+ throw new Error(`Could not load ${SOURCE_URL} (${response.status}).`);
426
+ }
427
+ const sourceText = await response.text();
428
+ const sourceDocument = new DOMParser().parseFromString(sourceText, "image/svg+xml");
429
+ if (sourceDocument.querySelector("parsererror")) {
430
+ throw new Error(`${SOURCE_URL} could not be parsed.`);
431
+ }
432
+
433
+ const sourceRoot = sourceDocument.documentElement;
434
+ sourceRoot.querySelectorAll("script, #building-tooltip").forEach((element) => element.remove());
435
+ sourceRoot.setAttribute("width", "850");
436
+ sourceRoot.setAttribute("height", "278");
437
+ const sourceHost = document.createElement("div");
438
+ sourceHost.style.cssText = "position:fixed;left:-10000px;top:0;width:850px;height:278px;opacity:0;pointer-events:none";
439
+ sourceHost.append(sourceRoot);
440
+ document.body.append(sourceHost);
441
+ await new Promise(requestAnimationFrame);
442
+
443
+ const serializer = new XMLSerializer();
444
+ const definitions = serializer.serializeToString(sourceRoot.querySelector("defs"));
445
+ const elements = [...sourceRoot.querySelectorAll(".interactive-building")];
446
+ const descriptors = elements.map((element, index) => {
447
+ const bounds = transformedBounds(element, sourceRoot);
448
+ return {
449
+ id: element.id,
450
+ buildingId: element.dataset.buildingId || element.id,
451
+ label: element.getAttribute("aria-label") || `Chicago building ${index + 1}`,
452
+ bounds,
453
+ content: serializer.serializeToString(element),
454
+ definitions,
455
+ parentTransform: matrixAttribute(parentMatrix(element, sourceRoot)),
456
+ pickable: true,
457
+ pickId: index + 1,
458
+ };
459
+ });
460
+
461
+ // A building can have several paint layers, separated by another
462
+ // building in SVG draw order. Keep those layers in place, but give
463
+ // them one raster grid, parallax depth, and hover identity.
464
+ const buildingBounds = new Map();
465
+ descriptors.forEach(({ buildingId, bounds }) => {
466
+ const previous = buildingBounds.get(buildingId);
467
+ if (!previous) {
468
+ buildingBounds.set(buildingId, { ...bounds });
469
+ return;
470
+ }
471
+ const x = Math.min(previous.x, bounds.x);
472
+ const y = Math.min(previous.y, bounds.y);
473
+ buildingBounds.set(buildingId, {
474
+ x, y,
475
+ width: Math.max(previous.x + previous.width, bounds.x + bounds.width) - x,
476
+ height: Math.max(previous.y + previous.height, bounds.y + bounds.height) - y,
477
+ });
478
+ });
479
+ descriptors.forEach((descriptor) => {
480
+ descriptor.bounds = buildingBounds.get(descriptor.buildingId);
481
+ descriptor.depth = 0.25 + (1 - Math.min(1, descriptor.bounds.height / VIEWBOX.height)) * 0.75;
482
+ });
483
+
484
+ const terrain = sourceRoot.querySelector("#path6762");
485
+ const terrainDescriptor = {
486
+ id: terrain.id,
487
+ label: "foreground",
488
+ bounds: transformedBounds(terrain, sourceRoot),
489
+ content: serializer.serializeToString(terrain),
490
+ definitions,
491
+ parentTransform: matrixAttribute(parentMatrix(terrain, sourceRoot)),
492
+ pickable: false,
493
+ pickId: 0,
494
+ depth: 1.15,
495
+ };
496
+
497
+ for (let index = 0; index < descriptors.length; index += 1) {
498
+ loading.textContent = `rasterizing WebGL building ${index + 1} / ${descriptors.length}…`;
499
+ buildingSprites.push(await rasterize(descriptors[index]));
500
+ }
501
+ loading.textContent = "rasterizing skyline foreground…";
502
+ terrainSprite = await rasterize(terrainDescriptor);
503
+ sourceHost.remove();
504
+ }
505
+
506
+ function drawSprite(sprite, options) {
507
+ const pixelRatio = Math.min(devicePixelRatio || 1, 2);
508
+ const parallax = reducedMotion.matches ? 0 : pointer.parallax;
509
+ const shift = parallax * sprite.depth * 4 * pixelRatio;
510
+ gl.bindTexture(gl.TEXTURE_2D, sprite.texture);
511
+ gl.uniform4f(
512
+ uniforms.u_bounds,
513
+ sprite.drawBounds.x,
514
+ sprite.drawBounds.y,
515
+ sprite.drawBounds.width,
516
+ sprite.drawBounds.height,
517
+ );
518
+ gl.uniform2f(uniforms.u_shift, shift, 0);
519
+ gl.uniform2f(uniforms.u_texel, sprite.texelX, sprite.texelY);
520
+ gl.uniform3f(uniforms.u_pickColor, sprite.pickId / 255, 0, 0);
521
+ gl.uniform1f(uniforms.u_selected, options.selected ? 1 : 0);
522
+ gl.uniform1f(uniforms.u_time, options.time);
523
+ gl.uniform1i(uniforms.u_pickPass, options.pickPass ? 1 : 0);
524
+ gl.uniform1i(uniforms.u_flatPass, options.flatPass ? 1 : 0);
525
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
526
+ }
527
+
528
+ function prepareDrawState() {
529
+ gl.useProgram(program);
530
+ gl.bindVertexArray(vao);
531
+ gl.activeTexture(gl.TEXTURE0);
532
+ gl.uniform1i(uniforms.u_texture, 0);
533
+ gl.uniform2f(uniforms.u_viewport, canvas.width, canvas.height);
534
+ gl.uniform2f(uniforms.u_origin, projection.originX, projection.originY);
535
+ gl.uniform1f(uniforms.u_scale, projection.scale);
536
+ }
537
+
538
+ function pickBuilding() {
539
+ if (!ready || !pointer.inside) {
540
+ selectedIndex = -1;
541
+ return;
542
+ }
543
+ gl.bindFramebuffer(gl.FRAMEBUFFER, pickFramebuffer);
544
+ gl.viewport(0, 0, canvas.width, canvas.height);
545
+ gl.disable(gl.BLEND);
546
+ gl.clearColor(0, 0, 0, 0);
547
+ gl.clear(gl.COLOR_BUFFER_BIT);
548
+ prepareDrawState();
549
+ buildingSprites.forEach((sprite) => {
550
+ drawSprite(sprite, { pickPass: true, flatPass: false, selected: false, time: 0 });
551
+ });
552
+ drawSprite(terrainSprite, { pickPass: true, flatPass: true, selected: false, time: 0 });
553
+
554
+ const pixelRatio = Math.min(devicePixelRatio || 1, 2);
555
+ const pixel = new Uint8Array(4);
556
+ const x = Math.max(0, Math.min(canvas.width - 1, Math.floor(pointer.x * pixelRatio)));
557
+ const y = Math.max(0, Math.min(canvas.height - 1, canvas.height - 1 - Math.floor(pointer.y * pixelRatio)));
558
+ gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
559
+ selectedIndex = pixel[0] ? pixel[0] - 1 : -1;
560
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
561
+ updateTooltip();
562
+ }
563
+
564
+ function drawScene(time) {
565
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
566
+ gl.viewport(0, 0, canvas.width, canvas.height);
567
+ gl.enable(gl.BLEND);
568
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
569
+ gl.clearColor(0, 0, 0, 0);
570
+ gl.clear(gl.COLOR_BUFFER_BIT);
571
+ prepareDrawState();
572
+ const selectedBuilding = buildingSprites[selectedIndex]?.buildingId;
573
+ buildingSprites.forEach((sprite) => {
574
+ drawSprite(sprite, {
575
+ pickPass: false,
576
+ flatPass: false,
577
+ selected: sprite.buildingId === selectedBuilding,
578
+ time: reducedMotion.matches ? 0 : time,
579
+ });
580
+ });
581
+ drawSprite(terrainSprite, { pickPass: false, flatPass: true, selected: false, time: 0 });
582
+ }
583
+
584
+ function updateTooltip() {
585
+ const sprite = buildingSprites[selectedIndex];
586
+ if (!sprite || !pointer.inside) {
587
+ tooltip.hidden = true;
588
+ return;
589
+ }
590
+ tooltip.textContent = sprite.label;
591
+ tooltip.hidden = false;
592
+ const bounds = tooltip.getBoundingClientRect();
593
+ const gap = 18;
594
+ let x = pointer.x + gap;
595
+ let y = pointer.y + gap;
596
+ if (x + bounds.width > innerWidth - 6) {
597
+ x = pointer.x - gap - bounds.width;
598
+ }
599
+ if (y + bounds.height > innerHeight - 6) {
600
+ y = pointer.y - gap - bounds.height;
601
+ }
602
+ tooltip.style.left = `${Math.max(6, x)}px`;
603
+ tooltip.style.top = `${Math.max(6, y)}px`;
604
+ }
605
+
606
+ function frame(time) {
607
+ const resized = resizeCanvas();
608
+ if (resized && pointer.inside) {
609
+ pickPending = true;
610
+ }
611
+ if (pickPending) {
612
+ pickPending = false;
613
+ pickBuilding();
614
+ }
615
+ if (ready) {
616
+ drawScene(time);
617
+ }
618
+ requestAnimationFrame(frame);
619
+ }
620
+
621
+ canvas.addEventListener("pointermove", (event) => {
622
+ const bounds = canvas.getBoundingClientRect();
623
+ pointer.x = event.clientX - bounds.left;
624
+ pointer.y = event.clientY - bounds.top;
625
+ pointer.inside = true;
626
+ pointer.parallax = (pointer.x / Math.max(1, bounds.width) - 0.5) * 2;
627
+ pickPending = true;
628
+ }, { passive: true });
629
+
630
+ canvas.addEventListener("pointerleave", () => {
631
+ pointer.inside = false;
632
+ pointer.parallax = 0;
633
+ selectedIndex = -1;
634
+ tooltip.hidden = true;
635
+ });
636
+
637
+ reducedMotion.addEventListener("change", () => {
638
+ if (pointer.inside) pickPending = true;
639
+ });
640
+
641
+ async function initialize() {
642
+ program = createProgram();
643
+ cacheUniforms();
644
+ setupGeometry();
645
+ resizeCanvas();
646
+ requestAnimationFrame(frame);
647
+ await prepareSprites();
648
+ ready = true;
649
+ loading.hidden = true;
650
+ badge.textContent = `WebGL · ${buildingSprites.length} layers`;
651
+ badge.hidden = false;
652
+ window.__skylineWebGL = {
653
+ ready: true,
654
+ layerCount: buildingSprites.length,
655
+ buildingCount: new Set(buildingSprites.map((sprite) => sprite.buildingId)).size,
656
+ get selectedBuilding() { return buildingSprites[selectedIndex]?.buildingId || null; },
657
+ renderer: gl.getParameter(gl.RENDERER),
658
+ };
659
+ }
660
+
661
+ initialize().catch((error) => {
662
+ fail("The WebGL skyline could not be prepared.", error);
663
+ });
664
+ })();
665
+ </script>
666
+ </body>
667
+ </html>