@@ -102,6 +102,137 @@ impl HslRepresentation {
102102 }
103103}
104104
105+ pub struct LchRepresentation ;
106+ impl LchRepresentation {
107+ // References available at http://brucelindbloom.com/ in the "Math" section
108+
109+ // CIE Constants
110+ // http://brucelindbloom.com/index.html?LContinuity.html (16) (17)
111+ const CIE_EPSILON : f32 = 216.0 / 24389.0 ;
112+ const CIE_KAPPA : f32 = 24389.0 / 27.0 ;
113+ // D65 White Reference:
114+ // https://en.wikipedia.org/wiki/Illuminant_D65#Definition
115+ const D65_WHITE_X : f32 = 0.95047 ;
116+ const D65_WHITE_Y : f32 = 1.0 ;
117+ const D65_WHITE_Z : f32 = 1.08883 ;
118+
119+ /// converts a color in LCH space to sRGB space
120+ #[ inline]
121+ pub fn lch_to_nonlinear_srgb ( lightness : f32 , chroma : f32 , hue : f32 ) -> [ f32 ; 3 ] {
122+ let lightness = lightness * 100.0 ;
123+ let chroma = chroma * 100.0 ;
124+
125+ // convert LCH to Lab
126+ // http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html
127+ let l = lightness;
128+ let a = chroma * hue. to_radians ( ) . cos ( ) ;
129+ let b = chroma * hue. to_radians ( ) . sin ( ) ;
130+
131+ // convert Lab to XYZ
132+ // http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
133+ let fy = ( l + 16.0 ) / 116.0 ;
134+ let fx = a / 500.0 + fy;
135+ let fz = fy - b / 200.0 ;
136+ let xr = {
137+ let fx3 = fx. powf ( 3.0 ) ;
138+
139+ if fx3 > Self :: CIE_EPSILON {
140+ fx3
141+ } else {
142+ ( 116.0 * fx - 16.0 ) / Self :: CIE_KAPPA
143+ }
144+ } ;
145+ let yr = if l > Self :: CIE_EPSILON * Self :: CIE_KAPPA {
146+ ( ( l + 16.0 ) / 116.0 ) . powf ( 3.0 )
147+ } else {
148+ l / Self :: CIE_KAPPA
149+ } ;
150+ let zr = {
151+ let fz3 = fz. powf ( 3.0 ) ;
152+
153+ if fz3 > Self :: CIE_EPSILON {
154+ fz3
155+ } else {
156+ ( 116.0 * fz - 16.0 ) / Self :: CIE_KAPPA
157+ }
158+ } ;
159+ let x = xr * Self :: D65_WHITE_X ;
160+ let y = yr * Self :: D65_WHITE_Y ;
161+ let z = zr * Self :: D65_WHITE_Z ;
162+
163+ // XYZ to sRGB
164+ // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
165+ // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, XYZ to RGB [M]-1)
166+ let red = x * 3.2404542 + y * -1.5371385 + z * -0.4985314 ;
167+ let green = x * -0.969266 + y * 1.8760108 + z * 0.041556 ;
168+ let blue = x * 0.0556434 + y * -0.2040259 + z * 1.0572252 ;
169+
170+ [
171+ red. linear_to_nonlinear_srgb ( ) . max ( 0.0 ) . min ( 1.0 ) ,
172+ green. linear_to_nonlinear_srgb ( ) . max ( 0.0 ) . min ( 1.0 ) ,
173+ blue. linear_to_nonlinear_srgb ( ) . max ( 0.0 ) . min ( 1.0 ) ,
174+ ]
175+ }
176+
177+ /// converts a color in sRGB space to LCH space
178+ #[ inline]
179+ pub fn nonlinear_srgb_to_lch ( [ red, green, blue] : [ f32 ; 3 ] ) -> ( f32 , f32 , f32 ) {
180+ // RGB to XYZ
181+ // http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
182+ let red = red. nonlinear_to_linear_srgb ( ) ;
183+ let green = green. nonlinear_to_linear_srgb ( ) ;
184+ let blue = blue. nonlinear_to_linear_srgb ( ) ;
185+
186+ // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, RGB to XYZ [M])
187+ let x = red * 0.4124564 + green * 0.3575761 + blue * 0.1804375 ;
188+ let y = red * 0.2126729 + green * 0.7151522 + blue * 0.072175 ;
189+ let z = red * 0.0193339 + green * 0.119192 + blue * 0.9503041 ;
190+
191+ // XYZ to Lab
192+ // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
193+ let xr = x / Self :: D65_WHITE_X ;
194+ let yr = y / Self :: D65_WHITE_Y ;
195+ let zr = z / Self :: D65_WHITE_Z ;
196+ let fx = if xr > Self :: CIE_EPSILON {
197+ xr. cbrt ( )
198+ } else {
199+ ( Self :: CIE_KAPPA * xr + 16.0 ) / 116.0
200+ } ;
201+ let fy = if yr > Self :: CIE_EPSILON {
202+ yr. cbrt ( )
203+ } else {
204+ ( Self :: CIE_KAPPA * yr + 16.0 ) / 116.0
205+ } ;
206+ let fz = if yr > Self :: CIE_EPSILON {
207+ zr. cbrt ( )
208+ } else {
209+ ( Self :: CIE_KAPPA * zr + 16.0 ) / 116.0
210+ } ;
211+ let l = 116.0 * fy - 16.0 ;
212+ let a = 500.0 * ( fx - fy) ;
213+ let b = 200.0 * ( fy - fz) ;
214+
215+ // Lab to LCH
216+ // http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
217+ let c = ( a. powf ( 2.0 ) + b. powf ( 2.0 ) ) . sqrt ( ) ;
218+ let h = {
219+ let h = b. to_radians ( ) . atan2 ( a. to_radians ( ) ) . to_degrees ( ) ;
220+
221+ if h < 0.0 {
222+ h + 360.0
223+ } else {
224+ h
225+ }
226+ } ;
227+
228+ (
229+ ( l / 100.0 ) . max ( 0.0 ) . min ( 1.5 ) ,
230+ ( c / 100.0 ) . max ( 0.0 ) . min ( 1.5 ) ,
231+ h,
232+ )
233+ }
234+ }
235+
105236#[ cfg( test) ]
106237mod test {
107238 use super :: * ;
@@ -214,4 +345,90 @@ mod test {
214345 assert_eq ! ( ( saturation * 100.0 ) . round( ) as u32 , 83 ) ;
215346 assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 51 ) ;
216347 }
348+
349+ #[ test]
350+ fn lch_to_srgb ( ) {
351+ // "truth" from http://www.brucelindbloom.com/ColorCalculator.html
352+
353+ // black
354+ let ( lightness, chroma, hue) = ( 0.0 , 0.0 , 0.0 ) ;
355+ let [ r, g, b] = LchRepresentation :: lch_to_nonlinear_srgb ( lightness, chroma, hue) ;
356+ assert_eq ! ( ( r * 100.0 ) . round( ) as u32 , 0 ) ;
357+ assert_eq ! ( ( g * 100.0 ) . round( ) as u32 , 0 ) ;
358+ assert_eq ! ( ( b * 100.0 ) . round( ) as u32 , 0 ) ;
359+
360+ // white
361+ let ( lightness, chroma, hue) = ( 1.0 , 0.0 , 0.0 ) ;
362+ let [ r, g, b] = LchRepresentation :: lch_to_nonlinear_srgb ( lightness, chroma, hue) ;
363+ assert_eq ! ( ( r * 100.0 ) . round( ) as u32 , 100 ) ;
364+ assert_eq ! ( ( g * 100.0 ) . round( ) as u32 , 100 ) ;
365+ assert_eq ! ( ( b * 100.0 ) . round( ) as u32 , 100 ) ;
366+
367+ let ( lightness, chroma, hue) = ( 0.501236 , 0.777514 , 327.6608 ) ;
368+ let [ r, g, b] = LchRepresentation :: lch_to_nonlinear_srgb ( lightness, chroma, hue) ;
369+ assert_eq ! ( ( r * 100.0 ) . round( ) as u32 , 75 ) ;
370+ assert_eq ! ( ( g * 100.0 ) . round( ) as u32 , 25 ) ;
371+ assert_eq ! ( ( b * 100.0 ) . round( ) as u32 , 75 ) ;
372+
373+ // a red
374+ let ( lightness, chroma, hue) = ( 0.487122 , 0.999531 , 318.7684 ) ;
375+ let [ r, g, b] = LchRepresentation :: lch_to_nonlinear_srgb ( lightness, chroma, hue) ;
376+ assert_eq ! ( ( r * 100.0 ) . round( ) as u32 , 70 ) ;
377+ assert_eq ! ( ( g * 100.0 ) . round( ) as u32 , 19 ) ;
378+ assert_eq ! ( ( b * 100.0 ) . round( ) as u32 , 90 ) ;
379+
380+ // a green
381+ let ( lightness, chroma, hue) = ( 0.732929 , 0.560925 , 164.3216 ) ;
382+ let [ r, g, b] = LchRepresentation :: lch_to_nonlinear_srgb ( lightness, chroma, hue) ;
383+ assert_eq ! ( ( r * 100.0 ) . round( ) as u32 , 10 ) ;
384+ assert_eq ! ( ( g * 100.0 ) . round( ) as u32 , 80 ) ;
385+ assert_eq ! ( ( b * 100.0 ) . round( ) as u32 , 59 ) ;
386+
387+ // a blue
388+ let ( lightness, chroma, hue) = ( 0.335030 , 1.176923 , 306.7828 ) ;
389+ let [ r, g, b] = LchRepresentation :: lch_to_nonlinear_srgb ( lightness, chroma, hue) ;
390+ assert_eq ! ( ( r * 100.0 ) . round( ) as u32 , 25 ) ;
391+ assert_eq ! ( ( g * 100.0 ) . round( ) as u32 , 10 ) ;
392+ assert_eq ! ( ( b * 100.0 ) . round( ) as u32 , 92 ) ;
393+ }
394+
395+ #[ test]
396+ fn srgb_to_lch ( ) {
397+ // "truth" from http://www.brucelindbloom.com/ColorCalculator.html
398+
399+ // black
400+ let ( lightness, chroma, hue) = LchRepresentation :: nonlinear_srgb_to_lch ( [ 0.0 , 0.0 , 0.0 ] ) ;
401+ assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 0 ) ;
402+ assert_eq ! ( ( chroma * 100.0 ) . round( ) as u32 , 0 ) ;
403+ assert_eq ! ( hue. round( ) as u32 , 0 ) ;
404+
405+ // white
406+ let ( lightness, chroma, hue) = LchRepresentation :: nonlinear_srgb_to_lch ( [ 1.0 , 1.0 , 1.0 ] ) ;
407+ assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 100 ) ;
408+ assert_eq ! ( ( chroma * 100.0 ) . round( ) as u32 , 0 ) ;
409+ assert_eq ! ( hue. round( ) as u32 , 0 ) ;
410+
411+ let ( lightness, chroma, hue) = LchRepresentation :: nonlinear_srgb_to_lch ( [ 0.75 , 0.25 , 0.75 ] ) ;
412+ assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 50 ) ;
413+ assert_eq ! ( ( chroma * 100.0 ) . round( ) as u32 , 78 ) ;
414+ assert_eq ! ( hue. round( ) as u32 , 328 ) ;
415+
416+ // a red
417+ let ( lightness, chroma, hue) = LchRepresentation :: nonlinear_srgb_to_lch ( [ 0.70 , 0.19 , 0.90 ] ) ;
418+ assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 49 ) ;
419+ assert_eq ! ( ( chroma * 100.0 ) . round( ) as u32 , 100 ) ;
420+ assert_eq ! ( hue. round( ) as u32 , 319 ) ;
421+
422+ // a green
423+ let ( lightness, chroma, hue) = LchRepresentation :: nonlinear_srgb_to_lch ( [ 0.10 , 0.80 , 0.59 ] ) ;
424+ assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 73 ) ;
425+ assert_eq ! ( ( chroma * 100.0 ) . round( ) as u32 , 56 ) ;
426+ assert_eq ! ( hue. round( ) as u32 , 164 ) ;
427+
428+ // a blue
429+ let ( lightness, chroma, hue) = LchRepresentation :: nonlinear_srgb_to_lch ( [ 0.25 , 0.10 , 0.92 ] ) ;
430+ assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 34 ) ;
431+ assert_eq ! ( ( chroma * 100.0 ) . round( ) as u32 , 118 ) ;
432+ assert_eq ! ( hue. round( ) as u32 , 307 ) ;
433+ }
217434}
0 commit comments