Files
roncarve-official-web/theme-webgl.js
T

853 lines
42 KiB
JavaScript

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 });
}
}