183 lines
4.5 KiB
TypeScript
183 lines
4.5 KiB
TypeScript
import {
|
|
AbsoluteFill,
|
|
Easing,
|
|
interpolate,
|
|
useCurrentFrame,
|
|
useVideoConfig,
|
|
Composition,
|
|
OffthreadVideo,
|
|
Series,
|
|
delayRender,
|
|
continueRender,
|
|
cancelRender,
|
|
registerRoot,
|
|
staticFile,
|
|
} from "remotion";
|
|
import { useEffect, useState, type CSSProperties } from "react";
|
|
import { phaseCaption } from "./phase-captions";
|
|
import {
|
|
phoneViewport,
|
|
demoFrame,
|
|
phoneHeight,
|
|
phoneWidth,
|
|
phoneLeft,
|
|
demoFps,
|
|
} from "./dimensions";
|
|
|
|
export type DemoClip = {
|
|
id: string;
|
|
caption: string;
|
|
startFrame: number;
|
|
frames: number;
|
|
};
|
|
export type DemoProps = {
|
|
source: string;
|
|
background: string;
|
|
captionColor: string;
|
|
clips: DemoClip[];
|
|
opening?: DemoClip;
|
|
plain: boolean;
|
|
fps: number;
|
|
};
|
|
const defaults: DemoProps = {
|
|
source: "viewport.mp4",
|
|
background: "#000000",
|
|
captionColor: "#ffffff",
|
|
clips: [],
|
|
plain: false,
|
|
fps: demoFps,
|
|
};
|
|
|
|
function DemoMoment({
|
|
source,
|
|
captionColor,
|
|
clip,
|
|
plain,
|
|
}: Pick<DemoProps, "source" | "captionColor" | "plain"> & { clip: DemoClip }) {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
const isOpening = clip.id === "opening";
|
|
const left = isOpening
|
|
? interpolate(
|
|
frame,
|
|
[0.55 * fps, 1.45 * fps],
|
|
[(demoFrame.width - phoneWidth) / 2, phoneLeft],
|
|
{
|
|
easing: Easing.inOut((value) => Easing.cubic(value)),
|
|
extrapolateLeft: "clamp",
|
|
extrapolateRight: "clamp",
|
|
},
|
|
)
|
|
: phoneLeft;
|
|
const caption: CSSProperties = {
|
|
position: "absolute",
|
|
left: 48,
|
|
top: 0,
|
|
height: "100%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
fontFamily: "Arizona Sans, sans-serif",
|
|
fontWeight: 400,
|
|
fontSize: 32,
|
|
color: captionColor,
|
|
width: 660,
|
|
lineHeight: 1.45,
|
|
letterSpacing: "-.01em",
|
|
};
|
|
return (
|
|
<>
|
|
{!plain && !isOpening && (
|
|
<div style={caption}>
|
|
<p style={{ margin: 0 }}>
|
|
{phaseCaption(clip.id, clip.caption).map((segment, index) => (
|
|
<span key={index} style={{ opacity: segment.clips.includes(clip.id) ? 1 : 0.4 }}>
|
|
{segment.text}{" "}
|
|
</span>
|
|
))}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div
|
|
style={
|
|
plain
|
|
? { position: "absolute", inset: 0 }
|
|
: {
|
|
position: "absolute",
|
|
left,
|
|
top: 30,
|
|
width: phoneWidth,
|
|
height: phoneHeight,
|
|
}
|
|
}
|
|
>
|
|
<OffthreadVideo
|
|
src={staticFile(source)}
|
|
trimBefore={clip.startFrame}
|
|
muted
|
|
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function CommentedDemo({
|
|
source,
|
|
background,
|
|
captionColor,
|
|
clips,
|
|
opening,
|
|
plain,
|
|
}: DemoProps) {
|
|
const [fontReady] = useState(() => delayRender("Load Arizona Sans"));
|
|
useEffect(() => {
|
|
void document.fonts
|
|
.load('400 32px "Arizona Sans"')
|
|
.then(() => continueRender(fontReady))
|
|
.catch(cancelRender);
|
|
}, [fontReady]);
|
|
return (
|
|
<AbsoluteFill style={{ backgroundColor: background }}>
|
|
<style>{`
|
|
@font-face { font-family: 'Arizona Sans'; src: url('${staticFile("ArizonaSans.woff2")}') format('woff2'); font-weight: 100 900; }
|
|
`}</style>
|
|
<Series>
|
|
{!plain && opening && (
|
|
<Series.Sequence durationInFrames={opening.frames}>
|
|
<DemoMoment source={source} captionColor={captionColor} clip={opening} plain={false} />
|
|
</Series.Sequence>
|
|
)}
|
|
{clips.map((clip) => (
|
|
<Series.Sequence key={clip.id} durationInFrames={clip.frames}>
|
|
<DemoMoment source={source} captionColor={captionColor} clip={clip} plain={plain} />
|
|
</Series.Sequence>
|
|
))}
|
|
</Series>
|
|
</AbsoluteFill>
|
|
);
|
|
}
|
|
|
|
registerRoot(() => (
|
|
<>
|
|
{[false, true].map((plain) => (
|
|
<Composition
|
|
key={String(plain)}
|
|
id={plain ? "PlainViewport" : "CommentedDemo"}
|
|
component={CommentedDemo}
|
|
width={plain ? phoneViewport.width : demoFrame.width}
|
|
height={plain ? phoneViewport.height : demoFrame.height}
|
|
fps={demoFps}
|
|
durationInFrames={demoFps}
|
|
defaultProps={{ ...defaults, plain }}
|
|
calculateMetadata={({ props }) => ({
|
|
fps: props.fps,
|
|
durationInFrames: Math.max(
|
|
1,
|
|
props.clips.reduce((sum, clip) => sum + clip.frames, 0) +
|
|
(props.plain ? 0 : (props.opening?.frames ?? 0)),
|
|
),
|
|
})}
|
|
/>
|
|
))}
|
|
</>
|
|
));
|