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() { export default function CTA() {
return ( return (
<section className="py-24 bg-white relative overflow-hidden"> <section className="py-24 bg-black 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>
<div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -20,20 +14,20 @@ export default function CTA() {
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.8 }} 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"> <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 text-lime-400" /> <Sparkles className="w-4 h-4" />
LIMITED TIME LIMITED TIME
</div> </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 Ready to Showcase
<br /> <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? Your Masterpiece?
</span> </span>
</h2> </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 Join hundreds of talented artists from SJEC in this creative
celebration. Submit your artwork and let the world vote for your celebration. Submit your artwork and let the world vote for your
talent. talent.
@@ -49,7 +43,7 @@ export default function CTA() {
</Link> </Link>
<Link <Link
href="/rules" 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 VIEW RULES
</Link> </Link>
@@ -61,11 +55,11 @@ export default function CTA() {
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ delay: 0.3 }} 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="w-3 h-3 bg-red-500 rounded-full animate-pulse" />
<span className="text-zinc-700 font-medium"> <span className="text-zinc-400 font-medium">
Submissions close on <span className="text-black font-bold">February 28, 2026</span> Submissions close on <span className="text-lime-400 font-bold">February 28, 2026</span>
</span> </span>
</motion.div> </motion.div>
</motion.div> </motion.div>

View File

@@ -1,60 +1,235 @@
"use client"; "use client";
import { useEffect, useRef } from "react";
import { motion } from "framer-motion"; 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 Link from "next/link";
import gsap from "gsap";
const categories = [ const categories = [
{ {
icon: Monitor, icon: Monitor,
title: "Digital Art", title: "Digital Art",
description: 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", color: "from-violet-500 to-purple-600",
bgColor: "bg-violet-50", bgColor: "bg-violet-500",
}, },
{ {
icon: Palette, icon: Palette,
title: "Paintings", title: "Paintings",
description: 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", color: "from-orange-500 to-red-500",
bgColor: "bg-orange-50", bgColor: "bg-orange-500",
}, },
{ {
icon: FileImage, icon: FileImage,
title: "Poster Making", title: "Poster Making",
description: 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", color: "from-lime-500 to-green-500",
bgColor: "bg-lime-50", bgColor: "bg-lime-500",
}, },
]; ];
const containerVariants = { // BounceCards Component for Category Cards
hidden: { opacity: 0 }, interface CategoryCard {
visible: { icon: LucideIcon;
opacity: 1, title: string;
transition: { description: string;
staggerChildren: 0.2, color: string;
}, bgColor: string;
}, }
};
const itemVariants = { interface BounceCardsProps {
hidden: { opacity: 0, y: 30 }, className?: string;
visible: { cards: CategoryCard[];
opacity: 1, containerWidth?: number;
y: 0, containerHeight?: number;
transition: { animationDelay?: number;
duration: 0.6, 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() { export default function Categories() {
const transformStyles = [
"rotate(-8deg) translate(-220px)",
"rotate(0deg)",
"rotate(8deg) translate(220px)",
];
return ( 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"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */} {/* Section Header */}
<motion.div <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"> <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 CATEGORIES
</span> </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 Three Ways to
<span className="text-transparent bg-clip-text bg-gradient-to-r from-lime-400 to-lime-600"> <span className="text-lime-400"> Express</span>
{" "}
Express
</span>
</h2> </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 Choose your canvas, unleash your creativity. Each category offers a
unique way to showcase your artistic vision. unique way to showcase your artistic vision.
</p> </p>
</motion.div> </motion.div>
{/* Categories Grid */} {/* BounceCards with Category Cards */}
<motion.div <motion.div
variants={containerVariants} initial={{ opacity: 0 }}
initial="hidden" whileInView={{ opacity: 1 }}
whileInView="visible"
viewport={{ once: true }} 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) => ( <BounceCards
<motion.div cards={categories}
key={category.title} containerWidth={700}
variants={itemVariants} containerHeight={380}
className={`group relative ${category.bgColor} rounded-3xl p-8 hover:shadow-2xl transition-all duration-500 overflow-hidden`} animationDelay={0.3}
> animationStagger={0.1}
{/* Background Gradient on Hover */} easeType="elastic.out(1, 0.8)"
<div transformStyles={transformStyles}
className={`absolute inset-0 bg-gradient-to-br ${category.color} opacity-0 group-hover:opacity-10 transition-opacity duration-500`} enableHover={true}
/> />
{/* 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>
))}
</motion.div> </motion.div>
</div> </div>
</section> </section>

View File

@@ -14,10 +14,12 @@ function TileGrid() {
const handleMouseEnter = useCallback((index: number) => { const handleMouseEnter = useCallback((index: number) => {
const tile = tilesRef.current[index]; const tile = tilesRef.current[index];
if (tile) { if (tile) {
gsap.killTweensOf(tile);
gsap.to(tile, { gsap.to(tile, {
rotateY: 180, rotateY: 180,
duration: 0.4, duration: 0.5,
ease: "power2.out", ease: "power2.inOut",
overwrite: true,
}); });
} }
}, []); }, []);
@@ -25,10 +27,12 @@ function TileGrid() {
const handleMouseLeave = useCallback((index: number) => { const handleMouseLeave = useCallback((index: number) => {
const tile = tilesRef.current[index]; const tile = tilesRef.current[index];
if (tile) { if (tile) {
gsap.killTweensOf(tile);
gsap.to(tile, { gsap.to(tile, {
rotateY: 0, rotateY: 0,
duration: 0.4, duration: 0.5,
ease: "power2.out", ease: "power2.inOut",
overwrite: true,
}); });
} }
}, []); }, []);
@@ -53,7 +57,10 @@ function TileGrid() {
<div <div
ref={(el) => { tilesRef.current[index] = el; }} ref={(el) => { tilesRef.current[index] = el; }}
className="w-full h-full relative" className="w-full h-full relative"
style={{ transformStyle: "preserve-3d" }} style={{
transformStyle: "preserve-3d",
willChange: "transform",
}}
> >
{/* Front - Black */} {/* Front - Black */}
<div <div
@@ -133,12 +140,12 @@ export default function Hero() {
</h1> </h1>
<div ref={contentRef}> <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 From canvas to screen, showcase your creativity and let your
artwork speak to the world. artwork speak to the world.
</p> </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 <Link
href="/register" 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)]" 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,14 +159,14 @@ export default function Hero() {
EXPLORE GALLERY EXPLORE GALLERY
</Link> </Link>
</div> </div>
</div>
</div>
</div>
{/* Scroll Indicator - positioned above the marquee */} {/* Scroll Indicator */}
<div className="absolute bottom-28 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-zinc-500 z-10"> <div className="flex flex-col items-center gap-2">
<span className="text-[10px] tracking-[0.3em] uppercase">Scroll</span> <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-zinc-500 to-transparent" /> <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> </div>
</div> </div>

View File

@@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
}; };
export default nextConfig; export default nextConfig;