import React from "react";
import {
AbsoluteFill,
interpolate,
Sequence,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import {
characters,
type SpeechEvent,
type TimelineEvent,
} from "./data/pizza-oven-project-01/script";
import {
audioFileForSpeech,
hasAudioForSpeech,
pizzaOvenProject01Scenario,
} from "./data/pizza-oven-project-01/timing";
import {roundedFontFamily} from "./fonts";
import {
activeVQChronologicalScenarioSegmentForFrame,
scheduleVQChronologicalScenario,
VQLipSyncedStandeeImage,
VQSpeechOverlay,
VQWarmGradientBackground,
} from "./lib/VQRemotionLib";
import {getMouthForSpeechFrame} from "./lipsync/manifest";
const sayoAvatar = characters.sayo.avatar;
const PizzaOvenSpeechOverlay = VQSpeechOverlay<SpeechEvent>;
const clampInterpolation = {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
} as const;
const SayoStandee: React.FC<
Readonly<{
frame: number;
fps: number;
activeSpeech?: SpeechEvent;
speakingLocalFrame: number;
}>
> = ({frame, fps, activeSpeech, speakingLocalFrame}) => {
const entrance = spring({
frame,
fps,
config: {damping: 18, mass: 0.6},
});
const translateY = interpolate(entrance, [0, 1], [32, 0], clampInterpolation);
const mouth = activeSpeech
? getMouthForSpeechFrame(activeSpeech.id, speakingLocalFrame, fps)
: "rest";
return React.createElement(
"div",
{
style: {
position: "absolute",
right: 330,
bottom: -82,
width: 520,
height: 720,
display: "flex",
justifyContent: "center",
alignItems: "flex-end",
transform: `translateY(${translateY}px)`,
zIndex: 2,
} satisfies React.CSSProperties,
},
React.createElement(VQLipSyncedStandeeImage, {
imagePath: sayoAvatar.imagePath,
mouthImageDir: sayoAvatar.mouthImageDir,
mouth,
width: "100%",
height: "100%",
maxHeight: "100%",
filter: "drop-shadow(0 18px 40px rgba(31, 42, 68, 0.22))",
})
);
};
const TimelineOverlay: React.FC<Readonly<{event: TimelineEvent}>> = ({
event,
}) => {
const character = characters[event.character];
return React.createElement(PizzaOvenSpeechOverlay, {
speech: event,
speakerName: character.displayName,
accentColor: character.avatar.accentColor,
hasAudio: hasAudioForSpeech,
getAudioPath: audioFileForSpeech,
subtitleOptions: {
fontFamily: roundedFontFamily,
fontSize: 40,
lineHeight: 1.35,
backgroundColor: "rgba(255, 255, 255, 0.9)",
},
containerStyle: {zIndex: 3},
});
};
export const PizzaOvenProject01: React.FC = () => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const scheduledEvents = scheduleVQChronologicalScenario(
pizzaOvenProject01Scenario,
fps
);
const activeSegment = activeVQChronologicalScenarioSegmentForFrame(
scheduledEvents,
frame
);
const isInsideActiveSegment = activeSegment
? frame < activeSegment.from + activeSegment.durationInFrames
: false;
const activeSpeech =
activeSegment && isInsideActiveSegment ? activeSegment.event : undefined;
const speakingLocalFrame =
activeSegment && activeSpeech ? frame - activeSegment.from : 0;
const sequences = scheduledEvents.map((scheduledEvent) =>
React.createElement(
Sequence,
{
key: scheduledEvent.event.id,
from: scheduledEvent.from,
durationInFrames: scheduledEvent.durationInFrames,
premountFor: Math.min(fps, scheduledEvent.from),
},
React.createElement(TimelineOverlay, {event: scheduledEvent.event})
)
);
return React.createElement(
AbsoluteFill,
{
style: {
display: "flex",
flexDirection: "column",
alignItems: "center",
},
},
React.createElement(VQWarmGradientBackground, null),
React.createElement(SayoStandee, {
frame,
fps,
activeSpeech,
speakingLocalFrame,
}),
sequences
);
};