Metamerism Demo - Control Flow Diagram v2.0

Mode 1: Normal Operation (Lock Perception OFF)

┌─────────────────────────────────────────────────────────────────────────────┐
│                           USER INPUT LAYER                                   │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
        ┌─────────────────────────────┼─────────────────────────────┐
        ▼                             ▼                             ▼
┌──────────────────┐          ┌──────────────────┐         ┌──────────────────┐
│  LAMP BUTTONS    │          │   CCT SLIDER     │         │   RGB SLIDERS    │
│  (8 presets)     │          │   (1800-6500K)   │         │   (0-255 each)   │
│  Inc/Hal/Flu/    │          │                  │         │   R / G / B      │
│  LED/Day/Grow/   │          │  genSpecFromCCT() │         │  rgbToSpec()     │
│  Plant/User      │          │  (Planck's Law)  │         │  (Gaussian mix)  │
└──────────────────┘          └──────────────────┘         └──────────────────┘
        │                             │                             │
        │                             │                             │
        └─────────────────────────────┼─────────────────────────────┘
                                      │
                                      ▼
                    ┌──────────────────────────────────────┐
                    │   genSpec(lampID)              │
                    │   • Standard: Blackbody radiation    │
                    │   • Grow: Blue(420-480) + Red(630)   │
                    │   • Plant: Chlorophyll peaks         │
                    │   • User: rgbToSpec() gaussian       │
                    └──────────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                    SPECTRAL POWER DISTRIBUTION (SPD)                         │
│                         st.spec (81 values)                                  │
│                      380nm → 780nm @ 5nm steps                               │
│                    ═══ SOURCE OF TRUTH ═══                                    │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
            ┌─────────────────────────┼─────────────────────────┐
            │                         │                         │
            ▼                         ▼                         ▼
    ┌──────────────┐          ┌──────────────┐        ┌──────────────┐
    │  toRGB()     │          │  cone()      │        │  drawSPD()   │
    │  CIE XYZ     │          │  L/M/S       │        │  Bar Chart   │
    │  → sRGB      │          │  Cone        │        │  + Y-axis    │
    │  γ = 2.4     │          │  Response    │        │  Scale       │
    └──────────────┘          └──────────────┘        └──────────────┘
            │                         │                         │
            ▼                         ▼                         ▼
    ┌──────────────┐          ┌──────────────┐        ┌──────────────┐
    │ RGB (0-255)³ │          │ drawCone()  │        │  SPD Canvas  │
    │              │          │ Cone-shaped │        │  Display     │
    └──────────────┘          │ Bars with:  │        └──────────────┘
            │                 │ • 36px L/M/S│
            │                 │ • 18px Long/│
            ├─────────────────┤   Med/Short │
            │                 │ • 28px %    │
            ▼                 └──────────────┘
    ┌──────────────┐                  │
    │ rgbToCCT()  │                  │
    │ Estimate     │                  ▼
    │ Temperature  │          ┌──────────────┐
    └──────────────┘          │ L/M/S Stats  │
            │                 │ Box Values   │
            ▼                 └──────────────┘
    ┌──────────────┐
    │ CCT Slider   │
    │ Update       │
    │ (if from RGB)│
    └──────────────┘
            │
            ▼
    ┌──────────────┐          ┌──────────────┐
    │ RGB Sliders  │          │ User Color   │
    │ Update       │          │ Picker       │
    │ (if from CCT)│          └──────────────┘
    └──────────────┘
            │
            └─────────────────────┬─────────────────────┐
                                  ▼                     ▼
                          ┌──────────────┐      ┌──────────────┐
                          │applyToWindows│      │ House Paint  │
                          │  RGB → img   │      │   Picker     │
                          └──────────────┘      └──────────────┘
                                  │                     │
                                  └──────────┬──────────┘
                                             ▼
                                     ┌──────────────┐
                                     │processImage()│
                                     │ brightness   │
                                     │   < 50 ?     │
                                     └──────────────┘
                                             │
                                ┌────────────┴────────────┐
                                ▼                         ▼
                        ┌──────────────┐          ┌──────────────┐
                        │ YES: Window  │          │ NO: House    │
                        │ Apply RGB    │          │ Apply Paint  │
                        │ from spectrum│          │ from picker  │
                        └──────────────┘          └──────────────┘
                                │                         │
                                └──────────┬──────────────┘
                                           ▼
                                   ┌──────────────┐
                                   │ Main Image   │
                                   │  Display     │
                                   │ (w01-w11.png)│
                                   └──────────────┘
            

Mode 2: Metamerism Demo (Lock Perception ON)

┌─────────────────────────────────────────────────────────────────────────────┐
│                      APPLY SHIFT BUTTON PRESSED                               │
│                    Lock Perception: ☑ CHECKED                                 │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
                          ┌──────────────────────┐
                          │ Capture Current State│
                          │ oldSpec = st.spec    │
                          │ targetLMS = cone(oldSpec) │
                          │ oldRGB = [r, g, b]   │
                          │ oldCCT = current CCT │
                          └──────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                    CONES ARE NOW THE BOSS!                                   │
│                    targetLMS = [L%, M%, S%]                                  │
│                    These values are LOCKED                                        │
└─────────────────────────────────────────────────────────────────────────────┘
                                      │
                                      ▼
                    ┌──────────────────────────────────────┐
                    │  findMetamericMatch(targetLMS)      │
                    │  Search for different SPD with       │
                    │  SAME cone response                  │
                    │                                      │
                    │  Algorithm (100 iterations):         │
                    │  1. Generate random blackbody        │
                    │     curves (2000-3000K range)        │
                    │  2. Add random spectral peaks        │
                    │  3. Normalize each candidate         │
                    │  4. Calculate cone(candidate)        │
                    │  5. Score = |L-targetL| +            │
                    │             |M-targetM| +            │
                    │             |S-targetS|              │
                    │  6. Return best match (min score)    │
                    └──────────────────────────────────────┘
                                      │
                                      ▼
                          ┌──────────────────────┐
                          │ newSpec = metameric  │
                          │ spectrum found       │
                          │ newRGB = toRGB(new)  │
                          │ newCCT = oldCCT      │
                          │ (CCT stays constant) │
                          └──────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                    ANIMATION LOOP (2 seconds)                                │
│                    Cubic ease-in-out function                                │
└─────────────────────────────────────────────────────────────────────────────┘
            │
            ├─────────────────────────────────────────────────────────┐
            │                                                         │
            ▼                                                         ▼
    ┌──────────────┐                                         ┌──────────────┐
    │  SPD ANIMATES │                                         │ CONES FROZEN │
    │              │                                         │              │
    │ currentSpec  │                                         │ Draw using   │
    │ = oldSpec +  │                                         │ targetLMS    │
    │ (newSpec -   │                                         │ (ORIGINAL    │
    │  oldSpec)    │                                         │  VALUES)     │
    │  * ease      │                                         │              │
    │              │                                         │ L/M/S bars   │
    │ drawSPD()    │                                         │ do NOT move! │
    │ shows morph  │                                         └──────────────┘
    └──────────────┘                                                 │
            │                                                        │
            ▼                                                        │
    ┌──────────────┐                                                │
    │ RGB ANIMATES │                                                │
    │              │                                                │
    │ currentRGB   │                                                │
    │ = oldRGB +   │                                                │
    │ (newRGB -    │                                                │
    │  oldRGB)     │                                                │
    │  * ease      │                                                │
    │              │                                                │
    │ Sliders move │◄───────────────────────────────────────────────┤
    │ Values update│                                                │
    └──────────────┘                                                │
            │                                                        │
            ▼                                                        │
    ┌──────────────┐                                                │
    │ DELTA DISPLAY│                                                │
    │              │                                                │
    │ deltaR =     │                                                │
    │ currentRGB[0]│                                                │
    │ - oldRGB[0]  │                                                │
    │              │                                                │
    │ Shows as:    │                                                │
    │ "+12,-5,+8"  │                                                │
    │              │                                                │
    │ Persists until│                                                │
    │ user touches  │                                                │
    │ any control   │                                                │
    └──────────────┘                                                │
            │                                                        │
            ▼                                                        │
    ┌──────────────┐                                                │
    │ CCT STAYS    │                                                │
    │ CONSTANT     │                                                │
    │ (perception  │                                                │
    │  is locked)  │                                                │
    └──────────────┘                                                │
            │                                                        │
            └────────────────────────┬───────────────────────────────┘
                                     │
                                     ▼
                          ┌──────────────────────┐
                          │   Window Colors      │
                          │   Update with        │
                          │   currentRGB values  │
                          │   during animation   │
                          └──────────────────────┘
                                     │
                                     ▼
                          ┌──────────────────────┐
                          │ METAMERISM PROVEN! │
                          │                      │
                          │ Same perception:     │
                          │ • Cones locked       │
                          │ • L/M/S unchanged    │
                          │                      │
                          │ Different stimulus:  │
                          │ • SPD changed        │
                          │ • RGB changed        │
                          │ • Spectrum shape ≠   │
                          └──────────────────────┘
            

Bidirectional Control Flow

    CCT Slider  ←─────────────────────→  RGB Sliders
         │                                      │
         │ st.updatingFromRGB flag              │ st.updatingFromCCT flag
         │ prevents circular updates            │ prevents circular updates
         │                                      │
         ▼                                      ▼
    genSpecFromCCT()                      rgbToSpec()
    (Planck's Law)                        (Gaussian peaks)
         │                                      │
         │                                      │
         └─────────────┬────────────────────────┘
                       │
                       ▼
            ┌──────────────────────┐
            │   SPD (st.spec)      │
            │  81 values @ 5nm     │
            │  380-780nm range     │
            │                      │
            │  SOURCE OF TRUTH  │
            └──────────────────────┘
                       │
         ┌─────────────┼─────────────┐
         │             │             │
         ▼             ▼             ▼
    toRGB()       cone()        drawSPD()
    (CIE XYZ)     (L/M/S)       (Visualize)
         │             │             │
         ▼             ▼             ▼
    RGB Values    Cone Bars      SPD Chart
         │             │
         ├─────────────┤
         │             │
         ▼             ▼
    rgbToCCT()    L/M/S Stats
    (Estimate)    (Display)
         │
         └───────→ Update CCT slider
                  (if changed by RGB)
            

RGB to CCT Algorithm

rgbToCCT(r, g, b):

1. Calculate "blueness" ratio:
   blueness = (b/255) / (r/255 + 0.01)

2. Map blueness to CCT (monotonic):
   blueness < 0.7   → CCT = 1800K
   0.7  ≤ b < 0.85  → CCT = 1800-2800K (linear)
   0.85 ≤ b < 1.0   → CCT = 2800-4000K (linear)
   1.0  ≤ b < 1.2   → CCT = 4000-5000K (linear)
   1.2  ≤ b < 1.5   → CCT = 5000-6000K (linear)
   1.5  ≤ b         → CCT = 6000-6500K (linear)

3. Apply green modifier:
   greenMod = 1 - (|g/255 - 0.5| * 0.3)
   CCT = CCT * greenMod

4. Clamp to range:
   return clamp(CCT, 1800, 6500)

Key property: More blue ALWAYS increases CCT (monotonic)
                

State Variables

st = {
    spec:             Array[81]  - Current spectral power distribution
    targetLMS:        Array[3]   - Target L/M/S for metamerism matching
    selLamp:          String     - Selected lamp ID
    selWin:           Integer    - Selected window (0=main, 1-11)
    userMode:         Boolean    - User lamp mode active
    anim:             Boolean    - Animation in progress
    updatingFromCCT:  Boolean    - Prevent RGB→CCT circular update
    updatingFromRGB:  Boolean    - Prevent CCT→RGB circular update
    persistDelta:     Boolean    - Keep delta display after animation
    deltaR/G/B:       Integer    - RGB change values for display
    phot:            Object     - Photopic luminosity data
}
            

Key Features Summary

✓ Bidirectional Control

CCT ↔ RGB sliders update each other through SPD as source of truth

✓ True Metamerism

Lock Perception: Cones boss the search for different SPD with same perception

✓ Persistent Delta

RGB change values stick until user touches any control

✓ Enhanced Visuals

• Cone labels: 36px L/M/S + 18px descriptor + 28px percentage
• SPD Y-axis: Bold 20px scale (0.0-1.0)
• Green pulsing indicator on active lamp

✓ Monotonic CCT

More blue always increases CCT (no contradictory reversals)

Data Flow Priority

Highest Priority: SPD (Spectral Power Distribution)
    ↓
All outputs derive from SPD:
    • RGB values (via CIE XYZ conversion)
    • Cone response (via L/M/S sensitivity curves)
    • CCT estimation (via RGB ratio calculation)
    • Visual displays (SPD chart, cone bars)
    • Window colors (via RGB application)

Special Case - Lock Perception ON:
    Cone response becomes input constraint
    SPD search tries to match target L/M/S
    Demonstrates metamerism phenomenon