11/**
22 * @module ol/color
33 */
4- import parseRgba from 'color-rgba' ;
5- import lchuv from 'color-space/lchuv.js' ;
6- import rgb from 'color-space/rgb.js' ;
7- import xyz from 'color-space/xyz.js' ;
8- import { clamp } from './math.js' ;
4+ import { createCanvasContext2D } from './dom.js' ;
5+ import { clamp , toFixed } from './math.js' ;
96
107/**
118 * A color represented as a short array [red, green, blue, alpha].
@@ -23,6 +20,112 @@ import {clamp} from './math.js';
2320 */
2421export const NO_COLOR = [ NaN , NaN , NaN , 0 ] ;
2522
23+ let colorParseContext ;
24+ /**
25+ * @return {CanvasRenderingContext2D } The color parse context
26+ */
27+ function getColorParseContext ( ) {
28+ if ( ! colorParseContext ) {
29+ colorParseContext = createCanvasContext2D ( 1 , 1 , undefined , {
30+ willReadFrequently : true ,
31+ desynchronized : true ,
32+ } ) ;
33+ }
34+ return colorParseContext ;
35+ }
36+
37+ const rgbModernRegEx =
38+ / ^ r g b a ? \( \s * ( \d + % ? ) \s + ( \d + % ? ) \s + ( \d + % ? ) (?: \s * \/ \s * ( \d + % | \d * \. \d + | [ 0 1 ] ) ) ? \s * \) $ / i;
39+ const rgbLegacyAbsoluteRegEx =
40+ / ^ r g b a ? \( \s * ( \d + ) \s * , \s * ( \d + ) \s * , \s * ( \d + ) (?: \s * , \s * ( \d + % | \d * \. \d + | [ 0 1 ] ) ) ? \s * \) $ / i;
41+ const rgbLegacyPercentageRegEx =
42+ / ^ r g b a ? \( \s * ( \d + % ) \s * , \s * ( \d + % ) \s * , \s * ( \d + % ) (?: \s * , \s * ( \d + % | \d * \. \d + | [ 0 1 ] ) ) ? \s * \) $ / i;
43+ const hexRegEx = / ^ # ( [ \d a - f ] { 3 , 4 } | [ \d a - f ] { 6 } | [ \d a - f ] { 8 } ) $ / i;
44+
45+ /**
46+ * @param {string } s Color component as number or percentage.
47+ * @param {number } divider Divider for percentage.
48+ * @return {number } Color component.
49+ */
50+ function toColorComponent ( s , divider ) {
51+ return s . endsWith ( '%' )
52+ ? Number ( s . substring ( 0 , s . length - 1 ) ) / divider
53+ : Number ( s ) ;
54+ }
55+
56+ /**
57+ * @param {string } color Color string.
58+ */
59+ function throwInvalidColor ( color ) {
60+ throw new Error ( 'failed to parse "' + color + '" as color' ) ;
61+ }
62+
63+ /**
64+ * @param {string } color Color string.
65+ * @return {Color } RGBa color array.
66+ */
67+ function parseRgba ( color ) {
68+ // Fast lane for rgb(a) colors
69+ if ( color . toLowerCase ( ) . startsWith ( 'rgb' ) ) {
70+ const rgb =
71+ color . match ( rgbLegacyAbsoluteRegEx ) ||
72+ color . match ( rgbModernRegEx ) ||
73+ color . match ( rgbLegacyPercentageRegEx ) ;
74+ if ( rgb ) {
75+ const alpha = rgb [ 4 ] ;
76+ const rgbDivider = 100 / 255 ;
77+ return [
78+ clamp ( ( toColorComponent ( rgb [ 1 ] , rgbDivider ) + 0.5 ) | 0 , 0 , 255 ) ,
79+ clamp ( ( toColorComponent ( rgb [ 2 ] , rgbDivider ) + 0.5 ) | 0 , 0 , 255 ) ,
80+ clamp ( ( toColorComponent ( rgb [ 3 ] , rgbDivider ) + 0.5 ) | 0 , 0 , 255 ) ,
81+ alpha !== undefined ? clamp ( toColorComponent ( alpha , 100 ) , 0 , 1 ) : 1 ,
82+ ] ;
83+ }
84+ throwInvalidColor ( color ) ;
85+ }
86+ // Fast lane for hex colors (also with alpha)
87+ if ( color . startsWith ( '#' ) ) {
88+ if ( hexRegEx . test ( color ) ) {
89+ const hex = color . substring ( 1 ) ;
90+ const step = hex . length <= 4 ? 1 : 2 ;
91+ const colorFromHex = [ 0 , 0 , 0 , 255 ] ;
92+ for ( let i = 0 , ii = hex . length ; i < ii ; i += step ) {
93+ let colorComponent = parseInt ( hex . substring ( i , i + step ) , 16 ) ;
94+ if ( step === 1 ) {
95+ colorComponent += colorComponent << 4 ;
96+ }
97+ colorFromHex [ i / step ] = colorComponent ;
98+ }
99+ colorFromHex [ 3 ] = colorFromHex [ 3 ] / 255 ;
100+ return colorFromHex ;
101+ }
102+ throwInvalidColor ( color ) ;
103+ }
104+ // Use canvas color serialization to parse the color into hex or rgba
105+ // See https://www.w3.org/TR/2021/SPSD-2dcontext-20210128/#serialization-of-a-color
106+ const context = getColorParseContext ( ) ;
107+ context . fillStyle = '#abcdef' ;
108+ let invalidCheckFillStyle = context . fillStyle ;
109+ context . fillStyle = color ;
110+ if ( context . fillStyle === invalidCheckFillStyle ) {
111+ context . fillStyle = '#fedcba' ;
112+ invalidCheckFillStyle = context . fillStyle ;
113+ context . fillStyle = color ;
114+ if ( context . fillStyle === invalidCheckFillStyle ) {
115+ throwInvalidColor ( color ) ;
116+ }
117+ }
118+ const colorString = context . fillStyle ;
119+ if ( colorString . startsWith ( '#' ) || colorString . startsWith ( 'rgba' ) ) {
120+ return parseRgba ( colorString ) ;
121+ }
122+ context . clearRect ( 0 , 0 , 1 , 1 ) ;
123+ context . fillRect ( 0 , 0 , 1 , 1 ) ;
124+ const colorFromImage = Array . from ( context . getImageData ( 0 , 0 , 1 , 1 ) . data ) ;
125+ colorFromImage [ 3 ] = toFixed ( colorFromImage [ 3 ] / 255 , 3 ) ;
126+ return colorFromImage ;
127+ }
128+
26129/**
27130 * Return the color as an rgba string.
28131 * @param {Color|string } color Color.
@@ -69,24 +172,81 @@ export function withAlpha(color) {
69172 return output ;
70173}
71174
175+ // The functions b1, b2, a1, a2, rgbaToLcha and lchaToRgba below are adapted from
176+ // https://stackoverflow.com/a/67219995/2389327
177+
178+ /**
179+ * @param {number } v Input value.
180+ * @return {number } Output value.
181+ */
182+ function b1 ( v ) {
183+ return v > 0.0031308 ? Math . pow ( v , 1 / 2.4 ) * 269.025 - 14.025 : v * 3294.6 ;
184+ }
185+
186+ /**
187+ * @param {number } v Input value.
188+ * @return {number } Output value.
189+ */
190+ function b2 ( v ) {
191+ return v > 0.2068965 ? Math . pow ( v , 3 ) : ( v - 4 / 29 ) * ( 108 / 841 ) ;
192+ }
193+
194+ /**
195+ * @param {number } v Input value.
196+ * @return {number } Output value.
197+ */
198+ function a1 ( v ) {
199+ return v > 10.314724 ? Math . pow ( ( v + 14.025 ) / 269.025 , 2.4 ) : v / 3294.6 ;
200+ }
201+
202+ /**
203+ * @param {number } v Input value.
204+ * @return {number } Output value.
205+ */
206+ function a2 ( v ) {
207+ return v > 0.0088564 ? Math . pow ( v , 1 / 3 ) : v / ( 108 / 841 ) + 4 / 29 ;
208+ }
209+
72210/**
73211 * @param {Color } color RGBA color.
74212 * @return {Color } LCHuv color with alpha.
75213 */
76214export function rgbaToLcha ( color ) {
77- const output = xyz . lchuv ( rgb . xyz ( color ) ) ;
78- output [ 3 ] = color [ 3 ] ;
79- return output ;
215+ const r = a1 ( color [ 0 ] ) ;
216+ const g = a1 ( color [ 1 ] ) ;
217+ const b = a1 ( color [ 2 ] ) ;
218+ const y = a2 ( r * 0.222488403 + g * 0.716873169 + b * 0.06060791 ) ;
219+ const l = 500 * ( a2 ( r * 0.452247074 + g * 0.399439023 + b * 0.148375274 ) - y ) ;
220+ const q = 200 * ( y - a2 ( r * 0.016863605 + g * 0.117638439 + b * 0.865350722 ) ) ;
221+ const h = Math . atan2 ( q , l ) * ( 180 / Math . PI ) ;
222+ return [
223+ 116 * y - 16 ,
224+ Math . sqrt ( l * l + q * q ) ,
225+ h < 0 ? h + 360 : h ,
226+ color [ 3 ] ,
227+ ] ;
80228}
81229
82230/**
83231 * @param {Color } color LCHuv color with alpha.
84232 * @return {Color } RGBA color.
85233 */
86234export function lchaToRgba ( color ) {
87- const output = xyz . rgb ( lchuv . xyz ( color ) ) ;
88- output [ 3 ] = color [ 3 ] ;
89- return output ;
235+ const l = ( color [ 0 ] + 16 ) / 116 ;
236+ const c = color [ 1 ] ;
237+ const h = ( color [ 2 ] * Math . PI ) / 180 ;
238+ const y = b2 ( l ) ;
239+ const x = b2 ( l + ( c / 500 ) * Math . cos ( h ) ) ;
240+ const z = b2 ( l - ( c / 200 ) * Math . sin ( h ) ) ;
241+ const r = b1 ( x * 3.021973625 - y * 1.617392459 - z * 0.404875592 ) ;
242+ const g = b1 ( x * - 0.943766287 + y * 1.916279586 + z * 0.027607165 ) ;
243+ const b = b1 ( x * 0.069407491 - y * 0.22898585 + z * 1.159737864 ) ;
244+ return [
245+ clamp ( ( r + 0.5 ) | 0 , 0 , 255 ) ,
246+ clamp ( ( g + 0.5 ) | 0 , 0 , 255 ) ,
247+ clamp ( ( b + 0.5 ) | 0 , 0 , 255 ) ,
248+ color [ 3 ] ,
249+ ] ;
90250}
91251
92252/**
@@ -112,14 +272,13 @@ export function fromString(s) {
112272
113273 const color = parseRgba ( s ) ;
114274 if ( color . length !== 4 ) {
115- throw new Error ( 'failed to parse "' + s + '" as color' ) ;
275+ throwInvalidColor ( s ) ;
116276 }
117277 for ( const c of color ) {
118278 if ( isNaN ( c ) ) {
119- throw new Error ( 'failed to parse "' + s + '" as color' ) ;
279+ throwInvalidColor ( s ) ;
120280 }
121281 }
122- normalize ( color ) ;
123282 cache [ s ] = color ;
124283 ++ cacheSize ;
125284 return color ;
@@ -139,19 +298,6 @@ export function asArray(color) {
139298 return fromString ( color ) ;
140299}
141300
142- /**
143- * Exported for the tests.
144- * @param {Color } color Color.
145- * @return {Color } Clamped color.
146- */
147- export function normalize ( color ) {
148- color [ 0 ] = clamp ( ( color [ 0 ] + 0.5 ) | 0 , 0 , 255 ) ;
149- color [ 1 ] = clamp ( ( color [ 1 ] + 0.5 ) | 0 , 0 , 255 ) ;
150- color [ 2 ] = clamp ( ( color [ 2 ] + 0.5 ) | 0 , 0 , 255 ) ;
151- color [ 3 ] = clamp ( color [ 3 ] , 0 , 1 ) ;
152- return color ;
153- }
154-
155301/**
156302 * @param {Color } color Color.
157303 * @return {string } String.
0 commit comments