mcu-dynamiccolor 0.2.2

Dynamic color system for Material Design 3
Documentation
// <FILE>crates/mcu-dynamiccolor/src/impl_palettes_2025.rs</FILE> - <DESC>2025 spec palette generation delegate implementation</DESC>
// <VERS>VERSION: 1.0.0</VERS>
// <WCTX>OFPF refactor: Extract 2025 palette delegate from dynamic_scheme_palettes.rs</WCTX>
// <CLOG>Initial extraction of DynamicSchemePalettesDelegateImpl2025 to comply with OFPF LOC limits</CLOG>

//! 2025 Material Design specification palette generation delegate.
//!
//! This module contains the palette generation logic for the updated
//! Material Design 3 specification (2025), which introduces platform-aware
//! palette generation and additional adjustments based on blue hue and dark mode.

use mcu_hct::Hct;
use mcu_palettes::TonalPalette;
use mcu_utils::math::sanitize_degrees_double;

use crate::dynamic_scheme_palettes::{
    get_piecewise_value, get_rotated_hue, is_blue_hue, DynamicSchemePalettesDelegate, HUES,
    SECONDARY_ROTATIONS_EXPRESSIVE, TERTIARY_ROTATIONS_EXPRESSIVE, VIBRANT_SECONDARY_ROTATIONS,
    VIBRANT_TERTIARY_ROTATIONS,
};
use crate::impl_palettes_2021::DynamicSchemePalettesDelegateImpl2021;
use crate::{Platform, Variant};

/// Palette generation delegate for the 2025 Material Design 3 specification.
///
/// The 2025 spec introduces platform-aware palette generation and additional
/// adjustments based on whether the color is blue and the dark/light mode.
pub struct DynamicSchemePalettesDelegateImpl2025;

impl DynamicSchemePalettesDelegateImpl2025 {
    /// Fallback to 2021 delegate for base behavior.
    fn delegate_2021() -> &'static DynamicSchemePalettesDelegateImpl2021 {
        &DynamicSchemePalettesDelegateImpl2021
    }
}

impl DynamicSchemePalettesDelegate for DynamicSchemePalettesDelegateImpl2025 {
    fn get_primary_palette(
        &self,
        variant: Variant,
        source: &Hct,
        is_dark: bool,
        platform: Platform,
        contrast: f64,
    ) -> TonalPalette {
        let is_blue = is_blue_hue(source.hue());

        match variant {
            Variant::Neutral => {
                let chroma = if platform == Platform::Phone {
                    if is_blue {
                        12.0
                    } else {
                        8.0
                    }
                } else {
                    if is_blue {
                        16.0
                    } else {
                        12.0
                    }
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::TonalSpot => {
                let chroma = if platform == Platform::Phone && is_dark {
                    26.0
                } else {
                    32.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::Expressive => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        36.0
                    } else {
                        48.0
                    }
                } else {
                    40.0
                };
                TonalPalette::from_hue_and_chroma(
                    sanitize_degrees_double(source.hue() + 240.0),
                    chroma,
                )
            }
            Variant::Vibrant => {
                let chroma = if platform == Platform::Phone {
                    74.0
                } else {
                    56.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            // Fall back to 2021 for other variants
            _ => Self::delegate_2021()
                .get_primary_palette(variant, source, is_dark, platform, contrast),
        }
    }

    fn get_secondary_palette(
        &self,
        variant: Variant,
        source: &Hct,
        is_dark: bool,
        platform: Platform,
        contrast: f64,
    ) -> TonalPalette {
        match variant {
            Variant::TonalSpot => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        14.0
                    } else {
                        16.0
                    }
                } else {
                    16.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::Expressive => {
                let rotated_hue = get_rotated_hue(source, &HUES, &SECONDARY_ROTATIONS_EXPRESSIVE);
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        20.0
                    } else {
                        24.0
                    }
                } else {
                    24.0
                };
                TonalPalette::from_hue_and_chroma(rotated_hue, chroma)
            }
            Variant::Vibrant => {
                let rotated_hue = get_rotated_hue(source, &HUES, &VIBRANT_SECONDARY_ROTATIONS);
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        20.0
                    } else {
                        24.0
                    }
                } else {
                    24.0
                };
                TonalPalette::from_hue_and_chroma(rotated_hue, chroma)
            }
            // Fall back to 2021 for other variants
            _ => Self::delegate_2021()
                .get_secondary_palette(variant, source, is_dark, platform, contrast),
        }
    }

    fn get_tertiary_palette(
        &self,
        variant: Variant,
        source: &Hct,
        is_dark: bool,
        platform: Platform,
        contrast: f64,
    ) -> TonalPalette {
        match variant {
            Variant::TonalSpot => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        20.0
                    } else {
                        24.0
                    }
                } else {
                    24.0
                };
                TonalPalette::from_hue_and_chroma(
                    sanitize_degrees_double(source.hue() + 60.0),
                    chroma,
                )
            }
            Variant::Expressive => {
                let rotated_hue = get_rotated_hue(source, &HUES, &TERTIARY_ROTATIONS_EXPRESSIVE);
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        28.0
                    } else {
                        32.0
                    }
                } else {
                    32.0
                };
                TonalPalette::from_hue_and_chroma(rotated_hue, chroma)
            }
            Variant::Vibrant => {
                let rotated_hue = get_rotated_hue(source, &HUES, &VIBRANT_TERTIARY_ROTATIONS);
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        28.0
                    } else {
                        32.0
                    }
                } else {
                    32.0
                };
                TonalPalette::from_hue_and_chroma(rotated_hue, chroma)
            }
            // Fall back to 2021 for other variants
            _ => Self::delegate_2021()
                .get_tertiary_palette(variant, source, is_dark, platform, contrast),
        }
    }

    fn get_neutral_palette(
        &self,
        variant: Variant,
        source: &Hct,
        is_dark: bool,
        platform: Platform,
        contrast: f64,
    ) -> TonalPalette {
        match variant {
            Variant::TonalSpot => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        5.0
                    } else {
                        6.0
                    }
                } else {
                    6.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::Expressive => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        7.0
                    } else {
                        8.0
                    }
                } else {
                    8.0
                };
                TonalPalette::from_hue_and_chroma(
                    sanitize_degrees_double(source.hue() + 15.0),
                    chroma,
                )
            }
            Variant::Vibrant => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        8.0
                    } else {
                        10.0
                    }
                } else {
                    10.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::Neutral => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        2.0
                    } else {
                        2.0
                    }
                } else {
                    2.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            // Fall back to 2021 for other variants
            _ => Self::delegate_2021()
                .get_neutral_palette(variant, source, is_dark, platform, contrast),
        }
    }

    fn get_neutral_variant_palette(
        &self,
        variant: Variant,
        source: &Hct,
        is_dark: bool,
        platform: Platform,
        contrast: f64,
    ) -> TonalPalette {
        match variant {
            Variant::TonalSpot => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        7.0
                    } else {
                        8.0
                    }
                } else {
                    8.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::Expressive => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        11.0
                    } else {
                        12.0
                    }
                } else {
                    12.0
                };
                TonalPalette::from_hue_and_chroma(
                    sanitize_degrees_double(source.hue() + 15.0),
                    chroma,
                )
            }
            Variant::Vibrant => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        10.0
                    } else {
                        12.0
                    }
                } else {
                    12.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            Variant::Neutral => {
                let chroma = if platform == Platform::Phone {
                    if is_dark {
                        2.0
                    } else {
                        2.0
                    }
                } else {
                    2.0
                };
                TonalPalette::from_hue_and_chroma(source.hue(), chroma)
            }
            // Fall back to 2021 for other variants
            _ => Self::delegate_2021()
                .get_neutral_variant_palette(variant, source, is_dark, platform, contrast),
        }
    }

    fn get_error_palette(
        &self,
        variant: Variant,
        source: &Hct,
        _is_dark: bool,
        _platform: Platform,
        _contrast: f64,
    ) -> Option<TonalPalette> {
        // 2025 spec uses piecewise error hue for certain variants
        match variant {
            Variant::Expressive | Variant::Vibrant | Variant::TonalSpot | Variant::Neutral => {
                // Error hue breakpoints and values
                const ERROR_HUES: [f64; 8] = [0.0, 21.0, 51.0, 121.0, 151.0, 191.0, 271.0, 321.0];
                const ERROR_ROTATIONS: [f64; 8] = [25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0];

                let error_hue = get_piecewise_value(source.hue(), &ERROR_HUES, &ERROR_ROTATIONS);
                Some(TonalPalette::from_hue_and_chroma(error_hue, 84.0))
            }
            _ => None,
        }
    }
}

// <FILE>crates/mcu-dynamiccolor/src/impl_palettes_2025.rs</FILE> - <DESC>2025 spec palette generation delegate implementation</DESC>
// <VERS>END OF VERSION: 1.0.0</VERS>