diff --git a/src/components/card.module.css b/src/components/card.module.css index 69d09ec..faab0c7 100644 --- a/src/components/card.module.css +++ b/src/components/card.module.css @@ -1,11 +1,13 @@ .card { display: inline-flex; + position: relative; border: 1px solid var(--color-stone-400); border-radius: var(--rounding-100); overflow: hidden; height: var(--card-height); width: var(--card-width); - perspective: 1000px; + transform-style: preserve-3d; + transform-origin: center center; background: linear-gradient( to bottom, var(--color-stone-800), @@ -13,9 +15,39 @@ var(--color-stone-900) ); color: var(--color-stone-100); - transition-duration: var(--anim-dur-300); - transition-timing-function: var(--anim-easing-100); - transition-property: transform, box-shadow; + transition: + transform 0.1s ease-out, + box-shadow 0.2s ease-out; + --sheen-x: 50%; + --sheen-y: 50%; +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient( + circle at var(--sheen-x) var(--sheen-y), + rgba(255, 255, 255, 0.25) 0%, + rgba(255, 255, 255, 0.1) 30%, + transparent 60% + ); + opacity: 0; + pointer-events: none; + z-index: 10; + transition: opacity 0.3s ease-out; + border-radius: var(--rounding-100); +} + +.card.face-up.idle:hover { + will-change: transform; +} + +.card.face-up.idle:hover::before { + opacity: 1; } .card-front { @@ -24,18 +56,17 @@ outline-offset: -2px; } -.card.face-up.idle:hover { - transform: translateY(calc(-1 * var(--spacing-100))); -} - .card.face-up { - box-shadow: 0 var(--spacing-100) var(--spacing-300) calc(var(--spacing-200) * -1) - var(--color-stone-700); + box-shadow: + 0 2px 4px -1px rgba(0, 0, 0, 0.2), + 0 1px 2px rgba(0, 0, 0, 0.1); } -.card.face-up:hover { - box-shadow: 0 var(--spacing-200) var(--spacing-400) calc(var(--spacing-100) * -1) - var(--color-stone-700); +.card.face-up.idle:hover { + box-shadow: + 0 12px 24px -4px rgba(0, 0, 0, 0.3), + 0 6px 12px -2px rgba(0, 0, 0, 0.2), + 0 3px 6px -1px rgba(0, 0, 0, 0.15); } .card.in-play { diff --git a/src/components/card.tsx b/src/components/card.tsx index 83c4d05..effbd6c 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -1,4 +1,5 @@ import { clsx } from 'clsx' +import { useState, type MouseEvent } from 'react' import type { Card } from '../types/cards' import cardBackImg from '../images/card-back.png' import swordIcon from '../images/sword.png' @@ -18,6 +19,9 @@ export function Card({ artwork, id, }: { isStacked?: boolean; onClick?: () => void; className?: string } & Card) { + const [tilt, setTilt] = useState({ rotateX: 0, rotateY: 0 }) + const [isHovering, setIsHovering] = useState(false) + const withClsx = (rootClass: string, additionalClassName?: string) => { return clsx( { @@ -37,8 +41,53 @@ export function Card({ ) } + const handleMouseMove = (e: MouseEvent) => { + if (orientation !== 'face-up' || status !== 'idle') return + + if (!isHovering) setIsHovering(true) + + const card = e.currentTarget + const rect = card.getBoundingClientRect() + + // Calculate cursor position relative to card center (-0.5 to 0.5) + const x = (e.clientX - rect.left) / rect.width - 0.5 + const y = (e.clientY - rect.top) / rect.height - 0.5 + + // Convert to rotation degrees (max ±8 degrees for subtle effect) + const maxTilt = 8 + const rotateY = x * maxTilt * 2 // Multiply by 2 since x ranges from -0.5 to 0.5 + const rotateX = -y * maxTilt * 2 // Negative for natural tilt direction + + setTilt({ rotateX, rotateY }) + } + + const handleMouseLeave = () => { + setTilt({ rotateX: 0, rotateY: 0 }) + setIsHovering(false) + } + + const isHoverable = orientation === 'face-up' && status === 'idle' + const translateY = isHoverable && isHovering ? 'calc(-1 * var(--spacing-100))' : '0px' + + // Calculate sheen position (0 to 100%) + const sheenX = isHoverable && isHovering ? ((tilt.rotateY / 16 + 0.5) * 100).toFixed(1) : '50' + const sheenY = isHoverable && isHovering ? ((-tilt.rotateX / 16 + 0.5) * 100).toFixed(1) : '50' + return ( -
+
{name}