/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.react.uimanager.style import android.content.Context import android.graphics.LinearGradient as AndroidLinearGradient import android.graphics.Shader import com.facebook.react.bridge.ColorPropConverter import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableType import com.facebook.react.uimanager.LengthPercentage import kotlin.math.atan import kotlin.math.sqrt import kotlin.math.tan internal class LinearGradient(val direction: Direction, val colorStops: List) : Gradient { companion object { fun parse(gradientMap: ReadableMap, context: Context): Gradient? { val direction = gradientMap .takeIf { it.hasKey("direction") } ?.let { map -> val directionMap = map.getMap("direction") ?: return null when (directionMap.getString("type")) { "angle" -> { val angle = directionMap.getDouble("value") Direction.Angle(angle) } "keyword" -> Direction.KeywordType.fromString(directionMap.getString("value"))?.let { keywordType -> Direction.Keyword(keywordType) } else -> null } } val colorStops = gradientMap .takeIf { it.hasKey("colorStops") } ?.let { map -> val colorStopsArray = map.getArray("colorStops") ?: return null 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 colorStopPosition = LengthPercentage.setFromDynamic(colorStop.getDynamic("position")) stops.add(ColorStop(color, colorStopPosition)) } stops } if (direction != null && colorStops != null) { return LinearGradient(direction, colorStops) } return null } } sealed class Direction { class Angle(val angle: Double) : Direction() class Keyword(val keyword: KeywordType) : Direction() enum class KeywordType(val value: String) { TO_TOP_RIGHT("to top right"), TO_BOTTOM_RIGHT("to bottom right"), TO_TOP_LEFT("to top left"), TO_BOTTOM_LEFT("to bottom left"); companion object { fun fromString(value: String?) = enumValues().find { it.value == value } } } } override fun getShader(width: Float, height: Float): Shader { val angle = when (direction) { is Direction.Angle -> direction.angle is Direction.Keyword -> getAngleForKeyword(direction.keyword, 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 finalStops = ColorStopUtils.getFixedColorStops(colorStops, gradientLineLength) val colors = IntArray(finalStops.size) val positions = FloatArray(finalStops.size) finalStops.forEachIndexed { i, colorStop -> val color = colorStop.color if (color != null && colorStop.position != null) { colors[i] = color positions[i] = colorStop.position } } return AndroidLinearGradient( startPoint[0], startPoint[1], endPoint[0], endPoint[1], colors, positions, Shader.TileMode.CLAMP, ) } // Spec: https://www.w3.org/TR/css-images-3/#linear-gradient-syntax // Refer `using keywords` section private fun getAngleForKeyword( keyword: Direction.KeywordType, width: Double, height: Double, ): Double { return when (keyword) { Direction.KeywordType.TO_TOP_RIGHT -> { val angleDeg = Math.toDegrees(atan(width / height)) 90 - angleDeg } Direction.KeywordType.TO_BOTTOM_RIGHT -> Math.toDegrees(atan(width / height)) + 90 Direction.KeywordType.TO_TOP_LEFT -> Math.toDegrees(atan(width / height)) + 270 Direction.KeywordType.TO_BOTTOM_LEFT -> Math.toDegrees(atan(height / width)) + 180 } } // Spec: https://www.w3.org/TR/css-images-3/#linear-gradient-syntax // Reference: // https://github.com/chromium/chromium/blob/d32abbe13f5d52be7127fe25d5b778498165fab8/third_party/blink/renderer/core/css/css_gradient_value.cc#L1057 private fun endPointsFromAngle( angle: Double, height: Float, width: Float, ): Pair { var adjustedAngle = angle % 360 if (adjustedAngle < 0) { adjustedAngle += 360 } when (adjustedAngle) { 0.0 -> return Pair(floatArrayOf(0f, height), floatArrayOf(0f, 0f)) 90.0 -> return Pair(floatArrayOf(0f, 0f), floatArrayOf(width, 0f)) 180.0 -> return Pair(floatArrayOf(0f, 0f), floatArrayOf(0f, height)) 270.0 -> return Pair(floatArrayOf(width, 0f), floatArrayOf(0f, 0f)) } val slope = tan(Math.toRadians((90 - adjustedAngle))).toFloat() val perpendicularSlope = -1 / slope val halfHeight = height / 2 val halfWidth = width / 2 val endCorner = when { adjustedAngle < 90 -> floatArrayOf(halfWidth, halfHeight) adjustedAngle < 180 -> floatArrayOf(halfWidth, -halfHeight) adjustedAngle < 270 -> floatArrayOf(-halfWidth, -halfHeight) else -> floatArrayOf(-halfWidth, halfHeight) } val c = endCorner[1] - perpendicularSlope * endCorner[0] val endX = c / (slope - perpendicularSlope) val endY = perpendicularSlope * endX + c val secondPoint = floatArrayOf(halfWidth + endX, halfHeight - endY) val firstPoint = floatArrayOf(halfWidth - endX, halfHeight + endY) return Pair(firstPoint, secondPoint) } }