performance tweaks

This commit is contained in:
cottongin 2026-01-18 18:51:11 -05:00
parent 373fe8b835
commit 7ef59a57ab
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
3 changed files with 163 additions and 117 deletions

View File

@ -167,6 +167,7 @@ body {
pointer-events: none;
animation: flicker 4s infinite;
z-index: 3;
will-change: opacity; /* Performance: GPU-accelerated opacity animation */
}
@keyframes flicker {
@ -277,11 +278,13 @@ body {
.vhs-tracking.active.vertical {
opacity: 1;
animation: vhsScrollVertical 0.4s linear;
will-change: transform, opacity; /* Performance: GPU-accelerated animation */
}
.vhs-tracking.active.horizontal {
opacity: 1;
animation: vhsScrollHorizontal 0.4s linear;
will-change: transform, opacity; /* Performance: GPU-accelerated animation */
}
@keyframes vhsScrollVertical {
@ -304,6 +307,7 @@ body {
pointer-events: none;
z-index: 20;
overflow: hidden;
contain: strict; /* Performance: isolates paint scope for animated children */
}
.dim-spot {
@ -340,24 +344,6 @@ body {
animation: dimSpotDrift3 50s ease-in-out infinite;
}
.dim-spot-4 {
bottom: 0%;
right: 0%;
width: 45%;
height: 55%;
background: radial-gradient(ellipse 80% 90% at center, rgba(0, 0, 0, 0.5) 0%, transparent 65%);
animation: dimSpotDrift4 60s ease-in-out infinite;
}
.dim-spot-5 {
top: 35%;
left: 55%;
width: 30%;
height: 35%;
background: radial-gradient(circle at center, rgba(0, 0, 0, 0.35) 0%, transparent 60%);
animation: dimSpotDrift5 40s ease-in-out infinite;
}
/* Slow drifting animations - each spot moves in a different pattern */
@keyframes dimSpotDrift1 {
0%, 100% { transform: translate(0%, 0%); }
@ -380,20 +366,6 @@ body {
75% { transform: translate(-4%, -10%); }
}
@keyframes dimSpotDrift4 {
0%, 100% { transform: translate(0%, 0%); }
25% { transform: translate(-8%, -6%); }
50% { transform: translate(-12%, 4%); }
75% { transform: translate(4%, -8%); }
}
@keyframes dimSpotDrift5 {
0%, 100% { transform: translate(0%, 0%); }
25% { transform: translate(-15%, 10%); }
50% { transform: translate(10%, 15%); }
75% { transform: translate(15%, -10%); }
}
/* ========================================
DISPLAY GLITCH EFFECTS - END
======================================== */

View File

@ -11,6 +11,8 @@
<meta name="apple-mobile-web-app-title" content="ECHO REALITY" />
<link rel="manifest" href="/assets/site.webmanifest" />
<link rel="stylesheet" href="assets/styles.css">
<!-- Preconnect to esm.sh for faster simple-boost loading -->
<link rel="preconnect" href="https://esm.sh">
</head>
<body>
<div class="player">
@ -18,13 +20,11 @@
<button class="lightning-btn" id="lightningBtn" title="Menu">&#9889;</button>
<div class="display" id="display">
<!-- Dim spots - worn phosphor effect (slowly drifting) -->
<!-- Dim spots - worn phosphor effect (slowly drifting) - reduced to 3 for performance -->
<div class="dim-spots-container">
<div class="dim-spot dim-spot-1"></div>
<div class="dim-spot dim-spot-2"></div>
<div class="dim-spot dim-spot-3"></div>
<div class="dim-spot dim-spot-4"></div>
<div class="dim-spot dim-spot-5"></div>
</div>
<!-- VHS tracking lines overlay (controlled by DisplayGlitch module) -->
<div class="vhs-tracking" id="vhsTracking"></div>

View File

@ -63,23 +63,20 @@ 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
* Uses parallel loading for better performance
* Falls back to 240 seconds (4 min) if metadata can't be loaded
*/
async function loadAllDurations() {
console.log('Loading track durations...');
trackDurations = [];
trackDurations = new Array(playlist.length);
for (let i = 0; i < playlist.length; i++) {
// Load track 0 from main audio element first
try {
// For track 0, use the main audio element since loadTrack(0) already loads it
if (i === 0 && currentTrack === 0) {
// Wait for main audio to load metadata if not already
if (currentTrack === 0) {
if (audio.duration && !isNaN(audio.duration)) {
trackDurations[i] = audio.duration;
trackDurations[0] = audio.duration;
} else {
// Wait for main audio metadata
trackDurations[i] = await new Promise((resolve) => {
trackDurations[0] = await new Promise((resolve) => {
if (audio.duration && !isNaN(audio.duration)) {
resolve(audio.duration);
} else {
@ -91,13 +88,31 @@ async function loadAllDurations() {
});
}
} else {
// For other tracks, use temp audio element
trackDurations[i] = await getTrackDuration(playlist[i].url);
trackDurations[0] = await getTrackDuration(playlist[0].url);
}
console.log(`Track ${i + 1} duration: ${formatTime(trackDurations[i])}`);
console.log(`Track 1 duration: ${formatTime(trackDurations[0])}`);
} catch (e) {
console.warn(`Failed to get duration for track ${i + 1}, using fallback`);
trackDurations[i] = 240; // 4 minute fallback
console.warn('Failed to get duration for track 1, using fallback');
trackDurations[0] = 240;
}
// Load remaining tracks in parallel for faster loading
if (playlist.length > 1) {
const otherTrackPromises = playlist.slice(1).map(async (track, index) => {
const trackIndex = index + 1;
try {
const duration = await getTrackDuration(track.url);
console.log(`Track ${trackIndex + 1} duration: ${formatTime(duration)}`);
return duration;
} catch (e) {
console.warn(`Failed to get duration for track ${trackIndex + 1}, using fallback`);
return 240; // 4 minute fallback
}
});
const otherDurations = await Promise.all(otherTrackPromises);
for (let i = 0; i < otherDurations.length; i++) {
trackDurations[i + 1] = otherDurations[i];
}
}
@ -227,14 +242,20 @@ function getTrackStartPosition(trackIndex) {
// ========================================
// SOUND EFFECTS MODULE - START
// Web Audio API synthesized sounds for tactile feedback
// Pre-generated noise buffers for better performance
// ========================================
const SoundEffects = {
ctx: null,
// Cached noise buffers - generated once, reused for all sounds
clickNoiseBuffer: null,
tapeWindNoiseBuffer: null,
// Initialize AudioContext (must be called after user gesture)
// Initialize AudioContext and pre-generate noise buffers (must be called after user gesture)
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
// Pre-generate noise buffers on first init
this.generateNoiseBuffers();
}
// Resume if suspended (browser autoplay policy)
if (this.ctx.state === 'suspended') {
@ -242,6 +263,29 @@ const SoundEffects = {
}
},
// Pre-generate all noise buffers once for reuse
generateNoiseBuffers() {
const sampleRate = this.ctx.sampleRate;
// Generate click noise buffer (20ms)
const clickBufferSize = Math.floor(sampleRate * 0.02);
this.clickNoiseBuffer = this.ctx.createBuffer(1, clickBufferSize, sampleRate);
const clickOutput = this.clickNoiseBuffer.getChannelData(0);
for (let i = 0; i < clickBufferSize; i++) {
clickOutput[i] = Math.random() * 2 - 1;
}
// Generate tape wind noise buffer (1 second with wobble)
const windBufferSize = sampleRate * 1;
this.tapeWindNoiseBuffer = this.ctx.createBuffer(1, windBufferSize, sampleRate);
const windOutput = this.tapeWindNoiseBuffer.getChannelData(0);
for (let i = 0; i < windBufferSize; i++) {
const noise = Math.random() * 2 - 1;
const wobble = 1 + 0.1 * Math.sin(i / sampleRate * 20 * Math.PI * 2);
windOutput[i] = noise * wobble;
}
},
// Play a mechanical button click sound
playButtonClick() {
this.init();
@ -264,16 +308,9 @@ const SoundEffects = {
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;
}
// High frequency "click" transient using cached noise buffer
const noise = this.ctx.createBufferSource();
noise.buffer = noiseBuffer;
noise.buffer = this.clickNoiseBuffer;
// Bandpass filter to shape the noise into a click
const filter = this.ctx.createBiquadFilter();
@ -296,7 +333,7 @@ const SoundEffects = {
},
// Start continuous tape wind sound (for reel dragging)
// Uses looping noise buffer for seamless continuous playback
// Uses cached looping noise buffer for seamless continuous playback
startTapeWindLoop() {
this.init();
@ -304,22 +341,10 @@ const SoundEffects = {
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;
}
// Use cached tape wind noise buffer
this.tapeWindSource = this.ctx.createBufferSource();
this.tapeWindSource.buffer = noiseBuffer;
this.tapeWindSource.buffer = this.tapeWindNoiseBuffer;
this.tapeWindSource.loop = true;
// Bandpass filter for tape character
@ -611,22 +636,49 @@ lightningBtn.addEventListener('click', () => {
// ========================================
// TITLE SCROLL ANIMATION (JavaScript controlled bounce)
// Uses requestAnimationFrame for better performance
// Moves 1px at a time, bounces at edges, pauses with player
// ========================================
let titleScrollPosition = 0;
let titleScrollDirection = 1; // 1 = moving left (text shifts left), -1 = moving right
let titleScrollInterval = null;
let titleScrollRAF = null;
let titleScrollLastTime = 0;
const SCROLL_SPEED = 333; // milliseconds between 1px moves (1 second per ~3 pixels)
// Cached dimensions - updated only when track changes or on resize
let cachedContainerWidth = 0;
let cachedTextWidth = 0;
let cachedMaxScroll = 0;
let cachedTextLonger = false;
/**
* Cache the container and text dimensions for title scroll
* Call this when track changes or window resizes
*/
function cacheTitleDimensions() {
cachedContainerWidth = trackName.offsetWidth;
cachedTextWidth = trackNameInner.offsetWidth;
cachedTextLonger = cachedTextWidth > cachedContainerWidth;
// Calculate max scroll
if (cachedTextLonger) {
cachedMaxScroll = cachedTextWidth - cachedContainerWidth;
} else {
cachedMaxScroll = cachedContainerWidth - cachedTextWidth;
}
}
function startTitleScroll() {
if (titleScrollInterval) return; // Already scrolling
titleScrollInterval = setInterval(updateTitleScroll, SCROLL_SPEED);
if (titleScrollRAF) return; // Already scrolling
cacheTitleDimensions();
titleScrollLastTime = performance.now();
titleScrollRAF = requestAnimationFrame(titleScrollLoop);
}
function stopTitleScroll() {
if (titleScrollInterval) {
clearInterval(titleScrollInterval);
titleScrollInterval = null;
if (titleScrollRAF) {
cancelAnimationFrame(titleScrollRAF);
titleScrollRAF = null;
}
}
@ -635,46 +687,52 @@ function resetTitleScroll() {
titleScrollPosition = 0;
titleScrollDirection = 1;
trackNameInner.style.left = '0px';
cacheTitleDimensions();
}
/**
* RAF-based scroll loop - tracks elapsed time to maintain consistent speed
*/
function titleScrollLoop(currentTime) {
const elapsed = currentTime - titleScrollLastTime;
// Only update position when enough time has passed (maintains same speed as before)
if (elapsed >= SCROLL_SPEED) {
titleScrollLastTime = currentTime - (elapsed % SCROLL_SPEED);
updateTitleScroll();
}
titleScrollRAF = requestAnimationFrame(titleScrollLoop);
}
function updateTitleScroll() {
const containerWidth = trackName.offsetWidth;
const textWidth = trackNameInner.offsetWidth;
// Calculate max scroll - text scrolls until its right edge hits container's right edge
// If text is shorter than container, scroll until text's left edge hits container's right edge
let maxScroll;
if (textWidth > containerWidth) {
// Text is longer - scroll until right edge of text reaches right edge of container
maxScroll = textWidth - containerWidth;
} else {
// Text is shorter - scroll across the container width but keep text visible
// Text starts at left (0), scrolls right until left edge is at (containerWidth - textWidth)
maxScroll = containerWidth - textWidth;
}
// Move 1px in current direction
titleScrollPosition += titleScrollDirection;
// Bounce at edges
if (titleScrollPosition >= maxScroll) {
titleScrollPosition = maxScroll;
if (titleScrollPosition >= cachedMaxScroll) {
titleScrollPosition = cachedMaxScroll;
titleScrollDirection = -1; // Start moving back
} else if (titleScrollPosition <= 0) {
titleScrollPosition = 0;
titleScrollDirection = 1; // Start moving forward
}
// Apply position
// If text is longer: shift text LEFT (negative value) so we see the end
// If text is shorter: shift text RIGHT (positive value) to scroll across display
if (textWidth > containerWidth) {
// Apply position using cached text length comparison
if (cachedTextLonger) {
trackNameInner.style.left = -titleScrollPosition + 'px';
} else {
trackNameInner.style.left = titleScrollPosition + 'px';
}
}
// Recache dimensions on window resize
window.addEventListener('resize', () => {
if (titleScrollRAF) {
cacheTitleDimensions();
}
});
// ========================================
// AUDIO LOADING HELPERS - START
// Unified helpers for loading tracks and waiting for audio ready
@ -1138,11 +1196,27 @@ function setTapeSizesAtProgress(progress) {
resetTapeSizes();
// Update time display and tape sizes
// Throttle timeupdate for better performance
let lastTimeUpdateTime = 0;
let lastFormattedTime = '';
const TIME_UPDATE_THROTTLE = 200; // Update at most 5 times per second
// Update time display and tape sizes (throttled)
audio.addEventListener('timeupdate', () => {
const now = performance.now();
if (now - lastTimeUpdateTime < TIME_UPDATE_THROTTLE) return;
lastTimeUpdateTime = now;
const current = formatTime(audio.currentTime);
const duration = formatTime(audio.duration);
timeDisplay.textContent = `${current} / ${duration}`;
const formatted = `${current} / ${duration}`;
// Skip DOM update if time display hasn't changed
if (formatted !== lastFormattedTime) {
lastFormattedTime = formatted;
timeDisplay.textContent = formatted;
}
updateTapeSizes();
});