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/[email protected]/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):处理缩略图点击事件,切换显示的图像。

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

相关推荐
Nano4 分钟前
前端适配方案深度解析:从响应式到自适应设计
前端
古夕9 分钟前
如何将异步操作封装为Promise
前端·javascript
小小小小宇9 分钟前
前端定高和不定高虚拟列表
前端
@一枝梅15 分钟前
vue3 vite.config.js 引入bem.scss文件报错
javascript·rust·vue·scss
古夕20 分钟前
JS 模块化
前端·javascript
wandongle20 分钟前
HTML面试整理
前端·面试·html
liucan23320 分钟前
JS执行速度似乎并不比Swift或者C语言慢
前端·ios
一只小风华~23 分钟前
HTML前端开发:JavaScript 常用事件详解
前端·javascript·html
Revol_C25 分钟前
【调试日志】我只是用wangeditor上传图片而已,页面咋就崩溃了呢~
前端·vue.js·程序员
天天码行空29 分钟前
GruntJS-前端自动化任务运行器从入门到实战
前端