(function () { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; if (!window.WebGLRenderingContext || !window.THREE) return; const canvas = document.getElementById('scene'); if (!canvas) return; const root = document.documentElement; const readVar = (name) => getComputedStyle(root).getPropertyValue(name).trim(); const readOpacity = () => parseFloat(readVar('--scene-opacity')) || 0.18; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 ); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight, false); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); const geometry = new THREE.TorusKnotGeometry(2.5, 0.4, 130, 20); const material = new THREE.MeshPhongMaterial({ color: readVar('--accent') || '#c03a28', wireframe: true, transparent: true, opacity: readOpacity() }); const core = new THREE.Mesh(geometry, material); scene.add(core); scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dir = new THREE.DirectionalLight(0xffffff, 0.8); dir.position.set(5, 5, 5); scene.add(dir); const BASE_Z = 9; camera.position.z = BASE_Z; let scrollY = window.scrollY || 0; window.addEventListener('scroll', () => { scrollY = window.scrollY || 0; }, { passive: true }); let baseOpacity = readOpacity(); let running = true; function tick() { if (!running) return; requestAnimationFrame(tick); // Continuous slow drift. core.rotation.y += 0.0015; core.rotation.z += 0.0006; // Scroll-driven motion: zoom in, scale up, tilt. const s = Math.min(scrollY, 2000); camera.position.z = BASE_Z - s * 0.0022; const scale = 1 + s * 0.00035; core.scale.set(scale, scale, scale); core.rotation.x = s * 0.0008; // Fade past hero so feature cards stay readable. const vh = window.innerHeight; const fadeStart = vh * 0.5; const fadeEnd = vh * 1.4; const t = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart))); material.opacity = baseOpacity * (1 - t * 0.92); renderer.render(scene, camera); } tick(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight, false); }); document.addEventListener('visibilitychange', () => { if (document.hidden) { running = false; } else if (!running) { running = true; tick(); } }); const mql = window.matchMedia('(prefers-color-scheme: dark)'); const updateTheme = () => { const accent = readVar('--accent'); if (accent) material.color.set(accent); baseOpacity = readOpacity(); }; if (mql.addEventListener) { mql.addEventListener('change', updateTheme); } else if (mql.addListener) { mql.addListener(updateTheme); } })();