@@ -102,6 +102,137 @@ impl HslRepresentation {
102
102
}
103
103
}
104
104
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
+
105
236
#[ cfg( test) ]
106
237
mod test {
107
238
use super :: * ;
@@ -214,4 +345,90 @@ mod test {
214
345
assert_eq ! ( ( saturation * 100.0 ) . round( ) as u32 , 83 ) ;
215
346
assert_eq ! ( ( lightness * 100.0 ) . round( ) as u32 , 51 ) ;
216
347
}
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
+ }
217
434
}
0 commit comments