Окно к Новому году я украсил лентой в сто сорок четыре пикселя и захотел конфетти — разноцветных хлопьев, которые вспыхивают в случайных местах, висят долю секунды и тают. Первая версия стреляла вспышкой на каждой итерации loop(): лента ревела всеми цветами, и через минуту на неё было больно смотреть. Вторая — одна вспышка раз в тридцать миллисекунд на жёстком затухании: по тёмному полю прыгали одиночные точки, между которыми царило болото. Рабочая пара нашлась посередине: затухание двадцать и две-четыре вспышки за кадр. Скелет «затухание плюс редкие вспышки» мы собирали в уроке про искры — там же стробоскоп и мерцание на том же каркасе; здесь разбираю конфетти как ремесло: длину хвоста, плотность и цвет. Для калибровки полезно знать эталон: фирменное конфетти из официального сборника DemoReel100 — это затухание десять и одна вспышка за кадр на ленте в шестьдесят с лишним пикселей, то есть та же механика с чуть другими пропорциями.
От искр конфетти отличается не механикой, а пропорциями. Искра — одиночный уголёк на тёмном поле, ей хватает короткого следа. Конфетти — поле, которое никогда не пустеет и никогда не заливается: одновременно светится примерно десятая-третья часть пикселей, каждое живёт свою короткую жизнь, и лента каждый кадр выглядит по-новому, хотя правила игры не менялись ни на символ. Всю картину держат три числа: длина хвоста задаётся затуханием, населённость ленты — плотностью вспышек, характер палитры — насыщенностью цвета. И у конфетти есть тихое достоинство: ему не нужен ни один буфер, кроме самого leds[] — прошлое стирает затухание, и вся память расходуется на пиксели.
План такой. Три ручки по отдельности, с числами и границами, за которыми эффект ломается. Потом арифметика плотности — формула из двух умножений, по которой я подбираю число вспышек под любую длину ленты, не крутя ручку наугад. И ловушка, которая портит конфетти тише всех остальных: без зерна генератор случайных чисел выдаёт одну и ту же хореографию при каждом включении — гости не заметят, а вы заметите на второй вечер. Все числа ниже — с ленты в сто сорок четыре пикселя при такте тридцать миллисекунд; на другой длине формулы пересчитываются на месте.
Три ручки: затухание, плотность, цвет
Затухание — это fadeToBlackBy(leds, NUM_LEDS, 20): каждый кадр каждый канал каждого пикселя худеет на двадцать из 255. Свежая вспышка догорает до незаметного за 255 / 20 — примерно тринадцать кадров, при такте тридцать миллисекунд это около четырёх десятых секунды хвоста: тот самый длинный след, за который конфетти и любят. Механику деления каналов и цену каждой единицы затухания считали в уроке про fadeToBlackBy; для конфетти я держу коридор 15-30. Десять — хлопьи висят полсекунды, лента перестаёт просыхать и светится туманом; шестьдесят — след сжимается до четырёх-пяти кадров, и эффект превращается в искры из прошлого урока. Нижнюю границу тоже держите в уме: при затухании меньше десяти лента к полуночи не просыхает вовсе, и лечится это только FastLED.clear() по кнопке.
Цвет вспышки — CHSV(random8(), 200, 255): оттенок берётся со всего круга, яркость полная, гасить будет затухание. Насыщенность двести вместо полных двухсот пятидесяти пяти — ручка, которую я вкрутил после первой вечеринки: чистые спектральные цвета в куче спорят друг с другом и на белой стене отдают кислотой, а лёгкое разбеливание делает палитру конфетной, как бумажные кружочки на детской открытке. Откуда берётся «разбел» и как устроено колесо оттенков — в уроке про HSV.
Запись вспышки делайте сложением: leds[pos] += CHSV(...), а не присваиванием. Две вспышки, попавшие в один пиксель за его тринадцатикадровую жизнь, складывают каналы и дают более белую точку — на плотных настройках это выглядит как перелив. Присваивание же каждый раз стирает горящий пиксель насмерть: на населённой ленте всплывают мигающие дыры на месте только что погасших хлопьев. Разницу видно при плотности четыре и выше; на редких настройках оба варианта неразличимы, но привычку к сложению я уже не меняю. Побочный эффект сложения — канал может доехать до потолка и остаться на нём: это нормально, затухание срежет и его, а насыщение в библиотеке не даст байту перевернуться в мусор.
#include <FastLED.h>#define LED_PIN 6#define NUM_LEDS 144 CRGB leds[NUM_LEDS];unsigned long lastFrame = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180); // зерно из воздуха: A5 ни к чему не подключён, АЦП читает наводки randomSeed(analogRead(A5) + (long)millis());} void loop() { if (millis() - lastFrame < 30) return; // такт 30 мс, около 33 кадров в секунду lastFrame = millis(); fadeToBlackBy(leds, NUM_LEDS, 20); // хвост: ~13 кадров, примерно 0,4 с for (byte k = 0; k < 4; k++) { // плотность: 4 вспышки за кадр leds[random16(NUM_LEDS)] += CHSV(random8(), 200, 255); } FastLED.show();}Позиция — random16(NUM_LEDS), не random8(): восьмибитный случайный не добирается дальше 254, и на ленте в триста пикселей правый конец остался бы тёмным навсегда, как ни крути. Шестнадцатибитный тянется на всю бытовую длину. Цикл с четвёркой — плотность, и это переменная сцены, а не константа скетча: на вечеринке четыре, в спальне одна, на ёлке у окна три. Ещё деталь: вспышки за кадр раскидываются подряд, и две из четырёх иногда ложатся рядом — это нормально: случайность кластеризуется, и именно такие пары делают картину живой, а не равномерно-механической. Каркас кадра на millis() — тот же, что в уроке про эффекты без delay, поэтому конфетти спокойно уживается с кнопкой и Serial в одном loop().
Плотность подбираю арифметикой, а не на глаз. Живых пикселей на ленте в каждый момент примерно столько: вспышек за кадр, умноженных на тринадцать кадров жизни при затухании двадцать. Четыре вспышки — около пятидесяти двух горящих ячеек; на ста сорока четырёх это треть ленты, густое праздничное конфетти. Правило, которое я вывел после пары испорченных сцен: живые ячейки держите в десяти-тридцати процентах длины. На шестидесяти пикселях четыре вспышки уже заливают ленту — там живёт одна-две; на короткой тридцатке — одна вспышка через кадр. Проверить прикидку просто: снимите ленту стоп-кадром на телефон и посчитайте горящие ячейки — если их больше трети длины, вы на границе каши, какой бы красивой ни была формула.
Плотность на ручке: симулятор
Крайние положения ручки показывают обе крайности моего первого вечера: слева — болото с одиночками, справа — помеха на экране. Между ними широкая рабочая зона, и глаз находит её сам: перевалите через кашу и отступите на одно деление. В Serial дублируется текущая плотность — на реальной плате я веду её точно так же, командой из монитора порта, чтобы менять густоту не перешивая скетч и не переставляя константу.
Один нюанс, о который я споткнулся, когда захотел поверх конфетти пустить бегущую точку-курсор: затухание каждый кадр худит весь буфер, включая чужие пиксели, и второй эффект либо гаснет за те же тринадцать кадров, либо требует своей копии массива. Дешевле всего честно признать: конфетти — эффект-одиночка, в моих проектах он живёт соло. Направленное продолжение той же механики — дождь из капель: там те же случайные вспышки получили скорость и гравитацию, и хлопья превратились в падающие капли.
Про цветные стратегии сверх полного круга. Полноцветное конфетти — не единственный вариант и не всегда лучший. На ёлке у меня лучше всего сработало двухцветное: hue = 0 + random8(40) — красно-оранжевый угол колеса, вспышки читаются как искры бенгальского огня, и сцена выглядит тематической, а не просто пёстрой. Для морской темы — узкий сине-бирюзовый сектор с базой сто пятьдесят. Узкий сектор — та же формула с меньшим диапазоном, и пересборка сцены стоит правки одной строки, а не нового эффекта.
И про общую яркость. setBrightness(180) в скетче — не прихоть: сто сорок четыре пикселя при плотных вспышках просят в пике заметный ток, и потолок яркости держит блок питания в спокойной зоне; полный бюджет по пикселям и каналам считали в уроке про яркость и ток. Если конфетти кажется тусклым, сначала поднимите плотность и добавьте вспышек: глаз читает населённость ленты сильнее, чем общую яркость, и сцена оживёт раньше, чем вы упрётесь в блок питания. Замер я делаю на самых плотных настройках, а не по среднему — тестер в пике показывает правду, которую средние значения прячут.
| Параметр | Рабочее значение | Что даёт и когда ломается |
|---|---|---|
| Затухание fadeToBlackBy | 20 при такте 30 мс | Хвост около 13 кадров (0,4 с); 10 — полсекунды тумана, 60 — искры без истории |
| Плотность вспышек | 2-5 за кадр на 144 пикселях | Живые ячейки = вспышки x 13; держите 10-30% длины ленты, иначе каша |
| Цвет вспышки | CHSV(random8(), 200, 255) | Весь круг оттенков; насыщенность 200 разбеливает спорящие краски |
| Запись вспышки | leds[pos] += CHSV(...) | Сложение каналов переливается; присваивание оставляет мигающие дыры |
- 1. Сколько кадров живёт хвост вспышки при fadeToBlackBy(leds, N, 20) и такте кадра 30 мс?
- 2. Лента 144 пикселя, 4 вспышки за кадр, затухание 20. Сколько ячеек светится одновременно в среднем?
- 3. Почему насыщенность цвета вспышки 200, а не 255?
- 4. Что даёт запись leds[pos] += CHSV(...) вместо leds[pos] = CHSV(...)?
- 5. Конфетти рисует одну и ту же картинку при каждом включении. Что сделать?
- Скелет конфетти: fadeToBlackBy(leds, N, 20) раз в 30 мс плюс 2-5 вспышек CHSV(random8(), 200, 255)
- Хвост в кадрах = 255 / затухание: при 20 и такте 30 мс — около 0,4 с
- Плотность по арифметике: живые ячейки = вспышки за кадр x 13; держите 10-30% длины ленты
- Позиция — random16(): байтовый случайный не достаёт дальше 254-го пикселя
- Без randomSeed(analogRead(A5)) конфетти повторяет одну и ту же хореографию при каждом включении
Частые вопросы по теме урока
- Сколько кадров живёт хвост вспышки при fadeToBlackBy(leds, N, 20) и такте кадра 30 мс?
- Яркость худеет на 20 из 255 за кадр: 255 / 20 — примерно 13 шагов до незаметного. При такте 30 мс это около 0,4 с. Около 13 кадров — почти четыре десятых секунды
- Лента 144 пикселя, 4 вспышки за кадр, затухание 20. Сколько ячеек светится одновременно в среднем?
- Живые ячейки = вспышки за кадр x кадров жизни: 4 x 13 = 52, это треть ленты — густое конфетти. Около 52: четыре вспышки на тринадцать кадров жизни
- Почему насыщенность цвета вспышки 200, а не 255?
- Насыщенность ниже полной подмешивает белизну: палитра становится мягче, как бумажное конфетти. Полная — кислотная. Полная насыщенность даёт чистые спектральные краски — в куче они спорят; 200 чуть разбеливает
- Что даёт запись leds[pos] += CHSV(...) вместо leds[pos] = CHSV(...)?
- Сложение складывает каналы с насыщением — вспышки переливаются. Присваивание стирает горящий пиксель и оставляет дыры. Сложение каналов: две вспышки в одну ячейку дают более белую точку
- Конфетти рисует одну и ту же картинку при каждом включении. Что сделать?
- Генератор без зерна стартует с фиксированного состояния. Зерно из неподключённого аналогового пина меняет хореографию каждого пуска. Позвать randomSeed(analogRead(A5)) в setup(): зерно из шума АЦП