mcpField
D
design-to-motion

design-to-motion

github.com/elayadesign创意与媒体11.0k206 次安装1 天前更新
代码仓库

关于 design-to-motion

design-to-motion 是将静态设计制作成动画的技能,适用于制作展示动画或网站动效的设计师和开发者。它将设计还原、动效制作与录制导出分阶段处理。

主要功能

  1. 01忠实还原源设计
  2. 02支持展示动画与网站场景
  3. 03制作加载和视觉叙事动效
  4. 04导出展示视频及 GIF
  5. 05导出前检查视觉还原度

适用场景

  1. 01将 Figma 画面制作成展示动画
  2. 02为落地页功能卡片添加动效
  3. 03为产品模型添加滚动触发动效
  4. 04生成 GIF 和 MP4 设计预览

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/elayadesign/ai-design-skills --skill design-to-motion

安装后在 Claude Code 中输入/design-to-motion确认已加载。

详细介绍

设计转动态

展示动画不是重新设计。观看者必须看到完全一致的设计,并且必须看到它鲜活起来。

精确画面 → 空状态 → 平滑加载 → 每个视觉元素都演绎自己的故事 → 无缝重复。

此技能分为三部分。A 部分重建设计,让完成后的画面与源设计毫无差别。B 部分是动态系统。C 部分负责录制和导出。先完成 A 部分,再处理 B 部分;在 A7 和 B9 的检查通过前,绝不要编码。A 部分和 B 部分适用于以下两种场景;C 部分适用于展示场景,或仅在用户要求导出时适用于网站场景。

适用范围

适用于任何将静态设计制作成动画的请求:Figma 画面、UI 卡片、仪表板、应用界面、营销页面主视觉、产品模型图,以及置于照片背景上的设计。当此处的规则与框架默认设置冲突时,以本文件为准。当用户的明确要求与规则冲突时,以用户要求为准。

不适用于正式产品中的交互动画(悬停、菜单、提示框)。这类需求请使用产品动画技能,例如 emilkowalski/skills。**配套技能:**当动画用于落地页时,使用此仓库中的 landing-page-design。

两种场景

首先判断场景。场景会影响动画内容、动画的起止方式,以及交付内容。

展示网站
典型请求“帮我给这个设计制作动画”,只提供 Figma 链接构建或编辑落地页或网站,或请求中明确提到落地页或网站
动画范围整个画面仅适用于符合条件的版块(见下表)
开始时间页面加载时,从空状态开始版块滚动进入视口时,仅播放一次
片头之后故事动画播放,然后画面淡出并循环(B5)故事动画持续播放。不淡出,不循环
文本导出为图层,确保画面像素级精确使用网站字体的实时 HTML 文本,以支持 SEO 和无障碍访问。只有视觉部分导出为图像或 SVG
交付内容页面以及用于 X 的 MP4、网站 GIF、800×500 GIF(C 部分)网站代码中的动画版块。仅在用户要求时导出

如果请求未说明场景,请根据当前工作位置判断:在网站代码库中工作时,默认按网站场景处理。只有设计稿、没有网站时,默认按展示场景处理。如果仍不明确,只询问一次。

网站:各版块适用的动画

逐个检查页面版块。仅对含有视觉部分的版块应用此技能。不要用此技能为纯文本版块制作动画。

版块应用方式
Bento 和功能版块:包含文本和视觉内容的卡片(图表、列表、流程、日历、提示词、播放器)滚动进入视口时播放卡片片头(B2,card 类型,包含文本),然后让每张卡片的视觉内容播放故事循环(B4)。这是落地页上的主要应用场景
产品或仪表板模型图(应用主视觉、产品截图)滚动进入视口时播放加载片头:保留表层,加载内容(A5)。然后询问用户加载完成后是否还需要故事动画。不要擅自添加
Logo 列、集成项跑马灯或柔和的逐一亮起动画
纯文本版块:标题区、用户评价、常见问题、定价、CTA、页脚不适用此技能。使用页面常规的滚动显现效果(例如 landing-page-design B7)

网站规则:

  • 使用 IntersectionObserver 触发。 当版块约 30% 进入视口时开始片头动画。只播放一次。绝不要使用滚动监听器。
  • 版块移出屏幕时暂停故事动画。 版块离开视口时停止计时,返回视口时恢复。
  • 按版块内部的阅读顺序错开动画,不要跨整个页面错开。
  • 遵循网站自身的系统。 使用网站的组件、设计令牌、圆角和字体。仍须确保设计还原度(A7),并按版块逐一核对。
  • prefers-reduced-motion: reduce 时,每个版块均显示最终状态,不播放故事动画。

A 部分 — 还原度

A1. 需求收集

开始制作前,先收集以下信息。只询问缺少的信息,并一次性问完。

  • 背景信息。 展示页还是网站(参见「两种场景」)。先确定这一点。
  • 素材来源。 最好提供包含节点 ID 的 Figma 链接。也可以使用扁平图片,但无法完美分离其中的图层(参见 A2)。
  • 布局。 根据图层树(A5)自行判断:应用框架(由多个拼接表面组成的仪表板或应用界面,例如侧边栏和主体),或独立卡片(便当网格、卡片组、小组件)。两者的加载方式不同。
  • 背景。 UI 后方是否有照片或纹理?假设它必须与设计完全一致。
  • 视觉元素。 哪些部分是加载后应演绎故事的视觉元素(B4)?假设所有图表、列表、堆栈、日历、流程、提示词、照片、媒体控件和状态指示器都属于此类。
  • 交付内容。 展示页:默认提供全部三种,分别为用于 X 的 MP4、网站 GIF 和 800×500 GIF。网站:提供网站代码中的动画片段,仅在用户要求时导出文件。
  • 镜头。 假设不缩放、不平移(B7)。

如果用户无法回答,请做出合理假设,用一行说明,然后继续。在会话中首次设计时,先展示实时页面,再录制导出文件,以便低成本调整方向。

A2. 唯一真实来源

Figma。 使用 REST API 和个人访问令牌。

  • 从安全存储(环境变量或操作系统钥匙串)读取令牌。绝不打印、回显、粘贴到聊天中或提交该令牌。
  • GET /v1/files/:key/nodes?ids=:node&geometry=paths 可获取图层树、边界、填充、效果和圆角。
  • GET /v1/images/:key?ids=:node&format=png&scale=2 可渲染参考画面。最终静态画面必须与此图一致(A7)。
  • 记录画面的绝对原点。每个图层的位置均为 absolute bounds − origin。
  • API 返回的图片 URL 指向 CDN,有时会重置连接。每次下载都要重试,并在使用前检查每个 PNG 的大小。

仅有扁平图片。 该图片就是参考图。尽可能重建可分离的内容,并在动画制作前明确告知用户哪些部分无法分离。绝不要把截图作为一个整体制作动画来伪装成高保真效果。

A3. 图层导出

通过 /v1/images 将每个会移动的元素分别导出为 3x PNG,每次调用批量处理 20 到 25 个 ID。

情况放置方式
PNG 尺寸等于 absoluteRenderBounds(文本图层始终如此)放在渲染边界处
PNG 大于渲染边界(被父级裁剪),且溢出的效果均匀分布x = bbox.x − (pngW − bbox.w) / 2。若顶部未被裁剪,则使用 renderBounds.y;否则,根据效果半径和偏移量推算
溢出不均匀(带箭头的线、只有一端为方形端点的描边)使用 geometry=paths 响应中的 strokeGeometry 或 fillGeometry 边界,而不是使用公式
带大范围图层模糊的旋转矢量(柔和的色块)使用 use_absolute_bounds=true 导出,并放在边界框处。仅当该区域的 PSNR 低于约 45 dB 时,才针对参考图进行 PSNR 偏移搜索。色块可以按 1x 存储,损失肉眼不可见
高度或宽度为零的描边矢量use_absolute_bounds=true 会返回 null。请在不使用该参数的情况下导出
带模糊内容的蒙版组以未被裁剪的边缘为锚点(通常是蒙版底部加上描边)
圆弧、圆环、部分椭圆使用 use_absolute_bounds=true 导出
图案或覆盖层远大于其表面使用 use_absolute_bounds=true 导出,然后裁剪到表面矩形
实例子节点(ID 类似 I12:34;56:78)它们通常会返回 null。请导出父实例
应绘制出来的线条,无论是否为虚线Figma 会将描边轮廓化为填充形状,因此没有可供动画使用的中心线。请自行编写中心线,并将其用作导出 PNG 的蒙版(示例如下)
嵌在较大导出图层中的照片也要单独导出照片节点。用骨架占位图遮住烘焙在图层中的照片,并在 photo 解析时将单独的照片图层置于顶层。静止状态下必须与参考图一致

确保画面精确还原的规则:

  • 在展示页中,文本应保留为导出的图层。 不要在 HTML 中重新输入文本:字体、字距和行度量都无法匹配。在网站上,文本应使用实时 HTML(参见“两个场景”)。
  • 保持 Figma 的 z 轴顺序。 静态图层可以与动画图层交错排列,顺序应与图层树一致。
  • 框架描边通常绘制在子元素上方。 将卡片的描边作为单独的覆盖层,放在其裁剪内容上方,然后与参考图核对:在 Figma 渲染中,卡片内的辉光可能会给描边染色。
  • 子元素会自行移动的容器。 API 总是将框架及其子元素一起导出,因此导出卡片或面板会将其中的所有内容烘焙在一起。如果子元素需要单独加载或响应操作,则应根据节点数据用 CSS 重建容器:填充、圆角半径、内侧描边(包括 individualStrokeWeights)和效果。然后分别导出每个子元素。
  • 阴影映射。 Figma 中模糊值为 R 的投影,对应单个元素上模糊值为 R 的 CSS box-shadow,或组上模糊值约为 R / 2 的 filter: drop-shadow()。两者会产生相同的高斯模糊。请与参考图核对。
  • 组阴影是整体并集的阴影。 当投影应用于一组相连的表面(如应用框架)时,应将所有表面形状放在同一个静态容器中,并应用 filter: drop-shadow()。对每个元素单独使用 box-shadow 会使表面重叠处的相邻元素变暗。单独的卡片各自在自己的 card 单元中带有阴影,因此阴影会随卡片移动。
  • 跳过隐藏图层(visible: false)以及被不透明形状遮住的零散图层。

绘制线条。 从 strokeGeometry 的轮廓中读取拐角点和弧半径:中心线位于描边宽度的一半处,圆角中心线半径则是轮廓内侧半径加上描边宽度的一半。大多数 UI 连接线由直线段和弧线组成,因此路径很短:

<svg class="ly" viewBox="0 0 1440 990" style="left:0;top:0;width:1440px;height:990px">
  <mask id="draw1">
    <path d="M193 832 V700 A45 45 0 0 1 238 655 H380" fill="none" stroke="#fff" stroke-width="8"
          pathLength="1" stroke-dasharray="1" stroke-dashoffset="1"/>
  </mask>
  <image href="data:image/png;base64,..." x="192" y="653" width="190" height="181" mask="url(#draw1)"/>
</svg>

在 JS 时钟上将 stroke-dashoffset 从 1 动画到 0。pathLength="1" 表示无需测量长度,宽遮罩描边可覆盖线条的抗锯齿效果。若要创建沿线移动的脉冲,请在同一路径上使用 getPointAtLength 读取位置。

A4. 背景(神圣不可变)

当设计呈现在照片或纹理上时,背景是设计的一部分,而非装饰。通常,应用框架以外的整个画布都是背景。

  • 背景绝不改变。 无论是开场、循环、溶解还是任何缩放,背景都绝不移动、模糊、缩放、淡化或重新着色。
  • 每个版本都保留背景。 “仅加载”或“无缩放”变体仍然是在同一背景上的同一设计。绝不要为了简化变体而移除照片。
  • 导出原始图像,而非渲染结果。 GET /v1/files/:key/images 会返回原始图像填充。请将填充的缩放模式和 imageTransform 应用为 CSS:对于带变换的拉伸填充,background-size 为框架尺寸除以变换缩放比例,background-position 为负偏移量。
  • 进行验证。 将静帧中的背景区域与参考图进行比较。PSNR 目标应高于 45 dB。裁切或偏移错误会立即显现。
  • 磨砂面板必须能看到照片。 使用 backdrop-filter: blur() 的表面必须是独立元素,作为背景上方的同级元素;绝不能放在 filter 或 opacity 小于 1 的祖先元素内。无头 Chrome 只有在该元素设置了 transform: translateZ(0) 和 z-index 时才会渲染它。
  • 如果无法复现,请在开始制作前说明。 如果无法访问图像、纹理是生成的或填充缺失,请告知用户并询问。绝不要悄悄替换成纯色。
  • 照片请以高质量 JPEG 或 WebP 格式存储,而非 PNG。

A5. 布局:应用框架或独立卡片

先判断设计属于以下哪种布局。这会影响空状态包含哪些内容,以及哪些元素会移动。

应用框架(连接的表面)。 仪表盘或应用屏幕的框架由两个或更多表面连接而成,例如侧边栏和主体,且常有几像素的重叠。只有这种布局才使用静态表面。

  • 空状态中就有这些表面。 侧边栏和主体从第一帧开始就已就位、完全不透明且保持静止。它们绝不淡入、滑入、缩放或模糊。只有其中的元素会加载。
  • 在交界处保留 Figma 的堆叠顺序。 主体位于侧边栏内容裁切区域上方,因此侧边栏内容绝不会绘制到主体上,主体也绝不会滑到侧边栏上方。
  • 主体中的卡片属于内容:它们与其他元素一样加载。

独立卡片(便当盒式布局、卡片组、小组件)。 卡片彼此分离,间有空隙。这些不是连接的表面。

  • 卡片本身会加载。 每张卡片都带模糊效果上移(B2 中的 card 类型),按阅读顺序依次错开,卡片内容随后在其中加载。
  • 空状态中只有背景。

两种布局都适用:

  • **每个容器都裁切自身内容,**严格贴合自身形状,包括每个角的圆角半径。任何内容都不得溢出到相邻元素或背景上。
  • 图像形状的表面。 如果容器轮廓由照片构成(例如封面标题栏或满版卡片图片),请用图像的平均颜色作为占位色,然后再将照片加载到其中。
  • 嵌套内容也属于内容。 托盘、工具栏和内层面板都与其他元素一样加载。

裁切模式可确保舞台坐标在裁切区域内保持有效。对于独立卡片,卡片单元会包裹其裁切区域,使卡片及其内容一起移动:

<div class="clip" style="left:113px;top:138px;width:305px;height:907px;border-radius:16px 0 0 16px">
  <div class="cin" style="left:-113px;top:-138px"></div>
</div>
.clip { position: absolute; overflow: hidden; isolation: isolate; }
.cin  { position: absolute; width: var(--stage-w); height: var(--stage-h); }

A6. 页面架构(展示页)

在网站上,应按照“Two contexts”中的网站规则,直接在网站代码中构建。以下时钟、单位和模式仍适用于各个部分。

  • 单个自包含的 HTML 文件:舞台尺寸固定,与画面的精确尺寸一致,并缩放至适合视口。图片以内联 base64 形式嵌入;重复使用同一张图片时进行去重。仅等待 <img> 元素解码;CSS 渐变不属于图片。
  • 模式: ?still 以静态形式渲染与 Figma 画面完全一致的内容(也用于减少动态效果的场景)。?loop 像导出内容一样重复播放循环。?capture 会关闭 requestAnimationFrame,并公开 window.__start()、window.__tick(dt) 和循环时间 window.__lt,由录制器驱动时钟(C1)。
  • 单位。 每个动画元素都由一个覆盖整个舞台的 .u 图层包裹,并携带其延迟 --d。入场效果使用以 .stage.in 为选择器的 CSS 过渡。由于单位覆盖整个舞台,其中的 clip-path 或 transform-origin 百分比是相对于舞台而非元素计算的。请使用根据元素矩形计算出的像素值。
  • 静态图层保持合成状态。 为剪辑和描边叠加层设置 transform: translateZ(0),避免附近的过渡开始时抗锯齿效果发生变化。否则,每次循环开始时都会有一条 1px 的边缘闪烁。
  • 使用一个 JS 时钟(requestAnimationFrame)驱动所有基于时间的内容:蒙版、扫光、故事和强调效果。应使用循环时间驱动,而不是墙上时钟,以确保捕获结果具有确定性。在 ?capture 模式下,时钟仅通过 __tick 前进。
  • 页面默认行为:等待图片解码,播放一次入场效果(如果画面位于较长页面中,则在滚动进入视口时播放),之后持续播放故事内容。

A7. 验证静态画面

在进行任何动态效果处理之前,以 2x 尺寸渲染 ?still,并与参考图对比。

  • 对整幅画面和每个容器分别计算 PSNR,并先进行轻微模糊(gblur sigma≈1.2),避免子像素抗锯齿产生过大影响。整幅画面应高于 35 dB,每个容器应高于 30 dB。文字较多的容器通常接近 30 dB,因为 3x 图层会重采样到 2x;若某个容器低于此值,请在 sigma≈2.5 下重新测试,此时文字边缘不再计入,只保留结构。
  • 查看经过模糊和增强的差异图。它能揭示结构错误:光晕位置过高、图层堆叠顺序错误、缺少描边或阴影偏移。
  • 不要追求子像素级偏移。 以 2x 尺寸测量无法分辨小于半个像素的误差,“修正”反而会造成过度调整。应修正结构,而非抗锯齿。

PART B — 动态效果系统

以下所有时间、距离和曲线均来自 Make it yours 中的数值。切勿自行编造数值。

B1. 从空白状态开始

每次页面加载和每次导出的第一帧都是空白状态。空白状态再变为已加载状态。切勿以最终画面作为开场。

  • 应用画面: 背景和静态拼接表面,不包含其他内容。
  • 独立卡片: 只有背景。

B2. 加载编排

出现动画流畅、缓慢、柔和:元素从下方升起,并从模糊变清晰,依次错开出现。

类型用途起始状态
card整体显示独立卡片(便当盒布局、卡片组),包括其阴影translateY(24px)、scale(0.985)、blur(6px)、透明度为 0
up文本、标题、数值、行translateY(10px)、blur(4px)、透明度为 0
down顶部工具栏、顶部栏translateY(-8px)、blur(4px)
left侧边栏项、列表行、标签translateX(-10px)、blur(4px)
pop图标、徽章、头像、按钮、圆点从元素自身中心开始 scale(0.82–0.88),blur(4px)
photo图片先显示骨架屏闪光效果,再从 blur(14px) saturate(0.6) 变清晰。对于嵌入较大图层中的照片,请参阅 A3
wipe条形、填充、强调效果在完整舞台单元(A6)上,以像素为单位从左向右通过 clip-path 揭示。右侧内缩从 stageW − x 开始(宽度为 0,因此不会露出细缝),结束于 stageW − (x + w)(元素完全显示)。顶部和底部内缩保持为 y 和 stageH − (y + h)
line / grow分隔线、刻度、时间线从起始边缘开始 scaleX(0) / scaleY(0)
donut环形图、仪表盘圆锥遮罩从 12 点方向开始顺时针扫过
tray / rise面板、托盘、文件夹从容器底边内侧向上滑动 40px,并由容器裁切
fade位于另一图层下方的图层仅改变透明度
  • 顺序。 容器按阅读顺序加载,每隔 80 到 90ms 加载一个:对于应用框架,按静态表面内的区域顺序加载;对于独立卡片,按卡片顺序加载,每张卡片的内容比卡片本身晚约 0.15s 开始加载。容器内部依次显示标题、标签、数值,然后是可视化元素。组内项目间隔 30 到 80ms 依次出现。
  • 绝不对对象使用 scale(0)。 只有线条和条形会从零开始增长,因为它们需要绘制出来。
  • 被遮挡的图层最后出现、最先离开。 最终画面中被其他图层遮住的内容(例如会议托盘下方的日历周)应在遮挡层就位后再出现,并在溶解效果中更快退出,避免透出残影。
  • 骨架屏与实际形状一致,并向内缩 1px,确保图片加载后周围不会出现光晕。
  • 完整画面的入场动画总时长约为 2 到 2.5 秒。

B3. 条形按进度填充

  • 数据条、计量器和强调效果像进度条一样填充:使用带有缓入缓出曲线的左向右 clip-path 擦除效果,时长为 0.7 到 1.1s。
  • 绝不添加加载线。 不要在卡片或表面的顶边添加进度线,也不要添加设计中没有的其他条形或分隔线。只有设计中实际存在的形状才应进行动画处理。

B4. 故事循环(主要内容)

入场动画只是开场。主要内容是让每个可视化元素演绎自身的功能,并循环播放:线索列表收到新线索,图表正在被阅读,日历找到空闲时段,提示词逐字输入。用户说“为可视化元素添加动画”时,指的就是这些内容。如果画面只是在加载后静止不动,只带有几次脉冲效果,那就还没完成。

先编写故事。 构建前,用一句话描述每个可视化元素的故事,例如“新的线索落在最上方,旧线索随之向后移”。从下表中选择一种,或按照同样的思路自行设计。

视觉元素故事
一叠项目(潜在客户、通知、文件)新项目带着模糊效果落在最上方,其他项目后退一格,最后一个项目退出
折线图或面积图刮动条滑过,圆点沿着曲线移动,工具提示中的数值随之计数。或者:折线重新绘制,标记弹出,数字随之递增
柱状图、均衡器、单元格网格柱条像实时数据流一样起伏,然后回到设计中的高度。圆点和工具提示沿着当前柱条移动
环形图、仪表各分段依次平滑向外展开,再回到原位。或者:圆环再次顺时针扫过
表格、列表光标逐行查看,行高亮,并加载相应预览。或者:柔和的高亮逐行向下移动
日历、日程表高亮搜索空闲时段,事件滑入其中,然后返回
流程、连接线、管道脉冲沿流程方向穿过连接线,经过节点或头像时将其点亮。虚线依次向前移动
AI、提示词、表单删除并重新输入提示词,逐个评估标签,然后模糊显示结果
计数器、KPI数字随驱动它们的图表一同递增
个人资料、长内容内容在裁剪区域内缓慢滚动,然后返回
Logo 行连续滚动字幕,每轮恰好移动一整组,因此能无缝循环
媒体播放器播放控件和进度向前移动,同时播放按钮脉冲

故事规则

  • 开始和结束时都要完全还原设计。 每个故事都从 Figma 画框开始,执行动画,然后回到画框状态。静止时,画框与 ?still 完全一致。
  • 使用设计本身的元素。 使用设计中的卡片、行、柱条、圆点、标签和工具提示。故事所需的辅助元素(光标、重复的堆叠项目、额外脉冲等)必须符合设计风格,并在 ?still 中隐藏。
  • 只有在表达重点时才更改文本。 例如计数器、打字效果和工具提示数值。其他文本保持静止。
    • 打字效果应逐字形显示导出的文本图层,绝不能使用平滑擦除效果。根据文本 PNG 中 Alpha 通道为空的列找出字形边界,每隔 45 至 70ms 显示一个字形,并在静止时每秒让光标在边缘闪烁一次。删除时按相同步骤倒序进行,速度稍快。
    • 计数器需要使用设计中的字体、精确的字号和字重渲染实时文本,并在静止时准确显示设计中的文本。
  • 明显易察觉,但不要忙乱。 每张带有故事的卡片都要每隔一到两秒呈现一次清晰的动作。故事由多个节拍组成(脉冲离开、节点点亮、箭头发射),这些节拍构成动画节奏。如果观看者在两秒内看不出画框有动画效果,说明故事太含蓄(“增加更多动效”)。如果用户喜欢,文档仪表板等安静的屏幕可以在加载后保持静止。询问一次即可。
  • 仅含文本的卡片在展示中不添加故事,而是随卡片一起升入画面并保持静止。并非每张卡片都需要动效,但每个视觉元素都需要。
  • 在最小导出尺寸下仍须清晰可读。 故事在 800×500 的 GIF 中仍必须清晰可见。如果移动元素在此尺寸下小于约 4px(例如脉冲圆点、细线),请在设计风格范围内放大它、添加光晕,或换成更大的故事。
  • 错开卡片的动画。 卡片绝不能同步动作。调整每张卡片故事的时间,让注意力按阅读顺序在画面中移动。
  • 每张卡片只设计一个故事,时长为 3 到 8 秒。短故事可以在一个周期内重复(在 8.6s 的故事阶段内重复两次很常见),第二次播放时可以改变一个节拍。每次播放仍须在溶解效果开始前回到静止状态。
  • 故事使用 JS 时钟上的缓动关键帧轨道(A6)构建,以确保捕获结果确定:
// [[time, value], ...] eased per segment; values may be numbers or arrays
function track(keys, t, ease = easeInOut) {
  if (t <= keys[0][0]) return keys[0][1];
  for (let i = 1; i < keys.length; i++) {
    const [t1, v1] = keys[i], [t0, v0] = keys[i - 1];
    if (t <= t1) {
      const k = ease(Math.min(1, (t - t0) / (t1 - t0 || 1)));
      return Array.isArray(v0) ? v0.map((v, j) => v + (v1[j] - v) * k) : v0 + (v1 - v0) * k;
    }
  }
  return keys[keys.length - 1][1];
}
// a slot that scrubs along a curve and returns to its design position
slot.style.transform = `translate(${track([[0, [0, 0]], [1.2, [46, -18]], [2.6, [46, -18]], [3.8, [0, 0]]], t).join('px,')}px)`;

点缀动效叠加在故事之上,绝不能取代故事:

强调效果用途参数
脉冲环状态指示点、实时指示器、播放按钮周期 1.8–2.2s,环形缩放 1 → 2.6,不透明度 0.55 → 0
心跳爱心、点赞、闪光0.35s 内放大 20%,每 3s 一次
轻柔浮动漂浮物体(文件夹、设备、卡片中的卡片)±2.6px,2.8s 正弦波
光泽扫过照片、条形元素、按钮光带宽度为元素宽度的 38%,峰值为 34% 白色,每 3.4s 扫过一次,耗时 1.5s。在浅灰色形状上会消失;请改用填充动画
骨架屏微光设计中的占位线条2.4s 闪烁
  • 引言动画结束后(约 2.4s),故事动画和强调效果开始。故事动画从精确的设计开始,因此无需淡入。强调效果和新增辅助元素在 0.6s 内淡入。

B5. 循环周期(展示)

网站没有循环周期:引言动画在滚动到该区域时播放一次,故事动画会持续播放,直到该区域离开视口。

阶段默认时间
展开0 → 2.4s
故事动画2.4 → 11.0s
淡出11.0 → 11.6s(不透明度动画 0.5s)
空白间隔11.6 → 12.0s
  • 故事阶段必须让每张卡片都完整播放至少一个故事。 延长循环周期,不要截断故事动画。安排故事动画的时间,确保每张卡片在淡出开始时都恢复静止。
  • 实时页面在引言动画结束后会一直讲述故事。 只有 ?loop 和导出内容会淡出并重新开始。
  • 淡出至空状态(B1)。 对于应用框架,只有内容淡出,组合表面保持不变。对于分离的卡片,卡片也要淡出。背景永不淡出。
  • 重置所有内容。 禁用过渡效果一帧的类会重置 CSS,但所有由 JS 驱动的值(遮罩、轨迹、辅助元素、输入文本、脉冲)也必须恢复到空状态,否则它们会在空白间隔中残留。循环结束时的帧必须与循环开始时的帧相同。

B6. 缓动和时长

进入动画绝不使用默认的 ease 或 linear。

标记曲线用途
--ecubic-bezier(0.16, 1, 0.3, 1)进入时的变换(1.05s)
--e2cubic-bezier(0.33, 1, 0.68, 1)不透明度和模糊(0.7–0.8s)
--iocubic-bezier(0.65, 0, 0.35, 1)擦除、填充、扫光、镜头移动
linearlinear仅用于匀速运动(微光、播放)

展示动画的时间安排刻意比产品 UI 动画更慢。此处不适用“低于 300ms”之类的产品规则;要求是缓慢、平滑。

B7. 镜头

  • 默认不缩放、不平移。 用户必须明确提出要求。
  • 如有要求:仅应用框架朝内容缩放,背景绝不移动(A4),移动使用 --io,并在淡出前让框架回到静止位置。

B8. 性能和无障碍

  • 仅对 transform、opacity、filter 和 clip-path 设置动画。绝不对 top、left、width 或 height 设置动画。
  • 动画模糊值不超过 14px。静态 backdrop-filter 可以使用。
  • prefers-reduced-motion: reduce 渲染静态帧。
  • 引言动画使用 CSS 过渡,基于时间的效果仅使用 JS 时钟。

B9. 动效检查

编码前,对录制的帧(C1)运行这些检查。

  • 溢出检查。 使用容器的真实形状进行测试,包括圆角,而不是矩形:从页面本身渲染遮罩(每个容器都是黑底上的纯白形状,使用其精确圆角半径;如果有阴影,也包括阴影区域),将遮罩扩张 2px 以跳过抗锯齿,然后只比较黑色像素。此处用于比较的任何 ?still 都应按录制的设备缩放系数渲染,而不是按 A7 中的 2x 渲染。
    • 应用画面: 在循环的每一帧中,拼接表面之外的每个像素都必须与空白的第一帧完全一致(PSNR 为 inf)。
    • 独立卡片: 在故事阶段,卡片(及其阴影,如果有)之外的每个像素都必须与 ?still 帧完全一致,确保故事、辅助元素或内容都不会溢出卡片。引入帧不纳入检查,因为卡片自身的上移会使像素移动;联系表必须显示内容随卡片移动,绝不超前或出现在卡片外。
  • 接缝检查。 导出窗口的第一帧必须与一个周期后的帧相同。
  • 空白节拍检查。 空白节拍的每一帧都必须与 __start 之前的帧相同。接缝检查无法发现未重置的 JS 状态,因为参与比较的两帧都处于循环时间 0;此项检查可以发现。
  • 静止状态检查。 淡出前的最后一帧必须与录制缩放系数下的 ?still 相同,以证明每个故事都回到了设计状态。
  • 联系表。 将关键时刻的帧(空白、中间引入、落位、故事中的多个时刻、中间淡出、结束)拼成网格,检查是否有溢出、残影、突变,以及故事动效是否过于微弱而难以察觉。

PART C — 录制与导出

C1. 确定性录制

绝不要实时录屏。推进一个虚拟时钟并逐帧截图。三个细节决定循环是否流畅、接缝是否干净;它们都在代码片段中。

await page.clock.install({ time: 0 });
await page.goto(url + '?capture', { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.clock.pauseAt(10000);
const loopTime = [];
for (let i = 0; i < frames; i++) {
  // 1. whole milliseconds that average exactly 1000/60 (17, 17, 16, ...); runFor(1000 / 60) drifts
  const dt = Math.round((i + 1) * 1000 / 60) - Math.round(i * 1000 / 60);
  await page.clock.runFor(dt);
  loopTime.push(await page.evaluate(([i, PRE, dt]) => {
    if (i === PRE) window.__start();          // 2. start inside the same evaluate as the seek
    if (i >= PRE) window.__tick(dt / 1000);   // 3. the page clock advances here, never through rAF
    const now = performance.now();
    for (const a of document.getAnimations()) {  // CSS transitions follow the fake clock
      a.__m ??= window.__mark;                    // each one keeps the mark it was created under
      a.pause(); a.currentTime = Math.max(0, now - a.__m);
    }
    return i >= PRE ? window.__lt : 0;
  }, [i, PRE, dt]));
  await page.screenshot({ path: `${out}/${String(i).padStart(5, '0')}.png` });
}

在页面端的 ?capture 中:

  • __start() 会添加 in 类。每次舞台状态类发生变化(in、out、重置)时,都会设置 window.__mark = performance.now(),这样每次过渡都会从创建时刻开始寻址。过渡延迟(--d)计入 currentTime。
  • __tick(dt) 会推进循环时间,运行循环阶段(B5),绘制每个故事和强调元素,并设置 window.__lt。
  • 不会结束的循环(闪光、跑马灯)由 JS 时钟驱动,而不是由无限 CSS 关键帧驱动;寻址会重新启动后者。

PRE 是开始前录制的几帧(约 15 帧),用于让页面稳定下来。将 loopTime 与帧一并保存。

  • 视口尺寸即帧尺寸。deviceScaleFactor = min(1920 / W, 1200 / H),使较长的一边达到 X 限制。截图会取整到整像素,因此尺寸可能出现 1 像素的偏差(1440×990 的帧可能变成 1745×1200);MP4 会裁剪为偶数尺寸,因此预期尺寸为 1744×1200。
  • 导出窗口从第 PRE − 1 帧开始(循环时间 0,空白状态),总长度正好为 cycle × 60 帧。录制时至少多录 10 帧,以便进行接缝检查。

C2. 格式

文件规格
<name>-x.mp4H.264,yuv420p,60fps,CRF 14–15,预设 slow,+faststart,最大 1920×1200,偶数尺寸
<name>-loop.gif宽 960px,15–20fps,约 2 到 3MB,用于网站
<name>-800x500.gif始终交付。当画面有宽阔的空白边缘(大多数卡片和 Bento 画面)时,裁剪到内容区域,保留约 40px 边距,宽高比严格为 1.6:1。内容铺满画面时,按比例缩放并用页面背景色填充。背景为照片时,始终裁剪,避免照片旁出现纯色条带。20fps;负载较重时用 12fps
ffmpeg -start_number $A -framerate 60 -i frames/%05d.png -frames:v $N \
  -vf "crop=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -preset slow -crf 15 \
  -pix_fmt yuv420p -movflags +faststart name-x.mp4

ffmpeg -start_number $A -framerate 60 -i frames/%05d.png -filter_complex \
  "trim=end_frame=$N,fps=20,scale=960:-2:flags=lanczos,split[a][b];\
   [a]palettegen=max_colors=192:stats_mode=diff[p];\
   [b][p]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" -loop 0 name-loop.gif

对于 800×500 GIF,在 split 前添加 scale=800:500:force_original_aspect_ratio=decrease,pad=800:500:(ow-iw)/2:(oh-ih)/2:color=<page bg>。

  • 画面中有照片时,始终使用 Bayer 抖动;否则照片会出现色带。
  • 如果 GIF 体积过大,先降低帧率或颜色数,再考虑缩小尺寸。

输出格式

开始制作前,用几行说明:

  1. 背景——展示页还是网站。若是网站,列出每个版块及其包含的内容(介绍和故事、仅介绍,或无内容)
  2. 布局——应用框架(哪些相连的表面保持静止),或独立卡片(哪些卡片升起),以及裁剪形状
  3. 背景——原样保留,或说明哪些内容无法复现
  4. 动画编排——加载顺序,以及每组元素的动画类型
  5. 故事——每个视觉元素用一句话说明其演绎的内容,并列出任何强调元素
  6. 循环——总时长和各阶段

制作完成后,报告:

  • 静帧保真度(整体和最差表面的 PSNR)
  • 溢出检查和接缝检查结果
  • 三个文件的大小、尺寸、帧率和时长
  • 未能原样复现的内容,请明确列出

按你的需求定制

所有动画和导出参数都在这里。编辑本节,其余技能说明将随之调整。

参数默认值
升起距离10px(up)、8px(down)、10px(left)
入场模糊4px,照片为 14px
弹出起始缩放0.82–0.88
变换时长1.05s,使用 --e
不透明度和模糊时长0.7–0.8s,使用 --e2
擦除时长0.7s,慢速条为 1.1s,使用 --io
卡片升起24px、scale(0.985)、blur(6px),变换时长 0.9s,使用 --e;不透明度时长 0.6s
容器交错间隔80–90ms
托盘和文件夹升起40px
打字每 45–70ms 输入一个字形,光标闪烁周期 1s
项目交错间隔30–80ms
故事开始时间2.4s
故事时长每张卡片 3–8s,卡片间错开
循环12s(11.0s 开始溶解,11.6s 重置);若故事需要,可延长
MP460fps,CRF 15,最大 1920×1200
网站 GIF宽 960px,20fps
小型 GIF800×500,20fps

快速检查清单

保真度

  • 从源文件渲染参考帧
  • 将每个运动元素(包括文本)作为独立图层导出
  • 保留 Figma 的 z 轴顺序;描边置于子元素上方;组阴影作用于组合后的整体
  • 使用原始图像作为背景,按其变换定位,并通过 PSNR 验证
  • 每个变体都保留背景;磨砂面板模糊其后方的照片
  • 静帧与参考图一致(没有结构差异)

背景与布局

  • 确定背景:展示页,或逐个映射各版块的网站(纯文本版块保持原样)
  • 网站模型:在加载介绍之后添加故事前先询问用户
  • 确认布局:具有相连表面的应用框架,或独立卡片
  • 应用框架:相连表面均存在、不透明,并保持空状态;交界处保留 Figma 的堆叠顺序
  • 独立卡片:卡片本身升起,空状态仅显示背景
  • 每个容器都按精确圆角裁剪自身内容
  • 对应布局类型的溢出检查通过

动画

  • 导出文件和页面加载均从空状态开始
  • 按阅读顺序依次升起并模糊,随后变清晰
  • 条形区域像进度条一样填充;不添加设计中没有的加载线或形状
  • 加载完成后,每个视觉元素都演绎一个故事,且从完全符合设计的画面开始和结束;纯文本卡片保持静止
  • 故事在 800×500 GIF 中清晰可见
  • 故事内容明确、卡片间错开,且只有计数器或打字会改变文本
  • 被遮挡的图层最后出现、最先离开
  • 除非用户要求,否则不缩放或平移;背景始终静止
  • 溶解回空状态,背景保持不变
  • 接缝、空白间隔和静止检查均通过

导出

  • 确定性帧捕获:按毫秒整数步进,平均间隔为 1000/60;在 seek 操作内开始并推进,不使用实时录制
  • 交付 <name>-x.mp4、<name>-loop.gif、<name>-800x500.gif
  • 不打印或提交令牌

致谢

部分内容来自 Elaya 的 ai-design-skills,采用 MIT 许可。

若干通用动效规则参考了以下采用 MIT 许可的技能集合,但并未照搬:

评论

支持 Markdown