程序化漸層背景:Mesh Gradient、Conic Rotation、SVG Turbulence 三種技法
不用 WebGL 也能做出高級感背景。這篇示範三種技法:4 顆 radial gradient 模糊融合做 mesh gradient、CSS conic-gradient 720° 旋轉、SVG feTurbulence 做雲霧感。純 CSS + SVG,0 依賴。
成品預覽
這是一支 10 秒的背景技法示範,被拆成三段(每段 100 frames,銜接處 10 frame crossfade):
- 0~100f:Mesh Gradient — 4 顆 radial blobs 慢速漂移,
filter: blur(80px)融合 - 100~200f:Conic Rotation —
conic-gradient的from角度從 0° 旋到 720°,外層疊 vignette - 200~300f:SVG Turbulence —
<feTurbulence>每幀換 seed,用feColorMatrix調成紫藍雲霧
三種技法都只用瀏覽器內建功能,0 個外部套件、0 張圖片、0 行 WebGL。
這篇會做出什麼
一支 10 秒的程序化背景示範片,展示三種可以直接當成任何影片背景的漸層技法。特點:
- 純 CSS + SVG:沒有 canvas、沒有 shader、沒有
@react-three/fiber - Remotion 原生友善:每幀可以由
useCurrentFrame()驅動參數 - 輕量:bundle 不會因此變大,渲染也比 WebGL 穩
- 可複用:抽成三個元件後,隨時
<MeshGradientBG>/<ConicBG>/<TurbulenceBG>就能套
做完後你會得到三個可重複使用的背景元件,以後做任何 explainer、intro、音樂視覺化都能直接套。
前置知識
- T01 Remotion 入門 — 熟悉
<Composition>、useCurrentFrame() - T05 interpolate 動畫基礎 — 知道怎麼用
interpolate做時間對應
這篇假設你會 React + 基本 CSS(知道什麼是 radial-gradient、filter、mix-blend-mode)。如果這些字完全沒聽過,先去補 MDN 的 CSS Gradients 再回來。
原理解析:為什麼漸層背景值得一整篇教學
漸層看起來很基礎,但做得好跟做得爛差距非常大。「一張單色底 + 一點光暈」跟「Apple 官網那種會呼吸的 mesh」之間,差的不是顏色挑得好不好,而是對瀏覽器渲染管線的理解。這節先把三種技法背後的原理講清楚,你寫 code 時才知道每個數字為什麼是那個數字。
Mesh gradient 的歷史與近似
真正的 mesh gradient 最早出現在 Adobe Illustrator,後來 SVG 2 草案加進去(但瀏覽器支援到現在還是半殘)。它的數學定義是:一張四邊形網格,每個頂點有顏色,格子內部用色彩空間插值(通常是 Lab 或 OKLCH)填色。結果會有那種「顏色之間自然過渡、不會出現灰泥巴」的質感。
現代 CSS 沒有原生 mesh gradient,但有個廉價替代方案:疊四層 radial-gradient(),每層一個顏色,再套 filter: blur(80px)。模糊會把邊緣融成一片,看起來就像 mesh。代價是什麼?插值發生在 screen space 而不是 color space,所以如果你用互補色(紅綠、藍橘)會出現灰泥巴過渡區。解法:選同色溫的顏色(紫、粉、青、琥珀)或加 saturate(1.4) 補飽和度。
// 核心概念:4 顆 blob + blur
<div style={{ filter: "blur(80px) saturate(1.4)" }}>
{blobs.map((b) => (
<div style={{
background: `radial-gradient(circle, ${b.color}, transparent 65%)`,
mixBlendMode: "screen",
}} />
))}
</div>對 99% 的影片背景來說,這個近似已經夠用了。
Conic-gradient:被低估的 CSS 屬性
conic-gradient 是 CSS Images Level 4 的功能,語法是 conic-gradient(from <angle> at <pos>, color1 <stop>, color2 <stop>, ...)。它從一個中心點往外放射,顏色沿著圓周排列(不是半徑),結果會是一塊像切披薩的色盤。
它被低估有兩個原因:一是大家誤以為它只能做圓餅圖,二是早期瀏覽器支援不好。但 2021 之後 Chrome 69+、Safari 12.1+、Firefox 83+ 全部支援,現在拿來做背景完全沒問題。
最好玩的用法:把 from 角度每幀加 1 度,整塊顏色會以中心為軸旋轉,配上 vignette 遮掉中心,就是高級感十足的動態背景。注意一點:conic-gradient 的顏色切換會有鋸齒(因為是硬邊),想要柔和就加一層 filter: blur(40px),或用 vignette 蓋掉邊緣。
background: `conic-gradient(from ${angle}deg at 50% 50%,
#5b21b6 0deg, #ec4899 60deg, #f59e0b 120deg,
#06b6d4 180deg, #22c55e 240deg, #3b82f6 300deg, #5b21b6 360deg)`SVG feTurbulence:瀏覽器內建的 Perlin noise
這個是老東西了。SVG 1.1 就有 <feTurbulence>,本質是一個 Perlin-style noise generator,跟遊戲引擎裡做雲霧、地形用的是同一家族。三個關鍵參數:
baseFrequency:控制噪點尺度。0.005 是很大片的雲,0.05 是細密顆粒。seed:整數,換 seed 圖案就整個洗牌 — 所以我們每幀seed={frame}就是動畫!numOctaves:疊幾層不同頻率的 noise,越多越細節(但越慢)。通常 3~5。
原始 turbulence 是灰階的,要上色就串接 <feColorMatrix>,用一個 4×5 矩陣把 RGBA 重新映射。對 Remotion 特別友善:因為每幀 SVG 會被重新光柵化,所以 seed={frame} 每幀不同就自動變成動畫,不需要 @keyframes。
<filter id="turb">
<feTurbulence type="fractalNoise" baseFrequency={0.01} seed={frame} numOctaves={4} />
<feColorMatrix values="0.2 0 0.6 0 0.05 0.1 0.1 0.7 0 0.05 0.5 0.2 0.9 0 0.15 0 0 0 0 1" />
</filter>Step 1:建立 composition 與 crossfade 結構
Claude Code:
新增 Composition "GradientDemo":
- 1920x1080, fps 30, durationInFrames 300(10 秒)
- 新建 src/compositions/GradientDemo.tsx
- 在 Root.tsx 註冊
元件結構:
1. 三個 <AbsoluteFill> 疊放,各自放 MeshGradientSection / ConicRotationSection / TurbulenceSection
2. 用 interpolate 計算 opacity1/2/3,在 frame 90-100 和 190-200 做 10 幀 crossfade
3. 底下再疊一層 <ProgressBar />
const opacity1 = interpolate(frame, [90, 100], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
const opacity2 = interpolate(frame, [90, 100, 190, 200], [0, 1, 1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
const opacity3 = interpolate(frame, [190, 200], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });為什麼用 opacity 疊加而不是 <Sequence> 切換? 因為 <Sequence> 是硬切,crossfade 需要兩層同時存在 10 幀。三層 <AbsoluteFill> 用 opacity 控是最直接的做法。
💡
<Sequence>vs opacity crossfade 的取捨:段落完全獨立用 Sequence,有淡入淡出就用 opacity。
Step 2:Mesh Gradient — 4 顆 blob + blur
Claude Code:
新增 MeshGradientSection 元件:
1. 4 顆 blob,顏色用 #5b21b6 / #ec4899 / #06b6d4 / #f59e0b
2. 每顆位置用 Math.sin(t * 0.6 + phase) 做軌道漂移
3. 外層 div 加 filter: "blur(80px) saturate(1.4)"
4. 每顆 blob 用 radial-gradient + mixBlendMode: "screen"
5. 整層 transform rotate 12deg 做緩慢旋轉
const blobs = [
{ color: "#5b21b6", cx: 25 + Math.sin(t * 0.6) * 18, cy: 30 + Math.cos(t * 0.5) * 14 },
{ color: "#ec4899", cx: 75 + Math.sin(t * 0.4 + 1.5) * 16, cy: 28 + Math.cos(t * 0.7 + 0.8) * 12 },
{ color: "#06b6d4", cx: 28 + Math.cos(t * 0.5 + 2.0) * 18, cy: 72 + Math.sin(t * 0.6 + 1.2) * 14 },
{ color: "#f59e0b", cx: 72 + Math.cos(t * 0.7 + 0.4) * 16, cy: 74 + Math.sin(t * 0.5 + 2.3) * 12 },
];
return (
<AbsoluteFill style={{ backgroundColor: "#0b0820", overflow: "hidden" }}>
<div style={{ position: "absolute", inset: -200, filter: "blur(80px) saturate(1.4)" }}>
{blobs.map((b, i) => (
<div key={i} style={{
position: "absolute", left: `${b.cx}%`, top: `${b.cy}%`,
width: 1100, height: 1100, transform: "translate(-50%, -50%)",
background: `radial-gradient(circle, ${b.color} 0%, ${b.color}cc 25%, transparent 65%)`,
mixBlendMode: "screen",
}} />
))}
</div>
</AbsoluteFill>
);inset: -200 是關鍵:因為 blur(80px) 會讓邊緣變透明,如果 blob 層不比 viewport 大,邊角會糊出黑色。往外撐 200px 就蓋住了。
💡
mixBlendMode: "screen"讓顏色相加而不是覆蓋,這就是多色 blob 能融成連續漸層的魔法。
Step 3:Conic Rotation — 旋轉 + vignette
Claude Code:
新增 ConicRotationSection:
1. 用 interpolate 把 local frame (0-100) 對應到 angle (0-720 度)
2. 底層 AbsoluteFill 套 conic-gradient(from ${angle}deg, ...)
3. 疊第二層:中心亮外圍透 → 壓低中心飽和
4. 疊第三層:中心透外圍暗 → 做 vignette
const local = frame - 100;
const angle = interpolate(local, [0, 100], [0, 720]);
const stops = [
"#5b21b6 0deg", "#ec4899 60deg", "#f59e0b 120deg",
"#06b6d4 180deg", "#22c55e 240deg", "#3b82f6 300deg", "#5b21b6 360deg",
].join(", ");
return (
<AbsoluteFill style={{ backgroundColor: "#000" }}>
<AbsoluteFill style={{ background: `conic-gradient(from ${angle}deg at 50% 50%, ${stops})` }} />
<AbsoluteFill style={{
background: "radial-gradient(circle at 50% 50%, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.55) 25%, rgba(0,0,0,0) 70%)",
}} />
<AbsoluteFill style={{
background: "radial-gradient(circle at 50% 50%, transparent 55%, rgba(0,0,0,0.7) 100%)",
}} />
</AbsoluteFill>
);轉 720° 而不是 360°:100 幀(3.3 秒)轉一圈太慢、觀眾幾乎看不出在動。轉兩圈比較明顯,也剛好在 section 結尾回到原點。
雙層 vignette:第一層壓中心避免色相太亮爆掉,第二層壓邊緣做聚焦效果。沒有 vignette 的 conic gradient 會像萬花筒玩具,很廉價。
Step 4:SVG Turbulence — 動態 baseFrequency + seed + 調色
Claude Code:
新增 TurbulenceSection:
1. 用 interpolate 把 local frame (0-100) 對應到 baseFrequency 0.005 → 0.02
2. seed 直接用 frame(每幀洗牌 = 雲在流動)
3. numOctaves={4}
4. feColorMatrix 用 4x5 矩陣把灰階 noise 映射到紫藍色系
5. 疊一個 radialGradient 當 vignette
const local = frame - 200;
const baseFrequency = interpolate(local, [0, 100], [0.005, 0.02]);
return (
<AbsoluteFill style={{ backgroundColor: "#0b1029" }}>
<svg width="1920" height="1080" viewBox="0 0 1920 1080">
<defs>
<filter id="turb">
<feTurbulence type="fractalNoise" baseFrequency={baseFrequency} numOctaves={4} seed={frame} />
<feColorMatrix type="matrix" values="
0.2 0.0 0.6 0 0.05
0.1 0.1 0.7 0 0.05
0.5 0.2 0.9 0 0.15
0 0 0 0 1" />
</filter>
</defs>
<rect width="1920" height="1080" filter="url(#turb)" />
</svg>
</AbsoluteFill>
);baseFrequency 慢慢加大:一開始是大片雲(0.005),漸漸變細密顆粒(0.02),有一種「鏡頭拉近」的感覺,不用真的改 viewBox。
seed= 就是動畫:其他 SVG 濾鏡做動畫要 <animate> 或 SMIL,但 Remotion 每幀重新光柵化,所以只要每幀算出新的 seed 就自動動了。這是 Remotion 特別適合 SVG 的原因。
💡
feColorMatrix矩陣是 4×5:前四欄是 RGBA 的係數,第五欄是 bias(加的常數)。想做不同色溫改第五欄就好。
Step 5:加 label 與 progress bar UI
Claude Code:
加 UI overlay:
1. Label 元件:section 標題 + 副標(monospace 字體展示 CSS 語法)
- 淡入淡出(前 12 幀、後 12 幀)
- 同時 translateY(-30 → 0) 滑入
2. ProgressBar:底部 8px 高橫條,背景白色 18%,填色用 linear-gradient 四色
3. 三個 section 各用 <Sequence from={start} durationInFrames={100}> 包 Label
const opacity = interpolate(local, [0, 12, length - 12, length], [0, 1, 1, 0], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
});
const translateY = interpolate(local, [0, 18], [-30, 0], { extrapolateRight: "clamp" });為什麼 ProgressBar 不包在 Sequence 裡? 它需要跨整支片的 frame 才能算對應比例,包 Sequence 會讓 useCurrentFrame() 從 0 開始。這類「全片全域」的元件就留在根層。
Step 6:加音效(whoosh 切換 + outro bell)
Claude Code:
加音效層:
1. Sequence from={0} durationInFrames={30}:開場 cut-air(淡入感)
2. Sequence from={95} durationInFrames={20}:第一次切換 whoosh
3. Sequence from={195} durationInFrames={20}:第二次切換 whoosh
4. Sequence from={270} durationInFrames={30}:結尾 bell
每個 <Audio volume={0.6~0.7}>
<Sequence from={95} durationInFrames={20}>
<Audio src={staticFile("audio/whoosh.mp3")} volume={0.7} />
</Sequence>
<Sequence from={270} durationInFrames={30}>
<Audio src={staticFile("audio/outro-bell.mp3")} volume={0.6} />
</Sequence>音效放在 crossfade 前 5 frame:95 而不是 100,因為 whoosh 前半段是「起音」,要在畫面開始轉換之前就啟動,聽感才對。聽覺先行、視覺跟上是影片剪輯的基本功。
Browser 支援與渲染成本
| 技法 | 瀏覽器支援 | 渲染成本 | 備註 |
|---|---|---|---|
conic-gradient | Chrome 69+、Safari 12.1+、Firefox 83+ | 低 | Chromium 是 GPU 光柵化,very fast |
filter: blur(80px) | 全支援 | 中到高 | blur 半徑越大越慢,mobile 會卡 |
<feTurbulence> | 全支援 | 高 | 每幀重算 noise,1920×1080 時每幀約 30-80ms |
Remotion 渲染是 headless Chromium,所以所有技法都會用到 GPU(如果 machine 有)。最燒的是 feTurbulence:numOctaves 每加一層成本翻倍。想省時間先把 numOctaves 調到 3、baseFrequency 上限不要超過 0.03。blur() 則是 radius 的平方級複雜度,80px 是 Remotion 還能接受的上限,超過 120px 就會明顯拖慢。
延伸:自訂調色盤
把顏色 array 抽出來,就能一鍵換主題:
const PALETTES = {
sunset: ["#f59e0b", "#ec4899", "#8b5cf6", "#0ea5e9"],
ocean: ["#0c4a6e", "#06b6d4", "#67e8f9", "#a5f3fc"],
neon: ["#ff006e", "#fb5607", "#ffbe0b", "#8338ec"],
pastel: ["#fecaca", "#fed7aa", "#bbf7d0", "#bfdbfe"],
};怎麼挑一組好看的 palette?在 HSL 色輪上選等距角度:4 個顏色就是 90 度一組(互補+分割互補)、3 個就是 120 度(三等分色)。手算不方便,可以用 culori.js 或 chroma.js 幫你在 OKLCH 色彩空間轉換。
import chroma from "chroma-js";
const palette = chroma.scale(["#5b21b6", "#f59e0b"]).mode("lch").colors(4);為什麼用 LCH/OKLCH 而不是 HSL? HSL 的亮度其實不均勻(一樣 L=50%,藍色看起來比黃色暗得多),LCH 是感知均勻的,插出來的中間色不會出現「泥色」。這跟前面 mesh gradient 的問題是同一件事。
FAQ
Q:feTurbulence 每幀重算 seed 會不會很慢?
A:會,但在可接受範圍。1920×1080、numOctaves={4} 下大約每幀 40-60ms(M1 Mac)。如果你只要「氛圍」不要「流動感」,可以固定 seed 不動,改成動畫 baseFrequency 或疊 <feDisplacementMap> 做流動。另一個省時方案:把 SVG 畫在 960×540 再用 CSS transform: scale(2) 拉大,noise 運算只剩 1/4。
Q:conic-gradient 在 iOS Safari 15 以下會怎樣?
A:Safari 12.1+ 就支援了,所以 iOS 15 完全沒問題。真的要管到 iOS 12 以下,降級方案是改用 linear-gradient + transform: rotate()(視覺上差很多就是)。但因為 Remotion 渲染是 Chromium,實際輸出的 mp4 檔不受觀眾瀏覽器影響,只有做 Player 嵌網頁才要擔心。
Q:可以做真正的 mesh gradient(color-space interpolation)嗎?
A:可以,但要靠 <canvas> 自己畫。核心做法:把 4 個頂點顏色轉到 OKLCH,然後對每個像素做雙線性插值再轉回 RGB,putImageData 寫進 canvas。成本很高(1920×1080 純 JS 要 200ms+),Remotion 裡實務上比較少人這樣搞。99% 情況下 CSS 近似已經夠。想試的話看 Kevin Powell 的 CSS mesh gradient 影片 或 mesh-gradient npm 套件。
Q:blur(80px) 在 mobile 渲染很慢怎麼辦?
A:Remotion 渲染是在你的 Mac/Linux/CI 機器上跑 headless Chromium,跟觀眾看影片的裝置無關。所以 blur 大小對「渲染時間」有影響,但對「觀眾裝置播放」沒影響(mp4 是固定像素)。如果你是用 <Player> 嵌網頁 realtime 預覽,就要考慮降 blur 到 40px、或把 blob 層做小再 scale。
本篇涵蓋的官方文件
- /docs/the-fundamentals — Remotion 的時間與 composition 基礎
- /docs/animating-properties — 用
useCurrentFrame驅動樣式
外部參考:
- MDN:
conic-gradient() - MDN:
radial-gradient() - MDN:
filter: blur() - MDN:
<feTurbulence> - MDN:
<feColorMatrix>
下一步
把背景做好了,下一步就是把前景也做出高級感:
- T22:Kinetic Typography 動態字排 — 文字本身當主角的動畫
- T23:Particles 粒子系統 — 用純 React 做 1000 顆粒子(不用 canvas)
- T22:Motion Blur + Noise 做出 35mm 底片感 — 把今天的漸層背景再套 film grain
有做出漂亮的 palette 歡迎到 FB 社群 貼圖分享!