feat: complete steam train sound system and smooth time-of-day transitions

- Add realistic train sounds: chuffing, whistles, coal spilling, steam hissing
- Implement momentum-based audio that varies with train speed (faster = more frequent chuffs)
- Add train whistle celebrations for streaks and high momentum
- Enhance coal spilling sound effects with multiple chunks and realistic timing
- Add smooth CSS transitions for time-of-day background gradients using @property
- Define complete gradient palette: dawn, morning, midday, afternoon, dusk, night
- All steam train journey features now fully restored and enhanced

🚂 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-12 21:57:59 -05:00
parent 045dc9fb32
commit 6c60f94a56

View File

@@ -333,6 +333,23 @@ def generate_web_flashcards(numbers, config, output_path):
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soroban Flashcards</title>
<style>
/* CSS @property declarations for smooth gradient transitions */
@property --sky-gradient {{
syntax: '<image>';
inherits: false;
initial-value: linear-gradient(135deg, #ffb347 0%, #ffcc5c 50%, #87ceeb 100%);
}}
/* Time-of-day gradient definitions */
:root {{
--dawn-gradient: linear-gradient(135deg, #ffb347 0%, #ffcc5c 30%, #87ceeb 70%, #98d8e8 100%);
--morning-gradient: linear-gradient(135deg, #87ceeb 0%, #98d8e8 30%, #b6e2ff 70%, #cce7ff 100%);
--midday-gradient: linear-gradient(135deg, #87ceeb 0%, #a8d8ea 30%, #c7e2f7 70%, #e3f2fd 100%);
--afternoon-gradient: linear-gradient(135deg, #ffecd2 0%, #fcb69f 30%, #ff8a65 70%, #ff7043 100%);
--dusk-gradient: linear-gradient(135deg, #ff8a65 0%, #ff7043 30%, #8e44ad 70%, #5b2c87 100%);
--night-gradient: linear-gradient(135deg, #2c3e50 0%, #34495e 30%, #1a252f 70%, #0f1419 100%);
}}
body {{
font-family: {font_family}, sans-serif;
margin: 0;
@@ -5073,14 +5090,14 @@ def generate_web_flashcards(numbers, config, output_path):
/* Steam Train Journey Visualization for Sprint Mode */
.race-track-section.steam-journey {{
/* Dynamic sky gradient based on time of day */
/* Dynamic sky gradient based on time of day with smooth transitions */
background: var(--sky-gradient, var(--dawn-gradient));
transition: --sky-gradient 3s ease-in-out, background 2s ease-out;
padding: 20px;
height: 400px;
max-height: 400px;
position: relative;
overflow: hidden;
transition: background 2s ease-out;
}}
@@ -13144,6 +13161,22 @@ def generate_web_flashcards(numbers, config, output_path):
// Update time of day
this.updateTimeOfDay();
// Play train whistle for celebrations and milestones
if (this.streak >= 5 && this.streak % 3 === 0) {{
// Major milestone - play train whistle
setTimeout(() => {{
this.playSound('train_whistle', 0.4);
}}, 200);
console.log(`🚂💨 TOOT TOOT! Milestone whistle for streak of ${{this.streak}}!`);
}} else if (this.momentum >= 90) {{
// High momentum celebration - occasional whistle
if (Math.random() < 0.3) {{
setTimeout(() => {{
this.playSound('train_whistle', 0.25);
}}, 150);
}}
}}
this.playSound('correct');
}}
@@ -13399,15 +13432,19 @@ def generate_web_flashcards(numbers, config, output_path):
}}
playCoalSpillingSounds() {{
// Play multiple coal chunk falling sounds in sequence
const sounds = ['incorrect', 'timeout']; // Using available sound types
sounds.forEach((sound, index) => {{
// Play realistic coal chunk falling sounds
for (let i = 0; i < 4; i++) {{
setTimeout(() => {{
this.playSound(sound);
}}, index * 200); // Staggered sound effects
}});
this.playSound('coal_spill', 0.3);
}}, i * 150); // Staggered coal chunks falling
}}
console.log('🔊 Playing coal spilling sound effects');
// Add a final "clatter" with incorrect sound
setTimeout(() => {{
this.playSound('incorrect', 0.2);
}}, 600);
console.log('🔊 Playing realistic coal spilling sound effects');
}}
updateTrainPosition() {{
@@ -13420,6 +13457,9 @@ def generate_web_flashcards(numbers, config, output_path):
// Update visual position along the path
this.updateTrainVisualization();
// Play momentum-based train sounds (chuffing varies with speed)
this.updateMomentumBasedAudio(speed);
// Check for station arrivals and passenger deliveries
this.checkStationArrivals();
}}
@@ -13453,6 +13493,38 @@ def generate_web_flashcards(numbers, config, output_path):
console.log(`🚂 Train at position ${{Math.round(this.trainPosition)}}% - ${{Math.round(point.x)}}, ${{Math.round(point.y)}} (on track)`);
}}
updateMomentumBasedAudio(speed) {{
// Play chuffing sounds that vary with train speed/momentum
const now = Date.now();
// Initialize chuff timing if not set
if (!this.lastChuffTime) {{
this.lastChuffTime = now;
this.chuffInterval = 1000; // Start with slow chuffing
}}
// Calculate chuffing interval based on speed (faster train = faster chuffs)
// Speed range: 0-1, chuff interval: 2000ms (slow) to 300ms (fast)
this.chuffInterval = Math.max(300, 2000 - (speed * 1700));
// Play chuff sound if enough time has passed
if (now - this.lastChuffTime >= this.chuffInterval) {{
// Higher speed = louder chuffs (more steam)
const chuffVolume = Math.min(0.4, 0.1 + (speed * 0.3));
this.playSound('train_chuff', chuffVolume);
// Occasionally add steam hiss at high speeds
if (speed > 0.7 && Math.random() < 0.3) {{
setTimeout(() => {{
this.playSound('steam_hiss', chuffVolume * 0.5);
}}, 100);
}}
this.lastChuffTime = now;
console.log(`🚂 Chuff! Speed: ${{Math.round(speed * 100)}}%, Interval: ${{Math.round(this.chuffInterval)}}ms`);
}}
}}
handleDisplaySwitching() {{
// Only apply display switching logic in Lightning Sprint train mode
if (!document.querySelector('.route-path')) return;
@@ -13976,6 +14048,90 @@ def generate_web_flashcards(numbers, config, output_path):
whooshOsc.start(audioContext.currentTime);
whooshOsc.stop(audioContext.currentTime + 0.3);
}} else if (type === 'train_chuff') {{
// Realistic steam train chuffing sound
const chuffOsc = audioContext.createOscillator();
const chuffGain = audioContext.createGain();
const chuffFilter = audioContext.createBiquadFilter();
chuffOsc.connect(chuffFilter);
chuffFilter.connect(chuffGain);
chuffGain.connect(audioContext.destination);
chuffOsc.type = 'sawtooth';
chuffFilter.type = 'bandpass';
chuffFilter.frequency.setValueAtTime(150, audioContext.currentTime);
chuffFilter.Q.setValueAtTime(5, audioContext.currentTime);
chuffOsc.frequency.setValueAtTime(80, audioContext.currentTime);
chuffOsc.frequency.exponentialRampToValueAtTime(120, audioContext.currentTime + 0.05);
chuffOsc.frequency.exponentialRampToValueAtTime(60, audioContext.currentTime + 0.2);
chuffGain.gain.setValueAtTime(0, audioContext.currentTime);
chuffGain.gain.exponentialRampToValueAtTime(volume * 0.8, audioContext.currentTime + 0.01);
chuffGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.2);
chuffOsc.start(audioContext.currentTime);
chuffOsc.stop(audioContext.currentTime + 0.2);
}} else if (type === 'train_whistle') {{
// Classic steam train whistle
this.play90sSound(audioContext, [
{{freq: 523, time: 0, duration: 0.3}}, // C5 - long whistle
{{freq: 659, time: 0.1, duration: 0.4}}, // E5 - harmony
{{freq: 523, time: 0.3, duration: 0.2}} // C5 - fade out
], volume * 1.2, 'sine');
}} else if (type === 'coal_spill') {{
// Coal chunks spilling sound effect
const coalOsc = audioContext.createOscillator();
const coalGain = audioContext.createGain();
const coalFilter = audioContext.createBiquadFilter();
coalOsc.connect(coalFilter);
coalFilter.connect(coalGain);
coalGain.connect(audioContext.destination);
coalOsc.type = 'square';
coalFilter.type = 'lowpass';
coalFilter.frequency.setValueAtTime(300, audioContext.currentTime);
// Simulate coal chunks falling with random frequency bursts
coalOsc.frequency.setValueAtTime(200 + Math.random() * 100, audioContext.currentTime);
coalOsc.frequency.exponentialRampToValueAtTime(100 + Math.random() * 50, audioContext.currentTime + 0.1);
coalOsc.frequency.exponentialRampToValueAtTime(80 + Math.random() * 40, audioContext.currentTime + 0.3);
coalGain.gain.setValueAtTime(0, audioContext.currentTime);
coalGain.gain.exponentialRampToValueAtTime(volume * 0.6, audioContext.currentTime + 0.01);
coalGain.gain.exponentialRampToValueAtTime(volume * 0.3, audioContext.currentTime + 0.15);
coalGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.4);
coalOsc.start(audioContext.currentTime);
coalOsc.stop(audioContext.currentTime + 0.4);
}} else if (type === 'steam_hiss') {{
// Steam hissing sound for locomotive
const steamOsc = audioContext.createOscillator();
const steamGain = audioContext.createGain();
const steamFilter = audioContext.createBiquadFilter();
steamOsc.connect(steamFilter);
steamFilter.connect(steamGain);
steamGain.connect(audioContext.destination);
steamOsc.type = 'triangle';
steamFilter.type = 'highpass';
steamFilter.frequency.setValueAtTime(2000, audioContext.currentTime);
steamOsc.frequency.setValueAtTime(4000 + Math.random() * 1000, audioContext.currentTime);
steamGain.gain.setValueAtTime(0, audioContext.currentTime);
steamGain.gain.exponentialRampToValueAtTime(volume * 0.4, audioContext.currentTime + 0.02);
steamGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.6);
steamOsc.start(audioContext.currentTime);
steamOsc.stop(audioContext.currentTime + 0.6);
}}
}} catch (e) {{