Monorepo for Aesthetic.Computer
aesthetic.computer
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>