CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
ColorPickerView.cpp
Go to the documentation of this file.
1
6
11#include "cdc_ui/ViewStack.h"
12#include "cdc_ui/I18n.h"
13#include "cdc_hal/IDisplay.h"
14#include "cdc_hal/IKeypad.h"
15#include "cdc_log.h"
16#include <goodisplay/gdey029T94.h>
17#include <algorithm>
18#include <cstdio>
19#include <cstring>
20
21static constexpr uint32_t REPEAT_INITIAL_MS = 300;
22static constexpr uint32_t REPEAT_PERIOD_MS = 70;
23
24namespace cdc::ui {
25
26namespace {
27
28constexpr int16_t HEADER_LINE_OFF = 6; // line at y = 16
29
30constexpr int16_t TRI_OX = 10;
31constexpr int16_t TRI_OY = 20;
32constexpr int16_t R_X = 50, R_Y = 0;
33constexpr int16_t G_X = 0, G_Y = 60;
34constexpr int16_t B_X = 100, B_Y = 60;
35
36constexpr int16_t TEXT_X = 160;
37constexpr int16_t TEXT_Y = 22;
38
39constexpr int16_t SLIDER_X = 160;
40constexpr int16_t SLIDER_Y = 80;
41constexpr int16_t SLIDER_W = 125;
42constexpr int16_t SLIDER_H = 8;
43
44constexpr int16_t NAME_Y = 98;
45
46constexpr uint8_t kBayer4[4][4] = {
47 { 15, 135, 47, 175 },
48 { 207, 79, 239, 111 },
49 { 63, 191, 31, 159 },
50 { 255, 127, 223, 95 }
51};
52
53inline int32_t sign(int16_t x1, int16_t y1,
54 int16_t x2, int16_t y2,
55 int16_t x3, int16_t y3) {
56 return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3);
57}
58
59inline uint8_t lum(uint8_t r, uint8_t g, uint8_t b) {
60 return static_cast<uint8_t>((77u * r + 150u * g + 29u * b) >> 8);
61}
62
63const char* colorName(uint8_t r, uint8_t g, uint8_t b) {
64 uint8_t mx = std::max({r, g, b});
65 uint8_t mn = std::min({r, g, b});
66 if (mx < 24) return "Black";
67 if (mx - mn < 24) {
68 if (mx < 90) return "Dark gray";
69 if (mx < 200) return "Gray";
70 return "White";
71 }
72 int16_t delta = mx - mn;
73 int16_t hue;
74 if (mx == r) hue = ((int16_t)(g - b) * 60) / delta;
75 else if (mx == g) hue = 120 + ((int16_t)(b - r) * 60) / delta;
76 else hue = 240 + ((int16_t)(r - g) * 60) / delta;
77 if (hue < 0) hue += 360;
78 bool dark = mx < 110;
79 if (hue < 15) return dark ? "Brown" : "Red";
80 if (hue < 35) return dark ? "Brown" : "Orange";
81 if (hue < 55) return dark ? "Olive" : "Yellow";
82 if (hue < 80) return dark ? "Olive" : "Lime";
83 if (hue < 150) return dark ? "Dark green" : "Green";
84 if (hue < 180) return dark ? "Teal" : "Cyan";
85 if (hue < 220) return dark ? "Teal" : "Sky blue";
86 if (hue < 260) return dark ? "Navy" : "Blue";
87 if (hue < 290) return dark ? "Purple" : "Violet";
88 if (hue < 330) return dark ? "Magenta" : "Pink";
89 return dark ? "Brown" : "Red";
90}
91
92} // namespace
93
94void ColorPickerView::init(uint8_t r, uint8_t g, uint8_t b) {
95 value_ = std::max({r, g, b, static_cast<uint8_t>(1)});
96 cursorX_ = (R_X + G_X + B_X) / 3;
97 cursorY_ = (R_Y + G_Y + B_Y) / 3;
98 setFromRGB(r, g, b);
99 repeatStartMs_ = 0;
100 lastRepeatMs_ = 0;
101 onSave_ = nullptr;
102 onCancel_ = nullptr;
103 dirty_ = true;
104}
105
106void ColorPickerView::barycentric(int16_t px, int16_t py,
107 int32_t& aR, int32_t& aG, int32_t& aB) const {
108 int32_t d = sign(R_X, R_Y, G_X, G_Y, B_X, B_Y);
109 if (d == 0) { aR = aG = aB = 0; return; }
110 aR = sign(px, py, G_X, G_Y, B_X, B_Y) * 255 / d;
111 aG = sign(R_X, R_Y, px, py, B_X, B_Y) * 255 / d;
112 aB = sign(R_X, R_Y, G_X, G_Y, px, py) * 255 / d;
113}
114
115void ColorPickerView::clampInsideTriangle() {
116 int32_t aR, aG, aB;
117 int16_t bestX = cursorX_;
118 int16_t bestY = cursorY_;
119 barycentric(cursorX_, cursorY_, aR, aG, aB);
120 if (aR < 0 || aG < 0 || aB < 0) {
121 int32_t total = aR + aG + aB;
122 if (total != 0) {
123 int32_t cx = (R_X * std::max<int32_t>(aR, 0) +
124 G_X * std::max<int32_t>(aG, 0) +
125 B_X * std::max<int32_t>(aB, 0)) /
126 std::max<int32_t>(1, std::max<int32_t>(aR, 0) +
127 std::max<int32_t>(aG, 0) +
128 std::max<int32_t>(aB, 0));
129 int32_t cy = (R_Y * std::max<int32_t>(aR, 0) +
130 G_Y * std::max<int32_t>(aG, 0) +
131 B_Y * std::max<int32_t>(aB, 0)) /
132 std::max<int32_t>(1, std::max<int32_t>(aR, 0) +
133 std::max<int32_t>(aG, 0) +
134 std::max<int32_t>(aB, 0));
135 bestX = static_cast<int16_t>(cx);
136 bestY = static_cast<int16_t>(cy);
137 }
138 cursorX_ = bestX;
139 cursorY_ = bestY;
140 }
141}
142
143void ColorPickerView::setFromRGB(uint8_t r, uint8_t g, uint8_t b) {
144 int32_t sum = static_cast<int32_t>(r) + g + b;
145 if (sum == 0) {
146 cursorX_ = (R_X + G_X + B_X) / 3;
147 cursorY_ = (R_Y + G_Y + B_Y) / 3;
148 return;
149 }
150 int32_t cx = (r * R_X + g * G_X + b * B_X) / sum;
151 int32_t cy = (r * R_Y + g * G_Y + b * B_Y) / sum;
152 cursorX_ = static_cast<int16_t>(cx);
153 cursorY_ = static_cast<int16_t>(cy);
154}
155
156void ColorPickerView::moveCursor(int16_t dx, int16_t dy) {
157 int16_t nx = cursorX_ + dx;
158 int16_t ny = cursorY_ + dy;
159 int32_t aR, aG, aB;
160 int16_t saved_x = cursorX_;
161 int16_t saved_y = cursorY_;
162 cursorX_ = nx;
163 cursorY_ = ny;
164 barycentric(cursorX_, cursorY_, aR, aG, aB);
165 if (aR < 0 || aG < 0 || aB < 0) {
166 cursorX_ = saved_x;
167 cursorY_ = saved_y;
168 return;
169 }
170 dirty_ = true;
171}
172
173void ColorPickerView::adjustValue(int8_t delta) {
174 int16_t nv = value_ + delta;
175 nv = std::clamp<int16_t>(nv, 1, 255);
176 if (nv != value_) {
177 value_ = static_cast<uint8_t>(nv);
178 dirty_ = true;
179 }
180}
181
182void ColorPickerView::rawColor(uint8_t& r, uint8_t& g, uint8_t& b) const {
183 int32_t aR, aG, aB;
184 barycentric(cursorX_, cursorY_, aR, aG, aB);
185 aR = std::max<int32_t>(0, aR);
186 aG = std::max<int32_t>(0, aG);
187 aB = std::max<int32_t>(0, aB);
188 int32_t sum = aR + aG + aB;
189 if (sum == 0) { r = g = b = 0; return; }
190 r = static_cast<uint8_t>(aR * 255 / sum);
191 g = static_cast<uint8_t>(aG * 255 / sum);
192 b = static_cast<uint8_t>(aB * 255 / sum);
193}
194
195void ColorPickerView::currentColor(uint8_t& r, uint8_t& g, uint8_t& b) const {
196 rawColor(r, g, b);
197 r = static_cast<uint8_t>((static_cast<uint16_t>(r) * value_) / 255);
198 g = static_cast<uint8_t>((static_cast<uint16_t>(g) * value_) / 255);
199 b = static_cast<uint8_t>((static_cast<uint16_t>(b) * value_) / 255);
200}
201
202void ColorPickerView::drawDitheredBox(Gdey029T94* gfx, int16_t x, int16_t y,
203 int16_t w, int16_t h, uint8_t luminance) const {
204 if (!gfx) return;
205 for (int16_t row = 0; row < h; ++row) {
206 for (int16_t col = 0; col < w; ++col) {
207 uint8_t threshold = kBayer4[row & 3][col & 3];
208 if (luminance < threshold) {
209 gfx->drawPixel(x + col, y + row, EPD_BLACK);
210 }
211 }
212 }
213}
214
215void ColorPickerView::drawTriangle(Gdey029T94* gfx, int16_t ox, int16_t oy) const {
216 gfx->drawLine(ox + R_X, oy + R_Y, ox + G_X, oy + G_Y, EPD_BLACK);
217 gfx->drawLine(ox + G_X, oy + G_Y, ox + B_X, oy + B_Y, EPD_BLACK);
218 gfx->drawLine(ox + B_X, oy + B_Y, ox + R_X, oy + R_Y, EPD_BLACK);
219}
220
222 switch (key) {
223 case '2': moveCursor(0, -3); repeatStartMs_ = 0; return InputResult::CONSUMED;
224 case '8': moveCursor(0, 3); repeatStartMs_ = 0; return InputResult::CONSUMED;
225 case '4': moveCursor(-3, 0); repeatStartMs_ = 0; return InputResult::CONSUMED;
226 case '6': moveCursor( 3, 0); repeatStartMs_ = 0; return InputResult::CONSUMED;
227 case '7': adjustValue(-10); repeatStartMs_ = 0; return InputResult::CONSUMED;
228 case '9': adjustValue( 10); repeatStartMs_ = 0; return InputResult::CONSUMED;
229 case KEY_YES: {
231 if (onSave_) {
232 uint8_t r, g, b;
233 currentColor(r, g, b);
234 onSave_(r, g, b);
235 }
237 }
238 case KEY_NO:
240 if (onCancel_) {
241 onCancel_();
242 }
244 default:
246 }
247}
248
249void ColorPickerView::onTick(uint32_t nowMs) {
250 auto* kp = cdc::hal::getKeypadInstance();
251 if (!kp) return;
252 bool any = false;
253 int16_t dx = 0, dy = 0;
254 int8_t dv = 0;
255 using cdc::hal::Key;
256 if (kp->isKeyPressed(Key::KEY_2)) { dy -= 3; any = true; }
257 if (kp->isKeyPressed(Key::KEY_8)) { dy += 3; any = true; }
258 if (kp->isKeyPressed(Key::KEY_4)) { dx -= 3; any = true; }
259 if (kp->isKeyPressed(Key::KEY_6)) { dx += 3; any = true; }
260 if (kp->isKeyPressed(Key::KEY_7)) { dv -= 10; any = true; }
261 if (kp->isKeyPressed(Key::KEY_9)) { dv += 10; any = true; }
262 if (!any) { repeatStartMs_ = 0; return; }
263 if (repeatStartMs_ == 0) {
264 repeatStartMs_ = nowMs;
265 lastRepeatMs_ = nowMs;
266 return;
267 }
268 if (nowMs - repeatStartMs_ < REPEAT_INITIAL_MS) return;
269 if (nowMs - lastRepeatMs_ < REPEAT_PERIOD_MS) return;
270 lastRepeatMs_ = nowMs;
271 if (dx || dy) moveCursor(dx, dy);
272 if (dv) adjustValue(dv);
273}
274
276 if (customFooter_) return customFooter_;
277 return "[2/4/6/8] mix [7/9] intensity [Y] OK";
278}
279
280void ColorPickerView::render(bool partial) {
282 if (!display) return;
283 auto* gfx = static_cast<Gdey029T94*>(display->getNativeHandle());
284 if (!gfx) return;
285 const uint16_t width = display->getWidth();
286 const uint16_t height = display->getHeight();
287
288 if (!partial) {
289 gfx->fillScreen(EPD_WHITE);
290 } else {
291 gfx->fillRect(0, 0, width, height - layout::FOOTER_HEIGHT, EPD_WHITE);
292 }
293
294 gfx->setTextColor(EPD_BLACK);
295 gfx->setTextSize(1);
296 gfx->setTextWrap(false);
297
298 int16_t ox = TRI_OX;
299 int16_t oy = TRI_OY;
300
301 // Subtle ~6% textured background inside the triangle.
302 int32_t det = sign(R_X, R_Y, G_X, G_Y, B_X, B_Y);
303 int16_t bx0 = std::min({R_X, G_X, B_X});
304 int16_t bx1 = std::max({R_X, G_X, B_X});
305 int16_t by0 = std::min({R_Y, G_Y, B_Y});
306 int16_t by1 = std::max({R_Y, G_Y, B_Y});
307 for (int16_t py = by0; py <= by1; ++py) {
308 for (int16_t px = bx0; px <= bx1; ++px) {
309 int32_t s1 = sign(px, py, G_X, G_Y, B_X, B_Y);
310 int32_t s2 = sign(R_X, R_Y, px, py, B_X, B_Y);
311 int32_t s3 = sign(R_X, R_Y, G_X, G_Y, px, py);
312 bool inside = (s1 >= 0 && s2 >= 0 && s3 >= 0) ||
313 (s1 <= 0 && s2 <= 0 && s3 <= 0);
314 if (!inside) continue;
315 if (kBayer4[py & 3][px & 3] < 16) {
316 gfx->drawPixel(ox + px, oy + py, EPD_BLACK);
317 }
318 }
319 }
320 (void)det;
321
322 drawTriangle(gfx, ox, oy);
323
324 gfx->setCursor(ox + R_X - 3, oy + R_Y - 9);
325 gfx->print("R");
326 gfx->setCursor(ox + G_X - 6, oy + G_Y + 2);
327 gfx->print("G");
328 gfx->setCursor(ox + B_X + 2, oy + B_Y + 2);
329 gfx->print("B");
330
331 int16_t cx = ox + cursorX_;
332 int16_t cy = oy + cursorY_;
333 gfx->fillRect(cx - 2, cy - 2, 5, 5, EPD_BLACK);
334 gfx->drawPixel(cx, cy, EPD_WHITE);
335
336 uint8_t r, g, b;
337 currentColor(r, g, b);
338
339 char buf[24];
340 gfx->setTextSize(1);
341 std::snprintf(buf, sizeof(buf), "R: %3u", r);
342 gfx->setCursor(TEXT_X, TEXT_Y);
343 gfx->print(buf);
344 std::snprintf(buf, sizeof(buf), "G: %3u", g);
345 gfx->setCursor(TEXT_X, TEXT_Y + 14);
346 gfx->print(buf);
347 std::snprintf(buf, sizeof(buf), "B: %3u", b);
348 gfx->setCursor(TEXT_X, TEXT_Y + 28);
349 gfx->print(buf);
350
351 int16_t sy = SLIDER_Y;
352 gfx->setCursor(SLIDER_X, sy - 10);
353 std::snprintf(buf, sizeof(buf), "Intensity: %3u", value_);
354 gfx->print(buf);
355 gfx->drawRect(SLIDER_X, sy, SLIDER_W, SLIDER_H, EPD_BLACK);
356 int16_t fill = static_cast<int16_t>((static_cast<uint32_t>(value_) * (SLIDER_W - 4)) / 255);
357 gfx->fillRect(SLIDER_X + 2, sy + 2, fill, SLIDER_H - 4, EPD_BLACK);
358
359 const char* name = colorName(r, g, b);
360 int16_t bx, by;
361 uint16_t bw, bh;
362 gfx->setTextSize(1);
363 gfx->getTextBounds(name, 0, 0, &bx, &by, &bw, &bh);
364 gfx->setCursor((width - bw) / 2, NAME_Y);
366
367 render::drawFooterBar(gfx, width, height, nullptr, getFooterHint(), true);
368
369 dirty_ = false;
370}
371
372} // namespace cdc::ui
static constexpr uint32_t REPEAT_PERIOD_MS
static constexpr uint32_t REPEAT_INITIAL_MS
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
static constexpr int NAME_Y
static constexpr int TEXT_Y
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
CDC Log: logging over TinyUSB CDC and UART.
void onTick(uint32_t nowMs) override
void currentColor(uint8_t &r, uint8_t &g, uint8_t &b) const
Read the currently chosen color (post brightness scaling).
void init(uint8_t r, uint8_t g, uint8_t b)
Initialize the picker with a starting color.
void render(bool partial) override
const char * getFooterHint() const override
InputResult onKey(char key) override
const char * customFooter_
Definition IView.h:203
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
IKeypad * getKeypadInstance()
Returns the singleton keypad service instance.
constexpr int FOOTER_HEIGHT
Footer bar height in pixels (used by drawFooterBar).
void drawFooterBar(Gdey029T94 *gfx, uint16_t width, uint16_t height, const char *prefix, const char *hint, bool force=false)
Draws footer bar with optional prefix and hint text.
void printText(Gdey029T94 *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
static constexpr int TEXT_Y
Gdey029T94 * display
InputResult
Definition IView.h:10
static constexpr char KEY_NO
Cancel / Back / Backspace.
Definition KeyCodes.h:44
static constexpr char KEY_YES
Confirm / OK / Save.
Definition KeyCodes.h:41