![](https://habrastorage.org/storage2/c88/a3c/603/c88a3c6039caac6d8eb87f4dd5551262.png)
В качестве хобби я занимаюсь светодиодным реквизитом и столкнулся с интересной задачей — показать что-то «красивое» на управляемой светодиодной ленте вместо традиционной радуги, не тратя на это половину памяти микроконтроллера и значительную часть процессорного времени.
Пиксели светодиодной ленты отличаются от пикселей экрана отсутствием фоновой подсветки. Чёрный пиксель не будет выглядеть «чёрным» — он сольётся с фоном, а в движении фактически будет «прозрачным», но если добавить хотя бы единицу к любому цветовому каналу — этот пиксель будет светиться. В свою очередь, «серый» пиксель от белого будет отличаться только яркостью и будет казаться более тусклым, но всё же именно «белым».
Хранится и передаётся цвет пикселя в 24-bit RGB, но значительная часть этого цветового диапазона (ненасыщенные и яркие цвета) не слишком репрезентабельна в отдельных светодиодах. Кроме того, строить симпатичные градиенты в модели RGB не получится — смешивание RGB-цветов даёт не интуитивно-очевидный результат (жёлтый + синий = серый, а хочется — зелёный). Модели HSL и HSV подойдут лучше, но стандартные реализации используют нецелочисленную арифметику. Удобно будет использовать модель, которая сможет компактно хранить параметры цвета и быстро считать их RGB-значения, не используя числа с плавающей запятой и деление на произвольное число — речь идёт о микроконтроллере и сложные алгоритмы нам ни к чему, а деление (кроме небольших степеней двойки) и вовсе противопоказано.
Решение
Для своих нужд я использую модель HSV (HSB) с определёнными диапазонами для каждой из координат (немного magic numbers).
- Hue — тон, цикличная угловая координата.
- Value, Brightness — яркость, воспринимается как альфа-канал, при
пиксель не светится, при
— светится максимально ярко, в зависимости от
и
.
- Saturation. С отсутствием фона, значения
дадут не серый цвет, а белый разной яркости, поэтому параметр
можно называть Whiteness — он отражает степень «белизны» цвета. При
цвет полностью определяется Hue, при
цвет пикселя будет белым.
Математика модели строится на целочисленном делении на одну шестую максимального значения тона (размер одного сектора), поэтому в качестве
![Hmax](https://habrastorage.org/getpro/geektimes/post_images/440/d92/5a5/440d925a5b984f2616bbdfca24e7dac2.gif)
![6 * 2^x](https://habrastorage.org/getpro/geektimes/post_images/0cb/cc9/ac2/0cbcc9ac29efb0f282063035b3d115a7.gif)
![Hmax = 360](https://habrastorage.org/getpro/geektimes/post_images/820/5f1/515/8205f1515b401d3e7573ece51789703e.gif)
При выборе максимальных значений
![Bmax = 17](https://habrastorage.org/getpro/geektimes/post_images/733/e2f/23e/733e2f23e4d2390a1c5224d67eb015db.gif)
![Wmax = 15](https://habrastorage.org/getpro/geektimes/post_images/732/e45/afb/732e45afb7a3183aac001a2614dc698c.gif)
![B*W](https://habrastorage.org/getpro/geektimes/post_images/856/1b3/216/8561b321635a6adcc606bc03a6d1e595.gif)
![0..255](https://habrastorage.org/getpro/geektimes/post_images/a36/07b/fc4/a3607bfc4fe3fd75d3a36c57838d0896.gif)
Минимальная конфигурация HSV, простая в расчёте и ограниченная 8-битными значениями, при диапазонах
![H = 0..11](https://habrastorage.org/getpro/geektimes/post_images/16d/f5f/a31/16df5fa31b04e1bb44f5367b9f368082.gif)
![B = 0..17](https://habrastorage.org/getpro/geektimes/post_images/e04/d83/dc4/e04d83dc4c8f02a26703acfc7afb36e3.gif)
![W = 0..3](https://habrastorage.org/getpro/geektimes/post_images/571/0d7/a67/5710d7a67ea1686d602f27db5b3754a5.gif)
![12*18*4 = 864](https://habrastorage.org/getpro/geektimes/post_images/e0f/ae3/4aa/e0fae34aaa63e37bb97c85653f8278e2.gif)
Разумеется, разрешение HSV можно и нужно повышать до удобного. Я использую
![W = 0..15](https://habrastorage.org/getpro/geektimes/post_images/74f/f13/e2d/74ff13e2d40cc61a2a1345b72ccb01bf.gif)
Код
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.