实现一个专辑列滚动轮播图

说在前面

大家平时做图片展示,最常见的方案就是宫格、瀑布流、横向轮播。

这些方案都能用,但如果放在音乐专辑、影视海报、作品集、活动视觉墙这种页面里,总会感觉差点"沉浸感"。

这次这个效果的核心思路很简单:

  • 用多列图片组成一个封面墙
  • 整体做 3D 旋转,营造斜向空间
  • 每一列上下滚动,相邻列方向相反
  • 鼠标 hover 时暂停当前列,并突出当前封面

代码实现

1、html

页面结构其实非常简单,真正复杂的部分都交给 CSS 和 JS。

html 复制代码
<section class="stage">
  <div class="column-wrapper">
    <div class="columns" id="columns"></div>
  </div>
</section>

这里有三层:

  1. stage:负责把整个效果放到页面正中间。
  2. column-wrapper:负责限制可视区域,同时提供边框、阴影、遮罩。
  3. columns:真正放多列图片的容器,后面会用 JS 动态生成。

对应的基础 CSS 如下:

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

css 复制代码
.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%);
}

这里有几个小细节:

  1. perspective: 1000px 是空间感的基础,后面子元素旋转时才会有近大远小的视觉。
  2. overflow: clip 把超出容器的图片裁掉,封面墙就像在一个窗口里滚动。
  3. ::before 做暗角,边缘压暗,中间视觉更聚焦。
  4. ::after 做一点高光,让画面更像玻璃橱窗。

效果说明:同样是图片列表,加上透视、裁剪、暗角之后,立刻就从"表格感"变成了"舞台感" 。

3、3D 感:把整组列旋转起来

重点来了,封面墙不是正着放的,而是整体做了一组 3D 变换。

css 复制代码
.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;
}

这一段可以拆成三件事:

  1. grid-template-columns 把容器切成 5 列。
  2. rotateX + rotateY + rotate 让整组图片倾斜到一个比较舒服的角度。
  3. 不同列加不同的 padding-top,避免每列图片起点完全对齐,看起来更自然。

这里的 width: 175%topright 也很关键。

因为旋转之后,图片会占据比原来更大的视觉范围,如果容器还是 100% 宽,就很容易露出空白。所以直接把内部内容做大,再移动到合适位置,最后由外层窗口裁剪。

效果说明:图片不再像"贴在屏幕上",而是像从右上方向左下方铺开的一面动态封面墙 🎬。

4、背景图 + hover 微交互

每一张专辑封面其实就是一个普通的 div,图片通过 background-image 设置。

css 复制代码
.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、滚动动画

这个效果最有意思的地方,是相邻列的滚动方向不同。

css 复制代码
.down {
  animation: imageScrollingDown 25s linear infinite alternate;
}

.up {
  animation: imageScrollingUp 25s linear infinite alternate;
}

.column.up:hover,
.column.down:hover {
  animation-play-state: paused;
}

updown 两个类分别控制向上、向下滚动。

鼠标移入时,通过 animation-play-state: paused 暂停动画。这个做法很适合图片墙,不然用户刚想看某张图,它"嗖"一下滚走了,多少有点不讲武德 😂。

滚动距离的计算也很巧:

css 复制代码
@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);
  }
}

核心公式是:

text 复制代码
滚动距离 = 图片总高度 - 外层窗口高度

也就是:

text 复制代码
(单张图片高度 + 图片间距) * 图片数量 - 可视窗口高度

这样写的好处是,后续如果想改图片高度、图片数量、窗口高度,只需要改 :root 里的变量,动画距离会自动跟着变。

效果说明:每一列都在自己的可滚动范围内来回移动,不会滚出一大片空白,也不会突然跳动。

6、图片预加载 + 动态生成列

HTML 里没有提前写死图片节点,而是用 JS 根据图片数组动态生成。

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);
});
  1. preloadImages 会先尝试加载所有图片,过滤掉加载失败的地址。
  2. DocumentFragment 用来批量插入 DOM,减少多次触发布局计算。
  3. col % 2 === 0 ? "up" : "down" 控制奇偶列反向滚动。
  4. (col * imagesPerColumn + row) % sourceList.length 让图片不够时循环复用。

效果说明:图片列表可以随便换成自己的专辑封面、影视海报、商品图,布局逻辑不用改。

7、移动端适配

最后再处理一下小屏幕。

css 复制代码
@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

源码地址:github.com/yongtaozhen...


🌟 觉得有帮助的可以点个 star~

🖊 有什么问题或错误可以指出,欢迎 pr~

📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱16 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家16 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端16 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue16 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花16 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
明月_清风16 小时前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈
windliang17 小时前
Claude Code 源码分析(三):一次模型回答如何流进 Agent
前端·算法·ai编程