feat: Implement dynamic art cropping for small cards and refine preview suppression for large cards.

This commit is contained in:
2025-12-17 01:28:26 +01:00
parent f9819b324e
commit 4ad0cd6fdc
10 changed files with 92 additions and 29 deletions

View File

@@ -58,3 +58,6 @@
- [Smart Preview Suppression](./devlog/2025-12-17-023000_smart_preview_suppression.md): Completed. Disabled hover preview for card elements that are already rendered large enough on screen.
- [Compact Card Layout](./devlog/2025-12-17-023500_compact_card_layout.md): Completed. Decreased card sizes in Grid and Stack views for a denser UI.
- [View Scale Slider](./devlog/2025-12-17-024000_view_scale_slider.md): Completed. Added a slider to dynamically adjust card dimensions, synced across Grid and Stack views.
- [Dynamic Art Cropping](./devlog/2025-12-17-024500_dynamic_art_cropping.md): Completed. Implemented automatic switching to full-art/art-crop images when card size is reduced below readability threshold.
- [Refined Preview Suppression](./devlog/2025-12-17-025000_refined_preview_suppression.md): Completed. Adjusted suppression threshold to 200px to better support Stack View's pop-up behavior.
- [Explicit Preview Suppression](./devlog/2025-12-17-025500_explicit_preview_suppression.md): Completed. Implemented strict `preventPreview` prop to enforce suppression logic reliably regardless of card overlap or DOM state.

View File

@@ -0,0 +1,18 @@
# Dynamic Art Cropping
## Objective
Automatically switch card visualizations to "Full Art" (Art Crop) mode when the thumbnail size is reduced below a readability threshold, maximizing the visual impact of the artwork when text is too small to read.
## Changes
- **Backend (Client & Server)**:
- Updated `DraftCard` interface to include `imageArtCrop`.
- Modified parsing services (`PackGeneratorService`) to extract and populate `imageArtCrop` from Scryfall data.
- **Frontend (UI)**:
- **PackCard (Grid View)**: Implemented a conditional check: if `cardWidth < 170px`, the image source switches to `imageArtCrop`.
- **StackView (Deck/Collection)**: Applied the same logic.
- **Visuals**:
- The `object-cover` CSS property ensures the rectangular art crop fills the entire card frame, creating a "borderless/full-art" look.
- The **Foil Overlay** and **Rarity Stripe** remain visible on top of the art crop, maintaining game state clarity.
## Result
As you slide the size slider down, the cards seamlessly transform from standard cards (with borders and text) to vibrant, full-art thumbnails. This creates a stunning "mosaic" effect for the cube overview and deck stacks, solving the issue of illegible text at small scales.

View File

@@ -0,0 +1,14 @@
# Refined Preview Suppression
## Objective
Tune the "Smart Preview Suppression" logic to better align with the Stack View's behavior. In Stack View, hovering a card causes it to "pop" to the front (`z-index` shift), making the card fully visible in-place. Because of this, showing a floating preview is redundant and distracting once the card is large enough to be read directly.
## Changes
- Modified `handleMouseEnter` in `src/client/src/components/CardPreview.tsx`:
- Lowered the suppression threshold from `>240x300` to `>200x270`.
- **Logic**:
- Cards sized via the slider to be larger than **200px** wide are now considered "readable" (especially since the 'Art Crop' mode turns off at 170px, leaving a range of 170-199 where preview is explicitly ON for text, and 200+ where it's suppressed).
- This effectively disables the popup in Stack View for medium-to-large settings, relying on the native "pop-to-front" hover effect for inspection.
## Result
A cleaner, less jittery drafting experience where large cards simply "lift up" for inspection, while smaller cards still get the helpful magnified popup.

View File

@@ -0,0 +1,16 @@
# Explicit Preview Suppression
## Objective
Enforce strict preview suppression when card sizes are large (`>= 200px`), regardless of element visibility, overlap, or DOM layout quirks. This ensures that in Stack View, where cards overlap, no stray previews are triggered for cards that are ostensibly "big enough" to be read directly.
## Changes
- **CardPreview (`CardHoverWrapper`)**:
- Added an optional `preventPreview?: boolean` prop.
- Updated `handleMouseEnter` to immediately return if `preventPreview` is true, bypassing any DOM size checks that might be inaccurate for obscured elements.
- **PackCard (Grid View)**:
- Passed `preventPreview={cardWidth >= 200}` to the wrapper.
- **StackView (Stack View)**:
- Passed `preventPreview={cardWidth >= 200}` to the wrapper.
## Result
Total consistency: if your slider is set to 200/300, floating previews are globally disabled for those views. This specifically fixes the issue where overlapping cards in a stack might have triggered previews unnecessarily.

View File

@@ -83,7 +83,7 @@ export const FloatingPreview: React.FC<{ card: DraftCard; x: number; y: number;
};
// --- Hover Wrapper to handle mouse events ---
export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.ReactNode; className?: string }> = ({ card, children, className }) => {
export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.ReactNode; className?: string; preventPreview?: boolean }> = ({ card, children, className, preventPreview }) => {
const [isHovering, setIsHovering] = useState(false);
const [isLongPressing, setIsLongPressing] = useState(false);
const [renderPreview, setRenderPreview] = useState(false);
@@ -127,11 +127,12 @@ export const CardHoverWrapper: React.FC<{ card: DraftCard; children: React.React
const handleMouseEnter = (e: React.MouseEvent) => {
if (isMobile) return;
if (preventPreview) return;
// Check if the card is already "big enough" on screen
const rect = e.currentTarget.getBoundingClientRect();
// Width > 240 && Height > 300 targets large grid items but excludes thin list rows
if (rect.width > 240 && rect.height > 300) {
// Width > 200 && Height > 270 targets readable cards (Stack/Grid) but excludes list rows
if (rect.width > 200 && rect.height > 270) {
return;
}

View File

@@ -99,31 +99,36 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode, cardWidth =
{viewMode === 'grid' && (
<div className="flex flex-wrap gap-3">
{pack.cards.map((card) => (
<CardHoverWrapper key={card.id} card={card}>
<div style={{ width: cardWidth }} className="relative group bg-slate-900 rounded-lg shrink-0">
{/* Visual Card */}
<div className={`relative aspect-[2.5/3.5] overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 cursor-pointer ${isFoil(card) ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
{isFoil(card) && <FoilOverlay />}
{isFoil(card) && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1 rounded backdrop-blur-sm">FOIL</div>}
{pack.cards.map((card) => {
const useArtCrop = cardWidth < 170 && !!card.imageArtCrop;
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
{card.image ? (
<img src={card.image} alt={card.name} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">
{card.name}
</div>
)}
{/* Rarity Stripe */}
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' :
card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' :
card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' :
'bg-black'
}`} />
return (
<CardHoverWrapper key={card.id} card={card} preventPreview={cardWidth >= 200}>
<div style={{ width: cardWidth }} className="relative group bg-slate-900 rounded-lg shrink-0">
{/* Visual Card */}
<div className={`relative aspect-[2.5/3.5] overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 cursor-pointer ${isFoil(card) ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
{isFoil(card) && <FoilOverlay />}
{isFoil(card) && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1 rounded backdrop-blur-sm">FOIL</div>}
{displayImage ? (
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">
{card.name}
</div>
)}
{/* Rarity Stripe */}
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' :
card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' :
card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' :
'bg-black'
}`} />
</div>
</div>
</div>
</CardHoverWrapper>
))}
</CardHoverWrapper>
);
})}
</div>
)}

View File

@@ -73,9 +73,11 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150 })
// Margin calculation: Negative margin to pull up next cards.
// To show a "strip" of say 35px at the top of each card.
const isLast = index === catCards.length - 1;
const useArtCrop = cardWidth < 170 && !!card.imageArtCrop;
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
return (
<CardHoverWrapper key={card.id} card={card} className="relative w-full z-0 hover:z-50 transition-all duration-200">
<CardHoverWrapper key={card.id} card={card} className="relative w-full z-0 hover:z-50 transition-all duration-200" preventPreview={cardWidth >= 200}>
<div
className={`relative w-full rounded-lg bg-slate-800 shadow-md border border-slate-950 overflow-hidden cursor-pointer group`}
style={{
@@ -85,7 +87,7 @@ export const StackView: React.FC<StackViewProps> = ({ cards, cardWidth = 150 })
aspectRatio: '2.5/3.5'
}}
>
<img src={card.image} alt={card.name} className="w-full h-full object-cover" />
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" />
{/* Optional: Shine effect for foils if visible? */}
{card.finish === 'foil' && <FoilOverlay />}
</div>

View File

@@ -9,6 +9,7 @@ export interface DraftCard {
layout?: string; // Add layout
colors: string[];
image: string;
imageArtCrop?: string;
set: string;
setCode: string;
setType: string;
@@ -107,6 +108,7 @@ export class PackGeneratorService {
image: useLocalImages
? `${window.location.origin}/cards/images/${cardData.set}/${cardData.id}.jpg`
: (cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || ''),
imageArtCrop: cardData.image_uris?.art_crop || cardData.card_faces?.[0]?.image_uris?.art_crop || '',
set: cardData.set_name,
setCode: cardData.set,
setType: setType,

View File

@@ -10,6 +10,7 @@ export interface DraftCard {
layout?: string;
colors: string[];
image: string;
imageArtCrop?: string;
set: string;
setCode: string;
setType: string;
@@ -95,6 +96,7 @@ export class PackGeneratorService {
layout: layout,
colors: cardData.colors || [],
image: cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || '',
imageArtCrop: cardData.image_uris?.art_crop || cardData.card_faces?.[0]?.image_uris?.art_crop || '',
set: cardData.set_name,
setCode: cardData.set,
setType: setType,

View File

@@ -31,7 +31,7 @@ export interface ScryfallCard {
image_uris?: { normal: string; small?: string; large?: string; png?: string; art_crop?: string; border_crop?: string };
card_faces?: {
name: string;
image_uris?: { normal: string; };
image_uris?: { normal: string; art_crop?: string; };
type_line?: string;
mana_cost?: string;
oracle_text?: string;