UNPKG

16.7 kBPlain TextView Raw
1'use strict';
2/**
3 * Copied from:
4 * react-native/Libraries/StyleSheet/normalizeColor.js
5 * react-native/Libraries/StyleSheet/processColor.js
6 * https://github.com/wcandillon/react-native-redash/blob/master/src/Colors.ts
7 */
8
9/* eslint no-bitwise: 0 */
10import type { StyleProps } from './commonTypes';
11import { makeShareable } from './core';
12import { isAndroid, isWeb } from './PlatformChecker';
13
14interface RGB {
15 r: number;
16 g: number;
17 b: number;
18}
19
20interface HSV {
21 h: number;
22 s: number;
23 v: number;
24}
25
26const NUMBER: string = '[-+]?\\d*\\.?\\d+';
27const PERCENTAGE = NUMBER + '%';
28
29function call(...args: (RegExp | string)[]) {
30 return '\\(\\s*(' + args.join(')\\s*,?\\s*(') + ')\\s*\\)';
31}
32
33function callWithSlashSeparator(...args: (RegExp | string)[]) {
34 return (
35 '\\(\\s*(' +
36 args.slice(0, args.length - 1).join(')\\s*,?\\s*(') +
37 ')\\s*/\\s*(' +
38 args[args.length - 1] +
39 ')\\s*\\)'
40 );
41}
42
43function commaSeparatedCall(...args: (RegExp | string)[]) {
44 return '\\(\\s*(' + args.join(')\\s*,\\s*(') + ')\\s*\\)';
45}
46
47const MATCHERS = {
48 rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)),
49 rgba: new RegExp(
50 'rgba(' +
51 commaSeparatedCall(NUMBER, NUMBER, NUMBER, NUMBER) +
52 '|' +
53 callWithSlashSeparator(NUMBER, NUMBER, NUMBER, NUMBER) +
54 ')'
55 ),
56 hsl: new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)),
57 hsla: new RegExp(
58 'hsla(' +
59 commaSeparatedCall(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) +
60 '|' +
61 callWithSlashSeparator(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) +
62 ')'
63 ),
64 hwb: new RegExp('hwb' + call(NUMBER, PERCENTAGE, PERCENTAGE)),
65 hex3: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
66 hex4: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
67 hex6: /^#([0-9a-fA-F]{6})$/,
68 hex8: /^#([0-9a-fA-F]{8})$/,
69};
70
71function hue2rgb(p: number, q: number, t: number): number {
72 'worklet';
73 if (t < 0) {
74 t += 1;
75 }
76 if (t > 1) {
77 t -= 1;
78 }
79 if (t < 1 / 6) {
80 return p + (q - p) * 6 * t;
81 }
82 if (t < 1 / 2) {
83 return q;
84 }
85 if (t < 2 / 3) {
86 return p + (q - p) * (2 / 3 - t) * 6;
87 }
88 return p;
89}
90
91function hslToRgb(h: number, s: number, l: number): number {
92 'worklet';
93 const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
94 const p = 2 * l - q;
95 const r = hue2rgb(p, q, h + 1 / 3);
96 const g = hue2rgb(p, q, h);
97 const b = hue2rgb(p, q, h - 1 / 3);
98
99 return (
100 (Math.round(r * 255) << 24) |
101 (Math.round(g * 255) << 16) |
102 (Math.round(b * 255) << 8)
103 );
104}
105
106function hwbToRgb(h: number, w: number, b: number): number {
107 'worklet';
108 if (w + b >= 1) {
109 const gray = Math.round((w * 255) / (w + b));
110
111 return (gray << 24) | (gray << 16) | (gray << 8);
112 }
113
114 const red = hue2rgb(0, 1, h + 1 / 3) * (1 - w - b) + w;
115 const green = hue2rgb(0, 1, h) * (1 - w - b) + w;
116 const blue = hue2rgb(0, 1, h - 1 / 3) * (1 - w - b) + w;
117
118 return (
119 (Math.round(red * 255) << 24) |
120 (Math.round(green * 255) << 16) |
121 (Math.round(blue * 255) << 8)
122 );
123}
124
125function parse255(str: string): number {
126 'worklet';
127 const int = Number.parseInt(str, 10);
128 if (int < 0) {
129 return 0;
130 }
131 if (int > 255) {
132 return 255;
133 }
134 return int;
135}
136
137function parse360(str: string): number {
138 'worklet';
139 const int = Number.parseFloat(str);
140 return (((int % 360) + 360) % 360) / 360;
141}
142
143function parse1(str: string): number {
144 'worklet';
145 const num = Number.parseFloat(str);
146 if (num < 0) {
147 return 0;
148 }
149 if (num > 1) {
150 return 255;
151 }
152 return Math.round(num * 255);
153}
154
155function parsePercentage(str: string): number {
156 'worklet';
157 // parseFloat conveniently ignores the final %
158 const int = Number.parseFloat(str);
159 if (int < 0) {
160 return 0;
161 }
162 if (int > 100) {
163 return 1;
164 }
165 return int / 100;
166}
167
168const names: Record<string, number> = makeShareable({
169 transparent: 0x00000000,
170
171 // http://www.w3.org/TR/css3-color/#svg-color
172 aliceblue: 0xf0f8ffff,
173 antiquewhite: 0xfaebd7ff,
174 aqua: 0x00ffffff,
175 aquamarine: 0x7fffd4ff,
176 azure: 0xf0ffffff,
177 beige: 0xf5f5dcff,
178 bisque: 0xffe4c4ff,
179 black: 0x000000ff,
180 blanchedalmond: 0xffebcdff,
181 blue: 0x0000ffff,
182 blueviolet: 0x8a2be2ff,
183 brown: 0xa52a2aff,
184 burlywood: 0xdeb887ff,
185 burntsienna: 0xea7e5dff,
186 cadetblue: 0x5f9ea0ff,
187 chartreuse: 0x7fff00ff,
188 chocolate: 0xd2691eff,
189 coral: 0xff7f50ff,
190 cornflowerblue: 0x6495edff,
191 cornsilk: 0xfff8dcff,
192 crimson: 0xdc143cff,
193 cyan: 0x00ffffff,
194 darkblue: 0x00008bff,
195 darkcyan: 0x008b8bff,
196 darkgoldenrod: 0xb8860bff,
197 darkgray: 0xa9a9a9ff,
198 darkgreen: 0x006400ff,
199 darkgrey: 0xa9a9a9ff,
200 darkkhaki: 0xbdb76bff,
201 darkmagenta: 0x8b008bff,
202 darkolivegreen: 0x556b2fff,
203 darkorange: 0xff8c00ff,
204 darkorchid: 0x9932ccff,
205 darkred: 0x8b0000ff,
206 darksalmon: 0xe9967aff,
207 darkseagreen: 0x8fbc8fff,
208 darkslateblue: 0x483d8bff,
209 darkslategray: 0x2f4f4fff,
210 darkslategrey: 0x2f4f4fff,
211 darkturquoise: 0x00ced1ff,
212 darkviolet: 0x9400d3ff,
213 deeppink: 0xff1493ff,
214 deepskyblue: 0x00bfffff,
215 dimgray: 0x696969ff,
216 dimgrey: 0x696969ff,
217 dodgerblue: 0x1e90ffff,
218 firebrick: 0xb22222ff,
219 floralwhite: 0xfffaf0ff,
220 forestgreen: 0x228b22ff,
221 fuchsia: 0xff00ffff,
222 gainsboro: 0xdcdcdcff,
223 ghostwhite: 0xf8f8ffff,
224 gold: 0xffd700ff,
225 goldenrod: 0xdaa520ff,
226 gray: 0x808080ff,
227 green: 0x008000ff,
228 greenyellow: 0xadff2fff,
229 grey: 0x808080ff,
230 honeydew: 0xf0fff0ff,
231 hotpink: 0xff69b4ff,
232 indianred: 0xcd5c5cff,
233 indigo: 0x4b0082ff,
234 ivory: 0xfffff0ff,
235 khaki: 0xf0e68cff,
236 lavender: 0xe6e6faff,
237 lavenderblush: 0xfff0f5ff,
238 lawngreen: 0x7cfc00ff,
239 lemonchiffon: 0xfffacdff,
240 lightblue: 0xadd8e6ff,
241 lightcoral: 0xf08080ff,
242 lightcyan: 0xe0ffffff,
243 lightgoldenrodyellow: 0xfafad2ff,
244 lightgray: 0xd3d3d3ff,
245 lightgreen: 0x90ee90ff,
246 lightgrey: 0xd3d3d3ff,
247 lightpink: 0xffb6c1ff,
248 lightsalmon: 0xffa07aff,
249 lightseagreen: 0x20b2aaff,
250 lightskyblue: 0x87cefaff,
251 lightslategray: 0x778899ff,
252 lightslategrey: 0x778899ff,
253 lightsteelblue: 0xb0c4deff,
254 lightyellow: 0xffffe0ff,
255 lime: 0x00ff00ff,
256 limegreen: 0x32cd32ff,
257 linen: 0xfaf0e6ff,
258 magenta: 0xff00ffff,
259 maroon: 0x800000ff,
260 mediumaquamarine: 0x66cdaaff,
261 mediumblue: 0x0000cdff,
262 mediumorchid: 0xba55d3ff,
263 mediumpurple: 0x9370dbff,
264 mediumseagreen: 0x3cb371ff,
265 mediumslateblue: 0x7b68eeff,
266 mediumspringgreen: 0x00fa9aff,
267 mediumturquoise: 0x48d1ccff,
268 mediumvioletred: 0xc71585ff,
269 midnightblue: 0x191970ff,
270 mintcream: 0xf5fffaff,
271 mistyrose: 0xffe4e1ff,
272 moccasin: 0xffe4b5ff,
273 navajowhite: 0xffdeadff,
274 navy: 0x000080ff,
275 oldlace: 0xfdf5e6ff,
276 olive: 0x808000ff,
277 olivedrab: 0x6b8e23ff,
278 orange: 0xffa500ff,
279 orangered: 0xff4500ff,
280 orchid: 0xda70d6ff,
281 palegoldenrod: 0xeee8aaff,
282 palegreen: 0x98fb98ff,
283 paleturquoise: 0xafeeeeff,
284 palevioletred: 0xdb7093ff,
285 papayawhip: 0xffefd5ff,
286 peachpuff: 0xffdab9ff,
287 peru: 0xcd853fff,
288 pink: 0xffc0cbff,
289 plum: 0xdda0ddff,
290 powderblue: 0xb0e0e6ff,
291 purple: 0x800080ff,
292 rebeccapurple: 0x663399ff,
293 red: 0xff0000ff,
294 rosybrown: 0xbc8f8fff,
295 royalblue: 0x4169e1ff,
296 saddlebrown: 0x8b4513ff,
297 salmon: 0xfa8072ff,
298 sandybrown: 0xf4a460ff,
299 seagreen: 0x2e8b57ff,
300 seashell: 0xfff5eeff,
301 sienna: 0xa0522dff,
302 silver: 0xc0c0c0ff,
303 skyblue: 0x87ceebff,
304 slateblue: 0x6a5acdff,
305 slategray: 0x708090ff,
306 slategrey: 0x708090ff,
307 snow: 0xfffafaff,
308 springgreen: 0x00ff7fff,
309 steelblue: 0x4682b4ff,
310 tan: 0xd2b48cff,
311 teal: 0x008080ff,
312 thistle: 0xd8bfd8ff,
313 tomato: 0xff6347ff,
314 turquoise: 0x40e0d0ff,
315 violet: 0xee82eeff,
316 wheat: 0xf5deb3ff,
317 white: 0xffffffff,
318 whitesmoke: 0xf5f5f5ff,
319 yellow: 0xffff00ff,
320 yellowgreen: 0x9acd32ff,
321});
322
323// copied from react-native/Libraries/Components/View/ReactNativeStyleAttributes
324export const ColorProperties = makeShareable([
325 'backgroundColor',
326 'borderBottomColor',
327 'borderColor',
328 'borderLeftColor',
329 'borderRightColor',
330 'borderTopColor',
331 'borderStartColor',
332 'borderEndColor',
333 'borderBlockColor',
334 'borderBlockEndColor',
335 'borderBlockStartColor',
336 'color',
337 'shadowColor',
338 'textDecorationColor',
339 'tintColor',
340 'textShadowColor',
341 'overlayColor',
342]);
343
344export function normalizeColor(color: unknown): number | null {
345 'worklet';
346
347 if (typeof color === 'number') {
348 if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) {
349 return color;
350 }
351 return null;
352 }
353
354 if (typeof color !== 'string') {
355 return null;
356 }
357
358 let match: RegExpExecArray | null | undefined;
359
360 // Ordered based on occurrences on Facebook codebase
361 if ((match = MATCHERS.hex6.exec(color))) {
362 return Number.parseInt(match[1] + 'ff', 16) >>> 0;
363 }
364
365 if (names[color] !== undefined) {
366 return names[color];
367 }
368
369 if ((match = MATCHERS.rgb.exec(color))) {
370 return (
371 // b
372 ((parse255(match[1]) << 24) | // r
373 (parse255(match[2]) << 16) | // g
374 (parse255(match[3]) << 8) |
375 0x000000ff) >>> // a
376 0
377 );
378 }
379
380 if ((match = MATCHERS.rgba.exec(color))) {
381 // rgba(R G B / A) notation
382 if (match[6] !== undefined) {
383 return (
384 ((parse255(match[6]) << 24) | // r
385 (parse255(match[7]) << 16) | // g
386 (parse255(match[8]) << 8) | // b
387 parse1(match[9])) >>> // a
388 0
389 );
390 }
391
392 // rgba(R, G, B, A) notation
393 return (
394 ((parse255(match[2]) << 24) | // r
395 (parse255(match[3]) << 16) | // g
396 (parse255(match[4]) << 8) | // b
397 parse1(match[5])) >>> // a
398 0
399 );
400 }
401
402 if ((match = MATCHERS.hex3.exec(color))) {
403 return (
404 Number.parseInt(
405 match[1] +
406 match[1] + // r
407 match[2] +
408 match[2] + // g
409 match[3] +
410 match[3] + // b
411 'ff', // a
412 16
413 ) >>> 0
414 );
415 }
416
417 // https://drafts.csswg.org/css-color-4/#hex-notation
418 if ((match = MATCHERS.hex8.exec(color))) {
419 return Number.parseInt(match[1], 16) >>> 0;
420 }
421
422 if ((match = MATCHERS.hex4.exec(color))) {
423 return (
424 Number.parseInt(
425 match[1] +
426 match[1] + // r
427 match[2] +
428 match[2] + // g
429 match[3] +
430 match[3] + // b
431 match[4] +
432 match[4], // a
433 16
434 ) >>> 0
435 );
436 }
437
438 if ((match = MATCHERS.hsl.exec(color))) {
439 return (
440 (hslToRgb(
441 parse360(match[1]), // h
442 parsePercentage(match[2]), // s
443 parsePercentage(match[3]) // l
444 ) |
445 0x000000ff) >>> // a
446 0
447 );
448 }
449
450 if ((match = MATCHERS.hsla.exec(color))) {
451 // hsla(H S L / A) notation
452 if (match[6] !== undefined) {
453 return (
454 (hslToRgb(
455 parse360(match[6]), // h
456 parsePercentage(match[7]), // s
457 parsePercentage(match[8]) // l
458 ) |
459 parse1(match[9])) >>> // a
460 0
461 );
462 }
463
464 // hsla(H, S, L, A) notation
465 return (
466 (hslToRgb(
467 parse360(match[2]), // h
468 parsePercentage(match[3]), // s
469 parsePercentage(match[4]) // l
470 ) |
471 parse1(match[5])) >>> // a
472 0
473 );
474 }
475
476 if ((match = MATCHERS.hwb.exec(color))) {
477 return (
478 (hwbToRgb(
479 parse360(match[1]), // h
480 parsePercentage(match[2]), // w
481 parsePercentage(match[3]) // b
482 ) |
483 0x000000ff) >>> // a
484 0
485 );
486 }
487
488 return null;
489}
490
491export const opacity = (c: number): number => {
492 'worklet';
493 return ((c >> 24) & 255) / 255;
494};
495
496export const red = (c: number): number => {
497 'worklet';
498 return (c >> 16) & 255;
499};
500
501export const green = (c: number): number => {
502 'worklet';
503 return (c >> 8) & 255;
504};
505
506export const blue = (c: number): number => {
507 'worklet';
508 return c & 255;
509};
510
511const IS_WEB = isWeb();
512const IS_ANDROID = isAndroid();
513
514export const rgbaColor = (
515 r: number,
516 g: number,
517 b: number,
518 alpha = 1
519): number | string => {
520 'worklet';
521 if (IS_WEB || !_WORKLET) {
522 return `rgba(${r}, ${g}, ${b}, ${alpha})`;
523 }
524
525 const c =
526 Math.round(alpha * 255) * (1 << 24) +
527 Math.round(r) * (1 << 16) +
528 Math.round(g) * (1 << 8) +
529 Math.round(b);
530 if (IS_ANDROID) {
531 // on Android color is represented as signed 32 bit int
532 return c < (1 << 31) >>> 0 ? c : c - 4294967296; // 4294967296 == Math.pow(2, 32);
533 }
534 return c;
535};
536
537/**
538 *
539 * @param r - red value (0-255)
540 * @param g - green value (0-255)
541 * @param b - blue value (0-255)
542 * @returns \{h: hue (0-1), s: saturation (0-1), v: value (0-1)\}
543 */
544export function RGBtoHSV(r: number, g: number, b: number): HSV {
545 'worklet';
546 const max = Math.max(r, g, b);
547 const min = Math.min(r, g, b);
548 const d = max - min;
549 const s = max === 0 ? 0 : d / max;
550 const v = max / 255;
551
552 let h = 0;
553
554 switch (max) {
555 case min:
556 break;
557 case r:
558 h = g - b + d * (g < b ? 6 : 0);
559 h /= 6 * d;
560 break;
561 case g:
562 h = b - r + d * 2;
563 h /= 6 * d;
564 break;
565 case b:
566 h = r - g + d * 4;
567 h /= 6 * d;
568 break;
569 }
570
571 return { h, s, v };
572}
573
574/**
575 *
576 * @param h - hue (0-1)
577 * @param s - saturation (0-1)
578 * @param v - value (0-1)
579 * @returns \{r: red (0-255), g: green (0-255), b: blue (0-255)\}
580 */
581function HSVtoRGB(h: number, s: number, v: number): RGB {
582 'worklet';
583 let r, g, b;
584
585 const i = Math.floor(h * 6);
586 const f = h * 6 - i;
587 const p = v * (1 - s);
588 const q = v * (1 - f * s);
589 const t = v * (1 - (1 - f) * s);
590 switch ((i % 6) as 0 | 1 | 2 | 3 | 4 | 5) {
591 case 0:
592 [r, g, b] = [v, t, p];
593 break;
594 case 1:
595 [r, g, b] = [q, v, p];
596 break;
597 case 2:
598 [r, g, b] = [p, v, t];
599 break;
600 case 3:
601 [r, g, b] = [p, q, v];
602 break;
603 case 4:
604 [r, g, b] = [t, p, v];
605 break;
606 case 5:
607 [r, g, b] = [v, p, q];
608 break;
609 }
610 return {
611 r: Math.round(r * 255),
612 g: Math.round(g * 255),
613 b: Math.round(b * 255),
614 };
615}
616
617export const hsvToColor = (
618 h: number,
619 s: number,
620 v: number,
621 a: number
622): number | string => {
623 'worklet';
624 const { r, g, b } = HSVtoRGB(h, s, v);
625 return rgbaColor(r, g, b, a);
626};
627
628function processColorInitially(color: unknown): number | null | undefined {
629 'worklet';
630 if (color === null || color === undefined || typeof color === 'number') {
631 return color;
632 }
633
634 let normalizedColor = normalizeColor(color);
635
636 if (normalizedColor === null || normalizedColor === undefined) {
637 return undefined;
638 }
639
640 if (typeof normalizedColor !== 'number') {
641 return null;
642 }
643
644 normalizedColor = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0; // argb
645 return normalizedColor;
646}
647
648export function isColor(value: unknown): boolean {
649 'worklet';
650 if (typeof value !== 'string') {
651 return false;
652 }
653 return processColorInitially(value) != null;
654}
655
656export function processColor(color: unknown): number | null | undefined {
657 'worklet';
658 let normalizedColor = processColorInitially(color);
659 if (normalizedColor === null || normalizedColor === undefined) {
660 return undefined;
661 }
662
663 if (typeof normalizedColor !== 'number') {
664 return null;
665 }
666
667 if (IS_ANDROID) {
668 // Android use 32 bit *signed* integer to represent the color
669 // We utilize the fact that bitwise operations in JS also operates on
670 // signed 32 bit integers, so that we can use those to convert from
671 // *unsigned* to *signed* 32bit int that way.
672 normalizedColor = normalizedColor | 0x0;
673 }
674
675 return normalizedColor;
676}
677
678export function processColorsInProps(props: StyleProps) {
679 'worklet';
680 for (const key in props) {
681 if (ColorProperties.includes(key)) {
682 props[key] = processColor(props[key]);
683 }
684 }
685}
686
687export type ParsedColorArray = [number, number, number, number];
688
689export function convertToRGBA(color: unknown): ParsedColorArray {
690 'worklet';
691 const processedColor = processColorInitially(color)!; // argb;
692 const a = (processedColor >>> 24) / 255;
693 const r = ((processedColor << 8) >>> 24) / 255;
694 const g = ((processedColor << 16) >>> 24) / 255;
695 const b = ((processedColor << 24) >>> 24) / 255;
696 return [r, g, b, a];
697}
698
699export function rgbaArrayToRGBAColor(RGBA: ParsedColorArray): string {
700 'worklet';
701 return `rgba(${Math.round(RGBA[0] * 255)}, ${Math.round(
702 RGBA[1] * 255
703 )}, ${Math.round(RGBA[2] * 255)}, ${RGBA[3]})`;
704}
705
706export function toLinearSpace(
707 RGBA: ParsedColorArray,
708 gamma = 2.2
709): ParsedColorArray {
710 'worklet';
711 const res = [];
712 for (let i = 0; i < 3; ++i) {
713 res.push(Math.pow(RGBA[i], gamma));
714 }
715 res.push(RGBA[3]);
716 return res as ParsedColorArray;
717}
718
719export function toGammaSpace(
720 RGBA: ParsedColorArray,
721 gamma = 2.2
722): ParsedColorArray {
723 'worklet';
724 const res = [];
725 for (let i = 0; i < 3; ++i) {
726 res.push(Math.pow(RGBA[i], 1 / gamma));
727 }
728 res.push(RGBA[3]);
729 return res as ParsedColorArray;
730}