feat: add CSS animations and visual feedback system

Added complete animation system with Panda CSS:

1. Panda CSS Keyframe Animations (panda.config.ts):
   - shake: Horizontal oscillation for errors (-5px to 5px)
   - successPulse: Gentle scale for correct answers (1 to 1.05)
   - errorShake: Stronger shake for errors (-10px to 10px)
   - pulse: Continuous breathing effect (1 to 1.05)
   - bounce: Vertical bounce (0 to -10px)
   - bounceIn: Entry animation with scale and rotate
   - glow: Expanding box shadow effect

   All animations match exact timing and easing from original
   Python implementation (web_generator.py lines 1996-6274)

2. Visual Feedback Integration (GameDisplay.tsx):
   - Trigger successPulse animation on correct answers
   - Trigger errorShake animation on incorrect answers
   - Auto-clear animations after 500ms (matching duration)
   - Applied to answer input element for clear visual feedback
   - Synchronized with sound effects for multi-sensory feedback

Animations enhance user experience by providing immediate
visual confirmation of answer correctness alongside audio cues.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-30 14:25:10 -05:00
parent 600bc35bc3
commit 80e33e25b3
2 changed files with 80 additions and 2 deletions

View File

@@ -47,6 +47,60 @@ export default defineConfig({
shadows: {
card: { value: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)' },
modal: { value: '0 25px 50px -12px rgba(0, 0, 0, 0.25)' }
},
animations: {
// Shake animation for errors (web_generator.py line 3419)
shake: { value: 'shake 0.5s ease-in-out' },
// Pulse animation for success feedback (line 2004)
successPulse: { value: 'successPulse 0.5s ease' },
pulse: { value: 'pulse 2s infinite' },
// Error shake with larger amplitude (line 2009)
errorShake: { value: 'errorShake 0.5s ease' },
// Bounce animations (line 6271, 5065)
bounce: { value: 'bounce 1s infinite alternate' },
bounceIn: { value: 'bounceIn 1s ease-out' },
// Glow animation (line 6260)
glow: { value: 'glow 1s ease-in-out infinite alternate' }
}
},
keyframes: {
// Shake - horizontal oscillation for errors (line 3419)
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'75%': { transform: 'translateX(5px)' }
},
// Success pulse - gentle scale for correct answers (line 2004)
successPulse: {
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.05)' }
},
// Pulse - continuous breathing effect (line 6255)
pulse: {
'0%, 100%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.05)' }
},
// Error shake - stronger horizontal oscillation (line 2009)
errorShake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-10px)' },
'75%': { transform: 'translateX(10px)' }
},
// Bounce - vertical oscillation (line 6271)
bounce: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' }
},
// Bounce in - entry animation with scale and rotate (line 6265)
bounceIn: {
'0%': { transform: 'scale(0.3) rotate(-10deg)', opacity: '0' },
'50%': { transform: 'scale(1.1) rotate(5deg)' },
'100%': { transform: 'scale(1) rotate(0deg)', opacity: '1' }
},
// Glow - expanding box shadow (line 6260)
glow: {
'0%': { boxShadow: '0 0 5px rgba(255, 255, 255, 0.5)' },
'100%': { boxShadow: '0 0 20px rgba(255, 255, 255, 0.8), 0 0 30px rgba(255, 255, 255, 0.6)' }
}
}
}

View File

@@ -1,6 +1,6 @@
'use client'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useComplementRace } from '../context/ComplementRaceContext'
import { useAIRacers } from '../hooks/useAIRacers'
import { useAdaptiveDifficulty } from '../hooks/useAdaptiveDifficulty'
@@ -12,12 +12,25 @@ import { SteamTrainJourney } from './RaceTrack/SteamTrainJourney'
import { RouteCelebration } from './RouteCelebration'
import { generatePassengers } from '../lib/passengerGenerator'
type FeedbackAnimation = 'correct' | 'incorrect' | null
export function GameDisplay() {
const { state, dispatch } = useComplementRace()
useAIRacers() // Activate AI racer updates (not used in sprint mode)
const { trackPerformance, getAdaptiveFeedbackMessage } = useAdaptiveDifficulty()
const { boostMomentum } = useSteamJourney()
const { playSound } = useSoundEffects()
const [feedbackAnimation, setFeedbackAnimation] = useState<FeedbackAnimation>(null)
// Clear feedback animation after it plays (line 1996, 2001)
useEffect(() => {
if (feedbackAnimation) {
const timer = setTimeout(() => {
setFeedbackAnimation(null)
}, 500) // Match animation duration
return () => clearTimeout(timer)
}
}, [feedbackAnimation])
// Show adaptive feedback with auto-hide
useEffect(() => {
@@ -70,6 +83,9 @@ export function GameDisplay() {
dispatch({ type: 'SUBMIT_ANSWER', answer })
trackPerformance(true, responseTime)
// Trigger correct answer animation (line 1996)
setFeedbackAnimation('correct')
// Play appropriate sound based on performance (from web_generator.py lines 11530-11542)
const newStreak = state.streak + 1
if (newStreak > 0 && newStreak % 5 === 0) {
@@ -117,6 +133,9 @@ export function GameDisplay() {
// Incorrect answer
trackPerformance(false, responseTime)
// Trigger incorrect answer animation (line 2001)
setFeedbackAnimation('incorrect')
// Play incorrect sound (from web_generator.py line 11589)
playSound('incorrect')
@@ -319,7 +338,12 @@ export function GameDisplay() {
padding: '16px 36px',
boxShadow: '0 4px 20px rgba(59, 130, 246, 0.3)',
textAlign: 'center',
minWidth: '160px'
minWidth: '160px',
animation: feedbackAnimation === 'correct'
? 'successPulse 0.5s ease'
: feedbackAnimation === 'incorrect'
? 'errorShake 0.5s ease'
: undefined
}}>
<div data-element="input-value" style={{
fontSize: '60px',