时间轴 · 遮罩 · 渲染:一条贯穿三端的技术主线
摘要
综艺节目里那种“字一个个亮起来、像荧光笔从左往右划过去”的字幕效果,看起来是美术问题,本质上是时间轴、遮罩与渲染三件事的协同。本文提出一条贯穿全文的分析主线:逐字高亮 = 时间轴驱动 + 遮罩裁剪 + 渲染合成。围绕这条主线,先讲清荧光笔底色与擦除动画的视觉机理,再分别给出 ASS/SSA、Web(CSS+Canvas)、移动端(原生/Flutter)三端可落地的实现路径,随后讨论分词对齐、性能预算、跨端一致性与工程化封装,最后对实时字幕、AI 驱动的动态排版等前沿方向做技术预判。全文以工程视角组织,代码骨架与参数表可直接复用。
目录
一、问题的本质:为什么“逐字高亮”这么难做对
很多团队第一次做逐字高亮字幕,都会经历同一个过程:先用最简单的“整句淡入”糊过去,被导演打回;然后尝试用逐字出现,发现节奏不对;再尝试加底色,发现底色和文字不同步;最后加擦除动画,发现擦除边缘和字宽对不上。问题不在于某个特效不会写,而在于没有把“时间轴、遮罩、渲染”这三件事分开建模。
逐字高亮字幕的难点可以归纳为三层:
- 时间层:每个字的起止时间戳从哪里来?是等分、按音素、还是按语音识别对齐?
- 几何层:荧光笔底色的宽度如何随字宽变化?擦除的“笔尖”如何精确落在字与字之间?
- 渲染层:用什么技术栈画?ASS 的 clip、CSS 的 background-clip、Canvas 的 clip 路径,三者的坐标系和性能特征完全不同。
本文评述:把这三层混在一起写代码,是绝大多数“字幕特效做不稳”的根因。一旦分层,每一层都可以独立测试、独立替换——时间层换对齐算法不影响渲染层,渲染层换技术栈不影响时间层。这条分层主线会贯穿全文。
1.1 一个常见的错误直觉
最常见的错误直觉是“逐字高亮 = 每个字单独做一个动画”。如果真这么做,你会得到一串彼此独立、边缘生硬的色块,字与字之间会出现缝隙或重叠,节奏也会因为每个字独立缓动而变得零碎。正确的直觉应该是:整句是一条连续的时间轴,逐字高亮只是在这条时间轴上切出若干区间,底色是一条连续的“笔迹”,擦除是一个连续移动的遮罩边界。
换句话说,观众看到的“一个字一个字亮”,其实是“一条笔迹匀速划过,经过哪个字就点亮哪个字”。这个视角的转换,直接决定了后面所有实现方案的形态。
1.2 综艺感从哪来
综艺字幕和新闻字幕的差别,不在信息量,而在“节奏感”和“手工感”。新闻字幕追求稳定、可读、不抢戏;综艺字幕追求的是“跟得上情绪”。荧光笔底色带来的手工感、擦除动画带来的推进感,本质上都是在模拟“有人在实时地划重点”。
笔者认为,这种“模拟手工”的思路,是理解整个特效设计的关键。底色不是纯色块,而是带一点透明度和边缘柔化的“马克笔”;擦除不是硬切,而是带一点缓动和轻微回弹的“笔尖”。这些细节决定了成品是“像 PPT 动画”还是“像综艺”。
二、视觉机理拆解:荧光笔底色与擦除动画
在动手写代码之前,先把两个视觉元素拆到参数级别。只有把“荧光笔底色”和“擦除动画”翻译成可调参数,才能在不同技术栈之间做等价实现。
2.1 荧光笔底色的参数化
荧光笔底色(highlighter)不是简单的 background-color。真实的马克笔划过纸面,有几个特征:颜色半透明、边缘略深、两端有轻微收笔、笔画高度略高于字高。把这些特征参数化:
本文评述:multiply 混合模式在浅色背景上效果最好,但在深色背景上会失效甚至变黑。工程上建议做“背景亮度检测”,浅底用 multiply,深底用 screen 或直接提高不透明度。这个细节很多教程不会提,但实际项目里一定会踩。
2.2 擦除动画的参数化
擦除动画(wipe)的核心是“一个从左到右移动的裁剪边界”。它的参数比底色更微妙,因为直接决定节奏感:
- duration:整句擦除总时长。综艺字幕通常 0.3–0.8 秒/句,太快看不清,太慢拖节奏。
- easing:缓动曲线。
linear最像匀速划笔,ease-out有收笔感,cubic-bezier(.2,.8,.2,1)是综艺常用的“快起慢收”。 - softEdge:擦除边缘柔化宽度。0 是硬切,几像素是“笔尖模糊”。
- perCharSync:是否与逐字时间戳严格同步。同步则节奏准,不同步则更顺滑但可能“字没亮完笔已过”。
笔者认为,perCharSync 是区分“专业做法”和“业余做法”的分水岭。业余做法只做一条匀速擦除,不管字的时间戳;专业做法让擦除边界在每个字的起止时间点上“对齐”,这样即使语速不均,视觉上也和语音严丝合缝。
2.3 两者的耦合关系
底色和擦除不是两个独立特效,而是同一件事的两个阶段:底色是“笔迹的完整形态”,擦除是“笔迹逐渐显现的过程”。所以正确的实现是——先画出完整的底色层,再用一个遮罩把它从左到右“揭开”。这个“先画后揭”的思路,在 ASS、CSS、Canvas 里都能一一对应,是跨端一致性的基础。
核心公式:逐字高亮 = 完整底色层 × 移动遮罩。遮罩的位置由时间轴决定,遮罩的形状由几何层决定,最终由渲染层合成。
三、时间轴模型:从整句到逐字的三层结构
时间轴是整个特效的“指挥中心”。一个清晰的时间轴模型,应该有三层:句子层、词层、字层。每层都有自己的起止时间和相对偏移。
3.1 三层时间轴的定义
// 时间轴数据结构(单位:毫秒)
{
"sentence": {
"start": 1200, "end": 3400,
"text": "这个效果其实很简单"
},
"words": [
{ "start": 1200, "end": 1600, "text": "这个" },
{ "start": 1600, "end": 2000, "text": "效果" },
{ "start": 2000, "end": 2400, "text": "其实" },
{ "start": 2400, "end": 3400, "text": "很简单" }
],
"chars": [
{ "start": 1200, "end": 1300, "text": "这", "wordIdx": 0 },
{ "start": 1300, "end": 1400, "text": "个", "wordIdx": 0 },
{ "start": 1400, "end": 1500, "text": "效", "wordIdx": 1 },
{ "start": 1500, "end": 1600, "text": "果", "wordIdx": 1 }
// ... 其余字
]
}
本文评述:三层结构的好处是“各取所需”。整句淡入用 sentence 层,逐词高亮用 words 层,逐字擦除用 chars 层。很多实现只做 chars 层,结果做整句动画时又要重新算,反而更麻烦。三层都保留,是工程上更省事的选择。
3.2 时间戳的三种来源
等分估算最简单:句子总时长除以字数。但中文语速不均,等分出来的节奏会很“机械”。人工打轴最准,但成本高。ASR 强制对齐(forced alignment)是当前性价比最高的方案,后文第七章会详细展开。
3.3 时间轴的归一化
不同来源的时间戳精度不同,工程上建议统一归一化到“句子相对时间”。即把 sentence.start 记为 0,所有字的时间减去 sentence.start,得到 0 到 duration 的相对值。这样渲染层只关心相对时间,换播放器、换帧率都不用改。
// 归一化示例
function normalize(chars, sentenceStart) {
return chars.map(c => ({
text: c.text,
t0: c.start - sentenceStart,
t1: c.end - sentenceStart
}));
}
// 渲染时:progress = (currentTime - sentenceStart) / duration
笔者认为,归一化还有一个隐藏好处:它让“预览”和“导出”共用同一套时间逻辑。预览时用 requestAnimationFrame 推进 currentTime,导出时用固定帧率推进 currentTime,只要归一化做好,两边结果一致。
四、ASS/SSA 路线:Karaoke 与 clip 遮罩
ASS(Advanced SubStation Alpha)是视频字幕领域的事实标准,Aegisub、libass、FFmpeg 都支持。做逐字高亮,ASS 有两条路:Karaoke 标签和 clip 遮罩。两者各有适用场景。
4.1 Karaoke 标签:\k、\kf、\ko
ASS 原生支持卡拉 OK 效果,通过 \k 系列标签实现逐字变色:
\k:硬切,字在指定时间瞬间变色。\kf:扫过(sweep),颜色从左到右填充,最接近“擦除”效果。\ko:描边扫过,用于描边动画。
{\kf40}这{\kf40}个{\kf40}效{\kf40}果{\kf40}很{\kf40}简{\kf40}单
本文评述:\kf 的“扫过”是文字颜色本身从左到右变化,而不是底色。如果你想要的是“荧光笔底色扫过”,\kf 直接做不到——它变的是字色,不是背景。这是很多人第一次用 ASS 做荧光笔时的最大误区。
4.2 clip 遮罩:真正的“底色擦除”
要做荧光笔底色擦除,ASS 里正确的做法是:画一个底色矩形(用 \p1 绘图模式),然后用 \clip 或 \iclip 逐帧改变裁剪区域。
; 底色层(独立 Dialogue 行)
Dialogue: 0,0:00:01.20,0:00:03.40,Highlight,,0,0,0,,{\clip(0,0,0,1080)\p1\c&H00E0FF&}m 0 0 l 800 0 800 120 0 120{\p0}
; 文字层(在底色之上)
Dialogue: 0,0:00:01.20,0:00:03.40,Text,,0,0,0,,{\kf...}这个效果其实很简单
关键在 \clip(x1,y1,x2,y2) 的 x2 随时间从 0 增加到 800。用 Aegisub 的自动化脚本或 \t 标签可以做插值:
{\clip(0,0,0,1080)\t(0,2200,\clip(0,0,800,1080))\p1\c&H00E0FF&}...
本文评述:\t 插值虽然方便,但它是线性插值,做不出“快起慢收”的综艺感。要精确控制缓动,还是得逐帧生成 clip 参数。这也是为什么很多商业字幕工具会自己写脚本生成 ASS,而不是手写。
4.3 逐字同步的 ASS 实现
如果擦除要和逐字时间戳严格同步,思路是:把整句的 clip 动画拆成 N 段,每段对应一个字,每段的 x2 从该字左边界移动到右边界,时长等于该字的持续时间。这样即使语速不均,笔尖也总在“正在读的那个字”上。
表中数据为模拟示例,用于说明分段逻辑,非真实测量值。实际工程中,x 坐标需要根据字体度量(font metrics)精确计算每个字的宽度,这部分在第七章展开。
五、Web 路线:CSS 变量与 Canvas 双方案
Web 端做逐字高亮,有两条主流路线:纯 CSS(配合少量 JS)和 Canvas。前者适合字幕量不大、需要 SEO 和可访问性的场景;后者适合字幕量大、需要精确控制和导出视频的场景。
5.1 CSS 方案:background-clip 与 mask
CSS 方案的核心是用 background 画底色,用 mask-image 或 clip-path 做擦除。最简洁的写法是用 CSS 变量驱动遮罩位置:
.highlight {
--progress: 0; /* 0 → 1 */
background: linear-gradient(90deg,
rgba(250, 204, 21, .7) calc(var(--progress) * 100%),
transparent calc(var(--progress) * 100%));
background-size: 100% 1.2em;
background-position: 0 0.15em;
background-repeat: no-repeat;
display: inline;
padding: 0 .08em;
border-radius: .1em;
}
然后用 JS 在 requestAnimationFrame 里更新 --progress。这种写法的好处是:底色和文字在同一层,不需要额外 DOM,性能好,且天然支持换行。
本文评述:background-size: 100% 1.2em 这个技巧很关键。它让底色高度独立于行高,可以做出“划出格”的效果。但要注意,display: inline 元素的 background 在换行时会断开,如果字幕跨行,需要给每行单独处理。
5.2 逐字同步的 CSS 实现
要让擦除和逐字时间戳同步,可以用 Web Animations API 给每个字单独做动画,但更优雅的做法是:把整句的 progress 映射到“当前正在读的字”,用分段函数计算:
function progressAt(chars, t) {
// t: 当前时间(相对句子起点,ms)
for (let i = 0; i < chars.length; i++) {
const c = chars[i];
if (t < c.t0) return i / chars.length;
if (t <= c.t1) {
const local = (t - c.t0) / (c.t1 - c.t0);
return (i + local) / chars.length;
}
}
return 1;
}
这个函数把“时间”映射成“进度”,进度再映射成遮罩位置。它的优点是:无论语速多不均,笔尖永远在“正在读的字”上,且字与字之间平滑过渡。
5.3 Canvas 方案:精确控制与导出
Canvas 方案适合需要“所见即所得导出”的场景。核心是用 ctx.clip() 做遮罩,用 ctx.measureText() 精确测量每个字的宽度:
function drawHighlight(ctx, chars, t, style) {
const progress = progressAt(chars, t);
const totalWidth = chars.reduce((s, c) => s + c.width, 0);
const wipeX = totalWidth * progress;
ctx.save();
// 1. 画完整底色
ctx.fillStyle = style.color;
ctx.fillRect(0, 0, totalWidth, style.height);
// 2. 裁剪到擦除位置
ctx.beginPath();
ctx.rect(0, 0, wipeX, style.height);
ctx.clip();
// 3. 在裁剪区内重画底色(或直接画文字)
ctx.fillStyle = style.color;
ctx.fillRect(0, 0, totalWidth, style.height);
ctx.restore();
}
本文评述:Canvas 的 measureText 在不同浏览器、不同字体下的结果可能有细微差异,做导出时建议固定字体文件(用 FontFace API 加载),避免“预览和导出不一致”。
5.4 两种方案的取舍
笔者认为,实际项目里最常见的组合是“CSS 做预览,Canvas 做导出”。两者共用同一套时间轴和 progress 函数,保证视觉一致。这种“双引擎”架构在字幕工具里很常见。
六、移动端路线:原生与 Flutter 的取舍
移动端做逐字高亮,挑战和 Web 不同:屏幕小、性能敏感、字体渲染差异大。主流路线有三条:iOS 原生(Core Text / CATextLayer)、Android 原生(Canvas / StaticLayout)、Flutter(CustomPainter)。
6.1 iOS:CATextLayer + CAShapeLayer 遮罩
iOS 上最自然的做法是用 CATextLayer 画文字,用 CAShapeLayer 做遮罩,用 CABasicAnimation 驱动遮罩宽度:
let mask = CAShapeLayer()
mask.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 0, height: h)).cgPath
highlightLayer.mask = mask
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 0, height: h)).cgPath
anim.toValue = UIBezierPath(rect: CGRect(x: 0, y: 0, width: w, height: h)).cgPath
anim.duration = duration
anim.timingFunction = CAMediaTimingFunction(name: .easeOut)
mask.add(anim, forKey: "wipe")
本文评述:iOS 的 CATextLayer 有个坑——它默认不做子像素抗锯齿,文字边缘会偏硬。做综艺字幕时反而可以利用这一点,做出更“锐利”的观感,但做长文阅读就不合适。
6.2 Android:StaticLayout + Canvas.clipRect
Android 上用 StaticLayout 做文字排版,用 Canvas.clipRect 做擦除:
canvas.save();
canvas.clipRect(0, 0, wipeX, height);
canvas.drawRect(0, 0, totalWidth, height, highlightPaint);
canvas.restore();
staticLayout.draw(canvas);
Android 的 StaticLayout 在 API 23 之后支持 getPrimaryHorizontal 等方法,可以精确拿到每个字符的 x 坐标,这对逐字同步非常关键。
6.3 Flutter:CustomPainter 统一双端
Flutter 的 CustomPainter 可以一套代码跑双端,且 TextPainter 提供了 getOffsetForCaret 等精确度量接口:
class HighlightPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final tp = TextPainter(text: text, textDirection: TextDirection.ltr)
..layout();
final progress = progressAt(chars, t);
final wipeX = tp.width * progress;
canvas.save();
canvas.clipRect(Rect.fromLTWH(0, 0, wipeX, size.height));
canvas.drawRect(
Rect.fromLTWH(0, 0, tp.width, size.height),
Paint()..color = highlightColor,
);
canvas.restore();
tp.paint(canvas, Offset.zero);
}
}
本文评述:Flutter 的优势是“一套逻辑双端一致”,但要注意 TextPainter 的布局结果在 iOS 和 Android 上可能因字体回退(font fallback)而略有差异。做严格一致的字幕,建议打包自定义字体。
6.4 三端一致性对照
七、分词与对齐:逐字时间戳从哪来
逐字高亮的效果好不好,七分靠时间戳,三分靠渲染。时间戳不准,再漂亮的动画也是白搭。这一章讲清楚时间戳的三种来源和各自的工程细节。
7.1 等分估算:快速但机械
等分估算就是“句子时长 ÷ 字数”。实现最简单,但中文语速受语义影响很大:虚词快、实词慢、数字和专有名词更慢。等分出来的节奏会明显“赶”。
本文评述:等分不是不能用,而是要加“权重”。比如按字宽加权、按词性加权(实词权重高)。一个简单的改进是按字符宽度加权:
function estimateByWidth(chars, totalDuration) {
const totalW = chars.reduce((s, c) => s + c.width, 0);
let t = 0;
return chars.map(c => {
const d = totalDuration * (c.width / totalW);
const item = { ...c, t0: t, t1: t + d };
t += d;
return item;
});
}
7.2 ASR 强制对齐:当前性价比最高
强制对齐(forced alignment)的思路是:给一段音频和对应的文本,让模型输出每个字/词的时间戳。开源方案里,WhisperX、Montreal Forced Aligner (MFA)、Kaldi 是三条主流路线。WhisperX 基于 Whisper 做词级对齐,中文支持较好;MFA 基于 Kaldi,精度高但配置复杂。
本文评述:WhisperX 的 word-level 对齐在中文上有个已知问题——它默认按空格分词,中文没有空格,需要先做分词再对齐。实践中常用 jieba 或 pkuseg 分词,再把词级时间戳按字宽分配到字级。这一步的误差会累积,建议对精度要求高的场景用 MFA。
7.3 人工打轴:精修与质检
无论 ASR 多准,商业交付前都需要人工过一遍。Aegisub 是打轴的事实标准工具,它的 karaoke 模板和自动化脚本可以大幅提高效率。实践中常见的流程是:ASR 出初稿 → Aegisub 导入 → 人工微调 → 导出 ASS。
笔者认为,人工打轴的价值不只是“修正误差”,更是“注入节奏”。哪些字该快、哪些字该慢、哪里该停顿,这些是模型很难学到的“表演感”。综艺字幕的“味道”,很大一部分来自打轴师的节奏判断。
7.4 数据集与预处理
做对齐研究常用的公开数据集包括 AISHELL-1/2/3(中文语音)、THCHS-30、Common Voice 等。以 AISHELL-1 为例,预处理通常包括:重采样到 16kHz、去除首尾静音、文本正则化(数字转中文)、分词。这些步骤会直接影响对齐精度,做实验时需要在论文里说明。
本文评述:很多对齐论文只报“平均误差”,但字幕场景更关心“最大误差”和“误差分布”。一个字偏了 200ms,观众就能感觉到“字幕和声音不同步”。所以评估对齐质量时,建议同时看 P50、P90、P99 误差。
八、性能预算与渲染优化
逐字高亮字幕的性能瓶颈,通常不在“画一个字”,而在“每帧重算所有字的位置”。这一章讲几个工程上真正有效的优化手段。
8.1 预计算与缓存
字宽、字位置这些几何信息,在字体和字号确定后就是常量。应该在初始化时一次性算好,缓存起来,而不是每帧重算。measureText 这类调用在部分浏览器上开销不小,缓存能显著降低 CPU 占用。
// 初始化时算一次
const layout = chars.map(c => ({
text: c.text,
x: measureWidth(prefix), // 前缀宽度
w: measureWidth(c.text)
}));
// 渲染时直接用 layout[i].x 和 .w
8.2 分层渲染与脏矩形
把“静态文字层”和“动态底色层”分开,是字幕渲染的经典做法。文字层只在内容变化时重绘,底色层每帧重绘。如果底色层只覆盖当前正在擦除的区域(脏矩形),开销可以进一步降低。
8.3 帧率与同步
字幕动画的帧率不必和视频帧率一致。24fps 的视频,字幕用 30fps 或 60fps 渲染都行,但要注意“时间驱动”而不是“帧驱动”——用 performance.now() 或 media.currentTime 算 progress,而不是每帧固定加一个增量。这样即使掉帧,动画也不会“变慢”。
本文评述:很多字幕特效在低端设备上“越播越慢”,根因就是用了帧驱动。改成时间驱动后,掉帧只会让动画“跳”,不会“拖”。对字幕这种短时长动画,跳帧比拖帧观感好得多。
九、工程化封装与跨端一致性
把前面所有内容封装成一个可复用的“字幕特效引擎”,需要考虑接口设计、配置格式、跨端一致性三件事。
9.1 配置格式设计
一个好的配置格式,应该把“时间轴”“样式”“动画”分开:
{
"timeline": { "start": 1200, "end": 3400, "chars": [...] },
"style": {
"font": "SourceHanSans-Bold",
"size": 48,
"color": "#FFFFFF",
"highlight": { "color": "#FACC15", "opacity": 0.7, "heightRatio": 1.25 }
},
"animation": {
"type": "wipe",
"easing": "cubic-bezier(.2,.8,.2,1)",
"softEdge": 2,
"perCharSync": true
}
}
这种格式的好处是:时间轴可以来自 ASR,样式可以来自设计稿,动画可以来自预设库,三者独立替换。
9.2 跨端一致性清单
- 字体:三端打包同一份字体文件,避免回退差异。
- 度量:用同一套字宽表,或至少保证度量算法一致。
- 时间:统一用毫秒整数,避免浮点累积误差。
- 缓动:用同一套贝塞尔曲线参数,不要各端用“默认缓动”。
- 混合:multiply/screen 的实现在各端有差异,建议做视觉回归测试。
9.3 测试与回归
字幕特效的测试,不能只看“能不能跑”,要看“跑出来对不对”。建议做三件事:一是关键帧截图对比(在固定时间点截图,逐像素比对);二是时间戳单元测试(progressAt 函数的边界条件);三是性能基准测试(在目标设备上测每帧耗时)。
本文评述:关键帧截图对比是最有效的回归手段。字幕特效的 bug 往往是“某个时间点视觉不对”,纯逻辑测试很难覆盖。把“时间点 → 期望截图”做成测试用例,能挡住绝大多数回归。
十、前沿预判与结语
逐字高亮字幕的技术,正在从“手工特效”走向“智能生成”。几个值得关注的方向:
10.1 实时字幕与低延迟对齐
直播场景对延迟极其敏感。传统的“先识别再对齐”有两段延迟,新的流式对齐方案(如基于 CTC 前缀的在线对齐)可以把延迟压到几百毫秒。这对逐字高亮的实时性提出了新要求:动画必须能在“字还没完全确定”时就开始。
10.2 AI 驱动的动态排版
大模型可以理解语义,从而决定“哪些字该高亮、哪些该放大、哪些该换色”。这比固定规则灵活得多。笔者认为,未来的字幕工具会从“参数调节”走向“意图描述”——你告诉它“这里要强调”,它自动选择最合适的特效组合。
10.3 可微分渲染与自动优化
学术上已经有“可微分字幕渲染”的探索,把字幕渲染建成可微函数,用梯度下降优化参数(颜色、时长、缓动)以匹配目标视频的风格。这条路虽然还早,但方向值得关注。
10.4 结语
回到本文的主线:逐字高亮 = 时间轴驱动 + 遮罩裁剪 + 渲染合成。这条主线之所以重要,是因为它把“看起来很难”的综艺特效,拆成了三个各自可解的问题。时间轴解决“什么时候亮”,遮罩解决“亮到哪里”,渲染解决“怎么画出来”。三端实现、性能优化、工程封装,都是这条主线的延伸。
笔者认为,掌握这条主线后,你不仅能做荧光笔擦除,还能做描边扫过、色块弹跳、逐词放大等一整套综艺字幕特效——因为它们本质上都是“时间轴 + 遮罩 + 渲染”的不同组合。这才是这篇文章真正想传递的东西。
延伸阅读与工具链接
- Aegisub 官方文档(ASS 标签与自动化脚本):https://aegisub.org/docs/latest/
- libass 项目(ASS 渲染库,含 clip 实现细节):https://github.com/libass/libass
- WhisperX(词级强制对齐):https://github.com/m-bain/whisperX
- Montreal Forced Aligner:https://montreal-forced-aligner.readthedocs.io/
- MDN CSS mask 教程:https://developer.mozilla.org/docs/Web/CSS/mask
- MDN Canvas clip 教程: 视频动画 视频动画技术
微信扫一扫分享
打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。
💬 评论 (0)
评论功能已关闭

