use crate::dynamic::color_spec::{Platform, SpecVersion};
use crate::dynamic::dynamic_color::DynamicColor;
use crate::dynamic::material_dynamic_colors::MaterialDynamicColors;
use crate::dynamic::variant::Variant;
use crate::hct::hct_color::Hct;
use crate::palettes::tonal_palette::TonalPalette;
use crate::utils::color_utils::Argb;
use crate::utils::math_utils::MathUtils;
use std::sync::OnceLock;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DynamicScheme {
pub source_color_hct_list: Vec<Hct>,
pub variant: Variant,
pub is_dark: bool,
pub contrast_level: f64,
pub platform: Platform,
pub spec_version: SpecVersion,
pub primary_palette: TonalPalette,
pub secondary_palette: TonalPalette,
pub tertiary_palette: TonalPalette,
pub neutral_palette: TonalPalette,
pub neutral_variant_palette: TonalPalette,
pub error_palette: TonalPalette,
#[cfg_attr(feature = "serde", serde(skip))]
pub argb_cache: papaya::HashMap<String, Argb>,
#[cfg_attr(feature = "serde", serde(skip))]
pub tone_cache: papaya::HashMap<String, f64>,
#[cfg_attr(feature = "serde", serde(skip))]
pub hct_cache: papaya::HashMap<String, Hct>,
}
impl PartialEq for DynamicScheme {
fn eq(&self, other: &Self) -> bool {
self.source_color_hct_list == other.source_color_hct_list
&& self.variant == other.variant
&& self.is_dark == other.is_dark
&& self.contrast_level.to_bits() == other.contrast_level.to_bits()
&& self.platform == other.platform
&& self.spec_version == other.spec_version
&& self.primary_palette == other.primary_palette
&& self.secondary_palette == other.secondary_palette
&& self.tertiary_palette == other.tertiary_palette
&& self.neutral_palette == other.neutral_palette
&& self.neutral_variant_palette == other.neutral_variant_palette
&& self.error_palette == other.error_palette
}
}
impl Eq for DynamicScheme {}
impl std::hash::Hash for DynamicScheme {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.source_color_hct_list.hash(state);
self.variant.hash(state);
self.is_dark.hash(state);
self.contrast_level.to_bits().hash(state);
self.platform.hash(state);
self.spec_version.hash(state);
}
}
impl DynamicScheme {
#[must_use]
pub fn new(
source_color_hct: Hct,
variant: Variant,
is_dark: bool,
contrast_level: f64,
primary_palette: TonalPalette,
secondary_palette: TonalPalette,
tertiary_palette: TonalPalette,
neutral_palette: TonalPalette,
neutral_variant_palette: TonalPalette,
error_palette: TonalPalette,
) -> Self {
Self::new_with_platform_and_spec(
source_color_hct,
variant,
is_dark,
contrast_level,
Platform::Phone,
SpecVersion::Spec2021,
primary_palette,
secondary_palette,
tertiary_palette,
neutral_palette,
neutral_variant_palette,
error_palette,
)
}
#[must_use]
pub fn new_with_platform_and_spec(
source_color_hct: Hct,
variant: Variant,
is_dark: bool,
contrast_level: f64,
platform: Platform,
spec_version: SpecVersion,
primary_palette: TonalPalette,
secondary_palette: TonalPalette,
tertiary_palette: TonalPalette,
neutral_palette: TonalPalette,
neutral_variant_palette: TonalPalette,
error_palette: TonalPalette,
) -> Self {
Self {
source_color_hct_list: vec![source_color_hct],
variant,
is_dark,
contrast_level,
platform,
spec_version: Self::maybe_fallback_spec_version(spec_version, variant),
primary_palette,
secondary_palette,
tertiary_palette,
neutral_palette,
neutral_variant_palette,
error_palette,
argb_cache: papaya::HashMap::new(),
tone_cache: papaya::HashMap::new(),
hct_cache: papaya::HashMap::new(),
}
}
#[must_use]
pub fn from_scheme(other: &Self, is_dark: bool) -> Self {
Self::from_scheme_with_contrast(other, is_dark, other.contrast_level)
}
#[must_use]
pub fn from_scheme_with_contrast(other: &Self, is_dark: bool, contrast_level: f64) -> Self {
Self {
source_color_hct_list: other.source_color_hct_list.clone(),
variant: other.variant,
is_dark,
contrast_level,
platform: other.platform,
spec_version: other.spec_version,
primary_palette: other.primary_palette.clone(),
secondary_palette: other.secondary_palette.clone(),
tertiary_palette: other.tertiary_palette.clone(),
neutral_palette: other.neutral_palette.clone(),
neutral_variant_palette: other.neutral_variant_palette.clone(),
error_palette: other.error_palette.clone(),
argb_cache: papaya::HashMap::new(),
tone_cache: papaya::HashMap::new(),
hct_cache: papaya::HashMap::new(),
}
}
#[must_use]
pub fn source_color_hct(&self) -> &Hct {
&self.source_color_hct_list[0]
}
#[must_use]
pub fn source_color_argb(&self) -> Argb {
self.source_color_hct().to_argb()
}
#[must_use]
pub fn get_hct(&self, dynamic_color: &DynamicColor) -> Hct {
let pin = self.hct_cache.pin();
if let Some(&hct) = pin.get(&dynamic_color.name) {
return hct;
}
let hct = crate::dynamic::color_specs::ColorSpecs::get(self.spec_version)
.call()
.get_hct(self, dynamic_color);
pin.insert(dynamic_color.name.clone(), hct);
hct
}
#[must_use]
pub fn get_argb(&self, dynamic_color: &DynamicColor) -> Argb {
let pin = self.argb_cache.pin();
if let Some(&argb) = pin.get(&dynamic_color.name) {
return argb;
}
let hct = self.get_hct(dynamic_color);
let mut argb = hct.to_argb();
if let Some(ref opacity_func) = dynamic_color.opacity
&& let Some(opacity_percentage) = opacity_func(self)
{
let alpha = (opacity_percentage * 255.0).round() as u32;
let alpha = alpha.clamp(0, 255);
argb = Argb((argb.0 & 0x00ffffff) | (alpha << 24));
}
pin.insert(dynamic_color.name.clone(), argb);
argb
}
#[must_use]
pub fn get_tone(&self, dynamic_color: &DynamicColor) -> f64 {
let pin = self.tone_cache.pin();
if let Some(&tone) = pin.get(&dynamic_color.name) {
return tone;
}
let tone = crate::dynamic::color_specs::ColorSpecs::get(self.spec_version)
.call()
.get_tone(self, dynamic_color);
pin.insert(dynamic_color.name.clone(), tone);
tone
}
#[must_use]
pub fn get_piecewise_value(
source_color_hct: &Hct,
hue_breakpoints: &[f64],
hues: &[f64],
) -> f64 {
let size = (hue_breakpoints.len().saturating_sub(1)).min(hues.len());
let source_hue = source_color_hct.hue();
for i in 0..size {
if source_hue >= hue_breakpoints[i] && source_hue < hue_breakpoints[i + 1] {
return MathUtils::sanitize_degrees_double(hues[i]);
}
}
source_hue
}
#[must_use]
pub fn get_rotated_hue(
source_color_hct: &Hct,
hue_breakpoints: &[f64],
rotations: &[f64],
) -> f64 {
let mut rotation = Self::get_piecewise_value(source_color_hct, hue_breakpoints, rotations);
let size = (hue_breakpoints.len().saturating_sub(1)).min(rotations.len());
if size == 0 {
rotation = 0.0;
}
MathUtils::sanitize_degrees_double(source_color_hct.hue() + rotation)
}
fn maybe_fallback_spec_version(spec_version: SpecVersion, variant: Variant) -> SpecVersion {
if variant == Variant::Cmf {
return spec_version;
}
if variant == Variant::Expressive
|| variant == Variant::Vibrant
|| variant == Variant::TonalSpot
|| variant == Variant::Neutral
{
if spec_version == SpecVersion::Spec2026 {
return SpecVersion::Spec2025;
}
return spec_version;
}
SpecVersion::Spec2021
}
#[must_use]
pub fn primary_palette_key_color(&self) -> Argb {
self.get_argb(&dynamic_colors().primary_palette_key_color())
}
#[must_use]
pub fn secondary_palette_key_color(&self) -> Argb {
self.get_argb(&dynamic_colors().secondary_palette_key_color())
}
#[must_use]
pub fn tertiary_palette_key_color(&self) -> Argb {
self.get_argb(&dynamic_colors().tertiary_palette_key_color())
}
#[must_use]
pub fn neutral_palette_key_color(&self) -> Argb {
self.get_argb(&dynamic_colors().neutral_palette_key_color())
}
#[must_use]
pub fn neutral_variant_palette_key_color(&self) -> Argb {
self.get_argb(&dynamic_colors().neutral_variant_palette_key_color())
}
#[must_use]
pub fn background(&self) -> Argb {
self.get_argb(&dynamic_colors().background())
}
#[must_use]
pub fn on_background(&self) -> Argb {
self.get_argb(&dynamic_colors().on_background())
}
#[must_use]
pub fn surface(&self) -> Argb {
self.get_argb(&dynamic_colors().surface())
}
#[must_use]
pub fn surface_dim(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_dim())
}
#[must_use]
pub fn surface_bright(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_bright())
}
#[must_use]
pub fn surface_container_lowest(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_container_lowest())
}
#[must_use]
pub fn surface_container_low(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_container_low())
}
#[must_use]
pub fn surface_container(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_container())
}
#[must_use]
pub fn surface_container_high(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_container_high())
}
#[must_use]
pub fn surface_container_highest(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_container_highest())
}
#[must_use]
pub fn on_surface(&self) -> Argb {
self.get_argb(&dynamic_colors().on_surface())
}
#[must_use]
pub fn surface_variant(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_variant())
}
#[must_use]
pub fn on_surface_variant(&self) -> Argb {
self.get_argb(&dynamic_colors().on_surface_variant())
}
#[must_use]
pub fn inverse_surface(&self) -> Argb {
self.get_argb(&dynamic_colors().inverse_surface())
}
#[must_use]
pub fn inverse_on_surface(&self) -> Argb {
self.get_argb(&dynamic_colors().inverse_on_surface())
}
#[must_use]
pub fn outline(&self) -> Argb {
self.get_argb(&dynamic_colors().outline())
}
#[must_use]
pub fn outline_variant(&self) -> Argb {
self.get_argb(&dynamic_colors().outline_variant())
}
#[must_use]
pub fn shadow(&self) -> Argb {
self.get_argb(&dynamic_colors().shadow())
}
#[must_use]
pub fn scrim(&self) -> Argb {
self.get_argb(&dynamic_colors().scrim())
}
#[must_use]
pub fn surface_tint(&self) -> Argb {
self.get_argb(&dynamic_colors().surface_tint())
}
#[must_use]
pub fn primary(&self) -> Argb {
self.get_argb(&dynamic_colors().primary())
}
#[must_use]
pub fn on_primary(&self) -> Argb {
self.get_argb(&dynamic_colors().on_primary())
}
#[must_use]
pub fn primary_container(&self) -> Argb {
self.get_argb(&dynamic_colors().primary_container())
}
#[must_use]
pub fn on_primary_container(&self) -> Argb {
self.get_argb(&dynamic_colors().on_primary_container())
}
#[must_use]
pub fn inverse_primary(&self) -> Argb {
self.get_argb(&dynamic_colors().inverse_primary())
}
#[must_use]
pub fn secondary(&self) -> Argb {
self.get_argb(&dynamic_colors().secondary())
}
#[must_use]
pub fn on_secondary(&self) -> Argb {
self.get_argb(&dynamic_colors().on_secondary())
}
#[must_use]
pub fn secondary_container(&self) -> Argb {
self.get_argb(&dynamic_colors().secondary_container())
}
#[must_use]
pub fn on_secondary_container(&self) -> Argb {
self.get_argb(&dynamic_colors().on_secondary_container())
}
#[must_use]
pub fn tertiary(&self) -> Argb {
self.get_argb(&dynamic_colors().tertiary())
}
#[must_use]
pub fn on_tertiary(&self) -> Argb {
self.get_argb(&dynamic_colors().on_tertiary())
}
#[must_use]
pub fn tertiary_container(&self) -> Argb {
self.get_argb(&dynamic_colors().tertiary_container())
}
#[must_use]
pub fn on_tertiary_container(&self) -> Argb {
self.get_argb(&dynamic_colors().on_tertiary_container())
}
#[must_use]
pub fn error(&self) -> Argb {
self.get_argb(&dynamic_colors().error())
}
#[must_use]
pub fn on_error(&self) -> Argb {
self.get_argb(&dynamic_colors().on_error())
}
#[must_use]
pub fn error_container(&self) -> Argb {
self.get_argb(&dynamic_colors().error_container())
}
#[must_use]
pub fn on_error_container(&self) -> Argb {
self.get_argb(&dynamic_colors().on_error_container())
}
#[must_use]
pub fn primary_fixed(&self) -> Argb {
self.get_argb(&dynamic_colors().primary_fixed())
}
#[must_use]
pub fn primary_fixed_dim(&self) -> Argb {
self.get_argb(&dynamic_colors().primary_fixed_dim())
}
#[must_use]
pub fn on_primary_fixed(&self) -> Argb {
self.get_argb(&dynamic_colors().on_primary_fixed())
}
#[must_use]
pub fn on_primary_fixed_variant(&self) -> Argb {
self.get_argb(&dynamic_colors().on_primary_fixed_variant())
}
#[must_use]
pub fn secondary_fixed(&self) -> Argb {
self.get_argb(&dynamic_colors().secondary_fixed())
}
#[must_use]
pub fn secondary_fixed_dim(&self) -> Argb {
self.get_argb(&dynamic_colors().secondary_fixed_dim())
}
#[must_use]
pub fn on_secondary_fixed(&self) -> Argb {
self.get_argb(&dynamic_colors().on_secondary_fixed())
}
#[must_use]
pub fn on_secondary_fixed_variant(&self) -> Argb {
self.get_argb(&dynamic_colors().on_secondary_fixed_variant())
}
#[must_use]
pub fn tertiary_fixed(&self) -> Argb {
self.get_argb(&dynamic_colors().tertiary_fixed())
}
#[must_use]
pub fn tertiary_fixed_dim(&self) -> Argb {
self.get_argb(&dynamic_colors().tertiary_fixed_dim())
}
#[must_use]
pub fn on_tertiary_fixed(&self) -> Argb {
self.get_argb(&dynamic_colors().on_tertiary_fixed())
}
#[must_use]
pub fn on_tertiary_fixed_variant(&self) -> Argb {
self.get_argb(&dynamic_colors().on_tertiary_fixed_variant())
}
}
fn dynamic_colors() -> &'static MaterialDynamicColors {
static DYNAMIC_COLORS: OnceLock<MaterialDynamicColors> = OnceLock::new();
DYNAMIC_COLORS.get_or_init(MaterialDynamicColors::new)
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
use crate::utils::color_utils::Argb;
#[test]
fn test_get_piecewise_value() {
let hct = Hct::from_argb(Argb(0xff0000ff)); let hue_breakpoints = [0.0, 100.0, 200.0, 300.0, 360.0];
let values = [10.0, 20.0, 30.0, 40.0];
assert_eq!(
DynamicScheme::get_piecewise_value(&hct, &hue_breakpoints, &values),
30.0
);
}
#[test]
fn test_get_rotated_hue() {
let hct = Hct::from_argb(Argb(0xff0000ff)); let hue_breakpoints = [0.0, 100.0, 200.0, 300.0, 360.0];
let rotations = [10.0, 20.0, -30.0, 40.0];
let expected_hue = MathUtils::sanitize_degrees_double(hct.hue() - 30.0);
let rotated = DynamicScheme::get_rotated_hue(&hct, &hue_breakpoints, &rotations);
assert!((rotated - expected_hue).abs() < 1e-4);
}
}