次像素渲染問題
說明 Chrome 次像素渲染如何影響 Remotion 動畫的平滑度,以及如何使用 transform 和 perspective() 修復文字動畫的抖動問題
次像素渲染問題
什麼是次像素渲染
Chrome 預設會將文字對齊到最接近的像素。這意味著 margin-top: 10px 和 margin-top: 10.4px 渲染出的效果完全相同。
這種行為會導致振盪效果,使您的動畫看起來不夠平滑,甚至出現抖動感。
問題範例
考慮以下場景:您想要文字從位置 0 平滑地移動到位置 50px,整個動畫持續 200 幀:
// ❌ 這種寫法可能出現抖動
import { interpolate, useCurrentFrame } from "remotion";
export const JitteringText = () => {
const frame = useCurrentFrame();
return (
<div
style={{
marginTop: interpolate(frame, [0, 200], [0, 50]),
// Chrome 會將小數像素值四捨五入到最近的整數
// 導致動畫不連貫,出現跳動感
}}
>
hi there
</div>
);
};在此範例中,當 interpolate 產生如 10.4px、10.6px 這樣的值時,它們都會被渲染為 10px 或 11px,導致動畫在某些幀之間「跳格」。
解決方案:使用 transform 屬性
使用 transform 屬性移動元素,Chrome 對 transform 使用次像素精度渲染:
// ✅ 使用 transform,實現平滑動畫
import { interpolate, useCurrentFrame } from "remotion";
export const SmoothText = () => {
const frame = useCurrentFrame();
return (
<div
style={{
transform: `translateY(${interpolate(frame, [0, 200], [0, 50])}px)`,
// transform 使用次像素精度,動畫更平滑
}}
>
hi there
</div>
);
};進一步最佳化:加入 perspective() 和 willChange
在 transform 中加入 perspective() 並設定 willChange: "transform" 可以讓次像素渲染更加精確:
// ✅✅ 最佳寫法:加入 perspective 和 willChange
import { interpolate, useCurrentFrame } from "remotion";
export const OptimizedText = () => {
const frame = useCurrentFrame();
return (
<div
style={{
transform: `perspective(100px) translateY(${interpolate(
frame,
[0, 200],
[0, 50]
)}px)`,
willChange: "transform",
// perspective() 觸發 GPU 加速渲染
// willChange 提示瀏覽器提前進行最佳化
}}
>
hi there
</div>
);
};200x100 視訊的視覺差異
官方文件展示了一個 200x100 像素的視訊對比:
左側(有抖動):
<div
style={{
transform: `translateY(${interpolate(frame, [0, 200], [0, 50])}px)`,
}}
>
hi there
</div>右側(平滑):
<div
style={{
transform: `perspective(100px) translateY(${interpolate(
frame,
[0, 200],
[0, 50]
)}px)`,
willChange: "transform",
}}
>
hi there
</div>使用 perspective() 和 willChange: "transform" 的版本動畫明顯更加平滑。
適用場景和注意事項
適合使用此最佳化的情況
- 包含緩慢移動文字的動畫
- 精細的位置動畫(移動距離很小)
- 高品質輸出要求
需要注意的地方
過多使用 willChange: "transform" 會消耗更多系統資源,因為它強制瀏覽器為每個元素建立獨立的合成層(compositing layer)。
建議的做法:
// 僅在渲染時啟用此最佳化
const IS_RENDERING = process.env.NODE_ENV === "production";
<div
style={{
transform: `perspective(100px) translateY(${y}px)`,
willChange: IS_RENDERING ? "transform" : "auto",
}}
>
hi there
</div>其他受次像素渲染影響的屬性
除了位置(top、left、margin)之外,以下屬性也受到次像素問題的影響:
| 屬性 | 是否受影響 | 建議替代方案 |
|---|---|---|
top / left | 是 | transform: translateX/Y |
margin | 是 | transform: translateX/Y |
width / height | 是 | transform: scale() |
font-size | 是 | transform: scale() |
transform: translateX/Y | 否(已使用次像素精度) | — |
完整動畫範例
import { interpolate, useCurrentFrame } from "remotion";
export const SubpixelDemo = () => {
const frame = useCurrentFrame();
const yOffset = interpolate(frame, [0, 120], [0, 100], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div style={{ position: "relative", width: 400, height: 300 }}>
{/* 啟用次像素最佳化的文字 */}
<p
style={{
transform: `perspective(100px) translateY(${yOffset}px)`,
willChange: "transform",
fontSize: 24,
color: "white",
}}
>
平滑移動的文字
</p>
</div>
);
};