Один вечер я потратил на «закат» из пяти пикселей. Захотелось плавно перетечь от красного к фиолетовому, взял RGB и начал подбирать числа: 255,0,0 → 200,0,80 → 150,0,150 → 100,0,200. На бумаге выглядело логично, на ленте получились ступеньки и какая-то грязная серо-фиолетовая каша посередине. Три часа подборов, результат — «ну, вроде похоже». Потом я узнал про HSV, собрал тот же эффект за пять минут — и с тех пор цвет на лентах подбираю одной ручкой, а не тремя. Этот урок про то самое цветовое пространство, из-за которого с FastLED не хочется возвращаться к RGB.
План такой: разберём, почему RGB неудобен для анимаций, познакомимся с оттенком, насыщенностью и яркостью в терминах FastLED, научимся одной строкой fill_rainbow() получать радугу и заставим её плавно ползти по ленте. Как всегда, всё проверяется в симуляторе — радуга и «дыхание» будут крутиться прямо на этой странице.
Почему RGB неудобен для анимации
RGB описывает свет так, как его видит машина: сколько подать в каждый из трёх кристаллов. Человек же думает другими словами: «какой оттенок», «насколько чистый цвет» и «насколько ярко». Когда вы анимируете цвет в RGB, вам приходится переводить человеческую задачу в машинные числа вручную — и вот тут начинаются ступеньки.
Классический пример: путь от красного (255, 0, 0) к синему (0, 0, 255) по прямой проходит через (128, 0, 128). По числам это «посередине», по ощущению — грязный тёмный фиолетовый, а не красивый переход. Хуже того: чтобы просто затемнить всю ленту, нужно уменьшать три числа сразу и следить, чтобы ни одно не ушло за ноль. Чтобы сдвинуть оттенок — пересчитывать все три канала заново. Анимация в RGB превращается в ведение ручных таблиц. Ровно это и была моя пятичасовая ступенька из начала урока: я подбирал три канала одновременно, когда правильный ответ — одно число, оттенок.
HSV: оттенок, насыщенность, яркость
Цветовое пространство HSV устроено по-человечески. H — hue, оттенок — точка на цветовом круге. В FastLED круг разбит на 256 шагов: от 0 до 255, а не на привычные графическим редакторам 360 градусов — потому что оттенок хранится в одном байте. S — saturation, насыщенность: 0 — бесцветная серая заливка, 255 — максимально чистый цвет. V — value, яркость: 0 — чёрный, 255 — вся мощь выбранного оттенка.
Круг запоминается круглыми числами: каждые 32 шага — следующий цвет. Красный 0, оранжевый 32, жёлтый 64, зелёный 96, аква 128, синий 160, пурпурный 192, розовый 224. И замыкание круга: оттенок 255 почти совпадает с оттенком 0 — после розового круг приходит к красному. Именно поэтому переливы оттенка в HSV идут без швов: у круга просто нет конца.
| Оттенок (hue) | Цвет |
|---|---|
| 0 | Красный |
| 32 | Оранжевый |
| 64 | Жёлтый |
| 96 | Зелёный |
| 128 | Аква (циан) |
| 160 | Синий |
| 192 | Пурпурный |
| 224 | Розовый |
Смотрите, что произошло: три «разных» цвета собраны из одного оттенка. В RGB мне пришлось бы держать в голове три числа на каждый вариант; в HSV крутится одна ручка за раз. CHSV возвращает цвет, который присваивается пикселю точно так же, как CRGB, — FastLED сам пересчитает оттенок, насыщенность и яркость в три канала на этапе отрисовки, вам об этом думать не нужно.
Важно не перепутать яркость v с FastLED.setBrightness(). v — свойство конкретного пикселя, им анимируют «дыхание» и затухание. setBrightness — глобальный множитель на всю ленту, его ставят один раз в setup(), чтобы не слепить глаза и не жарить блок питания. Моё правило простое: если приглушение одно для всех и навсегда — setBrightness; если пиксель по сценарию гаснет и разгорается — это v, и только он. Смешивать оба можно, но тогда заранее прикиньте итоговую яркость: перемножение легко съест половину диапазона.
fill_rainbow: радуга одной строкой
Радуга в FastLED — одна функция: fill_rainbow(leds, 16, startHue, deltaHue). Она идёт по буферу и каждому следующему пикселю выдаёт оттенок на deltaHue больше, чем у предыдущего. Стартовый оттенок задаёте вы, а дельта определяет, сколько цветового круга поместится на ленту: дельта 16 при 16 пикселях — ровно один полный круг; дельта 8 — половина круга, растянутая на всю ленту; дельта 64 — радуга в четыре раза плотнее, цвета пойдут с повторами. Ориентир простой: дельта, умноженная на число пикселей, равная 256, — полный круг без нахлёста.
А теперь главный трюк. Пока startHue неизменен, радуга застыла. Добавьте в loop() строку hue = hue + 1; — и круг поедет: каждый кадр вся радуга сдвигается на один оттенок, плавно и без единого RGB-числа в коде. В симуляторе ниже шаг происходит каждые 40 миллисекунд — это примерно 25 кадров в секунду, глаз видит непрерывное течение, а не мигание: мерцание перестаёт различаться у большинства людей где-то от 50 Гц, так что запас у нас большой.
Дыхание и индикаторы: v в движении
Оттенок — не единственная живая ручка. Если в цикле менять v, лента «дышит»: разгорается и гаснет, как экран ноутбука в тёмной комнате. В симуляторе ниже яркость ходит туда-обратно между 20 и 250: направление хранится в переменной dir, а на границах переключается. Тот же принцип, что и у оттенка: одна переменная, одна ручка — и анимация готова. Граница 25 выбрана не случайно: если дать v упасть до нуля, лента на мгновение полностью слепнет, и «дыхание» превращается в мигание.
Отдельно скажу про индикаторы — тут HSV выигрывает всухую. Заряд батареи: при 100% зелёный (96), при 0% красный (0), посередине естественным образом получается жёлтый. Перевод заряда в оттенок — одна строка: hue = map(level, 0, 100, 0, 96). Никаких таблиц «при 70% бери такие-то числа». Тревога — оттенок 0, спокойствие — 96, промежуточные состояния — промежуточные точки круга. Если делаете хоть какой-то статусный индикатор на ленте — делайте его в HSV, и он будет читаться интуитивно.
Эксперименты для закрепления
HSV щёлкает только через руки. Четыре задачи по возрастанию — все решаются в симуляторах этого урока, инструменты у вас уже есть: CHSV, fill_rainbow, map и циклы.
- Радуга в одном цвете: fill_rainbow(leds, 16, 140, 3) — лента зальётся оттенками синего. Подберите старт и дельту так, чтобы лента выглядела как «лёд».
- Пульс тревоги: зелёное «дыхание» из третьего симулятора перекрасьте в красный CHSV(0, 255, v) и ускорьте delay(15) до delay(8). Разница настроения — мгновенная.
- Индикатор заряда: заведите
int level = 73;, зажгите level / 6 пикселей цветом CHSV(map(level, 0, 100, 0, 96), 255, 255). Поменяйте level на 40 и на 10 и проверьте, что индикатор честно желтеет и краснеет. - Двухцветная лента: первая половина — CHSV(0, 255, 255), вторая — CHSV(160, 255, 255); каждые полсекунды меняйте их местами через if и счётчик. Это заготовка «полицейской» подсветки без единого RGB-числа.
- HSV = оттенок (hue 0–255) + насыщенность (s) + яркость (v); CHSV(h, s, v) присваивается пикселю как обычный CRGB
- Круг оттенков: каждые 32 шага новый цвет; 0 и 255 — почти один красный, поэтому переливы без швов
- fill_rainbow(leds, n, startHue, deltaHue) — радуга одной строкой; сдвиг startHue в loop() оживляет её
- v — яркость пикселя для анимаций; setBrightness — глобальный множитель, ставится один раз
- Индикаторы уровней делаются через map(): заряд 0–100 превращается в оттенок 0–96 без таблиц
- 1. Сколько шагов оттенка в цветовом круге FastLED?
- 2. Какой оттенок в FastLED у чистого зелёного?
- 3. Что делает третий параметр в вызове fill_rainbow(leds, 16, hue, 16)?
- 4. Чем v из CHSV отличается от FastLED.setBrightness()?
- 5. Заряд батареи 100% — зелёный, 0% — красный. Какой hue даст map(level, 0, 100, 0, 96) при 50%?
- 6. Почему переливы оттенка в HSV получаются без «швов»?
Теперь вы красите пиксели осмысленно и умеете оживлять один параметр — оттенок или яркость. Осталась главная привычка настоящего светодиодного аниматора: рисовать не «когда дошли руки в loop()», а по таймеру кадров. На millis() держатся все серьёзные эффекты — бегущие волны, огонь, кометы с хвостами, — и следующий урок целиком посвящён этому каркасу.
Частые вопросы по теме урока
- Сколько шагов оттенка в цветовом круге FastLED?
- Оттенок хранится в одном байте, который вмещает 256 значений. Круг замкнут: 255 стоит рядом с 0. 256 (от 0 до 255)
- Какой оттенок в FastLED у чистого зелёного?
- 96 — зелёный. Для сравнения: 32 — оранжевый, 64 — жёлтый, 160 — синий. Каждые 32 шага — новый цвет. 96
- Что делает третий параметр в вызове fill_rainbow(leds, 16, hue, 16)?
- Первый параметр — буфер, второй — число пикселей, третий — стартовый оттенок, четвёртый — приращение оттенка от пикселя к пикселю. Задаёт шаг оттенка между соседними пикселями
- Чем v из CHSV отличается от FastLED.setBrightness()?
- v — свойство отдельного пикселя, им анимируют затухания. setBrightness — глобальная крутилка на всю ленту, её обычно ставят один раз. v — яркость пикселя, setBrightness — множитель на всю ленту
- Заряд батареи 100% — зелёный, 0% — красный. Какой hue даст map(level, 0, 100, 0, 96) при 50%?
- Половина пути от 0 до 96 — это 48, жёлто-зелёный оттенок. Полноценный жёлтый (64) получился бы при уровне около 67%. 48
- Почему переливы оттенка в HSV получаются без «швов»?
- Оттенок ходит по кругу: после 255 снова идёт 0 — красный. Переходы получаются непрерывными без какого-либо кода сглаживания. Круг оттенков замкнут: 255 стоит рядом с 0