Loading...
Searching...
No Matches
Paint.hpp
1#pragma once
2#include <score/model/Skin.hpp>
3
4#include <QColor>
5#include <QImage>
6#include <QLinearGradient>
7#include <QPainter>
8#include <QString>
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <cstdint>
14#include <vector>
15
20namespace Ui::Monitor
21{
22struct Tick
23{
24 double value{};
25 QString label;
26 bool major{true};
27};
28
29inline QColor background()
30{
31 return QColor(18, 18, 20);
32}
33inline QColor gridColor()
34{
35 return QColor(255, 255, 255, 28);
36}
37inline QColor majorGridColor()
38{
39 return QColor(255, 255, 255, 48);
40}
41inline QColor textColor()
42{
43 return QColor(170, 170, 175);
44}
45inline QColor traceColor()
46{
47 return score::Skin::instance().Light.main.brush.color();
48}
49inline QColor warnColor()
50{
51 return QColor(255, 196, 0);
52}
53inline QColor overColor()
54{
55 return QColor(235, 45, 40);
56}
57
58inline const QFont& smallFont()
59{
60 static const QFont font = [] {
61 QFont f = score::Skin::instance().SansFontSmall;
62 f.setPixelSize(9);
63 return f;
64 }();
65 return font;
66}
67
69inline QColor channelColor(int i)
70{
71 static constexpr std::array<QRgb, 8> c{
72 0xff5ec8f2, 0xfff2a65e, 0xff8be07a, 0xffe76f8f,
73 0xffb69cf5, 0xfff2e35e, 0xff5ef2c4, 0xffd0d0d0};
74 return QColor::fromRgba(c[std::size_t(i) % c.size()]);
75}
76
79inline QColor zoneColor(double v, double warn, double over)
80{
81 if(v >= over)
82 return QColor(235, 45, 40);
83 if(v >= warn)
84 {
85 const double t = std::clamp((v - warn) / std::max(over - warn, 1e-9), 0., 1.);
86 return QColor(int(200 + 50 * t), int(205 - 40 * t), 40);
87 }
88 return QColor(76, 175, 80);
89}
90
95inline QLinearGradient
96zoneGradient(double y_lo, double y_hi, double lo, double hi, double warn, double over, double alpha = 1.)
97{
98 QLinearGradient g{QPointF{0., y_lo}, QPointF{0., y_hi}};
99 const double span = std::max(hi - lo, 1e-9);
100 auto stop = [&](double v, QColor c) {
101 c.setAlphaF(alpha);
102 g.setColorAt(std::clamp((v - lo) / span, 0., 1.), c);
103 };
104 stop(lo, zoneColor(lo, warn, over));
105 // Each zone edge inside the range: the colour just under it, then over it
106 const double eps = 1e-6 * span;
107 for(double edge : {warn, over})
108 if(edge - eps > lo && edge < hi)
109 {
110 stop(edge - eps, zoneColor(edge - eps, warn, over));
111 stop(edge, zoneColor(edge, warn, over));
112 }
113 stop(hi, zoneColor(hi, warn, over));
114 return g;
115}
116
121enum class ColorMap
122{
123 Inferno,
124 Viridis,
125 Gray,
126 Magma,
127 Turbo,
130};
131
132inline const std::array<QRgb, 256>& colorMap(ColorMap m)
133{
134 auto build = [](std::initializer_list<std::array<int, 3>> anchors) {
135 std::array<QRgb, 256> lut{};
136 const std::vector<std::array<int, 3>> a(anchors);
137 const int n = int(a.size()) - 1;
138 for(int i = 0; i < 256; i++)
139 {
140 const double t = i / 255. * n;
141 const int k = std::min(int(t), n - 1);
142 const double f = t - k;
143 auto mix = [&](int c) { return int(std::lround(a[k][c] * (1 - f) + a[k + 1][c] * f)); };
144 lut[i] = qRgb(mix(0), mix(1), mix(2));
145 }
146 return lut;
147 };
148 static const auto inferno = build(
149 {{0, 0, 4},
150 {40, 11, 84},
151 {101, 21, 110},
152 {159, 42, 99},
153 {212, 72, 66},
154 {245, 125, 21},
155 {250, 193, 39},
156 {252, 255, 164}});
157 static const auto viridis = build(
158 {{68, 1, 84},
159 {70, 50, 127},
160 {54, 92, 141},
161 {39, 127, 142},
162 {31, 161, 135},
163 {74, 194, 109},
164 {159, 218, 58},
165 {253, 231, 37}});
166 static const auto gray = build({{0, 0, 0}, {255, 255, 255}});
167 static const auto magma = build(
168 {{0, 0, 4},
169 {28, 16, 68},
170 {79, 18, 123},
171 {129, 37, 129},
172 {181, 54, 122},
173 {229, 80, 100},
174 {251, 135, 97},
175 {254, 194, 135},
176 {252, 253, 191}});
177 static const auto turbo = build(
178 {{48, 18, 59},
179 {70, 107, 227},
180 {41, 187, 236},
181 {49, 242, 153},
182 {163, 253, 61},
183 {237, 208, 58},
184 {251, 128, 34},
185 {212, 50, 8},
186 {122, 4, 3}});
187 static const auto coolwarm
188 = build({{59, 76, 192}, {141, 176, 254}, {221, 221, 221}, {244, 154, 123}, {180, 4, 38}});
189 switch(m)
190 {
191 case ColorMap::Viridis:
192 return viridis;
193 case ColorMap::Gray:
194 return gray;
195 case ColorMap::Magma:
196 return magma;
197 case ColorMap::Turbo:
198 return turbo;
200 return coolwarm;
201 default:
202 return inferno;
203 }
204}
205
207inline QString formatHz(double hz)
208{
209 if(hz >= 1000.)
210 {
211 const double k = hz / 1000.;
212 return QString::number(k, 'g', k < 10 ? 2 : 3) + "k";
213 }
214 return QString::number(hz, 'g', 3);
215}
216
221inline std::vector<Tick>
222linearTicks(double lo, double hi, double pixels, double min_spacing = 18., QString unit = {})
223{
224 std::vector<Tick> ticks;
225 if(!(hi > lo) || pixels <= 0)
226 return ticks;
227 const double span = hi - lo;
228 const double raw = span * min_spacing / pixels;
229 const double mag = std::pow(10., std::floor(std::log10(raw)));
230 double step = mag;
231 for(double m : {1., 2., 2.5, 5., 10.})
232 {
233 step = m * mag;
234 if(step >= raw)
235 break;
236 }
237 const double first = std::ceil(lo / step) * step;
238 for(double v = first; v <= hi + step * 1e-6; v += step)
239 {
240 const double r = std::abs(v) < step * 1e-6 ? 0. : v;
241 ticks.push_back({r, QString::number(r, 'g', 4) + unit, true});
242 }
243 return ticks;
244}
245
247inline std::vector<Tick> frequencyTicks(double lo, double hi, double pixels)
248{
249 std::vector<Tick> ticks;
250 if(lo <= 0 || hi <= lo)
251 return ticks;
252 const double decades = std::log10(hi / lo);
253 const bool dense = pixels / std::max(decades, 1e-3) > 90.;
254 for(double dec = std::pow(10., std::floor(std::log10(lo))); dec <= hi; dec *= 10.)
255 {
256 for(double m : {1., 2., 5.})
257 {
258 const double f = dec * m;
259 if(f < lo || f > hi)
260 continue;
261 const bool major = m == 1.;
262 if(!major && !dense && m == 5.)
263 continue;
264 ticks.push_back({f, formatHz(f), major});
265 }
266 }
267 return ticks;
268}
269
276{
278 static constexpr int levels = 1024;
279
280 int width{}, height{};
281 std::vector<float> acc;
282 QImage image;
284 std::array<uint8_t, levels + 1> gamma_lut{};
285 float lut_gamma{-1.f};
286
287 void resize(int w, int h)
288 {
289 w = std::max(w, 1);
290 h = std::max(h, 1);
291 if(w == width && h == height)
292 return;
293 width = w;
294 height = h;
295 acc.assign(std::size_t(w) * h, 0.f);
296 image = QImage(w, h, QImage::Format_ARGB32_Premultiplied);
297 image.fill(Qt::transparent);
298 }
299
300 void clear() { std::fill(acc.begin(), acc.end(), 0.f); }
301
302 void decay(float factor)
303 {
304 for(auto& v : acc)
305 v *= factor;
306 }
307
308 void hit(double x, double y, float energy = 1.f)
309 {
310 const int ix = int(x), iy = int(y);
311 if(ix < 0 || iy < 0 || ix >= width || iy >= height)
312 return;
313 acc[std::size_t(iy) * width + ix] += energy;
314 }
315
318 void line(double x0, double y0, double x1, double y1, float energy = 1.f)
319 {
320 const double dx = x1 - x0, dy = y1 - y0;
321 const int n = std::max(1, int(std::ceil(std::max(std::abs(dx), std::abs(dy)))));
322 const float e = energy / n;
323 for(int i = 0; i <= n; i++)
324 hit(x0 + dx * i / n, y0 + dy * i / n, e);
325 }
326
327 void buildGamma(float gamma)
328 {
329 if(gamma == lut_gamma)
330 return;
331 lut_gamma = gamma;
332 for(int i = 0; i <= levels; i++)
333 gamma_lut[i] = uint8_t(std::lround(std::pow(float(i) / levels, gamma) * 255.f));
334 }
335
337 template <typename F>
338 void map(float full, float gamma, F&& f)
339 {
340 buildGamma(gamma);
341 const float k = levels / std::max(full, 1e-9f);
342 for(int y = 0; y < height; y++)
343 {
344 auto* line = reinterpret_cast<QRgb*>(image.scanLine(y));
345 const float* a = acc.data() + std::size_t(y) * width;
346 for(int x = 0; x < width; x++)
347 line[x] = f(gamma_lut[std::size_t(std::min(a[x] * k, float(levels)))]);
348 }
349 }
350
352 void render(QColor color, float full, float gamma = 0.5f)
353 {
354 std::array<QRgb, 256> shade;
355 for(int i = 0; i < 256; i++)
356 shade[i] = qPremultiply(qRgba(color.red(), color.green(), color.blue(), i));
357 map(full, gamma, [&](uint8_t v) { return shade[v]; });
358 }
359
361 void render(ColorMap cmap, float full, float gamma = 0.5f)
362 {
363 const auto& lut = colorMap(cmap);
364 map(full, gamma, [&](uint8_t v) { return lut[v]; });
365 }
366};
367
369inline void
370drawLabel(QPainter& p, const QRectF& r, int flags, const QString& text, QColor c = textColor())
371{
372 p.setPen(c);
373 p.setFont(smallFont());
374 // A single line never runs out of its box: what does not fit is left out
375 if(!text.contains(QLatin1Char('\n'))
376 && QFontMetricsF{smallFont()}.horizontalAdvance(text) > r.width() + 0.5)
377 return;
378 p.drawText(r, flags, text);
379}
380
387template <typename XOf>
388void drawXAxisLabels(QPainter& p, const QRectF& plot, const std::vector<Tick>& ticks, XOf x_of)
389{
390 constexpr double gap = 6.;
391 const QFontMetricsF fm{smallFont()};
392 std::vector<std::pair<double, double>> taken;
393 auto place = [&](const Tick& t) {
394 const double w = fm.horizontalAdvance(t.label);
395 if(w > plot.width())
396 return;
397 const double x0 = std::clamp(x_of(t.value) - w / 2., plot.left(), plot.right() - w);
398 for(auto [a, b] : taken)
399 if(x0 < b + gap && x0 + w > a - gap)
400 return;
401 taken.emplace_back(x0, x0 + w);
402 drawLabel(p, QRectF(x0, plot.bottom() + 1, w + 1, 11), Qt::AlignLeft | Qt::AlignTop, t.label);
403 };
404 for(const auto& t : ticks)
405 if(t.major)
406 place(t);
407 for(const auto& t : ticks)
408 if(!t.major)
409 place(t);
410}
411
417template <typename YOf>
418void drawYAxisLabels(QPainter& p, const QRectF& plot, const std::vector<Tick>& ticks, YOf y_of)
419{
420 constexpr double h = 10., gap = 3.;
421 std::vector<double> taken;
422 auto place = [&](const Tick& t) {
423 const double y0 = std::clamp(y_of(t.value) - h / 2., plot.top() - h / 2., plot.bottom() - h);
424 for(double o : taken)
425 if(std::abs(o - y0) < h + gap)
426 return;
427 taken.push_back(y0);
428 drawLabel(
429 p, QRectF(0, y0, plot.left() - 3, h), Qt::AlignRight | Qt::AlignVCenter, t.label);
430 };
431 for(const auto& t : ticks)
432 if(t.major)
433 place(t);
434 for(const auto& t : ticks)
435 if(!t.major)
436 place(t);
437}
438
440inline void drawReadout(QPainter& p, QPointF topLeft, const QStringList& lines)
441{
442 if(lines.isEmpty())
443 return;
444 p.setFont(smallFont());
445 const QFontMetricsF fm{p.font()};
446 double w = 0;
447 for(auto& l : lines)
448 w = std::max(w, fm.horizontalAdvance(l));
449 const double lh = fm.height();
450 QRectF box{topLeft, QSizeF{w + 6, lh * lines.size() + 4}};
451 p.fillRect(box, QColor(0, 0, 0, 150));
452 p.setPen(textColor());
453 for(int i = 0; i < lines.size(); i++)
454 p.drawText(QPointF{box.left() + 3, box.top() + 2 + fm.ascent() + i * lh}, lines[i]);
455}
456}
Definition BufferDecode.hpp:16
QColor channelColor(int i)
Colours for the n-th channel or trace, distinct on a dark background.
Definition Paint.hpp:69
QString formatHz(double hz)
"1.5k", "20k", "440"
Definition Paint.hpp:207
ColorMap
Definition Paint.hpp:122
@ CoolWarm
Diverging: blue below the middle, red above.
std::vector< Tick > linearTicks(double lo, double hi, double pixels, double min_spacing=18., QString unit={})
Definition Paint.hpp:222
std::vector< Tick > frequencyTicks(double lo, double hi, double pixels)
Ticks of a log frequency axis: decades labelled, 2 and 5 in between.
Definition Paint.hpp:247
void drawXAxisLabels(QPainter &p, const QRectF &plot, const std::vector< Tick > &ticks, XOf x_of)
Definition Paint.hpp:388
void drawLabel(QPainter &p, const QRectF &r, int flags, const QString &text, QColor c=textColor())
Draws text in the small monitor font, flags as for QPainter::drawText.
Definition Paint.hpp:370
void drawReadout(QPainter &p, QPointF topLeft, const QStringList &lines)
A readout box in a corner: semi-opaque so that it stays legible over traces.
Definition Paint.hpp:440
void drawYAxisLabels(QPainter &p, const QRectF &plot, const std::vector< Tick > &ticks, YOf y_of)
Definition Paint.hpp:418
QLinearGradient zoneGradient(double y_lo, double y_hi, double lo, double hi, double warn, double over, double alpha=1.)
Definition Paint.hpp:96
QColor zoneColor(double v, double warn, double over)
Definition Paint.hpp:79
Definition Paint.hpp:276
static constexpr int levels
Resolution of the intensities the gamma table maps.
Definition Paint.hpp:278
void line(double x0, double y0, double x1, double y1, float energy=1.f)
Definition Paint.hpp:318
std::array< uint8_t, levels+1 > gamma_lut
Intensity level to 0-255 after the gamma, built when the gamma changes.
Definition Paint.hpp:284
void render(QColor color, float full, float gamma=0.5f)
Maps the accumulator to color at full intensity for full energy.
Definition Paint.hpp:352
void render(ColorMap cmap, float full, float gamma=0.5f)
Maps the accumulator through a colour map.
Definition Paint.hpp:361
void map(float full, float gamma, F &&f)
Calls f(row, x, level 0-255) for every pixel, full energy at 255.
Definition Paint.hpp:338
Definition Paint.hpp:23