前端:优化加载图片动画

This commit is contained in:
lsy 2024-12-05 02:43:58 +08:00
parent b29fe30c71
commit 76ee04bdde

View File

@ -42,8 +42,10 @@ const createErrorParticles = (width: number, height: number) => {
const randomY = y + (Math.random() - 0.5) * randomOffset; const randomY = y + (Math.random() - 0.5) * randomOffset;
// 缩放到适合容器的大小 // 缩放到适合容器的大小
const scaledX = randomX * (width * 0.3); const size = Math.min(width, height);
const scaledY = randomY * (height * 0.3); const scaleFactor = size * 0.3;
const scaledX = randomX * scaleFactor;
const scaledY = randomY * scaleFactor;
particles.push({ particles.push({
x: scaledX, x: scaledX,
@ -57,9 +59,9 @@ const createErrorParticles = (width: number, height: number) => {
// 随机初始位置 // 随机初始位置
positionArray.push( positionArray.push(
(Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * size * 2,
(Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * size * 2,
(Math.random() - 0.5) * 100 (Math.random() - 0.5) * 50
); );
// 随机初始颜色 // 随机初始颜色
@ -67,21 +69,29 @@ const createErrorParticles = (width: number, height: number) => {
} }
}); });
return { particles, positionArray, colorArray }; const size = Math.min(width, height);
const scale = size / 200;
const particleSize = Math.max(1.2, scale * 1.2);
return { particles, positionArray, colorArray, particleSize };
}; };
// 添加笑脸粒子生成函数 // 修改 createSmileParticles 函数
const createSmileParticles = (width: number, height: number) => { const createSmileParticles = (width: number, height: number) => {
const particles: Particle[] = []; const particles: Particle[] = [];
const positionArray: number[] = []; const positionArray: number[] = [];
const colorArray: number[] = []; const colorArray: number[] = [];
// 调整笑脸参数 // 根据容器大小动态调整参数
const radius = Math.min(width, height) * 0.35; // 脸部大小 const size = Math.min(width, height);
const particlesCount = 400; // 轮廓粒子数量 const scale = size / 200; // 基准子
// 修改颜色为更深的金色 // 调整笑脸参数
const particleColor = new THREE.Color(0.8, 0.6, 0); // 更深的金色 const radius = size * 0.25; // 更合理的脸部大小比例
const particlesCount = Math.floor(150 * scale); // 减少粒子数量
const particleSize = Math.max(1.2, scale * 1.2); // 确保粒子大小适应屏幕
const particleColor = new THREE.Color(0.8, 0.6, 0);
// 创建圆形脸部轮廓 // 创建圆形脸部轮廓
for (let i = 0; i < particlesCount / 2; i++) { for (let i = 0; i < particlesCount / 2; i++) {
@ -98,24 +108,25 @@ const createSmileParticles = (width: number, height: number) => {
}); });
positionArray.push( positionArray.push(
(Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * size * 4,
(Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * size * 4,
(Math.random() - 0.5) * 100 (Math.random() - 0.5) * 150
); );
colorArray.push(particleColor.r, particleColor.g, particleColor.b); colorArray.push(particleColor.r, particleColor.g, particleColor.b);
} }
// 眼睛参数 // 眼睛参数调整
const eyeOffset = radius * 0.2; // 眼睛水平间距 const eyeOffset = radius * 0.3; // 增加眼睛间距
const eyeY = radius * 0.2; // 眼睛垂直位置 const eyeY = radius * 0.15; // 调整眼睛垂直位置
const eyeSize = radius * 0.08; // 眼睛大小 const eyeSize = radius * 0.12; // 增加眼睛大小
// 眼睛粒子数量也要根据比例调整
const eyeParticles = Math.floor(20 * scale);
// 创建实心眼睛
[-1, 1].forEach(side => { [-1, 1].forEach(side => {
// 创建密集的点来填充眼睛 for (let i = 0; i < eyeParticles; i++) {
for (let i = 0; i < 30; i++) { const r = Math.random() * eyeSize;
const r = Math.random() * eyeSize; // 随机半径 const angle = Math.random() * Math.PI * 2;
const angle = Math.random() * Math.PI * 2; // 随机角度
const x = side * eyeOffset + Math.cos(angle) * r; const x = side * eyeOffset + Math.cos(angle) * r;
const y = eyeY + Math.sin(angle) * r; const y = eyeY + Math.sin(angle) * r;
@ -128,18 +139,18 @@ const createSmileParticles = (width: number, height: number) => {
}); });
positionArray.push( positionArray.push(
(Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * size * 4,
(Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * size * 4,
(Math.random() - 0.5) * 100 (Math.random() - 0.5) * 150
); );
colorArray.push(particleColor.r, particleColor.g, particleColor.b); colorArray.push(particleColor.r, particleColor.g, particleColor.b);
} }
}); });
// 嘴巴参数 // 嘴巴参数调整
const smileWidth = radius * 0.5; // 嘴巴宽度 const smileWidth = radius * 0.6; // 增加嘴巴宽度
const smileY = -radius * 0.3; // 将嘴巴位置向下移动更多 const smileY = -radius * 0.25; // 调整嘴巴位置
const smilePoints = 40; // 嘴巴粒子数量 const smilePoints = Math.floor(30 * scale); // 根据大小调整嘴巴粒子数量
// 创建微笑 // 创建微笑
for (let i = 0; i < smilePoints; i++) { for (let i = 0; i < smilePoints; i++) {
@ -158,14 +169,14 @@ const createSmileParticles = (width: number, height: number) => {
}); });
positionArray.push( positionArray.push(
(Math.random() - 0.5) * width * 2, (Math.random() - 0.5) * size * 4,
(Math.random() - 0.5) * height * 2, (Math.random() - 0.5) * size * 4,
(Math.random() - 0.5) * 100 (Math.random() - 0.5) * 150
); );
colorArray.push(particleColor.r, particleColor.g, particleColor.b); colorArray.push(particleColor.r, particleColor.g, particleColor.b);
} }
return { particles, positionArray, colorArray }; return { particles, positionArray, colorArray, particleSize };
}; };
// 在文件开头添加新的 helper 函数 // 在文件开头添加新的 helper 函数
@ -179,33 +190,104 @@ const customEase = (t: number) => {
: 1 - Math.pow(-2 * t + 2, 3) / 2; : 1 - Math.pow(-2 * t + 2, 3) / 2;
}; };
export const ParticleImage = ({ src, onLoad, onError }: { interface ParticleImageProps {
src?: string; src?: string;
onLoad?: () => void; onLoad?: () => void;
onError?: () => void; onError?: () => void;
}) => { performanceMode?: boolean; // 新增性能模式开关
}
export const ParticleImage = ({
src,
onLoad,
onError,
performanceMode = false // 默认关闭
}: ParticleImageProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef<THREE.Scene>(); const sceneRef = useRef<THREE.Scene>();
const cameraRef = useRef<THREE.OrthographicCamera>(); const cameraRef = useRef<THREE.OrthographicCamera>();
const rendererRef = useRef<THREE.WebGLRenderer>(); const rendererRef = useRef<THREE.WebGLRenderer>();
const animationFrameRef = useRef<number>(); const animationFrameRef = useRef<number>();
// 添加 resize 处理函数 const width = containerRef.current?.offsetWidth || 0;
const height = containerRef.current?.offsetHeight || 0;
const size = Math.min(width, height);
const scale = size / 200; // 基准因子
// 在性能模式下使用更保守的参数
const particleCount = performanceMode ?
Math.floor(100 * scale) :
Math.floor(200 * scale);
// 添 resize 处理函数
const handleResize = useCallback(() => { const handleResize = useCallback(() => {
if (!containerRef.current || !cameraRef.current || !rendererRef.current) return; if (!containerRef.current || !cameraRef.current || !rendererRef.current || !sceneRef.current) return;
const width = containerRef.current.offsetWidth; const width = containerRef.current.offsetWidth;
const height = containerRef.current.offsetHeight; const height = containerRef.current.offsetHeight;
const camera = cameraRef.current; const camera = cameraRef.current;
camera.left = width / -2.1; camera.left = width / -2;
camera.right = width / 2.1; camera.right = width / 2;
camera.top = height / 2.1; camera.top = height / 2;
camera.bottom = height / -2.1; camera.bottom = height / -2;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
rendererRef.current.setSize(width, height); rendererRef.current.setSize(width, height);
}, []);
// 重新生成粒子
if (src === '') {
// 清除现有的 GSAP 动画
gsap.killTweensOf('*');
// 重新生成笑脸
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
const material = new THREE.PointsMaterial({
size: particleSize,
vertexColors: true,
transparent: true,
opacity: 1,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
depthTest: false
});
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3));
sceneRef.current.clear();
const points = new THREE.Points(geometry, material);
sceneRef.current.add(points);
// 修改这部分,添加动画而不是直接设置位置
const positionAttribute = geometry.attributes.position;
particles.forEach((particle, i) => {
const i3 = i * 3;
const distanceToCenter = Math.sqrt(
Math.pow(particle.originalX, 2) +
Math.pow(particle.originalY, 2)
);
const maxDistance = Math.sqrt(Math.pow(width/2, 2) + Math.pow(height/2, 2));
const normalizedDistance = distanceToCenter / maxDistance;
gsap.to(positionAttribute.array, {
duration: 0.8,
delay: normalizedDistance * 0.6,
[i3]: particle.originalX,
[i3 + 1]: particle.originalY,
[i3 + 2]: 0,
ease: "sine.inOut",
onUpdate: () => {
positionAttribute.needsUpdate = true;
}
});
});
}
}, [src]);
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
@ -218,14 +300,15 @@ export const ParticleImage = ({ src, onLoad, onError }: {
sceneRef.current = scene; sceneRef.current = scene;
const camera = new THREE.OrthographicCamera( const camera = new THREE.OrthographicCamera(
width / -1.5, // 扩大视野范围,从 -2 改为 -1.5 width / -2,
width / 1.5, // 扩大视野范围,从 2 改为 1.5 width / 2,
height / 1.5, // 扩大视野范围 height / 2,
height / -1.5, // 扩大视野范围 height / -2,
1, 1,
1000 1000
); );
camera.position.z = 100; camera.position.z = 500;
camera.lookAt(new THREE.Vector3(0, 0, 0));
cameraRef.current = camera; cameraRef.current = camera;
const renderer = new THREE.WebGLRenderer({ const renderer = new THREE.WebGLRenderer({
@ -237,9 +320,13 @@ export const ParticleImage = ({ src, onLoad, onError }: {
rendererRef.current = renderer; rendererRef.current = renderer;
containerRef.current.appendChild(renderer.domElement); containerRef.current.appendChild(renderer.domElement);
const geometry = new THREE.BufferGeometry(); // 检查是否应该显示笑脸
if (src === '') {
console.log('Showing smile animation');
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
const material = new THREE.PointsMaterial({ const material = new THREE.PointsMaterial({
size: 1.2, size: particleSize,
vertexColors: true, vertexColors: true,
transparent: true, transparent: true,
opacity: 1, opacity: 1,
@ -249,28 +336,33 @@ export const ParticleImage = ({ src, onLoad, onError }: {
depthTest: false depthTest: false
}); });
// 检查是否应该显示笑脸 const geometry = new THREE.BufferGeometry();
if (src === '') {
console.log('Showing smile animation');
const { particles, positionArray, colorArray } = createSmileParticles(width, height);
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3));
const points = new THREE.Points(geometry, material); const points = new THREE.Points(geometry, material);
scene.add(points); scene.add(points);
// 修改动画效果
const positionAttribute = geometry.attributes.position; const positionAttribute = geometry.attributes.position;
// 计算到中心的距离用于延迟
particles.forEach((particle, i) => { particles.forEach((particle, i) => {
const i3 = i * 3; const i3 = i * 3;
const distanceToCenter = Math.sqrt(
Math.pow(particle.originalX, 2) +
Math.pow(particle.originalY, 2)
);
const maxDistance = Math.sqrt(Math.pow(width/2, 2) + Math.pow(height/2, 2));
const normalizedDistance = distanceToCenter / maxDistance;
gsap.to(positionAttribute.array, { gsap.to(positionAttribute.array, {
duration: 1, duration: 0.8,
delay: Math.random() * 0.3, delay: normalizedDistance * 0.6,
[i3]: particle.originalX, [i3]: particle.originalX,
[i3 + 1]: particle.originalY, [i3 + 1]: particle.originalY,
[i3 + 2]: 0, [i3 + 2]: 0,
ease: "back.out(1.7)", ease: "sine.inOut",
onUpdate: () => { onUpdate: () => {
positionAttribute.needsUpdate = true; positionAttribute.needsUpdate = true;
} }
@ -286,19 +378,56 @@ export const ParticleImage = ({ src, onLoad, onError }: {
}; };
animate(); animate();
return; // 添加 resize 监听
const resizeObserver = new ResizeObserver(() => {
if (containerRef.current) {
handleResize();
}
});
resizeObserver.observe(containerRef.current);
// 添加窗口 resize 监听
window.addEventListener('resize', handleResize);
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
if (renderer && containerRef.current) {
containerRef.current.removeChild(renderer.domElement);
renderer.dispose();
}
gsap.killTweensOf('*');
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
window.removeEventListener('resize', handleResize);
};
} }
// 创建错误动画函数 // 创建错误动画函数
const showErrorAnimation = () => { const showErrorAnimation = () => {
if (!scene) return; if (!scene) return;
const { particles, positionArray, colorArray } = createErrorParticles(width, height); const { particles, positionArray, colorArray, particleSize } = createErrorParticles(width, height);
const material = new THREE.PointsMaterial({
size: particleSize,
vertexColors: true,
transparent: true,
opacity: 1,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
depthTest: false
});
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3));
const points = new THREE.Points(geometry, material); const points = new THREE.Points(geometry, material);
scene.clear(); // 清除现有内容 scene.clear();
scene.add(points); scene.add(points);
const positionAttribute = geometry.attributes.position; const positionAttribute = geometry.attributes.position;
@ -360,14 +489,14 @@ export const ParticleImage = ({ src, onLoad, onError }: {
offsetX = (width - drawWidth) / 2; offsetX = (width - drawWidth) / 2;
} }
// 绘制 // 绘制
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
const imageData = ctx.getImageData(0, 0, width, height); const imageData = ctx.getImageData(0, 0, width, height);
const particles: Particle[] = []; const particles: Particle[] = [];
const positionArray = []; const positionArray = [];
const colorArray = []; const colorArray = [];
const samplingGap = Math.ceil(Math.max(width, height) / 100); // 动态采样间隔,确保粒子数量适中 const samplingGap = Math.ceil(Math.max(width, height) / 80); // 减少采样密度
// 采样图片像素 // 采样图片像素
for (let y = 0; y < height; y += samplingGap) { for (let y = 0; y < height; y += samplingGap) {
@ -412,6 +541,22 @@ export const ParticleImage = ({ src, onLoad, onError }: {
} }
} }
} }
const size = Math.min(width, height);
const scale = size / 200;
const particleSize = Math.max(1.2, scale * 1.2);
const geometry = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial({
size: particleSize,
vertexColors: true,
transparent: true,
opacity: 1,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
depthTest: false
});
scene.clear(); scene.clear();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3));
@ -487,6 +632,8 @@ export const ParticleImage = ({ src, onLoad, onError }: {
duration: 0.8 duration: 0.8
}, "-=0.6"); // 提前开始消失 }, "-=0.6"); // 提前开始消失
} }
return { particles, positionArray, colorArray, particleSize };
} }
}; };
@ -523,7 +670,7 @@ export const ParticleImage = ({ src, onLoad, onError }: {
renderer.dispose(); renderer.dispose();
} }
// 清除所有 GSAP 动画 // 清除所有 GSAP 动画
gsap.killTweensOf(geometry.attributes.position?.array); gsap.killTweensOf('*');
// 移除 resize 监听 // 移除 resize 监听
if (containerRef.current) { if (containerRef.current) {