card #20: update ventures/anthem/app.js
This commit is contained in:
1 parent
70be7c1a05
commit
1695e9d561
1 file changed
+289
@@ -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);
|
||||
Reference in new issue
Block a user