From 1695e9d56166ac6abbb3db8b958a3579c555fac9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:20:26 +0000 Subject: [PATCH] card #20: update ventures/anthem/app.js --- ventures/anthem/app.js | 289 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 ventures/anthem/app.js diff --git a/ventures/anthem/app.js b/ventures/anthem/app.js new file mode 100644 index 0000000..12b2d8a --- /dev/null +++ b/ventures/anthem/app.js @@ -0,0 +1,289 @@ +// Anthem - Main Application Logic + +// Audio context for WebAudio API +let audioContext; +let isPlaying = false; +let scheduledNotes = []; + +// Style constants +const STYLES = { + CHIPTUNE: 'chiptune', + LOFI: 'lo-fi', + FANFARE: 'fanfare' +}; + +// Current state +let currentState = { + name: 'Anthem', + style: STYLES.CHIPTUNE, + volume: 0.7 +}; + +// Note mapping for melody generation +const NOTE_MAPPING = ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5']; +const OCTAVE_NOTES = { + 'C4': 261.63, + 'D4': 293.66, + 'E4': 329.63, + 'F4': 349.23, + 'G4': 392.00, + 'A4': 440.00, + 'B4': 493.88, + 'C5': 523.25 +}; + +// Initialize the app +function initApp() { + // Check if we have a saved state from URL + const loadedState = Time.load(); + if (loadedState) { + currentState = loadedState; + } + + // Set up UI elements + setupUI(); + + // Initialize audio context on first user interaction + document.body.addEventListener('click', initAudio, { once: true }); + + // Render initial state + renderState(); +} + +// Set up UI event listeners +function setupUI() { + const nameInput = document.getElementById('name-input'); + const playButton = document.getElementById('play-button'); + const styleChips = document.querySelectorAll('.style-chip'); + const shareButton = document.getElementById('share-button'); + const downloadButton = document.getElementById('download-button'); + + // Name input handler + nameInput.addEventListener('input', function(e) { + currentState.name = e.target.value || 'Anthem'; + renderState(); + }); + + // Play button handler + playButton.addEventListener('click', function() { + if (isPlaying) { + stopPlayback(); + playButton.textContent = 'Play'; + isPlaying = false; + } else { + playMelody(); + playButton.textContent = 'Stop'; + isPlaying = true; + } + }); + + // Style selection handlers + styleChips.forEach(chip => { + chip.addEventListener('click', function() { + // Remove active class from all chips + styleChips.forEach(c => c.classList.remove('active')); + // Add active class to clicked chip + this.classList.add('active'); + // Update style + currentState.style = this.dataset.style; + renderState(); + }); + }); + + // Share button handler + shareButton.addEventListener('click', function() { + const link = Time.save(currentState); + if (link) { + navigator.clipboard.writeText(link).then(() => { + const originalText = shareButton.textContent; + shareButton.textContent = 'Copied!'; + setTimeout(() => { + shareButton.textContent = originalText; + }, 2000); + }); + } else { + // Fallback for when Time.save returns null + const url = new URL(window.location); + url.searchParams.set('s', btoa(JSON.stringify(currentState)).replace(/[^a-zA-Z0-9]/g, '')); + navigator.clipboard.writeText(url.toString()); + const originalText = shareButton.textContent; + shareButton.textContent = 'Copied!'; + setTimeout(() => { + shareButton.textContent = originalText; + }, 2000); + } + }); + + // Download button handler + downloadButton.addEventListener('click', function() { + generateWAV(); + }); +} + +// Render current state to UI +function renderState() { + // Update name input + document.getElementById('name-input').value = currentState.name; + + // Update title card + document.getElementById('title-text').textContent = currentState.name; + + // Update style chips + document.querySelectorAll('.style-chip').forEach(chip => { + chip.classList.toggle('active', chip.dataset.style === currentState.style); + }); + + // Update visualization + updateVisualization(); +} + +// Update visualization based on current state +function updateVisualization() { + const viz = document.getElementById('visualization'); + viz.innerHTML = ''; + + // Create bars representing the melody + for (let i = 0; i < 8; i++) { + const bar = document.createElement('div'); + bar.className = 'bar'; + + // Get note for this position + const charIndex = i % currentState.name.length; + const charCode = currentState.name.charCodeAt(charIndex); + const noteIndex = charCode % NOTE_MAPPING.length; + const note = NOTE_MAPPING[noteIndex]; + + // Set height based on note frequency + const frequency = OCTAVE_NOTES[note]; + const height = 20 + (frequency / OCTAVE_NOTES['C5']) * 80; + bar.style.height = `${height}%`; + + // Set color based on style + switch (currentState.style) { + case STYLES.CHIPTUNE: + bar.style.backgroundColor = '#00ff00'; + break; + case STYLES.LOFI: + bar.style.backgroundColor = '#ff6b6b'; + break; + case STYLES.FANFARE: + bar.style.backgroundColor = '#4cc9f0'; + break; + } + + viz.appendChild(bar); + } +} + +// Initialize audio context +function initAudio() { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } +} + +// Play the melody +function playMelody() { + if (!audioContext) { + initAudio(); + } + + if (audioContext.state === 'suspended') { + audioContext.resume(); + } + + stopPlayback(); + + // Schedule notes for playback + const startTime = audioContext.currentTime + 0.1; + const tempo = 120; // BPM + const secondsPerBeat = 60 / tempo; + + for (let i = 0; i < 8; i++) { + const charIndex = i % currentState.name.length; + const charCode = currentState.name.charCodeAt(charIndex); + const noteIndex = charCode % NOTE_MAPPING.length; + const note = NOTE_MAPPING[noteIndex]; + const frequency = OCTAVE_NOTES[note]; + + const noteTime = startTime + i * secondsPerBeat; + scheduleNote(frequency, noteTime, secondsPerBeat * 0.8); + } +} + +// Schedule a single note +function scheduleNote(frequency, startTime, duration) { + if (!audioContext) return; + + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Set oscillator type based on style + switch (currentState.style) { + case STYLES.CHIPTUNE: + oscillator.type = 'square'; + break; + case STYLES.LOFI: + oscillator.type = 'sawtooth'; + gainNode.gain.value = 0.3; // Lower volume for lo-fi + break; + case STYLES.FANFARE: + oscillator.type = 'triangle'; + break; + } + + oscillator.frequency.value = frequency; + + // Apply envelope + gainNode.gain.setValueAtTime(0, startTime); + gainNode.gain.linearRampToValueAtTime(currentState.volume, startTime + 0.01); + gainNode.gain.linearRampToValueAtTime(0, startTime + duration); + + oscillator.start(startTime); + oscillator.stop(startTime + duration); + + // Keep track of scheduled notes for stopping + scheduledNotes.push({ + oscillator: oscillator, + gainNode: gainNode + }); + + // Clean up after note finishes + oscillator.onended = function() { + const index = scheduledNotes.findIndex(note => note.oscillator === oscillator); + if (index !== -1) { + scheduledNotes.splice(index, 1); + } + + // If this was the last note, reset play button + if (scheduledNotes.length === 0) { + document.getElementById('play-button').textContent = 'Play'; + isPlaying = false; + } + }; +} + +// Stop playback +function stopPlayback() { + scheduledNotes.forEach(note => { + try { + note.oscillator.stop(); + } catch (e) { + // Ignore errors when stopping + } + }); + scheduledNotes = []; +} + +// Generate WAV file +function generateWAV() { + // In a real implementation, this would generate actual audio data + // For now, we'll just simulate the download + alert('In a complete implementation, this would download a WAV file of your theme song.'); +} + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', initApp); \ No newline at end of file