Ночник у кровати я научил дышать за один вечер: один beatsin8 на всю ленту, яркость ходит между 20 и 180, красота. Через неделю эффект надое — дыхание читалось как пульс: глаз ловил края цикла и ждал следующего вздоха. Хотелось воды — чтобы по ленте шли валы, которые набегают вразнобой, где-то пересекаются, где-то расходятся, и ритм нельзя предсказать заранее. Контракт вызова и фазу пикселя разобрали в уроке про beatsin8 — там один горб едет по ленте за счёт пятого аргумента; здесь я собираю из нескольких волн воду и показываю, где синусная математика требует пересчёта единиц, а не веры в байты.
Маршрут такой. Сначала разведём sin8 и beatsin8 — это одна и та же синусоида в двух ролях: позиция против времени, и от выбора роли зависит вся структура скетча. Потом дыхание с ручной амплитудой: байт от волны ещё нужно ужать до той высоты, которая нужна сцене, иначе он бьёт в потолок 255. Потом океан: три волны с разными темпами и разными шагами фазы, сложенные в одну высоту, плюс градиент цвета вдоль ленты, который собирается из колеса HSV одной строкой.
Две волны я пробовал раньше — слой поверх слоя, как в совете из урока про фазу, и для качелей этого хватало. Вода начинается с трёх: зыбь, ход и рябь живут на разных горизонтах, и глаз различает их как отдельные движения, а не как один горб с призвуком. Три — и потолок разумного: четвёртая волна добавляет вызов и байты бюджета кадра, а различимого глазом движения почти не прибавляет. Все дальнейшие числа — из ночника на тридцати пикселях, который месяц висел у кровати вместо первого дышащего.
sin8 против beatsin8: позиция и время
sin8(x) — чистая функция позиции: подали число от 0 до 255, получили высоту синусоиды в этой точке круга. Сколько бы кадров ни прошло, sin8(64) вернёт одно и то же — картинка статична, пока вы сами не двигаете аргумент. beatsin8(BPM, low, high) живёт во времени: внутри тикает шестнадцатибитный счётчик от millis(), значение качается само с периодом 60000, делённым на BPM, миллисекунд. Отсюда правило выбора, которое я вывел после пары переписанных скетчей: если волна стоит в координатах — столбцы матрицы, ячейки поля, — берите sin8 и двигайте фазу сами; если лента просто показывает срез волны через сдвиг фазы i * шаг, берите beatsin8 и не заводите ни одного счётчика.
Минимум и максимум — второй и третий аргументы — это уже не про время, а про амплитуду. beatsin8(30, 40, 120) качается между 40 и 120: так ставят фон, который не гаснет до нуля. Но здесь же первая ловушка масштабирования: байт приходит в единицах диапазона, а «высота волны» в проекте может быть любой другой — восемь ступеней яркости, столбик матрицы, миллисекунды паузы. Каждый раз нужен явный пересчёт: высота равна low плюс значение, умноженное на размах и делённое на 255. Забыли — и волна, которая должна качать матрицу в пределах восьми пикселей, уезжает в полный потолок и ломает картину на пиках.
Сложение волн — вторая ловушка. Два байта в лоб складывать нельзя: сумма уедет за 255, и на пиках вылезет мусор; в lib8tion для пары слоёв есть qadd8 с насыщением, его трогали в уроке про фазу пикселя. Я сначала собрал три волны именно через qadd8 — и похоронил вариант: насыщение сплющило пики в плато, вода заблестела металлом. Для трёх и больше слоёв живее среднее: (w1 + w2 + w3) / 3 — сумма байтов в C++ сама расширяется до int, переполнения нет, результат заведомо в 0-255. Среднее тихо глушит одиночные пики, и для воды это ровно то, что нужно: настоящие валы не бывают стопроцентными.
#include <FastLED.h>#define LED_PIN 6#define NUM_LEDS 30 CRGB leds[NUM_LEDS];unsigned long lastFrame = 0; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180);} void loop() { if (millis() - lastFrame < 20) return; // 50 кадров в секунду lastFrame = millis(); for (int i = 0; i < NUM_LEDS; i++) { // три волны: зыбь BPM 9, ход BPM 14, рябь BPM 23 byte w1 = beatsin8(9, 0, 255, 0, i * 5); byte w2 = beatsin8(14, 0, 255, 0, i * 11); byte w3 = beatsin8(23, 0, 255, 0, i * 19); byte v = (w1 + w2 + w3) / 3; // среднее: сумма байтов живёт в int int wind = 30 + v * 225 / 255; // масштаб: высота 30..255 leds[i] = CHSV(150 - i * 3, 205, wind); // синий уходит в бирюзу } FastLED.show();}Три вызова отличаются только BPM и шагом фазы — 5, 11 и 19. Темпы подобраны не ровно друг под другом: 9, 14 и 23 дают циклы 6,7 секунды, 4,3 и 2,6, и общий узор повторяется лишь через много секунд, потому что периоды не кратны. Поставьте 10, 20 и 30 — волны начнут периодически выстраиваться в лад, и раз в несколько секунд лента вспыхнет согласованным горбом. Это отдельный легальный эффект, но воде он фальшивит: настоящий океан состоит из несогласованных ритмов, и в официальном сборнике рецептов FastLED многослойные волны именно так и рекомендованы — для океана, плазмы и ряби.
Градиент hue = 150 - i * 3 красит ленту от синего к бирюзе: глубина уходит в мелководье, как у настоящего берега, и глаз читает ленту как воду, а не как качели яркости. Формула высоты 30 + v * 225 / 255 держит минимум на тридцати: впадины не гасят сцену в ноль, и ночью комната не мигает темнотой — тот же приём с нижней границей, что и в одиночном дыхании из урока про пульсацию. Каркас кадра — знакомая millis-метка из урока про millis вместо delay: между кадрами плата свободна, и в реальной сцене там живёт кнопка, датчик или пульт.
Подбор темпов делаю по одному правилу: старт с девятки, четырнадцати и двадцати трёх, дальше кручу по одному BPM и смотрю на стыки минуту-другую. Слились в общее дыхание — ускорил зыбь или замедлил рябь. На короткой ленте волн стало слишком много и она дробится в кашу — уменьшил шаги фазы: 5, 11 и 19 дают три разных масштаба движения, а шаги 8 и 16 на тридцати пикселях нарежут ленту почти в противофазу. Плотность волн — та же ручка, что и длина волны: делить 256 на шаг фазы я держу в голове наизусть, и вам советую.
Океан из трёх волн в симуляторе
Посмотрите на стыки валов при спокойной ручке. Три несогласованных осциллятора дают характерную картину: горб то вырастает там, где волны сошлись в фазе, то распадается, когда разошлись, и повторение уплывает далеко за горизонт, потому что периоды не кратны. Включите шторм и сравните: темп вырос вчетверо, характер остался тем же — это признак воды, а не качелей. Если волны вдруг строятся в ряд и бегут строем, темпы стали кратны: разведите их хотя бы на единицу BPM.
Градиент цвета здесь стоит на месте, а высота ходит — и этого хватает глазу. Живее всего океан выглядит, когда цвет тоже чуть дышит: добавьте к hue медленный дрейф на единицу-другую в секунду, и мелководье начнёт переливаться. Ток на такой сцене считается по каналам: у бирюзы из CHSV синий и зелёный каналы примерно по трети, красный нулевой, и тридцать пикселей берут в пике порядка 0,5 ампера при полном ветре — USB-порт держится, только пока ручка не выкручена до упора. Бюджет по каналам и потолкам считали в уроке про яркость и ток.
И ещё одно наблюдение для ночной сцены: океан не обязан быть синим. Тот же ход волн на угольной раскладке — оранжевый низ, тёмный верх — читается как море на закате, на зелёно-жёлтой — как северное сияние над водой. Цвет держится одной формулой hue вдоль ленты, и смена сцены — правка одного числа, а не переработка скетча. Я держу три раскладки в комментариях над setup() и переключаю их комментарием же: для ночника этого хватает, а ток новой сцены пересчитывается по каналам за минуту.
| Аргумент | Что делает | Типовые значения |
|---|---|---|
| beatsPerMinute | Темп полного цикла: 60000/BPM мс на взлёт и спад | 9-23 для воды, 30-60 для дыхания |
| low, high (min, max) | Пределы качания байта: сжимая их, задаёшь амплитуду | 0-255 для высоты, 30-120 для фона |
| timebase | Сдвиг начала отсчёта, чтобы волны стартовали не с нуля | 0 — почти всегда |
| phase_offset | Фаза: на неё пиксель или целый слой опаздывает | 0, 85, 171 — три слоя вразнобой |
Масштабирование из ловушки превращается в ручку, как только волне дают работу кроме яркости. На матрице 8x8 высота волны — столбик от дна: map(v, 0, 255, 1, 8), и вода оживает в вертикали. В маяке высота — пауза между вспышками, в индикаторе загрузки — число горящих сегментов. Везде одна и та же пара действий: взять байт от волны, пересчитать в свои единицы с нижним порогом, не дать уехать за потолок. Держите пересчёт в одном месте функции кадра — растыкать его по коду я перестал после скетча, где поправил масштаб в двух местах из трёх и полчаса смотрел на рассинхрон слоёв.
И мнение напоследок. Дыхание одним beatsin8 — честный эффект для ночника, и усложнять его ради усложнения не стоит: одиночная синусоида на двадцати пикселях у кровати работает годами и никого не раздражала бы, если бы не мой перфекционизм. Но если сцена называется «вода», одной синусоидой она не собирается: вода — это всегда сумма трёх-четырёх несогласованных ритмов, и FastLED даёт каждый слой одним вызовом. Разницу с одиночной волной видно невооружённым глазом за десять секунд — прогоните оба варианта в симуляторе и оставьте тот, который нравится вашей комнате.
- 1. Чем sin8 отличается от beatsin8?
- 2. Сколько длится полный цикл beatsin8 при BPM 23?
- 3. Волна должна качать столбик матрицы в пределах 1-8 пикселей. Что делать с байтом от beatsin8?
- 4. Почему три волны складывают средним, а не прямой суммой?
- 5. Темпы волн 10, 20 и 30 BPM — что не так?
- sin8(x) — синусоида в координатах, beatsin8(BPM, low, high) — во времени: цикл 60000/BPM мс
- Байт волны переводится в единицы сцены формулой low + значение * размах / 255 — иначе потолок 255 ломает пики
- Океан — три волны с некратными темпами (9, 14, 23 BPM) и разными шагами фазы, сложенные средним
- Среднее трёх байтов безопасно: сумма живёт в int, результат заведомо в 0-255; qadd8 сплющивает пики в плато
- Градиент hue вдоль ленты (150 - i * 3) даёт глубину, а нижний порог 30 не гасит сцену в ноль
Частые вопросы по теме урока
- Чем sin8 отличается от beatsin8?
- sin8(x) возвращает высоту синусоиды в точке круга и никуда не движется сам. beatsin8 внутри тикает от millis() и качает байт с периодом 60000/BPM мс. sin8 — функция позиции на круге, beatsin8 качает значение во времени сам
- Сколько длится полный цикл beatsin8 при BPM 23?
- BPM — циклов в минуту, период равен 60000/BPM. При 23 это 2609 мс; темпы 9, 14 и 23 дают некратные циклы 6,7, 4,3 и 2,6 секунды. Около 2,6 секунды: 60000/23
- Волна должна качать столбик матрицы в пределах 1-8 пикселей. Что делать с байтом от beatsin8?
- Байт волны всегда пересчитывается в единицы сцены: low + значение * размах / 255. Остаток от деления даст пилу вместо синусоиды, умножение — потолок 255. Пересчитать в свои единицы: 1 + байт * 7 / 255
- Почему три волны складывают средним, а не прямой суммой?
- Сумма трёх байтов доходит до 765; в байте остаётся хвост переполнения. Среднее (w1+w2+w3)/3 живёт в int и заведомо укладывается в 0-255, заодно глуша одиночные пики. Прямая сумма байтов уезжает за 255, и пики обрезаются мусором
- Темпы волн 10, 20 и 30 BPM — что не так?
- Периоды 6, 3 и 2 секунды кратны, и каждые несколько секунд волны сходятся в фазе. Рабочая вода — некратные темпы вроде 9, 14 и 23. Циклы кратны: волны периодически выстраиваются в лад и бегут строем