Last active
October 27, 2025 13:02
-
-
Save skaldebane/8e042b76023fbe20a7d70b59a9938f90 to your computer and use it in GitHub Desktop.
Revisions
-
skaldebane revised this gist
Oct 12, 2023 . 1 changed file with 191 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,191 @@ import android.graphics.SweepGradient import android.os.Build import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.* import androidx.compose.ui.util.fastForEachIndexed import androidx.core.graphics.transform @Stable fun Brush.Companion.angledSweepGradient( vararg colorStops: Pair<Float, Color>, center: Offset = Offset.Unspecified, startAngle: Float = 0f ): Brush = AngledSweepGradient( colors = List(colorStops.size) { i -> colorStops[i].second }, stops = List(colorStops.size) { i -> colorStops[i].first }, center = center, startAngle = startAngle ) @Stable fun Brush.Companion.angledSweepGradient( colors: List<Color>, center: Offset = Offset.Unspecified, startAngle: Float = 0f ): Brush = AngledSweepGradient( colors = colors, stops = null, center = center, startAngle = startAngle ) @Immutable class AngledSweepGradient internal constructor( private val center: Offset, private val colors: List<Color>, private val stops: List<Float>? = null, private val startAngle: Float, ) : ShaderBrush() { override fun createShader(size: Size): Shader = AngledSweepGradientShader( if (center.isUnspecified) { size.center } else { Offset( if (center.x == Float.POSITIVE_INFINITY) size.width else center.x, if (center.y == Float.POSITIVE_INFINITY) size.height else center.y ) }, colors, stops, startAngle ) override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AngledSweepGradient) return false if (center != other.center) return false if (colors != other.colors) return false if (stops != other.stops) return false if (startAngle != other.startAngle) return false return true } override fun hashCode(): Int { var result = center.hashCode() result = 31 * result + colors.hashCode() result = 31 * result + (stops?.hashCode() ?: 0) result = 31 * result + startAngle.hashCode() return result } override fun toString(): String { val centerValue = if (center.isSpecified) "center=$center, " else "" return "AngledSweepGradient(" + centerValue + "colors=$colors, stops=$stops, startAngle=$startAngle)" } } internal fun AngledSweepGradientShader( center: Offset, colors: List<Color>, colorStops: List<Float>?, startAngle: Float, ): Shader { validateColorStops(colors, colorStops) val numTransparentColors = countTransparentColors(colors) val shader = SweepGradient( center.x, center.y, makeTransparentColors(colors, numTransparentColors), makeTransparentStops(colorStops, colors, numTransparentColors) ) shader.transform { setRotate(startAngle, center.x, center.y) } return shader } /*private fun List<Color>.toIntArray(): IntArray = IntArray(size) { i -> this[i].toArgb() }*/ private fun validateColorStops(colors: List<Color>, colorStops: List<Float>?) { if (colorStops == null) { if (colors.size < 2) { throw IllegalArgumentException( "colors must have length of at least 2 if colorStops " + "is omitted." ) } } else if (colors.size != colorStops.size) { throw IllegalArgumentException( "colors and colorStops arguments must have" + " equal length." ) } } @VisibleForTesting internal fun countTransparentColors(colors: List<Color>): Int { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return 0 } var numTransparentColors = 0 // Don't count the first and last value because we don't add stops for those for (i in 1 until colors.lastIndex) { if (colors[i].alpha == 0f) { numTransparentColors++ } } return numTransparentColors } @VisibleForTesting internal fun makeTransparentColors( colors: List<Color>, numTransparentColors: Int ): IntArray { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // No change for Android O+, map the colors directly to their argb equivalent return IntArray(colors.size) { i -> colors[i].toArgb() } } val values = IntArray(colors.size + numTransparentColors) var valuesIndex = 0 val lastIndex = colors.lastIndex colors.fastForEachIndexed { index, color -> if (color.alpha == 0f) { if (index == 0) { values[valuesIndex++] = colors[1].copy(alpha = 0f).toArgb() } else if (index == lastIndex) { values[valuesIndex++] = colors[index - 1].copy(alpha = 0f).toArgb() } else { val previousColor = colors[index - 1] values[valuesIndex++] = previousColor.copy(alpha = 0f).toArgb() val nextColor = colors[index + 1] values[valuesIndex++] = nextColor.copy(alpha = 0f).toArgb() } } else { values[valuesIndex++] = color.toArgb() } } return values } internal fun makeTransparentStops( stops: List<Float>?, colors: List<Color>, numTransparentColors: Int ): FloatArray? { if (numTransparentColors == 0) { return stops?.toFloatArray() } val newStops = FloatArray(colors.size + numTransparentColors) newStops[0] = stops?.get(0) ?: 0f var newStopsIndex = 1 for (i in 1 until colors.lastIndex) { val color = colors[i] val stop = stops?.get(i) ?: i.toFloat() / colors.lastIndex newStops[newStopsIndex++] = stop if (color.alpha == 0f) { newStops[newStopsIndex++] = stop } } newStops[newStopsIndex] = stops?.get(colors.lastIndex) ?: 1f return newStops } -
skaldebane revised this gist
Oct 12, 2023 . No changes.There are no files selected for viewing
-
skaldebane renamed this gist
Oct 12, 2023 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
skaldebane revised this gist
Oct 12, 2023 . No changes.There are no files selected for viewing
-
skaldebane revised this gist
Oct 9, 2023 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -68,6 +68,7 @@ class AngledSweepGradient internal constructor( var result = center.hashCode() result = 31 * result + colors.hashCode() result = 31 * result + (stops?.hashCode() ?: 0) result = 31 * result + startAngle.hashCode() return result } -
skaldebane created this gist
Oct 9, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,117 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.* import org.jetbrains.skia.FilterTileMode import org.jetbrains.skia.GradientStyle @Stable fun Brush.Companion.angledSweepGradient( vararg colorStops: Pair<Float, Color>, center: Offset = Offset.Unspecified, startAngle: Float = 0f ): Brush = AngledSweepGradient( colors = List(colorStops.size) { i -> colorStops[i].second }, stops = List(colorStops.size) { i -> colorStops[i].first }, center = center, startAngle = startAngle ) @Stable fun Brush.Companion.angledSweepGradient( colors: List<Color>, center: Offset = Offset.Unspecified, startAngle: Float = 0f ): Brush = AngledSweepGradient( colors = colors, stops = null, center = center, startAngle = startAngle ) @Immutable class AngledSweepGradient internal constructor( private val center: Offset, private val colors: List<Color>, private val stops: List<Float>? = null, private val startAngle: Float, ) : ShaderBrush() { override fun createShader(size: Size): Shader = AngledSweepGradientShader( if (center.isUnspecified) { size.center } else { Offset( if (center.x == Float.POSITIVE_INFINITY) size.width else center.x, if (center.y == Float.POSITIVE_INFINITY) size.height else center.y ) }, colors, stops, startAngle ) override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AngledSweepGradient) return false if (center != other.center) return false if (colors != other.colors) return false if (stops != other.stops) return false if (startAngle != other.startAngle) return false return true } override fun hashCode(): Int { var result = center.hashCode() result = 31 * result + colors.hashCode() result = 31 * result + (stops?.hashCode() ?: 0) return result } override fun toString(): String { val centerValue = if (center.isSpecified) "center=$center, " else "" return "AngledSweepGradient(" + centerValue + "colors=$colors, stops=$stops, startAngle=$startAngle)" } } internal fun AngledSweepGradientShader( center: Offset, colors: List<Color>, colorStops: List<Float>?, startAngle: Float, ): Shader { validateColorStops(colors, colorStops) return Shader.makeSweepGradient( x = center.x, y = center.y, startAngle = 0f + startAngle, endAngle = 360f + startAngle, colors = colors.toIntArray(), positions = colorStops?.toFloatArray(), style = GradientStyle.DEFAULT.withTileMode(FilterTileMode.REPEAT) ) } private fun List<Color>.toIntArray(): IntArray = IntArray(size) { i -> this[i].toArgb() } private fun validateColorStops(colors: List<Color>, colorStops: List<Float>?) { if (colorStops == null) { if (colors.size < 2) { throw IllegalArgumentException( "colors must have length of at least 2 if colorStops " + "is omitted." ) } } else if (colors.size != colorStops.size) { throw IllegalArgumentException( "colors and colorStops arguments must have" + " equal length." ) } }