44from materialyoucolor .scheme .dynamic_scheme import DynamicScheme
55from materialyoucolor .dynamiccolor .contrast_curve import ContrastCurve
66from materialyoucolor .dynamiccolor .tone_delta_pair import ToneDeltaPair
7+ from materialyoucolor .utils .math_utils import clamp_double
78
9+ from dataclasses import dataclass
810
11+
12+ @dataclass
913class FromPaletteOptions :
10- def __init__ (
11- self ,
12- name = str ,
13- palette = None ,
14- tone = int ,
15- is_background = bool ,
16- background = None ,
17- second_background = None ,
18- contrast_curve = None ,
19- tone_delta_pair = None ,
20- ):
21- self .name = name
22- self .palette = palette
23- self .tone = tone
24- self .is_background = is_background
25- self .background = background
26- self .second_background = second_background
27- self .contrast_curve = contrast_curve
28- self .tone_delta_pair = tone_delta_pair
29-
30-
31- class DynamicColor :
32- hct_cache = dict [DynamicScheme , Hct ]
33- name = str
34- palette = None
35- tone = int
36- is_background = bool
37- background = None
38- second_background = None
39- contrast_curve = None
40- tone_delta_pair = None
41-
42- def __init__ (
43- self ,
44- name = str ,
45- palette = None ,
46- tone = int ,
47- is_background = bool ,
48- background = None ,
49- second_background = None ,
50- contrast_curve = None ,
51- tone_delta_pair = None ,
52- ):
53- self .name = name
54- self .palette = palette
55- self .tone = tone
56- self .is_background = is_background
57- self .background = background
58- self .second_background = second_background
59- self .contrast_curve = contrast_curve
60- self .tone_delta_pair = tone_delta_pair
14+ name : str = ""
15+ palette : TonalPalette = None
16+ tone : float = None
17+ is_background : bool = None
18+ background : object = None
19+ second_background : object = None
20+ contrast_curve : ContrastCurve = None
21+ tone_delta_pair : ToneDeltaPair = None
22+ chroma_multiplier : float = None
23+
24+
25+ @dataclass
26+ class DynamicColor (FromPaletteOptions ):
27+
28+ hct_cache = {}
29+ def __post_init__ (self ):
6130 self .hct_cache = {}
6231 if not self .background and self .second_background :
6332 raise ValueError (
@@ -74,16 +43,7 @@ def __init__(
7443
7544 @staticmethod
7645 def from_palette (args : FromPaletteOptions ):
77- return DynamicColor (
78- args .name ,
79- args .palette ,
80- args .tone ,
81- args .is_background ,
82- args .background ,
83- args .second_background ,
84- args .contrast_curve ,
85- args .tone_delta_pair ,
86- )
46+ return DynamicColor (** vars (args ))
8747
8848 def get_argb (self , scheme : DynamicScheme ) -> int :
8949 return self .get_hct (scheme ).to_int ()
@@ -101,129 +61,126 @@ def get_hct(self, scheme: DynamicScheme) -> Hct:
10161 return answer
10262
10363 def get_tone (self , scheme ):
104- decreasing_contrast = scheme .contrast_level < 0
105-
106- if self .tone_delta_pair :
107- tone_delta_pair = self .tone_delta_pair (scheme )
108- role_a , role_b = tone_delta_pair .role_a , tone_delta_pair .role_b
109- delta , polarity , stay_together = (
110- tone_delta_pair .delta ,
111- tone_delta_pair .polarity ,
112- tone_delta_pair .stay_together ,
113- )
64+ tone_delta_pair = self .tone_delta_pair (scheme ) if self .tone_delta_pair else None
65+ print (self .name )
66+ # Case 0: tone delta constraint
67+ if tone_delta_pair :
68+ role_a = tone_delta_pair .role_a
69+ role_b = tone_delta_pair .role_b
70+ polarity = tone_delta_pair .polarity
71+ constraint = tone_delta_pair .constraint
72+ delta = tone_delta_pair .delta
73+
74+ # Determine direction of tone shift
75+ if polarity in ("darker" ,) or (polarity == "relative_lighter" and scheme .is_dark ) or (
76+ polarity == "relative_darker" and not scheme .is_dark
77+ ):
78+ absolute_delta = - delta
79+ else :
80+ absolute_delta = delta
81+
82+ am_role_a = self .name == role_a .name
83+ self_role = role_a if am_role_a else role_b
84+ ref_role = role_b if am_role_a else role_a
85+
86+ self_tone = self_role .tone (scheme )
87+ ref_tone = ref_role .get_tone (scheme )
88+ relative_delta = absolute_delta * (1 if am_role_a else - 1 )
89+
90+ # Handle constraints
91+ if constraint == "exact" :
92+ self_tone = clamp_double (0 , 100 , ref_tone + relative_delta )
93+ elif constraint == "nearer" :
94+ if relative_delta > 0 :
95+ self_tone = clamp_double (
96+ 0 ,
97+ 100 ,
98+ clamp_double (ref_tone , ref_tone + relative_delta , self_tone ),
99+ )
100+ else :
101+ self_tone = clamp_double (
102+ 0 ,
103+ 100 ,
104+ clamp_double (ref_tone + relative_delta , ref_tone , self_tone ),
105+ )
106+ elif constraint == "farther" :
107+ if relative_delta > 0 :
108+ self_tone = clamp_double (ref_tone + relative_delta , 100 , self_tone )
109+ else :
110+ self_tone = clamp_double (0 , ref_tone + relative_delta , self_tone )
111+
112+ # Adjust for contrast curve if applicable
113+ if self .background and self .contrast_curve :
114+ background = self .background (scheme )
115+ contrast_curve = self .contrast_curve (scheme )
116+ if background and contrast_curve :
117+ bg_tone = background .get_tone (scheme )
118+ self_contrast = contrast_curve .get (scheme .contrast_level )
119+ if Contrast .ratio_of_tones (bg_tone , self_tone ) < self_contrast or scheme .contrast_level < 0 :
120+ self_tone = DynamicColor .foreground_tone (bg_tone , self_contrast )
121+
122+ # Avoid awkward tones for background colors
123+ if self .is_background and not self .name .endswith ("_fixed_dim" ):
124+ if self_tone >= 57 :
125+ self_tone = clamp_double (65 , 100 , self_tone )
126+ else :
127+ self_tone = clamp_double (0 , 49 , self_tone )
114128
115- bg = self .background (scheme )
116- bg_tone = bg .get_tone (scheme )
129+ return self_tone
117130
118- a_is_nearer = (
119- polarity == "nearer"
120- or (polarity == "lighter" and not scheme .is_dark )
121- or (polarity == "darker" and scheme .is_dark )
122- )
123- nearer , farther = (role_a , role_b ) if a_is_nearer else (role_b , role_a )
124- am_nearer = self .name == nearer .name
125- expansion_dir = 1 if scheme .is_dark else - 1
126-
127- n_contrast = nearer .contrast_curve .get (scheme .contrast_level )
128- f_contrast = farther .contrast_curve .get (scheme .contrast_level )
129-
130- n_initial_tone = nearer .tone (scheme )
131- n_tone = (
132- n_initial_tone
133- if Contrast .ratio_of_tones (bg_tone , n_initial_tone ) >= n_contrast
134- else DynamicColor .foreground_tone (bg_tone , n_contrast )
135- )
131+ # Case 1: no tone delta pair; solve self
132+ answer = self .tone (scheme )
136133
137- f_initial_tone = farther .tone (scheme )
138- f_tone = (
139- f_initial_tone
140- if Contrast .ratio_of_tones (bg_tone , f_initial_tone ) >= f_contrast
141- else DynamicColor .foreground_tone (bg_tone , f_contrast )
142- )
134+ if (
135+ not self .background
136+ or not self .contrast_curve
137+ ):
138+ return answer
139+
140+ bg_tone = self .background (scheme ).get_tone (scheme )
141+ desired_ratio = self .contrast_curve .get (scheme .contrast_level )
143142
144- if decreasing_contrast :
145- n_tone = DynamicColor .foreground_tone (bg_tone , n_contrast )
146- f_tone = DynamicColor .foreground_tone (bg_tone , f_contrast )
143+ if Contrast .ratio_of_tones (bg_tone , answer ) < desired_ratio or scheme .contrast_level < 0 :
144+ answer = DynamicColor .foreground_tone (bg_tone , desired_ratio )
147145
148- if (f_tone - n_tone ) * expansion_dir >= delta :
149- pass
146+ if self .is_background and not self .name .endswith ("_fixed_dim" ):
147+ if answer >= 57 :
148+ answer = clamp_double (65 , 100 , answer )
150149 else :
151- f_tone = (
152- min (max (n_tone + delta * expansion_dir , 0 ), 100 )
153- if (f_tone - n_tone ) * expansion_dir >= delta
154- else min (max (f_tone - delta * expansion_dir , 0 ), 100 )
155- )
156-
157- if 50 <= n_tone < 60 :
158- if expansion_dir > 0 :
159- n_tone , f_tone = 60 , max (f_tone , n_tone + delta * expansion_dir )
160- else :
161- n_tone , f_tone = 49 , min (f_tone , n_tone + delta * expansion_dir )
162- elif 50 <= f_tone < 60 :
163- if stay_together :
164- if expansion_dir > 0 :
165- n_tone , f_tone = 60 , max (f_tone , n_tone + delta * expansion_dir )
166- else :
167- n_tone , f_tone = 49 , min (f_tone , n_tone + delta * expansion_dir )
168- else :
169- if expansion_dir > 0 :
170- f_tone = 60
171- else :
172- f_tone = 49
150+ answer = clamp_double (0 , 49 , answer )
173151
174- return n_tone if am_nearer else f_tone
152+ if not self .second_background or not self .second_background (scheme ):
153+ return answer
175154
176- else :
177- answer = self .tone (scheme )
155+ # Case 2: dual backgrounds
156+ bg1 = self .background
157+ bg2 = self .second_background
158+ bg_tone1 = bg1 (scheme ).get_tone (scheme )
159+ bg_tone2 = bg2 (scheme ).get_tone (scheme )
160+ upper , lower = max (bg_tone1 , bg_tone2 ), min (bg_tone1 , bg_tone2 )
161+
162+ if (
163+ Contrast .ratio_of_tones (upper , answer ) >= desired_ratio
164+ and Contrast .ratio_of_tones (lower , answer ) >= desired_ratio
165+ ):
166+ return answer
178167
179- if self . background is None :
180- return answer
168+ light_option = Contrast . lighter ( upper , desired_ratio )
169+ dark_option = Contrast . darker ( lower , desired_ratio )
181170
182- bg_tone = self .background (scheme ).get_tone (scheme )
183- desired_ratio = self .contrast_curve .get (scheme .contrast_level )
171+ availables = []
172+ if light_option != - 1 :
173+ availables .append (light_option )
174+ if dark_option != - 1 :
175+ availables .append (dark_option )
184176
185- if Contrast .ratio_of_tones (bg_tone , answer ) >= desired_ratio :
186- pass
187- else :
188- answer = DynamicColor .foreground_tone (bg_tone , desired_ratio )
189-
190- if decreasing_contrast :
191- answer = DynamicColor .foreground_tone (bg_tone , desired_ratio )
192-
193- if self .is_background and 50 <= answer < 60 :
194- answer = (
195- 49 if Contrast .ratio_of_tones (49 , bg_tone ) >= desired_ratio else 60
196- )
197-
198- if self .second_background :
199- bg1 , bg2 = self .background , self .second_background
200- bg_tone1 , bg_tone2 = bg1 (scheme ).get_tone (scheme ), bg2 (scheme ).get_tone (
201- scheme
202- )
203- upper , lower = max (bg_tone1 , bg_tone2 ), min (bg_tone1 , bg_tone2 )
204-
205- if (
206- Contrast .ratio_of_tones (upper , answer ) >= desired_ratio
207- and Contrast .ratio_of_tones (lower , answer ) >= desired_ratio
208- ):
209- return answer
210-
211- light_option = Contrast .lighter (upper , desired_ratio )
212- dark_option = Contrast .darker (lower , desired_ratio )
213- availables = [light_option ] if light_option != - 1 else []
214- if dark_option != - 1 :
215- availables .append (dark_option )
216-
217- prefers_light = DynamicColor .tone_prefers_light_foreground (
218- bg_tone1
219- ) or DynamicColor .tone_prefers_light_foreground (bg_tone2 )
220- return (
221- light_option
222- if prefers_light and (light_option == - 1 or dark_option == - 1 )
223- else dark_option
224- )
177+ prefers_light = DynamicColor .tone_prefers_light_foreground (bg_tone1 ) or DynamicColor .tone_prefers_light_foreground (bg_tone2 )
178+ if prefers_light :
179+ return 100 if light_option < 0 else light_option
180+ if len (availables ) == 1 :
181+ return availables [0 ]
182+ return 0 if dark_option < 0 else dark_option
225183
226- return answer
227184
228185 @staticmethod
229186 def foreground_tone (bg_tone , ratio ):
0 commit comments