Communitygithub.com

shubham001312/滚动动效

用于构建或修复由滚动位置驱动的动效章节,例如随滚轮逐帧播放的 Canvas 影片、固定粘性叙事舞台、滚动关联的说明文字章节,或可通过滚动、拖拽和方向键旋转的 3D 故事转盘。

滚动动效 是什么?

该技能提供了一套完整的滚动驱动动效解决方案,适用于需要将滚动位置作为唯一时间轴、实现精确逐帧控制的高端叙事场景。其核心理念是“滚动位置即唯一时钟”,将进度 p 映射到 [0, 1],并让每个像素、说明文字和旋转角度都是 p 的纯函数。技能支持两种模式:在“固定模式”下,章节会粘性固定在视口内,播放完整个故事;在“视图模式”下,动效随内容滚动而呈现。引擎通过 IntersectionObserver 和纯函数晶片(reel)确保空闲时零 CPU 消耗,并完整支持 prefers-reduced-motion。该项目提供了完整的参考文档、React 封装和零依赖的独立演示页面,主要面向需要高质量滚动叙事的开发者。

兼容平台✓Claude Code~Codex CLI✓Cursor✓OpenCode
npx skills add https://github.com/shubham001312/imutechz-skills/tree/HEAD/skills/scroll-motion-graphics

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

滚动动效 是做什么的?

一个由滚动位置驱动的 Canvas 时间线:一个固定的舞台,完全由进度决定状态的晶片,以及绘制层永不触碰的说明文字。此方法驱动了 ImuTechz 首页的故事转盘、研发转盘和创始人路径影片。它还包含了 assets/demo.html 中的五晶片演示。

整个技术可以用一句话概括:滚动位置是唯一的时钟:将其映射到 p ∈ [0, 1],并让每个像素、说明文字和旋转角度都是 p 的函数。

理想效果

  • 像视频一样逐帧播放。 向前滚动,影片播放;向后滚动,影片逐帧倒放。中途停止时,停留的画面有意义。
  • 从不劫持页面。 滚轮、触控板、触摸和键盘的操作方式与页面上的其他任何地方完全一致。动效章节只是变得很高。
  • 文字始终可读。 说明文字是真实的 DOM 文本,位于卡片上。绘制内容在说明文字留下的空间中展开,Canvas 会围绕说明文字裁剪作为安全网。
  • 是控制器,而非展示品。 故事转盘的各个面都是链接。其导轨、箭头按钮、方向键和鼠标拖拽都通过滚动页面来定位,因此滚动和转盘永远不会不一致。
  • 空闲页面零消耗。 一旦 p 稳定或章节离开视口,不再运行 requestAnimationFrame。
  • 尊重减少动效设置。 无平滑动画,无转盘中间滚动,无说明文字入场动画。所有内容仍然存在。
  • 在 360 到 1920 px 宽、浅色和深色模式、DPR 1 和 2 下均表现良好。

硬性规则

  1. p 来自滚动位置,绝不来自时钟。 不要在晶片内部使用 performance.now()、Date、CSS 动画或 setInterval。允许平滑处理(向目标 p 进行线性插值),因为它会收敛到相同的画面。
  2. 晶片是纯函数 draw(ctx, t, env)。它不跨帧保持状态,且从不调用 Math.random();应使用种子化的 hash(i) 代替。相同的 (t, env) 必须绘制相同的像素。
  3. 使用 CSS 而非 JS 进行固定。 使用高大的轨道(position: relative; height: N×vh)容纳一个 position: sticky; top: 0; height: 100svh 的舞台。不要对舞台设置 position: fixed,不要锁定 body,也不要在轨道的祖先元素上设置 overflow: hidden|auto|clip。
  4. 永不捕获输入。 不要在滚轮或触摸事件上使用 preventDefault,不要在 html 或 body 上使用 overflow: hidden,也不要在轨道上使用滚动吸附。只有获得焦点的转盘可以处理方向键,且它通过滚动页面来实现。
  5. 通过滚动定位。 每个按钮、导轨项、键和锚点都改变 window.scrollY。绝不直接设置 p 或旋转角度。对于不得产生动画的程序化跳跃,使用 behavior: "instant",因为 "auto" 会遵循页面的 CSS scroll-behavior: smooth。
  6. 文字存在于 DOM 中。 说明文字、标题和链接是 HTML。Canvas 文本仅限于装饰性标签,如年份、坐标轴刻度和步骤名称。
  7. 每帧测量说明文字并避开它们。 每个说明文字和 HUD 元素都是禁区(data-keep)。晶片在 env.free 内布局,引擎使用奇偶裁剪法裁剪禁区。
  8. 永不同时显示两个说明文字。 说明文字窗口位于其晶片内部,每个边界处留有间隙,晶片在该间隙内交叉淡入淡出。
  9. 空闲时停止循环。 仅当章节接近视口时(IntersectionObserver),在滚动或调整大小时启动 rAF,并在 p 等于目标时停止。
  10. Canvas 尺寸设置为舞台尺寸乘以 min(devicePixelRatio, 2)。 每帧调用一次 setTransform(dpr, …) 并每帧清除。
  11. 颜色来自 CSS 自定义属性。 不要在晶片中硬编码浅色模式调色板。在 prefers-color-scheme 变化时重新读取调色板。
  12. 尊重 prefers-reduced-motion: reduce。 不使用平滑处理,让转盘面对面对齐,关闭说明文字入场动画。画面仍然跟随滚动,因为由用户驱动。

构建顺序

按此顺序进行。每一步在下一步之前都可测试。

  1. 内容优先。 编写章节:每个章节包括一个引子、一个标题、一到两句话和一个可选的链接。选择晶片数量(3 到 7 个),并为每个晶片分配大约 80 到 100 svh 的滚动高度。
  2. 轨道和舞台。 添加包含固定 100svh 舞台的高大章节,并检查它无需任何 JS 即可固定(参见 layout-and-captions)。
  3. 擦洗引擎。 连接 ScrollMotion.scrub 或 film(参见 engine)并记录 p 值。检查它在固定开始和结束时是否分别为 0 和 1,并且可以双向移动。
  4. DOM 中的说明文字和 HUD。 按断点放置它们,标记为 data-keep,并让引擎根据 p 驱动其不透明度。
  5. 晶片,一次一个。 在 env.free 内编写 draw(ctx, t, env)(参见 drawing),并在编写下一个之前来回擦洗它。
  6. 动效语法。 添加入场、保持和退场包络线、交错、停留和交叉淡入淡出(参见 motion-grammar)。
  7. 交互性(如果是转盘)。 添加面链接、导轨、上一个和下一个按钮、键盘和拖拽,全部通过滚动定位。
  8. 性能和无障碍检查。 涵盖空闲循环、DPR、减少动效、焦点顺序、屏幕阅读器文字和深色模式(参见 performance-and-a11y)。
  9. 验证 在 360、390、414、768、1024、1280、1440 和 1920 宽度下使用无头脚本(参见 verification)。在所有检查通过之前不要宣布完成。

决策清单

Canvas 影片还是 DOM/CSS 变换?

  • 许多微小运动部件、绘制的线条、粒子或图表 → Canvas 影片 (ScrollMotion.film)。
  • 几张卡片、图像或文本面板 → 由 scrub 驱动的 DOM 变换(转盘就是一种)。
  • 真实的 3D 网格或着色器 → 此技能不足。使用 WebGL,但保留规则 1 到 5 和 9 到 12。

固定模式还是视图模式?

  • 章节应在播放故事时保持屏幕 → mode: "pin"(高大轨道和固定舞台)。
  • 效果随内容滚动而移动,如跑马灯或视差 → mode: "view",无需固定。

轨道应该多高?

  • 对于影片使用 n × 80–100svh + 100svh,对于转盘使用 n × 70vh + 100vh。如果测试读者反馈“太快了”,增加高度。绝不要通过捕获滚轮来减速。

说明文字放在哪里?

  • 桌面端(≥ 760 px):放在绘制旁边(左侧或右侧卡片)或作为宽底部卡片。手机端:始终作为底部工作表,绘制内容在其上方。
  • 如果有固定站点头部,设置 --sm-nav-h 以使 HUD 和转盘避开它。

是否需要转盘?

  • 当章节是用户可能想要跳转到的事物(页面、产品、章节)时使用转盘。否则仅使用影片。

故障模式与修复

症状原因修复
舞台随滚动离开而非固定祖先元素有 overflow: hidden/auto/clip,或舞台没有设置 top移除溢出(必要时在 body 上使用 overflow-x: clip)并设置 top: 0
动画仅向前播放,或向上滚动时跳跃跨帧累积状态,或使用了时钟或 Math.random使晶片成为 t 的纯函数;使用 hash(i) 实现随机性
绘制内容位于说明文字下方晶片在完整舞台上布局在 env.free 中布局;在说明文字上保留 data-keep;让引擎裁剪
说明文字框在交叠过程中测量位置错误使用了 getBoundingClientRect(包含变换)测量布局框(offsetLeft/Top/Width/Height,参见 boxIn)
两个说明文字在边界处重叠说明文字窗口相接在 1 − cross 时淡出说明文字,在 cross 时淡入;晶片在间隙中交叉淡入淡出
空闲页面上风扇旋转rAF 循环从未停止当 cur === target 时停止;通过 IntersectionObserver 控制
手机画布模糊 / 4K 上变慢Canvas 使用 CSS 大小,或 DPR 未设上限使用 width = W × min(dpr, 2) 和 setTransform(dpr, 0, 0, dpr, 0, 0)
点击导轨后转盘和页面不一致控件直接设置了旋转角度每个控件都调用 scrollToProgress;旋转角度始终来自 p
拖拽抖动然后弹回页面有 scroll-behavior: smooth,导致 scrollTo(x, y) 产生动画拖拽时使用 scrollTo({ top, behavior: "instant" })
拖拽后点击面打开了链接结束拖拽的点击事件触发了在拖拽移动超过 4 px 后,吞掉一个捕获阶段的点击事件
移动端 100vh 在 URL 栏隐藏时跳动舞台上使用了 vh舞台使用 100svh;使用舞台自身高度计算 p
深色模式显示浅色模式绘制内容硬编码颜色通过 readPalette 读取 --sm-* 令牌;在方案改变时重新读取
转盘背面透出缺少 backface-visibility: hidden连同 -webkit- 前缀在每个面上设置
转盘面缩略图尺寸错误使用了经过 3D 变换的面的投影矩形使用 clientWidth/clientHeight 确定大小

文件

从演示开始:打开它,查看源码,替换内容。文件底部的连接代码大约有 60 行。

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能