Skip to content

Instantly share code, notes, and snippets.

@chan-ume
Created February 15, 2026 06:46
Show Gist options
  • Select an option

  • Save chan-ume/c7f2ab424a1b273fac195d1b143a8701 to your computer and use it in GitHub Desktop.

Select an option

Save chan-ume/c7f2ab424a1b273fac195d1b143a8701 to your computer and use it in GitHub Desktop.
Remotion の Spring と interpolate を使った例
// https://www.randpy.tokyo/entry/remotion-3-animation
import {
AbsoluteFill,
interpolate,
Sequence,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { z } from "zod";
import { zColor } from "@remotion/zod-types";
export const animationDemoSchema = z.object({
backgroundColor: zColor(),
accentColor: zColor(),
});
// --- Phase 1: interpolate ショーケース ---
const InterpolateShowcase: React.FC = () => {
const frame = useCurrentFrame();
// 4つの interpolate パターン
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: "clamp",
});
const translateX = interpolate(frame, [0, 40], [-400, 0], {
extrapolateRight: "clamp",
});
const rotate = interpolate(frame, [0, 60], [0, 360]);
const scale = interpolate(frame, [0, 50], [0.2, 1], {
extrapolateRight: "clamp",
});
// clamp あり・なしの比較
const withoutClamp = interpolate(frame, [0, 30], [0, 100]);
const withClamp = interpolate(frame, [0, 30], [0, 100], {
extrapolateRight: "clamp",
});
const labelStyle: React.CSSProperties = {
color: "#666",
fontSize: 20,
fontFamily: "monospace",
marginTop: 8,
textAlign: "center",
};
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "#fafafa",
}}
>
{/* タイトル */}
<div
style={{
position: "absolute",
top: 60,
fontSize: 64,
fontWeight: "bold",
fontFamily: "sans-serif",
color: "#333",
}}
>
interpolate()
</div>
{/* 4つのデモ */}
<div
style={{
display: "flex",
gap: 80,
alignItems: "center",
marginTop: 20,
}}
>
{/* opacity */}
<div style={{ textAlign: "center" }}>
<div
style={{
width: 120,
height: 120,
backgroundColor: "#3498db",
borderRadius: 12,
opacity,
}}
/>
<div style={labelStyle}>opacity</div>
</div>
{/* translateX */}
<div style={{ textAlign: "center" }}>
<div
style={{
width: 120,
height: 120,
backgroundColor: "#e74c3c",
borderRadius: 12,
transform: `translateX(${translateX}px)`,
}}
/>
<div style={labelStyle}>translateX</div>
</div>
{/* rotate */}
<div style={{ textAlign: "center" }}>
<div
style={{
width: 120,
height: 120,
backgroundColor: "#2ecc71",
borderRadius: 12,
transform: `rotate(${rotate}deg)`,
}}
/>
<div style={labelStyle}>rotate</div>
</div>
{/* scale */}
<div style={{ textAlign: "center" }}>
<div
style={{
width: 120,
height: 120,
backgroundColor: "#9b59b6",
borderRadius: 12,
transform: `scale(${scale})`,
}}
/>
<div style={labelStyle}>scale</div>
</div>
</div>
{/* clamp 比較 */}
<div
style={{
position: "absolute",
bottom: 120,
display: "flex",
gap: 60,
alignItems: "flex-end",
}}
>
<div style={{ textAlign: "center" }}>
<div
style={{
width: Math.max(0, withoutClamp) * 3,
height: 30,
backgroundColor: "#e67e22",
borderRadius: 4,
transition: "width 0.05s",
maxWidth: 600,
}}
/>
<div style={{ ...labelStyle, color: "#e67e22" }}>
clamp なし: {Math.round(withoutClamp)}
</div>
</div>
<div style={{ textAlign: "center" }}>
<div
style={{
width: withClamp * 3,
height: 30,
backgroundColor: "#27ae60",
borderRadius: 4,
}}
/>
<div style={{ ...labelStyle, color: "#27ae60" }}>
clamp あり: {Math.round(withClamp)}
</div>
</div>
</div>
</AbsoluteFill>
);
};
// --- Phase 2: spring ショーケース ---
const SpringShowcase: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const configs = [
{ label: "damping: 10", config: { damping: 10 }, color: "#e74c3c" },
{ label: "damping: 100", config: { damping: 100 }, color: "#3498db" },
{
label: "mass: 5, stiffness: 200",
config: { mass: 5, stiffness: 200 },
color: "#2ecc71",
},
];
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "#fafafa",
}}
>
{/* タイトル */}
<div
style={{
position: "absolute",
top: 60,
fontSize: 64,
fontWeight: "bold",
fontFamily: "sans-serif",
color: "#333",
}}
>
spring()
</div>
<div style={{ display: "flex", gap: 160, alignItems: "center" }}>
{configs.map((item) => {
const value = spring({ frame, fps, config: item.config });
return (
<div key={item.label} style={{ textAlign: "center" }}>
<div
style={{
width: 180,
height: 180,
backgroundColor: item.color,
borderRadius: "50%",
transform: `scale(${value})`,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<span
style={{
color: "white",
fontSize: 32,
fontWeight: "bold",
fontFamily: "monospace",
}}
>
{value.toFixed(2)}
</span>
</div>
<div
style={{
color: item.color,
fontSize: 22,
fontFamily: "monospace",
marginTop: 16,
fontWeight: "bold",
}}
>
{item.label}
</div>
</div>
);
})}
</div>
<div
style={{
position: "absolute",
bottom: 80,
fontSize: 28,
fontFamily: "sans-serif",
color: "#888",
}}
>
spring は 0→1 を物理シミュレーションで生成する
</div>
</AbsoluteFill>
);
};
// --- Phase 3: spring + interpolate 組み合わせ ---
const CombinedShowcase: React.FC<{ accentColor: string }> = ({
accentColor,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// ① spring で 0→1 のバネ曲線
const progress = spring({
frame: frame - 10,
fps,
config: { damping: 100 },
});
// ② interpolate で 0→1 を 0→-200px にマッピング
const translateY = interpolate(progress, [0, 1], [0, -200]);
// 単語ごとのスタガーアニメーション(Title.tsx パターン)
const words = ["spring()", "→", "interpolate()", "→", "CSS"];
return (
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
backgroundColor: "#fafafa",
}}
>
{/* タイトル */}
<div
style={{
position: "absolute",
top: 60,
fontSize: 56,
fontWeight: "bold",
fontFamily: "sans-serif",
color: "#333",
}}
>
spring() + interpolate()
</div>
{/* 2段構成デモ:円が上に移動 */}
<div
style={{
display: "flex",
gap: 60,
alignItems: "center",
marginBottom: 40,
}}
>
{/* spring 値の表示 */}
<div style={{ textAlign: "center" }}>
<div
style={{
fontSize: 24,
fontFamily: "monospace",
color: "#888",
marginBottom: 8,
}}
>
progress: {progress.toFixed(2)}
</div>
<div
style={{
width: 200,
height: 8,
backgroundColor: "#ddd",
borderRadius: 4,
}}
>
<div
style={{
width: `${progress * 100}%`,
height: 8,
backgroundColor: accentColor,
borderRadius: 4,
}}
/>
</div>
</div>
<div style={{ fontSize: 40, color: "#ccc" }}>→</div>
{/* translateY 値の表示 */}
<div style={{ textAlign: "center" }}>
<div
style={{
fontSize: 24,
fontFamily: "monospace",
color: "#888",
marginBottom: 8,
}}
>
translateY: {Math.round(translateY)}px
</div>
<div
style={{
width: 120,
height: 120,
backgroundColor: accentColor,
borderRadius: "50%",
transform: `translateY(${translateY}px)`,
}}
/>
</div>
</div>
{/* 単語スタガーアニメーション */}
<div
style={{
position: "absolute",
bottom: 100,
display: "flex",
gap: 16,
}}
>
{words.map((w, i) => {
const delay = i * 5;
const wordScale = spring({
fps,
frame: frame - delay - 15,
config: { damping: 200 },
});
return (
<span
key={`${w}-${i}`}
style={{
fontSize: 48,
fontWeight: "bold",
fontFamily: "monospace",
color: w === "→" ? "#ccc" : "#333",
transform: `scale(${wordScale})`,
display: "inline-block",
}}
>
{w}
</span>
);
})}
</div>
</AbsoluteFill>
);
};
// --- メインコンポーネント ---
export const AnimationDemo: React.FC<
z.infer<typeof animationDemoSchema>
> = ({ accentColor }) => {
return (
<AbsoluteFill>
<Sequence from={0} durationInFrames={90}>
<InterpolateShowcase />
</Sequence>
<Sequence from={90} durationInFrames={110}>
<SpringShowcase />
</Sequence>
<Sequence from={200} durationInFrames={100}>
<CombinedShowcase accentColor={accentColor} />
</Sequence>
</AbsoluteFill>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment