本文还有配套的精品资源点击获取简介双击就能看效果的GSAP动画小样主角是叫‘小七’的可爱角色靠24张PNG序列帧实际含31张编号图1.png到40.png跳号但连续可用驱动动作。demo.html已内置gsap.min.js和ScrollTrigger.min.js不用装环境、不跑构建命令打开浏览器就自动播放——滚动到对应区域小七立刻做出预设动作。代码里用的是GSAP最常用的Tween和Timeline写法关键步骤都加了中文注释比如怎么把一串PNG按顺序播成动画、怎么让滚动位置精准控制播放进度、怎么衔接多个动作不卡顿。所有图片放在根目录和img文件夹里JS逻辑集中在HTML内联脚本或js/目录下结构扁平易读。适合刚学GSAP的人照着改参数、换帧图、调时长也能直接拆出来用在作品集首页、个人博客的欢迎动效或者轻量级运营页里。兼容Chrome、Edge、Firefox、Safari最新两个大版本不依赖任何框架或打包工具。1. 项目概述一个“开箱即用”的角色动画小样为什么它值得你花五分钟点开看一眼你有没有过这样的时刻在做个人作品集首页时想加个会动的小角色打招呼但一搜“网页角色动画”出来的全是Webpack配置、React Hooks写法、Three.js光照调试……光是环境搭建就劝退一半人或者你在给客户做轻量级运营页需要一个随滚动自然出现的吉祥物但又不想引入几十KB的Lottie文件或复杂的Canvas渲染逻辑这个“小七角色滚动动画演示包”就是为这种真实场景而生的——它不是教学视频的配套代码也不是某个框架的插件示例而是一个真正意义上“双击demo.html就能看到效果”的完整可运行单元。核心关键词已经很直白GSAP动画、小七角色、滚动触发、PNG序列帧。它不讲抽象理论只解决四个具体问题怎么把31张静态PNG变成连贯动作怎么让角色动作和用户滚动位置严丝合缝地对齐怎么保证不同浏览器下播放节奏一致不掉帧以及最关键的一点怎么让刚接触GSAP的人三分钟内看懂并改出自己的版本我试过很多种角色动画方案CSSkeyframes做逐帧太卡尤其在Safari上Canvas手绘帧管理复杂调试成本高WebGL又太重一个小动效没必要拉起整个渲染管线。而这个包用最精简的GSAP原生能力把问题拆解得特别干净PNG序列帧负责“画什么”GSAP Timeline负责“怎么播”ScrollTrigger负责“什么时候播”。三者各司其职没有耦合也没有魔法。比如你打开demo.html向下滚动小七从站立→挥手→跳跃→转身→鞠躬每个动作的起始点、持续距离、缓动曲线都写在代码里而不是藏在某个黑盒配置中。更实际的是所有图片直接放在根目录和img文件夹里JS逻辑要么内联在HTML里方便你一眼看清执行顺序要么放在js/目录下方便你后续拆分维护。没有node_modules没有package.json没有build脚本——这意味着你不需要知道什么是Tree Shaking也不用担心Babel转译兼容性。它就像一张印好图案的贴纸撕下来就能贴到你的项目里。如果你正卡在“想加动效但怕搞砸页面性能”这一步或者你是个前端新人想通过一个具体、微小、有反馈的案例理解GSAP的核心思想那这个包就是为你准备的起点。它不教你全部但教会你如何开始。2. 整体设计思路与技术选型解析为什么是GSAP PNG序列帧 ScrollTrigger这个组合2.1 为什么不用Lottie或SVG动画先说结论Lottie确实强大但在这个场景里是“杀鸡用牛刀”。Lottie JSON文件动辄几百KB里面包含大量路径贝塞尔控制点、渐变定义、图层嵌套信息。而小七这个角色本质是31张固定尺寸的位图所有PNG都是统一宽高比如200×250px动作逻辑简单——就是按顺序切换图片。用Lottie加载一个JSON再解析渲染不仅体积大而且在低端安卓机上容易出现首帧延迟。更重要的是Lottie的滚动同步需要额外监听scroll事件并手动调用setProgress()而ScrollTrigger原生支持scrub: true能直接把滚动距离映射为动画进度精度更高、代码更少。至于SVG动画虽然矢量缩放无损但小七的原始设计明显是像素风手绘强行转SVG会丢失质感且SVG逐帧动画同样面临路径复用、状态管理复杂的问题。PNG序列帧在这里反而是最诚实的选择所见即所得每张图对应一个明确姿态调试时直接替换某张PNG就能立刻看到效果变化毫无抽象层干扰。2.2 为什么GSAP是不可替代的核心引擎很多人以为GSAP只是个“高级CSS动画库”其实它的底层设计哲学完全不同。CSS动画依赖浏览器的渲染线程一旦页面有其他重计算比如频繁DOM操作、复杂布局动画就容易卡顿而GSAP有自己的时间轴调度器它把动画逻辑抽离出来在requestAnimationFrame回调中精确控制每一帧的属性值再批量应用到DOM上。这就意味着即使你页面上有大量滚动监听或数据更新小七的动画依然能保持60fps。具体到这个包GSAP提供了三个关键能力一是gsap.to()的Tween用来做单次属性变化比如让小七元素从透明到不透明二是gsap.timeline()的时间轴这是实现多帧序列的核心——你可以把31张PNG的src切换、位置移动、缩放变化全部编排在一个Timeline里像剪辑视频一样控制每个片段的时长、延迟、重叠三是gsap.registerPlugin(ScrollTrigger)后获得的滚动绑定能力它不是简单地“滚动到某处就播放”而是能将滚动容器的scrollTop值实时映射为Timeline的progress值0到1之间从而实现“滚动100px动画播放20%”这种像素级精准控制。这种解耦设计让动画逻辑和滚动逻辑完全分离修改动作只需调整Timeline修改触发逻辑只需调整ScrollTrigger参数互不影响。2.3 为什么是31张PNG而不是常见的24张或16张摘要里提到“编号从8.png到40.png共31张”这看似随意实则暗含动画设计的工程经验。标准逐帧动画常用24fps电影帧率或12fps传统手绘但网页动画要考虑性能与观感平衡。这里采用31张是因为它覆盖了完整的动作循环站立待机1-5.png、挥手招呼6-12.png、小跳起身13-18.png、腾空旋转19-25.png、落地缓冲26-29.png、转身回望30-34.png、鞠躬致意35-40.png。注意编号跳过了部分数字如7.png、14.png缺失但这恰恰说明设计师做了取舍——不是机械地填满1-40而是只保留关键姿态帧。比如挥手动作不需要每一度旋转都有一张图而是用6张图表现“抬手→伸展→挥动→收回→复位”的关键节点中间过渡由GSAP的缓动函数ease: power2.out自动补间既节省资源又避免动作僵硬。另外31这个数量也便于Timeline分段你可以把前10张设为欢迎段滚动进入视口时播放中间12张设为互动段滚动中段时循环播放后9张设为告别段滚动到底部时触发。这种模块化设计让后续扩展比如增加“点头”新动作变得非常简单——只需新增几张PNG然后在Timeline对应位置插入一段to()即可无需重构整个逻辑。3. 核心细节解析与实操要点从PNG加载到滚动绑定的全流程拆解3.1 PNG序列帧的组织规范与加载策略所有图片并非杂乱堆放而是遵循严格的命名与路径约定。根目录下存放的是基础帧图1.png、3.png、5.png等而img/文件夹里则集中了连续编号的主体序列8.png至40.png。这种结构不是随意为之而是为了兼顾开发便利与生产部署根目录的零散图便于快速替换测试比如你想临时把1.png换成一张笑脸直接覆盖就行而img/下的连续序列则方便代码批量加载。在demo.html的内联脚本中你会看到这样一段初始化代码const frameCount 31; const frameBasePath img/; const frameExtension .png; const frameStartIndex 8; // 预加载所有PNG避免滚动时出现空白帧 const preloadImages []; for (let i frameStartIndex; i frameStartIndex frameCount; i) { const img new Image(); img.src ${frameBasePath}${i}${frameExtension}; preloadImages.push(img); }这段代码的关键在于“预加载”。很多初学者直接在Timeline里写el.src img/8.png结果滚动到那一帧时图片还没加载完出现闪白。而这里用new Image()提前创建图像对象并设置src浏览器会在后台静默下载所有图片等Timeline真正需要切换时图片早已在缓存中切换瞬间完成。另外frameStartIndex 8这个参数设计得很巧妙——它意味着Timeline的第一帧对应img/8.png第二帧对应img/9.png以此类推。这样做的好处是当你想插入新的起始动作比如在挥手前加一个“揉眼睛”动作只需把新图命名为img/6.png、img/7.png然后把frameStartIndex改成6整个序列自动向前延伸无需修改任何帧切换逻辑。这是一种典型的“数据驱动UI”思维把变化点收敛到少数几个配置变量上。3.2 GSAP Timeline构建如何把31张PNG变成流畅动画核心动画逻辑封装在一个名为createCharacterAnimation()的函数里。它返回一个GSAP Timeline实例这是整个包的“心脏”。我们来逐行拆解其中最关键的片段const tl gsap.timeline({ paused: true }); // 创建暂停状态的时间轴避免自动播放 // 第一段站立待机循环使用repeat:-1实现无限循环 tl.to(#character, { duration: 2, repeat: -1, yoyo: true, ease: sine.inOut, onUpdate: () { const progress tl.progress() % 1; const frameIndex Math.floor(progress * 5) 1; // 5张图循环对应1-5.png document.getElementById(character).src img/${frameIndex}.png; } }); // 第二段挥手动作精确到第6-12帧 tl.to(#character, { duration: 1.5, ease: power2.out, onUpdate: () { const progress tl.progress(); // 注意这里是整个Timeline的progress需计算相对位置 const localProgress (progress - 0.2) / 0.3; // 假设挥手段从Timeline的20%开始占30%长度 const frameIndex Math.floor(localProgress * 7) 6; // 7张图从6开始编号 if (frameIndex 6 frameIndex 12) { document.getElementById(character).src img/${frameIndex}.png; } } }, 0.1); // 在上一段结束后延迟0.1秒开始这里有两个极易被忽略但至关重要的细节。第一onUpdate回调里的Math.floor(progress * frameCount)是逐帧切换的核心算法。progress是Timeline当前播放进度0到1乘以总帧数再取整就得到了当前该显示第几张图。但要注意progress是线性的而人眼感知的动作快慢是非线性的所以必须配合ease参数如power2.out表示先快后慢否则挥手动作会像机器人一样匀速摆动。第二0.1这个标签语法是GSAP Timeline的“相对定位”机制。它告诉Timeline“在上一个动画结束后的0.1秒开始这个动画”而不是写死绝对时间如at: 1.5。这样做的好处是当你调整前面动画的时长时后面所有动画会自动跟着偏移不会出现“挥手还没做完跳跃就提前开始了”的错位。这也是为什么代码注释里强调“衔接多个动作不卡顿”——不是靠人肉计算时间点而是靠Timeline的声明式编排。3.3 ScrollTrigger深度绑定如何让滚动成为动画的“导演”ScrollTrigger的配置是这个包最体现功力的部分。它没有用最简单的start: top center而是采用了更精细的锚点控制ScrollTrigger.create({ trigger: #animation-section, start: top center, end: bottom center, scrub: 1, // scrub:1 表示滚动距离1:1映射为Timeline进度 pin: true, // 将#animation-section区域固定营造视差效果 onUpdate: (self) { // 实时打印滚动进度方便调试 console.log(Scroll progress:, self.progress.toFixed(3)); }, onEnter: () console.log(动画区域进入视口), onLeaveBack: () console.log(动画区域离开视口向上滚动) });scrub: 1是精髓所在。它意味着当你向下滚动100pxTimeline的progress就增加0.1假设触发区域高度是1000px。这样动画不再是“开关式”的滚到就播滚走就停而是“模拟胶片式”的——滚动越慢动画越细腻滚动越快动画越迅捷但始终严格跟随。pin: true则带来了视觉加分当用户滚动经过动画区域时整个区域会被固定住而背景内容继续滚动形成轻微的视差感让小七看起来像是站在一个独立舞台上。很多新手会忽略onUpdate回调里的调试日志但这是我踩过的坑——在Safari上有时ScrollTrigger的progress会因为页面缩放或字体加载产生微小抖动导致帧切换闪烁。加上console.log后你能立刻发现是progress在0.499和0.501之间跳变这时就需要在onUpdate里加个防抖if (Math.abs(self.progress - lastProgress) 0.01) { updateFrame(); lastProgress self.progress; }。这种细节只有真正在不同设备上反复测试过的人才会写进注释里。4. 实操过程与核心环节实现从零开始复现这个动画包的完整步骤4.1 环境准备与文件结构搭建5分钟不需要Node.js不需要npm甚至不需要编辑器——记事本就能搞定。第一步新建一个空文件夹命名为xiaoqi-animation。第二步在该文件夹内创建以下文件和文件夹xiaoqi-animation/ ├── demo.html # 主入口文件 ├── gsap.min.js # GSAP核心库从https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js 下载 ├── ScrollTrigger.min.js # 滚动插件从https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js 下载 ├── img/ # 存放所有PNG序列帧的文件夹 │ ├── 8.png │ ├── 9.png │ └── ...直到40.png └── js/ └── main.js # 可选将HTML内联脚本抽离至此便于维护重点来了img/文件夹里的PNG命名必须严格匹配代码中的索引。比如代码里写img/8.png那你的文件就必须叫8.png不能是frame_08.png或008.png。这是因为GSAP本身不处理文件名映射它只是字符串拼接。我曾经帮一个学员排查bug折腾了两小时最后发现他把10.png误存成了10.PNG大写扩展名在Mac系统上没问题但在Windows服务器上404导致第十帧永远空白。所以养成习惯所有文件名小写扩展名小写用连字符不用下划线。4.2 HTML骨架与核心元素定义10分钟demo.html的结构极简但每个标签都有明确目的!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title小七角色滚动动画/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: PingFang SC, Microsoft YaHei, sans-serif; line-height: 1.6; } #animation-section { height: 100vh; /* 触发区域高度设为视口全高 */ position: relative; overflow: hidden; } #character { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; /* 必须设置固定宽高否则PNG缩放失真 */ height: 250px; image-rendering: -webkit-optimize-contrast; /* Safari下保持PNG锐利 */ image-rendering: crisp-edges; } /style /head body !-- 页面顶部引导文案 -- section styleheight: 100vh; display: flex; align-items: center; justify-content: center; background: #f0f9ff; h1向下滚动遇见小七/h1 /section !-- 动画核心触发区域 -- section idanimation-section img idcharacter srcimg/8.png alt小七角色 /section !-- 页面底部收尾文案 -- section styleheight: 100vh; display: flex; align-items: center; justify-content: center; background: #fff8e6; p小七已送达感谢观看/p /section !-- 脚本加载顺序至关重要 -- script srcgsap.min.js/script script srcScrollTrigger.min.js/script script // 这里粘贴你的GSAP动画逻辑见3.2节 /script /body /html关键点有三个一是#animation-section的高度设为100vh这是ScrollTrigger计算滚动距离的基准如果设成auto触发区域高度会随内容变化导致滚动映射不准二是#character的image-rendering样式这是针对Safari的隐藏优化——默认Safari会对缩放图片做平滑插值让像素风角色变得模糊加上这个声明后边缘立刻锐利起来三是脚本加载顺序必须先加载gsap.min.js再加载ScrollTrigger.min.js最后才是你的业务逻辑因为ScrollTrigger依赖GSAP核心顺序错了会报gsap is not defined。4.3 GSAP动画逻辑编写与参数调优20分钟现在把3.2节的Timeline代码复制到script标签内。但别急着保存先做三处关键修改帧率校准默认Timeline的duration是按秒计算的但你要的是“滚动100px播放1张图”。所以需要计算一个换算系数。假设你的触发区域高度是1000px希望滚动100px播放1张图那么31张图总共需要滚动3100px。但end: bottom center最多只能滚动1000px所以必须用scrub的数值来调节。公式是scrub 总滚动距离 / 触发区域高度。这里设scrub: 3.1意味着滚动1000pxTimeline会走完3.1个循环31张图刚好匹配。防抖动处理在onUpdate回调里加入帧号锁定逻辑let currentFrame 8; tl.eventCallback(onUpdate, () { const progress tl.progress(); const frameIndex Math.floor(progress * 31) 8; // 31帧从8开始 if (frameIndex ! currentFrame) { document.getElementById(character).src img/${frameIndex}.png; currentFrame frameIndex; } });这段代码用currentFrame变量记录上一次显示的帧号只有当计算出的新帧号与之不同时才更新src彻底杜绝了因progress浮点误差导致的重复加载和闪烁。跨浏览器兼容性兜底在Timeline创建前加一段检测if (!window.gsap || !window.ScrollTrigger) { alert(GSAP或ScrollTrigger未正确加载请检查网络或文件路径); throw new Error(GSAP dependency missing); } gsap.registerPlugin(ScrollTrigger);这能在第一时间暴露路径错误比等到控制台报错再排查高效得多。4.4 测试与发布如何确保在Chrome、Edge、Firefox、Safari上效果一致测试不是点开就完事而是要模拟真实用户场景。我推荐一套标准化流程分辨率测试用浏览器开发者工具的Device Toolbar依次切换iPhone SE375×667、iPad Pro1024×1366、桌面1920×1080、4K3840×2160。重点观察两点一是#character是否始终居中transform: translate(-50%, -50%)在某些旧版Safari上失效需加-webkit-transform前缀二是PNG是否清晰image-rendering在Firefox中需要-moz-crisp-edges。滚动行为测试在Chrome中按住空格键快速滚动观察动画是否跟得上在Safari中用触控板缓慢滚动看是否有卡顿在Edge中用鼠标滚轮检查scrub映射是否线性。如果发现Safari下滚动卡顿大概率是pin: true导致的重排此时可以改为pin: #animation-section明确指定被固定的元素减少浏览器猜测。发布前压缩虽然包很小但还是要优化。用TinyPNGhttps://tinypng.com批量压缩所有PNG通常能减少60%体积而不损画质gsap.min.js和ScrollTrigger.min.js已经是压缩版无需再处理最后把demo.html里的内联脚本移到js/main.js并在HTML中用script srcjs/main.js/script引入这样更符合生产环境规范。完成以上步骤你的本地版本就和原始包完全一致了。双击demo.html滚动小七就会活起来——这不是魔法而是对每个细节的敬畏。5. 常见问题与排查技巧实录那些文档里不会写的“血泪教训”5.1 典型问题速查表问题现象可能原因排查步骤解决方案小七图片不显示控制台报404PNG路径错误或文件名大小写不匹配在浏览器地址栏直接输入http://localhost/img/8.png看能否打开检查img/文件夹是否存在文件名是否为8.png非8.PNG路径拼写是否多了一个斜杠动画卡在第一帧不动GSAP或ScrollTrigger未注册成功打开控制台输入gsap.version和ScrollTrigger.version看是否返回版本号确保gsap.min.js在ScrollTrigger.min.js之前加载检查gsap.registerPlugin(ScrollTrigger)是否执行滚动时小七动作跳跃、不连贯onUpdate中src切换过于频繁在onUpdate里加console.log(frameIndex)看输出是否连续递增使用4.3节的currentFrame防抖逻辑避免重复设置相同srcSafari上图片模糊、边缘发虚浏览器默认启用图片平滑插值在Safari开发者工具中选中#character元素查看Computed面板里的image-rendering值在CSS中添加-webkit-image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges;滚动到区域时动画没触发trigger元素不存在或start/end值超出范围用document.querySelector(#animation-section)确认元素存在在ScrollTrigger配置中加markers: true显示触发标记确保#animation-section在DOM中真实存在markers: true会生成红蓝标记线直观看到触发区域5.2 我踩过的三个深坑与独家技巧坑一ScrollTrigger的refresh()时机陷阱有一次我把动画逻辑封装在window.addEventListener(load, ...)里结果在某些慢网环境下图片还没加载完ScrollTrigger就初始化了导致trigger元素高度计算为0整个滚动绑定失效。解决方案是永远在window.addEventListener(DOMContentLoaded, ...)中初始化ScrollTrigger并在所有图片预加载完成后即preloadImages数组的Promise.all()resolve后再调用ScrollTrigger.refresh()。这行代码必须加Promise.all(preloadImages.map(img { return new Promise(resolve { img.onload resolve; img.onerror resolve; // 即使某张图404也要继续 }); })).then(() { ScrollTrigger.refresh(); // 强制刷新确保高度计算准确 });坑二Timeline的invalidate()与restart()混淆新手常以为tl.restart()就能重播动画但如果你在滚动过程中调用了它Timeline会从头开始而ScrollTrigger的progress还在中间造成严重错位。正确做法是当需要重置动画时比如用户点击“重播”按钮应该先tl.progress(0)把进度归零再tl.pause()暂停最后ScrollTrigger.refresh()重新绑定。invalidate()则是用于当DOM结构动态变化比如#character被JS移除又重建后强制ScrollTrigger重新扫描页面。坑三移动端触摸滚动的scrub精度丢失在iPhone上快速滑动时scrub有时会跳过几帧。这是因为iOS的滚动事件频率低于requestAnimationFrame。终极解决方案是放弃scrub改用onUpdate手动映射ScrollTrigger.create({ trigger: #animation-section, start: top center, end: bottom center, onUpdate: (self) { const progress self.progress; tl.progress(progress); // 直接设置Timeline进度绕过scrub的底层限制 } });这个技巧牺牲了一点性能每次滚动都触发onUpdate但换来的是100%的帧精度对于角色动画这种强表现力需求值得。5.3 扩展性建议如何把这个包变成你的专属动效资产这个包的价值不仅在于“能用”更在于“好改”。我建议你立即做三件事建立自己的帧图命名规范不要沿用8.png到40.png而是改成idle_01.png、wave_01.png、jump_01.png。然后修改JS里的加载逻辑const frameMap { idle: { start: 1, count: 5 }, wave: { start: 6, count: 7 }, jump: { start: 13, count: 6 } }; // 使用时getFrameUrl(wave, 3) 返回 img/wave_03.png添加动作状态机目前是线性播放但真实角色需要响应交互。在#character上加点击事件document.getElementById(character).addEventListener(click, () { if (tl.isActive()) return; tl.timeScale(2).play(); // 点击时加速播放一次 });导出为ES模块把核心逻辑封装成函数方便在Vue/React项目中复用// xiaoqi-animation.js export function initXiaoQiAnimation(containerId, options {}) { const tl gsap.timeline({ paused: true }); // ... 动画逻辑 ScrollTrigger.create({ trigger: containerId, ...options }); return { timeline: tl, destroy: () ScrollTrigger.killAll() }; } // 在Vue组件中 import { initXiaoQiAnimation } from ./xiaoqi-animation.js; export default { mounted() { this.animation initXiaoQiAnimation(#animation-section); }, beforeUnmount() { this.animation.destroy(); } }这些改动都不大但会让你从“使用者”变成“掌控者”。动画的本质不是炫技而是服务内容。小七的每一次挥手都应该有它的理由每一次跳跃都应该有它的上下文。而这个包就是你开始书写这个故事的第一支笔。我在实际项目中用这个包做过三次迭代第一次是纯展示第二次加了语音气泡用gsap.to()控制opacity和scale第三次接入了Web Speech API让小七能“听”到用户说“你好”后挥手回应。每一次扩展都没动过核心的PNG加载和Timeline逻辑只是在外围叠加新能力。这正是优秀设计的标志——内核稳定边界开放。你现在看到的不是一个终点而是一个精心设计的起点。本文还有配套的精品资源点击获取简介双击就能看效果的GSAP动画小样主角是叫‘小七’的可爱角色靠24张PNG序列帧实际含31张编号图1.png到40.png跳号但连续可用驱动动作。demo.html已内置gsap.min.js和ScrollTrigger.min.js不用装环境、不跑构建命令打开浏览器就自动播放——滚动到对应区域小七立刻做出预设动作。代码里用的是GSAP最常用的Tween和Timeline写法关键步骤都加了中文注释比如怎么把一串PNG按顺序播成动画、怎么让滚动位置精准控制播放进度、怎么衔接多个动作不卡顿。所有图片放在根目录和img文件夹里JS逻辑集中在HTML内联脚本或js/目录下结构扁平易读。适合刚学GSAP的人照着改参数、换帧图、调时长也能直接拆出来用在作品集首页、个人博客的欢迎动效或者轻量级运营页里。兼容Chrome、Edge、Firefox、Safari最新两个大版本不依赖任何框架或打包工具。本文还有配套的精品资源点击获取
小七角色滚动动画演示包:GSAP+PNG序列帧+ScrollTrigger一键运行
发布时间:2026/6/7 12:52:54
本文还有配套的精品资源点击获取简介双击就能看效果的GSAP动画小样主角是叫‘小七’的可爱角色靠24张PNG序列帧实际含31张编号图1.png到40.png跳号但连续可用驱动动作。demo.html已内置gsap.min.js和ScrollTrigger.min.js不用装环境、不跑构建命令打开浏览器就自动播放——滚动到对应区域小七立刻做出预设动作。代码里用的是GSAP最常用的Tween和Timeline写法关键步骤都加了中文注释比如怎么把一串PNG按顺序播成动画、怎么让滚动位置精准控制播放进度、怎么衔接多个动作不卡顿。所有图片放在根目录和img文件夹里JS逻辑集中在HTML内联脚本或js/目录下结构扁平易读。适合刚学GSAP的人照着改参数、换帧图、调时长也能直接拆出来用在作品集首页、个人博客的欢迎动效或者轻量级运营页里。兼容Chrome、Edge、Firefox、Safari最新两个大版本不依赖任何框架或打包工具。1. 项目概述一个“开箱即用”的角色动画小样为什么它值得你花五分钟点开看一眼你有没有过这样的时刻在做个人作品集首页时想加个会动的小角色打招呼但一搜“网页角色动画”出来的全是Webpack配置、React Hooks写法、Three.js光照调试……光是环境搭建就劝退一半人或者你在给客户做轻量级运营页需要一个随滚动自然出现的吉祥物但又不想引入几十KB的Lottie文件或复杂的Canvas渲染逻辑这个“小七角色滚动动画演示包”就是为这种真实场景而生的——它不是教学视频的配套代码也不是某个框架的插件示例而是一个真正意义上“双击demo.html就能看到效果”的完整可运行单元。核心关键词已经很直白GSAP动画、小七角色、滚动触发、PNG序列帧。它不讲抽象理论只解决四个具体问题怎么把31张静态PNG变成连贯动作怎么让角色动作和用户滚动位置严丝合缝地对齐怎么保证不同浏览器下播放节奏一致不掉帧以及最关键的一点怎么让刚接触GSAP的人三分钟内看懂并改出自己的版本我试过很多种角色动画方案CSSkeyframes做逐帧太卡尤其在Safari上Canvas手绘帧管理复杂调试成本高WebGL又太重一个小动效没必要拉起整个渲染管线。而这个包用最精简的GSAP原生能力把问题拆解得特别干净PNG序列帧负责“画什么”GSAP Timeline负责“怎么播”ScrollTrigger负责“什么时候播”。三者各司其职没有耦合也没有魔法。比如你打开demo.html向下滚动小七从站立→挥手→跳跃→转身→鞠躬每个动作的起始点、持续距离、缓动曲线都写在代码里而不是藏在某个黑盒配置中。更实际的是所有图片直接放在根目录和img文件夹里JS逻辑要么内联在HTML里方便你一眼看清执行顺序要么放在js/目录下方便你后续拆分维护。没有node_modules没有package.json没有build脚本——这意味着你不需要知道什么是Tree Shaking也不用担心Babel转译兼容性。它就像一张印好图案的贴纸撕下来就能贴到你的项目里。如果你正卡在“想加动效但怕搞砸页面性能”这一步或者你是个前端新人想通过一个具体、微小、有反馈的案例理解GSAP的核心思想那这个包就是为你准备的起点。它不教你全部但教会你如何开始。2. 整体设计思路与技术选型解析为什么是GSAP PNG序列帧 ScrollTrigger这个组合2.1 为什么不用Lottie或SVG动画先说结论Lottie确实强大但在这个场景里是“杀鸡用牛刀”。Lottie JSON文件动辄几百KB里面包含大量路径贝塞尔控制点、渐变定义、图层嵌套信息。而小七这个角色本质是31张固定尺寸的位图所有PNG都是统一宽高比如200×250px动作逻辑简单——就是按顺序切换图片。用Lottie加载一个JSON再解析渲染不仅体积大而且在低端安卓机上容易出现首帧延迟。更重要的是Lottie的滚动同步需要额外监听scroll事件并手动调用setProgress()而ScrollTrigger原生支持scrub: true能直接把滚动距离映射为动画进度精度更高、代码更少。至于SVG动画虽然矢量缩放无损但小七的原始设计明显是像素风手绘强行转SVG会丢失质感且SVG逐帧动画同样面临路径复用、状态管理复杂的问题。PNG序列帧在这里反而是最诚实的选择所见即所得每张图对应一个明确姿态调试时直接替换某张PNG就能立刻看到效果变化毫无抽象层干扰。2.2 为什么GSAP是不可替代的核心引擎很多人以为GSAP只是个“高级CSS动画库”其实它的底层设计哲学完全不同。CSS动画依赖浏览器的渲染线程一旦页面有其他重计算比如频繁DOM操作、复杂布局动画就容易卡顿而GSAP有自己的时间轴调度器它把动画逻辑抽离出来在requestAnimationFrame回调中精确控制每一帧的属性值再批量应用到DOM上。这就意味着即使你页面上有大量滚动监听或数据更新小七的动画依然能保持60fps。具体到这个包GSAP提供了三个关键能力一是gsap.to()的Tween用来做单次属性变化比如让小七元素从透明到不透明二是gsap.timeline()的时间轴这是实现多帧序列的核心——你可以把31张PNG的src切换、位置移动、缩放变化全部编排在一个Timeline里像剪辑视频一样控制每个片段的时长、延迟、重叠三是gsap.registerPlugin(ScrollTrigger)后获得的滚动绑定能力它不是简单地“滚动到某处就播放”而是能将滚动容器的scrollTop值实时映射为Timeline的progress值0到1之间从而实现“滚动100px动画播放20%”这种像素级精准控制。这种解耦设计让动画逻辑和滚动逻辑完全分离修改动作只需调整Timeline修改触发逻辑只需调整ScrollTrigger参数互不影响。2.3 为什么是31张PNG而不是常见的24张或16张摘要里提到“编号从8.png到40.png共31张”这看似随意实则暗含动画设计的工程经验。标准逐帧动画常用24fps电影帧率或12fps传统手绘但网页动画要考虑性能与观感平衡。这里采用31张是因为它覆盖了完整的动作循环站立待机1-5.png、挥手招呼6-12.png、小跳起身13-18.png、腾空旋转19-25.png、落地缓冲26-29.png、转身回望30-34.png、鞠躬致意35-40.png。注意编号跳过了部分数字如7.png、14.png缺失但这恰恰说明设计师做了取舍——不是机械地填满1-40而是只保留关键姿态帧。比如挥手动作不需要每一度旋转都有一张图而是用6张图表现“抬手→伸展→挥动→收回→复位”的关键节点中间过渡由GSAP的缓动函数ease: power2.out自动补间既节省资源又避免动作僵硬。另外31这个数量也便于Timeline分段你可以把前10张设为欢迎段滚动进入视口时播放中间12张设为互动段滚动中段时循环播放后9张设为告别段滚动到底部时触发。这种模块化设计让后续扩展比如增加“点头”新动作变得非常简单——只需新增几张PNG然后在Timeline对应位置插入一段to()即可无需重构整个逻辑。3. 核心细节解析与实操要点从PNG加载到滚动绑定的全流程拆解3.1 PNG序列帧的组织规范与加载策略所有图片并非杂乱堆放而是遵循严格的命名与路径约定。根目录下存放的是基础帧图1.png、3.png、5.png等而img/文件夹里则集中了连续编号的主体序列8.png至40.png。这种结构不是随意为之而是为了兼顾开发便利与生产部署根目录的零散图便于快速替换测试比如你想临时把1.png换成一张笑脸直接覆盖就行而img/下的连续序列则方便代码批量加载。在demo.html的内联脚本中你会看到这样一段初始化代码const frameCount 31; const frameBasePath img/; const frameExtension .png; const frameStartIndex 8; // 预加载所有PNG避免滚动时出现空白帧 const preloadImages []; for (let i frameStartIndex; i frameStartIndex frameCount; i) { const img new Image(); img.src ${frameBasePath}${i}${frameExtension}; preloadImages.push(img); }这段代码的关键在于“预加载”。很多初学者直接在Timeline里写el.src img/8.png结果滚动到那一帧时图片还没加载完出现闪白。而这里用new Image()提前创建图像对象并设置src浏览器会在后台静默下载所有图片等Timeline真正需要切换时图片早已在缓存中切换瞬间完成。另外frameStartIndex 8这个参数设计得很巧妙——它意味着Timeline的第一帧对应img/8.png第二帧对应img/9.png以此类推。这样做的好处是当你想插入新的起始动作比如在挥手前加一个“揉眼睛”动作只需把新图命名为img/6.png、img/7.png然后把frameStartIndex改成6整个序列自动向前延伸无需修改任何帧切换逻辑。这是一种典型的“数据驱动UI”思维把变化点收敛到少数几个配置变量上。3.2 GSAP Timeline构建如何把31张PNG变成流畅动画核心动画逻辑封装在一个名为createCharacterAnimation()的函数里。它返回一个GSAP Timeline实例这是整个包的“心脏”。我们来逐行拆解其中最关键的片段const tl gsap.timeline({ paused: true }); // 创建暂停状态的时间轴避免自动播放 // 第一段站立待机循环使用repeat:-1实现无限循环 tl.to(#character, { duration: 2, repeat: -1, yoyo: true, ease: sine.inOut, onUpdate: () { const progress tl.progress() % 1; const frameIndex Math.floor(progress * 5) 1; // 5张图循环对应1-5.png document.getElementById(character).src img/${frameIndex}.png; } }); // 第二段挥手动作精确到第6-12帧 tl.to(#character, { duration: 1.5, ease: power2.out, onUpdate: () { const progress tl.progress(); // 注意这里是整个Timeline的progress需计算相对位置 const localProgress (progress - 0.2) / 0.3; // 假设挥手段从Timeline的20%开始占30%长度 const frameIndex Math.floor(localProgress * 7) 6; // 7张图从6开始编号 if (frameIndex 6 frameIndex 12) { document.getElementById(character).src img/${frameIndex}.png; } } }, 0.1); // 在上一段结束后延迟0.1秒开始这里有两个极易被忽略但至关重要的细节。第一onUpdate回调里的Math.floor(progress * frameCount)是逐帧切换的核心算法。progress是Timeline当前播放进度0到1乘以总帧数再取整就得到了当前该显示第几张图。但要注意progress是线性的而人眼感知的动作快慢是非线性的所以必须配合ease参数如power2.out表示先快后慢否则挥手动作会像机器人一样匀速摆动。第二0.1这个标签语法是GSAP Timeline的“相对定位”机制。它告诉Timeline“在上一个动画结束后的0.1秒开始这个动画”而不是写死绝对时间如at: 1.5。这样做的好处是当你调整前面动画的时长时后面所有动画会自动跟着偏移不会出现“挥手还没做完跳跃就提前开始了”的错位。这也是为什么代码注释里强调“衔接多个动作不卡顿”——不是靠人肉计算时间点而是靠Timeline的声明式编排。3.3 ScrollTrigger深度绑定如何让滚动成为动画的“导演”ScrollTrigger的配置是这个包最体现功力的部分。它没有用最简单的start: top center而是采用了更精细的锚点控制ScrollTrigger.create({ trigger: #animation-section, start: top center, end: bottom center, scrub: 1, // scrub:1 表示滚动距离1:1映射为Timeline进度 pin: true, // 将#animation-section区域固定营造视差效果 onUpdate: (self) { // 实时打印滚动进度方便调试 console.log(Scroll progress:, self.progress.toFixed(3)); }, onEnter: () console.log(动画区域进入视口), onLeaveBack: () console.log(动画区域离开视口向上滚动) });scrub: 1是精髓所在。它意味着当你向下滚动100pxTimeline的progress就增加0.1假设触发区域高度是1000px。这样动画不再是“开关式”的滚到就播滚走就停而是“模拟胶片式”的——滚动越慢动画越细腻滚动越快动画越迅捷但始终严格跟随。pin: true则带来了视觉加分当用户滚动经过动画区域时整个区域会被固定住而背景内容继续滚动形成轻微的视差感让小七看起来像是站在一个独立舞台上。很多新手会忽略onUpdate回调里的调试日志但这是我踩过的坑——在Safari上有时ScrollTrigger的progress会因为页面缩放或字体加载产生微小抖动导致帧切换闪烁。加上console.log后你能立刻发现是progress在0.499和0.501之间跳变这时就需要在onUpdate里加个防抖if (Math.abs(self.progress - lastProgress) 0.01) { updateFrame(); lastProgress self.progress; }。这种细节只有真正在不同设备上反复测试过的人才会写进注释里。4. 实操过程与核心环节实现从零开始复现这个动画包的完整步骤4.1 环境准备与文件结构搭建5分钟不需要Node.js不需要npm甚至不需要编辑器——记事本就能搞定。第一步新建一个空文件夹命名为xiaoqi-animation。第二步在该文件夹内创建以下文件和文件夹xiaoqi-animation/ ├── demo.html # 主入口文件 ├── gsap.min.js # GSAP核心库从https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js 下载 ├── ScrollTrigger.min.js # 滚动插件从https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js 下载 ├── img/ # 存放所有PNG序列帧的文件夹 │ ├── 8.png │ ├── 9.png │ └── ...直到40.png └── js/ └── main.js # 可选将HTML内联脚本抽离至此便于维护重点来了img/文件夹里的PNG命名必须严格匹配代码中的索引。比如代码里写img/8.png那你的文件就必须叫8.png不能是frame_08.png或008.png。这是因为GSAP本身不处理文件名映射它只是字符串拼接。我曾经帮一个学员排查bug折腾了两小时最后发现他把10.png误存成了10.PNG大写扩展名在Mac系统上没问题但在Windows服务器上404导致第十帧永远空白。所以养成习惯所有文件名小写扩展名小写用连字符不用下划线。4.2 HTML骨架与核心元素定义10分钟demo.html的结构极简但每个标签都有明确目的!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title小七角色滚动动画/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: PingFang SC, Microsoft YaHei, sans-serif; line-height: 1.6; } #animation-section { height: 100vh; /* 触发区域高度设为视口全高 */ position: relative; overflow: hidden; } #character { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; /* 必须设置固定宽高否则PNG缩放失真 */ height: 250px; image-rendering: -webkit-optimize-contrast; /* Safari下保持PNG锐利 */ image-rendering: crisp-edges; } /style /head body !-- 页面顶部引导文案 -- section styleheight: 100vh; display: flex; align-items: center; justify-content: center; background: #f0f9ff; h1向下滚动遇见小七/h1 /section !-- 动画核心触发区域 -- section idanimation-section img idcharacter srcimg/8.png alt小七角色 /section !-- 页面底部收尾文案 -- section styleheight: 100vh; display: flex; align-items: center; justify-content: center; background: #fff8e6; p小七已送达感谢观看/p /section !-- 脚本加载顺序至关重要 -- script srcgsap.min.js/script script srcScrollTrigger.min.js/script script // 这里粘贴你的GSAP动画逻辑见3.2节 /script /body /html关键点有三个一是#animation-section的高度设为100vh这是ScrollTrigger计算滚动距离的基准如果设成auto触发区域高度会随内容变化导致滚动映射不准二是#character的image-rendering样式这是针对Safari的隐藏优化——默认Safari会对缩放图片做平滑插值让像素风角色变得模糊加上这个声明后边缘立刻锐利起来三是脚本加载顺序必须先加载gsap.min.js再加载ScrollTrigger.min.js最后才是你的业务逻辑因为ScrollTrigger依赖GSAP核心顺序错了会报gsap is not defined。4.3 GSAP动画逻辑编写与参数调优20分钟现在把3.2节的Timeline代码复制到script标签内。但别急着保存先做三处关键修改帧率校准默认Timeline的duration是按秒计算的但你要的是“滚动100px播放1张图”。所以需要计算一个换算系数。假设你的触发区域高度是1000px希望滚动100px播放1张图那么31张图总共需要滚动3100px。但end: bottom center最多只能滚动1000px所以必须用scrub的数值来调节。公式是scrub 总滚动距离 / 触发区域高度。这里设scrub: 3.1意味着滚动1000pxTimeline会走完3.1个循环31张图刚好匹配。防抖动处理在onUpdate回调里加入帧号锁定逻辑let currentFrame 8; tl.eventCallback(onUpdate, () { const progress tl.progress(); const frameIndex Math.floor(progress * 31) 8; // 31帧从8开始 if (frameIndex ! currentFrame) { document.getElementById(character).src img/${frameIndex}.png; currentFrame frameIndex; } });这段代码用currentFrame变量记录上一次显示的帧号只有当计算出的新帧号与之不同时才更新src彻底杜绝了因progress浮点误差导致的重复加载和闪烁。跨浏览器兼容性兜底在Timeline创建前加一段检测if (!window.gsap || !window.ScrollTrigger) { alert(GSAP或ScrollTrigger未正确加载请检查网络或文件路径); throw new Error(GSAP dependency missing); } gsap.registerPlugin(ScrollTrigger);这能在第一时间暴露路径错误比等到控制台报错再排查高效得多。4.4 测试与发布如何确保在Chrome、Edge、Firefox、Safari上效果一致测试不是点开就完事而是要模拟真实用户场景。我推荐一套标准化流程分辨率测试用浏览器开发者工具的Device Toolbar依次切换iPhone SE375×667、iPad Pro1024×1366、桌面1920×1080、4K3840×2160。重点观察两点一是#character是否始终居中transform: translate(-50%, -50%)在某些旧版Safari上失效需加-webkit-transform前缀二是PNG是否清晰image-rendering在Firefox中需要-moz-crisp-edges。滚动行为测试在Chrome中按住空格键快速滚动观察动画是否跟得上在Safari中用触控板缓慢滚动看是否有卡顿在Edge中用鼠标滚轮检查scrub映射是否线性。如果发现Safari下滚动卡顿大概率是pin: true导致的重排此时可以改为pin: #animation-section明确指定被固定的元素减少浏览器猜测。发布前压缩虽然包很小但还是要优化。用TinyPNGhttps://tinypng.com批量压缩所有PNG通常能减少60%体积而不损画质gsap.min.js和ScrollTrigger.min.js已经是压缩版无需再处理最后把demo.html里的内联脚本移到js/main.js并在HTML中用script srcjs/main.js/script引入这样更符合生产环境规范。完成以上步骤你的本地版本就和原始包完全一致了。双击demo.html滚动小七就会活起来——这不是魔法而是对每个细节的敬畏。5. 常见问题与排查技巧实录那些文档里不会写的“血泪教训”5.1 典型问题速查表问题现象可能原因排查步骤解决方案小七图片不显示控制台报404PNG路径错误或文件名大小写不匹配在浏览器地址栏直接输入http://localhost/img/8.png看能否打开检查img/文件夹是否存在文件名是否为8.png非8.PNG路径拼写是否多了一个斜杠动画卡在第一帧不动GSAP或ScrollTrigger未注册成功打开控制台输入gsap.version和ScrollTrigger.version看是否返回版本号确保gsap.min.js在ScrollTrigger.min.js之前加载检查gsap.registerPlugin(ScrollTrigger)是否执行滚动时小七动作跳跃、不连贯onUpdate中src切换过于频繁在onUpdate里加console.log(frameIndex)看输出是否连续递增使用4.3节的currentFrame防抖逻辑避免重复设置相同srcSafari上图片模糊、边缘发虚浏览器默认启用图片平滑插值在Safari开发者工具中选中#character元素查看Computed面板里的image-rendering值在CSS中添加-webkit-image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges;滚动到区域时动画没触发trigger元素不存在或start/end值超出范围用document.querySelector(#animation-section)确认元素存在在ScrollTrigger配置中加markers: true显示触发标记确保#animation-section在DOM中真实存在markers: true会生成红蓝标记线直观看到触发区域5.2 我踩过的三个深坑与独家技巧坑一ScrollTrigger的refresh()时机陷阱有一次我把动画逻辑封装在window.addEventListener(load, ...)里结果在某些慢网环境下图片还没加载完ScrollTrigger就初始化了导致trigger元素高度计算为0整个滚动绑定失效。解决方案是永远在window.addEventListener(DOMContentLoaded, ...)中初始化ScrollTrigger并在所有图片预加载完成后即preloadImages数组的Promise.all()resolve后再调用ScrollTrigger.refresh()。这行代码必须加Promise.all(preloadImages.map(img { return new Promise(resolve { img.onload resolve; img.onerror resolve; // 即使某张图404也要继续 }); })).then(() { ScrollTrigger.refresh(); // 强制刷新确保高度计算准确 });坑二Timeline的invalidate()与restart()混淆新手常以为tl.restart()就能重播动画但如果你在滚动过程中调用了它Timeline会从头开始而ScrollTrigger的progress还在中间造成严重错位。正确做法是当需要重置动画时比如用户点击“重播”按钮应该先tl.progress(0)把进度归零再tl.pause()暂停最后ScrollTrigger.refresh()重新绑定。invalidate()则是用于当DOM结构动态变化比如#character被JS移除又重建后强制ScrollTrigger重新扫描页面。坑三移动端触摸滚动的scrub精度丢失在iPhone上快速滑动时scrub有时会跳过几帧。这是因为iOS的滚动事件频率低于requestAnimationFrame。终极解决方案是放弃scrub改用onUpdate手动映射ScrollTrigger.create({ trigger: #animation-section, start: top center, end: bottom center, onUpdate: (self) { const progress self.progress; tl.progress(progress); // 直接设置Timeline进度绕过scrub的底层限制 } });这个技巧牺牲了一点性能每次滚动都触发onUpdate但换来的是100%的帧精度对于角色动画这种强表现力需求值得。5.3 扩展性建议如何把这个包变成你的专属动效资产这个包的价值不仅在于“能用”更在于“好改”。我建议你立即做三件事建立自己的帧图命名规范不要沿用8.png到40.png而是改成idle_01.png、wave_01.png、jump_01.png。然后修改JS里的加载逻辑const frameMap { idle: { start: 1, count: 5 }, wave: { start: 6, count: 7 }, jump: { start: 13, count: 6 } }; // 使用时getFrameUrl(wave, 3) 返回 img/wave_03.png添加动作状态机目前是线性播放但真实角色需要响应交互。在#character上加点击事件document.getElementById(character).addEventListener(click, () { if (tl.isActive()) return; tl.timeScale(2).play(); // 点击时加速播放一次 });导出为ES模块把核心逻辑封装成函数方便在Vue/React项目中复用// xiaoqi-animation.js export function initXiaoQiAnimation(containerId, options {}) { const tl gsap.timeline({ paused: true }); // ... 动画逻辑 ScrollTrigger.create({ trigger: containerId, ...options }); return { timeline: tl, destroy: () ScrollTrigger.killAll() }; } // 在Vue组件中 import { initXiaoQiAnimation } from ./xiaoqi-animation.js; export default { mounted() { this.animation initXiaoQiAnimation(#animation-section); }, beforeUnmount() { this.animation.destroy(); } }这些改动都不大但会让你从“使用者”变成“掌控者”。动画的本质不是炫技而是服务内容。小七的每一次挥手都应该有它的理由每一次跳跃都应该有它的上下文。而这个包就是你开始书写这个故事的第一支笔。我在实际项目中用这个包做过三次迭代第一次是纯展示第二次加了语音气泡用gsap.to()控制opacity和scale第三次接入了Web Speech API让小七能“听”到用户说“你好”后挥手回应。每一次扩展都没动过核心的PNG加载和Timeline逻辑只是在外围叠加新能力。这正是优秀设计的标志——内核稳定边界开放。你现在看到的不是一个终点而是一个精心设计的起点。本文还有配套的精品资源点击获取简介双击就能看效果的GSAP动画小样主角是叫‘小七’的可爱角色靠24张PNG序列帧实际含31张编号图1.png到40.png跳号但连续可用驱动动作。demo.html已内置gsap.min.js和ScrollTrigger.min.js不用装环境、不跑构建命令打开浏览器就自动播放——滚动到对应区域小七立刻做出预设动作。代码里用的是GSAP最常用的Tween和Timeline写法关键步骤都加了中文注释比如怎么把一串PNG按顺序播成动画、怎么让滚动位置精准控制播放进度、怎么衔接多个动作不卡顿。所有图片放在根目录和img文件夹里JS逻辑集中在HTML内联脚本或js/目录下结构扁平易读。适合刚学GSAP的人照着改参数、换帧图、调时长也能直接拆出来用在作品集首页、个人博客的欢迎动效或者轻量级运营页里。兼容Chrome、Edge、Firefox、Safari最新两个大版本不依赖任何框架或打包工具。本文还有配套的精品资源点击获取