From cf4c346e2dbf9bd10232cf441cbf74020ec209bb Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 27 Nov 2025 20:08:03 +0100 Subject: [PATCH] add diagonal infinite scrolling card mosaic background --- src/components/CardMosaicBackground.tsx | 155 ++++++++++++++++++++++++ src/index.css | 28 +++++ 2 files changed, 183 insertions(+) create mode 100644 src/components/CardMosaicBackground.tsx diff --git a/src/components/CardMosaicBackground.tsx b/src/components/CardMosaicBackground.tsx new file mode 100644 index 0000000..e7193c7 --- /dev/null +++ b/src/components/CardMosaicBackground.tsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { getRandomCards } from '../services/api'; +import { Card } from '../types'; + +export default function CardMosaicBackground() { + const [cards, setCards] = useState([]); + const [offset, setOffset] = useState({ x: 0, y: 0 }); + const animationRef = useRef(); + + // Grid configuration - large grid to cover entire screen + const cardsPerRow = 18; + const cardsPerCol = 15; + // Spacing adjusted for card transforms: w-64 = 256px + // With rotateZ(15deg) and rotateX(60deg), cards need more space + const cardWidth = 130; // Horizontal spacing to avoid overlap + const cardHeight = 85; // Vertical spacing to avoid overlap + const gridWidth = cardsPerRow * cardWidth; + const gridHeight = cardsPerCol * cardHeight; + + useEffect(() => { + const fetchCards = async () => { + try { + // Fetch enough cards for one grid + const totalCards = cardsPerRow * cardsPerCol; + const randomCards = await getRandomCards(totalCards); + setCards(randomCards); + } catch (error) { + console.error('Error fetching background cards:', error); + } + }; + + fetchCards(); + }, []); + + // Diagonal infinite scroll animation + useEffect(() => { + if (cards.length === 0) return; + + const speed = 0.5; // Pixels per frame (diagonal speed) + let lastTime = Date.now(); + + const animate = () => { + const now = Date.now(); + const delta = now - lastTime; + lastTime = now; + + setOffset((prev) => { + // Move diagonally: right and up + let newX = prev.x + (speed * delta) / 16; + let newY = prev.y - (speed * delta) / 16; + + // Loop seamlessly when we've moved one full grid + if (newX >= gridWidth) newX = newX % gridWidth; + if (newY <= -gridHeight) newY = newY % gridHeight; + + return { x: newX, y: newY }; + }); + + animationRef.current = requestAnimationFrame(animate); + }; + + animationRef.current = requestAnimationFrame(animate); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [cards.length, gridWidth, gridHeight]); + + if (cards.length === 0) return null; + + // Render the card grid (will be duplicated 4 times for infinite effect) + const renderGrid = (offsetX: number, offsetY: number, key: string) => ( +
+ {cards.map((card, index) => { + const col = index % cardsPerRow; + const row = Math.floor(index / cardsPerRow); + + return ( +
+ +
+ ); + })} +
+ ); + + return ( +
+ {/* Scrolling grid container */} +
+ {/* Duplicate grids in 2x2 pattern for seamless infinite scroll */} + {/* Position grids to cover entire viewport and beyond */} + {renderGrid(-gridWidth, window.innerHeight - gridHeight / 2, 'grid-tl')} + {renderGrid(0, window.innerHeight - gridHeight / 2, 'grid-tr')} + {renderGrid(-gridWidth, window.innerHeight - gridHeight / 2 + gridHeight, 'grid-bl')} + {renderGrid(0, window.innerHeight - gridHeight / 2 + gridHeight, 'grid-br')} +
+ + {/* Fixed gradient overlay - cards pass UNDER and fade naturally */} +
+
+ ); +} diff --git a/src/index.css b/src/index.css index 1e05960..ab30aaf 100644 --- a/src/index.css +++ b/src/index.css @@ -167,6 +167,34 @@ animation: float 3s ease-in-out infinite; } +/* Diagonal Carousel - Cards flow from bottom-left to top-right diagonally and loop */ +@keyframes flowDiagonal { + 0% { + translate: 0 0; + opacity: 1; + } + 60% { + translate: 600px -600px; + opacity: 1; + } + 75% { + translate: 900px -900px; + opacity: 0.6; + } + 85% { + translate: 1100px -1100px; + opacity: 0.3; + } + 100% { + translate: 1400px -1400px; + opacity: 0; + } +} + +.animate-flow-right { + animation: flowDiagonal 40s linear infinite; +} + /* Gradient Animation */ @keyframes gradientShift { 0% {