Animating with Framer Motion (Half-Circle towards Origin)
@Type: Report
@Published:November 10, 2024 at 11:05
@Last Updated:November 12, 2024 at 17:16
@Author: Jakke Korpelainen
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) 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 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
Study
Animation Logic
helper.ts
Created a helper for randomizing a point inside a half circle.
1export function pickPointInHalfCircle( 2 cx: number = 0, 3 cy: number = 0, 4 r: number = 200, 5): { x: number; y: number } { 6 const theta = Math.random() * Math.PI; 7 const u = Math.random() + Math.random(); 8 const r2 = u > 1 ? 2 - u : u; 9 const x = cx - r2 * r * Math.cos(theta); 10 const y = cy - r2 * r * Math.sin(theta); 11 12 // bounds could perhaps be configurable 13 const clampedY: number = Math.min(y, -150); 14 const clampedX = Math.max(Math.min(x, 150), -150); 15 16 return { x: clampedX, y: clampedY }; 17} 18 19export const getRandomDelay = () => -(Math.random() * 0.7 + 0.05); 20 21export const randomDuration = () => Math.random() * 0.07 + 0.23; 22 23
AnimatedPipeline/index.tsx
1'use client'; 2 3import { AnimatePresence, motion } from 'framer-motion'; 4import { 5 ArrowDown, 6 FileArchive, 7 FileBox, 8 FileDigit, 9 FileJson2, 10 FileLineChart, 11 FilePieChart, 12 FileScan, 13 FileSpreadsheet, 14 FileTextIcon, 15 LandPlot, 16 TableProperties, 17} from 'lucide-react'; 18import { useEffect, useState } from 'react'; 19import { v4 } from 'uuid'; 20 21import { AnimatedPipelineItem } from './Item'; 22 23const classes = { 24 icon: 'text-secondary-readable/[0.5]', 25}; 26 27const pipelineItemIcons = [ 28 <LandPlot className={classes.icon} key="land-plot-icon" />, 29 <FileSpreadsheet className={classes.icon} key="file-scan-icon" />, 30 <FileScan className={classes.icon} key="file-scan-icon" />, 31 <FilePieChart className={classes.icon} key="file-pie-chart-icon" />, 32 <FileLineChart className={classes.icon} key="file-line-chart-icon" />, 33 <FileBox className={classes.icon} key="file-box-icon" />, 34 <FileJson2 className={classes.icon} key="file-json2-icon" />, 35 <FileArchive className={classes.icon} key="file-archive-icon" />, 36 <FileDigit className={classes.icon} key="file-digit-icon" />, 37 <FileTextIcon className={classes.icon} key="file-text-icon" />, 38] as const; 39 40const getRandomIcon = () => { 41 return pipelineItemIcons[ 42 Math.floor(Math.random() * pipelineItemIcons.length) 43 ]; 44}; 45 46export const ANIMATION_SPEED_MS = 750; 47 48function AnimatedPipeline() { 49 const [data, setData] = useState<{ id: string; icon: React.ReactElement }[]>( 50 [], 51 ); 52 53 useEffect(() => { 54 const interval = setInterval(() => { 55 setData(() => [{ id: v4(), icon: getRandomIcon() }]); 56 }, ANIMATION_SPEED_MS); 57 58 //Clearing the interval 59 return () => clearInterval(interval); 60 }, []); 61 62 return ( 63 <div className="flex items-center justify-center"> 64 <div className="absolute mb-[4rem] select-none"> 65 <AnimatePresence> 66 {data.map((i) => ( 67 <AnimatedPipelineItem item={i} key={i.id} /> 68 ))} 69 </AnimatePresence> 70 </div> 71 72 <div 73 style={{ 74 backgroundImage: 75 'radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 35%, transparent 60%, transparent 100%)', 76 }} 77 className="z-10 pt-10 px-20 mb-10" 78 > 79 <div className="items-center justify-center flex flex-col space-y-2"> 80 <div className="relative"> 81 <div className="w-20 h-20"> 82 <motion.svg 83 xmlns="http://www.w3.org/2000/svg" 84 viewBox="0 0 24 24" 85 fill="none" 86 stroke="currentColor" 87 strokeWidth="1" 88 strokeLinecap="round" 89 strokeLinejoin="round" 90 className="text-primary w-20 h-20" 91 initial="hidden" 92 animate="visible" 93 > 94 <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" /> 95 <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" /> 96 <motion.path 97 animate={{ 98 color: ['#4f0063', 'hsl(119, 100%, 30%)'], 99 strokeWidth: [2, 2.1], 100 }} 101 transition={{ 102 duration: 0.75, 103 ease: 'easeInOut', 104 repeat: Infinity, 105 delay: 0.75, 106 repeatType: 'loop', 107 repeatDelay: 0, 108 }} 109 strokeWidth={2} 110 d="M6 6h.01" 111 /> 112 </motion.svg> 113 </div> 114 </div> 115 <ArrowDown className="text-primary/[0.5]" /> 116 <div className="flex items-center gap-2"> 117 <TableProperties className="text-primary w-7 h-7" /> 118 <span className="font-mono text-sm">Data Catalog</span> 119 </div> 120 </div> 121 </div> 122 </div> 123 ); 124} 125 126export default AnimatedPipeline; 127
AnimatedPipeline/Item.tsx
1import { motion } from 'framer-motion'; 2 3import { ANIMATION_SPEED_MS } from '.'; 4import { pickPointInHalfCircle } from '../helper'; 5 6export const AnimatedPipelineItem = ({ 7 item, 8}: { 9 item: { id: string; icon: React.ReactNode }; 10}) => ( 11 <motion.div 12 key={`animation-${item.id}`} 13 className="absolute flex shadow-xl" 14 transition={{ 15 duration: (ANIMATION_SPEED_MS / 1000) * 1.5, 16 ease: 'easeInOut', 17 times: [0, 0.2, 0.5, 0.8, 1], 18 }} 19 initial={{ ...pickPointInHalfCircle(), opacity: 0 }} 20 animate={{ 21 x: 0, 22 y: 0, 23 opacity: 1, 24 }} 25 > 26 {item.icon} 27 </motion.div> 28); 29