feat(rithmomachia): enhance capture relation UI with smooth animations

- Add z-index layering (Z_INDEX.GAME.OVERLAY) for capture options to appear above other elements
- Implement bidirectional spring animations (opening: fly out + fade in, closing: retract + fade out)
- Add click-to-dismiss functionality with smooth closing animation
- Fix CSS transition conflict by limiting transitions to transform and box-shadow only
- Remove opacity from hover handlers to prevent interference with spring animation
- Add SVG overflow: visible to prevent clipping at board edges
- Add 400ms delay before unmounting to allow animation completion

Capture relation buttons now smoothly fly out from and retract back to the center
of captured pieces with synchronized fade transitions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-29 11:13:20 -05:00
parent c463cbb2a3
commit 0a308016e9

View File

@@ -7,6 +7,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import { PageWithNav } from '@/components/PageWithNav'
import { StandardGameLayout } from '@/components/StandardGameLayout'
import { useFullscreen } from '@/contexts/FullscreenContext'
import { Z_INDEX } from '@/constants/zIndex'
import { css } from '../../../../styled-system/css'
import { useRithmomachia } from '../Provider'
import type { RithmomachiaConfig, Piece } from '../types'
@@ -444,11 +445,13 @@ function CaptureRelationOptions({
cellSize,
gap,
onSelectRelation,
closing = false,
}: {
targetPos: { x: number; y: number }
cellSize: number
gap: number
onSelectRelation: (relation: string) => void
closing?: boolean
}) {
const relations = [
{ relation: 'EQUAL', label: '=', tooltip: 'Equality: a = b', angle: 0, color: '#8b5cf6' },
@@ -499,10 +502,11 @@ function CaptureRelationOptions({
const maxRadius = cellSize * 1.2
const buttonSize = 64
// Animate all buttons simultaneously (not trail)
// Animate all buttons simultaneously - reverse animation when closing
const spring = useSpring({
from: { radius: 0, opacity: 0 },
to: { radius: maxRadius, opacity: 0.85 },
radius: closing ? 0 : maxRadius,
opacity: closing ? 0 : 0.85,
config: { tension: 280, friction: 20 },
})
@@ -548,17 +552,15 @@ function CaptureRelationOptions({
alignItems: 'center',
justifyContent: 'center',
opacity: spring.opacity,
transition: 'all 0.15s ease',
transition: 'transform 0.15s ease, box-shadow 0.15s ease',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
textShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '1'
e.currentTarget.style.transform = 'scale(1.15)'
e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.4)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '0.85'
e.currentTarget.style.transform = 'scale(1)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'
}}
@@ -659,6 +661,7 @@ function BoardDisplay() {
const { state, makeMove, playerColor, isMyTurn } = useRithmomachia()
const [selectedSquare, setSelectedSquare] = useState<string | null>(null)
const [captureDialogOpen, setCaptureDialogOpen] = useState(false)
const [closingDialog, setClosingDialog] = useState(false)
const [captureTarget, setCaptureTarget] = useState<{
from: string
to: string
@@ -666,6 +669,24 @@ function BoardDisplay() {
} | null>(null)
const [hoveredRelation, setHoveredRelation] = useState<string | null>(null)
// Handle closing animation completion
useEffect(() => {
if (closingDialog) {
// Wait for animation to complete (400ms allows spring to fully settle)
const timer = setTimeout(() => {
setCaptureDialogOpen(false)
setCaptureTarget(null)
setClosingDialog(false)
}, 400)
return () => clearTimeout(timer)
}
}, [closingDialog])
// Function to dismiss the dialog with animation
const dismissDialog = () => {
setClosingDialog(true)
}
const handleSquareClick = (square: string, piece: (typeof state.pieces)[string] | undefined) => {
if (!isMyTurn) return
@@ -743,10 +764,9 @@ function BoardDisplay() {
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
if (!isMyTurn) return
// If capture dialog is open, dismiss it on any click (buttons have stopPropagation)
if (captureDialogOpen) {
setCaptureDialogOpen(false)
setCaptureTarget(null)
// If capture dialog is open, dismiss it with animation on any click (buttons have stopPropagation)
if (captureDialogOpen && !closingDialog) {
dismissDialog()
return
}
@@ -785,6 +805,8 @@ function BoardDisplay() {
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
position: 'relative',
zIndex: Z_INDEX.GAME.OVERLAY,
})}
>
{/* Unified SVG Board */}
@@ -841,6 +863,7 @@ function BoardDisplay() {
cellSize={cellSize}
gap={gap}
onSelectRelation={handleCaptureWithRelation}
closing={closingDialog}
/>
)}
</svg>