前端:优化图片加载长连接

This commit is contained in:
lsy 2024-12-05 18:33:08 +08:00
parent 3f52d609a3
commit caa23c6ac5
2 changed files with 113 additions and 29 deletions

View File

@ -185,18 +185,26 @@ const customEase = (t: number) => {
: 1 - Math.pow(-2 * t + 2, 3) / 2; : 1 - Math.pow(-2 * t + 2, 3) / 2;
}; };
// 在文件开头添加新的 LoaderStatus 接口
interface LoaderStatus {
isLoading: boolean;
hasError: boolean;
timeoutError: boolean;
}
// 修改 ParticleImage 组件的 props 接口
interface ParticleImageProps { interface ParticleImageProps {
src?: string; src?: string;
status?: LoaderStatus;
onLoad?: () => void; onLoad?: () => void;
onError?: () => void; onAnimationComplete?: () => void;
performanceMode?: boolean; // 新增性能模式开关
} }
export const ParticleImage = ({ export const ParticleImage = ({
src, src,
onLoad, status,
onError, onLoad,
performanceMode = false onAnimationComplete
}: ParticleImageProps) => { }: ParticleImageProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef<THREE.Scene>(); const sceneRef = useRef<THREE.Scene>();
@ -222,7 +230,7 @@ export const ParticleImage = ({
// 更新渲染器大小 // 更新渲染器大小
rendererRef.current.setSize(width, height); rendererRef.current.setSize(width, height);
// 只有当尺寸变化超过阈值时才重生成粒子 // 只有当尺寸变化超过阈值时才重生成粒子
const currentSize = Math.min(width, height); const currentSize = Math.min(width, height);
const previousSize = sceneRef.current.userData.previousSize || currentSize; const previousSize = sceneRef.current.userData.previousSize || currentSize;
const sizeChange = Math.abs(currentSize - previousSize) / previousSize; const sizeChange = Math.abs(currentSize - previousSize) / previousSize;
@ -400,7 +408,7 @@ export const ParticleImage = ({
}; };
} }
// 创建错误动画函数 // 建错误动<EFBFBD><EFBFBD>函数
const showErrorAnimation = () => { const showErrorAnimation = () => {
if (!scene) return; if (!scene) return;
@ -441,11 +449,9 @@ export const ParticleImage = ({
} }
}); });
}); });
onError?.();
}; };
// 加载图 // 加载图<EFBFBD><EFBFBD><EFBFBD>
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous'; img.crossOrigin = 'anonymous';
@ -475,7 +481,7 @@ export const ParticleImage = ({
sourceWidth = img.height * targetAspect; sourceWidth = img.height * targetAspect;
sourceX = (img.width - sourceWidth) / 2; sourceX = (img.width - sourceWidth) / 2;
} else { } else {
// 图片较高,需要裁剪 // 图片较高,需要裁剪
sourceHeight = img.width / targetAspect; sourceHeight = img.width / targetAspect;
sourceY = (img.height - sourceHeight) / 2; sourceY = (img.height - sourceHeight) / 2;
} }
@ -574,7 +580,8 @@ export const ParticleImage = ({
const checkComplete = () => { const checkComplete = () => {
completedAnimations++; completedAnimations++;
if (completedAnimations === totalAnimations) { if (completedAnimations === totalAnimations) {
onLoad?.(); // 所有画完成后调用 onLoad onLoad?.();
onAnimationComplete?.();
} }
}; };
@ -583,8 +590,8 @@ export const ParticleImage = ({
// 位置动画 // 位置动画
gsap.to(positionAttribute.array, { gsap.to(positionAttribute.array, {
duration: 1.2 + Math.random() * 0.3, // 减少随机性范围 duration: 1.2 + Math.random() * 0.3,
delay: particle.delay, // 使用基于距离的延迟 delay: particle.delay,
[i3]: particle.originalX, [i3]: particle.originalX,
[i3 + 1]: particle.originalY, [i3 + 1]: particle.originalY,
[i3 + 2]: 0, [i3 + 2]: 0,
@ -598,7 +605,7 @@ export const ParticleImage = ({
// 颜色动画 // 颜色动画
gsap.to(colorAttribute.array, { gsap.to(colorAttribute.array, {
duration: 1, duration: 1,
delay: particle.delay + 0.2, // 稍微延迟颜色变化 delay: particle.delay + 0.2,
[i3]: particle.originalColor.r, [i3]: particle.originalColor.r,
[i3 + 1]: particle.originalColor.g, [i3 + 1]: particle.originalColor.g,
[i3 + 2]: particle.originalColor.b, [i3 + 2]: particle.originalColor.b,
@ -668,7 +675,7 @@ export const ParticleImage = ({
containerRef.current.removeChild(renderer.domElement); containerRef.current.removeChild(renderer.domElement);
renderer.dispose(); renderer.dispose();
} }
// 清所有 GSAP 动画 // 清所有 GSAP 动画
gsap.killTweensOf('*'); gsap.killTweensOf('*');
// 移除 resize 监听 // 移除 resize 监听
@ -677,45 +684,122 @@ export const ParticleImage = ({
} }
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}; };
}, [src, onError, handleResize]); }, [src, handleResize, onLoad, onAnimationComplete]);
return <div ref={containerRef} className="w-full h-full" />; return <div ref={containerRef} className="w-full h-full" />;
}; };
// 图片加载组件 // 图片加载组件
export const ImageLoader = ({ src, alt, className }: { export const ImageLoader = ({
src,
alt,
className
}: {
src?: string; src?: string;
alt: string; alt: string;
className: string; className: string;
}) => { }) => {
const [isLoading, setIsLoading] = useState(true); const [status, setStatus] = useState<LoaderStatus>({
const [hasError, setHasError] = useState(false); isLoading: true,
hasError: false,
timeoutError: false
});
const [showImage, setShowImage] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const loadingRef = useRef(false);
const imageRef = useRef<HTMLImageElement | null>(null);
// 处理图片预加载
const preloadImage = useCallback(() => {
if (!src || loadingRef.current) return;
loadingRef.current = true;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setStatus({
isLoading: true,
hasError: false,
timeoutError: false
});
setShowImage(false);
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
loadingRef.current = false;
// 图片加载成功后,不立即显示,等待粒子动画完成
imageRef.current = img;
setStatus({
isLoading: false,
hasError: false,
timeoutError: false
});
};
img.onerror = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
loadingRef.current = false;
setStatus({
isLoading: false,
hasError: true,
timeoutError: false
});
};
img.src = src;
}, [src]);
useEffect(() => {
preloadImage();
return () => {
loadingRef.current = false;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [src, preloadImage]);
return ( return (
<div className="relative w-[140px] md:w-[180px] h-[140px] md:h-[180px] shrink-0 overflow-hidden"> <div className="relative w-[140px] md:w-[180px] h-[140px] md:h-[180px] shrink-0 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-[rgb(10,37,77)] via-[rgb(8,27,57)] to-[rgb(2,8,23)] rounded-lg overflow-hidden"> <div className="absolute inset-0 bg-gradient-to-br from-[rgb(10,37,77)] via-[rgb(8,27,57)] to-[rgb(2,8,23)] rounded-lg overflow-hidden">
<ParticleImage <ParticleImage
src={src} src={src}
onLoad={() => setIsLoading(false)} status={status}
onError={() => { onLoad={() => {
setIsLoading(false); // 粒子动画完成后,延迟显示图片
setHasError(true); setTimeout(() => {
setShowImage(true);
}, 800); // 延迟时间可以根据需要调整
}}
onAnimationComplete={() => {
setShowImage(true);
}} }}
/> />
</div> </div>
{!hasError && ( {!status.hasError && !status.timeoutError && imageRef.current && (
<div className="absolute inset-0 rounded-lg overflow-hidden"> <div className="absolute inset-0 rounded-lg overflow-hidden">
<img <img
src={src} src={imageRef.current.src}
alt={alt} alt={alt}
className={` className={`
w-full h-full object-cover w-full h-full object-cover
transition-opacity duration-1000 transition-opacity duration-1000
${className} ${className}
${isLoading ? 'opacity-0' : 'opacity-100'} ${showImage ? 'opacity-100' : 'opacity-0'}
`} `}
style={{ style={{
visibility: isLoading ? 'hidden' : 'visible', visibility: showImage ? 'visible' : 'hidden',
objectFit: 'cover', objectFit: 'cover',
objectPosition: 'center' objectPosition: 'center'
}} }}

View File

@ -6,9 +6,9 @@ import {
LinkedInLogoIcon, LinkedInLogoIcon,
EnvelopeClosedIcon, EnvelopeClosedIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { ParticleImage } from "hooks/ParticleImage";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ParticleImage } from "hooks/ParticleImage";
const socialLinks = [ const socialLinks = [
{ {