10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
- import { clamp } from '@react-aria /utils' ;
13
+ import { clamp , toFixedNumber } from '@react-stately /utils' ;
14
14
import { ColorChannel , ColorChannelRange , ColorFormat , Color as IColor } from '@react-types/color' ;
15
15
// @ts -ignore
16
16
import intlMessages from '../intl/*.json' ;
@@ -32,7 +32,7 @@ export function parseColor(value: string): IColor {
32
32
abstract class Color implements IColor {
33
33
abstract toFormat ( format : ColorFormat ) : IColor ;
34
34
abstract toString ( format : ColorFormat | 'css' ) : string ;
35
- abstract clone ( ) : Color ;
35
+ abstract clone ( ) : IColor ;
36
36
abstract getChannelRange ( channel : ColorChannel ) : ColorChannelRange ;
37
37
abstract formatChannelValue ( channel : ColorChannel , locale : string ) : string ;
38
38
@@ -61,6 +61,8 @@ abstract class Color implements IColor {
61
61
getChannelName ( channel : ColorChannel , locale : string ) {
62
62
return messages . getStringForLocale ( channel , locale ) ;
63
63
}
64
+
65
+ abstract getColorSpace ( ) : ColorFormat
64
66
}
65
67
66
68
const HEX_REGEX = / ^ # (?: ( [ 0 - 9 a - f ] { 3 } ) | ( [ 0 - 9 a - f ] { 6 } ) ) $ / i;
@@ -90,7 +92,9 @@ class RGBColor extends Color {
90
92
let b = parseInt ( m [ 2 ] [ 4 ] + m [ 2 ] [ 5 ] , 16 ) ;
91
93
return new RGBColor ( r , g , b , 1 ) ;
92
94
}
93
- } if ( ( m = value . match ( RGB_REGEX ) ) ) {
95
+ }
96
+
97
+ if ( ( m = value . match ( RGB_REGEX ) ) ) {
94
98
const [ r , g , b , a ] = ( m [ 1 ] ?? m [ 2 ] ) . split ( ',' ) . map ( n => Number ( n . trim ( ) ) ) ;
95
99
return new RGBColor ( clamp ( r , 0 , 255 ) , clamp ( g , 0 , 255 ) , clamp ( b , 0 , 255 ) , clamp ( a ?? 1 , 0 , 1 ) ) ;
96
100
}
@@ -119,6 +123,12 @@ class RGBColor extends Color {
119
123
case 'rgb' :
120
124
case 'rgba' :
121
125
return this ;
126
+ case 'hsb' :
127
+ case 'hsba' :
128
+ return this . toHSB ( ) ;
129
+ case 'hsl' :
130
+ case 'hsla' :
131
+ return this . toHSL ( ) ;
122
132
default :
123
133
throw new Error ( 'Unsupported color conversion: rgb -> ' + format ) ;
124
134
}
@@ -128,7 +138,89 @@ class RGBColor extends Color {
128
138
return this . red << 16 | this . green << 8 | this . blue ;
129
139
}
130
140
131
- clone ( ) : Color {
141
+ /**
142
+ * Converts an RGB color value to HSB.
143
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
144
+ * @returns An HSBColor object.
145
+ */
146
+ private toHSB ( ) : IColor {
147
+ const red = this . red / 255 ;
148
+ const green = this . green / 255 ;
149
+ const blue = this . blue / 255 ;
150
+ const min = Math . min ( red , green , blue ) ;
151
+ const brightness = Math . max ( red , green , blue ) ;
152
+ const chroma = brightness - min ;
153
+ const saturation = brightness === 0 ? 0 : chroma / brightness ;
154
+ let hue = 0 ; // achromatic
155
+
156
+ if ( chroma !== 0 ) {
157
+ switch ( brightness ) {
158
+ case red :
159
+ hue = ( green - blue ) / chroma + ( green < blue ? 6 : 0 ) ;
160
+ break ;
161
+ case green :
162
+ hue = ( blue - red ) / chroma + 2 ;
163
+ break ;
164
+ case blue :
165
+ hue = ( red - green ) / chroma + 4 ;
166
+ break ;
167
+ }
168
+
169
+ hue /= 6 ;
170
+ }
171
+
172
+ return new HSBColor (
173
+ toFixedNumber ( hue * 360 , 2 ) ,
174
+ toFixedNumber ( saturation * 100 , 2 ) ,
175
+ toFixedNumber ( brightness * 100 , 2 ) ,
176
+ this . alpha
177
+ ) ;
178
+ }
179
+
180
+ /**
181
+ * Converts an RGB color value to HSL.
182
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB.
183
+ * @returns An HSLColor object.
184
+ */
185
+ private toHSL ( ) : IColor {
186
+ const red = this . red / 255 ;
187
+ const green = this . green / 255 ;
188
+ const blue = this . blue / 255 ;
189
+ const min = Math . min ( red , green , blue ) ;
190
+ const max = Math . max ( red , green , blue ) ;
191
+ const lightness = ( max + min ) / 2 ;
192
+ const chroma = max - min ;
193
+ let hue : number ;
194
+ let saturation : number ;
195
+
196
+ if ( chroma === 0 ) {
197
+ hue = saturation = 0 ; // achromatic
198
+ } else {
199
+ saturation = chroma / ( lightness < .5 ? max + min : 2 - max - min ) ;
200
+
201
+ switch ( max ) {
202
+ case red :
203
+ hue = ( green - blue ) / chroma + ( green < blue ? 6 : 0 ) ;
204
+ break ;
205
+ case green :
206
+ hue = ( blue - red ) / chroma + 2 ;
207
+ break ;
208
+ case blue :
209
+ hue = ( red - green ) / chroma + 4 ;
210
+ break ;
211
+ }
212
+
213
+ hue /= 6 ;
214
+ }
215
+
216
+ return new HSLColor (
217
+ toFixedNumber ( hue * 360 , 2 ) ,
218
+ toFixedNumber ( saturation * 100 , 2 ) ,
219
+ toFixedNumber ( lightness * 100 , 2 ) ,
220
+ this . alpha ) ;
221
+ }
222
+
223
+ clone ( ) : IColor {
132
224
return new RGBColor ( this . red , this . green , this . blue , this . alpha ) ;
133
225
}
134
226
@@ -162,6 +254,10 @@ class RGBColor extends Color {
162
254
}
163
255
return new NumberFormatter ( locale , options ) . format ( value ) ;
164
256
}
257
+
258
+ getColorSpace ( ) : ColorFormat {
259
+ return 'rgb' ;
260
+ }
165
261
}
166
262
167
263
// X = <negative/positive number with/without decimal places>
@@ -187,10 +283,14 @@ class HSBColor extends Color {
187
283
switch ( format ) {
188
284
case 'css' :
189
285
return this . toHSL ( ) . toString ( 'css' ) ;
286
+ case 'hex' :
287
+ return this . toRGB ( ) . toString ( 'hex' ) ;
288
+ case 'hexa' :
289
+ return this . toRGB ( ) . toString ( 'hexa' ) ;
190
290
case 'hsb' :
191
- return `hsb(${ this . hue } , ${ this . saturation } %, ${ this . brightness } %)` ;
291
+ return `hsb(${ this . hue } , ${ toFixedNumber ( this . saturation , 2 ) } %, ${ toFixedNumber ( this . brightness , 2 ) } %)` ;
192
292
case 'hsba' :
193
- return `hsba(${ this . hue } , ${ this . saturation } %, ${ this . brightness } %, ${ this . alpha } )` ;
293
+ return `hsba(${ this . hue } , ${ toFixedNumber ( this . saturation , 2 ) } %, ${ toFixedNumber ( this . brightness , 2 ) } %, ${ this . alpha } )` ;
194
294
default :
195
295
return this . toFormat ( format ) . toString ( format ) ;
196
296
}
@@ -204,29 +304,52 @@ class HSBColor extends Color {
204
304
case 'hsl' :
205
305
case 'hsla' :
206
306
return this . toHSL ( ) ;
307
+ case 'rgb' :
308
+ case 'rgba' :
309
+ return this . toRGB ( ) ;
207
310
default :
208
311
throw new Error ( 'Unsupported color conversion: hsb -> ' + format ) ;
209
312
}
210
313
}
211
314
212
- private toHSL ( ) : Color {
213
- // determine the lightness in the range [0,100]
214
- var l = ( 2 - this . saturation / 100 ) * this . brightness / 2 ;
315
+ /**
316
+ * Converts a HSB color to HSL.
317
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL.
318
+ * @returns An HSLColor object.
319
+ */
320
+ private toHSL ( ) : IColor {
321
+ let saturation = this . saturation / 100 ;
322
+ let brightness = this . brightness / 100 ;
323
+ let lightness = brightness * ( 1 - saturation / 2 ) ;
324
+ saturation = lightness === 0 || lightness === 1 ? 0 : ( brightness - lightness ) / Math . min ( lightness , 1 - lightness ) ;
325
+
326
+ return new HSLColor (
327
+ toFixedNumber ( this . hue , 2 ) ,
328
+ toFixedNumber ( saturation * 100 , 2 ) ,
329
+ toFixedNumber ( lightness * 100 , 2 ) ,
330
+ this . alpha
331
+ ) ;
332
+ }
215
333
216
- // store the HSL components
334
+ /**
335
+ * Converts a HSV color value to RGB.
336
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative.
337
+ * @returns An RGBColor object.
338
+ */
339
+ private toRGB ( ) : IColor {
217
340
let hue = this . hue ;
218
- let saturation = this . saturation * this . brightness / ( l < 50 ? l * 2 : 200 - l * 2 ) ;
219
- let lightness = l ;
220
-
221
- // correct a division-by-zero error
222
- if ( isNaN ( saturation ) ) {
223
- saturation = 0 ;
224
- }
225
-
226
- return new HSLColor ( hue , saturation , lightness , this . alpha ) ;
341
+ let saturation = this . saturation / 100 ;
342
+ let brightness = this . brightness / 100 ;
343
+ let fn = ( n : number , k = ( n + hue / 60 ) % 6 ) => brightness - saturation * brightness * Math . max ( Math . min ( k , 4 - k , 1 ) , 0 ) ;
344
+ return new RGBColor (
345
+ Math . round ( fn ( 5 ) * 255 ) ,
346
+ Math . round ( fn ( 3 ) * 255 ) ,
347
+ Math . round ( fn ( 1 ) * 255 ) ,
348
+ this . alpha
349
+ ) ;
227
350
}
228
351
229
- clone ( ) : Color {
352
+ clone ( ) : IColor {
230
353
return new HSBColor ( this . hue , this . saturation , this . brightness , this . alpha ) ;
231
354
}
232
355
@@ -264,6 +387,10 @@ class HSBColor extends Color {
264
387
}
265
388
return new NumberFormatter ( locale , options ) . format ( value ) ;
266
389
}
390
+
391
+ getColorSpace ( ) : ColorFormat {
392
+ return 'hsb' ;
393
+ }
267
394
}
268
395
269
396
// X = <negative/positive number with/without decimal places>
@@ -276,13 +403,11 @@ function mod(n, m) {
276
403
return ( ( n % m ) + m ) % m ;
277
404
}
278
405
279
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
280
406
class HSLColor extends Color {
281
407
constructor ( private hue : number , private saturation : number , private lightness : number , private alpha : number ) {
282
408
super ( ) ;
283
409
}
284
410
285
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
286
411
static parse ( value : string ) : HSLColor | void {
287
412
let m : RegExpMatchArray | void ;
288
413
if ( ( m = value . match ( HSL_REGEX ) ) ) {
@@ -293,27 +418,73 @@ class HSLColor extends Color {
293
418
294
419
toString ( format : ColorFormat | 'css' ) {
295
420
switch ( format ) {
421
+ case 'hex' :
422
+ return this . toRGB ( ) . toString ( 'hex' ) ;
423
+ case 'hexa' :
424
+ return this . toRGB ( ) . toString ( 'hexa' ) ;
296
425
case 'hsl' :
297
- return `hsl(${ this . hue } , ${ this . saturation } %, ${ this . lightness } %)` ;
426
+ return `hsl(${ this . hue } , ${ toFixedNumber ( this . saturation , 2 ) } %, ${ toFixedNumber ( this . lightness , 2 ) } %)` ;
298
427
case 'css' :
299
428
case 'hsla' :
300
- return `hsla(${ this . hue } , ${ this . saturation } %, ${ this . lightness } %, ${ this . alpha } )` ;
429
+ return `hsla(${ this . hue } , ${ toFixedNumber ( this . saturation , 2 ) } %, ${ toFixedNumber ( this . lightness , 2 ) } %, ${ this . alpha } )` ;
301
430
default :
302
431
return this . toFormat ( format ) . toString ( format ) ;
303
432
}
304
433
}
305
-
306
434
toFormat ( format : ColorFormat ) : IColor {
307
435
switch ( format ) {
308
436
case 'hsl' :
309
437
case 'hsla' :
310
438
return this ;
439
+ case 'hsb' :
440
+ case 'hsba' :
441
+ return this . toHSB ( ) ;
442
+ case 'rgb' :
443
+ case 'rgba' :
444
+ return this . toRGB ( ) ;
311
445
default :
312
446
throw new Error ( 'Unsupported color conversion: hsl -> ' + format ) ;
313
447
}
314
448
}
315
449
316
- clone ( ) : Color {
450
+ /**
451
+ * Converts a HSL color to HSB.
452
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV.
453
+ * @returns An HSBColor object.
454
+ */
455
+ private toHSB ( ) : IColor {
456
+ let saturation = this . saturation / 100 ;
457
+ let lightness = this . lightness / 100 ;
458
+ let brightness = lightness + saturation * Math . min ( lightness , 1 - lightness ) ;
459
+ saturation = brightness === 0 ? 0 : 2 * ( 1 - lightness / brightness ) ;
460
+ return new HSBColor (
461
+ toFixedNumber ( this . hue , 2 ) ,
462
+ toFixedNumber ( saturation * 100 , 2 ) ,
463
+ toFixedNumber ( brightness * 100 , 2 ) ,
464
+ this . alpha
465
+ ) ;
466
+ }
467
+
468
+ /**
469
+ * Converts a HSL color to RGB.
470
+ * Conversion formula adapted from https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative.
471
+ * @returns An RGBColor object.
472
+ */
473
+ private toRGB ( ) : IColor {
474
+ let hue = this . hue ;
475
+ let saturation = this . saturation / 100 ;
476
+ let lightness = this . lightness / 100 ;
477
+ let a = saturation * Math . min ( lightness , 1 - lightness ) ;
478
+ let fn = ( n : number , k = ( n + hue / 30 ) % 12 ) => lightness - a * Math . max ( Math . min ( k - 3 , 9 - k , 1 ) , - 1 ) ;
479
+ return new RGBColor (
480
+ Math . round ( fn ( 0 ) * 255 ) ,
481
+ Math . round ( fn ( 8 ) * 255 ) ,
482
+ Math . round ( fn ( 4 ) * 255 ) ,
483
+ this . alpha
484
+ ) ;
485
+ }
486
+
487
+ clone ( ) : IColor {
317
488
return new HSLColor ( this . hue , this . saturation , this . lightness , this . alpha ) ;
318
489
}
319
490
@@ -351,4 +522,8 @@ class HSLColor extends Color {
351
522
}
352
523
return new NumberFormatter ( locale , options ) . format ( value ) ;
353
524
}
525
+
526
+ getColorSpace ( ) : ColorFormat {
527
+ return 'hsl' ;
528
+ }
354
529
}
0 commit comments