HTML&CSS:3D图片切换效果
2025年6月9日 14:31
这个页面实现了一个具有 3D 效果的画廊展示,用户可以通过点击缩略图来切换显示的图像。页面使用了 Three.js 库来实现 3D 渲染和动画效果,整体设计风格现代且具有视觉吸引力。
大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。
演示效果
HTML&CSS
<!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):处理缩略图点击事件,切换显示的图像。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!