Return home

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