feat(film): synthetic cursor + type-reveal
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user