@echomem/mcp 1.4.8 → 1.4.9
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/README.md +23 -3
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1104 -105
- package/dist/city/echo-ai-city-only.template.html +1104 -105
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +135 -16
- package/dist/hud/web.js +344 -298
- package/dist/index.js +7 -3
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +37 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +3290 -196
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +432 -34
- package/package.json +5 -4
- package/templates/echomem-recall.md +2 -2
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Circular Context City — Pencil WebGL Study</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--paper: #f3efe5;
|
|
10
|
+
--ink: #30312d;
|
|
11
|
+
--muted: #76736b;
|
|
12
|
+
--codex: #343541;
|
|
13
|
+
--claude: #d97757;
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
17
|
+
body {
|
|
18
|
+
background:
|
|
19
|
+
radial-gradient(circle at 48% 42%, rgba(255,255,255,.98) 0 25%, rgba(249,247,241,.92) 58%, rgba(235,230,218,.82) 100%),
|
|
20
|
+
var(--paper);
|
|
21
|
+
color: var(--ink);
|
|
22
|
+
font-family: "IBM Plex Mono", "SFMono-Regular", ui-monospace, Menlo, monospace;
|
|
23
|
+
}
|
|
24
|
+
body::after {
|
|
25
|
+
content: "";
|
|
26
|
+
position: fixed;
|
|
27
|
+
inset: 0;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
opacity: .22;
|
|
30
|
+
mix-blend-mode: multiply;
|
|
31
|
+
background-image:
|
|
32
|
+
repeating-linear-gradient(8deg, transparent 0 4px, rgba(64,60,50,.016) 4px 5px),
|
|
33
|
+
repeating-linear-gradient(98deg, transparent 0 9px, rgba(255,255,255,.16) 9px 10px);
|
|
34
|
+
}
|
|
35
|
+
#stage { position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: grab; }
|
|
36
|
+
#stage:active { cursor: grabbing; }
|
|
37
|
+
.summary {
|
|
38
|
+
position: fixed;
|
|
39
|
+
left: clamp(20px, 4vw, 54px);
|
|
40
|
+
top: clamp(20px, 4vw, 48px);
|
|
41
|
+
z-index: 3;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
.eyebrow { font-size: 10px; letter-spacing: .18em; color: var(--muted); text-transform: uppercase; }
|
|
45
|
+
.metric { margin-top: 8px; font: 500 clamp(38px, 5vw, 74px)/.9 Georgia, "Times New Roman", serif; letter-spacing: -.04em; }
|
|
46
|
+
.metric span { font: 500 12px/1.2 "IBM Plex Mono", ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
|
|
47
|
+
.caption { max-width: 360px; margin-top: 12px; font-size: 11px; line-height: 1.55; color: var(--muted); }
|
|
48
|
+
.legend { display: flex; gap: 16px; margin-top: 13px; font-size: 10px; color: var(--muted); }
|
|
49
|
+
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
50
|
+
.legend i { width: 9px; height: 9px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(48,49,45,.14); }
|
|
51
|
+
.legend .codex { background: var(--codex); }
|
|
52
|
+
.legend .claude { background: var(--claude); }
|
|
53
|
+
.actions {
|
|
54
|
+
position: fixed;
|
|
55
|
+
right: clamp(18px, 3vw, 42px);
|
|
56
|
+
bottom: clamp(18px, 3vw, 36px);
|
|
57
|
+
z-index: 4;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 12px;
|
|
61
|
+
}
|
|
62
|
+
.hint { font-size: 10px; color: var(--muted); }
|
|
63
|
+
button {
|
|
64
|
+
border: 1px solid rgba(48,49,45,.26);
|
|
65
|
+
background: rgba(248,245,237,.84);
|
|
66
|
+
color: var(--ink);
|
|
67
|
+
padding: 8px 12px;
|
|
68
|
+
font: 500 10px/1 "IBM Plex Mono", ui-monospace, monospace;
|
|
69
|
+
letter-spacing: .08em;
|
|
70
|
+
text-transform: uppercase;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
backdrop-filter: blur(8px);
|
|
73
|
+
}
|
|
74
|
+
button[aria-pressed="true"] { color: #f6f1e6; background: var(--codex); border-color: var(--codex); }
|
|
75
|
+
@media (max-width: 640px) {
|
|
76
|
+
.caption, .hint { display: none; }
|
|
77
|
+
.summary { top: 18px; left: 18px; }
|
|
78
|
+
.actions { right: 16px; bottom: 16px; }
|
|
79
|
+
}
|
|
80
|
+
</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<canvas id="stage" aria-label="Interactive hand-drawn WebGL city showing repository context morphing from flat graphite pucks into tall frosted-glass columns on a thin paper platform"></canvas>
|
|
84
|
+
<div class="summary" aria-live="polite">
|
|
85
|
+
<div class="eyebrow">Context scan · repository city</div>
|
|
86
|
+
<div class="metric"><b id="dirty-value">82%</b> <span id="dirty-label">dirty context</span></div>
|
|
87
|
+
<div class="caption" id="summary-caption">Dirty context spreads low and wide, carrying graphite, dust and repeated reads.</div>
|
|
88
|
+
<div class="legend" aria-label="Coding agent colors">
|
|
89
|
+
<span><i class="codex"></i>Codex</span>
|
|
90
|
+
<span><i class="claude"></i>Claude Code</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="actions">
|
|
94
|
+
<span class="hint">Drag to orbit · wheel to zoom</span>
|
|
95
|
+
<button id="dirty" type="button" aria-pressed="true">Dirty</button>
|
|
96
|
+
<button id="repair" type="button" aria-pressed="false">Repair city</button>
|
|
97
|
+
<button id="authentic" type="button" aria-pressed="false">Authentic version</button>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script type="importmap">
|
|
101
|
+
{ "imports": { "three": "./vendor/three.module.min.js" } }
|
|
102
|
+
</script>
|
|
103
|
+
<script type="module">
|
|
104
|
+
import * as THREE from "three";
|
|
105
|
+
import { OrbitControls } from "./vendor/OrbitControls.js";
|
|
106
|
+
import { RoundedBoxGeometry } from "./vendor/RoundedBoxGeometry.js";
|
|
107
|
+
|
|
108
|
+
const canvas = document.getElementById("stage");
|
|
109
|
+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
110
|
+
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
111
|
+
renderer.setClearColor(0xf3efe5, 0);
|
|
112
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
113
|
+
renderer.shadowMap.enabled = true;
|
|
114
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
115
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
116
|
+
renderer.toneMappingExposure = 1.05;
|
|
117
|
+
|
|
118
|
+
const scene = new THREE.Scene();
|
|
119
|
+
|
|
120
|
+
// Soft studio lighting from the earlier clean city: the frosted colour lives
|
|
121
|
+
// inside the glass volume instead of reading as a flat transparent surface.
|
|
122
|
+
function buildStudioEnvironment() {
|
|
123
|
+
const cv = document.createElement("canvas");
|
|
124
|
+
cv.width = 512;
|
|
125
|
+
cv.height = 256;
|
|
126
|
+
const g = cv.getContext("2d");
|
|
127
|
+
const gradient = g.createLinearGradient(0, 0, 0, 256);
|
|
128
|
+
gradient.addColorStop(0, "#ffffff");
|
|
129
|
+
gradient.addColorStop(.5, "#eef1f6");
|
|
130
|
+
gradient.addColorStop(1, "#d2d7e0");
|
|
131
|
+
g.fillStyle = gradient;
|
|
132
|
+
g.fillRect(0, 0, 512, 256);
|
|
133
|
+
const spot = g.createRadialGradient(150, 62, 8, 150, 62, 150);
|
|
134
|
+
spot.addColorStop(0, "rgba(255,255,255,.95)");
|
|
135
|
+
spot.addColorStop(1, "rgba(255,255,255,0)");
|
|
136
|
+
g.fillStyle = spot;
|
|
137
|
+
g.fillRect(0, 0, 512, 256);
|
|
138
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
139
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
140
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
141
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
142
|
+
pmrem.compileEquirectangularShader();
|
|
143
|
+
const environment = pmrem.fromEquirectangular(texture).texture;
|
|
144
|
+
texture.dispose();
|
|
145
|
+
pmrem.dispose();
|
|
146
|
+
return environment;
|
|
147
|
+
}
|
|
148
|
+
scene.environment = buildStudioEnvironment();
|
|
149
|
+
const camera = new THREE.OrthographicCamera(-16, 16, 12, -12, .1, 180);
|
|
150
|
+
camera.position.set(22, 18, 25);
|
|
151
|
+
camera.lookAt(0, 2.3, 0);
|
|
152
|
+
|
|
153
|
+
const controls = new OrbitControls(camera, canvas);
|
|
154
|
+
controls.enableDamping = true;
|
|
155
|
+
controls.dampingFactor = .075;
|
|
156
|
+
controls.enablePan = false;
|
|
157
|
+
controls.minZoom = .72;
|
|
158
|
+
controls.maxZoom = 2.3;
|
|
159
|
+
controls.target.set(0, 2.4, 0);
|
|
160
|
+
|
|
161
|
+
const hemisphere = new THREE.HemisphereLight(0xfffdf7, 0xb8b2a6, .88);
|
|
162
|
+
scene.add(hemisphere);
|
|
163
|
+
const ambient = new THREE.AmbientLight(0xffffff, .3);
|
|
164
|
+
scene.add(ambient);
|
|
165
|
+
const key = new THREE.DirectionalLight(0xfffdf7, 1.28);
|
|
166
|
+
key.position.set(-12, 24, 16);
|
|
167
|
+
key.castShadow = true;
|
|
168
|
+
key.shadow.mapSize.set(2048, 2048);
|
|
169
|
+
key.shadow.camera.left = -22;
|
|
170
|
+
key.shadow.camera.right = 22;
|
|
171
|
+
key.shadow.camera.top = 22;
|
|
172
|
+
key.shadow.camera.bottom = -22;
|
|
173
|
+
key.shadow.camera.near = 1;
|
|
174
|
+
key.shadow.camera.far = 70;
|
|
175
|
+
key.shadow.radius = 7;
|
|
176
|
+
key.shadow.bias = -.00035;
|
|
177
|
+
scene.add(key);
|
|
178
|
+
const fill = new THREE.DirectionalLight(0xffffff, 0);
|
|
179
|
+
fill.position.set(12, 10, 9);
|
|
180
|
+
scene.add(fill);
|
|
181
|
+
|
|
182
|
+
const floor = new THREE.Mesh(
|
|
183
|
+
new THREE.PlaneGeometry(160, 160),
|
|
184
|
+
new THREE.ShadowMaterial({ color: 0x3b3a35, opacity: .085 })
|
|
185
|
+
);
|
|
186
|
+
floor.rotation.x = -Math.PI / 2;
|
|
187
|
+
floor.position.y = -.67;
|
|
188
|
+
floor.receiveShadow = true;
|
|
189
|
+
scene.add(floor);
|
|
190
|
+
|
|
191
|
+
function randomFactory(seed) {
|
|
192
|
+
let value = seed >>> 0;
|
|
193
|
+
return () => {
|
|
194
|
+
value = (Math.imul(value, 1664525) + 1013904223) >>> 0;
|
|
195
|
+
return value / 4294967296;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function paperTexture(seed = 1, base = "#f2eee4", colored = false, wash = .22) {
|
|
200
|
+
const size = 256;
|
|
201
|
+
const cv = document.createElement("canvas");
|
|
202
|
+
cv.width = cv.height = size;
|
|
203
|
+
const g = cv.getContext("2d");
|
|
204
|
+
const random = randomFactory(seed);
|
|
205
|
+
g.fillStyle = base;
|
|
206
|
+
g.fillRect(0, 0, size, size);
|
|
207
|
+
const image = g.getImageData(0, 0, size, size);
|
|
208
|
+
const data = image.data;
|
|
209
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
210
|
+
const grain = Math.round((random() - .5) * (colored ? 13 : 18));
|
|
211
|
+
data[i] = Math.max(0, Math.min(255, data[i] + grain));
|
|
212
|
+
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + grain));
|
|
213
|
+
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + grain));
|
|
214
|
+
}
|
|
215
|
+
g.putImageData(image, 0, 0);
|
|
216
|
+
if (colored) {
|
|
217
|
+
// Colored pencil never fully seals the paper: lift the pigment and leave tooth showing.
|
|
218
|
+
g.fillStyle = `rgba(243,239,229,${wash})`;
|
|
219
|
+
g.fillRect(0, 0, size, size);
|
|
220
|
+
g.lineCap = "round";
|
|
221
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
222
|
+
g.beginPath();
|
|
223
|
+
for (let i = 0; i < 90; i++) {
|
|
224
|
+
const x = random() * size;
|
|
225
|
+
const y = random() * size;
|
|
226
|
+
const length = 8 + random() * 34;
|
|
227
|
+
const angle = (pass === 0 ? -.13 : .18) + (random() - .5) * .08;
|
|
228
|
+
g.moveTo(x, y);
|
|
229
|
+
g.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length);
|
|
230
|
+
}
|
|
231
|
+
g.strokeStyle = pass === 0 ? "rgba(34,35,31,.055)" : "rgba(255,247,230,.075)";
|
|
232
|
+
g.lineWidth = pass === 0 ? .8 : .65;
|
|
233
|
+
g.stroke();
|
|
234
|
+
}
|
|
235
|
+
g.beginPath();
|
|
236
|
+
for (let i = 0; i < 340; i++) {
|
|
237
|
+
const r = .35 + random() * 1.15;
|
|
238
|
+
g.rect(random() * size, random() * size, r, r * (.35 + random() * .5));
|
|
239
|
+
}
|
|
240
|
+
g.fillStyle = "rgba(246,241,229,.11)";
|
|
241
|
+
g.fill();
|
|
242
|
+
}
|
|
243
|
+
g.strokeStyle = colored ? "rgba(255,249,235,.11)" : "rgba(57,54,46,.04)";
|
|
244
|
+
g.lineWidth = colored ? .72 : .55;
|
|
245
|
+
g.beginPath();
|
|
246
|
+
for (let i = 0; i < 120; i++) {
|
|
247
|
+
const x = random() * size;
|
|
248
|
+
const y = random() * size;
|
|
249
|
+
g.moveTo(x, y);
|
|
250
|
+
g.lineTo(x + 5 + random() * 18, y + (random() - .5) * 2);
|
|
251
|
+
}
|
|
252
|
+
g.stroke();
|
|
253
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
254
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
255
|
+
texture.repeat.set(3, 3);
|
|
256
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
257
|
+
texture.anisotropy = 8;
|
|
258
|
+
return texture;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function frostTexture(seed = 1) {
|
|
262
|
+
const size = 256;
|
|
263
|
+
const cv = document.createElement("canvas");
|
|
264
|
+
cv.width = cv.height = size;
|
|
265
|
+
const g = cv.getContext("2d");
|
|
266
|
+
const random = randomFactory(seed);
|
|
267
|
+
const image = g.createImageData(size, size);
|
|
268
|
+
for (let i = 0; i < image.data.length; i += 4) {
|
|
269
|
+
const broad = Math.sin(i / 740) * 5;
|
|
270
|
+
const grain = 204 + broad + (random() - .5) * 42;
|
|
271
|
+
image.data[i] = grain;
|
|
272
|
+
image.data[i + 1] = grain;
|
|
273
|
+
image.data[i + 2] = grain;
|
|
274
|
+
image.data[i + 3] = 255;
|
|
275
|
+
}
|
|
276
|
+
g.putImageData(image, 0, 0);
|
|
277
|
+
g.globalAlpha = .08;
|
|
278
|
+
g.fillStyle = "#ffffff";
|
|
279
|
+
for (let i = 0; i < 900; i++) {
|
|
280
|
+
const r = .25 + random() * .7;
|
|
281
|
+
g.fillRect(random() * size, random() * size, r, r);
|
|
282
|
+
}
|
|
283
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
284
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
285
|
+
texture.repeat.set(4, 2.4);
|
|
286
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
287
|
+
texture.anisotropy = 8;
|
|
288
|
+
return texture;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function marbleTexture(seed = 1) {
|
|
292
|
+
const size = 512;
|
|
293
|
+
const cv = document.createElement("canvas");
|
|
294
|
+
cv.width = cv.height = size;
|
|
295
|
+
const g = cv.getContext("2d");
|
|
296
|
+
const random = randomFactory(seed);
|
|
297
|
+
g.fillStyle = "#fafaf8";
|
|
298
|
+
g.fillRect(0, 0, size, size);
|
|
299
|
+
|
|
300
|
+
// Sparse, low-contrast veins keep the base feeling white and architectural.
|
|
301
|
+
for (let i = 0; i < 8; i++) {
|
|
302
|
+
const startY = random() * size;
|
|
303
|
+
g.beginPath();
|
|
304
|
+
g.moveTo(-40, startY);
|
|
305
|
+
g.bezierCurveTo(
|
|
306
|
+
size * .22, startY - 90 + random() * 180,
|
|
307
|
+
size * .7, startY - 120 + random() * 240,
|
|
308
|
+
size + 40, startY - 70 + random() * 140
|
|
309
|
+
);
|
|
310
|
+
g.strokeStyle = i % 3 === 0 ? "rgba(154,164,166,.13)" : "rgba(190,184,177,.085)";
|
|
311
|
+
g.lineWidth = .45 + random() * 1.15;
|
|
312
|
+
g.stroke();
|
|
313
|
+
}
|
|
314
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
315
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
316
|
+
texture.repeat.set(1.45, 1.1);
|
|
317
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
318
|
+
texture.anisotropy = 8;
|
|
319
|
+
return texture;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const paperMap = paperTexture(90210);
|
|
323
|
+
const frostMap = frostTexture(90213);
|
|
324
|
+
const marbleMap = marbleTexture(90214);
|
|
325
|
+
const codexMap = paperTexture(90211, "#343541", true, .34);
|
|
326
|
+
const claudeMap = paperTexture(90212, "#d97757", true, .2);
|
|
327
|
+
const topMaterial = new THREE.MeshLambertMaterial({ color: 0xf1ede2, map: paperMap, flatShading: true });
|
|
328
|
+
const sideMaterial = new THREE.MeshLambertMaterial({ color: 0xc9c5bb, map: paperMap, flatShading: true });
|
|
329
|
+
const codexMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, map: codexMap, flatShading: true });
|
|
330
|
+
const claudeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, map: claudeMap, flatShading: true });
|
|
331
|
+
const codexTopMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, map: codexMap, side: THREE.DoubleSide, toneMapped: false, transparent: true, opacity: .64, depthWrite: false });
|
|
332
|
+
const claudeTopMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, map: claudeMap, side: THREE.DoubleSide, toneMapped: false, transparent: true, opacity: .64, depthWrite: false });
|
|
333
|
+
const chunkMaterial = new THREE.MeshLambertMaterial({ color: 0x595a57, map: paperMap, flatShading: true });
|
|
334
|
+
const inkMaterial = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .54 });
|
|
335
|
+
const faintInkMaterial = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .16 });
|
|
336
|
+
|
|
337
|
+
const hatchMaterials = [];
|
|
338
|
+
const dirtyPieces = [];
|
|
339
|
+
const dirtySlices = [];
|
|
340
|
+
const shadowMaterials = [];
|
|
341
|
+
const dirtyShellMaterials = [];
|
|
342
|
+
const glassMaterials = [];
|
|
343
|
+
const topLineMaterials = [];
|
|
344
|
+
const cleanGlassLineMaterials = [];
|
|
345
|
+
const platformDirtyMaterials = [];
|
|
346
|
+
const cleanPlatformMaterials = [];
|
|
347
|
+
const authenticGlassMaterials = [];
|
|
348
|
+
const authenticPlatformMaterials = [];
|
|
349
|
+
const repoLabels = [];
|
|
350
|
+
|
|
351
|
+
function outline(mesh, opacity = .56, threshold = 24) {
|
|
352
|
+
const geometry = new THREE.EdgesGeometry(mesh.geometry, threshold);
|
|
353
|
+
const main = new THREE.LineSegments(
|
|
354
|
+
geometry,
|
|
355
|
+
new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity })
|
|
356
|
+
);
|
|
357
|
+
main.renderOrder = 5;
|
|
358
|
+
mesh.add(main);
|
|
359
|
+
const echoGeometry = geometry.clone();
|
|
360
|
+
const echoPosition = echoGeometry.attributes.position;
|
|
361
|
+
const random = randomFactory(9000 + mesh.id * 17);
|
|
362
|
+
for (let i = 0; i < echoPosition.count; i++) {
|
|
363
|
+
echoPosition.setXYZ(
|
|
364
|
+
i,
|
|
365
|
+
echoPosition.getX(i) + (random() - .5) * .032,
|
|
366
|
+
echoPosition.getY(i) + (random() - .5) * .024,
|
|
367
|
+
echoPosition.getZ(i) + (random() - .5) * .032
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const echo = new THREE.LineSegments(
|
|
371
|
+
echoGeometry,
|
|
372
|
+
new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity: .12 })
|
|
373
|
+
);
|
|
374
|
+
echo.position.set(.018, .012, -.014);
|
|
375
|
+
echo.renderOrder = 4;
|
|
376
|
+
mesh.add(echo);
|
|
377
|
+
return [main.material, echo.material];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const PLATFORM = { width: 27, depth: 20.5, thickness: .75, top: .22 };
|
|
381
|
+
const bodyPaperMaterial = topMaterial.clone();
|
|
382
|
+
bodyPaperMaterial.transparent = true;
|
|
383
|
+
bodyPaperMaterial.userData.dirtyOpacity = 1;
|
|
384
|
+
const body = new THREE.Mesh(
|
|
385
|
+
new RoundedBoxGeometry(PLATFORM.width, PLATFORM.thickness, PLATFORM.depth, 10, .82),
|
|
386
|
+
bodyPaperMaterial
|
|
387
|
+
);
|
|
388
|
+
body.position.y = PLATFORM.top - PLATFORM.thickness / 2;
|
|
389
|
+
body.castShadow = true;
|
|
390
|
+
body.receiveShadow = true;
|
|
391
|
+
scene.add(body);
|
|
392
|
+
platformDirtyMaterials.push(bodyPaperMaterial, ...outline(body, .42, 18));
|
|
393
|
+
|
|
394
|
+
for (const material of platformDirtyMaterials) {
|
|
395
|
+
if (material.userData.dirtyOpacity === undefined) material.userData.dirtyOpacity = material.opacity;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function makeMarbleMaterial() {
|
|
399
|
+
const material = new THREE.MeshPhysicalMaterial({
|
|
400
|
+
color: 0xffffff,
|
|
401
|
+
map: marbleMap,
|
|
402
|
+
roughness: .3,
|
|
403
|
+
metalness: 0,
|
|
404
|
+
clearcoat: .62,
|
|
405
|
+
clearcoatRoughness: .22,
|
|
406
|
+
transparent: true,
|
|
407
|
+
opacity: 0,
|
|
408
|
+
depthWrite: false
|
|
409
|
+
});
|
|
410
|
+
cleanPlatformMaterials.push(material);
|
|
411
|
+
return material;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const marbleBody = new THREE.Mesh(body.geometry.clone(), makeMarbleMaterial());
|
|
415
|
+
marbleBody.position.copy(body.position);
|
|
416
|
+
marbleBody.scale.set(1.002, 1.006, 1.002);
|
|
417
|
+
marbleBody.receiveShadow = true;
|
|
418
|
+
marbleBody.renderOrder = 2;
|
|
419
|
+
scene.add(marbleBody);
|
|
420
|
+
|
|
421
|
+
// Authentic setup-page tray, restored from main@07b9638. The historical city
|
|
422
|
+
// used one ivory clay slab with fine bump grain and a clipped grid decal.
|
|
423
|
+
const AUTHENTIC_TRAY = { width: 27, depth: 27, thickness: 1.35, top: .68 };
|
|
424
|
+
function authenticGrain() {
|
|
425
|
+
const size = 256;
|
|
426
|
+
const cv = document.createElement("canvas");
|
|
427
|
+
cv.width = cv.height = size;
|
|
428
|
+
const g = cv.getContext("2d");
|
|
429
|
+
const random = randomFactory(71621);
|
|
430
|
+
const image = g.createImageData(size, size);
|
|
431
|
+
for (let i = 0; i < image.data.length; i += 4) {
|
|
432
|
+
const n = 128 + (random() * 2 - 1) * 38;
|
|
433
|
+
image.data[i] = image.data[i + 1] = image.data[i + 2] = n;
|
|
434
|
+
image.data[i + 3] = 255;
|
|
435
|
+
}
|
|
436
|
+
g.putImageData(image, 0, 0);
|
|
437
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
438
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
439
|
+
texture.anisotropy = 8;
|
|
440
|
+
texture.generateMipmaps = false;
|
|
441
|
+
texture.minFilter = THREE.LinearFilter;
|
|
442
|
+
texture.repeat.set(AUTHENTIC_TRAY.width / 3.8, AUTHENTIC_TRAY.depth / 3.8);
|
|
443
|
+
return texture;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const authenticTrayMaterial = new THREE.MeshPhysicalMaterial({
|
|
447
|
+
color: 0xece5d3,
|
|
448
|
+
roughness: .6,
|
|
449
|
+
metalness: 0,
|
|
450
|
+
clearcoat: .3,
|
|
451
|
+
clearcoatRoughness: .5,
|
|
452
|
+
envMapIntensity: .6,
|
|
453
|
+
bumpMap: authenticGrain(),
|
|
454
|
+
bumpScale: .022,
|
|
455
|
+
transparent: true,
|
|
456
|
+
opacity: 0,
|
|
457
|
+
depthWrite: false
|
|
458
|
+
});
|
|
459
|
+
authenticPlatformMaterials.push(authenticTrayMaterial);
|
|
460
|
+
const authenticTray = new THREE.Mesh(
|
|
461
|
+
new RoundedBoxGeometry(AUTHENTIC_TRAY.width, AUTHENTIC_TRAY.thickness, AUTHENTIC_TRAY.depth, 8, .65),
|
|
462
|
+
authenticTrayMaterial
|
|
463
|
+
);
|
|
464
|
+
authenticTray.position.y = AUTHENTIC_TRAY.top - AUTHENTIC_TRAY.thickness / 2;
|
|
465
|
+
authenticTray.castShadow = true;
|
|
466
|
+
authenticTray.receiveShadow = true;
|
|
467
|
+
authenticTray.renderOrder = 2;
|
|
468
|
+
scene.add(authenticTray);
|
|
469
|
+
|
|
470
|
+
function authenticGridTexture() {
|
|
471
|
+
const size = 768;
|
|
472
|
+
const cv = document.createElement("canvas");
|
|
473
|
+
cv.width = cv.height = size;
|
|
474
|
+
const g = cv.getContext("2d");
|
|
475
|
+
const margin = 32;
|
|
476
|
+
const radius = 58;
|
|
477
|
+
const span = size - margin * 2;
|
|
478
|
+
g.save();
|
|
479
|
+
g.beginPath();
|
|
480
|
+
g.roundRect(margin, margin, span, span, radius);
|
|
481
|
+
g.clip();
|
|
482
|
+
const cell = 54;
|
|
483
|
+
g.strokeStyle = "rgba(74,80,96,.45)";
|
|
484
|
+
g.lineWidth = 1.8;
|
|
485
|
+
g.beginPath();
|
|
486
|
+
for (let p = margin; p <= margin + span + .5; p += cell) {
|
|
487
|
+
g.moveTo(p, margin); g.lineTo(p, margin + span);
|
|
488
|
+
g.moveTo(margin, p); g.lineTo(margin + span, p);
|
|
489
|
+
}
|
|
490
|
+
g.stroke();
|
|
491
|
+
g.restore();
|
|
492
|
+
g.strokeStyle = "rgba(120,126,140,.24)";
|
|
493
|
+
g.lineWidth = 2.1;
|
|
494
|
+
g.beginPath();
|
|
495
|
+
g.roundRect(margin, margin, span, span, radius);
|
|
496
|
+
g.stroke();
|
|
497
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
498
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
499
|
+
texture.anisotropy = 8;
|
|
500
|
+
return texture;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const authenticGridMaterial = new THREE.MeshBasicMaterial({
|
|
504
|
+
map: authenticGridTexture(),
|
|
505
|
+
transparent: true,
|
|
506
|
+
opacity: 0,
|
|
507
|
+
depthWrite: false,
|
|
508
|
+
toneMapped: false
|
|
509
|
+
});
|
|
510
|
+
authenticPlatformMaterials.push(authenticGridMaterial);
|
|
511
|
+
const authenticGrid = new THREE.Mesh(
|
|
512
|
+
new THREE.PlaneGeometry(AUTHENTIC_TRAY.width, AUTHENTIC_TRAY.depth),
|
|
513
|
+
authenticGridMaterial
|
|
514
|
+
);
|
|
515
|
+
authenticGrid.rotation.x = -Math.PI / 2;
|
|
516
|
+
authenticGrid.position.y = AUTHENTIC_TRAY.top + .012;
|
|
517
|
+
authenticGrid.renderOrder = 6;
|
|
518
|
+
scene.add(authenticGrid);
|
|
519
|
+
|
|
520
|
+
function addPlatformGrid() {
|
|
521
|
+
const vertices = [];
|
|
522
|
+
const y = PLATFORM.top + .028;
|
|
523
|
+
const insetX = PLATFORM.width / 2 - 1.05;
|
|
524
|
+
const insetZ = PLATFORM.depth / 2 - 1.05;
|
|
525
|
+
const step = 1.7;
|
|
526
|
+
for (let x = -insetX; x <= insetX; x += step) vertices.push(x, y, -insetZ, x, y, insetZ);
|
|
527
|
+
for (let z = -insetZ; z <= insetZ; z += step) vertices.push(-insetX, y, z, insetX, y, z);
|
|
528
|
+
const geometry = new THREE.BufferGeometry();
|
|
529
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
530
|
+
const material = new THREE.LineBasicMaterial({ color: 0x50514c, transparent: true, opacity: .105 });
|
|
531
|
+
material.userData.dirtyOpacity = .105;
|
|
532
|
+
scene.add(new THREE.LineSegments(geometry, material));
|
|
533
|
+
platformDirtyMaterials.push(material);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function addPlatformSideGrain() {
|
|
537
|
+
const random = randomFactory(5519);
|
|
538
|
+
const vertices = [];
|
|
539
|
+
const frontZ = PLATFORM.depth / 2 + .004;
|
|
540
|
+
const bottom = -PLATFORM.thickness + .14;
|
|
541
|
+
const top = -.05;
|
|
542
|
+
for (let x = -PLATFORM.width / 2 + .9; x < PLATFORM.width / 2 - .9; x += .52) {
|
|
543
|
+
if (random() < .2) continue;
|
|
544
|
+
const jitter = (random() - .5) * .1;
|
|
545
|
+
vertices.push(x + jitter, bottom + random() * .18, frontZ, x + jitter + (random() - .5) * .12, top - random() * .22, frontZ);
|
|
546
|
+
}
|
|
547
|
+
const rightX = PLATFORM.width / 2 + .004;
|
|
548
|
+
for (let z = -PLATFORM.depth / 2 + .9; z < PLATFORM.depth / 2 - .9; z += .55) {
|
|
549
|
+
if (random() < .24) continue;
|
|
550
|
+
const jitter = (random() - .5) * .1;
|
|
551
|
+
vertices.push(rightX, bottom + random() * .2, z + jitter, rightX, top - random() * .2, z + jitter + (random() - .5) * .12);
|
|
552
|
+
}
|
|
553
|
+
// Sparse diagonal passes keep the wall hand-shaded without turning it into a striped field.
|
|
554
|
+
for (let x = -PLATFORM.width / 2 + 1.2; x < PLATFORM.width / 2 - 1.2; x += 2.3) {
|
|
555
|
+
const startY = bottom + .18 + random() * .45;
|
|
556
|
+
vertices.push(
|
|
557
|
+
x, startY, frontZ + .002,
|
|
558
|
+
Math.min(PLATFORM.width / 2 - .7, x + .8 + random() * .5),
|
|
559
|
+
Math.min(top - .08, startY + .35),
|
|
560
|
+
frontZ + .002
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
const geometry = new THREE.BufferGeometry();
|
|
564
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
565
|
+
const material = new THREE.LineBasicMaterial({ color: 0x373833, transparent: true, opacity: .19 });
|
|
566
|
+
material.userData.dirtyOpacity = .19;
|
|
567
|
+
scene.add(new THREE.LineSegments(geometry, material));
|
|
568
|
+
platformDirtyMaterials.push(material);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
addPlatformGrid();
|
|
572
|
+
addPlatformSideGrain();
|
|
573
|
+
|
|
574
|
+
const repos = [
|
|
575
|
+
{ name: "WebPageReactVersion", hours: 92.8, x: -5.3, z: -2.1, w: 4.2, d: 4.0, h: 8.5, slices: 5, codexPct: 31, claudePct: 69, label: true },
|
|
576
|
+
{ name: "EchoMem-Chrome", hours: 70.1, x: 2.2, z: -2.5, w: 3.9, d: 3.7, h: 7.2, slices: 5, codexPct: 19, claudePct: 81, label: true },
|
|
577
|
+
{ name: "echo-design-system", hours: 21.2, x: -1.2, z: 3.6, w: 2.9, d: 2.7, h: 4.4, slices: 4, codexPct: 44, claudePct: 56 },
|
|
578
|
+
{ name: "Kobe Life", hours: 9.6, x: 6.8, z: 3.7, w: 2.1, d: 2.0, h: 3.0, slices: 3, codexPct: 100, claudePct: 0 },
|
|
579
|
+
{ name: "public", hours: 1.9, x: -7.9, z: 4.8, w: 1.7, d: 1.6, h: 2.1, slices: 2, codexPct: 0, claudePct: 100 },
|
|
580
|
+
{ name: "EchoMemory_log", hours: 1.8, x: 3.0, z: 4.4, w: 1.65, d: 1.6, h: 1.95, slices: 2, codexPct: 63, claudePct: 37 },
|
|
581
|
+
{ name: "brands", hours: .2, x: 8.5, z: -.5, w: 1.25, d: 1.2, h: 1.45, slices: 1, codexPct: 0, claudePct: 100 }
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
function makeGraphiteShadow(repo, seed) {
|
|
585
|
+
const size = 256;
|
|
586
|
+
const cv = document.createElement("canvas");
|
|
587
|
+
cv.width = cv.height = size;
|
|
588
|
+
const g = cv.getContext("2d");
|
|
589
|
+
const random = randomFactory(seed);
|
|
590
|
+
const cx = 118;
|
|
591
|
+
const cy = 136;
|
|
592
|
+
const rx = 82;
|
|
593
|
+
const ry = 48;
|
|
594
|
+
|
|
595
|
+
g.save();
|
|
596
|
+
g.filter = "blur(7px)";
|
|
597
|
+
g.fillStyle = "rgba(35,36,32,.10)";
|
|
598
|
+
g.beginPath();
|
|
599
|
+
g.ellipse(cx + 12, cy + 8, rx, ry, -.18, 0, Math.PI * 2);
|
|
600
|
+
g.fill();
|
|
601
|
+
g.restore();
|
|
602
|
+
|
|
603
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
604
|
+
g.beginPath();
|
|
605
|
+
const count = 190;
|
|
606
|
+
for (let i = 0; i < count; i++) {
|
|
607
|
+
const ring = Math.pow(random(), .68);
|
|
608
|
+
const a = random() * Math.PI * 2;
|
|
609
|
+
const x = cx + Math.cos(a) * rx * ring + random() * 28;
|
|
610
|
+
const y = cy + Math.sin(a) * ry * ring + random() * 14;
|
|
611
|
+
const length = 3 + random() * 13;
|
|
612
|
+
const angle = pass === 0 ? -.42 : .38;
|
|
613
|
+
if (random() < .18) continue;
|
|
614
|
+
g.moveTo(x, y);
|
|
615
|
+
g.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length * .7);
|
|
616
|
+
}
|
|
617
|
+
g.strokeStyle = pass === 0 ? "rgba(38,39,35,.16)" : "rgba(38,39,35,.10)";
|
|
618
|
+
g.lineWidth = .75;
|
|
619
|
+
g.lineCap = "round";
|
|
620
|
+
g.stroke();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
g.beginPath();
|
|
624
|
+
for (let i = 0; i < 190; i++) {
|
|
625
|
+
const a = random() * Math.PI * 2;
|
|
626
|
+
const ring = Math.pow(random(), .7);
|
|
627
|
+
g.rect(cx + Math.cos(a) * rx * ring, cy + Math.sin(a) * ry * ring, .5 + random() * 1.3, .5 + random());
|
|
628
|
+
}
|
|
629
|
+
g.fillStyle = "rgba(45,45,41,.08)";
|
|
630
|
+
g.fill();
|
|
631
|
+
|
|
632
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
633
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
634
|
+
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, depthWrite: false, opacity: 1, toneMapped: false });
|
|
635
|
+
const plane = new THREE.Mesh(new THREE.PlaneGeometry(repo.w * 1.65, repo.d * 1.25), material);
|
|
636
|
+
plane.rotation.x = -Math.PI / 2;
|
|
637
|
+
plane.rotation.z = -.18;
|
|
638
|
+
plane.position.set(repo.x + repo.w * .18, PLATFORM.top + .048, repo.z + repo.d * .12);
|
|
639
|
+
plane.renderOrder = 2;
|
|
640
|
+
scene.add(plane);
|
|
641
|
+
shadowMaterials.push(material);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function faceHatching(mesh, w, h, d, intensity, seed) {
|
|
645
|
+
const random = randomFactory(seed);
|
|
646
|
+
const vertices = [];
|
|
647
|
+
const count = Math.max(5, Math.round((w + h) * intensity * .82));
|
|
648
|
+
const front = d / 2 + .015;
|
|
649
|
+
for (let i = 0; i < count; i++) {
|
|
650
|
+
if (random() < .17) continue;
|
|
651
|
+
const y = -h / 2 + .2 + random() * Math.max(.1, h - .4);
|
|
652
|
+
const x = -w / 2 + .18 + random() * Math.max(.1, w - .36);
|
|
653
|
+
const length = .35 + random() * Math.min(1.5, w * .52);
|
|
654
|
+
const rise = length * (.28 + random() * .18);
|
|
655
|
+
const endX = Math.min(w / 2 - .1, x + length);
|
|
656
|
+
vertices.push(x, y, front, endX, Math.min(h / 2 - .1, y + rise), front);
|
|
657
|
+
}
|
|
658
|
+
const right = w / 2 + .015;
|
|
659
|
+
for (let i = 0; i < Math.round(count * .58); i++) {
|
|
660
|
+
if (random() < .22) continue;
|
|
661
|
+
const y = -h / 2 + .2 + random() * Math.max(.1, h - .4);
|
|
662
|
+
const z = -d / 2 + .15 + random() * Math.max(.1, d - .3);
|
|
663
|
+
const length = .3 + random() * Math.min(1.1, d * .45);
|
|
664
|
+
vertices.push(right, y, z, right, Math.min(h / 2 - .1, y + length * .34), Math.min(d / 2 - .08, z + length));
|
|
665
|
+
}
|
|
666
|
+
const geometry = new THREE.BufferGeometry();
|
|
667
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
668
|
+
const material = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .32 });
|
|
669
|
+
material.userData.baseOpacity = .32;
|
|
670
|
+
const lines = new THREE.LineSegments(geometry, material);
|
|
671
|
+
lines.renderOrder = 6;
|
|
672
|
+
mesh.add(lines);
|
|
673
|
+
hatchMaterials.push(material);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function crackMarks(mesh, w, h, d, seed) {
|
|
677
|
+
const random = randomFactory(seed);
|
|
678
|
+
const vertices = [];
|
|
679
|
+
const front = d / 2 + .02;
|
|
680
|
+
const count = Math.max(1, Math.round(w * .65));
|
|
681
|
+
for (let i = 0; i < count; i++) {
|
|
682
|
+
const x = (random() - .5) * w * .72;
|
|
683
|
+
const y = (random() - .5) * h * .52;
|
|
684
|
+
vertices.push(x, y, front, x + (random() - .5) * .42, y - .25 - random() * .35, front);
|
|
685
|
+
if (random() > .45) vertices.push(x, y - .22, front, x + (random() - .5) * .4, y - .45 - random() * .32, front);
|
|
686
|
+
}
|
|
687
|
+
const geometry = new THREE.BufferGeometry();
|
|
688
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
689
|
+
const material = new THREE.LineBasicMaterial({ color: 0x292a27, transparent: true, opacity: .46 });
|
|
690
|
+
material.userData.baseOpacity = .46;
|
|
691
|
+
mesh.add(new THREE.LineSegments(geometry, material));
|
|
692
|
+
hatchMaterials.push(material);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function labelTexture(repo) {
|
|
696
|
+
const cv = document.createElement("canvas");
|
|
697
|
+
cv.width = 512;
|
|
698
|
+
cv.height = 180;
|
|
699
|
+
const g = cv.getContext("2d");
|
|
700
|
+
g.textAlign = "center";
|
|
701
|
+
g.fillStyle = "#343530";
|
|
702
|
+
g.font = "600 62px IBM Plex Mono, monospace";
|
|
703
|
+
g.fillText(`${repo.hours}h`, 256, 68);
|
|
704
|
+
g.fillStyle = "rgba(52,53,48,.72)";
|
|
705
|
+
g.font = "500 23px IBM Plex Mono, monospace";
|
|
706
|
+
g.fillText(repo.name, 256, 112);
|
|
707
|
+
const texture = new THREE.CanvasTexture(cv);
|
|
708
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
709
|
+
return texture;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Verbatim optical model from the setup-page city on main@07b9638.
|
|
713
|
+
function authenticClayGlass(hex) {
|
|
714
|
+
const color = new THREE.Color(hex);
|
|
715
|
+
const material = new THREE.MeshPhysicalMaterial({
|
|
716
|
+
color,
|
|
717
|
+
roughness: .42,
|
|
718
|
+
metalness: 0,
|
|
719
|
+
transmission: .85,
|
|
720
|
+
ior: 1.34,
|
|
721
|
+
thickness: 70,
|
|
722
|
+
attenuationColor: color.clone(),
|
|
723
|
+
attenuationDistance: 95,
|
|
724
|
+
clearcoat: .28,
|
|
725
|
+
clearcoatRoughness: .45,
|
|
726
|
+
envMapIntensity: 1.05,
|
|
727
|
+
transparent: true,
|
|
728
|
+
opacity: 0,
|
|
729
|
+
depthWrite: false
|
|
730
|
+
});
|
|
731
|
+
authenticGlassMaterials.push(material);
|
|
732
|
+
return material;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// The historical implementation only rounded the exterior top/bottom of a
|
|
736
|
+
// split tower, leaving the provider seam perfectly planar.
|
|
737
|
+
function selectiveRoundedBoxGeometry(width, height, depth, segments = 8, radius = .1, opts = {}) {
|
|
738
|
+
const roundTop = opts.roundTop !== false;
|
|
739
|
+
const roundBottom = opts.roundBottom !== false;
|
|
740
|
+
const subdivisions = segments * 2 + 1;
|
|
741
|
+
const r = Math.min(width / 2, height / 2, depth / 2, radius);
|
|
742
|
+
const base = new THREE.BoxGeometry(1, 1, 1, subdivisions, subdivisions, subdivisions);
|
|
743
|
+
const geometry = base.toNonIndexed();
|
|
744
|
+
base.dispose();
|
|
745
|
+
const positions = geometry.attributes.position.array;
|
|
746
|
+
const normals = geometry.attributes.normal.array;
|
|
747
|
+
const position = new THREE.Vector3();
|
|
748
|
+
const normal = new THREE.Vector3();
|
|
749
|
+
const faceSpan = positions.length / 6;
|
|
750
|
+
const halfStep = .5 / subdivisions;
|
|
751
|
+
const bx = width / 2 - r;
|
|
752
|
+
const by = height / 2 - r;
|
|
753
|
+
const bz = depth / 2 - r;
|
|
754
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
755
|
+
position.fromArray(positions, i);
|
|
756
|
+
const sx = Math.sign(position.x) || 1;
|
|
757
|
+
const sy = Math.sign(position.y) || 1;
|
|
758
|
+
const sz = Math.sign(position.z) || 1;
|
|
759
|
+
const side = Math.floor(i / faceSpan);
|
|
760
|
+
const roundY = sy > 0 ? roundTop : roundBottom;
|
|
761
|
+
normal.copy(position);
|
|
762
|
+
normal.x -= sx * halfStep;
|
|
763
|
+
normal.z -= sz * halfStep;
|
|
764
|
+
if (roundY) normal.y -= sy * halfStep;
|
|
765
|
+
else normal.y = 0;
|
|
766
|
+
if (normal.lengthSq() > 0) normal.normalize();
|
|
767
|
+
positions[i] = bx * sx + normal.x * r;
|
|
768
|
+
positions[i + 1] = roundY ? by * sy + normal.y * r : height / 2 * sy;
|
|
769
|
+
positions[i + 2] = bz * sz + normal.z * r;
|
|
770
|
+
if (!roundY && (side === 2 || side === 3)) {
|
|
771
|
+
normals[i] = 0; normals[i + 1] = sy; normals[i + 2] = 0;
|
|
772
|
+
} else {
|
|
773
|
+
normals[i] = normal.x; normals[i + 1] = normal.y; normals[i + 2] = normal.z;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
geometry.attributes.position.needsUpdate = true;
|
|
777
|
+
geometry.attributes.normal.needsUpdate = true;
|
|
778
|
+
geometry.computeBoundingBox();
|
|
779
|
+
geometry.computeBoundingSphere();
|
|
780
|
+
return geometry;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function createRepo(repo, index) {
|
|
784
|
+
const authenticSize = { width: repo.w, height: repo.h, depth: repo.d };
|
|
785
|
+
const scale = Math.sqrt(Math.max(.001, repo.hours) / 92.8);
|
|
786
|
+
const radius = .76 + scale * 2.52;
|
|
787
|
+
const thickness = .44 + scale * 1.08;
|
|
788
|
+
repo.w = radius * 2;
|
|
789
|
+
repo.d = radius * 2;
|
|
790
|
+
repo.h = thickness;
|
|
791
|
+
makeGraphiteShadow(repo, 800 + index * 31);
|
|
792
|
+
|
|
793
|
+
const group = new THREE.Group();
|
|
794
|
+
group.position.set(repo.x, PLATFORM.top, repo.z);
|
|
795
|
+
scene.add(group);
|
|
796
|
+
|
|
797
|
+
const random = randomFactory(3000 + index * 101);
|
|
798
|
+
const puck = new THREE.Group();
|
|
799
|
+
const shiftX = (random() - .5) * Math.min(.26, radius * .1);
|
|
800
|
+
const shiftZ = (random() - .5) * Math.min(.22, radius * .08);
|
|
801
|
+
const tiltX = (random() - .5) * .035;
|
|
802
|
+
const tiltZ = (random() - .5) * .035;
|
|
803
|
+
const turn = (random() - .5) * .045;
|
|
804
|
+
puck.position.set(shiftX, thickness / 2, shiftZ);
|
|
805
|
+
puck.rotation.set(tiltX, turn, tiltZ);
|
|
806
|
+
group.add(puck);
|
|
807
|
+
|
|
808
|
+
const wall = sideMaterial.clone();
|
|
809
|
+
wall.color.setScalar(.94 + random() * .035);
|
|
810
|
+
wall.transparent = true;
|
|
811
|
+
wall.opacity = 1;
|
|
812
|
+
const topPaper = topMaterial.clone();
|
|
813
|
+
topPaper.transparent = true;
|
|
814
|
+
topPaper.opacity = 1;
|
|
815
|
+
dirtyShellMaterials.push(wall, topPaper);
|
|
816
|
+
const cylinderGeometry = new THREE.CylinderGeometry(radius, radius, thickness, 96, 1, false);
|
|
817
|
+
const cylinder = new THREE.Mesh(
|
|
818
|
+
cylinderGeometry,
|
|
819
|
+
[wall, topPaper, wall]
|
|
820
|
+
);
|
|
821
|
+
cylinder.castShadow = true;
|
|
822
|
+
cylinder.receiveShadow = true;
|
|
823
|
+
puck.add(cylinder);
|
|
824
|
+
|
|
825
|
+
const start = -Math.PI / 2;
|
|
826
|
+
const claudeAngle = Math.PI * 2 * repo.claudePct / 100;
|
|
827
|
+
const codexAngle = Math.PI * 2 * repo.codexPct / 100;
|
|
828
|
+
const codexGlass = new THREE.Color(0x7fb7d8);
|
|
829
|
+
const claudeGlass = new THREE.Color(0xda8da4);
|
|
830
|
+
|
|
831
|
+
// Clear context becomes an ordered glass city, not a stretched circular puck.
|
|
832
|
+
// Provider share is preserved as adjacent translucent blocks inside one footprint.
|
|
833
|
+
const cleanWidth = Math.max(.76, radius * .88);
|
|
834
|
+
const cleanDepth = Math.max(.68, radius * .72);
|
|
835
|
+
const cleanHeight = Math.max(1.75, thickness * 4.65);
|
|
836
|
+
const cleanCity = new THREE.Group();
|
|
837
|
+
cleanCity.scale.y = .06;
|
|
838
|
+
cleanCity.visible = false;
|
|
839
|
+
group.add(cleanCity);
|
|
840
|
+
|
|
841
|
+
const authenticCity = new THREE.Group();
|
|
842
|
+
authenticCity.visible = false;
|
|
843
|
+
group.add(authenticCity);
|
|
844
|
+
const authenticRadius = Math.min(
|
|
845
|
+
Math.min(authenticSize.width, authenticSize.height, authenticSize.depth) * .12,
|
|
846
|
+
.65
|
|
847
|
+
);
|
|
848
|
+
const authenticSegments = [];
|
|
849
|
+
if (repo.claudePct > 0) authenticSegments.push({
|
|
850
|
+
height: authenticSize.height * repo.claudePct / 100,
|
|
851
|
+
color: 0xd97757
|
|
852
|
+
});
|
|
853
|
+
if (repo.codexPct > 0) authenticSegments.push({
|
|
854
|
+
height: authenticSize.height * repo.codexPct / 100,
|
|
855
|
+
color: 0x10a37f
|
|
856
|
+
});
|
|
857
|
+
let authenticY = AUTHENTIC_TRAY.top - PLATFORM.top;
|
|
858
|
+
authenticSegments.forEach((segment, segmentIndex) => {
|
|
859
|
+
const mixed = authenticSegments.length > 1;
|
|
860
|
+
const geometry = mixed
|
|
861
|
+
? selectiveRoundedBoxGeometry(
|
|
862
|
+
authenticSize.width,
|
|
863
|
+
segment.height,
|
|
864
|
+
authenticSize.depth,
|
|
865
|
+
8,
|
|
866
|
+
Math.min(authenticRadius, segment.height * .45),
|
|
867
|
+
{ roundBottom: segmentIndex === 0, roundTop: segmentIndex === authenticSegments.length - 1 }
|
|
868
|
+
)
|
|
869
|
+
: new RoundedBoxGeometry(
|
|
870
|
+
authenticSize.width,
|
|
871
|
+
segment.height,
|
|
872
|
+
authenticSize.depth,
|
|
873
|
+
8,
|
|
874
|
+
Math.min(authenticRadius, segment.height * .45)
|
|
875
|
+
);
|
|
876
|
+
const tower = new THREE.Mesh(geometry, authenticClayGlass(segment.color));
|
|
877
|
+
tower.position.y = authenticY + segment.height / 2;
|
|
878
|
+
tower.castShadow = true;
|
|
879
|
+
tower.receiveShadow = true;
|
|
880
|
+
tower.renderOrder = 4;
|
|
881
|
+
authenticCity.add(tower);
|
|
882
|
+
authenticY += segment.height;
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
function addGlassBlock(share, color, cursor) {
|
|
886
|
+
if (share <= .0001) return cursor;
|
|
887
|
+
const gap = repo.codexPct > 0 && repo.claudePct > 0 ? .055 : 0;
|
|
888
|
+
const availableWidth = cleanWidth - gap;
|
|
889
|
+
const width = Math.max(.12, availableWidth * share);
|
|
890
|
+
const centerX = -cleanWidth / 2 + cursor + width / 2;
|
|
891
|
+
const surfaceColor = color.clone();
|
|
892
|
+
const material = new THREE.MeshPhysicalMaterial({
|
|
893
|
+
color: surfaceColor,
|
|
894
|
+
roughness: .42,
|
|
895
|
+
roughnessMap: frostMap,
|
|
896
|
+
bumpMap: frostMap,
|
|
897
|
+
bumpScale: .0018,
|
|
898
|
+
metalness: 0,
|
|
899
|
+
transmission: .85,
|
|
900
|
+
thickness: 2.4,
|
|
901
|
+
ior: 1.34,
|
|
902
|
+
attenuationColor: color,
|
|
903
|
+
attenuationDistance: 3.25,
|
|
904
|
+
clearcoat: .28,
|
|
905
|
+
clearcoatRoughness: .45,
|
|
906
|
+
envMapIntensity: 1.05,
|
|
907
|
+
transparent: true,
|
|
908
|
+
opacity: 0,
|
|
909
|
+
depthWrite: false,
|
|
910
|
+
side: THREE.DoubleSide
|
|
911
|
+
});
|
|
912
|
+
material.userData.cleanOpacity = .96;
|
|
913
|
+
const geometry = new RoundedBoxGeometry(width, cleanHeight, cleanDepth, 5, Math.min(.055, width * .08));
|
|
914
|
+
const block = new THREE.Mesh(geometry, material);
|
|
915
|
+
block.position.set(centerX, cleanHeight / 2, 0);
|
|
916
|
+
// Three.js shadow maps treat transmissive glass as opaque and create giant
|
|
917
|
+
// charcoal rectangles, so the clean glass uses studio light without a hard cast shadow.
|
|
918
|
+
block.castShadow = false;
|
|
919
|
+
block.receiveShadow = true;
|
|
920
|
+
block.renderOrder = 3;
|
|
921
|
+
cleanCity.add(block);
|
|
922
|
+
glassMaterials.push(material);
|
|
923
|
+
|
|
924
|
+
const edgeMaterial = new THREE.LineBasicMaterial({
|
|
925
|
+
color: color.clone().lerp(new THREE.Color(0xffffff), .62),
|
|
926
|
+
transparent: true,
|
|
927
|
+
opacity: 0,
|
|
928
|
+
depthWrite: false
|
|
929
|
+
});
|
|
930
|
+
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(geometry, 24), edgeMaterial);
|
|
931
|
+
edges.renderOrder = 7;
|
|
932
|
+
block.add(edges);
|
|
933
|
+
cleanGlassLineMaterials.push({ material: edgeMaterial, clean: .48 });
|
|
934
|
+
|
|
935
|
+
return cursor + width + gap;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let glassCursor = 0;
|
|
939
|
+
glassCursor = addGlassBlock(repo.claudePct / 100, claudeGlass, glassCursor);
|
|
940
|
+
addGlassBlock(repo.codexPct / 100, codexGlass, glassCursor);
|
|
941
|
+
|
|
942
|
+
const topY = thickness / 2 + .001;
|
|
943
|
+
const insetRadius = radius * .965;
|
|
944
|
+
|
|
945
|
+
function addCleanGlassArc(angle, length, color, y, cleanOpacity) {
|
|
946
|
+
if (length <= .0001) return;
|
|
947
|
+
const points = [];
|
|
948
|
+
const segments = Math.max(12, Math.round(96 * length / (Math.PI * 2)));
|
|
949
|
+
for (let i = 0; i <= segments; i++) {
|
|
950
|
+
const a = angle + length * i / segments;
|
|
951
|
+
points.push(new THREE.Vector3(Math.cos(a) * radius * 1.006, y, Math.sin(a) * radius * 1.006));
|
|
952
|
+
}
|
|
953
|
+
const material = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0, depthWrite: false });
|
|
954
|
+
const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), material);
|
|
955
|
+
line.renderOrder = 7;
|
|
956
|
+
puck.add(line);
|
|
957
|
+
cleanGlassLineMaterials.push({ material, clean: cleanOpacity });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
addCleanGlassArc(start, claudeAngle, claudeGlass, topY + .0012, .68);
|
|
961
|
+
addCleanGlassArc(start + claudeAngle, codexAngle, codexGlass, topY + .0012, .68);
|
|
962
|
+
addCleanGlassArc(start, claudeAngle, claudeGlass, -thickness / 2 + .012, .055);
|
|
963
|
+
addCleanGlassArc(start + claudeAngle, codexAngle, codexGlass, -thickness / 2 + .012, .055);
|
|
964
|
+
|
|
965
|
+
for (const [angle, color] of [[start, claudeGlass], [start + claudeAngle, codexGlass]]) {
|
|
966
|
+
const x = Math.cos(angle) * radius * 1.006;
|
|
967
|
+
const z = Math.sin(angle) * radius * 1.006;
|
|
968
|
+
const material = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0, depthWrite: false });
|
|
969
|
+
const geometry = new THREE.BufferGeometry().setFromPoints([
|
|
970
|
+
new THREE.Vector3(x, -thickness / 2 + .02, z),
|
|
971
|
+
new THREE.Vector3(x, thickness / 2 - .01, z)
|
|
972
|
+
]);
|
|
973
|
+
const line = new THREE.Line(geometry, material);
|
|
974
|
+
line.renderOrder = 7;
|
|
975
|
+
puck.add(line);
|
|
976
|
+
cleanGlassLineMaterials.push({ material, clean: .52 });
|
|
977
|
+
}
|
|
978
|
+
if (claudeAngle > .0001) {
|
|
979
|
+
const geometry = new THREE.CircleGeometry(insetRadius, 72, start, claudeAngle);
|
|
980
|
+
geometry.rotateX(-Math.PI / 2);
|
|
981
|
+
const sector = new THREE.Mesh(geometry, claudeTopMaterial);
|
|
982
|
+
sector.position.y = topY;
|
|
983
|
+
sector.renderOrder = 4;
|
|
984
|
+
puck.add(sector);
|
|
985
|
+
}
|
|
986
|
+
if (codexAngle > .0001) {
|
|
987
|
+
const geometry = new THREE.CircleGeometry(insetRadius, 72, start + claudeAngle, codexAngle);
|
|
988
|
+
geometry.rotateX(-Math.PI / 2);
|
|
989
|
+
const sector = new THREE.Mesh(geometry, codexTopMaterial);
|
|
990
|
+
sector.position.y = topY + .0002;
|
|
991
|
+
sector.renderOrder = 4;
|
|
992
|
+
puck.add(sector);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Pencil boundaries: doubled circumference plus the two radial split seams.
|
|
996
|
+
const circlePoints = [];
|
|
997
|
+
for (let i = 0; i <= 96; i++) {
|
|
998
|
+
const angle = i / 96 * Math.PI * 2;
|
|
999
|
+
circlePoints.push(new THREE.Vector3(Math.cos(angle) * radius, topY + .0005, Math.sin(angle) * radius));
|
|
1000
|
+
}
|
|
1001
|
+
const rimMaterial = new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity: .72 });
|
|
1002
|
+
const rim = new THREE.Line(
|
|
1003
|
+
new THREE.BufferGeometry().setFromPoints(circlePoints),
|
|
1004
|
+
rimMaterial
|
|
1005
|
+
);
|
|
1006
|
+
puck.add(rim);
|
|
1007
|
+
topLineMaterials.push({ material: rimMaterial, dirty: .72, clean: .035 });
|
|
1008
|
+
const rimEcho = rim.clone();
|
|
1009
|
+
const rimEchoMaterial = new THREE.LineBasicMaterial({ color: 0x2d2e2a, transparent: true, opacity: .16 });
|
|
1010
|
+
rimEcho.material = rimEchoMaterial;
|
|
1011
|
+
rimEcho.scale.set(1.006, 1, .996);
|
|
1012
|
+
puck.add(rimEcho);
|
|
1013
|
+
topLineMaterials.push({ material: rimEchoMaterial, dirty: .16, clean: .008 });
|
|
1014
|
+
|
|
1015
|
+
const seamVertices = [];
|
|
1016
|
+
for (const angle of [start, start + claudeAngle]) {
|
|
1017
|
+
seamVertices.push(0, topY + .0007, 0, Math.cos(angle) * insetRadius, topY + .0007, Math.sin(angle) * insetRadius);
|
|
1018
|
+
}
|
|
1019
|
+
const seamGeometry = new THREE.BufferGeometry();
|
|
1020
|
+
seamGeometry.setAttribute("position", new THREE.Float32BufferAttribute(seamVertices, 3));
|
|
1021
|
+
const seamMaterial = new THREE.LineBasicMaterial({ color: 0x292a27, transparent: true, opacity: .62 });
|
|
1022
|
+
puck.add(new THREE.LineSegments(seamGeometry, seamMaterial));
|
|
1023
|
+
topLineMaterials.push({ material: seamMaterial, dirty: .62, clean: .025 });
|
|
1024
|
+
|
|
1025
|
+
// Cylindrical wall grain: mostly vertical, with a few soft cross-hatched strokes on the dark side.
|
|
1026
|
+
const sideVertices = [];
|
|
1027
|
+
const strokeCount = Math.max(28, Math.round(radius * 19));
|
|
1028
|
+
for (let i = 0; i < strokeCount; i++) {
|
|
1029
|
+
const angle = random() * Math.PI * 2;
|
|
1030
|
+
if (random() < .16) continue;
|
|
1031
|
+
const x = Math.cos(angle) * (radius + .009);
|
|
1032
|
+
const z = Math.sin(angle) * (radius + .009);
|
|
1033
|
+
const bottom = -thickness / 2 + .08 + random() * .12;
|
|
1034
|
+
const top = thickness / 2 - .08 - random() * .12;
|
|
1035
|
+
sideVertices.push(x, bottom, z, x + (random() - .5) * .025, top, z + (random() - .5) * .025);
|
|
1036
|
+
if (Math.sin(angle + .8) > .15 && random() > .58) {
|
|
1037
|
+
const rise = .12 + random() * thickness * .24;
|
|
1038
|
+
const sweep = .055 + random() * .09;
|
|
1039
|
+
sideVertices.push(
|
|
1040
|
+
x, bottom + random() * Math.max(.1, thickness - .3), z,
|
|
1041
|
+
Math.cos(angle + sweep) * (radius + .011), Math.min(top, bottom + rise + random() * .35), Math.sin(angle + sweep) * (radius + .011)
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const sideGeometry = new THREE.BufferGeometry();
|
|
1046
|
+
sideGeometry.setAttribute("position", new THREE.Float32BufferAttribute(sideVertices, 3));
|
|
1047
|
+
const sideInk = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .22 });
|
|
1048
|
+
sideInk.userData.baseOpacity = .22;
|
|
1049
|
+
puck.add(new THREE.LineSegments(sideGeometry, sideInk));
|
|
1050
|
+
hatchMaterials.push(sideInk);
|
|
1051
|
+
|
|
1052
|
+
// Bottom seam carries more graphite pressure, like the reference drawing.
|
|
1053
|
+
const bottomPoints = circlePoints.map(point => new THREE.Vector3(point.x, -thickness / 2 + .001, point.z));
|
|
1054
|
+
const bottomRimMaterial = new THREE.LineBasicMaterial({ color: 0x282924, transparent: true, opacity: .5 });
|
|
1055
|
+
bottomRimMaterial.userData.baseOpacity = .5;
|
|
1056
|
+
const bottomRim = new THREE.Line(
|
|
1057
|
+
new THREE.BufferGeometry().setFromPoints(bottomPoints),
|
|
1058
|
+
bottomRimMaterial
|
|
1059
|
+
);
|
|
1060
|
+
puck.add(bottomRim);
|
|
1061
|
+
hatchMaterials.push(bottomRimMaterial);
|
|
1062
|
+
|
|
1063
|
+
dirtySlices.push({
|
|
1064
|
+
mesh: puck,
|
|
1065
|
+
dirtyPosition: puck.position.clone(),
|
|
1066
|
+
dirtyRotation: turn,
|
|
1067
|
+
dirtyRotationX: tiltX,
|
|
1068
|
+
dirtyRotationZ: tiltZ,
|
|
1069
|
+
baseThickness: thickness,
|
|
1070
|
+
cleanCity,
|
|
1071
|
+
cleanHeight,
|
|
1072
|
+
authenticCity,
|
|
1073
|
+
authenticHeight: authenticSize.height,
|
|
1074
|
+
dirtyCylinder: cylinder,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
if (repo.label) {
|
|
1078
|
+
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: labelTexture(repo), transparent: true, depthWrite: false, toneMapped: false }));
|
|
1079
|
+
sprite.scale.set(radius * 2.05, radius * .73, 1);
|
|
1080
|
+
sprite.position.set(repo.x, PLATFORM.top + thickness + 1.0, repo.z);
|
|
1081
|
+
scene.add(sprite);
|
|
1082
|
+
repoLabels.push({
|
|
1083
|
+
sprite,
|
|
1084
|
+
dirtyY: sprite.position.y,
|
|
1085
|
+
cleanY: PLATFORM.top + cleanHeight + 1.1,
|
|
1086
|
+
authenticY: AUTHENTIC_TRAY.top + authenticSize.height + 1.1
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const pieceCount = thickness > 1.2 ? 2 : thickness > .8 ? 1 : 0;
|
|
1091
|
+
for (let i = 0; i < pieceCount; i++) {
|
|
1092
|
+
const size = .22 + random() * .28;
|
|
1093
|
+
const piece = new THREE.Mesh(
|
|
1094
|
+
i % 2 ? new THREE.TetrahedronGeometry(size, 0) : new THREE.BoxGeometry(size, size * .65, size * .8),
|
|
1095
|
+
chunkMaterial
|
|
1096
|
+
);
|
|
1097
|
+
const side = random() > .5 ? 1 : -1;
|
|
1098
|
+
piece.position.set(repo.x + side * (radius * 1.08 + random() * .42), PLATFORM.top + size * .5, repo.z + (random() - .5) * radius * 1.3);
|
|
1099
|
+
piece.rotation.set(random() * 1.2, random() * 1.2, random() * 1.2);
|
|
1100
|
+
piece.castShadow = true;
|
|
1101
|
+
scene.add(piece);
|
|
1102
|
+
outline(piece, .42, 15);
|
|
1103
|
+
dirtyPieces.push(piece);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
repos.forEach(createRepo);
|
|
1108
|
+
|
|
1109
|
+
function addDirtyTrail() {
|
|
1110
|
+
const random = randomFactory(7711);
|
|
1111
|
+
const vertices = [];
|
|
1112
|
+
for (let i = 0; i < 24; i++) {
|
|
1113
|
+
const x = -9.6 + random() * 18.5;
|
|
1114
|
+
const z = -6.7 + random() * 13.3;
|
|
1115
|
+
if (random() < .6) continue;
|
|
1116
|
+
const length = .22 + random() * .72;
|
|
1117
|
+
vertices.push(x, PLATFORM.top + .055, z, x + length, PLATFORM.top + .057, z + length * (.3 + random() * .22));
|
|
1118
|
+
}
|
|
1119
|
+
const geometry = new THREE.BufferGeometry();
|
|
1120
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
1121
|
+
const material = new THREE.LineBasicMaterial({ color: 0x30312d, transparent: true, opacity: .2 });
|
|
1122
|
+
material.userData.baseOpacity = .2;
|
|
1123
|
+
scene.add(new THREE.LineSegments(geometry, material));
|
|
1124
|
+
hatchMaterials.push(material);
|
|
1125
|
+
}
|
|
1126
|
+
addDirtyTrail();
|
|
1127
|
+
|
|
1128
|
+
const dirtyButton = document.getElementById("dirty");
|
|
1129
|
+
const repairButton = document.getElementById("repair");
|
|
1130
|
+
const authenticButton = document.getElementById("authentic");
|
|
1131
|
+
const dirtyValue = document.getElementById("dirty-value");
|
|
1132
|
+
const dirtyLabel = document.getElementById("dirty-label");
|
|
1133
|
+
const summaryCaption = document.getElementById("summary-caption");
|
|
1134
|
+
let repairTarget = 0;
|
|
1135
|
+
let repairProgress = 0;
|
|
1136
|
+
|
|
1137
|
+
function setVersion(version) {
|
|
1138
|
+
repairTarget = version === "dirty" ? 0 : 1;
|
|
1139
|
+
dirtyButton.setAttribute("aria-pressed", String(version === "dirty"));
|
|
1140
|
+
repairButton.setAttribute("aria-pressed", String(version === "repair"));
|
|
1141
|
+
authenticButton.setAttribute("aria-pressed", String(version === "authentic"));
|
|
1142
|
+
dirtyValue.textContent = version === "dirty" ? "82%" : "5%";
|
|
1143
|
+
dirtyLabel.textContent = version === "dirty" ? "dirty context" : "residual context";
|
|
1144
|
+
summaryCaption.textContent = version === "dirty"
|
|
1145
|
+
? "Dirty context spreads low and wide, carrying graphite, dust and repeated reads."
|
|
1146
|
+
: version === "repair"
|
|
1147
|
+
? "Repair city: authentic frosted volume glass on one ivory clay tray."
|
|
1148
|
+
: "Authentic setup-page city: frosted volume glass on one ivory clay tray.";
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
dirtyButton.addEventListener("click", () => setVersion("dirty"));
|
|
1152
|
+
repairButton.addEventListener("click", () => setVersion("repair"));
|
|
1153
|
+
authenticButton.addEventListener("click", () => setVersion("authentic"));
|
|
1154
|
+
|
|
1155
|
+
function updateRepair() {
|
|
1156
|
+
repairProgress += (repairTarget - repairProgress) * .075;
|
|
1157
|
+
const ease = repairProgress * repairProgress * (3 - 2 * repairProgress);
|
|
1158
|
+
for (const material of dirtyShellMaterials) {
|
|
1159
|
+
material.opacity = THREE.MathUtils.lerp(1, .018, ease);
|
|
1160
|
+
material.depthWrite = ease < .52;
|
|
1161
|
+
}
|
|
1162
|
+
// Repair now uses the exact same geometry, optics, palette and base as the
|
|
1163
|
+
// authentic setup-page city. The old marble/side-by-side experiment remains
|
|
1164
|
+
// allocated only so the dirty morphology code stays easy to compare, but it
|
|
1165
|
+
// never enters the render pass and therefore cannot corrupt transparent sort.
|
|
1166
|
+
for (const material of glassMaterials) material.opacity = 0;
|
|
1167
|
+
for (const material of authenticGlassMaterials) {
|
|
1168
|
+
material.opacity = ease;
|
|
1169
|
+
material.depthWrite = ease > .97;
|
|
1170
|
+
}
|
|
1171
|
+
for (const entry of topLineMaterials) {
|
|
1172
|
+
entry.material.opacity = THREE.MathUtils.lerp(entry.dirty, 0, ease);
|
|
1173
|
+
}
|
|
1174
|
+
for (const entry of cleanGlassLineMaterials) entry.material.opacity = 0;
|
|
1175
|
+
for (const material of platformDirtyMaterials) {
|
|
1176
|
+
material.opacity = material.userData.dirtyOpacity * (1 - ease);
|
|
1177
|
+
material.depthWrite = ease < .5;
|
|
1178
|
+
}
|
|
1179
|
+
for (const material of cleanPlatformMaterials) material.opacity = 0;
|
|
1180
|
+
for (const material of authenticPlatformMaterials) {
|
|
1181
|
+
material.opacity = ease;
|
|
1182
|
+
if (material === authenticTrayMaterial) material.depthWrite = ease > .02;
|
|
1183
|
+
}
|
|
1184
|
+
codexTopMaterial.opacity = THREE.MathUtils.lerp(.64, 0, ease);
|
|
1185
|
+
claudeTopMaterial.opacity = THREE.MathUtils.lerp(.64, 0, ease);
|
|
1186
|
+
for (const material of hatchMaterials) material.opacity = Math.max(.02, material.userData?.baseOpacity || .28) * (1 - ease * .98);
|
|
1187
|
+
for (const material of shadowMaterials) material.opacity = 1 - ease;
|
|
1188
|
+
for (const entry of dirtySlices) {
|
|
1189
|
+
const dirtyScale = THREE.MathUtils.lerp(1, .82, ease);
|
|
1190
|
+
entry.mesh.scale.set(dirtyScale, dirtyScale, dirtyScale);
|
|
1191
|
+
entry.mesh.position.x = THREE.MathUtils.lerp(entry.dirtyPosition.x, 0, ease);
|
|
1192
|
+
entry.mesh.position.y = entry.baseThickness / 2;
|
|
1193
|
+
entry.mesh.position.z = THREE.MathUtils.lerp(entry.dirtyPosition.z, 0, ease);
|
|
1194
|
+
entry.mesh.rotation.y = entry.dirtyRotation * (1 - ease);
|
|
1195
|
+
entry.mesh.rotation.x = entry.dirtyRotationX * (1 - ease);
|
|
1196
|
+
entry.mesh.rotation.z = entry.dirtyRotationZ * (1 - ease);
|
|
1197
|
+
entry.dirtyCylinder.castShadow = ease < .58;
|
|
1198
|
+
entry.mesh.visible = ease < .995;
|
|
1199
|
+
entry.cleanCity.visible = false;
|
|
1200
|
+
entry.authenticCity.visible = ease > .01;
|
|
1201
|
+
}
|
|
1202
|
+
for (const entry of repoLabels) {
|
|
1203
|
+
entry.sprite.position.y = THREE.MathUtils.lerp(entry.dirtyY, entry.authenticY, ease);
|
|
1204
|
+
}
|
|
1205
|
+
for (const piece of dirtyPieces) piece.scale.setScalar(Math.max(.001, 1 - ease));
|
|
1206
|
+
hemisphere.intensity = THREE.MathUtils.lerp(.88, .7, ease);
|
|
1207
|
+
ambient.intensity = THREE.MathUtils.lerp(.3, .2, ease);
|
|
1208
|
+
key.intensity = THREE.MathUtils.lerp(1.28, 2.3, ease);
|
|
1209
|
+
fill.intensity = .4 * ease;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function resize() {
|
|
1213
|
+
const width = innerWidth;
|
|
1214
|
+
const height = innerHeight;
|
|
1215
|
+
renderer.setSize(width, height, false);
|
|
1216
|
+
const aspect = width / height;
|
|
1217
|
+
const vertical = width < 800 ? 16 : 12.6;
|
|
1218
|
+
camera.left = -vertical * aspect;
|
|
1219
|
+
camera.right = vertical * aspect;
|
|
1220
|
+
camera.top = vertical;
|
|
1221
|
+
camera.bottom = -vertical;
|
|
1222
|
+
camera.updateProjectionMatrix();
|
|
1223
|
+
}
|
|
1224
|
+
addEventListener("resize", resize);
|
|
1225
|
+
resize();
|
|
1226
|
+
|
|
1227
|
+
let visible = true;
|
|
1228
|
+
document.addEventListener("visibilitychange", () => { visible = !document.hidden; });
|
|
1229
|
+
function animate() {
|
|
1230
|
+
requestAnimationFrame(animate);
|
|
1231
|
+
if (!visible) return;
|
|
1232
|
+
updateRepair();
|
|
1233
|
+
controls.update();
|
|
1234
|
+
renderer.render(scene, camera);
|
|
1235
|
+
}
|
|
1236
|
+
animate();
|
|
1237
|
+
</script>
|
|
1238
|
+
</body>
|
|
1239
|
+
</html>
|