feat(videos): add FeaturePill, ProgressBar, NumberPunch components

This commit is contained in:
SnapOtter
2026-05-08 16:37:41 +08:00
parent e3187f1740
commit 1702492ef3
3 changed files with 177 additions and 0 deletions
@@ -0,0 +1,55 @@
import type React from "react";
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLOR } from "@/lib/colors";
import { FONT } from "@/lib/fonts";
import { SPRING } from "@/lib/motion";
export const FeaturePill: React.FC<{
label: string;
enterFrame: number;
targetX: number;
targetY: number;
fromDirection?: "left" | "right" | "top" | "bottom";
}> = ({ label, enterFrame, targetX, targetY, fromDirection = "left" }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame: frame - enterFrame,
fps,
config: SPRING.popIn,
});
const offscreen = {
left: { x: -300, y: targetY },
right: { x: 1400, y: targetY },
top: { x: targetX, y: -100 },
bottom: { x: targetX, y: 1200 },
}[fromDirection];
const x = interpolate(progress, [0, 1], [offscreen.x, targetX]);
const y = interpolate(progress, [0, 1], [offscreen.y, targetY]);
const scale = interpolate(progress, [0, 1], [0.8, 1]);
return (
<div
style={{
position: "absolute",
left: x,
top: y,
transform: `scale(${scale})`,
padding: "8px 18px",
borderRadius: 8,
backgroundColor: `${COLOR.accent}26`,
border: `1px solid ${COLOR.accent}40`,
color: "white",
fontFamily: FONT.body,
fontWeight: 600,
fontSize: 16,
whiteSpace: "nowrap",
}}
>
{label}
</div>
);
};
@@ -0,0 +1,72 @@
import type React from "react";
import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLOR } from "@/lib/colors";
import { TEXT } from "@/lib/fonts";
import { SPRING } from "@/lib/motion";
export const NumberPunch: React.FC<{
number: string;
descriptor: string;
enterFrame: number;
numberSize?: number;
shakeIntensity?: number;
}> = ({ number, descriptor, enterFrame, numberSize = 120, shakeIntensity = 2 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const elapsed = frame - enterFrame;
const scaleSpring = spring({ frame: elapsed, fps, config: SPRING.settle });
const scale = interpolate(scaleSpring, [0, 1], [1.5, 1]);
const shakeDecay = interpolate(elapsed, [0, 12], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const shakeX = Math.sin(elapsed * 3) * shakeIntensity * shakeDecay;
const shakeY = Math.cos(elapsed * 4) * shakeIntensity * shakeDecay;
const descOpacity = interpolate(elapsed, [8, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const numberOpacity = interpolate(elapsed, [0, 4], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "center",
gap: 20,
transform: `translate(${shakeX}px, ${shakeY}px)`,
}}
>
<span
style={{
...TEXT.counter,
fontSize: numberSize,
transform: `scale(${scale})`,
opacity: numberOpacity,
display: "inline-block",
}}
>
{number}
</span>
<span
style={{
fontFamily: TEXT.heroSub.fontFamily,
fontWeight: 500,
fontSize: 36,
color: COLOR.accent,
opacity: descOpacity,
}}
>
{descriptor}
</span>
</div>
);
};
@@ -0,0 +1,50 @@
import type React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { EASE } from "@/lib/motion";
export const ProgressBar: React.FC<{
startFrame: number;
duration: number;
width?: number;
height?: number;
color?: string;
bgColor?: string;
style?: React.CSSProperties;
}> = ({
startFrame,
duration,
width = 200,
height = 6,
color = "#3b82f6",
bgColor = "rgba(255,255,255,0.1)",
style,
}) => {
const frame = useCurrentFrame();
const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 100], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: EASE.smooth,
});
return (
<div
style={{
width,
height,
borderRadius: height / 2,
backgroundColor: bgColor,
overflow: "hidden",
...style,
}}
>
<div
style={{
width: `${progress}%`,
height: "100%",
borderRadius: height / 2,
backgroundColor: color,
}}
/>
</div>
);
};