Root Cause: Attempting to add auto-end functionality while simultaneously trying to fix rendering issues led to a complex, hard-to-debug state.
Attempted Solution: Use background(0, 0, 0) to fully clear the canvas each frame, eliminating trail accumulation.
// OLD CODE (v15): Semi-transparent overlay method
fill(0, 0, 0, TRAIL_FADE_AMOUNT);
rect(0, 0, width, height);
// NEW CODE (v16b): Full clear method
background(0, 0, 0); // Complete black background each frame
"This version offered no basic improvement. I want to start again with the last version (15) that worked as I wanted it to..."
This is a crucial moment in software development. When a solution doesn't improve the situation:
This is called "incremental development" - make small, testable changes rather than large, risky rewrites.
Instead of trying to fix rendering issues, we returned to v15 (which worked perfectly) and added ONLY the auto-end sequence.
Two constants control the end sequence timing:
// ============================================
// STAGE 16c: END SEQUENCE TIMING
// ============================================
// *** DELAY TILL END ***
// Wait time after last credit appears before starting music fade (milliseconds)
// Gives viewers time to read the final credit
const DELAY_TILL_END = 2000; // 2 seconds
// *** MUSIC FADE TIME ***
// Duration of music fade out effect (milliseconds)
// Gradual fade is more professional than abrupt stop
const MUSIC_FADE_TIME = 2000; // 2 seconds
Five variables track the end sequence state:
// *** STAGE 16c: End sequence tracking ***
let allCreditsFinished = false; // Track when all credits have completed
let endSequenceStartTime = 0; // When end sequence begins
let isFadingOut = false; // Track if currently fading music
let fadeStartVolume = 0; // Starting volume for fade
let fadeStartTime = 0; // When fade begins
New function monitors when all credits are done:
/**
* Check if all credits have finished and start end sequence
*/
function checkEndSequence() {
// Only check if animation is running
if (!isAnimating || isPaused) return;
// Check if all credits have been displayed
if (currentCreditIndex >= credits.length) {
// Check if all credits are inactive (finished animating)
const allInactive = credits.every(credit => !credit.active);
if (allInactive && !allCreditsFinished) {
// Start the end sequence
allCreditsFinished = true;
endSequenceStartTime = millis();
console.log('đŦ All credits finished. Starting end sequence...');
}
}
// If in end sequence, check if delay has passed
if (allCreditsFinished && !isFadingOut) {
const elapsed = millis() - endSequenceStartTime;
if (elapsed >= DELAY_TILL_END) {
startMusicFadeOut();
}
}
// Update music fade if active
if (isFadingOut) {
updateMusicFade();
}
}
Two functions handle the smooth volume reduction:
/**
* Start the music fade out process
*/
function startMusicFadeOut() {
isFadingOut = true;
fadeStartVolume = supermanMusic.getVolume();
fadeStartTime = millis();
console.log('đ Starting music fade from volume', fadeStartVolume);
}
/**
* Update music volume during fade
*/
function updateMusicFade() {
const elapsed = millis() - fadeStartTime;
const fadeProgress = elapsed / MUSIC_FADE_TIME;
if (fadeProgress >= 1.0) {
// Fade complete
supermanMusic.stop();
console.log('đĩ Music fade complete');
returnToInitialState();
} else {
// Linear fade: volume decreases proportionally with time
const newVolume = fadeStartVolume * (1 - fadeProgress);
supermanMusic.setVolume(newVolume);
}
}
The fade uses a mathematical concept called linear interpolation:
newVolume = startVolume * (1 - progress)
// When progress = 0.0 (start): newVolume = startVolume * 1.0 = full volume
// When progress = 0.5 (middle): newVolume = startVolume * 0.5 = half volume
// When progress = 1.0 (end): newVolume = startVolume * 0.0 = silent
This creates a smooth, professional fade rather than an abrupt stop.
Function resets everything for a fresh start:
/**
* Return animation to initial state after end sequence
*/
function returnToInitialState() {
// Stop animation
isAnimating = false;
isPaused = false;
// Reset end sequence tracking
allCreditsFinished = false;
endSequenceStartTime = 0;
isFadingOut = false;
fadeStartVolume = 0;
fadeStartTime = 0;
// Reset sequential credit tracking
currentCreditIndex = 0;
creditDelayTimer = 0;
waitingForDelay = false;
// Reinitialize all objects
createStarfield();
initializeIntroPhrases();
initializeMovieTitle();
initializeCredits();
// Update button
const btn = document.getElementById('startPauseBtn');
btn.textContent = 'START';
console.log('đ Returned to initial state');
}
In v16c, the music volume faded to 0 but was never restored. When the user pressed START again, the music would play at 0 volume (silent)!
Add volume restoration in both auto-end and manual reset paths:
function returnToInitialState() {
// Stop animation
isAnimating = false;
isPaused = false;
// Reset end sequence tracking
allCreditsFinished = false;
endSequenceStartTime = 0;
isFadingOut = false;
fadeStartVolume = 0;
fadeStartTime = 0;
// **NEW: Reset music volume to original level (in case it was faded)**
if (supermanMusic && !isMuted) {
supermanMusic.setVolume(AUDIO_VOLUME);
}
// Reset sequential credit tracking
currentCreditIndex = 0;
creditDelayTimer = 0;
waitingForDelay = false;
// Reinitialize all objects
createStarfield();
initializeIntroPhrases();
initializeMovieTitle();
initializeCredits();
// Update button
const btn = document.getElementById('startPauseBtn');
btn.textContent = 'START';
console.log('đ Returned to initial state with music volume restored');
}
function resetAnimation() {
console.log('đ Resetting animation...');
// Stop animation
isAnimating = false;
isPaused = false;
// Stop and reset music
if (supermanMusic && supermanMusic.isPlaying()) {
supermanMusic.stop();
}
// **NEW: Reset music volume to original level (in case it was faded)**
if (supermanMusic && !isMuted) {
supermanMusic.setVolume(AUDIO_VOLUME);
}
// Reset timing
animationStartTime = 0;
totalPausedDuration = 0;
pausedTime = 0;
// Reset refresh tracking
refreshCountdown = 0;
lastActivatedCreditIndex = -1;
// Reset sequential credit tracking
currentCreditIndex = 0;
creditDelayTimer = 0;
waitingForDelay = false;
creditsStartTime = 0;
// Reset end sequence tracking
allCreditsFinished = false;
endSequenceStartTime = 0;
isFadingOut = false;
fadeStartVolume = 0;
fadeStartTime = 0;
// Recreate all objects
createStarfield();
initializeIntroPhrases();
initializeMovieTitle();
initializeCredits();
// Update button
const btn = document.getElementById('startPauseBtn');
btn.textContent = 'START';
}
Notice the condition if (supermanMusic && !isMuted). This is important:
This respects the user's preference - if they muted the sound, it stays muted!
| Component | v15 (Baseline) | v16c (Auto-End) | v16d (Volume Reset) |
|---|---|---|---|
| Constants | Audio volume only | +DELAY_TILL_END, +MUSIC_FADE_TIME | Same as v16c |
| State Variables | Animation/pause tracking | +5 end sequence variables | Same as v16c |
| draw() Loop | Stars, title, credits | +checkEndSequence() call | Same as v16c |
| New Functions | - | +checkEndSequence() +startMusicFadeOut() +updateMusicFade() +returnToInitialState() |
Same as v16c |
| returnToInitialState() | - | Reset state, no volume restore | +Volume restoration |
| resetAnimation() | Basic reset | Same as v15 | +Volume restoration |
| Lines of Code | ~1752 | ~1700 | ~1750 |
âââââââââââââââââââââââââââââââââââââââââââ
â Animation plays normally â
â - Intro phrases â
â - Movie title â
â - Student credits (sequential) â
â - Technical credits (sequential) â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â checkEndSequence() monitors: â
â currentCreditIndex >= credits.length â
â && all credits inactive â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Last credit finishes â
â allCreditsFinished = true â
â endSequenceStartTime = millis() â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Wait DELAY_TILL_END (2000ms) â
â Viewer can read final credit â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â startMusicFadeOut() â
â isFadingOut = true â
â fadeStartVolume = current volume â
â fadeStartTime = millis() â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â updateMusicFade() every frame â
â newVolume = fadeStartVolume * â
â (1 - elapsed/FADE_TIME) â
â Linear fade over MUSIC_FADE_TIME â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Fade complete (progress >= 1.0) â
â supermanMusic.stop() â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â returnToInitialState() â
â - Reset all state variables â
â - Restore music volume to 0.7 â
â - Reinitialize objects â
â - Change button to "START" â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Ready for replay! â
â User can press START again â
â Music plays at full volume â
âââââââââââââââââââââââââââââââââââââââââââ
| Feature | v15 | v16a | v16b | v16c | v16d |
|---|---|---|---|---|---|
| Content Customization | â | â | â | â | â |
| Visual Effects Working | â | â Broken | â No improvement | â | â |
| Auto-End Detection | â | Attempted | Attempted | â | â |
| Music Fade | â | Attempted | Attempted | â | â |
| Volume Restoration | N/A | N/A | N/A | â | â |
| Ready for Replay | â (manual) | â | â | â ī¸ (silent music) | â |
| User Feedback | Working baseline | "Still broken" | "No improvement" | "What I wanted" | Complete |
Version 15 was our "known-good" state. When v16a and v16b failed, returning to v15 was the right decision because:
Professional Practice: This is why version control (like Git) is essential - you can always return to a previous working state.
The progression from v16c to v16d shows good incremental development:
Each version adds ONE new capability, making it easy to test and verify.
Notice how user feedback guided the development:
Don't be afraid to pivot based on feedback. Failed experiments teach you what NOT to do!
v16d manages multiple states that must all be reset properly:
Forgetting to reset even one variable (like volume) breaks the replay feature. Good state management requires tracking ALL state and resetting it properly.
The music fade uses a fundamental animation technique:
// Generic lerp formula:
value = start + (end - start) * progress
// Our volume fade (start=0.7, end=0.0):
newVolume = fadeStartVolume * (1 - fadeProgress)
// Equivalent to: fadeStartVolume + (0 - fadeStartVolume) * fadeProgress
This same technique is used in:
Master this concept and you can create smooth transitions for anything!
Version 16d delivers a complete, professional movie credits simulator: