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