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:
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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%; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user