无缝循环动态图形(代码实现),可叠加Scenario MCP中的主体
概述
这里的循环是时间的纯函数。draw(ctx, w, h, t, P) 绘制 t 秒时的帧,其中每个运动的周期都是循环长度(12秒)的约数,因此12秒时的帧与0秒时的帧完全相同,片段无缝循环。无需生成任何内容:不变形、不闪烁,改变一种颜色即可完成品牌换色,同一代码可在任意尺寸下渲染。Scenario.com 上的 Motion Loops 探索页面展示了80个以这种方式制作的循环(40个背景,40个形状动画),每个都附有代码。scripts/loop-template.html 包含了这些循环所依赖的相同辅助函数(M.*)、调色板(P)和 tile(...) 注册方法,因此它们的切片代码可以在其中无修改运行。
Scenario MCP 提供了代码无法绘制的内容:用户的主体。吉祥物、产品或角色以透明背景切图的形式从 Scenario 输出,循环页面将其绘制在循环之上,并使用循环自身的时钟,因此可以获得相同的运动模糊效果,合成画面仍保持循环。对于真正需要运动的主体,会从该切图生成AI视频片段,在Scenario中移除其背景,然后通过 ffmpeg 将片段叠加在渲染的循环上。循环本身无需模型,渲染成本为零。
需要 Node 18 或更高版本、Playwright(npm install playwright,然后 npx playwright install chromium;如果已安装 Google Chrome,可使用 --channel chrome)、ffmpeg 和 ffprobe。仅当需要主体时才需要 Scenario MCP。连接、模型选择和生成循环:如果已安装 scenario 技能,则使用它;否则参考 references/scenario-mcp.md。
快速参考
cp scripts/loop-template.html loop.html # 每个文件一个循环:设置、draw()、叠加
# 在浏览器中打开 loop.html:实时预览;?t=3 冻结画面,?tone=accent 或 ?tone=light 切换色调
node scripts/render-loop.mjs loop.html --check # 确定性检查和循环接缝,不生成视频
node scripts/render-loop.mjs loop.html --stills look --at 0,3,6,9 # 生成静止帧以供查看
node scripts/render-loop.mjs loop.html --out loop.mp4 # 12秒,1920x1080,24 fps
node scripts/render-loop.mjs loop.html --params "overlay=subject.png" --out loop.mp4 # 带切图
node scripts/render-loop.mjs loop.html --width 1080 --height 1920 --out loop-9x16.mp4 # 竖屏
node scripts/render-loop.mjs --qa loop.mp4 composite.mp4 # 对任意MP4进行接缝和格式检查
在已安装 Playwright 的文件夹中运行。不会覆盖现有文件,除非使用 --force。页面检查会渲染同一时刻两次(结果必须一致),将12秒整的帧与0秒帧进行比较,并将循环点处的步长与周围运动及其他周期边界处的运动进行比较。除非输出 PASS,否则渲染拒绝编码;--ignore-seam 仅用于特意设计在循环点处的剪切。
| 规则 | 原因 |
|---|---|
每个周期都是12秒的约数(1、1.5、2、3、4、6、12);使用 M.phase(t, period, offset) | 12秒时的帧等于0秒时的帧 |
| 滚动在每周期内移动整数个图案重复 | 分数重复会在接缝处产生跳跃 |
仅通过 M.rand(i, salt) 生成随机性;帧之间不使用 Math.random、Date 或状态 | 运动模糊和检查会按乱序渲染帧 |
尺寸使用 u = h / 100,数量使用 w | 在16:9、9:16、1:1和缩略图尺寸下均正确 |
颜色仅来自 P(bg、fg、fg2、mid、accent、paper、ink),每种品牌颜色有三种色调 | 通过改变 BRAND 重新品牌;通过改变色调获得对比度 |
缓动函数来自 M.ease,包含预备动作、过冲和停顿;仅滚动使用恒定速度 | 经过设计的运动,而非机械运动 |
渲染设置(与80个循环发布时一致,也是脚本的默认值):24 fps、8个运动模糊子帧(180度快门)、2倍超采样、H.264 High、CRF 16、4:2:0、BT.709有限范围、faststart。循环闭合的图案、深度技巧和审查习惯:references/loop-craft.md。
| Scenario MCP 步骤 | 调用的方法 |
|---|---|
| 连接,选择团队和项目 | teams_list(有多个时询问用户) |
| 导入用户自己的素材 | upload_asset、对每个部分的curl PUT、upload_asset_complete |
| 选择模型 | recommend(txt2img、img2img、img2video、video2video),或 search |
| 读取其输入 | model_schema_get |
| 估价,用户确认后运行 | model_run(先 dry_run: true),然后 model_run 和 jobs_wait |
| 获取结果 | asset_download,然后 curl -L -o |
工作示例:品牌吉祥物漂浮在编码循环之上
目标:创建一个12秒的无缝1920x1080循环,使用品牌颜色,品牌吉祥物漂浮其中,并可选择创建吉祥物挥手的版本。
-
连接。 调用
teams_list。如果返回多个团队或项目,询问使用哪一个,并将其team_id和project_id传递给后续所有调用:对于已登录账户,search、model_schema_get和model_run在没有它们时会拒绝运行。 -
首先创建循环(本地,免费)。
cp scripts/loop-template.html mascot-loop.html,将BRAND.accent设置为品牌颜色,保留示例切片(透视地板,一个朝向摄像头的网格流)或编写你自己的draw()。打开文件预览,然后:node scripts/render-loop.mjs mascot-loop.html --check node scripts/render-loop.mjs mascot-loop.html --stills look --at 0,3,6,9查看静止帧。
--check必须以PASS结束。 -
导入吉祥物。 如果用户有文件,则上传:
{ "file_name": "mascot.png", "content_type": "image/png", "kind": "image", "file_size": 482113 }(
file_size来自wc -c mascot.png。)使用 curl 将部分 PUT 到返回的 URL,然后调用upload_asset_complete并传入{"upload_id": "<upload id>"},保留资产 ID。 -
选择模型。 使用以下参数调用
recommend:{ "capability": "img2img", "prompt": "参考图像中的吉祥物,全身,四分之三视角,轻盈漂浮姿势,透明背景切图,边缘干净,无阴影,无文字", "limit": 5 }如果没有文件,使用
"capability": "txt2img"并描述吉祥物。如果next_step.type是ask_user,显示选项并等待。优先选择模式中具有透明背景选项的模型;否则计划在之后进行背景移除步骤。 -
读取模式并估价。 使用
{"model_id": "<来自 recommend 的 id>"}调用model_schema_get。根据模式构建参数:参考图像字段接受上传的资产 ID(如果模式要求数组则使用数组),并设置透明背景选项。然后:{ "model_id": "<来自 recommend 的 id>", "dry_run": true, "parameters": { "prompt": "<上述提示>", "<参考图像字段>": ["<吉祥物资产 id>"], "<背景字段>": "transparent" } }告知用户价格并等待确认。
-
运行并下载。 使用相同的参数和
"wait": false调用model_run,然后使用{"job_ids": ["<job id>"]}调用jobs_wait。然后使用{"asset_id": "<asset id>", "format": "png"}调用asset_download,并使用curl -L -o subject.png "<url>"将文件下载到mascot-loop.html所在文件夹。在循环背景色上以100%比例打开该PNG:深色色调上的边缘光晕会显现;建议重新生成而非隐藏它们。 -
在画布中合成并渲染。
node scripts/render-loop.mjs mascot-loop.html --params "overlay=subject.png" --stills look --at 0,0.75,1.5,2.25 --force node scripts/render-loop.mjs mascot-loop.html --params "overlay=subject.png" --out mascot-loop.mp4 --psnr 4drawOverlay()使吉祥物以3秒的上下浮沉和6秒的左右摆动运动,并带有接触阴影,所有周期均为12的约数,因此合成画面仍保持循环;在此调整其大小和位置。脚本会检查页面、渲染288帧、编码,并检查文件的循环点。--psnr比较一个编码帧与其无损渲染版本。 -
可选:吉祥物运动的镜头。 使用
"capability": "img2video"调用recommend,以切图的资产ID作为起始图像,提示词如“吉祥物挥手并转头,纯色平背景,锁定的摄像机,无剪切”。然后执行模式读取、估价、确认、运行、jobs_wait、下载。接着使用"capability": "video2video"和“从此片段中移除背景,保留带alpha通道的主体”调用recommend,选择 WebM VP9 带 alpha 作为输出。再次执行模式读取、估价、确认、运行、下载。不带--params overlay=...渲染循环一次(--out loop-plain.mp4),然后使用 ffmpeg 将片段叠加到循环之上(命令在 references/scenario-mcp.md 中)。合成片段的长度与片段相同;除非片段本身循环,否则不是无缝循环。 -
交付 HTML(循环保持可编辑)、MP4、静止帧以及任何合成文件。
常见错误
- 在12秒处出现接缝跳跃。 周期不是12的约数(如5秒周期),或滚动移动了2.5个重复。
--check报告SEAM JUMP(12秒帧与0秒帧不同)。修复周期;切勿在两端进行交叉淡入淡出。 - 在切片中使用状态、
Math.random或Date。 相同的t会产生不同的帧,运动模糊子帧不一致,循环无法闭合。--check报告NOT DETERMINISTIC。所有计算应基于t和M.rand(i, salt)。 - 仅读取十六进制的颜色辅助函数。
P.mid和M.mixColor返回rgb()字符串,仅支持十六进制的解析器会静默地将其转为黑色。模板的M.rgba和M.mixColor两者都能读取。 - 快速运动时出现闪烁。 对于整帧推进,8个子帧不够;会显示步进复制的痕迹。构建中的全页滑动需要40个子帧。对于非常快的循环,增加
--samples。 - 线框被视为杂乱。 一个3D波浪网格就曾如此;重新构建为隐藏线山脊线(每行填充自身下方的背景,从远到近绘制),看起来就是地形了。
- 深度除以两次。 一个投影将焦距再次按深度缩放,导致场景收缩到中心。使用
M.proj投影一次。 - 每1.5秒采样静止帧来检查3秒周期。 这只显示了两个姿态。应在单个周期内以及循环点两侧采样(
--at 11.9,11.95,0,0.05)。 - 将所有标记视为错误。 设计好的快切(弹出、爆发)或循环点处的停顿可能触发接缝或闪烁检测。先查看帧。脚本会将循环点与周围运动以及其他周期边界进行比较,这避免了大部分此类问题:构建中的所有80个循环均通过了检查。
- 标签或计数器换行错误。 任何按周期索引的内容必须使用
M.mod进行换行;在构建中,页面计数器在循环点处显示“06 / 05”,直到修正后才正常。 - 在某一种色调下对比度低。 为该循环更改色调(深色、强调色、浅色),而不是添加颜色。
- 从磁盘加载叠加PNG。 从
file://打开的页面绘制本地图像并回读像素会失败,因为图像会污染画布。render-loop.mjs通过本地地址提供文件夹服务;浏览器预览仅用于显示,因此不受影响。 - 字体加载前绘制文字。 Canvas 文字不会等待网页字体;在第一帧之前应 await
document.fonts.load(...),否则在循环中不要绘制文字。 - 追求高码率。 在纯色区域上,x264 会使用很少的比特,文件看起来很小。在构建中,PSNR 显示编码器几乎没有增加损失;唯一的实际损失是在细饱和线条上的4:2:0色度采样。使用
--psnr检查,并通过加粗细彩色线条而不是提高码率来解决。 - 丢失 VP9 片段的 alpha 通道。 ffmpeg 内置的 VP9 解码器会忽略 alpha;请在
-i shot.webm之前加上-c:v libvpx-vp9。 - 使用视频模型创建循环。 生成的片段最后一帧很少与第一帧完全匹配。请保持循环由代码生成,仅在需要主体时使用 Scenario。
- 凭记忆使用模型 ID 或价格,或在
model_run中使用本地路径。 使用recommend或search选择模型,读取model_schema_get,通过dry_run估价并等待确认,并先上传文件。 - 有多个团队但不询问。 如果
teams_list返回多个团队或项目,请询问用户。