messy letters
Where did that last letter go again?
> COMPONENTS:
> motion.div
> spring
> useAnimate
> layout
> KEYWORDS:
textstringstaggertranslatescaleopacitytimelinesequence
> SOURCE CODE:
"use client"
import { AnimationSequence, motion, stagger, useAnimate } from "motion/react"
import { useEffect, useState } from "react"
interface Position {
x: number;
y: number;
}
export function generatePositions(count: number, maxDistance: number, size: number): Position[] {
const positions: Position[] = [];
const isOverlapping = (x: number, y: number) => {
const previousElement = positions.find(
position =>
((position.x > x && position.x <= x + size) && (position.y > y && position.y <= y + size)) ||
((position.x > x && position.x <= x + size) && (y > position.y && y <= position.y + size )) ||
((x > position.x && x <= position.x + size) && (position.y > y && position.y <= y + size)) ||
((x > position.x && x <= position.x + size) && (y > position.y && y <= position.y + size))
)
return !!previousElement
}
for (let i = 0; i < count; i++) {
let x, y
do {
x = Math.random() * (maxDistance * 2) - maxDistance
y = Math.random() * (maxDistance * 2) - maxDistance
} while (isOverlapping(x, y))
positions.push({ x, y })
}
console.log(positions)
return positions
}
const MessyLetters = ({ word = 'animations' }: { isHovered?: boolean, word?: string }) => {
const [scope, animate] = useAnimate()
const [positions, setPositions] = useState<{ x: number; y: number }[]>([])
const [isReady, setIsReady] = useState(false)
const [isAnimationOver, setIsAnimationOver] = useState(false)
useEffect(() => {
setIsReady(false)
const newPositions = generatePositions(word.length, 100, 40)
setPositions(newPositions)
setIsReady(true)
}, [word])
useEffect(() => {
const sequence: AnimationSequence = [
['.letter', { opacity: 1, scale: 1 }, { duration: 0.3, delay: stagger(0.1) }],
['.letter', { scale: 1.5 }, { duration: 0.2, type: 'spring', stiffness: 200 }],
['.letter', { scale: 1, x: 0, y: 0 }, { duration: 0.3, type: 'spring', stiffness: 75 }],
]
if (isReady && isHovered) {
// Mandatory setTimeout
setTimeout(() => {
animate(sequence).then(() => setIsAnimationOver(true))
}, 0)
}
}, [animate, isHovered, isReady, scope])
return (
<div className="w-40 h-40 flex items-center justify-center" ref={scope}>
<div className="flex items-center justify-center relative">
{isReady && word.split('').map((letter, index) => (
<motion.div
className={"text-orange-400 text-5xl letter" + isAnimationOver ? "" : "absolute"}
initial={{ x: positions[index].x, y: positions[index].y, opacity: 0, scale: 0 }}
key={letter + "-" + index}
layout
>
{letter}
</motion.div>
))}
</div>
</div>
)
}
export default MessyLetters