ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

原生JS实现异形轮播图:复刻网易云、QQ音乐播放器效果

原生JS实现异形轮播图:复刻网易云、QQ音乐播放器效果 1. 从网易云、QQ音乐播放器说起异形轮播图到底难在哪打开网易云音乐或者 QQ 音乐的播放页你会看到一个很特别的东西中间一张大图左右两侧各缩着一张小图再往外还有更小、更暗的图整体像一条微微拱起的弧线。点一下右边的箭头所有图会平滑地往左挪一格中间那张换人两侧的层级、大小、透明度跟着一起变。这就是大家常说的异形轮播图也有人叫它 3D 弧形轮播、缩放层级轮播。它和普通轮播图最大的区别在于普通轮播图所有图片尺寸一样只是横向平移而异形轮播图里每一张图在不同位置上的宽、高、left、z-index、透明度都不一样。也就是说位置决定样式样式决定视觉层级。你要做的不是移动图片而是给每张图重新分配一个位置编号然后让它的样式平滑过渡到那个编号对应的值。用原生 JS 实现它核心就三件事第一把每个位置的样式参数抽出来存成数组第二写一个能对任意 CSS 数值做补间的动画函数第三用取模运算把当前索引和位置编号对应起来让首尾能循环。听起来抽象但代码量其实不大一个 HTML 文件就能跑通。这篇会带你从零复刻这个效果给出可直接复制的 HTML/CSS/JS拆解弧形排列和缩放层级的数学计算最后在浏览器里验证并教你调参。适合已经会一点 DOM 操作、想搞懂动画补间 位置映射这套思路的前端同学。如果你平时用 AI 辅助写代码后面我也会提一下怎么把这类配置和模型调用串起来方便你批量生成不同风格的轮播参数。先说清楚一个容易踩的坑很多人一上来就想用transform: translateX()加scale()来做结果发现层级和透明度不好同步图片还会互相盖住。网易云那种效果用绝对定位 left/width/height/z-index 直接改反而更直观因为每个位置的参数是离散的、可枚举的你只要把 7 个位置的参数写死剩下的交给补间动画就行。下面我们就按这个思路来。2. 前置准备目录结构、图片素材与 TaoToken 配置思路动手之前先把工程骨架搭好。这个 demo 不需要任何构建工具一个文件夹、一个 HTML、七张图就够了。目录结构建议这样arc-carousel/ ├── index.html └── img/ ├── w2.jpg ├── w3.jpg ├── w4.jpg ├── w5.jpg ├── w6.jpg ├── w7.jpg └── w8.jpg图片尺寸不用太讲究但建议统一用横图比例接近 5:3比如 1000×600。因为中间那张大图会被拉到 500×300两侧小图是 300×280比例差得太多会变形。我试过用竖图两侧小图会被压得很扁观感很差。HTML 骨架就是 excerpt 里那套一个#box容器里面一个ul放 7 个li每个li里一个a img加一个遮罩i再配左右按钮和底部圆点。这里不重复贴下一节给完整版。关于 TaoToken 的配置如果你打算用 AI 帮你生成不同风格的轮播参数比如换一套弧形半径、换一组缩放比例可以先把调用环境准备好。TaoToken 是一个模型调用入口官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。它的作用是让你用统一的 Base URL 和 Key 去调不同模型省得每个模型单独配一遍。具体怎么配取决于你用哪个客户端。如果你用 Claude Code 这类命令行工具需要在配置里写清楚三件套Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiKey 去控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 生成Model ID 按你实际要用的模型填。想先试试模型对话效果可以直接开 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 聊两句确认通了再往代码里接。这一步不是必须的纯手写也能完成轮播。但如果你想让 AI 帮你算7 个位置的 left 值怎么分布才像一条弧线或者批量生成不同主题的配置提前把环境配好会省事很多。配置细节我在第 3 节给一份可复制的片段。3. 可复制配置HTML/CSS/JS 三件套与 settings 片段这一节是全文的核心直接给能跑的代码。你可以新建index.html把下面三段依次贴进去。3.1 HTML 结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 title原生JS异形轮播图/title link relstylesheet hrefstyle.css /head body div idbox ul li classb0a hrefjavascript:;img srcimg/w2.jpg alt/ai classi0/i/li li classb1a hrefjavascript:;img srcimg/w3.jpg alt/ai classi1/i/li li classb2a hrefjavascript:;img srcimg/w4.jpg alt/ai classi2/i/li li classb3a hrefjavascript:;img srcimg/w5.jpg alt/ai classi3/i/li li classb4a hrefjavascript:;img srcimg/w6.jpg alt/ai classi4/i/li li classb5a hrefjavascript:;img srcimg/w7.jpg alt/ai classi5/i/li li classb6a hrefjavascript:;img srcimg/w8.jpg alt/ai classi6/i/li /ul div idRbtn classRbtna hrefjavascript:;gt;/a/div div idLbtn classLbtna hrefjavascript:;lt;/a/div dl dd/dddd/dddd classcur/dddd/dd dd/dddd/dddd/dd /dl /div script srccarousel.js/script /body /html注意li上的b0到b6是初始位置类i上的i0到i6是初始遮罩透明度类。这两个类名只是给页面加载时一个初始状态真正的动画由 JS 接管。3.2 CSS弧形排列与层级* { margin: 0; padding: 0; } body { background: #1a1a1a; } #box { height: 500px; width: 1000px; position: relative; margin: 100px auto; overflow: hidden; } #box ul li { position: absolute; width: 500px; height: 300px; bottom: 150px; cursor: pointer; transition: none; } /* 7 个位置的样式left 决定弧形width/height 决定缩放z-index 决定层级 */ #box ul li.b0 { height: 280px; left: 250px; width: 300px; z-index: 80; } #box ul li.b1 { height: 280px; left: 50px; width: 300px; z-index: 90; } #box ul li.b2 { height: 300px; left: 250px; width: 500px; z-index: 100; } #box ul li.b3 { height: 280px; left: 650px; width: 300px; z-index: 90; } #box ul li.b4 { height: 280px; left: 250px; width: 300px; z-index: 80; } #box ul li.b5 { height: 280px; left: 250px; width: 300px; z-index: 80; } #box ul li.b6 { height: 280px; left: 250px; width: 300px; z-index: 80; } #box ul li i { position: absolute; width: 100%; height: 100%; background: #000; top: 0; left: 0; } #box ul li i.i0 { opacity: 0; } #box ul li i.i1 { opacity: 0.6; } #box ul li i.i2 { opacity: 0; } #box ul li i.i3 { opacity: 0.6; } #box ul li i.i4 { opacity: 0; } #box ul li i.i5 { opacity: 0; } #box ul li i.i6 { opacity: 0; } #box ul li img { width: 100%; height: 100%; display: block; } .Rbtn, .Lbtn { position: absolute; height: 40px; width: 40px; border-radius: 50%; background: rgba(0,0,0,0.3); top: 50%; margin-top: -20px; text-align: center; cursor: pointer; z-index: 200; } .Rbtn { right: 0; } .Lbtn { left: 0; } .Rbtn a, .Lbtn a { display: inline-block; font: 30px/40px ; color: #fff; text-decoration: none; } dl { position: absolute; height: 40px; bottom: 60px; left: 50%; margin-left: -105px; z-index: 200; } dd { height: 20px; width: 20px; border-radius: 50%; background: #ccc; float: left; margin: 0 5px; cursor: pointer; } dd.cur { background: #f99; }这里的关键是b0到b6这 7 个类。你会发现b0、b4、b5、b6的 left 都是 250px宽高也一样它们其实是藏在中间大图后面的位置。真正露出来的是b1左小图、b2中间大图、b3右小图。这就是网易云那种只露三张、其余藏在后面的做法——用 z-index 把后面的压住视觉上只看得到三张。3.3 JS位置映射与补间动画var obox document.getElementById(box); var oul obox.getElementsByTagName(ul)[0]; var olis oul.getElementsByTagName(li); var oRbtn document.getElementById(Rbtn); var oLbtn document.getElementById(Lbtn); var ois obox.getElementsByTagName(i); var odd obox.getElementsByTagName(dd); var target []; // 每个位置的 li 样式 var target2 []; // 每个位置的 i 透明度 var liarr []; // 真实数组方便 shift/unshift var iarr []; var index 2; // 当前中间图的索引 // 读取初始样式存进 target for (var i 0; i olis.length; i) { target.push({ height: parseInt(getComputedStyle(olis[i]).height), left: parseInt(getComputedStyle(olis[i]).left), width: parseInt(getComputedStyle(olis[i]).width), z-index: parseInt(getComputedStyle(olis[i]).zIndex) }); target2.push({ opacity: parseFloat(getComputedStyle(ois[i]).opacity) }); liarr.push(olis[i]); iarr.push(ois[i]); } // 补间动画函数 function animate(el, obj, time) { if (el.isanimated) return; el.isanimated true; var start {}; var speed {}; var t 0; for (var k in obj) { start[k] parseFloat(getComputedStyle(el)[k]) || 0; speed[k] (obj[k] - start[k]) / time; } el.timer setInterval(function () { t 20; var flag true; for (var k in obj) { var cur start[k] speed[k] * t; if (k opacity) { el.style[k] cur; } else { el.style[k] cur px; } if (Math.abs(cur - obj[k]) 0.5) flag false; } if (flag) { clearInterval(el.timer); el.isanimated false; for (var k in obj) { el.style[k] (k opacity) ? obj[k] : obj[k] px; } } }, 20); } // 核心根据 index 计算每张图应该去哪个位置 function move() { for (var i 0; i olis.length; i) { var pos i - index 2; if (pos 0) pos olis.length; if (pos olis.length) pos - olis.length; animate(liarr[i], target[pos], 300); animate(iarr[i], target2[pos], 300); } } function changeyuan() { for (var i 0; i odd.length; i) odd[i].className ; odd[index].className cur; } oRbtn.onclick function () { if (liarr[1].isanimated) return; index; if (index olis.length - 1) index 0; move(); changeyuan(); }; oLbtn.onclick function () { if (liarr[1].isanimated) return; index--; if (index 0) index olis.length - 1; move(); changeyuan(); }; for (var j 0; j odd.length; j) { odd[j].idx j; odd[j].onclick function () { index this.idx; move(); changeyuan(); }; } for (var k 0; k olis.length; k) { olis[k].idx k; olis[k].onclick function () { index this.idx; move(); changeyuan(); }; }move()里那句var pos i - index 2;是整个效果的数学核心。2是因为中间位置在 target 数组里的下标是 2。当index变化时每张图的pos跟着变再对 7 取模就实现了循环。比如index从 2 变成 3原来在位置 2 的图pos变成 1就往左挪了一格。3.4 settings 片段如果你用 AI 辅助生成参数如果你想把上面这套参数交给 AI 帮你改可以准备一份 JSON 配置路径放在项目根目录carousel.config.json{ container: { width: 1000, height: 500 }, positions: [ { name: hidden-left, left: 250, width: 300, height: 280, zIndex: 80, opacity: 0 }, { name: side-left, left: 50, width: 300, height: 280, zIndex: 90, opacity: 0.6 }, { name: center, left: 250, width: 500, height: 300, zIndex: 100, opacity: 0 }, { name: side-right, left: 650, width: 300, height: 280, zIndex: 90, opacity: 0.6 }, { name: hidden-right1,left: 250, width: 300, height: 280, zIndex: 80, opacity: 0 }, { name: hidden-right2,left: 250, width: 300, height: 280, zIndex: 80, opacity: 0 }, { name: hidden-right3,left: 250, width: 300, height: 280, zIndex: 80, opacity: 0 } ], duration: 300 }这份配置和 CSS 里的b0到b6一一对应。你想调弧形就改left想调缩放就改width/height想调层级就改zIndex。改完让 AI 帮你重新生成 CSS 类比手改快。如果你用 Claude Code 或类似工具配置三件套可以写成这样以环境变量为例export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY你的Key export ANTHROPIC_MODEL你的ModelIDKey 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 生成。想长期跑编码任务可以看 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 的套餐说明。这些配置和轮播本身无关但能让你在调参时有个顺手的 AI 环境。4. 浏览器验证跑通、点击、观察补间过程代码贴完接下来是验证。这一步别跳过很多人代码能跑但效果不对问题都出在验证环节没看仔细。第一步用本地服务器打开。直接双击index.html也能跑但有些浏览器对本地图片加载有限制建议起个静态服务# 在项目根目录执行 python -m http.server 8080然后浏览器访问http://localhost:8080。你应该看到中间一张大图左右各一张小图小图上有半透明黑色遮罩底部有 7 个圆点第三个是粉色对应index2。第二步点右箭头。观察三件事中间大图是不是平滑缩小并往左移左边小图是不是往左移出并变暗右边小图是不是往中间放大并变亮如果这三件事同时发生说明move()的位置映射对了。第三步打开控制台看有没有报错。常见的是getComputedStyle返回空字符串导致parseInt得到NaN。这通常是因为 CSS 没加载或者li的position不是absolute。检查#box ul li那条规则有没有生效。第四步验证循环。连续点右箭头 7 次应该回到初始状态。如果点到第 7 次时图片位置乱了说明取模那两行if (pos 0) pos olis.length;有问题检查一下pos的计算。第五步验证节流。快速连点右箭头图片不应该出现卡在半路或者跳帧的情况。if (liarr[1].isanimated) return;这行就是干这个的——只要有一张图还在动画中就不响应新的点击。你可以把这行注释掉再快速连点就能看到不加节流会乱成什么样这个对比很直观。第六步验证圆点。点第 5 个圆点中间图应该直接切到第 5 张同时左右两侧跟着更新。如果圆点高亮和图片对不上检查changeyuan()里odd[index].className cur的index是不是和move()用的同一个。实测下来最容易出问题的是animate()里的getComputedStyle(el)[k]。当k是z-index时返回的是字符串parseFloat能处理但当元素还没渲染时可能返回autoparseFloat得到NaN动画就卡住了。解决办法是在读取前确保元素已经渲染或者给z-index一个默认值。另外width和height在getComputedStyle里返回的是带px的字符串parseInt能正确解析。但opacity返回的是0.6这种要用parseFloat。这两个别搞混混了就会出现透明度跳变或者尺寸不补间。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节针对两类问题一类是轮播本身的 JS 报错一类是你用 AI 辅助时可能遇到的接口报错。分开说。5.1 轮播本身的报错报错一Cannot read properties of undefined (reading style)这个通常出现在animate(liarr[i], target[pos], 300)里target[pos]是undefined。原因是pos算出来超出了 0 到 6 的范围。检查move()里的取模逻辑确保pos在[0, 6]之间。我踩过的坑是index初始值设成了 0但2之后位置全偏了中间图跑到了左边。把index初始值设成 2 就对了。报错二图片不显示控制台 404检查img路径。HTML 里写的是img/w2.jpg那图片就得放在和index.html同级的img文件夹里。文件名大小写也要一致Linux 服务器上W2.jpg和w2.jpg是两个文件。报错三动画卡顿、掉帧setInterval的 20ms 间隔在低端设备上可能不够。可以改成requestAnimationFrame或者把间隔调到 16ms。另外animate()里每次都调getComputedStyle这个操作比较贵可以在动画开始时读一次之后用变量存着。报错四点击后图片重叠、层级错乱检查z-index有没有被正确补间。z-index是整数补间过程中会出现小数浏览器会取整可能导致瞬间层级跳变。解决办法是给z-index单独处理不做补间直接赋值。或者把z-index的补间时间设短一点。5.2 AI 辅助时的接口报错如果你在调 AI 生成轮播参数时遇到下面这些对照处理401 UnauthorizedKey 不对或者没带。检查ANTHROPIC_API_KEY是不是从 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 生成的有没有多余空格。Base URL 要写https://taotoken.net/api别漏了/api。local proxy failed本地代理配置有问题。检查环境变量里有没有残留的代理设置或者客户端配置里的 Base URL 写错了。把 Base URL 改成https://taotoken.net/api再试。reading choices这个报错通常出现在用 OpenAI 格式调 Claude 模型时。Claude 的返回结构里没有choices字段是content。检查你用的客户端是不是把模型类型配错了。Model ID 要和你实际调用的模型一致。OAuth 相关报错如果你用 Claude Code 的 OAuth 登录方式注意它和 API Key 是两套认证。用 API Key 的话在配置里写ANTHROPIC_API_KEY不要走 OAuth 流程。具体看 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 的接入文档。排查顺序建议先确认 Base URL 和 Key再确认 Model ID最后看客户端版本。三件套里任何一个不对都会报错。如果你用的是 Cline 或 CC Switch 这类工具配置里同样要写全 Base URL、Key、Model ID 三项缺一不可。6. 继续深入把轮播参数交给 AI 批量生成轮播跑通之后你可以做一件更有意思的事把第 3 节那份carousel.config.json当成输入让 AI 帮你生成不同风格的参数。比如你想要更扁的弧形就把left的分布改一改想要更明显的缩放就把width的差值拉大。手动调要试很多次交给 AI 一次生成几套你挑一套顺眼的。具体做法把配置 JSON 和你的需求描述一起发给模型。需求描述可以写7 个位置中间最大两侧递减弧形半径更大透明度从 0 到 0.6 渐变。模型会返回一份新的 JSON你替换掉原来的刷新页面就能看效果。如果你用 Claude Code 做这件事配置参考第 3 节的三件套。想先验证模型能不能正确理解你的需求可以开 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 直接对话测试。长期做这类批量生成https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 的套餐会更划算。最后留一个实用技巧把duration从 300 改成 500动画会变慢适合演示改成 150会变得很跟手适合实际产品。这个参数在animate()的第三个参数里改一处就行。另外target数组是在页面加载时读一次 CSS 生成的如果你用 JS 动态改了 CSS 类记得重新读一遍否则动画会往旧值上补间。
返回列表