@a2f0/skyline 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/package.md CHANGED
@@ -314,6 +314,10 @@ the selection and `litWindows` counts illuminated office panes. Each instance
314
314
  has its own state. Use the model from `createGeographicSkyline()` or
315
315
  `createGeographicBuilding()` for these displays.
316
316
 
317
+ `models/colour-materials` and `models/colour-palette` are an opt-in colour trial for the
318
+ viewer's colour toggle, published only because every model module is; they are not a stable
319
+ API and may change or disappear in any release.
320
+
317
321
  `@a2f0/skyline/models/building-kit` exposes the same plan runs and mesh builder
318
322
  used by this repository, for adding buildings. Coordinate and model assumptions
319
323
  are in [the geography notes](skyline-geography.md). Asset paths are also available
@@ -0,0 +1,22 @@
1
+ import type { BuildingModel } from "./building-kit.js";
2
+ import type { MaterialName } from "./colour-palette.js";
3
+ export type Tones = Readonly<Record<number, MaterialName>>;
4
+ export type Batch = readonly [material: MaterialName, tones?: Tones];
5
+ export type BuildingMaterials = Readonly<Record<string, Batch>>;
6
+ export declare const colourMaterials: Readonly<Record<string, BuildingMaterials>>;
7
+ export declare function batchOf(buildingId: string, meshName: string): Batch | undefined;
8
+ export declare const toneOf: (grey: number) => number;
9
+ export declare function toOklab(r: number, g: number, b: number): [number, number, number];
10
+ export declare function tint(grey: number, material: MaterialName): [number, number, number];
11
+ export interface SkylineColour {
12
+ /** Whether the buildings show their materials' colours. */
13
+ readonly enabled: boolean;
14
+ /** Shows the colours, or restores the greys exactly. */
15
+ set(enabled: boolean): void;
16
+ /**
17
+ * Recolours vertices another writer has since rewritten in grey, such as window
18
+ * illumination resetting a facade for a celebration.
19
+ */
20
+ refresh(): void;
21
+ }
22
+ export declare function createSkylineColour(models: readonly BuildingModel[]): SkylineColour;
@@ -0,0 +1,385 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { colourPalette } from "./colour-palette.js";
3
+ // A batch's lit and dim office windows.
4
+ const windows = (lit, dim) => ({ [lit]: "lit window", [dim]: "dim window" });
5
+ export const colourMaterials = {
6
+ "building-heritage-at-millennium-park": {
7
+ "tower and lower wing": ["green glass"],
8
+ "mapped ground footprint": ["concrete"],
9
+ "limestone frame": ["limestone"],
10
+ "bronze mullions and slab edges": ["bronze"],
11
+ "roof terraces": ["neutral"],
12
+ "mechanical penthouses": ["concrete"],
13
+ glazing: ["green glass", { 0x7a7a7a: "lit window" }],
14
+ // The four preserved Wabash facades.
15
+ "Wabash facade": ["buff brick"],
16
+ },
17
+ "building-kemper": {
18
+ "marble shell": ["marble"],
19
+ "mullions and bands": ["marble"],
20
+ "crown glazing": ["glass"],
21
+ "crown fins": ["aluminium"],
22
+ "roof cap": ["marble"],
23
+ "lit glazing": ["lit window"],
24
+ "dim glazing": ["dim window"],
25
+ glazing: ["glass"],
26
+ },
27
+ // Silver-reflective glass in white aluminium.
28
+ "building-crain-communications": {
29
+ "aluminum spandrels": ["aluminium"],
30
+ "ribbon glazing": ["glass", { ...windows(0x8a8a8a, 0x565656), 0x585858: "aluminium" }],
31
+ "sloped glazing": ["glass"],
32
+ "glazing grid": ["aluminium"],
33
+ "diamond outline lights": ["lamp"],
34
+ coping: ["aluminium"],
35
+ "slot floor": ["neutral"],
36
+ "mechanical louvers": ["aluminium", { 0x232323: "glass", 0x303030: "dark metal" }],
37
+ "louver blades": ["aluminium", { 0x232323: "glass" }],
38
+ "crown shadow strips": ["neutral", { 0x232323: "glass", 0x585858: "aluminium" }],
39
+ },
40
+ // Bronze-tinted glass and bronze spandrels, on a granite plaza.
41
+ "building-330-north-wabash": {
42
+ shell: ["bronze", { 0x3a3a3a: "neutral", 0x464646: "dark granite" }],
43
+ "glass, spandrels and louvers": ["bronze glass", { ...windows(0x7a7a7a, 0x4a4a4a), 0x262626: "bronze", 0x353535: "bronze" }],
44
+ "bronze mullions and columns": ["bronze"],
45
+ },
46
+ "building-michigan-plaza-south-tower": {
47
+ "mapped tower shell": ["glass"],
48
+ "mullions and bands": ["aluminium"],
49
+ "lit glazing": ["lit window"],
50
+ "dim glazing": ["dim window"],
51
+ glazing: ["glass"],
52
+ },
53
+ "building-trump-tower-only": {
54
+ "tower shell": ["glass"],
55
+ "glass and spandrels": ["glass", windows(0x7c7c7c, 0x505050)],
56
+ "stainless mullions": ["stainless"],
57
+ crown: ["glass", { 0x3c3c3c: "stainless" }],
58
+ "crown mullions": ["stainless"],
59
+ spire: ["stainless"],
60
+ },
61
+ // Limestone piers and aluminium spandrels; the sign's white letters stay white.
62
+ "building-one-prudential-plaza": {
63
+ shell: ["limestone", { 0x242424: "dark metal", 0x4c4c4c: "neutral", 0x8e8e8e: "aluminium" }],
64
+ "windows and spandrels": ["glass", { ...windows(0x7c7c7c, 0x4e4e4e), 0x8a8a8a: "lit window", 0x242424: "dark metal", 0x8e8e8e: "aluminium" }],
65
+ "limestone piers": ["limestone"],
66
+ "limestone courses": ["limestone"],
67
+ "penthouse and sign": ["limestone", { 0x2e2e2e: "dark metal", 0x383838: "dark metal", 0x4c4c4c: "neutral", 0xb0b0b0: "neutral", 0xc4c4c4: "neutral" }],
68
+ "screen louvers": ["aluminium"],
69
+ "antenna mast": ["stainless"],
70
+ },
71
+ // Stone faces and gables, and the dark glass strips between them.
72
+ "building-two-prudential-plaza": {
73
+ "limestone, glass and crown": ["granite", {
74
+ 0x353535: "glass", 0x3a3a3a: "glass", 0x3f3f3f: "glass", 0x404040: "glass", 0x454545: "glass", 0x4a4a4a: "glass", 0x565656: "glass",
75
+ ...windows(0x8e8e8e, 0x5e5e5e),
76
+ }],
77
+ piers: ["granite"],
78
+ copings: ["granite"],
79
+ "strip mullions": ["aluminium"],
80
+ "crown ribs": ["granite"],
81
+ spire: ["stainless"],
82
+ "spire inset panels": ["glass"],
83
+ lobby: ["glass"],
84
+ },
85
+ // Mount Airy white granite.
86
+ layer3: {
87
+ "tube shell": ["white granite", { 0x202020: "neutral", 0x4c4c4c: "neutral" }],
88
+ "window ribbons": ["glass", windows(0x7c7c7c, 0x4e4e4e)],
89
+ "granite piers": ["white granite"],
90
+ "granite cap": ["white granite"],
91
+ "rooftop enclosure": ["neutral", { 0x9c9c9c: "aluminium" }],
92
+ "antenna mast": ["stainless"],
93
+ },
94
+ "building-blue-cross-blue-shield": {
95
+ shell: ["glass", { 0x3a3a3a: "neutral" }],
96
+ "glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "aluminium" }],
97
+ mullions: ["dark metal"],
98
+ "band columns and emblems": ["aluminium"],
99
+ },
100
+ // Blue-green glass, and the south face's white concrete frame.
101
+ "building-340-on-the-park": {
102
+ shell: ["neutral"],
103
+ "curtain wall": ["green glass", { ...windows(0x7a7a7a, 0x4a4a4a), 0x242424: "neutral", 0x2a2a2a: "concrete", 0xb4b4b4: "concrete" }],
104
+ "concrete frame": ["concrete"],
105
+ "railings and guards": ["aluminium"],
106
+ },
107
+ // A concrete frame over bronze ribbon windows.
108
+ "building-the-buckingham": {
109
+ shell: ["neutral"],
110
+ "frame and glass": ["bronze glass", { ...windows(0x6e6e6e, 0x464646), 0x4c4c4c: "concrete" }],
111
+ piers: ["concrete"],
112
+ "corner balconies": ["concrete"],
113
+ },
114
+ // A grey precast slab.
115
+ "building-michigan-plaza-front-tall": {
116
+ shell: ["neutral"],
117
+ "walls and windows": ["glass", { ...windows(0x727272, 0x484848), 0x666666: "concrete" }],
118
+ },
119
+ // Buff limestone over a granite base.
120
+ "building-willoughby-tower": {
121
+ shell: ["neutral"],
122
+ "stone and windows": ["glass", { ...windows(0x6e6e6e, 0x444444), 0x4a4a4a: "dark granite", 0x585858: "limestone" }],
123
+ pinnacles: ["limestone"],
124
+ },
125
+ // Tan brick and terracotta.
126
+ "building-six-north-michigan": {
127
+ shell: ["neutral"],
128
+ "brick and windows": ["glass", { ...windows(0x8a8a8a, 0x5e5e5e), 0x6e6e6e: "buff brick", 0x9c9c9c: "white terracotta" }],
129
+ cornices: ["white terracotta"],
130
+ },
131
+ // A cream terracotta front, and common brick on its south wall.
132
+ "building-six-north-far-east": {
133
+ shell: ["neutral"],
134
+ "terracotta and windows": ["glass", {
135
+ ...windows(0x6e6e6e, 0x3a3a3a),
136
+ 0x363636: "white terracotta", 0x404040: "common brick", 0x444444: "white terracotta", 0x505050: "white terracotta", 0x585858: "white terracotta",
137
+ }],
138
+ parapet: ["white terracotta"],
139
+ },
140
+ // Brown masonry.
141
+ "building-michigan-plaza-front-middle": {
142
+ shell: ["neutral"],
143
+ "masonry and windows": ["glass", { ...windows(0x9a9a9a, 0x525252), 0x7a7a7a: "brown brick", 0x888888: "limestone" }],
144
+ },
145
+ // Buff limestone under a slate roof.
146
+ "building-university-club": {
147
+ shell: ["limestone", { 0x2e2e2e: "neutral", 0x343434: "slate", 0x4a4a4a: "neutral" }],
148
+ "stone and windows": ["glass", { ...windows(0x8c8c8c, 0x444444), 0xa4a4a4: "limestone", 0xb4b4b4: "limestone" }],
149
+ "merlons and pinnacles": ["limestone"],
150
+ },
151
+ // Pink terracotta over granite, under a green tile roof.
152
+ "building-monroe": {
153
+ shell: ["pink terracotta", { 0x2e2e2e: "neutral", 0x303030: "green tile", 0x484848: "neutral" }],
154
+ "terracotta and windows": ["glass", { ...windows(0x929292, 0x484848), 0x9a9a9a: "granite", 0xb2b2b2: "pink terracotta", 0xc2c2c2: "pink terracotta" }],
155
+ "attic windows": ["glass"],
156
+ },
157
+ "building-maclean-center": {
158
+ shell: ["neutral"],
159
+ "stone and windows": ["glass", { ...windows(0x6e6e6e, 0x3c3c3c), 0x242424: "neutral", 0x7e7e7e: "limestone", 0x8e8e8e: "limestone" }],
160
+ cornice: ["limestone"],
161
+ },
162
+ "building-lakeview": {
163
+ shell: ["neutral"],
164
+ "stone and windows": ["glass", { ...windows(0x6e6e6e, 0x3c3c3c), 0x767676: "limestone", 0x888888: "limestone" }],
165
+ },
166
+ // Terracotta over granite columns, round a light court of white brick.
167
+ "building-peoples-gas": {
168
+ shell: ["neutral", { 0x646464: "common brick", 0x9c9c9c: "glazed brick" }],
169
+ "terracotta and windows": ["glass", {
170
+ ...windows(0x6e6e6e, 0x3c3c3c),
171
+ 0x242424: "neutral", 0x5a5a5a: "dark granite", 0x7c7c7c: "white terracotta", 0x8e8e8e: "white terracotta", 0x9c9c9c: "glazed brick",
172
+ }],
173
+ cornice: ["white terracotta"],
174
+ },
175
+ // Natural aluminium mullions and blue porcelain-enamelled spandrels.
176
+ "building-200-south-michigan": {
177
+ shell: ["neutral"],
178
+ "curtain wall": ["glass", {
179
+ ...windows(0x7a7a7a, 0x4a4a4a),
180
+ 0x2d2d2d: "blue enamel", 0x525252: "aluminium", 0x585858: "aluminium", 0x5e5e5e: "aluminium", 0x6a6a6a: "aluminium",
181
+ }],
182
+ "rooftop block": ["neutral"],
183
+ penthouse: ["neutral"],
184
+ },
185
+ // White terracotta under a green copper roof.
186
+ "building-railway-exchange": {
187
+ shell: ["neutral", { 0x9c9c9c: "copper" }],
188
+ "terracotta and windows": ["glass", { ...windows(0x6e6e6e, 0x3c3c3c), 0x626262: "common brick", 0x808080: "white terracotta", 0x909090: "white terracotta" }],
189
+ cornice: ["white terracotta"],
190
+ },
191
+ // Sullivan's cream terracotta front, and common brick above its neighbours.
192
+ "building-michigan-west-right": {
193
+ shell: ["neutral"],
194
+ "terracotta and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x404040: "common brick", 0x484848: "white terracotta", 0x5e5e5e: "white terracotta" }],
195
+ cartouches: ["white terracotta"],
196
+ },
197
+ // Red brick fronts with stone bands.
198
+ "building-michigan-west-front": {
199
+ shell: ["neutral"],
200
+ "brick and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x464646: "brick", 0x525252: "limestone" }],
201
+ },
202
+ "building-30-south-michigan": {
203
+ shell: ["neutral"],
204
+ "brick and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x464646: "brick", 0x525252: "limestone" }],
205
+ },
206
+ // Limestone, and red and grey brick.
207
+ "building-chicago-athletic-association": {
208
+ shell: ["neutral"],
209
+ "stone, brick and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x5a5a5a: "brick", 0x6d6d6d: "limestone", 0x7a7a7a: "limestone" }],
210
+ cornice: ["limestone"],
211
+ },
212
+ // Mies's black curtain wall and bronze-tinted glass.
213
+ "building-office-west-of-aon": {
214
+ shell: ["neutral"],
215
+ "curtain wall": ["bronze glass", { ...windows(0x7a7a7a, 0x4a4a4a), 0x2b2b2b: "dark metal", 0x303030: "dark metal", 0x333333: "dark metal" }],
216
+ },
217
+ // White concrete.
218
+ "building-river-plaza": {
219
+ shell: ["concrete"],
220
+ "concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete" }],
221
+ "rooftop box": ["concrete"],
222
+ },
223
+ // Dark orange brick and bronze glass.
224
+ "building-hyatt-regency-west-tower": {
225
+ shell: ["neutral"],
226
+ "brick and windows": ["bronze glass", { ...windows(0x6d6d6d, 0x4a4a4a), 0x2c2c2c: "dark metal", 0x3b3b3b: "orange brick" }],
227
+ },
228
+ // Cream precast, and the drums' maroon fins.
229
+ "building-buckingham-east": {
230
+ shell: ["precast", { 0x686868: "neutral" }],
231
+ "precast and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x848484: "precast" }],
232
+ drums: ["precast", { 0x5a5a5a: "maroon", 0x686868: "neutral" }],
233
+ },
234
+ // Dark brown aluminium and bronze glass.
235
+ "building-three-illinois-center": {
236
+ shell: ["neutral"],
237
+ "curtain wall": ["bronze glass", { ...windows(0x707070, 0x464646), 0x282828: "bronze", 0x323232: "bronze" }],
238
+ },
239
+ // Reflective blue-green glass in its frame.
240
+ "building-swissotel": {
241
+ shell: ["neutral"],
242
+ "curtain wall": ["blue-green glass", windows(0x878787, 0x515151)],
243
+ },
244
+ };
245
+ // A batch's materials from its mesh's name, if the trial knows the building and the batch.
246
+ export function batchOf(buildingId, meshName) {
247
+ const materials = colourMaterials[buildingId];
248
+ const batch = meshName.split(" · ").at(-1);
249
+ return materials?.[batch] ?? materials?.[batch.replace(/ \d+$/, "")];
250
+ }
251
+ // A grey's tone: the hex the model wrote it as, from its linear channel.
252
+ export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
253
+ // OKLab from linear sRGB, and back, after Björn Ottosson's matrices.
254
+ export function toOklab(r, g, b) {
255
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
256
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
257
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
258
+ return [
259
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
260
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
261
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
262
+ ];
263
+ }
264
+ function fromOklab(lightness, a, b) {
265
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
266
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
267
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
268
+ return [
269
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
270
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
271
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
272
+ ];
273
+ }
274
+ const inGamut = (rgb) => rgb.every((channel) => channel >= -1e-7 && channel <= 1 + 1e-7);
275
+ // A grey in a material's colour: the grey's own lightness, with the material's hue and chroma,
276
+ // less of it where the grey is darker than the material, and less again where sRGB cannot
277
+ // show it. A material with no hue leaves the grey exactly as it was.
278
+ export function tint(grey, material) {
279
+ const swatch = new THREE.Color(colourPalette[material]);
280
+ const [swatchLightness, a, b] = toOklab(swatch.r, swatch.g, swatch.b);
281
+ if (Math.hypot(a, b) < 1e-4)
282
+ return [grey, grey, grey];
283
+ const [lightness] = toOklab(grey, grey, grey);
284
+ const at = (scale) => fromOklab(lightness, a * scale, b * scale);
285
+ let scale = Math.min(1, lightness / swatchLightness);
286
+ if (!inGamut(at(scale))) {
287
+ let low = 0;
288
+ for (let step = 0; step < 20; step += 1) {
289
+ const middle = (low + scale) / 2;
290
+ if (inGamut(at(middle)))
291
+ low = middle;
292
+ else
293
+ scale = middle;
294
+ }
295
+ scale = low;
296
+ }
297
+ return at(scale).map((channel) => Math.min(1, Math.max(0, channel)));
298
+ }
299
+ // The colour layer for a skyline's models. A building or batch the trial does not know stays
300
+ // in grey, as does a mesh without a single toon material, a vertex-coloured batch whose
301
+ // colours are not plain floats, and a material or colour buffer another batch already took,
302
+ // since the builder lets batches share them.
303
+ export function createSkylineColour(models) {
304
+ const painted = [], flat = [], taken = new Set();
305
+ for (const model of models) {
306
+ const buildingId = String(model.building.userData["buildingId"]);
307
+ model.building.traverse((object) => {
308
+ const mesh = object;
309
+ if (!mesh.isMesh)
310
+ return;
311
+ const batch = batchOf(buildingId, mesh.name);
312
+ if (!batch)
313
+ return;
314
+ const [base, tones = {}] = batch;
315
+ if (Array.isArray(mesh.material) || !mesh.material.isMeshToonMaterial)
316
+ return;
317
+ const material = mesh.material;
318
+ const colourOf = (grey) => tint(grey, tones[toneOf(grey)] ?? base);
319
+ const attribute = mesh.geometry.getAttribute("color");
320
+ if (material.vertexColors) {
321
+ if (!(attribute?.array instanceof Float32Array) || taken.has(attribute))
322
+ return;
323
+ taken.add(attribute);
324
+ const cache = new Map();
325
+ const paint = (grey) => {
326
+ let colour = cache.get(grey);
327
+ if (!colour)
328
+ cache.set(grey, colour = Float32Array.from(colourOf(grey)));
329
+ return colour;
330
+ };
331
+ painted.push({ attribute, array: attribute.array, paint, greys: null });
332
+ }
333
+ else if (!taken.has(material)) {
334
+ taken.add(material);
335
+ flat.push({ material, grey: material.color.clone(), colour: new THREE.Color(...colourOf(material.color.r)) });
336
+ }
337
+ });
338
+ }
339
+ let enabled = false;
340
+ return {
341
+ get enabled() { return enabled; },
342
+ set(next) {
343
+ if (next === enabled)
344
+ return;
345
+ enabled = next;
346
+ for (const batch of painted) {
347
+ const { array } = batch;
348
+ if (next) {
349
+ const greys = batch.greys = new Float32Array(array.length / 3);
350
+ for (let vertex = 0; vertex < greys.length; vertex += 1) {
351
+ const grey = array[vertex * 3];
352
+ greys[vertex] = grey;
353
+ array.set(batch.paint(grey), vertex * 3);
354
+ }
355
+ }
356
+ else {
357
+ batch.greys.forEach((grey, vertex) => array.fill(grey, vertex * 3, vertex * 3 + 3));
358
+ batch.greys = null;
359
+ }
360
+ batch.attribute.needsUpdate = true;
361
+ }
362
+ for (const { material, grey, colour } of flat)
363
+ material.color.copy(next ? colour : grey);
364
+ },
365
+ refresh() {
366
+ if (!enabled)
367
+ return;
368
+ for (const batch of painted) {
369
+ const { array } = batch, greys = batch.greys;
370
+ let changed = false;
371
+ for (let vertex = 0; vertex < greys.length; vertex += 1) {
372
+ const colour = batch.paint(greys[vertex]), at = vertex * 3;
373
+ if (array[at] === colour[0] && array[at + 1] === colour[1] && array[at + 2] === colour[2])
374
+ continue;
375
+ const grey = array[at];
376
+ greys[vertex] = grey;
377
+ array.set(batch.paint(grey), at);
378
+ changed = true;
379
+ }
380
+ if (changed)
381
+ batch.attribute.needsUpdate = true;
382
+ }
383
+ },
384
+ };
385
+ }
@@ -0,0 +1,35 @@
1
+ export declare const colourPalette: {
2
+ readonly neutral: 8421504;
3
+ readonly glass: 5398118;
4
+ readonly "green glass": 4157036;
5
+ readonly "blue-green glass": 4883088;
6
+ readonly "bronze glass": 5917502;
7
+ readonly "lit window": 16044954;
8
+ readonly "dim window": 12098418;
9
+ readonly lamp: 16773330;
10
+ readonly limestone: 14207656;
11
+ readonly marble: 14999508;
12
+ readonly "white granite": 14473165;
13
+ readonly granite: 12366504;
14
+ readonly "dark granite": 6444111;
15
+ readonly slate: 5001824;
16
+ readonly "white terracotta": 15326916;
17
+ readonly "pink terracotta": 14198936;
18
+ readonly "green tile": 6196326;
19
+ readonly brick: 10243128;
20
+ readonly "common brick": 9202778;
21
+ readonly "orange brick": 10771002;
22
+ readonly "brown brick": 8147522;
23
+ readonly "buff brick": 13216640;
24
+ readonly "glazed brick": 14999503;
25
+ readonly concrete: 13617595;
26
+ readonly precast: 14338210;
27
+ readonly copper: 7316623;
28
+ readonly maroon: 7612463;
29
+ readonly "blue enamel": 4088212;
30
+ readonly bronze: 7098420;
31
+ readonly "dark metal": 3158842;
32
+ readonly aluminium: 12567753;
33
+ readonly stainless: 11844806;
34
+ };
35
+ export type MaterialName = keyof typeof colourPalette;
@@ -0,0 +1,48 @@
1
+ // The colour trial's named materials (https://github.com/a2f0/skyline/issues/115): the one file
2
+ // in the repository allowed colours, which tests/grayscale.test.ts exempts by name. Each colour
3
+ // gives a material's hue and how strongly it shows; the lightness every surface keeps is its
4
+ // model's own grey, so colour mode adds hue to the skyline without changing its values. A
5
+ // material darker than its colour here carries proportionally less of it. These are hand-picked
6
+ // from the materials and colours each building's reference audit records, not measured from
7
+ // photographs; a material with no hue stays grey.
8
+ export const colourPalette = {
9
+ neutral: 0x808080,
10
+ // Vision glass: the default cool tint, the teal of 340 on the Park and the Heritage, Swissôtel's
11
+ // reflective blue-green, and the bronze tint of Illinois Center and its neighbours.
12
+ glass: 0x525e66,
13
+ "green glass": 0x3f6e6c,
14
+ "blue-green glass": 0x4a8290,
15
+ "bronze glass": 0x5a4b3e,
16
+ // Lit offices, and the dimmer ones further in.
17
+ "lit window": 0xf4d39a,
18
+ "dim window": 0xb89b72,
19
+ lamp: 0xfff0d2,
20
+ // Stone.
21
+ limestone: 0xd8caa8,
22
+ marble: 0xe4dfd4,
23
+ "white granite": 0xdcd7cd,
24
+ granite: 0xbcb2a8,
25
+ "dark granite": 0x62544f,
26
+ slate: 0x4c5260,
27
+ // Clay.
28
+ "white terracotta": 0xe9dec4,
29
+ "pink terracotta": 0xd8a898,
30
+ "green tile": 0x5e8c66,
31
+ brick: 0x9c4c38,
32
+ "common brick": 0x8c6c5a,
33
+ "orange brick": 0xa45a3a,
34
+ "brown brick": 0x7c5242,
35
+ "buff brick": 0xc9ab80,
36
+ "glazed brick": 0xe4dfcf,
37
+ // Concrete.
38
+ concrete: 0xcfc9bb,
39
+ precast: 0xdac8a2,
40
+ // Metal and paint.
41
+ copper: 0x6fa48f,
42
+ maroon: 0x74282f,
43
+ "blue enamel": 0x3e6194,
44
+ bronze: 0x6c5034,
45
+ "dark metal": 0x30333a,
46
+ aluminium: 0xbfc4c9,
47
+ stainless: 0xb4bcc6,
48
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2f0/skyline",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Interactive Chicago skyline viewer and reusable Three.js building models",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -0,0 +1,385 @@
1
+ import * as THREE from "../vendor/three-r186.js";
2
+ import { colourPalette } from "./colour-palette.js";
3
+ // A batch's lit and dim office windows.
4
+ const windows = (lit, dim) => ({ [lit]: "lit window", [dim]: "dim window" });
5
+ export const colourMaterials = {
6
+ "building-heritage-at-millennium-park": {
7
+ "tower and lower wing": ["green glass"],
8
+ "mapped ground footprint": ["concrete"],
9
+ "limestone frame": ["limestone"],
10
+ "bronze mullions and slab edges": ["bronze"],
11
+ "roof terraces": ["neutral"],
12
+ "mechanical penthouses": ["concrete"],
13
+ glazing: ["green glass", { 0x7a7a7a: "lit window" }],
14
+ // The four preserved Wabash facades.
15
+ "Wabash facade": ["buff brick"],
16
+ },
17
+ "building-kemper": {
18
+ "marble shell": ["marble"],
19
+ "mullions and bands": ["marble"],
20
+ "crown glazing": ["glass"],
21
+ "crown fins": ["aluminium"],
22
+ "roof cap": ["marble"],
23
+ "lit glazing": ["lit window"],
24
+ "dim glazing": ["dim window"],
25
+ glazing: ["glass"],
26
+ },
27
+ // Silver-reflective glass in white aluminium.
28
+ "building-crain-communications": {
29
+ "aluminum spandrels": ["aluminium"],
30
+ "ribbon glazing": ["glass", { ...windows(0x8a8a8a, 0x565656), 0x585858: "aluminium" }],
31
+ "sloped glazing": ["glass"],
32
+ "glazing grid": ["aluminium"],
33
+ "diamond outline lights": ["lamp"],
34
+ coping: ["aluminium"],
35
+ "slot floor": ["neutral"],
36
+ "mechanical louvers": ["aluminium", { 0x232323: "glass", 0x303030: "dark metal" }],
37
+ "louver blades": ["aluminium", { 0x232323: "glass" }],
38
+ "crown shadow strips": ["neutral", { 0x232323: "glass", 0x585858: "aluminium" }],
39
+ },
40
+ // Bronze-tinted glass and bronze spandrels, on a granite plaza.
41
+ "building-330-north-wabash": {
42
+ shell: ["bronze", { 0x3a3a3a: "neutral", 0x464646: "dark granite" }],
43
+ "glass, spandrels and louvers": ["bronze glass", { ...windows(0x7a7a7a, 0x4a4a4a), 0x262626: "bronze", 0x353535: "bronze" }],
44
+ "bronze mullions and columns": ["bronze"],
45
+ },
46
+ "building-michigan-plaza-south-tower": {
47
+ "mapped tower shell": ["glass"],
48
+ "mullions and bands": ["aluminium"],
49
+ "lit glazing": ["lit window"],
50
+ "dim glazing": ["dim window"],
51
+ glazing: ["glass"],
52
+ },
53
+ "building-trump-tower-only": {
54
+ "tower shell": ["glass"],
55
+ "glass and spandrels": ["glass", windows(0x7c7c7c, 0x505050)],
56
+ "stainless mullions": ["stainless"],
57
+ crown: ["glass", { 0x3c3c3c: "stainless" }],
58
+ "crown mullions": ["stainless"],
59
+ spire: ["stainless"],
60
+ },
61
+ // Limestone piers and aluminium spandrels; the sign's white letters stay white.
62
+ "building-one-prudential-plaza": {
63
+ shell: ["limestone", { 0x242424: "dark metal", 0x4c4c4c: "neutral", 0x8e8e8e: "aluminium" }],
64
+ "windows and spandrels": ["glass", { ...windows(0x7c7c7c, 0x4e4e4e), 0x8a8a8a: "lit window", 0x242424: "dark metal", 0x8e8e8e: "aluminium" }],
65
+ "limestone piers": ["limestone"],
66
+ "limestone courses": ["limestone"],
67
+ "penthouse and sign": ["limestone", { 0x2e2e2e: "dark metal", 0x383838: "dark metal", 0x4c4c4c: "neutral", 0xb0b0b0: "neutral", 0xc4c4c4: "neutral" }],
68
+ "screen louvers": ["aluminium"],
69
+ "antenna mast": ["stainless"],
70
+ },
71
+ // Stone faces and gables, and the dark glass strips between them.
72
+ "building-two-prudential-plaza": {
73
+ "limestone, glass and crown": ["granite", {
74
+ 0x353535: "glass", 0x3a3a3a: "glass", 0x3f3f3f: "glass", 0x404040: "glass", 0x454545: "glass", 0x4a4a4a: "glass", 0x565656: "glass",
75
+ ...windows(0x8e8e8e, 0x5e5e5e),
76
+ }],
77
+ piers: ["granite"],
78
+ copings: ["granite"],
79
+ "strip mullions": ["aluminium"],
80
+ "crown ribs": ["granite"],
81
+ spire: ["stainless"],
82
+ "spire inset panels": ["glass"],
83
+ lobby: ["glass"],
84
+ },
85
+ // Mount Airy white granite.
86
+ layer3: {
87
+ "tube shell": ["white granite", { 0x202020: "neutral", 0x4c4c4c: "neutral" }],
88
+ "window ribbons": ["glass", windows(0x7c7c7c, 0x4e4e4e)],
89
+ "granite piers": ["white granite"],
90
+ "granite cap": ["white granite"],
91
+ "rooftop enclosure": ["neutral", { 0x9c9c9c: "aluminium" }],
92
+ "antenna mast": ["stainless"],
93
+ },
94
+ "building-blue-cross-blue-shield": {
95
+ shell: ["glass", { 0x3a3a3a: "neutral" }],
96
+ "glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "aluminium" }],
97
+ mullions: ["dark metal"],
98
+ "band columns and emblems": ["aluminium"],
99
+ },
100
+ // Blue-green glass, and the south face's white concrete frame.
101
+ "building-340-on-the-park": {
102
+ shell: ["neutral"],
103
+ "curtain wall": ["green glass", { ...windows(0x7a7a7a, 0x4a4a4a), 0x242424: "neutral", 0x2a2a2a: "concrete", 0xb4b4b4: "concrete" }],
104
+ "concrete frame": ["concrete"],
105
+ "railings and guards": ["aluminium"],
106
+ },
107
+ // A concrete frame over bronze ribbon windows.
108
+ "building-the-buckingham": {
109
+ shell: ["neutral"],
110
+ "frame and glass": ["bronze glass", { ...windows(0x6e6e6e, 0x464646), 0x4c4c4c: "concrete" }],
111
+ piers: ["concrete"],
112
+ "corner balconies": ["concrete"],
113
+ },
114
+ // A grey precast slab.
115
+ "building-michigan-plaza-front-tall": {
116
+ shell: ["neutral"],
117
+ "walls and windows": ["glass", { ...windows(0x727272, 0x484848), 0x666666: "concrete" }],
118
+ },
119
+ // Buff limestone over a granite base.
120
+ "building-willoughby-tower": {
121
+ shell: ["neutral"],
122
+ "stone and windows": ["glass", { ...windows(0x6e6e6e, 0x444444), 0x4a4a4a: "dark granite", 0x585858: "limestone" }],
123
+ pinnacles: ["limestone"],
124
+ },
125
+ // Tan brick and terracotta.
126
+ "building-six-north-michigan": {
127
+ shell: ["neutral"],
128
+ "brick and windows": ["glass", { ...windows(0x8a8a8a, 0x5e5e5e), 0x6e6e6e: "buff brick", 0x9c9c9c: "white terracotta" }],
129
+ cornices: ["white terracotta"],
130
+ },
131
+ // A cream terracotta front, and common brick on its south wall.
132
+ "building-six-north-far-east": {
133
+ shell: ["neutral"],
134
+ "terracotta and windows": ["glass", {
135
+ ...windows(0x6e6e6e, 0x3a3a3a),
136
+ 0x363636: "white terracotta", 0x404040: "common brick", 0x444444: "white terracotta", 0x505050: "white terracotta", 0x585858: "white terracotta",
137
+ }],
138
+ parapet: ["white terracotta"],
139
+ },
140
+ // Brown masonry.
141
+ "building-michigan-plaza-front-middle": {
142
+ shell: ["neutral"],
143
+ "masonry and windows": ["glass", { ...windows(0x9a9a9a, 0x525252), 0x7a7a7a: "brown brick", 0x888888: "limestone" }],
144
+ },
145
+ // Buff limestone under a slate roof.
146
+ "building-university-club": {
147
+ shell: ["limestone", { 0x2e2e2e: "neutral", 0x343434: "slate", 0x4a4a4a: "neutral" }],
148
+ "stone and windows": ["glass", { ...windows(0x8c8c8c, 0x444444), 0xa4a4a4: "limestone", 0xb4b4b4: "limestone" }],
149
+ "merlons and pinnacles": ["limestone"],
150
+ },
151
+ // Pink terracotta over granite, under a green tile roof.
152
+ "building-monroe": {
153
+ shell: ["pink terracotta", { 0x2e2e2e: "neutral", 0x303030: "green tile", 0x484848: "neutral" }],
154
+ "terracotta and windows": ["glass", { ...windows(0x929292, 0x484848), 0x9a9a9a: "granite", 0xb2b2b2: "pink terracotta", 0xc2c2c2: "pink terracotta" }],
155
+ "attic windows": ["glass"],
156
+ },
157
+ "building-maclean-center": {
158
+ shell: ["neutral"],
159
+ "stone and windows": ["glass", { ...windows(0x6e6e6e, 0x3c3c3c), 0x242424: "neutral", 0x7e7e7e: "limestone", 0x8e8e8e: "limestone" }],
160
+ cornice: ["limestone"],
161
+ },
162
+ "building-lakeview": {
163
+ shell: ["neutral"],
164
+ "stone and windows": ["glass", { ...windows(0x6e6e6e, 0x3c3c3c), 0x767676: "limestone", 0x888888: "limestone" }],
165
+ },
166
+ // Terracotta over granite columns, round a light court of white brick.
167
+ "building-peoples-gas": {
168
+ shell: ["neutral", { 0x646464: "common brick", 0x9c9c9c: "glazed brick" }],
169
+ "terracotta and windows": ["glass", {
170
+ ...windows(0x6e6e6e, 0x3c3c3c),
171
+ 0x242424: "neutral", 0x5a5a5a: "dark granite", 0x7c7c7c: "white terracotta", 0x8e8e8e: "white terracotta", 0x9c9c9c: "glazed brick",
172
+ }],
173
+ cornice: ["white terracotta"],
174
+ },
175
+ // Natural aluminium mullions and blue porcelain-enamelled spandrels.
176
+ "building-200-south-michigan": {
177
+ shell: ["neutral"],
178
+ "curtain wall": ["glass", {
179
+ ...windows(0x7a7a7a, 0x4a4a4a),
180
+ 0x2d2d2d: "blue enamel", 0x525252: "aluminium", 0x585858: "aluminium", 0x5e5e5e: "aluminium", 0x6a6a6a: "aluminium",
181
+ }],
182
+ "rooftop block": ["neutral"],
183
+ penthouse: ["neutral"],
184
+ },
185
+ // White terracotta under a green copper roof.
186
+ "building-railway-exchange": {
187
+ shell: ["neutral", { 0x9c9c9c: "copper" }],
188
+ "terracotta and windows": ["glass", { ...windows(0x6e6e6e, 0x3c3c3c), 0x626262: "common brick", 0x808080: "white terracotta", 0x909090: "white terracotta" }],
189
+ cornice: ["white terracotta"],
190
+ },
191
+ // Sullivan's cream terracotta front, and common brick above its neighbours.
192
+ "building-michigan-west-right": {
193
+ shell: ["neutral"],
194
+ "terracotta and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x404040: "common brick", 0x484848: "white terracotta", 0x5e5e5e: "white terracotta" }],
195
+ cartouches: ["white terracotta"],
196
+ },
197
+ // Red brick fronts with stone bands.
198
+ "building-michigan-west-front": {
199
+ shell: ["neutral"],
200
+ "brick and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x464646: "brick", 0x525252: "limestone" }],
201
+ },
202
+ "building-30-south-michigan": {
203
+ shell: ["neutral"],
204
+ "brick and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x464646: "brick", 0x525252: "limestone" }],
205
+ },
206
+ // Limestone, and red and grey brick.
207
+ "building-chicago-athletic-association": {
208
+ shell: ["neutral"],
209
+ "stone, brick and windows": ["glass", { ...windows(0x6e6e6e, 0x3a3a3a), 0x5a5a5a: "brick", 0x6d6d6d: "limestone", 0x7a7a7a: "limestone" }],
210
+ cornice: ["limestone"],
211
+ },
212
+ // Mies's black curtain wall and bronze-tinted glass.
213
+ "building-office-west-of-aon": {
214
+ shell: ["neutral"],
215
+ "curtain wall": ["bronze glass", { ...windows(0x7a7a7a, 0x4a4a4a), 0x2b2b2b: "dark metal", 0x303030: "dark metal", 0x333333: "dark metal" }],
216
+ },
217
+ // White concrete.
218
+ "building-river-plaza": {
219
+ shell: ["concrete"],
220
+ "concrete and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x9a9a9a: "concrete" }],
221
+ "rooftop box": ["concrete"],
222
+ },
223
+ // Dark orange brick and bronze glass.
224
+ "building-hyatt-regency-west-tower": {
225
+ shell: ["neutral"],
226
+ "brick and windows": ["bronze glass", { ...windows(0x6d6d6d, 0x4a4a4a), 0x2c2c2c: "dark metal", 0x3b3b3b: "orange brick" }],
227
+ },
228
+ // Cream precast, and the drums' maroon fins.
229
+ "building-buckingham-east": {
230
+ shell: ["precast", { 0x686868: "neutral" }],
231
+ "precast and windows": ["glass", { ...windows(0x8a8a8a, 0x505050), 0x848484: "precast" }],
232
+ drums: ["precast", { 0x5a5a5a: "maroon", 0x686868: "neutral" }],
233
+ },
234
+ // Dark brown aluminium and bronze glass.
235
+ "building-three-illinois-center": {
236
+ shell: ["neutral"],
237
+ "curtain wall": ["bronze glass", { ...windows(0x707070, 0x464646), 0x282828: "bronze", 0x323232: "bronze" }],
238
+ },
239
+ // Reflective blue-green glass in its frame.
240
+ "building-swissotel": {
241
+ shell: ["neutral"],
242
+ "curtain wall": ["blue-green glass", windows(0x878787, 0x515151)],
243
+ },
244
+ };
245
+ // A batch's materials from its mesh's name, if the trial knows the building and the batch.
246
+ export function batchOf(buildingId, meshName) {
247
+ const materials = colourMaterials[buildingId];
248
+ const batch = meshName.split(" · ").at(-1);
249
+ return materials?.[batch] ?? materials?.[batch.replace(/ \d+$/, "")];
250
+ }
251
+ // A grey's tone: the hex the model wrote it as, from its linear channel.
252
+ export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
253
+ // OKLab from linear sRGB, and back, after Björn Ottosson's matrices.
254
+ export function toOklab(r, g, b) {
255
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
256
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
257
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
258
+ return [
259
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
260
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
261
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
262
+ ];
263
+ }
264
+ function fromOklab(lightness, a, b) {
265
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
266
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
267
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
268
+ return [
269
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
270
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
271
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
272
+ ];
273
+ }
274
+ const inGamut = (rgb) => rgb.every((channel) => channel >= -1e-7 && channel <= 1 + 1e-7);
275
+ // A grey in a material's colour: the grey's own lightness, with the material's hue and chroma,
276
+ // less of it where the grey is darker than the material, and less again where sRGB cannot
277
+ // show it. A material with no hue leaves the grey exactly as it was.
278
+ export function tint(grey, material) {
279
+ const swatch = new THREE.Color(colourPalette[material]);
280
+ const [swatchLightness, a, b] = toOklab(swatch.r, swatch.g, swatch.b);
281
+ if (Math.hypot(a, b) < 1e-4)
282
+ return [grey, grey, grey];
283
+ const [lightness] = toOklab(grey, grey, grey);
284
+ const at = (scale) => fromOklab(lightness, a * scale, b * scale);
285
+ let scale = Math.min(1, lightness / swatchLightness);
286
+ if (!inGamut(at(scale))) {
287
+ let low = 0;
288
+ for (let step = 0; step < 20; step += 1) {
289
+ const middle = (low + scale) / 2;
290
+ if (inGamut(at(middle)))
291
+ low = middle;
292
+ else
293
+ scale = middle;
294
+ }
295
+ scale = low;
296
+ }
297
+ return at(scale).map((channel) => Math.min(1, Math.max(0, channel)));
298
+ }
299
+ // The colour layer for a skyline's models. A building or batch the trial does not know stays
300
+ // in grey, as does a mesh without a single toon material, a vertex-coloured batch whose
301
+ // colours are not plain floats, and a material or colour buffer another batch already took,
302
+ // since the builder lets batches share them.
303
+ export function createSkylineColour(models) {
304
+ const painted = [], flat = [], taken = new Set();
305
+ for (const model of models) {
306
+ const buildingId = String(model.building.userData["buildingId"]);
307
+ model.building.traverse((object) => {
308
+ const mesh = object;
309
+ if (!mesh.isMesh)
310
+ return;
311
+ const batch = batchOf(buildingId, mesh.name);
312
+ if (!batch)
313
+ return;
314
+ const [base, tones = {}] = batch;
315
+ if (Array.isArray(mesh.material) || !mesh.material.isMeshToonMaterial)
316
+ return;
317
+ const material = mesh.material;
318
+ const colourOf = (grey) => tint(grey, tones[toneOf(grey)] ?? base);
319
+ const attribute = mesh.geometry.getAttribute("color");
320
+ if (material.vertexColors) {
321
+ if (!(attribute?.array instanceof Float32Array) || taken.has(attribute))
322
+ return;
323
+ taken.add(attribute);
324
+ const cache = new Map();
325
+ const paint = (grey) => {
326
+ let colour = cache.get(grey);
327
+ if (!colour)
328
+ cache.set(grey, colour = Float32Array.from(colourOf(grey)));
329
+ return colour;
330
+ };
331
+ painted.push({ attribute, array: attribute.array, paint, greys: null });
332
+ }
333
+ else if (!taken.has(material)) {
334
+ taken.add(material);
335
+ flat.push({ material, grey: material.color.clone(), colour: new THREE.Color(...colourOf(material.color.r)) });
336
+ }
337
+ });
338
+ }
339
+ let enabled = false;
340
+ return {
341
+ get enabled() { return enabled; },
342
+ set(next) {
343
+ if (next === enabled)
344
+ return;
345
+ enabled = next;
346
+ for (const batch of painted) {
347
+ const { array } = batch;
348
+ if (next) {
349
+ const greys = batch.greys = new Float32Array(array.length / 3);
350
+ for (let vertex = 0; vertex < greys.length; vertex += 1) {
351
+ const grey = array[vertex * 3];
352
+ greys[vertex] = grey;
353
+ array.set(batch.paint(grey), vertex * 3);
354
+ }
355
+ }
356
+ else {
357
+ batch.greys.forEach((grey, vertex) => array.fill(grey, vertex * 3, vertex * 3 + 3));
358
+ batch.greys = null;
359
+ }
360
+ batch.attribute.needsUpdate = true;
361
+ }
362
+ for (const { material, grey, colour } of flat)
363
+ material.color.copy(next ? colour : grey);
364
+ },
365
+ refresh() {
366
+ if (!enabled)
367
+ return;
368
+ for (const batch of painted) {
369
+ const { array } = batch, greys = batch.greys;
370
+ let changed = false;
371
+ for (let vertex = 0; vertex < greys.length; vertex += 1) {
372
+ const colour = batch.paint(greys[vertex]), at = vertex * 3;
373
+ if (array[at] === colour[0] && array[at + 1] === colour[1] && array[at + 2] === colour[2])
374
+ continue;
375
+ const grey = array[at];
376
+ greys[vertex] = grey;
377
+ array.set(batch.paint(grey), at);
378
+ changed = true;
379
+ }
380
+ if (changed)
381
+ batch.attribute.needsUpdate = true;
382
+ }
383
+ },
384
+ };
385
+ }
@@ -0,0 +1,48 @@
1
+ // The colour trial's named materials (https://github.com/a2f0/skyline/issues/115): the one file
2
+ // in the repository allowed colours, which tests/grayscale.test.ts exempts by name. Each colour
3
+ // gives a material's hue and how strongly it shows; the lightness every surface keeps is its
4
+ // model's own grey, so colour mode adds hue to the skyline without changing its values. A
5
+ // material darker than its colour here carries proportionally less of it. These are hand-picked
6
+ // from the materials and colours each building's reference audit records, not measured from
7
+ // photographs; a material with no hue stays grey.
8
+ export const colourPalette = {
9
+ neutral: 0x808080,
10
+ // Vision glass: the default cool tint, the teal of 340 on the Park and the Heritage, Swissôtel's
11
+ // reflective blue-green, and the bronze tint of Illinois Center and its neighbours.
12
+ glass: 0x525e66,
13
+ "green glass": 0x3f6e6c,
14
+ "blue-green glass": 0x4a8290,
15
+ "bronze glass": 0x5a4b3e,
16
+ // Lit offices, and the dimmer ones further in.
17
+ "lit window": 0xf4d39a,
18
+ "dim window": 0xb89b72,
19
+ lamp: 0xfff0d2,
20
+ // Stone.
21
+ limestone: 0xd8caa8,
22
+ marble: 0xe4dfd4,
23
+ "white granite": 0xdcd7cd,
24
+ granite: 0xbcb2a8,
25
+ "dark granite": 0x62544f,
26
+ slate: 0x4c5260,
27
+ // Clay.
28
+ "white terracotta": 0xe9dec4,
29
+ "pink terracotta": 0xd8a898,
30
+ "green tile": 0x5e8c66,
31
+ brick: 0x9c4c38,
32
+ "common brick": 0x8c6c5a,
33
+ "orange brick": 0xa45a3a,
34
+ "brown brick": 0x7c5242,
35
+ "buff brick": 0xc9ab80,
36
+ "glazed brick": 0xe4dfcf,
37
+ // Concrete.
38
+ concrete: 0xcfc9bb,
39
+ precast: 0xdac8a2,
40
+ // Metal and paint.
41
+ copper: 0x6fa48f,
42
+ maroon: 0x74282f,
43
+ "blue enamel": 0x3e6194,
44
+ bronze: 0x6c5034,
45
+ "dark metal": 0x30333a,
46
+ aluminium: 0xbfc4c9,
47
+ stainless: 0xb4bcc6,
48
+ };
@@ -53,6 +53,7 @@
53
53
  <button type="button" id="streets" aria-pressed="true">streets</button>
54
54
  <button type="button" id="footprints" aria-pressed="false">footprints</button>
55
55
  <button type="button" id="wireframe" aria-pressed="false">wireframe</button>
56
+ <button type="button" id="colour" aria-pressed="false" title="Show each building's materials in colour, a trial">colour</button>
56
57
  <button type="button" id="turntable" aria-pressed="false" aria-describedby="motion-status">turntable</button>
57
58
  <button type="button" id="show-original" title="Compare with the original skyline SVG" hidden>show original</button>
58
59
  <div class="celebration-controls" id="celebration-controls" role="group" aria-label="Celebratory window lighting">
@@ -2,6 +2,7 @@ import { createBuildingStudy, frameBox } from "./study-viewer.js";
2
2
  import { createGeographicSkyline } from "./skyline-scene.js";
3
3
  import { createGeographicRoads } from "./models/skyline-geography.js";
4
4
  import { celebrations } from "./models/celebrations.js";
5
+ import { createSkylineColour } from "./models/colour-materials.js";
5
6
  import { loadMarkup, loadStylesheet } from "./markup.js";
6
7
  // The skyline viewer's full-screen 3D mode: every mapped building, seen through the
7
8
  // drawing's own camera and framed as the viewer frames the drawing, so each tower stands
@@ -55,6 +56,8 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
55
56
  enablePan: true,
56
57
  minimumCameraHeight: 1,
57
58
  });
59
+ // The colour trial's layer (models/colour-materials.ts), which the colour toggle turns on.
60
+ const colour = createSkylineColour(models);
58
61
  // A toolbar badge applies one celebration across the skyline, using each
59
62
  // building's own supported wording. Context menus change only their building.
60
63
  const illuminated = models.filter((model) => model.illumination);
@@ -63,6 +66,7 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
63
66
  const celebrationButtons = [...root.querySelectorAll("[data-celebration]")];
64
67
  function setCelebration(targets, next) {
65
68
  targets.forEach((model) => model.illumination.set(model.illumination.presets.some(({ id }) => id === next) ? next : null));
69
+ colour.refresh();
66
70
  for (const button of celebrationButtons) {
67
71
  const eligible = illuminated.filter((model) => model.illumination.presets.some(({ id }) => id === button.dataset["celebration"]));
68
72
  const active = eligible.filter((model) => model.illumination.active === button.dataset["celebration"]).length;
@@ -103,6 +107,14 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
103
107
  viewer.requestRender();
104
108
  }, listening);
105
109
  }
110
+ // The colour trial's toggle, pressed while the buildings show their materials' colours over
111
+ // the greys, which it restores exactly when pressed again.
112
+ const colourButton = root.querySelector("#colour");
113
+ colourButton.addEventListener("click", () => {
114
+ colour.set(!colour.enabled);
115
+ colourButton.setAttribute("aria-pressed", String(colour.enabled));
116
+ viewer.requestRender();
117
+ }, listening);
106
118
  // The star in the middle of the bar opens and closes the toolbar. Each group unfolds from
107
119
  // the star: clipped open from its `--fold` inset while it fades in, one beside the star
108
120
  // slides out from it to its edge of the bar, and it folds back the same way before it hides. A toggle mid-way reverses the moving