HTML&CSS:3D图片切换效果

这个页面实现了一个具有 3D 效果的画廊展示,用户可以通过点击缩略图来切换显示的图像。页面使用了 Three.js 库来实现 3D 渲染和动画效果,整体设计风格现代且具有视觉吸引力。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
    <title>Document</title>
    <style>
        body,
        html {
            margin: 0;
            padding: 0;
            overflow: hidden;
            font-family: sans-serif;
            background: #ffdfc4;
        }

        .container-gallary {
            position: relative;
            width: 100vw;
            height: 100vh;
            background-image: url(https://img.blacklead.work/grid.svg)
        }

        .canvas-wrapper {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 350px;
            height: 350px;
            transform: translate(-50%, -50%);
            clip-path: circle(50% at 50% 50%);
            overflow: hidden;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
        }

        .border-inside {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 340px;
            height: 340px;
            border: 10px solid black;
            border-radius: 100%;
            transform: translate(-50%, -50%);
            clip-path: circle(50% at 50% 50%);
        }


        .border-outside {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 364px;
            height: 364px;
            background: black;
            border-radius: 100%;
            transform: translate(-50%, -50%);
            clip-path: circle(50% at 50% 50%);
        }

        .border-outside::after {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            width: 354px;
            height: 354px;
            background-image: linear-gradient(180deg, #ffff82, #f4d2ba00 50%, #e8a5f3);
            border-radius: 100%;
            transform: translate(-50%, -50%);
            z-index: -1;
        }

        .thumbnails {
            position: absolute;
            bottom: 20px;
            right: 20px;
            display: flex;
            flex-direction: row;
            gap: 10px;
        }

        .thumbnail {
            display: flex;
            justify-content: center;
            align-items: center;
            position: relative;
            width: 74px;
            height: 105px;
            cursor: pointer;
            opacity: 0.6;
            overflow: hidden;
            transition: all 0.4s ease;
        }

        .thumbnail .frame {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-image: url("https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/6762b98cb5e68f0b74323e61_collection-card-frame.svg");
            background-size: cover;
            background-repeat: no-repeat;
            opacity: 0;
            transition: opacity 0.4s ease;
        }

        .thumbnail.active .frame,
        .thumbnail:hover .frame {
            opacity: 1;
        }

        .thumbnail.active {
            opacity: 1;
        }

        .thumbnail img {
            width: 66px;
            height: 99px;
            object-fit: cover;
        }
    </style>
</head>

<body>
    <div class="container-gallary">
        <div class="border-outside">
            <div class="canvas-wrapper" id="canvasWrapper">
                <span class="border-inside"></span>
            </div>
        </div>
        <div class="thumbnails" id="thumbnails"></div>
    </div>
    <script type="module">
        let renderer, scene, camera;
        let plane, material;
        let textures = [];
        let activeImage = 0;
        let transitionImage = null;
        let progress = 1;
        let isAnimating = false;

        const images = [
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c61f6db7df2e5218bc_collections-oranith-1.webp",
                title: "Image 1",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c6bd8971b3e73ee7c8_collections-anturax-1.webp",
                title: "Image 2",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c6648fdd5236d5b972_collections-oranith-2.webp",
                title: "Image 3",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c71c67e1e5c7edbcc0c3f_collections-anturax-3.webp",
                title: "Image 4",
            },
        ];
        const imagesThumbnail = [
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c41d8916da35baa9c_card-Oraniths-1.webp",
                title: "Image 1",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c65d779e7cfe7a75a_card-anturax-1.webp",
                title: "Image 2",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c5225fefdd3302e57_card-Oraniths-2.webp",
                title: "Image 3",
            },
            {
                url: "https://cdn.prod.website-files.com/675835c7f4ae1fa1a79b3733/682c7c7c8c0dbe0a8563fe55_card-anturax-3.webp",
                title: "Image 4",
            },
        ];

        const PIXELS = new Float32Array(
            [
                1, 1.5, 2, 2.5, 3, 1, 1.5, 2, 2.5, 3, 3.5, 4, 2, 2.5, 3, 3.5, 4, 4.5, 5,
                5.5, 6, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 20, 100,
            ].map((v) => v / 100)
        );

        function init() {
            const containerNext = document.getElementById("canvasWrapper");

            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
            camera.position.z = 10;

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(350, 350);
            containerNext.appendChild(renderer.domElement);

            const loader = new THREE.TextureLoader();
            let loadCount = 0;
            images.forEach((img, idx) => {
                loader.load(img.url, (tex) => {
                    tex.minFilter = THREE.LinearFilter;
                    tex.magFilter = THREE.LinearFilter;

                    textures[idx] = tex;
                    loadCount++;
                    if (loadCount === images.length) {
                        createScene();
                        animate();
                    }
                });
            });

            createThumbnails();
        }

        function createScene() {
            const vertexShader = `
          varying vec2 vUv;
          void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `;

            const fragmentShader = `
          uniform float uTime;
          uniform vec3 uFillColor;
          uniform float uProgress;
          uniform float uType;
          uniform float uPixels[36];
          uniform vec2 uTextureSize;
          uniform vec2 uElementSize;
          uniform sampler2D uTexture;
          varying vec2 vUv;

          vec2 fade(vec2 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
          vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
          vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
          vec3 fade3(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}

          float mapf(float value, float min1, float max1, float min2, float max2) {
            float val = min2 + (value - min1) * (max2 - min2) / (max1 - min1);
            return clamp(val, min2, max2);
          }

          float quadraticInOut(float t) {
            float p = 2.0 * t * t;
            return t < 0.5 ? p : -p + (4.0 * t) - 1.0;
          }

          void main() {
            vec2 uv = vUv - vec2(0.5);
            float aspect1 = uTextureSize.x/uTextureSize.y;
            float aspect2 = uElementSize.x/uElementSize.y;
            if(aspect1>aspect2){uv *= vec2( aspect2/aspect1,1.);}
            else{uv *= vec2( 1.,aspect1/aspect2);}
            uv += vec2(0.5);
            vec4 defaultColor = texture2D(uTexture, uv);

            if(uType==3.0){
              float progress = quadraticInOut(1.0-uProgress);
              float s = 50.0;
              float imageAspect = uTextureSize.x/uTextureSize.y;
              vec2 gridSize = vec2(
                s,
                floor(s/imageAspect)
              );

              float v = smoothstep(0.0, 1.0, vUv.y + sin(vUv.x*4.0+progress*6.0) * mix(0.3, 0.1, abs(0.5-vUv.x)) * 0.5 * smoothstep(0.0, 0.2, progress) + (1.0 - progress * 2.0));
              float mixnewUV = (vUv.x * 3.0 + (1.0-v) * 50.0)*progress;
              vec2 subUv = mix(uv, floor(uv * gridSize) / gridSize, mixnewUV);

              vec4 color = texture2D(uTexture, subUv);
              color.a =  mix(1.0, pow(v, 5.0) , step(0.0, progress));
              color.a = pow(v, 1.0);
              color.rgb = mix(color.rgb, uFillColor, smoothstep(0.5, 0.0, abs(0.5-color.a)) * progress);
              gl_FragColor = color;
            }
            gl_FragColor.rgb = pow(gl_FragColor.rgb,vec3(1.0/1.2));
          }
        `;

            material = new THREE.ShaderMaterial({
                vertexShader,
                fragmentShader,
                uniforms: {
                    uTime: { value: 0 },
                    uFillColor: { value: new THREE.Color("#000000") },
                    uProgress: { value: 1 },
                    uType: { value: 3 },
                    uPixels: { value: PIXELS },
                    uTextureSize: { value: new THREE.Vector2(1, 1) },
                    uElementSize: { value: new THREE.Vector2(1, 1) },
                    uTexture: { value: textures[activeImage] },
                },
                transparent: true,
            });

            material.uniforms.uTextureSize.value.set(
                textures[activeImage].image.width,
                textures[activeImage].image.height
            );

            const geometry = new THREE.PlaneGeometry(8.3, 8.3);
            plane = new THREE.Mesh(geometry, material);
            scene.add(plane);
        }

        function animate() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
            updateAnimation();
        }

        function updateAnimation() {
            if (transitionImage !== null && isAnimating) {
                progress += 0.015;

                if (
                    progress > 0.1 &&
                    material.uniforms.uTexture.value !== textures[transitionImage]
                ) {
                    material.uniforms.uTexture.value = textures[transitionImage];
                    material.uniforms.uTextureSize.value.set(
                        textures[transitionImage].image.width,
                        textures[transitionImage].image.height
                    );
                }

                if (progress >= 1) {
                    progress = 1;
                    activeImage = transitionImage;
                    transitionImage = null;
                    isAnimating = false;
                }
                material.uniforms.uProgress.value = progress;
            }
        }

        function createThumbnails() {
            const thumbsContainer = document.getElementById("thumbnails");
            imagesThumbnail.forEach((img, idx) => {
                const thumb = document.createElement("div");
                thumb.className = "thumbnail" + (idx === activeImage ? " active" : "");

                const thumbnailImg = document.createElement("img");
                thumbnailImg.src = img.url;
                thumbnailImg.alt = img.title;
                thumb.appendChild(thumbnailImg);

                const frame = document.createElement("div");
                frame.className = "frame";
                thumb.appendChild(frame);

                thumb.addEventListener("click", () => handleThumbnailClick(idx));

                thumbsContainer.appendChild(thumb);
            });
        }

        function handleThumbnailClick(index) {
            if (index === activeImage || isAnimating) return;
            transitionImage = index;
            progress = 0;
            isAnimating = true;

            const thumbs = document.querySelectorAll(".thumbnail");
            thumbs.forEach((t, i) => {
                t.classList.remove("active");
                if (i === index) t.classList.add("active");
            });
        }

        document.addEventListener("DOMContentLoaded", init);

    </script>
</body>

</html>

HTML

  • container-gallary:定义了一个画廊容器,包含一个 3D 渲染的画布和缩略图导航。
  • border-outside:定义了一个外部边框,包含一个画布容器和一个内部边框。
  • canvas-wrapper" id="canvasWrapper:定义了一个画布容器,用于显示 3D 内容。
  • border-inside:定义了一个内部边框。
  • thumbnails" id="thumbnails:定义了一个缩略图导航容器,包含多个缩略图项。

CSS

  • body, html:设置页面的外边距和内边距为 0,隐藏溢出内容,设置字体系列为无衬线字体,并定义背景颜色。
  • .container-gallary:定义了画廊容器的样式,包括宽度、高度和背景图像。
  • .canvas-wrapper:定义了画布容器的样式,包括位置、宽度、高度和变换效果。
  • .border-inside:定义了内部边框的样式,包括位置、宽度、高度、边框和圆角。
  • .border-outside:定义了外部边框的样式,包括位置、宽度、高度、背景颜色和圆角。
  • .border-outside::after:定义了外部边框的伪元素样式,用于创建渐变背景。
  • .thumbnails:定义了缩略图容器的样式,包括位置、底部和右侧的偏移、布局和间隙。
  • .thumbnail:定义了单个缩略图的样式,包括位置、宽度、高度、鼠标指针样式、透明度和过渡效果。
  • .thumbnail .frame:定义了缩略图框架的样式,包括位置、宽度、高度、背景图像和透明度。
  • .thumbnail.active:定义了活动缩略图的样式,包括透明度。
  • .thumbnail img:定义了缩略图图像的样式,包括宽度、高度和对象适应方式。

JavaScript

  • init():初始化 Three.js 场景、相机和渲染器,并加载纹理。
  • createScene():创建 3D 场景,包括几何体和着色器材质。
  • animate():渲染场景并更新动画。
  • updateAnimation():更新动画进度,控制图像切换效果。
  • createThumbnails():创建缩略图项并添加到页面。
  • handleThumbnailClick(index):处理缩略图点击事件,切换显示的图像。

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼12 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax