Monorepo for Aesthetic.Computer aesthetic.computer
at main 498 lines 16 kB view raw
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.0"> 6 <title>Aesthetic Computer - 3D View</title> 7 <style> 8 * { margin: 0; padding: 0; box-sizing: border-box; } 9 html, body { 10 width: 100%; 11 height: 100%; 12 overflow: hidden; 13 background: transparent !important; 14 -webkit-user-select: none; 15 } 16 17 /* Drag handle for frameless window - top bar only */ 18 #drag-handle { 19 position: fixed; 20 top: 0; 21 left: 0; 22 right: 0; 23 height: 28px; 24 -webkit-app-region: drag; 25 z-index: 100; 26 } 27 28 /* Canvas container */ 29 #canvas-container { 30 position: absolute; 31 top: 0; left: 0; right: 0; bottom: 0; 32 z-index: 10; 33 } 34 35 canvas { 36 display: block; 37 width: 100%; 38 height: 100%; 39 } 40 41 /* Mode indicator - shows briefly on flip */ 42 #mode-indicator { 43 position: fixed; 44 bottom: 30px; 45 left: 50%; 46 transform: translateX(-50%); 47 background: rgba(0, 0, 0, 0.8); 48 color: #fff; 49 padding: 10px 24px; 50 border-radius: 25px; 51 font-family: system-ui, sans-serif; 52 font-size: 16px; 53 opacity: 0; 54 transition: opacity 0.3s ease; 55 z-index: 1000; 56 pointer-events: none; 57 border: 1px solid rgba(255, 255, 255, 0.1); 58 } 59 60 #mode-indicator.visible { 61 opacity: 1; 62 } 63 64 /* Hint text */ 65 #hint { 66 position: fixed; 67 top: 8px; 68 left: 50%; 69 transform: translateX(-50%); 70 color: rgba(255, 255, 255, 0.35); 71 font-family: system-ui, sans-serif; 72 font-size: 11px; 73 z-index: 1000; 74 pointer-events: none; 75 opacity: 1; 76 transition: opacity 2s ease; 77 } 78 79 #hint.hidden { 80 opacity: 0; 81 } 82 </style> 83</head> 84<body> 85 <div id="drag-handle"></div> 86 <div id="canvas-container"></div> 87 <div id="hint">Click edges to flip · Click center to interact · Scroll to zoom</div> 88 <div id="mode-indicator">⚡ Front</div> 89 90 <script type="module"> 91 import * as THREE from '../node_modules/three/build/three.module.js'; 92 93 const { ipcRenderer } = require('electron'); 94 95 // ========== Three.js Setup ========== 96 const container = document.getElementById('canvas-container'); 97 const scene = new THREE.Scene(); 98 scene.background = null; // Transparent background 99 100 const camera = new THREE.PerspectiveCamera( 101 50, 102 window.innerWidth / window.innerHeight, 103 0.1, 104 1000 105 ); 106 camera.position.z = 2.8; 107 108 const renderer = new THREE.WebGLRenderer({ 109 antialias: true, 110 alpha: true, 111 premultipliedAlpha: false 112 }); 113 renderer.setSize(window.innerWidth, window.innerHeight); 114 renderer.setPixelRatio(window.devicePixelRatio); 115 renderer.setClearColor(0x000000, 0); // Transparent 116 container.appendChild(renderer.domElement); 117 118 // ========== Card Geometry ========== 119 const aspect = 16 / 10; 120 const cardWidth = 2.4; 121 const cardHeight = cardWidth / aspect; 122 123 // Margin size for flip zone (0-1 UV space, 0.1 = 10% from edges) 124 const MARGIN_SIZE = 0.12; 125 126 // Offscreen texture dimensions 127 const TEX_WIDTH = 1280; 128 const TEX_HEIGHT = 800; 129 130 // Front texture placeholder 131 const frontCanvas = document.createElement('canvas'); 132 frontCanvas.width = TEX_WIDTH; 133 frontCanvas.height = TEX_HEIGHT; 134 const frontCtx = frontCanvas.getContext('2d'); 135 frontCtx.fillStyle = '#0a0a15'; 136 frontCtx.fillRect(0, 0, frontCanvas.width, frontCanvas.height); 137 frontCtx.fillStyle = '#fff'; 138 frontCtx.font = '32px system-ui'; 139 frontCtx.textAlign = 'center'; 140 frontCtx.fillText('⚡ Loading...', frontCanvas.width / 2, frontCanvas.height / 2); 141 const frontTexture = new THREE.CanvasTexture(frontCanvas); 142 143 // Back texture placeholder 144 const backCanvas = document.createElement('canvas'); 145 backCanvas.width = TEX_WIDTH; 146 backCanvas.height = TEX_HEIGHT; 147 const backCtx = backCanvas.getContext('2d'); 148 backCtx.fillStyle = '#0a0012'; 149 backCtx.fillRect(0, 0, backCanvas.width, backCanvas.height); 150 backCtx.fillStyle = '#fff'; 151 backCtx.font = '32px system-ui'; 152 backCtx.textAlign = 'center'; 153 backCtx.fillText('🩸 Terminal', backCanvas.width / 2, backCanvas.height / 2); 154 const backTexture = new THREE.CanvasTexture(backCanvas); 155 156 // Semi-transparent materials - see through to other side 157 const frontMaterial = new THREE.MeshBasicMaterial({ 158 map: frontTexture, 159 side: THREE.FrontSide, 160 transparent: true, 161 opacity: 0.88 162 }); 163 164 const backMaterial = new THREE.MeshBasicMaterial({ 165 map: backTexture, 166 side: THREE.BackSide, 167 transparent: true, 168 opacity: 0.88 169 }); 170 171 const cardGeometry = new THREE.PlaneGeometry(cardWidth, cardHeight); 172 173 // Front and back meshes 174 const frontMesh = new THREE.Mesh(cardGeometry, frontMaterial); 175 const backMesh = new THREE.Mesh(cardGeometry, backMaterial); 176 177 // Card group 178 const card = new THREE.Group(); 179 card.add(frontMesh); 180 card.add(backMesh); 181 scene.add(card); 182 183 // ========== Edge Glow Effect ========== 184 const edgeGeometry = new THREE.EdgesGeometry(cardGeometry); 185 const edgeMaterial = new THREE.LineBasicMaterial({ 186 color: 0x8844ff, 187 transparent: true, 188 opacity: 0.4 189 }); 190 const edges = new THREE.LineSegments(edgeGeometry, edgeMaterial); 191 card.add(edges); 192 193 // ========== Animation State ========== 194 let targetRotation = 0; 195 let currentRotation = 0; 196 let showingBack = false; 197 let isInMargin = false; 198 let isInCenter = false; 199 let lastUV = null; 200 201 const modeIndicator = document.getElementById('mode-indicator'); 202 const hint = document.getElementById('hint'); 203 204 // Hide hint after first interaction 205 let hintShown = true; 206 function hideHint() { 207 if (hintShown) { 208 hint.classList.add('hidden'); 209 hintShown = false; 210 } 211 } 212 213 // ========== Raycaster for Mouse Picking ========== 214 const raycaster = new THREE.Raycaster(); 215 const mouse = new THREE.Vector2(); 216 217 function updateMouse(event) { 218 mouse.x = (event.clientX / window.innerWidth) * 2 - 1; 219 mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 220 } 221 222 function checkIntersection() { 223 raycaster.setFromCamera(mouse, camera); 224 const intersects = raycaster.intersectObjects([frontMesh, backMesh]); 225 return intersects.length > 0 ? intersects[0] : null; 226 } 227 228 // Check if UV is in the margin zone (edges) 229 function isInMarginZone(uv) { 230 if (!uv) return false; 231 return uv.x < MARGIN_SIZE || uv.x > (1 - MARGIN_SIZE) || 232 uv.y < MARGIN_SIZE || uv.y > (1 - MARGIN_SIZE); 233 } 234 235 // Convert UV to offscreen pixel coordinates 236 function uvToPixel(uv, width, height) { 237 return { 238 x: Math.floor(uv.x * width), 239 y: Math.floor((1 - uv.y) * height) // Flip Y 240 }; 241 } 242 243 // ========== Mouse Events ========== 244 renderer.domElement.addEventListener('mousemove', (e) => { 245 updateMouse(e); 246 const hit = checkIntersection(); 247 248 if (hit && hit.uv) { 249 lastUV = hit.uv.clone(); 250 const inMargin = isInMarginZone(hit.uv); 251 252 if (inMargin) { 253 // In margin - show flip cursor 254 isInMargin = true; 255 isInCenter = false; 256 edgeMaterial.opacity = 0.7; 257 edgeMaterial.color.setHex(0xaa66ff); 258 renderer.domElement.style.cursor = 'pointer'; 259 } else { 260 // In center - show interact cursor 261 isInMargin = false; 262 isInCenter = true; 263 edgeMaterial.opacity = 0.3; 264 edgeMaterial.color.setHex(0x8844ff); 265 renderer.domElement.style.cursor = 'crosshair'; 266 267 // Forward mouse move to offscreen window 268 const target = showingBack ? 'back' : 'front'; 269 const pixel = uvToPixel(hit.uv, TEX_WIDTH, TEX_HEIGHT); 270 ipcRenderer.send('forward-mouse', { 271 target, 272 type: 'mouseMove', 273 x: pixel.x, 274 y: pixel.y 275 }); 276 } 277 } else { 278 isInMargin = false; 279 isInCenter = false; 280 lastUV = null; 281 edgeMaterial.opacity = 0.3; 282 edgeMaterial.color.setHex(0x8844ff); 283 renderer.domElement.style.cursor = 'default'; 284 } 285 }); 286 287 renderer.domElement.addEventListener('mousedown', (e) => { 288 updateMouse(e); 289 const hit = checkIntersection(); 290 291 if (hit && hit.uv) { 292 hideHint(); 293 294 if (isInMarginZone(hit.uv)) { 295 // Clicked margin = FLIP 296 showingBack = !showingBack; 297 targetRotation = showingBack ? Math.PI : 0; 298 299 modeIndicator.textContent = showingBack ? '🩸 Back' : '⚡ Front'; 300 modeIndicator.classList.add('visible'); 301 setTimeout(() => modeIndicator.classList.remove('visible'), 1200); 302 } else { 303 // Clicked center = forward click to offscreen 304 const target = showingBack ? 'back' : 'front'; 305 const pixel = uvToPixel(hit.uv, TEX_WIDTH, TEX_HEIGHT); 306 ipcRenderer.send('forward-mouse', { 307 target, 308 type: 'mouseDown', 309 x: pixel.x, 310 y: pixel.y, 311 button: 'left', 312 clickCount: 1 313 }); 314 } 315 } 316 }); 317 318 renderer.domElement.addEventListener('mouseup', (e) => { 319 if (isInCenter && lastUV) { 320 const target = showingBack ? 'back' : 'front'; 321 const pixel = uvToPixel(lastUV, TEX_WIDTH, TEX_HEIGHT); 322 ipcRenderer.send('forward-mouse', { 323 target, 324 type: 'mouseUp', 325 x: pixel.x, 326 y: pixel.y, 327 button: 'left', 328 clickCount: 1 329 }); 330 } 331 }); 332 333 // Scroll to zoom 334 renderer.domElement.addEventListener('wheel', (e) => { 335 e.preventDefault(); 336 hideHint(); 337 338 if (isInCenter && lastUV) { 339 // Forward scroll to offscreen window 340 const target = showingBack ? 'back' : 'front'; 341 const pixel = uvToPixel(lastUV, TEX_WIDTH, TEX_HEIGHT); 342 ipcRenderer.send('forward-mouse', { 343 target, 344 type: 'mouseWheel', 345 x: pixel.x, 346 y: pixel.y, 347 button: e.deltaY // Use button field for delta 348 }); 349 } else { 350 // Zoom camera 351 camera.position.z = Math.max(1.5, Math.min(5, camera.position.z + e.deltaY * 0.003)); 352 } 353 }, { passive: false }); 354 355 // Keyboard input - forward to offscreen or use as shortcuts 356 document.addEventListener('keydown', (e) => { 357 // Tab = flip shortcut 358 if (e.key === 'Tab') { 359 e.preventDefault(); 360 hideHint(); 361 showingBack = !showingBack; 362 targetRotation = showingBack ? Math.PI : 0; 363 modeIndicator.textContent = showingBack ? '🩸 Back' : '⚡ Front'; 364 modeIndicator.classList.add('visible'); 365 setTimeout(() => modeIndicator.classList.remove('visible'), 1200); 366 return; 367 } 368 369 // Escape = reset view 370 if (e.key === 'Escape') { 371 targetRotation = 0; 372 showingBack = false; 373 camera.position.z = 2.8; 374 return; 375 } 376 377 // Forward all other keys to the appropriate offscreen window 378 const target = showingBack ? 'back' : 'front'; 379 const modifiers = []; 380 if (e.metaKey) modifiers.push('meta'); 381 if (e.ctrlKey) modifiers.push('control'); 382 if (e.altKey) modifiers.push('alt'); 383 if (e.shiftKey) modifiers.push('shift'); 384 385 // For terminal (back), send directly to PTY 386 if (showingBack) { 387 // Convert key to PTY-compatible string 388 let ptyKey = e.key; 389 if (e.key === 'Enter') ptyKey = '\r'; 390 else if (e.key === 'Backspace') ptyKey = '\x7f'; 391 else if (e.key === 'ArrowUp') ptyKey = '\x1b[A'; 392 else if (e.key === 'ArrowDown') ptyKey = '\x1b[B'; 393 else if (e.key === 'ArrowRight') ptyKey = '\x1b[C'; 394 else if (e.key === 'ArrowLeft') ptyKey = '\x1b[D'; 395 else if (e.ctrlKey && e.key.length === 1) { 396 // Ctrl+letter = control character 397 ptyKey = String.fromCharCode(e.key.toUpperCase().charCodeAt(0) - 64); 398 } else if (e.key.length > 1) { 399 return; // Skip function keys, etc. 400 } 401 402 ipcRenderer.send('forward-pty-input', ptyKey); 403 } else { 404 // Forward to front window 405 ipcRenderer.send('forward-key', { 406 target, 407 type: 'keyDown', 408 keyCode: e.key, 409 modifiers 410 }); 411 } 412 }); 413 414 // ========== Offscreen Render Updates ========== 415 let frontDataTexture = null; 416 let backDataTexture = null; 417 418 ipcRenderer.on('front-frame', (event, frame) => { 419 updateDataTexture('front', frame, frontMaterial); 420 }); 421 422 ipcRenderer.on('back-frame', (event, frame) => { 423 updateDataTexture('back', frame, backMaterial); 424 }); 425 426 function updateDataTexture(side, frame, material) { 427 const { width, height, data } = frame; 428 if (!width || !height || width <= 0 || height <= 0) return; 429 430 const pixels = data instanceof Uint8Array ? data : new Uint8Array(data); 431 432 if (side === 'front') { 433 if (!frontDataTexture || frontDataTexture.image.width !== width) { 434 frontDataTexture = new THREE.DataTexture(pixels, width, height, THREE.RGBAFormat); 435 frontDataTexture.flipY = true; 436 frontDataTexture.needsUpdate = true; 437 material.map = frontDataTexture; 438 material.needsUpdate = true; 439 } else { 440 frontDataTexture.image.data.set(pixels); 441 frontDataTexture.needsUpdate = true; 442 } 443 } else { 444 if (!backDataTexture || backDataTexture.image.width !== width) { 445 backDataTexture = new THREE.DataTexture(pixels, width, height, THREE.RGBAFormat); 446 backDataTexture.flipY = true; 447 backDataTexture.needsUpdate = true; 448 material.map = backDataTexture; 449 material.needsUpdate = true; 450 } else { 451 backDataTexture.image.data.set(pixels); 452 backDataTexture.needsUpdate = true; 453 } 454 } 455 } 456 457 // Request offscreen windows to start 458 ipcRenderer.send('start-offscreen-rendering'); 459 460 // ========== Animation Loop ========== 461 let lastTime = performance.now(); 462 463 function animate() { 464 requestAnimationFrame(animate); 465 466 const now = performance.now(); 467 const delta = (now - lastTime) / 1000; 468 lastTime = now; 469 470 // Smooth rotation interpolation 471 const rotationSpeed = 6; 472 currentRotation += (targetRotation - currentRotation) * Math.min(delta * rotationSpeed, 1); 473 card.rotation.y = currentRotation; 474 475 // Subtle floating effect 476 card.position.y = Math.sin(now / 1500) * 0.01; 477 478 // Edge pulse when in margin zone 479 if (isInMargin) { 480 edgeMaterial.opacity = 0.5 + Math.sin(now / 150) * 0.2; 481 } 482 483 renderer.render(scene, camera); 484 } 485 486 animate(); 487 488 // ========== Resize Handler ========== 489 window.addEventListener('resize', () => { 490 camera.aspect = window.innerWidth / window.innerHeight; 491 camera.updateProjectionMatrix(); 492 renderer.setSize(window.innerWidth, window.innerHeight); 493 }); 494 495 console.log('3D View initialized - click edges to flip, center to interact'); 496 </script> 497</body> 498</html>