Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save chan-ume/b75ea8bcd1999494eea4996635a34477 to your computer and use it in GitHub Desktop.
Remotion でテロップをつける様々な実装パターン
// https://www.randpy.tokyo/entry/remotion-5-telop
import {
AbsoluteFill,
interpolate,
Sequence,
useCurrentFrame,
} from "remotion";
import { z } from "zod";
import { zColor } from "@remotion/zod-types";
import { FadeInOutCaption } from "./CaptionsDemo/FadeInOutCaption";
import { ScaleInCaption } from "./CaptionsDemo/ScaleInCaption";
import { TypewriterCaption } from "./CaptionsDemo/TypewriterCaption";
const captionDataSchema = z.object({
text: z.string(),
from: z.number(),
duration: z.number(),
});
export const captionsDemoSchema = z.object({
backgroundColor: zColor(),
captions: z.array(captionDataSchema),
});
// --- Phase 1: テロップスタイル3種 ---
const StyleShowcase: React.FC = () => {
const frame = useCurrentFrame();
const styles = [
{
label: "半透明背景",
style: {
color: "white",
fontSize: 48,
backgroundColor: "rgba(0, 0, 0, 0.6)",
padding: "12px 24px",
borderRadius: 8,
} as React.CSSProperties,
},
{
label: "縁取り文字",
style: {
color: "white",
fontSize: 48,
WebkitTextStroke: "2px black",
} as React.CSSProperties,
},
{
label: "色付き強調",
style: {
color: "white",
fontSize: 48,
backgroundColor: "#e74c3c",
padding: "8px 32px",
borderRadius: 4,
fontWeight: "bold",
} as React.CSSProperties,
},
];
return (
<AbsoluteFill
style={{
backgroundColor: "#1a1a2e",
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
position: "absolute",
top: 60,
fontSize: 56,
fontWeight: "bold",
color: "white",
fontFamily: "sans-serif",
}}
>
テロップスタイル
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 50,
alignItems: "center",
}}
>
{styles.map((item, i) => {
const delay = i * 15;
const opacity = interpolate(frame - delay, [0, 15], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div key={item.label} style={{ textAlign: "center" }}>
<div
style={{
color: "#888",
fontSize: 22,
fontFamily: "monospace",
marginBottom: 8,
opacity,
}}
>
{item.label}
</div>
<div style={{ ...item.style, fontFamily: "sans-serif", opacity }}>
テロップテキスト
</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};
// --- Phase 2: アニメーション4種 ---
const AnimationShowcase: React.FC = () => {
return (
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
<div
style={{
position: "absolute",
top: 60,
width: "100%",
textAlign: "center",
fontSize: 56,
fontWeight: "bold",
color: "white",
fontFamily: "sans-serif",
}}
>
テロップアニメーション
</div>
{/* FadeIn のみ */}
<Sequence from={0} durationInFrames={55}>
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<FadeInOnly text="FadeIn — opacity 0→1" />
</AbsoluteFill>
</Sequence>
{/* FadeIn + FadeOut */}
<Sequence from={60} durationInFrames={55}>
<FadeInOutCaption text="FadeIn + FadeOut — Math.min()" />
</Sequence>
{/* ScaleIn */}
<Sequence from={120} durationInFrames={50}>
<ScaleInCaption text="ScaleIn — spring でポップアップ" />
</Sequence>
</AbsoluteFill>
);
};
// FadeIn のみのコンポーネント
const FadeInOnly: React.FC<{ text: string }> = ({ text }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: "clamp",
});
return (
<div
style={{
opacity,
color: "white",
fontSize: 48,
fontFamily: "sans-serif",
backgroundColor: "rgba(0, 0, 0, 0.6)",
padding: "12px 24px",
borderRadius: 8,
}}
>
{text}
</div>
);
};
// --- Phase 2b: タイプライター ---
const TypewriterShowcase: React.FC = () => {
return (
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
<div
style={{
position: "absolute",
top: 60,
width: "100%",
textAlign: "center",
fontSize: 56,
fontWeight: "bold",
color: "white",
fontFamily: "sans-serif",
}}
>
タイプライター効果
</div>
<TypewriterCaption text="text.slice(0, Math.floor(frame / 2)) で1文字ずつ表示" />
</AbsoluteFill>
);
};
// --- Phase 3: データ駆動テロップ ---
const DataDrivenShowcase: React.FC<{
captions: { text: string; from: number; duration: number }[];
}> = ({ captions }) => {
return (
<AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}>
<div
style={{
position: "absolute",
top: 60,
width: "100%",
textAlign: "center",
fontSize: 56,
fontWeight: "bold",
color: "white",
fontFamily: "sans-serif",
}}
>
データ駆動テロップ
</div>
<div
style={{
position: "absolute",
top: 160,
width: "100%",
textAlign: "center",
fontSize: 24,
color: "#888",
fontFamily: "monospace",
}}
>
captions.map() → Sequence → FadeInOutCaption
</div>
{captions.map((cap, i) => (
<Sequence key={i} from={cap.from} durationInFrames={cap.duration}>
<FadeInOutCaption text={cap.text} />
</Sequence>
))}
</AbsoluteFill>
);
};
// --- メインコンポーネント ---
export const CaptionsDemo: React.FC<
z.infer<typeof captionsDemoSchema>
> = ({ captions }) => {
return (
<AbsoluteFill>
{/* Phase 1: スタイル3種 (0-120f) */}
<Sequence from={0} durationInFrames={120}>
<StyleShowcase />
</Sequence>
{/* Phase 2: アニメーション (120-290f) */}
<Sequence from={120} durationInFrames={170}>
<AnimationShowcase />
</Sequence>
{/* Phase 2b: タイプライター (290-340f) */}
<Sequence from={290} durationInFrames={50}>
<TypewriterShowcase />
</Sequence>
{/* Phase 3: データ駆動 (340-520f) */}
<Sequence from={340} durationInFrames={180}>
<DataDrivenShowcase captions={captions} />
</Sequence>
</AbsoluteFill>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment