feat(videos): add ToolGrid and BeforeAfter components

This commit is contained in:
SnapOtter
2026-05-08 16:39:03 +08:00
parent 43fbb23f19
commit 7ff96a6515
2 changed files with 127 additions and 0 deletions
@@ -0,0 +1,65 @@
import type React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { COLOR } from "@/lib/colors";
import { EASE } from "@/lib/motion";
export const BeforeAfter: React.FC<{
before: React.ReactNode;
after: React.ReactNode;
scanStartFrame: number;
scanDuration: number;
width: number;
height: number;
style?: React.CSSProperties;
}> = ({ before, after, scanStartFrame, scanDuration, width, height, style }) => {
const frame = useCurrentFrame();
const scanProgress = interpolate(
frame,
[scanStartFrame, scanStartFrame + scanDuration],
[0, 100],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth },
);
const scanX = (scanProgress / 100) * width;
return (
<div
style={{
position: "relative",
width,
height,
overflow: "hidden",
borderRadius: 8,
...style,
}}
>
<div style={{ position: "absolute", inset: 0 }}>{before}</div>
<div
style={{
position: "absolute",
inset: 0,
clipPath: `inset(0 ${100 - scanProgress}% 0 0)`,
}}
>
{after}
</div>
{scanProgress > 0 && scanProgress < 100 && (
<div
style={{
position: "absolute",
left: scanX,
top: 0,
width: 2,
height: "100%",
backgroundColor: COLOR.accent,
boxShadow: `0 0 20px ${COLOR.accent}80`,
zIndex: 10,
}}
/>
)}
</div>
);
};
+62
View File
@@ -0,0 +1,62 @@
import type React from "react";
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
import { ToolPill } from "@/components/ToolPill";
import { CATEGORY_ORDER } from "@/lib/colors";
import { SPRING, TIMING } from "@/lib/motion";
import { getToolsByCategory, TOOLS } from "@/lib/tools";
export const ToolGrid: React.FC<{
startFrame: number;
cellWidth?: number;
cellHeight?: number;
gap?: number;
style?: React.CSSProperties;
}> = ({ startFrame, cellWidth = 140, cellHeight = 32, gap = 6, style }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
let globalIndex = 0;
return (
<div
style={{
display: "flex",
gap: gap * 2,
...style,
}}
>
{CATEGORY_ORDER.map((cat) => {
const tools = getToolsByCategory(cat);
return (
<div key={cat} style={{ display: "flex", flexDirection: "column", gap }}>
{tools.map((tool) => {
const i = globalIndex++;
const enterDelay = startFrame + i * TIMING.staggerFrames;
const s = spring({
frame: frame - enterDelay,
fps,
config: SPRING.settle,
});
return (
<div
key={tool.name}
style={{
opacity: s,
transform: `translateX(${(1 - s) * 100}px) scale(${0.8 + s * 0.2})`,
}}
>
<ToolPill
name={tool.name}
category={tool.category}
style={{ width: cellWidth, fontSize: 11, padding: "3px 8px" }}
/>
</div>
);
})}
</div>
);
})}
</div>
);
};