diff --git a/src/reanimated2/Colors.ts b/src/reanimated2/Colors.ts index 90876a4d86c..93d5c220a52 100644 --- a/src/reanimated2/Colors.ts +++ b/src/reanimated2/Colors.ts @@ -23,20 +23,45 @@ interface HSV { v: number; } -// var INTEGER = '[-+]?\\d+'; -const NUMBER = '[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)'; +const NUMBER: string = '[-+]?\\d*\\.?\\d+'; const PERCENTAGE = NUMBER + '%'; -function call(...args: unknown[]): string { - 'worklet'; +function call(...args: (RegExp | string)[]) { + return '\\(\\s*(' + args.join(')\\s*,?\\s*(') + ')\\s*\\)'; +} + +function callWithSlashSeparator(...args: (RegExp | string)[]) { + return ( + '\\(\\s*(' + + args.slice(0, args.length - 1).join(')\\s*,?\\s*(') + + ')\\s*/\\s*(' + + args[args.length - 1] + + ')\\s*\\)' + ); +} + +function commaSeparatedCall(...args: (RegExp | string)[]) { return '\\(\\s*(' + args.join(')\\s*,\\s*(') + ')\\s*\\)'; } const MATCHERS = { rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)), - rgba: new RegExp('rgba' + call(NUMBER, NUMBER, NUMBER, NUMBER)), + rgba: new RegExp( + 'rgba(' + + commaSeparatedCall(NUMBER, NUMBER, NUMBER, NUMBER) + + '|' + + callWithSlashSeparator(NUMBER, NUMBER, NUMBER, NUMBER) + + ')' + ), hsl: new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)), - hsla: new RegExp('hsla' + call(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)), + hsla: new RegExp( + 'hsla(' + + commaSeparatedCall(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) + + '|' + + callWithSlashSeparator(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) + + ')' + ), + hwb: new RegExp('hwb' + call(NUMBER, PERCENTAGE, PERCENTAGE)), hex3: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, hex4: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, hex6: /^#([0-9a-fA-F]{6})$/, @@ -78,6 +103,25 @@ function hslToRgb(h: number, s: number, l: number): number { ); } +function hwbToRgb(h: number, w: number, b: number): number { + 'worklet'; + if (w + b >= 1) { + const gray = Math.round((w * 255) / (w + b)); + + return (gray << 24) | (gray << 16) | (gray << 8); + } + + const red = hue2rgb(0, 1, h + 1 / 3) * (1 - w - b) + w; + const green = hue2rgb(0, 1, h) * (1 - w - b) + w; + const blue = hue2rgb(0, 1, h - 1 / 3) * (1 - w - b) + w; + + return ( + (Math.round(red * 255) << 24) | + (Math.round(green * 255) << 16) | + (Math.round(blue * 255) << 8) + ); +} + function parse255(str: string): number { 'worklet'; const int = Number.parseInt(str, 10); @@ -297,7 +341,7 @@ export const ColorProperties = makeShareable([ 'overlayColor', ]); -function normalizeColor(color: unknown): number | null { +export function normalizeColor(color: unknown): number | null { 'worklet'; if (typeof color === 'number') { @@ -334,12 +378,23 @@ function normalizeColor(color: unknown): number | null { } if ((match = MATCHERS.rgba.exec(color))) { + // rgba(R G B / A) notation + if (match[6] !== undefined) { + return ( + ((parse255(match[6]) << 24) | // r + (parse255(match[7]) << 16) | // g + (parse255(match[8]) << 8) | // b + parse1(match[9])) >>> // a + 0 + ); + } + + // rgba(R, G, B, A) notation return ( - // b - ((parse255(match[1]) << 24) | // r - (parse255(match[2]) << 16) | // g - (parse255(match[3]) << 8) | - parse1(match[4])) >>> // a + ((parse255(match[2]) << 24) | // r + (parse255(match[3]) << 16) | // g + (parse255(match[4]) << 8) | // b + parse1(match[5])) >>> // a 0 ); } @@ -393,13 +448,39 @@ function normalizeColor(color: unknown): number | null { } if ((match = MATCHERS.hsla.exec(color))) { + // hsla(H S L / A) notation + if (match[6] !== undefined) { + return ( + (hslToRgb( + parse360(match[6]), // h + parsePercentage(match[7]), // s + parsePercentage(match[8]) // l + ) | + parse1(match[9])) >>> // a + 0 + ); + } + + // hsla(H, S, L, A) notation return ( (hslToRgb( + parse360(match[2]), // h + parsePercentage(match[3]), // s + parsePercentage(match[4]) // l + ) | + parse1(match[5])) >>> // a + 0 + ); + } + + if ((match = MATCHERS.hwb.exec(color))) { + return ( + (hwbToRgb( parse360(match[1]), // h - parsePercentage(match[2]), // s - parsePercentage(match[3]) // l + parsePercentage(match[2]), // w + parsePercentage(match[3]) // b ) | - parse1(match[4])) >>> // a + 0x000000ff) >>> // a 0 ); } diff --git a/src/reanimated2/__tests__/Colors.test.ts b/src/reanimated2/__tests__/Colors.test.ts new file mode 100644 index 00000000000..9ee08b667c3 --- /dev/null +++ b/src/reanimated2/__tests__/Colors.test.ts @@ -0,0 +1,145 @@ +'use strict'; +import { normalizeColor } from '../Colors'; + +it('accepts only spec compliant colors', () => { + expect(normalizeColor('#abc')).not.toBe(null); + expect(normalizeColor('#abcd')).not.toBe(null); + expect(normalizeColor('#abcdef')).not.toBe(null); + expect(normalizeColor('#abcdef01')).not.toBe(null); + expect(normalizeColor('rgb(1,2,3)')).not.toBe(null); + expect(normalizeColor('rgb(1 2 3)')).not.toBe(null); + expect(normalizeColor('rgb(1, 2, 3)')).not.toBe(null); + expect(normalizeColor('rgb( 1 , 2 , 3 )')).not.toBe(null); + expect(normalizeColor('rgb(-1, -2, -3)')).not.toBe(null); + expect(normalizeColor('rgba(0, 0, 0, 1)')).not.toBe(null); + expect(normalizeColor(0x01234567 + 0.5)).toBe(null); + expect(normalizeColor(-1)).toBe(null); + expect(normalizeColor(0xffffffff + 1)).toBe(null); +}); + +it('temporarilys accept floating point values for rgb', () => { + expect(normalizeColor('rgb(1.1, 2.1, 3.1)')).toBe(0x010203ff); + expect(normalizeColor('rgba(1.1, 2.1, 3.1, 1.0)')).toBe(0x010203ff); +}); + +it('refuses non-spec compliant colors', () => { + expect(normalizeColor('#00gg00')).toBe(null); + expect(normalizeColor('rgb(1, 2, 3,)')).toBe(null); + expect(normalizeColor('rgb(1, 2, 3')).toBe(null); + + // Used to be accepted by normalizeColor + expect(normalizeColor('abc')).toBe(null); + expect(normalizeColor(' #abc ')).toBe(null); + expect(normalizeColor('##abc')).toBe(null); + expect(normalizeColor('rgb 255 0 0')).toBe(null); + expect(normalizeColor('RGBA(0, 1, 2)')).toBe(null); + expect(normalizeColor('rgb (0, 1, 2)')).toBe(null); + expect(normalizeColor('rgba(0 0 0 0.0)')).toBe(null); + expect(normalizeColor('hsv(0, 1, 2)')).toBe(null); + // $FlowExpectedError - Intentionally malformed argument. + expect(normalizeColor({ r: 10, g: 10, b: 10 })).toBe(null); + expect(normalizeColor('hsl(1%, 2, 3)')).toBe(null); + expect(normalizeColor('rgb(1%, 2%, 3%)')).toBe(null); +}); + +it('handles hex6 properly', () => { + expect(normalizeColor('#000000')).toBe(0x000000ff); + expect(normalizeColor('#ffffff')).toBe(0xffffffff); + expect(normalizeColor('#ff00ff')).toBe(0xff00ffff); + expect(normalizeColor('#abcdef')).toBe(0xabcdefff); + expect(normalizeColor('#012345')).toBe(0x012345ff); +}); + +it('handles hex3 properly', () => { + expect(normalizeColor('#000')).toBe(0x000000ff); + expect(normalizeColor('#fff')).toBe(0xffffffff); + expect(normalizeColor('#f0f')).toBe(0xff00ffff); +}); + +it('handles hex8 properly', () => { + expect(normalizeColor('#00000000')).toBe(0x00000000); + expect(normalizeColor('#ffffffff')).toBe(0xffffffff); + expect(normalizeColor('#ffff00ff')).toBe(0xffff00ff); + expect(normalizeColor('#abcdef01')).toBe(0xabcdef01); + expect(normalizeColor('#01234567')).toBe(0x01234567); +}); + +it('handles rgb properly', () => { + expect(normalizeColor('rgb(0, 0, 0)')).toBe(0x000000ff); + expect(normalizeColor('rgb(-1, -2, -3)')).toBe(0x000000ff); + expect(normalizeColor('rgb(0, 0, 255)')).toBe(0x0000ffff); + expect(normalizeColor('rgb(100, 15, 69)')).toBe(0x640f45ff); + expect(normalizeColor('rgb(255, 255, 255)')).toBe(0xffffffff); + expect(normalizeColor('rgb(256, 256, 256)')).toBe(0xffffffff); + expect(normalizeColor('rgb(0 0 0)')).toBe(0x000000ff); + expect(normalizeColor('rgb(0 0 255)')).toBe(0x0000ffff); +}); + +it('handles rgba properly', () => { + expect(normalizeColor('rgba(0, 0, 0, .5)')).toBe(0x00000080); + expect(normalizeColor('rgba(0, 0, 0, 0.0)')).toBe(0x00000000); + expect(normalizeColor('rgba(0, 0, 0, 0)')).toBe(0x00000000); + expect(normalizeColor('rgba(0, 0, 0, -0.5)')).toBe(0x00000000); + expect(normalizeColor('rgba(0, 0, 0, 1.0)')).toBe(0x000000ff); + expect(normalizeColor('rgba(0, 0, 0, 1)')).toBe(0x000000ff); + expect(normalizeColor('rgba(0, 0, 0, 1.5)')).toBe(0x000000ff); + expect(normalizeColor('rgba(100, 15, 69, 0.5)')).toBe(0x640f4580); + expect(normalizeColor('rgba(0 0 0 / 0.0)')).toBe(0x00000000); + expect(normalizeColor('rgba(0 0 0 / 1)')).toBe(0x000000ff); + expect(normalizeColor('rgba(100 15 69 / 0.5)')).toBe(0x640f4580); +}); + +it('handles hsl properly', () => { + expect(normalizeColor('hsl(0, 0%, 0%)')).toBe(0x000000ff); + expect(normalizeColor('hsl(360, 100%, 100%)')).toBe(0xffffffff); + expect(normalizeColor('hsl(180, 50%, 50%)')).toBe(0x40bfbfff); + expect(normalizeColor('hsl(540, 50%, 50%)')).toBe(0x40bfbfff); + expect(normalizeColor('hsl(70, 25%, 75%)')).toBe(0xcacfafff); + expect(normalizeColor('hsl(70, 100%, 75%)')).toBe(0xeaff80ff); + expect(normalizeColor('hsl(70, 110%, 75%)')).toBe(0xeaff80ff); + expect(normalizeColor('hsl(70, 0%, 75%)')).toBe(0xbfbfbfff); + expect(normalizeColor('hsl(70, -10%, 75%)')).toBe(0xbfbfbfff); + expect(normalizeColor('hsl(0 0% 0%)')).toBe(0x000000ff); + expect(normalizeColor('hsl(360 100% 100%)')).toBe(0xffffffff); + expect(normalizeColor('hsl(180 50% 50%)')).toBe(0x40bfbfff); +}); + +it('handles hsla properly', () => { + expect(normalizeColor('hsla(0, 0%, 0%, 0)')).toBe(0x00000000); + expect(normalizeColor('hsla(360, 100%, 100%, 1)')).toBe(0xffffffff); + expect(normalizeColor('hsla(360, 100%, 100%, 0)')).toBe(0xffffff00); + expect(normalizeColor('hsla(180, 50%, 50%, 0.2)')).toBe(0x40bfbf33); + expect(normalizeColor('hsla(0 0% 0% / 0)')).toBe(0x00000000); + expect(normalizeColor('hsla(360 100% 100% / 1)')).toBe(0xffffffff); + expect(normalizeColor('hsla(360 100% 100% / 0)')).toBe(0xffffff00); + expect(normalizeColor('hsla(180 50% 50% / 0.2)')).toBe(0x40bfbf33); +}); + +it('handles hwb properly', () => { + expect(normalizeColor('hwb(0, 0%, 100%)')).toBe(0x000000ff); + expect(normalizeColor('hwb(0, 100%, 0%)')).toBe(0xffffffff); + expect(normalizeColor('hwb(0, 0%, 0%)')).toBe(0xff0000ff); + expect(normalizeColor('hwb(70, 50%, 0%)')).toBe(0xeaff80ff); + expect(normalizeColor('hwb(0, 50%, 50%)')).toBe(0x808080ff); + expect(normalizeColor('hwb(360, 100%, 100%)')).toBe(0x808080ff); + expect(normalizeColor('hwb(0 0% 0%)')).toBe(0xff0000ff); + expect(normalizeColor('hwb(70 50% 0%)')).toBe(0xeaff80ff); +}); + +it('handles named colors properly', () => { + expect(normalizeColor('red')).toBe(0xff0000ff); + expect(normalizeColor('transparent')).toBe(0x00000000); + expect(normalizeColor('peachpuff')).toBe(0xffdab9ff); +}); + +it('handles number colors properly', () => { + expect(normalizeColor(0x00000000)).toBe(0x00000000); + expect(normalizeColor(0xff0000ff)).toBe(0xff0000ff); + expect(normalizeColor(0xffffffff)).toBe(0xffffffff); + expect(normalizeColor(0x01234567)).toBe(0x01234567); +}); + +it('returns the same color when it is already normalized', () => { + const normalizedColor = normalizeColor('red') || 0; + expect(normalizeColor(normalizedColor)).toBe(normalizedColor); +});