You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 14:12:01