Logo SmoothUI
ComponentsFluid Morph

Fluid Morph

Component that morphs a fluid shape into another fluid shape

Code

Install with shadcn Beta

Terminal

npx shadcn@latest add "https://smoothui.dev/r/fluid-morph.json"

Manual install

Terminal

npm install motion

FluidMorph.tsx

"use client"

import { useEffect, useRef, useState } from "react"
import { animate } from "motion/react"

const numbers = [
  "M128 40L230 215.766H26L128 40Z",
  "M212 132L132 212M196 44L44 196",
  "M45.125 134.46C45.125 148.06 54.6705 159.909 68.1345 161.89C77.374 163.25 86.707 164.295 96.125 165.026V204.5L130.771 169.854C133.121 167.519 136.274 166.172 139.585 166.089C155.747 165.641 171.869 164.239 187.865 161.89C201.329 159.909 210.875 148.069 210.875 134.452V83.2985C210.875 69.6815 201.329 57.841 187.865 55.8605C168.043 52.9511 148.035 51.4937 128 51.5C107.668 51.5 87.676 52.9875 68.1345 55.8605C54.6705 57.841 45.125 69.69 45.125 83.2985V134.452V134.46Z",
]

type CircleRef = SVGCircleElement | null
type PathRef = SVGPathElement | null

export default function FluidMorph() {
  const [index, setIndex] = useState(0)
  const circles = useRef<CircleRef[]>([])
  const paths = useRef<PathRef[]>([])

  const nbOfCircles = 60
  const radius = 10

  useEffect(() => {
    const currentPath = paths.current[index]
    if (currentPath) {
      const length = currentPath.getTotalLength()
      const step = length / nbOfCircles

      circles.current.forEach((circle, i) => {
        if (circle && currentPath) {
          const { x, y } = currentPath.getPointAtLength(i * step)
          animate(
            circle,
            { cx: x, cy: y },
            { delay: i * 0.015, ease: "easeOut", type: "spring" }
          )
        }
      })
    }
  }, [index])

  return (
    <main className="flex flex-col items-center justify-center p-4">
      <svg
        viewBox="0 0 256 256"
        className="w-[500px]"
        style={{ filter: "url(#filter)" }}
      >
        <defs>
          <filter id="filter">
            <feGaussianBlur stdDeviation="12" result="blur-sm" />
            <feColorMatrix
              in="blur-sm"
              mode="matrix"
              values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 25 -15"
              result="filter"
            />
          </filter>
        </defs>
        <g>
          {numbers.map((path, i) => (
            <path
              className="hidden"
              key={`p_${i}`}
              ref={(ref) => {
                if (ref) paths.current[i] = ref
              }}
              d={path}
            />
          ))}
        </g>

        <g>
          {[...Array(nbOfCircles)].map((_, i) => (
            <circle
              className="fill-light-950 dark:fill-dark-950"
              key={`c_${i}`}
              ref={(ref) => {
                if (ref) circles.current[i] = ref
              }}
              cx="128"
              cy="128"
              r={radius}
            />
          ))}
        </g>
      </svg>
      <div className="flex cursor-pointer flex-row gap-4 text-3xl">
        {numbers.map((_, i) => (
          <span
            key={i}
            className={`border-light-200 bg-light-50 dark:border-dark-200 dark:bg-dark-50 flex h-12 w-12 items-center justify-center rounded-full border text-sm transition disabled:opacity-50 ${
              i === index
                ? "cursor-not-allowed text-amber-500"
                : "text-light-950 dark:text-dark-950 cursor-pointer"
            }`}
            onClick={() => setIndex(i)}
          >
            {i + 1}
          </span>
        ))}
      </div>
    </main>
  )
}