React/Next.js实现随机定位且全视口覆盖的预加载图片排布方案
解决方案
你当前的问题根源是固定范围的纯随机偏移没有和视口区域绑定,所有图片都集中在固定像素范围内随机排布,自然容易出现边缘空白、堆叠不均匀的问题。可以通过网格分块随机+视口自适应计算的方案实现,既保留随机散列的视觉效果,又保证全视口无空白覆盖,具体实现如下:
核心思路
- 先将整个视口等分为和图片数量匹配的网格区域,每张图片分配到独立网格,从根上保证所有区域都有图片覆盖,不会出现整块空白
- 图片的偏移范围限制在所属网格内,仅在网格内部做随机偏移,同时增加小幅度跨网格偏移量,避免出现明显的网格分割感
- 偏移计算基于实时视口尺寸,适配不同设备屏幕,不写死固定像素范围
- 额外增加随机缩放、随机旋转、随机层级配置,让散列效果更自然
修改后代码
import { useEffect, useRef, useState, useContext } from 'react' import { gsap, Linear } from 'gsap' import imagesLoaded from 'imagesloaded' import styles from '../../styles/Preloader.module.scss' import ScrollContext from "../Context" // 仅保留图片基础配置,样式后续动态生成 const baseImages = [ { src: "/preloader/img.png", landscape: true, alt: "Code" }, { src: "/preloader/img.png", landscape: true, alt: "Styleguide" }, { src: "/preloader/img.png", landscape: true, alt: "Office" }, { src: "/preloader/img.png", landscape: true, alt: "Device screens" }, { src: "/preloader/img.png", landscape: true, alt: "Zoom meetings" }, ] const Preloader = () => { let [progress, setProgress] = useState(0); const [processedImages, setProcessedImages] = useState([]); const scrollingStatus = useContext(ScrollContext); const {scrolable, setScrollable} = scrollingStatus; const indicatorRef = useRef(null); const welcomeRef = useRef(null); const imgRef = useRef(null); const TLImages = gsap.timeline({ paused: true }); const [rendered, setRendered] = useState(false); useEffect(() => { // 获取实时视口尺寸 const viewportW = window.innerWidth; const viewportH = window.innerHeight; // 配置网格行列数,总网格数≥图片数量即可,此处5张图用2行3列共6个网格 const gridCols = 3; const gridRows = 2; const cellW = viewportW / gridCols; const cellH = viewportH / gridRows; // 生成所有网格坐标并打乱 const allGridPos = []; for(let i=0; i<gridRows; i++) { for(let j=0; j<gridCols; j++) { allGridPos.push({row: i, col: j}) } } const shuffledPos = allGridPos.sort(() => Math.random() - 0.5).slice(0, baseImages.length); // 生成带随机样式的最终图片配置 const finalImgs = baseImages.map((item, idx) => { const pos = shuffledPos[idx]; // 所属网格的左上角基准坐标 const baseLeft = pos.col * cellW; const baseTop = pos.row * cellH; // 网格内随机偏移+±50px跨网格偏移,避免网格感 const randomLeft = baseLeft + Math.floor(Math.random() * cellW) + Math.floor((Math.random()-0.5)*100); const randomTop = baseTop + Math.floor(Math.random() * cellH) + Math.floor((Math.random()-0.5)*100); // 随机缩放、旋转、层级,增强随机效果 const randomScale = 0.8 + Math.random() * 0.4; const randomRotate = Math.floor((Math.random()-0.5)*30); const randomZIndex = Math.floor(Math.random() * 10); return { ...item, styles: { position: "absolute", left: `${randomLeft}px`, top: `${randomTop}px`, transform: `scale(${randomScale}) rotate(${randomRotate}deg)`, zIndex: randomZIndex } } }) setProcessedImages(finalImgs); setRendered(true); }, []) useEffect(() => { if (typeof document !== "undefined") { document.body.style.overflowY = scrolable ? "auto" : "hidden"; } console.log(scrolable) }, [scrolable]); useEffect(() => { if (welcomeRef.current) { gsap.fromTo(welcomeRef.current, { alpha: 0, duration: 40 }, { alpha: 1 }); setScrollable(false); } TLImages.to(".imagesWrapper", { alpha: 1, ease: Linear.easeNone }); TLImages.add( gsap.fromTo( ".images", { visibility: "hidden", stagger: 0.5 }, { visibility: "visible", stagger: 0.5 } ) ); TLImages.add( gsap.to(".preloader", { alpha: 0, onComplete: function () { setScrollable(true); }, }) ); }, []); useEffect(() => { setTimeout(() => { let loadedCount = 0; let loadingProgress = 0; var imgLoad = imagesLoaded("#__next"); if (imgLoad.images.length === 0) { loadingProgress = 100; doneLoading(); } imgLoad.on("progress", function (instance) { var imagesToLoad = instance.images.length; loadProgress(imagesToLoad); }); function loadProgress(imagesToLoad) { loadedCount++; loadingProgress = (loadedCount / imagesToLoad) * 100; setProgress(loadingProgress); if (loadingProgress === 100) { doneLoading(); } } function doneLoading() { gsap.to(indicatorRef.current, { width: "100%", onComplete: function () { gsap.to(welcomeRef.current, { alpha: 0, duration: 0.4, onComplete: function () { TLImages.play(); }, }); }, }); } }, 1000); }, []); return ( <div className={`${styles.preloader_container} preloader`} style={scrolable ? { zIndex: "-1" } : {zIndex: "99999"}}> <div className={styles.preloader_wrapper}> <div className={styles.preloader_inner_wrapper}> <div className={styles.future} ref={welcomeRef}> <p className={styles.mid_uppercase}>Do you want to see what the future of digital services looks like?</p> <div className={styles.progress_bar}> <div className={styles.progress} ref={indicatorRef}></div> </div> </div> <div className={`${styles.images} imagesWrapper`}> <div className={styles.imagesInner}> {processedImages.map((item, idx) => ( <figure ref={imgRef} className={`${styles.figure} ${item.landscape ? styles.landscape : styles.portrait} images`} key={`img-${idx}`} > <img src={item.src} alt={item.alt} style={rendered ? item.styles : {}} className={styles.image} /> </figure> ))} </div> </div> </div> </div> </div> ); }; export default Preloader
额外注意事项
- 必须给
styles.imagesInner对应的SCSS样式添加position: relative; width: 100vw; height: 100vh; overflow: hidden;配置,否则绝对定位的图片会布局错乱 - 如果后续增加图片数量,对应调整
gridCols和gridRows的数值,保证总网格数≥图片数量即可 - 可以自行调整跨网格偏移量、缩放范围、旋转范围的数值,匹配你需要的散列效果强度
内容的提问来源于stack exchange,提问作者T.Doe
相关产品推荐
相关产品推荐

