← Catalog
Edit Props (live)
TEXT
TEXT
config
config
config
config
Source
214 linesimport React from "react";
import {
AbsoluteFill,
Img,
OffthreadVideo,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
spring,
staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
loadInter("normal", { weights: ["400", "700", "900"] });
interface CenteredAssetProps {
assetSrc: string;
backgroundColor?: string;
tilt?: number; // degrees, default 0 (straight)
scroll?: boolean; // if true, asset is tall — auto-scroll vertically
captionText?: string;
captionStyle?: "chip" | "magenta_outline";
}
const FONT = "'Inter', sans-serif";
const isVideo = (src: string) => /\.(mp4|mov|webm)(\?|$)/i.test(src);
export const CenteredAsset: React.FC<CenteredAssetProps> = ({
assetSrc,
backgroundColor = "#f2eee4",
tilt = 0,
scroll = false,
captionText,
captionStyle = "chip",
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
const assetSpring = spring({ frame, fps, config: { mass: 1, damping: 14, stiffness: 110 } });
const scaleIn = interpolate(assetSpring, [0, 1], [0.88, 1]);
const opIn = interpolate(frame, [0, 12], [0, 1], { extrapolateRight: "clamp" });
const floatY = Math.sin((frame / fps) * 1.1) * 6;
const scrollAmt = interpolate(frame, [15, durationInFrames], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.cubic),
});
const captionOp = interpolate(frame, [14, 28], [0, 1], { extrapolateRight: "clamp" });
const captionY = interpolate(frame, [14, 28], [16, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const resolvedSrc = assetSrc.startsWith("http") ? assetSrc : staticFile(assetSrc);
const video = isVideo(assetSrc);
return (
<AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
{/* Subtle noise texture via radial gradient */}
<div
style={{
position: "absolute",
inset: 0,
background: "transparent",
pointerEvents: "none",
}}
/>
{/* Asset box — for videos, use a 16:9 panel parked at upper-third
(eye level on a phone), so kinetic captions at the bottom don't
overlap the demo. For images, keep the portrait card layout. */}
{video ? (
(() => {
const panelWidth = width * 0.96;
const panelHeight = panelWidth * 9 / 16;
return (
<div
style={{
position: "absolute",
top: height * 0.15,
left: (width - panelWidth) / 2,
width: panelWidth,
height: panelHeight,
transform: `translateY(${floatY}px) rotate(${tilt}deg) scale(${scaleIn})`,
opacity: opIn,
borderRadius: 32,
overflow: "hidden",
boxShadow:
"0 50px 100px rgba(0,0,0,0.32), 0 20px 40px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.06)",
background: "#000",
}}
>
<OffthreadVideo
src={resolvedSrc}
muted
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
/>
</div>
);
})()
) : (
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: `translate(-50%, calc(-50% + ${floatY}px)) rotate(${tilt}deg) scale(${scaleIn})`,
opacity: opIn,
width: width * 0.88,
height: height * 0.76,
borderRadius: 32,
overflow: "hidden",
boxShadow:
"0 50px 100px rgba(0,0,0,0.32), 0 20px 40px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.06)",
background: "#fff",
}}
>
<Img
src={resolvedSrc}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
objectPosition: scroll ? `center ${10 + scrollAmt * 80}%` : "center center",
display: "block",
}}
/>
</div>
)}
{/* Caption */}
{captionText && (
<Caption
text={captionText}
style={captionStyle}
opacity={captionOp}
translateY={captionY}
bottom={220}
/>
)}
</AbsoluteFill>
);
};
const Caption: React.FC<{
text: string;
style: "chip" | "magenta_outline";
opacity: number;
translateY: number;
bottom: number;
}> = ({ text, style, opacity, translateY, bottom }) => {
if (style === "magenta_outline") {
return (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom,
textAlign: "center",
fontFamily: FONT,
fontSize: 88,
fontWeight: 900,
color: "#ff3bd1",
WebkitTextStroke: "4px #000",
letterSpacing: -2,
textShadow:
"0 6px 0 #000, 3px 3px 0 #000, -3px 3px 0 #000, 3px -3px 0 #000, -3px -3px 0 #000",
opacity,
transform: `translateY(${translateY}px)`,
zIndex: 20,
}}
>
{text}
</div>
);
}
return (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom,
display: "flex",
justifyContent: "center",
opacity,
transform: `translateY(${translateY}px)`,
zIndex: 20,
}}
>
<div
style={{
background: "rgba(0,0,0,0.78)",
color: "#fff",
padding: "18px 32px",
borderRadius: 16,
fontFamily: FONT,
fontSize: 54,
fontWeight: 700,
letterSpacing: -0.5,
backdropFilter: "blur(6px)",
}}
>
{text}
</div>
</div>
);
};
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: CenteredAsset
// 6 props detected · 2 port(s)
Centered Asset
centered_assetCentered Asset — Andy's hand-crafted Remotion scene. 6 props. captionText:TEXT, captionStyle:TEXT.
Workflow Inputs (2)
- Caption Text
captionTextTEXT - Caption Style
captionStyleTEXT
Config Fields (4)
tiltscrollassetSrcbackgroundColor
Meta
- Updated
- 4/21/2026