@a2f0/skyline 0.2.3 → 0.2.5
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/lib/models/colour-materials.d.ts +20 -3
- package/lib/models/colour-materials.js +135 -66
- package/lib/models/colour-palette.d.ts +7 -34
- package/lib/models/colour-palette.js +61 -47
- package/lib/study-types.d.ts +8 -0
- package/package.json +1 -1
- package/site/models/colour-materials.js +135 -66
- package/site/models/colour-palette.js +61 -47
- package/site/skyline-3d.html +1 -1
- package/site/skyline-3d.js +10 -5
- package/site/study-viewer.js +18 -4
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import type { BuildingModel } from "./building-kit.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Daylight } from "../study-types.js";
|
|
3
|
+
export declare const materialNames: readonly ["neutral", "glass", "green glass", "blue-green glass", "bronze glass", "lit window", "dim window", "lamp", "limestone", "marble", "white granite", "granite", "dark granite", "slate", "white terracotta", "pink terracotta", "green tile", "brick", "common brick", "orange brick", "brown brick", "buff brick", "glazed brick", "concrete", "precast", "copper", "maroon", "blue enamel", "bronze", "dark metal", "aluminium", "stainless", "crown glass", "crown lights", "screen", "band lights", "sign board", "sign letters", "cap lights"];
|
|
4
|
+
export type MaterialName = (typeof materialNames)[number];
|
|
3
5
|
export type Tones = Readonly<Record<number, MaterialName>>;
|
|
4
6
|
export type Batch = readonly [material: MaterialName, tones?: Tones];
|
|
5
7
|
export type BuildingMaterials = Readonly<Record<string, Batch>>;
|
|
6
8
|
export declare const colourMaterials: Readonly<Record<string, BuildingMaterials>>;
|
|
7
9
|
export declare function batchOf(buildingId: string, meshName: string): Batch | undefined;
|
|
8
10
|
export declare const toneOf: (grey: number) => number;
|
|
9
|
-
export declare
|
|
10
|
-
export
|
|
11
|
+
export declare const exposure = 1.4;
|
|
12
|
+
export interface Swatch {
|
|
13
|
+
colour: readonly [number, number, number];
|
|
14
|
+
reference: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function paint(grey: number, { colour: [r, g, b], reference }: Swatch): [number, number, number];
|
|
17
|
+
export declare function swatchesOf(buildingId: string, greys: ReadonlyMap<MaterialName, ReadonlyMap<number, number>>): Map<MaterialName, Swatch | null>;
|
|
18
|
+
export declare const daylightIntensities: {
|
|
19
|
+
readonly sun: 3.4;
|
|
20
|
+
readonly ambient: 0.9;
|
|
21
|
+
readonly fill: 0.5;
|
|
22
|
+
};
|
|
23
|
+
export declare const daylightGround: {
|
|
24
|
+
readonly blocks: 7237230;
|
|
25
|
+
readonly roads: 4868682;
|
|
26
|
+
};
|
|
27
|
+
export declare function createDaylight(): Daylight;
|
|
11
28
|
export interface SkylineColour {
|
|
12
29
|
/** Whether the buildings show their materials' colours. */
|
|
13
30
|
readonly enabled: boolean;
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
-
import {
|
|
2
|
+
import { daylightColours, measuredColours } from "./colour-palette.js";
|
|
3
|
+
// The colour trial (https://github.com/a2f0/skyline/issues/115): which named material each
|
|
4
|
+
// surface of each building is, and a layer that shows the colours measured for those materials
|
|
5
|
+
// (colour-palette.ts) over the greys the models build in, restoring the greys exactly when it is
|
|
6
|
+
// turned off. The models are unchanged; this reads a built model's meshes by its building ID
|
|
7
|
+
// and each mesh's batch, the part of its name after the last " · ", or that without a trailing
|
|
8
|
+
// number, so "glazing 3" reads as "glazing". A surface is its batch's material unless its grey
|
|
9
|
+
// names another.
|
|
10
|
+
export const materialNames = [
|
|
11
|
+
"neutral", "glass", "green glass", "blue-green glass", "bronze glass", "lit window", "dim window", "lamp",
|
|
12
|
+
"limestone", "marble", "white granite", "granite", "dark granite", "slate",
|
|
13
|
+
"white terracotta", "pink terracotta", "green tile", "brick", "common brick", "orange brick", "brown brick", "buff brick", "glazed brick",
|
|
14
|
+
"concrete", "precast", "copper", "maroon", "blue enamel", "bronze", "dark metal", "aluminium", "stainless",
|
|
15
|
+
// Parts the photographs show on their own, named as the night first showed them: crown glass
|
|
16
|
+
// and crown lights, an emblem screen and the lights in its bands, a sign's board and letters,
|
|
17
|
+
// and a lit cap.
|
|
18
|
+
"crown glass", "crown lights", "screen", "band lights", "sign board", "sign letters", "cap lights",
|
|
19
|
+
];
|
|
3
20
|
// A batch's lit and dim office windows.
|
|
4
21
|
const windows = (lit, dim) => ({ [lit]: "lit window", [dim]: "dim window" });
|
|
5
22
|
export const colourMaterials = {
|
|
@@ -28,8 +45,8 @@ export const colourMaterials = {
|
|
|
28
45
|
"building-crain-communications": {
|
|
29
46
|
"aluminum spandrels": ["aluminium"],
|
|
30
47
|
"ribbon glazing": ["glass", { ...windows(0x8a8a8a, 0x565656), 0x585858: "aluminium" }],
|
|
31
|
-
"sloped glazing": ["glass"],
|
|
32
|
-
"glazing grid": ["
|
|
48
|
+
"sloped glazing": ["crown glass"],
|
|
49
|
+
"glazing grid": ["crown glass"],
|
|
33
50
|
"diamond outline lights": ["lamp"],
|
|
34
51
|
coping: ["aluminium"],
|
|
35
52
|
"slot floor": ["neutral"],
|
|
@@ -58,26 +75,28 @@ export const colourMaterials = {
|
|
|
58
75
|
"crown mullions": ["stainless"],
|
|
59
76
|
spire: ["stainless"],
|
|
60
77
|
},
|
|
61
|
-
// Limestone piers and aluminium spandrels
|
|
78
|
+
// Limestone piers and aluminium spandrels, under the sign's board and letters.
|
|
62
79
|
"building-one-prudential-plaza": {
|
|
63
80
|
shell: ["limestone", { 0x242424: "dark metal", 0x4c4c4c: "neutral", 0x8e8e8e: "aluminium" }],
|
|
64
81
|
"windows and spandrels": ["glass", { ...windows(0x7c7c7c, 0x4e4e4e), 0x8a8a8a: "lit window", 0x242424: "dark metal", 0x8e8e8e: "aluminium" }],
|
|
65
82
|
"limestone piers": ["limestone"],
|
|
66
83
|
"limestone courses": ["limestone"],
|
|
67
|
-
"penthouse and sign": ["limestone", { 0x2e2e2e: "
|
|
84
|
+
"penthouse and sign": ["limestone", { 0x2e2e2e: "sign board", 0x383838: "dark metal", 0x4c4c4c: "neutral", 0xb0b0b0: "sign letters", 0xc4c4c4: "sign letters" }],
|
|
68
85
|
"screen louvers": ["aluminium"],
|
|
69
86
|
"antenna mast": ["stainless"],
|
|
70
87
|
},
|
|
71
|
-
// Stone faces and gables, and the dark glass strips between them
|
|
88
|
+
// Stone faces and gables, and the dark glass strips between them; the crown's risers and
|
|
89
|
+
// sloped glass, and the white bands and ribs on its ridges.
|
|
72
90
|
"building-two-prudential-plaza": {
|
|
73
91
|
"limestone, glass and crown": ["granite", {
|
|
74
|
-
0x353535: "glass", 0x3a3a3a: "glass",
|
|
92
|
+
0x353535: "glass", 0x3a3a3a: "glass", 0x404040: "glass", 0x454545: "glass", 0x4a4a4a: "glass",
|
|
93
|
+
0x3f3f3f: "crown glass", 0x565656: "crown glass", 0xcdcdcd: "crown lights",
|
|
75
94
|
...windows(0x8e8e8e, 0x5e5e5e),
|
|
76
95
|
}],
|
|
77
96
|
piers: ["granite"],
|
|
78
97
|
copings: ["granite"],
|
|
79
98
|
"strip mullions": ["aluminium"],
|
|
80
|
-
"crown ribs": ["
|
|
99
|
+
"crown ribs": ["crown lights"],
|
|
81
100
|
spire: ["stainless"],
|
|
82
101
|
"spire inset panels": ["glass"],
|
|
83
102
|
lobby: ["glass"],
|
|
@@ -87,15 +106,15 @@ export const colourMaterials = {
|
|
|
87
106
|
"tube shell": ["white granite", { 0x202020: "neutral", 0x4c4c4c: "neutral" }],
|
|
88
107
|
"window ribbons": ["glass", windows(0x7c7c7c, 0x4e4e4e)],
|
|
89
108
|
"granite piers": ["white granite"],
|
|
90
|
-
"granite cap": ["
|
|
109
|
+
"granite cap": ["cap lights"],
|
|
91
110
|
"rooftop enclosure": ["neutral", { 0x9c9c9c: "aluminium" }],
|
|
92
111
|
"antenna mast": ["stainless"],
|
|
93
112
|
},
|
|
94
113
|
"building-blue-cross-blue-shield": {
|
|
95
114
|
shell: ["glass", { 0x3a3a3a: "neutral" }],
|
|
96
|
-
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "
|
|
115
|
+
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "screen" }],
|
|
97
116
|
mullions: ["dark metal"],
|
|
98
|
-
"band columns and emblems": ["
|
|
117
|
+
"band columns and emblems": ["band lights"],
|
|
99
118
|
},
|
|
100
119
|
// Blue-green glass, and the south face's white concrete frame.
|
|
101
120
|
"building-340-on-the-park": {
|
|
@@ -250,91 +269,141 @@ export function batchOf(buildingId, meshName) {
|
|
|
250
269
|
}
|
|
251
270
|
// A grey's tone: the hex the model wrote it as, from its linear channel.
|
|
252
271
|
export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
272
|
+
// The measured colours scale to the scene by this factor in linear light, so the skyline view's
|
|
273
|
+
// faces show about the photograph's brightness under the day's lights. At 1.4 the skyline view's
|
|
274
|
+
// 1600 x 900 render, its pixels chosen by each photograph sample's rule, reaches 0.97 (Two
|
|
275
|
+
// Prudential's stone), 0.96 (Blue Cross's glass), 0.86 (Aon's granite), 0.80 (One Prudential's
|
|
276
|
+
// limestone) and 0.66 (Crain's aluminium) of the photograph's brightness in linear light. Aon's
|
|
277
|
+
// granite and Crain's aluminium pass white at their own greys here, so a higher factor would
|
|
278
|
+
// brighten only the darker materials. The Railway Exchange is left out: the view shows its shaded
|
|
279
|
+
// Michigan front, not the sunlit Jackson front its colour is measured on; that front renders 0.82
|
|
280
|
+
// of the photograph's shaded Michigan front.
|
|
281
|
+
export const exposure = 1.4;
|
|
282
|
+
// A grey in its measured colour: the swatch's colour scaled by the grey's brightness against the
|
|
283
|
+
// swatch's grey, both in linear light, so a darker surface of one material stays darker. A colour
|
|
284
|
+
// pushed past white keeps its hue at its brightest, so a bright material's lightest greys can
|
|
285
|
+
// meet there, as Aon's granite and Crain's aluminium do.
|
|
286
|
+
export function paint(grey, { colour: [r, g, b], reference }) {
|
|
287
|
+
const scale = exposure * (reference > 0 ? grey / reference : 1);
|
|
288
|
+
const fit = scale / Math.max(1, r * scale, g * scale, b * scale);
|
|
289
|
+
return [r * fit, g * fit, b * fit];
|
|
263
290
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
291
|
+
// Every measured material's swatch for a building, from how many vertices of each linear grey
|
|
292
|
+
// its surfaces hold: a material naming another takes that one's colour and grey, and a null one
|
|
293
|
+
// stays grey.
|
|
294
|
+
export function swatchesOf(buildingId, greys) {
|
|
295
|
+
const measured = measuredColours[buildingId];
|
|
296
|
+
const swatches = new Map();
|
|
297
|
+
if (!measured)
|
|
298
|
+
return swatches;
|
|
299
|
+
// The most common grey of a material, by vertices; the lighter of a tie.
|
|
300
|
+
const common = (material) => [...greys.get(material) ?? []].reduce((best, entry) => !best || entry[1] > best[1] || (entry[1] === best[1] && entry[0] > best[0]) ? entry : best, null)?.[0];
|
|
301
|
+
for (const material of greys.keys()) {
|
|
302
|
+
const entry = measured[material];
|
|
303
|
+
const source = typeof entry === "string" ? entry : material;
|
|
304
|
+
const value = typeof entry === "string" ? measured[entry] : entry;
|
|
305
|
+
const reference = common(source);
|
|
306
|
+
if (typeof value !== "number" || reference === undefined) {
|
|
307
|
+
swatches.set(material, null);
|
|
308
|
+
continue;
|
|
294
309
|
}
|
|
295
|
-
|
|
310
|
+
const { r, g, b } = new THREE.Color(value);
|
|
311
|
+
swatches.set(material, { colour: [r, g, b], reference });
|
|
312
|
+
}
|
|
313
|
+
return swatches;
|
|
314
|
+
}
|
|
315
|
+
// The sun's, the shade's and the fill light's intensities on the sunny day: the sun and the shade
|
|
316
|
+
// brighter than the night's 2.4 and 0.7, the fill a little dimmer than its 0.55. And the ground's
|
|
317
|
+
// greys in the sun: the platform's blocks, and the roads' darker asphalt.
|
|
318
|
+
export const daylightIntensities = { sun: 3.4, ambient: 0.9, fill: 0.5 };
|
|
319
|
+
export const daylightGround = { blocks: 0x6e6e6e, roads: 0x4a4a4a };
|
|
320
|
+
// The sunny day the colour toggle shows the buildings in: a sky fading from the photograph's blue
|
|
321
|
+
// at the top of the view to its haze at the horizon, blended in linear light, the warm sun, the
|
|
322
|
+
// sky's blue for the light in the shade, and its haze for the fill.
|
|
323
|
+
export function createDaylight() {
|
|
324
|
+
const rows = 256, data = new Uint8Array(rows * 4);
|
|
325
|
+
const top = new THREE.Color(daylightColours.sky), bottom = new THREE.Color(daylightColours.horizon);
|
|
326
|
+
for (let row = 0; row < rows; row += 1) {
|
|
327
|
+
const hex = bottom.clone().lerp(top, row / (rows - 1)).getHex();
|
|
328
|
+
data.set([(hex >> 16) & 255, (hex >> 8) & 255, hex & 255, 255], row * 4);
|
|
296
329
|
}
|
|
297
|
-
|
|
330
|
+
// A data texture's first row is the bottom of the view.
|
|
331
|
+
const sky = new THREE.DataTexture(data, 1, rows);
|
|
332
|
+
sky.colorSpace = "srgb";
|
|
333
|
+
sky.needsUpdate = true;
|
|
334
|
+
return {
|
|
335
|
+
sky,
|
|
336
|
+
sun: [daylightColours.sun, daylightIntensities.sun],
|
|
337
|
+
ambient: [daylightColours.sky, daylightIntensities.ambient],
|
|
338
|
+
fill: [daylightColours.horizon, daylightIntensities.fill],
|
|
339
|
+
ground: daylightGround.blocks,
|
|
340
|
+
};
|
|
298
341
|
}
|
|
299
|
-
// The colour layer for a skyline's models. A building
|
|
300
|
-
//
|
|
301
|
-
// colours are not plain floats, and a material or colour buffer
|
|
302
|
-
// since the builder lets batches share them.
|
|
342
|
+
// The colour layer for a skyline's models. A building the trial has no colours for stays in
|
|
343
|
+
// grey, as does a batch it does not know, a mesh without a single toon material, a
|
|
344
|
+
// vertex-coloured batch whose colours are not plain floats, and a material or colour buffer
|
|
345
|
+
// another batch already took, since the builder lets batches share them.
|
|
303
346
|
export function createSkylineColour(models) {
|
|
304
347
|
const painted = [], flat = [], taken = new Set();
|
|
305
348
|
for (const model of models) {
|
|
306
349
|
const buildingId = String(model.building.userData["buildingId"]);
|
|
350
|
+
if (!measuredColours[buildingId])
|
|
351
|
+
continue;
|
|
352
|
+
// The building's batches, and how many vertices of each grey each material holds.
|
|
353
|
+
const surfaces = [];
|
|
354
|
+
const greys = new Map();
|
|
355
|
+
const count = (material, grey, vertices) => {
|
|
356
|
+
const held = greys.get(material) ?? new Map();
|
|
357
|
+
held.set(grey, (held.get(grey) ?? 0) + vertices);
|
|
358
|
+
greys.set(material, held);
|
|
359
|
+
};
|
|
307
360
|
model.building.traverse((object) => {
|
|
308
361
|
const mesh = object;
|
|
309
362
|
if (!mesh.isMesh)
|
|
310
363
|
return;
|
|
311
364
|
const batch = batchOf(buildingId, mesh.name);
|
|
312
|
-
if (!batch)
|
|
365
|
+
if (!batch || Array.isArray(mesh.material) || !mesh.material.isMeshToonMaterial)
|
|
313
366
|
return;
|
|
314
367
|
const [base, tones = {}] = batch;
|
|
315
|
-
if (Array.isArray(mesh.material) || !mesh.material.isMeshToonMaterial)
|
|
316
|
-
return;
|
|
317
368
|
const material = mesh.material;
|
|
318
|
-
const
|
|
369
|
+
const materialOf = (grey) => tones[toneOf(grey)] ?? base;
|
|
319
370
|
const attribute = mesh.geometry.getAttribute("color");
|
|
320
371
|
if (material.vertexColors) {
|
|
321
372
|
if (!(attribute?.array instanceof Float32Array) || taken.has(attribute))
|
|
322
373
|
return;
|
|
323
374
|
taken.add(attribute);
|
|
375
|
+
const vertices = new Map();
|
|
376
|
+
for (let vertex = 0; vertex < attribute.count; vertex += 1)
|
|
377
|
+
vertices.set(attribute.array[vertex * 3], (vertices.get(attribute.array[vertex * 3]) ?? 0) + 1);
|
|
378
|
+
vertices.forEach((n, grey) => count(materialOf(grey), grey, n));
|
|
379
|
+
surfaces.push({ material, colours: { attribute, array: attribute.array }, materialOf });
|
|
380
|
+
}
|
|
381
|
+
else if (!taken.has(material)) {
|
|
382
|
+
taken.add(material);
|
|
383
|
+
count(materialOf(material.color.r), material.color.r, mesh.geometry.getAttribute("position").count);
|
|
384
|
+
surfaces.push({ material, colours: null, materialOf });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
const swatches = swatchesOf(buildingId, greys);
|
|
388
|
+
for (const { material, colours, materialOf } of surfaces) {
|
|
389
|
+
const colourOf = (grey) => {
|
|
390
|
+
const swatch = swatches.get(materialOf(grey));
|
|
391
|
+
return swatch ? paint(grey, swatch) : [grey, grey, grey];
|
|
392
|
+
};
|
|
393
|
+
if (colours) {
|
|
324
394
|
const cache = new Map();
|
|
325
|
-
const
|
|
395
|
+
const paintGrey = (grey) => {
|
|
326
396
|
let colour = cache.get(grey);
|
|
327
397
|
if (!colour)
|
|
328
398
|
cache.set(grey, colour = Float32Array.from(colourOf(grey)));
|
|
329
399
|
return colour;
|
|
330
400
|
};
|
|
331
|
-
painted.push({
|
|
401
|
+
painted.push({ ...colours, paint: paintGrey, greys: null });
|
|
332
402
|
}
|
|
333
|
-
else
|
|
334
|
-
taken.add(material);
|
|
403
|
+
else {
|
|
335
404
|
flat.push({ material, grey: material.color.clone(), colour: new THREE.Color(...colourOf(material.color.r)) });
|
|
336
405
|
}
|
|
337
|
-
}
|
|
406
|
+
}
|
|
338
407
|
}
|
|
339
408
|
let enabled = false;
|
|
340
409
|
return {
|
|
@@ -1,35 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readonly
|
|
6
|
-
readonly
|
|
7
|
-
readonly
|
|
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;
|
|
1
|
+
import type { MaterialName } from "./colour-materials.js";
|
|
2
|
+
export type Measurement = number | MaterialName | null;
|
|
3
|
+
export declare const measuredColours: Readonly<Record<string, Readonly<Partial<Record<MaterialName, Measurement>>>>>;
|
|
4
|
+
export declare const daylightColours: {
|
|
5
|
+
readonly sky: 10402779;
|
|
6
|
+
readonly horizon: 12898785;
|
|
7
|
+
readonly sun: 16774374;
|
|
34
8
|
};
|
|
35
|
-
export type MaterialName = keyof typeof colourPalette;
|
|
@@ -1,48 +1,62 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
// By day no office lights show: lit and dim windows are the glass, in its measured colour.
|
|
2
|
+
const glass = (colour) => ({ glass: colour, "lit window": colour, "dim window": colour });
|
|
3
|
+
export const measuredColours = {
|
|
4
|
+
// docs/railway-exchange-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
5
|
+
"building-railway-exchange": {
|
|
6
|
+
"white terracotta": 0xc8c1b3,
|
|
7
|
+
...glass(0x3d4243),
|
|
8
|
+
copper: null,
|
|
9
|
+
"common brick": null,
|
|
10
|
+
neutral: null,
|
|
11
|
+
},
|
|
12
|
+
// docs/crain-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
13
|
+
"building-crain-communications": {
|
|
14
|
+
aluminium: 0xfbfcfa,
|
|
15
|
+
...glass(0x1e2425),
|
|
16
|
+
"crown glass": 0x79828c,
|
|
17
|
+
lamp: "aluminium",
|
|
18
|
+
"dark metal": null,
|
|
19
|
+
neutral: null,
|
|
20
|
+
},
|
|
21
|
+
// docs/one-prudential-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
22
|
+
"building-one-prudential-plaza": {
|
|
23
|
+
limestone: 0xc2bcb4,
|
|
24
|
+
aluminium: "limestone",
|
|
25
|
+
...glass(0x625e59),
|
|
26
|
+
"sign board": 0xc4c0b9,
|
|
27
|
+
"sign letters": 0x6684a0,
|
|
28
|
+
"dark metal": null,
|
|
29
|
+
stainless: null,
|
|
30
|
+
neutral: null,
|
|
31
|
+
},
|
|
32
|
+
// docs/two-prudential-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
33
|
+
"building-two-prudential-plaza": {
|
|
34
|
+
granite: 0xb1b3b0,
|
|
35
|
+
...glass(0x424c55),
|
|
36
|
+
"crown glass": 0x5e7686,
|
|
37
|
+
"crown lights": 0xb6bbbb,
|
|
38
|
+
stainless: null,
|
|
39
|
+
aluminium: null,
|
|
40
|
+
},
|
|
41
|
+
// docs/aon-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
42
|
+
layer3: {
|
|
43
|
+
"white granite": 0xdfdfdd,
|
|
44
|
+
...glass(0x424240),
|
|
45
|
+
"cap lights": "white granite",
|
|
46
|
+
aluminium: null,
|
|
47
|
+
stainless: null,
|
|
48
|
+
neutral: null,
|
|
49
|
+
},
|
|
50
|
+
// docs/blue-cross-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg and a close-up.
|
|
51
|
+
"building-blue-cross-blue-shield": {
|
|
52
|
+
...glass(0x7d8992),
|
|
53
|
+
screen: 0x334a60,
|
|
54
|
+
"band lights": 0xb0bdcd,
|
|
55
|
+
"dark metal": null,
|
|
56
|
+
neutral: null,
|
|
57
|
+
},
|
|
48
58
|
};
|
|
59
|
+
// The sunny day the colour toggle shows: the sky's blue at the top of the view and its haze just
|
|
60
|
+
// above the horizon, each the median of a clear box of the same panorama (scripts/sample-colours.ts,
|
|
61
|
+
// the day study's sky), and the sun's warm white, chosen.
|
|
62
|
+
export const daylightColours = { sky: 0x9ebbdb, horizon: 0xc4d1e1, sun: 0xfff4e6 };
|
package/lib/study-types.d.ts
CHANGED
|
@@ -47,3 +47,11 @@ export interface StudyLabel {
|
|
|
47
47
|
text: string;
|
|
48
48
|
placement?: string;
|
|
49
49
|
}
|
|
50
|
+
export interface Daylight {
|
|
51
|
+
sky: THREE.Texture;
|
|
52
|
+
sun: readonly [colour: number, intensity: number];
|
|
53
|
+
ambient: readonly [colour: number, intensity: number];
|
|
54
|
+
fill: readonly [colour: number, intensity: number];
|
|
55
|
+
/** The platform's grey in sunlight. */
|
|
56
|
+
ground: number;
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
-
import {
|
|
2
|
+
import { daylightColours, measuredColours } from "./colour-palette.js";
|
|
3
|
+
// The colour trial (https://github.com/a2f0/skyline/issues/115): which named material each
|
|
4
|
+
// surface of each building is, and a layer that shows the colours measured for those materials
|
|
5
|
+
// (colour-palette.ts) over the greys the models build in, restoring the greys exactly when it is
|
|
6
|
+
// turned off. The models are unchanged; this reads a built model's meshes by its building ID
|
|
7
|
+
// and each mesh's batch, the part of its name after the last " · ", or that without a trailing
|
|
8
|
+
// number, so "glazing 3" reads as "glazing". A surface is its batch's material unless its grey
|
|
9
|
+
// names another.
|
|
10
|
+
export const materialNames = [
|
|
11
|
+
"neutral", "glass", "green glass", "blue-green glass", "bronze glass", "lit window", "dim window", "lamp",
|
|
12
|
+
"limestone", "marble", "white granite", "granite", "dark granite", "slate",
|
|
13
|
+
"white terracotta", "pink terracotta", "green tile", "brick", "common brick", "orange brick", "brown brick", "buff brick", "glazed brick",
|
|
14
|
+
"concrete", "precast", "copper", "maroon", "blue enamel", "bronze", "dark metal", "aluminium", "stainless",
|
|
15
|
+
// Parts the photographs show on their own, named as the night first showed them: crown glass
|
|
16
|
+
// and crown lights, an emblem screen and the lights in its bands, a sign's board and letters,
|
|
17
|
+
// and a lit cap.
|
|
18
|
+
"crown glass", "crown lights", "screen", "band lights", "sign board", "sign letters", "cap lights",
|
|
19
|
+
];
|
|
3
20
|
// A batch's lit and dim office windows.
|
|
4
21
|
const windows = (lit, dim) => ({ [lit]: "lit window", [dim]: "dim window" });
|
|
5
22
|
export const colourMaterials = {
|
|
@@ -28,8 +45,8 @@ export const colourMaterials = {
|
|
|
28
45
|
"building-crain-communications": {
|
|
29
46
|
"aluminum spandrels": ["aluminium"],
|
|
30
47
|
"ribbon glazing": ["glass", { ...windows(0x8a8a8a, 0x565656), 0x585858: "aluminium" }],
|
|
31
|
-
"sloped glazing": ["glass"],
|
|
32
|
-
"glazing grid": ["
|
|
48
|
+
"sloped glazing": ["crown glass"],
|
|
49
|
+
"glazing grid": ["crown glass"],
|
|
33
50
|
"diamond outline lights": ["lamp"],
|
|
34
51
|
coping: ["aluminium"],
|
|
35
52
|
"slot floor": ["neutral"],
|
|
@@ -58,26 +75,28 @@ export const colourMaterials = {
|
|
|
58
75
|
"crown mullions": ["stainless"],
|
|
59
76
|
spire: ["stainless"],
|
|
60
77
|
},
|
|
61
|
-
// Limestone piers and aluminium spandrels
|
|
78
|
+
// Limestone piers and aluminium spandrels, under the sign's board and letters.
|
|
62
79
|
"building-one-prudential-plaza": {
|
|
63
80
|
shell: ["limestone", { 0x242424: "dark metal", 0x4c4c4c: "neutral", 0x8e8e8e: "aluminium" }],
|
|
64
81
|
"windows and spandrels": ["glass", { ...windows(0x7c7c7c, 0x4e4e4e), 0x8a8a8a: "lit window", 0x242424: "dark metal", 0x8e8e8e: "aluminium" }],
|
|
65
82
|
"limestone piers": ["limestone"],
|
|
66
83
|
"limestone courses": ["limestone"],
|
|
67
|
-
"penthouse and sign": ["limestone", { 0x2e2e2e: "
|
|
84
|
+
"penthouse and sign": ["limestone", { 0x2e2e2e: "sign board", 0x383838: "dark metal", 0x4c4c4c: "neutral", 0xb0b0b0: "sign letters", 0xc4c4c4: "sign letters" }],
|
|
68
85
|
"screen louvers": ["aluminium"],
|
|
69
86
|
"antenna mast": ["stainless"],
|
|
70
87
|
},
|
|
71
|
-
// Stone faces and gables, and the dark glass strips between them
|
|
88
|
+
// Stone faces and gables, and the dark glass strips between them; the crown's risers and
|
|
89
|
+
// sloped glass, and the white bands and ribs on its ridges.
|
|
72
90
|
"building-two-prudential-plaza": {
|
|
73
91
|
"limestone, glass and crown": ["granite", {
|
|
74
|
-
0x353535: "glass", 0x3a3a3a: "glass",
|
|
92
|
+
0x353535: "glass", 0x3a3a3a: "glass", 0x404040: "glass", 0x454545: "glass", 0x4a4a4a: "glass",
|
|
93
|
+
0x3f3f3f: "crown glass", 0x565656: "crown glass", 0xcdcdcd: "crown lights",
|
|
75
94
|
...windows(0x8e8e8e, 0x5e5e5e),
|
|
76
95
|
}],
|
|
77
96
|
piers: ["granite"],
|
|
78
97
|
copings: ["granite"],
|
|
79
98
|
"strip mullions": ["aluminium"],
|
|
80
|
-
"crown ribs": ["
|
|
99
|
+
"crown ribs": ["crown lights"],
|
|
81
100
|
spire: ["stainless"],
|
|
82
101
|
"spire inset panels": ["glass"],
|
|
83
102
|
lobby: ["glass"],
|
|
@@ -87,15 +106,15 @@ export const colourMaterials = {
|
|
|
87
106
|
"tube shell": ["white granite", { 0x202020: "neutral", 0x4c4c4c: "neutral" }],
|
|
88
107
|
"window ribbons": ["glass", windows(0x7c7c7c, 0x4e4e4e)],
|
|
89
108
|
"granite piers": ["white granite"],
|
|
90
|
-
"granite cap": ["
|
|
109
|
+
"granite cap": ["cap lights"],
|
|
91
110
|
"rooftop enclosure": ["neutral", { 0x9c9c9c: "aluminium" }],
|
|
92
111
|
"antenna mast": ["stainless"],
|
|
93
112
|
},
|
|
94
113
|
"building-blue-cross-blue-shield": {
|
|
95
114
|
shell: ["glass", { 0x3a3a3a: "neutral" }],
|
|
96
|
-
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "
|
|
115
|
+
"glass, spandrels and bands": ["glass", { ...windows(0x8a8a8a, 0x5c5c5c), 0x2a2a2a: "neutral", 0x3e3e3e: "screen" }],
|
|
97
116
|
mullions: ["dark metal"],
|
|
98
|
-
"band columns and emblems": ["
|
|
117
|
+
"band columns and emblems": ["band lights"],
|
|
99
118
|
},
|
|
100
119
|
// Blue-green glass, and the south face's white concrete frame.
|
|
101
120
|
"building-340-on-the-park": {
|
|
@@ -250,91 +269,141 @@ export function batchOf(buildingId, meshName) {
|
|
|
250
269
|
}
|
|
251
270
|
// A grey's tone: the hex the model wrote it as, from its linear channel.
|
|
252
271
|
export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
272
|
+
// The measured colours scale to the scene by this factor in linear light, so the skyline view's
|
|
273
|
+
// faces show about the photograph's brightness under the day's lights. At 1.4 the skyline view's
|
|
274
|
+
// 1600 x 900 render, its pixels chosen by each photograph sample's rule, reaches 0.97 (Two
|
|
275
|
+
// Prudential's stone), 0.96 (Blue Cross's glass), 0.86 (Aon's granite), 0.80 (One Prudential's
|
|
276
|
+
// limestone) and 0.66 (Crain's aluminium) of the photograph's brightness in linear light. Aon's
|
|
277
|
+
// granite and Crain's aluminium pass white at their own greys here, so a higher factor would
|
|
278
|
+
// brighten only the darker materials. The Railway Exchange is left out: the view shows its shaded
|
|
279
|
+
// Michigan front, not the sunlit Jackson front its colour is measured on; that front renders 0.82
|
|
280
|
+
// of the photograph's shaded Michigan front.
|
|
281
|
+
export const exposure = 1.4;
|
|
282
|
+
// A grey in its measured colour: the swatch's colour scaled by the grey's brightness against the
|
|
283
|
+
// swatch's grey, both in linear light, so a darker surface of one material stays darker. A colour
|
|
284
|
+
// pushed past white keeps its hue at its brightest, so a bright material's lightest greys can
|
|
285
|
+
// meet there, as Aon's granite and Crain's aluminium do.
|
|
286
|
+
export function paint(grey, { colour: [r, g, b], reference }) {
|
|
287
|
+
const scale = exposure * (reference > 0 ? grey / reference : 1);
|
|
288
|
+
const fit = scale / Math.max(1, r * scale, g * scale, b * scale);
|
|
289
|
+
return [r * fit, g * fit, b * fit];
|
|
263
290
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
291
|
+
// Every measured material's swatch for a building, from how many vertices of each linear grey
|
|
292
|
+
// its surfaces hold: a material naming another takes that one's colour and grey, and a null one
|
|
293
|
+
// stays grey.
|
|
294
|
+
export function swatchesOf(buildingId, greys) {
|
|
295
|
+
const measured = measuredColours[buildingId];
|
|
296
|
+
const swatches = new Map();
|
|
297
|
+
if (!measured)
|
|
298
|
+
return swatches;
|
|
299
|
+
// The most common grey of a material, by vertices; the lighter of a tie.
|
|
300
|
+
const common = (material) => [...greys.get(material) ?? []].reduce((best, entry) => !best || entry[1] > best[1] || (entry[1] === best[1] && entry[0] > best[0]) ? entry : best, null)?.[0];
|
|
301
|
+
for (const material of greys.keys()) {
|
|
302
|
+
const entry = measured[material];
|
|
303
|
+
const source = typeof entry === "string" ? entry : material;
|
|
304
|
+
const value = typeof entry === "string" ? measured[entry] : entry;
|
|
305
|
+
const reference = common(source);
|
|
306
|
+
if (typeof value !== "number" || reference === undefined) {
|
|
307
|
+
swatches.set(material, null);
|
|
308
|
+
continue;
|
|
294
309
|
}
|
|
295
|
-
|
|
310
|
+
const { r, g, b } = new THREE.Color(value);
|
|
311
|
+
swatches.set(material, { colour: [r, g, b], reference });
|
|
312
|
+
}
|
|
313
|
+
return swatches;
|
|
314
|
+
}
|
|
315
|
+
// The sun's, the shade's and the fill light's intensities on the sunny day: the sun and the shade
|
|
316
|
+
// brighter than the night's 2.4 and 0.7, the fill a little dimmer than its 0.55. And the ground's
|
|
317
|
+
// greys in the sun: the platform's blocks, and the roads' darker asphalt.
|
|
318
|
+
export const daylightIntensities = { sun: 3.4, ambient: 0.9, fill: 0.5 };
|
|
319
|
+
export const daylightGround = { blocks: 0x6e6e6e, roads: 0x4a4a4a };
|
|
320
|
+
// The sunny day the colour toggle shows the buildings in: a sky fading from the photograph's blue
|
|
321
|
+
// at the top of the view to its haze at the horizon, blended in linear light, the warm sun, the
|
|
322
|
+
// sky's blue for the light in the shade, and its haze for the fill.
|
|
323
|
+
export function createDaylight() {
|
|
324
|
+
const rows = 256, data = new Uint8Array(rows * 4);
|
|
325
|
+
const top = new THREE.Color(daylightColours.sky), bottom = new THREE.Color(daylightColours.horizon);
|
|
326
|
+
for (let row = 0; row < rows; row += 1) {
|
|
327
|
+
const hex = bottom.clone().lerp(top, row / (rows - 1)).getHex();
|
|
328
|
+
data.set([(hex >> 16) & 255, (hex >> 8) & 255, hex & 255, 255], row * 4);
|
|
296
329
|
}
|
|
297
|
-
|
|
330
|
+
// A data texture's first row is the bottom of the view.
|
|
331
|
+
const sky = new THREE.DataTexture(data, 1, rows);
|
|
332
|
+
sky.colorSpace = "srgb";
|
|
333
|
+
sky.needsUpdate = true;
|
|
334
|
+
return {
|
|
335
|
+
sky,
|
|
336
|
+
sun: [daylightColours.sun, daylightIntensities.sun],
|
|
337
|
+
ambient: [daylightColours.sky, daylightIntensities.ambient],
|
|
338
|
+
fill: [daylightColours.horizon, daylightIntensities.fill],
|
|
339
|
+
ground: daylightGround.blocks,
|
|
340
|
+
};
|
|
298
341
|
}
|
|
299
|
-
// The colour layer for a skyline's models. A building
|
|
300
|
-
//
|
|
301
|
-
// colours are not plain floats, and a material or colour buffer
|
|
302
|
-
// since the builder lets batches share them.
|
|
342
|
+
// The colour layer for a skyline's models. A building the trial has no colours for stays in
|
|
343
|
+
// grey, as does a batch it does not know, a mesh without a single toon material, a
|
|
344
|
+
// vertex-coloured batch whose colours are not plain floats, and a material or colour buffer
|
|
345
|
+
// another batch already took, since the builder lets batches share them.
|
|
303
346
|
export function createSkylineColour(models) {
|
|
304
347
|
const painted = [], flat = [], taken = new Set();
|
|
305
348
|
for (const model of models) {
|
|
306
349
|
const buildingId = String(model.building.userData["buildingId"]);
|
|
350
|
+
if (!measuredColours[buildingId])
|
|
351
|
+
continue;
|
|
352
|
+
// The building's batches, and how many vertices of each grey each material holds.
|
|
353
|
+
const surfaces = [];
|
|
354
|
+
const greys = new Map();
|
|
355
|
+
const count = (material, grey, vertices) => {
|
|
356
|
+
const held = greys.get(material) ?? new Map();
|
|
357
|
+
held.set(grey, (held.get(grey) ?? 0) + vertices);
|
|
358
|
+
greys.set(material, held);
|
|
359
|
+
};
|
|
307
360
|
model.building.traverse((object) => {
|
|
308
361
|
const mesh = object;
|
|
309
362
|
if (!mesh.isMesh)
|
|
310
363
|
return;
|
|
311
364
|
const batch = batchOf(buildingId, mesh.name);
|
|
312
|
-
if (!batch)
|
|
365
|
+
if (!batch || Array.isArray(mesh.material) || !mesh.material.isMeshToonMaterial)
|
|
313
366
|
return;
|
|
314
367
|
const [base, tones = {}] = batch;
|
|
315
|
-
if (Array.isArray(mesh.material) || !mesh.material.isMeshToonMaterial)
|
|
316
|
-
return;
|
|
317
368
|
const material = mesh.material;
|
|
318
|
-
const
|
|
369
|
+
const materialOf = (grey) => tones[toneOf(grey)] ?? base;
|
|
319
370
|
const attribute = mesh.geometry.getAttribute("color");
|
|
320
371
|
if (material.vertexColors) {
|
|
321
372
|
if (!(attribute?.array instanceof Float32Array) || taken.has(attribute))
|
|
322
373
|
return;
|
|
323
374
|
taken.add(attribute);
|
|
375
|
+
const vertices = new Map();
|
|
376
|
+
for (let vertex = 0; vertex < attribute.count; vertex += 1)
|
|
377
|
+
vertices.set(attribute.array[vertex * 3], (vertices.get(attribute.array[vertex * 3]) ?? 0) + 1);
|
|
378
|
+
vertices.forEach((n, grey) => count(materialOf(grey), grey, n));
|
|
379
|
+
surfaces.push({ material, colours: { attribute, array: attribute.array }, materialOf });
|
|
380
|
+
}
|
|
381
|
+
else if (!taken.has(material)) {
|
|
382
|
+
taken.add(material);
|
|
383
|
+
count(materialOf(material.color.r), material.color.r, mesh.geometry.getAttribute("position").count);
|
|
384
|
+
surfaces.push({ material, colours: null, materialOf });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
const swatches = swatchesOf(buildingId, greys);
|
|
388
|
+
for (const { material, colours, materialOf } of surfaces) {
|
|
389
|
+
const colourOf = (grey) => {
|
|
390
|
+
const swatch = swatches.get(materialOf(grey));
|
|
391
|
+
return swatch ? paint(grey, swatch) : [grey, grey, grey];
|
|
392
|
+
};
|
|
393
|
+
if (colours) {
|
|
324
394
|
const cache = new Map();
|
|
325
|
-
const
|
|
395
|
+
const paintGrey = (grey) => {
|
|
326
396
|
let colour = cache.get(grey);
|
|
327
397
|
if (!colour)
|
|
328
398
|
cache.set(grey, colour = Float32Array.from(colourOf(grey)));
|
|
329
399
|
return colour;
|
|
330
400
|
};
|
|
331
|
-
painted.push({
|
|
401
|
+
painted.push({ ...colours, paint: paintGrey, greys: null });
|
|
332
402
|
}
|
|
333
|
-
else
|
|
334
|
-
taken.add(material);
|
|
403
|
+
else {
|
|
335
404
|
flat.push({ material, grey: material.color.clone(), colour: new THREE.Color(...colourOf(material.color.r)) });
|
|
336
405
|
}
|
|
337
|
-
}
|
|
406
|
+
}
|
|
338
407
|
}
|
|
339
408
|
let enabled = false;
|
|
340
409
|
return {
|
|
@@ -1,48 +1,62 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
// By day no office lights show: lit and dim windows are the glass, in its measured colour.
|
|
2
|
+
const glass = (colour) => ({ glass: colour, "lit window": colour, "dim window": colour });
|
|
3
|
+
export const measuredColours = {
|
|
4
|
+
// docs/railway-exchange-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
5
|
+
"building-railway-exchange": {
|
|
6
|
+
"white terracotta": 0xc8c1b3,
|
|
7
|
+
...glass(0x3d4243),
|
|
8
|
+
copper: null,
|
|
9
|
+
"common brick": null,
|
|
10
|
+
neutral: null,
|
|
11
|
+
},
|
|
12
|
+
// docs/crain-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
13
|
+
"building-crain-communications": {
|
|
14
|
+
aluminium: 0xfbfcfa,
|
|
15
|
+
...glass(0x1e2425),
|
|
16
|
+
"crown glass": 0x79828c,
|
|
17
|
+
lamp: "aluminium",
|
|
18
|
+
"dark metal": null,
|
|
19
|
+
neutral: null,
|
|
20
|
+
},
|
|
21
|
+
// docs/one-prudential-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
22
|
+
"building-one-prudential-plaza": {
|
|
23
|
+
limestone: 0xc2bcb4,
|
|
24
|
+
aluminium: "limestone",
|
|
25
|
+
...glass(0x625e59),
|
|
26
|
+
"sign board": 0xc4c0b9,
|
|
27
|
+
"sign letters": 0x6684a0,
|
|
28
|
+
"dark metal": null,
|
|
29
|
+
stainless: null,
|
|
30
|
+
neutral: null,
|
|
31
|
+
},
|
|
32
|
+
// docs/two-prudential-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
33
|
+
"building-two-prudential-plaza": {
|
|
34
|
+
granite: 0xb1b3b0,
|
|
35
|
+
...glass(0x424c55),
|
|
36
|
+
"crown glass": 0x5e7686,
|
|
37
|
+
"crown lights": 0xb6bbbb,
|
|
38
|
+
stainless: null,
|
|
39
|
+
aluminium: null,
|
|
40
|
+
},
|
|
41
|
+
// docs/aon-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
42
|
+
layer3: {
|
|
43
|
+
"white granite": 0xdfdfdd,
|
|
44
|
+
...glass(0x424240),
|
|
45
|
+
"cap lights": "white granite",
|
|
46
|
+
aluminium: null,
|
|
47
|
+
stainless: null,
|
|
48
|
+
neutral: null,
|
|
49
|
+
},
|
|
50
|
+
// docs/blue-cross-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg and a close-up.
|
|
51
|
+
"building-blue-cross-blue-shield": {
|
|
52
|
+
...glass(0x7d8992),
|
|
53
|
+
screen: 0x334a60,
|
|
54
|
+
"band lights": 0xb0bdcd,
|
|
55
|
+
"dark metal": null,
|
|
56
|
+
neutral: null,
|
|
57
|
+
},
|
|
48
58
|
};
|
|
59
|
+
// The sunny day the colour toggle shows: the sky's blue at the top of the view and its haze just
|
|
60
|
+
// above the horizon, each the median of a clear box of the same panorama (scripts/sample-colours.ts,
|
|
61
|
+
// the day study's sky), and the sun's warm white, chosen.
|
|
62
|
+
export const daylightColours = { sky: 0x9ebbdb, horizon: 0xc4d1e1, sun: 0xfff4e6 };
|
package/site/skyline-3d.html
CHANGED
|
@@ -53,7 +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="
|
|
56
|
+
<button type="button" id="colour" aria-pressed="false" title="Colour trial: a sunny day, each measured building in its photographed colours; the rest stay grey">colour</button>
|
|
57
57
|
<button type="button" id="turntable" aria-pressed="false" aria-describedby="motion-status">turntable</button>
|
|
58
58
|
<button type="button" id="show-original" title="Compare with the original skyline SVG" hidden>show original</button>
|
|
59
59
|
<div class="celebration-controls" id="celebration-controls" role="group" aria-label="Celebratory window lighting">
|
package/site/skyline-3d.js
CHANGED
|
@@ -2,7 +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
|
+
import { createDaylight, createSkylineColour, daylightGround } from "./models/colour-materials.js";
|
|
6
6
|
import { loadMarkup, loadStylesheet } from "./markup.js";
|
|
7
7
|
// The skyline viewer's full-screen 3D mode: every mapped building, seen through the
|
|
8
8
|
// drawing's own camera and framed as the viewer frames the drawing, so each tower stands
|
|
@@ -17,10 +17,12 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
|
|
|
17
17
|
// footprints control.
|
|
18
18
|
ground.group.children.forEach((child) => { child.visible = false; });
|
|
19
19
|
const { platform } = settings;
|
|
20
|
+
// The roads' grey at night, which the colour trial's day lightens and turning it off restores.
|
|
21
|
+
const nightRoads = 0x2c2c2c;
|
|
20
22
|
const roads = createGeographicRoads({
|
|
21
23
|
x: [platform.x - platform.width / 2, platform.x + platform.width / 2],
|
|
22
24
|
z: [platform.z - platform.depth / 2, platform.z + platform.depth / 2],
|
|
23
|
-
},
|
|
25
|
+
}, nightRoads);
|
|
24
26
|
const { azimuth } = drawingView;
|
|
25
27
|
// The study renders its first frame as it starts; the scene says so once its controls,
|
|
26
28
|
// menus and detail are wired too, at the end.
|
|
@@ -107,13 +109,16 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
|
|
|
107
109
|
viewer.requestRender();
|
|
108
110
|
}, listening);
|
|
109
111
|
}
|
|
110
|
-
// The colour trial's toggle, pressed while the
|
|
111
|
-
// the greys,
|
|
112
|
+
// The colour trial's toggle, pressed while the scene shows a sunny day, the measured buildings in
|
|
113
|
+
// their colours over the greys; pressed again, it restores the night and every grey exactly.
|
|
112
114
|
const colourButton = root.querySelector("#colour");
|
|
115
|
+
const daylight = createDaylight(), roadway = roads.material;
|
|
116
|
+
signal.addEventListener("abort", () => daylight.sky.dispose(), { once: true });
|
|
113
117
|
colourButton.addEventListener("click", () => {
|
|
114
118
|
colour.set(!colour.enabled);
|
|
119
|
+
roadway.color.setHex(colour.enabled ? daylightGround.roads : nightRoads);
|
|
120
|
+
viewer.setDaylight(colour.enabled ? daylight : null);
|
|
115
121
|
colourButton.setAttribute("aria-pressed", String(colour.enabled));
|
|
116
|
-
viewer.requestRender();
|
|
117
122
|
}, listening);
|
|
118
123
|
// The star in the middle of the bar opens and closes the toolbar. Each group unfolds from
|
|
119
124
|
// the star: clipped open from its `--fold` inset while it fades in, one beside the star
|
package/site/study-viewer.js
CHANGED
|
@@ -105,8 +105,11 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
105
105
|
controls.target.fromArray(target);
|
|
106
106
|
controls.rotateSpeed = 0.65;
|
|
107
107
|
controls.zoomSpeed = 0.7;
|
|
108
|
-
|
|
109
|
-
const
|
|
108
|
+
// The night's lights, which setDaylight(null) restores.
|
|
109
|
+
const night = { sun: [0xffffff, 2.4], ambient: [0xffffff, 0.7], fill: [0xffffff, 0.55] };
|
|
110
|
+
const ambientLight = new THREE.AmbientLight(...night.ambient);
|
|
111
|
+
scene.add(ambientLight);
|
|
112
|
+
const keyLight = new THREE.DirectionalLight(...night.sun);
|
|
110
113
|
keyLight.position.fromArray(lightPosition);
|
|
111
114
|
keyLight.target.position.set(0, 80, 0);
|
|
112
115
|
keyLight.castShadow = true;
|
|
@@ -114,7 +117,7 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
114
117
|
Object.assign(keyLight.shadow.camera, shadowCamera);
|
|
115
118
|
keyLight.shadow.normalBias = 0.2;
|
|
116
119
|
scene.add(keyLight, keyLight.target);
|
|
117
|
-
const fillLight = new THREE.DirectionalLight(
|
|
120
|
+
const fillLight = new THREE.DirectionalLight(...night.fill);
|
|
118
121
|
fillLight.position.set(100, 140, -140);
|
|
119
122
|
scene.add(fillLight);
|
|
120
123
|
let modelOwners = new Map(models.map((model) => [model.building, model]));
|
|
@@ -123,6 +126,7 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
123
126
|
const base = new THREE.Mesh(new THREE.BoxGeometry(platform.width, 2, platform.depth), new THREE.MeshToonMaterial({ color: platform.color ?? 0x3a3a3a }));
|
|
124
127
|
base.position.set(platform.x || 0, -1.1, platform.z || 0);
|
|
125
128
|
base.receiveShadow = true;
|
|
129
|
+
const nightGround = base.material.color.getHex();
|
|
126
130
|
scene.add(base);
|
|
127
131
|
const baseEdges = new THREE.LineSegments(new THREE.EdgesGeometry(base.geometry), new THREE.LineBasicMaterial({ color: 0x555555 }));
|
|
128
132
|
baseEdges.position.copy(base.position);
|
|
@@ -662,5 +666,15 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
662
666
|
window.__buildingStudy = api;
|
|
663
667
|
Object.defineProperty(canvas, "__buildingStudy", { value: api, configurable: true });
|
|
664
668
|
onReady();
|
|
665
|
-
|
|
669
|
+
// Day or night: the transparent canvas shows the page's night behind it until a sky covers it.
|
|
670
|
+
function setDaylight(day) {
|
|
671
|
+
scene.background = day?.sky ?? null;
|
|
672
|
+
base.material.color.setHex(day?.ground ?? nightGround);
|
|
673
|
+
for (const [light, [colour, intensity]] of [[keyLight, day?.sun ?? night.sun], [ambientLight, day?.ambient ?? night.ambient], [fillLight, day?.fill ?? night.fill]]) {
|
|
674
|
+
light.color.setHex(colour);
|
|
675
|
+
light.intensity = intensity;
|
|
676
|
+
}
|
|
677
|
+
requestRender();
|
|
678
|
+
}
|
|
679
|
+
return { setLayout, setView, requestRender, buildingAt, setDaylight };
|
|
666
680
|
}
|