Skip to content

Instantly share code, notes, and snippets.

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