import { useEffect, useRef, useState, useCallback } from 'react'; import * as THREE from 'three'; import { gsap } from 'gsap'; 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); // 更深的红色 // X 形状的两条线 const lines = [ // 左上到右下的线 { start: [-1, 1], end: [1, -1] }, // 右上到左下的线 { start: [1, 1], end: [-1, -1] } ]; // 每条线上的粒子数量 const particlesPerLine = 50; 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 * (width * 0.3); const scaledY = randomY * (height * 0.3); particles.push({ x: scaledX, y: scaledY, z: 0, originalX: scaledX, originalY: scaledY, originalColor: errorColor, delay: 0 }); // 随机初始位置 positionArray.push( (Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * 100 ); // 随机初始颜色 colorArray.push(errorColor.r, errorColor.g, errorColor.b); } }); return { particles, positionArray, colorArray }; }; // 添加笑脸粒子生成函数 const createSmileParticles = (width: number, height: number) => { const particles: Particle[] = []; const positionArray: number[] = []; const colorArray: number[] = []; // 调整笑脸参数 const radius = Math.min(width, height) * 0.35; // 脸部大小 const particlesCount = 400; // 轮廓粒子数量 // 修改颜色为更深的金色 const particleColor = new THREE.Color(0.8, 0.6, 0); // 更深的金色 // 创建圆形脸部轮廓 for (let i = 0; i < particlesCount / 2; i++) { const angle = (i / (particlesCount / 2)) * Math.PI * 2; const x = Math.cos(angle) * radius; const y = Math.sin(angle) * radius; particles.push({ x, y, z: 0, originalX: x, originalY: y, originalColor: particleColor, delay: 0 }); positionArray.push( (Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * 100 ); colorArray.push(particleColor.r, particleColor.g, particleColor.b); } // 眼睛参数 const eyeOffset = radius * 0.2; // 眼睛水平间距 const eyeY = radius * 0.2; // 眼睛垂直位置 const eyeSize = radius * 0.08; // 眼睛大小 // 创建实心眼睛 [-1, 1].forEach(side => { // 创建密集的点来填充眼睛 for (let i = 0; i < 30; i++) { const r = Math.random() * eyeSize; // 随机半径 const angle = Math.random() * Math.PI * 2; // 随机角度 const x = side * eyeOffset + Math.cos(angle) * r; const y = eyeY + Math.sin(angle) * r; particles.push({ x, y, z: 0, originalX: x, originalY: y, originalColor: particleColor, delay: 0 }); positionArray.push( (Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * 100 ); colorArray.push(particleColor.r, particleColor.g, particleColor.b); } }); // 嘴巴参数 const smileWidth = radius * 0.5; // 嘴巴宽度 const smileY = -radius * 0.3; // 将嘴巴位置向下移动更多 const smilePoints = 40; // 嘴巴粒子数量 // 创建微笑 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); particles.push({ x, y, z: 0, originalX: x, originalY: y, originalColor: particleColor, delay: 0 }); positionArray.push( (Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * 100 ); colorArray.push(particleColor.r, particleColor.g, particleColor.b); } return { particles, positionArray, colorArray }; }; // 在文件开头添加新的 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; }; export const ParticleImage = ({ src, onLoad, onError }: { src?: string; onLoad?: () => void; onError?: () => void; }) => { const containerRef = useRef(null); const sceneRef = useRef(); const cameraRef = useRef(); const rendererRef = useRef(); const animationFrameRef = useRef(); // 添加 resize 处理函数 const handleResize = useCallback(() => { if (!containerRef.current || !cameraRef.current || !rendererRef.current) return; const width = containerRef.current.offsetWidth; const height = containerRef.current.offsetHeight; const camera = cameraRef.current; camera.left = width / -2.1; camera.right = width / 2.1; camera.top = height / 2.1; camera.bottom = height / -2.1; camera.updateProjectionMatrix(); rendererRef.current.setSize(width, height); }, []); useEffect(() => { if (!containerRef.current) return; console.log('Current src:', src); const width = containerRef.current.offsetWidth; const height = containerRef.current.offsetHeight; const scene = new THREE.Scene(); sceneRef.current = scene; const camera = new THREE.OrthographicCamera( width / -1.5, // 扩大视野范围,从 -2 改为 -1.5 width / 1.5, // 扩大视野范围,从 2 改为 1.5 height / 1.5, // 扩大视野范围 height / -1.5, // 扩大视野范围 1, 1000 ); camera.position.z = 100; cameraRef.current = camera; const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); rendererRef.current = renderer; containerRef.current.appendChild(renderer.domElement); const geometry = new THREE.BufferGeometry(); const material = new THREE.PointsMaterial({ size: 1.2, vertexColors: true, transparent: true, opacity: 1, sizeAttenuation: true, blending: THREE.AdditiveBlending, depthWrite: false, depthTest: false }); // 检查是否应该显示笑脸 if (src === '') { console.log('Showing smile animation'); const { particles, positionArray, colorArray } = createSmileParticles(width, height); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); const points = new THREE.Points(geometry, material); scene.add(points); const positionAttribute = geometry.attributes.position; particles.forEach((particle, i) => { const i3 = i * 3; gsap.to(positionAttribute.array, { duration: 1, delay: Math.random() * 0.3, [i3]: particle.originalX, [i3 + 1]: particle.originalY, [i3 + 2]: 0, ease: "back.out(1.7)", onUpdate: () => { positionAttribute.needsUpdate = true; } }); }); // 启动动画循环 const animate = () => { if (renderer && scene && camera) { renderer.render(scene, camera); } animationFrameRef.current = requestAnimationFrame(animate); }; animate(); return; } // 创建错误动画函数 const showErrorAnimation = () => { if (!scene) return; const { particles, positionArray, colorArray } = createErrorParticles(width, height); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); const points = new THREE.Points(geometry, material); scene.clear(); // 清除现有内容 scene.add(points); const positionAttribute = geometry.attributes.position; particles.forEach((particle, i) => { const i3 = i * 3; gsap.to(positionAttribute.array, { duration: 0.6, delay: Math.random() * 0.2, [i3]: particle.originalX, [i3 + 1]: particle.originalY, [i3 + 2]: 0, ease: "back.out(1.7)", onUpdate: () => { positionAttribute.needsUpdate = true; } }); }); onError?.(); }; // 加载图片 const img = new Image(); img.crossOrigin = 'anonymous'; const timeoutId = setTimeout(() => { showErrorAnimation(); }, 5000); // 5秒超时 img.onload = () => { clearTimeout(timeoutId); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (ctx) { canvas.width = width; canvas.height = height; // 计算图片绘制尺寸和位置,确保不会超出容器 const imgAspect = img.width / img.height; const containerAspect = width / height; let drawWidth = width; let drawHeight = height; let offsetX = 0; let offsetY = 0; if (imgAspect > containerAspect) { // 图片较宽,以容器宽度为准,确保高度不超出 drawWidth = width; drawHeight = width / imgAspect; offsetY = (height - drawHeight) / 2; } else { // 图片较高,以容器高度为准,确保宽度不超出 drawHeight = height; drawWidth = height * imgAspect; offsetX = (width - drawWidth) / 2; } // 绘制图片 ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); const imageData = ctx.getImageData(0, 0, width, height); const particles: Particle[] = []; const positionArray = []; const colorArray = []; const samplingGap = Math.ceil(Math.max(width, height) / 100); // 动态采样间隔,确保粒子数量适中 // 采样图片像素 for (let y = 0; y < height; y += samplingGap) { for (let x = 0; x < width; x += samplingGap) { const i = (y * width + x) * 4; const r = imageData.data[i] / 255; const g = imageData.data[i + 1] / 255; const b = imageData.data[i + 2] / 255; const a = imageData.data[i + 3] / 255; if (a > 0.3) { // 计算距离中心的距离,用于动画延迟 const distanceToCenter = Math.sqrt( Math.pow(x - width/2, 2) + Math.pow(y - height/2, 2) ); const maxDistance = Math.sqrt(Math.pow(width/2, 2) + Math.pow(height/2, 2)); const normalizedDistance = distanceToCenter / maxDistance; const px = x - width / 2; const py = height / 2 - y; particles.push({ x: px, y: py, z: 0, originalX: px, originalY: py, originalColor: new THREE.Color(r, g, b), delay: normalizedDistance * 0.3 // 基于距离的延迟 }); // 随机初始位置(根据距离调整范围) const spread = 1 - normalizedDistance * 0.5; // 距离越远,初始扩散越小 positionArray.push( (Math.random() - 0.5) * width * spread, (Math.random() - 0.5) * height * spread, (Math.random() - 0.5) * 50 * spread ); colorArray.push(r, g, b); } } } scene.clear(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); const points = new THREE.Points(geometry, material); scene.add(points); // 动画 const positionAttribute = geometry.attributes.position; const colorAttribute = geometry.attributes.color; let completedAnimations = 0; const totalAnimations = particles.length * 2; // 位置和颜色动画 const checkComplete = () => { completedAnimations++; if (completedAnimations === totalAnimations) { onLoad?.(); // 所有画完成后调用 onLoad } }; particles.forEach((particle, i) => { const i3 = i * 3; // 位置动画 gsap.to(positionAttribute.array, { duration: 1.2 + Math.random() * 0.3, // 减少随机性范围 delay: particle.delay, // 使用基于距离的延迟 [i3]: particle.originalX, [i3 + 1]: particle.originalY, [i3 + 2]: 0, ease: customEase, onUpdate: () => { positionAttribute.needsUpdate = true; }, onComplete: checkComplete }); // 颜色动画 gsap.to(colorAttribute.array, { duration: 1, 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; }, onComplete: checkComplete }); }); // 修改动画序列部分 const timeline = gsap.timeline({ defaults: { ease: "power2.inOut" } }); const imgElement = document.querySelector(`img[src="${src}"]`) as HTMLImageElement; if (imgElement) { // 设置初始状态 gsap.set(imgElement, { opacity: 0 }); timeline .to(imgElement, { opacity: 1, duration: 0.8, delay: 1.6 }) .to(material, { opacity: 0, duration: 0.8 }, "-=0.6"); // 提前开始消失 } } }; img.onerror = () => { clearTimeout(timeoutId); showErrorAnimation(); }; img.src = src || ''; // 动画循环 const animate = () => { if (renderer && scene && camera) { renderer.render(scene, camera); } animationFrameRef.current = requestAnimationFrame(animate); }; animate(); // 添加 resize 监听 const resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(containerRef.current); // 添加窗口 resize 监听 window.addEventListener('resize', handleResize); return () => { clearTimeout(timeoutId); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } if (renderer && containerRef.current) { containerRef.current.removeChild(renderer.domElement); renderer.dispose(); } // 清除所有 GSAP 动画 gsap.killTweensOf(geometry.attributes.position?.array); // 移除 resize 监听 if (containerRef.current) { resizeObserver.unobserve(containerRef.current); } window.removeEventListener('resize', handleResize); }; }, [src, onError, handleResize]); return
; }; // 图片加载组件 export const ImageLoader = ({ src, alt, className }: { src?: string; alt: string; className: string; }) => { const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); return (
setIsLoading(false)} onError={() => { setIsLoading(false); setHasError(true); }} />
{!hasError && (
{alt}
)}
); };