📝 Stage 14 Development Log

Visual Customizer, localStorage Integration & Sequential Credits

đŸŽ¯ Stage 14 Objectives

User Request: "Let's create a customizeText1.html file where we feature the word 'JavaScript' in the canvas and have a panel adjacent to (right side) the canvas with controls that let us choose the max and min size, the font, the border thickness, whether or not to fill the inside of the text, the max angle of rotation, all the variables we've mentioned in the simulation. Once we've got a design, we should have the option to save the settings to localstorage and then load those values in the simulation for use in the movie credits. We'll do that in version 14 of the simulation."

🎓 Educational Context

Stage 14 represents a significant architectural shift - building a separate tool that generates configuration for the main application. This introduces students to:

🔄 The Development Journey

customizeText1Initial Customizer Creation

Goal: Build visual design tool with live preview and localStorage

Status: ✅ Created Successfully

Key Features Implemented:

  • Live Preview Canvas: 700x500px with animated "JavaScript" text
  • 16 Control Parameters: Font, sizes, rotation, stroke, fill, blur, timing
  • Starfield Background: Matches simulator aesthetic
  • Save/Load/Reset Buttons: localStorage persistence
  • Real-time Updates: Changes reflect immediately on preview

Configuration Object Structure:

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
};

🐛 Issue #1: HTML Validation Errors

Problem: Page failed W3C validation due to:

  • Missing standard background-clip property (only had -webkit-background-clip)
  • Heading hierarchy jump from <h3> to <h5>

✅ Fix Applied

  • Added background-clip: text; alongside webkit version
  • Changed <h5> to <h4> with adjusted font-size

🐛 Issue #2: UI Readability

Problem: Dropdown options had white text on white background

✅ Fix Applied

.control-group select option {
    background-color: #2a2a2a;
    color: white;
}

customizeText2Credit Delay Functionality Fix

Goal: Make the Credit Delay slider actually work

Status: ✅ Fixed Successfully

🐛 Issue: Credit Delay Not Working

Problem: Text reset immediately without waiting for delay period

Root Cause: No state tracking for delay timing

✅ Solution: Add Delay State Machine

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();
}

🐛 Issue: Default Value Mismatch

User changed slider HTML to 1500 but behavior didn't change

Root Cause: Config object still had 5000

✅ Fix: Synchronize All Defaults

Updated config object and resetToDefaults() function to match:

creditDelay: 1500  // All three locations

customizeText3Stroke Opacity Coordination

Goal: Make stroke fade with fill for unified animation

Status: ✅ Implemented

User: "In version 3, let's try an adjustment where the stroke color opacity changes in concert with the fill color opacity. It seems that it's always at 100%. I'm wondering how it would look otherwise. Let's try it!"

✅ Implementation

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.

movieCreditsSim14localStorage Integration

Goal: Load saved config from customizer into simulator

Status: ✅ Fully Integrated

Implementation Strategy:

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;

🎓 Modern JavaScript Pattern

The ?? (nullish coalescing) operator is perfect for default values:

  • value ?? default uses default only if value is null or undefined
  • Unlike ||, it doesn't treat 0 or false as "missing"
  • Essential for numeric configs where 0 is a valid value

3. 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');
}

Navigation Integration:

Added link to customizer in navigation bar:

<li class="nav-item">
    <a class="nav-link" href="customizeText3.html">âš™ī¸ Customize</a>
</li>

Bug Fix #1Credits Stacking Issue

Status: ✅ Resolved

User: "This works well until the student credits and other 'actor' names appear. Each one never really disappears: they stack on top of each other."

🐛 Root Cause Analysis

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.

✅ Solution: Dual-Condition Deactivation

// 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.

Bug Fix #2Credits Blinking and Re-stacking

Status: ✅ Resolved

User: "Now the credits after the titles just blink and stack."

🐛 Root Cause: Reactivation Loop

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!

✅ Solution: hasPlayed Flag

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.

Bug Fix #3Credits Overlapping (Sequential Fix)

Status: ✅ Completely Redesigned

User: "There is still a problem. The list of students and tech features load on top of each other and don't wait for one to clear before starting another. By the end it's a flashing mess."

🐛 Fundamental Design Flaw

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:

  • Credit 1 starts at t=12000ms
  • Credit 2 starts at t=13500ms (1500ms later)
  • But if fadeSpeed is slow, Credit 1 is still visible at t=13500ms
  • Result: Both credits visible simultaneously → stacking!

✅ Complete System Redesign: Sequential State Machine

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]);
}

🎓 Sequential State Machine Pattern

This redesign demonstrates a fundamental computer science concept:

State Machine Components:

  • States: waiting, displaying, delaying
  • Transitions: Triggered by completion events, not fixed time
  • Index Tracking: Know which item is "current"
  • Guards: Prevent invalid transitions (check boundaries)

Key Advantages:

  • Works with ANY timing values (from customizer)
  • Guarantees one-at-a-time display
  • Self-pacing (faster credits finish faster, slower take longer)
  • Easy to pause/resume (just stop checking)

Real-World Applications:

  • Video game cutscenes
  • Slideshow presentations
  • Multi-step form wizards
  • Loading sequences
  • Tutorial systems

đŸ› ī¸ Technical Implementation Highlights

localStorage Save/Load Pattern

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";

Sequential Activation Flow

┌─────────────────────────────────────────┐
│  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 Discovery & Resolution Summary

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

🎓 Educational Value

What Students Learn from Stage 14

Software Architecture:

Data Persistence:

State Management:

Debugging Process:

Modern JavaScript:

đŸŽŦ Final Result

Version 14 delivers a complete, customizable movie credits system:

Summary: Stage 14 represents a major milestone - transforming the simulator from a fixed animation into a customizable platform. Students can now design their own effects, save them, and see them in action. The sequential credit system ensures smooth, professional-looking results regardless of timing values. This stage teaches tool creation, data persistence, state machines, and the importance of robust deactivation logic in animation systems.

🔗 Related Documentation

đŸŽŦ Launch Simulator v14