- Extract WebSocket handlers from main.py into handlers.py - Add V3 feature docs (dealer rotation, dealing animation, round end reveal, column pair celebration, final turn urgency, opponent thinking, score tallying, card hover/selection, knock early drama, column pair indicator, swap animation improvements, draw source distinction, card value tooltips, active rules context, discard pile history, realistic card sounds) - Add V3 refactoring docs (ai.py, main.py/game.py, misc improvements) - Add installation guide with Docker, systemd, and nginx setup - Add helper scripts (install.sh, dev-server.sh, docker-build.sh) - Add animation flow diagrams documentation - Add test files for handlers, rooms, and V3 features - Add e2e test specs for V3 features - Update README with complete project structure and current tech stack - Update CLAUDE.md with full architecture tree and server layer descriptions - Update .env.example to reflect PostgreSQL (remove SQLite references) - Update .gitignore to exclude virtualenv files, .claude/, and .db files - Remove tracked virtualenv files (bin/, lib64, pyvenv.cfg) - Remove obsolete game_log.py (SQLite) and games.db Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.8 KiB
Card Animation System
This document describes the unified animation system for the Golf card game client.
For detailed animation flow diagrams (what triggers what, in what order, with what flags), see docs/ANIMATION-FLOWS.md.
Architecture
When to use anime.js vs CSS:
- Anime.js (CardAnimations): Card movements, flips, swaps, draws - anything involving card elements
- CSS keyframes/transitions: Simple UI feedback (button hover, badge entrance, status message fades) - non-card elements
General rule: If it moves a card, use anime.js. If it's UI chrome, CSS is fine.
| What | How |
|---|---|
| Card movements | anime.js |
| Card flips | anime.js |
| Swap animations | anime.js |
| Pulse/glow effects on cards | anime.js |
| Button hover/active states | CSS transitions |
| Badge entrance/exit | CSS transitions |
| Status message fades | CSS transitions |
| Card hover states | anime.js hoverIn()/hoverOut() |
| Show/hide | CSS .hidden class only |
Why anime.js?
- Consistent timing and easing across all animations
- Coordinated multi-element sequences via timelines
- Proper animation cancellation via
activeAnimationstracking - No conflicts between CSS and JS animation systems
Core Files
| File | Purpose |
|---|---|
card-animations.js |
Unified CardAnimations class - all animation logic |
timing-config.js |
Centralized timing/easing configuration |
style.css |
Static styles only (no transitions on cards) |
CardAnimations Class API
Global instance available at window.cardAnimations.
Draw Animations
// Draw from deck - lift, move to hold area, flip to reveal
cardAnimations.animateDrawDeck(cardData, onComplete)
// Draw from discard - quick grab, no flip
cardAnimations.animateDrawDiscard(cardData, onComplete)
// For opponent draw-then-discard - deck to discard with flip
cardAnimations.animateDeckToDiscard(card, onComplete)
Flip Animations
// Generic flip animation on any card element
cardAnimations.animateFlip(element, cardData, onComplete)
// Initial flip at game start (local player)
cardAnimations.animateInitialFlip(cardElement, cardData, onComplete)
// Opponent card flip (fire-and-forget)
cardAnimations.animateOpponentFlip(cardElement, cardData, rotation)
Swap Animations
// Player swaps drawn card with hand card
cardAnimations.animateSwap(position, oldCard, newCard, handCardElement, onComplete)
// Opponent swap (fire-and-forget)
cardAnimations.animateOpponentSwap(playerId, position, discardCard, sourceCardElement, rotation, wasFaceUp)
Discard Animations
// Animate held card swooping to discard pile
cardAnimations.animateDiscard(heldCardElement, targetCard, onComplete)
Ambient Effects (Looping)
// "Your turn to draw" shake effect
cardAnimations.startTurnPulse(element)
cardAnimations.stopTurnPulse(element)
// CPU thinking glow
cardAnimations.startCpuThinking(element)
cardAnimations.stopCpuThinking(element)
// Initial flip phase - clickable cards glow
cardAnimations.startInitialFlipPulse(element)
cardAnimations.stopInitialFlipPulse(element)
cardAnimations.stopAllInitialFlipPulses()
One-Shot Effects
// Pulse when card lands on discard
cardAnimations.pulseDiscard()
// Pulse effect on face-up swap
cardAnimations.pulseSwap(element)
// Pop-in when element appears (use sparingly)
cardAnimations.popIn(element)
// Gold ring expanding effect before draw
cardAnimations.startDrawPulse(element)
Utility Methods
// Check if animation is in progress
cardAnimations.isBusy()
// Cancel all running animations
cardAnimations.cancel()
cardAnimations.cancelAll()
// Clean up animation elements
cardAnimations.cleanup()
Animation Coordination
Server-Client Timing
Server CPU timing (in server/ai.py CPU_TIMING) must account for client animation durations:
post_draw_settle: Must be >= draw animation duration (~1.1s for deck draw)post_action_pause: Must be >= swap/discard animation duration (~0.5s)
Preventing Animation Overlap
Animation overlay cards are marked with data-animating="true" while active.
Methods like animateUnifiedSwap and animateOpponentDiscard check for active
animations and wait before starting new ones.
Card Hover Initialization
Call cardAnimations.initHoverListeners(container) after dynamically creating cards.
This is done automatically in renderGame() for player and opponent card areas.
Animation Overlay Pattern
For complex animations (flips, swaps), the system:
- Creates a temporary overlay element (
.draw-anim-card) - Positions it exactly over the source card
- Hides the original card (
opacity: 0or.swap-out) - Animates the overlay
- Removes overlay and reveals updated original card
This ensures smooth animations without modifying the DOM structure of game cards.
Timing Configuration
All timing values are in timing-config.js and exposed as window.TIMING.
Key Durations
All durations are configured in timing-config.js and read via window.TIMING.
| Animation | Duration | Config Key | Notes |
|---|---|---|---|
| Flip | 320ms | card.flip |
3D rotateY with slight overshoot |
| Deck lift | 120ms | draw.deckLift |
Visible lift before travel |
| Deck move | 250ms | draw.deckMove |
Smooth travel to hold position |
| Deck flip | 320ms | draw.deckFlip |
Reveal drawn card |
| Discard lift | 80ms | draw.discardLift |
Quick decisive grab |
| Discard move | 200ms | draw.discardMove |
Travel to hold position |
| Swap lift | 100ms | swap.lift |
Pickup before arc travel |
| Swap arc | 320ms | swap.arc |
Arc travel between positions |
| Swap settle | 100ms | swap.settle |
Landing with gentle overshoot |
| Swap pulse | 400ms | — | Scale + brightness (face-up swap) |
| Turn shake | 400ms | — | Every 3 seconds |
Easing Functions
Custom cubic bezier curves give cards natural weight and momentum:
window.TIMING.anime.easing = {
flip: 'cubicBezier(0.34, 1.2, 0.64, 1)', // Slight overshoot snap
move: 'cubicBezier(0.22, 0.68, 0.35, 1.0)', // Smooth deceleration
lift: 'cubicBezier(0.0, 0.0, 0.2, 1)', // Quick out, soft stop
settle: 'cubicBezier(0.34, 1.05, 0.64, 1)', // Tiny overshoot on landing
arc: 'cubicBezier(0.45, 0, 0.15, 1)', // Smooth S-curve for arcs
pulse: 'easeInOutSine', // Smooth oscillation (loops)
}
CSS Rules
What CSS Does
- Static card appearance (colors, borders, sizing)
- Layout and positioning
- Card hover states (
:hoverscale/shadow - no movement) - Show/hide via
.hiddenclass - UI chrome animations (buttons, badges, status messages):
- Button hover/active transitions
- Badge entrance/exit animations
- Status message fade in/out
- Modal transitions
What CSS Does NOT Do (on card elements)
- No
transitionon any card element (.card,.card-inner,.real-card,.swap-card,.held-card-floating) - No
@keyframesfor card movements or flips - No
.flipped,.moving,.flippingtransition triggers for cards
Important Classes
| Class | Purpose |
|---|---|
.draw-anim-card |
Temporary overlay during animation |
.draw-anim-inner |
3D flip container |
.swap-out |
Hides original during swap animation |
.hidden |
Opacity 0, no display change |
.draw-pulse |
Gold ring expanding effect |
Common Patterns
Preventing Premature UI Updates
The isDrawAnimating flag in app.js prevents the held card from appearing before the draw animation completes:
// In renderGame()
if (!this.isDrawAnimating && /* other conditions */) {
// Show held card
}
Animation Sequencing
Use anime.js timelines for coordinated sequences:
const T = window.TIMING;
const timeline = anime.timeline({
easing: T.anime.easing.move,
complete: () => { /* cleanup */ }
});
timeline.add({ targets: el, translateY: -15, duration: T.card.lift, easing: T.anime.easing.lift });
timeline.add({ targets: el, left: x, top: y, duration: T.card.move });
timeline.add({ targets: inner, rotateY: 0, duration: T.card.flip, easing: T.anime.easing.flip });
Fire-and-Forget Animations
For opponent/CPU animations that don't block game flow:
// No onComplete callback needed
cardAnimations.animateOpponentFlip(cardElement, cardData);
Debugging
Check Active Animations
console.log(window.cardAnimations.activeAnimations);
Force Cleanup
window.cardAnimations.cancelAll();
Animation Not Working?
- Check that anime.js is loaded before card-animations.js
- Verify element exists and is visible
- Check for CSS transitions that might conflict
- Look for errors in console