Created
October 28, 2024 14:58
-
-
Save devalade/b598e1aee188d6c39f6621b98263e8ec to your computer and use it in GitHub Desktop.
Coverflow animation
This file contains 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