Салют на окне к Новому году я собрал на ленте в шестьдесят пикселей и по наивности сделал так: в каждый кадр зажигал случайный пиксель случайным цветом — а затухание забыл вовсе. Получилась не гирлянда, а помеха на экране: лента яростно мерцала всеми цветами радуги, и через минуту на неё было больно смотреть. Вторая попытка уехала в другую крайность: я поставил вялые редкие вспышки, и по ленте тоскливо прыгали одиночные точки, между которыми царило болото. Рабочую схему я нашёл не сразу, и она оказалась обидно простой: сначала каждый кадр вся лента чуть темнеет, и только поверх этого — редкие случайные вспышки. Порядок этих двух действий и есть весь эффект.
В этом уроке разберём этот скелет по косточкам и познакомимся со случайными числами FastLED: random8 и random16. Потом соберём из одной заготовки сразу несколько эффектов — конфетти, искры-угольки, мерцание и стробоскоп — и поймём, почему все «дорогие» салюты из интернета отличаются друг от друга парой чисел. Кадровый цикл на millis() мы уже строили в уроке про эффекты — каркас останется прежним, поменяется только начинка кадра.
Скелет: затухание плюс редкие вспышки
Скелет состоит из двух действий на кадр. Первое — fadeToBlackBy(leds, 60, 60): каждый канал каждого пикселя уменьшается на величину затухания, в моём примере на шестьдесят из 255, то есть примерно на четверть. Яркая точка при таком темпе догорает до нуля за пять-шесть кадров. Число затухания — это ручка длины шлейфа: 255 гасит всё мгновенно, 60 оставляет след на несколько десятков кадров, а 20 делает догорание почти вечным — лента перестаёт «просыхать», и новая вспышка ложится на светящийся туман. Второе действие: один или несколько случайных пикселей получают свежий цвет. Всё. Никаких массивов с историей, никаких циклов по памяти — прошлое стирает само затухание, и буфер каждый раз уже готов к новому кадру. Сам счёт кадров построен на millis() — том же приёме, что в уроке про millis вместо delay, поэтому анимация искр не мешает ни кнопкам, ни Serial.
Порядок внутри кадра тоже имеет значение, и я на этом спотыкался. Вспышки рисуйте ПОСЛЕ затухания, а не до: тогда свежая искра входит в кадр с полной яркостью, а терять её начинает только со следующего. Если перепутать порядок, каждая искра сразу теряет свою четверть яркости в кадре рождения, и эффект становится вялым, хотя код с виду тот же. Вторая мелочь: вспышек должно быть мало. Одна искра на кадр при кадре в 25 мс — это сорок вспышек в секунду на всю ленту, и уже этого хватает для плотного «дождя». Когда я оставлял затухание, но ставил три-четыре вспышки за кадр, лента превращалась обратно в ту самую помеху из первой попытки.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 60 CRGB leds[NUM_LEDS]; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180);} void loop() { fadeToBlackBy(leds, NUM_LEDS, 60); // тёплая искра: оттенок 0-49, полная насыщенность, случайная яркость leds[random8(NUM_LEDS)] += CHSV(random8(50), 255, random8(160, 256)); if (random8() < 10) { // примерно 4% кадров — белая вспышка на всю ленту fill_solid(leds, NUM_LEDS, CRGB::White); } FastLED.show(); delay(25);}Теперь случайные числа, и тут у FastLED своя кухня, отличная от Arduino-ядра. random8() возвращает байт от 0 до 255; random8(N) — число от 0 до N-1, удобно для позиций пикселей; random8(min, max) — от min до max-1, удобно для диапазонов яркости. Для более широких диапазонов есть random16() — до 65535, его приятно делить и масштабировать на длинных лентах, когда байта перестаёт хватать. В скетче выше random8() без аргументов работает как фильтр вероятности: условие random8() < 10 срабатывает примерно в десять случаев из 256, то есть в 4% кадров — так делают редкие события без единого таймера. Скорость у этих функций тоже резонная: генератор библиотеки легче ядерного random(), что на восьмибитной плате при сотнях вызовов в кадр заметно.
Отдельно скажу про воспроизводимость, о которой я узнал позже, чем стоило. random16_set_seed(1234) засеивает генератор, и после перезапуска платы последовательность случайных чисел повторяется точь-в-точь. Для отладки это роскошь: я кручу затухание и плотность вспышек, а последовательность искр остаётся той же самой — сравнивать варианты становится честно. Настроил — убрал seed, и эффект снова живёт своей жизнью.
В симуляторе курса функции random8 нет — движок понимает только ядерный random(min, max). Поэтому во всех sim-скетчах ниже я пишу random(0, 16) там, где на настоящей плате поставил бы random8(16). Замена механическая, а правило про границы одно и то же: верхнее значение исключено, random(0, 16) никогда не вернёт шестнадцать. В реальных скетчах я всегда пишу random8 — меньше путаницы между двумя библиотеками.
Из одной заготовки — четыре эффекта
Приятная новость: в скелете меняется ровно одна ручка — что именно мы подбрасываем в случайный пиксель. Случайный оттенок из всего круга при средней насыщенности и приглушённой яркости даёт конфетти, праздничный дождик из цветных крошек — именно так оформляют сцены побед в играх. Плотность крошек — та же ручка, что и плотность радуги в уроке про fill_rainbow: сколько случайных пикселей за кадр — столько и цвета. Узкий диапазон оттенков от 0 до 50 — тёплые искры-угольки, как у сварки или костра из базового урока, только там огонь ещё и подкармливали снизу. Белые редкие вспышки на медленном затухании — это мерцание звёзд на ночном небе: пара «звёзд» одновременно, тьма вокруг. А если вспышку сделать белой заливкой всей ленты на один кадр и убрать шлейф — выйдет стробоскоп.
Стробоскоп стоит разобрать по числам отдельно, потому что тут легко переборщить. Вспышка в один кадр при кадре 25 мс читается как резкий удар света; пауза между вспышками в 200–400 мс — классическая дискотека. Если сжимать паузу ниже 100 мс, эффект перестаёт быть эффектом и начинает раздражать — я проверял на родных. И одна вещь, о которой в гайдах пишут редко: частое мелькание тяжело переносится людям с фоточувствительностью, поэтому стробоскоп в постоянном цикле я оставляю только на короткие вставки эффекта, а не на весь вечер. Белая вспышка с обычным затуханием 60 гаснет за пять-шесть кадров и выглядит уже не стробоскопом, а молнией — это отдельный красивый вариант, у нас в скетче выше именно он.
| Эффект | Что рандомим | Числа для старта |
|---|---|---|
| Конфетти | оттенок random8(), насыщенность 200, яркость 192 | затухание 100, одна вспышка в кадр |
| Искры-уголёк | оттенок random8(50), насыщенность 255 | затухание 60, 1–2 вспышки в кадр |
| Мерцание звёзд | белые вспышки: random8() < 8 | затухание 30 — точки догорают медленно |
| Стробоскоп | сам факт вспышки: fill_solid белым | затухание 255, вспышка на 1 кадр, пауза 200–400 мс |
Ещё одна деталь, которая отличает аккуратный эффект от наспех собранного: оператор плюс-равно. Строка leds[3] += CHSV(30, 255, 120) не перезаписывает пиксель, а добавляет свет к тому, что уже горит: две искры, попавшие в один пиксель подряд, сложатся в более яркую вспышку, и лента станет живее. Побочный эффект — каналы могут упереться в потолок 255, библиотека сама аккуратно обрежет. Для стробоскопа и мерцания этот приём не нужен — там свет управляется заливками, — а вот для искр и конфетти я ставлю его почти всегда.
Тюнинг сводится к трём числам, и их полезно крутить по очереди, а не все сразу. Затухание задаёт длину следа: при кадре 25 мс и затухании 60 след живёт около 150 мс, и вспышки успевают складываться в непрерывный дождь. Плотность — сколько искр на кадр: одна на кадр для ленты в 60 пикселей выглядит хорошо, для шестнадцати уже перебор, у меня в симуляторе ровно одна. Яркость вспышек random8(160, 256) держит искры в верхнем диапазоне — тусклые искры на затухающем фоне выглядят как грязь, а не как свет. И про ток не забывайте: каждая искра на полной яркости — это до 60 мА, и на большой ленте салют легко съедает весь бюджет питания, о котором подробно поговорим в следующем уроке.
Эксперименты
Скелет закрепляется только руками, и все четыре задачи решаются в симуляторе выше за десять минут. Первая: сделайте из искр конфетти — поменяйте диапазон оттенков на полный круг и снизьте насыщенность до 200, яркость до 192. Вторая: соберите мерцание звёзд — уберите цветные искры, оставьте белые вспышки через if (random(0, 12) == 0) на одиночный пиксель вместо заливки, а затухание опустите до 30: точки должны догорать медленно и величаво. Третья: превратите белую вспышку в честный стробоскоп — поставьте затухание 255 и добавьте после fill_solid сразу FastLED.show() и delay(30), а затем замерьте, при какой паузе в delay эффект становится неприятным лично вам; у меня это случилось около 80 мс. Четвёртая, самая вкусная: заведите переменную-режим и переключайте конфетти и искры каждые пять секунд по счётчику кадров — у вас уже есть всё, чтобы плейлист эффектов листался сам.
Если после этих задач захочется красивее, следующий шаг — палитры: вместо случайного оттенка из диапазона искра берёт цвет из готового градиента, и огонь становится объёмным. Мы касались HSV в уроке про колесо цвета — там же объяснено, почему оттенок 0–50 это короткая тёплая дуга от красного к жёлтому. А готовые наборы для пламени — HeatColors и компания — ждут в уроке про HSV-палитры. Скелет при этом не меняется ни на символ.
- Скелет искр: каждый кадр fadeToBlackBy (затухание стирает историю) плюс редкие случайные вспышки поверх
- Вспышки рисуйте после затухания — свежая искра входит в кадр с полной яркостью
- random8()/random8(N)/random8(min,max) и random16() — случайные числа FastLED; random8() < 10 — событие с вероятностью около 4%
- В симуляторе random8 нет — пишем random(min, max) ядерный; верхняя граница исключена в обоих
- Конфетти, угольки, мерцание и стробоскоп — один скелет и разные значения трёх ручек: цвет вспышки, затухание, плотность
- 1. Что делает fadeToBlackBy(leds, 60, 60) с пикселем, который горит красным на полную?
- 2. Как сделать редкое событие — примерно 4% кадров, без таймеров?
- 3. Чем отличается random8(50) от random16()?
- 4. Почему вспышки искр рисуют ПОСЛЕ вызова fadeToBlackBy, а не до?
- 5. Как из скелета искр получить конфетти?
Частые вопросы по теме урока
- Что делает fadeToBlackBy(leds, 60, 60) с пикселем, который горит красным на полную?
- Каждый канал уменьшается на 60 из 255 — около четверти. За пять-шесть кадров красный канал доходит до нуля, и след исчезает. Уменьшает яркость примерно на четверть, пиксель догорает за 5–6 кадров
- Как сделать редкое событие — примерно 4% кадров, без таймеров?
- random8() возвращает 0–255, условие меньше десяти ловит около 4% значений. Это стандартный трюк редких вспышек в FastLED. if (random8() < 10) — срабатывает примерно в 10 случаях из 256
- Чем отличается random8(50) от random16()?
- Первое — диапазон до 50 для позиций и оттенков, второе — широкий диапазон до 65535 для масштабирования на длинных лентах. random8(50) даёт 0–49 и укладывается в байт, random16() даёт 0–65535
- Почему вспышки искр рисуют ПОСЛЕ вызова fadeToBlackBy, а не до?
- Затухание уменьшает всё, что горит. Если искра нарисована до него, в первом же кадре она уже потемнела — вспышки становятся тусклыми. Иначе свежая искра теряет часть яркости в кадре рождения и эффект делается вялым
- Как из скелета искр получить конфетти?
- Конфетти — тот же скелет: случайный оттенок из полного диапазона, насыщенность около 200, яркость около 192, затухание около 100. Разрешить случайный оттенок из всего круга при приглушённой насыщенности и яркости