mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(videos): add FeaturePill, ProgressBar, NumberPunch components
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user