Glitch 故障藝術:RGB 分離、掃描線錯位、Datamosh、VHS 質感五種效果
用純 CSS clip-path + mix-blend-mode 做出故障美學。5 個 phase:RGB 通道分離、scanline 切片抖動、datamosh 方塊錯位、VHS 雜訊。全部 seeded,無隨機抖動焦慮。
成品預覽
這是一支 10 秒、五個 phase 的故障美學示範。從乾淨的 BASE SIGNAL 開始,依序過渡到 RGB CHANNEL SPLIT、SCANLINE JITTER、DATAMOSH、最後收在 VHS ARTIFACT。全程用 GLITCH + SIGNAL LOST 兩組文字當主體,讓觀眾在每種故障效果下都還能「勉強」讀得出來——這才是故障美學的精髓:不是破壞,而是受控的破壞。
這篇會做出什麼
一支 10 秒 300 frame 的 Glitch Demo,分成 5 個 phase,每個 phase 60 frame:
- Phase 1(0~2s)BASE SIGNAL:乾淨白字黑底 + 永久 scanline + CRT vignette,當作「對照組」
- Phase 2(2~4s)RGB CHANNEL SPLIT:三份內容分別染成純紅/綠/藍,每 3 frame 抖動,
mix-blend-mode: screen相加回近白 - Phase 3(4~6s)SCANLINE JITTER:畫面切成 12 條水平條,每條
clip-path: inset()顯示自己那塊,各自水平位移 - Phase 4(6~8s)DATAMOSH:12 個隨機方塊區域,各自裁切並位移,像壓縮失敗的 P-frame 殘影
- Phase 5(8~10s)VHS ARTIFACT:sine wave 水平扭曲 + 飽和度呼吸 + 厚重 scanline + 輕微 RGB bleed
核心精神:GLITCH 與 SIGNAL LOST 兩組文字在每個 phase 都要保持可讀。故障不是馬賽克,是資訊在破碎邊緣求生。
前置知識
- T01 Hello Remotion —
<AbsoluteFill>、useCurrentFrame - T02 文字淡入動畫 —
interpolate基礎 - T3 YouTube 片頭動畫 —
<Sequence>分段 - T04 時間控制 + T05 Spring 動畫
- T22 Kinetic Typography — seeded random 概念(本篇會用到
mulberry32)
💡 這篇全程不需要 GLSL、不需要
@remotion/shapes、不需要外部圖片。只有 CSS。
原理解析:故障美學的三個 CSS 技巧
這段是整篇的核心,讀懂它你之後自己都能發明新故障效果。
1. RGB channel split 的 mix-blend-mode: screen 魔法
傳統上要做色差(chromatic aberration),要嘛進 Photoshop 手動分離 R/G/B 通道,要嘛在 GLSL shader 用 texture2D 對同一張紋理做 3 次 UV 偏移採樣。CSS 的做法優雅很多:渲染三份完全相同的內容,分別染成純紅 #ff0000、純綠 #00ff00、純藍 #0000ff,三層都加 mix-blend-mode: screen,再給每層一點點 translate 偏移。
screen 是加法混色:out = 1 - (1 - a)(1 - b)。三層純色完全疊在一起會回到白色,只有單層紅色的地方就看到紅,紅+綠看到黃,完全對應 RGB 光的加色模型。
<AbsoluteFill style={{ mixBlendMode: "screen", transform: "translate(-6px, 0)" }}>
<BaseContent color="#ff0040" />
</AbsoluteFill>
{/* 綠層 translate(0,0) #00ff80、藍層 translate(6px,0) #0080ff,結構相同 */}💡 為什麼不是
multiply?multiply是減法混色(印刷 CMYK 邏輯),純紅×純綠=黑,你會得到一團黑,沒有故障感。screen才是螢幕發光邏輯。
2. clip-path: inset() 做 scanline 切片
clip-path: inset(top right bottom left) 的四個值是從各邊向內裁掉多少。例如 inset(40% 0 55% 0) 代表「從上方切掉 40%、從下方切掉 55%」,結果只剩下畫面 40%~45% 那條橫帶。
把同一份內容複製 12 份,每份分配一條 stripPct = 100/12 ≈ 8.33% 的橫帶,用 inset(i*stripPct% 0 (100-(i+1)*stripPct)% 0) 切出該條,再對每條獨立 translateX(seeded)。關鍵洞察:12 條加起來就是完整畫面,所以文字永遠沒有完全消失,只是被水平撕裂。
{Array.from({ length: 12 }).map((_, i) => {
const top = (i * 100) / 12;
const bottom = 100 - ((i + 1) * 100) / 12;
const dx = rand(tick * 31 + i * 17, -40, 40);
return (
<AbsoluteFill
key={i}
style={{
clipPath: `inset(${top}% 0 ${bottom}% 0)`,
transform: `translateX(${dx}px)`,
}}
>
<BaseContent />
</AbsoluteFill>
);
})}3. Seeded jitter 的時間步進
如果 jitter offset 每一幀都變,你得到的是純噪點——眼睛抓不到形狀,只覺得畫面在沸騰。如果 10 幀以上才變一次,又太慢,看起來像卡頓不像故障。實驗出來的甜蜜點是每 3~5 幀換一次 seed,也就是每個偏移值停留 3~5 幀,產生那種典型的「咚、咚、咚」chunky glitch 節奏。
做法是把 frame 做 floor 除法產生「tick」,tick 當 seed:
const frame = useCurrentFrame();
const tick = Math.floor(frame / 3); // 每 3 幀升一階
const jx = rand(tick * 7 + 1, -10, 10); // seed 隨 tick 變,同一 tick 內保持不變💡 RGB split 我用
/3、scanline 我用/4、datamosh 我用/5——不同效果刻意錯開頻率,三個 phase 疊看就不會覺得節奏太機械。這是故障設計的小秘密。
Step 1:Base layer 設定
先把「對照組」的底建起來:黑底 + 大字 GLITCH + 副標 SIGNAL LOST + 永久 scanline + CRT vignette。這層之後每個 phase 都會重用。
Claude Code:
新增 Composition "TutorialGlitchDemo":
- 1920x1080 @ 30fps
- durationInFrames 300(10 秒)
- 背景 #050814(接近黑但帶一點藍)
抽出 BaseContent 元件:
- 置中 column
- "GLITCH" fontWeight 900, fontSize 360, letterSpacing 0.02em
- "SIGNAL LOST" monospace, fontSize 64, letterSpacing 0.4em
- BaseContent 可接 color prop,預設 #ffffff
再建兩個永久 overlay:
- ScanlineOverlay:repeating-linear-gradient 2px 深黑 + 2px 透明
mixBlendMode multiply
- Vignette:radial-gradient 外圍 0.75 黑
const BaseContent: React.FC<{ color?: string }> = ({ color = "#ffffff" }) => (
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
<div style={{ fontWeight: 900, fontSize: 360, color, letterSpacing: "0.02em" }}>GLITCH</div>
<div style={{ marginTop: 24, fontSize: 64, color, letterSpacing: "0.4em", opacity: 0.85 }}>
SIGNAL LOST
</div>
</AbsoluteFill>
);為什麼把 BaseContent 抽出來? 後面 5 個 phase 都會把它當積木用,有的染色、有的切片、有的位移。抽出來就能保證所有 phase 的「原始內容」完全一致,只差效果層。
Step 2:Phase 1 + Phase 2 RGB Split
Phase 1 就是 <BaseContent />,零特效。Phase 2 才是重頭戲:
新增 Phase2 元件:
- useCurrentFrame 拿 frame
- tick = Math.floor(frame / 3)
- 用 mulberry32 seeded random 產 6 個 jitter 值(3 軸 × xy)
- 渲染 3 份 BaseContent,分別 #ff0040 / #00ff80 / #0080ff
- 三份都 mixBlendMode: "screen"
- 三份都 translate:基礎位移 -6 / 0 / +6 + jitter
- 範圍 jx ±10、jy ±6
const Phase2: React.FC = () => {
const frame = useCurrentFrame();
const tick = Math.floor(frame / 3);
const jx1 = rand(tick * 7 + 1, -10, 10);
const jy1 = rand(tick * 7 + 2, -6, 6);
// ...jx2,jy2,jx3,jy3
return (
<AbsoluteFill style={{ backgroundColor: "#050814" }}>
<AbsoluteFill style={{ mixBlendMode: "screen", transform: `translate(${-6 + jx1}px, ${jy1}px)` }}>
<BaseContent color="#ff0040" />
</AbsoluteFill>
{/* ...綠、藍兩層同理 */}
</AbsoluteFill>
);
};為什麼不用純 #ff0000? 純紅在 screen blend 下偏橘,#ff0040 加了一點洋紅讓它更像 CRT 螢光粉、霓虹的感覺。綠藍同理,這是調色學不是技術問題。
💡
mulberry32的實作可以從 T22 直接複製過來,或用這裡的版本。重點是輸入 seed → 輸出 [0,1) 的純函式,這樣每次渲染同一 frame 結果都一樣,可以放心用 Remotion render farm。
Step 3:Phase 3 Scanline Jitter
新增 Phase3 元件:
- STRIPS = 12
- stripPct = 100 / STRIPS
- tick = Math.floor(frame / 4)
- 先畫一層乾淨的 BaseContent(底層「無位移版」)
- 再 map 12 條 AbsoluteFill:
clipPath: inset(i*stripPct% 0 (100-(i+1)*stripPct)% 0)
transform: translateX(rand(tick*31 + i*17 + 11, -40, 40))
裡面放一份 BaseContent
const Phase3: React.FC = () => {
const frame = useCurrentFrame();
const tick = Math.floor(frame / 4);
const STRIPS = 12;
const stripPct = 100 / STRIPS;
return (
<AbsoluteFill>
<BaseContent />
{Array.from({ length: STRIPS }).map((_, i) => {
const top = i * stripPct;
const bottom = 100 - (i + 1) * stripPct;
const dx = rand(tick * 31 + i * 17 + 11, -40, 40);
return (
<AbsoluteFill
key={i}
style={{
clipPath: `inset(${top}% 0 ${bottom}% 0)`,
transform: `translateX(${dx}px)`,
}}
>
<BaseContent />
</AbsoluteFill>
);
})}
</AbsoluteFill>
);
};為什麼底下要先鋪一層乾淨 BaseContent? 當 12 條都往同一方向位移時,邊緣會露出黑背景。底層作為「安全網」補那些露出的地方,看起來會比較像畫面局部撕裂而不是畫面整個破。
💡
seed = tick*31 + i*17 + 11,用質數乘法避免 hash collision,是 seeded random 的常見小技巧。
Step 4:Phase 4 Datamosh
新增 Phase4 元件:
- BLOCKS = 12
- tick = Math.floor(frame / 5)
- palette = 6 色(紅綠藍黃洋紫青)
- 底層一份 BaseContent
- map 12 個方塊:
- 隨機 x/y 位置
- 隨機寬 120~360、高 40~140
- 隨機內容位移 dx/dy ±120/±60
- 隨機是否顯示「切片」(rand > 0.4)
- 方塊用 overflow:hidden + 邊框 palette[i]
- mixBlendMode: "screen"
- 內部放一份 absolute 定位的 BaseContent,用 -x + dx 讓它「看起來是整個畫面錯位後被這個方塊框住」
{Array.from({ length: 12 }).map((_, i) => {
const seed = tick * 101 + i * 53;
const x = rand(seed + 1, 0, 1920 - 200);
const y = rand(seed + 2, 0, 1080 - 100);
const w = rand(seed + 3, 120, 360);
const h = rand(seed + 4, 40, 140);
const dx = rand(seed + 5, -120, 120);
return (
<div style={{ position: "absolute", left: x, top: y, width: w, height: h, overflow: "hidden", mixBlendMode: "screen" }}>
<div style={{ position: "absolute", left: -x + dx, top: -y, width: 1920, height: 1080 }}>
<BaseContent />
</div>
</div>
);
})}關鍵:left: -x + dx 這段。 方塊本身定位在 (x, y),內部子元素用負 x 把「大畫面」拉回原點,再加 dx 就是「錯位後的版本」。這樣每個小方塊看起來都是畫面的錯位切片,不是獨立的裝飾方塊。這是整個 datamosh 的靈魂。
Step 5:Phase 5 VHS
新增 Phase5 元件:
- wave = Math.sin(frame / 6) * 12 (水平扭曲)
- sat = 1.2 + (sin(frame/8)+1) * 0.4 (飽和度呼吸 1.2~2.0)
- 外層 filter: saturate(sat)
- 中間 BaseContent translateX(wave)
- 兩層 RGB bleed:紅 wave-4、藍 wave+4,mixBlendMode screen, opacity 0.55
- 厚 scanline overlay:repeating-linear-gradient 2px 白(0.08) + 4px 透明
const Phase5: React.FC = () => {
const frame = useCurrentFrame();
const wave = Math.sin(frame / 6) * 12;
const sat = 1.2 + (Math.sin(frame / 8) + 1) * 0.4;
return (
<AbsoluteFill style={{ filter: `saturate(${sat})` }}>
<AbsoluteFill style={{ transform: `translateX(${wave}px)` }}>
<BaseContent />
</AbsoluteFill>
<AbsoluteFill style={{ transform: `translateX(${wave - 4}px)`, mixBlendMode: "screen", opacity: 0.55 }}>
<BaseContent color="#ff0040" />
</AbsoluteFill>
{/* 藍通道同理 + scanline overlay */}
</AbsoluteFill>
);
};VHS 跟 Phase 2 RGB split 的差別? Phase 2 用高頻 seeded jitter模擬「訊號中斷」,VHS 用低頻連續 sine wave模擬「磁帶老化」。一個是數位故障、一個是類比衰減,節奏完全不一樣。
Step 6:音效層 + phase transitions
在根 composition 放 5 個 Audio Sequence:
- from 0, 20 frame: audio/t13/01-rec-beep.mp3(Phase 1 開場)
- from 60, 20 frame: audio/t13/04-cut-arrow.mp3(Phase 2 進入)
- from 120, 20 frame: audio/t13/06-cut-storm.mp3(Phase 3 撕裂)
- from 180, 20 frame: audio/t13/08-flash-snap.mp3(Phase 4 碎裂)
- from 240, 40 frame: audio/t13/06-cut-storm.mp3 volume 0.5(Phase 5 衰減)
每個 Audio 都用 <Sequence from={X} durationInFrames={Y}> 包,
這樣 Remotion 才會在正確時間 mount/unmount。
💡 音效素材在
public/audio/t13/底下——T13 音效庫是整個 Remotion Community 的共用資源,所有 tutorial 都可以引用。
為什麼 phase 邊界才上音效? 故障效果如果 2 秒內一直響,觀眾會疲勞。只在**「從 A phase 切到 B phase」的 0.5 秒窗**放音效,大腦才會把「聽到的」跟「看到的」綁成同一個事件。這是影音同步的心理學。
調整故障強度
五個參數決定故障「量」:
| 參數 | Subtle | Medium (本篇) | Intense |
|---|---|---|---|
| RGB split 基礎位移 | ±3 px | ±6 px | ±14 px |
| RGB split jitter 範圍 | ±4 px | ±10 px | ±24 px |
| RGB split tick 間隔 | 每 6 frame | 每 3 frame | 每 2 frame |
| Scanline 切片數 | 6 條 | 12 條 | 24 條 |
| Scanline 位移範圍 | ±15 px | ±40 px | ±90 px |
| Datamosh 方塊數 | 5 | 12 | 24 |
調整原則:切片數跟位移範圍不要同時拉高。24 條切片 × ±90 px 位移的結果是「完全看不出來是什麼字」——你丟了可讀性,故障美學就不成立了。我自己的經驗是:先把位移拉到極限,再慢慢減到看得出輪廓為止,那個邊界就是你這支影片的「故障量甜蜜點」。
💡 想做一個「故障漸進加強」的 phase?把上面所有數值用
interpolate(frame, [0, 60], [subtle, intense])做線性過渡就好。
進階技巧
- 加 horizontal tear:在 scanline phase 隨機挑 1~2 條切片,位移範圍從 ±40 px 爆到 ±200 px,其他維持原值。結果是「大部分畫面輕微抖動,偶爾一條狂甩」——這是真實 CRT 訊號中斷的特徵。用
if (rand(seed, 0, 1) > 0.85) dx *= 5即可。 - 加 color bleed:在最外層包
filter: hue-rotate(${frame % 360}deg),讓色相緩慢旋轉,配合 RGB split 會產生「訊號發散」的感覺。注意hue-rotate對黑白沒作用,底層要先有顏色(Phase 2 最適合)。 - 加 static noise:用 SVG
<feTurbulence baseFrequency="0.9" />+<feColorMatrix>產生灰階雜訊,外層mixBlendMode: "overlay"。overlay 會保留底層明暗但注入雜訊質感,比screen自然很多。雜訊 seed 一樣用tick = floor(frame/3)階梯式。
FAQ
Q:mix-blend-mode 在 Safari 渲染有問題嗎?
A:Safari 支援很好,但有一個陷阱:父層若是預設 isolation: auto,blend mode 會穿透到更上層,結果 Phase 2 的紅綠藍跟 Vignette 混在一起顏色全錯。解法是在 Phase 2 最外層加 isolation: "isolate" 強制建立 stacking context。Remotion render(headless Chromium)跟 Chrome 行為一致,通常只在 Safari preview 才踩到。
Q:為什麼 clip-path 沒有吃我的 transition?
A:clip-path 的值必須形狀函式一樣才能 interpolate。inset(0% 0 50% 0) → inset(0% 0 30% 0) 可以,但 inset() → circle() 不行。Remotion 裡我們不用 CSS transition(每幀都重新 render),這個限制影響不大,但記得不要混用 clip-path 形狀函式。
Q:想做更極端的故障效果,CSS 做得到嗎還是要上 shader?
A:真正的 displacement map、feedback loop(前一幀當本幀輸入)、2D convolution——這些 CSS 做不到,要上 GLSL。但 80% 的「故障 YouTube 片頭」需求 CSS 都能搞定。判斷分界:效果可以「分層」思考就用 CSS,需要「逐像素採樣」就上 shader。Remotion 支援 @remotion/shaders(beta)或直接寫 <canvas> WebGL。
Q:故障效果會影響無障礙閱讀嗎? A:會。高頻閃爍可能觸發光敏性癲癇,WCAG 2.1 規定每秒閃爍不能超過 3 次且不能占大面積。本篇的 seeded jitter 是「位移」不是「閃爍」(亮度變化小),相對安全。商業作品建議:亮度對比變化 < 10%、故障 phase 不超過 2 秒、關鍵字幕放在不受故障影響的靜止區域。
本篇涵蓋的官方文件
- /docs/use-current-frame — 每幀取得當前 frame
- /docs/interpolate — 數值插值(漸進式故障會用到)
- /docs/sequence — 分段 mount/unmount 元件
- MDN: clip-path —
inset()等形狀函式 - MDN: mix-blend-mode — screen / multiply / overlay 差異
- MDN: filter —
saturate/hue-rotate
下一步
- T22 Kinetic Typography:文字排版動力學 — seeded random 的完整玩法
- T23 Particles:粒子系統與物理 — 把 seeded random 延伸到 2D 空間
- T22 Motion Blur + Film Noise:電影感後製層 — 本篇的「類比化」延伸,把故障美學推向底片質感
故障做爛的臨界點很微妙——太少像 bug、太多像壞掉——歡迎到 FB 社群 丟你的 demo 請大家 review 強度!