diff --git a/packages/react-native-reanimated/__tests__/bezier.test.ts b/packages/react-native-reanimated/__tests__/bezier.test.ts new file mode 100644 index 00000000000..1cdd3056162 --- /dev/null +++ b/packages/react-native-reanimated/__tests__/bezier.test.ts @@ -0,0 +1,239 @@ +import { Bezier } from '../src/Bezier'; + +// spell-checker:disable +/* + * https://github.com/gre/bezier-easing/blob/master/test/test.js + * BezierEasing - use bezier curve for transition easing function + * by Gaëtan Renaudeau 2014 - 2015 – MIT License + */ +// spell-checker:enable + +function repeat(n: number) { + return (f: () => void) => { + for (let i = 0; i < n; ++i) { + f(); + } + }; +} + +function functionsAreEqual( + fun1: (x: number) => number, + fun2: (x: number) => number, + maxError = 0 +) { + let allPointsAreEqual = true; + + const points = Array.from(Array(100).keys()).map((x) => x / 100); + points.forEach((point) => { + if (Math.abs(fun1(point) - fun2(point)) > maxError) { + allPointsAreEqual = false; + } + }); + return allPointsAreEqual; +} + +describe('Test `Bezier` function', () => { + it('Should be a function', () => { + expect(typeof Bezier === 'function').toBeTruthy(); + }); + it('Should create a function', () => { + expect(typeof Bezier(0, 0, 1, 1) === 'function').toBeTruthy(); + expect(typeof Bezier(0, 0, 1, 1)(0.5) === 'number').toBeTruthy(); + }); + + describe('Function throws error in case of incorrect arguments', () => { + test.each([ + [0.5, 0.5, -5, 0.5], + [0, 0.5, -0.005, 0.5], + [0.5, 0.5, 5, 0.5], + [-2, 0.5, 0.5, 0.5], + [2, 0.5, 0.5, 0.5], + ])( + 'Invalid arguments point1 = (%d, %d) point2 = (%d, %d), x should be in range [0,1]', + (x1, x2, y1, y2) => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _func = Bezier(x1, x2, y1, y2); + }).toThrow(); + } + ); + + test.each([ + [0, 0, 1, 1], + [0.5, 100, 0.5, 50], + [0.5, -100, 0.5, 50], + [0.5, 0, 0.5, -50], + [1, 0, 0.5, -50], + ])( + 'Valid arguments point1 = (%d, %d) point2 = (%d, %d)', + (x1, x2, y1, y2) => { + expect(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _func = Bezier(x1, x2, y1, y2); + }).not.toThrow(); + } + ); + }); + + describe('Function bezier(a,a,b,b) is alway linear', () => { + test.each([ + [0.0000001, 0.0000001, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 1], + [1, 1, 0, 0], + [1, 1, 1, 1], + ])(`Bezier(%d,%d,%d,%d)`, (a, b, c, d) => { + expect(functionsAreEqual(Bezier(a, b, c, d), (x) => x)).toBeTruthy(); + }); + + const MONKEY_TRIES = 1000; + let allTestPass = true; + repeat(MONKEY_TRIES)(() => { + const a = Math.random(); + const b = Math.random(); + + const easing = Bezier(a, a, b, b); + + if (!functionsAreEqual(easing, (x: number) => x)) { + allTestPass = false; + test(`Bezier(${a},${a}, ${b}, ${b}) is linear`, () => { + expect(false).toBeTruthy(); + }); + } + }); + test(`All ${MONKEY_TRIES} monkey tests pass`, () => { + expect(allTestPass).toBeTruthy(); + }); + }); + + describe('Should satisfy that bezier(0) = 0 and bezier(1)=1', () => { + test.each([ + [0.0000001, 0.0000001, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 1000000], + [1, 1000000, 0, 1000000], + [1, 1, 1, -1000000], + [1, -1000000, 1, -1000000], + [1, -1000000, 1, 0], + ])(`Bezier(%d,%d,%d,%d)`, (a, b, c, d) => { + expect(Bezier(a, b, c, d)(0)).toBe(0); + expect(Bezier(a, b, c, d)(1)).toBe(1); + }); + + const MONKEY_TRIES = 1000; + let allTestPass = true; + repeat(MONKEY_TRIES)(() => { + const a = Math.random(); + const b = 3 * Math.random() - 1; + const c = Math.random(); + const d = 3 * Math.random() - 1; + const easing = Bezier(a, b, c, d); + + const satisfiesCondition = easing(0) === 0 && easing(1) === 1; + if (!satisfiesCondition) { + allTestPass = false; + test(`Bezier(${a},${b}, ${c}, ${d})(0) = 0 \n Bezier(${a},${b}, ${c}, ${d})(1) = 1`, () => { + expect(easing(0)).toBe(0); + expect(easing(1)).toBe(1); + }); + } + }); + test(`All ${MONKEY_TRIES} monkey tests pass`, () => { + expect(allTestPass).toBeTruthy(); + }); + }); + + describe('Bezier(a,b,c,d) and Bezier(b,a,d,c) are symmetric about the axis f(x)=x', () => { + // The b1 = bezier(a,b,c,d) curve is defined through four points: (0,0), (a,b), (c,d) and (1,1) + // The b2 = bezier(b,a,d,c) curve is defined through four points: (0,0), (b,a), (d,c) and (1,1) + // These two bezier curves are each others reflection relative to line f(x)=x + // So if b1(x) = y, then b2(y) = x and b2(b1(x)) = x + + test.each([ + [0.0000001, 0, 0.0000001, 0, 0.00000001], + [0.0000001, 0, 0.005, 0, 0.00000001], + [0.1, 0.2, 0.05, 0.7, 0.00000001], + [0.98, 0.45, 0.05, 0.17, 0.00000001], + [0.98, 0.45, 0.85, 0.17, 0.00000001], + + // Precision drop when c gets close to one (same drop when d gets close to one due to symmetry) + // may be worth to investigate it in the future + [0.98, 0.45, 0.9, 0.17, 0.00000001], + [0.98, 0.45, 0.99, 0.17, 0.00000001], + [0.98, 0.45, 0.999, 0.17, 0.000001], + [0.98, 0.45, 0.9999, 0.17, 0.01], + [0.98, 0.45, 1, 0.17, 0.01], + + // Precision drop when a gets close to zero (same drop when b gets close to one due to symmetry) + [0.99, 0.45, 0.85, 0.17, 0.00000001], + [0.99, 0.45, 0.85, 0.17, 0.00000001], + [0.999, 0.45, 0.85, 0.17, 0.00000001], + [0.9999, 0.45, 0.85, 0.17, 0.00000001], + [0.99999, 0.45, 1, 0.85, 0.01], + [1, 0.45, 1, 0.85, 0.01], + + [0, 0, 1, 0, 0.01], + [1, 1, 0, 1, 0.1], + [0, 1, 0, 1, 0.1], + ])(`Bezier(%d,%d,%d,%d)`, (a, b, c, d, precision) => { + const easing1 = Bezier(a, b, c, d); + const easing2 = Bezier(b, a, d, c); + expect( + functionsAreEqual( + (x) => easing1(easing2(x)), + (x: number) => x, + precision + ) + ).toBeTruthy(); + }); + + const MONKEY_TRIES = 20000; + const PRECISION_0 = 0.01; + let allTestPass0 = true; + repeat(MONKEY_TRIES)(() => { + const a = Math.random(); + const b = Math.random(); + const c = Math.random(); + const d = Math.random(); + + const easing1 = Bezier(a, b, c, d); + const easing2 = Bezier(b, a, d, c); + + const almostIdentity = (x: number) => easing1(easing2(x)); + if (!functionsAreEqual(almostIdentity, (x: number) => x, PRECISION_0)) { + allTestPass0 = false; + test(`Bezier(${a},${b}, ${c}, ${d}) is symmetric to its reflection about the axis f(x)=x`, () => { + expect(false).toBeTruthy(); + }); + } + }); + test(`All ${MONKEY_TRIES} monkey tests for random a,b,c,d in [0,1] pass, precision=${PRECISION_0}`, () => { + expect(allTestPass0).toBeTruthy(); + }); + + const PRECISION_1 = 1e-12; + let allTestPass1 = true; + repeat(MONKEY_TRIES)(() => { + const a = 0.9 * Math.random() + 0.05; + const b = 0.9 * Math.random() + 0.05; + const c = 0.9 * Math.random() + 0.05; + const d = 0.9 * Math.random() + 0.05; + + const easing1 = Bezier(a, b, c, d); + const easing2 = Bezier(b, a, d, c); + + const almostIdentity = (x: number) => easing1(easing2(x)); + if (!functionsAreEqual(almostIdentity, (x: number) => x, PRECISION_1)) { + allTestPass1 = false; + test(`Bezier(${a},${b}, ${c}, ${d}) is symmetric to its reflection about the axis f(x)=x`, () => { + expect(false).toBeTruthy(); + }); + } + }); + test(`All ${MONKEY_TRIES} monkey tests for random a,b,c,d in [0.05,0.95] pass, precision=${PRECISION_1}`, () => { + expect(allTestPass1).toBeTruthy(); + }); + }); + + describe('Bezier(a,b,(1-a),(1-b)) should have point symmetry at point (0.5, 0.5)', () => {}); +}); diff --git a/packages/react-native-reanimated/__tests__/interpolate.test.tsx b/packages/react-native-reanimated/__tests__/interpolate.test.tsx new file mode 100644 index 00000000000..e4811671c8b --- /dev/null +++ b/packages/react-native-reanimated/__tests__/interpolate.test.tsx @@ -0,0 +1,240 @@ +import { Extrapolation, interpolate } from '../src/interpolation'; + +describe('Test `interpolate` function', () => { + describe('The provided range is ordered ([0,1] - ordered, [1,0] - unordered)', () => { + describe('Interpolation within input range', () => { + const BIG_NUM = 123456789.12345678; + test.each([ + { value: -50, input: [-100, 100], output: [0, 100], expected: 25 }, + { value: -50, input: [-100, 100], output: [-200, 200], expected: -100 }, + { value: 50, input: [0, 100], output: [-100, 100], expected: 0 }, + { value: 25, input: [0, 100], output: [-100, 100], expected: -50 }, + { value: 75, input: [0, 100], output: [-100, 100], expected: 50 }, + { value: 50, input: [0, 100], output: [0, 1], expected: 0.5 }, + { value: 50e-10, input: [0, 100], output: [0, 1], expected: 0.5e-10 }, + { value: 50e-15, input: [0, 100], output: [0, 1], expected: 0.5e-15 }, + { value: 50, input: [0, 1e20], output: [0, 1], expected: 50e-20 }, + { value: 88, input: [50, 100], output: [150, 300], expected: 264 }, + { value: 200, input: [200, 300], output: [111, 200], expected: 111 }, + { value: 300, input: [200, 300], output: [100, 222], expected: 222 }, + { + value: 22222, + input: [11111, 33333], + output: [44444, 55555], + expected: 49999.5, + }, + { + value: 88, + input: [50, 100], + output: [BIG_NUM + 50, BIG_NUM + 100], + expected: BIG_NUM + 88, + }, + { + value: BIG_NUM + 88, + input: [BIG_NUM + 50, BIG_NUM + 100], + output: [50, 100], + expected: 88, + }, + { + value: 88, + input: [50, 100], + output: [2 * (BIG_NUM + 50), 2 * (BIG_NUM + 100)], + expected: 2 * (BIG_NUM + 88), + }, + ])( + 'Interpolate $value from $input to $output', + ({ value, input, output, expected }) => { + // Lets make sure that all these test cases contain number within range + expect(value).toBeGreaterThanOrEqual(input[0]); + expect(value).toBeLessThanOrEqual(input[1]); + + expect(interpolate(value, input, output)).toBe(expected); + expect(interpolate(value, input, output, Extrapolation.CLAMP)).toBe( + expected + ); + expect( + interpolate(value, input, output, Extrapolation.IDENTITY) + ).toBe(expected); + expect(interpolate(value, input, output, Extrapolation.EXTEND)).toBe( + expected + ); + } + ); + }); + + describe('Interpolation outside input range', () => { + test.each([ + { + value: 50, + input: [100, 200], + output: [100, 200], + expected: [100, 50, 50], + }, + { + value: 0, + input: [200, 300], + output: [100, 200], + expected: [100, 0, -100], + }, + { + value: 50, + input: [200, 300], + output: [100, 200], + expected: [100, 50, -50], + }, + { + value: 190, + input: [200, 300], + output: [100, 200], + expected: [100, 190, 90], + }, + + { + value: 400, + input: [100, 200], + output: [0, 100], + expected: [100, 400, 300], + }, + ])( + 'Interpolate $value from $input to $output', + ({ value, input, output, expected }) => { + const [clamp, identity, extend] = expected; + + if (value < input[0]) { + // make sure we are below the range + expect(value).toBeLessThan(input[0]); + } else { + // make sure we are above the range + expect(value).toBeGreaterThan(input[1]); + } + + expect(interpolate(value, input, output, Extrapolation.CLAMP)).toBe( + clamp + ); + expect( + interpolate(value, input, output, Extrapolation.IDENTITY) + ).toBe(identity); + expect(interpolate(value, input, output, Extrapolation.EXTEND)).toBe( + extend + ); + expect(interpolate(value, input, output)).toBe(extend); + } + ); + }); + + const PRECISION = 16; + describe(`test precision up to ${PRECISION} digits`, () => { + test.each([ + { + value: 1234, + input: [123, 12345], + output: [12, 123], + expected: + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + 22.090083456062837506136475208640157093765341188021600392734413352, + }, + ])( + 'Interpolate value $value from $input to $output', + ({ value, input, output, expected }) => { + const applyPrecision = (x: number) => { + return Number(x.toPrecision(PRECISION)); + }; + + expect( + applyPrecision(interpolate(applyPrecision(value), input, output)) + ).toBe(applyPrecision(expected)); + } + ); + }); + }); + + describe('At least one of the provided ranges is unordered', () => { + describe('Interpolation within input range', () => { + test.each([ + { + value: 100, + input: [200, 0], + output: [100, 200], + expected: 150, + }, + { + value: 150, + input: [200, 0], + output: [100, 200], + expected: 125, + }, + ])( + 'Interpolate $value from $input to $output', + ({ value, input, output, expected }) => { + expect(interpolate(value, input, output, Extrapolation.CLAMP)).toBe( + expected + ); + expect( + interpolate(value, input, output, Extrapolation.IDENTITY) + ).toBe(expected); + expect(interpolate(value, input, output, Extrapolation.EXTEND)).toBe( + expected + ); + expect(interpolate(value, input, output)).toBe(expected); + } + ); + }); + + describe('Interpolation outside input range', () => { + test.each([ + { + value: 300, + input: [200, 0], + output: [100, 200], + expected: [100, 300, 50], + }, + { + value: -200, + input: [200, 0], + output: [100, 200], + expected: [200, -200, 300], + }, + { + value: -200, + input: [200, 0], + output: [400, 200], + expected: [200, -200, 0], + }, + { + value: -200, + input: [200, 0], + output: [0, 200], + expected: [200, -200, 400], + }, + { + value: -200, + input: [0, 100], + output: [200, 0], + expected: [200, -200, 600], + }, + { + value: 200, + input: [0, 100], + output: [200, 0], + expected: [0, 200, -200], + }, + ])( + 'Interpolate $value from $input to $output', + ({ value, input, output, expected }) => { + const [clamp, identity, extend] = expected; + + expect(interpolate(value, input, output, Extrapolation.CLAMP)).toBe( + clamp + ); + expect( + interpolate(value, input, output, Extrapolation.IDENTITY) + ).toBe(identity); + expect(interpolate(value, input, output, Extrapolation.EXTEND)).toBe( + extend + ); + expect(interpolate(value, input, output)).toBe(extend); + } + ); + }); + }); +}); diff --git a/packages/react-native-reanimated/__tests__/normalizeColor.test.tsx b/packages/react-native-reanimated/__tests__/normalizeColor.test.tsx new file mode 100644 index 00000000000..b3187d40185 --- /dev/null +++ b/packages/react-native-reanimated/__tests__/normalizeColor.test.tsx @@ -0,0 +1,315 @@ +import { normalizeColor } from '../src/Colors'; + +describe('Test `normalizeColor` function', () => { + describe('Only compliant color are accepted', () => { + test('test invalid colors', () => { + [ + null, + undefined, + [12, 34, 56], + { r: 12, g: 55, b: 156 }, + 0x01234567 + 0.5, + -1, + 0xffffffff + 1, + '#00gg00', + 'rgb(1, 2, 3,)', + 'rgb(1, 2, 3', + ].forEach((color) => { + expect(normalizeColor(color)).toBe(null); + }); + }); + + test('test invalid colors (format was previously accepted)', () => { + [ + 'abc', + ' #abc ', + '##abc', + 'rgb 255 0 0', + 'RGBA(0, 1, 2)', + 'rgb (0, 1, 2)', + 'rgba(0 0 0 0.0)', + 'hsv(0, 1, 2)', + { r: 10, g: 10, b: 10 }, + 'hsl(1%, 2, 3)', + 'rg b( 1%, 2%, 3%)', + ].forEach((color) => { + expect(normalizeColor(color)).toBe(null); + }); + }); + + test('test valid colors', () => { + [ + '#abc', + '#abcd', + '#abcdef', + '#abcdef01', + 'rgb(1,2,3)', + 'rgb(100 200 3)', + 'rgb(1, 2, 3)', + 'rgb( 1 , 2 , 3 )', + 'rgb(-1, -2, -3)', + 'rgba(0, 0, 0, 1)', + ].forEach((color) => { + expect(normalizeColor(color)).not.toBe(null); + }); + }); + }); + + describe('Test colors being a number ', () => { + test.each([ + [0x1, 0x1], + [0x12, 0x12], + [0x123, 0x123], + [0x1234, 0x1234], + [0x12345, 0x12345], + [0x123456, 0x123456], + [0x1234567, 0x1234567], + [0x12345678, 0x12345678], + [0x123456789, null], + [11, 11], + [158, 158], + [300.78, null], + [-300, null], + [0, 0], + [NaN, null], + [Infinity, null], + [-Infinity, null], + ])('normalizeColor(%d) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors being a number as string', () => { + test.each([ + ['0x1', null], + ['0x12', null], + ['0x123', null], + ['0x1234', null], + ['0x12345', null], + ['0x123456', null], + ['0x1234567', null], + ['0x12345678', null], + ['0x123456789', null], + ['11', null], + ['158', null], + ['300.78', null], + ['-300', null], + ['0', null], + ])('normalizeColor("%s") = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors being a hex string', () => { + describe('Test hex3', () => { + test.each([ + ['#123', 0x112233ff], + ['#abc', 0xaabbccff], + ['#ABC', 0xaabbccff], + ['#AbC', 0xaabbccff], + ['#000', 0x000000ff], + ['#fff', 0xffffffff], + ['#f0f', 0xff00ffff], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test hex4', () => { + test.each([ + ['#1234', 0x11223344], + ['#abcd', 0xaabbccdd], + ['#ABCD', 0xaabbccdd], + ['#AbcD', 0xaabbccdd], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test hex6', () => { + test.each([ + ['#000000', 0x000000ff], + ['#ffffff', 0xffffffff], + ['#ff00ff', 0xff00ffff], + ['#abcdef', 0xabcdefff], + ['#012345', 0x012345ff], + ['#123456', 0x123456ff], + ['#abcdef', 0xabcdefff], + ['#ABCDEF', 0xabcdefff], + ['#AbCdEf', 0xabcdefff], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test hex8', () => { + test.each([ + ['#00000000', 0x00000000], + ['#ffffffff', 0xffffffff], + ['#ffff00ff', 0xffff00ff], + ['#abcdef01', 0xabcdef01], + ['#01234567', 0x01234567], + ['#12345678', 0x12345678], + ['#abcdefff', 0xabcdefff], + ['#ABCDEFFF', 0xabcdefff], + ['#AbcDEfFF', 0xabcdefff], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test invalid hex', () => { + test.each([ + ['#12345', null], + ['#12345g', null], + ['#1234567', null], + ['#abcde', null], + ['#abcdeff', null], + ['#abcde', null], + ['#abcdeff', null], + ['#abcde', null], + ['#abcdeff', null], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + }); + describe('Test colors being a rgb string', () => { + test.each([ + ['rgb (0,0,0)', null], + ['rgb(50,200,150, 45)', null], + ['RGB(50,200,150)', null], + ['rgb(50,200,150, 0.45)', null], + ['rgb(0, 0, 255)', 0x0000ffff], + ['rgb(0 0 255)', 0x0000ffff], + ['rgb(100, 15, 69)', 0x640f45ff], + ['rgb(255, 255, 255)', 0xffffffff], + ['rgb(256, 256, 256)', 0xffffffff], + ['rgb(-1, -2, -3)', 0x000000ff], + ['rgb(0, 0, 0)', 0x000000ff], + ['rgb(0 0 0)', 0x000000ff], + ['rgb(0,0,0)', 0x000000ff], + ['rgb(0 ,0 ,0 )', 0x000000ff], + ['rgb( 0,0,0 )', 0x000000ff], + ['rgb(101,255,50)', 0x65ff32ff], + ['rgb(100.99999,255,50)', 0x64ff32ff], + ['rgb(100.75,255,50)', 0x64ff32ff], + ['rgb(100.5,255,50)', 0x64ff32ff], + ['rgb(100.25,255,50)', 0x64ff32ff], + ['rgb(100,255,50)', 0x64ff32ff], + ['rgb(99,255,50)', 0x63ff32ff], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors being a rgba string', () => { + test.each([ + ['RGBA(100 ,255 ,50 ,50 )', null], + ['rgba (100,255,50,.5)', null], + ['rgba(0, 0, 0, .5)', 0x00000080], + ['rgba(0, 0, 0, 0.0)', 0x00000000], + ['rgba(0, 0, 0, 0)', 0x00000000], + ['rgba(0, 0, 0, -0.5)', 0x00000000], + ['rgba(0, 0, 0, 1.0)', 0x000000ff], + ['rgba(0, 0, 0, 1)', 0x000000ff], + ['rgba(0, 0, 0, 1.5)', 0x000000ff], + ['rgba(0 0 0 / 0.0)', 0x00000000], + ['rgba(0 0 0 / 1)', 0x000000ff], + ['rgba(100, 15, 69, 0.5)', 0x640f4580], + ['rgba(100 15 69 / 0.5)', 0x640f4580], + [' rgba(100,255,50, 50)', 0x64ff32ff], + ['rgba(100 ,255 ,50 ,50 )', 0x64ff32ff], + ['rgba(100,255,50,0.5)', 0x64ff3280], + ['rgba(100,255,50,.5)', 0x64ff3280], + ['rgba(50,50,50,.5)', 0x32323280], + ['rgba(50,50,50,1)', 0x323232ff], + ['rgba(50,50,50,0)', 0x32323200], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors being a hsl string', () => { + test.each([ + ['HSL(0,100%,50%)', null], + ['hsl(120 ,0.99, 0.1 )', null], + ['hsl(0,100,50)', null], + ['hsl(0,100%,50%, 0.5)', null], + ['hsl(0, 0%, 0%)', 0x000000ff], + ['hsl(360, 100%, 100%)', 0xffffffff], + ['hsl(180, 50%, 50%)', 0x40bfbfff], + ['hsl(540, 50%, 50%)', 0x40bfbfff], + ['hsl(70, 25%, 75%)', 0xcacfafff], + ['hsl(70, 100%, 75%)', 0xeaff80ff], + ['hsl(70, 110%, 75%)', 0xeaff80ff], + ['hsl(70, 0%, 75%)', 0xbfbfbfff], + ['hsl(70, -10%, 75%)', 0xbfbfbfff], + ['hsl(0 0% 0%)', 0x000000ff], + ['hsl(360 100% 100%)', 0xffffffff], + ['hsl(180 50% 50%)', 0x40bfbfff], + ['hsl(0,100%,50%)', 0xff0000ff], + ['hsl(0,100%,50%)', 0xff0000ff], + ['hsl(120,100%,50%)', 0x00ff00ff], + ['hsl(120.5,100%,50%)', 0x00ff02ff], + ['hsl(120.999,100%,50%)', 0x00ff04ff], + ['hsl(120,50%,50%)', 0x40bf40ff], + ['hsl(120 ,50%, 50% )', 0x40bf40ff], + ['hsl(130 ,13.33%, 100% )', 0xffffffff], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors being a hsla string', () => { + test.each([ + ['hsla(0, 0%, 0%, 0)', 0x00000000], + ['hsla(360, 100%, 100%, 1)', 0xffffffff], + ['hsla(360, 100%, 100%, 0)', 0xffffff00], + ['hsla(180, 50%, 50%, 0.2)', 0x40bfbf33], + ['hsla(0 0% 0% / 0)', 0x00000000], + ['hsla(360 100% 100% / 1)', 0xffffffff], + ['hsla(360 100% 100% / 0)', 0xffffff00], + ['hsla(180 50% 50% / 0.2)', 0x40bfbf33], + ['HSLA(0,100%,50%,0.5)', null], + ['hsla(0,100%,50%,0.5)', 0xff000080], + ['hsla(120,100%,50%, 0.5)', 0x00ff0080], + ['hsla(120,100%,50%, 1)', 0x00ff00ff], + ['hsla(120,100%,50%, 0)', 0x00ff0000], + [' hsla( 120 , 100% , 50%, 0.5 ) ', 0x00ff0080], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors being a hwb string', () => { + test.each([ + ['hwb(0, 0%, 100%)', 0x000000ff], + ['hwb(0, 100%, 0%)', 0xffffffff], + ['hwb(0, 0%, 0%)', 0xff0000ff], + ['hwb(70, 50%, 0%)', 0xeaff80ff], + ['hwb(0, 50%, 50%)', 0x808080ff], + ['hwb(360, 100%, 100%)', 0x808080ff], + ['hwb(0 0% 0%)', 0xff0000ff], + ['hwb(70 50% 0%)', 0xeaff80ff], + ['HWB(0,100%,50%)', null], + ['hwb(0,67%, 33%)', 0xabababff], + ['hwb(0,67% , 33%)', 0xabababff], + ['hwb(48, 38%, 6%)', 0xf0d361ff], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); + + describe('Test colors a colorName string', () => { + test.each([ + ['red', 0xff0000ff], + ['transparent', 0x00000000], + ['peachpuff', 0xffdab9ff], + ['peachPuff', null], + ['PeachPuff', null], + ])('normalizeColor(%s) = %p', (color, expectedColor) => { + expect(normalizeColor(color)).toBe(expectedColor); + }); + }); +}); diff --git a/packages/react-native-reanimated/src/Colors.ts b/packages/react-native-reanimated/src/Colors.ts index 93d5c220a52..2b01b32668a 100644 --- a/packages/react-native-reanimated/src/Colors.ts +++ b/packages/react-native-reanimated/src/Colors.ts @@ -168,6 +168,7 @@ function parsePercentage(str: string): number { const names: Record = makeShareable({ transparent: 0x00000000, + /* spell-checker: disable */ // http://www.w3.org/TR/css3-color/#svg-color aliceblue: 0xf0f8ffff, antiquewhite: 0xfaebd7ff, @@ -318,6 +319,7 @@ const names: Record = makeShareable({ whitesmoke: 0xf5f5f5ff, yellow: 0xffff00ff, yellowgreen: 0x9acd32ff, + /* spell-checker: enable */ }); // copied from react-native/Libraries/Components/View/ReactNativeStyleAttributes @@ -341,6 +343,7 @@ export const ColorProperties = makeShareable([ 'overlayColor', ]); +// // ts-prune-ignore-next Exported for the purpose of tests only export function normalizeColor(color: unknown): number | null { 'worklet'; @@ -641,7 +644,7 @@ function processColorInitially(color: unknown): number | null | undefined { return null; } - normalizedColor = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0; // argb + normalizedColor = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0; // alpha rgb return normalizedColor; } @@ -688,7 +691,7 @@ export type ParsedColorArray = [number, number, number, number]; export function convertToRGBA(color: unknown): ParsedColorArray { 'worklet'; - const processedColor = processColorInitially(color)!; // argb; + const processedColor = processColorInitially(color)!; // alpha rgb; const a = (processedColor >>> 24) / 255; const r = ((processedColor << 8) >>> 24) / 255; const g = ((processedColor << 16) >>> 24) / 255; diff --git a/packages/react-native-reanimated/src/__tests__/Colors.test.ts b/packages/react-native-reanimated/src/__tests__/Colors.test.ts deleted file mode 100644 index 9ee08b667c3..00000000000 --- a/packages/react-native-reanimated/src/__tests__/Colors.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -'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); -});