Brush.kt

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui.graphics

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.util.nativeClass

@Immutable
sealed class Brush {
    abstract fun applyTo(p: Paint, alpha: Float)
}

@Immutable
class SolidColor(val value: Color) : Brush() {
    override fun applyTo(p: Paint, alpha: Float) {
        p.alpha = DefaultAlpha
        p.color = if (alpha != DefaultAlpha) {
            value.copy(alpha = value.alpha * alpha)
        } else {
            value
        }
        if (p.shader != null) p.shader = null
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (nativeClass() != other?.nativeClass()) return false

        other as SolidColor

        if (value != other.value) return false

        return true
    }

    override fun hashCode(): Int {
        return value.hashCode()
    }

    override fun toString(): String {
        return "SolidColor(value=$value)"
    }
}

typealias ColorStop = Pair<Float, Color>

/**
 * Creates a linear gradient with the provided colors along the given start and end coordinates.
 * The colors are
 *
 * ```
 *  LinearGradient(
 *      listOf(Color.Red, Color.Green, Color.Blue),
 *      startX = 0.0f,
 *      startY = 50.0f,
 *      endY = 0.0f,
 *      endY = 100.0f
 * )
 * ```
 */
@Stable
fun LinearGradient(
    colors: List<Color>,
    startX: Float,
    startY: Float,
    endX: Float,
    endY: Float,
    tileMode: TileMode = TileMode.Clamp
) = LinearGradient(
    colors,
    null,
    startX,
    startY,
    endX,
    endY,
    tileMode
)

/**
 * Creates a linear gradient with the provided colors along the given start and end coordinates.
 * The colors are dispersed at the provided offset defined in the [ColorStop]
 *
 * ```
 *  LinearGradient(
 *      0.0f to Color.Red,
 *      0.3f to Color.Green,
 *      1.0f to Color.Blue,
 *      startX = 0.0f,
 *      startY = 50.0f,
 *      endY = 0.0f,
 *      endY = 100.0f
 * )
 * ```
 */
@Stable
fun LinearGradient(
    vararg colorStops: ColorStop,
    startX: Float,
    startY: Float,
    endX: Float,
    endY: Float,
    tileMode: TileMode = TileMode.Clamp
) = LinearGradient(
    List<Color>(colorStops.size) { i -> colorStops[i].second },
    List<Float>(colorStops.size) { i -> colorStops[i].first },
    startX,
    startY,
    endX,
    endY,
    tileMode
)

/**
 * Creates a radial gradient with the given colors at the provided offset defined in the [ColorStop]
 * ```
 * RadialGradient(
 *      0.0f to Color.Red,
 *      0.3f to Color.Green,
 *      1.0f to Color.Blue,
 *      centerX = side1 / 2.0f,
 *      centerY = side2 / 2.0f,
 *      radius = side1 / 2.0f,
 *      tileMode = TileMode.Repeated
 * )
 * ```
 */
@Stable
fun RadialGradient(
    vararg colorStops: ColorStop,
    centerX: Float,
    centerY: Float,
    radius: Float,
    tileMode: TileMode = TileMode.Clamp
) = RadialGradient(
    List<Color>(colorStops.size) { i -> colorStops[i].second },
    List<Float>(colorStops.size) { i -> colorStops[i].first },
    centerX,
    centerY,
    radius,
    tileMode
)

/**
 * Creates a radial gradient with the given colors evenly dispersed within the gradient
 * ```
 * RadialGradient(
 *      listOf(Color.Red, Color.Green, Color.Blue),
 *      centerX = side1 / 2.0f,
 *      centerY = side2 / 2.0f,
 *      radius = side1 / 2.0f,
 *      tileMode = TileMode.Repeated
 * )
 * ```
 */
@Stable
fun RadialGradient(
    colors: List<Color>,
    centerX: Float,
    centerY: Float,
    radius: Float,
    tileMode: TileMode = TileMode.Clamp
) = RadialGradient(colors, null, centerX, centerY, radius, tileMode)

/**
 * Creates a vertical gradient with the given colors evenly dispersed within the gradient
 * Ex:
 * ```
 *  VerticalGradient(
 *      listOf(Color.Red, Color.Green, Color.Blue),
 *      startY = 0.0f,
 *      endY = 100.0f
 * )
 *
 * ```
 */
@Stable
fun VerticalGradient(
    colors: List<Color>,
    startY: Float,
    endY: Float,
    tileMode: TileMode = TileMode.Clamp
) = LinearGradient(
    colors,
    null,
    startX = 0.0f,
    startY = startY,
    endX = 0.0f,
    endY = endY,
    tileMode = tileMode
)

/**
 * Creates a vertical gradient with the given colors at the provided offset defined in the [ColorStop]
 * Ex:
 * ```
 *  VerticalGradient(
 *      0.1f to Color.Red,
 *      0.3f to Color.Green,
 *      0.5f to Color.Blue,
 *      startY = 0.0f,
 *      endY = 100.0f
 * )
 * ```
 */
@Stable
fun VerticalGradient(
    vararg colorStops: ColorStop,
    startY: Float,
    endY: Float,
    tileMode: TileMode = TileMode.Clamp
) = LinearGradient(
    List<Color>(colorStops.size) { i -> colorStops[i].second },
    List<Float>(colorStops.size) { i -> colorStops[i].first },
    startX = 0.0f,
    startY = startY,
    endX = 0.0f,
    endY = endY,
    tileMode = tileMode
)

/**
 * Creates a horizontal gradient with the given colors evenly dispersed within the gradient
 *
 * Ex:
 * ```
 *  HorizontalGradient(
 *      listOf(Color.Red, Color.Green, Color.Blue),
 *      startX = 10.0f,
 *      endX = 20.0f
 * )
 * ```
 */
@Stable
fun HorizontalGradient(
    colors: List<Color>,
    startX: Float,
    endX: Float,
    tileMode: TileMode = TileMode.Clamp
) = LinearGradient(
    colors,
    null,
    startX = startX,
    startY = 0.0f,
    endX = endX,
    endY = 0.0f,
    tileMode = tileMode
)

/**
 * Creates a horizontal gradient with the given colors dispersed at the provided offset defined in the [ColorStop]
 *
 * Ex:
 * ```
 *  HorizontalGradient(
 *      0.0f to Color.Red,
 *      0.3f to Color.Green,
 *      1.0f to Color.Blue,
 *      startX = 0.0f,
 *      endX = 100.0f
 * )
 * ```
 */
@Stable
fun HorizontalGradient(
    vararg colorStops: ColorStop,
    startX: Float,
    endX: Float,
    tileMode: TileMode = TileMode.Clamp
) = LinearGradient(
    List<Color>(colorStops.size) { i -> colorStops[i].second },
    List<Float>(colorStops.size) { i -> colorStops[i].first },
    startX = startX,
    startY = 0.0f,
    endX = endX,
    endY = 0.0f,
    tileMode = tileMode
)

/**
 * Creates a sweep gradient with the given colors dispersed around the center with
 * offsets defined in each [ColorStop]. The sweep begins relative to 3 o'clock and continues
 * clockwise until it reaches the starting position again.
 *
 * Ex:
 * ```
 *  SweepGradient(
 *      0.0f to Color.Red,
 *      0.3f to Color.Green,
 *      1.0f to Color.Blue,
 *      center = Offset(0.0f, 100.0f)
 * )
 * ```
 */
@Stable
fun SweepGradient(
    vararg colorStops: ColorStop,
    center: Offset
) = SweepGradient(
    center,
    List<Color>(colorStops.size) { i -> colorStops[i].second },
    List<Float>(colorStops.size) { i -> colorStops[i].first },
)

/**
 * Creates a sweep gradient with the given colors dispersed evenly around the center.
 * The sweep begins relative to 3 o'clock and continues clockwise until it reaches the starting
 * position again.
 *
 * Ex:
 * ```
 *  SweepGradient(
 *      listOf(Color.Red, Color.Green, Color.Blue),
 *      center = Offset(10.0f, 20.0f)
 * )
 * ```
 */
@Stable
fun SweepGradient(
    colors: List<Color>,
    center: Offset
) = SweepGradient(center, colors, null)

/**
 * Brush implementation used to apply a linear gradient on a given [Paint]
 */
@Immutable
class LinearGradient internal constructor(
    private val colors: List<Color>,
    private val stops: List<Float>? = null,
    private val startX: Float,
    private val startY: Float,
    private val endX: Float,
    private val endY: Float,
    private val tileMode: TileMode = TileMode.Clamp
) : ShaderBrush(
    LinearGradientShader(
        Offset(startX, startY),
        Offset(endX, endY),
        colors,
        stops,
        tileMode
    )
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (nativeClass() != other?.nativeClass()) return false

        other as LinearGradient

        if (colors != other.colors) return false
        if (stops != other.stops) return false
        if (startX != other.startX) return false
        if (startY != other.startY) return false
        if (endX != other.endX) return false
        if (endY != other.endY) return false
        if (tileMode != other.tileMode) return false

        return true
    }

    override fun hashCode(): Int {
        var result = colors.hashCode()
        result = 31 * result + (stops?.hashCode() ?: 0)
        result = 31 * result + startX.hashCode()
        result = 31 * result + startY.hashCode()
        result = 31 * result + endX.hashCode()
        result = 31 * result + endY.hashCode()
        result = 31 * result + tileMode.hashCode()
        return result
    }

    override fun toString(): String {
        return "LinearGradient(colors=$colors, " +
            "stops=$stops, " +
            "startX=$startX, " +
            "startY=$startY, " +
            "endX=$endX, " +
            "endY=$endY, " +
            "tileMode=$tileMode)"
    }
}

/**
 * Brush implementation used to apply a radial gradient on a given [Paint]
 */
@Immutable
class RadialGradient internal constructor(
    private val colors: List<Color>,
    private val stops: List<Float>? = null,
    private val centerX: Float,
    private val centerY: Float,
    private val radius: Float,
    private val tileMode: TileMode = TileMode.Clamp
) : ShaderBrush(
    RadialGradientShader(
        Offset(centerX, centerY),
        radius,
        colors,
        stops,
        tileMode
    )
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (nativeClass() != other?.nativeClass()) return false

        other as RadialGradient

        if (colors != other.colors) return false
        if (stops != other.stops) return false
        if (centerX != other.centerX) return false
        if (centerY != other.centerY) return false
        if (radius != other.radius) return false
        if (tileMode != other.tileMode) return false

        return true
    }

    override fun hashCode(): Int {
        var result = colors.hashCode()
        result = 31 * result + (stops?.hashCode() ?: 0)
        result = 31 * result + centerX.hashCode()
        result = 31 * result + centerY.hashCode()
        result = 31 * result + radius.hashCode()
        result = 31 * result + tileMode.hashCode()
        return result
    }

    override fun toString(): String {
        return "RadialGradient(" +
            "colors=$colors, " +
            "stops=$stops, " +
            "centerX=$centerX, " +
            "centerY=$centerY, " +
            "radius=$radius, " +
            "tileMode=$tileMode)"
    }
}

/**
 * Brush implementation used to apply a sweep gradient on a given [Paint]
 */
@Immutable
class SweepGradient internal constructor(
    private val center: Offset,
    private val colors: List<Color>,
    private val stops: List<Float>? = null,
) : ShaderBrush(
    SweepGradientShader(
        center,
        colors,
        stops
    )
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (nativeClass() != other?.nativeClass()) return false

        other as SweepGradient

        if (center != other.center) return false
        if (colors != other.colors) return false
        if (stops != other.stops) 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 {
        return "SweepGradient(center=$center, colors=$colors, stops=$stops)"
    }
}

/**
 * Brush implementation that wraps and applies a the provided shader to a [Paint]
 */
@Immutable
open class ShaderBrush(val shader: Shader) : Brush() {
    final override fun applyTo(p: Paint, alpha: Float) {
        if (p.color != Color.Black) p.color = Color.Black
        if (p.shader != shader) p.shader = shader
        if (p.alpha != alpha) p.alpha = alpha
    }
}