← 视频设计 02 / 03

构成主义动效(视频)设计理念 v1.0

基准实现:../../constructivism-motion-20s.html — 1920×1080 / 20 秒 / 六幕 / 无缝循环 / 单文件 姊妹文档(网页):../constructivism/01-美学谱系与形式法则.md、02-设计系统规格.md 本文只讲时间媒介。网页那份的九条形式法则有 4 条在视频里不成立,第 3 节给出实测依据。 配套经验:02-视频制作·结构主义经验.md(8 条硬经验速查 + 出片管线 + 验收脚本 + 本机环境的坑)。


0. 为什么视频必须单独一份文档

同一套构成主义,在网页和视频里是两门手艺。把网页的构图法则直接搬到视频上会明确地做坏——本项目的 v2 就是一次实证:它严格遵守网页法则(红 ≤15%、斜轴 -15°、动效 120–320ms),结果力量感消失,被判定「不如最初版」。

维度 网页 视频
时间 用户驱动(滚动 / hover / 点击) 播放器驱动,观众无法干预
单帧 vs 时间轴 每一帧都必须是完整可读的构图 单帧可以「不完整」,信息在时间里累积
面积法则 红 ≤15%(保证正文可读) 用时间平均面积衡量,单帧可以整幅铺红
动效时长 120–320ms(交互反馈的感知阈值) 0.4–1.2s(镜头级动作,观众在看而不是在操作)
可读性约束 正文必须过 WCAG AA 只有「承载文字的少数几帧」需要过
声音 通常无 有时间性,可与画面互为节拍器
循环 不适用 无缝循环是硬约束

一句话:网页是在一帧里讲完构图,视频是在二十秒里讲完构图。用单帧的克制换时间的爆发,是视频这边的基本盘。


1. 力量从哪来:基准实现的五个决策

「有力量」不是形容词,是五个可复现的决策叠加:

决策 1:红色不是点缀,是色场(field)

整幅朱红铺底,黑色斜切巨块压在上面。红色在 01 / 03 / 04 / 06 四幕里是背景而不是强调色。

实测各幕色彩面积(1920×1080 全画幅像素占比):

幕 时间码 红 墨 纸 结构
01 ВПЕРЁД 0.0–3.0s 61.7% 32.8% 5.2% 红场 + 黑斜块
02 НАШ ТРУД 3.0–7.0s 2.4% 15.9% 81.4% 纸场 + 黑字
03 ПРОЛЕТАРИИ 7.0–11.0s 66.5% 27.2% 6.0% 红场 + 白圆 + 黑楔
04 СТРОЙКА 11.0–14.5s 69.1% 20.7% 9.0% 红网格满幅
05 КОНСТРУКТИВИЗМ 14.5–17.5s 4.3% 11.6% 83.3% 纸场 + 网点 + 黑巨字
06 ВПЕРЁД К ПОБЕДЕ 17.5–20.0s 68.6% 6.5% 24.6% 红场 + 白圆 + 快门收束

平均红色面积 ≈ 45%(网页法则上限 15% 的 3 倍)。红/纸交替本身就是节拍器:红 → 纸 → 红 → 红 → 纸 → 红。

⚠️ 这是本文档最重要的修正:网页的「红只占 5–15%」是单帧构图律,不能迁移到视频。视频要按时间平均面积(建议 30–55%)与单帧峰值(可到 100%)两个指标管,而不是按单帧构图管。

决策 2:黑不是文字色,是斜切巨块

--ink 在视频里主要承担「体量」而不是「可读性」:整块的黑色斜切多边形(01 幕右侧、03 幕左侧的楔形、06 幕的快门)与红色形成硬碰撞。文字反而常常是纸色压在红上。

决策 3:字号要超出安全区

「一个字占满整幅」是常规操作。基准实现的峰值字级 164–292px,画幅宽度 1920px(占比 8.5–15%)。 反面教训:КОНСТРУКТИВИЗМ(14 字母)在 188px 时会溢出画框切掉末字,已收到 164px(占画幅宽约 86%)。巨字必须实测宽高,不能凭感觉给字号。

决策 4:动效要分段、突然、有阻尼

全部动效只允许两种曲线:linear(等速横扫)与 steps() 量化跳步。禁止 ease-in-out、禁止回弹/弹性。位移一律硬切:色块从画外平移进来,不做淡入。

量化跳步是「机械感」的核心——把连续进度切成 3–12 段再取值(Math.floor(p*n)/n),于是运动呈现为顿挫的阶梯而不是丝滑滑动。

决策 5:红黑反色当重音

同一画面内,纸色文字压在红块上、黑字压在纸面上、红块切断字面——红与黑互换角色即构成重音,不需要渐变、阴影或发光。


2. 时间结构

2.1 六幕节奏表

# 时码 时长 功能 主色 核心动作
01 0.0–3.0 3.0s 起(宣告) 红 + 黑 红色斜带自左横扫入场 → 巨字硬切展开 → 黑块下沉 → 位置编号
02 3.0–7.0 4.0s 承(结构) 纸 + 黑 逐字跳出,红色标尺拉开,几何方块跳变入场
03 7.0–11.0 4.0s 转(冲突) 红 + 纸 + 黑 放射线机械转动 → 白圆涨出 → 黑楔自右刺入 → 竖排标签展开
04 11.0–14.5 3.5s 展(建造) 红网格 84 格斜向波次填充 → 红色斜杠横扫 → 描边字被擦出 → 实心翻转 → 年份跳变
05 14.5–17.5 3.0s 点题 纸 + 黑 半调网点浮现 → 红块扫过字底 → 巨字逐字左右对撞入场
06 17.5–20.0 2.5s 合(收束) 红 + 黑 白圆涨出 → 大字入场 → 快门上下对合 → 全黑停留 0.55s → 循环

节奏规律:三长(4.0/4.0s)两中(3.5/3.0s)一短(2.5s)。收束幕最短,因为它的任务只是「关灯」。

2.2 幕内结构(固定四拍)

每一幕内部都是同一种四拍,这是「像一部片子」而不是「像一堆动图」的原因:

  1. 底:底色/色场就位(≤0.2s,硬切)
  2. 主:主体(巨字或几何原语)入场(0.4–0.7s)
  3. 咬:一个次要元素切断/压住主体(0.2–0.5s)
  4. 记:编号、年份、位置标记跳变出现(瞬时,不滚动)

3. 与网页设计系统的对照:哪条通用,哪条不通用

网页法则(../constructivism/01-*.md) 视频里 说明
法则 1 对角线是默认动势(15°–45°) ✅ 通用 视频里斜轴还多一层作用:它是运动方向,斜带横扫的轨迹就是斜轴本身
法则 2 红/黑/白三色且比例失衡(红 5–15%) 🔴 不通用 见 §1 决策 1。视频按时间平均面积管,单帧可 100%
法则 3 几何原语只用四种 ✅ 通用 圆 / 矩形 / 三角楔 / 线;视频里三角楔升格为「切割动作」
法则 4 字号对比 ≥6:1 ✅ 通用且要更狠 视频无正文阅读负担,实测峰值 164–292px / 标签 22px ≈ 7.5–13:1
法则 5 照片是材料不是配图 ⚠️ 可选 基准实现完全没用照片(纯几何 + 半调网点块),零版权风险
法则 6 图文必须咬合 ✅ 通用 视频里升级为「文字与色块在时间上互相打断」
法则 7 网格要露出来并可被打破 ✅ 通用 网格在视频里是逐格填充的动画主体(04 幕)
法则 8 质感来自廉价印刷 ✅ 通用 颗粒、半调、硬边偏移阴影;套印错位在视频里可用逐帧抖动强化
法则 9 动效必须机械化 ⚠️ 通用,但「机械化」的含义要改 网页那份把它落实为 steps() 量化;视频里量化会读成卡顿(见 §8.4),机械感必须靠「果断缓动 + 硬切 + 时长节奏」承担
规格 §1.2 对比度实测表(朱红 4.15:1 仅大字) ⚠️ 部分 视频里只有「承载文字那几帧」需要过;巨字全部远超阈值
规格 §3.1 不对称布局、不居中 ⚠️ 放宽 视频里 05/06 幕用了中心对称构图(「巨字居中占满」是海报传统)
规格 §6 禁止超过 400ms 的动画 🔴 不通用 视频里 0.4–1.2s 是主运动区间;400ms 以下反而像卡帧

4. 循环点规范(硬约束)

无缝循环是视频相对网页独有的硬要求,也是基准实现踩过坑的地方。

4.1 三条硬约束

  1. 收束后必须留 ≥0.4s 纯黑停留(30fps 下 12 帧)。 - 教训:最初实现中快门在 t=19.95 才完全合拢,留给循环点的纯黑只有 50ms(1.5 帧)。实测该时刻画面仍是「红缝里的大字」,紧接着跳到下一轮的黑场 —— 成片里读作故障闪帧而不是收束。 - 修正:把对合区间从 lin(L, 0.85, 1.60) 收紧到 lin(L, 0.80, 1.15),合拢于 t=19.45,留出 0.55s 全黑。
  2. 帧号 / 时间码必须可关。场记 HUD 在循环点会从 КАДР 0597 跳回 0000,是可见的闪动。基准实现提供 K 键 / Tweaks 开关 / ?hud=0。
  3. 首末帧必须像素级一致(除有意保留的颗粒抖动)。

4.2 验收方法(可复现)

用无头浏览器抓 t=0.02 与 t=19.99 两帧做像素差:

mean=0.691 / 255   max=10 / 255   差异>30 的像素 = 0 个

残差全部来自颗粒换贴图(见 §8),属于有意保留的抖动。判定标准:「差异>30 的像素」必须为 0;max 残差应 ≤ 12/255,若明显超标说明某一层没对齐。


5. 动效语汇(只允许这六种)

动作 实现 时长 用在哪
斜扫入场 色块从画外平移 + 旋转,连续缓动 0.6–0.7s 01 幕红斜带、04 幕红斜杠
硬边滑入 外层 overflow:hidden + 内层 translate 0.3–0.9s 巨字、竖排标签、描边字(不用 clip-path,见 §8.1)
逐字/逐行跳出 每个字独立延迟 + 连续缓动 单字 0.3–0.5s,字间 35–75ms 02 幕、05 幕
果断缓动 1-(1-p)³:起步快、到位果断、无回弹无过冲 0.4–1.6s 除硬切外的全部动作
硬切 单帧瞬时切换(hard()),一次事件而不是连续运动 瞬时 标签出现、全片反相
编号跳变 直接替换文本,不做滚动计数 瞬时 年份 1929→1932、元素计数

禁止:回弹/弹性缓动;ease-in-out 默认缓动;模糊、辉光、渐变。

⛔ 重要更正:视频里不要用量化跳步。 早期版本把动作进度量化成 3–12 段(Math.floor(p*n)/n / steps()),以为那就是"机械感"。实测(从成品 mp4 逐帧量 01 幕红斜带横扫的 21 帧):14 帧画面完全没变(67% 静止),其余帧一次跳 12 万~25 万像素——观感就是"一顿一顿"。而且这个顿挫写在数值里,离线逐帧渲染也救不了:它不是时序问题,是内容问题。

修正:机械化由三样东西承担 —— 果断的缓动曲线、硬切、时长节奏,而不是把数值切成台阶。量化跳步降级为可选风格(Tweaks「机械跳步」/ ?quant=1),默认关闭。


8. 卡顿感诊断(实测)

「感觉卡一卡的」有两个完全不同的成因,必须先分清再动手——掉帧要优化性能,顿挫要改设计。用 ?perf=1(页面跑 8 秒,把 rAF 帧间隔分布打到左上角)在无头 Chrome 实测:

变体 p50 p95 max >33ms 的帧
全开(基线) 6.1ms 6.1ms 115ms 2 / 1293
关颗粒层 6.1ms 6.1ms 103ms 2 / 1313
关放射线 6.1ms 6.2ms 133ms 2 / 1298
全关 6.1ms 6.2ms 103ms 2 / 1316

结论:没有掉帧。 每帧 6.1ms(≈164fps 的余量),超时帧只有启动那 2 帧,且与颗粒层、repeating-conic-gradient 放射线都无关。所以「卡」是设计上的顿挫,来源两个:

  1. 机械跳步(设计意图,不是缺陷):所有动作都用 Math.floor(p*n)/n 量化成 3–12 段,进度不再连续 → 动作是「跳」的。这就是 v1 的工业机械感本身。读成卡顿时,用 Tweaks「机械跳步」切成连续即可。
  2. 颗粒层的位移抖动(真缺陷,已修):原实现每 50ms 把整张噪点贴图 background-position 跳一次,等于平移同一张图案——人眼会追踪这种相关性,读作「整幅画面在抖」。已改为 4 张独立噪点贴图轮换(12.5Hz),并顺势把内联 feTurbulence 换成 canvas 预生成的位图(kv.css 早已警告过全屏 SVG 滤镜的重栅格化代价)。

一般规律:胶片颗粒要「换一张」(独立随机),不要「平移同一张」。平移会产生可被追踪的相关性——这就是「像素级正确、观感却在抖」的根因。

8.1 决定性线索:只有「变化中的元素」卡

实测过程中用户给出了最关键的描述:

动起来的元素是一卡一卡的,尤其是在移动过程中自身也在变化的元素;等这个变化完成,它剩下的动作就流畅了。

这不是掉帧(掉帧会均匀影响整幅画面),而是光栅滞后:元素每帧都要重新栅格化,主线程不掉帧、rAF 间隔完全正常,但光栅线程交付慢一拍 —— 于是"只有正在变化的那个元素"看起来一顿一顿。

三类属性会触发逐帧重栅格化,必须换成合成器友好的等价写法:

危险写法 后果 等价替换
clip-path: inset() 逐帧揭示 那块巨字每帧重新栅格化 外层 overflow:hidden + 内层 translate 滑入
width / height 逐帧变化 每帧重排 + 重绘整块 transform: scaleX() + transform-origin
background-position 逐帧移动 整块背景每帧重新填充 背景挪到子元素,动它的 transform

⚠️ rAF 帧间隔指标测不出这一类问题。 主线程完全正常,光栅滞后只体现在观感上。碰到"只有变化中的元素卡"时不要看帧率,直接查该元素有没有在动非合成属性。

8.2 另一条硬规则:任何一幕都不许「定格」

「卡」的第二种来源是节奏而非渲染:动作做完就冻结、然后硬切。实测相邻 0.05s 的两帧做像素差,四幕的保持段全是 changed = 0 px(画面完全静止)——观众读作"动一下停一下"。而当时唯一"整段都在动"的第 04 幕(红色格子斜向波次持续填充),用户反馈不卡。

结论:入场动作可以短促、可以机械跳步,但从入场结束到落幕之间必须有持续动势。本项目给每幕补了连续的缓移/缓转/爬行(斜带缓移、圆徽慢转、放射线持续转、网点爬行、网格呼吸),并且这些环境动势一律用连续插值而不是 step() 量化。

8.3 真实窗口按幕统计

?perf=1&pskip=3000(跳过启动阶段),165Hz 屏、缩放 0.89:

幕 p50 p95 长帧
01 6.1 6.1 9 / 475(几乎全在启动阶段)
02 6.1 6.2 0 / 660
03 6.1 6.1 2 / 655
04 6.1 6.1 2 / 574
05 6.1 6.1 1 / 494
06 6.1 6.2 0 / 412

垂直同步间隔是 6.06ms —— p50 与 p95 都等于它,长帧率 ~1%,主线程层面已经完全干净。剩下的观感问题只能从 §8.1(光栅)与 §8.2(节奏)解决。

排查顺序:先 8.2 看节奏(有没有定格),再 8.1 查属性(有没有动非合成属性),最后 8.4 查数值(有没有量化台阶)——这三条都不成立时才去怀疑性能。 如果仍要求绝对均匀的帧,走 §9.3 的离线逐帧渲染——那条路根本不经过浏览器调度。

8.4 数值层面的顿挫:量化跳步(最隐蔽的一类)

前两节讲「节奏」和「光栅」,第三类顿挫来自数值本身:把动画进度量化成台阶。

实测方法(可直接复用):从成品视频里抽出某段动作的连续帧,逐对做像素差。

版本 01 幕红斜带横扫 21 帧的逐帧变化 结论
量化 Math.floor(p*6)/6 0,0,0,0,0,0,86711,0,0,249508,17,16,249493,… → 67% 的帧对完全静止 ⛔ 一顿一顿
连续 1-(1-p)³ 13664,176216,201603,179390,158382,138736,120438,103373,87656,73270,… → 单调衰减,无静止帧 ✅ 流畅且果断

判别口诀:如果"离线逐帧渲染出来的视频"仍然卡,那就不是渲染/时序问题,而是数值里写了台阶。 去查所有 Math.floor(p*n)/n、steps()、离散取整。

这条最值得记:前三轮我都在优化"怎么画得更快"(合成层、光栅、调度),而真正的元凶是"数值本来就在跳"。先验证数值是否连续,再谈性能。


6. 文字与字幕


7. 质感(廉价印刷)

强度上限仍然是「缩到 50% 看,几乎看不见」:

效果 手段 强度
纸张纤维颗粒 内联 SVG feTurbulence(baseFrequency 0.85)+ mix-blend-mode: multiply opacity 0.07–0.08
半调网点 repeating-radial-gradient 点阵(点 1.5px / 间距 6px) 只在「材料块」上
硬边偏移阴影 box-shadow: 8px 8px 0(无模糊) 少量
印刷标记 四角套准十字、构造网格线(33% / 66% 红线) 常驻,可 G 关

9. 画幅、录屏与交付

8.1 推荐参数

画幅      1920×1080(16:9)
帧率      30fps(基准实现的场记帧号与时间码就是按 30fps 计的,导出帧率与它一致即可)
时长      20.00s,循环播放
录屏 URL  constructivism-motion-20s.html?pure=1&hud=0      ← 隐藏控件条与场记
抓帧 URL  constructivism-motion-20s.html?t=15.5&pure=1&hud=0  ← 定格任意时刻

8.2 交互键位(录制前)

键 作用
空格 播放 / 暂停
← / → ±0.25s 微调
0 回到开头
H 纯净模式(隐藏全部界面)
K 场记开关(帧号 / 时间码)
G 构造网格线开关
I 全片反相印刷

Tweaks 面板(右下):版画颗粒、机械跳步、构造网格线、场记、红黑反色、颗粒强度、斜切角度、播放速度。

正片之外的诊断参数:?perf=1 打印帧间隔分布(&pms=20000 跑满一轮、&pskip=3000 跳过启动阶段);&grain=0 关颗粒、&quant=0 切连续动效、&guides=0 关构造网格线,用于 A/B 与出片。

⚠️ 两个已踩过的坑: ① 半调网点不要用 repeating-radial-gradient(circle at 0 0, ...) —— 那是以元素左上角为圆心的同心圆环,在距原点上千像素处曲率趋近于零,会退化成近乎平行的弧线(斜线摩尔纹),而且不同位置的稀疏方向还会变。真网点用 radial-gradient() + background-size 铺固定网格。 ② 颗粒要「换一张」不要「平移同一张」,理由见 §8。

9.3 出片:离线逐帧渲染(推荐),不要在线录屏

为什么不用「浏览器播放 + 录屏」:在线播放要同时跟合成器、光栅线程、垂直同步赛跑。§8 的每一次修复(clip-path、width、background-position、颗粒位移)都是在把这条赛道的损失压低,但它永远不可能归零——丢帧与光栅滞后是浏览器调度的固有噪声。

离线逐帧渲染把「时间」变成参数:每一帧按精确的 t 单独渲染,编码器按固定帧率输出;帧间隔由编码器保证,与浏览器调度完全无关。同一份 HTML 既是预览也是渲染源,不存在「预览和成片不一致」。

流程(本仓库 src/tools/render-frames.cjs):

# 1) 启动一个带调试端口的无头 Chrome,打开同一份 HTML
chrome --headless=new --remote-debugging-port=9223 \
       --window-size=1920,1080 --force-device-scale-factor=1 \
       --user-data-dir=<临时目录> \
       "file:///<绝对路径>/constructivism-motion-20s.html?pure=1&hud=0"

# 2) 逐帧渲染(帧 i 对应 t = i / fps;30fps × 20s = 600 帧)
node src/tools/render-frames.cjs 9223 frames 30 600

# 3) 编码
ffmpeg -y -framerate 30 -i frames/frame_%04d.png \
       -c:v libx264 -preset medium -crf 15 -pix_fmt yuv420p -tune animation \
       -movflags +faststart constructivism-20s-30fps.mp4

要点:

9.4 备选:在线录屏

只在「边改边看、快速出草稿」时用:全屏 + ?pure=1&hud=0,播一整轮后停止,裁掉首尾各一轮取中间完整循环段。它的时序质量上限就是浏览器调度质量,定稿不要用它。


10. 已知限制与待办

项 状态
视频成片(mp4) 暂缓。600 帧 1920×1080 母版已在 .scratch/frames-30fps/,编码一条命令、约 6 秒(§9.3)
中文标题字体 当前为系统黑体 / Noto Sans SC 900 回落;要上得意黑需自托管 SmileySans-Oblique.woff2
半调网点在 4K 上采样 点阵按 6px 网格铺设,1920 下正常;若改 4K 输出需同步放大点阵(否则网点变细)
视频压缩对纯朱红的影响 #cc0000 在低码率 H.264 下会有色带;建议 ≥12 Mbps 或改用 H.265 / ProRes
音频 当前无配乐。20s 循环建议按 100 BPM(每拍 0.6s)或 120 BPM(每拍 0.5s)做卡点,正好与 0.4–0.7s 的动作时长同量级
竖屏 / 方形版本 未做。九条法则里「斜轴 + 极端字号对比」在 9:16 下需重新分配,不能直接裁切

11. 版本沿革

版本 文件 定位 结论
v1(基准) constructivism-motion-20s.html 按「视频=时间媒介」直觉构建 ✅ 采用。力量感来自大红场 + 黑斜块 + 超大字号 + 分段机械动效
v2(对照) constructivism-motion-20s-v2.html 严格按网页形式法则重做(红 ≤15%、斜轴 -15°、动效 120–320ms、左对齐不对称) ❌ 作为主视觉被否 —— 红被压到 5–15%、动效被压到 0.32s 后力量感明显流失。保留为本文 §0 与 §3 的实证反例,其可复用的部分:贯穿主梁、真实套印错位、编号下沉到各幕、无字体的几何标签