← Catalog
Edit Props (live)
TEXT
TEXT
config
config
config
config
config
config
config
config
config
config
config
config
Source
303 linesimport React from "react";
import {
AbsoluteFill,
Img,
OffthreadVideo,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
loadInter("normal", { weights: ["400", "500", "700", "900"] });
type InsetPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
interface TalkingHeadInsetProps {
assetSrc?: string; // image or video shown as the corner inset
headSrc?: string | null; // talking-head .mov (transparent)
insetPosition?: InsetPosition; // default top-left (head sits bottom-right)
insetScale?: number; // width as fraction of frame (default 0.42)
insetTilt?: number; // degrees (default -3)
insetAspect?: number; // height/width override; otherwise auto from asset
showChrome?: boolean; // mac-style window chrome bar (default true)
chromeLabel?: string; // optional URL/title shown in chrome bar
backgroundColor?: string; // bg behind everything (default solid blue)
headScale?: number; // talking-head width as frac of frame (default 0.66)
headPosition?: "right" | "left" | "center"; // default right
headStartFrame?: number; // seek the head video this many frames in (keeps sync w/ VO)
captionText?: string;
captionStyle?: "chip" | "magenta_outline";
}
const FONT = "'Inter', sans-serif";
const isVideo = (s: string) => /\.(mp4|mov|webm)(\?|$)/i.test(s);
const isImage = (s: string) => /\.(png|jpg|jpeg|webp|gif)(\?|$)/i.test(s);
const resolveStatic = (s: string) =>
s.startsWith("http") ? s : staticFile(s);
export const TalkingHeadInset: React.FC<TalkingHeadInsetProps> = ({
assetSrc,
headSrc,
insetPosition = "top-left",
insetScale = 0.42,
insetTilt = -3,
insetAspect,
showChrome = true,
chromeLabel,
backgroundColor = "#509ADB",
headScale = 0.66,
headPosition = "right",
headStartFrame,
captionText,
captionStyle = "chip",
}) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
// Inset entry: slight scale-up + fade over ~10 frames
const insetOp = interpolate(frame, [0, 10], [0, 1], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const insetScaleAnim = interpolate(frame, [0, 14], [0.94, 1], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Caption fade-in
const capOp = interpolate(frame, [8, 20], [0, 1], { extrapolateRight: "clamp" });
const capY = interpolate(frame, [8, 20], [10, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Inset positioning — inset rect anchor in frame
const insetW = width * insetScale;
const insetH = insetW * (insetAspect ?? 0.62); // wide-screenshot default ~16:10
const margin = width * 0.045;
const chromeH = showChrome ? Math.round(insetW * 0.08) : 0;
const totalH = insetH + chromeH;
let leftPx: number;
let topPx: number;
switch (insetPosition) {
case "top-right":
leftPx = width - insetW - margin;
topPx = margin + height * 0.08;
break;
case "bottom-left":
leftPx = margin;
topPx = height - totalH - margin - height * 0.18;
break;
case "bottom-right":
leftPx = width - insetW - margin;
topPx = height - totalH - margin - height * 0.18;
break;
case "top-left":
default:
leftPx = margin;
topPx = margin + height * 0.08;
}
// Talking-head positioning — fills frame from bottom anchor
const headW = width * headScale;
const headH = headW; // square crop window — same as ReelVideo per-scene TH
let headLeft: number;
switch (headPosition) {
case "left":
headLeft = -headW * 0.1;
break;
case "center":
headLeft = (width - headW) / 2;
break;
case "right":
default:
headLeft = width - headW * 0.95;
}
const headTop = height - headH;
return (
<AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
{/* Subtle radial vignette behind everything for depth */}
<AbsoluteFill
style={{
background:
"radial-gradient(ellipse at center, rgba(255,255,255,0.06) 0%, transparent 60%)",
}}
/>
{/* Talking head — fills bottom-right of frame */}
{headSrc && (
<div
style={{
position: "absolute",
left: headLeft,
top: headTop,
width: headW,
height: headH,
overflow: "hidden",
zIndex: 5,
}}
>
{isImage(headSrc) ? (
<Img
src={resolveStatic(`visuals/${headSrc}`)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
objectPosition: "center center",
}}
/>
) : (
<OffthreadVideo
src={resolveStatic(`visuals/${headSrc}`)}
transparent
muted
startFrom={headStartFrame}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
objectPosition: "center center",
}}
/>
)}
</div>
)}
{/* Inset card — asset with optional chrome, sits over the head */}
{assetSrc && (
<div
style={{
position: "absolute",
left: leftPx,
top: topPx,
width: insetW,
height: totalH,
transform: `rotate(${insetTilt}deg) scale(${insetScaleAnim})`,
transformOrigin: "center center",
opacity: insetOp,
borderRadius: 18,
overflow: "hidden",
boxShadow:
"0 30px 70px rgba(0,0,0,0.45), 0 8px 24px rgba(0,0,0,0.3)",
zIndex: 20,
background: "#0a0a0a",
}}
>
{showChrome && (
<div
style={{
width: "100%",
height: chromeH,
background:
"linear-gradient(180deg, #2a2a2e 0%, #1d1d20 100%)",
display: "flex",
alignItems: "center",
paddingLeft: chromeH * 0.5,
gap: chromeH * 0.25,
borderBottom: "1px solid rgba(0,0,0,0.4)",
}}
>
<div style={dot("#FF5F57", chromeH)} />
<div style={dot("#FEBC2E", chromeH)} />
<div style={dot("#28C840", chromeH)} />
{chromeLabel && (
<div
style={{
flex: 1,
textAlign: "center",
color: "#aaa",
fontFamily: FONT,
fontSize: chromeH * 0.42,
fontWeight: 500,
paddingRight: chromeH * 1.5,
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
>
{chromeLabel}
</div>
)}
</div>
)}
<div style={{ width: "100%", height: insetH, background: "#000" }}>
{isVideo(assetSrc) ? (
<OffthreadVideo
src={resolveStatic(`visuals/${assetSrc}`)}
muted
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
) : (
<Img
src={resolveStatic(`visuals/${assetSrc}`)}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
)}
</div>
</div>
)}
{/* Caption chip */}
{captionText && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: height * 0.08,
display: "flex",
justifyContent: "center",
zIndex: 30,
opacity: capOp,
transform: `translateY(${capY}px)`,
}}
>
<div
style={
captionStyle === "magenta_outline"
? {
fontFamily: FONT,
fontWeight: 900,
fontSize: 64,
color: "#fff",
WebkitTextStroke: "3px #ff2eb0",
letterSpacing: -1,
textTransform: "uppercase",
padding: "0 28px",
}
: {
fontFamily: FONT,
fontWeight: 700,
fontSize: 46,
color: "#111",
background: "#fff",
padding: "14px 28px",
borderRadius: 14,
boxShadow: "0 8px 24px rgba(0,0,0,0.35)",
letterSpacing: -0.3,
}
}
>
{captionText}
</div>
</div>
)}
</AbsoluteFill>
);
};
const dot = (color: string, h: number): React.CSSProperties => ({
width: h * 0.42,
height: h * 0.42,
borderRadius: "50%",
background: color,
});
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: TalkingHeadInset
// 14 props detected · 2 port(s)
Talking Head Inset
talking_head_insetTalking Head Inset — Andy's hand-crafted Remotion scene. 14 props. captionText:TEXT, captionStyle:TEXT.
Workflow Inputs (2)
- Caption Text
captionTextTEXT - Caption Style
captionStyleTEXT
Config Fields (12)
headSrcassetSrcheadScaleinsetTiltinsetScaleshowChromechromeLabelinsetAspectheadPositioninsetPositionheadStartFramebackgroundColor
Meta
- Updated
- 4/21/2026