|
79 | 79 | transform-origin: bottom center; |
80 | 80 | } |
81 | 81 | .dock-icon img { width: 26px; height: 26px; border-radius: 6px; object-fit: contain; } |
| 82 | + .dock-icon canvas { width: 100%; height: 100%; display: block; } |
82 | 83 | .dock-item:hover .dock-icon { transform: translateY(-9px) scale(1.22); } |
83 | 84 | .dock-item:active .dock-icon { transform: translateY(-3px) scale(1.08); } |
84 | 85 | .dock-dot { width: 4px; height: 4px; border-radius: 50%; background: rgba(255,255,255,0.85); } |
|
1190 | 1191 | // tile is a solid 3D icon that magnifies on hover; a dot under it marks |
1191 | 1192 | // a running app. Core apps have a distinct colored tile; installed / |
1192 | 1193 | // 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. |
1193 | 1196 | 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)' }, |
1200 | 1203 | ]; |
1201 | 1204 |
|
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 }) { |
1203 | 1262 | const running = id && !!openWindows[id]; |
1204 | 1263 | const b = document.createElement('button'); |
1205 | 1264 | b.className = 'dock-item'; |
1206 | 1265 | 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); |
1213 | 1274 | b.onclick = onclick; |
1214 | 1275 | return b; |
1215 | 1276 | } |
|
1234 | 1295 | onclick: () => openApp(app), |
1235 | 1296 | }))); |
1236 | 1297 | } |
| 1298 | + if (dockReduceMotion) startDockIconLoop(); // one static pass after (re)build |
1237 | 1299 | } |
1238 | 1300 |
|
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(); } |
1241 | 1310 |
|
1242 | 1311 | // The desktop no longer holds app icons — they live in the dock. |
1243 | 1312 | function renderDesktop() { |
|
0 commit comments