11import { Colorjs } from '../../core/imports' ;
22
33const hexRegex = / ^ # [ 0 - 9 a - f ] { 3 , 8 } $ / i;
4- const hslRegex = / h s l \( \s * ( [ \d . ] + ) ( r a d | t u r n ) / ;
54
65/**
76 * @class Color
@@ -12,7 +11,26 @@ const hslRegex = /hsl\(\s*([\d.]+)(rad|turn)/;
1211 * @param {number } alpha
1312 */
1413export default class Color {
14+ // color channel values typically in the range of 0-1 (can go below or above)
15+ #r;
16+ #g;
17+ #b;
18+ // color component values resolved to the sRGB color space (0-255)
19+ #red;
20+ #green;
21+ #blue;
22+
1523 constructor ( red , green , blue , alpha = 1 ) {
24+ if ( red instanceof Color ) {
25+ // preserve out of gamut values
26+ const { r, g, b } = red ;
27+ this . r = r ;
28+ this . g = g ;
29+ this . b = b ;
30+ this . alpha = red . alpha ;
31+ return ;
32+ }
33+
1634 /** @type {number } */
1735 this . red = red ;
1836
@@ -26,6 +44,60 @@ export default class Color {
2644 this . alpha = alpha ;
2745 }
2846
47+ get r ( ) {
48+ return this . #r;
49+ }
50+
51+ set r ( value ) {
52+ this . #r = value ;
53+ this . #red = Math . round ( clamp ( value , 0 , 1 ) * 255 ) ;
54+ }
55+
56+ get g ( ) {
57+ return this . #g;
58+ }
59+
60+ set g ( value ) {
61+ this . #g = value ;
62+ this . #green = Math . round ( clamp ( value , 0 , 1 ) * 255 ) ;
63+ }
64+
65+ get b ( ) {
66+ return this . #b;
67+ }
68+
69+ set b ( value ) {
70+ this . #b = value ;
71+ this . #blue = Math . round ( clamp ( value , 0 , 1 ) * 255 ) ;
72+ }
73+
74+ get red ( ) {
75+ return this . #red;
76+ }
77+
78+ set red ( value ) {
79+ this . #r = value / 255 ;
80+ this . #red = clamp ( value , 0 , 255 ) ;
81+ }
82+
83+ get green ( ) {
84+ return this . #green;
85+ }
86+
87+ set green ( value ) {
88+ this . #g = value / 255 ;
89+ this . #green = clamp ( value , 0 , 255 ) ;
90+ }
91+
92+ get blue ( ) {
93+ return this . #blue;
94+ }
95+
96+ set blue ( value ) {
97+ this . #b = value / 255 ;
98+ this . #blue = clamp ( value , 0 , 255 ) ;
99+ }
100+
29101 /**
30102 * Provide the hex string value for the color
31103 * @method toHexString
@@ -34,9 +106,9 @@ export default class Color {
34106 * @return {string }
35107 */
36108 toHexString ( ) {
37- var redString = Math . round ( this . red ) . toString ( 16 ) ;
38- var greenString = Math . round ( this . green ) . toString ( 16 ) ;
39- var blueString = Math . round ( this . blue ) . toString ( 16 ) ;
109+ const redString = Math . round ( this . red ) . toString ( 16 ) ;
110+ const greenString = Math . round ( this . green ) . toString ( 16 ) ;
111+ const blueString = Math . round ( this . blue ) . toString ( 16 ) ;
40112 return (
41113 '#' +
42114 ( this . red > 15.5 ? redString : '0' + redString ) +
@@ -57,28 +129,12 @@ export default class Color {
57129 * @instance
58130 */
59131 parseString ( colorString ) {
60- // Colorjs currently does not support rad or turn angle values
61- // @see https://github.com/LeaVerou/color.js/issues/311
62- colorString = colorString . replace ( hslRegex , ( match , angle , unit ) => {
63- const value = angle + unit ;
64-
65- switch ( unit ) {
66- case 'rad' :
67- return match . replace ( value , radToDeg ( angle ) ) ;
68- case 'turn' :
69- return match . replace ( value , turnToDeg ( angle ) ) ;
70- }
71- } ) ;
72-
73132 try {
74133 // srgb values are between 0 and 1
75134 const color = new Colorjs ( colorString ) . to ( 'srgb' ) ;
76- // when converting from one color space to srgb
77- // the values of rgb may be above 1 so we need to clamp them
78- // we also need to round the final value as rgb values don't have decimals
79- this . red = Math . round ( clamp ( color . r , 0 , 1 ) * 255 ) ;
80- this . green = Math . round ( clamp ( color . g , 0 , 1 ) * 255 ) ;
81- this . blue = Math . round ( clamp ( color . b , 0 , 1 ) * 255 ) ;
135+ this . r = color . r ;
136+ this . g = color . g ;
137+ this . b = color . b ;
82138 // color.alpha is a Number object so convert it to a number
83139 this . alpha = + color . alpha ;
84140 } catch ( err ) {
@@ -137,32 +193,138 @@ export default class Color {
137193 * @return {number } The luminance value, ranges from 0 to 1
138194 */
139195 getRelativeLuminance ( ) {
140- var rSRGB = this . red / 255 ;
141- var gSRGB = this . green / 255 ;
142- var bSRGB = this . blue / 255 ;
196+ const { r : rSRGB , g : gSRGB , b : bSRGB } = this ;
143197
144- var r =
198+ const r =
145199 rSRGB <= 0.03928 ? rSRGB / 12.92 : Math . pow ( ( rSRGB + 0.055 ) / 1.055 , 2.4 ) ;
146- var g =
200+ const g =
147201 gSRGB <= 0.03928 ? gSRGB / 12.92 : Math . pow ( ( gSRGB + 0.055 ) / 1.055 , 2.4 ) ;
148- var b =
202+ const b =
149203 bSRGB <= 0.03928 ? bSRGB / 12.92 : Math . pow ( ( bSRGB + 0.055 ) / 1.055 , 2.4 ) ;
150204
151205 return 0.2126 * r + 0.7152 * g + 0.0722 * b ;
152206 }
207+
208+ /**
209+ * Add a value to the color channels
210+ * @private
211+ * @param {number } value The value to add
212+ * @return {Color } A new color instance
213+ */
214+ #add( value ) {
215+ const C = new Color ( this ) ;
216+ C . r += value ;
217+ C . g += value ;
218+ C . b += value ;
219+ return C ;
220+ }
221+
222+ /**
223+ * Get the luminosity of a color
224+ * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable
225+ * @method getLuminosity
226+ * @memberof axe.commons.color.Color
227+ * @instance
228+ * @return {number } The luminosity of the color
229+ */
230+ getLuminosity ( ) {
231+ return 0.3 * this . r + 0.59 * this . g + 0.11 * this . b ;
232+ }
233+
234+ /**
235+ * Set the luminosity of a color
236+ * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable
237+ * @method setLuminosity
238+ * @memberof axe.commons.color.Color
239+ * @instance
240+ * @param {number } L The luminosity
241+ * @return {Color } A new color instance
242+ */
243+ setLuminosity ( L ) {
244+ const d = L - this . getLuminosity ( ) ;
245+ return this . #add( d ) . clip ( ) ;
246+ }
247+
248+ /**
249+ * Get the saturation of a color
250+ * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable
251+ * @method getSaturation
252+ * @memberof axe.commons.color.Color
253+ * @instance
254+ * @return {number } The saturation of the color
255+ */
256+ getSaturation ( ) {
257+ return Math . max ( this . r , this . g , this . b ) - Math . min ( this . r , this . g , this . b ) ;
258+ }
259+
260+ /**
261+ * Set the saturation of a color
262+ * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable
263+ * @method setSaturation
264+ * @memberof axe.commons.color.Color
265+ * @instance
266+ * @param {number } s The saturation
267+ * @return {Color } A new color instance
268+ */
269+ setSaturation ( s ) {
270+ const C = new Color ( this ) ;
271+ const colorEntires = [
272+ { name : 'r' , value : C . r } ,
273+ { name : 'g' , value : C . g } ,
274+ { name : 'b' , value : C . b }
275+ ] ;
276+
277+ // find the min, mid, and max values of the color components
278+ const [ Cmin , Cmid , Cmax ] = colorEntires . sort ( ( a , b ) => {
279+ return a . value - b . value ;
280+ } ) ;
281+
282+ if ( Cmax . value > Cmin . value ) {
283+ Cmid . value = ( ( Cmid . value - Cmin . value ) * s ) / ( Cmax . value - Cmin . value ) ;
284+ Cmax . value = s ;
285+ } else {
286+ Cmid . value = Cmax . value = 0 ;
287+ }
288+
289+ Cmin . value = 0 ;
290+
291+ C [ Cmax . name ] = Cmax . value ;
292+ C [ Cmin . name ] = Cmin . value ;
293+ C [ Cmid . name ] = Cmid . value ;
294+ return C ;
295+ }
296+
297+ /**
298+ * Clip the color between RGB 0-1 accounting for the luminosity of the color. Color must be normalized before calling.
299+ * using algorithm from https://www.w3.org/TR/compositing-1/#blendingnonseparable
300+ * @method clip
301+ * @memberof axe.commons.color.Color
302+ * @instance
303+ * @return {Color } A new color instance clipped between 0-1
304+ */
305+ clip ( ) {
306+ const C = new Color ( this ) ;
307+ const L = C . getLuminosity ( ) ;
308+ const n = Math . min ( C . r , C . g , C . b ) ;
309+ const x = Math . max ( C . r , C . g , C . b ) ;
310+
311+ if ( n < 0 ) {
312+ C . r = L + ( ( C . r - L ) * L ) / ( L - n ) ;
313+ C . g = L + ( ( C . g - L ) * L ) / ( L - n ) ;
314+ C . b = L + ( ( C . b - L ) * L ) / ( L - n ) ;
315+ }
316+
317+ if ( x > 1 ) {
318+ C . r = L + ( ( C . r - L ) * ( 1 - L ) ) / ( x - L ) ;
319+ C . g = L + ( ( C . g - L ) * ( 1 - L ) ) / ( x - L ) ;
320+ C . b = L + ( ( C . b - L ) * ( 1 - L ) ) / ( x - L ) ;
321+ }
322+
323+ return C ;
324+ }
153325}
154326
155327// clamp a value between two numbers (inclusive)
156328function clamp ( value , min , max ) {
157329 return Math . min ( Math . max ( min , value ) , max ) ;
158330}
159-
160- // convert radians to degrees
161- function radToDeg ( rad ) {
162- return ( rad * 180 ) / Math . PI ;
163- }
164-
165- // convert turn to degrees
166- function turnToDeg ( turn ) {
167- return turn * 360 ;
168- }
0 commit comments