В качестве хобби я занимаюсь светодиодным реквизитом и столкнулся с интересной задачей — показать что-то «красивое» на управляемой светодиодной ленте вместо традиционной радуги, не тратя на это половину памяти микроконтроллера и значительную часть процессорного времени.
Пиксели светодиодной ленты отличаются от пикселей экрана отсутствием фоновой подсветки. Чёрный пиксель не будет выглядеть «чёрным» — он сольётся с фоном, а в движении фактически будет «прозрачным», но если добавить хотя бы единицу к любому цветовому каналу — этот пиксель будет светиться. В свою очередь, «серый» пиксель от белого будет отличаться только яркостью и будет казаться более тусклым, но всё же именно «белым».
Хранится и передаётся цвет пикселя в 24-bit RGB, но значительная часть этого цветового диапазона (ненасыщенные и яркие цвета) не слишком репрезентабельна в отдельных светодиодах. Кроме того, строить симпатичные градиенты в модели RGB не получится — смешивание RGB-цветов даёт не интуитивно-очевидный результат (жёлтый + синий = серый, а хочется — зелёный). Модели HSL и HSV подойдут лучше, но стандартные реализации используют нецелочисленную арифметику. Удобно будет использовать модель, которая сможет компактно хранить параметры цвета и быстро считать их RGB-значения, не используя числа с плавающей запятой и деление на произвольное число — речь идёт о микроконтроллере и сложные алгоритмы нам ни к чему, а деление (кроме небольших степеней двойки) и вовсе противопоказано.
Решение
Для своих нужд я использую модель HSV (HSB) с определёнными диапазонами для каждой из координат (немного magic numbers).
- Hue — тон, цикличная угловая координата.
- Value, Brightness — яркость, воспринимается как альфа-канал, при пиксель не светится, при — светится максимально ярко, в зависимости от и .
- Saturation. С отсутствием фона, значения дадут не серый цвет, а белый разной яркости, поэтому параметр можно называть Whiteness — он отражает степень «белизны» цвета. При цвет полностью определяется Hue, при цвет пикселя будет белым.
Математика модели строится на целочисленном делении на одну шестую максимального значения тона (размер одного сектора), поэтому в качестве удобно взять максимальное значение равное , например 48 или 96. Это позволит удобно вычислять RGB-цвет, а значение меньше 128 позволит строить градиент, который несколько раз содержит полный цветовой круг. В моделях HSV/HSL , в MS Paint — 240, в некоторых библиотеках — 255.
При выборе максимальных значений и перемножение даёт результат, лежащий в диапазоне .
Минимальная конфигурация HSV, простая в расчёте и ограниченная 8-битными значениями, при диапазонах , и даёт нам цвета, часть из которых практически повторяется, а часть отстоит довольно далеко друг от друга (справедливости ради замечу, что этим грешат все цветовые модели, оперирующие H — натянуть три стороны куба на конус не исказив длины не смог бы и Меркатор). Цифра кажется скудной в сравнении с 24-битным цветом в типичном мониторе (16,7 млн уникальных цветов), но её достаточно, чтобы разнообразить светодиодный реквизит, в котором раньше и семь цветов вместо одного зачастую были приятным бонусом. Координаты цвета в такой модели можно хранить в двух байтах.
Разумеется, разрешение HSV можно и нужно повышать до удобного. Я использую и 96 тонов, что даёт уже 27,6 тысяч оттенков. Пример кода с такими параметрами (конфигурация модели — max_value, max_whiteness, sixth_hue):
Код
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} RGB_t;
typedef struct {
uint8_t h;
uint8_t s;
uint8_t v;
} HSV_t;
const uint8_t max_whiteness = 15;
const uint8_t max_value = 17;
const uint8_t sixth_hue = 16;
const uint8_t third_hue = sixth_hue * 2;
const uint8_t half_hue = sixth_hue * 3;
const uint8_t two_thirds_hue = sixth_hue * 4;
const uint8_t five_sixths_hue = sixth_hue * 5;
const uint8_t full_hue = sixth_hue * 6;
inline RGB_t rgb(uint8_t r, uint8_t g, uint8_t b) {
return (RGB_t) {r, g, b};
}
inline HSV_t hsv(uint8_t h, uint8_t s, uint8_t v) {
return (HSV_t) {h, s, v};
}
const RGB_t black = {0, 0, 0};
RGB_t hsv2rgb(HSV_t hsv) {
if (hsv.v == 0) return black;
uint8_t high = hsv.v * max_whiteness;//channel with max value
if (hsv.s == 0) return rgb(high, high, high);
uint8_t W = max_whiteness - hsv.s;
uint8_t low = hsv.v * W;//channel with min value
uint8_t rising = low;
uint8_t falling = high;
uint8_t h_after_sixth = hsv.h % sixth_hue;
if (h_after_sixth > 0) {//not at primary color? ok, h_after_sixth = 1..sixth_hue - 1
uint8_t z = hsv.s * uint8_t(hsv.v * h_after_sixth) / sixth_hue;
rising += z;
falling -= z + 1;//it's never 255, so ok
}
uint8_t H = hsv.h;
while (H >= full_hue) H -= full_hue;
if (H < sixth_hue) return rgb(high, rising, low);
if (H < third_hue) return rgb(falling, high, low);
if (H < half_hue) return rgb(low, high, rising);
if (H < two_thirds_hue) return rgb(low, falling, high);
if (H < five_sixths_hue) return rgb(rising, low, high);
return rgb(high, low, falling);
}
P.S.
TeX-коды в топик включил в порядке эксперимента. Если есть способ делать это удобнее или правильнее — намекните в ПМ.
Если будет интересно — могу отдельно пояснить особенности обсчёта HSV, в частности механику функций rising/falling и функцию обратного расчёта в «такой» HSV.
Автор: Devgru