Ночник «закат» не получился у меня с первой попытки — и даже с третьей. Хотелось, чтобы верх ленты уходил в глубокую синеву, а низ тлел оранжевым у «горизонта», то есть подоконника. Инструмент казался очевидным: HSV из урока про HSV и палитры, оттенок ползёт от синего к красному. Я завёл hue, прогнал его от 160 к 0 по длине ленты — и получил на полпути грязный болотный цвет: короткий путь по кругу оттенков от синего к красному лежит через пурпур, а мой линейный подъём повёл через зелёную половину колеса. Пятьдесят ватт ленты светили цветом лужи.
Дело не в кривых руках, а в выборе маршрута: круг оттенков сам решает, через какие цвета вам ехать, и стороной объехать зелень — развлечение на вечер. А правильный инструмент в FastLED лежит рядом и называется палитра: вы задаёте опорные цвета в RGB, библиотека сама рисует переходы между ними — по прямой, без кругов и без болота. В этом уроке собираем палитру из опорных точек, красим ленту через ColorFromPalette и запускаем перелив по времени. Механика та же, что у радуги из fill_rainbow, только цвета выбираете вы, а не колесо оттенков.
CRGBPalette16: шестнадцать ячеек и 256 ступеней между ними
Палитра в FastLED — массив из 16 опорных цветов, спрятанный за типом CRGBPalette16. Красите вы не по номеру ячейки, а по индексу от 0 до 255: ColorFromPalette(pal, index) находит, между какими двумя опорными цветами оказался ваш индекс, и смешивает их в нужной пропорции. По умолчанию смешивание линейное — режим LINEARBLEND, поэтому вместо шестнадцати ступенек получается почти непрерывный градиент: между соседними ячейками по 16 уровней интерполяции.
Палитра и fill_rainbow дополняют друг друга, и разделять их помогает простое правило: если эффект описывается словом «радуга» — берите fill_rainbow, она всегда рисует полное колесо и не спрашивает у вас цвета. Если словом «закат», «огонь», «вода» — палитру: маршрут цвета задан вручную. По памяти разница тоже понятная: палитра занимает 48 байт оперативки, 16 ячеек по три канала, так что даже на Uno можно держать с десяток палитр под разные режимы гирлянды и переключать их по кнопке из базовых уроков.
Объявляется палитра перечислением опорных цветов: CRGBPalette16 sunset = CRGBPalette16(CRGB(20, 0, 60), CRGB(180, 20, 90), CRGB(255, 110, 20), CRGB(255, 210, 120)); — четыре цвета разъезжаются по шестнадцати ячейкам поровну, по четыре слота на каждый, и между ними библиотека рисует переходы. Красный канал плавно растёт от 20 к 180, синий подрастает к 90 — вот и пурпурная середина заката, ради которой я мучил круг оттенков. Индекс живёт по тем же правилам, что и оттенок в уроке про колесо цвета: тип byte, 256 значений, переполнение само замыкает счёт.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 16 CRGB leds[NUM_LEDS]; // Четыре опорных цвета: синева, пурпур, оранжевый, тёплая кромкаCRGBPalette16 sunset = CRGBPalette16( CRGB(20, 0, 60), CRGB(180, 20, 90), CRGB(255, 110, 20), CRGB(255, 210, 120)); byte startIndex = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(80);} void loop() { for (int i = 0; i < NUM_LEDS; i++) { leds[i] = ColorFromPalette(sunset, startIndex + i * 8, 255, LINEARBLEND); } FastLED.show(); EVERY_N_MILLISECONDS(40) { startIndex++; } // окно палитры едет}Что тут происходит. Каждый пиксель получает свой индекс: стартовый startIndex плюс i * 8. Множитель 8 — это дельта из мира fill_rainbow: 16 пикселей по 8 шагов набирают 128, то есть ровно половину палитры на ленту. Хотите весь закат от края до края — ставьте 16, хотите растянуть только середину — 4. Арифметика та же, что мы разбирали в уроке про радугу: дельта на пиксель, умноженная на число пикселей, сравнивается с 256.
Перелив по времени делает EVERY_N_MILLISECONDS(40): макрос раз в 40 мс пропускает внутрь свой блок, то есть 25 раз в секунду. Счётчик байтовый — после 255 переполнится в ноль сам, оборот из 256 шагов при шаге 40 мс занимает около десяти секунд. Это тот же millis-планировщик, который мы собирали руками в уроке про millis вместо delay, только свёрнутый в однострочник. Скорость перелива — ровно одна ручка: поменяйте 40 на 15, и закат заживёт втрое быстрее.
Пару слов про яркость: в setup() стоит setBrightness(80). Тёплая часть палитры доходит до 255 по красному каналу, и на полной мощности ночник светит как печка. Глобальный множитель глушит всю ленту разом, не трогая интерполяцию: переходы остаются прежними, слабеет только свет. Для спальни 80 из 255 — комфортный потолок; для витрины за стеклом я ставлю 200 и выше, приглушённые цвета на расстоянии выглядят пыльно.
| Инструмент | Как красит | Когда беру |
|---|---|---|
| fill_solid | Вся лента одним цветом | Фон, статус, индикация на два состояния |
| fill_rainbow | Полное колесо оттенков с шагом deltaHue | Радуга, где маршрут цвета не важен |
| Палитра + ColorFromPalette | Ваши опорные цвета и переходы между ними | Закаты, вода, пламя — где маршрут цвета важен |
| Палитра с NOBLEND | 16 ячеек без смешивания | Полосатые флаги, ступенчатые шкалы |
Внутри симулятора нет ColorFromPalette — зато есть честная математика, и ручная интерполяция как раз показывает, что палитра делает под капотом. Функция lerpCh() получает два опорных значения канала и позицию между ними: a + (b - a) * t / tMax — целочисленная интерполяция, на которой держится любой градиент, от шкал прогресса до шейдеров. Три опорных цвета разбивают «растяжку» в 48 позиций на две ветки: фиолетово-синяя перетекает в оранжевый, оранжевый — в тёплый жёлтый.
Окно цвета едет по растяжке: shift сдвигается на одну позицию каждые 25 мс, полный цикл — 48 умножить на 25, около 1,2 секунды. Пиксель i смотрит в позицию i * 3 + shift: множитель 3 задаёт, какой кусок градиента виден на ленте, — 16 пикселей по 3 позиции покрывают все 48, весь градиент от начала до конца. Поменяйте 3 на 6 — на ленту уляжется половина градиента, переходы станут крупнее и медленнее на глаз, хотя скорость сдвига не изменится ни на миллисекунду.
За формулой стоит полезная привычка: карта длиннее окна. Растяжка в 48 позиций, лента показывает 16 из них, сдвиг едет окном по карте — точно так же устроены и настоящие палитры: индекс 0–255 — карта, лента — окно в неё. Поэтому один и тот же градиент можно крутить, растягивать и сжимать, не трогая сами цвета: множитель при i меняет окно, карта остаётся на месте. Это же окно-карта потом поможет с волнами: следующая ступень движения — гнать окно не по времени, а по пространству.
Градиентные палитры: опорные точки по месту
Равномерные четыре точки — простейший случай, но настоящая сила палитр в том, что опорные точки можно ставить неравномерно. Второй способ записи — градиентная палитра: пары «индекс: цвет» в макросе DEFINE_GRADIENT_PALETTE, где каждый индекс от 0 до 255 прибивает цвет к конкретному месту растяжки. Мой закат в такой записи держит тёмную синеву до отметки 80, разгоняет пурпур к середине и сжимает тёплую кромку в последние 40 шагов — расстановкой индексов, а не новыми цветами.
#include <FastLED.h> DEFINE_GRADIENT_PALETTE(sunset_gp) { 0, 15, 0, 55, // тёмная синева 80, 40, 0, 90, // синева держится до отметки 80 150, 220, 40, 90, // пурпур в середине 215, 255, 120, 25, // оранжевый 255, 255, 215, 130 // сжатая тёплая кромка};CRGBPalette16 sunset = sunset_gp;Разница между способами — в управлении плотностью переходов: скобочная запись расставляет цвета поровну, градиентная — по вашим индексам, и один и тот же набор цветов можно растянуть и сжать в разных местах. Удобная аналогия — поезд: скобочная запись ставит четыре станции на равных расстояниях, градиентная — расписание с привязкой к вашим остановкам. Мой тест простой: если переход должен быть заметен только на трети ленты, а остальное — плато, это повод сразу брать градиентную запись.
На этом же механизме стоят готовые палитры библиотеки: HeatColors_p, которой обычно красят огонь, — те же пары индекс: цвет, только опорные точки расставлены авторами FastLED, от чёрного через винно-красный и оранжевый к жёлто-белому. Собственный огонь на ней — дело одной строки; в уроке про эффекты мы собирали пламя вручную на красно-оранжевых CHSV, а палитра — следующий шаг туда же. Ощущение «своего» заката даёт только своя палитра: попробуйте обе в одном скетче из кода выше, поменяв имя палитры в ColorFromPalette.
Задачи на закрепление — все решаются в симуляторе этого урока. Первая: переведите закат в «океанскую воду» — опорные цвета (5, 30, 90), (10, 120, 140) и (120, 220, 200) — и проверьте, что между ними не пролезает зелёная топь. Вторая: соберите индикатор заряда из урока про HSV на палитре из трёх точек — красный, жёлтый, зелёный — и сравните, где переходы смотрятся честнее. Третья: замкните палитру первым цветом и убедитесь, что шов на обороте исчез. Палитра — это карта цветов, но карта сама не движется: волны, которые заставляют окно палитры катиться по ленте, — тема следующего урока.
- CRGBPalette16 — 16 опорных ячеек; красите через ColorFromPalette(pal, index) с индексом 0–255, а не по номеру ячейки
- По умолчанию ячейки смешиваются (LINEARBLEND): между соседними опорными цветами по 16 уровней; NOBLEND оставляет ступени
- Дельта на пиксель как в fill_rainbow: i * 8 на 16 пикселях уложит половину палитры, i * 16 — всю
- EVERY_N_MILLISECONDS(40) вращает байт-счётчик: переполнение замыкает круг само, оборот 256 шагов — около 10 секунд
- Градиентная запись с парами индекс:цвет ставит опорные точки неравномерно и управляет плотностью переходов
- 1. Сколько опорных ячеек хранит CRGBPalette16?
- 2. Что вернёт ColorFromPalette(pal, 70) для палитры из четырёх равномерных цветов?
- 3. Множитель i * 8 на ленте из 16 пикселей уложит сколько палитры?
- 4. Почему на стыке вращающейся палитры виден шов?
- 5. Какая запись палитры позволяет ставить опорные точки неравномерно?
Частые вопросы по теме урока
- Сколько опорных ячеек хранит CRGBPalette16?
- Внутри палитры ровно 16 слотов. Красите вы по индексу 0–255, а библиотека интерполирует между ячейками. Шестнадцать
- Что вернёт ColorFromPalette(pal, 70) для палитры из четырёх равномерных цветов?
- Индекс 70 попадает между ячейками с опорными цветами, и режим LINEARBLEND по умолчанию даёт переходный оттенок. Смесь соседних опорных цветов в пропорции позиции 70
- Множитель i * 8 на ленте из 16 пикселей уложит сколько палитры?
- 16 пикселей по 8 шагов набирают 128 из 256 — половину. Для всей палитры нужна дельта 16. Половину
- Почему на стыке вращающейся палитры виден шов?
- Палитра — отрезок, а не круг. Замкните её: последним опорным цветом перечислите первый — и шов исчезнет. Цвет на индексе 255 не совпадает с цветом на 0 и прыгает при переполнении
- Какая запись палитры позволяет ставить опорные точки неравномерно?
- В градиентной записи каждый индекс прибивает цвет к месту растяжки: плотность переходов управляете вы. Градиентная с парами индекс: цвет в DEFINE_GRADIENT_PALETTE