@a2f0/skyline 0.2.9 → 0.2.11
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 +5 -0
- package/lib/models/colour-materials.js +28 -9
- package/lib/models/colour-palette.js +30 -2
- package/lib/models/colour-reflections.d.ts +2 -0
- package/lib/models/colour-reflections.js +53 -0
- package/lib/skyline-package.d.ts +3 -1
- package/lib/skyline-package.js +4 -2
- package/lib/skyline-shell.d.ts +2 -0
- package/package.json +1 -1
- package/site/building-detail-page.js +1 -0
- package/site/building-detail.html +1 -0
- package/site/building-detail.js +24 -4
- package/site/models/colour-materials.js +28 -9
- package/site/models/colour-palette.js +30 -2
- package/site/models/colour-reflections.js +53 -0
- package/site/skyline-3d-page.js +1 -1
- package/site/skyline-3d.js +18 -8
- package/site/skyline-viewer.js +2 -1
- package/site/viewer-page.js +1 -0
package/docs/package.md
CHANGED
|
@@ -5,6 +5,11 @@ modules. Package builds emit ordinary ESM JavaScript and TypeScript declarations
|
|
|
5
5
|
consumers of the published package do not compile repository sources or run installation
|
|
6
6
|
scripts. Browser code and Node/Bun build helpers have separate entrypoints.
|
|
7
7
|
|
|
8
|
+
To start an embed in sunny-day colour, pass `colour: true` to `mountSkyline` alongside
|
|
9
|
+
`assetsUrl`. Its toolbar can return to greyscale, and building details inherit and share its mode.
|
|
10
|
+
Omitting `colour` keeps the existing greyscale default. The setting belongs to that instance;
|
|
11
|
+
it does not use storage or affect another embed. The standalone viewer accepts `?colour=1`.
|
|
12
|
+
|
|
8
13
|
## Install from npm
|
|
9
14
|
|
|
10
15
|
```sh
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from "../vendor/three-r186.js";
|
|
2
2
|
import { daylightColours, measuredColours } from "./colour-palette.js";
|
|
3
|
+
import { glassReflection } from "./colour-reflections.js";
|
|
3
4
|
// The colour trial (https://github.com/a2f0/skyline/issues/115): which named material each
|
|
4
5
|
// surface of each building is, and a layer that shows the colours measured for those materials
|
|
5
6
|
// (colour-palette.ts) over the greys the models build in, restoring the greys exactly when it is
|
|
@@ -61,7 +62,7 @@ export const colourMaterials = {
|
|
|
61
62
|
"bronze mullions and columns": ["bronze"],
|
|
62
63
|
},
|
|
63
64
|
"building-michigan-plaza-south-tower": {
|
|
64
|
-
"mapped tower shell": ["
|
|
65
|
+
"mapped tower shell": ["aluminium"],
|
|
65
66
|
"mullions and bands": ["aluminium"],
|
|
66
67
|
"lit glazing": ["lit window"],
|
|
67
68
|
"dim glazing": ["dim window"],
|
|
@@ -281,11 +282,11 @@ export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
|
|
|
281
282
|
// 0.25 to 1.56 on their measured materials and 0.14 on Kemper's whole face, where the scene's sun
|
|
282
283
|
// lights their faces differently from the photograph's. On the Michigan Avenue wall (FID-COL-005),
|
|
283
284
|
// whose shaded fronts are estimated as if sunlit, the fronts render 0.81 to 1.21 of the
|
|
284
|
-
// photograph's shaded fronts and the south faces 0.77 to 0.86 of its sunlit ones. The next
|
|
285
|
-
// (FID-COL-006) render 0.79 to 0.98 on sunlit faces, 1.27 on 180 North Michigan's stone
|
|
286
|
-
// Swissôtel's half-shadowed sliver, and 0.41 to 0.51 of their
|
|
287
|
-
// close-ups' shaded Michigan fronts. docs/viewer.md
|
|
288
|
-
//
|
|
285
|
+
// photograph's shaded fronts and the south faces 0.77 to 0.86 of its sunlit ones. The next eleven
|
|
286
|
+
// (FID-COL-006) render 0.79 to 0.98 on sunlit faces, 1.27 and 1.21 on 180 North Michigan's stone
|
|
287
|
+
// and River Plaza's top, 0.5 on Swissôtel's half-shadowed sliver, and 0.41 to 0.51 of their
|
|
288
|
+
// calibrated sunlit colours on the close-ups' shaded Michigan fronts. docs/viewer.md and the
|
|
289
|
+
// audits give each.
|
|
289
290
|
export const exposure = 1.4;
|
|
290
291
|
// A grey in its measured colour: the swatch's colour scaled by the grey's brightness against the
|
|
291
292
|
// swatch's grey, both in linear light, so a darker surface of one material stays darker. A colour
|
|
@@ -353,6 +354,8 @@ export function createDaylight() {
|
|
|
353
354
|
// another batch already took, since the builder lets batches share them.
|
|
354
355
|
export function createSkylineColour(models) {
|
|
355
356
|
const painted = [], flat = [], taken = new Set();
|
|
357
|
+
const reflections = [];
|
|
358
|
+
const glassMaterials = new Set(["glass", "green glass", "blue-green glass", "bronze glass", "crown glass", "lit window", "dim window"]);
|
|
356
359
|
for (const model of models) {
|
|
357
360
|
const buildingId = String(model.building.userData["buildingId"]);
|
|
358
361
|
if (!measuredColours[buildingId])
|
|
@@ -384,16 +387,30 @@ export function createSkylineColour(models) {
|
|
|
384
387
|
for (let vertex = 0; vertex < attribute.count; vertex += 1)
|
|
385
388
|
vertices.set(attribute.array[vertex * 3], (vertices.get(attribute.array[vertex * 3]) ?? 0) + 1);
|
|
386
389
|
vertices.forEach((n, grey) => count(materialOf(grey), grey, n));
|
|
387
|
-
surfaces.push({ material, colours: { attribute, array: attribute.array }, materialOf });
|
|
390
|
+
surfaces.push({ geometry: mesh.geometry, material, colours: { attribute, array: attribute.array }, materialOf });
|
|
388
391
|
}
|
|
389
392
|
else if (!taken.has(material)) {
|
|
390
393
|
taken.add(material);
|
|
391
394
|
count(materialOf(material.color.r), material.color.r, mesh.geometry.getAttribute("position").count);
|
|
392
|
-
surfaces.push({ material, colours: null, materialOf });
|
|
395
|
+
surfaces.push({ geometry: mesh.geometry, material, colours: null, materialOf });
|
|
393
396
|
}
|
|
394
397
|
});
|
|
395
398
|
const swatches = swatchesOf(buildingId, greys);
|
|
396
|
-
for (const { material, colours, materialOf } of surfaces) {
|
|
399
|
+
for (const { geometry, material, colours, materialOf } of surfaces) {
|
|
400
|
+
const mask = new Float32Array(geometry.getAttribute("position").count);
|
|
401
|
+
const reflecting = new Map();
|
|
402
|
+
for (let vertex = 0; vertex < mask.length; vertex += 1) {
|
|
403
|
+
const grey = colours ? colours.array[vertex * 3] : material.color.r;
|
|
404
|
+
let reflects = reflecting.get(grey);
|
|
405
|
+
if (reflects === undefined) {
|
|
406
|
+
const name = materialOf(grey);
|
|
407
|
+
reflects = glassMaterials.has(name) && swatches.get(name) ? 1 : 0;
|
|
408
|
+
reflecting.set(grey, reflects);
|
|
409
|
+
}
|
|
410
|
+
mask[vertex] = reflects;
|
|
411
|
+
}
|
|
412
|
+
if (mask.some((value) => value > 0))
|
|
413
|
+
reflections.push(glassReflection(material, geometry, mask));
|
|
397
414
|
const colourOf = (grey) => {
|
|
398
415
|
const swatch = swatches.get(materialOf(grey));
|
|
399
416
|
return swatch ? paint(grey, swatch) : [grey, grey, grey];
|
|
@@ -438,6 +455,8 @@ export function createSkylineColour(models) {
|
|
|
438
455
|
}
|
|
439
456
|
for (const { material, grey, colour } of flat)
|
|
440
457
|
material.color.copy(next ? colour : grey);
|
|
458
|
+
for (const reflect of reflections)
|
|
459
|
+
reflect(next);
|
|
441
460
|
},
|
|
442
461
|
refresh() {
|
|
443
462
|
if (!enabled)
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
// By day no office lights show: lit and dim windows are the glass, in its measured colour.
|
|
2
2
|
const glass = (colour, material = "glass") => ({ [material]: colour, "lit window": colour, "dim window": colour });
|
|
3
3
|
export const measuredColours = {
|
|
4
|
+
// FID-COL-007: provisional, direct 2014 daytime close-ups, WITHOUT the panorama's light
|
|
5
|
+
// calibration. These measure appearance (sky reflections included), not intrinsic albedo.
|
|
6
|
+
// docs/three-illinois-center-reference.md and docs/michigan-plaza-south-geographic-reference.md.
|
|
7
|
+
"building-three-illinois-center": {
|
|
8
|
+
bronze: 0x55585f,
|
|
9
|
+
...glass(0x707d8e, "bronze glass"),
|
|
10
|
+
neutral: null,
|
|
11
|
+
},
|
|
12
|
+
"building-michigan-plaza-south-tower": {
|
|
13
|
+
aluminium: 0x1d2736,
|
|
14
|
+
...glass(0x6385a2),
|
|
15
|
+
},
|
|
4
16
|
// docs/railway-exchange-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
5
17
|
"building-railway-exchange": {
|
|
6
18
|
"white terracotta": 0xc8c1b3,
|
|
@@ -166,7 +178,7 @@ export const measuredColours = {
|
|
|
166
178
|
...glass(0x3b3f42),
|
|
167
179
|
neutral: null,
|
|
168
180
|
},
|
|
169
|
-
// FID-COL-006,
|
|
181
|
+
// FID-COL-006, six towers the panorama shows, measured on their sunlit south faces.
|
|
170
182
|
// docs/buckingham-reference.md, 2026-10-10 — Daytime colours from Chicago.jpg.
|
|
171
183
|
"building-the-buckingham": {
|
|
172
184
|
concrete: 0xc0beb6,
|
|
@@ -195,6 +207,21 @@ export const measuredColours = {
|
|
|
195
207
|
"dim window": null,
|
|
196
208
|
neutral: null,
|
|
197
209
|
},
|
|
210
|
+
// docs/river-plaza-two-illinois-center-reference.md, 2026-10-10 — Daytime colours from Chicago.jpg.
|
|
211
|
+
// Two Illinois Center's black curtain wall, its lighter and darker pixels apart, and River Plaza's
|
|
212
|
+
// white concrete, the top floors that show above it. Two Illinois Center's identification is
|
|
213
|
+
// provisional: its photographed top stands further above its model than the group's others.
|
|
214
|
+
"building-office-west-of-aon": {
|
|
215
|
+
"dark metal": 0x444c57,
|
|
216
|
+
...glass(0x2e3641, "bronze glass"),
|
|
217
|
+
neutral: null,
|
|
218
|
+
},
|
|
219
|
+
"building-river-plaza": {
|
|
220
|
+
concrete: 0xe5dfd3,
|
|
221
|
+
glass: null,
|
|
222
|
+
"lit window": null,
|
|
223
|
+
"dim window": null,
|
|
224
|
+
},
|
|
198
225
|
// Michigan Avenue fronts Grant Park's trees hide in the panorama: measured in a close-up under an
|
|
199
226
|
// overcast sky and calibrated to the panorama through Willoughby Tower's limestone beside them,
|
|
200
227
|
// its ratio in linear light between the two photographs (scripts/sample-colours.ts, the day
|
|
@@ -238,7 +265,8 @@ export const measuredColours = {
|
|
|
238
265
|
neutral: null,
|
|
239
266
|
},
|
|
240
267
|
// The Hyatt Regency's west tower, a few pixels wide in the panorama, in an overcast close-up
|
|
241
|
-
// calibrated through Aon's granite behind it.
|
|
268
|
+
// calibrated through Aon's granite behind it. Provisional: haze over Aon, and the panorama's
|
|
269
|
+
// brighter strip, say the brick may be up to twice as bright as this.
|
|
242
270
|
// docs/hyatt-west-tower-reference.md, 2026-10-10 — Daytime colours from a close-up calibrated to Chicago.jpg.
|
|
243
271
|
"building-hyatt-regency-west-tower": {
|
|
244
272
|
"orange brick": 0x65554b,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { daylightColours } from "./colour-palette.js";
|
|
3
|
+
// A small sky reflection over the measured glass colour. This is an illustration of an open
|
|
4
|
+
// sky, not a reflection of nearby buildings or a replacement for the toon renderer. The mask
|
|
5
|
+
// separates glass from masonry and mullions even when the builder batches them in one mesh.
|
|
6
|
+
// Install only in colour mode, and restore the original shader on exit. Keep the mask attached
|
|
7
|
+
// once used so Three.js releases its GPU buffer when it disposes the geometry.
|
|
8
|
+
export function glassReflection(material, geometry, mask) {
|
|
9
|
+
const attribute = new THREE.Float32BufferAttribute(mask, 1);
|
|
10
|
+
const originalCompile = material.onBeforeCompile, originalKey = material.customProgramCacheKey;
|
|
11
|
+
const key = originalKey.call(material);
|
|
12
|
+
const uniforms = {
|
|
13
|
+
skylineSky: { value: new THREE.Color(daylightColours.sky) },
|
|
14
|
+
skylineHorizon: { value: new THREE.Color(daylightColours.horizon) },
|
|
15
|
+
skylineGround: { value: new THREE.Color(0x444444) },
|
|
16
|
+
};
|
|
17
|
+
const compile = (shader, renderer) => {
|
|
18
|
+
originalCompile.call(material, shader, renderer);
|
|
19
|
+
Object.assign(shader.uniforms, uniforms);
|
|
20
|
+
shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
|
|
21
|
+
attribute float skylineGlass;
|
|
22
|
+
varying float vSkylineGlass;`).replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
23
|
+
vSkylineGlass = skylineGlass;`);
|
|
24
|
+
shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
|
|
25
|
+
varying float vSkylineGlass;
|
|
26
|
+
uniform vec3 skylineSky;
|
|
27
|
+
uniform vec3 skylineHorizon;
|
|
28
|
+
uniform vec3 skylineGround;`).replace("#include <opaque_fragment>", `
|
|
29
|
+
if (vSkylineGlass > 0.0) {
|
|
30
|
+
vec3 eye = vec3(0.0);
|
|
31
|
+
eye.z = 1.0;
|
|
32
|
+
if (!isOrthographic) eye = normalize(vViewPosition);
|
|
33
|
+
vec3 reflected = inverseTransformDirection(reflect(-eye, normal), viewMatrix);
|
|
34
|
+
vec3 sky = mix(skylineHorizon, skylineSky, smoothstep(0.0, 0.7, reflected.y));
|
|
35
|
+
vec3 environment = mix(skylineGround, sky, smoothstep(-0.12, 0.12, reflected.y));
|
|
36
|
+
float grazing = pow(1.0 - clamp(dot(normal, eye), 0.0, 1.0), 5.0);
|
|
37
|
+
outgoingLight = mix(outgoingLight, environment, vSkylineGlass * (0.06 + 0.54 * grazing));
|
|
38
|
+
}
|
|
39
|
+
#include <opaque_fragment>`);
|
|
40
|
+
};
|
|
41
|
+
return (enabled) => {
|
|
42
|
+
if (enabled) {
|
|
43
|
+
geometry.setAttribute("skylineGlass", attribute);
|
|
44
|
+
material.onBeforeCompile = compile;
|
|
45
|
+
material.customProgramCacheKey = () => `${key}:skyline-glass-v1`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
material.onBeforeCompile = originalCompile;
|
|
49
|
+
material.customProgramCacheKey = originalKey;
|
|
50
|
+
}
|
|
51
|
+
material.needsUpdate = true;
|
|
52
|
+
};
|
|
53
|
+
}
|
package/lib/skyline-package.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface SkylineOptions {
|
|
|
16
16
|
readonly title?: string;
|
|
17
17
|
/** Start the 3D scene's control bar open, or folded behind its star. Closed by default. */
|
|
18
18
|
readonly controls?: "open" | "closed";
|
|
19
|
+
/** Start in the opt-in sunny-day colour view. Greyscale by default. */
|
|
20
|
+
readonly colour?: boolean;
|
|
19
21
|
/**
|
|
20
22
|
* Show the OpenStreetMap credit with the 3D scene's controls. Shown by default. OpenStreetMap's
|
|
21
23
|
* licence requires that credit wherever its data appears, so hide it only when the host page
|
|
@@ -36,4 +38,4 @@ export interface SkylineOptions {
|
|
|
36
38
|
* size and placement: give it an explicit height. Call `destroy` when the hosting component
|
|
37
39
|
* leaves.
|
|
38
40
|
*/
|
|
39
|
-
export declare function mountSkyline(container: HTMLElement, { assetsUrl, title, controls, attribution, three }: SkylineOptions): SkylineInstance;
|
|
41
|
+
export declare function mountSkyline(container: HTMLElement, { assetsUrl, title, controls, colour, attribution, three }: SkylineOptions): SkylineInstance;
|
package/lib/skyline-package.js
CHANGED
|
@@ -8,8 +8,10 @@ import { mountViewer } from "./skyline-shell.js";
|
|
|
8
8
|
* size and placement: give it an explicit height. Call `destroy` when the hosting component
|
|
9
9
|
* leaves.
|
|
10
10
|
*/
|
|
11
|
-
export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline", controls = "closed", attribution = true, three }) {
|
|
11
|
+
export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline", controls = "closed", colour = false, attribution = true, three }) {
|
|
12
12
|
if (controls !== "open" && controls !== "closed")
|
|
13
13
|
throw new TypeError('Skyline controls must be "open" or "closed".');
|
|
14
|
-
|
|
14
|
+
if (typeof colour !== "boolean")
|
|
15
|
+
throw new TypeError("Skyline colour must be a boolean.");
|
|
16
|
+
return mountViewer(container, { assetsUrl, title, controls, colour, attribution, navigation: false, page: false, three });
|
|
15
17
|
}
|
package/lib/skyline-shell.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ViewerContext {
|
|
|
11
11
|
readonly signal: AbortSignal;
|
|
12
12
|
/** Start the 3D scene's control bar open, rather than folded behind its star. */
|
|
13
13
|
readonly controls: "open" | "closed";
|
|
14
|
+
/** Start the scene and its building details in sunny-day colour. */
|
|
15
|
+
readonly colour?: boolean;
|
|
14
16
|
/** Show the OpenStreetMap credit with the scene's controls. */
|
|
15
17
|
readonly attribution: boolean;
|
|
16
18
|
/**
|
package/package.json
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
</div>
|
|
42
42
|
<div class="button-group" role="group" aria-label="Model display">
|
|
43
43
|
<button type="button" id="wireframe" aria-pressed="false">wireframe</button>
|
|
44
|
+
<button type="button" id="colour" aria-pressed="false" title="Show the building in sunny-day colour">colour</button>
|
|
44
45
|
<button type="button" id="turntable" aria-pressed="false" aria-describedby="motion-status">turntable</button>
|
|
45
46
|
</div>
|
|
46
47
|
</div>
|
package/site/building-detail.js
CHANGED
|
@@ -3,19 +3,21 @@ import { createBuildingStudy } from "./study-viewer.js";
|
|
|
3
3
|
import { skylineAzimuth } from "./skyline-comparison.js";
|
|
4
4
|
import { geographicBuildings } from "./models/skyline-geography-data.js";
|
|
5
5
|
import { createGeographicBuilding, footprintMetrics } from "./models/skyline-geography.js";
|
|
6
|
+
import { createDaylight, createSkylineColour } from "./models/colour-materials.js";
|
|
6
7
|
// One mapped building of the 3D skyline, alone on its own platform, as the building study
|
|
7
8
|
// shows Crain: building-detail.html?building=<id>, and the panel the 3D skyline floats over
|
|
8
9
|
// the skyline from a building's context menu.
|
|
9
|
-
export function showBuildingDetail({ root, id, frame, signal }) {
|
|
10
|
+
export function showBuildingDetail({ root, id, frame, signal, colour = false, onColourChange }) {
|
|
10
11
|
const record = geographicBuildings.find((entry) => entry.id === id);
|
|
11
12
|
if (record)
|
|
12
|
-
showBuilding(record, root, frame, signal);
|
|
13
|
+
return showBuilding(record, root, frame, signal, colour, onColourChange);
|
|
13
14
|
else {
|
|
14
15
|
root.querySelector("#loading").textContent = "No mapped building by that name. Right-click a building on the 3D skyline to see its detail.";
|
|
15
16
|
root.querySelectorAll("button").forEach((button) => { button.disabled = true; });
|
|
16
17
|
}
|
|
18
|
+
return undefined;
|
|
17
19
|
}
|
|
18
|
-
function showBuilding(record, root, frame, signal) {
|
|
20
|
+
function showBuilding(record, root, frame, signal, initiallyColoured, onColourChange) {
|
|
19
21
|
const footprint = footprintMetrics(record.footprint.coordinates);
|
|
20
22
|
// Centred on its mapped outline: ground coordinates are east and north, the scene's x and -z.
|
|
21
23
|
const model = createGeographicBuilding(record, [-footprint.center[0], footprint.center[1]]);
|
|
@@ -62,7 +64,7 @@ function showBuilding(record, root, frame, signal) {
|
|
|
62
64
|
quarter: { azimuth: skylineAzimuth + 0.22, polar: Math.PI / 2 - 0.15, label: "three-quarter view" },
|
|
63
65
|
side: { azimuth: skylineAzimuth + Math.PI / 2, polar: Math.PI / 2 - 0.12, label: "side view" },
|
|
64
66
|
};
|
|
65
|
-
createBuildingStudy({
|
|
67
|
+
const viewer = createBuildingStudy({
|
|
66
68
|
root,
|
|
67
69
|
frame,
|
|
68
70
|
signal,
|
|
@@ -78,6 +80,24 @@ function showBuilding(record, root, frame, signal) {
|
|
|
78
80
|
// button stops.
|
|
79
81
|
turntable: true,
|
|
80
82
|
});
|
|
83
|
+
const colour = createSkylineColour([model]), button = root.querySelector("#colour");
|
|
84
|
+
let daylight;
|
|
85
|
+
signal.addEventListener("abort", () => daylight?.sky.dispose(), { once: true });
|
|
86
|
+
function setColour(enabled) {
|
|
87
|
+
colour.set(enabled);
|
|
88
|
+
if (enabled)
|
|
89
|
+
daylight ??= createDaylight();
|
|
90
|
+
viewer.setDaylight(enabled ? daylight : null);
|
|
91
|
+
button.setAttribute("aria-pressed", String(enabled));
|
|
92
|
+
button.title = enabled ? "Restore the building's greyscale view" : "Show the building in sunny-day colour";
|
|
93
|
+
}
|
|
94
|
+
button.addEventListener("click", () => {
|
|
95
|
+
setColour(!colour.enabled);
|
|
96
|
+
onColourChange?.(colour.enabled);
|
|
97
|
+
}, { signal });
|
|
98
|
+
if (initiallyColoured)
|
|
99
|
+
setColour(true);
|
|
100
|
+
return { setColour };
|
|
81
101
|
}
|
|
82
102
|
// Sources open beside the page, so a detail floated over the skyline stays where it is.
|
|
83
103
|
function link(document, href, text) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from "../vendor/three-r186.js";
|
|
2
2
|
import { daylightColours, measuredColours } from "./colour-palette.js";
|
|
3
|
+
import { glassReflection } from "./colour-reflections.js";
|
|
3
4
|
// The colour trial (https://github.com/a2f0/skyline/issues/115): which named material each
|
|
4
5
|
// surface of each building is, and a layer that shows the colours measured for those materials
|
|
5
6
|
// (colour-palette.ts) over the greys the models build in, restoring the greys exactly when it is
|
|
@@ -61,7 +62,7 @@ export const colourMaterials = {
|
|
|
61
62
|
"bronze mullions and columns": ["bronze"],
|
|
62
63
|
},
|
|
63
64
|
"building-michigan-plaza-south-tower": {
|
|
64
|
-
"mapped tower shell": ["
|
|
65
|
+
"mapped tower shell": ["aluminium"],
|
|
65
66
|
"mullions and bands": ["aluminium"],
|
|
66
67
|
"lit glazing": ["lit window"],
|
|
67
68
|
"dim glazing": ["dim window"],
|
|
@@ -281,11 +282,11 @@ export const toneOf = (grey) => new THREE.Color(grey, grey, grey).getHex();
|
|
|
281
282
|
// 0.25 to 1.56 on their measured materials and 0.14 on Kemper's whole face, where the scene's sun
|
|
282
283
|
// lights their faces differently from the photograph's. On the Michigan Avenue wall (FID-COL-005),
|
|
283
284
|
// whose shaded fronts are estimated as if sunlit, the fronts render 0.81 to 1.21 of the
|
|
284
|
-
// photograph's shaded fronts and the south faces 0.77 to 0.86 of its sunlit ones. The next
|
|
285
|
-
// (FID-COL-006) render 0.79 to 0.98 on sunlit faces, 1.27 on 180 North Michigan's stone
|
|
286
|
-
// Swissôtel's half-shadowed sliver, and 0.41 to 0.51 of their
|
|
287
|
-
// close-ups' shaded Michigan fronts. docs/viewer.md
|
|
288
|
-
//
|
|
285
|
+
// photograph's shaded fronts and the south faces 0.77 to 0.86 of its sunlit ones. The next eleven
|
|
286
|
+
// (FID-COL-006) render 0.79 to 0.98 on sunlit faces, 1.27 and 1.21 on 180 North Michigan's stone
|
|
287
|
+
// and River Plaza's top, 0.5 on Swissôtel's half-shadowed sliver, and 0.41 to 0.51 of their
|
|
288
|
+
// calibrated sunlit colours on the close-ups' shaded Michigan fronts. docs/viewer.md and the
|
|
289
|
+
// audits give each.
|
|
289
290
|
export const exposure = 1.4;
|
|
290
291
|
// A grey in its measured colour: the swatch's colour scaled by the grey's brightness against the
|
|
291
292
|
// swatch's grey, both in linear light, so a darker surface of one material stays darker. A colour
|
|
@@ -353,6 +354,8 @@ export function createDaylight() {
|
|
|
353
354
|
// another batch already took, since the builder lets batches share them.
|
|
354
355
|
export function createSkylineColour(models) {
|
|
355
356
|
const painted = [], flat = [], taken = new Set();
|
|
357
|
+
const reflections = [];
|
|
358
|
+
const glassMaterials = new Set(["glass", "green glass", "blue-green glass", "bronze glass", "crown glass", "lit window", "dim window"]);
|
|
356
359
|
for (const model of models) {
|
|
357
360
|
const buildingId = String(model.building.userData["buildingId"]);
|
|
358
361
|
if (!measuredColours[buildingId])
|
|
@@ -384,16 +387,30 @@ export function createSkylineColour(models) {
|
|
|
384
387
|
for (let vertex = 0; vertex < attribute.count; vertex += 1)
|
|
385
388
|
vertices.set(attribute.array[vertex * 3], (vertices.get(attribute.array[vertex * 3]) ?? 0) + 1);
|
|
386
389
|
vertices.forEach((n, grey) => count(materialOf(grey), grey, n));
|
|
387
|
-
surfaces.push({ material, colours: { attribute, array: attribute.array }, materialOf });
|
|
390
|
+
surfaces.push({ geometry: mesh.geometry, material, colours: { attribute, array: attribute.array }, materialOf });
|
|
388
391
|
}
|
|
389
392
|
else if (!taken.has(material)) {
|
|
390
393
|
taken.add(material);
|
|
391
394
|
count(materialOf(material.color.r), material.color.r, mesh.geometry.getAttribute("position").count);
|
|
392
|
-
surfaces.push({ material, colours: null, materialOf });
|
|
395
|
+
surfaces.push({ geometry: mesh.geometry, material, colours: null, materialOf });
|
|
393
396
|
}
|
|
394
397
|
});
|
|
395
398
|
const swatches = swatchesOf(buildingId, greys);
|
|
396
|
-
for (const { material, colours, materialOf } of surfaces) {
|
|
399
|
+
for (const { geometry, material, colours, materialOf } of surfaces) {
|
|
400
|
+
const mask = new Float32Array(geometry.getAttribute("position").count);
|
|
401
|
+
const reflecting = new Map();
|
|
402
|
+
for (let vertex = 0; vertex < mask.length; vertex += 1) {
|
|
403
|
+
const grey = colours ? colours.array[vertex * 3] : material.color.r;
|
|
404
|
+
let reflects = reflecting.get(grey);
|
|
405
|
+
if (reflects === undefined) {
|
|
406
|
+
const name = materialOf(grey);
|
|
407
|
+
reflects = glassMaterials.has(name) && swatches.get(name) ? 1 : 0;
|
|
408
|
+
reflecting.set(grey, reflects);
|
|
409
|
+
}
|
|
410
|
+
mask[vertex] = reflects;
|
|
411
|
+
}
|
|
412
|
+
if (mask.some((value) => value > 0))
|
|
413
|
+
reflections.push(glassReflection(material, geometry, mask));
|
|
397
414
|
const colourOf = (grey) => {
|
|
398
415
|
const swatch = swatches.get(materialOf(grey));
|
|
399
416
|
return swatch ? paint(grey, swatch) : [grey, grey, grey];
|
|
@@ -438,6 +455,8 @@ export function createSkylineColour(models) {
|
|
|
438
455
|
}
|
|
439
456
|
for (const { material, grey, colour } of flat)
|
|
440
457
|
material.color.copy(next ? colour : grey);
|
|
458
|
+
for (const reflect of reflections)
|
|
459
|
+
reflect(next);
|
|
441
460
|
},
|
|
442
461
|
refresh() {
|
|
443
462
|
if (!enabled)
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
// By day no office lights show: lit and dim windows are the glass, in its measured colour.
|
|
2
2
|
const glass = (colour, material = "glass") => ({ [material]: colour, "lit window": colour, "dim window": colour });
|
|
3
3
|
export const measuredColours = {
|
|
4
|
+
// FID-COL-007: provisional, direct 2014 daytime close-ups, WITHOUT the panorama's light
|
|
5
|
+
// calibration. These measure appearance (sky reflections included), not intrinsic albedo.
|
|
6
|
+
// docs/three-illinois-center-reference.md and docs/michigan-plaza-south-geographic-reference.md.
|
|
7
|
+
"building-three-illinois-center": {
|
|
8
|
+
bronze: 0x55585f,
|
|
9
|
+
...glass(0x707d8e, "bronze glass"),
|
|
10
|
+
neutral: null,
|
|
11
|
+
},
|
|
12
|
+
"building-michigan-plaza-south-tower": {
|
|
13
|
+
aluminium: 0x1d2736,
|
|
14
|
+
...glass(0x6385a2),
|
|
15
|
+
},
|
|
4
16
|
// docs/railway-exchange-reference.md, 2026-10-09 — Daytime colours from Chicago.jpg.
|
|
5
17
|
"building-railway-exchange": {
|
|
6
18
|
"white terracotta": 0xc8c1b3,
|
|
@@ -166,7 +178,7 @@ export const measuredColours = {
|
|
|
166
178
|
...glass(0x3b3f42),
|
|
167
179
|
neutral: null,
|
|
168
180
|
},
|
|
169
|
-
// FID-COL-006,
|
|
181
|
+
// FID-COL-006, six towers the panorama shows, measured on their sunlit south faces.
|
|
170
182
|
// docs/buckingham-reference.md, 2026-10-10 — Daytime colours from Chicago.jpg.
|
|
171
183
|
"building-the-buckingham": {
|
|
172
184
|
concrete: 0xc0beb6,
|
|
@@ -195,6 +207,21 @@ export const measuredColours = {
|
|
|
195
207
|
"dim window": null,
|
|
196
208
|
neutral: null,
|
|
197
209
|
},
|
|
210
|
+
// docs/river-plaza-two-illinois-center-reference.md, 2026-10-10 — Daytime colours from Chicago.jpg.
|
|
211
|
+
// Two Illinois Center's black curtain wall, its lighter and darker pixels apart, and River Plaza's
|
|
212
|
+
// white concrete, the top floors that show above it. Two Illinois Center's identification is
|
|
213
|
+
// provisional: its photographed top stands further above its model than the group's others.
|
|
214
|
+
"building-office-west-of-aon": {
|
|
215
|
+
"dark metal": 0x444c57,
|
|
216
|
+
...glass(0x2e3641, "bronze glass"),
|
|
217
|
+
neutral: null,
|
|
218
|
+
},
|
|
219
|
+
"building-river-plaza": {
|
|
220
|
+
concrete: 0xe5dfd3,
|
|
221
|
+
glass: null,
|
|
222
|
+
"lit window": null,
|
|
223
|
+
"dim window": null,
|
|
224
|
+
},
|
|
198
225
|
// Michigan Avenue fronts Grant Park's trees hide in the panorama: measured in a close-up under an
|
|
199
226
|
// overcast sky and calibrated to the panorama through Willoughby Tower's limestone beside them,
|
|
200
227
|
// its ratio in linear light between the two photographs (scripts/sample-colours.ts, the day
|
|
@@ -238,7 +265,8 @@ export const measuredColours = {
|
|
|
238
265
|
neutral: null,
|
|
239
266
|
},
|
|
240
267
|
// The Hyatt Regency's west tower, a few pixels wide in the panorama, in an overcast close-up
|
|
241
|
-
// calibrated through Aon's granite behind it.
|
|
268
|
+
// calibrated through Aon's granite behind it. Provisional: haze over Aon, and the panorama's
|
|
269
|
+
// brighter strip, say the brick may be up to twice as bright as this.
|
|
242
270
|
// docs/hyatt-west-tower-reference.md, 2026-10-10 — Daytime colours from a close-up calibrated to Chicago.jpg.
|
|
243
271
|
"building-hyatt-regency-west-tower": {
|
|
244
272
|
"orange brick": 0x65554b,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as THREE from "../vendor/three-r186.js";
|
|
2
|
+
import { daylightColours } from "./colour-palette.js";
|
|
3
|
+
// A small sky reflection over the measured glass colour. This is an illustration of an open
|
|
4
|
+
// sky, not a reflection of nearby buildings or a replacement for the toon renderer. The mask
|
|
5
|
+
// separates glass from masonry and mullions even when the builder batches them in one mesh.
|
|
6
|
+
// Install only in colour mode, and restore the original shader on exit. Keep the mask attached
|
|
7
|
+
// once used so Three.js releases its GPU buffer when it disposes the geometry.
|
|
8
|
+
export function glassReflection(material, geometry, mask) {
|
|
9
|
+
const attribute = new THREE.Float32BufferAttribute(mask, 1);
|
|
10
|
+
const originalCompile = material.onBeforeCompile, originalKey = material.customProgramCacheKey;
|
|
11
|
+
const key = originalKey.call(material);
|
|
12
|
+
const uniforms = {
|
|
13
|
+
skylineSky: { value: new THREE.Color(daylightColours.sky) },
|
|
14
|
+
skylineHorizon: { value: new THREE.Color(daylightColours.horizon) },
|
|
15
|
+
skylineGround: { value: new THREE.Color(0x444444) },
|
|
16
|
+
};
|
|
17
|
+
const compile = (shader, renderer) => {
|
|
18
|
+
originalCompile.call(material, shader, renderer);
|
|
19
|
+
Object.assign(shader.uniforms, uniforms);
|
|
20
|
+
shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
|
|
21
|
+
attribute float skylineGlass;
|
|
22
|
+
varying float vSkylineGlass;`).replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
23
|
+
vSkylineGlass = skylineGlass;`);
|
|
24
|
+
shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
|
|
25
|
+
varying float vSkylineGlass;
|
|
26
|
+
uniform vec3 skylineSky;
|
|
27
|
+
uniform vec3 skylineHorizon;
|
|
28
|
+
uniform vec3 skylineGround;`).replace("#include <opaque_fragment>", `
|
|
29
|
+
if (vSkylineGlass > 0.0) {
|
|
30
|
+
vec3 eye = vec3(0.0);
|
|
31
|
+
eye.z = 1.0;
|
|
32
|
+
if (!isOrthographic) eye = normalize(vViewPosition);
|
|
33
|
+
vec3 reflected = inverseTransformDirection(reflect(-eye, normal), viewMatrix);
|
|
34
|
+
vec3 sky = mix(skylineHorizon, skylineSky, smoothstep(0.0, 0.7, reflected.y));
|
|
35
|
+
vec3 environment = mix(skylineGround, sky, smoothstep(-0.12, 0.12, reflected.y));
|
|
36
|
+
float grazing = pow(1.0 - clamp(dot(normal, eye), 0.0, 1.0), 5.0);
|
|
37
|
+
outgoingLight = mix(outgoingLight, environment, vSkylineGlass * (0.06 + 0.54 * grazing));
|
|
38
|
+
}
|
|
39
|
+
#include <opaque_fragment>`);
|
|
40
|
+
};
|
|
41
|
+
return (enabled) => {
|
|
42
|
+
if (enabled) {
|
|
43
|
+
geometry.setAttribute("skylineGlass", attribute);
|
|
44
|
+
material.onBeforeCompile = compile;
|
|
45
|
+
material.customProgramCacheKey = () => `${key}:skyline-glass-v1`;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
material.onBeforeCompile = originalCompile;
|
|
49
|
+
material.customProgramCacheKey = originalKey;
|
|
50
|
+
}
|
|
51
|
+
material.needsUpdate = true;
|
|
52
|
+
};
|
|
53
|
+
}
|
package/site/skyline-3d-page.js
CHANGED
|
@@ -3,7 +3,7 @@ import { startSkyline3d } from "./skyline-3d.js";
|
|
|
3
3
|
// switch to. Its address's settings were applied before the scene's code arrived.
|
|
4
4
|
const life = new AbortController();
|
|
5
5
|
try {
|
|
6
|
-
startSkyline3d({ root: document, frame: document.body, assets: new URL("./", location.href), signal: life.signal });
|
|
6
|
+
startSkyline3d({ root: document, frame: document.body, assets: new URL("./", location.href), signal: life.signal, colour: new URLSearchParams(location.search).get("colour") === "1" });
|
|
7
7
|
}
|
|
8
8
|
catch (error) {
|
|
9
9
|
// A scene that fails part way releases what it started; study-loader.js says why.
|
package/site/skyline-3d.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadMarkup, loadStylesheet } from "./markup.js";
|
|
|
8
8
|
// drawing's own camera and framed as the viewer frames the drawing, so each tower stands
|
|
9
9
|
// where its drawn one does until the camera moves. It renders into skyline-3d.html's own
|
|
10
10
|
// document, or into the viewer's shadow root, from the same markup.
|
|
11
|
-
export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, onReady, onUnavailable }) {
|
|
11
|
+
export function startSkyline3d({ root, frame, assets, signal, colour: initiallyColoured = false, onShowOriginal, onReady, onUnavailable }) {
|
|
12
12
|
const document = root instanceof Document ? root : root.ownerDocument;
|
|
13
13
|
const listening = { signal };
|
|
14
14
|
const { models, ground, settings, drawingView, comparisonViews, labels } = createGeographicSkyline();
|
|
@@ -112,14 +112,23 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
|
|
|
112
112
|
// The colour trial's toggle, pressed while the scene shows a sunny day, the measured buildings in
|
|
113
113
|
// their colours over the greys; pressed again, it restores the night and every grey exactly.
|
|
114
114
|
const colourButton = root.querySelector("#colour");
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
const roadway = roads.material;
|
|
116
|
+
let daylight;
|
|
117
|
+
let detailView;
|
|
118
|
+
signal.addEventListener("abort", () => daylight?.sky.dispose(), { once: true });
|
|
119
|
+
function setColour(enabled) {
|
|
120
|
+
colour.set(enabled);
|
|
121
|
+
if (enabled)
|
|
122
|
+
daylight ??= createDaylight();
|
|
119
123
|
roadway.color.setHex(colour.enabled ? daylightGround.roads : nightRoads);
|
|
120
124
|
viewer.setDaylight(colour.enabled ? daylight : null);
|
|
121
125
|
colourButton.setAttribute("aria-pressed", String(colour.enabled));
|
|
122
|
-
|
|
126
|
+
colourButton.title = enabled ? "Restore the greyscale skyline" : "Show the skyline in sunny-day colour";
|
|
127
|
+
detailView?.setColour(enabled);
|
|
128
|
+
}
|
|
129
|
+
colourButton.addEventListener("click", () => setColour(!colour.enabled), listening);
|
|
130
|
+
if (initiallyColoured)
|
|
131
|
+
setColour(true);
|
|
123
132
|
// The star in the middle of the bar opens and closes the toolbar. Each group unfolds from
|
|
124
133
|
// the star: clipped open from its `--fold` inset while it fades in, one beside the star
|
|
125
134
|
// 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
|
|
@@ -223,7 +232,7 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
|
|
|
223
232
|
function openMenu(building, x, y) {
|
|
224
233
|
menuBuilding = building.building.userData["buildingId"];
|
|
225
234
|
menuTitle.textContent = building.building.userData["geography"]?.name ?? building.building.name;
|
|
226
|
-
detailLink.href = new URL(`building-detail.html?building=${encodeURIComponent(menuBuilding)}`, assets).href;
|
|
235
|
+
detailLink.href = new URL(`building-detail.html?building=${encodeURIComponent(menuBuilding)}${colour.enabled ? "&colour=1" : ""}`, assets).href;
|
|
227
236
|
lightingMenu.replaceChildren();
|
|
228
237
|
lightingMenu.hidden = !building.illumination;
|
|
229
238
|
if (building.illumination) {
|
|
@@ -377,6 +386,7 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
|
|
|
377
386
|
function clearDetail() {
|
|
378
387
|
detail?.abort();
|
|
379
388
|
detail = null;
|
|
389
|
+
detailView = undefined;
|
|
380
390
|
detailPanel.querySelector(".detail-host")?.remove();
|
|
381
391
|
}
|
|
382
392
|
function openDetail(id, name) {
|
|
@@ -401,7 +411,7 @@ export function startSkyline3d({ root, frame, assets, signal, onShowOriginal, on
|
|
|
401
411
|
// The panel names the detail and closes it, so the detail leaves out its own links.
|
|
402
412
|
markup.classList.add("framed");
|
|
403
413
|
detailRoot.append(document.importNode(markup, true));
|
|
404
|
-
showBuildingDetail({ root: detailRoot, id, frame: detailRoot.querySelector(".detail-root"), signal: controller.signal });
|
|
414
|
+
detailView = showBuildingDetail({ root: detailRoot, id, frame: detailRoot.querySelector(".detail-root"), signal: controller.signal, colour: colour.enabled, onColourChange: setColour });
|
|
405
415
|
}).catch((error) => {
|
|
406
416
|
if (controller.signal.aborted)
|
|
407
417
|
return;
|
package/site/skyline-viewer.js
CHANGED
|
@@ -22,7 +22,7 @@ function element(document, tag, properties = {}, attributes = {}) {
|
|
|
22
22
|
created.setAttribute(name, value);
|
|
23
23
|
return created;
|
|
24
24
|
}
|
|
25
|
-
export function mount(root, { assets, signal, controls, attribution, navigation, page, three }) {
|
|
25
|
+
export function mount(root, { assets, signal, controls, colour, attribution, navigation, page, three }) {
|
|
26
26
|
const document = root.ownerDocument;
|
|
27
27
|
const host = root.host;
|
|
28
28
|
const listening = { signal };
|
|
@@ -180,6 +180,7 @@ export function mount(root, { assets, signal, controls, attribution, navigation,
|
|
|
180
180
|
frame: scene,
|
|
181
181
|
assets,
|
|
182
182
|
signal: sceneLife.signal,
|
|
183
|
+
colour,
|
|
183
184
|
onShowOriginal: showOriginal,
|
|
184
185
|
onReady() {
|
|
185
186
|
sceneUnavailable = false;
|
package/site/viewer-page.js
CHANGED
|
@@ -14,6 +14,7 @@ mountViewer(document.querySelector("#viewer"), {
|
|
|
14
14
|
title: "Chicago skyline",
|
|
15
15
|
controls: controls === "open" || controls === "closed" ? controls : embedded ? "closed" : "open",
|
|
16
16
|
attribution: settings.get("attribution") !== "hidden",
|
|
17
|
+
colour: settings.get("colour") === "1",
|
|
17
18
|
navigation: !embedded,
|
|
18
19
|
page: true,
|
|
19
20
|
});
|