feat(film): synthetic cursor + type-reveal

This commit is contained in:
Satyam Rastogi
2026-05-30 02:33:06 +05:30
parent ba7186fe7b
commit 6898ccb7d9
2 changed files with 77 additions and 0 deletions
@@ -0,0 +1,57 @@
import { useCurrentFrame, useVideoConfig, spring, interpolate, AbsoluteFill } from "remotion";
import { theme } from "../theme";
type Pt = { x: number; y: number }; // pixels in the 1920x1080 canvas
export const Cursor: React.FC<{
from: Pt;
to: Pt;
moveStartFrame: number;
moveDurationFrames?: number;
clickAtFrame?: number;
}> = ({ from, to, moveStartFrame, moveDurationFrames = 22, clickAtFrame }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = spring({
frame: frame - moveStartFrame,
fps,
durationInFrames: moveDurationFrames,
config: { damping: 200 },
});
const x = interpolate(p, [0, 1], [from.x, to.x]);
const y = interpolate(p, [0, 1], [from.y, to.y]);
let pulse = 0;
if (clickAtFrame !== undefined) {
pulse = interpolate(frame - clickAtFrame, [0, 8], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
}
return (
<AbsoluteFill>
<div style={{ position: "absolute", left: x, top: y, transform: "translate(-3px,-3px)" }}>
{pulse > 0 && (
<div
style={{
position: "absolute",
left: -18,
top: -18,
width: 36,
height: 36,
borderRadius: 999,
background: theme.blue,
opacity: pulse * 0.4,
transform: `scale(${1 + (1 - pulse)})`,
}}
/>
)}
<svg width="30" height="30" viewBox="0 0 24 24" fill="none">
<path d="M5 3l14 8-6 1.5L10 18 5 3z" fill="white" stroke="#0c0c0e" strokeWidth="1.2" />
</svg>
</div>
</AbsoluteFill>
);
};
@@ -0,0 +1,20 @@
import { useCurrentFrame } from "remotion";
import { theme } from "../theme";
export const TypeReveal: React.FC<{
text: string;
startFrame: number;
framesPerChar?: number;
style?: React.CSSProperties;
}> = ({ text, startFrame, framesPerChar = 1.6, style }) => {
const frame = useCurrentFrame();
const shown = Math.max(0, Math.floor((frame - startFrame) / framesPerChar));
const visible = text.slice(0, shown);
const blinkOn = Math.floor(frame / 15) % 2 === 0;
return (
<span style={{ fontFamily: theme.font, color: theme.text, ...style }}>
{visible}
{shown < text.length && blinkOn ? <span style={{ opacity: 0.7 }}>|</span> : null}
</span>
);
};