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