fframes 1.0.0-beta.7

A video generation framework
use std::fmt;
use usvgr::svgtree::SvgAttributeValue;

use crate::animation;

/// The basic rgb color type. It is limited to the range of 0-255 because the most of video
/// codecs only support 8-bit color depth.  
///
/// Can be used in fframes animations and as a raw svgr! value.
///
/// @example
/// ```no_run
///use crate::fframes::{Color, Svgr, svgr};
///
///const WHITE: Color = Color::hex("#FFFFFF");
///
/// svgr!(<rect fill={WHITE} />);
/// ```
#[derive(Copy, Default, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Color {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

impl From<Color> for SvgAttributeValue<'static> {
    fn from(color: Color) -> Self {
        SvgAttributeValue::Color(usvgr::svgtree::svgrtypes::Color::new_rgb(
            color.r, color.g, color.b,
        ))
    }
}

const HASH: u8 = b'#';

pub const fn char_to_digit(char: char, radix: u32) -> u32 {
    // If not a digit, a number greater than radix will be created.
    let mut digit = (char as u32).wrapping_sub('0' as u32);
    if radix > 10 {
        assert!(radix <= 36, "to_digit: radix is too high (maximum 36)");
        if digit < 10 {
            return digit;
        }

        // Force the 6th bit to be set to ensure ascii is lower case.
        digit = (char as u32 | 0b10_0000)
            .wrapping_sub('a' as u32)
            .saturating_add(10);
    }
    // FIXME: once then_some is const fn, use it here
    if digit < radix {
        digit
    } else {
        panic!("can not parse char in a radix string");
    }
}

impl Color {
    pub const BLACK: Color = Color { r: 0, g: 0, b: 0 };
    pub const WHITE: Color = Color {
        r: 255,
        g: 255,
        b: 255,
    };

    /// Creates new color from hex number
    pub const fn hex_num(num: u32) -> Self {
        if num <= 0xfff {
            // Handle 3-digit hex
            let r = ((num >> 8) & 0xF) as u8;
            let g = ((num >> 4) & 0xF) as u8;
            let b = (num & 0xF) as u8;
            Color {
                r: (r * 16 + r),
                g: (g * 16 + g),
                b: (b * 16 + b),
            }
        } else {
            // Handle 6-digit hex
            Color {
                r: ((num >> 16) & 0xFF) as u8,
                g: ((num >> 8) & 0xFF) as u8,
                b: (num & 0xFF) as u8,
            }
        }
    }

    /// Creates a new color from a hex string. Supports both #RGB and #RRGGBB formats.
    /// Works as a constant, so it's available for **const variables** and avoids runtime parsing.
    /// If parsing fails, it fallbacks to the solid black color.
    pub const fn hex(hex_str: &str) -> Self {
        let buffer = hex_str.as_bytes();

        if buffer[0] != HASH {
            return Color::BLACK;
        }

        match buffer.len() {
            4 => {
                let r = char_to_digit(buffer[1] as char, 16) as u8;
                let g = char_to_digit(buffer[2] as char, 16) as u8;
                let b = char_to_digit(buffer[3] as char, 16) as u8;

                Color {
                    r: (r + r * 16),
                    g: (g + g * 16),
                    b: (b + b * 16),
                }
            }
            7 => {
                let r = char_to_digit(buffer[1] as char, 16) as u8;
                let r1 = char_to_digit(buffer[2] as char, 16) as u8;
                let g = char_to_digit(buffer[3] as char, 16) as u8;
                let g1 = char_to_digit(buffer[4] as char, 16) as u8;
                let b = char_to_digit(buffer[5] as char, 16) as u8;
                let b1 = char_to_digit(buffer[6] as char, 16) as u8;

                Color {
                    r: (r * 16 + r1),
                    g: (g * 16 + g1),
                    b: (b * 16 + b1),
                }
            }
            _ => Color::BLACK,
        }
    }

    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
        Color { r, g, b }
    }
}

struct ColorF32 {
    r: f32,
    g: f32,
    b: f32,
}

impl From<Color> for ColorF32 {
    fn from(Color { r, g, b }: Color) -> Self {
        ColorF32 {
            r: r as f32,
            g: g as f32,
            b: b as f32,
        }
    }
}

impl animation::Animatable for Color {
    fn apply_progress(&self, to: &Self, progress: f32) -> Self {
        let from = ColorF32::from(*self);
        let to = ColorF32::from(*to);

        Color {
            r: (from.r + ((to.r - from.r) * progress)) as u8,
            g: (from.g + ((to.g - from.g) * progress)) as u8,
            b: (from.b + ((to.b - from.b) * progress)) as u8,
        }
    }
}

impl fmt::Display for Color {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        let Color { r, g, b } = self;

        write!(formatter, "rgb({r}, {g}, {b})",)
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    pub fn char_to_digit_test() {
        assert_eq!(char_to_digit('f', 16), 15);
        assert_eq!(char_to_digit('F', 16), 15);
        assert_eq!(char_to_digit('9', 16), 9);
    }

    #[test]
    pub fn hex_parsing() {
        assert_eq!(Color::hex("#f0f"), Color::rgb(255, 0, 255));
        assert_eq!(Color::hex("#ff00ff"), Color::rgb(255, 0, 255));
        assert_eq!(Color::hex("#ef4444"), Color::rgb(239, 68, 68));
        assert_eq!(Color::hex("#1c1917"), Color::rgb(28, 25, 23));
    }

    #[test]
    pub fn hex_num_parsing() {
        assert_eq!(Color::hex_num(0xf0f), Color::rgb(255, 0, 255));
        assert_eq!(Color::hex_num(0xff00ff), Color::rgb(255, 0, 255));
        assert_eq!(Color::hex_num(0xef4444), Color::rgb(239, 68, 68));
        assert_eq!(Color::hex_num(0x1c1917), Color::rgb(28, 25, 23));
    }
}