Created
February 17, 2026 06:20
-
-
Save chan-ume/b75ea8bcd1999494eea4996635a34477 to your computer and use it in GitHub Desktop.
Remotion でテロップをつける様々な実装パターン
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-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