Last active
February 15, 2026 07:28
-
-
Save chan-ume/f3aa0bdaa5e86476c9db3c49ea6e55cc 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-4-assets | |
| import { | |
| AbsoluteFill, | |
| Img, | |
| interpolate, | |
| Sequence, | |
| staticFile, | |
| useCurrentFrame, | |
| useVideoConfig, | |
| } from "remotion"; | |
| import { Video } from "@remotion/media"; | |
| import { Audio } from "@remotion/media"; | |
| import { z } from "zod"; | |
| export const assetsDemoSchema = z.object({ | |
| imagePath: z.string(), | |
| videoPath: z.string(), | |
| audioPath: z.string(), | |
| }); | |
| // --- Phase 1: Img + staticFile + Ken Burns --- | |
| const ImageShowcase: React.FC<{ imagePath: string }> = ({ imagePath }) => { | |
| const frame = useCurrentFrame(); | |
| // Ken Burns 効果: ゆっくりズーム | |
| const scale = interpolate(frame, [0, 100], [1, 1.15]); | |
| return ( | |
| <AbsoluteFill> | |
| <AbsoluteFill> | |
| <Img | |
| src={staticFile(imagePath)} | |
| style={{ | |
| width: "100%", | |
| height: "100%", | |
| objectFit: "cover", | |
| transform: `scale(${scale})`, | |
| }} | |
| /> | |
| </AbsoluteFill> | |
| {/* ラベルオーバーレイ */} | |
| <AbsoluteFill | |
| style={{ | |
| justifyContent: "flex-start", | |
| alignItems: "flex-start", | |
| padding: 40, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| backgroundColor: "rgba(0, 0, 0, 0.7)", | |
| color: "#4ade80", | |
| fontSize: 28, | |
| fontFamily: "monospace", | |
| padding: "12px 20px", | |
| borderRadius: 8, | |
| }} | |
| > | |
| {"<Img src={staticFile(\"" + imagePath + "\")} />"} | |
| </div> | |
| </AbsoluteFill> | |
| <AbsoluteFill | |
| style={{ | |
| justifyContent: "flex-start", | |
| alignItems: "flex-start", | |
| padding: 40, | |
| paddingTop: 100, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| backgroundColor: "rgba(0, 0, 0, 0.7)", | |
| color: "#60a5fa", | |
| fontSize: 24, | |
| fontFamily: "monospace", | |
| padding: "8px 16px", | |
| borderRadius: 8, | |
| }} | |
| > | |
| objectFit: "cover" + Ken Burns (scale{" "} | |
| {scale.toFixed(2)}) | |
| </div> | |
| </AbsoluteFill> | |
| {/* タイトル */} | |
| <AbsoluteFill | |
| style={{ | |
| justifyContent: "flex-end", | |
| alignItems: "center", | |
| paddingBottom: 60, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontSize: 56, | |
| fontWeight: "bold", | |
| color: "white", | |
| fontFamily: "sans-serif", | |
| textShadow: "0 2px 8px rgba(0,0,0,0.8)", | |
| }} | |
| > | |
| staticFile + Img + Ken Burns | |
| </div> | |
| </AbsoluteFill> | |
| </AbsoluteFill> | |
| ); | |
| }; | |
| // --- Phase 2: Video --- | |
| const VideoShowcase: React.FC<{ videoPath: string }> = ({ videoPath }) => { | |
| const { fps } = useVideoConfig(); | |
| return ( | |
| <AbsoluteFill style={{ backgroundColor: "#111" }}> | |
| {/* 動画を中央に表示 */} | |
| <AbsoluteFill | |
| style={{ justifyContent: "center", alignItems: "center" }} | |
| > | |
| <Video | |
| src={staticFile(videoPath)} | |
| style={{ | |
| width: 1280, | |
| height: 720, | |
| objectFit: "cover", | |
| borderRadius: 16, | |
| }} | |
| volume={0.5} | |
| /> | |
| </AbsoluteFill> | |
| {/* ラベル */} | |
| <AbsoluteFill | |
| style={{ | |
| justifyContent: "flex-start", | |
| alignItems: "flex-start", | |
| padding: 40, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| backgroundColor: "rgba(0, 0, 0, 0.8)", | |
| color: "#4ade80", | |
| fontSize: 24, | |
| fontFamily: "monospace", | |
| padding: "10px 16px", | |
| borderRadius: 8, | |
| display: "flex", | |
| flexDirection: "column", | |
| gap: 4, | |
| }} | |
| > | |
| <span>{"<Video src={staticFile(\"" + videoPath + "\")} />"}</span> | |
| <span style={{ color: "#fbbf24" }}> | |
| volume={"{0.5}"} / trimBefore={"{" + (2 * fps) + "}"} / | |
| playbackRate={"{1.5}"} | |
| </span> | |
| </div> | |
| </AbsoluteFill> | |
| {/* タイトル */} | |
| <AbsoluteFill | |
| style={{ | |
| justifyContent: "flex-end", | |
| alignItems: "center", | |
| paddingBottom: 40, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontSize: 56, | |
| fontWeight: "bold", | |
| color: "white", | |
| fontFamily: "sans-serif", | |
| }} | |
| > | |
| Video コンポーネント | |
| </div> | |
| </AbsoluteFill> | |
| </AbsoluteFill> | |
| ); | |
| }; | |
| // --- Phase 3: Audio + volume callback 可視化 --- | |
| const AudioShowcase: React.FC<{ audioPath: string }> = ({ audioPath }) => { | |
| const frame = useCurrentFrame(); | |
| const { fps, durationInFrames } = useVideoConfig(); | |
| // volume コールバックと同じ計算を可視化用に再現 | |
| const fadeIn = interpolate(frame, [0, fps], [0, 1], { | |
| extrapolateRight: "clamp", | |
| }); | |
| const fadeOut = interpolate( | |
| frame, | |
| [durationInFrames - 2 * fps, durationInFrames], | |
| [1, 0], | |
| { extrapolateLeft: "clamp" }, | |
| ); | |
| const volume = fadeIn * fadeOut; | |
| // グラフ描画用のポイント | |
| const graphWidth = 800; | |
| const graphHeight = 200; | |
| return ( | |
| <AbsoluteFill style={{ backgroundColor: "#0f172a" }}> | |
| {/* 実際の Audio コンポーネント */} | |
| <Audio | |
| src={staticFile(audioPath)} | |
| volume={(f) => { | |
| const fi = interpolate(f, [0, fps], [0, 1], { | |
| extrapolateRight: "clamp", | |
| }); | |
| const fo = interpolate( | |
| f, | |
| [durationInFrames - 2 * fps, durationInFrames], | |
| [1, 0], | |
| { extrapolateLeft: "clamp" }, | |
| ); | |
| return fi * fo; | |
| }} | |
| /> | |
| {/* タイトル */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: 60, | |
| width: "100%", | |
| textAlign: "center", | |
| fontSize: 56, | |
| fontWeight: "bold", | |
| color: "white", | |
| fontFamily: "sans-serif", | |
| }} | |
| > | |
| Audio + volume callback | |
| </div> | |
| {/* volume カーブのグラフ */} | |
| <AbsoluteFill | |
| style={{ justifyContent: "center", alignItems: "center" }} | |
| > | |
| <div style={{ position: "relative", width: graphWidth, height: graphHeight + 60 }}> | |
| {/* グラフ背景 */} | |
| <div | |
| style={{ | |
| width: graphWidth, | |
| height: graphHeight, | |
| backgroundColor: "rgba(255,255,255,0.05)", | |
| borderRadius: 8, | |
| border: "1px solid rgba(255,255,255,0.1)", | |
| position: "relative", | |
| overflow: "hidden", | |
| }} | |
| > | |
| {/* 現在位置インジケータ */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| left: (frame / durationInFrames) * graphWidth, | |
| top: 0, | |
| width: 2, | |
| height: graphHeight, | |
| backgroundColor: "#f43f5e", | |
| }} | |
| /> | |
| {/* volume バー */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| bottom: 0, | |
| left: (frame / durationInFrames) * graphWidth - 20, | |
| width: 40, | |
| height: volume * graphHeight, | |
| backgroundColor: "#38bdf8", | |
| borderRadius: "4px 4px 0 0", | |
| opacity: 0.8, | |
| }} | |
| /> | |
| </div> | |
| {/* ラベル */} | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "space-between", | |
| marginTop: 8, | |
| color: "#64748b", | |
| fontSize: 18, | |
| fontFamily: "monospace", | |
| }} | |
| > | |
| <span>frame 0</span> | |
| <span>fadeIn ↑</span> | |
| <span>volume = 1.0</span> | |
| <span>↓ fadeOut</span> | |
| <span>frame {durationInFrames}</span> | |
| </div> | |
| </div> | |
| </AbsoluteFill> | |
| {/* 現在の volume 値 */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| bottom: 100, | |
| width: "100%", | |
| textAlign: "center", | |
| fontSize: 36, | |
| fontFamily: "monospace", | |
| color: "#38bdf8", | |
| }} | |
| > | |
| volume: {volume.toFixed(2)} = Math.min(fadeIn: {fadeIn.toFixed(2)}, | |
| fadeOut: {fadeOut.toFixed(2)}) × fadeOut | |
| </div> | |
| {/* コード表示 */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| bottom: 40, | |
| width: "100%", | |
| textAlign: "center", | |
| fontSize: 22, | |
| fontFamily: "monospace", | |
| color: "#4ade80", | |
| }} | |
| > | |
| {"volume={(f) => interpolate(f, ...) * interpolate(f, ...)}"} | |
| </div> | |
| </AbsoluteFill> | |
| ); | |
| }; | |
| // --- メインコンポーネント --- | |
| export const AssetsDemo: React.FC<z.infer<typeof assetsDemoSchema>> = ({ | |
| imagePath, | |
| videoPath, | |
| audioPath, | |
| }) => { | |
| return ( | |
| <AbsoluteFill> | |
| <Sequence from={0} durationInFrames={100}> | |
| <ImageShowcase imagePath={imagePath} /> | |
| </Sequence> | |
| <Sequence from={100} durationInFrames={100}> | |
| <VideoShowcase videoPath={videoPath} /> | |
| </Sequence> | |
| <Sequence from={200} durationInFrames={100}> | |
| <AudioShowcase audioPath={audioPath} /> | |
| </Sequence> | |
| </AbsoluteFill> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment