- 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>
308 lines
8.8 KiB
Markdown
308 lines
8.8 KiB
Markdown
# 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`](../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 `activeAnimations` tracking
|
|
- 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// Animate held card swooping to discard pile
|
|
cardAnimations.animateDiscard(heldCardElement, targetCard, onComplete)
|
|
```
|
|
|
|
### Ambient Effects (Looping)
|
|
|
|
```javascript
|
|
// "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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
1. Creates a temporary overlay element (`.draw-anim-card`)
|
|
2. Positions it exactly over the source card
|
|
3. Hides the original card (`opacity: 0` or `.swap-out`)
|
|
4. Animates the overlay
|
|
5. 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:
|
|
|
|
```javascript
|
|
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 (`:hover` scale/shadow - no movement)
|
|
- Show/hide via `.hidden` class
|
|
- **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 `transition` on any card element (`.card`, `.card-inner`, `.real-card`, `.swap-card`, `.held-card-floating`)
|
|
- No `@keyframes` for card movements or flips
|
|
- No `.flipped`, `.moving`, `.flipping` transition 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:
|
|
|
|
```javascript
|
|
// In renderGame()
|
|
if (!this.isDrawAnimating && /* other conditions */) {
|
|
// Show held card
|
|
}
|
|
```
|
|
|
|
### Animation Sequencing
|
|
|
|
Use anime.js timelines for coordinated sequences:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
// No onComplete callback needed
|
|
cardAnimations.animateOpponentFlip(cardElement, cardData);
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging
|
|
|
|
### Check Active Animations
|
|
|
|
```javascript
|
|
console.log(window.cardAnimations.activeAnimations);
|
|
```
|
|
|
|
### Force Cleanup
|
|
|
|
```javascript
|
|
window.cardAnimations.cancelAll();
|
|
```
|
|
|
|
### Animation Not Working?
|
|
|
|
1. Check that anime.js is loaded before card-animations.js
|
|
2. Verify element exists and is visible
|
|
3. Check for CSS transitions that might conflict
|
|
4. Look for errors in console
|