feat: Implement advanced foil effects with rolling rainbow, circular glare, and mobile entrance animations, alongside a fix for foil rendering on non-foil cards.

This commit is contained in:
2025-12-17 01:11:50 +01:00
parent 119af95cee
commit f7d22377fa
16 changed files with 314 additions and 10 deletions

View File

@@ -2,12 +2,31 @@ import React, { useState, useEffect, useRef } from 'react';
import { DraftCard } from '../services/PackGeneratorService';
// --- Floating Preview Component ---
const FoilOverlay = () => (
<div className="absolute inset-0 z-20 pointer-events-none rounded-xl overflow-hidden">
{/* CSS-based Holographic Pattern */}
<div className="absolute inset-0 foil-holo" />
{/* Gaussian Circular Glare - Spinning Radial Gradient (Mildly visible) */}
<div className="absolute inset-[-50%] bg-[radial-gradient(circle_at_50%_50%,_rgba(255,255,255,0.25)_0%,_transparent_60%)] mix-blend-overlay opacity-25 animate-spin-slow" />
</div>
);
export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number; isMobile?: boolean; isClosing?: boolean }> = ({ card, x, y, isMobile, isClosing }) => {
const isFoil = card.finish === 'foil';
// Cast finishes to any to allow loose string matching if needed, or just standard check
const isFoil = (card.finish as string) === 'foil' || (card.finish as string) === 'etched';
const imgRef = useRef<HTMLImageElement>(null);
// Basic boundary detection
const [adjustedPos, setAdjustedPos] = useState({ top: y, left: x });
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
// Trigger entrance animation
requestAnimationFrame(() => setIsMounted(true));
}, []);
const isActive = isMounted && !isClosing;
useEffect(() => {
if (isMobile) return;
@@ -33,10 +52,12 @@ export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number;
if (isMobile) {
return (
<div className={`fixed inset-0 z-[9999] pointer-events-none flex items-center justify-center bg-black/60 backdrop-blur-[2px] transition-all duration-300 ${isClosing ? 'animate-out fade-out' : 'animate-in fade-in'}`}>
<div className={`relative w-[85vw] max-w-sm rounded-2xl overflow-hidden shadow-2xl ring-4 ring-black/50 transition-all duration-300 ${isClosing ? 'animate-out zoom-out-95' : 'animate-in zoom-in-95'}`}>
<div className={`fixed inset-0 z-[9999] pointer-events-none flex items-center justify-center bg-black/60 backdrop-blur-[2px] transition-all duration-300 ease-in-out ${isActive ? 'opacity-100' : 'opacity-0'}`}>
<div className={`relative w-[85vw] max-w-sm rounded-2xl overflow-hidden shadow-2xl ring-4 ring-black/50 transition-all duration-300 ${isActive ? 'scale-100 opacity-100 ease-out' : 'scale-95 opacity-0 ease-in'}`}>
<img src={card.image} alt={card.name} className="w-full h-auto" />
{isFoil && <div className="absolute inset-0 bg-gradient-to-br from-purple-500/20 to-blue-500/20 mix-blend-overlay animate-pulse"></div>}
{/* Universal mild brightening overlay */}
<div className="absolute inset-0 bg-white/10 pointer-events-none mix-blend-overlay" />
{isFoil && <FoilOverlay />}
</div>
</div>
);
@@ -44,15 +65,18 @@ export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number;
return (
<div
className="fixed z-[9999] pointer-events-none transition-opacity duration-75"
className="fixed z-[9999] pointer-events-none"
style={{
top: adjustedPos.top,
left: adjustedPos.left
}}
>
<div className="relative w-[300px] rounded-xl overflow-hidden shadow-2xl border-4 border-slate-900 bg-black">
<div className={`relative w-[300px] rounded-xl overflow-hidden shadow-2xl border-4 border-slate-900 bg-black transition-all duration-300 ${isActive ? 'scale-100 opacity-100 ease-out' : 'scale-95 opacity-0 ease-in'}`}>
<img ref={imgRef} src={card.image} alt={card.name} className="w-full h-auto" />
{isFoil && <div className="absolute inset-0 bg-gradient-to-br from-purple-500/20 to-blue-500/20 mix-blend-overlay animate-pulse"></div>}
{/* Universal mild brightening overlay */}
<div className="absolute inset-0 bg-white/10 pointer-events-none mix-blend-overlay" />
{/* CSS-based Holographic Pattern & Glare */}
{isFoil && <FoilOverlay />}
</div>
</div>
);
@@ -82,8 +106,8 @@ export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.React
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
setRenderPreview(true);
} else {
// Delay unmount for mobile animation
if (isMobile && renderPreview) {
// Delay unmount for animation (all devices)
if (renderPreview) {
closeTimerRef.current = setTimeout(() => {
setRenderPreview(false);
}, 300); // 300ms matches duration-300
@@ -144,7 +168,7 @@ export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.React
clearTimeout(timerRef.current);
timerRef.current = null;
}
setIsLongPressing(false);
// Do not close if already long pressing
}
};

View File

@@ -1,3 +1,63 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.animate-bg-roll {
animation: bg-roll 3s linear infinite;
}
.animate-spin-slow {
animation: spin 8s linear infinite;
}
}
@keyframes bg-roll {
0% {
background-position: 0% 50%;
}
100% {
background-position: 100% 50%;
}
}
.foil-holo {
--space: 5%;
--angle: 133deg;
background-image:
repeating-linear-gradient(
0deg,
rgb(255, 119, 115) calc(var(--space)*1),
rgba(255,237,95,1) calc(var(--space)*2),
rgba(168,255,95,1) calc(var(--space)*3),
rgba(131,255,247,1) calc(var(--space)*4),
rgba(120,148,255,1) calc(var(--space)*5),
rgb(216,117,255) calc(var(--space)*6),
rgb(255,119,115) calc(var(--space)*7)
),
repeating-linear-gradient(
var(--angle),
#0e152e 0%,
hsl(180, 10%, 60%) 3.8%,
hsl(180, 29%, 66%) 4.5%,
hsl(180, 10%, 60%) 5.2%,
#0e152e 10%,
#0e152e 12%
);
background-blend-mode: screen, hue;
background-size: 200% 700%, 300% 200%;
background-position: 0% 50%, 0% 50%;
filter: brightness(0.8) contrast(1.5) saturate(0.8);
mix-blend-mode: color-dodge;
opacity: 0.35;
animation: foil-shift 15s linear infinite;
}
@keyframes foil-shift {
0% { background-position: 0% 50%, 0% 0%; }
50% { background-position: 100% 50%, 100% 100%; }
100% { background-position: 0% 50%, 0% 0%; }
}