import { useEffect, useRef, useState, useCallback } from 'react'; import * as THREE from 'three'; import { gsap } from 'gsap'; import throttle from 'lodash/throttle'; interface HSL { h: number; s: number; l: number; } interface Particle { x: number; y: number; z: number; originalX: number; originalY: number; originalColor: THREE.Color; delay: number; } const createErrorParticles = (width: number, height: number) => { const particles: Particle[] = []; const positionArray: number[] = []; const colorArray: number[] = []; const errorColor = new THREE.Color(0.8, 0, 0); const size = Math.min(width, height); const scaleFactor = size * 0.3; const particlesPerLine = 50; // X 形状的两条线 const lines = [ // 左上到右下的线 { start: [-1, 1], end: [1, -1] }, // 右上到左下的线 { start: [1, 1], end: [-1, -1] } ]; lines.forEach(line => { for (let i = 0; i < particlesPerLine; i++) { const t = i / (particlesPerLine - 1); const x = line.start[0] + (line.end[0] - line.start[0]) * t; const y = line.start[1] + (line.end[1] - line.start[1]) * t; // 添加一些随机偏移 const randomOffset = 0.1; const randomX = x + (Math.random() - 0.5) * randomOffset; const randomY = y + (Math.random() - 0.5) * randomOffset; const scaledX = randomX * scaleFactor; const scaledY = randomY * scaleFactor; particles.push({ x: scaledX, y: scaledY, z: 0, originalX: scaledX, originalY: scaledY, originalColor: errorColor, delay: 0 }); // 修改初始位置生成方式 const angle = Math.random() * Math.PI * 2; const distance = size * 2; positionArray.push( Math.cos(angle) * distance, Math.sin(angle) * distance, 0 ); // 初始颜色设置为最终颜色的一半亮度 colorArray.push(errorColor.r * 0.5, errorColor.g * 0.5, errorColor.b * 0.5); } }); const particleSize = Math.max(1.2, (size / 200) * 1.2); return { particles, positionArray, colorArray, particleSize }; }; // 修改 createSmileParticles 函数 const createSmileParticles = (width: number, height: number) => { const particles: Particle[] = []; const positionArray: number[] = []; const colorArray: number[] = []; const size = Math.min(width, height); const scale = size / 200; const radius = size * 0.35; const particleSize = Math.max(1.2, scale * 1.2); const particleColor = new THREE.Color(0.8, 0.6, 0); // 预先计算所有需要的粒子位置 const allPoints: { x: number; y: number }[] = []; // 计算脸部轮廓的点 const outlinePoints = Math.floor(60 * scale); for (let i = 0; i < outlinePoints; i++) { const angle = (i / outlinePoints) * Math.PI * 2; allPoints.push({ x: Math.cos(angle) * radius, y: Math.sin(angle) * radius }); } // 修改眼睛的生成方式 const eyeOffset = radius * 0.3; const eyeY = radius * 0.15; const eyeSize = radius * 0.1; // 稍微减小眼睛尺寸 const eyePoints = Math.floor(20 * scale); [-1, 1].forEach(side => { // 使用同心圆的方式生成眼睛 const eyeCenterX = side * eyeOffset; const rings = 3; // 同心圆的数量 for (let ring = 0; ring < rings; ring++) { const ringRadius = eyeSize * (1 - ring / rings); // 从外到内递减半径 const pointsInRing = Math.floor(eyePoints / rings); for (let i = 0; i < pointsInRing; i++) { const angle = (i / pointsInRing) * Math.PI * 2; allPoints.push({ x: eyeCenterX + Math.cos(angle) * ringRadius, y: eyeY + Math.sin(angle) * ringRadius }); } } // 添加中心点 allPoints.push({ x: eyeCenterX, y: eyeY }); }); // 计算嘴巴的点 const smileWidth = radius * 0.6; const smileY = -radius * 0.35; const smilePoints = Math.floor(25 * scale); for (let i = 0; i < smilePoints; i++) { const t = i / (smilePoints - 1); const x = (t * 2 - 1) * smileWidth; const y = smileY + Math.pow(x / smileWidth, 2) * radius * 0.2; allPoints.push({ x, y }); } // 为所有点创建粒子 allPoints.forEach(point => { particles.push({ x: point.x, y: point.y, z: 0, originalX: point.x, originalY: point.y, originalColor: particleColor, delay: 0 }); // 生成初始位置(从外围圆形区域开始) const initAngle = Math.random() * Math.PI * 2; const distance = size * 2; positionArray.push( Math.cos(initAngle) * distance, Math.sin(initAngle) * distance, 0 ); // 初始颜色设置为最终颜色的一半亮度 colorArray.push( particleColor.r * 0.5, particleColor.g * 0.5, particleColor.b * 0.5 ); }); return { particles, positionArray, colorArray, particleSize }; }; // 在文件开头添加新的 helper 函数 const easeOutCubic = (t: number) => { return 1 - Math.pow(1 - t, 3); }; const customEase = (t: number) => { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }; // 在文件开头添加新的 LoaderStatus 接口 interface LoaderStatus { isLoading: boolean; hasError: boolean; timeoutError: boolean; animationPhase: 'assembling' | 'image' | 'dissolving' | 'transitioning'; } // 修改 ParticleImage 组件的 props 接口 interface ParticleImageProps { src?: string; status?: LoaderStatus; onLoad?: () => void; onAnimationComplete?: () => void; transitionType?: 'in' | 'out' | 'none'; previousParticles?: Particle[]; onParticlesCreated?: (particles: Particle[]) => void; onAnimationPhaseChange?: (phase: 'assembling' | 'image' | 'dissolving' | 'transitioning') => void; } // 修改 BG_CONFIG const BG_CONFIG = { colors: { from: 'rgb(10,37,77)', via: 'rgb(8,27,57)', to: 'rgb(2,8,23)' }, className: 'bg-gradient-to-br from-[rgb(248,250,252)] via-[rgb(241,245,249)] to-[rgb(236,241,247)] dark:from-[rgb(10,37,77)] dark:via-[rgb(8,27,57)] dark:to-[rgb(2,8,23)]', size: { container: '' } }; // 修改图像采样函数 const createParticlesFromImage = (imageData: ImageData, width: number, height: number) => { const particles: Particle[] = []; const positionArray: number[] = []; const colorArray: number[] = []; // 根据容器尺寸计算缩放因子 const aspectRatio = width / height; const scaleFactor = width / 2; // 使用容器宽度的一半作为基准 // 固定粒子数量以保持一致的视觉效果 const particlesPerSide = Math.floor(Math.min(150, Math.max(80, Math.min(width, height) / 4))); const stepX = width / particlesPerSide; const stepY = height / particlesPerSide; for (let y = 0; y < height; y += stepY) { for (let x = 0; x < width; x += stepX) { const pixelX = Math.floor(x); const pixelY = Math.floor(y); const i = (pixelY * width + pixelX) * 4; const r = imageData.data[i] / 255; const g = imageData.data[i + 1] / 255; const b = imageData.data[i + 2] / 255; // 计算亮度并设置最小值 const brightness = Math.max(0.1, (r + g + b) / 3); // 将坐标映射到容器范围 const px = ((x / width) * 2 - 1) * scaleFactor; const py = ((1 - y / height) * 2 - 1) * (scaleFactor / aspectRatio); // 创建粒子 const finalColor = new THREE.Color( Math.max(0.1, r), Math.max(0.1, g), Math.max(0.1, b) ); particles.push({ x: px, y: py, z: 0, originalX: px, originalY: py, originalColor: finalColor, delay: Math.random() * 0.3 }); // 设置初始位置 const angle = Math.random() * Math.PI * 2; const distance = Math.max(width, height); positionArray.push( Math.cos(angle) * distance, Math.sin(angle) * distance, 0 ); // 设置初始颜色 colorArray.push( finalColor.r * 0.3, finalColor.g * 0.3, finalColor.b * 0.3 ); } } // 调整粒子大小 const particleSize = Math.max(2, Math.min(width, height) / 150); return { particles, positionArray, colorArray, particleSize }; }; // 优化动画效果 const animateParticles = ( particles: Particle[], geometry: THREE.BufferGeometry, onComplete?: () => void ) => { const positionAttribute = geometry.attributes.position; const colorAttribute = geometry.attributes.color; particles.forEach((particle, i) => { const i3 = i * 3; // 位置动画 gsap.to(positionAttribute.array, { duration: 1 + Math.random() * 0.5, delay: particle.delay, [i3]: particle.originalX, [i3 + 1]: particle.originalY, [i3 + 2]: 0, ease: "power2.out", onUpdate: () => void (positionAttribute.needsUpdate = true) }); // 颜色动画 gsap.to(colorAttribute.array, { duration: 0.8, delay: particle.delay, [i3]: particle.originalColor.r, [i3 + 1]: particle.originalColor.g, [i3 + 2]: particle.originalColor.b, ease: "power2.inOut", onUpdate: () => { colorAttribute.needsUpdate = true; return; }, onComplete: i === particles.length - 1 ? onComplete : undefined }); }); }; export const ParticleImage = ({ src, status, onLoad, onAnimationComplete, transitionType = 'in', previousParticles, onParticlesCreated, onAnimationPhaseChange }: ParticleImageProps) => { const containerRef = useRef(null); const sceneRef = useRef(); const cameraRef = useRef(); const rendererRef = useRef(); const animationFrameRef = useRef(); const geometryRef = useRef(); const materialRef = useRef(); const pointsRef = useRef(); // 清理函数 const cleanup = useCallback(() => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } // 清理 Three.js 资源 if (geometryRef.current) { geometryRef.current.dispose(); } if (materialRef.current) { materialRef.current.dispose(); } if (pointsRef.current) { if (pointsRef.current.geometry) { pointsRef.current.geometry.dispose(); } if (pointsRef.current.material instanceof THREE.Material) { pointsRef.current.material.dispose(); } sceneRef.current?.remove(pointsRef.current); } if (rendererRef.current) { rendererRef.current.dispose(); if (containerRef.current?.contains(rendererRef.current.domElement)) { containerRef.current.removeChild(rendererRef.current.domElement); } } // 清理 GSAP 动画 gsap.killTweensOf('*'); }, []); useEffect(() => { if (!containerRef.current || !src) return; const width = containerRef.current.offsetWidth; const height = containerRef.current.offsetHeight; // 建错误动画函数 const showErrorAnimation = () => { if (!sceneRef.current) return; const { particles, positionArray, colorArray, particleSize } = createErrorParticles(width, height); // ... 其余错误动画代码 ... }; const timeoutId = setTimeout(() => showErrorAnimation(), 5000); // 初始化场景 const scene = new THREE.Scene(); sceneRef.current = scene; // 调整相机视角 const camera = new THREE.OrthographicCamera( width / -2, width / 2, height / 2, height / -2, 1, 1000 ); camera.position.z = 500; cameraRef.current = camera; const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); renderer.setSize(width, height); rendererRef.current = renderer; containerRef.current.appendChild(renderer.domElement); // 加载图片 const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { clearTimeout(timeoutId); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (ctx) { // 计算目标尺寸和裁剪区域 const targetAspect = width / height; const imgAspect = img.width / img.height; let sourceWidth = img.width; let sourceHeight = img.height; let sourceX = 0; let sourceY = 0; // 裁剪源图片,确保比例匹配目标容器 if (imgAspect > targetAspect) { sourceWidth = img.height * targetAspect; sourceX = (img.width - sourceWidth) / 2; } else { sourceHeight = img.width / targetAspect; sourceY = (img.height - sourceHeight) / 2; } canvas.width = width; canvas.height = height; // 清除画布 ctx.clearRect(0, 0, width, height); // 绘制图像 ctx.drawImage( img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, width, height ); const imageData = ctx.getImageData(0, 0, width, height); const { particles, positionArray, colorArray, particleSize } = createParticlesFromImage(imageData, width, height); // 通知父组件新的粒子已创建 onParticlesCreated?.(particles); // 创建粒子系统 const geometry = new THREE.BufferGeometry(); geometryRef.current = geometry; const material = new THREE.PointsMaterial({ size: particleSize, vertexColors: true, transparent: true, opacity: 1, sizeAttenuation: true, blending: THREE.AdditiveBlending, depthWrite: false, depthTest: false }); materialRef.current = material; geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); const points = new THREE.Points(geometry, material); pointsRef.current = points; scene.add(points); // 动画分 const timeline = gsap.timeline(); const positionAttribute = geometry.attributes.position; const colorAttribute = geometry.attributes.color; // 立即执动画 if (transitionType === 'in') { // 设置初始位置和颜色 particles.forEach((particle, i) => { const i3 = i * 3; // 设置随机初始位置 positionAttribute.array[i3] = (Math.random() - 0.5) * width * 2; positionAttribute.array[i3 + 1] = (Math.random() - 0.5) * height * 2; positionAttribute.array[i3 + 2] = Math.random() * 100; // 设置初始颜色 colorAttribute.array[i3] = particle.originalColor.r * 0.2; colorAttribute.array[i3 + 1] = particle.originalColor.g * 0.2; colorAttribute.array[i3 + 2] = particle.originalColor.b * 0.2; }); positionAttribute.needsUpdate = true; colorAttribute.needsUpdate = true; // 创建动画 particles.forEach((particle, i) => { const i3 = i * 3; // 位置动画 gsap.to(positionAttribute.array, { duration: 2, delay: particle.delay, [i3]: particle.originalX, [i3 + 1]: particle.originalY, [i3 + 2]: 0, ease: "power2.inOut", onUpdate: () => { positionAttribute.needsUpdate = true; } }); // 颜色动画 gsap.to(colorAttribute.array, { duration: 1.8, delay: particle.delay + 0.2, [i3]: particle.originalColor.r, [i3 + 1]: particle.originalColor.g, [i3 + 2]: particle.originalColor.b, ease: "power2.inOut", onUpdate: () => { colorAttribute.needsUpdate = true; } }); }); } else if (transitionType === 'out') { particles.forEach((particle, i) => { const i3 = i * 3; gsap.to(colorAttribute.array, { duration: 1, [i3]: particle.originalColor.r * 0.2, [i3 + 1]: particle.originalColor.g * 0.2, [i3 + 2]: particle.originalColor.b * 0.2, ease: "power2.in", onUpdate: () => { colorAttribute.needsUpdate = true; } }); }); } // 画循环 const animate = () => { animationFrameRef.current = requestAnimationFrame(animate); renderer.render(scene, camera); }; animate(); onLoad?.(); } }; img.src = src; return () => { clearTimeout(timeoutId); cleanup(); }; }, [src, cleanup, onLoad, onAnimationComplete, transitionType, previousParticles, onParticlesCreated]); return
; }; // 修改 ImageLoader 组件的 props 接口 interface ImageLoaderProps { src?: string; alt: string; className?: string; containerClassName?: string; // 新增容器类名属性 } // 修改 ImageLoader 组件 export const ImageLoader = ({ src, alt, className = '', containerClassName = '' }: ImageLoaderProps) => { const [status, setStatus] = useState({ isLoading: true, hasError: false, timeoutError: false, animationPhase: 'assembling' }); const [showImage, setShowImage] = useState(false); const timeoutRef = useRef(); const loadingRef = useRef(false); const imageRef = useRef(null); const [currentParticles, setCurrentParticles] = useState([]); // 动画循环 const startAnimationCycle = useCallback(async () => { while (true) { // 1. 粒子组合成图像 setStatus(prev => ({ ...prev, animationPhase: 'assembling' })); setShowImage(false); await new Promise(resolve => setTimeout(resolve, 2500)); // 2. 显示实际图片 setShowImage(true); await new Promise(resolve => setTimeout(resolve, 3000)); // 3. 隐藏图片,显示粒子 setShowImage(false); setStatus(prev => ({ ...prev, animationPhase: 'dissolving' })); await new Promise(resolve => setTimeout(resolve, 3000)); // 4. 粒子过渡状态 setStatus(prev => ({ ...prev, animationPhase: 'transitioning' })); await new Promise(resolve => setTimeout(resolve, 1500)); } }, []); // 处理图片预加载 const preloadImage = useCallback(() => { if (!src || loadingRef.current) return; loadingRef.current = true; const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { imageRef.current = img; setStatus(prev => ({ ...prev, isLoading: false, hasError: false, timeoutError: false })); startAnimationCycle(); }; img.onerror = () => { setStatus(prev => ({ ...prev, isLoading: false, hasError: true, timeoutError: false })); }; img.src = src; }, [src, startAnimationCycle]); useEffect(() => { preloadImage(); return () => { loadingRef.current = false; }; }, [preloadImage]); return (
{ setStatus(prev => ({ ...prev, animationPhase: phase })); }} />
{!status.hasError && !status.timeoutError && imageRef.current && (
{alt}
)}
); };