GCC Code Coverage Report


Directory: ./
File: overlays/TextColorizer.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 273 305 89.5%
Functions: 22 22 100.0%
Branches: 216 306 70.6%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2025 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include <algorithm>
16 #include <cmath>
17
18 #include "TextColorizer.h"
19
20 namespace TextColor {
21
22 namespace {
23
24 /** Phase is quantised to this many steps before seeding the sparkle noise, so
25 * flecks hold for a frame or two instead of boiling at the repaint rate. */
26 constexpr uint32_t SPARKLE_STEPS = 12;
27 /** Coverage above which a sparkle pixel is blown out to white. */
28 constexpr double SPARKLE_WHITE_AT = 0.93;
29 /** Dimmest a non-flecked sparkle pixel gets, as a fraction of its colour. */
30 constexpr double SPARKLE_FLOOR = 0.35;
31 /** How far a random-per-letter mode re-rolls its colours over one phase cycle. */
32 constexpr uint32_t RANDOM_STEPS = 8;
33 /** Width of the Chase highlight as a fraction of the text, as a gaussian sigma. */
34 constexpr double CHASE_SIGMA = 0.07;
35 /** Brightness of the text outside the Chase highlight. */
36 constexpr double CHASE_FLOOR = 0.2;
37
38 75407 inline double fract(double v) {
39 75407 return v - std::floor(v);
40 }
41
42 12080 inline uint32_t hash32(uint32_t a, uint32_t b, uint32_t c) {
43 12080 uint32_t h = a * 0x9E3779B1u;
44 12080 h ^= b + 0x85EBCA6Bu + (h << 6) + (h >> 2);
45 12080 h ^= c + 0xC2B2AE35u + (h << 6) + (h >> 2);
46 12080 h ^= h >> 16;
47 12080 h *= 0x7FEB352Du;
48 12080 h ^= h >> 15;
49 12080 return h;
50 }
51
52 /** hash32 folded down to [0, 1). */
53 11900 inline double hashUnit(uint32_t a, uint32_t b, uint32_t c) {
54 11900 return (double)(hash32(a, b, c) & 0xFFFFFF) / (double)0x1000000;
55 }
56
57 105977 inline uint32_t packRGB(int r, int g, int b) {
58
1/2
✓ Branch 0 taken 105977 times.
✗ Branch 1 not taken.
105977 return ((uint32_t)std::clamp(r, 0, 255) << 16) |
59
1/2
✓ Branch 0 taken 105977 times.
✗ Branch 1 not taken.
105977 ((uint32_t)std::clamp(g, 0, 255) << 8) |
60
1/2
✓ Branch 0 taken 105977 times.
✗ Branch 1 not taken.
105977 (uint32_t)std::clamp(b, 0, 255);
61 }
62
63 /** Full-saturation, full-value hue sweep. h is wrapped into [0, 1). */
64 140 uint32_t hueColor(double h) {
65 140 h = fract(h) * 6.0;
66 140 int sector = (int)h;
67 140 double f = h - sector;
68 140 int v = 255;
69 140 int p = 0;
70 140 int q = (int)std::lround(255.0 * (1.0 - f));
71 140 int t = (int)std::lround(255.0 * f);
72
6/6
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 31 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 23 times.
✓ Branch 4 taken 22 times.
✓ Branch 5 taken 15 times.
140 switch (sector) {
73 35 case 0:
74 35 return packRGB(v, t, p);
75 31 case 1:
76 31 return packRGB(q, v, p);
77 14 case 2:
78 14 return packRGB(p, v, t);
79 23 case 3:
80 23 return packRGB(p, q, v);
81 22 case 4:
82 22 return packRGB(t, p, v);
83 15 default:
84 15 return packRGB(v, p, q);
85 }
86 }
87
88 75643 uint32_t lerpColor(uint32_t a, uint32_t b, double f) {
89
1/2
✓ Branch 0 taken 75643 times.
✗ Branch 1 not taken.
75643 f = std::clamp(f, 0.0, 1.0);
90 75643 int ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF;
91 75643 int br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF;
92 151286 return packRGB((int)std::lround(ar + (br - ar) * f),
93 75643 (int)std::lround(ag + (bg - ag) * f),
94 75643 (int)std::lround(ab + (bb - ab) * f));
95 }
96
97 /**
98 * Sample the palette as a continuous ramp.
99 *
100 * `cyclic` places the stops at i/N with a wrap from the last back to the
101 * first, which is what an animated ramp needs to avoid a hard seam sliding
102 * through the text once per cycle. A still ramp instead places them at i/(N-1)
103 * so a two-colour gradient reads exactly as asked: first colour at one end,
104 * second at the other.
105 */
106 77480 uint32_t rampColor(const Palette& pal, double t, bool cyclic) {
107
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 77340 times.
77480 if (pal.rainbow) {
108 140 return hueColor(t);
109 }
110 77340 size_t n = pal.colors.size();
111
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 77340 times.
77340 if (n == 0) {
112 ✗ return 0xFFFFFF;
113 }
114
2/2
✓ Branch 0 taken 140 times.
✓ Branch 1 taken 77200 times.
77340 if (n == 1) {
115 140 return pal.colors[0];
116 }
117
2/2
✓ Branch 0 taken 55715 times.
✓ Branch 1 taken 21485 times.
77200 if (cyclic) {
118 55715 double pos = fract(t) * (double)n;
119 55715 size_t i = (size_t)pos;
120
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 55715 times.
55715 if (i >= n) {
121 ✗ i = n - 1;
122 }
123 55715 return lerpColor(pal.colors[i], pal.colors[(i + 1) % n], pos - (double)i);
124 }
125
1/2
✓ Branch 0 taken 21485 times.
✗ Branch 1 not taken.
21485 double pos = std::clamp(t, 0.0, 1.0) * (double)(n - 1);
126 21485 size_t i = (size_t)pos;
127
2/2
✓ Branch 0 taken 1557 times.
✓ Branch 1 taken 19928 times.
21485 if (i >= n - 1) {
128 1557 return pal.colors[n - 1];
129 }
130 19928 return lerpColor(pal.colors[i], pal.colors[i + 1], pos - (double)i);
131 }
132
133 /**
134 * Pick a flat colour for span `index` of `count`. A rainbow palette spreads
135 * evenly over the spans so e.g. per-letter text reads as one clean sweep
136 * rather than an arbitrary slice of the hue circle; a listed palette simply
137 * cycles, advancing by `count` positions over a full phase cycle so animation
138 * chases the colours along the text.
139 */
140 665 uint32_t spanColor(const Palette& pal, int index, int count, double phase) {
141
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 665 times.
665 if (index < 0) {
142 ✗ index = 0;
143 }
144
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 665 times.
665 if (count < 1) {
145 ✗ count = 1;
146 }
147
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 665 times.
665 if (pal.rainbow) {
148 ✗ return hueColor((double)index / (double)count + phase);
149 }
150 665 size_t n = pal.colors.size();
151
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 665 times.
665 if (n == 0) {
152 ✗ return 0xFFFFFF;
153 }
154 665 uint32_t shift = (uint32_t)(phase * (double)n);
155 665 return pal.colors[((uint32_t)index + shift) % n];
156 }
157
158 /** As spanColor, but the assignment is scrambled rather than sequential. */
159 180 uint32_t randomSpanColor(const Palette& pal, int index, double phase, uint32_t seed) {
160
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 180 times.
180 if (index < 0) {
161 ✗ index = 0;
162 }
163 180 uint32_t bucket = (uint32_t)(phase * (double)RANDOM_STEPS);
164 180 uint32_t h = hash32(seed, (uint32_t)index, bucket);
165
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 180 times.
180 if (pal.rainbow) {
166 ✗ return hueColor((double)(h & 0xFFFFFF) / (double)0x1000000);
167 }
168 180 size_t n = pal.colors.size();
169
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 180 times.
180 if (n == 0) {
170 ✗ return 0xFFFFFF;
171 }
172 180 return pal.colors[h % n];
173 }
174
175 30194 uint32_t scaleColor(uint32_t c, double f) {
176
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 30194 times.
30194 if (f >= 1.0) {
177 ✗ return c;
178 }
179
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 30194 times.
30194 if (f <= 0.0) {
180 ✗ return 0;
181 }
182 60388 return packRGB((int)std::lround(((c >> 16) & 0xFF) * f),
183 30194 (int)std::lround(((c >> 8) & 0xFF) * f),
184 30194 (int)std::lround((c & 0xFF) * f));
185 }
186
187 } // namespace
188
189 4692 const std::vector<std::string>& ModeNames() {
190 static const std::vector<std::string> NAMES = {
191 "Single",
192 "Gradient Horizontal",
193 "Gradient Vertical",
194 "Gradient Diagonal",
195 "Per Letter",
196 "Per Word",
197 "Per Line",
198 "Random Letter",
199 "Random Word",
200 "Sparkle",
201 "Chase"
202
4/8
✓ Branch 0 taken 2346 times.
✓ Branch 1 taken 2346 times.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
9384 };
203 4692 return NAMES;
204 }
205
206 240 Mode ParseMode(const std::string& name) {
207
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 225 times.
240 if (name == "Gradient Horizontal") {
208 15 return Mode::GradientHorizontal;
209
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 223 times.
225 } else if (name == "Gradient Vertical") {
210 2 return Mode::GradientVertical;
211
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 222 times.
223 } else if (name == "Gradient Diagonal") {
212 1 return Mode::GradientDiagonal;
213
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 220 times.
222 } else if (name == "Per Letter") {
214 2 return Mode::PerLetter;
215
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 219 times.
220 } else if (name == "Per Word") {
216 1 return Mode::PerWord;
217
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 218 times.
219 } else if (name == "Per Line") {
218 1 return Mode::PerLine;
219
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 217 times.
218 } else if (name == "Random Letter") {
220 1 return Mode::RandomLetter;
221
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 216 times.
217 } else if (name == "Random Word") {
222 1 return Mode::RandomWord;
223
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 215 times.
216 } else if (name == "Sparkle") {
224 1 return Mode::Sparkle;
225
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 212 times.
215 } else if (name == "Chase") {
226 3 return Mode::Chase;
227 }
228 212 return Mode::Single;
229 }
230
231 400 bool NeedsLetterSpans(Mode m) {
232
4/4
✓ Branch 0 taken 396 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 394 times.
400 return m == Mode::PerLetter || m == Mode::RandomLetter;
233 }
234 421 bool NeedsWordSpans(Mode m) {
235
4/4
✓ Branch 0 taken 416 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 410 times.
421 return m == Mode::PerWord || m == Mode::RandomWord;
236 }
237 396 bool NeedsLineSpans(Mode m) {
238 396 return m == Mode::PerLine;
239 }
240 480 bool IsAnimated(Mode m) {
241 480 return m != Mode::Single;
242 }
243
244 2346 const std::vector<std::string>& PaletteNames() {
245 static const std::vector<std::string> NAMES = {
246 "Custom",
247 "Rainbow",
248 "Christmas",
249 "Halloween",
250 "Winter",
251 "Fire",
252 "Ocean",
253 "Forest",
254 "Patriotic",
255 "Pastel",
256 "Party"
257
3/8
✓ Branch 0 taken 2346 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2346 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2346 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
7038 };
258 2346 return NAMES;
259 }
260
261 28 Palette BuildPalette(const std::string& name, const std::vector<uint32_t>& custom) {
262 28 Palette p;
263
3/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 27 times.
28 if (name == "Rainbow") {
264 1 p.rainbow = true;
265 1 return p;
266 }
267
3/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 26 times.
27 if (name == "Christmas") {
268
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFF0000, 0x00FF00, 0xFFFFFF };
269
3/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 25 times.
26 } else if (name == "Halloween") {
270
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFF6000, 0x8000FF, 0x00FF00 };
271
3/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 24 times.
25 } else if (name == "Winter") {
272
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFFFFFF, 0x80D0FF, 0x0040FF };
273
3/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 23 times.
24 } else if (name == "Fire") {
274
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFF0000, 0xFF6000, 0xFFC000, 0xFFFF80 };
275
3/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 22 times.
23 } else if (name == "Ocean") {
276
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0x0020FF, 0x00A0FF, 0x00FFC0 };
277
3/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 21 times.
22 } else if (name == "Forest") {
278
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0x004000, 0x00A000, 0x80FF00 };
279
3/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 20 times.
21 } else if (name == "Patriotic") {
280
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFF0000, 0xFFFFFF, 0x0000FF };
281
3/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 19 times.
20 } else if (name == "Pastel") {
282
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFFB0C0, 0xB0E0FF, 0xFFF0A0, 0xB0FFC0 };
283
3/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 18 times.
19 } else if (name == "Party") {
284
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 p.colors = { 0xFF00A0, 0x00FFFF, 0xFFFF00, 0x80FF00, 0xFF4000 };
285 } else {
286 // "Custom" and anything unrecognised: the caller's own colours.
287
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 p.colors = custom;
288 }
289
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (p.colors.empty()) {
290 ✗ p.colors.push_back(0xFFFFFF);
291 }
292 27 return p;
293 ✗ }
294
295 28 bool InkBounds(const uint8_t* mask, int w, int h, Layout& out) {
296
3/6
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 28 times.
28 if (!mask || w <= 0 || h <= 0) {
297 ✗ return false;
298 }
299 28 int minX = w, minY = h, maxX = -1, maxY = -1;
300
2/2
✓ Branch 0 taken 656 times.
✓ Branch 1 taken 28 times.
684 for (int y = 0; y < h; ++y) {
301 656 const uint8_t* row = mask + (size_t)y * w;
302
2/2
✓ Branch 0 taken 43952 times.
✓ Branch 1 taken 656 times.
44608 for (int x = 0; x < w; ++x) {
303
2/2
✓ Branch 0 taken 4335 times.
✓ Branch 1 taken 39617 times.
43952 if (row[x]) {
304
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 4307 times.
4335 if (x < minX) {
305 28 minX = x;
306 }
307
2/2
✓ Branch 0 taken 423 times.
✓ Branch 1 taken 3912 times.
4335 if (x > maxX) {
308 423 maxX = x;
309 }
310
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 4307 times.
4335 if (y < minY) {
311 28 minY = y;
312 }
313 4335 maxY = y;
314 }
315 }
316 }
317
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 28 times.
28 if (maxX < 0) {
318 ✗ return false;
319 }
320 28 out.x = minX;
321 28 out.y = minY;
322 28 out.w = maxX - minX + 1;
323 28 out.h = maxY - minY + 1;
324 28 return true;
325 }
326
327 6 std::vector<std::pair<int, int>> InkRowRuns(const uint8_t* mask, int w, int h) {
328 6 std::vector<std::pair<int, int>> runs;
329
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
6 if (!mask || w <= 0 || h <= 0) {
330 ✗ return runs;
331 }
332 6 int start = -1;
333
2/2
✓ Branch 0 taken 144 times.
✓ Branch 1 taken 6 times.
150 for (int y = 0; y < h; ++y) {
334 144 const uint8_t* row = mask + (size_t)y * w;
335 144 bool lit = false;
336
2/2
✓ Branch 0 taken 6656 times.
✓ Branch 1 taken 89 times.
6745 for (int x = 0; x < w; ++x) {
337
2/2
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 6601 times.
6656 if (row[x]) {
338 55 lit = true;
339 55 break;
340 }
341 }
342
4/4
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 89 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 48 times.
144 if (lit && start < 0) {
343 7 start = y;
344
4/4
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 48 times.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 82 times.
137 } else if (!lit && start >= 0) {
345
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 runs.emplace_back(start, y - 1);
346 7 start = -1;
347 }
348 }
349
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (start >= 0) {
350 ✗ runs.emplace_back(start, h - 1);
351 }
352 6 return runs;
353 ✗ }
354
355 5 bool InkColumnExtent(const uint8_t* mask, int w, int h, int y0, int y1, int& x0, int& x1) {
356
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
5 if (!mask || w <= 0 || h <= 0) {
357 ✗ return false;
358 }
359 5 y0 = std::max(0, y0);
360 5 y1 = std::min(h, y1);
361 5 int minX = w;
362 5 int maxX = -1;
363
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 5 times.
46 for (int y = y0; y < y1; ++y) {
364 41 const uint8_t* row = mask + (size_t)y * w;
365
2/2
✓ Branch 0 taken 3034 times.
✓ Branch 1 taken 41 times.
3075 for (int x = 0; x < w; ++x) {
366
2/2
✓ Branch 0 taken 773 times.
✓ Branch 1 taken 2261 times.
3034 if (row[x]) {
367
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 768 times.
773 if (x < minX) {
368 5 minX = x;
369 }
370
2/2
✓ Branch 0 taken 89 times.
✓ Branch 1 taken 684 times.
773 if (x > maxX) {
371 89 maxX = x;
372 }
373 }
374 }
375 }
376
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (maxX < 0) {
377 ✗ return false;
378 }
379 5 x0 = minX;
380 5 x1 = maxX;
381 5 return true;
382 }
383
384 372 void Colorize(const uint8_t* mask, int w, int h, const Layout& layout,
385 Mode mode, const Palette& pal, double phase, bool cyclic,
386 uint32_t seed, uint8_t* rgbOut) {
387
4/8
✓ Branch 0 taken 372 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 372 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 372 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 372 times.
372 if (!mask || !rgbOut || w <= 0 || h <= 0) {
388 ✗ return;
389 }
390 372 const double ph = fract(phase);
391
392 // A span mode whose geometry could not be measured (no ink, or a font that
393 // would not report metrics) degrades to a horizontal gradient rather than
394 // silently flattening to one colour -- the palette the user picked still
395 // shows, which is a much better clue that something rendered than a block
396 // of Colour 1 would be.
397
9/12
✓ Branch 0 taken 372 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 369 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 369 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 367 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 367 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 366 times.
372 bool spanMode = NeedsLetterSpans(mode) || NeedsWordSpans(mode) || NeedsLineSpans(mode);
398
4/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 366 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 372 times.
372 if (spanMode && layout.spans.empty()) {
399 ✗ mode = Mode::GradientHorizontal;
400 ✗ spanMode = false;
401 }
402
4/4
✓ Branch 0 taken 371 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 370 times.
372 const bool randomSpans = (mode == Mode::RandomLetter || mode == Mode::RandomWord);
403
404 372 std::vector<int32_t> spanIndex;
405
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 366 times.
372 if (spanMode) {
406
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 spanIndex.assign((size_t)w * h, -1);
407
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 6 times.
35 for (const auto& s : layout.spans) {
408 29 int sy0 = std::max(0, s.y0);
409 29 int sy1 = std::min(h, s.y1);
410 29 int sx0 = std::max(0, s.x0);
411 29 int sx1 = std::min(w, s.x1);
412
2/2
✓ Branch 0 taken 245 times.
✓ Branch 1 taken 29 times.
274 for (int y = sy0; y < sy1; ++y) {
413 245 int32_t* row = spanIndex.data() + (size_t)y * w;
414
2/2
✓ Branch 0 taken 2595 times.
✓ Branch 1 taken 245 times.
2840 for (int x = sx0; x < sx1; ++x) {
415 2595 row[x] = s.index;
416 }
417 }
418 }
419 }
420
421 372 const int lx = layout.x;
422 372 const int ly = layout.y;
423 // Normalised against the span BETWEEN the outermost lit pixels, not the
424 // pixel count, so the last column of ink lands on 1.0 and a two-colour
425 // gradient actually finishes on its second colour rather than stopping one
426 // pixel short of it.
427
1/2
✓ Branch 0 taken 372 times.
✗ Branch 1 not taken.
372 const double lw = layout.w > 1 ? (double)(layout.w - 1) : 1.0;
428
1/2
✓ Branch 0 taken 372 times.
✗ Branch 1 not taken.
372 const double lh = layout.h > 1 ? (double)(layout.h - 1) : 1.0;
429 372 const int spanCount = std::max(1, layout.spanCount);
430 372 const uint32_t sparkleFrame = (uint32_t)(ph * (double)SPARKLE_STEPS);
431
432 // Single is by far the most common case and has no per-pixel colour work at
433 // all, so it gets its own loop rather than paying the switch below.
434
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 372 times.
372 if (mode == Mode::Single) {
435 ✗ uint32_t c = pal.colors.empty() ? 0xFFFFFF : pal.colors[0];
436 ✗ int cr = (c >> 16) & 0xFF, cg = (c >> 8) & 0xFF, cb = c & 0xFF;
437 ✗ for (size_t i = 0, n = (size_t)w * h; i < n; ++i) {
438 ✗ uint32_t cov = mask[i];
439 ✗ uint8_t* o = rgbOut + i * 3;
440 ✗ o[0] = (uint8_t)((cr * cov) / 255);
441 ✗ o[1] = (uint8_t)((cg * cov) / 255);
442 ✗ o[2] = (uint8_t)((cb * cov) / 255);
443 }
444 ✗ return;
445 }
446
447
2/2
✓ Branch 0 taken 7904 times.
✓ Branch 1 taken 372 times.
8276 for (int y = 0; y < h; ++y) {
448
2/2
✓ Branch 0 taken 590480 times.
✓ Branch 1 taken 7904 times.
598384 for (int x = 0; x < w; ++x) {
449 590480 size_t i = (size_t)y * w + x;
450 590480 uint32_t cov = mask[i];
451 590480 uint8_t* o = rgbOut + i * 3;
452
2/2
✓ Branch 0 taken 512155 times.
✓ Branch 1 taken 78325 times.
590480 if (!cov) {
453 512155 o[0] = o[1] = o[2] = 0;
454 512155 continue;
455 }
456
457 78325 double tx = (double)(x - lx) / lw;
458 78325 double ty = (double)(y - ly) / lh;
459 78325 uint32_t c = 0xFFFFFF;
460 78325 double bright = 1.0;
461
462
6/6
✓ Branch 0 taken 45775 times.
✓ Branch 1 taken 485 times.
✓ Branch 2 taken 140 times.
✓ Branch 3 taken 11900 times.
✓ Branch 4 taken 19180 times.
✓ Branch 5 taken 845 times.
78325 switch (mode) {
463 45775 case Mode::GradientHorizontal:
464
1/2
✓ Branch 0 taken 45775 times.
✗ Branch 1 not taken.
45775 c = rampColor(pal, tx + ph, cyclic);
465 45775 break;
466 485 case Mode::GradientVertical:
467
1/2
✓ Branch 0 taken 485 times.
✗ Branch 1 not taken.
485 c = rampColor(pal, ty + ph, cyclic);
468 485 break;
469 140 case Mode::GradientDiagonal:
470
1/2
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
140 c = rampColor(pal, (tx + ty) * 0.5 + ph, cyclic);
471 140 break;
472 11900 case Mode::Sparkle: {
473
1/2
✓ Branch 0 taken 11900 times.
✗ Branch 1 not taken.
11900 c = rampColor(pal, tx + ph, cyclic);
474 11900 double u = hashUnit(seed, (uint32_t)i, sparkleFrame);
475
2/2
✓ Branch 0 taken 826 times.
✓ Branch 1 taken 11074 times.
11900 if (u >= SPARKLE_WHITE_AT) {
476 826 c = 0xFFFFFF;
477 } else {
478 11074 bright = SPARKLE_FLOOR + (1.0 - SPARKLE_FLOOR) * (u / SPARKLE_WHITE_AT) * 0.6;
479 }
480 11900 break;
481 }
482 19180 case Mode::Chase: {
483
1/2
✓ Branch 0 taken 19180 times.
✗ Branch 1 not taken.
19180 c = rampColor(pal, tx, false);
484 19180 double d = fract(tx - ph);
485 19180 double dist = std::min(d, 1.0 - d);
486 19180 bright = CHASE_FLOOR + (1.0 - CHASE_FLOOR) *
487 19180 std::exp(-(dist * dist) / (2.0 * CHASE_SIGMA * CHASE_SIGMA));
488 19180 break;
489 }
490 845 default: {
491
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 845 times.
845 int32_t idx = spanIndex.empty() ? 0 : spanIndex[i];
492
3/4
✓ Branch 0 taken 180 times.
✓ Branch 1 taken 665 times.
✓ Branch 2 taken 180 times.
✗ Branch 3 not taken.
845 c = randomSpans ? randomSpanColor(pal, idx, ph, seed)
493
1/2
✓ Branch 0 taken 665 times.
✗ Branch 1 not taken.
665 : spanColor(pal, idx, spanCount, ph);
494 845 break;
495 }
496 }
497
498
2/2
✓ Branch 0 taken 30194 times.
✓ Branch 1 taken 48131 times.
78325 if (bright < 1.0) {
499
1/2
✓ Branch 0 taken 30194 times.
✗ Branch 1 not taken.
30194 c = scaleColor(c, bright);
500 }
501 78325 o[0] = (uint8_t)((((c >> 16) & 0xFF) * cov) / 255);
502 78325 o[1] = (uint8_t)((((c >> 8) & 0xFF) * cov) / 255);
503 78325 o[2] = (uint8_t)(((c & 0xFF) * cov) / 255);
504 }
505 }
506
1/2
✓ Branch 0 taken 372 times.
✗ Branch 1 not taken.
372 }
507
508 } // namespace TextColor
509