diff --git a/next.config.ts b/next.config.ts index e9ffa30..a9194aa 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "images.unsplash.com", + }, + ], + }, }; export default nextConfig; diff --git a/src/app/gallery/page.tsx b/src/app/gallery/page.tsx new file mode 100644 index 0000000..e4e4899 --- /dev/null +++ b/src/app/gallery/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import Image from "next/image"; +import Gallery from "@/components/Gallery"; + +export default function GalleryPage() { + return ( +
+ {/* Same space background as home & events pages */} +
+ Space Background +
+
+ +
+
+ ); +} diff --git a/src/components/Cinematic3DGallery.css b/src/components/Cinematic3DGallery.css new file mode 100644 index 0000000..7e11c1c --- /dev/null +++ b/src/components/Cinematic3DGallery.css @@ -0,0 +1,145 @@ +/* ========================================================================== + CINEMATIC 3D GALLERY STYLING (SCOPED STRICTLY TO .cinematic-gallery-*) + ========================================================================== */ + +.cinematic-gallery-wrapper { + position: relative; + width: 100%; + min-height: 100vh; + overflow: hidden; + user-select: none; + -webkit-user-select: none; + font-family: inherit; +} + +/* Dynamic 3D Scene Perspective Stage */ +.cinematic-gallery-perspective { + position: relative; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + perspective: 1200px; + perspective-origin: 50% 45%; + transform-style: preserve-3d; +} + +@media (min-width: 1024px) { + .cinematic-gallery-perspective { + perspective: 1400px; + } +} + +.cinematic-gallery-stage { + position: relative; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + transform-style: preserve-3d; + touch-action: pan-y; + cursor: grab; +} + +.cinematic-gallery-stage-dragging, +.cinematic-gallery-stage-dragging .cinematic-gallery-card { + cursor: grabbing !important; +} + +/* Card 3D Frame & Surface */ +.cinematic-gallery-card-frame { + position: relative; + width: 100%; + height: 100%; + border-radius: 18px; + overflow: hidden; + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.75), + 0 0 30px rgba(0, 0, 0, 0.5), + inset 0 1px 1px rgba(255, 255, 255, 0.25); + transition: box-shadow 0.4s ease, border-color 0.4s ease; + border: 1px solid rgba(255, 255, 255, 0.12); + background-color: #0d1527; +} + +.cinematic-gallery-card-active .cinematic-gallery-card-frame { + border-color: rgba(251, 191, 36, 0.4); + box-shadow: + 0 30px 60px -10px rgba(0, 0, 0, 0.9), + 0 0 35px rgba(251, 191, 36, 0.25), + inset 0 1px 2px rgba(255, 255, 255, 0.4); +} + +/* Card Image */ +.cinematic-gallery-card-img { + object-fit: cover; + object-position: center; + width: 100%; + height: 100%; + pointer-events: none; + transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1); +} + +.cinematic-gallery-card-frame:hover .cinematic-gallery-card-img { + transform: scale(1.06); +} + +/* Subtle Vignette & Light Reflections */ +.cinematic-gallery-card-overlay { + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient( + to bottom, + rgba(2, 7, 18, 0) 0%, + rgba(2, 7, 18, 0.2) 60%, + rgba(2, 7, 18, 0.85) 100% + ); +} + +/* Control Buttons Glassmorphism */ +.cinematic-nav-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + background: rgba(13, 21, 39, 0.65); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.15); + color: #ffffff; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); +} + +.cinematic-nav-btn:hover { + background: rgba(251, 191, 36, 0.2); + border-color: rgba(251, 191, 36, 0.6); + color: #fbbf24; + transform: scale(1.08); + box-shadow: 0 0 20px rgba(251, 191, 36, 0.3); +} + +.cinematic-nav-btn:active { + transform: scale(0.95); +} + +/* Indicator Dots */ +.cinematic-dot { + width: 8px; + height: 8px; + border-radius: 9999px; + background: rgba(255, 255, 255, 0.25); + transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1); + cursor: pointer; +} + +.cinematic-dot-active { + width: 28px; + background: linear-gradient(90deg, #fbbf24, #2dd4bf); + box-shadow: 0 0 12px rgba(251, 191, 36, 0.6); +} diff --git a/src/components/Cinematic3DGallery.tsx b/src/components/Cinematic3DGallery.tsx new file mode 100644 index 0000000..93bafaa --- /dev/null +++ b/src/components/Cinematic3DGallery.tsx @@ -0,0 +1,583 @@ +"use client"; + +import React, { useState, useEffect, useRef, useCallback } from "react"; +import Image from "next/image"; +import { motion, AnimatePresence, useMotionValue, useSpring } from "framer-motion"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import "./Cinematic3DGallery.css"; + +export interface GalleryItem { + id: string | number; + image: string; + title: string; + subtitle: string; + category: string; + location?: string; +} + +const DEFAULT_GALLERY_ITEMS: GalleryItem[] = [ + { + id: 1, + image: "https://images.unsplash.com/photo-1511497584788-876761c119ef?q=80&w=1200&auto=format&fit=crop", + title: "MIMISA ROCKS", + location: "AUSTRALIA", + subtitle: "A piece of heaven", + category: "#Oceania", + }, + { + id: 2, + image: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=1200&auto=format&fit=crop", + title: "EMERALD COAST", + location: "MALDIVES", + subtitle: "Crystal clear ocean waters", + category: "#Coastal", + }, + { + id: 3, + image: "https://images.unsplash.com/photo-1516426122078-c23e76319801?q=80&w=1200&auto=format&fit=crop", + title: "SERENGETI SAFARI", + location: "KENYA", + subtitle: "Majestic animal frontiers", + category: "#Wildlife", + }, + { + id: 4, + image: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=1200&auto=format&fit=crop", + title: "DOLOMITE PEAKS", + location: "ITALY", + subtitle: "Sun-drenched alpine spires", + category: "#Mountains", + }, + { + id: 5, + image: "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?q=80&w=1200&auto=format&fit=crop", + title: "AURORA BOREALIS", + location: "NORWAY", + subtitle: "Luminous green arctic night", + category: "#PolarSky", + }, + { + id: 6, + image: "https://images.unsplash.com/photo-1448375240586-882707db888b?q=80&w=1200&auto=format&fit=crop", + title: "JUNGLE TREK", + location: "COSTA RICA", + subtitle: "Hidden crystal waterfalls", + category: "#Rainforest", + }, + { + id: 7, + image: "https://images.unsplash.com/photo-1509316975850-ff9c5deb0cd9?q=80&w=1200&auto=format&fit=crop", + title: "SAHARA DUNES", + location: "MOROCCO", + subtitle: "Endless golden sand twilight", + category: "#Desert", + }, + { + id: 8, + image: "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=1200&auto=format&fit=crop", + title: "MISTY VALLEY", + location: "SWITZERLAND", + subtitle: "Quiet morning fog lake", + category: "#Nature", + }, +]; + +// Ambient floating particles +const AMBIENT_PARTICLES = Array.from({ length: 16 }, (_, i) => ({ + id: i, + x: Math.sin(i * 1.37) * 44 + 50, // 6% - 94% + y: Math.cos(i * 0.93) * 40 + 50, // 10% - 90% + size: (i % 3) + 2.5, + duration: 4.5 + (i % 4) * 1.6, + delay: (i % 4) * 0.6, +})); + +interface Cinematic3DGalleryProps { + items?: GalleryItem[]; +} + +// ─── Coverflow Circular Track Math ──────────────────────────────────────────── +function getCoverflowTransform( + diff: number, // signed offset from active: -3 -2 -1 0 1 2 3 + isMobile: boolean, + isTablet: boolean, +) { + const distance = Math.abs(diff); + if (distance > 3) return { x: 0, z: -320, rotateY: 0, scale: 0.3, opacity: 0, brightness: 0.2, blur: 5 }; + + if (isMobile) { + const angleStep = 32; + const cylinderRadius = 310; + const angleRad = (diff * angleStep * Math.PI) / 180; + const x = cylinderRadius * Math.sin(angleRad); + const zBase = cylinderRadius * (Math.cos(angleRad) - 1); + const z = diff === 0 ? zBase + 160 : zBase; + const rotateY = -diff * angleStep; + const scale = distance === 0 ? 1.15 : distance === 1 ? 0.88 : 0.68; + const opacity = distance === 0 ? 1 : distance === 1 ? 0.95 : 0.72; + const brightness = distance === 0 ? 1.12 : distance === 1 ? 0.92 : 0.70; + const blur = distance === 0 ? 0 : distance === 1 ? 0 : 0.8; + return { x, z, rotateY, scale, opacity, brightness, blur }; + } + + // Progressive curved alignment with increased horizontal dimension + const rotateAngles = [0, 18, 36, 52]; // progressive inward tilt + const rotateY = diff > 0 ? -rotateAngles[distance] : rotateAngles[distance]; + + const xOffsetsTablet = [0, 195, 365, 515]; + const xOffsetsDesktop = [0, 255, 485, 685]; // wider horizontal span reaching screen borders + const xOffsets = isTablet ? xOffsetsTablet : xOffsetsDesktop; + const x = diff < 0 ? -xOffsets[distance] : xOffsets[distance]; + + const zDepths = [160, 10, -105, -230]; + const z = zDepths[distance]; + + const scales = [1.15, 0.88, 0.70, 0.54]; + const scale = scales[distance]; + + const opacities = [1, 0.96, 0.84, 0.65]; + const opacity = opacities[distance]; + + const brightnesses = [1.12, 0.96, 0.80, 0.64]; + const brightness = brightnesses[distance]; + + const blurs = [0, 0, 0.6, 1.2]; + const blur = blurs[distance]; + + return { x, z, rotateY, scale, opacity, brightness, blur }; +} + +// Pre-computed stable chaos offsets per card index +const CARD_CHAOS = Array.from({ length: 12 }, (_, i) => ({ + x: (Math.sin(i * 2.3 + 1) * 600), // -600 to +600 px scatter + y: (Math.cos(i * 1.7 + 0.5) * 500), // -500 to +500 px scatter + rotateY: (Math.sin(i * 3.1) * 180), // wild rotateY -180 to +180 + rotateZ: (Math.cos(i * 2.7) * 35), // skew tilt -35 to +35 + scale: 0.18 + (i % 4) * 0.06, // tiny chaotic scale +})); + +export default function Cinematic3DGallery({ + items = DEFAULT_GALLERY_ITEMS, +}: Cinematic3DGalleryProps) { + const [activeIndex, setActiveIndex] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const [viewportWidth, setViewportWidth] = useState(1200); + const [touchStartX, setTouchStartX] = useState(null); + const [isArranged, setIsArranged] = useState(false); + const hasArrangedOnceRef = useRef(false); + + const autoplayTimerRef = useRef(null); + const userInteractionTimeoutRef = useRef(null); + const totalItems = items.length; + + // Drag / swipe state + const [isDragging, setIsDragging] = useState(false); + const dragStartXRef = useRef(null); + const dragDeltaRef = useRef(0); + const wheelCooldownRef = useRef(false); + + const rawDragAngle = useMotionValue(0); + useSpring(rawDragAngle, { stiffness: 280, damping: 32, mass: 0.6 }); + + // Page entrance trigger — first mount chaotic, then arrange after noticeable delay + useEffect(() => { + const t2 = setTimeout(() => setIsArranged(true), 500); // 500ms clear view of chaotic state + const t3 = setTimeout(() => { hasArrangedOnceRef.current = true; }, 3200); + return () => { clearTimeout(t2); clearTimeout(t3); }; + }, []); + + // Viewport tracking + useEffect(() => { + const onResize = () => setViewportWidth(window.innerWidth); + onResize(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + const isMobile = viewportWidth < 640; + const isTablet = viewportWidth >= 640 && viewportWidth < 1024; + const maxVisible = isMobile ? 2 : 3; + + const handleNext = useCallback(() => { + setActiveIndex((p) => (p + 1) % totalItems); + }, [totalItems]); + + const handlePrev = useCallback(() => { + setActiveIndex((p) => (p - 1 + totalItems) % totalItems); + }, [totalItems]); + + const triggerUserInteraction = useCallback(() => { + setIsPaused(true); + if (userInteractionTimeoutRef.current) clearTimeout(userInteractionTimeoutRef.current); + userInteractionTimeoutRef.current = setTimeout(() => setIsPaused(false), 4500); + }, []); + + // Autoplay + useEffect(() => { + if (isPaused || totalItems <= 1) return; + autoplayTimerRef.current = setInterval(handleNext, 4000); + return () => { if (autoplayTimerRef.current) clearInterval(autoplayTimerRef.current); }; + }, [isPaused, totalItems, handleNext]); + + // Keyboard navigation + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowLeft") { triggerUserInteraction(); handlePrev(); } + if (e.key === "ArrowRight") { triggerUserInteraction(); handleNext(); } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [handleNext, handlePrev, triggerUserInteraction]); + + // Mouse drag handlers + const handleMouseDown = (e: React.MouseEvent) => { + setIsDragging(true); + dragStartXRef.current = e.clientX; + dragDeltaRef.current = 0; + rawDragAngle.set(0); + triggerUserInteraction(); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging || dragStartXRef.current === null) return; + const delta = e.clientX - dragStartXRef.current; + dragDeltaRef.current = delta; + rawDragAngle.set(-delta * 0.12); + }; + + const handleMouseUp = () => { + if (!isDragging) return; + const threshold = isMobile ? 40 : 55; + if (dragDeltaRef.current < -threshold) handleNext(); + else if (dragDeltaRef.current > threshold) handlePrev(); + rawDragAngle.set(0); + setIsDragging(false); + dragStartXRef.current = null; + dragDeltaRef.current = 0; + }; + + // Touch handlers + const handleTouchStart = (e: React.TouchEvent) => { + setTouchStartX(e.touches[0].clientX); + rawDragAngle.set(0); + triggerUserInteraction(); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + if (touchStartX === null) return; + const delta = e.touches[0].clientX - touchStartX; + rawDragAngle.set(-delta * 0.12); + }; + + const handleTouchEnd = (e: React.TouchEvent) => { + if (touchStartX === null) return; + const delta = e.changedTouches[0].clientX - touchStartX; + if (Math.abs(delta) > 40) { + if (delta < 0) handleNext(); else handlePrev(); + } + rawDragAngle.set(0); + setTouchStartX(null); + }; + + // Wheel handler + const handleWheel = (e: React.WheelEvent) => { + if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) return; + if (wheelCooldownRef.current) return; + if (Math.abs(e.deltaX) > 15) { + triggerUserInteraction(); + if (e.deltaX > 0) handleNext(); else handlePrev(); + wheelCooldownRef.current = true; + setTimeout(() => { wheelCooldownRef.current = false; }, 350); + } + }; + + // Shortest circular diff + const getRelativeDiff = (index: number) => { + let diff = index - activeIndex; + const half = totalItems / 2; + if (diff > half) diff -= totalItems; + if (diff < -half) diff += totalItems; + return diff; + }; + + const activeItem = items[activeIndex] ?? items[0]; + + // Card dimensions + const cardW = isMobile ? 185 : isTablet ? 245 : 310; + const cardH = isMobile ? 280 : isTablet ? 365 : 460; + const stageH = Math.round(cardH * 1.22) + 20; + + return ( +
setIsPaused(true)} + onMouseLeave={() => { setIsPaused(false); handleMouseUp(); }} + onMouseMove={handleMouseMove} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + onTouchStart={handleTouchStart} + onTouchMove={handleTouchMove} + onTouchEnd={handleTouchEnd} + onWheel={handleWheel} + > + {/* ── Dark-to-bright opening veil ─────────────────────────────────────── */} + + + {/* ── Transparent overlay for ambient glows (bg.png shows through) ────── */} +
+ {/* Ambient static background glows matching site theme */} +
+
+
+
+ + {/* Floating ambient particle embers */} + {AMBIENT_PARTICLES.map((p) => ( + + ))} + + {/* ── Brief Smoky / Mist pulse animation on active card change (~500ms) ── */} + + + +
+ + {/* ── Header ───────────────────────────────────────────────────────────── */} + +
+ ✦ +

+ OUR{" "} + + GALLERY + +

+ ✦ +
+
+ + {/* ── 3D Stage with Side Navigation Arrows ─────────────────────────────── */} +
+ {/* Left Arrow Button */} + + + {/* Right Arrow Button */} + + +
+ {items.map((item, index) => { + const diff = getRelativeDiff(index); + const distance = Math.abs(diff); + if (distance > maxVisible) return null; + + const isActive = diff === 0; + const { x, z, rotateY, scale, opacity, brightness, blur } = + getCoverflowTransform(diff, isMobile, isTablet); + + // Per-card chaos offsets for the distorted initial state + const chaos = CARD_CHAOS[index % CARD_CHAOS.length]; + const arrangeDelay = isArranged ? 0.08 + distance * 0.12 : 0; + + const navDuration = isDragging ? 0.04 : 0.7; + const navEase: [number, number, number, number] = [0.22, 1, 0.36, 1]; + const floatY = [0, -6, 0]; + + return ( + { + if (Math.abs(dragDeltaRef.current) < 6) { + triggerUserInteraction(); + setActiveIndex(index); + } + }} + > +
+ {item.title} + + {/* Dark vignette overlay */} +
+ + {/* ── Active Card Layout & Typography ───── */} + {isActive ? ( + + {/* Top Right Tag Badge */} +
+ + {item.category} + +
+ + {/* Center Overlay Text Block */} +
+

+ {item.title} +

+ {item.location && ( +

+ - {item.location} +

+ )} +
+

+ {item.subtitle} +

+
+ + ) : ( + /* Side Cards subtle bottom label */ +
+

+ {item.title} +

+
+ )} + + {/* Active Card Gold Border Glow */} + {isActive && ( +
+ )} +
+ + ); + })} +
+
+
+ ); +} diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index 0783fba..62499e5 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -1,11 +1,8 @@ -import React from 'react'; +"use client"; -const Gallery = () => { - return ( -
-

Gallery Component

-
- ); -}; +import React from "react"; +import Cinematic3DGallery from "./Cinematic3DGallery"; -export default Gallery; +export default function Gallery() { + return ; +} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 13d9e2c..794a8b5 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -16,7 +16,7 @@ export default function Navbar({ isPlaying = false, onToggleAudio }: NavbarProps { name: "Home", href: "/" }, { name: "About", href: "/#about" }, { name: "Events", href: "/events" }, - { name: "Gallery", href: "/#gallery" }, + { name: "Gallery", href: "/gallery" }, { name: "Merch", href: "/#Merch" }, ];