diff --git a/haze/api/api.txt b/haze/api/api.txt index b81652ab..5b06af38 100644 --- a/haze/api/api.txt +++ b/haze/api/api.txt @@ -251,14 +251,18 @@ package dev.chrisbanes.haze { } @androidx.compose.runtime.Stable public final class HazeTint { + ctor public HazeTint(androidx.compose.ui.graphics.Brush brush, optional int blendMode); ctor public HazeTint(long color, optional int blendMode); method public long component1-0d7_KjU(); method public int component2-0nO6VwU(); - method public dev.chrisbanes.haze.HazeTint copy-xETnrds(long color, int blendMode); + method public androidx.compose.ui.graphics.Brush? component3(); + method public dev.chrisbanes.haze.HazeTint copy-39kzsgs(long color, int blendMode, androidx.compose.ui.graphics.Brush? brush); method public int getBlendMode(); + method public androidx.compose.ui.graphics.Brush? getBrush(); method public long getColor(); method public boolean isSpecified(); property public final int blendMode; + property public final androidx.compose.ui.graphics.Brush? brush; property public final long color; property public final boolean isSpecified; field public static final dev.chrisbanes.haze.HazeTint.Companion Companion; diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint.png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint.png new file mode 100644 index 00000000..5f1e34a1 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint.png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint[28].png new file mode 100644 index 00000000..605e2326 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint[28].png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint[32].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint[32].png new file mode 100644 index 00000000..e7ecc779 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint[32].png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask.png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask.png new file mode 100644 index 00000000..fa6ebf09 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask.png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask[28].png new file mode 100644 index 00000000..0a6054fd Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask[28].png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask[32].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask[32].png new file mode 100644 index 00000000..0cab92dc Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_mask[32].png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive.png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive.png new file mode 100644 index 00000000..e8f8ab1e Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive.png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive[28].png new file mode 100644 index 00000000..943d579a Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive[28].png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive[32].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive[32].png new file mode 100644 index 00000000..445aa9e1 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_brushTint_progressive[32].png differ diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint.png new file mode 100644 index 00000000..150468ef Binary files /dev/null and b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint.png differ diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint_mask.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint_mask.png new file mode 100644 index 00000000..d7674587 Binary files /dev/null and b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint_mask.png differ diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint_progressive.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint_progressive.png new file mode 100644 index 00000000..99e7aa2e Binary files /dev/null and b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_brushTint_progressive.png differ diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Paint.android.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Paint.android.kt new file mode 100644 index 00000000..b61a0ac9 --- /dev/null +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Paint.android.kt @@ -0,0 +1,10 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.ui.graphics.Paint + +internal actual fun Paint.reset() { + asFrameworkPaint().reset() +} diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt index 0fba39fe..805a2b92 100644 --- a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt @@ -16,16 +16,15 @@ import android.graphics.Shader import android.graphics.Shader.TileMode.REPEAT import android.os.Build import androidx.annotation.RequiresApi -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RenderEffect import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.asComposeRenderEffect import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.node.CompositionLocalConsumerModifierNode @@ -72,7 +71,7 @@ internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(para return blur .withNoise(currentValueOf(LocalContext), params.noiseFactor) - .withTints(params.tints, params.tintAlphaModulate, progressiveShader) + .withTints(params.tints, params.contentSize, params.tintAlphaModulate, progressiveShader) .withMask(params.mask, params.contentSize) .asComposeRenderEffect() } @@ -120,11 +119,10 @@ private fun AndroidRenderEffect.withNoise( private fun AndroidRenderEffect.withMask( mask: Brush?, size: Size, - offset: Offset = Offset.Zero, blendMode: BlendMode = BlendMode.DST_IN, ): AndroidRenderEffect { val shader = mask?.toShader(size) ?: return this - return blendWith(AndroidRenderEffect.createShaderEffect(shader), blendMode, offset) + return blendWith(AndroidRenderEffect.createShaderEffect(shader), blendMode) } private fun Brush.toShader(size: Size): Shader? = when (this) { @@ -135,55 +133,83 @@ private fun Brush.toShader(size: Size): Shader? = when (this) { @RequiresApi(31) private fun AndroidRenderEffect.withTints( tints: List, + size: Size, alphaModulate: Float = 1f, - mask: Shader? = null, - maskOffset: Offset = Offset.Zero, + mask: Shader?, ): AndroidRenderEffect = tints.fold(this) { acc, tint -> - acc.withTint(tint, alphaModulate, mask, maskOffset) + acc.withTint(tint, size, alphaModulate, mask) } @RequiresApi(31) private fun AndroidRenderEffect.withTint( tint: HazeTint, + size: Size, alphaModulate: Float = 1f, mask: Shader?, - maskOffset: Offset, ): AndroidRenderEffect { - if (!tint.color.isSpecified) return this - val color = when { + if (!tint.isSpecified) return this + + val tintBrush = tint.brush?.toShader(size) + if (tintBrush != null) { + val brushEffect = when { + alphaModulate >= 1f -> { + AndroidRenderEffect.createShaderEffect(tintBrush) + } + else -> { + // If we need to modulate the alpha, we'll need to wrap it in a ColorFilter + AndroidRenderEffect.createColorFilterEffect( + BlendModeColorFilter(Color.Blue.copy(alpha = alphaModulate).toArgb(), BlendMode.SRC_IN), + AndroidRenderEffect.createShaderEffect(tintBrush), + ) + } + } + + return if (mask != null) { + blendWith( + AndroidRenderEffect.createBlendModeEffect( + AndroidRenderEffect.createShaderEffect(mask), + brushEffect, + BlendMode.SRC_IN, + ), + blendMode = tint.blendMode.toAndroidBlendMode(), + ) + } else { + blendWith( + foreground = brushEffect, + blendMode = tint.blendMode.toAndroidBlendMode(), + ) + } + } + + val tintColor = when { alphaModulate < 1f -> tint.color.copy(alpha = tint.color.alpha * alphaModulate) else -> tint.color } - if (color.alpha < 0.005f) return this - - return if (mask != null) { - blendWith( - foreground = AndroidRenderEffect.createColorFilterEffect( - BlendModeColorFilter(color.toArgb(), BlendMode.SRC_IN), - AndroidRenderEffect.createShaderEffect(mask), - ), - blendMode = tint.blendMode.toAndroidBlendMode(), - offset = maskOffset, - ) - } else { - AndroidRenderEffect.createColorFilterEffect( - BlendModeColorFilter(color.toArgb(), tint.blendMode.toAndroidBlendMode()), - this, - ) + if (tintColor.alpha >= 0.005f) { + return if (mask != null) { + return blendWith( + foreground = AndroidRenderEffect.createColorFilterEffect( + BlendModeColorFilter(tintColor.toArgb(), BlendMode.SRC_IN), + AndroidRenderEffect.createShaderEffect(mask), + ), + blendMode = tint.blendMode.toAndroidBlendMode(), + ) + } else { + AndroidRenderEffect.createColorFilterEffect( + BlendModeColorFilter(tintColor.toArgb(), tint.blendMode.toAndroidBlendMode()), + this, + ) + } } + + return this } @RequiresApi(31) private fun AndroidRenderEffect.blendWith( foreground: AndroidRenderEffect, blendMode: BlendMode, - offset: Offset = Offset.Zero, -): AndroidRenderEffect = AndroidRenderEffect.createBlendModeEffect( - this, - // We need to offset the shader to the bounds - AndroidRenderEffect.createOffsetEffect(offset.x, offset.y, foreground), - blendMode, -) +): AndroidRenderEffect = AndroidRenderEffect.createBlendModeEffect(this, foreground, blendMode) @RequiresApi(33) private fun createBlurImageFilterWithMask( diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt index 63ec572d..715dcd03 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt @@ -15,11 +15,13 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.geometry.isUnspecified import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.clipRect @@ -30,7 +32,6 @@ import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.withSaveLayer import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.modifier.ModifierLocalModifierNode import androidx.compose.ui.modifier.modifierLocalOf import androidx.compose.ui.node.CompositionLocalConsumerModifierNode @@ -73,8 +74,6 @@ class HazeEffectNode( override val shouldAutoInvalidate: Boolean = false - private val paint by unsynchronizedLazy { Paint() } - private var renderEffect: RenderEffect? = null private var dirtyTracker = Bitmask() @@ -415,20 +414,52 @@ class HazeEffectNode( val m = mask val p = progressive - if (m != null) { - drawRect(brush = m, colorFilter = ColorFilter.tint(tint.color)) - } else if (p is HazeProgressive.LinearGradient) { - drawRect(brush = p.asBrush(), colorFilter = ColorFilter.tint(tint.color)) + if (tint.brush != null) { + val maskingShader = when { + m is ShaderBrush -> m.createShader(size) + p is HazeProgressive.LinearGradient -> (p.asBrush() as ShaderBrush).createShader(size) + else -> null + } + + if (maskingShader != null) { + val outerPaint = PaintPool.getOrCreate() + + drawContext.canvas.withSaveLayer(size.toRect(), outerPaint) { + drawRect(brush = tint.brush, blendMode = tint.blendMode) + + val maskPaint = PaintPool.getOrCreate().apply { + shader = maskingShader + blendMode = BlendMode.DstIn + } + drawContext.canvas.drawRect(size.toRect(), maskPaint) + + maskPaint.reset() + PaintPool.release(maskPaint) + } + PaintPool.release(outerPaint) + + } else { + drawRect(brush = tint.brush, blendMode = tint.blendMode) + } } else { - drawRect(color = tint.color, blendMode = tint.blendMode) + // This must be a color + if (m != null) { + drawRect(brush = m, colorFilter = ColorFilter.tint(tint.color)) + } else if (p is HazeProgressive.LinearGradient) { + drawRect(brush = p.asBrush(), colorFilter = ColorFilter.tint(tint.color)) + } else { + drawRect(color = tint.color, blendMode = tint.blendMode) + } } } if (alpha != 1f) { + val paint = PaintPool.getOrCreate() paint.alpha = alpha drawContext.canvas.withSaveLayer(size.toRect(), paint) { scrim(scrimTint) } + PaintPool.release(paint) } else { scrim(scrimTint) } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt index 991c7e33..2e899f09 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt @@ -198,6 +198,11 @@ internal expect fun isBlurEnabledByDefault(): Boolean internal expect fun DrawScope.canUseGraphicLayers(): Boolean internal fun HazeTint.boostForFallback(blurRadius: Dp): HazeTint { + if (brush != null) { + // We can't boost brush tints + return this + } + // For color, we can boost the alpha val resolved = blurRadius.takeOrElse { HazeDefaults.blurRadius } val boosted = color.boostAlphaForBlurRadius(resolved) diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeStyle.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeStyle.kt index c256c933..280ecf45 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeStyle.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeStyle.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.unit.Dp @@ -23,7 +24,8 @@ import androidx.compose.ui.unit.Dp * - Value set in style provided to [hazeEffect] (or [HazeEffectScope.style]), if specified. * - Value set in this composition local. */ -val LocalHazeStyle: ProvidableCompositionLocal = compositionLocalOf { HazeStyle.Unspecified } +val LocalHazeStyle: ProvidableCompositionLocal = + compositionLocalOf { HazeStyle.Unspecified } /** * A holder for the style properties used by Haze. @@ -67,16 +69,28 @@ data class HazeStyle( } } +/** + * Describes a 'tint' drawn by the haze effect. + * + * Ideally this class would be a sealed class, but unfortunately that would require breaking the + * API so we need to use this merged class for v1.x. + */ +@ExposedCopyVisibility @Stable -data class HazeTint( +data class HazeTint internal constructor( val color: Color, - val blendMode: BlendMode = BlendMode.SrcOver, + val blendMode: BlendMode, + val brush: Brush?, ) { + constructor(color: Color, blendMode: BlendMode = BlendMode.SrcOver) : this(color = color, brush = null, blendMode = blendMode) + + constructor(brush: Brush, blendMode: BlendMode = BlendMode.SrcOver) : this(color = Color.Unspecified, brush = brush, blendMode = blendMode) + companion object { - val Unspecified: HazeTint = HazeTint(Color.Unspecified) + val Unspecified: HazeTint = HazeTint(Color.Unspecified, BlendMode.SrcOver, null) } - val isSpecified: Boolean get() = color.isSpecified + val isSpecified: Boolean get() = color.isSpecified || brush != null } internal inline fun Float.takeOrElse(block: () -> Float): Float = diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Paint.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Paint.kt new file mode 100644 index 00000000..5ac424aa --- /dev/null +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Paint.kt @@ -0,0 +1,12 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.ui.graphics.Paint + +internal val PaintPool: Pool = Pool(3) + +internal fun Pool.getOrCreate(): Paint = get() ?: Paint() + +internal expect fun Paint.reset() diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Pool.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Pool.kt new file mode 100644 index 00000000..40c63d11 --- /dev/null +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Pool.kt @@ -0,0 +1,18 @@ +package dev.chrisbanes.haze + +internal open class Pool(private val maxSize: Int) { + private val pool = mutableListOf() + + fun get(): T? = pool.removeFirst() + + fun release(instance: T) { + pool.add(instance) + maintainSize() + } + + private fun maintainSize() { + while (pool.size > maxSize) { + pool.removeFirst() + } + } +} diff --git a/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt b/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt index 30464bd0..0e056ec0 100644 --- a/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt +++ b/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt @@ -107,11 +107,7 @@ class HazeScreenshotTest : ScreenshotTest() { ScreenshotTheme { CreditCardSample( tint = DefaultTint, - mask = Brush.verticalGradient( - 0f to Color.Transparent, - 0.5f to Color.Black, - 1f to Color.Transparent, - ), + mask = VerticalMask, ) } } @@ -289,8 +285,53 @@ class HazeScreenshotTest : ScreenshotTest() { captureRoot("red") } + @Test + fun creditCard_brushTint() = runScreenshotTest { + setContent { + ScreenshotTheme { + CreditCardSample(tint = BrushTint) + } + } + captureRoot() + } + + @Test + fun creditCard_brushTint_mask() = runScreenshotTest { + setContent { + ScreenshotTheme { + CreditCardSample(tint = BrushTint, mask = VerticalMask) + } + } + captureRoot() + } + + @Test + fun creditCard_brushTint_progressive() = runScreenshotTest { + setContent { + ScreenshotTheme { + CreditCardSample(tint = BrushTint, progressive = HazeProgressive.verticalGradient()) + } + } + captureRoot() + } + companion object { val DefaultTint = HazeTint(Color.White.copy(alpha = 0.1f)) val OverrideStyle = HazeStyle(tints = listOf(HazeTint(Color.Red.copy(alpha = 0.5f)))) + + val BrushTint = HazeTint( + brush = Brush.radialGradient( + colors = listOf( + Color.Yellow.copy(alpha = 0.5f), + Color.Red.copy(alpha = 0.5f), + ), + ), + ) + + val VerticalMask = Brush.verticalGradient( + 0f to Color.Transparent, + 0.5f to Color.Black, + 1f to Color.Transparent, + ) } } diff --git a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/Paint.skiko.kt b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/Paint.skiko.kt new file mode 100644 index 00000000..b61a0ac9 --- /dev/null +++ b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/Paint.skiko.kt @@ -0,0 +1,10 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.ui.graphics.Paint + +internal actual fun Paint.reset() { + asFrameworkPaint().reset() +} diff --git a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt index c2825b57..86b9b3ce 100644 --- a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt +++ b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.BlurEffect import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RenderEffect import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.asComposeRenderEffect @@ -59,7 +60,7 @@ internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(para shaderNames = arrayOf("content", "blur"), inputs = arrayOf(null, blur), ) - .withTints(params.tints, params.tintAlphaModulate, progressiveShader) + .withTints(params.tints, params.tintAlphaModulate, params.contentSize, progressiveShader) .withMask(params.mask, params.contentSize) .asComposeRenderEffect() } @@ -71,42 +72,78 @@ private fun ImageFilter.chainWith(imageFilter: ImageFilter): ImageFilter { private fun ImageFilter.withTints( tints: List, alphaModulate: Float = 1f, - mask: Shader? = null, - maskOffset: Offset = Offset.Zero, + size: Size, + mask: Shader?, ): ImageFilter = tints.fold(this) { acc, tint -> - acc.withTint(tint, alphaModulate, mask, maskOffset) + acc.withTint(tint, alphaModulate, size, mask) } private fun ImageFilter.withTint( tint: HazeTint, alphaModulate: Float = 1f, + size: Size, mask: Shader?, - maskOffset: Offset, ): ImageFilter { - if (!tint.color.isSpecified) return this - val color = when { + if (!tint.isSpecified) return this + + val tintBrush = tint.brush?.toShader(size) + if (tintBrush != null) { + val brushEffect = when { + alphaModulate >= 1f -> { + ImageFilter.makeShader(tintBrush, crop = null) + } + else -> { + // If we need to modulate the alpha, we'll need to wrap it in a ColorFilter + ImageFilter.makeColorFilter( + ColorFilter.makeBlend(Color.Black.copy(alpha = alphaModulate).toArgb(), BlendMode.SRC_IN), + ImageFilter.makeShader(tintBrush, crop = null), + crop = null, + ) + } + } + + return if (mask != null) { + blendWith( + ImageFilter.makeBlend( + blendMode = BlendMode.SRC_IN, + bg = ImageFilter.makeShader(mask, crop = null), + fg = brushEffect, + crop = null, + ), + blendMode = tint.blendMode.toSkiaBlendMode(), + ) + } else { + blendWith( + foreground = brushEffect, + blendMode = tint.blendMode.toSkiaBlendMode(), + ) + } + } + + val tintColor = when { alphaModulate < 1f -> tint.color.copy(alpha = tint.color.alpha * alphaModulate) else -> tint.color } - if (color.alpha < 0.005f) return this - - return if (mask != null) { - blendWith( - foreground = ImageFilter.makeColorFilter( - f = ColorFilter.makeBlend(color.toArgb(), BlendMode.SRC_IN), - input = ImageFilter.makeShader(shader = mask, crop = null), + if (tintColor.alpha >= 0.005f) { + return if (mask != null) { + return blendWith( + foreground = ImageFilter.makeColorFilter( + ColorFilter.makeBlend(tintColor.toArgb(), BlendMode.SRC_IN), + ImageFilter.makeShader(mask, crop = null), + crop = null, + ), + blendMode = tint.blendMode.toSkiaBlendMode(), + ) + } else { + ImageFilter.makeColorFilter( + ColorFilter.makeBlend(tintColor.toArgb(), tint.blendMode.toSkiaBlendMode()), + this, crop = null, - ), - blendMode = tint.blendMode.toSkiaBlendMode(), - offset = maskOffset, - ) - } else { - ImageFilter.makeColorFilter( - f = ColorFilter.makeBlend(color.toArgb(), tint.blendMode.toSkiaBlendMode()), - input = this, - crop = null, - ) + ) + } } + + return this } private fun ImageFilter.withMask(