说在前面
大家平时做图片展示,最常见的方案就是宫格、瀑布流、横向轮播。
这些方案都能用,但如果放在音乐专辑、影视海报、作品集、活动视觉墙这种页面里,总会感觉差点"沉浸感"。
这次这个效果的核心思路很简单:
- 用多列图片组成一个封面墙
- 整体做 3D 旋转,营造斜向空间
- 每一列上下滚动,相邻列方向相反
- 鼠标 hover 时暂停当前列,并突出当前封面
代码实现
1、html
页面结构其实非常简单,真正复杂的部分都交给 CSS 和 JS。
<section class="stage">
<div class="column-wrapper">
<div class="columns" id="columns"></div>
</div>
</section>
这里有三层:
stage:负责把整个效果放到页面正中间。column-wrapper:负责限制可视区域,同时提供边框、阴影、遮罩。columns:真正放多列图片的容器,后面会用 JS 动态生成。
对应的基础 CSS 如下:
:root {
--body-bg: #000;
--base-font-size: 16px;
--window-width: 900px;
--column-height: 420px;
--image-height: 200px;
--row-gap: 0.5em;
--column-gap: 0.25em;
--number-of-images: 10;
--number-of-columns: 5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
min-height: 100%;
}
body {
background: radial-gradient(circle at 50% 25%, #141414 0%, #060606 45%, #000 100%);
font-size: var(--base-font-size);
font-family: "Poppins", "PingFang SC", sans-serif;
color: #586270;
overflow-x: hidden;
}
.stage {
height: 100vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
代码说明:
:root里把尺寸都抽成 CSS 变量,后续改大小更方便。stage使用 flex 居中,保证效果一直在视口中心。- 背景用了径向渐变,让黑色背景中间稍微亮一点,封面墙不会显得太死板。
2、透视容器 + 暗角遮罩
如果只是把图片排成 5 列,那就是普通宫格。
想让它有"展示橱窗"的感觉,第一步就是给外层容器加 perspective。
.column-wrapper {
width: var(--window-width);
height: var(--column-height);
perspective: 1000px;
position: relative;
overflow: clip;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 40px 90px rgba(0, 0, 0, 0.65),
inset 0 0 0 1px rgba(255, 255, 255, 0.03);
background: #050505;
}
.column-wrapper::before {
content: "";
position: absolute;
inset: -12%;
z-index: 3;
pointer-events: none;
background: radial-gradient(
circle at 50% 50%,
transparent 22%,
rgba(0, 0, 0, 0.45) 68%,
rgba(0, 0, 0, 0.82) 100%
);
}
.column-wrapper::after {
content: "";
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
background:
radial-gradient(circle at 52% 45%, rgba(255, 255, 255, 0.2), transparent 42%),
linear-gradient(to bottom, rgba(255, 255, 255, 0.06), transparent 30%);
}
这里有几个小细节:
perspective: 1000px是空间感的基础,后面子元素旋转时才会有近大远小的视觉。overflow: clip把超出容器的图片裁掉,封面墙就像在一个窗口里滚动。::before做暗角,边缘压暗,中间视觉更聚焦。::after做一点高光,让画面更像玻璃橱窗。
效果说明:同样是图片列表,加上透视、裁剪、暗角之后,立刻就从"表格感"变成了"舞台感" 。
3、3D 感:把整组列旋转起来
重点来了,封面墙不是正着放的,而是整体做了一组 3D 变换。
.columns {
position: absolute;
width: 175%;
top: calc(-1 * var(--column-height));
right: calc(-1 * var(--window-width));
display: grid;
grid-template-columns: repeat(var(--number-of-columns), 1fr);
transform: rotateX(45deg) rotateY(20deg) rotate(-25deg) translate3d(-6em, 8em, 8em);
transform-origin: 50%;
transform-style: preserve-3d;
z-index: 1;
mask-image: linear-gradient(
#0000 0%,
#00000005 2.3%,
#00000009 2.57%,
#00000013 3.65%,
#00000026 5.25%,
#0000004d 7.5%,
#000 15%
);
}
.column {
display: flex;
flex-direction: column;
margin-left: var(--column-gap);
margin-right: var(--column-gap);
}
.column:nth-child(1),
.column:nth-child(4) {
padding-top: 100px;
}
.column:nth-child(2),
.column:nth-child(5) {
padding-top: 50px;
}
这一段可以拆成三件事:
grid-template-columns把容器切成 5 列。rotateX + rotateY + rotate让整组图片倾斜到一个比较舒服的角度。- 不同列加不同的
padding-top,避免每列图片起点完全对齐,看起来更自然。
这里的 width: 175%、top、right 也很关键。
因为旋转之后,图片会占据比原来更大的视觉范围,如果容器还是 100% 宽,就很容易露出空白。所以直接把内部内容做大,再移动到合适位置,最后由外层窗口裁剪。
效果说明:图片不再像"贴在屏幕上",而是像从右上方向左下方铺开的一面动态封面墙 🎬。
4、背景图 + hover 微交互
每一张专辑封面其实就是一个普通的 div,图片通过 background-image 设置。
.column > div {
height: var(--image-height);
margin-bottom: var(--row-gap);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
border-radius: 4px;
transition:
transform 280ms ease,
filter 280ms ease,
box-shadow 280ms ease;
transform-origin: center;
cursor: pointer;
}
.column:hover > div {
filter: brightness(0.72) saturate(0.78);
}
.column:hover > div:hover {
transform: scale(1.08);
filter: brightness(1.05) saturate(1.08);
box-shadow:
0 18px 28px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.4);
z-index: 4;
}
这一段做的是交互质感:
- 鼠标进入某一列时,这一列所有图片先整体压暗。
- 鼠标再指向某一张时,当前图片放大、提亮、加阴影。
transition控制变化节奏,避免 hover 时很生硬。
效果说明:用户移动鼠标时,视觉焦点会自然落在当前封面上,不用额外写提示文字 。
5、滚动动画
这个效果最有意思的地方,是相邻列的滚动方向不同。
.down {
animation: imageScrollingDown 25s linear infinite alternate;
}
.up {
animation: imageScrollingUp 25s linear infinite alternate;
}
.column.up:hover,
.column.down:hover {
animation-play-state: paused;
}
up 和 down 两个类分别控制向上、向下滚动。
鼠标移入时,通过 animation-play-state: paused 暂停动画。这个做法很适合图片墙,不然用户刚想看某张图,它"嗖"一下滚走了,多少有点不讲武德 😂。
滚动距离的计算也很巧:
@keyframes imageScrollingUp {
0% {
transform: translateY(0);
}
100% {
transform: translateY(
calc(
-1 *
(((var(--image-height) + var(--row-gap)) * var(--number-of-images)) - var(--column-height))
)
);
}
}
@keyframes imageScrollingDown {
0% {
transform: translateY(
calc(
-1 *
(((var(--image-height) + var(--row-gap)) * var(--number-of-images)) - var(--column-height))
)
);
}
100% {
transform: translateY(0);
}
}
核心公式是:
滚动距离 = 图片总高度 - 外层窗口高度
也就是:
(单张图片高度 + 图片间距) * 图片数量 - 可视窗口高度
这样写的好处是,后续如果想改图片高度、图片数量、窗口高度,只需要改 :root 里的变量,动画距离会自动跟着变。
效果说明:每一列都在自己的可滚动范围内来回移动,不会滚出一大片空白,也不会突然跳动。
6、图片预加载 + 动态生成列
HTML 里没有提前写死图片节点,而是用 JS 根据图片数组动态生成。
const imageList = [
"http://5b0988e595225.cdn.sohucs.com/images/20180130/24aecf0d680546ccbf6480badce389dc.jpeg",
"https://pic3.zhimg.com/v2-f1159536dc58af663e3955e774a7a13a_r.jpg",
"http://qqpublic.qpic.cn/qq_public_cover/0/0-2083471255-1DB77954D274653D98B57D000A686040_vsmcut/0",
"http://p6.itc.cn/q_70/images03/20210106/6cd651a28f9a46268205410dcc5cad4d.jpeg",
"http://5b0988e595225.cdn.sohucs.com/images/20180824/781646fe72fc4788b7dfc24c112b5bbb.png",
"http://5b0988e595225.cdn.sohucs.com/images/20171122/c839ec116c7e4853ac9d34a7abf8b4cc.jpeg",
"https://pic3.zhimg.com/v2-d0590fa634c9aae77339e820e185b75b_1440w.jpg?source=172ae18b",
"https://pic1.zhimg.com/v2-8c3fee751e08bd3df46a54fee8b21761_1440w.jpg?source=172ae18b",
"http://5b0988e595225.cdn.sohucs.com/images/20180109/b9beeb33475e4fa9b160590434324c98.jpeg",
"https://upload-images.jianshu.io/upload_images/12146303-33f6a5bb122c112a.jpg",
"http://i0.hdslb.com/bfs/article/8ac9ca5c4737304747c0ffede73c60b06d0deb2c.jpg",
"http://qqpublic.qpic.cn/qq_public_cover/0/0-2925470415-C0AA5AD7CE48CB549EE81AA2E673308A_vsmcut/0",
];
const columnsRoot = document.getElementById("columns");
const columnCount = 5;
const imagesPerColumn = 10;
function preloadImages(urls) {
return Promise.all(
urls.map(
(url) =>
new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve({ url, ok: true });
img.onerror = () => resolve({ url, ok: false });
img.src = url;
}),
),
);
}
function renderColumns(sourceList) {
const fragment = document.createDocumentFragment();
for (let col = 0; col < columnCount; col += 1) {
const column = document.createElement("div");
column.className = `column ${col % 2 === 0 ? "up" : "down"}`;
for (let row = 0; row < imagesPerColumn; row += 1) {
const tile = document.createElement("div");
const index = (col * imagesPerColumn + row) % sourceList.length;
tile.style.backgroundImage = `url(${sourceList[index]})`;
column.appendChild(tile);
}
fragment.appendChild(column);
}
columnsRoot.innerHTML = "";
columnsRoot.appendChild(fragment);
}
preloadImages(imageList).then((results) => {
const validList = results.filter((item) => item.ok).map((item) => item.url);
const finalList = validList.length > 0 ? validList : imageList;
renderColumns(finalList);
});
preloadImages会先尝试加载所有图片,过滤掉加载失败的地址。DocumentFragment用来批量插入 DOM,减少多次触发布局计算。col % 2 === 0 ? "up" : "down"控制奇偶列反向滚动。(col * imagesPerColumn + row) % sourceList.length让图片不够时循环复用。
效果说明:图片列表可以随便换成自己的专辑封面、影视海报、商品图,布局逻辑不用改。
7、移动端适配
最后再处理一下小屏幕。
@media (max-width: 860px) {
.column-wrapper {
width: min(96vw, var(--window-width));
height: min(52vw, var(--column-height));
}
.columns {
right: -90vw;
}
}
这里没有做太复杂的响应式,只是把外层窗口限制到视口宽度内。
为什么这样就够了?
因为内部的 3D 封面墙本身就是被外层窗口裁剪的,移动端只要保证窗口别超屏,视觉依然成立。
8、完整思路小结
这个专辑列滚动轮播图其实没有复杂算法,主要是把几个 CSS 能力组合到一起:
| 模块 | 作用 |
|---|---|
perspective | 给容器提供 3D 观察距离 |
rotateX / rotateY / rotate | 让封面墙产生斜向空间感 |
grid | 把图片拆成多列 |
animation | 控制每列上下滚动 |
animation-play-state | hover 时暂停当前列 |
filter + transform | 实现图片聚焦和高亮 |
DocumentFragment | 批量生成 DOM,渲染更稳 |
所以它很适合这些场景:
- 音乐 App 首页专辑推荐
- 影视网站海报墙
- 摄影作品集展示
- 活动页品牌视觉墙
- 个人主页项目展示背景
源码地址
gitee
源码地址: gitee.com/zheng_yongt…
github
🌟 觉得有帮助的可以点个 star~
🖊 有什么问题或错误可以指出,欢迎 pr~
📬 有什么想要实现的功能或想法可以联系我~
公众号
关注公众号『 前端也能这么有趣 』,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。