tweak alby panel, add sound FX
This commit is contained in:
parent
399be86217
commit
3cd42426c6
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,3 @@
|
||||
alby.html
|
||||
alby.html
|
||||
.DS_Store
|
||||
.old/
|
||||
445
index.html
445
index.html
@ -675,27 +675,33 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Main slide-in panel container */
|
||||
/* Main floating modal container */
|
||||
.alby-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
opacity: 0;
|
||||
width: 380px;
|
||||
height: 100vh;
|
||||
max-height: 90vh;
|
||||
background: linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 50%, #0f0f0f 100%);
|
||||
border-left: 6px solid #0a0a0a;
|
||||
border: 6px solid #0a0a0a;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
inset 0 0 0 2px #3a3a3a,
|
||||
inset 0 0 50px rgba(0,0,0,0.9),
|
||||
-10px 0 30px rgba(0,0,0,0.8);
|
||||
0 20px 60px rgba(0,0,0,0.8);
|
||||
z-index: 1001;
|
||||
transition: right 0.3s ease-out;
|
||||
transition: transform 0.2s ease-out, opacity 0.2s ease-out;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.alby-panel.active {
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Noise texture overlay for worn appearance */
|
||||
@ -721,12 +727,13 @@
|
||||
padding: 20px;
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #0f0f0f 100%);
|
||||
border-bottom: 4px solid #000;
|
||||
border-radius: 4px 4px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alby-panel-title {
|
||||
color: #cc8800;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-shadow:
|
||||
0 0 10px rgba(255, 170, 0, 0.5),
|
||||
@ -779,7 +786,7 @@
|
||||
/* Section labels with green CRT glow */
|
||||
.alby-label {
|
||||
color: #00ff00;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
text-shadow:
|
||||
0 0 5px #00ff00,
|
||||
0 0 10px #00ff00;
|
||||
@ -814,7 +821,7 @@
|
||||
|
||||
.alby-address {
|
||||
color: #cc8800;
|
||||
font-size: 13px;
|
||||
font-size: 15px;
|
||||
text-shadow: 0 0 8px rgba(255, 170, 0, 0.5);
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
@ -887,7 +894,7 @@
|
||||
padding: 10px;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px #00ff00;
|
||||
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
|
||||
@ -919,7 +926,7 @@
|
||||
padding: 12px;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
|
||||
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
|
||||
resize: none;
|
||||
@ -952,7 +959,7 @@
|
||||
border: 4px solid #000;
|
||||
color: #000;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
@ -1148,6 +1155,221 @@
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
const closeVideo = document.getElementById('closeVideo');
|
||||
|
||||
// ========================================
|
||||
// SOUND EFFECTS MODULE - START
|
||||
// Web Audio API synthesized sounds for tactile feedback
|
||||
// ========================================
|
||||
const SoundEffects = {
|
||||
ctx: null,
|
||||
scrubOscillator: null,
|
||||
scrubGain: null,
|
||||
|
||||
// Initialize AudioContext (must be called after user gesture)
|
||||
init() {
|
||||
if (!this.ctx) {
|
||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
// Resume if suspended (browser autoplay policy)
|
||||
if (this.ctx.state === 'suspended') {
|
||||
this.ctx.resume();
|
||||
}
|
||||
},
|
||||
|
||||
// Play a mechanical button click sound
|
||||
playButtonClick() {
|
||||
this.init();
|
||||
const now = this.ctx.currentTime;
|
||||
|
||||
// Create nodes for the click sound
|
||||
const clickOsc = this.ctx.createOscillator();
|
||||
const clickGain = this.ctx.createGain();
|
||||
const noiseGain = this.ctx.createGain();
|
||||
|
||||
// Low frequency "thunk" component (80-150Hz)
|
||||
clickOsc.type = 'sine';
|
||||
clickOsc.frequency.setValueAtTime(150, now);
|
||||
clickOsc.frequency.exponentialRampToValueAtTime(80, now + 0.05);
|
||||
|
||||
// Gain envelope for the thunk - fast attack, quick decay
|
||||
clickGain.gain.setValueAtTime(0.6, now);
|
||||
clickGain.gain.exponentialRampToValueAtTime(0.01, now + 0.08);
|
||||
|
||||
clickOsc.connect(clickGain);
|
||||
clickGain.connect(this.ctx.destination);
|
||||
|
||||
// High frequency "click" transient using noise
|
||||
const bufferSize = this.ctx.sampleRate * 0.02; // 20ms of noise
|
||||
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const noise = this.ctx.createBufferSource();
|
||||
noise.buffer = noiseBuffer;
|
||||
|
||||
// Bandpass filter to shape the noise into a click
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = 2000;
|
||||
filter.Q.value = 1;
|
||||
|
||||
noiseGain.gain.setValueAtTime(0.35, now);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.03);
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(noiseGain);
|
||||
noiseGain.connect(this.ctx.destination);
|
||||
|
||||
// Start and stop
|
||||
clickOsc.start(now);
|
||||
clickOsc.stop(now + 0.1);
|
||||
noise.start(now);
|
||||
noise.stop(now + 0.03);
|
||||
},
|
||||
|
||||
// Play tape wind/fast-forward sound for a specified duration
|
||||
// Uses filtered noise to simulate tape rushing past heads
|
||||
playTapeWind(direction = 'forward', duration = 0.5) {
|
||||
this.init();
|
||||
const now = this.ctx.currentTime;
|
||||
|
||||
// Create noise buffer for tape wind sound
|
||||
const sampleRate = this.ctx.sampleRate;
|
||||
const bufferSize = sampleRate * duration;
|
||||
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
|
||||
// Generate noise with slight amplitude modulation for realism
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
// Base noise
|
||||
const noise = Math.random() * 2 - 1;
|
||||
// Subtle amplitude wobble to simulate motor variation
|
||||
const wobble = 1 + 0.1 * Math.sin(i / sampleRate * 20 * Math.PI * 2);
|
||||
output[i] = noise * wobble;
|
||||
}
|
||||
|
||||
const noiseSource = this.ctx.createBufferSource();
|
||||
noiseSource.buffer = noiseBuffer;
|
||||
|
||||
// Bandpass filter to shape noise into tape wind character
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = 1200; // Center frequency for tape hiss
|
||||
filter.Q.value = 0.5; // Wide band for natural sound
|
||||
|
||||
// High shelf to add some brightness
|
||||
const highShelf = this.ctx.createBiquadFilter();
|
||||
highShelf.type = 'highshelf';
|
||||
highShelf.frequency.value = 3000;
|
||||
highShelf.gain.value = -6; // Reduce harshness
|
||||
|
||||
// Gain with envelope
|
||||
const gainNode = this.ctx.createGain();
|
||||
gainNode.gain.setValueAtTime(0, now);
|
||||
gainNode.gain.linearRampToValueAtTime(0.07, now + 0.03); // Quick attack
|
||||
gainNode.gain.setValueAtTime(0.07, now + duration - 0.05);
|
||||
gainNode.gain.linearRampToValueAtTime(0, now + duration); // Fade out
|
||||
|
||||
// Connect chain
|
||||
noiseSource.connect(filter);
|
||||
filter.connect(highShelf);
|
||||
highShelf.connect(gainNode);
|
||||
gainNode.connect(this.ctx.destination);
|
||||
|
||||
// Start and stop
|
||||
noiseSource.start(now);
|
||||
noiseSource.stop(now + duration);
|
||||
},
|
||||
|
||||
// Start continuous tape wind sound (for reel dragging)
|
||||
// Uses looping noise buffer for seamless continuous playback
|
||||
startTapeWindLoop() {
|
||||
this.init();
|
||||
|
||||
// Stop any existing loop
|
||||
this.stopTapeWindLoop();
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
const sampleRate = this.ctx.sampleRate;
|
||||
|
||||
// Create 1 second of loopable noise
|
||||
const bufferSize = sampleRate * 1;
|
||||
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
|
||||
// Generate noise with amplitude modulation
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
const noise = Math.random() * 2 - 1;
|
||||
const wobble = 1 + 0.1 * Math.sin(i / sampleRate * 20 * Math.PI * 2);
|
||||
output[i] = noise * wobble;
|
||||
}
|
||||
|
||||
this.tapeWindSource = this.ctx.createBufferSource();
|
||||
this.tapeWindSource.buffer = noiseBuffer;
|
||||
this.tapeWindSource.loop = true;
|
||||
|
||||
// Bandpass filter for tape character
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = 1200;
|
||||
filter.Q.value = 0.5;
|
||||
|
||||
// High shelf to reduce harshness
|
||||
const highShelf = this.ctx.createBiquadFilter();
|
||||
highShelf.type = 'highshelf';
|
||||
highShelf.frequency.value = 3000;
|
||||
highShelf.gain.value = -6;
|
||||
|
||||
// Gain with fade in
|
||||
this.tapeWindGain = this.ctx.createGain();
|
||||
this.tapeWindGain.gain.setValueAtTime(0, now);
|
||||
this.tapeWindGain.gain.linearRampToValueAtTime(0.07, now + 0.05);
|
||||
|
||||
// Connect chain
|
||||
this.tapeWindSource.connect(filter);
|
||||
filter.connect(highShelf);
|
||||
highShelf.connect(this.tapeWindGain);
|
||||
this.tapeWindGain.connect(this.ctx.destination);
|
||||
|
||||
this.tapeWindSource.start(now);
|
||||
},
|
||||
|
||||
// Stop the continuous tape wind sound
|
||||
stopTapeWindLoop() {
|
||||
if (this.tapeWindGain && this.ctx) {
|
||||
const now = this.ctx.currentTime;
|
||||
this.tapeWindGain.gain.linearRampToValueAtTime(0, now + 0.1);
|
||||
|
||||
// Clean up after fade
|
||||
const sourceToStop = this.tapeWindSource;
|
||||
setTimeout(() => {
|
||||
if (sourceToStop) {
|
||||
try {
|
||||
sourceToStop.stop();
|
||||
} catch(e) {}
|
||||
}
|
||||
}, 150);
|
||||
|
||||
this.tapeWindSource = null;
|
||||
this.tapeWindGain = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
// ========================================
|
||||
// SOUND EFFECTS MODULE - END
|
||||
// ========================================
|
||||
|
||||
// ========================================
|
||||
// BUTTON CLICK SOUNDS - Add to all buttons
|
||||
// Uses mousedown for immediate tactile feedback
|
||||
// ========================================
|
||||
[playBtn, pauseBtn, stopBtn, prevBtn, nextBtn, ejectBtn, lightningBtn].forEach(btn => {
|
||||
btn.addEventListener('mousedown', () => {
|
||||
SoundEffects.playButtonClick();
|
||||
});
|
||||
});
|
||||
|
||||
// Lightning button - opens Alby Lightning panel
|
||||
// (toggleAlbyPanel function defined in ALBY PANEL FUNCTIONALITY section below)
|
||||
lightningBtn.addEventListener('click', () => {
|
||||
@ -1270,49 +1492,125 @@
|
||||
// Previous track (prev button with bar)
|
||||
prevBtn.addEventListener('click', () => {
|
||||
console.log('Prev button clicked');
|
||||
currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
|
||||
const wasPlaying = !audio.paused;
|
||||
|
||||
// Play tape wind sound
|
||||
SoundEffects.playTapeWind('forward', 0.5);
|
||||
|
||||
// Speed up audio to 4x during transition (if playing)
|
||||
if (wasPlaying) {
|
||||
audio.playbackRate = 4;
|
||||
}
|
||||
|
||||
// Speed up reel animation during transition
|
||||
reelLeft.style.animationDuration = '0.3s';
|
||||
reelRight.style.animationDuration = '0.3s';
|
||||
tapeLeft.style.animationDuration = '0.3s';
|
||||
tapeRight.style.animationDuration = '0.3s';
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
|
||||
// Delay before loading new track
|
||||
setTimeout(() => {
|
||||
// Reset animation speed and playback rate
|
||||
reelLeft.style.animationDuration = '';
|
||||
reelRight.style.animationDuration = '';
|
||||
tapeLeft.style.animationDuration = '';
|
||||
tapeRight.style.animationDuration = '';
|
||||
audio.playbackRate = 1;
|
||||
|
||||
currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
if (wasPlaying) {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
} else {
|
||||
reelLeft.classList.remove('spinning');
|
||||
reelRight.classList.remove('spinning');
|
||||
tapeLeft.classList.remove('spinning');
|
||||
tapeRight.classList.remove('spinning');
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Next track (next button with bar)
|
||||
nextBtn.addEventListener('click', () => {
|
||||
console.log('Next button clicked');
|
||||
currentTrack = (currentTrack + 1) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
|
||||
const wasPlaying = !audio.paused;
|
||||
|
||||
// Play tape wind sound
|
||||
SoundEffects.playTapeWind('forward', 0.5);
|
||||
|
||||
// Speed up audio to 4x during transition (if playing)
|
||||
if (wasPlaying) {
|
||||
audio.playbackRate = 4;
|
||||
}
|
||||
|
||||
// Speed up reel animation during transition
|
||||
reelLeft.style.animationDuration = '0.3s';
|
||||
reelRight.style.animationDuration = '0.3s';
|
||||
tapeLeft.style.animationDuration = '0.3s';
|
||||
tapeRight.style.animationDuration = '0.3s';
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
|
||||
// Delay before loading new track
|
||||
setTimeout(() => {
|
||||
// Reset animation speed and playback rate
|
||||
reelLeft.style.animationDuration = '';
|
||||
reelRight.style.animationDuration = '';
|
||||
tapeLeft.style.animationDuration = '';
|
||||
tapeRight.style.animationDuration = '';
|
||||
audio.playbackRate = 1;
|
||||
|
||||
currentTrack = (currentTrack + 1) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
if (wasPlaying) {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
} else {
|
||||
reelLeft.classList.remove('spinning');
|
||||
reelRight.classList.remove('spinning');
|
||||
tapeLeft.classList.remove('spinning');
|
||||
tapeRight.classList.remove('spinning');
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Drag on reels to scrub audio
|
||||
@ -1321,13 +1619,19 @@
|
||||
let startTime = 0;
|
||||
let wasPlayingBeforeDrag = false;
|
||||
|
||||
let lastDragY = 0;
|
||||
|
||||
let tapeWindPlaying = false;
|
||||
|
||||
function startDrag(e) {
|
||||
if (audio.duration) {
|
||||
e.preventDefault(); // Prevent text selection during drag
|
||||
isDragging = true;
|
||||
startY = e.clientY || e.touches[0].clientY;
|
||||
lastDragY = startY;
|
||||
startTime = audio.currentTime;
|
||||
wasPlayingBeforeDrag = !audio.paused;
|
||||
tapeWindPlaying = false;
|
||||
// Don't pause - let audio continue playing while scrubbing
|
||||
}
|
||||
}
|
||||
@ -1336,10 +1640,35 @@
|
||||
if (isDragging && audio.duration) {
|
||||
const currentY = e.clientY || e.touches[0].clientY;
|
||||
const deltaY = startY - currentY;
|
||||
const instantDeltaY = lastDragY - currentY;
|
||||
lastDragY = currentY;
|
||||
|
||||
const scrubAmount = (deltaY / 100) * audio.duration * 0.1;
|
||||
const newTime = Math.max(0, Math.min(audio.duration, startTime + scrubAmount));
|
||||
audio.currentTime = newTime;
|
||||
|
||||
// Calculate scrub speed for audio effects
|
||||
// Positive deltaY = dragging up = fast forward
|
||||
// Negative deltaY = dragging down = rewind
|
||||
const scrubSpeed = Math.min(Math.abs(instantDeltaY) / 20, 1); // Normalize to 0-1
|
||||
|
||||
// Start tape wind sound only when actually moving
|
||||
if (scrubSpeed > 0.1 && !tapeWindPlaying) {
|
||||
SoundEffects.startTapeWindLoop();
|
||||
tapeWindPlaying = true;
|
||||
} else if (scrubSpeed <= 0.1 && tapeWindPlaying) {
|
||||
SoundEffects.stopTapeWindLoop();
|
||||
tapeWindPlaying = false;
|
||||
}
|
||||
|
||||
// Bonus: Adjust playback rate for sped-up audio effect (both directions)
|
||||
// Speed up to 4x when scrubbing in either direction
|
||||
if (wasPlayingBeforeDrag && scrubSpeed > 0.1) {
|
||||
audio.playbackRate = 1 + (scrubSpeed * 3); // 1x to 4x
|
||||
} else if (wasPlayingBeforeDrag) {
|
||||
audio.playbackRate = 1;
|
||||
}
|
||||
|
||||
// Keep playing during scrub
|
||||
if (audio.paused && wasPlayingBeforeDrag) {
|
||||
audio.play();
|
||||
@ -1350,6 +1679,14 @@
|
||||
function endDrag() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
|
||||
// Stop tape wind sound and reset playback rate
|
||||
if (tapeWindPlaying) {
|
||||
SoundEffects.stopTapeWindLoop();
|
||||
tapeWindPlaying = false;
|
||||
}
|
||||
audio.playbackRate = 1;
|
||||
|
||||
// Continue playing if it was playing before
|
||||
if (wasPlayingBeforeDrag && audio.paused) {
|
||||
audio.play();
|
||||
@ -1576,13 +1913,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for successful payment and reset form fields
|
||||
// Listen for successful payment, reset form fields, and close modal
|
||||
albySimpleBoost.addEventListener('success', (e) => {
|
||||
// Reset form to default values
|
||||
albyAmount.value = '1.0';
|
||||
albyMemo.value = '';
|
||||
updateAlbyCharCount();
|
||||
updateAlbyBoostButton();
|
||||
// Close the modal after successful boost
|
||||
toggleAlbyPanel();
|
||||
});
|
||||
|
||||
// Escape key handler for Alby panel
|
||||
|
||||
461
no-damage.html
461
no-damage.html
@ -258,22 +258,6 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
/* Damage on cassette housing */
|
||||
/* commented out for this version
|
||||
.cassette::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
background: radial-gradient(ellipse, rgba(255, 0, 255, 0.2) 0%, transparent 70%);
|
||||
border: 2px solid rgba(255, 0, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
*/
|
||||
|
||||
.cassette::after {
|
||||
content: '';
|
||||
@ -678,27 +662,33 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Main slide-in panel container */
|
||||
/* Main floating modal container */
|
||||
.alby-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -400px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
opacity: 0;
|
||||
width: 380px;
|
||||
height: 100vh;
|
||||
max-height: 90vh;
|
||||
background: linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 50%, #0f0f0f 100%);
|
||||
border-left: 6px solid #0a0a0a;
|
||||
border: 6px solid #0a0a0a;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
inset 0 0 0 2px #3a3a3a,
|
||||
inset 0 0 50px rgba(0,0,0,0.9),
|
||||
-10px 0 30px rgba(0,0,0,0.8);
|
||||
0 20px 60px rgba(0,0,0,0.8);
|
||||
z-index: 1001;
|
||||
transition: right 0.3s ease-out;
|
||||
transition: transform 0.2s ease-out, opacity 0.2s ease-out;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.alby-panel.active {
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Noise texture overlay for worn appearance */
|
||||
@ -724,12 +714,13 @@
|
||||
padding: 20px;
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #0f0f0f 100%);
|
||||
border-bottom: 4px solid #000;
|
||||
border-radius: 4px 4px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alby-panel-title {
|
||||
color: #cc8800;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-shadow:
|
||||
0 0 10px rgba(255, 170, 0, 0.5),
|
||||
@ -782,7 +773,7 @@
|
||||
/* Section labels with green CRT glow */
|
||||
.alby-label {
|
||||
color: #00ff00;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
text-shadow:
|
||||
0 0 5px #00ff00,
|
||||
0 0 10px #00ff00;
|
||||
@ -817,7 +808,7 @@
|
||||
|
||||
.alby-address {
|
||||
color: #cc8800;
|
||||
font-size: 13px;
|
||||
font-size: 15px;
|
||||
text-shadow: 0 0 8px rgba(255, 170, 0, 0.5);
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
@ -890,7 +881,7 @@
|
||||
padding: 10px;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px #00ff00;
|
||||
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
|
||||
@ -922,7 +913,7 @@
|
||||
padding: 12px;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
|
||||
box-shadow: inset 0 0 15px rgba(0,0,0,0.9);
|
||||
resize: none;
|
||||
@ -955,7 +946,7 @@
|
||||
border: 4px solid #000;
|
||||
color: #000;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
@ -1151,6 +1142,221 @@
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
const closeVideo = document.getElementById('closeVideo');
|
||||
|
||||
// ========================================
|
||||
// SOUND EFFECTS MODULE - START
|
||||
// Web Audio API synthesized sounds for tactile feedback
|
||||
// ========================================
|
||||
const SoundEffects = {
|
||||
ctx: null,
|
||||
scrubOscillator: null,
|
||||
scrubGain: null,
|
||||
|
||||
// Initialize AudioContext (must be called after user gesture)
|
||||
init() {
|
||||
if (!this.ctx) {
|
||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
// Resume if suspended (browser autoplay policy)
|
||||
if (this.ctx.state === 'suspended') {
|
||||
this.ctx.resume();
|
||||
}
|
||||
},
|
||||
|
||||
// Play a mechanical button click sound
|
||||
playButtonClick() {
|
||||
this.init();
|
||||
const now = this.ctx.currentTime;
|
||||
|
||||
// Create nodes for the click sound
|
||||
const clickOsc = this.ctx.createOscillator();
|
||||
const clickGain = this.ctx.createGain();
|
||||
const noiseGain = this.ctx.createGain();
|
||||
|
||||
// Low frequency "thunk" component (80-150Hz)
|
||||
clickOsc.type = 'sine';
|
||||
clickOsc.frequency.setValueAtTime(150, now);
|
||||
clickOsc.frequency.exponentialRampToValueAtTime(80, now + 0.05);
|
||||
|
||||
// Gain envelope for the thunk - fast attack, quick decay
|
||||
clickGain.gain.setValueAtTime(0.6, now);
|
||||
clickGain.gain.exponentialRampToValueAtTime(0.01, now + 0.08);
|
||||
|
||||
clickOsc.connect(clickGain);
|
||||
clickGain.connect(this.ctx.destination);
|
||||
|
||||
// High frequency "click" transient using noise
|
||||
const bufferSize = this.ctx.sampleRate * 0.02; // 20ms of noise
|
||||
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const noise = this.ctx.createBufferSource();
|
||||
noise.buffer = noiseBuffer;
|
||||
|
||||
// Bandpass filter to shape the noise into a click
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = 2000;
|
||||
filter.Q.value = 1;
|
||||
|
||||
noiseGain.gain.setValueAtTime(0.35, now);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.03);
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(noiseGain);
|
||||
noiseGain.connect(this.ctx.destination);
|
||||
|
||||
// Start and stop
|
||||
clickOsc.start(now);
|
||||
clickOsc.stop(now + 0.1);
|
||||
noise.start(now);
|
||||
noise.stop(now + 0.03);
|
||||
},
|
||||
|
||||
// Play tape wind/fast-forward sound for a specified duration
|
||||
// Uses filtered noise to simulate tape rushing past heads
|
||||
playTapeWind(direction = 'forward', duration = 0.5) {
|
||||
this.init();
|
||||
const now = this.ctx.currentTime;
|
||||
|
||||
// Create noise buffer for tape wind sound
|
||||
const sampleRate = this.ctx.sampleRate;
|
||||
const bufferSize = sampleRate * duration;
|
||||
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
|
||||
// Generate noise with slight amplitude modulation for realism
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
// Base noise
|
||||
const noise = Math.random() * 2 - 1;
|
||||
// Subtle amplitude wobble to simulate motor variation
|
||||
const wobble = 1 + 0.1 * Math.sin(i / sampleRate * 20 * Math.PI * 2);
|
||||
output[i] = noise * wobble;
|
||||
}
|
||||
|
||||
const noiseSource = this.ctx.createBufferSource();
|
||||
noiseSource.buffer = noiseBuffer;
|
||||
|
||||
// Bandpass filter to shape noise into tape wind character
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = 1200; // Center frequency for tape hiss
|
||||
filter.Q.value = 0.5; // Wide band for natural sound
|
||||
|
||||
// High shelf to add some brightness
|
||||
const highShelf = this.ctx.createBiquadFilter();
|
||||
highShelf.type = 'highshelf';
|
||||
highShelf.frequency.value = 3000;
|
||||
highShelf.gain.value = -6; // Reduce harshness
|
||||
|
||||
// Gain with envelope
|
||||
const gainNode = this.ctx.createGain();
|
||||
gainNode.gain.setValueAtTime(0, now);
|
||||
gainNode.gain.linearRampToValueAtTime(0.07, now + 0.03); // Quick attack
|
||||
gainNode.gain.setValueAtTime(0.07, now + duration - 0.05);
|
||||
gainNode.gain.linearRampToValueAtTime(0, now + duration); // Fade out
|
||||
|
||||
// Connect chain
|
||||
noiseSource.connect(filter);
|
||||
filter.connect(highShelf);
|
||||
highShelf.connect(gainNode);
|
||||
gainNode.connect(this.ctx.destination);
|
||||
|
||||
// Start and stop
|
||||
noiseSource.start(now);
|
||||
noiseSource.stop(now + duration);
|
||||
},
|
||||
|
||||
// Start continuous tape wind sound (for reel dragging)
|
||||
// Uses looping noise buffer for seamless continuous playback
|
||||
startTapeWindLoop() {
|
||||
this.init();
|
||||
|
||||
// Stop any existing loop
|
||||
this.stopTapeWindLoop();
|
||||
|
||||
const now = this.ctx.currentTime;
|
||||
const sampleRate = this.ctx.sampleRate;
|
||||
|
||||
// Create 1 second of loopable noise
|
||||
const bufferSize = sampleRate * 1;
|
||||
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
|
||||
// Generate noise with amplitude modulation
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
const noise = Math.random() * 2 - 1;
|
||||
const wobble = 1 + 0.1 * Math.sin(i / sampleRate * 20 * Math.PI * 2);
|
||||
output[i] = noise * wobble;
|
||||
}
|
||||
|
||||
this.tapeWindSource = this.ctx.createBufferSource();
|
||||
this.tapeWindSource.buffer = noiseBuffer;
|
||||
this.tapeWindSource.loop = true;
|
||||
|
||||
// Bandpass filter for tape character
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.value = 1200;
|
||||
filter.Q.value = 0.5;
|
||||
|
||||
// High shelf to reduce harshness
|
||||
const highShelf = this.ctx.createBiquadFilter();
|
||||
highShelf.type = 'highshelf';
|
||||
highShelf.frequency.value = 3000;
|
||||
highShelf.gain.value = -6;
|
||||
|
||||
// Gain with fade in
|
||||
this.tapeWindGain = this.ctx.createGain();
|
||||
this.tapeWindGain.gain.setValueAtTime(0, now);
|
||||
this.tapeWindGain.gain.linearRampToValueAtTime(0.07, now + 0.05);
|
||||
|
||||
// Connect chain
|
||||
this.tapeWindSource.connect(filter);
|
||||
filter.connect(highShelf);
|
||||
highShelf.connect(this.tapeWindGain);
|
||||
this.tapeWindGain.connect(this.ctx.destination);
|
||||
|
||||
this.tapeWindSource.start(now);
|
||||
},
|
||||
|
||||
// Stop the continuous tape wind sound
|
||||
stopTapeWindLoop() {
|
||||
if (this.tapeWindGain && this.ctx) {
|
||||
const now = this.ctx.currentTime;
|
||||
this.tapeWindGain.gain.linearRampToValueAtTime(0, now + 0.1);
|
||||
|
||||
// Clean up after fade
|
||||
const sourceToStop = this.tapeWindSource;
|
||||
setTimeout(() => {
|
||||
if (sourceToStop) {
|
||||
try {
|
||||
sourceToStop.stop();
|
||||
} catch(e) {}
|
||||
}
|
||||
}, 150);
|
||||
|
||||
this.tapeWindSource = null;
|
||||
this.tapeWindGain = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
// ========================================
|
||||
// SOUND EFFECTS MODULE - END
|
||||
// ========================================
|
||||
|
||||
// ========================================
|
||||
// BUTTON CLICK SOUNDS - Add to all buttons
|
||||
// Uses mousedown for immediate tactile feedback
|
||||
// ========================================
|
||||
[playBtn, pauseBtn, stopBtn, prevBtn, nextBtn, ejectBtn, lightningBtn].forEach(btn => {
|
||||
btn.addEventListener('mousedown', () => {
|
||||
SoundEffects.playButtonClick();
|
||||
});
|
||||
});
|
||||
|
||||
// Lightning button - opens Alby Lightning panel
|
||||
// (toggleAlbyPanel function defined in ALBY PANEL FUNCTIONALITY section below)
|
||||
lightningBtn.addEventListener('click', () => {
|
||||
@ -1273,49 +1479,125 @@
|
||||
// Previous track (prev button with bar)
|
||||
prevBtn.addEventListener('click', () => {
|
||||
console.log('Prev button clicked');
|
||||
currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
|
||||
const wasPlaying = !audio.paused;
|
||||
|
||||
// Play tape wind sound
|
||||
SoundEffects.playTapeWind('forward', 0.5);
|
||||
|
||||
// Speed up audio to 4x during transition (if playing)
|
||||
if (wasPlaying) {
|
||||
audio.playbackRate = 4;
|
||||
}
|
||||
|
||||
// Speed up reel animation during transition
|
||||
reelLeft.style.animationDuration = '0.3s';
|
||||
reelRight.style.animationDuration = '0.3s';
|
||||
tapeLeft.style.animationDuration = '0.3s';
|
||||
tapeRight.style.animationDuration = '0.3s';
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
|
||||
// Delay before loading new track
|
||||
setTimeout(() => {
|
||||
// Reset animation speed and playback rate
|
||||
reelLeft.style.animationDuration = '';
|
||||
reelRight.style.animationDuration = '';
|
||||
tapeLeft.style.animationDuration = '';
|
||||
tapeRight.style.animationDuration = '';
|
||||
audio.playbackRate = 1;
|
||||
|
||||
currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
if (wasPlaying) {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
} else {
|
||||
reelLeft.classList.remove('spinning');
|
||||
reelRight.classList.remove('spinning');
|
||||
tapeLeft.classList.remove('spinning');
|
||||
tapeRight.classList.remove('spinning');
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Next track (next button with bar)
|
||||
nextBtn.addEventListener('click', () => {
|
||||
console.log('Next button clicked');
|
||||
currentTrack = (currentTrack + 1) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
|
||||
const wasPlaying = !audio.paused;
|
||||
|
||||
// Play tape wind sound
|
||||
SoundEffects.playTapeWind('forward', 0.5);
|
||||
|
||||
// Speed up audio to 4x during transition (if playing)
|
||||
if (wasPlaying) {
|
||||
audio.playbackRate = 4;
|
||||
}
|
||||
|
||||
// Speed up reel animation during transition
|
||||
reelLeft.style.animationDuration = '0.3s';
|
||||
reelRight.style.animationDuration = '0.3s';
|
||||
tapeLeft.style.animationDuration = '0.3s';
|
||||
tapeRight.style.animationDuration = '0.3s';
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
|
||||
// Delay before loading new track
|
||||
setTimeout(() => {
|
||||
// Reset animation speed and playback rate
|
||||
reelLeft.style.animationDuration = '';
|
||||
reelRight.style.animationDuration = '';
|
||||
tapeLeft.style.animationDuration = '';
|
||||
tapeRight.style.animationDuration = '';
|
||||
audio.playbackRate = 1;
|
||||
|
||||
currentTrack = (currentTrack + 1) % playlist.length;
|
||||
audio.src = playlist[currentTrack].url;
|
||||
trackNameInner.textContent = playlist[currentTrack].name;
|
||||
resetTitleScroll(); // Reset title for new track
|
||||
resetTapeSizes(); // Reset tape for new track
|
||||
audio.load();
|
||||
audio.oncanplay = function() {
|
||||
if (wasPlaying) {
|
||||
const playPromise = audio.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(() => {
|
||||
reelLeft.classList.add('spinning');
|
||||
reelRight.classList.add('spinning');
|
||||
tapeLeft.classList.add('spinning');
|
||||
tapeRight.classList.add('spinning');
|
||||
startTitleScroll();
|
||||
}).catch(e => console.log('Play failed:', e));
|
||||
}
|
||||
} else {
|
||||
reelLeft.classList.remove('spinning');
|
||||
reelRight.classList.remove('spinning');
|
||||
tapeLeft.classList.remove('spinning');
|
||||
tapeRight.classList.remove('spinning');
|
||||
}
|
||||
audio.oncanplay = null;
|
||||
};
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Drag on reels to scrub audio
|
||||
@ -1324,13 +1606,19 @@
|
||||
let startTime = 0;
|
||||
let wasPlayingBeforeDrag = false;
|
||||
|
||||
let lastDragY = 0;
|
||||
|
||||
let tapeWindPlaying = false;
|
||||
|
||||
function startDrag(e) {
|
||||
if (audio.duration) {
|
||||
e.preventDefault(); // Prevent text selection during drag
|
||||
isDragging = true;
|
||||
startY = e.clientY || e.touches[0].clientY;
|
||||
lastDragY = startY;
|
||||
startTime = audio.currentTime;
|
||||
wasPlayingBeforeDrag = !audio.paused;
|
||||
tapeWindPlaying = false;
|
||||
// Don't pause - let audio continue playing while scrubbing
|
||||
}
|
||||
}
|
||||
@ -1339,10 +1627,35 @@
|
||||
if (isDragging && audio.duration) {
|
||||
const currentY = e.clientY || e.touches[0].clientY;
|
||||
const deltaY = startY - currentY;
|
||||
const instantDeltaY = lastDragY - currentY;
|
||||
lastDragY = currentY;
|
||||
|
||||
const scrubAmount = (deltaY / 100) * audio.duration * 0.1;
|
||||
const newTime = Math.max(0, Math.min(audio.duration, startTime + scrubAmount));
|
||||
audio.currentTime = newTime;
|
||||
|
||||
// Calculate scrub speed for audio effects
|
||||
// Positive deltaY = dragging up = fast forward
|
||||
// Negative deltaY = dragging down = rewind
|
||||
const scrubSpeed = Math.min(Math.abs(instantDeltaY) / 20, 1); // Normalize to 0-1
|
||||
|
||||
// Start tape wind sound only when actually moving
|
||||
if (scrubSpeed > 0.1 && !tapeWindPlaying) {
|
||||
SoundEffects.startTapeWindLoop();
|
||||
tapeWindPlaying = true;
|
||||
} else if (scrubSpeed <= 0.1 && tapeWindPlaying) {
|
||||
SoundEffects.stopTapeWindLoop();
|
||||
tapeWindPlaying = false;
|
||||
}
|
||||
|
||||
// Bonus: Adjust playback rate for sped-up audio effect (both directions)
|
||||
// Speed up to 4x when scrubbing in either direction
|
||||
if (wasPlayingBeforeDrag && scrubSpeed > 0.1) {
|
||||
audio.playbackRate = 1 + (scrubSpeed * 3); // 1x to 4x
|
||||
} else if (wasPlayingBeforeDrag) {
|
||||
audio.playbackRate = 1;
|
||||
}
|
||||
|
||||
// Keep playing during scrub
|
||||
if (audio.paused && wasPlayingBeforeDrag) {
|
||||
audio.play();
|
||||
@ -1353,6 +1666,14 @@
|
||||
function endDrag() {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
|
||||
// Stop tape wind sound and reset playback rate
|
||||
if (tapeWindPlaying) {
|
||||
SoundEffects.stopTapeWindLoop();
|
||||
tapeWindPlaying = false;
|
||||
}
|
||||
audio.playbackRate = 1;
|
||||
|
||||
// Continue playing if it was playing before
|
||||
if (wasPlayingBeforeDrag && audio.paused) {
|
||||
audio.play();
|
||||
@ -1579,13 +1900,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for successful payment and reset form fields
|
||||
// Listen for successful payment, reset form fields, and close modal
|
||||
albySimpleBoost.addEventListener('success', (e) => {
|
||||
// Reset form to default values
|
||||
albyAmount.value = '1.0';
|
||||
albyMemo.value = '';
|
||||
updateAlbyCharCount();
|
||||
updateAlbyBoostButton();
|
||||
// Close the modal after successful boost
|
||||
toggleAlbyPanel();
|
||||
});
|
||||
|
||||
// Escape key handler for Alby panel
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user