Статусная лента в коридоре офиса переключала режимы fill_solid-ом, и каждое переключение било по глазам — даже краем зрения вспышку ловил каждый, кто шёл мимо. Заказчик формулировал это как «мигает как сигнализация», хотя сигнализацией там и не пахло: просто цвет менялся за один кадр, а глаз читает резкую смену как событие и тревогу. Цвету, как и яркости, нужен ход, а не прыжок. В уроке про fill_solid заливка — рабочий инструмент и там она на месте; здесь обратная задача: перейти от текущего цвета к целевому так, чтобы глаз не поймал момента перехода.
Инструмент — nblend из FastLED: за кадр он подмешивает к каждому пикселю долю целевого цвета. Разберём формулу, ручку скорости в единицах и секундах, родню — fadeToBlackBy, который работает тем же мотором, только цель всегда чёрная, и границу, где резкость всё-таки нужна.
blend, nblend и доля за кадр
В FastLED две функции с одним мотором. blend(c1, c2, amount) возвращает новый цвет, аргументы не трогая — удобно посчитать промежуточный оттенок заранее, например для таблицы. nblend(existing, overlay, amount) правит existing на месте, и для ленты это ровно то, что нужно: leds[i] сам становится смесью себя и цели. Доля — тип fract8 от 0 до 255: 0 — оставить как есть, 255 — перекрасить мгновенно, 128 — пополам. На каждый канал работает взвешенное среднее: текущий канал умножаем на 255 минус доля, прибавляем целевой на долю, делим на 255. Та же арифметика, что в ручных градиентах из урока про градиенты FastLED, только точки опоры едут каждый кадр.
Плавность живёт в повторении. Зовите nblend раз в кадр с небольшой долей, 20-30, и пиксель экспоненциально потянется к цели: каждый кадр съедает девять-десять процентов остатка, поэтому ближе к фину переход сам замедляется и садится в цель без щелчка. При доле 24 и кадре 30 мс половина пути занимает семь кадров — около 0,2 секунды, почти весь ход укладывается в секунду. Доля — единственная ручка характера: 12 даёт ленивое переливание для ночника, 64 — деловой темп для витрины, а 255 превращает вызов в ту же жёсткую заливку, от которой мы уходим.
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 16#define BTN_GOAL 2 // зажата - зелёная цель, отпущена - красная CRGB leds[NUM_LEDS];CRGB target = CRGB(255, 40, 0);unsigned long lastFrame = 0; void setup() { pinMode(BTN_GOAL, INPUT_PULLUP); FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(150); fill_solid(leds, NUM_LEDS, target); // стартуем уже в цели} void loop() { if (millis() - lastFrame < 30) return; // кадр раз в 30 мс lastFrame = millis(); if (digitalRead(BTN_GOAL) == LOW) target = CRGB(0, 255, 60); else target = CRGB(255, 40, 0); for (int i = 0; i < NUM_LEDS; i++) { nblend(leds[i], target, 24); // 24/255 - около 9 процентов остатка за кадр } FastLED.show();}Что здесь каркасное. Кадр раз в 30 мс держит millis-метка из урока про millis вместо delay — delay сломал бы отзывчивость кнопки, и переход ждал бы отпускания. nblend зовётся для каждого пикселя отдельно: на ленте с зонами цели бывают разные, а вызов на весь массив перекрасил бы всё одним цветом разом. Родственник по мотору — fadeToBlackBy из урока про затухание: та же доля за кадр, только цель чёрная. При 32 за кадр и 60 кадрах в секунду яркость падает вдвое примерно каждые 70 миллисекунд — это и есть мягкое затухание в противовес жёсткой записи нуля, после которой глаз видит выхлоп.
Отдельно про границы применимости, чтобы вы не тянули переход туда, где он вредит. Плавное подтягивание всегда опаздывает: между нажатием кнопки и приездом ленты в цель лежит весь экспоненциальный хвост, и на сигнале опасности это опоздание стоит дороже эстетики. Я измерял на кадре 30 мс с долей 24: до девяноста процентов цели — около 0,8 секунды, и все эти восемьдесят сотых лента честно считает, что ещё едет. Для атмосферы — идеально, для аварийного статуса — нет.
Резкость — тоже инструмент
Не вычёркивайте fill_solid из арсенала. Цвет-сигнал обязан включаться мгновенно: красный при аварии, зелёное «можно ехать», подтверждение принятой команды — здесь переход размазывает смысл, и я заливаю жёстко, без зазрения совести. Переход — для атмосферы: смена сцен гирлянды, ночная подсветка, режимы лампы, где смена цвета — фон, а не событие. Практическое правило у меня такое: если человек должен заметить смену — заливка; если должен её не заметить — nblend. Оба варианта встают в один и тот же каркас кадров, который собран в уроке про эффекты в millis-цикле — меняется только тело кадра.
И про соседа по теме — яркость. Переход по цвету не отменяет резкой смены яркости: если до этого лента горела на полную, даже красивый перелив выглядит дерзко. Команда FastLED.setBrightness тоже плавает не сама — её плавно ведут тем же приёмом, доля за кадр, и в уроке про диммер на потенциометре разобрано, как вести яркость без рывков. Два плавных хода — цвет и яркость — вместе дают тот уровень, когда ленту можно оставить в спальне.
Разбор модели. Плавный режим повторяет nblend буквально: каждый кадр к текущим каналам подмешивается доля 38 из 255 — те же проценты, что у вызова nblend(leds[i], target, 38) на плате. Резкий режим присваивает цель как есть. Перекиньте тумблер при зажатой кнопке и посмотрите на середину хода: красный к зелёному в RGB проезжает болотную смесь, потому что прямая в цветовом кубе проходит мимо чистых оттенков. Это не проказа симулятора — живой nblend поведёт себя так же, он честно лерпит по прямой. Лечение — маршрут через оттенок: в HSV тот же переезд красиво идёт через жёлтый, и урок про колесо цвета показывает, как считать путь в оттенках, а не в каналах.
Для коротких сдвигов — оранжевый к красному, синий к бирюзе — прямая в RGB честная, и nblend там лучший выбор: один вызов, ноль таблиц, переход за полсекунды. Мой рабочий компромисс: близкие цвета веду nblend-ом в RGB, далёкие — циклом по оттенку с тем же подходом доли за кадр. Оба пути сходятся в одну строку внутри кадра, а зритель получает чистые середины перехода вместо болота.
| Приём | Что делает | Когда беру |
|---|---|---|
| fill_solid | мгновенно перекрашивает весь массив | статусы и сигналы: смена должна быть замечена |
| blend | возвращает смесь, аргументы не трогает | посчитать промежуточный цвет заранее, для таблиц и шкал |
| nblend | правит пиксель на месте на долю за кадр | переходы сцен и режимов, где смена — фон |
| fadeToBlackBy | та же доля за кадр, цель чёрная | шлейфы и затухание после ярких событий |
- 1. Чем nblend отличается от blend?
- 2. Что произойдёт при nblend с долей 255?
- 3. Пиксель застыл в паре единиц от цели и не доезжает. Причина?
- 4. fadeToBlackBy(leds, 16, 32) на кадре 60 fps: когда яркость упадёт примерно вдвое?
- 5. Где жёсткая заливка уместнее плавного перехода?
- nblend(existing, target, amount) правит пиксель на месте; blend возвращает новый цвет и аргументы не трогает
- Доля fract8: 0 — ничего, 255 — мгновенно; 24 за кадр 30 мс — половина пути за 0,2 с
- Переход экспоненциальный: замедляется к фину сам, финального щелчка нет
- RGB-переход между далёкими цветами едет через грязь — далёкие ведут по оттенку, близкие — nblend-ом
- Сигнал — заливка, атмосфера — переход; fadeToBlackBy — тот же мотор с целью чёрная
Частые вопросы по теме урока
- Чем nblend отличается от blend?
- blend считает смесь и возвращает её как новый цвет. nblend записывает смесь прямо в первый аргумент — для ленты это значит: leds[i] обновился без лишних переменных. nblend правит первый аргумент на месте, blend возвращает новый
- Что произойдёт при nblend с долей 255?
- Доля 255 забирает весь вес у старого цвета и отдаёт цели: переход сжимается до одного кадра. Малые доли дают плавность, 255 — резкость. Мгновенная перекраска — тот же эффект, что у fill_solid
- Пиксель застыл в паре единиц от цели и не доезжает. Причина?
- В 8-битной арифметике (разница x 24) / 255 при разнице 1 даёт ноль. Хвост в пару единиц добирается редким кадром с большой долей или кратной целью. Целочисленная доля: разница меньше единицы не делится
- fadeToBlackBy(leds, 16, 32) на кадре 60 fps: когда яркость упадёт примерно вдвое?
- Каждый кадр умножает каналы на 223/255 — около 87 процентов. Половина остаётся через четыре кадра, что при 60 fps составляет около 70 мс. Примерно за четыре кадра — около 70 мс
- Где жёсткая заливка уместнее плавного перехода?
- Сигнал должен быть замечен мгновенно — заливка. Всё, где смена цвета фон, а не событие, ведут переходом с малой долей за кадр. Аварийный статус: красный обязан включиться сразу