Fixed the Jerks of the Marquee effect

This commit is contained in:
2026-02-21 15:19:04 +05:30
parent 29fc82c104
commit 58394b4236
4 changed files with 258 additions and 104 deletions

View File

@@ -6,13 +6,7 @@ import Link from "next/link";
export default function CTA() {
return (
<section className="py-24 bg-white relative overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-lime-100 rounded-full blur-3xl opacity-50" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-sky-100 rounded-full blur-3xl opacity-50" />
</div>
<section className="py-24 bg-black relative overflow-hidden">
<div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
@@ -20,20 +14,20 @@ export default function CTA() {
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<div className="inline-flex items-center gap-2 bg-black text-white px-4 py-2 text-xs font-medium tracking-wider rounded-full mb-8">
<Sparkles className="w-4 h-4 text-lime-400" />
<div className="inline-flex items-center gap-2 bg-lime-400 text-black px-4 py-2 text-xs font-bold tracking-wider rounded-full mb-8">
<Sparkles className="w-4 h-4" />
LIMITED TIME
</div>
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-black mb-6 leading-tight">
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-black tracking-tight text-white mb-6 leading-tight">
Ready to Showcase
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-lime-400 to-lime-600">
<span className="text-lime-400">
Your Masterpiece?
</span>
</h2>
<p className="text-zinc-600 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
<p className="text-zinc-400 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
Join hundreds of talented artists from SJEC in this creative
celebration. Submit your artwork and let the world vote for your
talent.
@@ -49,7 +43,7 @@ export default function CTA() {
</Link>
<Link
href="/rules"
className="inline-flex items-center justify-center gap-2 bg-white text-black px-10 py-5 text-base font-bold tracking-wide border-2 border-black hover:bg-black hover:text-white transition-all rounded-full"
className="inline-flex items-center justify-center gap-2 bg-transparent text-white px-10 py-5 text-base font-bold tracking-wide border-2 border-zinc-700 hover:border-lime-400 hover:text-lime-400 transition-all rounded-full"
>
VIEW RULES
</Link>
@@ -61,11 +55,11 @@ export default function CTA() {
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="mt-12 inline-flex items-center gap-3 bg-zinc-100 px-6 py-3 rounded-full"
className="mt-12 inline-flex items-center gap-3 bg-zinc-900 border border-zinc-800 px-6 py-3 rounded-full"
>
<span className="w-3 h-3 bg-red-500 rounded-full animate-pulse" />
<span className="text-zinc-700 font-medium">
Submissions close on <span className="text-black font-bold">February 28, 2026</span>
<span className="text-zinc-400 font-medium">
Submissions close on <span className="text-lime-400 font-bold">February 28, 2026</span>
</span>
</motion.div>
</motion.div>

View File

@@ -1,60 +1,235 @@
"use client";
import { useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { Palette, Monitor, FileImage, ArrowRight } from "lucide-react";
import { Palette, Monitor, FileImage, ArrowRight, LucideIcon } from "lucide-react";
import Link from "next/link";
import gsap from "gsap";
const categories = [
{
icon: Monitor,
title: "Digital Art",
description:
"Create stunning digital masterpieces using any software of your choice. From illustrations to photo manipulations.",
"Create stunning digital masterpieces using any software of your choice.",
color: "from-violet-500 to-purple-600",
bgColor: "bg-violet-50",
bgColor: "bg-violet-500",
},
{
icon: Palette,
title: "Paintings",
description:
"Traditional paintings in any medium - oil, acrylic, watercolor. Capture the essence of classic artistry.",
"Traditional paintings in any medium - oil, acrylic, watercolor.",
color: "from-orange-500 to-red-500",
bgColor: "bg-orange-50",
bgColor: "bg-orange-500",
},
{
icon: FileImage,
title: "Poster Making",
description:
"Design impactful posters that communicate powerful messages. Blend creativity with purpose.",
"Design impactful posters that communicate powerful messages.",
color: "from-lime-500 to-green-500",
bgColor: "bg-lime-50",
bgColor: "bg-lime-500",
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
};
// BounceCards Component for Category Cards
interface CategoryCard {
icon: LucideIcon;
title: string;
description: string;
color: string;
bgColor: string;
}
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
},
},
};
interface BounceCardsProps {
className?: string;
cards: CategoryCard[];
containerWidth?: number;
containerHeight?: number;
animationDelay?: number;
animationStagger?: number;
easeType?: string;
transformStyles?: string[];
enableHover?: boolean;
}
function BounceCards({
className = "",
cards,
containerWidth = 500,
containerHeight = 400,
animationDelay = 0.5,
animationStagger = 0.08,
easeType = "elastic.out(1, 0.8)",
transformStyles = [
"rotate(-8deg) translate(-200px)",
"rotate(0deg)",
"rotate(8deg) translate(200px)",
],
enableHover = true,
}: BounceCardsProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.fromTo(
".card",
{ scale: 0 },
{
scale: 1,
stagger: animationStagger,
ease: easeType,
delay: animationDelay,
}
);
}, containerRef);
return () => ctx.revert();
}, [animationDelay, animationStagger, easeType]);
const getNoRotationTransform = (transformStr: string): string => {
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
if (hasRotate) {
return transformStr.replace(/rotate\([\s\S]*?\)/, "rotate(0deg)");
} else if (transformStr === "none") {
return "rotate(0deg)";
} else {
return `${transformStr} rotate(0deg)`;
}
};
const getPushedTransform = (
baseTransform: string,
offsetX: number
): string => {
const translateRegex = /translate\(([-0-9.]+)px\)/;
const match = baseTransform.match(translateRegex);
if (match) {
const currentX = parseFloat(match[1]);
const newX = currentX + offsetX;
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
} else {
return baseTransform === "none"
? `translate(${offsetX}px)`
: `${baseTransform} translate(${offsetX}px)`;
}
};
const pushSiblings = (hoveredIdx: number) => {
const q = gsap.utils.selector(containerRef);
if (!enableHover || !containerRef.current) return;
cards.forEach((_, i) => {
const selector = q(`.card-${i}`);
gsap.killTweensOf(selector);
const baseTransform = transformStyles[i] || "none";
if (i === hoveredIdx) {
const noRotation = getNoRotationTransform(baseTransform);
gsap.to(selector, {
transform: noRotation,
zIndex: 10,
duration: 0.4,
ease: "back.out(1.4)",
overwrite: "auto",
});
} else {
const offsetX = i < hoveredIdx ? -80 : 80;
const pushedTransform = getPushedTransform(baseTransform, offsetX);
const distance = Math.abs(hoveredIdx - i);
const delay = distance * 0.05;
gsap.to(selector, {
transform: pushedTransform,
zIndex: 1,
duration: 0.4,
ease: "back.out(1.4)",
delay,
overwrite: "auto",
});
}
});
};
const resetSiblings = () => {
if (!enableHover || !containerRef.current) return;
const q = gsap.utils.selector(containerRef);
cards.forEach((_, i) => {
const selector = q(`.card-${i}`);
gsap.killTweensOf(selector);
const baseTransform = transformStyles[i] || "none";
gsap.to(selector, {
transform: baseTransform,
zIndex: cards.length - i,
duration: 0.4,
ease: "back.out(1.4)",
overwrite: "auto",
});
});
};
return (
<div
className={`relative flex items-center justify-center ${className}`}
ref={containerRef}
style={{
width: containerWidth,
height: containerHeight,
}}
>
{cards.map((card, idx) => (
<div
key={idx}
className={`card card-${idx} absolute w-[220px] h-[280px] border-4 border-zinc-800 rounded-[24px] overflow-hidden bg-zinc-900 p-6 flex flex-col cursor-pointer`}
style={{
boxShadow: "0 8px 40px rgba(0, 0, 0, 0.5)",
transform: transformStyles[idx] || "none",
zIndex: cards.length - idx,
}}
onMouseEnter={() => pushSiblings(idx)}
onMouseLeave={resetSiblings}
>
{/* Icon */}
<div
className={`w-14 h-14 rounded-xl bg-gradient-to-br ${card.color} flex items-center justify-center mb-4 flex-shrink-0`}
>
<card.icon className="w-7 h-7 text-white" />
</div>
{/* Title */}
<h3 className="text-xl font-bold text-white mb-2">{card.title}</h3>
{/* Description */}
<p className="text-zinc-400 text-sm leading-relaxed flex-grow">
{card.description}
</p>
{/* Submit Link */}
<div className="mt-4 pt-4 border-t border-zinc-800">
<span className="inline-flex items-center gap-2 text-lime-400 text-sm font-semibold">
Submit Now
<ArrowRight className="w-4 h-4" />
</span>
</div>
</div>
))}
</div>
);
}
export default function Categories() {
const transformStyles = [
"rotate(-8deg) translate(-220px)",
"rotate(0deg)",
"rotate(8deg) translate(220px)",
];
return (
<section className="py-24 bg-white">
<section className="py-24 bg-black overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<motion.div
@@ -67,63 +242,34 @@ export default function Categories() {
<span className="inline-block bg-lime-400 text-black px-4 py-1.5 text-xs font-bold tracking-wider rounded-full mb-4">
CATEGORIES
</span>
<h2 className="text-4xl sm:text-5xl font-black tracking-tight text-black mb-4">
<h2 className="text-4xl sm:text-5xl font-black tracking-tight text-white mb-4">
Three Ways to
<span className="text-transparent bg-clip-text bg-gradient-to-r from-lime-400 to-lime-600">
{" "}
Express
</span>
<span className="text-lime-400"> Express</span>
</h2>
<p className="text-zinc-600 text-lg max-w-2xl mx-auto">
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
Choose your canvas, unleash your creativity. Each category offers a
unique way to showcase your artistic vision.
</p>
</motion.div>
{/* Categories Grid */}
{/* BounceCards with Category Cards */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="grid md:grid-cols-3 gap-8"
transition={{ duration: 0.6, delay: 0.2 }}
className="flex justify-center"
>
{categories.map((category) => (
<motion.div
key={category.title}
variants={itemVariants}
className={`group relative ${category.bgColor} rounded-3xl p-8 hover:shadow-2xl transition-all duration-500 overflow-hidden`}
>
{/* Background Gradient on Hover */}
<div
className={`absolute inset-0 bg-gradient-to-br ${category.color} opacity-0 group-hover:opacity-10 transition-opacity duration-500`}
/>
{/* Icon */}
<div
className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${category.color} flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}
>
<category.icon className="w-8 h-8 text-white" />
</div>
{/* Content */}
<h3 className="text-2xl font-bold text-black mb-3">
{category.title}
</h3>
<p className="text-zinc-600 mb-6 leading-relaxed">
{category.description}
</p>
{/* Link */}
<Link
href="/submit"
className="inline-flex items-center gap-2 text-black font-semibold group-hover:gap-4 transition-all"
>
Submit Now
<ArrowRight className="w-4 h-4" />
</Link>
</motion.div>
))}
<BounceCards
cards={categories}
containerWidth={700}
containerHeight={380}
animationDelay={0.3}
animationStagger={0.1}
easeType="elastic.out(1, 0.8)"
transformStyles={transformStyles}
enableHover={true}
/>
</motion.div>
</div>
</section>

View File

@@ -14,10 +14,12 @@ function TileGrid() {
const handleMouseEnter = useCallback((index: number) => {
const tile = tilesRef.current[index];
if (tile) {
gsap.killTweensOf(tile);
gsap.to(tile, {
rotateY: 180,
duration: 0.4,
ease: "power2.out",
duration: 0.5,
ease: "power2.inOut",
overwrite: true,
});
}
}, []);
@@ -25,10 +27,12 @@ function TileGrid() {
const handleMouseLeave = useCallback((index: number) => {
const tile = tilesRef.current[index];
if (tile) {
gsap.killTweensOf(tile);
gsap.to(tile, {
rotateY: 0,
duration: 0.4,
ease: "power2.out",
duration: 0.5,
ease: "power2.inOut",
overwrite: true,
});
}
}, []);
@@ -53,7 +57,10 @@ function TileGrid() {
<div
ref={(el) => { tilesRef.current[index] = el; }}
className="w-full h-full relative"
style={{ transformStyle: "preserve-3d" }}
style={{
transformStyle: "preserve-3d",
willChange: "transform",
}}
>
{/* Front - Black */}
<div
@@ -133,12 +140,12 @@ export default function Hero() {
</h1>
<div ref={contentRef}>
<p className="text-zinc-400 text-base sm:text-lg max-w-lg mx-auto mb-10 leading-relaxed font-light">
<p className="text-zinc-300 text-base sm:text-lg max-w-lg mx-auto mb-10 leading-relaxed font-light drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)]">
From canvas to screen, showcase your creativity and let your
artwork speak to the world.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center pointer-events-auto">
<div className="flex flex-col sm:flex-row gap-4 justify-center pointer-events-auto mb-12">
<Link
href="/register"
className="inline-flex items-center justify-center gap-2 bg-lime-400 text-black px-10 py-4 text-sm font-bold tracking-wider hover:bg-lime-300 transition-all hover:scale-105 rounded-full shadow-[0_0_30px_rgba(163,230,53,0.3)]"
@@ -152,15 +159,15 @@ export default function Hero() {
EXPLORE GALLERY
</Link>
</div>
{/* Scroll Indicator */}
<div className="flex flex-col items-center gap-2">
<span className="text-lime-400 text-[10px] tracking-[0.3em] uppercase drop-shadow-[0_0_10px_rgba(163,230,53,0.8)]">Scroll</span>
<div className="w-px h-8 bg-gradient-to-b from-lime-400 to-transparent shadow-[0_0_10px_rgba(163,230,53,0.5)]" />
</div>
</div>
</div>
</div>
{/* Scroll Indicator - positioned above the marquee */}
<div className="absolute bottom-28 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-zinc-500 z-10">
<span className="text-[10px] tracking-[0.3em] uppercase">Scroll</span>
<div className="w-px h-8 bg-gradient-to-b from-zinc-500 to-transparent" />
</div>
</div>
{/* Bottom Marquee */}