Skip to content

Commit

Permalink
feat: add warp animation component (#307)
Browse files Browse the repository at this point in the history
* feat: add warp animation component

* fix: added the warp grid lines

* fix: update styles

* fix: update mdx

* fix: update code

* fix: converted to tailwind

* fix: converted more stuff to tailwind

---------

Co-authored-by: Nguyen Phuc Loc <nguyenphucloc@Locs-MacBook-Pro.local>
Co-authored-by: Arghya Das <arghyadasproject@gmail.com>
Co-authored-by: Dillion Verma <hello@dillion.io>
  • Loading branch information
4 people authored Dec 24, 2024
1 parent 247750d commit 792726a
Show file tree
Hide file tree
Showing 9 changed files with 991 additions and 329 deletions.
1,042 changes: 713 additions & 329 deletions __registry__/index.tsx

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ export const docsConfig: DocsConfig = {
items: [],
label: "",
},
{
title: "Warp Animation Container",
href: `/docs/components/warp-animation-container`,
items: [],
label: "New",
},
],
},
{
Expand Down
54 changes: 54 additions & 0 deletions content/docs/components/warp-animation-container.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: Warp Animation Container
date: 2024-12-24
description: A container component that applies a warp animation effect to its children
author: magicui
published: true
---

<ComponentPreview name="warp-animation-container-demo" />

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add "https://magicui.design/r/warp-animation-container"
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="warp-animation-container" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Props

| Prop | Type | Description | Default |
| ------------ | --------------- | ----------------------------------------------- | -------------------- |
| children | React.ReactNode | The content to be put inside the warp animation | - |
| perspective | number | The perspective of the warp animation | 100 |
| beamsPerSide | number | The number of beams per side | 3 |
| beamSize | number | The size of the beams | 5 |
| beamDelayMax | number | The maximum delay of the beams | 3 |
| beamDelayMin | number | The minimum delay of the beams | 0 |
| beamDuration | number | The duration of the beams | 3 |
| gridColor | string | The color of the grid lines | "hsl(var(--border))" |
13 changes: 13 additions & 0 deletions public/r/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@
}
]
},
{
"name": "warp-animation-container",
"type": "registry:ui",
"dependencies": [
"framer-motion"
],
"files": [
{
"path": "magicui/warp-animation-container.tsx",
"type": "registry:ui"
}
]
},
{
"name": "morphing-text",
"type": "registry:ui",
Expand Down
15 changes: 15 additions & 0 deletions public/r/styles/default/warp-animation-container.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "warp-animation-container",
"type": "registry:ui",
"dependencies": [
"framer-motion"
],
"files": [
{
"path": "magicui/warp-animation-container.tsx",
"content": "import { cn } from \"@/lib/utils\";\nimport { motion } from \"framer-motion\";\nimport React, { HTMLAttributes, useCallback, useMemo } from \"react\";\n\ninterface WarpAnimationContainerProps extends HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n perspective?: number;\n beamsPerSide?: number;\n beamSize?: number;\n beamDelayMax?: number;\n beamDelayMin?: number;\n beamDuration?: number;\n gridColor?: string;\n}\n\nconst Beam = ({\n width,\n x,\n delay,\n duration,\n}: {\n width: string | number;\n x: string | number;\n delay: number;\n duration: number;\n}) => {\n const hue = Math.floor(Math.random() * 360);\n const ar = Math.floor(Math.random() * 10) + 1;\n\n return (\n <motion.div\n className={`absolute top-0`}\n style={{\n left: x,\n width,\n aspectRatio: 1 / ar,\n background: `linear-gradient(hsl(${hue} 80% 60%), transparent)`,\n }}\n initial={{ y: \"100cqmax\", x: \"-50%\" }}\n animate={{ y: \"-100%\", x: \"-50%\" }}\n transition={{\n duration,\n delay,\n repeat: Infinity,\n ease: \"linear\",\n }}\n />\n );\n};\n\nconst WarpAnimationContainer: React.FC<WarpAnimationContainerProps> = ({\n children,\n perspective = 100,\n className,\n beamsPerSide = 3,\n beamSize = 5,\n beamDelayMax = 3,\n beamDelayMin = 0,\n beamDuration = 3,\n gridColor = \"hsl(var(--border))\",\n ...props\n}) => {\n const generateBeams = useCallback(() => {\n const beams = [];\n const cellsPerSide = Math.floor(100 / beamSize);\n const step = cellsPerSide / beamsPerSide;\n\n for (let i = 0; i < beamsPerSide; i++) {\n const x = Math.floor(i * step);\n const delay =\n Math.random() * (beamDelayMax - beamDelayMin) + beamDelayMin;\n beams.push({ x, delay });\n }\n return beams;\n }, [beamsPerSide, beamSize, beamDelayMax, beamDelayMin]);\n\n const topBeams = useMemo(() => generateBeams(), [generateBeams]);\n const rightBeams = useMemo(() => generateBeams(), [generateBeams]);\n const bottomBeams = useMemo(() => generateBeams(), [generateBeams]);\n const leftBeams = useMemo(() => generateBeams(), [generateBeams]);\n\n return (\n <div className={cn(\"relative rounded border p-20\", className)} {...props}>\n <div\n style={\n {\n \"--perspective\": `${perspective}px`,\n \"--grid-color\": gridColor,\n \"--beam-size\": `${beamSize}%`,\n } as React.CSSProperties\n }\n className={\n \"pointer-events-none absolute left-0 top-0 size-full overflow-hidden [clip-path:inset(0)] [container-type:size] [perspective:var(--perspective)] [transform-style:preserve-3d]\"\n }\n >\n <div\n style={{\n position: \"absolute\",\n transformStyle: \"preserve-3d\",\n containerType: \"inline-size\",\n width: \"100cqi\",\n height: \"100cqmax\",\n transformOrigin: \"50% 0%\",\n transform: \"rotateX(-90deg)\",\n backgroundSize: `var(--beam-size) var(--beam-size)`,\n background: `\n linear-gradient(var(--grid-color) 0 1px, transparent 1px var(--beam-size)) 50% -0.5px /\n var(--beam-size) var(--beam-size),\n linear-gradient(90deg, var(--grid-color) 0 1px, transparent 1px var(--beam-size))\n 50% 50% / var(--beam-size) var(--beam-size)\n `,\n }}\n >\n {topBeams.map((beam, index) => (\n <Beam\n key={`top-${index}`}\n width={`${beamSize}%`}\n x={`${beam.x * beamSize}%`}\n delay={beam.delay}\n duration={beamDuration}\n />\n ))}\n </div>\n <div\n style={{\n position: \"absolute\",\n transformStyle: \"preserve-3d\",\n containerType: \"inline-size\",\n width: \"100cqi\",\n height: \"100cqmax\",\n top: \"100%\",\n transformOrigin: \"50% 0%\",\n transform: \"rotateX(-90deg)\",\n backgroundSize: `var(--beam-size) var(--beam-size)`,\n background: `\n linear-gradient(var(--grid-color) 0 1px, transparent 1px var(--beam-size)) 50% -0.5px /\n var(--beam-size) var(--beam-size),\n linear-gradient(90deg, var(--grid-color) 0 1px, transparent 1px var(--beam-size))\n 50% 50% / var(--beam-size) var(--beam-size)\n `,\n }}\n >\n {bottomBeams.map((beam, index) => (\n <Beam\n key={`bottom-${index}`}\n width={`${beamSize}%`}\n x={`${beam.x * beamSize}%`}\n delay={beam.delay}\n duration={beamDuration}\n />\n ))}\n </div>\n <div\n style={{\n position: \"absolute\",\n transformStyle: \"preserve-3d\",\n containerType: \"inline-size\",\n width: \"100cqh\",\n height: \"100cqmax\",\n top: 0,\n left: 0,\n transformOrigin: \"0% 0%\",\n transform: \"rotate(90deg) rotateX(-90deg)\",\n backgroundSize: `var(--beam-size) var(--beam-size)`,\n background: `\n linear-gradient(var(--grid-color) 0 1px, transparent 1px var(--beam-size)) 50% -0.5px /\n var(--beam-size) var(--beam-size),\n linear-gradient(90deg, var(--grid-color) 0 1px, transparent 1px var(--beam-size))\n 50% 50% / var(--beam-size) var(--beam-size)\n `,\n }}\n >\n {leftBeams.map((beam, index) => (\n <Beam\n key={`left-${index}`}\n width={`${beamSize}%`}\n x={`${beam.x * beamSize}%`}\n delay={beam.delay}\n duration={beamDuration}\n />\n ))}\n </div>\n <div\n style={{\n position: \"absolute\",\n transformStyle: \"preserve-3d\",\n containerType: \"inline-size\",\n width: \"100cqh\",\n height: \"100cqmax\",\n top: 0,\n right: 0,\n transformOrigin: \"100% 0%\",\n transform: \"rotate(-90deg) rotateX(-90deg)\",\n backgroundSize: `var(--beam-size) var(--beam-size)`,\n background: `\n linear-gradient(var(--grid-color) 0 1px, transparent 1px var(--beam-size)) 50% -0.5px /\n var(--beam-size) var(--beam-size),\n linear-gradient(90deg, var(--grid-color) 0 1px, transparent 1px var(--beam-size))\n 50% 50% / var(--beam-size) var(--beam-size)\n `,\n }}\n >\n {rightBeams.map((beam, index) => (\n <Beam\n key={`right-${index}`}\n width={`${beamSize}%`}\n x={`${beam.x * beamSize}%`}\n delay={beam.delay}\n duration={beamDuration}\n />\n ))}\n </div>\n </div>\n <div className=\"relative\">{children}</div>\n </div>\n );\n};\n\nexport default WarpAnimationContainer;\n",
"type": "registry:ui",
"target": ""
}
]
}
26 changes: 26 additions & 0 deletions registry/default/example/warp-animation-container-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import WarpAnimationContainer from "@/components/magicui/warp-animation-container";

export default function ExampleComponentDemo() {
return (
<div className="grid place-items-center">
<WarpAnimationContainer className="bg-background">
<Card className="w-80">
<CardContent className="flex flex-col gap-2 p-4">
<CardTitle>Congratulations on Your Promotion!</CardTitle>
<CardDescription>
Your hard work and dedication have paid off. We&apos;re thrilled
to see you take this next step in your career. Keep up the
fantastic work!
</CardDescription>
</CardContent>
</Card>
</WarpAnimationContainer>
</div>
);
}
152 changes: 152 additions & 0 deletions registry/default/magicui/warp-animation-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import React, { HTMLAttributes, useCallback, useMemo } from "react";

interface WarpAnimationContainerProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
perspective?: number;
beamsPerSide?: number;
beamSize?: number;
beamDelayMax?: number;
beamDelayMin?: number;
beamDuration?: number;
gridColor?: string;
}

const Beam = ({
width,
x,
delay,
duration,
}: {
width: string | number;
x: string | number;
delay: number;
duration: number;
}) => {
const hue = Math.floor(Math.random() * 360);
const ar = Math.floor(Math.random() * 10) + 1;

return (
<motion.div
style={
{
"--x": `${x}`,
"--width": `${width}`,
"--aspect-ratio": `${ar}`,
"--background": `linear-gradient(hsl(${hue} 80% 60%), transparent)`,
} as React.CSSProperties
}
className={`absolute left-[var(--x)] top-0 [aspect-ratio:1/var(--aspect-ratio)] [background:var(--background)] [width:var(--width)]`}
initial={{ y: "100cqmax", x: "-50%" }}
animate={{ y: "-100%", x: "-50%" }}
transition={{
duration,
delay,
repeat: Infinity,
ease: "linear",
}}
/>
);
};

const WarpAnimationContainer: React.FC<WarpAnimationContainerProps> = ({
children,
perspective = 100,
className,
beamsPerSide = 3,
beamSize = 5,
beamDelayMax = 3,
beamDelayMin = 0,
beamDuration = 3,
gridColor = "hsl(var(--border))",
...props
}) => {
const generateBeams = useCallback(() => {
const beams = [];
const cellsPerSide = Math.floor(100 / beamSize);
const step = cellsPerSide / beamsPerSide;

for (let i = 0; i < beamsPerSide; i++) {
const x = Math.floor(i * step);
const delay =
Math.random() * (beamDelayMax - beamDelayMin) + beamDelayMin;
beams.push({ x, delay });
}
return beams;
}, [beamsPerSide, beamSize, beamDelayMax, beamDelayMin]);

const topBeams = useMemo(() => generateBeams(), [generateBeams]);
const rightBeams = useMemo(() => generateBeams(), [generateBeams]);
const bottomBeams = useMemo(() => generateBeams(), [generateBeams]);
const leftBeams = useMemo(() => generateBeams(), [generateBeams]);

return (
<div className={cn("relative rounded border p-20", className)} {...props}>
<div
style={
{
"--perspective": `${perspective}px`,
"--grid-color": gridColor,
"--beam-size": `${beamSize}%`,
} as React.CSSProperties
}
className={
"pointer-events-none absolute left-0 top-0 size-full overflow-hidden [clip-path:inset(0)] [container-type:size] [perspective:var(--perspective)] [transform-style:preserve-3d]"
}
>
{/* top side */}
<div className="absolute [transform-style:preserve-3d] [background-size:var(--beam-size)_var(--beam-size)] [background:linear-gradient(var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_-0.5px_/var(--beam-size)_var(--beam-size),linear-gradient(90deg,_var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_50%_/var(--beam-size)_var(--beam-size)] [container-type:inline-size] [height:100cqmax] [transform-origin:50%_0%] [transform:rotateX(-90deg)] [width:100cqi]">
{topBeams.map((beam, index) => (
<Beam
key={`top-${index}`}
width={`${beamSize}%`}
x={`${beam.x * beamSize}%`}
delay={beam.delay}
duration={beamDuration}
/>
))}
</div>
{/* bottom side */}
<div className="absolute top-full [transform-style:preserve-3d] [background-size:var(--beam-size)_var(--beam-size)] [background:linear-gradient(var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_-0.5px_/var(--beam-size)_var(--beam-size),linear-gradient(90deg,_var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_50%_/var(--beam-size)_var(--beam-size)] [container-type:inline-size] [height:100cqmax] [transform-origin:50%_0%] [transform:rotateX(-90deg)] [width:100cqi]">
{bottomBeams.map((beam, index) => (
<Beam
key={`bottom-${index}`}
width={`${beamSize}%`}
x={`${beam.x * beamSize}%`}
delay={beam.delay}
duration={beamDuration}
/>
))}
</div>
{/* left side */}
<div className="absolute left-0 top-0 [transform-style:preserve-3d] [background-size:var(--beam-size)_var(--beam-size)] [background:linear-gradient(var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_-0.5px_/var(--beam-size)_var(--beam-size),linear-gradient(90deg,_var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_50%_/var(--beam-size)_var(--beam-size)] [container-type:inline-size] [height:100cqmax] [transform-origin:0%_0%] [transform:rotate(90deg)_rotateX(-90deg)] [width:100cqh]">
{leftBeams.map((beam, index) => (
<Beam
key={`left-${index}`}
width={`${beamSize}%`}
x={`${beam.x * beamSize}%`}
delay={beam.delay}
duration={beamDuration}
/>
))}
</div>
{/* right side */}
<div className="absolute right-0 top-0 [transform-style:preserve-3d] [background-size:var(--beam-size)_var(--beam-size)] [background:linear-gradient(var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_-0.5px_/var(--beam-size)_var(--beam-size),linear-gradient(90deg,_var(--grid-color)_0_1px,_transparent_1px_var(--beam-size))_50%_50%_/var(--beam-size)_var(--beam-size)] [container-type:inline-size] [height:100cqmax] [width:100cqh] [transform-origin:100%_0%] [transform:rotate(-90deg)_rotateX(-90deg)]">
{rightBeams.map((beam, index) => (
<Beam
key={`right-${index}`}
width={`${beamSize}%`}
x={`${beam.x * beamSize}%`}
delay={beam.delay}
duration={beamDuration}
/>
))}
</div>
</div>
<div className="relative">{children}</div>
</div>
);
};

export default WarpAnimationContainer;
6 changes: 6 additions & 0 deletions registry/registry-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export const examples: Registry = [
registryDependencies: ["android"],
files: ["example/android-demo-3.tsx"],
},
{
name: "warp-animation-container-demo",
type: "registry:example",
registryDependencies: ["warp-animation-container"],
files: ["example/warp-animation-container-demo.tsx"],
},
{
name: "morphing-text-demo",
type: "registry:example",
Expand Down
6 changes: 6 additions & 0 deletions registry/registry-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export const ui: Registry = [
type: "registry:ui",
files: ["magicui/android.tsx"],
},
{
name: "warp-animation-container",
type: "registry:ui",
dependencies: ["framer-motion"],
files: ["magicui/warp-animation-container.tsx"],
},
{
name: "morphing-text",
type: "registry:ui",
Expand Down

0 comments on commit 792726a

Please sign in to comment.