mplusfonts/
color.rs

1//! Color math for bitmap fonts.
2//!
3//! When a bitmap font is created, its pixels are assigned one of four possible color types:
4//! [`BinaryColor`], [`Gray2`], [`Gray4`], or [`Gray8`]. This module provides the functions that
5//! enable downsampling, color conversion from any [`GrayColor`] to any other color type that a
6//! [`DrawTarget`](../../embedded_graphics_core/draw_target/trait.DrawTarget.html) expects,
7//! applying color settings, and mixing colors in [`Screen`] or [`WeightedAvg`] blend mode.
8
9use core::array;
10
11use embedded_graphics::pixelcolor::*;
12
13/// Array of colors having type `T`, for lookup-table-based color conversion.
14///
15/// The length of the array is equal to the number of gray values that can be converted.
16#[derive(Debug, Clone)]
17#[cfg_attr(feature = "defmt", derive(defmt::Format))]
18pub struct Colormap<T: Copy, const N: usize>([T; N]);
19
20/// Linear gradient definition.
21///
22/// A colormap that implements this trait can be created from two colors of type `T`, which become
23/// the first and the last element in the resulting array, while the colors in between are
24/// calculated using a linear equation.
25pub trait Linear<T> {
26    /// Returns a linear gradient with the specified start and end colors.
27    fn linear(start: T, end: T) -> Self;
28}
29
30/// Color inversion.
31///
32/// A color that implements this trait can be changed into its negative self.
33pub trait Invert {
34    /// Returns the inverse of the specified color.
35    fn invert(self) -> Self;
36}
37
38/// Screen blend mode.
39///
40/// A color that implements this trait can be mixed with another color of the same type,
41/// multiplying their inverse color components and inverting the resulting color, which may only
42/// shift a given color away from the start color and towards the end color.
43pub trait Screen {
44    /// Mixes the color with the specified other color. Two bright colors do not necessarily produce
45    /// an even brighter color; that is only the case when start is a darker color than end is.
46    fn screen(self, other: Self, start: Self, end: Self) -> Self;
47}
48
49/// Weighted arithmetic mean blend mode.
50///
51/// A color that implements this trait can be mixed with another color of the same type,
52/// calculating the component-wise weighted average of the two colors, where the weights are
53/// distributed based on how much more towards the respective end color one color is compared to
54/// the other color.
55pub trait WeightedAvg {
56    /// Mixes the color with the specified other color. Weights are not necessarily distributed as
57    /// 50–50 percent between the two colors; that is only the case when start and end are as far
58    /// away from the color as the other one is (from the other start and end — the ratio, that is).
59    fn weighted_avg(
60        self,
61        other: Self,
62        start: Self,
63        end: Self,
64        other_start: Self,
65        other_end: Self,
66    ) -> Self;
67}
68
69impl<T: Copy, const N: usize> Colormap<T, N> {
70    /// Returns the first element of the colormap.
71    pub const fn first(&self) -> T {
72        let Colormap(array) = self;
73
74        array[0]
75    }
76
77    /// Returns the last element of the colormap.
78    pub const fn last(&self) -> T {
79        let Colormap(array) = self;
80
81        array[N - 1]
82    }
83}
84
85macro_rules! impl_colormap {
86    (
87        $(
88            $color_ident:ident, $color_type:ty, $array_length:literal, $into_index:expr,
89        )*
90    ) => {
91        $(
92            impl<T: Copy> Colormap<T, $array_length> {
93                /// Returns the color that is mapped to the specified gray value.
94                pub fn get(&self, $color_ident: $color_type) -> T {
95                    let Colormap(array) = self;
96                    let index: usize = $into_index.into();
97
98                    array[index % $array_length]
99                }
100            }
101        )*
102    }
103}
104
105impl_colormap! {
106    color, BinaryColor, 2, color.is_on(),
107    color, Gray2, 4, color.luma(),
108    color, Gray4, 16, color.luma(),
109    color, Gray8, 256, color.luma(),
110}
111
112const fn convert_channel<const N: usize>(value: u8, start: u8, end: u8) -> u8 {
113    const SHIFT: usize = 23;
114    const CONST_0_5: i32 = 1 << (SHIFT - 1);
115
116    let diff = end as i32 - start as i32;
117    let a = (diff << SHIFT) / (N - 1) as i32;
118    let b = (start as i32) << SHIFT;
119    let result = a * value as i32 + b + CONST_0_5;
120
121    (result >> SHIFT) as u8
122}
123
124macro_rules! impl_linear_rgb {
125    ($($rgb_type:ty),+) => {
126        $(
127            impl<const N: usize> Linear<$rgb_type> for Colormap<$rgb_type, N> {
128                fn linear(start: $rgb_type, end: $rgb_type) -> Self {
129                    let colors = array::from_fn(|index| {
130                        let r = convert_channel::<N>(index as u8, start.r(), end.r());
131                        let g = convert_channel::<N>(index as u8, start.g(), end.g());
132                        let b = convert_channel::<N>(index as u8, start.b(), end.b());
133
134                        <$rgb_type>::new(r, g, b)
135                    });
136
137                    Self(colors)
138                }
139            }
140        )*
141    }
142}
143
144impl_linear_rgb!(
145    Rgb555, Bgr555, Rgb565, Bgr565, Rgb666, Bgr666, Rgb888, Bgr888
146);
147
148macro_rules! impl_linear_gray {
149    ($($gray_type:ty),+) => {
150        $(
151            impl<const N: usize> Linear<$gray_type> for Colormap<$gray_type, N> {
152                fn linear(start: $gray_type, end: $gray_type) -> Self {
153                    let colors = array::from_fn(|index| {
154                        let luma = convert_channel::<N>(index as u8, start.luma(), end.luma());
155
156                        <$gray_type>::new(luma)
157                    });
158
159                    Self(colors)
160                }
161            }
162        )*
163    }
164}
165
166impl_linear_gray!(Gray2, Gray4, Gray8);
167
168impl<const N: usize> Linear<BinaryColor> for Colormap<BinaryColor, N> {
169    fn linear(start: BinaryColor, end: BinaryColor) -> Self {
170        let colors = array::from_fn(|index| if index < N / 2 { start } else { end });
171
172        Self(colors)
173    }
174}
175
176macro_rules! impl_invert_rgb {
177    ($($rgb_type:ty),+) => {
178        $(
179            impl Invert for $rgb_type {
180                fn invert(self) -> Self {
181                    let r = <$rgb_type>::MAX_R - self.r();
182                    let g = <$rgb_type>::MAX_G - self.g();
183                    let b = <$rgb_type>::MAX_B - self.b();
184
185                    Self::new(r, g, b)
186                }
187            }
188        )*
189    }
190}
191
192impl_invert_rgb!(
193    Rgb555, Bgr555, Rgb565, Bgr565, Rgb666, Bgr666, Rgb888, Bgr888
194);
195
196impl Invert for Gray2 {
197    fn invert(self) -> Self {
198        Self::new(0b00000011 - self.luma())
199    }
200}
201
202impl Invert for Gray4 {
203    fn invert(self) -> Self {
204        Self::new(0b00001111 - self.luma())
205    }
206}
207
208impl Invert for Gray8 {
209    fn invert(self) -> Self {
210        Self::new(0b11111111 - self.luma())
211    }
212}
213
214impl Invert for BinaryColor {
215    fn invert(self) -> Self {
216        self.invert()
217    }
218}
219
220const fn screen_mix_channel(first: u8, second: u8, start: u8, end: u8) -> u8 {
221    const SHIFT: usize = 15;
222    const CONST_0_5: i32 = 1 << (SHIFT - 1);
223
224    if start == end {
225        return start;
226    }
227
228    let diff = end as i32 - start as i32;
229    let first = end as i32 - first as i32;
230    let second = end as i32 - second as i32;
231    let product = first * ((second << SHIFT) + CONST_0_5);
232    let minuend = ((end as i32) << SHIFT) + CONST_0_5;
233    let result = minuend - product / diff;
234
235    (result >> SHIFT) as u8
236}
237
238macro_rules! impl_screen_mix_rgb {
239    ($($rgb_type:ty),+) => {
240        $(
241            impl Screen for $rgb_type {
242                fn screen(self, other: Self, start: Self, end: Self) -> Self {
243                    let r = screen_mix_channel(self.r(), other.r(), start.r(), end.r());
244                    let g = screen_mix_channel(self.g(), other.g(), start.g(), end.g());
245                    let b = screen_mix_channel(self.b(), other.b(), start.b(), end.b());
246
247                    <$rgb_type>::new(r, g, b)
248                }
249            }
250        )*
251    }
252}
253
254impl_screen_mix_rgb!(
255    Rgb555, Bgr555, Rgb565, Bgr565, Rgb666, Bgr666, Rgb888, Bgr888
256);
257
258macro_rules! impl_screen_mix_gray {
259    ($($gray_type:ty),+) => {
260        $(
261            impl Screen for $gray_type {
262                fn screen(self, other: Self, start: Self, end: Self) -> Self {
263                    let luma = screen_mix_channel(
264                        self.luma(),
265                        other.luma(),
266                        start.luma(),
267                        end.luma());
268
269                    <$gray_type>::new(luma)
270                }
271            }
272        )*
273    }
274}
275
276impl_screen_mix_gray!(Gray2, Gray4, Gray8);
277
278impl Screen for BinaryColor {
279    fn screen(self, other: Self, start: Self, end: Self) -> Self {
280        if self == end || other == end {
281            end
282        } else {
283            start
284        }
285    }
286}
287
288const fn weighted_avg_mix_channel(
289    first: u8,
290    second: u8,
291    first_start: u8,
292    first_end: u8,
293    second_start: u8,
294    second_end: u8,
295) -> u8 {
296    const SHIFT: usize = 15;
297    const CONST_0_5: i32 = 1 << (SHIFT - 1);
298
299    const fn weight(value: u8, start: u8, end: u8) -> i32 {
300        if start == end {
301            return 0;
302        }
303
304        let diff = end as i32 - start as i32;
305        let value = value as i32 - start as i32;
306        let value = value << (SHIFT - 1);
307
308        value / diff
309    }
310
311    let first_weight = weight(first, first_start, first_end);
312    let second_weight = weight(second, second_start, second_end);
313    let sum_of_weights = first_weight + second_weight;
314    let [first_half, second_half] = if first_weight == second_weight || sum_of_weights == 0 {
315        let first_half = (first as i32) << (SHIFT - 1);
316        let second_half = (second as i32) << (SHIFT - 1);
317
318        [first_half, second_half]
319    } else {
320        let first_weight = first_weight << SHIFT;
321        let first_weight = first_weight / sum_of_weights;
322        let second_weight = second_weight << SHIFT;
323        let second_weight = second_weight / sum_of_weights;
324
325        [first_weight * first as i32, second_weight * second as i32]
326    };
327    let result = first_half + second_half + CONST_0_5;
328
329    (result >> SHIFT) as u8
330}
331
332macro_rules! impl_weighted_avg_mix_rgb {
333    ($($rgb_type:ty),+) => {
334        $(
335            impl WeightedAvg for $rgb_type {
336                fn weighted_avg(
337                    self,
338                    other: Self,
339                    start: Self,
340                    end: Self,
341                    other_start: Self,
342                    other_end: Self,
343                ) -> Self {
344                    let [r, g, b] = [Self::r, Self::g, Self::b].map(|value_of| {
345                        weighted_avg_mix_channel(
346                            value_of(&self),
347                            value_of(&other),
348                            value_of(&start),
349                            value_of(&end),
350                            value_of(&other_start),
351                            value_of(&other_end),
352                        )
353                    });
354
355                    <$rgb_type>::new(r, g, b)
356                }
357            }
358        )*
359    }
360}
361
362impl_weighted_avg_mix_rgb!(
363    Rgb555, Bgr555, Rgb565, Bgr565, Rgb666, Bgr666, Rgb888, Bgr888
364);
365
366macro_rules! impl_weighted_avg_mix_gray {
367    ($($gray_type:ty),+) => {
368        $(
369            impl WeightedAvg for $gray_type {
370                fn weighted_avg(
371                    self,
372                    other: Self,
373                    start: Self,
374                    end: Self,
375                    other_start: Self,
376                    other_end: Self,
377                ) -> Self {
378                    let luma = weighted_avg_mix_channel(
379                        self.luma(),
380                        other.luma(),
381                        start.luma(),
382                        end.luma(),
383                        other_start.luma(),
384                        other_end.luma(),
385                    );
386
387                    <$gray_type>::new(luma)
388                }
389            }
390        )*
391    }
392}
393
394impl_weighted_avg_mix_gray!(Gray2, Gray4, Gray8);
395
396impl WeightedAvg for BinaryColor {
397    fn weighted_avg(
398        self,
399        other: Self,
400        start: Self,
401        end: Self,
402        other_start: Self,
403        other_end: Self,
404    ) -> Self {
405        if start == other_start {
406            if self == end || other == other_end {
407                end
408            } else {
409                start
410            }
411        } else {
412            self
413        }
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    macro_rules! test_convert_channel {
422        (
423            $(
424                $fn_ident:ident, $n:expr, $value:expr, $start:expr, $end:expr, $expected:expr,
425            )*
426        ) => {
427            $(
428                #[test]
429                fn $fn_ident() {
430                    let result = convert_channel::<$n>($value, $start, $end);
431                    assert_eq!(result, $expected);
432                }
433            )*
434        }
435    }
436
437    test_convert_channel! {
438        convert_8bpp_255_to_0_255, { 2usize.pow(8) }, 255, 0, 255, 255,
439        convert_8bpp_255_to_0_128, { 2usize.pow(8) }, 255, 0, 128, 128,
440        convert_8bpp_128_to_0_128, { 2usize.pow(8) }, 128, 0, 128, 64,
441        convert_8bpp_128_to_0_64, { 2usize.pow(8) }, 128, 0, 64, 32,
442        convert_8bpp_64_to_0_64, { 2usize.pow(8) }, 64, 0, 64, 16,
443
444        convert_8bpp_255_to_255_0, { 2usize.pow(8) }, 255, 255, 0, 0,
445        convert_8bpp_255_to_255_128, { 2usize.pow(8) }, 255, 255, 128, 128,
446        convert_8bpp_128_to_255_128, { 2usize.pow(8) }, 128, 255, 128, 255 - 128 / 2,
447        convert_8bpp_128_to_255_64, { 2usize.pow(8) }, 128, 255, 64, 255 - 128 / 4 * 3,
448        convert_8bpp_64_to_255_64, { 2usize.pow(8) }, 64, 255, 64, 255 - 64 / 4 * 3,
449
450        convert_5bpp_31_to_0_255, { 2usize.pow(5) }, 31, 0, 255, 255,
451        convert_4bpp_15_to_0_255, { 2usize.pow(4) }, 15, 0, 255, 255,
452        convert_3bpp_7_to_0_255, { 2usize.pow(3) }, 7, 0, 255, 255,
453        convert_2bpp_3_to_0_255, { 2usize.pow(2) }, 3, 0, 255, 255,
454        convert_1bpp_1_to_0_255, { 2usize.pow(1) }, 1, 0, 255, 255,
455    }
456
457    macro_rules! test_screen_mix_channel {
458        (
459            $(
460                $fn_ident:ident,
461                $first:expr,
462                $second:expr,
463                $start:expr,
464                $end:expr,
465                $expected:expr,
466            )*
467        ) => {
468            $(
469                #[test]
470                fn $fn_ident() {
471                    let result = screen_mix_channel($first, $second, $start, $end);
472                    assert_eq!(result, $expected);
473                }
474            )*
475        }
476    }
477
478    test_screen_mix_channel! {
479        screen_mix_channel_255_255_on_0_255, 255, 255, 0, 255, 255,
480        screen_mix_channel_128_128_on_0_255, 128, 128, 0, 255, 128 + 128 / 2,
481        screen_mix_channel_64_64_on_0_255, 64, 64, 0, 255, 64 + 64 / 4 * 3,
482        screen_mix_channel_32_32_on_0_255, 32, 32, 0, 255, 32 + 32 / 8 * 7,
483        screen_mix_channel_0_0_on_0_255, 0, 0, 0, 255, 0,
484
485        screen_mix_channel_255_255_on_255_0, 255, 255, 255, 0, 255,
486        screen_mix_channel_224_224_on_255_0, 224, 224, 255, 0, 224 - 224 / 8,
487        screen_mix_channel_192_192_on_255_0, 192, 192, 255, 0, 192 - 192 / 4,
488        screen_mix_channel_128_128_on_255_0, 128, 128, 255, 0, 128 - 128 / 2,
489        screen_mix_channel_0_0_on_255_0, 0, 0, 255, 0, 0,
490
491        screen_mix_channel_255_0_on_0_255, 255, 0, 0, 255, 255,
492        screen_mix_channel_128_64_on_0_255, 128, 64, 0, 255, 128 + 64 / 2,
493        screen_mix_channel_128_32_on_0_255, 128, 32, 0, 255, 128 + 32 / 2,
494        screen_mix_channel_64_128_on_0_255, 64, 128, 0, 255, 64 + 128 / 4 * 3,
495        screen_mix_channel_32_128_on_0_255, 32, 128, 0, 255, 32 + 128 / 8 * 7,
496
497        screen_mix_channel_255_0_on_255_0, 255, 0, 255, 0, 0,
498        screen_mix_channel_224_192_on_255_0, 224, 192, 255, 0, 224 / 4 * 3,
499        screen_mix_channel_224_128_on_255_0, 224, 128, 255, 0, 224 / 2,
500        screen_mix_channel_192_224_on_255_0, 192, 224, 255, 0, 192 / 8 * 7,
501        screen_mix_channel_128_224_on_255_0, 128, 224, 255, 0, 128 / 8 * 7,
502
503        screen_mix_channel_128_128_on_0_128, 128, 128, 0, 128, 128,
504        screen_mix_channel_64_64_on_0_128, 64, 64, 0, 128, 64 + 64 / 2,
505        screen_mix_channel_64_64_on_0_64, 64, 64, 0, 64, 64,
506        screen_mix_channel_32_32_on_0_64, 32, 32, 0, 64, 32 + 32 / 2,
507        screen_mix_channel_32_32_on_0_32, 32, 32, 0, 32, 32,
508
509        screen_mix_channel_128_128_on_128_0, 128, 128, 128, 0, 128,
510        screen_mix_channel_128_64_on_128_0, 128, 64, 128, 0, 64,
511        screen_mix_channel_128_32_on_128_0, 128, 32, 128, 0, 32,
512        screen_mix_channel_64_128_on_128_0, 64, 128, 128, 0, 64,
513        screen_mix_channel_32_128_on_128_0, 32, 128, 128, 0, 32,
514
515        screen_mix_channel_255_255_on_255_255, 255, 255, 255, 255, 255,
516        screen_mix_channel_255_255_on_128_128, 255, 255, 128, 128, 128,
517        screen_mix_channel_128_128_on_128_128, 128, 128, 128, 128, 128,
518        screen_mix_channel_0_0_on_128_128, 0, 0, 128, 128, 128,
519        screen_mix_channel_0_0_on_0_0, 0, 0, 0, 0, 0,
520    }
521
522    macro_rules! test_weighted_avg_mix_channel {
523        (
524            $(
525                $fn_ident:ident,
526                $first:expr,
527                $second:expr,
528                $first_start:expr,
529                $first_end:expr,
530                $second_start:expr,
531                $second_end:expr,
532                $expected:expr,
533            )*
534        ) => {
535            $(
536                #[test]
537                fn $fn_ident() {
538                    let result = weighted_avg_mix_channel(
539                        $first,
540                        $second,
541                        $first_start,
542                        $first_end,
543                        $second_start,
544                        $second_end,
545                    );
546                    assert_eq!(result, $expected);
547                }
548            )*
549        }
550    }
551
552    test_weighted_avg_mix_channel! {
553        weighted_avg_mix_channel_255_255_on_0_255_and_0_255, 255, 255, 0, 255, 0, 255, 255,
554        weighted_avg_mix_channel_128_128_on_0_255_and_0_255, 128, 128, 0, 255, 0, 255, 128,
555        weighted_avg_mix_channel_64_192_on_0_255_and_0_255, 64, 192, 0, 255, 0, 255, 160,
556        weighted_avg_mix_channel_32_96_on_0_255_and_0_255, 32, 96, 0, 255, 0, 255, 80,
557        weighted_avg_mix_channel_32_224_on_0_255_and_0_255, 32, 224, 0, 255, 0, 255, 200,
558
559        weighted_avg_mix_channel_128_255_on_128_0_and_128_255, 128, 255, 128, 0, 128, 255, 255,
560        weighted_avg_mix_channel_128_128_on_128_0_and_128_255, 128, 128, 128, 0, 128, 255, 128,
561        weighted_avg_mix_channel_64_192_on_128_0_and_128_255, 64, 192, 128, 0, 128, 255, 128,
562        weighted_avg_mix_channel_0_255_on_128_0_and_128_255, 0, 255, 128, 0, 128, 255, 128,
563        weighted_avg_mix_channel_0_128_on_128_0_and_128_255, 0, 128, 128, 0, 128, 255, 0,
564
565        weighted_avg_mix_channel_255_255_on_255_0_and_0_255, 255, 255, 255, 0, 0, 255, 255,
566        weighted_avg_mix_channel_192_192_on_255_0_and_0_255, 192, 192, 255, 0, 0, 255, 192,
567        weighted_avg_mix_channel_128_128_on_255_0_and_0_255, 128, 128, 255, 0, 0, 255, 128,
568        weighted_avg_mix_channel_64_64_on_255_0_and_0_255, 64, 64, 255, 0, 0, 255, 64,
569        weighted_avg_mix_channel_0_0_on_255_0_and_0_255, 0, 0, 255, 0, 0, 255, 0,
570
571        weighted_avg_mix_channel_255_128_on_255_0_and_0_255, 255, 128, 255, 0, 0, 255, 128,
572        weighted_avg_mix_channel_255_0_on_255_0_and_0_255, 255, 0, 255, 0, 0, 255, 128,
573        weighted_avg_mix_channel_64_192_on_255_0_and_0_255, 64, 192, 255, 0, 0, 255, 128,
574        weighted_avg_mix_channel_32_224_on_255_0_and_0_255, 0, 255, 255, 0, 0, 255, 128,
575        weighted_avg_mix_channel_0_255_on_255_0_and_0_255, 0, 255, 255, 0, 0, 255, 128,
576
577        weighted_avg_mix_channel_255_255_on_255_255_and_255_255, 255, 255, 255, 255, 255, 255, 255,
578        weighted_avg_mix_channel_192_255_on_0_255_and_255_255, 192, 255, 0, 255, 255, 255, 192,
579        weighted_avg_mix_channel_128_255_on_0_255_and_255_255, 128, 255, 0, 255, 255, 255, 128,
580        weighted_avg_mix_channel_0_128_on_0_0_and_128_128, 0, 128, 0, 0, 128, 128, 64,
581        weighted_avg_mix_channel_0_0_on_0_0_and_0_0, 0, 0, 0, 0, 0, 0, 0,
582    }
583}