@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,217 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // The University Club of Chicago, 76 East Monroe Street (Holabird & Roche, 1909): the
5
+ // geographic layout's model, on the mapped outline 126982632. The original layout has no
6
+ // model of it; the drawing shows it left of Willoughby Tower, behind the Monroe Building.
7
+ //
8
+ // A Gothic club on the corner of Michigan and Monroe. Its walls rise to a crenellated
9
+ // parapet, with the tall arched windows of its top hall in four bays to each street front.
10
+ // Behind the parapet an upper floor carries a steep roof whose gable faces Michigan, with
11
+ // pinnacles at its foot and a cross on its peak at OpenStreetMap's 67.7 m, and a small
12
+ // gable with its own cross on the Monroe side. Heights are measured on the drawing down from that peak; see
13
+ // docs/university-club-reference.md. Units are meters; +x is east, +z is south.
14
+ export const universityClubLevels = Object.freeze({
15
+ band: 46.2, // the band under the parapet
16
+ parapet: 48.95, // the parapet wall
17
+ walls: 51, // the parapet's top, between the merlons
18
+ merlons: 51.75,
19
+ eaves: 56.4, // the upper floor's eaves
20
+ pinnacles: 62.5,
21
+ ridge: 67.7, // the gable's peak, OpenStreetMap's height
22
+ cross: 69.75,
23
+ dormer: 59.3, // the Monroe side's small gable
24
+ dormerCross: 60.4,
25
+ back: 3.3, // the upper floor's setback from Michigan
26
+ sides: 1.3, // and from Monroe and the north wall
27
+ });
28
+ const h = universityClubLevels;
29
+ // The skins' colours: the window tones, and the stone and its band, which no window shares.
30
+ export const universityClubPalette = Object.freeze({ glass: [0x4a4a4a, 0x4e4e4e, 0x525252, 0x565656], lit: 0x8c8c8c, dim: 0x444444, stone: 0xa4a4a4, band: 0xb4b4b4 });
31
+ const palette = universityClubPalette;
32
+ const color = (hex) => new THREE.Color(hex);
33
+ const glassTones = palette.glass.map(color);
34
+ const litGlass = color(palette.lit), dimGlass = color(palette.dim);
35
+ const stone = color(palette.stone), band = color(palette.band), core = color(0x2e2e2e), roofing = color(0x343434), flat = color(0x4a4a4a);
36
+ function paneColor(row, bay, wall) {
37
+ const hash = (Math.imul(row + 43, 0x9e3779b1) ^ Math.imul(bay + 71, 0x85ebca77) ^ Math.imul(wall + 59, 0xc2b2ae3d)) >>> 0;
38
+ const value = hash % 101;
39
+ if (value < 4)
40
+ return litGlass;
41
+ if (value < 8)
42
+ return dimGlass;
43
+ return glassTones[value % glassTones.length];
44
+ }
45
+ const wallRows = [
46
+ { lo: 0.25, hi: 1, kind: "wall", floor: 1 },
47
+ { lo: 1, hi: 4.4, kind: "full", floor: 1 },
48
+ ];
49
+ for (let n = 2; n <= 9; n += 1) {
50
+ const base = 5 + (n - 2) * 3.525;
51
+ wallRows.push({ lo: wallRows.at(-1).hi, hi: base + 0.8, kind: "wall", floor: n });
52
+ wallRows.push({ lo: base + 0.8, hi: base + 2.9, kind: "pair", floor: n });
53
+ }
54
+ wallRows.push({ lo: wallRows.at(-1).hi, hi: 33.65, kind: "wall", floor: 10 });
55
+ wallRows.push({ lo: 33.65, hi: 36.7, kind: "full", floor: 10 });
56
+ wallRows.push({ lo: 36.7, hi: 37.3, kind: "wall", floor: 11 });
57
+ wallRows.push({ lo: 37.3, hi: 43.1, kind: "full", floor: 11 });
58
+ wallRows.push({ lo: 43.1, hi: 43.9, kind: "head", floor: 11 });
59
+ wallRows.push({ lo: 43.9, hi: h.band, kind: "wall", floor: 11 });
60
+ wallRows.push({ lo: h.band, hi: h.parapet, kind: "band", floor: 12 });
61
+ wallRows.push({ lo: h.parapet, hi: h.walls, kind: "wall", floor: 12 });
62
+ // The upper floor's windows under its eaves, and the Monroe gable's.
63
+ const upperRows = [
64
+ { lo: h.walls, hi: 52, kind: "wall", floor: 13 },
65
+ { lo: 52, hi: 55.9, kind: "full", floor: 13 },
66
+ { lo: 55.9, hi: h.eaves, kind: "wall", floor: 13 },
67
+ ];
68
+ const dormerRows = [
69
+ { lo: h.walls, hi: 53.7, kind: "wall", floor: 13 },
70
+ { lo: 53.7, hi: 55.9, kind: "full", floor: 13 },
71
+ { lo: 55.9, hi: h.eaves, kind: "wall", floor: 13 },
72
+ ];
73
+ export function createUniversityClubGeographicBuilding(record, projectPlan, offset) {
74
+ const kit = createBuilder(record.name, record.id);
75
+ const shell = kit.batch("University Club · shell", kit.material(0xffffff, { vertexColors: true }));
76
+ const wall = kit.batch("University Club · stone and windows", kit.material(0xffffff, { vertexColors: true }));
77
+ const ornament = kit.batch("University Club · merlons and pinnacles", kit.material(0xa0a0a0));
78
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
79
+ // The lot's south-east corner, with its Monroe front running west and its Michigan front
80
+ // north.
81
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
82
+ const northEast = lot.reduce((best, p) => (p[0] - p[1] > best[0] - best[1] ? p : best));
83
+ const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
84
+ const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
85
+ const west = unit(neighbours[0][0] < neighbours[1][0] ? neighbours[0] : neighbours[1]), north = unit(northEast);
86
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
87
+ // The inverse of `at`: a point's distances along Monroe and Michigan, whose fronts meet a
88
+ // little off square.
89
+ const skew = west[0] * north[1] - west[1] * north[0];
90
+ const inLot = (p) => {
91
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
92
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
93
+ };
94
+ const [length, depth] = [Math.max(...lot.map((p) => inLot(p)[0])), inLot(northEast)[1]];
95
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
96
+ const lift = ([x, z], y) => [x, y, z];
97
+ // The main block, to the parapet's top.
98
+ planOf(lot).forEach((run) => {
99
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
100
+ kit.quad(shell, [[a[0], 0, a[1]], [b[0], 0, b[1]], [b[0], h.walls, b[1]], [a[0], h.walls, a[1]]], [[n[0], 0, n[1]]], core);
101
+ });
102
+ paintedSlab(kit, shell, lot, 0, false, flat);
103
+ paintedSlab(kit, shell, lot, h.walls, true, flat);
104
+ // A closed block whose walls rise from `base` to `eaves` under a gable roof to `ridge`,
105
+ // its ridge running along the lot's a axis (west from Michigan) or its b axis (north
106
+ // from Monroe); the gables are stone and the slopes roofing.
107
+ const gabled = (a0, a1, b0, b1, base, eaves, ridge, along) => {
108
+ const corners = rect(a0, a1, b0, b1);
109
+ planOf(corners).forEach((run) => {
110
+ const p = run.at(0), q = run.at(run.length), n = run.normal(0);
111
+ kit.quad(shell, [lift(p, base), lift(q, base), lift(q, eaves), lift(p, eaves)], [[n[0], 0, n[1]]], core);
112
+ });
113
+ paintedSlab(kit, shell, corners, base, false, flat);
114
+ const middle = along === "a" ? (b0 + b1) / 2 : (a0 + a1) / 2;
115
+ const ridgeAt = (s) => lift(along === "a" ? at(s, middle) : at(middle, s), ridge);
116
+ const [s0, s1] = along === "a" ? [a0, a1] : [b0, b1];
117
+ const eave = (s, t) => lift(along === "a" ? at(s, t) : at(t, s), eaves);
118
+ const [t0, t1] = along === "a" ? [b0, b1] : [a0, a1];
119
+ const centre = lift(at((a0 + a1) / 2, (b0 + b1) / 2), (eaves + ridge) / 2);
120
+ // Each face's outward normal, from its points and the block's middle.
121
+ const face = (points, paint) => {
122
+ const [a, b, c] = points;
123
+ const u = new THREE.Vector3(b[0] - a[0], b[1] - a[1], b[2] - a[2]), v = new THREE.Vector3(c[0] - a[0], c[1] - a[1], c[2] - a[2]);
124
+ const n = u.cross(v).normalize(), mid = points.reduce((sum, p) => [sum[0] + p[0] / points.length, sum[1] + p[1] / points.length, sum[2] + p[2] / points.length], [0, 0, 0]);
125
+ if (n.x * (mid[0] - centre[0]) + n.y * (mid[1] - centre[1]) + n.z * (mid[2] - centre[2]) < 0)
126
+ n.negate();
127
+ const normal = [n.x, n.y, n.z];
128
+ if (points.length === 3)
129
+ kit.triangle(shell, points, [normal, normal, normal], paint);
130
+ else
131
+ kit.quad(shell, points, [normal], paint);
132
+ };
133
+ face([eave(s0, t0), eave(s1, t0), ridgeAt(s1), ridgeAt(s0)], roofing);
134
+ face([eave(s1, t1), eave(s0, t1), ridgeAt(s0), ridgeAt(s1)], roofing);
135
+ face([eave(s0, t1), eave(s0, t0), ridgeAt(s0)], stone);
136
+ face([eave(s1, t0), eave(s1, t1), ridgeAt(s1)], stone);
137
+ return corners;
138
+ };
139
+ const upper = gabled(h.back, length - h.sides, h.sides, depth - h.sides, h.walls, h.eaves, h.ridge, "a");
140
+ // The Monroe side's small gable stands 15 cm proud of the upper floor, clear of its skin,
141
+ // its foot sunk 10 cm into the main block.
142
+ const dormer = gabled(13.7, 18.7, h.sides - 0.15, 5, h.walls - 0.1, h.eaves, h.dormer, "b");
143
+ // A wall's skin, one box to a chain the `keep` test accepts, held 2 cm clear of its ends
144
+ // or 9 cm where the outline turns in. `openings` lays a wall's columns out across it and
145
+ // says how much of a window each crosses: 0 none, 1 a full window's edge, 2 a pair's
146
+ // window, 3 a pair's window and the head, 4 the head between a pair. Floors below
147
+ // `lowest` stay stone.
148
+ const glazing = { wall: [], band: [], pair: [2, 3], full: [1, 2, 3, 4], head: [3, 4] };
149
+ const skin = (corners, rows, seed, keep, openings, lowest = 1) => {
150
+ const plan = planOf(corners), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
151
+ chainsOf(plan).forEach((chain, index) => {
152
+ if (!chain.runs.every((run) => keep(plan[run])))
153
+ return;
154
+ const first = chain.runs[0], last = chain.runs.at(-1);
155
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
156
+ const [columns, crossing] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
157
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
158
+ const row = rows[r];
159
+ if (row.kind === "band")
160
+ return band;
161
+ return row.floor >= lowest && glazing[row.kind].includes(crossing(bay)) ? paneColor(row.floor, bay, seed * 8 + index) : stone;
162
+ }, stone);
163
+ });
164
+ };
165
+ // A position along a wall from its start, given in lot coordinates across it.
166
+ const along = (length, start, end, target) => {
167
+ const [p, q] = [inLot(start), inLot(end)], axis = Math.abs(q[0] - p[0]) > Math.abs(q[1] - p[1]) ? 0 : 1;
168
+ return length * (target - p[axis]) / (q[axis] - p[axis]);
169
+ };
170
+ // The street fronts' bays, 3.5 m wide and 4.55 m apart from 3.1 m along each front, as the
171
+ // drawing has them; each splits into a full window's edges, a pair's two windows, and the
172
+ // head over their mullion.
173
+ const bays = (length, start, end) => {
174
+ const centres = Array.from({ length: 12 }, (_, k) => along(length, start, end, 3.1 + 4.55 * k)).filter((c) => c - 1.75 > 0.1 && c + 1.75 < length - 0.1).sort((p, q) => p - q);
175
+ const steps = [-1.75, -1.5, -0.875, -0.3, 0.3, 0.875, 1.5, 1.75];
176
+ const columns = [0, ...centres.flatMap((c) => steps.map((s) => c + s)), length];
177
+ // Bays count from one: a pier, then 1, 2, 3, 4, 3, 2, 1 across each window.
178
+ return [columns, (bay) => (bay > 8 * centres.length || (bay - 1) % 8 === 0 ? 0 : [0, 1, 2, 3, 4, 3, 2, 1][(bay - 1) % 8])];
179
+ };
180
+ // Windows `width` wide every `pitch` metres, centred on each wall, glazed as a pair's.
181
+ const spaced = (width, pitch) => (length) => {
182
+ const count = Math.max(1, Math.round(length / pitch)), centres = Array.from({ length: count }, (_, k) => length * (k + 0.5) / count);
183
+ const columns = [0, ...centres.flatMap((c) => [c - width / 2, c + width / 2]), length];
184
+ return [columns, (bay) => (bay % 2 === 0 && bay <= 2 * count ? 2 : 0)];
185
+ };
186
+ const facing = (x, z) => (run) => run.normal(0)[0] * x + run.normal(0)[1] * z > 0.9;
187
+ const street = (run) => facing(1, 0)(run) || facing(0, 1)(run), party = facing(0, -1);
188
+ skin(lot, wallRows, 0, street, bays);
189
+ // The west wall's windows run from the ground; the north wall is shared with the
190
+ // six-storey 30 South Michigan and stays blank below its eighth floor.
191
+ skin(lot, wallRows, 1, (run) => !street(run) && !party(run), spaced(2.4, 4.55));
192
+ skin(lot, wallRows, 4, party, spaced(2.4, 4.55), 8);
193
+ // The upper floor's windows, 3.6 m wide about 5.5 m apart, and the small gable's one.
194
+ skin(upper, upperRows, 2, () => true, spaced(3.6, 5.5));
195
+ skin(dormer, dormerRows, 3, facing(0, 1), spaced(2.4, 5));
196
+ // Merlons along the street fronts' parapet, 1.2 m wide every 2.1 m, 3 cm inside the walls;
197
+ // pinnacles at the gable's foot, rising from 10 cm inside the main block; and a cross on
198
+ // each gable's peak, its foot inside the roof: the Michigan gable's facing Michigan, the
199
+ // small gable's facing Monroe.
200
+ const block = (a0, a1, b0, b1, lo, hi) => kit.prism(ornament, planOf(rect(a0, a1, b0, b1)), [lo, hi]);
201
+ for (let s = 1.05; s + 0.6 < depth; s += 2.1)
202
+ block(0.03, 0.53, s - 0.6, s + 0.6, h.walls, h.merlons);
203
+ for (let s = 1.05; s + 0.6 < length; s += 2.1)
204
+ block(Math.max(s - 0.6, 0.56), s + 0.6, 0.03, 0.53, h.walls, h.merlons);
205
+ for (const b of [h.sides, depth - h.sides])
206
+ block(h.back - 0.6, h.back + 0.6, b - 0.6, b + 0.6, h.walls - 0.1, h.pinnacles);
207
+ const peak = depth / 2;
208
+ block(h.back + 0.15, h.back + 0.45, peak - 0.15, peak + 0.15, h.ridge - 0.7, h.cross);
209
+ block(h.back + 0.2, h.back + 0.4, peak - 0.6, peak + 0.6, 68.9, 69.25);
210
+ block(16.075, 16.325, h.sides + 0.2, h.sides + 0.45, h.dormer - 0.5, h.dormerCross);
211
+ block(15.7, 16.7, h.sides + 0.25, h.sides + 0.4, 59.65, 60);
212
+ const model = kit.finish({ height: h.cross, outlines: [shell, ornament], opacity: 0.16 });
213
+ model.building.position.set(offset[0], 0, offset[1]);
214
+ model.building.userData["geography"] = record;
215
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/university-club-reference.md" };
216
+ return model;
217
+ }
@@ -0,0 +1,195 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { createBuilder, inside, polygonOf } from "./building-kit.js";
3
+ import { chainSkin, chainsOf, orient, paintedSlab, planOf, turnsAt } from "./facade-grid.js";
4
+ // Willoughby Tower, 8 South Michigan Avenue (Samuel N. Crowen, 1929): the geographic
5
+ // layout's model, on the mapped outline 124873939. The original layout has no model of it;
6
+ // the drawing shows it left of Six North Michigan, outside the excerpt it is fitted to.
7
+ //
8
+ // A limestone Gothic tower on an L-shaped lot at Michigan and Madison. The base fills the
9
+ // lot to the setback over the 23rd floor, under a parapet of pinnacles. A shaft rises at
10
+ // the lot's south-east corner, 17.4 m along its south wall and 11 m along Michigan, with a
11
+ // shoulder four floors high west of it, to a crown of arched windows under pinnacles and
12
+ // gables, its tallest pinnacles at the published 133.5 m. Heights are measured on the
13
+ // drawing down from that top; see docs/willoughby-tower-reference.md. Units are meters; +x
14
+ // is east, +z is south.
15
+ export const willoughbyLevels = Object.freeze({
16
+ setback: 80.5, // the base's parapet, over the 23rd floor
17
+ shoulder: 92, // the shoulder's roof, west of the shaft
18
+ shaft: 123.5, // the shaft's top, under the crown
19
+ crown: 131.7, // the crown's parapet
20
+ gable: 132.2, // the gables over the crown's long faces
21
+ sidePinnacles: 132.5, // the pinnacles flanking the crown's short faces
22
+ top: 133.5, // the crown's central pinnacles, the published architectural height
23
+ shaftWidth: 17.4, // along the south wall from the south-east corner
24
+ shaftDepth: 11, // along Michigan from the south-east corner
25
+ crownEnds: 2.1, // the crown's inset from the shaft's Michigan and west faces
26
+ crownSides: 1.2, // and from its south and north faces
27
+ crownWindows: 10.2, // the long faces' windows, centred this far west of Michigan
28
+ });
29
+ const h = willoughbyLevels;
30
+ // Each floor's level: a two-storey granite base, then 3.35 m floors to the crown's.
31
+ const floor = (n) => (n < 2 ? 0 : 6 + (n - 2) * 3.35);
32
+ const color = (hex) => new THREE.Color(hex);
33
+ const glassTones = [0x2a2a2a, 0x2e2e2e, 0x323232, 0x363636].map(color);
34
+ const litGlass = color(0x6e6e6e), dimGlass = color(0x444444);
35
+ const stone = color(0x585858), granite = color(0x4a4a4a), strip = color(0x2d2d2d), core = color(0x262626), roofing = color(0x3e3e3e);
36
+ function paneColor(row, bay, wall) {
37
+ const hash = (Math.imul(row + 29, 0x9e3779b1) ^ Math.imul(bay + 53, 0x85ebca77) ^ Math.imul(wall + 41, 0xc2b2ae3d)) >>> 0;
38
+ const value = hash % 101;
39
+ if (value < 4)
40
+ return litGlass;
41
+ if (value < 8)
42
+ return dimGlass;
43
+ return glassTones[value % glassTones.length];
44
+ }
45
+ const allRows = [{ lo: 0.25, hi: floor(3) - 0.3, kind: "granite", floor: 1 }];
46
+ for (let n = 3; n <= 36; n += 1) {
47
+ allRows.push({ lo: floor(n) - 0.3, hi: floor(n) + 0.9, kind: "stone", floor: n });
48
+ allRows.push({ lo: floor(n) + 0.9, hi: floor(n + 1) - 0.3, kind: "glass", floor: n });
49
+ }
50
+ allRows.push({ lo: floor(37) - 0.3, hi: 124.7, kind: "stone", floor: 37 });
51
+ allRows.push({ lo: 124.7, hi: 127.1, kind: "tall", floor: 37 });
52
+ allRows.push({ lo: 127.1, hi: 128.6, kind: "arch", floor: 37 });
53
+ allRows.push({ lo: 128.6, hi: 129.1, kind: "head", floor: 37 });
54
+ allRows.push({ lo: 129.1, hi: h.crown, kind: "stone", floor: 38 });
55
+ const rowsBetween = (lo, hi) => allRows
56
+ .map((row) => ({ ...row, lo: Math.max(row.lo, lo), hi: Math.min(row.hi, hi) }))
57
+ .filter((row) => row.hi - row.lo > 0.05);
58
+ export function createWilloughbyTowerGeographicBuilding(record, projectPlan, offset) {
59
+ const kit = createBuilder(record.name, record.id);
60
+ const shell = kit.batch("Willoughby Tower · shell", kit.material(0xffffff, { vertexColors: true }));
61
+ const wall = kit.batch("Willoughby Tower · stone and windows", kit.material(0xffffff, { vertexColors: true }));
62
+ const pinnacles = kit.batch("Willoughby Tower · pinnacles", kit.material(0x626262));
63
+ const lot = orient(polygonOf(projectPlan(record.footprint.coordinates)));
64
+ // The lot's south-east corner, and its south wall and Michigan front running from it.
65
+ const corner = lot.reduce((best, p) => (p[0] + p[1] > best[0] + best[1] ? p : best));
66
+ const neighbours = [lot[(lot.indexOf(corner) + 1) % lot.length], lot[(lot.indexOf(corner) + lot.length - 1) % lot.length]];
67
+ const unit = (p) => { const d = Math.hypot(p[0] - corner[0], p[1] - corner[1]); return [(p[0] - corner[0]) / d, (p[1] - corner[1]) / d]; };
68
+ const [west, north] = neighbours[0][0] < neighbours[1][0] ? [unit(neighbours[0]), unit(neighbours[1])] : [unit(neighbours[1]), unit(neighbours[0])];
69
+ const at = (a, b) => [corner[0] + west[0] * a + north[0] * b, corner[1] + west[1] * a + north[1] * b];
70
+ const rect = (a0, a1, b0, b1) => orient([at(a0, b0), at(a1, b0), at(a1, b1), at(a0, b1)]);
71
+ // The south wall's length: the farthest lot corner on its line.
72
+ const southWall = Math.max(...lot.filter((p) => Math.abs((p[0] - corner[0]) * west[1] - (p[1] - corner[1]) * west[0]) < 0.5).map((p) => (p[0] - corner[0]) * west[0] + (p[1] - corner[1]) * west[1]));
73
+ const shaft = rect(0, h.shaftWidth, 0, h.shaftDepth), shoulder = rect(h.shaftWidth, southWall, 0, h.shaftDepth), crown = rect(h.crownEnds, h.shaftWidth - h.crownEnds, h.crownSides, h.shaftDepth - h.crownSides);
74
+ // A solid on an outline between two heights.
75
+ const solid = (corners, lo, hi) => {
76
+ planOf(corners).forEach((run) => {
77
+ const a = run.at(0), b = run.at(run.length), n = run.normal(0);
78
+ kit.quad(shell, [[a[0], lo, a[1]], [b[0], lo, b[1]], [b[0], hi, b[1]], [a[0], hi, a[1]]], [[n[0], 0, n[1]]], core);
79
+ });
80
+ paintedSlab(kit, shell, corners, lo, false, roofing);
81
+ paintedSlab(kit, shell, corners, hi, true, roofing);
82
+ };
83
+ solid(lot, 0, h.setback);
84
+ solid(shoulder, h.setback, h.shoulder);
85
+ solid(shaft, h.setback, h.shaft);
86
+ solid(crown, h.shaft, h.crown);
87
+ // Whether a run lies along an outline's edges.
88
+ const lies = (run, on) => [0, run.length / 2, run.length].every((s) => {
89
+ const p = run.at(s);
90
+ return on.some((q, i) => {
91
+ const r = on[(i + 1) % on.length], [dx, dz] = [r[0] - q[0], r[1] - q[1]], l = Math.hypot(dx, dz);
92
+ const t = Math.max(0, Math.min(1, ((p[0] - q[0]) * dx + (p[1] - q[1]) * dz) / (l * l)));
93
+ return Math.hypot(q[0] + dx * t - p[0], q[1] + dz * t - p[1]) < 0.05;
94
+ });
95
+ });
96
+ const crownGlass = { tall: ["tallSide", "tallMiddle"], arch: ["tallSide", "tallMiddle", "side", "middle"], head: ["tallMiddle", "middle"] };
97
+ const skin = (corners, lo, hi, seed, keep, openings) => {
98
+ const plan = planOf(corners), rows = rowsBetween(lo, hi), heights = [rows[0].lo, ...rows.map((row) => row.hi)];
99
+ chainsOf(plan).forEach((chain, index) => {
100
+ if (!chain.runs.every((run) => keep(plan[run])))
101
+ return;
102
+ const first = chain.runs[0], last = chain.runs.at(-1);
103
+ const s0 = turnsAt(plan, first)[0] > 0 ? 0.09 : 0.02, s1 = chain.length - (turnsAt(plan, last)[1] > 0 ? 0.09 : 0.02);
104
+ const [columns, kind] = openings(chain.length, plan[first].at(0), plan[last].at(plan[last].length));
105
+ chainSkin(kit, wall, plan, chain, 0, chain.runs.length - 1, s0, s1, columns, heights, 0.02, 0.07, (bay, r) => {
106
+ const row = rows[r], opening = kind(bay);
107
+ if (row.kind === "granite")
108
+ return granite;
109
+ if (opening === "strip")
110
+ return row.floor >= 37 ? stone : strip;
111
+ if (row.kind === "tall" || row.kind === "arch" || row.kind === "head")
112
+ return crownGlass[row.kind].includes(opening) ? paneColor(row.floor, bay, seed * 8 + index) : stone;
113
+ if (row.kind === "stone" || opening === "stone")
114
+ return stone;
115
+ return paneColor(row.floor, bay, seed * 8 + index);
116
+ }, stone);
117
+ });
118
+ };
119
+ // Windows every 3 m, 1.6 m wide, between stone piers; a short wall stays stone.
120
+ const punched = (length) => {
121
+ const bays = Math.round(length / 3);
122
+ if (bays < 1)
123
+ return [[0, length], () => "stone"];
124
+ const columns = [0, ...Array.from({ length: bays }, (_, i) => [length * (i + 0.5) / bays - 0.8, length * (i + 0.5) / bays + 0.8]).flat(), length];
125
+ return [columns, (bay) => (bay % 2 === 0 && bay >= 2 && bay <= 2 * bays ? "window" : "stone")];
126
+ };
127
+ // The shaft's Michigan face: three dark strips in the middle, a window near each edge,
128
+ // symmetrical about its middle.
129
+ const stripped = (length) => {
130
+ const at = (f) => length * f;
131
+ const columns = [0, at(0.15), at(0.25), at(0.315), at(0.385), at(0.465), at(0.535), at(0.615), at(0.685), at(0.75), at(0.85), length];
132
+ return [columns, (bay) => (bay === 2 || bay === 10 ? "window" : bay === 4 || bay === 6 || bay === 8 ? "strip" : "stone")];
133
+ };
134
+ // The inverse of `at`: a point's distances along the south wall and the Michigan front,
135
+ // which meet a degree off square.
136
+ const skew = west[0] * north[1] - west[1] * north[0];
137
+ const inLot = (p) => {
138
+ const [x, z] = [p[0] - corner[0], p[1] - corner[1]];
139
+ return [(x * north[1] - z * north[0]) / skew, (west[0] * z - west[1] * x) / skew];
140
+ };
141
+ // The crown: three arched windows to a face, 0.9 m wide and 1.7 m apart, their heads
142
+ // narrowing to the middle 0.45 m. A short face's are tall and centred on it; a long
143
+ // face's are short, centred crownWindows west of Michigan, as the drawing has them.
144
+ const arched = (length, start, end) => {
145
+ const [a0, b0] = inLot(start), [a1, b1] = inLot(end), long = Math.abs(a1 - a0) > Math.abs(b1 - b0);
146
+ const middle = long ? length * (h.crownWindows - a0) / (a1 - a0) : length * (h.shaftDepth / 2 - b0) / (b1 - b0);
147
+ const columns = [0, ...[-1.7, 0, 1.7].flatMap((d) => [middle + d - 0.45, middle + d - 0.225, middle + d + 0.225, middle + d + 0.45]), length];
148
+ // Bays count from one: a pier, then side, middle, side for each window, then the last pier.
149
+ return [columns, (bay) => { const k = (bay - 1) % 4; return bay > 12 || k === 0 ? "stone" : k === 2 ? (long ? "middle" : "tallMiddle") : long ? "side" : "tallSide"; }];
150
+ };
151
+ const michigan = (run) => run.normal(0)[0] > 0.9;
152
+ skin(lot, 0.25, h.setback, 0, () => true, punched);
153
+ skin(shoulder, h.setback, h.shoulder, 1, (run) => !lies(run, shaft), punched);
154
+ skin(shaft, h.setback, h.shaft, 2, (run) => michigan(run), stripped);
155
+ skin(shaft, h.setback, h.shaft, 3, (run) => !michigan(run) && !lies(run, shoulder), punched);
156
+ skin(shaft, h.shoulder, h.shaft, 4, (run) => lies(run, shoulder), punched);
157
+ skin(crown, h.shaft, h.crown, 5, () => true, arched);
158
+ // Pinnacles on the base's parapet along the two street fronts, clear of the shaft. On the
159
+ // crown, a central pinnacle reaching the published top stands over each face's windows,
160
+ // flanked on the short faces by lower pinnacles and on the long faces by a gable.
161
+ const pinnacle = (p, lo, hi, size) => {
162
+ const c = size / 2;
163
+ kit.prism(pinnacles, planOf(orient([[p[0] - c, p[1] - c], [p[0] + c, p[1] - c], [p[0] + c, p[1] + c], [p[0] - c, p[1] + c]])), [lo, hi]);
164
+ };
165
+ const stations = [];
166
+ planOf(lot).forEach((run) => {
167
+ const n = run.normal(0);
168
+ if (!(n[0] > 0.9 || n[1] < -0.9))
169
+ return;
170
+ const count = Math.max(1, Math.round(run.length / 6));
171
+ for (let i = 0; i <= count; i += 1) {
172
+ const p = run.at(run.length * i / count);
173
+ if (inside(rect(-1.5, h.shaftWidth + 1.5, -1.5, h.shaftDepth + 1.5), p) || stations.some((q) => Math.hypot(p[0] - q[0], p[1] - q[1]) < 2))
174
+ continue;
175
+ stations.push(p);
176
+ pinnacle(p, h.setback, h.setback + 3, 0.9);
177
+ }
178
+ });
179
+ for (const a of [h.crownEnds, h.shaftWidth - h.crownEnds]) {
180
+ pinnacle(at(a, h.shaftDepth / 2), h.crown, h.top, 1.6);
181
+ for (const d of [-2.9, 2.9])
182
+ pinnacle(at(a, h.shaftDepth / 2 + d), h.crown, h.sidePinnacles, 1.1);
183
+ }
184
+ for (const [b, inward] of [[h.crownSides, 1], [h.shaftDepth - h.crownSides, -1]]) {
185
+ pinnacle(at(h.crownWindows, b), h.crown, h.top, 1.6);
186
+ // The gable stands 10 cm inside the wall, its foot sunk into the crown.
187
+ const gable = rect(h.crownWindows - 3.45, h.crownWindows + 3.45, b + inward * 0.1, b + inward * 0.6);
188
+ kit.prism(pinnacles, planOf(gable), [h.crown - 0.1, h.gable]);
189
+ }
190
+ const model = kit.finish({ height: h.top, outlines: [shell, pinnacles], opacity: 0.16 });
191
+ model.building.position.set(offset[0], 0, offset[1]);
192
+ model.building.userData["geography"] = record;
193
+ model.building.userData["geographicDetail"] = { levels: h, source: "docs/willoughby-tower-reference.md" };
194
+ return model;
195
+ }
@@ -0,0 +1,64 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { celebrations, windowWord } from "./celebrations.js";
3
+ // Reuse the actual glass triangles. The emission mask changes neither the
4
+ // silhouette nor depth/hover ownership, and needs no overlay or extra draw call.
5
+ export function createWindowIllumination(geometry, material, cells, columns, lineTopFloors) {
6
+ const colors = geometry.getAttribute("color");
7
+ const original = new Float32Array(colors.array);
8
+ const emission = new THREE.Float32BufferAttribute(new Float32Array(colors.count), 1);
9
+ geometry.setAttribute("windowLight", emission);
10
+ material.onBeforeCompile = (shader) => {
11
+ shader.vertexShader = `attribute float windowLight;\nvarying float vWindowLight;\n${shader.vertexShader}`
12
+ .replace("#include <begin_vertex>", "#include <begin_vertex>\nvWindowLight = windowLight;");
13
+ shader.fragmentShader = `varying float vWindowLight;\n${shader.fragmentShader}`
14
+ .replace("#include <emissivemap_fragment>", "#include <emissivemap_fragment>\ntotalEmissiveRadiance += vec3(vWindowLight);");
15
+ };
16
+ material.customProgramCacheKey = () => "skyline-window-illumination-v1";
17
+ let active = null, litWindows = 0;
18
+ const available = new Set(cells.map((cell) => `${cell.column}:${cell.floor}`));
19
+ const masks = new Map(celebrations.map((preset) => {
20
+ const lit = new Set();
21
+ preset.lines.forEach((word, line) => {
22
+ const rows = windowWord(word), width = rows[0].length;
23
+ if (width > columns - 2)
24
+ throw new Error(`${word} does not fit the facade`);
25
+ const left = Math.floor((columns - width) / 2);
26
+ const top = lineTopFloors[line];
27
+ if (top === undefined)
28
+ throw new Error(`No facade zone for message line ${line + 1}`);
29
+ rows.forEach((row, down) => [...row].forEach((pixel, across) => {
30
+ if (pixel === "1")
31
+ lit.add(`${left + across}:${top - down}`);
32
+ }));
33
+ });
34
+ // Refuse a layout which silently drops letters into missing window cells.
35
+ for (const key of lit)
36
+ if (!available.has(key))
37
+ throw new Error(`Missing celebration window: ${key}`);
38
+ return [preset.id, lit];
39
+ }));
40
+ return {
41
+ presets: celebrations,
42
+ get active() { return active; },
43
+ get litWindows() { return litWindows; },
44
+ set(next) {
45
+ const lit = next === null ? undefined : masks.get(next);
46
+ if (next !== null && !lit)
47
+ throw new Error(`Unknown celebration: ${next}`);
48
+ active = next;
49
+ litWindows = lit?.size ?? 0;
50
+ colors.array.set(original);
51
+ emission.array.fill(0);
52
+ if (lit)
53
+ for (const cell of cells) {
54
+ const on = lit.has(`${cell.column}:${cell.floor}`);
55
+ for (let vertex = cell.vertex; vertex < cell.vertex + 6; vertex += 1) {
56
+ // Closed shades darken only the message face's office glass.
57
+ colors.setXYZ(vertex, 0.018, 0.018, 0.018);
58
+ emission.setX(vertex, on ? 1 : 0);
59
+ }
60
+ }
61
+ colors.needsUpdate = emission.needsUpdate = true;
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,92 @@
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
+ <link rel="icon" href="data:," />
9
+ <link rel="stylesheet" href="study.css" />
10
+ <title>3D Chicago Skyline</title>
11
+ </head>
12
+ <body class="skyline-3d">
13
+ <main class="viewport" id="viewport">
14
+ <canvas id="building" tabindex="0" role="img" aria-label="Interactive 3D Chicago skyline: every mapped building of the skyline study, seen from the drawing's camera on the lakefront by the Adler Planetarium" aria-describedby="camera-hint"></canvas>
15
+ <div class="loading" id="loading" role="status">
16
+ <span class="visually-hidden">Preparing the skyline…</span>
17
+ <!-- skyline-loading:start -->
18
+ <svg class="skyline-trace" viewBox="0 0 1000 390.01" aria-hidden="true" focusable="false">
19
+ <defs><path id="skyline-loading-outline" pathLength="1" d="M0 390.01 L24 390.01 L24 326.71 L36.78 320.28 L76.29 320.28 L78.85 321.63 L79.08 320.54 L87.68 320.54 L87.92 316.21 L133.24 316.74 L133.47 317.75 L133.94 317.75 L134.17 316.74 L164.38 316.74 L164.85 317.75 L165.08 321.6 L181.12 321.6 L181.35 329.03 L189.48 329.03 L190.41 330.18 L207.85 330.18 L208.08 328.36 L208.54 328.36 L209.01 331.14 L212.26 338.16 L212.49 334.77 L213.89 334.77 L214.12 344.27 L216.21 344.27 L216.45 348.52 L234.57 348.52 L234.81 318.86 L238.53 318.86 L238.76 308.7 L242.48 308.7 L242.71 280.86 L244.57 280.86 L244.8 273.62 L245.03 272.91 L245.73 272.91 L245.96 272.02 L249.68 272.02 L249.91 273.17 L251.54 273.17 L251.77 272.02 L253.87 272.02 L254.1 273.17 L255.26 272.91 L255.49 272.02 L257.58 272.02 L257.82 272.91 L258.75 272.91 L258.98 280.86 L260.84 280.86 L261.07 318.86 L261.3 316.21 L263.63 316.21 L263.86 318.86 L265.02 318.86 L265.25 316.21 L268.04 316.21 L268.28 333.8 L272.69 333.8 L272.92 321.07 L274.55 321.07 L274.78 314 L288.96 314 L289.19 317.54 L300.58 317.54 L300.81 310.9 L311.51 310.9 L311.74 229.87 L323.13 229.87 L323.36 222.27 L330.8 222.27 L331.03 219.97 L349.39 220.16 L349.62 229.87 L352.41 229.87 L352.64 249.49 L382.16 249.49 L382.39 383.11 L386.81 383.11 L387.04 262.54 L406.57 233.23 L407.26 233.23 L410.05 252.75 L410.52 252.78 L410.75 233.23 L411.21 233.23 L439.11 263.31 L439.34 313.74 L444.22 313.74 L444.45 202.79 L513.71 202.79 L513.95 228.1 L528.36 228.1 L528.59 76.89 L548.34 76.8 L548.58 67.61 L561.59 67.52 L562.29 16 L563.68 16 L564.61 67.52 L571.82 67.61 L572.05 83.08 L572.98 83.08 L573.21 152.98 L579.49 143.92 L580.88 121.96 L582.04 143.91 L595.53 163.38 L598.55 173.04 L598.78 176.62 L600.41 176.62 L600.64 180.12 L602.03 180.12 L602.27 183.62 L603.66 183.62 L603.89 187.12 L605.52 187.12 L605.75 336.98 L606.22 379.83 L608.77 379.83 L609.01 89.53 L622.72 89.53 L622.95 83.96 L641.78 83.96 L642.01 69.64 L642.71 69.64 L642.94 83.96 L661.77 83.96 L662 89.53 L675.25 89.53 L675.48 202.65 L675.94 189.66 L748.46 189.66 L748.69 202.65 L755.43 202.65 L755.66 208.92 L800.75 208.92 L801.22 295.71 L806.33 295.71 L806.56 284.84 L818.42 284.84 L818.65 282.28 L837.48 282.28 L837.71 284.84 L848.17 284.84 L848.4 291.4 L848.87 291.4 L849.1 295.71 L854.91 295.71 L855.14 266.9 L894.19 266.9 L894.42 390.01 L937.42 390.01 L937.65 306.26 L939.51 306.26 L939.74 290.76 L953.69 290.76 L953.92 306.26 L958.57 306.26 L958.8 295.48 L974.14 295.48 L974.37 306.26 L976 306.26 L976 390.01 L1000 390.01" /></defs>
20
+ <use class="skyline-trace-rail" href="#skyline-loading-outline" />
21
+ <use class="skyline-trace-line" href="#skyline-loading-outline" />
22
+ <use class="skyline-trace-tip" href="#skyline-loading-outline" />
23
+ </svg>
24
+ <!-- skyline-loading:end -->
25
+ </div>
26
+ <div class="scene-notes" id="scene-notes">
27
+ <p class="hint" id="camera-hint">Drag to orbit · scroll or pinch to zoom<br />Arrow keys rotate · + / − zoom · Home resets</p>
28
+ <p id="celebration-status" aria-hidden="true" hidden></p>
29
+ <p class="attribution">Streets and footprints <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a></p>
30
+ </div>
31
+ <p class="visually-hidden" id="motion-status" role="status">Camera moves only when you interact or start the turntable.</p>
32
+ <p class="visually-hidden" id="celebration-announcement" role="status"></p>
33
+ <noscript><style>#loading { display: none; }</style><p class="loading">Enable JavaScript to explore the 3D skyline.</p></noscript>
34
+ <dialog class="detail-panel" id="building-detail" aria-label="Building detail">
35
+ <button type="button" class="detail-close" id="detail-close" aria-label="Close building detail" title="Close (Esc)">×</button>
36
+ </dialog>
37
+ </main>
38
+ <footer class="control-bar" aria-label="3D skyline controls">
39
+ <!-- Chicago's six-pointed star, after devopsrockstars' white-star-only.svg, drawn in
40
+ the bar's muted grey: points 12 units out, the notches between them 4.66. -->
41
+ <button type="button" class="menu-toggle" id="menu-toggle" aria-expanded="false" aria-controls="camera-views model-display scene-notes" aria-label="Skyline controls" title="Show the controls">
42
+ <svg viewBox="-12 -12 24 24" aria-hidden="true" focusable="false"><path d="M0-12 2.33-4.04 10.39-6 4.66 0 10.39 6 2.33 4.04 0 12-2.33 4.04-10.39 6-4.66 0-10.39-6-2.33-4.04Z" /></svg>
43
+ </button>
44
+ <div class="button-group camera-views" id="camera-views" role="group" aria-label="Camera views">
45
+ <button type="button" data-view="skyline" aria-pressed="true">skyline view</button>
46
+ <button type="button" data-view="quarter" aria-pressed="false">three-quarter</button>
47
+ <button type="button" data-view="side" aria-pressed="false">side</button>
48
+ <button type="button" data-view="top" aria-pressed="false">ground plan ↓</button>
49
+ <button type="button" data-view="heights" aria-pressed="false">height comparison</button>
50
+ <button type="button" id="reset">reset view</button>
51
+ </div>
52
+ <div class="button-group model-display" id="model-display" role="group" aria-label="Model display">
53
+ <button type="button" id="streets" aria-pressed="true">streets</button>
54
+ <button type="button" id="footprints" aria-pressed="false">footprints</button>
55
+ <button type="button" id="wireframe" aria-pressed="false">wireframe</button>
56
+ <button type="button" id="turntable" aria-pressed="false" aria-describedby="motion-status">turntable</button>
57
+ <button type="button" id="show-original" title="Compare with the original skyline SVG" hidden>show original</button>
58
+ <div class="celebration-controls" id="celebration-controls" role="group" aria-label="Celebratory window lighting">
59
+ <span class="celebration-caption">lights</span>
60
+ <button type="button" data-celebration="bulls" aria-label="Bulls lighting" aria-pressed="false">
61
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="M7 12C3 12 2 8 3 4c2 4 4 4 7 5l6-2 6 2c3-1 5-1 7-5 1 4 0 8-4 8l-2 10-4 5h-6l-4-5Z"/><path class="logo-cut" d="m9 15 5 2m9-2-5 2m-4 6h4"/></svg>
62
+ </button>
63
+ <button type="button" data-celebration="cubs" aria-label="Cubs lighting" aria-pressed="false">
64
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle class="logo-line" cx="16" cy="16" r="13"/><path d="M24 10a10 10 0 1 0 0 12l-4-3a5 5 0 1 1 0-6Z"/></svg>
65
+ </button>
66
+ <button type="button" data-celebration="sox" aria-label="White Sox lighting" aria-pressed="false">
67
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><text x="2" y="13">S</text><text x="11" y="22">O</text><text x="21" y="30">X</text></svg>
68
+ </button>
69
+ <button type="button" data-celebration="bears" aria-label="Bears lighting" aria-pressed="false">
70
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m3 16 5-8 11-2 10 5-7 3-4-3-7 2-2 3 2 3 7 2 4-3 7 3-10 5-11-2Z"/></svg>
71
+ </button>
72
+ <button type="button" data-celebration="hawks" aria-label="Blackhawks lighting" aria-pressed="false">
73
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path class="logo-line" d="M10 5h12v7c0 5-3 7-6 7s-6-2-6-7ZM10 7H5v4c0 4 3 5 6 5m11-9h5v4c0 4-3 5-6 5m-5 3v6m-6 2h12"/></svg>
74
+ </button>
75
+ <button type="button" data-celebration="thanks" aria-label="Thanksgiving lighting" aria-pressed="false">
76
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path class="logo-line" d="M8 27 24 4M12 21C2 21 3 12 3 12c7 0 10 4 9 9Zm5-7C8 14 10 6 10 6c6 0 9 4 7 8Zm-3 4c3-6 10-5 13-3-2 6-8 9-13 3Zm6-9c0-5 5-7 8-7 1 6-2 10-8 7Z"/></svg>
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </footer>
81
+ <div class="tooltip" id="tooltip" aria-hidden="true" hidden></div>
82
+ <div class="context-menu" id="building-menu" hidden>
83
+ <p class="context-menu-title" id="building-menu-title"></p>
84
+ <div role="menu" aria-labelledby="building-menu-title">
85
+ <a role="menuitem" id="building-detail-link" href="building-detail.html">building detail</a>
86
+ <div id="building-lighting-menu" role="group" aria-label="Celebratory window lighting" hidden></div>
87
+ </div>
88
+ </div>
89
+ <!-- The loader is compiled into dist/; opened from the sources, say how to build it. -->
90
+ <script src="study-loader.js" data-study="./skyline-3d.js" onerror="document.querySelector('#loading').textContent = 'Build the site with bun run build:site, then serve dist/ with python3 -m http.server -d dist to explore the 3D skyline.'"></script>
91
+ </body>
92
+ </html>