Created
August 7, 2025 11:50
-
-
Save albertogalca/a46e9f8eb23286f5eca2559c88de0f34 to your computer and use it in GitHub Desktop.
Hexagonal animation for Wanda
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useEffect, useRef } from 'react'; | |
import { gsap } from 'gsap'; | |
export default function Hexagon() { | |
const hexagonRef = useRef<SVGSVGElement>(null); | |
const pathRef = useRef<SVGPathElement>(null); | |
const secondaryLinesRef = useRef<(SVGPathElement | null)[]>([]); | |
const cornerLabels = ['Top', 'Top Right', 'Bottom Right', 'Bottom', 'Bottom Left', 'Top Left']; | |
useEffect(() => { | |
if (!pathRef.current) return; | |
const path = pathRef.current; | |
const pathLength = path.getTotalLength(); | |
gsap.set(path, { | |
strokeDasharray: pathLength, | |
strokeDashoffset: pathLength | |
}); | |
secondaryLinesRef.current.forEach((path) => { | |
if (path) { | |
const pathLength = path.getTotalLength(); | |
gsap.set(path, { | |
strokeDasharray: pathLength, | |
strokeDashoffset: pathLength | |
}); | |
} | |
}); | |
}, []); | |
const getPathToCorner = (cornerIndex: number) => { | |
const startPoint = hexagonPoints[0]; // Start from top | |
let pathString = `M ${startPoint.x} ${startPoint.y}`; | |
// For top corner (index 0), complete the entire hexagon clockwise | |
if (cornerIndex === 0) { | |
// Complete full hexagon: top -> top right -> bottom right -> bottom -> bottom left -> top left -> back to top | |
for (let i = 1; i < hexagonPoints.length; i++) { | |
pathString += ` L ${hexagonPoints[i].x} ${hexagonPoints[i].y}`; | |
} | |
pathString += ` L ${hexagonPoints[0].x} ${hexagonPoints[0].y}`; // Close the loop | |
return pathString; | |
} | |
// For other corners, go clockwise around hexagon to reach them | |
for (let i = 1; i <= cornerIndex; i++) { | |
const point = hexagonPoints[i]; | |
pathString += ` L ${point.x} ${point.y}`; | |
} | |
return pathString; | |
}; | |
const handleTextHover = (index: number, isEntering: boolean) => { | |
const secondaryLine = secondaryLinesRef.current[index]; | |
if (!secondaryLine) return; | |
// Kill any existing animations on this element to prevent conflicts | |
gsap.killTweensOf(secondaryLine); | |
if (isEntering) { | |
// Draw animation: from current position to 0 (draws the line) | |
gsap.to(secondaryLine, { | |
strokeDashoffset: 0, | |
opacity: 1, | |
duration: 0.8, | |
ease: "power2.out" | |
}); | |
} else { | |
// Reverse animation: from current position to full offset (undoes/erases the line) | |
gsap.to(secondaryLine, { | |
strokeDashoffset: secondaryLine.getTotalLength(), | |
duration: 0.6, | |
ease: "power2.in" | |
}); | |
} | |
}; | |
const hexagonPoints = [ | |
{ x: 200, y: 50 }, // Top | |
{ x: 300, y: 100 }, // Top Right | |
{ x: 300, y: 200 }, // Bottom Right | |
{ x: 200, y: 250 }, // Bottom | |
{ x: 100, y: 200 }, // Bottom Left | |
{ x: 100, y: 100 } // Top Left | |
]; | |
const textPositions = [ | |
{ x: 200, y: 15 }, // Top - more margin | |
{ x: 350, y: 100 }, // Top Right - more margin | |
{ x: 350, y: 200 }, // Bottom Right - more margin | |
{ x: 200, y: 295 }, // Bottom - more margin | |
{ x: 50, y: 200 }, // Bottom Left - more margin | |
{ x: 50, y: 100 } // Top Left - more margin | |
]; | |
const hexagonCenter = { x: 200, y: 150 }; | |
return ( | |
<div className="flex items-center justify-center min-h-screen bg-gray-100"> | |
<svg | |
ref={hexagonRef} | |
width="400" | |
height="300" | |
viewBox="0 0 400 300" | |
> | |
<path | |
ref={pathRef} | |
d={`M ${hexagonPoints[0].x} ${hexagonPoints[0].y} | |
L ${hexagonPoints[1].x} ${hexagonPoints[1].y} | |
L ${hexagonPoints[2].x} ${hexagonPoints[2].y} | |
L ${hexagonPoints[3].x} ${hexagonPoints[3].y} | |
L ${hexagonPoints[4].x} ${hexagonPoints[4].y} | |
L ${hexagonPoints[5].x} ${hexagonPoints[5].y} Z`} | |
fill="none" | |
stroke="#3b82f6" | |
strokeWidth="3" | |
/> | |
{textPositions.map((pos, index) => ( | |
<path | |
key={`default-line-${index}`} | |
d={getPathToCorner(index)} | |
fill="none" | |
stroke="#000000" | |
strokeWidth="1" | |
opacity="0.8" | |
/> | |
))} | |
{textPositions.map((pos, index) => ( | |
<path | |
key={`secondary-line-${index}`} | |
ref={(el) => (secondaryLinesRef.current[index] = el)} | |
d={getPathToCorner(index)} | |
fill="none" | |
stroke="#ef4444" | |
strokeWidth="3" | |
strokeDasharray="100%" | |
strokeDashoffset="100%" | |
opacity="0" | |
/> | |
))} | |
{textPositions.map((pos, index) => ( | |
<text | |
key={`text-${index}`} | |
x={pos.x} | |
y={pos.y} | |
textAnchor="middle" | |
dominantBaseline="middle" | |
className="fill-gray-700 text-sm font-semibold cursor-default hover:fill-blue-600 select-none" | |
onMouseEnter={() => handleTextHover(index, true)} | |
onMouseLeave={() => handleTextHover(index, false)} | |
> | |
{cornerLabels[index]} | |
</text> | |
))} | |
</svg> | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment