Skip to content

Commit bf6dca4

Browse files
w3-surferclaude
andcommitted
ui(os dock): three.js 3D icons for the six factory apps
Replaced the flat FontAwesome glyphs on the core dock tiles with animated three.js icons — a distinct lit, glossy 3D solid slowly spinning on each app's colored tile: cube (My Virtual Machine), flat chip (Hardware Store), dodecahedron (App Store), icosahedron (Models), torus (My Wallet), cone (Terminal). Installed/custom apps keep their logo tiles. Rendered on the existing SHARED WebGL context (getSharedGL) and copied onto a per-tile 2D canvas — one GL context total, so it can't hit the browser's ~16-context limit (the reason the Hardware Store already shares one). The 3D canvases are cached by kind and reused across dock rebuilds; window open/close/minimize now only toggles the running dots (updateDockDots) instead of rebuilding the dock, so the canvases aren't thrashed. Respects prefers-reduced-motion (renders one static pass, no spin). Verified headless (ANGLE/SwiftShader): all six tiles hold a canvas, the Models icon renders non-transparent pixels, the Terminal running dot still lights on open, no page errors; script passes node --check. Screenshot confirms the 3D solids on the colored tiles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 599606b commit bf6dca4

1 file changed

Lines changed: 84 additions & 15 deletions

File tree

‎web/os.html‎

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
transform-origin: bottom center;
8080
}
8181
.dock-icon img { width: 26px; height: 26px; border-radius: 6px; object-fit: contain; }
82+
.dock-icon canvas { width: 100%; height: 100%; display: block; }
8283
.dock-item:hover .dock-icon { transform: translateY(-9px) scale(1.22); }
8384
.dock-item:active .dock-icon { transform: translateY(-3px) scale(1.08); }
8485
.dock-dot { width: 4px; height: 4px; border-radius: 50%; background: rgba(255,255,255,0.85); }
@@ -1190,26 +1191,86 @@
11901191
// tile is a solid 3D icon that magnifies on hover; a dot under it marks
11911192
// a running app. Core apps have a distinct colored tile; installed /
11921193
// custom apps show their official logo on a graphite tile.
1194+
// The six factory apps get animated three.js icons (a distinct lit 3D
1195+
// solid spinning on the colored tile), instead of a flat glyph.
11931196
const DOCK_CORE = [
1194-
{ icon: 'fa-computer', label: 'My Virtual Machine', open: () => openMyVM(), id: 'myvm', grad: 'linear-gradient(160deg,#5b9bff,#1e5fd0)' },
1195-
{ icon: 'fa-microchip', label: 'Hardware Store', open: () => openHardwareStore(), id: 'hw', grad: 'linear-gradient(160deg,#b07bff,#6d28d9)' },
1196-
{ icon: 'fa-store', label: 'App Store', open: () => openStore(), id: 'store', grad: 'linear-gradient(160deg,#eaff8f,#a4dc00)', dark: true },
1197-
{ icon: 'fa-brain', label: 'Models', open: () => openModels(), id: 'models', grad: 'linear-gradient(160deg,#ff86c8,#d61f8f)' },
1198-
{ icon: 'fa-wallet', label: 'My Wallet', open: () => openWallet(), id: 'wallet', grad: 'linear-gradient(160deg,#ffd257,#e0900a)' },
1199-
{ icon: 'fa-terminal', label: 'Terminal', open: () => openTerminal(), id: 'term', grad: 'linear-gradient(160deg,#5b6472,#1f2937)' },
1197+
{ three: 'vm', label: 'My Virtual Machine', open: () => openMyVM(), id: 'myvm', grad: 'linear-gradient(160deg,#5b9bff,#1e5fd0)' },
1198+
{ three: 'chip', label: 'Hardware Store', open: () => openHardwareStore(), id: 'hw', grad: 'linear-gradient(160deg,#b07bff,#6d28d9)' },
1199+
{ three: 'store', label: 'App Store', open: () => openStore(), id: 'store', grad: 'linear-gradient(160deg,#eaff8f,#a4dc00)' },
1200+
{ three: 'brain', label: 'Models', open: () => openModels(), id: 'models', grad: 'linear-gradient(160deg,#ff86c8,#d61f8f)' },
1201+
{ three: 'wallet', label: 'My Wallet', open: () => openWallet(), id: 'wallet', grad: 'linear-gradient(160deg,#ffd257,#e0900a)' },
1202+
{ three: 'term', label: 'Terminal', open: () => openTerminal(), id: 'term', grad: 'linear-gradient(160deg,#5b6472,#1f2937)' },
12001203
];
12011204

1202-
function dockIcon({ icon, label, onclick, id, grad, dark, app }) {
1205+
// ---- three.js dock icons: a lit 3D solid per app, animated on the
1206+
// SHARED WebGL context (getSharedGL) and copied onto each tile's 2D
1207+
// canvas — one context total, no per-icon context (see the note there). --
1208+
const ICON_PX = 46;
1209+
const dockIcon3D = {}; // kind -> { canvas, scene, cam, mesh }
1210+
function dockGeometry(kind) {
1211+
switch (kind) {
1212+
case 'vm': return new THREE.BoxGeometry(1.05, 1.05, 1.05);
1213+
case 'chip': return new THREE.BoxGeometry(1.4, 0.3, 1.4);
1214+
case 'store': return new THREE.DodecahedronGeometry(0.82);
1215+
case 'brain': return new THREE.IcosahedronGeometry(0.86);
1216+
case 'wallet': return new THREE.TorusGeometry(0.56, 0.24, 20, 44);
1217+
case 'term': return new THREE.ConeGeometry(0.82, 1.35, 4);
1218+
default: return new THREE.BoxGeometry(1, 1, 1);
1219+
}
1220+
}
1221+
function mkDockIcon3D(kind) {
1222+
if (dockIcon3D[kind]) return dockIcon3D[kind].canvas;
1223+
if (typeof THREE === 'undefined') return null;
1224+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
1225+
const canvas = document.createElement('canvas');
1226+
canvas.width = ICON_PX * dpr; canvas.height = ICON_PX * dpr;
1227+
const scene = new THREE.Scene();
1228+
const cam = new THREE.PerspectiveCamera(42, 1, 0.1, 20);
1229+
cam.position.set(0, 0, 3.4);
1230+
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
1231+
const key = new THREE.DirectionalLight(0xffffff, 1.25); key.position.set(3, 4, 5); scene.add(key);
1232+
const rim = new THREE.DirectionalLight(0xccff33, 0.75); rim.position.set(-4, -2, -3); scene.add(rim);
1233+
const mesh = new THREE.Mesh(dockGeometry(kind), new THREE.MeshStandardMaterial({ color: 0xf3f3f3, metalness: 0.45, roughness: 0.26 }));
1234+
mesh.rotation.x = 0.28;
1235+
scene.add(mesh);
1236+
dockIcon3D[kind] = { canvas, scene, cam, mesh };
1237+
startDockIconLoop();
1238+
return canvas;
1239+
}
1240+
let dockIconLoop = false;
1241+
const dockReduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1242+
function startDockIconLoop() {
1243+
if (dockIconLoop || typeof THREE === 'undefined') return;
1244+
dockIconLoop = true;
1245+
const gl = getSharedGL();
1246+
(function frame() {
1247+
for (const kind in dockIcon3D) {
1248+
const it = dockIcon3D[kind];
1249+
if (!it.canvas.isConnected) continue;
1250+
if (!dockReduceMotion) it.mesh.rotation.y += 0.014;
1251+
gl.setSize(it.canvas.width, it.canvas.height, false);
1252+
gl.render(it.scene, it.cam);
1253+
const ctx = it.canvas.getContext('2d');
1254+
if (ctx) { ctx.clearRect(0, 0, it.canvas.width, it.canvas.height); ctx.drawImage(gl.domElement, 0, 0, it.canvas.width, it.canvas.height); }
1255+
}
1256+
if (dockReduceMotion) { dockIconLoop = false; return; } // static: one pass
1257+
requestAnimationFrame(frame);
1258+
})();
1259+
}
1260+
1261+
function dockIcon({ icon, label, onclick, id, grad, dark, app, three }) {
12031262
const running = id && !!openWindows[id];
12041263
const b = document.createElement('button');
12051264
b.className = 'dock-item';
12061265
b.setAttribute('aria-label', label);
1207-
const glyph = app ? appGlyph(app, 'w-6 h-6', 'text-lg')
1208-
: `<i class="fa-solid ${icon}" style="color:${dark ? '#0b0b0d' : '#fff'}"></i>`;
1209-
b.innerHTML =
1210-
`<span class="dock-tip">${label}</span>` +
1211-
`<span class="dock-icon" style="background:${grad}">${glyph}</span>` +
1212-
`<span class="dock-dot ${running ? '' : 'off'}"></span>`;
1266+
const tip = document.createElement('span'); tip.className = 'dock-tip'; tip.textContent = label;
1267+
const tile = document.createElement('span'); tile.className = 'dock-icon'; tile.style.background = grad;
1268+
const canvas3d = three ? mkDockIcon3D(three) : null;
1269+
if (canvas3d) tile.appendChild(canvas3d);
1270+
else if (app) tile.innerHTML = appGlyph(app, 'w-6 h-6', 'text-lg');
1271+
else tile.innerHTML = `<i class="fa-solid ${icon}" style="color:${dark ? '#0b0b0d' : '#fff'}"></i>`;
1272+
const dot = document.createElement('span'); dot.className = 'dock-dot' + (running ? '' : ' off'); dot.dataset.dot = id || '';
1273+
b.append(tip, tile, dot);
12131274
b.onclick = onclick;
12141275
return b;
12151276
}
@@ -1234,10 +1295,18 @@
12341295
onclick: () => openApp(app),
12351296
})));
12361297
}
1298+
if (dockReduceMotion) startDockIconLoop(); // one static pass after (re)build
12371299
}
12381300

1239-
// Window lifecycle refreshes the dock (running dots).
1240-
function renderTaskbar() { renderDock(); }
1301+
// Window lifecycle only toggles the running dots — no full rebuild (which
1302+
// would thrash the reused 3D canvases).
1303+
function updateDockDots() {
1304+
document.querySelectorAll('#dock .dock-dot').forEach(d => {
1305+
const id = d.dataset.dot;
1306+
d.classList.toggle('off', !(id && !!openWindows[id]));
1307+
});
1308+
}
1309+
function renderTaskbar() { updateDockDots(); }
12411310

12421311
// The desktop no longer holds app icons — they live in the dock.
12431312
function renderDesktop() {

0 commit comments

Comments
 (0)