diff --git a/index.html b/index.html
index 3d6f0ce..c9f0319 100644
--- a/index.html
+++ b/index.html
@@ -1251,6 +1251,152 @@
// Tape size constants (in pixels)
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)
+
+ // ========================================
+ // 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} 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 lightningBtn = document.getElementById('lightningBtn');
const videoOverlay = document.getElementById('videoOverlay');
@@ -1759,6 +1905,9 @@
// Initial track load (async, no await needed for initial load)
loadTrack(0);
+ // Load all track durations for continuous tape model
+ loadAllDurations();
+
// Set initial volume
audio.volume = 0.7;
@@ -1782,226 +1931,222 @@
stopTitleScroll();
});
- // Stop button - resets title to start position
- stopBtn.addEventListener('click', () => {
+ // Stop button - rewinds tape to the beginning (Track 1, position 0) with animation
+ stopBtn.addEventListener('click', async () => {
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;
reelLeft.classList.remove('spinning');
reelRight.classList.remove('spinning');
tapeLeft.classList.remove('spinning');
tapeRight.classList.remove('spinning');
resetTitleScroll();
- resetTapeSizes();
+ resetTapeSizes(); // Ensure tape visuals are at beginning
});
// Previous track (prev button with bar)
+ // Uses animated seek to smoothly wind tape to previous track position
prevBtn.addEventListener('click', () => {
console.log('Prev button clicked');
-
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 (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);
+ const targetTrack = (currentTrack - 1 + playlist.length) % playlist.length;
+ animatedSeekToTrack(targetTrack, wasPlaying);
});
// Next track (next button with bar)
+ // Uses animated seek to smoothly wind tape to next track position
nextBtn.addEventListener('click', () => {
console.log('Next button clicked');
-
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 (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);
+ const targetTrack = (currentTrack + 1) % playlist.length;
+ animatedSeekToTrack(targetTrack, wasPlaying);
});
- // Drag on reels to scrub audio
+ // Drag on reels to scrub audio (supports cross-track scrubbing)
let isDragging = false;
let startY = 0;
- let startTime = 0;
+ let startTapePosition = 0; // Global tape position at drag start
let wasPlayingBeforeDrag = false;
-
let lastDragY = 0;
-
let tapeWindPlaying = false;
+ let scrubTrackChangeInProgress = false; // Prevents multiple track loads during scrub
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
isDragging = true;
startY = e.clientY || e.touches[0].clientY;
lastDragY = startY;
- startTime = audio.currentTime;
+ startTapePosition = getCurrentTapePosition();
wasPlayingBeforeDrag = !audio.paused;
tapeWindPlaying = false;
+ scrubTrackChangeInProgress = false;
// Don't pause - let audio continue playing while scrubbing
}
}
function drag(e) {
- 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();
- }
+ if (!isDragging) return;
+ if (scrubTrackChangeInProgress) return; // Wait for track change to complete
+
+ const currentY = e.clientY || e.touches[0].clientY;
+ const deltaY = startY - currentY;
+ const instantDeltaY = lastDragY - currentY;
+ lastDragY = currentY;
+
+ // Calculate scrub speed for audio effects
+ 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;
}
+
+ // Calculate target tape position based on drag distance
+ // 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) {
+ audio.playbackRate = 1 + (scrubSpeed * 3); // 1x to 4x
+ } else if (wasPlayingBeforeDrag) {
+ audio.playbackRate = 1;
+ }
+
+ // Keep playing during scrub
+ if (audio.paused && wasPlayingBeforeDrag) {
+ 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() {
if (isDragging) {
isDragging = false;
+ scrubTrackChangeInProgress = false;
// Stop tape wind sound and reset playback rate
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)
[reelLeft, reelRight, tapeLeft, tapeRight, reelContainerLeft, reelContainerRight].forEach(el => {
el.addEventListener('mousedown', startDrag);
@@ -2036,7 +2303,7 @@
audio.addEventListener('ended', async () => {
currentTrack = (currentTrack + 1) % playlist.length;
await loadTrack(currentTrack);
- resetTapeSizes();
+ // Tape position continues naturally - don't reset
audio.play();
reelLeft.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 global tape position
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;
- // Left spool: starts full, decreases as track plays
+ // Left spool: starts full, decreases as tape plays
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);
tapeLeft.style.setProperty('--tape-size', leftSize + 'px');
tapeRight.style.setProperty('--tape-size', rightSize + 'px');
}
- // Initialize tape sizes
+ // Initialize tape sizes - rewind to beginning of tape
function resetTapeSizes() {
tapeLeft.style.setProperty('--tape-size', TAPE_MAX_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();
// Update time display and tape sizes