| 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 |