card #20: update ventures/anthem/app.js

This commit is contained in:
github-actions[bot] authored and GitHub committed 2026-10-13 00:20:26 +00:00
1 parent 70be7c1a05
commit 1695e9d561
1 file changed
+289
+289
View File
@@ -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);