Ночник в спальне дышал как метроном. Я собрал его на линейном счётчике из базового урока про HSV: яркость v поднимается на единицу каждые 15 мс до 255, потом опускается до 20. Математика честная, а глаз ворчал: подъём идёт с постоянной скоростью, на верхней границе движение обрывается и разворачивается мгновенно — вместо засыпания получается пила. Просьба была «чтоб как у ноутбука в спящем режиме», а у той огибающая плавная: к краям движение замедляется само.
Ночнику нужен синус по времени. Не волна по ленте из прошлого урока, а один пульс на все пиксели сразу: медленный разгон у краёв, быстрый проход середины. В этом уроке соберём классическое дыхание на millis() и sin(), разберём, зачем нижней границе яркости подушка около 30, и добавим квадратичное сглаживание scale8 — трюк, который превращает ровную синусоиду в сонное дыхание с долгой тёмной паузой на выдохе.
Период дыхания подбирается под задачу, и ориентир у него физиологический: взрослый человек в покое делает 12–16 вдохов в минуту, один цикл занимает от 3,75 до 5 секунд. Ночник я настроил на 15 «вдохов» в минуту — период ровно четыре секунды: медленно, но не до тоски. Тревожная индикация живёт на других скоростях — от 60 циклов в минуту и выше, но там уже не дыхание, а пульс.
Второй ориентир — нижняя граница яркости. В уроке про HSV мы уже гасили «дыхание» до 20 и видели, как лента на мгновение слепает целиком. Оставляем подушку: минимум 30, максимум 255 — ход в 225 единиц. Фаза привязана к millis(): ритм не собьётся, если в loop() появится посторонний код, а скорость задана одним множителем — поменяйте его, и период пересчитается сам.
Почему линейный счётчик дёргает
У линейного счётчика скорость постоянна, а разворот на границе мгновенный — такая огибающая называется треугольной. Глаз же судит о плавности не по скорости в середине, а по поведению у краёв: настоящий вдох начинается медленно, разгоняется и так же мягко останавливается перед выдохом. Треугольник делает наоборот — краевые точки проносятся на полной скорости. Отсюда ощущение метронома, хотя частота у обоих вариантов одинаковая.
Классические примеры «плавного светодиода» на analogWrite с шагом fadeAmount и паузой 5–10 мс — тот же треугольник, растянутый во времени. Для индикатора на обычном LED с ШИМ из урока про analogWrite это нормально. Но WS2812 умеет попиксельную яркость от 0 до 255 — грех не взять синус: одна строка расчёта и плавность у краёв, которой у счётчика нет.
Есть и второй слой: глаз адаптируется к освещению. В тёмной комнате, где ночник и работает, зрачок расширен, и малые изменения яркости видны сильнее, чем на свету. Развороты счётчика в нижней трети амплитуды попадают ровно в зону максимальной чувствительности — потому метроном так заметен именно ночью. Синус в этой зоне движется медленнее всего, что совпадает с физиологией: у краёв цикла переходы плавнее, и тёмная часть дыхания не дёргает адаптированный глаз.
Разбор по числам. Фаза angle = millis() * 0.0016 растёт на 0,0016 радиана за миллисекунду; полный оборот 6,283 занимает 6,283 / 0,0016 — около 3,9 секунды, те самые 15 «вдохов» в минуту. Перевод в яркость делает map(): синус умножен на 100 и сдвинут в ход 0–200, чтобы работала целочисленная арифметика, а map(s100, 0, 200, 30, 255) развернул этот ход в подушку 30–255. На живой плате ту же работу делает beatsin8(15, 30, 255): период 60/15 = 4 секунды, границы зашиты в аргументы, таймер внутри.
Подушка на минимуме — не каприз, а защита от квантования. Яркость у WS2812 восьмибитная, и один шаг на низком уровне крупнее, чем на высоком: спад с 30 на 29 — это минус 3% видимой яркости за такт, спад с 255 на 254 — только 0,4%. Глаз замечает первый и не видит второй, поэтому ниже 20–30 вместо плавного затухания лента «пошагивает» по единицам. Подушка 30 прячет ступени, и дыхание остаётся гладким до самой нижней точки. Заодно экономится ток: лента из 30 пикселей на пике ест до 1,8 А, и бюджет питания мы считали в уроке про питание ленты — тёмная пауза на выдохе его заметно разгружает.
Serial в симуляторе печатает b раз в 600 мс, и по числам видно характер синуса: у минимума соседние отсчёты отличаются на единицы, в середине — на десятки. Это хорошая привычка для любого эффекта: пара «b=32» в логе подтверждает, что лента дышит по расчёту, а не потому что «вроде выглядит плавно».
Квадратичное сглаживание: scale8(x, x)
Синус уже дал плавные края. Следующий шаг — характер: у сонного дыхания выдох длиннее вдоха, а у синусоиды обе половины одинаковы. Квадратичное сглаживание в FastLED — это scale8(x, x): функция масштабирования scale8(i, scale) считает i * scale / 255, и если подставить её же аргумент вторым числом, получится квадрат. Квадрат прижимает низ: значения 30, 90, 180 и 255 после scale8(x, x) дают 3, 31, 127 и 255 — низ и середина проваливаются вниз, вершина остаётся полной.
На глазу эффект такой: тёмная пауза на выдохе растягивается в разы, пик остаётся ярким и коротким — дыхание получает характер сна, а не взмахов. Цена: минимум 30 после квадрата падает до 3, лента уходит почти в темноту. Для ночника это фича; для подсветки, которая не должна гаснуть, — грабли: поднимите нижнюю границу до 120, и после квадрата минимум станет 120 * 120 / 255 — около 56, лента дышит, но не слепнет.
scale8 полезен и сам по себе — это локальный множитель на один пиксель: scale8(i, 128) прижмёт значение до половины, scale8(i, 255) вернёт как есть. Связка «сначала квадрат, потом scale8 с коэффициентом» даёт второй сценарий: scale8(soft, 200) после квадрата приглушит и вершину, если полной мощности не нужно — например, когда лента стоит близко к изголовью.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 16 CRGB leds[NUM_LEDS]; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(200);} void loop() { byte breath = beatsin8(15, 30, 255); // период 60/15 = 4 с, ход 30..255 byte soft = scale8(breath, breath); // квадрат: пауза на выдохе глубже fill_solid(leds, NUM_LEDS, CHSV(25, 190, soft)); FastLED.show(); FastLED.delay(15);}beatsin8(15, 30, 255) держит весь таймер в одном вызове: 15 — это обороты в минуту из прошлого урока, период выходит четыре секунды, границы 30–255 не дают ленте ослепнуть. scale8(breath, breath) добавляет квадратичный характер. Кадр каждые 15 мс — 66 обновлений в секунду, глазу хватает с запасом: мерцание перестаёт различаться где-то от 50 Гц, так что и на этой частоте запас двойной.
Граница применимости синуса — плато. Медитативная схема «4–7–8» (вдох четыре секунды, пауза семь, выдох восемь) требует плоских задержек, а синусоида непрерывна и полок не держит. Как только в дыхании появились заданные паузы, переходите к кусочным интервалам по millis() из урока про millis вместо delay: каждый отрезок — своя кривая и своя длительность. Синус — для непрерывного дыхания; всё, что с паузами, собирайте руками.
Частота кадров тут не критична: анимация медленная, и даже 20 кадров в секунду дают гладкую картинку. Но раз кадр в 15 мс ничего не стоит ни по расчёту, ни по току, я ставлю 15 и забываю: запас по плавности бесплатный, а глаз разницы между 20 и 66 кадрами не увидит — но и не пожалуется.
| Способ | Ядро кода | Характер |
|---|---|---|
| Линейный счётчик | v = v + dir на каждом такте | Метроном: постоянная скорость, резкий разворот |
| sin() + millis() | map по синусу времени | Плавный, но float-математика тяжела для Uno |
| beatsin8() | beatsin8(15, 30, 255) | Тот же синус: таймер и границы одним вызовом |
| beatsin8 + scale8 | scale8(v, v) | Сонное дыхание: длинная тёмная пауза, полный пик |
Эксперименты. Первый — в симуляторе: перекрасьте дыхание в красный CHSV(0, 255, b) и удвойте темп до 0.0032 — получится тревожный пульс. Второй — на плате: дайте каждому пикселю свою фазу, beatsin8(15, 30, 255, 0, i * 16) на 16 пикселях, — и дыхание замкнётся с волной из прошлого урока: по ленте пройдёт бегущий горб, а не общий пульс. Третий: соберите ночник целиком — тёплая палитра из урока про свои палитры, ход яркости 60–255, темп 12 циклов в минуту — и проверьте на себе, через сколько минут он начинает клонить в сон.
Дальше по разделу — случайность: random8, вспышки и затухания fadeToBlackBy, из которых собираются искры и салют. Движение у вас теперь есть — волны и дыхание; следующая ступень — непредсказуемость.
- Ориентир темпа — физиология: 12–16 вдохов в минуту в покое, период 3,75–5 с; ночнику подходит 15 циклов = 4 с
- Треугольник (линейный счётчик) дёргает у краёв; синус замедляется к границам — глаз считывает именно края
- Подушка минимума 30 прячет квантование: шаг единица на уровне 30 — минус 3% яркости за такт, на 255 — 0,4%
- scale8(x, x) — квадрат в FastLED: 30 даёт 3, 180 — 127, 255 — 255; пауза глубже, пик полнее, минимум тонет
- beatsin8(BPM, low, high) — дыхание одним вызовом; плато 4–7–8 синусом не собирается, там нужны кусочные интервалы
- 1. Какой период у дыхания beatsin8(15, 30, 255)?
- 2. Почему у линейного счётчика дыхание выглядит дёрганым?
- 3. Что вернёт scale8(180, 180)?
- 4. Зачем минимуму дыхания подушка около 30?
- 5. Как собрать на синусе дыхание с паузами «4–7–8»?
Частые вопросы по теме урока
- Какой период у дыхания beatsin8(15, 30, 255)?
- Период равен 60/15: 15 циклов в минуту дают цикл в четыре секунды — темп спокойного дыхания. Четыре секунды
- Почему у линейного счётчика дыхание выглядит дёрганым?
- Треугольная огибающая проносит края на полной скорости. Глаз судит о плавности именно по краям цикла. Скорость постоянна, а у краёв разворот мгновенный
- Что вернёт scale8(180, 180)?
- scale8(i, scale) считает i * scale / 255: 180 * 180 / 255 — примерно 127. Квадрат прижимает низ и середину. Около 127
- Зачем минимуму дыхания подушка около 30?
- Один шаг на низком уровне — крупный скачок видимой яркости: с 30 на 29 это минус 3%. Подушка прячет «пошагивание». Ниже 20–30 глаз видит ступени квантования яркости
- Как собрать на синусе дыхание с паузами «4–7–8»?
- Синусоида непрерывна и плоских полок не делает. Паузы заданной длины собирают из millis-интервалов, свой участок кривой на каждый. Поставить delay(7000) в loop