1/* ============================================================ 2 scene.js â 3D animated background using Three.js 3 A drifting field of neon particles + a slowly rotating 4 wireframe icosahedron. Degrades gracefully if WebGL/Three 5 isn't available, and respects "reduce motion". 6 ============================================================ */ 7(function () { 8 const canvas = document.getElementById('bg-canvas'); 9 if (!canvas) return; 10 11 // Three.js is loaded on demand by a small loader in index.html (skipped on 12 // mobile / reduced-motion / data-saver). It may not be present yet, so we 13 // define everything up front and only render once start() confirms THREE. 14 15 const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 16 17 let renderer, scene, camera, particles, shape, frameId; 18 const mouse = { x: 0, y: 0 }; 19 20 function init() { 21 renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); 22 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 23 renderer.setSize(window.innerWidth, window.innerHeight); 24 25 scene = new THREE.Scene(); 26 camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000); 27 camera.position.z = 60; 28 29 // ---- Particle field ---- 30 const count = window.innerWidth < 768 ? 900 : 1800; 31 const positions = new Float32Array(count * 3); 32 const colors = new Float32Array(count * 3); 33 const palette = [ 34 new THREE.Color('#7c3aed'), 35 new THREE.Color('#ec4899'), 36 new THREE.Color('#06b6d4'), 37 ]; 38 39 for (let i = 0; i < count; i++) { 40 positions[i * 3] = (Math.random() - 0.5) * 200; 41 positions[i * 3 + 1] = (Math.random() - 0.5) * 140; 42 positions[i * 3 + 2] = (Math.random() - 0.5) * 160; 43 const c = palette[Math.floor(Math.random() * palette.length)]; 44 colors[i * 3] = c.r; colors[i * 3 + 1] = c.g; colors[i * 3 + 2] = c.b; 45 } 46 47 const geo = new THREE.BufferGeometry(); 48 geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 49 geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 50 51 const mat = new THREE.PointsMaterial({ 52 size: 1.1, 53 vertexColors: true, 54 transparent: true, 55 opacity: 0.85, 56 blending: THREE.AdditiveBlending, 57 depthWrite: false, 58 }); 59 particles = new THREE.Points(geo, mat); 60 scene.add(particles); 61 62 // ---- Central wireframe icosahedron ---- 63 const shapeGeo = new THREE.IcosahedronGeometry(18, 1); 64 const shapeMat = new THREE.MeshBasicMaterial({ 65 color: '#7c3aed', 66 wireframe: true, 67 transparent: true, 68 opacity: 0.18, 69 }); 70 shape = new THREE.Mesh(shapeGeo, shapeMat); 71 scene.add(shape); 72 73 window.addEventListener('resize', onResize); 74 window.addEventListener('mousemove', onMouseMove); 75 76 animate(); 77 } 78 79 function onResize() { 80 camera.aspect = window.innerWidth / window.innerHeight; 81 camera.updateProjectionMatrix(); 82 renderer.setSize(window.innerWidth, window.innerHeight); 83 } 84 85 function onMouseMove(e) { 86 mouse.x = (e.clientX / window.innerWidth) * 2 - 1; 87 mouse.y = (e.clientY / window.innerHeight) * 2 - 1; 88 } 89 90 function animate() { 91 frameId = requestAnimationFrame(animate); 92 const t = Date.now() * 0.0001; 93 94 if (particles) { 95 particles.rotation.y = t * 1.4; 96 particles.rotation.x = t * 0.6; 97 } 98 if (shape) { 99 shape.rotation.x += 0.0012; 100 shape.rotation.y += 0.0018; 101 } 102 103 // Gentle parallax toward the cursor. 104 camera.position.x += (mouse.x * 8 - camera.position.x) * 0.03; 105 camera.position.y += (-mouse.y * 6 - camera.position.y) * 0.03; 106 camera.lookAt(scene.position); 107 108 renderer.render(scene, camera); 109 } 110 111 // Render a single static frame if the user prefers reduced motion.
112 function renderStatic() { 113 init(); 114 if (frameId) cancelAnimationFrame(frameId); 115 renderer.render(scene, camera); 116 } 117 118 let started = false; 119 function start() { 120 if (started || typeof THREE === 'undefined') return; 121 started = true; 122 try { 123 if (reduceMotion) renderStatic(); 124 else init(); 125 } catch (err) { 126 console.warn('3D background failed to start:', err); 127 } 128 } 129 130 // The loader in index.html calls this once Three.js finishes downloading. 131 // If Three.js is somehow already present, start immediately. 132 window.__qraftStartScene = start; 133 start(); 134})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.