-
-
Save deifos/d5c1149d9b665402b31e98cdab119cdc 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