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> ); ```