Skip to content

Instantly share code, notes, and snippets.

@bilalesi
Forked from devalade/coverflow-animation.tsx
Created October 29, 2024 15:52
Show Gist options
  • Select an option

  • Save bilalesi/1eb2f18c4f4561c4312ac520af42a2ee to your computer and use it in GitHub Desktop.

Select an option

Save bilalesi/1eb2f18c4f4561c4312ac520af42a2ee to your computer and use it in GitHub Desktop.

Revisions

  1. @devalade devalade created this gist Oct 28, 2024.
    218 changes: 218 additions & 0 deletions coverflow-animation.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,218 @@
    import { PropsWithChildren, useEffect, useState } from "react";
    import { Container } from "~/components/container";
    import { ArrowLeft, ArrowRight } from "lucide-react";
    import { range } from "~/utils/range";

    const IMAGES = [
    {
    id: 1,
    url: "https://images.pexels.com/photos/1366919/pexels-photo-1366919.jpeg",
    },
    {
    id: 2,
    url: "https://images.pexels.com/photos/2662116/pexels-photo-2662116.jpeg",
    },
    {
    id: 3,
    url: "https://images.pexels.com/photos/1379636/pexels-photo-1379636.jpeg",
    },
    {
    id: 4,
    url: "https://images.pexels.com/photos/1470405/pexels-photo-1470405.jpeg",
    },
    {
    id: 5,
    url: "https://images.pexels.com/photos/3225517/pexels-photo-3225517.jpeg",
    },
    {
    id: 6,
    url: "https://images.pexels.com/photos/1133957/pexels-photo-1133957.jpeg",
    },
    {
    id: 7,
    url: "https://images.pexels.com/photos/1486974/pexels-photo-1486974.jpeg",
    },
    {
    id: 8,
    url: "https://images.pexels.com/photos/2662116/pexels-photo-2662116.jpeg",
    },
    {
    id: 9,
    url: "https://images.pexels.com/photos/1366630/pexels-photo-1366630.jpeg",
    },
    {
    id: 10,
    url: "https://images.pexels.com/photos/2486168/pexels-photo-2486168.jpeg",
    },
    ];

    const MAX_ITEMS = 4;

    export default function Carousel() {
    const [images, setImages] = useState(IMAGES);
    const [activeItemWidth, setActiveItemWidth] = useState<number>(60);
    const [currentIndex, setCurrentIndex] = useState(MAX_ITEMS);

    const scaleStep = 1 / MAX_ITEMS;
    const offsetStep = range(-3, MAX_ITEMS * 2 - 1).reduce(
    (acc: Record<number, number>, currentValue) => {
    acc[currentValue] =
    Math.sign(currentValue) * MAX_ITEMS - Math.abs(currentValue);
    return acc;
    },
    {}
    );

    function isBetween(value: number, start: number, stop: number) {
    return start <= value && value >= stop;
    }

    function isValidInterval(position: number) {
    return isBetween(position, -3, -1) || isBetween(position, 1, 3);
    }

    function computedOffset(position: number) {
    if (position !== 0 && position !== -1 && position !== 1) {
    return (
    (Math.abs(position) === MAX_ITEMS - 1 ? Math.abs(position) : 0) +
    range(1, Math.abs(position) + 1).reduce((acc, currentValue) => {
    if (offsetStep[currentValue]) {
    return acc + offsetStep[currentValue];
    }
    return acc;
    }, 0)
    );
    }

    return 0;
    }

    function computedWidth(position: number) {
    return MAX_ITEMS - Math.abs(position) + 1;
    }

    function computedScale(position: number) {
    if (position === 0) {
    return 1;
    } else if (
    isValidInterval(position) ||
    position === -3 ||
    position === -2
    ) {
    return 1 - scaleStep * Math.abs(position) + 0.1;
    }
    return 0;
    }

    function computedOpacity(position: number) {
    if (
    position === 0 ||
    position === -3 ||
    position === -2 ||
    isBetween(position, -3, -1)
    ) {
    return 1;
    }
    return 0;
    }

    function computedZIndex(position: number) {
    if (position !== 0) {
    return images.length - Math.abs(position);
    }
    return images.length + 1;
    }

    function onNext() {
    const imagesCopy = [...images];
    const firstItem = imagesCopy.shift();
    if (firstItem) {
    imagesCopy.push({ id: Date.now(), url: firstItem.url });
    }
    setImages(imagesCopy);
    }

    function onPrev() {
    const imagesCopy = [...images];
    const lastItem = imagesCopy.pop();
    if (lastItem) {
    imagesCopy.unshift({ id: Date.now(), url: lastItem.url });
    }
    setImages(imagesCopy);
    }

    return (
    <Container>
    <div className="relative h-screen w-full ">
    <Button onClick={onPrev} position="left">
    <ArrowLeft className="size-6 stroke-gray-900" />
    </Button>
    <Button onClick={onNext} position="right">
    <ArrowRight className="size-6 stroke-gray-900" />
    </Button>
    {images.map(({ url, id }, index) => {
    const shift = index - currentIndex;

    const width =
    shift === 0
    ? activeItemWidth + "rem"
    : Math.abs(shift) === MAX_ITEMS
    ? 0
    : computedWidth(shift) + "rem";

    return (
    <div
    key={id}
    style={
    {
    "--width": width,
    "--z-index": computedZIndex(shift),
    "--shift": shift,
    "--scale": computedScale(shift),
    "--opacity": computedOpacity(shift),
    "--skip":
    shift === 0
    ? "0rem"
    : Math.sign(shift) *
    (activeItemWidth / 2 +
    computedWidth(shift) +
    computedOffset(shift)) +
    "rem",
    "--height": "30rem",
    width: "var(--width)",
    zIndex: "var(--z-index)",
    transform:
    "translate(calc(-50% + var(--skip)), -50%) scale(var(--scale)",
    opacity: "var(--opacity)",
    } as any
    }
    className="inline-block flex-none w-[--width] h-[--height] inset-1/2 aspect-video absolute transition-all ease-in-out rounded-3xl overflow-hidden duration-700"
    >
    <img
    src={url}
    className="w-full h-full object-cover"
    // loading="lazy"
    />
    </div>
    );
    })}
    </div>
    </Container>
    );
    }

    function Button({
    children,
    position,
    onClick,
    }: PropsWithChildren<{ position: "left" | "right"; onClick: () => void }>) {
    return (
    <button
    data-position={position}
    onClick={onClick}
    className="bg-white rounded-full p-1.5 shadow-gray-800 shadow-sm absolute data-[position=left]:left-2 data-[position=right]:right-2 bottom-1/2 translate-y-1/2 z-10"
    >
    {children}
    </button>
    );
    }