From 01bcb3bf13d566443c0ecd2af36551e7a1527eea Mon Sep 17 00:00:00 2001 From: Nishan Date: Wed, 25 Dec 2024 20:59:37 +1300 Subject: [PATCH 1/3] Color transition hint --- .../Libraries/StyleSheet/StyleSheetTypes.d.ts | 2 +- .../__tests__/processBackgroundImage-test.js | 30 ++++ .../StyleSheet/processBackgroundImage.js | 149 ++++++++++++------ .../React/Fabric/Utils/RCTLinearGradient.mm | 131 ++++++++++++++- .../react/uimanager/style/Gradient.kt | 147 +++++++++++++++-- .../renderer/components/view/conversions.h | 8 +- .../LinearGradient/LinearGradientExample.js | 13 ++ 7 files changed, 418 insertions(+), 62 deletions(-) diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index 05a8b6410f454e..2857fae6e6ab06 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -372,7 +372,7 @@ export type GradientValue = { // Angle or direction enums direction?: string | undefined; colorStops: ReadonlyArray<{ - color: ColorValue; + color: ColorValue | null; positions?: ReadonlyArray | undefined; }>; }; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js index 624f79df47885d..0e65ace2a6b22f 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js @@ -697,4 +697,34 @@ describe('processBackgroundImage', () => { }); } }); + + it('should process color transition hint in object style', () => { + const input = [ + { + type: 'linearGradient', + direction: 'To Bottom', + colorStops: [{color: 'red'}, {positions: ['20%']}, {color: 'blue'}], + }, + ]; + const result = processBackgroundImage(input); + 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}, + ]); + }); + + it('should process color transition hint', () => { + const input = 'linear-gradient(red, 40%, blue)'; + const result = processBackgroundImage(input); + 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}, + ]); + }); }); diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index 5457579ea3714d..1e98cf5b5559f3 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -22,11 +22,13 @@ type LinearGradientDirection = | {type: 'angle', value: number} | {type: 'keyword', value: string}; +type ColorStopColor = ProcessedColorValue | null; + type ParsedGradientValue = { type: 'linearGradient', direction: LinearGradientDirection, colorStops: $ReadOnlyArray<{ - color: ProcessedColorValue, + color: ColorStopColor, position: number, }>, }; @@ -49,33 +51,52 @@ export default function processBackgroundImage( } else if (Array.isArray(backgroundImage)) { for (const bgImage of backgroundImage) { const processedColorStops: Array<{ - color: ProcessedColorValue, + color: ColorStopColor, position: number | null, }> = []; for (let index = 0; index < bgImage.colorStops.length; index++) { const colorStop = bgImage.colorStops[index]; - const processedColor = processColor(colorStop.color); - if (processedColor == null) { - // If a color is invalid, return an empty array and do not apply gradient. Same as web. - return []; - } - if (colorStop.positions != null && colorStop.positions.length > 0) { - for (const position of colorStop.positions) { - if (position.endsWith('%')) { - processedColorStops.push({ - color: processedColor, - position: parseFloat(position) / 100, - }); - } else { - // If a position is invalid, return an empty array and do not apply gradient. Same as web. - return []; - } + const positions = colorStop.positions; + // Color transition hint syntax (red, 20%, blue) + if ( + colorStop.color == null && + Array.isArray(positions) && + positions.length === 1 + ) { + const position = positions[0]; + if (typeof position === 'string' && position.endsWith('%')) { + processedColorStops.push({ + color: null, + position: parseFloat(position) / 100, + }); + } else { + // If a position is invalid, return an empty array and do not apply gradient. Same as web. + return []; } } else { - processedColorStops.push({ - color: processedColor, - position: null, - }); + const processedColor = processColor(colorStop.color); + if (processedColor == null) { + // If a color is invalid, return an empty array and do not apply gradient. Same as web. + return []; + } + if (positions != null && positions.length > 0) { + for (const position of positions) { + if (position.endsWith('%')) { + processedColorStops.push({ + color: processedColor, + position: parseFloat(position) / 100, + }); + } else { + // If a position is invalid, return an empty array and do not apply gradient. Same as web. + return []; + } + } + } else { + processedColorStops.push({ + color: processedColor, + position: null, + }); + } } } @@ -169,47 +190,85 @@ function parseCSSLinearGradient( // If first part is not an angle/direction or a color stop, return an empty array and do not apply any gradient. Same as web. return []; } - colorStopRegex.lastIndex = 0; + const colorStopsString = parts.join(','); const colorStops = []; - const fullColorStopsStr = parts.join(','); - let colorStopMatch; - while ((colorStopMatch = colorStopRegex.exec(fullColorStopsStr))) { - const [, color, position1, position2] = colorStopMatch; - const processedColor = processColor(color.trim().toLowerCase()); - if (processedColor == null) { - // If a color is invalid, return an empty array and do not apply any gradient. Same as web. + // 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(/,(?![^(]*\))/); + for (const stop of stops) { + const trimmedStop = stop.trim().toLowerCase(); + // Match function like pattern or single words + const parts = trimmedStop.match(/\S+\([^)]*\)|\S+/g); + if (parts == null) { + // If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. return []; } - - if (typeof position1 !== 'undefined') { - if (position1.endsWith('%')) { + // Case 1: [color, position, position] + if (parts.length === 3) { + const color = parts[0]; + const position1 = parts[1]; + const position2 = parts[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 a position is invalid, return an empty array and do not apply any gradient. Same as web. return []; } - } else { - colorStops.push({ - color: processedColor, - position: null, - }); } - - if (typeof position2 !== 'undefined') { - if (position2.endsWith('%')) { + // Case 2: [color, position] + else if (parts.length === 2) { + const color = parts[0]; + const position = parts[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(position2) / 100, + position: parseFloat(position) / 100, }); } else { // If a position is invalid, return an empty array and do not apply any gradient. Same as web. return []; } } + // Case 3: [color] + // Case 4: [position] => transition hint syntax + else if (parts.length === 1) { + if (parts[0].endsWith('%')) { + colorStops.push({ + color: null, + position: parseFloat(parts[0]) / 100, + }); + } else { + const processedColor = processColor(parts[0]); + if (processedColor == null) { + // If a color is invalid, return an empty array and do not apply any gradient. Same as web. + return []; + } + colorStops.push({ + color: processedColor, + position: null, + }); + } + } else { + // If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. + return []; + } } const fixedColorStops = getFixedColorStops(colorStops); @@ -286,15 +345,15 @@ function getAngleInDegrees(angle?: string): ?number { // https://drafts.csswg.org/css-images-4/#color-stop-fixup function getFixedColorStops( colorStops: $ReadOnlyArray<{ - color: ProcessedColorValue, + color: ColorStopColor, position: number | null, }>, ): Array<{ - color: ProcessedColorValue, + color: ColorStopColor, position: number, }> { let fixedColorStops: Array<{ - color: ProcessedColorValue, + color: ColorStopColor, position: number, }> = []; let hasNullPositions = false; diff --git a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm index 47181ea3e0ada2..eb1f75d837d511 100644 --- a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm @@ -8,6 +8,8 @@ #import "RCTLinearGradient.h" #import +#import +#import using namespace facebook::react; @@ -17,7 +19,7 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient & { UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size]; const auto &direction = gradient.direction; - const auto &colorStops = gradient.colorStops; + const auto colorStops = processColorTransitionHints(gradient.colorStops); UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { CGContextRef context = rendererContext.CGContext; @@ -131,4 +133,131 @@ static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) } } +// 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). +static std::vector processColorTransitionHints(const std::vector& originalStops) +{ + std::vector colorStops = originalStops; + int indexOffset = 0; + + for (size_t i = 1; i < colorStops.size() - 1; i++) { + auto &colorStop = colorStops[i]; + // Skip if not a color hint + if (colorStop.color) { + continue; + } + + size_t x = i + indexOffset; + if (x < 1) { + continue; + } + + Float offsetLeft = colorStops[x - 1].position; + Float offsetRight = colorStops[x + 1].position; + Float offset = colorStop.position; + Float leftDist = offset - offsetLeft; + Float rightDist = offsetRight - offset; + Float totalDist = offsetRight - offsetLeft; + SharedColor leftSharedColor = colorStops[x - 1].color; + SharedColor rightSharedColor = colorStops[x + 1].color; + + if (facebook::react::floatEquality(leftDist, rightDist)) { + colorStops.erase(colorStops.begin() + x); + --indexOffset; + continue; + } + + if (facebook::react::floatEquality(leftDist, .0f)) { + colorStop.color = rightSharedColor; + continue; + } + + if (facebook::react::floatEquality(rightDist, .0f)) { + colorStop.color = leftSharedColor; + continue; + } + + std::vector newStops; + newStops.reserve(9); + + // Position the new color stops + if (leftDist > rightDist) { + for (int y = 0; y < 7; ++y) { + ColorStop newStop{ + SharedColor(), + offsetLeft + leftDist * ((7.0f + y) / 13.0f) + }; + newStops.push_back(newStop); + } + ColorStop stop1{ + SharedColor(), + offset + rightDist * (1.0f / 3.0f) + }; + ColorStop stop2 { + SharedColor(), + offset + rightDist * (2.0f / 3.0f) + }; + newStops.push_back(stop1); + newStops.push_back(stop2); + } else { + ColorStop stop1 { + SharedColor(), + offsetLeft + leftDist * (1.0f / 3.0f) + }; + ColorStop stop2 { + SharedColor(), + offsetLeft + leftDist * (2.0f / 3.0f) + }; + newStops.push_back(stop1); + newStops.push_back(stop2); + for (int y = 0; y < 7; ++y) { + ColorStop newStop { + SharedColor(), + offset + rightDist * (y / 13.0f) + }; + newStops.push_back(newStop); + } + } + + // calculate colors for the new color hints. + // The color weighting for the new color stops will be + // pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)). + Float hintRelativeOffset = leftDist / totalDist; + for (auto &newStop : newStops) { + Float pointRelativeOffset = (newStop.position - offsetLeft) / totalDist; + Float weighting = pow( + pointRelativeOffset, + log(0.5) / log(hintRelativeOffset) + ); + + if (!std::isfinite(weighting) || std::isnan(weighting)) { + continue; + } + + NSArray *inputRange = @[@0.0, @1.0]; + auto leftColor = RCTUIColorFromSharedColor(leftSharedColor); + auto rightColor = RCTUIColorFromSharedColor(rightSharedColor); + NSArray *outputRange = @[leftColor, rightColor]; + + auto interpolatedColor = RCTInterpolateColorInRange(weighting, inputRange, outputRange); + + auto alpha = (interpolatedColor >> 24) & 0xFF; + auto red = (interpolatedColor >> 16) & 0xFF; + auto green = (interpolatedColor >> 8) & 0xFF; + auto blue = interpolatedColor & 0xFF; + + newStop.color = facebook::react::colorFromRGBA(red, green, blue, alpha); + + } + + // Replace the color hint with new color stops + colorStops.erase(colorStops.begin() + x); + colorStops.insert(colorStops.begin() + x, newStops.begin(), newStops.end()); + indexOffset += 8; + } + + return colorStops; +} + @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 1591d43c576887..ad0af1a40a3c16 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,11 +8,21 @@ 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 { @@ -36,23 +46,19 @@ internal class Gradient(gradient: ReadableMap?, context: Context) { gradient.getMap("direction") ?: throw IllegalArgumentException("Gradient must have direction") - val colorStops = + val colorStopsRaw = gradient.getArray("colorStops") ?: throw IllegalArgumentException("Invalid colorStops array") - val size = colorStops.size() - val colors = IntArray(size) - val positions = FloatArray(size) - - for (i in 0 until size) { - val colorStop = colorStops.getMap(i) ?: continue - colors[i] = - if (colorStop.getType("color") == ReadableType.Map) { - ColorPropConverter.getColor(colorStop.getMap("color"), context) - } else { - colorStop.getInt("color") - } - positions[i] = colorStop.getDouble("position").toFloat() + 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) @@ -64,4 +70,117 @@ 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()) + 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 + for (newStop in newStops) { + val pointRelativeOffset = (newStop.position - offsetLeft) / totalDist + val weighting = Math.pow( + pointRelativeOffset.toDouble(), + ln(0.5) / ln(hintRelativeOffset.toDouble()) + ).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/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index ef4202ce9580b8..a3b6a17f2d0a7f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -1331,7 +1331,13 @@ inline void fromRawValue( positionIt->second.hasType()) { ColorStop colorStop; colorStop.position = (Float)(positionIt->second); - fromRawValue(context, colorIt->second, colorStop.color); + if (colorIt->second.hasValue()) { + fromRawValue( + context.contextContainer, + context.surfaceId, + colorIt->second, + colorStop.color); + } linearGradient.colorStops.push_back(colorStop); } } diff --git a/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js b/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js index 620da9d4079320..3efdc57fcff024 100644 --- a/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js +++ b/packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js @@ -224,4 +224,17 @@ exports.examples = [ ); }, }, + { + title: 'Transition hint', + render(): React.Node { + return ( + + ); + }, + }, ]; From 3eee95b485b9edc9168256c7182baeec828c8f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Fri, 27 Dec 2024 10:15:08 +1300 Subject: [PATCH 2/3] fix snapshot test --- .../Libraries/__tests__/__snapshots__/public-api-test.js.snap | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index d974a6b27bd338..c9fbdae9d8f296 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -8375,11 +8375,12 @@ exports[`public API should not change unintentionally Libraries/StyleSheet/proce "type LinearGradientDirection = | { type: \\"angle\\", value: number } | { type: \\"keyword\\", value: string }; +type ColorStopColor = ProcessedColorValue | null; type ParsedGradientValue = { type: \\"linearGradient\\", direction: LinearGradientDirection, colorStops: $ReadOnlyArray<{ - color: ProcessedColorValue, + color: ColorStopColor, position: number, }>, }; From e67bf9b4a7cb20bd30493fcc3a1c0399a7f98876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Fri, 27 Dec 2024 10:25:56 +1300 Subject: [PATCH 3/3] rename parts to colorstopparts --- .../StyleSheet/processBackgroundImage.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index 1e98cf5b5559f3..3eac667446563a 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -198,16 +198,16 @@ function parseCSSLinearGradient( for (const stop of stops) { const trimmedStop = stop.trim().toLowerCase(); // Match function like pattern or single words - const parts = trimmedStop.match(/\S+\([^)]*\)|\S+/g); - if (parts == null) { + const colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g); + if (colorStopParts == null) { // If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. return []; } // Case 1: [color, position, position] - if (parts.length === 3) { - const color = parts[0]; - const position1 = parts[1]; - const position2 = parts[2]; + if (colorStopParts.length === 3) { + const color = colorStopParts[0]; + const position1 = colorStopParts[1]; + const position2 = 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. @@ -228,9 +228,9 @@ function parseCSSLinearGradient( } } // Case 2: [color, position] - else if (parts.length === 2) { - const color = parts[0]; - const position = parts[1]; + else if (colorStopParts.length === 2) { + const color = colorStopParts[0]; + const position = 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. @@ -248,14 +248,14 @@ function parseCSSLinearGradient( } // Case 3: [color] // Case 4: [position] => transition hint syntax - else if (parts.length === 1) { - if (parts[0].endsWith('%')) { + else if (colorStopParts.length === 1) { + if (colorStopParts[0].endsWith('%')) { colorStops.push({ color: null, - position: parseFloat(parts[0]) / 100, + position: parseFloat(colorStopParts[0]) / 100, }); } else { - const processedColor = processColor(parts[0]); + const processedColor = processColor(colorStopParts[0]); if (processedColor == null) { // If a color is invalid, return an empty array and do not apply any gradient. Same as web. return [];