diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js index 70c44d2910607d..306e43bc7107ad 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js @@ -30,8 +30,8 @@ describe('processBackgroundImage', () => { type: 'linearGradient', direction: {type: 'angle', value: 90}, colorStops: [ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ], }, ]); @@ -45,8 +45,8 @@ describe('processBackgroundImage', () => { type: 'linearGradient', direction: {type: 'keyword', value: 'to bottom right'}, colorStops: [ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ], }, ]); @@ -74,8 +74,8 @@ describe('processBackgroundImage', () => { type: 'linearGradient', direction: {type: 'keyword', value: 'to bottom right'}, colorStops: [ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ], }, ]); @@ -90,8 +90,8 @@ describe('processBackgroundImage', () => { type: 'linearGradient', direction: {type: 'keyword', value: 'to bottom right'}, colorStops: [ - {color: processColor('red'), position: 0.3}, - {color: processColor('blue'), position: 0.8}, + {color: processColor('red'), position: '30%'}, + {color: processColor('blue'), position: '80%'}, ], }, ]); @@ -103,8 +103,8 @@ describe('processBackgroundImage', () => { expect(result[0].type).toBe('linearGradient'); expect(result[0].direction).toEqual({type: 'angle', value: 45}); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); }); @@ -114,8 +114,8 @@ describe('processBackgroundImage', () => { expect(result[0].type).toBe('linearGradient'); expect(result[0].direction).toEqual({type: 'angle', value: 45}); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); }); @@ -131,8 +131,8 @@ describe('processBackgroundImage', () => { expect(result[0].type).toBe('linearGradient'); expect(result[0].direction).toEqual({type: 'angle', value: 180}); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); }); @@ -148,8 +148,8 @@ describe('processBackgroundImage', () => { expect(result[0].type).toBe('linearGradient'); expect(result[0].direction).toEqual({type: 'angle', value: 45}); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); }); @@ -162,8 +162,8 @@ describe('processBackgroundImage', () => { value: 180, }); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); }); @@ -175,8 +175,8 @@ describe('processBackgroundImage', () => { value: 90, }); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); }); @@ -189,9 +189,9 @@ describe('processBackgroundImage', () => { value: 270, }); expect(result[0].colorStops).toEqual([ - {color: processColor('rgba(0, 0, 0, 0.5)'), position: 0}, - {color: processColor('blue'), position: 0.5}, - {color: processColor('hsla(0, 100%, 50%, 0.5)'), position: 1}, + {color: processColor('rgba(0, 0, 0, 0.5)'), position: null}, + {color: processColor('blue'), position: null}, + {color: processColor('hsla(0, 100%, 50%, 0.5)'), position: null}, ]); }); @@ -207,8 +207,8 @@ describe('processBackgroundImage', () => { value: 0, }); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); expect(result[1].type).toEqual('linearGradient'); expect(result[1].direction).toEqual({ @@ -217,8 +217,8 @@ describe('processBackgroundImage', () => { }); expect(result[1].colorStops).toEqual([ - {color: processColor('green'), position: 0}, - {color: processColor('yellow'), position: 1}, + {color: processColor('green'), position: null}, + {color: processColor('yellow'), position: null}, ]); }); @@ -234,8 +234,8 @@ describe('processBackgroundImage', () => { value: 270, }); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('blue'), position: null}, ]); expect(result[1].type).toEqual('linearGradient'); expect(result[1].direction).toEqual({ @@ -244,8 +244,8 @@ describe('processBackgroundImage', () => { }); expect(result[1].colorStops).toEqual([ - {color: processColor('green'), position: 0}, - {color: processColor('yellow'), position: 1}, + {color: processColor('green'), position: null}, + {color: processColor('yellow'), position: null}, ]); }); @@ -253,9 +253,9 @@ describe('processBackgroundImage', () => { const input = 'linear-gradient(to bottom, red 0%, green 50%, blue 100%)'; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('green'), position: 0.5}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: '0%'}, + {color: processColor('green'), position: '50%'}, + {color: processColor('blue'), position: '100%'}, ]); }); @@ -264,11 +264,11 @@ describe('processBackgroundImage', () => { 'linear-gradient(to right, red, green, blue 60%, yellow, purple)'; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: processColor('green'), position: 0.3}, - {color: processColor('blue'), position: 0.6}, - {color: processColor('yellow'), position: 0.8}, - {color: processColor('purple'), position: 1}, + {color: processColor('red'), position: null}, + {color: processColor('green'), position: null}, + {color: processColor('blue'), position: '60%'}, + {color: processColor('yellow'), position: null}, + {color: processColor('purple'), position: null}, ]); }); @@ -277,8 +277,8 @@ describe('processBackgroundImage', () => { 'linear-gradient(to right, rgba(255,0,0,0.5), rgba(0,0,255,0.8))'; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('rgba(255,0,0,0.5)'), position: 0}, - {color: processColor('rgba(0,0,255,0.8)'), position: 1}, + {color: processColor('rgba(255,0,0,0.5)'), position: null}, + {color: processColor('rgba(0,0,255,0.8)'), position: null}, ]); }); @@ -286,8 +286,8 @@ describe('processBackgroundImage', () => { const input = `linear-gradient(hsl(330, 100%, 45.1%), hsl(0, 100%, 50%))`; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('hsl(330, 100%, 45.1%)'), position: 0}, - {color: processColor('hsl(0, 100%, 50%)'), position: 1}, + {color: processColor('hsl(330, 100%, 45.1%)'), position: null}, + {color: processColor('hsl(0, 100%, 50%)'), position: null}, ]); }); @@ -295,8 +295,8 @@ describe('processBackgroundImage', () => { const input = 'linear-gradient(#e66465, #9198e5)'; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('#e66465'), position: 0}, - {color: processColor('#9198e5'), position: 1}, + {color: processColor('#e66465'), position: null}, + {color: processColor('#9198e5'), position: null}, ]); }); @@ -315,12 +315,12 @@ describe('processBackgroundImage', () => { value: 180, }); expect(result[0].colorStops).toEqual([ - {color: processColor('rgba(255,0,0,0.5)'), position: 0}, - {color: processColor('rgba(0,0,255,0.8)'), position: 1}, + {color: processColor('rgba(255,0,0,0.5)'), position: null}, + {color: processColor('rgba(0,0,255,0.8)'), position: null}, ]); expect(result[1].colorStops).toEqual([ - {color: processColor('rgba(255,0,0,0.9)'), position: 0}, - {color: processColor('rgba(0,0,255,0.2)'), position: 1}, + {color: processColor('rgba(255,0,0,0.9)'), position: null}, + {color: processColor('rgba(0,0,255,0.2)'), position: null}, ]); }); @@ -365,8 +365,8 @@ describe('processBackgroundImage', () => { type: 'linearGradient', direction: {type: 'keyword', value: 'to bottom right'}, colorStops: [ - {color: processColor('red'), position: 0}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: '0%'}, + {color: processColor('blue'), position: '100%'}, ], }, ]); @@ -431,19 +431,19 @@ describe('processBackgroundImage', () => { const output = [ { color: processColor('red'), - position: 0.4, + position: '40%', }, { color: processColor('blue'), - position: 0.6, + position: null, }, { color: processColor('green'), - position: 0.8, + position: null, }, { color: processColor('purple'), - position: 1, + position: null, }, ]; const result = processBackgroundImage(input); @@ -472,19 +472,19 @@ describe('processBackgroundImage', () => { const output = [ { color: processColor('red'), - position: 0.4, + position: '40%', }, { color: processColor('red'), - position: 0.8, + position: '80%', }, { color: processColor('blue'), - position: 0.9, + position: null, }, { color: processColor('green'), - position: 1, + position: null, }, ]; expect(result[0].colorStops).toEqual(output); @@ -505,15 +505,15 @@ describe('processBackgroundImage', () => { const output = [ { color: processColor('red'), - position: 0, + position: null, }, { color: processColor('blue'), - position: 0.2, + position: '20%', }, { color: processColor('green'), - position: 1, + position: null, }, ]; const result = processBackgroundImage(input); @@ -538,15 +538,15 @@ describe('processBackgroundImage', () => { const output = [ { color: processColor('red'), - position: -0.5, + position: '-50%', }, { color: processColor('blue'), - position: 0.25, + position: null, }, { color: processColor('green'), - position: 1, + position: null, }, ]; const result = processBackgroundImage(input); @@ -572,19 +572,19 @@ describe('processBackgroundImage', () => { const output = [ { color: processColor('red'), - position: 0, + position: null, }, { color: processColor('blue'), - position: 0, + position: '-50%', }, { color: processColor('green'), - position: 1.5, + position: '150%', }, { color: processColor('yellow'), - position: 1.5, + position: null, }, ]; const result = processBackgroundImage(input); @@ -600,11 +600,11 @@ describe('processBackgroundImage', () => { 'linear-gradient(red 40% 20%, blue 90% 120% , green)', ); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0.4}, - {color: processColor('red'), position: 0.4}, - {color: processColor('blue'), position: 0.9}, - {color: processColor('blue'), position: 1.2}, - {color: processColor('green'), position: 1.2}, + {color: processColor('red'), position: '40%'}, + {color: processColor('red'), position: '20%'}, + {color: processColor('blue'), position: '90%'}, + {color: processColor('blue'), position: '120%'}, + {color: processColor('green'), position: null}, ]); }); @@ -613,12 +613,12 @@ describe('processBackgroundImage', () => { 'linear-gradient(red 40% 20%, blue 90% 120% , green 200% 300%)', ); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0.4}, - {color: processColor('red'), position: 0.4}, - {color: processColor('blue'), position: 0.9}, - {color: processColor('blue'), position: 1.2}, - {color: processColor('green'), position: 2}, - {color: processColor('green'), position: 3}, + {color: processColor('red'), position: '40%'}, + {color: processColor('red'), position: '20%'}, + {color: processColor('blue'), position: '90%'}, + {color: processColor('blue'), position: '120%'}, + {color: processColor('green'), position: '200%'}, + {color: processColor('green'), position: '300%'}, ]); }); @@ -710,9 +710,9 @@ describe('processBackgroundImage', () => { expect(result[0].type).toBe('linearGradient'); expect(result[0].direction).toEqual({type: 'angle', value: 180}); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.2}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: null, position: '20%'}, + {color: processColor('blue'), position: null}, ]); }); @@ -722,9 +722,9 @@ describe('processBackgroundImage', () => { expect(result[0].type).toBe('linearGradient'); expect(result[0].direction).toEqual({type: 'angle', value: 180}); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.4}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: null, position: '40%'}, + {color: processColor('blue'), position: null}, ]); }); @@ -732,9 +732,6 @@ describe('processBackgroundImage', () => { let result = processBackgroundImage('linear-gradient(red, 40, blue)'); expect(result).toEqual([]); - result = processBackgroundImage('linear-gradient(red, 40px, blue)'); - expect(result).toEqual([]); - // Multiple hints in a row result = processBackgroundImage('linear-gradient(red, 20%, 40%, blue)'); expect(result).toEqual([]); @@ -753,13 +750,13 @@ describe('processBackgroundImage', () => { const input = 'linear-gradient(red, 20%, blue, 60%, green, 80%, yellow)'; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.2}, - {color: processColor('blue'), position: 0.4}, - {color: null, position: 0.6}, - {color: processColor('green'), position: 0.7}, - {color: null, position: 0.8}, - {color: processColor('yellow'), position: 1}, + {color: processColor('red'), position: null}, + {color: null, position: '20%'}, + {color: processColor('blue'), position: null}, + {color: null, position: '60%'}, + {color: processColor('green'), position: null}, + {color: null, position: '80%'}, + {color: processColor('yellow'), position: null}, ]); }); @@ -781,13 +778,13 @@ describe('processBackgroundImage', () => { ]; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.2}, - {color: processColor('blue'), position: 0.4}, - {color: null, position: 0.6}, - {color: processColor('green'), position: 0.7}, - {color: null, position: 0.8}, - {color: processColor('yellow'), position: 1}, + {color: processColor('red'), position: null}, + {color: null, position: '20%'}, + {color: processColor('blue'), position: null}, + {color: null, position: '60%'}, + {color: processColor('green'), position: null}, + {color: null, position: '80%'}, + {color: processColor('yellow'), position: null}, ]); }); @@ -795,11 +792,11 @@ describe('processBackgroundImage', () => { const input = 'linear-gradient(red 0%, 25%, blue 50%, 75%, green 100%)'; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.25}, - {color: processColor('blue'), position: 0.5}, - {color: null, position: 0.75}, - {color: processColor('green'), position: 1}, + {color: processColor('red'), position: '0%'}, + {color: null, position: '25%'}, + {color: processColor('blue'), position: '50%'}, + {color: null, position: '75%'}, + {color: processColor('green'), position: '100%'}, ]); }); @@ -817,15 +814,15 @@ describe('processBackgroundImage', () => { )`; const result = processBackgroundImage(input); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.2}, - {color: processColor('blue'), position: 0.3}, - {color: null, position: 0.45}, - {color: processColor('green'), position: 0.5}, - {color: null, position: 0.65}, - {color: processColor('yellow'), position: 0.7}, - {color: null, position: 0.85}, - {color: processColor('purple'), position: 1}, + {color: processColor('red'), position: '0%'}, + {color: null, position: '20%'}, + {color: processColor('blue'), position: '30%'}, + {color: null, position: '45%'}, + {color: processColor('green'), position: '50%'}, + {color: null, position: '65%'}, + {color: processColor('yellow'), position: '70%'}, + {color: null, position: '85%'}, + {color: processColor('purple'), position: '100%'}, ]); }); @@ -837,14 +834,61 @@ describe('processBackgroundImage', () => { const result = processBackgroundImage(input); expect(result).toHaveLength(2); expect(result[0].colorStops).toEqual([ - {color: processColor('red'), position: 0}, - {color: null, position: 0.3}, - {color: processColor('blue'), position: 1}, + {color: processColor('red'), position: null}, + {color: null, position: '30%'}, + {color: processColor('blue'), position: null}, ]); expect(result[1].colorStops).toEqual([ - {color: processColor('green'), position: 0}, - {color: null, position: 0.6}, - {color: processColor('yellow'), position: 1}, + {color: processColor('green'), position: null}, + {color: null, position: '60%'}, + {color: processColor('yellow'), position: null}, + ]); + }); + + it('should handle invalid transition hint', () => { + const input = ` + linear-gradient(red, 30%, blue, 60%, green, 80%) + `; + const result = processBackgroundImage(input); + expect(result).toEqual([]); + const input1 = ` + linear-gradient(red, 30%, 60%, green) + `; + const result1 = processBackgroundImage(input1); + expect(result1).toEqual([]); + + const input2 = ` + linear-gradient(20%, red, green) + `; + const result2 = processBackgroundImage(input2); + expect(result2).toEqual([]); + }); + + it('should process gradient with % and px color stop positions', () => { + const input = 'linear-gradient(red 10%, 20px, blue 30%, purple 40px)'; + const result = processBackgroundImage(input); + expect(result[0].colorStops).toEqual([ + {color: processColor('red'), position: '10%'}, + {color: null, position: 20}, + {color: processColor('blue'), position: '30%'}, + {color: processColor('purple'), position: 40}, + ]); + + const input1 = [ + { + type: 'linearGradient', + colorStops: [ + {color: 'red', positions: ['10%', 20]}, + {color: 'blue', positions: ['30%', 40]}, + ], + }, + ]; + const result1 = processBackgroundImage(input1); + expect(result1[0].colorStops).toEqual([ + {color: processColor('red'), position: '10%'}, + {color: processColor('red'), position: 20}, + {color: processColor('blue'), position: '30%'}, + {color: processColor('blue'), position: 40}, ]); }); }); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index c3aef678fc59bf..d6599c50c6f91d 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -22,15 +22,17 @@ type LinearGradientDirection = | {type: 'angle', value: number} | {type: 'keyword', value: string}; -// null color stops indicate that the transition hint syntax is used. e.g. red, 20%, blue +// null color indicate that the transition hint syntax is used. e.g. red, 20%, blue type ColorStopColor = ProcessedColorValue | null; +// percentage or pixel value +type ColorStopPosition = number | string | null; type ParsedGradientValue = { type: 'linearGradient', direction: LinearGradientDirection, colorStops: $ReadOnlyArray<{ color: ColorStopColor, - position: number, + position: ColorStopPosition, }>, }; @@ -53,7 +55,7 @@ export default function processBackgroundImage( for (const bgImage of backgroundImage) { const processedColorStops: Array<{ color: ColorStopColor, - position: number | null, + position: ColorStopPosition, }> = []; for (let index = 0; index < bgImage.colorStops.length; index++) { const colorStop = bgImage.colorStops[index]; @@ -65,10 +67,13 @@ export default function processBackgroundImage( positions.length === 1 ) { const position = positions[0]; - if (typeof position === 'string' && position.endsWith('%')) { + if ( + typeof position === 'number' || + (typeof position === 'string' && position.endsWith('%')) + ) { processedColorStops.push({ color: null, - position: parseFloat(position) / 100, + position, }); } else { // If a position is invalid, return an empty array and do not apply gradient. Same as web. @@ -82,10 +87,13 @@ export default function processBackgroundImage( } if (positions != null && positions.length > 0) { for (const position of positions) { - if (position.endsWith('%')) { + if ( + typeof position === 'number' || + (typeof position === 'string' && position.endsWith('%')) + ) { processedColorStops.push({ color: processedColor, - position: parseFloat(position) / 100, + position, }); } else { // If a position is invalid, return an empty array and do not apply gradient. Same as web. @@ -131,12 +139,10 @@ export default function processBackgroundImage( } } - const fixedColorStops = getFixedColorStops(processedColorStops); - result = result.concat({ type: 'linearGradient', direction, - colorStops: fixedColorStops, + colorStops: processedColorStops, }); } } @@ -186,8 +192,9 @@ function parseCSSLinearGradient( const colorStops = []; // split by comma, but not if it's inside a parentheses. e.g. red, rgba(0, 0, 0, 0.5), green => ["red", "rgba(0, 0, 0, 0.5)", "green"] const stops = colorStopsString.split(/,(?![^(]*\))/); - let lastStop = null; - for (const stop of stops) { + let prevStop = null; + for (let i = 0; i < stops.length; i++) { + const stop = stops[i]; const trimmedStop = stop.trim().toLowerCase(); // Match function like pattern or single words const colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g); @@ -198,61 +205,65 @@ function parseCSSLinearGradient( // Case 1: [color, position, position] if (colorStopParts.length === 3) { const color = colorStopParts[0]; - const position1 = colorStopParts[1]; - const position2 = colorStopParts[2]; + const position1 = getPositionFromCSSValue(colorStopParts[1]); + const position2 = getPositionFromCSSValue(colorStopParts[2]); const processedColor = processColor(color); if (processedColor == null) { // If a color is invalid, return an empty array and do not apply any gradient. Same as web. return []; } - if (position1.endsWith('%') && position2.endsWith('%')) { - colorStops.push({ - color: processedColor, - position: parseFloat(position1) / 100, - }); - colorStops.push({ - color: processedColor, - position: parseFloat(position2) / 100, - }); - } else { + + if (position1 == null || position2 == null) { // If a position is invalid, return an empty array and do not apply any gradient. Same as web. return []; } + + colorStops.push({ + color: processedColor, + position: position1, + }); + colorStops.push({ + color: processedColor, + position: position2, + }); } // Case 2: [color, position] else if (colorStopParts.length === 2) { const color = colorStopParts[0]; - const position = colorStopParts[1]; + const position = getPositionFromCSSValue(colorStopParts[1]); const processedColor = processColor(color); if (processedColor == null) { // If a color is invalid, return an empty array and do not apply any gradient. Same as web. return []; } - if (position.endsWith('%')) { - colorStops.push({ - color: processedColor, - position: parseFloat(position) / 100, - }); - } else { + if (position == null) { // If a position is invalid, return an empty array and do not apply any gradient. Same as web. return []; } + colorStops.push({ + color: processedColor, + position, + }); } // Case 3: [color] // Case 4: [position] => transition hint syntax else if (colorStopParts.length === 1) { - if (colorStopParts[0].endsWith('%')) { + const position = getPositionFromCSSValue(colorStopParts[0]); + if (position != null) { + // handle invalid transition hint syntax. transition hint syntax must have color before and after the position. e.g. red, 20%, blue if ( - lastStop != null && - lastStop.length === 1 && - lastStop[0].endsWith('%') + (prevStop != null && + prevStop.length === 1 && + getPositionFromCSSValue(prevStop[0]) != null) || + i === stops.length - 1 || + i === 0 ) { // If the last stop is a transition hint syntax, return an empty array and do not apply any gradient. Same as web. return []; } colorStops.push({ color: null, - position: parseFloat(colorStopParts[0]) / 100, + position, }); } else { const processedColor = processColor(colorStopParts[0]); @@ -269,15 +280,13 @@ function parseCSSLinearGradient( // If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. return []; } - lastStop = colorStopParts; + prevStop = colorStopParts; } - const fixedColorStops = getFixedColorStops(colorStops); - gradients.push({ type: 'linearGradient', direction, - colorStops: fixedColorStops, + colorStops, }); } @@ -343,79 +352,12 @@ function getAngleInDegrees(angle?: string): ?number { } } -// https://drafts.csswg.org/css-images-4/#color-stop-fixup -function getFixedColorStops( - colorStops: $ReadOnlyArray<{ - color: ColorStopColor, - position: number | null, - }>, -): Array<{ - color: ColorStopColor, - position: number, -}> { - let fixedColorStops: Array<{ - color: ColorStopColor, - position: number, - }> = []; - let hasNullPositions = false; - let maxPositionSoFar = colorStops[0].position ?? 0; - for (let i = 0; i < colorStops.length; i++) { - const colorStop = colorStops[i]; - let newPosition = colorStop.position; - if (newPosition === null) { - // Step 1: - // If the first color stop does not have a position, - // set its position to 0%. If the last color stop does not have a position, - // set its position to 100%. - if (i === 0) { - newPosition = 0; - } else if (i === colorStops.length - 1) { - newPosition = 1; - } - } - // Step 2: - // If a color stop or transition hint has a position - // that is less than the specified position of any color stop or transition hint - // before it in the list, set its position to be equal to the - // largest specified position of any color stop or transition hint before it. - if (newPosition !== null) { - newPosition = Math.max(newPosition, maxPositionSoFar); - fixedColorStops[i] = { - color: colorStop.color, - position: newPosition, - }; - maxPositionSoFar = newPosition; - } else { - hasNullPositions = true; - } +function getPositionFromCSSValue(position: string) { + if (position.endsWith('px')) { + return parseFloat(position); } - // Step 3: - // If any color stop still does not have a position, - // then, for each run of adjacent color stops without positions, - // set their positions so that they are evenly spaced between the preceding and - // following color stops with positions. - if (hasNullPositions) { - let lastDefinedIndex = 0; - for (let i = 1; i < fixedColorStops.length; i++) { - if (fixedColorStops[i] !== undefined) { - const unpositionedStops = i - lastDefinedIndex - 1; - if (unpositionedStops > 0) { - const startPosition = fixedColorStops[lastDefinedIndex].position; - const endPosition = fixedColorStops[i].position; - const increment = - (endPosition - startPosition) / (unpositionedStops + 1); - for (let j = 1; j <= unpositionedStops; j++) { - fixedColorStops[lastDefinedIndex + j] = { - color: colorStops[lastDefinedIndex + j].color, - position: startPosition + increment * j, - }; - } - } - lastDefinedIndex = i; - } - } + if (position.endsWith('%')) { + return position; } - - return fixedColorStops; } diff --git a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm index 5dcc2fd18bf47b..0d621e9bbeadde 100644 --- a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm @@ -10,6 +10,7 @@ #import #import #import +#include using namespace facebook::react; @@ -19,22 +20,7 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient & { UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size]; const auto &direction = gradient.direction; - const auto colorStops = processColorTransitionHints(gradient.colorStops); - UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { - CGContextRef context = rendererContext.CGContext; - NSMutableArray *colors = [NSMutableArray array]; - CGFloat locations[colorStops.size()]; - - for (size_t i = 0; i < colorStops.size(); ++i) { - const auto &colorStop = colorStops[i]; - CGColorRef cgColor = RCTCreateCGColorRefFromSharedColor(colorStop.color); - [colors addObject:(__bridge id)cgColor]; - locations[i] = colorStop.position; - } - - CGGradientRef cgGradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations); - CGPoint startPoint; CGPoint endPoint; @@ -50,6 +36,25 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient & startPoint = CGPointMake(0.0, 0.0); endPoint = CGPointMake(0.0, size.height); } + + CGFloat dx = endPoint.x - startPoint.x; + CGFloat dy = endPoint.y - startPoint.y; + CGFloat gradientLineLength = sqrt(dx * dx + dy * dy); + const auto processedStops = getFixedColorStops(gradient.colorStops, gradientLineLength); + const auto colorStops = processColorTransitionHints(processedStops); + + CGContextRef context = rendererContext.CGContext; + NSMutableArray *colors = [NSMutableArray array]; + CGFloat locations[colorStops.size()]; + + for (size_t i = 0; i < colorStops.size(); ++i) { + const auto &colorStop = colorStops[i]; + CGColorRef cgColor = RCTCreateCGColorRefFromSharedColor(colorStop.color); + [colors addObject:(__bridge id)cgColor]; + locations[i] = std::max(std::min(colorStop.position.resolve(0.0f), 1.0f), 0.0f); + } + + CGGradientRef cgGradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations); CGContextDrawLinearGradient(context, cgGradient, startPoint, endPoint, 0); @@ -138,13 +143,12 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) // Algorithm is referred from Blink engine [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240). static std::vector processColorTransitionHints(const std::vector& originalStops) { - std::vector colorStops = originalStops; + std::vector colorStops = std::vector(originalStops); int indexOffset = 0; - for (size_t i = 1; i < colorStops.size() - 1; i++) { - auto &colorStop = colorStops[i]; + for (size_t i = 1; i < originalStops.size() - 1; ++i) { // Skip if not a color hint - if (colorStop.color) { + if (originalStops[i].color) { continue; } @@ -153,9 +157,9 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) continue; } - Float offsetLeft = colorStops[x - 1].position; - Float offsetRight = colorStops[x + 1].position; - Float offset = colorStop.position; + Float offsetLeft = colorStops[x - 1].position.resolve(0.0f); + Float offsetRight = colorStops[x + 1].position.resolve(0.0f); + Float offset = colorStops[x].position.resolve(0.0f); Float leftDist = offset - offsetLeft; Float rightDist = offsetRight - offset; Float totalDist = offsetRight - offsetLeft; @@ -169,12 +173,12 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) } if (facebook::react::floatEquality(leftDist, .0f)) { - colorStop.color = rightSharedColor; + colorStops[x].color = rightSharedColor; continue; } if (facebook::react::floatEquality(rightDist, .0f)) { - colorStop.color = leftSharedColor; + colorStops[x].color = leftSharedColor; continue; } @@ -186,35 +190,35 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) for (int y = 0; y < 7; ++y) { ColorStop newStop{ SharedColor(), - offsetLeft + leftDist * ((7.0f + y) / 13.0f) + ValueUnit(offsetLeft + leftDist * ((7.0f + y) / 13.0f), UnitType::Point) }; newStops.push_back(newStop); } ColorStop stop1{ SharedColor(), - offset + rightDist * (1.0f / 3.0f) + ValueUnit(offset + rightDist * (1.0f / 3.0f), UnitType::Point) }; ColorStop stop2 { SharedColor(), - offset + rightDist * (2.0f / 3.0f) + ValueUnit(offset + rightDist * (2.0f / 3.0f), UnitType::Point) }; newStops.push_back(stop1); newStops.push_back(stop2); } else { ColorStop stop1 { SharedColor(), - offsetLeft + leftDist * (1.0f / 3.0f) + ValueUnit(offsetLeft + leftDist * (1.0f / 3.0f), UnitType::Point) }; ColorStop stop2 { SharedColor(), - offsetLeft + leftDist * (2.0f / 3.0f) + ValueUnit(offsetLeft + leftDist * (2.0f / 3.0f), UnitType::Point) }; newStops.push_back(stop1); newStops.push_back(stop2); for (int y = 0; y < 7; ++y) { ColorStop newStop { SharedColor(), - offset + rightDist * (y / 13.0f) + ValueUnit(offset + rightDist * (y / 13.0f), UnitType::Point) }; newStops.push_back(newStop); } @@ -231,7 +235,7 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) NSArray *outputRange = @[leftColor, rightColor]; for (auto &newStop : newStops) { - Float pointRelativeOffset = (newStop.position - offsetLeft) / totalDist; + Float pointRelativeOffset = (newStop.position.resolve(0.0f) - offsetLeft) / totalDist; Float weighting = pow( pointRelativeOffset, logRatio @@ -261,4 +265,89 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) return colorStops; } +// https://drafts.csswg.org/css-images-4/#color-stop-fixup +static std::vector getFixedColorStops(const std::vector& colorStops, CGFloat gradientLineLength) { + std::vector fixedColorStops(colorStops.size()); + bool hasNullPositions = false; + float maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength).resolve(0.0f); + + for (size_t i = 0; i < colorStops.size(); i++) { + const auto& colorStop = colorStops[i]; + ValueUnit newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength); + + if (newPosition.unit == UnitType::Undefined) { + // Step 1: + // If the first color stop does not have a position, + // set its position to 0%. If the last color stop does not have a position, + // set its position to 100%. + if (i == 0) { + newPosition.value = 0.0f; + newPosition.unit = UnitType::Point; + } else if (i == colorStops.size() - 1) { + newPosition.value = 1.0f; + newPosition.unit = UnitType::Point; + } + } + + // Step 2: + // If a color stop or transition hint has a position + // that is less than the specified position of any color stop or transition hint + // before it in the list, set its position to be equal to the + // largest specified position of any color stop or transition hint before it. + if (newPosition.unit != UnitType::Undefined) { + newPosition.value = std::max(newPosition.resolve(0.0f), maxPositionSoFar); + newPosition.unit = UnitType::Point; + fixedColorStops[i] = ColorStop{ + colorStop.color, + newPosition + }; + maxPositionSoFar = newPosition.resolve(0.0f); + } else { + hasNullPositions = true; + } + } + + // Step 3: + // If any color stop still does not have a position, + // then, for each run of adjacent color stops without positions, + // set their positions so that they are evenly spaced between the preceding and + // following color stops with positions. + if (hasNullPositions) { + size_t lastDefinedIndex = 0; + for (size_t i = 1; i < fixedColorStops.size(); i++) { + if (fixedColorStops[i].position.unit != UnitType::Undefined) { + size_t unpositionedStops = i - lastDefinedIndex - 1; + if (unpositionedStops > 0) { + Float startPosition = fixedColorStops[lastDefinedIndex].position.resolve(0.0f); + Float endPosition = fixedColorStops[i].position.resolve(0.0f); + Float increment = (endPosition - startPosition) / (unpositionedStops + 1); + + for (size_t j = 1; j <= unpositionedStops; j++) { + fixedColorStops[lastDefinedIndex + j] = ColorStop{ + colorStops[lastDefinedIndex + j].color, + ValueUnit(startPosition + increment * j, UnitType::Point) + }; + } + } + lastDefinedIndex = i; + } + } + } + + return fixedColorStops; +} + +static ValueUnit resolveColorStopPosition(ValueUnit position, CGFloat gradientLineLength) { + if (position.unit == UnitType::Point) { + return ValueUnit(position.resolve(0.0f) / gradientLineLength, UnitType::Point); + } + + if (position.unit == UnitType::Percent) { + return ValueUnit(position.resolve(1.0f), UnitType::Point); + } + + return position; + +} + @end diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Gradient.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Gradient.kt index b4e337e80dfe50..01501a60d54b78 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Gradient.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Gradient.kt @@ -8,21 +8,10 @@ package com.facebook.react.uimanager.style import android.content.Context -import android.graphics.Color import android.graphics.Rect import android.graphics.Shader -import androidx.core.graphics.ColorUtils -import com.facebook.react.bridge.ColorPropConverter -import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.ReadableType -import com.facebook.react.uimanager.FloatUtil -import kotlin.math.ln -private data class ColorStop( - var color: Int? = null, - val position: Float -) internal class Gradient(gradient: ReadableMap?, context: Context) { private enum class GradientType { @@ -46,22 +35,11 @@ internal class Gradient(gradient: ReadableMap?, context: Context) { gradient.getMap("direction") ?: throw IllegalArgumentException("Gradient must have direction") - val colorStopsRaw = + val colorStops = gradient.getArray("colorStops") ?: throw IllegalArgumentException("Invalid colorStops array") - val colorStops = processColorTransitionHints(colorStopsRaw, context); - val colors = IntArray(colorStops.size) - val positions = FloatArray(colorStops.size) - - colorStops.forEachIndexed { i, colorStop -> - colorStop.color?.let { color -> - colors[i] = color - positions[i] = colorStop.position - } - } - - linearGradient = LinearGradient(directionMap, colors, positions) + linearGradient = LinearGradient(directionMap, colorStops, context) } public fun getShader(bounds: Rect): Shader? { @@ -70,118 +48,4 @@ internal class Gradient(gradient: ReadableMap?, context: Context) { linearGradient.getShader(bounds.width().toFloat(), bounds.height().toFloat()) } } - - // Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section) - // Browsers add 9 intermediate color stops when a transition hint is present - // Algorithm is referred from Blink engine [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240). - private fun processColorTransitionHints(originalStopsArray: ReadableArray, context: Context): List { - val colorStops = ArrayList(originalStopsArray.size() + 9) - for (i in 0 until originalStopsArray.size()) { - val colorStop = originalStopsArray.getMap(i) ?: continue - val position = colorStop.getDouble("position").toFloat() - val color = if (colorStop.hasKey("color") && !colorStop.isNull("color")) { - if (colorStop.getType("color") == ReadableType.Map) { - ColorPropConverter.getColor(colorStop.getMap("color"), context) - } else { - colorStop.getInt("color") - } - } else null - - colorStops.add(ColorStop(color, position)) - } - - var indexOffset = 0 - for (i in 1 until colorStops.size - 1) { - val colorStop = colorStops[i] - // Skip if not a color hint - if (colorStop.color != null) { - continue - } - - val x = i + indexOffset - if (x < 1) { - continue - } - - val offsetLeft = colorStops[x - 1].position - val offsetRight = colorStops[x + 1].position - val offset = colorStop.position - val leftDist = offset - offsetLeft - val rightDist = offsetRight - offset - val totalDist = offsetRight - offsetLeft - - val leftColor = colorStops[x - 1].color ?: Color.TRANSPARENT - val rightColor = colorStops[x + 1].color ?: Color.TRANSPARENT - - if (FloatUtil.floatsEqual(leftDist, rightDist)) { - colorStops.removeAt(x) - --indexOffset - continue - } - - if (FloatUtil.floatsEqual(leftDist, .0f)) { - colorStop.color = rightColor - continue - } - - if (FloatUtil.floatsEqual(rightDist, .0f)) { - colorStop.color = leftColor - continue - } - - val newStops = ArrayList(9) - // Position the new color stops - if (leftDist > rightDist) { - for (y in 0..6) { - newStops.add(ColorStop( - position = offsetLeft + leftDist * ((7f + y) / 13f) - )) - } - newStops.add(ColorStop( - position = offset + rightDist * (1f / 3f) - )) - newStops.add(ColorStop( - position = offset + rightDist * (2f / 3f) - )) - } else { - newStops.add(ColorStop( - position = offsetLeft + leftDist * (1f / 3f) - )) - newStops.add(ColorStop( - position = offsetLeft + leftDist * (2f / 3f) - )) - for (y in 0..6) { - newStops.add(ColorStop( - position = offset + rightDist * (y / 13f) - )) - } - } - - // calculate colors for the new color hints. - // The color weighting for the new color stops will be - // pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)). - val hintRelativeOffset = leftDist / totalDist - val logRatio = ln(0.5) / ln(hintRelativeOffset) - for (newStop in newStops) { - val pointRelativeOffset = (newStop.position - offsetLeft) / totalDist - val weighting = Math.pow( - pointRelativeOffset.toDouble(), - logRatio - ).toFloat() - - if (weighting.isInfinite() || weighting.isNaN()) { - continue - } - - newStop.color = ColorUtils.blendARGB(leftColor, rightColor, weighting) - } - - // Replace the color hint with new color stops. - colorStops.removeAt(x) - colorStops.addAll(x, newStops) - indexOffset += 8 - } - - return colorStops - } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LinearGradient.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LinearGradient.kt index ababf792b341b9..818b942caf3d43 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LinearGradient.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/LinearGradient.kt @@ -7,16 +7,42 @@ package com.facebook.react.uimanager.style -import android.graphics.LinearGradient as AndroidLinearGradient +import android.content.Context +import android.graphics.Color import android.graphics.Shader +import androidx.core.graphics.ColorUtils +import com.facebook.react.bridge.ColorPropConverter +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType +import com.facebook.react.uimanager.FloatUtil +import com.facebook.react.uimanager.PixelUtil import kotlin.math.atan +import kotlin.math.ln +import kotlin.math.sqrt import kotlin.math.tan +import android.graphics.LinearGradient as AndroidLinearGradient + +private enum class UnitType { + Point, + Percent, + Undefined +} + +private data class ValueUnit( + val value: Float = 0.0f, + val unit: UnitType = UnitType.Undefined +) + +private data class ColorStop( + var color: Int? = null, + val position: ValueUnit +) internal class LinearGradient( directionMap: ReadableMap, - private val colors: IntArray, - private val positions: FloatArray + private val colorStopsArray: ReadableArray, + private val context: Context ) { private sealed class Direction { public data class Angle(val value: Double) : Direction() @@ -55,6 +81,48 @@ internal class LinearGradient( else -> throw IllegalArgumentException("Invalid direction type: $type") } + private val colorStops: ArrayList = run { + val stops = ArrayList(colorStopsArray.size()) + for (i in 0 until colorStopsArray.size()) { + val colorStop = colorStopsArray.getMap(i) ?: continue + val color: Int? = when { + !colorStop.hasKey("color") || colorStop.isNull("color") -> { + null + } + colorStop.getType("color") == ReadableType.Map -> { + ColorPropConverter.getColor(colorStop.getMap("color"), context) + } + else -> colorStop.getInt("color") + } + + val position = when { + !colorStop.hasKey("position") || colorStop.isNull("position") -> { + ValueUnit() + } + colorStop.getType("position") == ReadableType.String -> { + val positionString = colorStop.getString("position") + if (positionString != null && positionString.endsWith("%")) { + try { + ValueUnit(positionString.removeSuffix("%").toFloat(), UnitType.Percent) + } catch (e: NumberFormatException) { + ValueUnit() + } + } else { + ValueUnit() + } + } + colorStop.getType("position") == ReadableType.Number -> { + val positionDouble = colorStop.getDouble("position") + ValueUnit(positionDouble.toFloat(), UnitType.Point) + } + else -> ValueUnit() + } + + stops.add(ColorStop(color, position)) + } + stops; + } + public fun getShader(width: Float, height: Float): Shader { val angle = when (direction) { @@ -63,6 +131,20 @@ internal class LinearGradient( getAngleForKeyword(direction.value, width.toDouble(), height.toDouble()) } val (startPoint, endPoint) = endPointsFromAngle(angle, height, width) + val dx = endPoint[0] - startPoint[0]; + val dy = endPoint[1] - startPoint[1]; + val gradientLineLength = sqrt(dx * dx + dy * dy) + val processedColorStops = getFixedColorStops(colorStops, gradientLineLength) + val finalStops = processColorTransitionHints(processedColorStops); + val colors = IntArray(finalStops.size) + val positions = FloatArray(finalStops.size) + + finalStops.forEachIndexed { i, colorStop -> + colorStop.color?.let { color -> + colors[i] = color + positions[i] = colorStop.position.value + } + } return AndroidLinearGradient( startPoint[0], startPoint[1], @@ -134,4 +216,199 @@ internal class LinearGradient( return Pair(firstPoint, secondPoint) } + + private fun getFixedColorStops( + colorStops: ArrayList, + gradientLineLength: Float + ): List { + val fixedColorStops = ArrayList(colorStops.size) + var hasNullPositions = false + var maxPositionSoFar = resolveColorStopPosition(colorStops[0].position, gradientLineLength).value + + for (i in colorStops.indices) { + val colorStop = colorStops[i] + var newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength) + + if (newPosition.unit == UnitType.Undefined) { + // Step 1: + // If the first color stop does not have a position, + // set its position to 0%. If the last color stop does not have a position, + // set its position to 100%. + when (i) { + 0 -> newPosition = ValueUnit(0f, UnitType.Point) + colorStops.size - 1 -> newPosition = ValueUnit(1f, UnitType.Point) + } + } + + // Step 2: + // If a color stop or transition hint has a position + // that is less than the specified position of any color stop or transition hint + // before it in the list, set its position to be equal to the + // largest specified position of any color stop or transition hint before it. + if (newPosition.unit != UnitType.Undefined) { + newPosition = ValueUnit( + maxOf(newPosition.value, maxPositionSoFar), + UnitType.Point + ) + fixedColorStops.add(ColorStop(colorStop.color, newPosition)) + maxPositionSoFar = newPosition.value + } else { + hasNullPositions = true + fixedColorStops.add(colorStop) + } + } + + // Step 3: + // If any color stop still does not have a position, + // then, for each run of adjacent color stops without positions, + // set their positions so that they are evenly spaced between the preceding and + // following color stops with positions. + if (hasNullPositions) { + var lastDefinedIndex = 0 + for (i in 1 until fixedColorStops.size) { + if (fixedColorStops[i].position.unit != UnitType.Undefined) { + val unpositionedStops = i - lastDefinedIndex - 1 + if (unpositionedStops > 0) { + val startPosition = fixedColorStops[lastDefinedIndex].position.value + val endPosition = fixedColorStops[i].position.value + val increment = (endPosition - startPosition) / (unpositionedStops + 1) + + for (j in 1..unpositionedStops) { + fixedColorStops[lastDefinedIndex + j] = ColorStop( + fixedColorStops[lastDefinedIndex + j].color, + ValueUnit(startPosition + increment * j, UnitType.Point) + ) + } + } + lastDefinedIndex = i + } + } + } + + return fixedColorStops + } + + private fun processColorTransitionHints(originalStops: List): List { + val colorStops = originalStops.toMutableList() + var indexOffset = 0 + + for (i in 1 until originalStops.size - 1) { + // Skip if not a color hint + if (originalStops[i].color != null) { + continue + } + + val x = i + indexOffset + if (x < 1) { + continue + } + + val offsetLeft = colorStops[x - 1].position.value + val offsetRight = colorStops[x + 1].position.value + val offset = colorStops[x].position.value + val leftDist = offset - offsetLeft + val rightDist = offsetRight - offset + val totalDist = offsetRight - offsetLeft + val leftColor = colorStops[x - 1].color + val rightColor = colorStops[x + 1].color + + if (FloatUtil.floatsEqual(leftDist, rightDist)) { + colorStops.removeAt(x) + --indexOffset + continue + } + + if (FloatUtil.floatsEqual(leftDist, 0f)) { + colorStops[x].color = rightColor + continue + } + + if (FloatUtil.floatsEqual(rightDist, 0f)) { + colorStops[x].color = leftColor + continue + } + + val newStops = ArrayList(9) + + // Position the new color stops + if (leftDist > rightDist) { + for (y in 0..6) { + newStops.add( + ColorStop( + null, + ValueUnit(offsetLeft + leftDist * ((7f + y) / 13f), UnitType.Point) + ) + ) + } + newStops.add( + ColorStop( + null, + ValueUnit(offset + rightDist * (1f / 3f), UnitType.Point) + ) + ) + newStops.add( + ColorStop( + null, + ValueUnit(offset + rightDist * (2f / 3f), UnitType.Point) + ) + ) + } else { + newStops.add( + ColorStop( + null, + ValueUnit(offsetLeft + leftDist * (1f / 3f), UnitType.Point) + ) + ) + newStops.add( + ColorStop( + null, + ValueUnit(offsetLeft + leftDist * (2f / 3f), UnitType.Point) + ) + ) + for (y in 0..6) { + newStops.add( + ColorStop( + null, + ValueUnit(offset + rightDist * (y / 13f), UnitType.Point) + ) + ) + } + } + + // Calculate colors for the new stops + val hintRelativeOffset = leftDist / totalDist + val logRatio = ln(0.5) / ln(hintRelativeOffset) + + for (newStop in newStops) { + val pointRelativeOffset = (newStop.position.value - offsetLeft) / totalDist + val weighting = Math.pow(pointRelativeOffset.toDouble(), logRatio).toFloat() + + if (!weighting.isFinite() || weighting.isNaN()) { + continue + } + + // Interpolate color using the calculated weighting + leftColor?.let { left -> + rightColor?.let { right -> + newStop.color = ColorUtils.blendARGB(left, right, weighting) + } + } + } + + // Replace the color hint with new color stops + colorStops.removeAt(x) + colorStops.addAll(x, newStops) + indexOffset += 8 + } + + return colorStops + } + + private fun resolveColorStopPosition(position: ValueUnit, gradientLineLength: Float): ValueUnit { + return when (position.unit) { + UnitType.Point -> ValueUnit(PixelUtil.toPixelFromDIP(position.value) / gradientLineLength, UnitType.Point) + UnitType.Percent -> ValueUnit(position.value / 100, UnitType.Point) + UnitType.Undefined -> position + } + } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index a3b6a17f2d0a7f..a0722ff56115ba 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -1327,10 +1327,14 @@ inline void fromRawValue( auto positionIt = stopMap.find("position"); auto colorIt = stopMap.find("color"); - if (positionIt != stopMap.end() && colorIt != stopMap.end() && - positionIt->second.hasType()) { + if (positionIt != stopMap.end() && colorIt != stopMap.end()) { ColorStop colorStop; - colorStop.position = (Float)(positionIt->second); + if (positionIt->second.hasValue()) { + fromRawValue( + context, + positionIt->second, + colorStop.position); + } if (colorIt->second.hasValue()) { fromRawValue( context.contextContainer, diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h index c7d1e0b602e48b..c592e0dfd5a29a 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/LinearGradient.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -35,7 +36,7 @@ struct GradientDirection { struct ColorStop { bool operator==(const ColorStop& other) const = default; SharedColor color; - Float position = 0.0f; + ValueUnit position; }; struct LinearGradient { diff --git a/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js b/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js index 3efdc57fcff024..9c9e493ca28c3f 100644 --- a/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js +++ b/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js @@ -19,12 +19,13 @@ import {Platform, PlatformColor, StyleSheet, View} from 'react-native'; type Props = $ReadOnly<{ style: ViewStyleProp, testID?: string, + children?: React.Node, }>; function GradientBox(props: Props): React.Node { return ( - Linear Gradient + {props.children} ); } @@ -56,8 +57,9 @@ exports.examples = [ style={{ experimental_backgroundImage: 'linear-gradient(#e66465, #9198e5);', }} - testID="linear-gradient-basic" - /> + testID="linear-gradient-basic"> + Linear Gradient + ); }, }, @@ -237,4 +239,26 @@ exports.examples = [ ); }, }, + { + title: 'with px and % combination', + render(): React.Node { + return ( + + ); + }, + }, ];