feat: add AnimatedGrid and InteractiveGridPattern components and their demo examples to MagicuiPage

This commit is contained in:
javayhu 2025-04-26 10:24:06 +08:00
parent 625bee14ef
commit 3fff508728
5 changed files with 266 additions and 0 deletions

View File

@ -1,8 +1,10 @@
import { AnimatedGridPatternDemo } from '@/components/magicui/example/animated-grid-pattern-example';
import { AnimatedListDemo } from '@/components/magicui/example/animated-list-example';
import { BentoDemo } from '@/components/magicui/example/bento-grid-example';
import { DotPatternDemo } from '@/components/magicui/example/dot-pattern-example';
import { GridPatternDemo } from '@/components/magicui/example/grid-pattern-example';
import { HeroVideoDialogDemoTopInBottomOut } from '@/components/magicui/example/hero-video-dialog-example';
import { InteractiveGridPatternDemo } from '@/components/magicui/example/interactive-grid-pattern-example';
import { MarqueeDemoVertical } from '@/components/magicui/example/marquee-example';
import { RippleDemo } from '@/components/magicui/example/ripple-example';
@ -16,6 +18,8 @@ export default async function MagicuiPage() {
<div className="mx-auto space-y-8">
<DotPatternDemo />
<GridPatternDemo />
<AnimatedGridPatternDemo />
<InteractiveGridPatternDemo />
<RippleDemo />
<BentoDemo />
<div className="grid md:grid-cols-2 gap-4">

View File

@ -0,0 +1,154 @@
"use client";
import { motion } from "motion/react";
import {
ComponentPropsWithoutRef,
useEffect,
useId,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
export interface AnimatedGridPatternProps
extends ComponentPropsWithoutRef<"svg"> {
width?: number;
height?: number;
x?: number;
y?: number;
strokeDasharray?: any;
numSquares?: number;
maxOpacity?: number;
duration?: number;
repeatDelay?: number;
}
export function AnimatedGridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
numSquares = 50,
className,
maxOpacity = 0.5,
duration = 4,
repeatDelay = 0.5,
...props
}: AnimatedGridPatternProps) {
const id = useId();
const containerRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [squares, setSquares] = useState(() => generateSquares(numSquares));
function getPos() {
return [
Math.floor((Math.random() * dimensions.width) / width),
Math.floor((Math.random() * dimensions.height) / height),
];
}
// Adjust the generateSquares function to return objects with an id, x, and y
function generateSquares(count: number) {
return Array.from({ length: count }, (_, i) => ({
id: i,
pos: getPos(),
}));
}
// Function to update a single square's position
const updateSquarePosition = (id: number) => {
setSquares((currentSquares) =>
currentSquares.map((sq) =>
sq.id === id
? {
...sq,
pos: getPos(),
}
: sq,
),
);
};
// Update squares to animate in
useEffect(() => {
if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares));
}
}, [dimensions, numSquares]);
// Resize observer to update container dimensions
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
if (containerRef.current) {
resizeObserver.unobserve(containerRef.current);
}
};
}, [containerRef]);
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
<svg x={x} y={y} className="overflow-visible">
{squares.map(({ pos: [x, y], id }, index) => (
<motion.rect
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatType: "reverse",
}}
onAnimationComplete={() => updateSquarePosition(id)}
key={`${x}-${y}-${index}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
fill="currentColor"
strokeWidth="0"
/>
))}
</svg>
</svg>
);
}

View File

@ -0,0 +1,19 @@
import { cn } from "@/lib/utils";
import { AnimatedGridPattern } from "@/components/magicui/animated-grid-pattern";
export function AnimatedGridPatternDemo() {
return (
<div className="relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-lg border bg-background p-20">
<AnimatedGridPattern
numSquares={30}
maxOpacity={0.1}
duration={3}
repeatDelay={1}
className={cn(
"[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]",
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
)}
/>
</div>
);
}

View File

@ -0,0 +1,17 @@
"use client";
import { cn } from "@/lib/utils";
import { InteractiveGridPattern } from "@/components/magicui/interactive-grid-pattern";
export function InteractiveGridPatternDemo() {
return (
<div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background">
<InteractiveGridPattern
className={cn(
"[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]",
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
)}
/>
</div>
);
}

View File

@ -0,0 +1,72 @@
"use client";
import { cn } from "@/lib/utils";
import React, { useState } from "react";
/**
* InteractiveGridPattern is a component that renders a grid pattern with interactive squares.
*
* @param width - The width of each square.
* @param height - The height of each square.
* @param squares - The number of squares in the grid. The first element is the number of horizontal squares, and the second element is the number of vertical squares.
* @param className - The class name of the grid.
* @param squaresClassName - The class name of the squares.
*/
interface InteractiveGridPatternProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
squares?: [number, number]; // [horizontal, vertical]
className?: string;
squaresClassName?: string;
}
/**
* The InteractiveGridPattern component.
*
* @see InteractiveGridPatternProps for the props interface.
* @returns A React component.
*/
export function InteractiveGridPattern({
width = 40,
height = 40,
squares = [24, 24],
className,
squaresClassName,
...props
}: InteractiveGridPatternProps) {
const [horizontal, vertical] = squares;
const [hoveredSquare, setHoveredSquare] = useState<number | null>(null);
return (
<svg
width={width * horizontal}
height={height * vertical}
className={cn(
"absolute inset-0 h-full w-full border border-gray-400/30",
className,
)}
{...props}
>
{Array.from({ length: horizontal * vertical }).map((_, index) => {
const x = (index % horizontal) * width;
const y = Math.floor(index / horizontal) * height;
return (
<rect
key={index}
x={x}
y={y}
width={width}
height={height}
className={cn(
"stroke-gray-400/30 transition-all duration-100 ease-in-out [&:not(:hover)]:duration-1000",
hoveredSquare === index ? "fill-gray-300/30" : "fill-transparent",
squaresClassName,
)}
onMouseEnter={() => setHoveredSquare(index)}
onMouseLeave={() => setHoveredSquare(null)}
/>
);
})}
</svg>
);
}