Skip to content

Commit

Permalink
Add brush support to HazeTint
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Jan 19, 2025
1 parent b24d83c commit b2c669f
Show file tree
Hide file tree
Showing 23 changed files with 285 additions and 77 deletions.
6 changes: 5 additions & 1 deletion haze/api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions haze/src/androidMain/kotlin/dev/chrisbanes/haze/Paint.android.kt
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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) {
Expand All @@ -135,55 +133,83 @@ private fun Brush.toShader(size: Size): Shader? = when (this) {
@RequiresApi(31)
private fun AndroidRenderEffect.withTints(
tints: List<HazeTint>,
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(
Expand Down
47 changes: 39 additions & 8 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 19 additions & 5 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeStyle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<HazeStyle> = compositionLocalOf { HazeStyle.Unspecified }
val LocalHazeStyle: ProvidableCompositionLocal<HazeStyle> =
compositionLocalOf { HazeStyle.Unspecified }

/**
* A holder for the style properties used by Haze.
Expand Down Expand Up @@ -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 =
Expand Down
12 changes: 12 additions & 0 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Paint.kt
Original file line number Diff line number Diff line change
@@ -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<Paint> = Pool(3)

internal fun Pool<Paint>.getOrCreate(): Paint = get() ?: Paint()

internal expect fun Paint.reset()
18 changes: 18 additions & 0 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Pool.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.chrisbanes.haze

internal open class Pool<T>(private val maxSize: Int) {
private val pool = mutableListOf<T>()

fun get(): T? = pool.removeFirst()

fun release(instance: T) {
pool.add(instance)
maintainSize()
}

private fun maintainSize() {
while (pool.size > maxSize) {
pool.removeFirst()
}
}
}
Loading

0 comments on commit b2c669f

Please sign in to comment.