-
-
Save bilalesi/1eb2f18c4f4561c4312ac520af42a2ee to your computer and use it in GitHub Desktop.
Coverflow animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment