简介:一份融合 jQuery 卷轴平滑打开动画与 HTML5 Canvas 烟花喷泉特效的网页交互示例包,面向初中级前端开发者与网页设计爱好者,适合用作个人网站、专题页或活动页的动态视觉增色。压缩包内共9个文件,主要包含1个htm演示页面、1个jQuery库文件、3个CSS样式文件以及4张PNG图片素材,结构简洁,便于直接运行与拆解学习。目前已有341人学习浏览。通过阅读源码与演示,可以掌握基于scroll事件判断元素进入视口并触发动画的通用写法,同时理解Canvas粒子系统中位置、速度、重力、随机性等参数对烟花绽放效果的影响。资源还展示了如何让烟花动画在滚动到指定区域时自动播放,为网页交互提供可借鉴的组合思路,适合动手实践并迁移到自己的项目中。 jQuery 这个老伙计,离开一线几年了,但存量项目里它依然是爹。最近接手一个老活动页的维护需求,要在页面加载时呈现一个古风画卷缓缓展开的揭幕特效,技术栈死死锁在 jQuery 上,理由很现实:整个页面的基础交互、轮播、Tab 切换全用它写的,为一个入场动画引一套新框架根本不划算。做完这个"卷轴平滑打开"效果之后,我发现它比想象中有代表性——动画原理不复杂,但牵扯到缓动函数、状态管理、事件冒泡、DOM 动态创建、XSS 防护这些点,放在今天任何前端项目里都还通用。这篇文章我就把这个组件的完整实现从头到尾拆一遍,包括可以直接抄走的代码和我在实际调试中踩过的坑,适合正在维护 jQuery 老项目、或者想补一补动画基本功的朋友。
1. 思路拆解:卷轴打开到底在动什么
1.1 卷轴动画的本质:两扇遮罩加两根轴的协同表演
很多人一听到"卷轴动画"就想着去找一堆卷轴图片素材,其实核心逻辑特别简单:所谓卷轴打开,就是页面初始状态下,用左右两块遮罩把内容盖上,动画开始后这两块遮罩分别往左右两侧退场,露出中间的内容。你看到的"卷起来的那部分纸",本质上就是两块平平无奇的 div 遮罩;而卷轴两端的木头轴杆,也只是一根细长的装饰元素,跟着遮罩边缘一起移动而已。
我把整个表演拆成三个角色:
- 遮罩层(左右各一块):负责"盖住"和"揭开",是动画的主要演员。
- 轴杆(左右各一根):负责营造出"纸在轴上卷动"的视觉暗示,是氛围担当。
- 内容层:真正要展示的画卷内容,从 display:none 到 fadeIn 淡入,配合遮罩退场的节奏。
想清楚这三层的关系,后面的代码就顺理成章了。动画进行时,遮罩从中间分离,轴杆跟着遮罩边缘往两侧走,同时内容层淡入,三个动作的时间轴一配合,观众眼里就是一幅画卷被缓缓拉开。有些实现还会在遮罩上做纸张纹理、给轴杆加阴影渐变,这些都属于锦上添花,核心机制就一句话:动态改变左右遮罩的 left 和 right 值。
1.2 技术选型:这个场景我为什么不用 CSS 动画而选 jQuery
可能有人会问:卷轴展开这种效果,CSS 的 transition 加 transform 不是更轻量吗?为什么非要用 jQuery 的 animate?问得没毛病,如果是新项目,我大概率也会用 CSS variable 加 transform: translateX 去实现。但放在这个老项目里,有几个非常现实的原因:
第一,项目里动画状态散落在多个 jQuery 事件回调里,用 jQuery 的 animate 可以把动画队列和回调统一管理,不用额外封装 CSS 类名的切换逻辑。第二,jQuery 动画天然支持链式调用、delay 回调、stop 中断,这些方法在老代码里已经被用得很熟练,直接复用成本最低。第三,也是最重要的——这个项目需要兼容到 IE11 和较老的 WebKit 内核,CSS transition 在这些环境下的表现并不稳定,而 jQuery 的 animate 基于 JS 逐帧设置样式,兼容性要可靠得多。
所以我的结论是:动画引擎没有绝对的好坏,只有适不适合当前的场景。CSS 动画适合标准浏览器的现代项目,jQuery 动画适合老项目维护和兼容性要求高的场景。如果你也在维护类似的项目,不用纠结"新技术更好"这个问题,能稳妥落地、不出 bug 的方案就是好方案。
2. 核心实现:结构、动画与状态管理
2.1 DOM 结构与关键 CSS 布局
先看结构。卷轴组件我建议用一个相对定位的容器把内容层、遮罩层、轴杆全部包起来,这样动画过程中不会有元素超出容器边界,也方便整体控制。完整结构如下:
<div class="scroll-stage" id="demoStage"> <div class="scroll-inner"> <div class="scroll-body"> <h2>云水间</h2> <p>这是一段被卷轴缓缓展开的内容,支持任意文字、图片或富文本模块。</p> <button class="btn-detail" type="button">查看详情</button> </div> <div class="scroll-mask scroll-mask-left"></div> <div class="scroll-mask scroll-mask-right"></div> <div class="scroll-rod scroll-rod-left"></div> <div class="scroll-rod scroll-rod-right"></div> </div> </div>注意这里我刻意把遮罩放在内容层之后,这样遮罩天然盖在内容上方,配合 z-index 就更稳妥。关键 CSS 部分如下:
.scroll-stage{ position:relative; width:960px; height:540px; margin:80px auto; overflow:hidden; border-radius:10px; box-shadow:0 16px 40px rgba(0,0,0,.35); background:#f7f1e3; } .scroll-inner{ position:relative; width:100%; height:100%; overflow:hidden; } .scroll-body{ position:absolute; top:0;left:0;right:0;bottom:0; z-index:1; display:none; flex-direction:column; align-items:center; justify-content:center; padding:50px; text-align:center; color:#5b4636; } .scroll-mask{ position:absolute; top:0; height:100%; width:50%; z-index:3; background-color:#e8d9bd; } .scroll-mask-left{left:0;} .scroll-mask-right{right:0;} .scroll-rod{ position:absolute; top:-2%; width:7%; height:104%; z-index:4; background:linear-gradient(90deg, #8a5a2b, #d8a05c 40%, #f2d5a0 50%, #d8a05c 60%, #8a5a2b); border-radius:8px; box-shadow:0 8px 16px rgba(0,0,0,.3); } .scroll-rod-left{ left:50%; transform:translateX(-50%); } .scroll-rod-right{ right:50%; transform:translateX(50%); }这里最容易搞错的就是轴杆的初始位置。遮罩左右各占 50%,两块遮罩闭合后,中间的接缝在容器的 50% 位置。所以左轴杆的 left 初始值就是 50%,右轴杆的 right 初始值也是 50%,再用 translate 把轴杆本身居中,视觉上两根杆就正好"贴"在画面中间。动画结束时,左轴杆的 left 从 50% 变到 0%,右轴杆的 right 从 50% 变到 0%,轴杆跟着遮罩边缘一起走出画面。
提示:轴杆的上下高度我设置成容器的 104%,上下各超出 2%,目的是在卷轴展开过程中两端不会因为边缘裁切而露白,这个细节在浅色背景下尤其明显,不加的话展开后上下边缘容易出现一条细缝。
2.2 缓动函数:平滑感的真正来源
卷轴动画里最影响观感的不是动画时长,而是缓动函数。jQuery 自带两个默认缓动:swing 和 linear。linear 是匀速运动,用上去的感觉就是机械地平移,完全没有"画卷被缓缓拉开"的手感。我在这里手动注册了一个 easeInOutCubic 缓动,让动画先慢、中间快、最后再慢下来,整体节奏更接近真实纸张展开时的物理感受。
注册自定义缓动的方式非常简洁,直接扩展现有的 easing 对象即可,标准写法如下:
$.extend($.easing, { easeInOutCubic: function (x, t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t * t + b; return c / 2 * ((t -= 2) * t * t + 2) + b; } });这个方法签名的五个参数分别是:当前进度百分比、当前耗时、起始值、变化量、总时长。你不需要完全理解里面的数学公式,只要知道在 jQuery 里注册后,animate 的 easing 参数直接写 'easeInOutCubic' 就能用。除了 cubic,我自己常用的还有 easeOutQuint、easeInOutQuart,这些在 jquery.easing 插件里都有现成的实现,不想手写的话引入插件即可。
实际体验下来,卷轴展开的时长控制在 1000 到 1300 毫秒之间观感最自然。短于 800 毫秒会显得仓促,没有"缓缓展开"的仪式感;超过 1600 毫秒又会让用户等得不耐烦,尤其是在活动页里,后面可能还排着其他动画。我最终用了 1200 毫秒,配合 easeInOutCubic,实测下来整个动画节奏非常舒服。
2.3 数组状态标记:防止动画互相"打架"
老项目里的常见问题就是动画和动画打架——用户狂点按钮,动画队列堆叠,页面抽搐。解决这个问题,我用了一个简单的数组任务队列来管理动画状态。原理是:开始某个动画前,先在数组里标记一下;动画结束后再把这个标记移除;下次触发时先查数组里有没有数据,有就说明动画还在执行,直接 return。
var taskQueue = []; function openScroll() { if (taskQueue.indexOf('open') !== -1) return; taskQueue.push('open'); // 执行动画... $body.delay(300).fadeIn(700, function () { var idx = taskQueue.indexOf('open'); if (idx !== -1) taskQueue.splice(idx, 1); }); }这个思路比布尔变量更灵活的地方在于:单个页面上可以同时存在多个卷轴组件,各自用独立的 taskQueue 互不干扰;如果你想全局控制"同一时间只允许一个动画播放",也可以把所有组件的动画标记都放在同一个数组里,判断数组里有没有数据就知道当前是否还有动画在跑。我甚至在排查性能问题时用过这个数组打印出当前所有待执行任务,定位问题特别直观。
用数组做状态标记,本质上就是在手动维护一个微型的"任务调度中心"。老项目里没有 Vuex 也没有 Pinia,这套写法在当时是标准操作,放在今天理解起来也毫不费力。
3. 交互细节、安全与内容适配
3.1 点击消失与事件冒泡控制
卷轴组件展开之后,用户点一下遮罩区域收起、点内容里的按钮不触发收起,这组交互我用事件代理加 stopPropagation 实现。核心代码如下:
$stage.on('click', '.scroll-mask, .scroll-rod', closeScroll); $stage.find('.btn-detail').on('click', function (e) { e.stopPropagation(); alert('这里是按钮自己的业务逻辑'); });这里的关键点在于:如果把关闭事件绑定在 stage 容器上,那么点击容器内的任何元素包括内容按钮,都会触发关闭。为了让内容按钮不触发关闭,必须在按钮的 click 回调里调用 e.stopPropagation(),阻止事件冒泡到 stage。反过来,把关闭事件绑定在 .scroll-mask 和 .scroll-rod 选择器上,本身就实现了"只点遮罩区域才收起"的效果,此时按钮里的 stopPropagation 更像是双保险,防止未来调整结构时引入 bug。
我踩过的一个真实坑是:遮罩收起的动画还没播完,用户又点了遮罩以外的空白区域,结果容器是空的,动画直接跳转,页面出现一块空白。解决方案就是上面提到的数组标记,在 closeScroll 开头也做一次重复触发拦截。另外,如果在遮罩上加了 CSS 的 transition,要注意过渡期间快速多次点击可能导致动画队列叠加,可以在动画前调用 .stop(true, true) 清空动画队列并直接跳到目标状态,保证不会无限排队。
3.2 动态创建结构时的 XSS 防线
如果卷轴组件要承载动态数据,比如从接口拿到用户昵称、公告标题后渲染进内容层,一个很容易被忽略的安全问题就来了:直接拼接 HTML 字符串然后塞进页面,等于把 XSS 漏洞亲手交出去。jQuery 的 .html() 方法不会帮你做任何转义,如果数据里夹着