┌─────────────────────────────────────────────────────────────────────────────┐
│ 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)│
└──────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐ │ 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 ≠ │ └──────────────────────┘
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)
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)
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
}
CCT ↔ RGB sliders update each other through SPD as source of truth
Lock Perception: Cones boss the search for different SPD with same perception
RGB change values stick until user touches any control
• 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
More blue always increases CCT (no contradictory reversals)
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