import * as THREE from 'three'; // OS1 加载环(参考 Her - OS1 Loading):白色扭曲环在陶土橙红背景上旋转, // 按下加速转向正面、淡出并显露完成圆环。 const BG = '#14100d'; class OS1Curve extends THREE.Curve { constructor(length, radius) { super(); this.length = length; this.radius = radius; this.pi2 = Math.PI * 2; } getPoint(percent) { const { length, radius, pi2 } = this; let t = (percent % 0.25) / 0.25; t = (percent % 0.25) - (2 * (1 - t) * t * -0.0185 + t * t * 0.25); if (Math.floor(percent / 0.25) === 0 || Math.floor(percent / 0.25) === 2) t *= -1; const x = length * Math.sin(pi2 * percent); const y = radius * Math.cos(pi2 * 3 * percent); const z = radius * Math.sin(pi2 * 2 * (percent - t)); return new THREE.Vector3(x, y, z); } } export function initLoading() { const wrap = document.getElementById('loading-canvas'); if (!wrap) return { dispose() {} }; const canvassize = 500; const length = 30; const radius = 5.6; const rotatevalue = 0.035; let acceleration = 0; let animatestep = 0; let toend = false; let disposed = false; let rafId = 0; const group = new THREE.Group(); const scene = new THREE.Scene(); scene.add(group); const camera = new THREE.PerspectiveCamera(65, 1, 1, 10000); camera.position.z = 150; // 白色扭曲环 const mesh = new THREE.Mesh( new THREE.TubeGeometry(new OS1Curve(length, radius), 120, 1.1, 2, true), new THREE.MeshBasicMaterial({ color: 0xe0c46a, transparent: true }), ); group.add(mesh); // 缺口遮挡(背景色) const ringcover = new THREE.Mesh( new THREE.PlaneGeometry(50, 15, 1), new THREE.MeshBasicMaterial({ color: 0x14100d, transparent: true, opacity: 0 }), ); ringcover.position.x = length + 1; ringcover.rotation.y = Math.PI / 2; group.add(ringcover); // 完成圆环 const ring = new THREE.Mesh( new THREE.RingGeometry(4.3, 5.55, 32), new THREE.MeshBasicMaterial({ color: 0xe0c46a, transparent: true, opacity: 0 }), ); ring.position.x = length + 1.1; ring.rotation.y = Math.PI / 2; group.add(ring); const renderer = new THREE.WebGLRenderer({ antialias: false }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); renderer.setSize(canvassize, canvassize); renderer.setClearColor(BG); wrap.appendChild(renderer.domElement); function easing(t, b, c, d) { if ((t /= d / 2) < 1) return (c / 2) * t * t + b; return (c / 2) * ((t -= 2) * t * t + 2) + b; } function render() { animatestep = Math.max(0, Math.min(240, toend ? animatestep + 1 : animatestep - 4)); acceleration = easing(animatestep, 0, 1, 240); if (acceleration > 0.35) { let progress = (acceleration - 0.35) / 0.65; group.rotation.y = (-Math.PI / 2) * progress; group.position.z = 50 * progress; progress = Math.max(0, (acceleration - 0.97) / 0.03); mesh.material.opacity = 1 - progress; ringcover.material.opacity = ring.material.opacity = progress; ring.scale.x = ring.scale.y = 0.9 + 0.1 * progress; } renderer.render(scene, camera); } function animate() { mesh.rotation.x += rotatevalue + acceleration; render(); if (!disposed) rafId = requestAnimationFrame(animate); } animate(); const press = () => { toend = true; }; const release = () => { toend = false; }; document.body.addEventListener('mousedown', press); document.body.addEventListener('touchstart', press); document.body.addEventListener('mouseup', release); document.body.addEventListener('touchend', release); return { dispose() { disposed = true; cancelAnimationFrame(rafId); document.body.removeEventListener('mousedown', press); document.body.removeEventListener('touchstart', press); document.body.removeEventListener('mouseup', release); document.body.removeEventListener('touchend', release); renderer.dispose(); if (renderer.domElement.parentNode === wrap) wrap.removeChild(renderer.domElement); }, }; }