golfgame/client/ANIMATIONS.md
adlee-was-taken 9fc6b83bba v3.0.0: V3 features, server refactoring, and documentation overhaul
- 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>
2026-02-14 10:03:45 -05:00

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 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

// 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:

  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:

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:

// 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?

  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