Remotion LabRemotion Lab
視覺特效程序化漸層背景:Mesh Gradient、Conic Rotation、SVG Turbulence 三種技法
gradientsvgcssfilterturbulenceintermediate

程序化漸層背景: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):

  1. 0~100f:Mesh Gradient — 4 顆 radial blobs 慢速漂移,filter: blur(80px) 融合
  2. 100~200f:Conic Rotation — conic-gradientfrom 角度從 0° 旋到 720°,外層疊 vignette
  3. 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、音樂視覺化都能直接套。


前置知識

這篇假設你會 React + 基本 CSS(知道什麼是 radial-gradientfiltermix-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-gradientChrome 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.jschroma.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。


本篇涵蓋的官方文件

外部參考:


下一步

把背景做好了,下一步就是把前景也做出高級感:

有做出漂亮的 palette 歡迎到 FB 社群 貼圖分享!