|
| 1 | +## 核心实现原理 |
| 2 | + |
| 3 | +### 1. Canvas 图像序列播放器 |
| 4 | + |
| 5 | +代码中有两个自定义类来处理 Canvas 帧动画: |
| 6 | +- `Sr` 类 → 控制 `.bannerCv`(120 帧) |
| 7 | +- `Tr` 类 → 控制 `.batteryCv`(150 帧) |
| 8 | + |
| 9 | +它们都暴露了一个 `upData(frameIndex)` 方法,传入帧编号就会绘制对应帧。 |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +### 2. GSAP ScrollTrigger + `scrub` — 滚轮驱动动画的核心 |
| 14 | + |
| 15 | +**Banner 区域(120帧):** |
| 16 | +```javascript |
| 17 | +var a = new Sr({ el: ".bannerCv" }); // 创建banner画布播放器 |
| 18 | +var o = { value: 0 }; // 虚拟帧计数器 |
| 19 | + |
| 20 | +gsap.timeline({ |
| 21 | + scrollTrigger: { |
| 22 | + trigger: n.banner, |
| 23 | + start: "-=0", |
| 24 | + end: "+=" + t.winsize.vh, // 滚动一个屏幕高度 |
| 25 | + scrub: 0.9 // ← 关键!将滚动条进度绑定到动画播放头 |
| 26 | + }, |
| 27 | + onUpdate: function() { |
| 28 | + a.upData(parseInt(o.value)); // 每帧更新时,通知画布绘制新帧 |
| 29 | + } |
| 30 | +}) |
| 31 | +.fromTo(o, { value: 0 }, { value: 120, ease: "none" }); // 将帧号从0变到120 |
| 32 | +``` |
| 33 | + |
| 34 | +**Battery 区域(150帧):** |
| 35 | +```javascript |
| 36 | +var A = new Tr({ el: ".batteryCv" }); |
| 37 | +var f = { value: 0 }; |
| 38 | + |
| 39 | +gsap.timeline({ |
| 40 | + scrollTrigger: { |
| 41 | + trigger: n.process, |
| 42 | + start: "-=" + t.winsize.vh, |
| 43 | + end: "+=" + t.winsize.vh * (c + 1), |
| 44 | + scrub: 0.8 // ← 同样用scrub绑定滚动 |
| 45 | + }, |
| 46 | + onUpdate: function() { A.upData(parseInt(f.value)); } |
| 47 | +}) |
| 48 | +.fromTo(f, { value: 0 }, { value: 150 * c, ease: "none" }); |
| 49 | +``` |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +### 3. `scrub` 属性 |
| 54 | + |
| 55 | +`scrub` 是 GSAP ScrollTrigger 的核心配置: |
| 56 | + |
| 57 | +```javascript |
| 58 | +gsap.timeline({ |
| 59 | + scrollTrigger: { |
| 60 | + trigger: banner, |
| 61 | + scrub: 0.9 // 这一个属性完成了全部"绑定"工作 |
| 62 | + } |
| 63 | +}).fromTo(frameObj, { value: 0 }, { value: 120 }) |
| 64 | +``` |
| 65 | + |
| 66 | +| 值 | 效果 | |
| 67 | +|---|---| |
| 68 | +| `true` | 滚动条直接映射到动画进度,无缓动 | |
| 69 | +| `0.9` | 有 0.9 秒的平滑缓动,使画面过渡更流畅 | |
| 70 | + |
| 71 | +**数据流向:** |
| 72 | +``` |
| 73 | +用户滚动 |
| 74 | + → ScrollTrigger 计算 [0, 1] 进度 |
| 75 | + → 驱动 GSAP Tween 更新 o.value (0 → 120) |
| 76 | + → onUpdate 回调调用 canvas.upData(frameIndex) |
| 77 | + → Canvas drawImage() 绘制对应帧 |
| 78 | + → 视觉上形成"滚动控制动画" |
| 79 | +``` |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +### 4. 其他元素的视差效果 |
| 84 | + |
| 85 | +非 Canvas 元素(文字、图片)使用同样的 `scrub` 机制实现视差: |
| 86 | +```javascript |
| 87 | +gsap.timeline({ |
| 88 | + scrollTrigger: { |
| 89 | + trigger: e, |
| 90 | + start: "-=" + t.winsize.vh, |
| 91 | + end: "+=" + (height + t.winsize.vh), |
| 92 | + scrub: 0.9 // 同样是scrub |
| 93 | + } |
| 94 | +}) |
| 95 | +.fromTo(e, { x: -n, y: -r }, { x: n, y: r }); // 上下/左右视差位移 |
| 96 | +``` |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +**总结:** 整个效果的技术本质是 **GSAP ScrollTrigger 的 `scrub` 属性**,它把滚动条的位置实时映射为动画的播放进度,配合 Canvas 逐帧绘制图像序列 |
| 101 | +```html |
| 102 | +<!-- 动画帧图片来源 --> |
| 103 | +<canvas class="bannerCv" |
| 104 | + data-path="/templates/assets/home/bannerFm/" |
| 105 | + data-count="120"> |
| 106 | +</canvas> |
| 107 | +``` |
| 108 | +到时候JS运行时读取 |
| 109 | +```javascript |
| 110 | +for (let i = 0; i < 120; i++) { |
| 111 | + const img = new Image(); |
| 112 | + img.src = `/templates/assets/home/bannerFm/${pad(i)}.jpg`; |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | + |
| 117 | +手写实现类似效果 |
| 118 | +```javascript |
| 119 | +window.addEventListener('scroll', () => { |
| 120 | + const progress = (scrollY - sectionTop) / sectionHeight; // 手算进度 |
| 121 | + const frame = Math.round(progress * 120); |
| 122 | + canvas.drawImage(images[frame], 0, 0); |
| 123 | +}); |
| 124 | +``` |
0 commit comments