Аквариумную подсветку я раскрашивал вручную: семь остановок цвета, семь if в цикле, между ними мой «плавный переход», который на поверку выходил ступеньками — промежуточные оттенки я ленился считать. На 30-пиксельной ленте швы читались, как несглаженный график, и рыбы, честно говоря, не жаловали. Тогда я ещё не знал, что библиотека умеет дорисовывать градиенты сама, и вручную изобретал колесо, которое уже было в FastLED. Правильный инструмент я нашёл позже, при сборке гирлянды-метеора, и с тех пор вручную раскрашиваю разве что тесты на два цвета. Палитры закрывают именно эту боль: вы описываете несколько опорных цветов, а библиотека сама рисует переходы между ними с точностью до 1/256 круга. В этом уроке разберём CRGBPalette16, функцию ColorFromPalette и готовую раскладку пламени HeatColors_p — и в финале соберём лаву, где индексом цвета служит шум Перлина из прошлого урока.
Как устроена палитра: 16 точек и 256 позиций
CRGBPalette16 — массив из шестнадцати опорных цветов, 16 × 3 байта = 48 байт. Но просит палитра у вас не номер точки, а индекс 0–255: ColorFromPalette(палитра, индекс, яркость, LINEARBLEND) делит круг на 16 секторов и внутри каждого тянет градиент между соседними опорными цветами. Четвёртый аргумент задаёт режим: LINEARBLEND смешивает плавно, NOBLEND выдаёт жёсткие ступени — иногда это нужно, для «мозаики» или быстрой отладки раскладки. Третий аргумент — яркость — умножается на итоговый цвет: отдельная ручка поверх, удобная для мерцания без переписывания палитры. Проще всего представить палитру линейкой в 256 делений с 16 отметками: ColorFromPalette читает показание в любом делении, даже между отметками — там и живёт смешивание. Вернёмся к моей аквариумной раскладке: семь if сжимаются в палитру из семи опорных — а переходов становится ровно шесть, по числу пар соседей, и каждый — бесступенчатый. Сама структура цвета с её тремя байтами была разобрана в уроке про CRGB и fill_solid — палитра просто шестнадцать таких троек подряд. Отсюда и цена опечатки: один испорченный опорный цвет перекрашивает целый сектор круга. Я однажды ловил «порченый» градиент полчаса, пока не вывел все шестнадцать значений палитры в монитор порта и не увидел, где синий влез в оранжевый сектор.
Готовых палитр в библиотеке с десяток: RainbowColors_p, OceanColors_p, CloudColors_p, LavaColors_p, ForestColors_p, PartyColors_p и другие. Нас интересует HeatColors_p — раскладка пламени: чёрный, тёмно-красный, красный, оранжевый, жёлтый, белый. Она же сидит внутри HeatColor() из урока про огонь, с той разницей, что HeatColor жёстко привязана к формуле, а палитру можно крутить: сжимать градиент, перекрашивать, собирать свою. Для огня HeatColors_p остаётся эталоном — её и перекрашивать не хочется, разве что сжать, чтобы белые кончики показывались реже. Встроенные палитры объявлены через PROGMEM — они хранятся во flash-памяти и не занимают оперативку: 48 байт на Uno — это 2,3% всей RAM, и отдавать их под данные, которые никогда не меняются, расточительно. Чтение из flash медленнее, чем из RAM, но на шестнадцати-шестидесяти обращениях за кадр этого не видно. Названия подсказывают настроение: OceanColors_p держит сине-бирюзовый диапазон, ForestColors_p — зелень с охрой, PartyColors_p — кислотные пары. Я держу в закладках страницу документации с картинками всех палитр и выбираю по ней, а не по памяти. Есть у палитр и старший брат — CRGBPalette256, та же линейка со всеми 256 позициями в памяти; на Uno я его не держу — это 768 байт RAM, а вот на ESP32 он живёт свободно. Как устроена PROGMEM, я разбирал отдельно в уроке про память.
| Палитра | Что рисует | Куда просится |
|---|---|---|
| HeatColors_p | Чёрный — красный — оранжевый — жёлтый — белый | Огонь, камины, индикатор нагрева |
| LavaColors_p | Тёплая лава, без белого | Лава-лампа, магма |
| OceanColors_p | Синие и бирюзовые | Вода, аквариум |
| ForestColors_p | Зелень, охра | Листва, «дыхание» растений |
| RainbowColors_p | Классическая полная радуга | Проверка ленты, яркая индикация |
| PartyColors_p | Кислотная радуга | Дискотека, карнавал |
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 60 CRGB leds[NUM_LEDS];CRGBPalette16 fire = HeatColors_p; // палитра лежит в PROGMEMbyte idx = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180);} void loop() { for (int i = 0; i < NUM_LEDS; i++) { // индекс 0-255: библиотека сама смешает опорные цвета leds[i] = ColorFromPalette(fire, i * 4 + idx, 255, LINEARBLEND); } idx = idx + 2; // скорость бегущего градиента FastLED.show(); delay(25);}Обратите внимание на i * 4 в индексе: это шаг палитры между пикселями, аналог дельты в fill_rainbow. При 60 пикселях и шаге 4 весь градиент помещается на ленте один раз (60 × 4 = 240 из 256), при шаге 8 — дважды. Свой градиент собирает конструктор: CRGBPalette16 pal(CRGB::Black, CRGB::Red, CRGB::Orange, CRGB::Yellow); — четыре опорных цвета, между которыми библиотека дорисует всё сама. Двух цветов тоже достаточно: получится честная растяжка от и до. Собрать раскладку можно и функцией fill_gradient_RGB(target, c0, c1, c2) — она заполняет готовый CRGBPalette16 теми же переходами; конструктор короче, функция гибче: ей можно перезаписать и часть палитры, а не всю целиком. Если хочется длинного градиента через всю ленту — уменьшайте шаг; коротких повторяющихся полос — увеличивайте: зависимость та же, что у дельты в радуге. И не путайте шаг палитры со скоростью бега: i * 4 раскладывает градиент по ленте, а idx = idx + 2 гонит его вдоль — две независимые ручки, крутите по очереди, иначе не поймёте, что именно поменяли.
Сколько остановок брать под задачу — вопрос вкуса с границей здравого смысла. Для индикаторов хватает двух-трёх: красный — жёлтый — зелёный, и весь смысл в том, что между ними всё уже смешано заранее. Для «атмосферных» эффектов — закат, лава, вода — я обычно беру четыре-пять: глазами градиент из четырёх остановок на 60 пикселях неотличим от идеальной рампы. Больше восьми не брал ни разу: если нужна такая точность, проще взять готовую палитру библиотеки. Цвета останавливают пипеткой из фотографии — скрин заката, два-три замера, готово. Единственная ловушка: очень тёмные опорные. Чёрный ставьте явным CRGB::Black, а не «тёмно-тёмно-синим» — на ленте полутёмные цвета читаются грязью. Собранную раскладку проверяйте ритуалом: выведите ленту одним градиентом на пару секунд — глаз ловит грязные переходы мгновенно, даже там, где числа выглядели правильными. И палитра не обязана быть радугой — монохромная раскладка из трёх красных разной яркости даёт «тепловизор», а двухцветная — зебру из двух тонов с мягкой границей.
CRGBPalette16 lava = CRGBPalette16( CRGB::Black, CRGB::Red, CRGB::Orange, CRGB::Yellow);unsigned int x = 0; void loop() { for (int i = 0; i < NUM_LEDS; i++) { byte n = inoise8(i * 30 + x, x * 4); // шум в индексе палитры — пятна жара ползут и гаснут leds[i] = ColorFromPalette(lava, n, 255, LINEARBLEND); } x = x + 4; FastLED.show(); delay(20);}Симулятор: раскладка и интерполяция руками
Палитр в движке симулятора нет, поэтому в сим-версии та же идея собрана руками — и это полезно тем, что показывает внутренность ColorFromPalette. Раскладка пламени из трёх остановок: чёрный в нуле, красный на индексе 128, жёлто-оранжевый на 255. Между остановками значения тянет map(): ровно то, что ColorFromPalette делает между шестнадцатью опорными, только грубее. Остановок три не случайно: столько нужно, чтобы увидеть и подъём, и спад, и полку раскладки — на четырёх-пяти ветвях принцип тот же. Индекс в симуляторе бежит по ленте — получается огненная волна; поменяете остановки — она перекрасится в закат или полярное сияние. Если захотите точности — увеличьте остановок до пяти: веток в if станет три, и это ровно то скучное ручное письмо, от которого палитры избавляют. А если ужесточить верхнюю ветвь — поставить вместо map() фиксированный hue = 45 — получится «двухфазный» огонь: сначала разгорание, потом ровное пламя. Полный код с настоящими палитрами — в блоках выше, для живой платы.
- Полярное сияние: вторую остановку перекрасьте — в нижней ветке CHSV(0, 255, v) замените на CHSV(150, 255, v), и градиент станет сине-зелёным.
- Плотность: шаг i * 16 поменяйте на i * 8 и на i * 32. Посчитайте, сколько кругов раскладки ляжет на шестнадцать пикселей, и сверьте с лентой.
- Ступени: замените map() в нижней ветке на жёсткое правило if (idx < 85) — чёрный, else — красный. Вы только что собрали NOBLEND своими руками.
- Дыхание поверх раскладки: на плате яркость удобнее крутить третьим аргументом ColorFromPalette; в симуляторе поднимите v из медленной синусоиды на millis() — градиент начнёт дышать.
- CRGBPalette16 — 16 опорных цветов (48 байт); ColorFromPalette берёт индекс 0–255 и сам смешивает переходы
- LINEARBLEND — плавные переходы, NOBLEND — ступени; яркость в вызове — отдельная ручка поверх
- HeatColors_p — готовая раскладка пламени, живёт в PROGMEM; HeatColor() — её жёсткая формула
- Свой градиент — конструктор из 2–4 цветов: CRGBPalette16(CRGB::Black, CRGB::Red, CRGB::Orange, CRGB::Yellow)
- Шум в индексе палитры — лава и облака: эффекты отличаются одной строкой приложения шума
- 1. Сколько опорных цветов у CRGBPalette16?
- 2. Что вернёт ColorFromPalette(pal, 137, 255, LINEARBLEND)?
- 3. Где хранятся встроенные палитры вроде HeatColors_p?
- 4. Что плохого в объявлении CRGBPalette16 внутри loop()?
- 5. Шум inoise8 подали в индекс ColorFromPalette. Что получится на ленте?
Частые вопросы по теме урока
- Сколько опорных цветов у CRGBPalette16?
- Шестнадцать опорных точек, 48 байт памяти (16 цветов по три канала). 256 — это число позиций индекса, 48 — размер в байтах, 3 — каналы одного цвета. Три разных числа из одной палитры — любимая путаница новичков. 16
- Что вернёт ColorFromPalette(pal, 137, 255, LINEARBLEND)?
- Индекс 0–255 делится на 16 секторов по 16 позиций; внутри сектора LINEARBLEND тянет градиент между двумя соседними опорными цветами. Опорных точек всего 16, а позиций — 256, поэтому цвет на позиции 137 смотрится «смешанным». Смесь соседних опорных цветов пропорционально позиции 137
- Где хранятся встроенные палитры вроде HeatColors_p?
- Библиотека объявляет их через PROGMEM: данные читаются из flash по мере надобности, и 48 байт на каждую палитру не съедают 2 КБ оперативки Uno. Чтение чуть медленнее, но за кадр это незаметно — проверки на глаз не пройдёт даже матрица. Во flash через PROGMEM — без расхода RAM
- Что плохого в объявлении CRGBPalette16 внутри loop()?
- Копирование мелкое, но регулярное: на десятках обращений в кадре оно уже заметно. А если палитру анимируют, пересоздание ломает саму анимацию. Палитру объявляют глобально или через static — жизнь у него та же, а шапка скетча чище. Палитра пересоздаётся каждый кадр — лишнее копирование 48 байт перед каждым show()
- Шум inoise8 подали в индекс ColorFromPalette. Что получится на ленте?
- Шум даёт плавно меняющийся индекс каждому пикселю — палитра превращает его в цвет. Так собирают лаву и облака; от воды эффект отличается только тем, куда приложили шум: в индекс или в яркость. Пятна цвета, которые рождаются, ползут и гаснут — лава