ComponentsChangelogPro

    Getting Started

    Animated Cards

    Components

    Backgrounds

    3d & Shaders

    Text Animations

    Buttons

    Docs
    Image Split

    Image Split

    This component provides a dynamic and visually captivating way to display images split into multiple sections. Each section can be offset, creating an intriguing sliced view of the image, which smoothly animates and reforms into its original state as it comes into view.

    Loading...

    Installation

    Install dependencies

    npm install

    npm install clsx tailwind-merge framer-motion

    Add util file

    lib/utils.ts

    import { ClassValue, clsx } from "clsx";
    import { twMerge } from "tailwind-merge";
     
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs));
    }

    Copy the source code

    image-split.tsx

    "use client";
     
    import React, { useState, useRef, useEffect } from "react";
    import { useInView } from "framer-motion";
    import { cn } from "@/lib/utils";
     
    interface ImageSplitProps extends React.HTMLAttributes<HTMLDivElement> {
      src: string;
      sections?: number;
      offsetStep?: number;
      duration?: number;
      className?: string;
    }
     
    export default function ImageSplit({
      src,
      sections = 9,
      offsetStep = 20,
      duration = 0.6,
      className,
      ...props
    }: ImageSplitProps) {
      const [imagePieces, setImagePieces] = useState<string[]>([]);
      const canvasRef = useRef<HTMLCanvasElement | null>(null);
      const parentRef = useRef<HTMLDivElement | null>(null);
      const imgRefs = useRef<(HTMLImageElement | null)[]>([]);
      const isInView = useInView(parentRef, { once: false });
     
      useEffect(() => {
        const image = new Image();
        image.src = src;
        image.onload = () => cutImageUp(image);
      }, [src, sections]);
     
      useEffect(() => {
        if (isInView) {
          animateSlices();
        } else {
          reverseSlices();
        }
      }, [isInView]);
     
      const cutImageUp = (image: HTMLImageElement) => {
        const canvas = canvasRef.current;
        if (!canvas) return;
        const pieceWidth = Math.floor(image.width / sections);
        const pieceHeight = image.height;
        const context = canvas.getContext("2d");
        if (!context) return;
     
        const newImagePieces: string[] = [];
        for (let i = 0; i < sections; i++) {
          canvas.width = pieceWidth;
          canvas.height = pieceHeight;
          context.clearRect(0, 0, canvas.width, canvas.height);
          context.drawImage(
            image,
            i * pieceWidth,
            0,
            pieceWidth,
            pieceHeight,
            0,
            0,
            pieceWidth,
            pieceHeight
          );
          newImagePieces.push(canvas.toDataURL());
        }
        setImagePieces(newImagePieces);
      };
     
      const getOffset = (index: number) => {
        if (index === 0 || index === sections - 1) return 0;
        const distanceFromEdge = Math.min(index, sections - 1 - index);
        return distanceFromEdge * offsetStep;
      };
     
      const animateSlices = () => {
        const startTime = performance.now();
     
        const animate = (time: number) => {
          const elapsed = time - startTime;
          const progress = Math.min(elapsed / (duration * 1000), 1);
     
          imgRefs.current.forEach((img, index) => {
            if (img) {
              const initialOffset = getOffset(index);
              img.style.top = `${initialOffset * (1 - progress)}px`;
            }
          });
     
          if (progress < 1) {
            requestAnimationFrame(animate);
          }
        };
     
        requestAnimationFrame(animate);
      };
     
      const reverseSlices = () => {
        const startTime = performance.now();
     
        const animate = (time: number) => {
          const elapsed = time - startTime;
          const progress = Math.min(elapsed / (duration * 1000), 1);
     
          imgRefs.current.forEach((img, index) => {
            if (img) {
              const initialOffset = getOffset(index);
              img.style.top = `${initialOffset * progress}px`;
            }
          });
     
          if (progress < 1) {
            requestAnimationFrame(animate);
          }
        };
     
        requestAnimationFrame(animate);
      };
     
      return (
        <div
          ref={parentRef}
          className={cn("flex relative w-full rounded-[inherit]", className)}
          {...props}
        >
          <canvas ref={canvasRef} className="hidden" />
     
          {imagePieces.map((piece, index) => (
            <img
              key={index}
              src={piece}
              alt={`section-${index}`}
              ref={(el) => {
                if (el) {
                  imgRefs.current[index] = el;
                }
              }}
              className={cn("object-contain", {
                "rounded-l-[inherit]": index === 0,
                "rounded-r-[inherit]": index === imagePieces.length - 1,
              })}
              style={{
                position: "relative",
                top: `${getOffset(index)}px`,
                width: `${100 / sections}%`,
              }}
            />
          ))}
        </div>
      );
    }

    Props

    PropTypeDescriptionDefault
    srcstringThe source URL of the image to be split into sections.-
    sectionsnumberThe number of sections to split the image into.9
    offsetStepnumberThe amount of vertical offset for each image section.20
    durationnumberThe duration of the animation (in milliseconds) for the slices to move.400

    Credits

  1. This component is inspired by Reflect
  2. The images are from ShadcnUI