Animating with Framer Motion (Half-Circle towards Origin)
## Intro
This was a bit wild to attempt to create a title for. Basically I wanted to create this animation using Framer Motion ([framer.com/motion](https://www.framer.com/motion/)) in which I spawn entities inside a half circle from an origin. In the animation, I'm animating icons that represent data sources that fly towards a server, implying that the server is consuming various amounts and types of data. For icons I'm using the [lucide.dev](https://lucide.dev/) library. Also included a minor detail that the server svg light is blinking in sync.
This animation is inside a nextjs application. On the same page other Framer Motion powered animations are also present.
## Source & Demo
Source: N/A (in report)
Demo: [smartdatahub.io/about](https://smartdatahub.io/about)
![animation gif](//images.ctfassets.net/z15dco8agirv/6nh9yyDWKNQwLinQ3uvkxA/08a90388b5bde7b8b6da8f04afaad0be/animation.gif)
## Study
### Animation Logic
**helper.ts**
Created a helper for randomizing a point inside a half circle.
```typescript
export function pickPointInHalfCircle(
cx: number = 0,
cy: number = 0,
r: number = 200,
): { x: number; y: number } {
const theta = Math.random() * Math.PI;
const u = Math.random() + Math.random();
const r2 = u > 1 ? 2 - u : u;
const x = cx - r2 * r * Math.cos(theta);
const y = cy - r2 * r * Math.sin(theta);
// bounds could perhaps be configurable
const clampedY: number = Math.min(y, -150);
const clampedX = Math.max(Math.min(x, 150), -150);
return { x: clampedX, y: clampedY };
}
export const getRandomDelay = () => -(Math.random() * 0.7 + 0.05);
export const randomDuration = () => Math.random() * 0.07 + 0.23;
```
**AnimatedPipeline/index.tsx**
```typescript
'use client';
import { AnimatePresence, motion } from 'framer-motion';
import {
ArrowDown,
FileArchive,
FileBox,
FileDigit,
FileJson2,
FileLineChart,
FilePieChart,
FileScan,
FileSpreadsheet,
FileTextIcon,
LandPlot,
TableProperties,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { v4 } from 'uuid';
import { AnimatedPipelineItem } from './Item';
const classes = {
icon: 'text-secondary-readable/[0.5]',
};
const pipelineItemIcons = [
<LandPlot className={classes.icon} key="land-plot-icon" />,
<FileSpreadsheet className={classes.icon} key="file-scan-icon" />,
<FileScan className={classes.icon} key="file-scan-icon" />,
<FilePieChart className={classes.icon} key="file-pie-chart-icon" />,
<FileLineChart className={classes.icon} key="file-line-chart-icon" />,
<FileBox className={classes.icon} key="file-box-icon" />,
<FileJson2 className={classes.icon} key="file-json2-icon" />,
<FileArchive className={classes.icon} key="file-archive-icon" />,
<FileDigit className={classes.icon} key="file-digit-icon" />,
<FileTextIcon className={classes.icon} key="file-text-icon" />,
] as const;
const getRandomIcon = () => {
return pipelineItemIcons[
Math.floor(Math.random() * pipelineItemIcons.length)
];
};
export const ANIMATION_SPEED_MS = 750;
function AnimatedPipeline() {
const [data, setData] = useState<{ id: string; icon: React.ReactElement }[]>(
[],
);
useEffect(() => {
const interval = setInterval(() => {
setData(() => [{ id: v4(), icon: getRandomIcon() }]);
}, ANIMATION_SPEED_MS);
//Clearing the interval
return () => clearInterval(interval);
}, []);
return (
<div className="flex items-center justify-center">
<div className="absolute mb-[4rem] select-none">
<AnimatePresence>
{data.map((i) => (
<AnimatedPipelineItem item={i} key={i.id} />
))}
</AnimatePresence>
</div>
<div
style={{
backgroundImage:
'radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 35%, transparent 60%, transparent 100%)',
}}
className="z-10 pt-10 px-20 mb-10"
>
<div className="items-center justify-center flex flex-col space-y-2">
<div className="relative">
<div className="w-20 h-20">
<motion.svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
className="text-primary w-20 h-20"
initial="hidden"
animate="visible"
>
<path d="M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5" />
<path d="M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5" />
<motion.path
animate={{
color: ['#4f0063', 'hsl(119, 100%, 30%)'],
strokeWidth: [2, 2.1],
}}
transition={{
duration: 0.75,
ease: 'easeInOut',
repeat: Infinity,
delay: 0.75,
repeatType: 'loop',
repeatDelay: 0,
}}
strokeWidth={2}
d="M6 6h.01"
/>
</motion.svg>
</div>
</div>
<ArrowDown className="text-primary/[0.5]" />
<div className="flex items-center gap-2">
<TableProperties className="text-primary w-7 h-7" />
<span className="font-mono text-sm">Data Catalog</span>
</div>
</div>
</div>
</div>
);
}
export default AnimatedPipeline;
```
**AnimatedPipeline/Item.tsx**
```typescript
import { motion } from 'framer-motion';
import { ANIMATION_SPEED_MS } from '.';
import { pickPointInHalfCircle } from '../helper';
export const AnimatedPipelineItem = ({
item,
}: {
item: { id: string; icon: React.ReactNode };
}) => (
<motion.div
key={`animation-${item.id}`}
className="absolute flex shadow-xl"
transition={{
duration: (ANIMATION_SPEED_MS / 1000) * 1.5,
ease: 'easeInOut',
times: [0, 0.2, 0.5, 0.8, 1],
}}
initial={{ ...pickPointInHalfCircle(), opacity: 0 }}
animate={{
x: 0,
y: 0,
opacity: 1,
}}
>
{item.icon}
</motion.div>
);
```