@a2f0/skyline 0.2.19 → 0.2.22
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/blue-cross-geographic.js +55 -20
- package/lib/models/colour-materials.d.ts +8 -1
- package/lib/models/colour-materials.js +40 -9
- package/lib/models/colour-palette.d.ts +32 -0
- package/lib/models/colour-palette.js +11 -0
- package/lib/models/colour-reflections.d.ts +13 -1
- package/lib/models/colour-reflections.js +69 -10
- package/lib/models/night-windows.d.ts +13 -0
- package/lib/models/night-windows.js +76 -0
- package/lib/study-types.d.ts +2 -0
- package/package.json +1 -1
- package/site/building-detail.js +30 -7
- package/site/models/blue-cross-geographic.js +55 -20
- package/site/models/colour-materials.js +40 -9
- package/site/models/colour-palette.js +11 -0
- package/site/models/colour-reflections.js +69 -10
- package/site/models/night-windows.js +76 -0
- package/site/skyline-3d.html +6 -0
- package/site/skyline-3d.js +50 -13
- package/site/skyline-viewer.js +1 -1
- package/site/solar-time.js +53 -0
- package/site/study-loader.js +1 -1
- package/site/study-viewer.js +27 -0
- package/site/study.css +14 -2
- package/site/time-of-day.js +56 -0
package/site/study-loader.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const entry = document.currentScript.dataset["study"];
|
|
4
4
|
function showStudyError(message) {
|
|
5
5
|
document.querySelector("#loading").textContent = message;
|
|
6
|
-
document.querySelectorAll("button").forEach((
|
|
6
|
+
document.querySelectorAll("button, input").forEach((control) => { control.disabled = true; });
|
|
7
7
|
}
|
|
8
8
|
if (location.protocol === "file:") {
|
|
9
9
|
const page = location.pathname.split("/").pop();
|
package/site/study-viewer.js
CHANGED
|
@@ -109,6 +109,7 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
109
109
|
const night = { sun: [0xffffff, 2.4], ambient: [0xffffff, 0.7], fill: [0xffffff, 0.55] };
|
|
110
110
|
const ambientLight = new THREE.AmbientLight(...night.ambient);
|
|
111
111
|
scene.add(ambientLight);
|
|
112
|
+
let lightBounds;
|
|
112
113
|
const keyLight = new THREE.DirectionalLight(...night.sun);
|
|
113
114
|
keyLight.position.fromArray(lightPosition);
|
|
114
115
|
keyLight.target.position.set(0, 80, 0);
|
|
@@ -489,6 +490,7 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
489
490
|
const next = { ...original, ...layouts?.[name] };
|
|
490
491
|
layout = name;
|
|
491
492
|
({ models, fit, target, platform, lightPosition, shadowCamera, clippingMargin } = next);
|
|
493
|
+
lightBounds = undefined;
|
|
492
494
|
extras = next.extras || [];
|
|
493
495
|
models.forEach((model) => { scene.add(model.building); model.setWireframe(wireframe); });
|
|
494
496
|
extras.forEach((object) => scene.add(object));
|
|
@@ -501,6 +503,7 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
501
503
|
base.position.set(platform.x || 0, -1.1, platform.z || 0);
|
|
502
504
|
baseEdges.position.copy(base.position);
|
|
503
505
|
keyLight.position.fromArray(lightPosition);
|
|
506
|
+
keyLight.target.position.set(0, 80, 0);
|
|
504
507
|
Object.assign(keyLight.shadow.camera, shadowCamera);
|
|
505
508
|
keyLight.shadow.camera.updateProjectionMatrix();
|
|
506
509
|
const nextView = previousView && previousView !== defaultView ? previousView : next.defaultView || defaultView;
|
|
@@ -586,6 +589,8 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
586
589
|
get groundShadows() { return base.receiveShadow; },
|
|
587
590
|
get activeView() { return activeView; },
|
|
588
591
|
get selectedBuilding() { return selectedModel?.building.userData["buildingId"] || null; },
|
|
592
|
+
get sunDirection() { return keyLight.position.clone().sub(keyLight.target.position).normalize().toArray(); },
|
|
593
|
+
get sunIntensity() { return keyLight.intensity; },
|
|
589
594
|
get shadowBounds() {
|
|
590
595
|
// Actual rendered geometry in the shadow camera's clip space, for coverage checks.
|
|
591
596
|
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
@@ -667,7 +672,29 @@ export function createBuildingStudy({ models, defaultView = "quarter", views = {
|
|
|
667
672
|
Object.defineProperty(canvas, "__buildingStudy", { value: api, configurable: true });
|
|
668
673
|
onReady();
|
|
669
674
|
// Day or night: the transparent canvas shows the page's night behind it until a sky covers it.
|
|
675
|
+
// A sphere enclosing every caster and the platform fits the shadow camera at any
|
|
676
|
+
// solar azimuth/elevation. Cache it: dragging time never traverses model vertices.
|
|
670
677
|
function setDaylight(day) {
|
|
678
|
+
if (day?.direction) {
|
|
679
|
+
if (!lightBounds) {
|
|
680
|
+
const min = new THREE.Vector3(Infinity, Infinity, Infinity), max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);
|
|
681
|
+
for (const bounds of [api.platformBounds, ...api.modelBounds]) {
|
|
682
|
+
min.min(new THREE.Vector3().fromArray(bounds.min));
|
|
683
|
+
max.max(new THREE.Vector3().fromArray(bounds.max));
|
|
684
|
+
}
|
|
685
|
+
lightBounds = { center: min.clone().add(max).multiplyScalar(0.5), radius: max.distanceTo(min) / 2 };
|
|
686
|
+
}
|
|
687
|
+
const radius = lightBounds.radius * 1.02;
|
|
688
|
+
keyLight.target.position.copy(lightBounds.center);
|
|
689
|
+
keyLight.position.fromArray(day.direction).multiplyScalar(2 * radius + 1).add(lightBounds.center);
|
|
690
|
+
Object.assign(keyLight.shadow.camera, { left: -radius, right: radius, top: radius, bottom: -radius, near: 1, far: 4 * radius + 2 });
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
keyLight.position.fromArray(lightPosition);
|
|
694
|
+
keyLight.target.position.set(0, 80, 0);
|
|
695
|
+
Object.assign(keyLight.shadow.camera, shadowCamera);
|
|
696
|
+
}
|
|
697
|
+
keyLight.shadow.camera.updateProjectionMatrix();
|
|
671
698
|
scene.background = day?.sky ?? null;
|
|
672
699
|
base.material.color.setHex(day?.ground ?? nightGround);
|
|
673
700
|
for (const [light, [colour, intensity]] of [[keyLight, day?.sun ?? night.sun], [ambientLight, day?.ambient ?? night.ambient], [fillLight, day?.fill ?? night.fill]]) {
|
package/site/study.css
CHANGED
|
@@ -162,6 +162,12 @@ body.skyline-3d { height: 100vh; height: 100dvh; }
|
|
|
162
162
|
.control-bar .button-group { gap: 4px; }
|
|
163
163
|
.control-bar .camera-views { grid-area: views; justify-self: start; --fold: inset(0 0 0 100%); }
|
|
164
164
|
.control-bar .model-display { grid-area: display; justify-self: end; justify-content: end; --fold: inset(0 100% 0 0); }
|
|
165
|
+
.time-controls { display: grid; grid-template-columns: auto minmax(60px, 1fr) auto auto; align-items: center; gap: 8px; flex-basis: 100%; width: 100%; font-size: 10px; }
|
|
166
|
+
.time-controls label { color: #bbb; }
|
|
167
|
+
.time-controls #time-date { display: block; margin-top: 2px; color: #888; font-size: 9px; }
|
|
168
|
+
.time-controls input { width: 100%; min-width: 0; height: 28px; margin: 0; accent-color: #bbb; cursor: pointer; }
|
|
169
|
+
.time-controls output { min-width: 88px; text-align: right; color: #ddd; font-variant-numeric: tabular-nums; }
|
|
170
|
+
.time-controls button:disabled { opacity: 0.45; cursor: default; }
|
|
165
171
|
.celebration-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: end; gap: 4px; flex-basis: 100%; }
|
|
166
172
|
.celebration-caption { margin-right: 4px; color: #888; font-size: 10px; }
|
|
167
173
|
.control-bar .celebration-controls button { display: grid; place-items: center; width: 36px; height: 36px; padding: 4px; }
|
|
@@ -179,8 +185,8 @@ body.skyline-3d { height: 100vh; height: 100dvh; }
|
|
|
179
185
|
.control-bar .menu-toggle:hover { color: #fff; background: #ffffff14; }
|
|
180
186
|
.menu-toggle svg { width: 20px; height: 20px; fill: currentColor; transition: rotate 0.35s cubic-bezier(0.2, 0.7, 0.2, 1); }
|
|
181
187
|
.menu-toggle[aria-expanded="true"] svg { rotate: 30deg; }
|
|
182
|
-
/*
|
|
183
|
-
|
|
188
|
+
/* Compact badges leave room for the time slider. Smaller screens retain 36px
|
|
189
|
+
touch targets. */
|
|
184
190
|
@container skyline (min-width: 1440px) {
|
|
185
191
|
.celebration-controls { flex-basis: auto; flex-wrap: nowrap; }
|
|
186
192
|
.celebration-caption { display: none; }
|
|
@@ -203,6 +209,12 @@ body.skyline-3d { height: 100vh; height: 100dvh; }
|
|
|
203
209
|
.skyline-trace-tip { display: none; animation: none; }
|
|
204
210
|
}
|
|
205
211
|
@container skyline (max-width: 600px) {
|
|
212
|
+
.time-controls { grid-template-columns: minmax(0, 1fr) auto; gap: 2px 8px; }
|
|
213
|
+
.time-controls label { grid-column: 1; grid-row: 1; }
|
|
214
|
+
.time-controls #time-date { display: inline; margin-left: 8px; }
|
|
215
|
+
.time-controls output { grid-column: 2; grid-row: 1; }
|
|
216
|
+
.time-controls input { grid-column: 1; grid-row: 2; height: 32px; }
|
|
217
|
+
.time-controls button { grid-column: 2; grid-row: 2; }
|
|
206
218
|
.skyline-trace { stroke-width: 8.4; }
|
|
207
219
|
.skyline-trace-tip { stroke-width: 12; }
|
|
208
220
|
.skyline-3d .hint { font-size: 9px; }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as THREE from "./vendor/three-r186.js";
|
|
2
|
+
import { daylightGround, daylightIntensities } from "./models/colour-materials.js";
|
|
3
|
+
import { nightColours, timeOfDayColours } from "./models/colour-palette.js";
|
|
4
|
+
import { smooth, windowActivity } from "./models/night-windows.js";
|
|
5
|
+
import { chicagoHour, solarPosition } from "./solar-time.js";
|
|
6
|
+
// One tiny texture per viewer, updated in place while dragging and disposed at teardown.
|
|
7
|
+
// Transparent night sky reveals the viewer's existing stars behind opaque buildings.
|
|
8
|
+
export function createTimeLighting(latitude, longitude, transparentNight) {
|
|
9
|
+
const rows = 256, data = new Uint8Array(rows * 4);
|
|
10
|
+
const sky = new THREE.DataTexture(data, 1, rows);
|
|
11
|
+
sky.colorSpace = "srgb";
|
|
12
|
+
const a = new THREE.Color(), b = new THREE.Color(), pixel = new THREE.Color();
|
|
13
|
+
function blend(from, to, amount, coloured) {
|
|
14
|
+
a.setHex(from).lerp(b.setHex(to), amount);
|
|
15
|
+
if (!coloured) {
|
|
16
|
+
const grey = a.r * 0.2126 + a.g * 0.7152 + a.b * 0.0722;
|
|
17
|
+
a.setRGB(grey, grey, grey);
|
|
18
|
+
}
|
|
19
|
+
return a.getHex();
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
sky,
|
|
23
|
+
update(date, coloured) {
|
|
24
|
+
const { elevation, direction } = solarPosition(date, latitude, longitude);
|
|
25
|
+
const upper = timeOfDayColours.findIndex((stop) => stop.elevation > elevation);
|
|
26
|
+
const last = timeOfDayColours.length - 1;
|
|
27
|
+
const low = timeOfDayColours[upper < 0 ? last : Math.max(0, upper - 1)];
|
|
28
|
+
const high = timeOfDayColours[upper < 0 ? last : upper];
|
|
29
|
+
const amount = high === low ? 0 : smooth(low.elevation, high.elevation, elevation);
|
|
30
|
+
const top = blend(low.sky, high.sky, amount, coloured);
|
|
31
|
+
const horizon = blend(low.horizon, high.horizon, amount, coloured);
|
|
32
|
+
const sun = blend(low.sun, high.sun, amount, coloured);
|
|
33
|
+
const day = smooth(-6, 18, elevation);
|
|
34
|
+
const opacity = transparentNight ? smooth(-18, -2, elevation) : 1;
|
|
35
|
+
const topColour = new THREE.Color(top), bottomColour = new THREE.Color(horizon);
|
|
36
|
+
for (let row = 0; row < rows; row += 1) {
|
|
37
|
+
const hex = pixel.copy(bottomColour).lerp(topColour, row / (rows - 1)).getHex();
|
|
38
|
+
// The canvas is composited with premultiplied alpha. Background textures write
|
|
39
|
+
// directly to it, so premultiply the final sRGB channels here as well.
|
|
40
|
+
data.set([Math.round(((hex >> 16) & 255) * opacity), Math.round(((hex >> 8) & 255) * opacity), Math.round((hex & 255) * opacity), Math.round(255 * opacity)], row * 4);
|
|
41
|
+
}
|
|
42
|
+
sky.needsUpdate = true;
|
|
43
|
+
const ground = blend(0x161616, daylightGround.blocks, day, false);
|
|
44
|
+
return {
|
|
45
|
+
sky, direction, ground,
|
|
46
|
+
roads: blend(0x2c2c2c, daylightGround.roads, day, false),
|
|
47
|
+
sun: [sun, daylightIntensities.sun * smooth(-0.833, 8, elevation)],
|
|
48
|
+
ambient: [blend(nightColours.ambient, top, day, coloured), 0.16 + day * (daylightIntensities.ambient - 0.16)],
|
|
49
|
+
fill: [blend(nightColours.fill, horizon, day, coloured), 0.08 + day * (daylightIntensities.fill - 0.08)],
|
|
50
|
+
reflections: { sky: top, horizon, ground: blend(0x080808, 0x444444, day, false) },
|
|
51
|
+
night: { darkness: 1 - smooth(-6, 3, elevation), activity: windowActivity(chicagoHour(date)), outlineStrength: 0.05 + 0.95 * day },
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
dispose() { sky.dispose(); },
|
|
55
|
+
};
|
|
56
|
+
}
|