import * as THREE from './vendor/three.module.min.js'; const root = document.getElementById('heroWebgl'); const canvas = document.getElementById('heroWebglCanvas'); if (root && canvas) { const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; const lowPower = window.innerWidth < 640 || Boolean(connection && connection.saveData) || (navigator.deviceMemory && navigator.deviceMemory <= 2); const staticMode = reducedMotion || lowPower; let renderer; try { renderer = new THREE.WebGLRenderer({ canvas, alpha: false, antialias: !lowPower, depth: true, stencil: false, powerPreference: staticMode ? 'low-power' : 'high-performance' }); } catch (error) { root.classList.add('has-error'); } if (renderer) { renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.96; renderer.shadowMap.enabled = !lowPower; renderer.shadowMap.type = THREE.PCFShadowMap; renderer.autoClear = false; const resolvedTheme = document.documentElement.dataset.theme || 'light'; const initialDark = resolvedTheme === 'dark' ? 1 : 0; const uniforms = { uTime: { value: 0 }, uDark: { value: initialDark }, uResolution: { value: new THREE.Vector2(1, 1) }, uMouse: { value: new THREE.Vector2(0.72, 0.4) } }; const backgroundScene = new THREE.Scene(); const backgroundCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const backgroundMaterial = new THREE.ShaderMaterial({ uniforms, depthTest: false, depthWrite: false, toneMapped: false, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); } `, fragmentShader: ` precision highp float; varying vec2 vUv; uniform float uTime; uniform float uDark; uniform vec2 uResolution; uniform vec2 uMouse; float hash21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } float segmentDistance(vec2 p, vec2 a, vec2 b) { vec2 pa = p - a; vec2 ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h); } float beam(vec2 p, vec2 a, vec2 b, float width) { return exp(-segmentDistance(p, a, b) / width); } void main() { vec2 uv = vUv; float aspect = uResolution.x / max(uResolution.y, 1.0); vec2 p = uv - 0.5; p.x *= aspect; float t = uTime; vec3 lightTop = vec3(0.992, 0.997, 1.0); vec3 lightBottom = vec3(0.88, 0.93, 1.0); vec3 darkTop = vec3(0.016, 0.027, 0.044); vec3 darkBottom = vec3(0.025, 0.055, 0.08); vec3 color = mix(mix(lightBottom, lightTop, uv.y), mix(darkBottom, darkTop, uv.y), uDark); vec2 focus = vec2(aspect * 0.18 + (uMouse.x - 0.5) * 0.035, -0.12 + (0.5 - uMouse.y) * 0.025); float halo = exp(-length((p - focus) / vec2(0.48, 0.42)) * 2.2); color += mix(vec3(0.19, 0.52, 1.0), vec3(0.06, 0.42, 0.72), uDark) * halo * mix(0.12, 0.23, uDark); float skyWash = exp(-length((p - vec2(aspect * 0.37, 0.39)) / vec2(0.52, 0.34)) * 2.0); color += mix(vec3(0.2, 0.43, 0.78), vec3(0.04, 0.18, 0.36), uDark) * skyWash * mix(0.11, 0.14, uDark); vec2 leftSource = vec2(-aspect * 0.58, -0.38); vec2 rightTarget = vec2(aspect * 0.62, -0.19); float cyanBeam = beam(p, leftSource, focus + vec2(-0.06, -0.08), 0.009); float cyanGlow = beam(p, leftSource, focus + vec2(-0.06, -0.08), 0.055); float warmBeam = beam(p, focus + vec2(0.04, 0.02), rightTarget, 0.008); float warmGlow = beam(p, focus + vec2(0.04, 0.02), rightTarget, 0.052); color += vec3(0.18, 0.88, 1.0) * (cyanBeam * 1.48 + cyanGlow * 0.34); color += vec3(1.0, 0.58, 0.18) * (warmBeam * 2.05 + warmGlow * 0.4); float spectrumA = beam(p, leftSource + vec2(0.0, 0.025), focus + vec2(0.1, -0.025), 0.0045); float spectrumB = beam(p, focus + vec2(-0.02, -0.03), rightTarget + vec2(0.0, -0.045), 0.0045); color += vec3(0.55, 0.42, 1.0) * spectrumA * 0.24; color += vec3(0.96, 0.35, 0.72) * spectrumB * 0.18; float fiberFlow = 0.0; for (int index = 0; index < 7; index++) { float offset = (float(index) - 3.0) * 0.012; fiberFlow += beam( p, leftSource + vec2(0.0, offset), focus + vec2(-0.07, -0.075 + offset * 0.45), 0.0022 ); } color += vec3(0.22, 0.92, 1.0) * fiberFlow * mix(0.12, 0.2, uDark); vec2 causticOrigin = focus + vec2(-0.055, -0.16); vec2 causticVector = p - causticOrigin; float causticDistance = length(causticVector); float causticAngle = atan(causticVector.y, causticVector.x); float causticBands = pow(max(0.0, sin(causticAngle * 18.0 + causticDistance * 5.0 - t * 0.18)), 18.0); float causticMask = smoothstep(0.05, 0.22, causticDistance) * exp(-causticDistance * 1.75) * smoothstep(0.1, -0.25, p.y); color += mix(vec3(0.36, 0.78, 1.0), vec3(0.68, 0.34, 1.0), smoothstep(-1.2, 0.5, causticAngle)) * causticBands * causticMask * mix(0.34, 0.44, uDark); float floorMask = smoothstep(0.06, -0.28, p.y); vec2 gp = p - vec2(0.0, -0.24); float horizontal = 1.0 - smoothstep(0.0, 0.018, abs(fract((gp.y + 0.55) * 18.0) - 0.5)); float perspectiveX = gp.x / max(0.12, abs(gp.y - 0.34)); float vertical = 1.0 - smoothstep(0.0, 0.025, abs(fract(perspectiveX * 3.2) - 0.5)); float grid = (horizontal * 0.48 + vertical * 0.35) * floorMask; color += mix(vec3(0.12, 0.34, 0.68), vec3(0.08, 0.48, 0.58), uDark) * grid * mix(0.2, 0.22, uDark); float dust = step(0.993, hash21(floor(p * 190.0 + t * 0.4))) * floorMask; color += vec3(0.35, 0.9, 1.0) * dust * 0.28; color += (hash21(p * 210.0) - 0.5) * mix(0.008, 0.014, uDark); float vignette = smoothstep(1.25, 0.2, length((uv - 0.5) * vec2(0.82, 1.0))); color *= mix(0.95, 1.0, vignette); gl_FragColor = vec4(color, 1.0); } ` }); const backgroundGeometry = new THREE.PlaneGeometry(2, 2); backgroundScene.add(new THREE.Mesh(backgroundGeometry, backgroundMaterial)); const worldScene = new THREE.Scene(); const worldCamera = new THREE.PerspectiveCamera(33, 1, 0.1, 50); worldCamera.position.set(0, 0.12, 9.4); const hemisphereLight = new THREE.HemisphereLight(0xe8f8ff, 0x0c1830, 1.8); const keyLight = new THREE.DirectionalLight(0xffffff, 5.2); keyLight.position.set(-3.8, 5.6, 5.8); keyLight.castShadow = !lowPower; keyLight.shadow.mapSize.set(lowPower ? 512 : 1024, lowPower ? 512 : 1024); keyLight.shadow.camera.near = 0.5; keyLight.shadow.camera.far = 20; keyLight.shadow.camera.left = -5; keyLight.shadow.camera.right = 5; keyLight.shadow.camera.top = 5; keyLight.shadow.camera.bottom = -5; const cyanLight = new THREE.PointLight(0x62dfff, 22, 15, 2); cyanLight.position.set(-3.2, -0.5, 4.4); const blueLight = new THREE.PointLight(0x4e78ff, 16, 13, 2); blueLight.position.set(3.5, 2.1, 3.2); const warmLight = new THREE.PointLight(0xffb75e, 12, 12, 2); warmLight.position.set(3.2, -1.0, 4.8); worldScene.add(hemisphereLight, keyLight, cyanLight, blueLight, warmLight); const disposableGeometries = []; const disposableMaterials = []; const trackGeometry = function (geometry) { disposableGeometries.push(geometry); return geometry; }; const trackMaterial = function (material) { disposableMaterials.push(material); return material; }; const environmentScene = new THREE.Scene(); environmentScene.background = new THREE.Color(0x06162d); function addEnvironmentPanel(color, position, rotation, scale, intensity) { const panelGeometry = trackGeometry(new THREE.PlaneGeometry(scale[0], scale[1])); const panelMaterial = trackMaterial(new THREE.MeshBasicMaterial({ color: new THREE.Color(color).multiplyScalar(intensity || 1), side: THREE.DoubleSide })); const panel = new THREE.Mesh(panelGeometry, panelMaterial); panel.position.set(position[0], position[1], position[2]); panel.rotation.set(rotation[0], rotation[1], rotation[2]); environmentScene.add(panel); } addEnvironmentPanel(0xffffff, [0, 5.6, 0], [Math.PI / 2, 0, 0], [14, 14], 1.15); addEnvironmentPanel(0x3ceaff, [-5.4, -0.6, 0], [0, Math.PI / 2, 0], [4.2, 13], 1.55); addEnvironmentPanel(0x1e55cf, [5.2, 0.8, -0.8], [0, -Math.PI / 2, 0], [5, 12], 1.45); addEnvironmentPanel(0xffb663, [2.6, -1.2, 5], [0, 0, 0], [3.2, 9], 1.5); addEnvironmentPanel(0x7b62ff, [-2.2, 0.2, -5], [0, 0, 0], [4, 10], 1.35); addEnvironmentPanel(0xffffff, [-1.8, 1.2, 4.8], [0, 0, 0], [0.8, 9], 1.65); const pmremGenerator = new THREE.PMREMGenerator(renderer); pmremGenerator.compileCubemapShader(); const environmentTarget = pmremGenerator.fromScene(environmentScene, lowPower ? 0.035 : 0.025, 0.1, 30); pmremGenerator.dispose(); worldScene.environment = environmentTarget.texture; const crystalShape = new THREE.Shape(); crystalShape.moveTo(-1.43, -1.48); crystalShape.lineTo(1.36, -1.48); crystalShape.lineTo(0.06, 2.08); crystalShape.closePath(); const crystalGeometry = trackGeometry(new THREE.ExtrudeGeometry(crystalShape, { depth: 1.02, bevelEnabled: true, bevelSegments: lowPower ? 1 : 4, bevelSize: 0.065, bevelThickness: 0.1, curveSegments: 1, steps: 1 })); crystalGeometry.translate(0, -0.22, -0.51); const glassFrontMaterial = trackMaterial(new THREE.MeshPhysicalMaterial({ color: 0xc8eaff, roughness: 0.008, metalness: 0, transmission: 0, thickness: 2.8, ior: 1.54, dispersion: lowPower ? 0.06 : 0.19, attenuationColor: new THREE.Color(0x62b8ef), attenuationDistance: 2.6, clearcoat: 1, clearcoatRoughness: 0.006, iridescence: lowPower ? 0.08 : 0.24, iridescenceIOR: 1.34, envMapIntensity: 0.65, specularIntensity: 1, specularColor: new THREE.Color(0xffffff), transparent: true, opacity: 0.055, side: THREE.DoubleSide, depthWrite: false })); glassFrontMaterial.userData.lightColor = new THREE.Color(0xc8eaff); glassFrontMaterial.userData.darkColor = new THREE.Color(0x8ed2f4); glassFrontMaterial.userData.lightOpacity = 0.055; glassFrontMaterial.userData.darkOpacity = 0.1; glassFrontMaterial.userData.lightEnv = 0.65; glassFrontMaterial.userData.darkEnv = 1.1; const glassSideMaterial = trackMaterial(new THREE.MeshPhysicalMaterial({ color: 0x2766ad, roughness: 0.015, metalness: 0.06, transmission: 0.4, thickness: 3.2, ior: 1.52, dispersion: lowPower ? 0.04 : 0.13, attenuationColor: new THREE.Color(0x164998), attenuationDistance: 1.45, clearcoat: 1, clearcoatRoughness: 0.008, iridescence: lowPower ? 0.04 : 0.16, iridescenceIOR: 1.3, envMapIntensity: 4.5, emissive: new THREE.Color(0x0d2e78), emissiveIntensity: 0.12, transparent: true, opacity: 0.94, side: THREE.DoubleSide, depthWrite: false })); glassSideMaterial.userData.lightColor = new THREE.Color(0x2766ad); glassSideMaterial.userData.darkColor = new THREE.Color(0x1d57a6); glassSideMaterial.userData.lightOpacity = 0.94; glassSideMaterial.userData.darkOpacity = 0.97; glassSideMaterial.userData.lightEnv = 4.5; glassSideMaterial.userData.darkEnv = 5.3; const crystalMaterials = [glassFrontMaterial, glassSideMaterial]; const crystalMesh = new THREE.Mesh(crystalGeometry, crystalMaterials); crystalMesh.castShadow = !lowPower; crystalMesh.receiveShadow = true; crystalMesh.renderOrder = 1; function createCrystalFacet(points, color, opacity, additive, z) { const shape = new THREE.Shape(); shape.moveTo(points[0][0], points[0][1]); points.slice(1).forEach(function (point) { shape.lineTo(point[0], point[1]); }); shape.closePath(); const geometry = trackGeometry(new THREE.ShapeGeometry(shape)); const material = trackMaterial(new THREE.MeshBasicMaterial({ color, transparent: true, opacity, side: THREE.DoubleSide, depthTest: false, depthWrite: false, toneMapped: false, blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending })); material.userData.lightOpacity = opacity; material.userData.darkOpacity = Math.min(0.68, opacity * 1.28); const facet = new THREE.Mesh(geometry, material); facet.position.z = z || 0.625; facet.renderOrder = additive ? 4 : 3; return { facet, material }; } const crystalFacets = [ createCrystalFacet([[-1.39, -1.65], [1.31, -1.65], [0.05, 1.86]], 0x93d7f1, 0.23, false, 0.615), createCrystalFacet([[-1.36, -1.68], [-0.9, -1.68], [0.05, 1.83], [-0.17, 1.44]], 0x129fd8, 0.42, false), createCrystalFacet([[0.05, 1.83], [0.3, 1.32], [1.29, -1.68], [0.84, -1.68]], 0x082f76, 0.6, false), createCrystalFacet([[-1.36, -1.68], [1.29, -1.68], [0.92, -1.23], [-1.02, -1.23]], 0x2d6fae, 0.38, false), createCrystalFacet([[0.02, 1.68], [0.25, 1.22], [-0.1, 0.84], [-0.25, 1.27]], 0xf8fdff, 0.18, true, 0.64), createCrystalFacet([[-0.25, 1.27], [-1.2, -1.34], [-0.56, -0.61], [-0.1, 0.84]], 0x17a9df, 0.48, false), createCrystalFacet([[-1.23, -1.36], [-0.56, -0.61], [0.18, -1.38]], 0x168bc9, 0.44, false, 0.646), createCrystalFacet([[-0.1, 0.84], [-0.56, -0.61], [0.2, -1.39], [0.46, -0.02]], 0x4ba9dd, 0.46, false), createCrystalFacet([[-0.56, -0.61], [0.46, -0.02], [0.18, -1.38]], 0xbfefff, 0.28, false, 0.648), createCrystalFacet([[-0.72, -0.82], [0.04, 0.74], [0.38, 0.04], [-0.02, -0.72]], 0xd5f5ff, 0.34, false, 0.638), createCrystalFacet([[0.25, 1.22], [0.64, 0.55], [0.46, -0.02], [-0.1, 0.84]], 0xfff8e8, 0.18, true, 0.64), createCrystalFacet([[0.46, -0.02], [0.64, 0.55], [1.12, -1.34], [0.2, -1.39]], 0x0a376e, 0.54, false), createCrystalFacet([[-1.2, -1.34], [0.2, -1.39], [1.12, -1.34], [0.82, -1.04], [-0.86, -1.02]], 0x5acff4, 0.25, true, 0.64), createCrystalFacet([[0.3, 1.32], [0.64, 0.55], [1.12, -1.34], [1.29, -1.68]], 0x06265b, 0.44, false), createCrystalFacet([[0.38, 0.04], [0.72, -0.36], [1.04, -1.2], [0.18, -1.32]], 0xffc982, 0.13, true, 0.644), createCrystalFacet([[-0.17, 1.44], [-0.1, 0.84], [-0.56, -0.61], [-0.9, -1.68]], 0xa7edff, 0.16, true, 0.64), createCrystalFacet([[0.02, 1.68], [0.3, 1.32], [0.25, 1.22]], 0xffffff, 0.25, true, 0.645), createCrystalFacet([[0.02, 1.7], [0.29, 1.31], [0.13, 1.04], [-0.06, 1.35]], 0xff82c8, 0.16, true, 0.65), createCrystalFacet([[0.38, -0.03], [0.83, -0.54], [1.03, -1.16], [0.64, -0.8]], 0xffb867, 0.13, true, 0.651) ]; const reflectionMaterials = crystalFacets.map(function (entry) { return entry.material; }); const spectrumGeometry = trackGeometry(new THREE.ShapeGeometry(crystalShape)); spectrumGeometry.translate(0, -0.22, 0); const spectrumMaterial = trackMaterial(new THREE.ShaderMaterial({ uniforms: { uTime: uniforms.uTime, uDark: uniforms.uDark }, transparent: true, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending, vertexShader: ` varying vec2 vUv; varying vec3 vLocal; void main() { vUv = uv; vLocal = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` precision highp float; varying vec2 vUv; varying vec3 vLocal; uniform float uTime; uniform float uDark; vec3 spectrum(float t) { return 0.56 + 0.44 * cos(6.28318 * (vec3(0.02, 0.35, 0.68) + t)); } void main() { float diagonalA = exp(-abs(vLocal.y - (0.37 * vLocal.x - 0.64)) * 24.0); float diagonalB = exp(-abs(vLocal.y - (-0.22 * vLocal.x - 0.22)) * 34.0); float spectralShard = pow(max(0.0, sin(vLocal.x * 8.4 - vLocal.y * 5.7 + uTime * 0.42)), 44.0); float strength = diagonalA * 0.2 + diagonalB * 0.12 + spectralShard * 0.075; vec3 color = spectrum(vLocal.x * 0.16 + vLocal.y * 0.08 + uTime * 0.018); color += vec3(0.76, 0.94, 1.0) * diagonalA * 0.42; gl_FragColor = vec4(color * strength, strength * mix(0.66, 0.9, uDark)); } ` })); const spectrumMesh = new THREE.Mesh(spectrumGeometry, spectrumMaterial); spectrumMesh.position.z = 0.655; spectrumMesh.renderOrder = 5; spectrumMesh.userData.noReflection = true; function createLightStreak(start, end, color, width, opacity, z) { const deltaX = end[0] - start[0]; const deltaY = end[1] - start[1]; const length = Math.hypot(deltaX, deltaY); const group = new THREE.Group(); [ { scale: 7.2, alpha: opacity * 0.12, z: (z || 0.67) - 0.006 }, { scale: 2.8, alpha: opacity * 0.28, z: (z || 0.67) - 0.002 }, { scale: 1, alpha: opacity, z: z || 0.67 } ].forEach(function (layer) { const geometry = trackGeometry(new THREE.PlaneGeometry(length, width * layer.scale)); const material = trackMaterial(new THREE.MeshBasicMaterial({ color, transparent: true, opacity: layer.alpha, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide })); const beam = new THREE.Mesh(geometry, material); beam.position.set((start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, layer.z); beam.rotation.z = Math.atan2(deltaY, deltaX); beam.renderOrder = 6; group.add(beam); }); group.userData.noReflection = true; return group; } const cyanStreak = createLightStreak([-1.42, -1.03], [1.32, -0.16], 0x8df6ff, 0.011, 0.92); const warmStreak = createLightStreak([0.24, -0.13], [1.47, -0.4], 0xffc36c, 0.012, 0.82, 0.69); const violetStreak = createLightStreak([-0.38, -0.52], [0.64, -0.75], 0xb478ff, 0.009, 0.68, 0.69); const rainbowStreak = createLightStreak([-0.08, 0.42], [0.62, 0.04], 0xff78d7, 0.007, 0.48, 0.695); const sparklePositions = []; const sparkleCount = lowPower ? 26 : 74; for (let index = 0; index < sparkleCount; index += 1) { const seedA = Math.abs(Math.sin(index * 91.73 + 0.47)); const seedB = Math.abs(Math.sin(index * 37.19 + 1.21)); const y = -1.57 + seedA * 2.28; const widthAtY = Math.max(0.08, 1.18 * (1 - (y + 1.57) / 3.4)); sparklePositions.push((seedB * 2 - 1) * widthAtY, y, 0.69 + (index % 5) * 0.001); } const sparkleGeometry = trackGeometry(new THREE.BufferGeometry()); sparkleGeometry.setAttribute('position', new THREE.Float32BufferAttribute(sparklePositions, 3)); const sparkleMaterial = trackMaterial(new THREE.ShaderMaterial({ uniforms: { uTime: uniforms.uTime, uDark: uniforms.uDark }, transparent: true, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending, vertexShader: ` uniform float uTime; varying float vPulse; void main() { vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); vPulse = 0.55 + 0.45 * sin(uTime * 1.9 + position.x * 11.0 + position.y * 7.0); gl_PointSize = (0.075 + vPulse * 0.085) * (260.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; } `, fragmentShader: ` precision highp float; varying float vPulse; uniform float uDark; void main() { float d = length(gl_PointCoord - 0.5); float alpha = smoothstep(0.5, 0.06, d) * vPulse * mix(0.34, 0.72, uDark); vec3 color = mix(vec3(0.48, 0.88, 1.0), vec3(1.0, 0.72, 0.48), vPulse * 0.34); gl_FragColor = vec4(color, alpha); } ` })); const crystalSparkles = new THREE.Points(sparkleGeometry, sparkleMaterial); crystalSparkles.renderOrder = 7; crystalSparkles.userData.noReflection = true; const edgeGeometry = trackGeometry(new THREE.EdgesGeometry(crystalGeometry, 16)); const edgeMaterial = trackMaterial(new THREE.LineBasicMaterial({ color: 0xc5f3ff, transparent: true, depthTest: false, opacity: 0.72 })); const crystalEdges = new THREE.LineSegments(edgeGeometry, edgeMaterial); crystalEdges.renderOrder = 7; const glintGeometry = trackGeometry(new THREE.PlaneGeometry(1.15, 1.15)); const glintMaterial = trackMaterial(new THREE.ShaderMaterial({ uniforms: { uTime: uniforms.uTime, uDark: uniforms.uDark }, transparent: true, depthTest: false, depthWrite: false, blending: THREE.AdditiveBlending, vertexShader: `varying vec2 vUv; void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`, fragmentShader: ` precision highp float; varying vec2 vUv; uniform float uTime; uniform float uDark; void main(){ vec2 p=vUv-0.5; float core=exp(-length(p)*24.0); float cross=exp(-abs(p.x)*95.0)*exp(-abs(p.y)*4.5)+exp(-abs(p.y)*95.0)*exp(-abs(p.x)*4.5); float pulse=0.86+0.14*sin(uTime*1.6); vec3 color=mix(vec3(0.42,0.86,1.0),vec3(1.0,0.42,0.92),smoothstep(-0.2,0.18,p.x)); gl_FragColor=vec4(color*(core*1.7+cross*0.72)*pulse,(core+cross*0.54)*mix(0.72,1.0,uDark)); } ` })); const baseGlint = new THREE.Mesh(glintGeometry, glintMaterial); baseGlint.position.set(-0.18, -1.6, 0.86); baseGlint.scale.setScalar(0.94); baseGlint.renderOrder = 8; const crystalGroup = new THREE.Group(); crystalGroup.add( crystalMesh, ...crystalFacets.map(function (entry) { return entry.facet; }), spectrumMesh, cyanStreak, warmStreak, violetStreak, rainbowStreak, crystalSparkles, crystalEdges, baseGlint ); crystalGroup.rotation.set(0.045, -0.44, -0.018); // Preserve the authored, high-frequency refraction from the approved visual // while keeping it inside the live Three.js composition. The procedural // crystal underneath supplies parallax, glow, reflection and the dark theme. let referenceTexture = null; const referenceGeometry = trackGeometry(new THREE.PlaneGeometry(3.29, 3.7)); const referenceMaterial = trackMaterial(new THREE.MeshBasicMaterial({ transparent: true, opacity: initialDark ? 0.92 : 0.97, depthTest: false, depthWrite: false, toneMapped: false })); const referenceCrystal = new THREE.Mesh(referenceGeometry, referenceMaterial); referenceCrystal.position.set(-0.29, 0.05, 1.28); referenceCrystal.renderOrder = 24; referenceCrystal.userData.noReflection = true; const referenceReflectionMaterial = trackMaterial(new THREE.ShaderMaterial({ uniforms: { uMap: { value: null }, uOpacity: { value: initialDark ? 0.14 : 0.22 } }, transparent: true, depthTest: false, depthWrite: false, toneMapped: false, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` precision highp float; varying vec2 vUv; uniform sampler2D uMap; uniform float uOpacity; void main() { vec4 sampleColor = texture2D(uMap, vUv); float fade = pow(clamp(1.0 - vUv.y, 0.0, 1.0), 2.1); vec3 reflected = mix(sampleColor.rgb, vec3(0.56, 0.76, 1.0), 0.38); gl_FragColor = vec4(reflected, sampleColor.a * fade * uOpacity); } ` })); const referenceReflection = new THREE.Mesh(referenceGeometry, referenceReflectionMaterial); referenceReflection.position.set(-0.29, -3.06, 0.96); referenceReflection.scale.set(1, -0.68, 1); referenceReflection.renderOrder = 2; referenceReflection.visible = false; new THREE.TextureLoader().load( './assets/prism-reference-light.png', function (texture) { referenceTexture = texture; texture.colorSpace = THREE.SRGBColorSpace; texture.minFilter = THREE.LinearMipmapLinearFilter; texture.magFilter = THREE.LinearFilter; referenceMaterial.map = texture; referenceMaterial.needsUpdate = true; referenceReflectionMaterial.uniforms.uMap.value = texture; referenceReflection.visible = true; root.dataset.referenceCrystal = 'loaded'; renderFrame(performance.now()); }, undefined, function () { referenceCrystal.visible = false; root.dataset.referenceCrystal = 'error'; } ); const crystalReflection = crystalGroup.clone(true); crystalReflection.traverse(function (object) { if (object.userData.noReflection) { object.visible = false; return; } if (!object.material) return; const materials = Array.isArray(object.material) ? object.material : [object.material]; const clonedMaterials = materials.map(function (material) { const clone = trackMaterial(material.clone()); clone.transparent = true; clone.depthWrite = false; clone.depthTest = false; clone.opacity = material.isLineBasicMaterial ? 0.08 : Math.min(0.14, (material.opacity || 1) * 0.16); if (clone.isMeshPhysicalMaterial) { clone.transmission = 0; clone.roughness = 0.24; clone.metalness = 0.08; clone.color = new THREE.Color(0x5ba8ff); } return clone; }); object.material = Array.isArray(object.material) ? clonedMaterials : clonedMaterials[0]; object.renderOrder = 0; }); crystalReflection.scale.set(1, -1, 1); crystalReflection.position.y = -3.42; const floorEffects = new THREE.Group(); function createFloorRay(points, color, opacity) { const geometry = trackGeometry(new THREE.BufferGeometry()); const positions = []; points.forEach(function (point) { positions.push(point[0], -1.705, point[1]); }); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geometry.computeVertexNormals(); const material = trackMaterial(new THREE.MeshBasicMaterial({ color, transparent: true, opacity, side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending })); material.userData.lightOpacity = opacity; material.userData.darkOpacity = opacity * 1.35; const ray = new THREE.Mesh(geometry, material); floorEffects.add(ray); return material; } const floorRayMaterials = [ createFloorRay([[-0.95, 0.2], [-5.6, 4.8], [-2.7, 4.8]], 0x6cecff, 0.2), createFloorRay([[-0.2, 0.3], [-2.2, 5.4], [-0.55, 5.4]], 0x7f6dff, 0.14), createFloorRay([[0.54, 0.14], [1.2, 5.5], [3.6, 5.5]], 0xffd58b, 0.16), createFloorRay([[0.72, 0.12], [3.4, 4.6], [5.4, 4.6]], 0x4f83ff, 0.12) ]; const worldRoot = new THREE.Group(); worldRoot.add(floorEffects, crystalReflection, referenceReflection, crystalGroup, referenceCrystal); worldScene.add(worldRoot); const floorGeometry = trackGeometry(new THREE.PlaneGeometry(10, 10)); const floorMaterial = trackMaterial(new THREE.MeshPhysicalMaterial({ color: 0xd8e8ff, roughness: 0.2, metalness: 0.08, envMapIntensity: 1.6, transparent: true, opacity: 0.22, depthWrite: false, side: THREE.DoubleSide })); const floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.position.set(0, -1.71, 0); floor.receiveShadow = !lowPower; worldScene.add(floor); const floorShadowMaterial = trackMaterial(new THREE.ShadowMaterial({ color: 0x0b3472, opacity: 0.18 })); const floorShadow = new THREE.Mesh(floorGeometry, floorShadowMaterial); floorShadow.rotation.x = -Math.PI / 2; floorShadow.position.set(0, -1.695, 0); floorShadow.receiveShadow = !lowPower; worldScene.add(floorShadow); const restPointer = new THREE.Vector2(0.58, 0.44); const targetMouse = restPointer.clone(); const currentMouse = targetMouse.clone(); const lightEdgeColor = new THREE.Color(0xbfeeff); const darkEdgeColor = new THREE.Color(0x73e9ff); const lightFloorColor = new THREE.Color(0xd9eaff); const darkFloorColor = new THREE.Color(0x0b2746); const lightReferenceColor = new THREE.Color(0xffffff); const darkReferenceColor = new THREE.Color(0xb7dbff); const currentColor = new THREE.Color(); let targetDark = initialDark; let heroVisible = true; let lastFrame = 0; let frameInterval = staticMode ? Infinity : (window.innerWidth < 900 ? 1000 / 30 : 1000 / 45); const startTime = performance.now(); function updateWorld(elapsed) { const dark = uniforms.uDark.value; const floatOffset = staticMode ? 0 : Math.sin(elapsed * 0.48) * 0.075; crystalGroup.position.y = floatOffset; crystalGroup.rotation.x = 0.02 + Math.sin(elapsed * 0.28) * 0.025 + (currentMouse.y - 0.5) * 0.1; crystalGroup.rotation.y = -0.44 + Math.sin(elapsed * 0.18) * 0.035 + (currentMouse.x - 0.5) * 0.1; crystalGroup.rotation.z = -0.018 + Math.sin(elapsed * 0.22) * 0.012; const pointerTiltX = (0.5 - currentMouse.y) * 0.16; const pointerTiltY = (currentMouse.x - 0.5) * 0.28; const pointerShiftX = (currentMouse.x - 0.5) * 0.11; const pointerShiftY = (0.5 - currentMouse.y) * 0.06; referenceCrystal.position.x = -0.29 + pointerShiftX; referenceCrystal.position.y = 0.05 + floatOffset * 0.72 + pointerShiftY; referenceCrystal.rotation.x = pointerTiltX; referenceCrystal.rotation.y = pointerTiltY; referenceCrystal.rotation.z = staticMode ? 0 : Math.sin(elapsed * 0.22) * 0.0035; referenceReflection.position.x = -0.29 + pointerShiftX * 0.7; referenceReflection.position.y = -3.06 - floatOffset * 0.46; referenceReflection.rotation.x = -pointerTiltX * 0.48; referenceReflection.rotation.y = pointerTiltY * 0.62; referenceReflection.rotation.z = -referenceCrystal.rotation.z * 0.5; referenceMaterial.opacity = THREE.MathUtils.lerp(0.97, 0.92, dark); referenceMaterial.color.copy(currentColor.copy(lightReferenceColor).lerp(darkReferenceColor, dark)); referenceCrystal.visible = Boolean(referenceMaterial.map); referenceReflectionMaterial.uniforms.uOpacity.value = THREE.MathUtils.lerp(0.22, 0.14, dark); referenceReflection.visible = Boolean(referenceMaterial.map); crystalGroup.visible = false; crystalReflection.visible = false; crystalReflection.rotation.copy(crystalGroup.rotation); crystalReflection.position.y = -3.42 - floatOffset; worldRoot.rotation.y = (currentMouse.x - 0.5) * 0.03; worldRoot.rotation.x = (0.5 - currentMouse.y) * 0.02; crystalMaterials.forEach(function (material) { material.color.copy(currentColor.copy(material.userData.lightColor).lerp(material.userData.darkColor, dark)); material.opacity = THREE.MathUtils.lerp(material.userData.lightOpacity, material.userData.darkOpacity, dark); material.envMapIntensity = THREE.MathUtils.lerp(material.userData.lightEnv, material.userData.darkEnv, dark); }); glassSideMaterial.emissiveIntensity = THREE.MathUtils.lerp(0.12, 0.2, dark); reflectionMaterials.forEach(function (material) { material.opacity = THREE.MathUtils.lerp(material.userData.lightOpacity, material.userData.darkOpacity, dark); }); floorRayMaterials.forEach(function (material) { material.opacity = THREE.MathUtils.lerp(material.userData.lightOpacity, material.userData.darkOpacity, dark); }); edgeMaterial.color.copy(currentColor.copy(lightEdgeColor).lerp(darkEdgeColor, dark)); edgeMaterial.opacity = THREE.MathUtils.lerp(0.72, 0.9, dark); floorMaterial.color.copy(currentColor.copy(lightFloorColor).lerp(darkFloorColor, dark)); floorMaterial.opacity = THREE.MathUtils.lerp(0.22, 0.13, dark); floorMaterial.envMapIntensity = THREE.MathUtils.lerp(1.6, 2.4, dark); floorShadowMaterial.opacity = THREE.MathUtils.lerp(0.16, 0.3, dark); hemisphereLight.intensity = THREE.MathUtils.lerp(1.8, 1.25, dark); keyLight.intensity = THREE.MathUtils.lerp(2.4, 2.6, dark); cyanLight.intensity = THREE.MathUtils.lerp(18, 26, dark); warmLight.intensity = THREE.MathUtils.lerp(13, 20, dark); } function renderFrame(now) { if (!staticMode && (!heroVisible || document.hidden || now - lastFrame < frameInterval)) return; lastFrame = now; const elapsed = staticMode ? 1.4 : (now - startTime) / 1000; if (staticMode) { uniforms.uDark.value = targetDark; uniforms.uTime.value = 1.4; } else { uniforms.uDark.value += (targetDark - uniforms.uDark.value) * 0.085; currentMouse.lerp(targetMouse, 0.055); uniforms.uMouse.value.copy(currentMouse); uniforms.uTime.value = elapsed; } updateWorld(elapsed); renderer.clear(true, true, true); renderer.render(backgroundScene, backgroundCamera); renderer.clearDepth(); renderer.render(worldScene, worldCamera); } function resize() { const rect = root.getBoundingClientRect(); const width = Math.max(1, Math.round(rect.width)); const height = Math.max(1, Math.round(rect.height)); const maxDpr = window.innerWidth < 900 ? 1 : 1.5; renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, maxDpr)); renderer.setSize(width, height, false); uniforms.uResolution.value.set(width * renderer.getPixelRatio(), height * renderer.getPixelRatio()); const aspect = width / height; worldCamera.aspect = aspect; worldCamera.fov = aspect < 0.75 ? 38 : 33; worldCamera.updateProjectionMatrix(); if (aspect > 1.1) { worldRoot.position.set(Math.min(1.92, aspect * 1.15), -0.02, 0); worldRoot.scale.setScalar(0.9); } else { worldRoot.position.set(0.08, -1.55, 0); worldRoot.scale.setScalar(0.56); } floor.position.set(worldRoot.position.x, worldRoot.position.y - 1.71 * worldRoot.scale.x, 0); floorShadow.position.set(worldRoot.position.x, worldRoot.position.y - 1.695 * worldRoot.scale.x, 0); frameInterval = staticMode ? Infinity : (width < 900 ? 1000 / 30 : 1000 / 45); renderFrame(performance.now()); } const resizeObserver = new ResizeObserver(resize); resizeObserver.observe(root); const hero = document.getElementById('home'); if (hero) { hero.addEventListener('pointermove', function (event) { if (staticMode) return; const rect = hero.getBoundingClientRect(); targetMouse.set( THREE.MathUtils.clamp((event.clientX - rect.left) / rect.width, 0, 1), THREE.MathUtils.clamp((event.clientY - rect.top) / rect.height, 0, 1) ); }, { passive: true }); hero.addEventListener('pointerleave', function () { targetMouse.copy(restPointer); }, { passive: true }); const visibilityObserver = new IntersectionObserver(function (entries) { heroVisible = entries[0] ? entries[0].isIntersecting : true; if (heroVisible && staticMode) renderFrame(performance.now()); }, { rootMargin: '120px 0px' }); visibilityObserver.observe(hero); } window.addEventListener('roncarve:themechange', function (event) { targetDark = event.detail.resolved === 'dark' ? 1 : 0; if (staticMode) renderFrame(performance.now()); }); canvas.addEventListener('webglcontextlost', function (event) { event.preventDefault(); root.classList.remove('is-ready'); root.classList.add('has-error'); renderer.setAnimationLoop(null); }); resize(); renderFrame(performance.now()); root.classList.add('is-ready'); if (!staticMode) renderer.setAnimationLoop(renderFrame); window.addEventListener('beforeunload', function () { renderer.setAnimationLoop(null); resizeObserver.disconnect(); backgroundGeometry.dispose(); backgroundMaterial.dispose(); disposableGeometries.forEach(function (geometry) { geometry.dispose(); }); disposableMaterials.forEach(function (material) { material.dispose(); }); if (referenceTexture) referenceTexture.dispose(); environmentTarget.dispose(); renderer.dispose(); }, { once: true }); } }