Created
February 15, 2026 06:46
-
-
Save chan-ume/c7f2ab424a1b273fac195d1b143a8701 to your computer and use it in GitHub Desktop.
Remotion の Spring と interpolate を使った例
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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