Learning Goals 5 min
- Combine the smooth-fade pattern (L02-04) with the RGB colour mixing (L02-05) to build a small "lamp" sketch that breathes through three calming colours forever.
- Apply a simple gamma curve so the breath looks linear to the eye, not linear to the duty cycle — fixing the "fast at the start, slow at the end" problem from L02-03.
- Polish a small Arduino project from start to finish — wire, code, test, tune, then leave it running.
Warm-Up 10 min
A "mood lamp" is a small piece of furniture. It sits on a desk, it does one thing, and it does that one thing beautifully — no buttons, no menu, no surprises. Today you build one.
Predict-the-output
Aisyah's sketch breathes the RGB LED, but holds each colour for the full breath instead of crossfading. What does it look like?
breath(R); // red in, red out
breath(G); // green in, green out
breath(B); // blue in, blue outReveal
Three distinct "coloured breaths" in sequence — a slow red swell, then a slow green swell, then a slow blue swell. Beautiful but a bit choppy at the transitions (red disappears, then green starts from zero). Today's project blends the transitions so the LED appears to morph through colours while breathing.
New Concept · The gamma curve in three lines of code 20 min
The problem we're solving
From L02-03: equal duty steps don't look equal. A linear ramp 0 → 255 spends too long looking bright, not enough time looking dim. The breath feels "snappy" at the bottom and "parked" at the top.
The fix — pre-computed brightness table
We'll pre-compute a 64-entry lookup table where each value is the duty cycle that matches a perceived brightness step. For γ = 2.2 the formula is:
duty[i] = round( pow(i / 63.0, 2.2) × 255 )The first few values come out very low (0, 0, 0, 1, 1, 2, 3, ...) and the last few come out high (240, 247, 255). Most of the entries sit in the middle — exactly where the eye has the most resolution. The result: a fade through this 64-entry table looks linear.
Instead of typing pow at runtime (which is slow on the AVR), we paste a hard-coded table into the sketch:
const byte GAMMA[64] = {
0, 0, 0, 1, 1, 2, 2, 3,
4, 5, 6, 7, 9, 10, 12, 14,
16, 18, 20, 23, 26, 28, 31, 35,
38, 42, 45, 49, 53, 58, 62, 67,
72, 77, 82, 87, 93, 99, 105, 111,
117, 124, 130, 137, 144, 152, 159, 167,
175, 183, 191, 199, 208, 217, 225, 235,
244, 253, 255, 255, 255, 255, 255, 255
};The breath loop now indexes the table instead of using v directly:
for (int i = 0; i < 64; i++) {
analogWrite(LED, GAMMA[i]);
delay(STEP_DELAY);
}64 steps instead of 256 — and because the duty values are spaced according to perception, the eye sees a beautifully smooth ramp.
Why const byte and not int?
Each entry fits in 0–255, which is exactly one byte. const byte GAMMA[64] uses 64 bytes of flash memory; using int would waste twice that. On a UNO with only 2 KB of RAM, the byte savings add up — but more importantly, const data lives in flash, not RAM, so this table costs zero RAM. Free!
Blending colours during the breath
For the lamp's "morph between colours" effect, we run two crossfades at once: the brightness fades up + down (the breath), while the colour also slowly shifts (the morph). The cleanest pattern is to step through a list of three target colours and crossfade between them, with the breath layered on top:
const int COLOURS[3][3] = {
{255, 80, 120}, // warm pink
{ 80, 180, 255}, // sky blue
{180, 255, 120} // soft lime
};Crossfade colour A → colour B over one full breath; while doing so, modulate the overall brightness with the gamma table. The two effects layer naturally because both feed into the same three analogWrite calls.
Worked Example · The full mood lamp 20 min
Step 1 — wiring
Reuse the L02-05 RGB wiring exactly. ~9 R, ~10 G, ~11 B, each through 220 Ω to its anode; cathode to GND. Nothing else changes.
Step 2 — the sketch
Save as mood-lamp.ino:
// L02-06: Breathing mood lamp — RGB + gamma-corrected breath
const int R = 9;
const int G = 10;
const int B = 11;
// 64-step gamma table (γ ≈ 2.2)
const byte GAMMA[64] = {
0, 0, 0, 1, 1, 2, 2, 3,
4, 5, 6, 7, 9, 10, 12, 14,
16, 18, 20, 23, 26, 28, 31, 35,
38, 42, 45, 49, 53, 58, 62, 67,
72, 77, 82, 87, 93, 99, 105, 111,
117, 124, 130, 137, 144, 152, 159, 167,
175, 183, 191, 199, 208, 217, 225, 235,
244, 253, 255, 255, 255, 255, 255, 255
};
// Three calming target colours
const int COLOURS[3][3] = {
{255, 80, 120}, // warm pink
{ 80, 180, 255}, // sky blue
{180, 255, 120} // soft lime
};
const int STEP_DELAY = 40; // 64 × 40 ms ≈ 2.5 s per ramp
void setColour(int r, int g, int b) {
analogWrite(R, r);
analogWrite(G, g);
analogWrite(B, b);
}
// One full breath in colour 'from', ending in colour 'to'.
void breathFromTo(int fromIdx, int toIdx) {
// inhale — brightness 0 → max, colour shifts halfway from → to
for (int i = 0; i < 64; i++) {
int bright = GAMMA[i];
// 0..63 of the way through the colour blend
int r = ((64 - i) * COLOURS[fromIdx][0] + i * COLOURS[toIdx][0]) / 64;
int g = ((64 - i) * COLOURS[fromIdx][1] + i * COLOURS[toIdx][1]) / 64;
int b = ((64 - i) * COLOURS[fromIdx][2] + i * COLOURS[toIdx][2]) / 64;
setColour(r * bright / 255, g * bright / 255, b * bright / 255);
delay(STEP_DELAY);
}
// exhale — brightness max → 0, colour finishes the blend to 'to'
for (int i = 63; i >= 0; i--) {
int bright = GAMMA[i];
setColour(COLOURS[toIdx][0] * bright / 255,
COLOURS[toIdx][1] * bright / 255,
COLOURS[toIdx][2] * bright / 255);
delay(STEP_DELAY);
}
}
void setup() {
pinMode(R, OUTPUT);
pinMode(G, OUTPUT);
pinMode(B, OUTPUT);
setColour(0, 0, 0);
}
void loop() {
breathFromTo(0, 1); // pink → blue
breathFromTo(1, 2); // blue → lime
breathFromTo(2, 0); // lime → pink
}Step 3 — upload and let it run
One full cycle = 3 breaths × ~5 s each ≈ 15 seconds. The LED appears to morph slowly through pink, blue and lime while gently breathing. Let it run for a minute. Notice how the breath feels continuous, not stepped — the gamma table is doing its work.
Step 4 — tune it
Three sliders you can tweak:
- STEP_DELAY — controls breath speed. 20 = active. 40 = relaxed. 80 = sleep-time.
- COLOURS — pick any three you like. Warm tones (red/orange/yellow) feel energising; cool tones (blue/cyan/teal) feel calming.
- Number of colours — drop to 2 (just pink → blue → pink) or expand to 5 for a longer cycle. Adjust the
loop()calls accordingly.
Try It Yourself 20 min
Goal: Run the worked example unchanged. While it's breathing, time one full breath cycle with your phone's stopwatch. Does it match the formula (64 × 40 × 2 ≈ 5.12 s per breath)?
Tip
Watch the LED dip to its dimmest, start the stopwatch, then count breaths. After 10 breaths divide. The Arduino is slightly slower than perfect — the analogWrite calls add a few microseconds each, so 10 breaths might come in at 52–55 s instead of exactly 51.
Goal: Replace the three colours with a different palette of your choice. Pick from a website (use the hex → RGB trick from L02-05 homework). Try a sunset palette: warm pink, deep orange, soft purple.
Goal: Replace the hand-pasted gamma table with one your sketch computes at boot time using pow(). Compare the result to the hard-coded one — does the lamp look the same?
Hint
byte GAMMA[64];
void computeGamma() {
for (int i = 0; i < 64; i++) {
float norm = i / 63.0;
GAMMA[i] = (byte)(pow(norm, 2.2) * 255 + 0.5);
}
}
void setup() {
computeGamma();
// ... rest of setup
}Note that GAMMA is no longer const — it lives in RAM now and uses 64 bytes. On an UNO that's 3% of total RAM. Worth it on a Nano 33 BLE (256 KB RAM); maybe not on a tiny ATtiny chip.
Mini-Challenge · Pick the moment 15 min
Choose one of the following lamp moods and build the colour palette + step delay for it. Justify your choices in a comment block at the top of the sketch.
- Study lamp — alert but not jarring. Should help concentration. Suggested colours: cool blues and whites. Breath ~6 s.
- Bedtime lamp — soothing, slow, warm. Should make you sleepy. Suggested colours: warm oranges, deep purples. Breath ~10 s.
- Hari Raya / festive lamp — celebratory, vivid, energetic. Suggested colours: ketupat green, festive yellow, deep red. Breath ~3 s.
- Aquarium lamp — like soft underwater light. Suggested colours: teal, sea-green, deep navy. Breath ~8 s.
It works if:
- The lamp's mood matches the brief — you can hold it up to a friend and they can guess which one you built.
- The breath speed feels appropriate (slow for calming, faster for festive).
- You added a comment block at the top explaining the colour and timing choices.
- Code is built on top of the worked example — you only changed
COLOURS,STEP_DELAYand (maybe) the number of colours in the cycle.
Recap 5 min
You built your first Level 2 project end-to-end: an LED that breathes, morphs through colours, and looks good doing it. The gamma table is the one piece of new craft — 64 pre-computed values that make a linear loop look linear to the eye, not to the duty cycle. The rest is L02-04's fade pattern × L02-05's RGB layered together. Tomorrow we switch to input — meeting the ADC for the first time.
- Gamma correction
- Pre-distorting brightness values to match the eye's non-linear sensitivity. γ ≈ 2.2 is the standard for screens and LEDs.
- Lookup table
- A pre-computed array of values you index into instead of recalculating on the fly. Trades a tiny bit of memory for big speed.
const byte ARRAY[64]- An immutable, fixed-size table that lives in flash memory (not RAM) on AVR chips like the UNO. Zero RAM cost.
- Crossfade between palettes
- Smooth transition between two colours using a counter:
r = ((N - i) × from_r + i × to_r) / Nfor each channel. - Project polish
- The difference between "sketch that works" and "thing you'd put on your desk". Comes from tuning constants, naming functions clearly, and timing the breath until it feels right.
Homework 5 min
Build the lamp for somebody. Pick a real person (parent, sibling, classmate) and design the palette + breath for their mood preference. Ask them what colours they like and how slow / fast they want it. Build it, run it for 5 minutes in front of them, take their feedback, iterate.
- Document who you built it for and three things they asked for (e.g. "greener, slower, no red").
- Build version 1. Show them.
- Note any change requests. Build version 2.
- If they say it's good, you've shipped a tiny product! Write a one-line "final spec" comment at the top of the sketch capturing what made it good.
Bring back next class:
- The sketch saved as
hw-l02-06-name.ino. - A short note: who, three requests, one thing that surprised you about their feedback.
- (Optional) A short phone video of the lamp running with them in shot.