switched to full-tape reel system

This commit is contained in:
cottongin 2026-01-17 12:12:44 -05:00
parent 7d16bdc436
commit b4e69b78a2
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262

View File

@ -1251,6 +1251,152 @@
// Tape size constants (in pixels) // Tape size constants (in pixels)
const TAPE_MIN_SIZE = 62; // Minimum tape size (just larger than reel-inner) const TAPE_MIN_SIZE = 62; // Minimum tape size (just larger than reel-inner)
const TAPE_MAX_SIZE = 166; // Maximum tape size (fills most of reel) const TAPE_MAX_SIZE = 166; // Maximum tape size (fills most of reel)
// ========================================
// CONTINUOUS TAPE MODEL - STATE
// Tracks are treated as sequential positions on a single tape
// ========================================
let trackDurations = []; // Duration of each track in seconds
let trackStartPositions = []; // Cumulative start position of each track on tape
let totalTapeDuration = 0; // Total length of entire tape
let durationsLoaded = false; // Flag indicating when all durations are known
/**
* Load metadata for all tracks to get their durations
* Uses a temporary audio element to fetch duration without full download
* Falls back to 240 seconds (4 min) if metadata can't be loaded
*/
async function loadAllDurations() {
console.log('Loading track durations...');
trackDurations = [];
for (let i = 0; i < playlist.length; i++) {
try {
const duration = await getTrackDuration(playlist[i].url);
trackDurations[i] = duration;
console.log(`Track ${i + 1} duration: ${formatTime(duration)}`);
} catch (e) {
console.warn(`Failed to get duration for track ${i + 1}, using fallback`);
trackDurations[i] = 240; // 4 minute fallback
}
}
// Calculate cumulative start positions
trackStartPositions = [0];
for (let i = 1; i < trackDurations.length; i++) {
trackStartPositions[i] = trackStartPositions[i - 1] + trackDurations[i - 1];
}
// Calculate total tape duration
totalTapeDuration = trackStartPositions[trackDurations.length - 1] + trackDurations[trackDurations.length - 1];
durationsLoaded = true;
console.log(`Total tape duration: ${formatTime(totalTapeDuration)}`);
console.log('Track start positions:', trackStartPositions.map(formatTime));
// Update tape sizes now that we have durations
updateTapeSizes();
}
/**
* Get duration of a single track using a temporary audio element
* @param {string} url - URL of the audio track
* @returns {Promise<number>} Duration in seconds
*/
function getTrackDuration(url) {
return new Promise((resolve, reject) => {
const tempAudio = new Audio();
// Set timeout for slow loads
const timeout = setTimeout(() => {
tempAudio.src = '';
reject(new Error('Timeout loading metadata'));
}, 10000); // 10 second timeout
tempAudio.addEventListener('loadedmetadata', () => {
clearTimeout(timeout);
const duration = tempAudio.duration;
tempAudio.src = ''; // Clean up
resolve(duration);
});
tempAudio.addEventListener('error', (e) => {
clearTimeout(timeout);
tempAudio.src = '';
reject(e);
});
// Try to use cached URL if available, otherwise use direct URL
TrackCache.getTrack(url).then(blobUrl => {
tempAudio.src = blobUrl;
}).catch(() => {
// Fall back to direct URL
tempAudio.src = url;
});
});
}
// ========================================
// TAPE POSITION HELPER FUNCTIONS
// Convert between track-local and global tape positions
// ========================================
/**
* Get the current position on the entire tape (global position)
* @returns {number} Position in seconds from tape start
*/
function getCurrentTapePosition() {
if (!durationsLoaded) return 0;
return trackStartPositions[currentTrack] + (audio.currentTime || 0);
}
/**
* Get the current tape progress as a value from 0 to 1
* @returns {number} Progress through entire tape (0 = start, 1 = end)
*/
function getTapeProgress() {
if (!durationsLoaded || !totalTapeDuration) return 0;
return getCurrentTapePosition() / totalTapeDuration;
}
/**
* Find which track contains a given global tape position
* @param {number} tapePosition - Position in seconds from tape start
* @returns {Object} { trackIndex, positionInTrack }
*/
function findTrackAtPosition(tapePosition) {
if (!durationsLoaded) return { trackIndex: 0, positionInTrack: 0 };
// Clamp to valid range
tapePosition = Math.max(0, Math.min(totalTapeDuration, tapePosition));
// Find the track that contains this position
for (let i = trackStartPositions.length - 1; i >= 0; i--) {
if (tapePosition >= trackStartPositions[i]) {
const positionInTrack = tapePosition - trackStartPositions[i];
// Clamp position within track duration
const clampedPosition = Math.min(positionInTrack, trackDurations[i] - 0.01);
return {
trackIndex: i,
positionInTrack: Math.max(0, clampedPosition)
};
}
}
// Fallback to start
return { trackIndex: 0, positionInTrack: 0 };
}
/**
* Get the global tape position for the start of a specific track
* @param {number} trackIndex - Index of the track
* @returns {number} Position in seconds from tape start
*/
function getTrackStartPosition(trackIndex) {
if (!durationsLoaded) return 0;
return trackStartPositions[trackIndex] || 0;
}
const ejectBtn = document.getElementById('ejectBtn'); const ejectBtn = document.getElementById('ejectBtn');
const lightningBtn = document.getElementById('lightningBtn'); const lightningBtn = document.getElementById('lightningBtn');
const videoOverlay = document.getElementById('videoOverlay'); const videoOverlay = document.getElementById('videoOverlay');
@ -1759,6 +1905,9 @@
// Initial track load (async, no await needed for initial load) // Initial track load (async, no await needed for initial load)
loadTrack(0); loadTrack(0);
// Load all track durations for continuous tape model
loadAllDurations();
// Set initial volume // Set initial volume
audio.volume = 0.7; audio.volume = 0.7;
@ -1782,197 +1931,132 @@
stopTitleScroll(); stopTitleScroll();
}); });
// Stop button - resets title to start position // Stop button - rewinds tape to the beginning (Track 1, position 0) with animation
stopBtn.addEventListener('click', () => { stopBtn.addEventListener('click', async () => {
audio.pause(); audio.pause();
// Check if we need to rewind (not already at beginning)
const needsRewind = currentTrack !== 0 || audio.currentTime > 0.5;
if (needsRewind && durationsLoaded) {
// Animate rewind to beginning
const animationDuration = 500;
const startProgress = getTapeProgress();
const endProgress = 0;
// Start rewind sound and animation
SoundEffects.startTapeWindLoop();
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');
// Animate tape sizes
const startTime = performance.now();
await new Promise(resolve => {
function animate(time) {
const elapsed = time - startTime;
const t = Math.min(elapsed / animationDuration, 1);
const easedT = easeInOutQuad(t);
const currentProgress = startProgress + (endProgress - startProgress) * easedT;
setTapeSizesAtProgress(currentProgress);
if (t < 1) {
requestAnimationFrame(animate);
} else {
resolve();
}
}
requestAnimationFrame(animate);
});
// Stop sound and reset animation speed
SoundEffects.stopTapeWindLoop();
reelLeft.style.animationDuration = '';
reelRight.style.animationDuration = '';
tapeLeft.style.animationDuration = '';
tapeRight.style.animationDuration = '';
}
// Load track 0 if not already on it
if (currentTrack !== 0) {
currentTrack = 0;
trackNameInner.textContent = playlist[0].name;
try {
const blobUrl = await TrackCache.getTrack(playlist[0].url);
audio.src = blobUrl;
} catch (e) {
audio.src = playlist[0].url;
}
audio.load();
}
audio.currentTime = 0; audio.currentTime = 0;
reelLeft.classList.remove('spinning'); reelLeft.classList.remove('spinning');
reelRight.classList.remove('spinning'); reelRight.classList.remove('spinning');
tapeLeft.classList.remove('spinning'); tapeLeft.classList.remove('spinning');
tapeRight.classList.remove('spinning'); tapeRight.classList.remove('spinning');
resetTitleScroll(); resetTitleScroll();
resetTapeSizes(); resetTapeSizes(); // Ensure tape visuals are at beginning
}); });
// Previous track (prev button with bar) // Previous track (prev button with bar)
// Uses animated seek to smoothly wind tape to previous track position
prevBtn.addEventListener('click', () => { prevBtn.addEventListener('click', () => {
console.log('Prev button clicked'); console.log('Prev button clicked');
const wasPlaying = !audio.paused; const wasPlaying = !audio.paused;
const targetTrack = (currentTrack - 1 + playlist.length) % playlist.length;
// Play tape wind sound animatedSeekToTrack(targetTrack, wasPlaying);
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 (async for cache support)
setTimeout(async () => {
// 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;
trackNameInner.textContent = playlist[currentTrack].name;
resetTitleScroll(); // Reset title for new track
resetTapeSizes(); // Reset tape for new track
// Load from cache or network
try {
const blobUrl = await TrackCache.getTrack(playlist[currentTrack].url);
audio.src = blobUrl;
} catch (e) {
console.warn('TrackCache: Caching unavailable, using direct URL');
audio.src = playlist[currentTrack].url;
}
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) // Next track (next button with bar)
// Uses animated seek to smoothly wind tape to next track position
nextBtn.addEventListener('click', () => { nextBtn.addEventListener('click', () => {
console.log('Next button clicked'); console.log('Next button clicked');
const wasPlaying = !audio.paused; const wasPlaying = !audio.paused;
const targetTrack = (currentTrack + 1) % playlist.length;
// Play tape wind sound animatedSeekToTrack(targetTrack, wasPlaying);
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 (async for cache support)
setTimeout(async () => {
// 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;
trackNameInner.textContent = playlist[currentTrack].name;
resetTitleScroll(); // Reset title for new track
resetTapeSizes(); // Reset tape for new track
// Load from cache or network
try {
const blobUrl = await TrackCache.getTrack(playlist[currentTrack].url);
audio.src = blobUrl;
} catch (e) {
console.warn('TrackCache: Caching unavailable, using direct URL');
audio.src = playlist[currentTrack].url;
}
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 // Drag on reels to scrub audio (supports cross-track scrubbing)
let isDragging = false; let isDragging = false;
let startY = 0; let startY = 0;
let startTime = 0; let startTapePosition = 0; // Global tape position at drag start
let wasPlayingBeforeDrag = false; let wasPlayingBeforeDrag = false;
let lastDragY = 0; let lastDragY = 0;
let tapeWindPlaying = false; let tapeWindPlaying = false;
let scrubTrackChangeInProgress = false; // Prevents multiple track loads during scrub
function startDrag(e) { function startDrag(e) {
if (audio.duration) { // Allow drag if we have duration info (either from track or global tape)
if (durationsLoaded || audio.duration) {
e.preventDefault(); // Prevent text selection during drag e.preventDefault(); // Prevent text selection during drag
isDragging = true; isDragging = true;
startY = e.clientY || e.touches[0].clientY; startY = e.clientY || e.touches[0].clientY;
lastDragY = startY; lastDragY = startY;
startTime = audio.currentTime; startTapePosition = getCurrentTapePosition();
wasPlayingBeforeDrag = !audio.paused; wasPlayingBeforeDrag = !audio.paused;
tapeWindPlaying = false; tapeWindPlaying = false;
scrubTrackChangeInProgress = false;
// Don't pause - let audio continue playing while scrubbing // Don't pause - let audio continue playing while scrubbing
} }
} }
function drag(e) { function drag(e) {
if (isDragging && audio.duration) { if (!isDragging) return;
if (scrubTrackChangeInProgress) return; // Wait for track change to complete
const currentY = e.clientY || e.touches[0].clientY; const currentY = e.clientY || e.touches[0].clientY;
const deltaY = startY - currentY; const deltaY = startY - currentY;
const instantDeltaY = lastDragY - currentY; const instantDeltaY = lastDragY - currentY;
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 // 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 const scrubSpeed = Math.min(Math.abs(instantDeltaY) / 20, 1); // Normalize to 0-1
// Start tape wind sound only when actually moving // Start tape wind sound only when actually moving
@ -1984,8 +2068,36 @@
tapeWindPlaying = false; tapeWindPlaying = false;
} }
// Bonus: Adjust playback rate for sped-up audio effect (both directions) // Calculate target tape position based on drag distance
// Speed up to 4x when scrubbing in either direction // Sensitivity: dragging 100px moves 10% of total tape (or current track if durations not loaded)
let targetTapePosition;
if (durationsLoaded && totalTapeDuration > 0) {
// Cross-track scrubbing mode
const scrubAmount = (deltaY / 100) * totalTapeDuration * 0.05;
targetTapePosition = Math.max(0, Math.min(totalTapeDuration - 0.01, startTapePosition + scrubAmount));
// Find which track this position falls in
const target = findTrackAtPosition(targetTapePosition);
if (target.trackIndex !== currentTrack) {
// Need to switch tracks
scrubTrackChangeInProgress = true;
loadTrackForScrub(target.trackIndex, target.positionInTrack).then(() => {
scrubTrackChangeInProgress = false;
updateTapeSizes();
});
} else {
// Same track, just seek
audio.currentTime = target.positionInTrack;
}
} else if (audio.duration) {
// Fallback to single-track scrubbing
const scrubAmount = (deltaY / 100) * audio.duration * 0.1;
const newTime = Math.max(0, Math.min(audio.duration, audio.currentTime + scrubAmount));
audio.currentTime = newTime;
}
// Adjust playback rate for sped-up audio effect
if (wasPlayingBeforeDrag && scrubSpeed > 0.1) { if (wasPlayingBeforeDrag && scrubSpeed > 0.1) {
audio.playbackRate = 1 + (scrubSpeed * 3); // 1x to 4x audio.playbackRate = 1 + (scrubSpeed * 3); // 1x to 4x
} else if (wasPlayingBeforeDrag) { } else if (wasPlayingBeforeDrag) {
@ -1996,12 +2108,45 @@
if (audio.paused && wasPlayingBeforeDrag) { if (audio.paused && wasPlayingBeforeDrag) {
audio.play(); audio.play();
} }
// Update tape visual
updateTapeSizes();
} }
/**
* Load a track during scrubbing and seek to position
* @param {number} trackIndex - Index of track to load
* @param {number} seekPosition - Position within track to seek to
*/
async function loadTrackForScrub(trackIndex, seekPosition) {
currentTrack = trackIndex;
trackNameInner.textContent = playlist[trackIndex].name;
resetTitleScroll();
try {
const blobUrl = await TrackCache.getTrack(playlist[trackIndex].url);
audio.src = blobUrl;
} catch (e) {
audio.src = playlist[trackIndex].url;
}
return new Promise((resolve) => {
audio.onloadedmetadata = () => {
audio.currentTime = seekPosition;
if (wasPlayingBeforeDrag) {
audio.play().catch(() => {});
}
audio.onloadedmetadata = null;
resolve();
};
audio.load();
});
} }
function endDrag() { function endDrag() {
if (isDragging) { if (isDragging) {
isDragging = false; isDragging = false;
scrubTrackChangeInProgress = false;
// Stop tape wind sound and reset playback rate // Stop tape wind sound and reset playback rate
if (tapeWindPlaying) { if (tapeWindPlaying) {
@ -2021,6 +2166,128 @@
} }
} }
// ========================================
// ANIMATED SEEK TO TRACK
// Smoothly animates tape reels when changing tracks via Next/Prev
// ========================================
let animatedSeekInProgress = false;
/**
* Easing function for smooth animation
*/
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
/**
* Animate tape reels to a target track with visual fast-forward/rewind effect
* @param {number} targetTrackIndex - Index of the track to seek to
* @param {boolean} wasPlaying - Whether audio was playing before seek
* @returns {Promise} Resolves when animation and track load complete
*/
async function animatedSeekToTrack(targetTrackIndex, wasPlaying) {
if (animatedSeekInProgress) return;
animatedSeekInProgress = true;
const animationDuration = 500; // ms
// Calculate start and end tape progress
const startProgress = durationsLoaded ? getTapeProgress() : 0;
const endProgress = durationsLoaded ? getTrackStartPosition(targetTrackIndex) / totalTapeDuration : 0;
// Determine direction
const direction = endProgress > startProgress ? 'forward' : 'backward';
// Start tape wind sound
SoundEffects.startTapeWindLoop();
// 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');
// Speed up audio during transition if playing
if (wasPlaying) {
audio.playbackRate = 4;
}
// Animate tape sizes from current to target position
const startTime = performance.now();
await new Promise(resolve => {
function animate(time) {
const elapsed = time - startTime;
const t = Math.min(elapsed / animationDuration, 1);
const easedT = easeInOutQuad(t);
// Interpolate progress
const currentProgress = startProgress + (endProgress - startProgress) * easedT;
// Update tape sizes
setTapeSizesAtProgress(currentProgress);
if (t < 1) {
requestAnimationFrame(animate);
} else {
resolve();
}
}
requestAnimationFrame(animate);
});
// Stop tape wind sound
SoundEffects.stopTapeWindLoop();
// Reset animation speed
reelLeft.style.animationDuration = '';
reelRight.style.animationDuration = '';
tapeLeft.style.animationDuration = '';
tapeRight.style.animationDuration = '';
audio.playbackRate = 1;
// Load the new track
currentTrack = targetTrackIndex;
trackNameInner.textContent = playlist[currentTrack].name;
resetTitleScroll();
try {
const blobUrl = await TrackCache.getTrack(playlist[currentTrack].url);
audio.src = blobUrl;
} catch (e) {
console.warn('TrackCache: Caching unavailable, using direct URL');
audio.src = playlist[currentTrack].url;
}
return new Promise(resolve => {
audio.oncanplay = function() {
if (wasPlaying) {
audio.play().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;
animatedSeekInProgress = false;
resolve();
};
audio.load();
});
}
// Add drag listeners to all reel elements (containers, tape, and inner spools) // Add drag listeners to all reel elements (containers, tape, and inner spools)
[reelLeft, reelRight, tapeLeft, tapeRight, reelContainerLeft, reelContainerRight].forEach(el => { [reelLeft, reelRight, tapeLeft, tapeRight, reelContainerLeft, reelContainerRight].forEach(el => {
el.addEventListener('mousedown', startDrag); el.addEventListener('mousedown', startDrag);
@ -2036,7 +2303,7 @@
audio.addEventListener('ended', async () => { audio.addEventListener('ended', async () => {
currentTrack = (currentTrack + 1) % playlist.length; currentTrack = (currentTrack + 1) % playlist.length;
await loadTrack(currentTrack); await loadTrack(currentTrack);
resetTapeSizes(); // Tape position continues naturally - don't reset
audio.play(); audio.play();
reelLeft.classList.add('spinning'); reelLeft.classList.add('spinning');
reelRight.classList.add('spinning'); reelRight.classList.add('spinning');
@ -2051,26 +2318,45 @@
}); });
// Update tape wound sizes based on playback progress // Update tape wound sizes based on playback progress
// Update tape wound sizes based on global tape position
function updateTapeSizes() { function updateTapeSizes() {
if (!audio.duration) return; // Use global tape progress if durations are loaded, otherwise fall back to current track
let progress;
if (durationsLoaded && totalTapeDuration > 0) {
progress = getTapeProgress();
} else if (audio.duration) {
// Fallback to per-track progress while durations are loading
progress = audio.currentTime / audio.duration;
} else {
return;
}
const progress = audio.currentTime / audio.duration;
const tapeRange = TAPE_MAX_SIZE - TAPE_MIN_SIZE; const tapeRange = TAPE_MAX_SIZE - TAPE_MIN_SIZE;
// Left spool: starts full, decreases as track plays // Left spool: starts full, decreases as tape plays
const leftSize = TAPE_MAX_SIZE - (progress * tapeRange); const leftSize = TAPE_MAX_SIZE - (progress * tapeRange);
// Right spool: starts empty, increases as track plays // Right spool: starts empty, increases as tape plays
const rightSize = TAPE_MIN_SIZE + (progress * tapeRange); const rightSize = TAPE_MIN_SIZE + (progress * tapeRange);
tapeLeft.style.setProperty('--tape-size', leftSize + 'px'); tapeLeft.style.setProperty('--tape-size', leftSize + 'px');
tapeRight.style.setProperty('--tape-size', rightSize + 'px'); tapeRight.style.setProperty('--tape-size', rightSize + 'px');
} }
// Initialize tape sizes // Initialize tape sizes - rewind to beginning of tape
function resetTapeSizes() { function resetTapeSizes() {
tapeLeft.style.setProperty('--tape-size', TAPE_MAX_SIZE + 'px'); tapeLeft.style.setProperty('--tape-size', TAPE_MAX_SIZE + 'px');
tapeRight.style.setProperty('--tape-size', TAPE_MIN_SIZE + 'px'); tapeRight.style.setProperty('--tape-size', TAPE_MIN_SIZE + 'px');
} }
// Set tape sizes to a specific progress value (0 to 1)
function setTapeSizesAtProgress(progress) {
const tapeRange = TAPE_MAX_SIZE - TAPE_MIN_SIZE;
const leftSize = TAPE_MAX_SIZE - (progress * tapeRange);
const rightSize = TAPE_MIN_SIZE + (progress * tapeRange);
tapeLeft.style.setProperty('--tape-size', leftSize + 'px');
tapeRight.style.setProperty('--tape-size', rightSize + 'px');
}
resetTapeSizes(); resetTapeSizes();
// Update time display and tape sizes // Update time display and tape sizes