Visual Customizer, localStorage Integration & Sequential Credits
Stage 14 represents a significant architectural shift - building a separate tool that generates configuration for the main application. This introduces students to:
Goal: Build visual design tool with live preview and localStorage
Status: â Created Successfully
let config = {
previewText: "JavaScript",
fontFamily: "Bebas Neue",
initialSize: 160,
minSize: 20,
maxTilt: 30,
strokeEnabled: true,
strokeWidth: 2,
strokeColor: "white",
fillEnabled: true,
blurTrigger: 60,
blurStrength: 10,
blurSpread: 8,
shrinkRate: 0.6,
fadeSpeed: 0.4,
tiltDecay: 0.98,
creditDelay: 5000
};
Problem: Page failed W3C validation due to:
background-clip property (only had -webkit-background-clip)<h3> to <h5>background-clip: text; alongside webkit version<h5> to <h4> with adjusted font-sizeProblem: Dropdown options had white text on white background
.control-group select option {
background-color: #2a2a2a;
color: white;
}
Goal: Make the Credit Delay slider actually work
Status: â Fixed Successfully
Problem: Text reset immediately without waiting for delay period
Root Cause: No state tracking for delay timing
Added two new variables to track waiting state:
let isWaiting = false;
let waitStartTime = 0;
Modified draw loop to check delay:
if (isWaiting) {
let elapsedWait = millis() - waitStartTime;
if (elapsedWait >= config.creditDelay) {
isWaiting = false;
resetTextObject();
}
}
Updated text deactivation to start waiting:
if (textObj.size < config.minSize || textObj.opacity <= 0) {
textObj.active = false;
isWaiting = true;
waitStartTime = millis();
}
User changed slider HTML to 1500 but behavior didn't change
Root Cause: Config object still had 5000
Updated config object and resetToDefaults() function to match:
creditDelay: 1500 // All three locations
Goal: Make stroke fade with fill for unified animation
Status: â Implemented
Modified getStrokeColor() to accept opacity parameter:
function getStrokeColor(opacity) {
switch(config.strokeColor) {
case "white": return color(0, 0, 100, opacity);
case "black": return color(0, 0, 0, opacity);
case "gray": return color(0, 0, 50, opacity);
case "gold": return color(45, 70, 85, opacity);
case "silver": return color(0, 0, 75, opacity);
default: return color(0, 0, 100, opacity);
}
}
Updated both drawing functions to pass opacity:
// Normal text
stroke(getStrokeColor(textObj.opacity));
// Blurred layers
stroke(getStrokeColor(layerOpacity));
Result: Stroke now fades harmoniously with fill, creating cohesive animation effect.
Goal: Load saved config from customizer into simulator
Status: â Fully Integrated
1. Load Config at Startup:
let savedConfig = null;
try {
const saved = localStorage.getItem('movieCreditsConfig');
if (saved) savedConfig = JSON.parse(saved);
} catch (error) {
console.warn('No saved config, using defaults');
}
2. Convert Constants to Variables:
Changed 11 constants to let variables with fallback defaults:
let CREDITS_FONT = savedConfig?.fontFamily || "Righteous";
let TEXT_STROKE_ENABLED = savedConfig?.strokeEnabled ?? true;
let TEXT_STROKE_WIDTH = savedConfig?.strokeWidth ?? 2;
let TEXT_FILL_ENABLED = savedConfig?.fillEnabled ?? true;
let BLUR_TRIGGER_THRESHOLD = savedConfig?.blurTrigger ?? 60;
let BLUR_STRENGTH = savedConfig?.blurStrength ?? 10;
let BLUR_SPREAD = savedConfig?.blurSpread ?? 8;
let INITIAL_FONT_SIZE = savedConfig?.initialSize ?? 160;
let CREDIT_DELAY_MS = savedConfig?.creditDelay ?? 5000;
let MAX_TILT = savedConfig?.maxTilt ?? 30;
let TILT_DECAY = savedConfig?.tiltDecay ?? 0.98;
let CREDIT_SHRINK_RATE = savedConfig?.shrinkRate ?? 0.6;
let CREDIT_MIN_SIZE = savedConfig?.minSize ?? 20;
let CREDIT_FADE_SPEED = savedConfig?.fadeSpeed ?? 0.4;
The ?? (nullish coalescing) operator is perfect for default values:
value ?? default uses default only if value is null or undefined||, it doesn't treat 0 or false as "missing"0 is a valid value3. Color Mapping:
Convert customizer color names to HSB values:
if (savedConfig?.strokeColor) {
switch(savedConfig.strokeColor) {
case "white":
TEXT_STROKE_HUE = 0;
TEXT_STROKE_SATURATION = 0;
TEXT_STROKE_BRIGHTNESS = 100;
break;
case "black":
TEXT_STROKE_HUE = 0;
TEXT_STROKE_SATURATION = 0;
TEXT_STROKE_BRIGHTNESS = 0;
break;
// ... other colors
}
}
4. Console Feedback:
if (savedConfig) {
console.log('â
Loaded custom configuration from localStorage');
console.log('Settings:', savedConfig);
} else {
console.log('âšī¸ No saved config found, using defaults');
}
Added link to customizer in navigation bar:
<li class="nav-item">
<a class="nav-link" href="customizeText3.html">âī¸ Customize</a>
</li>
Status: â Resolved
Credits were deactivating based only on size threshold, not opacity:
// OLD CODE (BROKEN):
if (credit.size > CREDIT_MIN_SIZE) {
credit.size -= CREDIT_SHRINK_RATE;
credit.size = max(credit.size, CREDIT_MIN_SIZE); // CLAMPED
}
// ...later...
if (credit.opacity <= 0) { // ONLY CHECK
credit.active = false;
}
Problem: Credits stopped shrinking at MIN_SIZE but continued fading slowly. They remained visible while fading â stacking effect.
// NEW CODE (FIXED):
credit.size -= CREDIT_SHRINK_RATE; // Always shrink
// ... fade and move ...
if (credit.size < CREDIT_MIN_SIZE || credit.opacity <= 0) {
credit.active = false;
credit.opacity = 0; // Ensure transparent
}
Why This Works: Credits deactivate when EITHER too small OR fully faded, not just one condition.
Status: â Resolved
Original activation logic:
if (!credit.active && timeSinceStart > credit.startTime) {
credit.active = true;
}
The Problem: Once a credit finished and became inactive, this condition was STILL TRUE (time only moves forward), causing immediate reactivation â blinking!
Added flag to each credit object:
credits.push({
name: studentNames[i],
// ... other properties ...
active: false,
hasPlayed: false // NEW FLAG
});
Updated activation logic:
if (!credit.active && !credit.hasPlayed && timeSinceStart > credit.startTime) {
credit.active = true;
credit.hasPlayed = true; // Mark as played
// Reset properties
credit.size = INITIAL_FONT_SIZE;
credit.opacity = 100;
credit.y = height / 2 - 30;
}
Result: Credits only activate once, preventing reactivation loop.
Status: â Completely Redesigned
Original System: Time-based activation - each credit had a fixed startTime
// OLD SYSTEM:
let startTime = introSequenceTime + (i * CREDIT_DELAY_MS);
credits.push({
name: studentNames[i],
startTime: startTime, // Fixed time
// ...
});
Problem: Credits started based on elapsed time, not completion of previous credit. If timing values changed (via customizer), credits would overlap!
Example Scenario:
New Architecture: Completion-based activation
1. New Tracking Variables:
let currentCreditIndex = 0; // Which credit to show next
let creditDelayTimer = 0; // When delay started
let waitingForDelay = false; // In delay period?
let creditsStartTime = 0; // When credits begin
2. Removed startTime from Credits:
credits.push({
name: studentNames[i],
// NO startTime property
active: false,
hasPlayed: false
// ...
});
3. New Activation Function:
function activateCredit(index) {
if (index >= 0 && index < credits.length && !credits[index].hasPlayed) {
let credit = credits[index];
credit.active = true;
credit.hasPlayed = true;
// Reset to fresh state
credit.size = INITIAL_FONT_SIZE;
credit.opacity = 100;
credit.y = height / 2 - 30;
credit.tiltAngle = random(-MAX_TILT, MAX_TILT);
console.log(`đŦ Activating credit ${index}: ${credit.name}`);
}
}
4. Sequential Check Function:
function checkNextCredit() {
let timeSinceStart = millis() - animationStartTime - totalPausedDuration;
// Wait for intro sequence to complete
if (currentCreditIndex === 0 && timeSinceStart < creditsStartTime) {
return;
}
// All credits shown?
if (currentCreditIndex >= credits.length) {
return;
}
// If waiting for delay after previous credit
if (waitingForDelay) {
let delayElapsed = millis() - creditDelayTimer;
if (delayElapsed >= CREDIT_DELAY_MS) {
waitingForDelay = false;
activateCredit(currentCreditIndex);
currentCreditIndex++;
}
} else {
// No active credits and not waiting? Show next one
let anyActive = credits.some(c => c.active);
if (!anyActive) {
activateCredit(currentCreditIndex);
currentCreditIndex++;
}
}
}
5. Updated Credit Deactivation:
if (credit.size < CREDIT_MIN_SIZE || credit.opacity <= 0) {
credit.active = false;
credit.opacity = 0;
// Start delay timer before next credit
if (!waitingForDelay) {
waitingForDelay = true;
creditDelayTimer = millis();
}
}
6. Added to Draw Loop:
// *** STAGE 14b: Check if next credit should activate ***
checkNextCredit();
// Draw and update each credit
for (let i = 0; i < credits.length; i++) {
updateCredit(credits[i]);
displayCredit(credits[i]);
}
This redesign demonstrates a fundamental computer science concept:
State Machine Components:
Key Advantages:
Real-World Applications:
Saving (customizeText3.html):
function saveSettings() {
try {
localStorage.setItem('movieCreditsConfig', JSON.stringify(config));
showStatus('Settings saved successfully! â
', 'success');
console.log('đž Settings saved to localStorage:', config);
} catch (error) {
showStatus('Error saving settings: ' + error.message, 'error');
console.error('â Error saving settings:', error);
}
}
Loading (movieCreditsSim14.html):
let savedConfig = null;
try {
const saved = localStorage.getItem('movieCreditsConfig');
if (saved) savedConfig = JSON.parse(saved);
} catch (error) {
console.warn('Could not load config:', error);
}
// Use with fallbacks
let CREDITS_FONT = savedConfig?.fontFamily || "Righteous";
âââââââââââââââââââââââââââââââââââââââââââ
â Intro Sequence Completes â
â creditsStartTime reached â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â checkNextCredit() called each frame â
â currentCreditIndex = 0 â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â activateCredit(0) â
â Credit 0 becomes active â
â currentCreditIndex++ â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â updateCredit() shrinks/fades credit â
â displayCredit() shows it on screen â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Credit finishes (size or opacity = 0) â
â credit.active = false â
â waitingForDelay = true â
â creditDelayTimer = millis() â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Wait CREDIT_DELAY_MS â
â (1500ms default, customizable) â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
âââââââââââââââââââââââââââââââââââââââââââ
â Delay complete â
â waitingForDelay = false â
â activateCredit(1) â
â currentCreditIndex++ â
ââââââââââââââââŦâââââââââââââââââââââââââââ
â
âŧ
(repeat for all credits)
| Bug | Root Cause | Solution |
|---|---|---|
| HTML Validation | Missing standard properties, heading skip | Added background-clip, fixed h4 |
| Dropdown Unreadable | White text on white background | Dark option backgrounds |
| Credit Delay Not Working | No delay state tracking | Added isWaiting/waitStartTime |
| Default Value Mismatch | Config had different value than HTML | Synchronized all to 1500 |
| Stroke Always 100% | getStrokeColor() didn't accept opacity | Added opacity parameter |
| Credits Stacking | Size clamped at MIN, only opacity check | Dual-condition deactivation |
| Credits Blinking | Reactivation loop (time always passes) | hasPlayed flag |
| Credits Overlapping | Fixed-time activation doesn't adapt | Sequential state machine |
Software Architecture:
Data Persistence:
State Management:
Debugging Process:
Modern JavaScript:
savedConfig?.property)?? operator).some() for checking)Version 14 delivers a complete, customizable movie credits system: