Skip to content

Commit

Permalink
Add radial progressive
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Jan 26, 2025
1 parent 122222d commit 5c3967b
Show file tree
Hide file tree
Showing 15 changed files with 122 additions and 29 deletions.
5 changes: 5 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ LargeTopAppBar(
)
```

There is two types of progressive effect support in Haze:

- `HazeProgressive.LinearGradient`: Linear gradients, usually vertical or horizontal but you can set any angle.
- `HazeProgressive.RadialGradient`: Radial gradients, with a defined center.

!!! warning "Performance of Progressive"

Please be aware that using progressive blurring does come with a performance cost. Please see the [Performance](performance.md) page for up-to-date benchmarks.
Expand Down
17 changes: 17 additions & 0 deletions haze/api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,20 @@ package dev.chrisbanes.haze {
property public final float startIntensity;
}

@dev.chrisbanes.haze.Poko public static final class HazeProgressive.RadialGradient implements dev.chrisbanes.haze.HazeProgressive {
ctor public HazeProgressive.RadialGradient(optional androidx.compose.animation.core.Easing easing, optional long center, optional float centerIntensity, optional float radius, optional float radiusIntensity);
method public long getCenter();
method public float getCenterIntensity();
method public androidx.compose.animation.core.Easing getEasing();
method public float getRadius();
method public float getRadiusIntensity();
property public final long center;
property public final float centerIntensity;
property public final androidx.compose.animation.core.Easing easing;
property public final float radius;
property public final float radiusIntensity;
}

@dev.chrisbanes.haze.ExperimentalHazeApi public final class HazeSourceNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.node.CompositionLocalConsumerModifierNode androidx.compose.ui.node.DrawModifierNode androidx.compose.ui.node.GlobalPositionAwareModifierNode androidx.compose.ui.node.LayoutAwareModifierNode androidx.compose.ui.modifier.ModifierLocalModifierNode androidx.compose.ui.node.ObserverModifierNode {
ctor public HazeSourceNode(dev.chrisbanes.haze.HazeState state, optional float zIndex, optional Object? key);
method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
Expand Down Expand Up @@ -273,5 +287,8 @@ package dev.chrisbanes.haze {
property public final dev.chrisbanes.haze.HazeTint Unspecified;
}

@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Poko {
}

}

4 changes: 4 additions & 0 deletions haze/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach
}
}

poko {
pokoAnnotation.set("dev/chrisbanes/haze/Poko")
}

baselineProfile {
filter { include("dev.chrisbanes.haze.*") }
}
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.
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,36 @@ import kotlin.math.min
private const val USE_RUNTIME_SHADER = true

@RequiresApi(31)
internal actual fun HazeEffectNode.drawLinearGradientProgressiveEffect(
internal actual fun HazeEffectNode.drawProgressiveEffect(
drawScope: DrawScope,
progressive: HazeProgressive.LinearGradient,
progressive: HazeProgressive,
contentLayer: GraphicsLayer,
) {
if (USE_RUNTIME_SHADER && Build.VERSION.SDK_INT >= 33) {
with(drawScope) {
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive.asBrush())
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive)
contentLayer.alpha = alpha

// Finally draw the layer
drawLayer(contentLayer)
}
} else if (progressive.preferPerformance) {
// When the 'prefer performance' flag is enabled, we switch to using a mask instead
} else if (progressive is HazeProgressive.LinearGradient && !progressive.preferPerformance) {
// If it's a linear gradient, and the 'preferPerformance' flag is not enabled, we can use
// our slow approximated version
drawLinearGradientProgressiveEffectUsingLayers(
drawScope = drawScope,
progressive = progressive,
contentLayer = contentLayer,
)
} else {
// Otherwise we convert it to a mask
with(drawScope) {
contentLayer.renderEffect = getOrCreateRenderEffect(mask = progressive.asBrush())
contentLayer.alpha = alpha

// Finally draw the layer
drawLayer(contentLayer)
}
} else {
drawLinearGradientProgressiveEffectUsingLayers(
drawScope = drawScope,
progressive = progressive,
contentLayer = contentLayer,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(para

require(params.blurRadius >= 0.dp) { "blurRadius needs to be equal or greater than 0.dp" }

val progressiveShader = params.progressive?.toShader(params.contentSize)
val progressiveShader = params.progressive?.asBrush()?.toShader(params.contentSize)

val blur = when {
params.blurRadius <= 0.dp -> AndroidRenderEffect.createOffsetEffect(0f, 0f)
Expand Down
21 changes: 18 additions & 3 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Gradient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,28 @@ package dev.chrisbanes.haze
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color

internal fun HazeProgressive.LinearGradient.asBrush(numStops: Int = 20): Brush {
return Brush.linearGradient(
internal fun HazeProgressive.asBrush(numStops: Int = 20): Brush? = when (this) {
is HazeProgressive.LinearGradient -> asBrush(numStops)
is HazeProgressive.RadialGradient -> asBrush(numStops)
else -> null
}

private fun HazeProgressive.LinearGradient.asBrush(numStops: Int = 20): Brush =
Brush.linearGradient(
colors = List(numStops) { i ->
val x = i * 1f / (numStops - 1)
Color.Magenta.copy(alpha = lerp(startIntensity, endIntensity, easing.transform(x)))
},
start = start,
end = end,
)
}

private fun HazeProgressive.RadialGradient.asBrush(numStops: Int = 20): Brush =
Brush.radialGradient(
colors = List(numStops) { i ->
val x = i * 1f / (numStops - 1)
Color.Magenta.copy(alpha = lerp(centerIntensity, radiusIntensity, easing.transform(x)))
},
center = center,
radius = radius,
)
49 changes: 39 additions & 10 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.unit.takeOrElse
import androidx.compose.ui.unit.toSize
import dev.drewhamilton.poko.Poko

internal val ModifierLocalCurrentHazeZIndex = modifierLocalOf<Float?> { null }

Expand Down Expand Up @@ -381,8 +380,8 @@ class HazeEffectNode(
clipRect {
scale(1f / scaleFactor, Offset.Zero) {
val p = progressive
if (p is HazeProgressive.LinearGradient) {
drawLinearGradientProgressiveEffect(
if (p != null) {
drawProgressiveEffect(
drawScope = this,
progressive = p,
contentLayer = layer,
Expand Down Expand Up @@ -416,7 +415,7 @@ class HazeEffectNode(
if (tint.brush != null) {
val maskingShader = when {
m is ShaderBrush -> m.createShader(size)
p is HazeProgressive.LinearGradient -> (p.asBrush() as ShaderBrush).createShader(size)
p != null -> (p.asBrush() as? ShaderBrush)?.createShader(size)
else -> null
}

Expand All @@ -437,10 +436,11 @@ class HazeEffectNode(
}
} else {
// This must be a color
val progressiveBrush = p?.asBrush()
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 if (progressiveBrush != null) {
drawRect(brush = progressiveBrush, colorFilter = ColorFilter.tint(tint.color))
} else {
drawRect(color = tint.color, blendMode = tint.blendMode)
}
Expand Down Expand Up @@ -523,6 +523,35 @@ sealed interface HazeProgressive {
val preferPerformance: Boolean = false,
) : HazeProgressive

/**
* A radial gradient effect.
*
* Platform support:
* - Skia backed platforms (iOS, Desktop, etc): ✅
* - Android SDK Level 33+: ✅
* - Android SDK Level 31-32: Falls back to a mask
* - Android SDK Level < 31: Falls back to a scrim
*
* @param easing - The easing function to use when applying the effect. Defaults to a
* linear easing effect.
* @param center Center position of the radial gradient circle. If this is set to
* [Offset.Unspecified] then the center of the drawing area is used as the center for
* the radial gradient. [Float.POSITIVE_INFINITY] can be used for either [Offset.x] or
* [Offset.y] to indicate the far right or far bottom of the drawing area respectively.
* @param centerIntensity - The intensity of the haze effect at the [center], in the range `0f`..`1f`.
* @param radius Radius for the radial gradient. Defaults to positive infinity to indicate
* the largest radius that can fit within the bounds of the drawing area.
* @param radiusIntensity - The intensity of the haze effect at the [radius], in the range `0f`..`1f`
*/
@Poko
class RadialGradient(
val easing: Easing = EaseIn,
val center: Offset = Offset.Unspecified,
val centerIntensity: Float = 1f,
val radius: Float = Float.POSITIVE_INFINITY,
val radiusIntensity: Float = 0f,
) : HazeProgressive

companion object {
/**
* A vertical gradient effect.
Expand Down Expand Up @@ -598,7 +627,7 @@ internal class RenderEffectParams(
val tintAlphaModulate: Float = 1f,
val contentSize: Size,
val mask: Brush? = null,
val progressive: Brush? = null,
val progressive: HazeProgressive? = null,
)

@ExperimentalHazeApi
Expand Down Expand Up @@ -630,7 +659,7 @@ internal fun HazeEffectNode.getOrCreateRenderEffect(
tintAlphaModulate: Float = 1f,
contentSize: Size = this.size * inputScale,
mask: Brush? = this.mask,
progressive: Brush? = null,
progressive: HazeProgressive? = null,
): RenderEffect? = getOrCreateRenderEffect(
RenderEffectParams(
blurRadius = blurRadius,
Expand Down Expand Up @@ -658,9 +687,9 @@ internal fun CompositionLocalConsumerModifierNode.getOrCreateRenderEffect(params

internal expect fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect?

internal expect fun HazeEffectNode.drawLinearGradientProgressiveEffect(
internal expect fun HazeEffectNode.drawProgressiveEffect(
drawScope: DrawScope,
progressive: HazeProgressive.LinearGradient,
progressive: HazeProgressive,
contentLayer: GraphicsLayer,
)

Expand Down
8 changes: 8 additions & 0 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Poko.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright 2025, Christopher Banes and the Haze project contributors
// SPDX-License-Identifier: Apache-2.0

package dev.chrisbanes.haze

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class Poko
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,19 @@ class HazeScreenshotTest : ScreenshotTest() {
captureRoot()
}

@Test
fun creditCard_progressive_radial() = runScreenshotTest {
setContent {
ScreenshotTheme {
CreditCardSample(
tint = DefaultTint,
progressive = HazeProgressive.RadialGradient(),
)
}
}
captureRoot()
}

@Test
fun creditCard_childTint() = runScreenshotTest {
var tint by mutableStateOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer

internal actual fun HazeEffectNode.drawLinearGradientProgressiveEffect(
internal actual fun HazeEffectNode.drawProgressiveEffect(
drawScope: DrawScope,
progressive: HazeProgressive.LinearGradient,
progressive: HazeProgressive,
contentLayer: GraphicsLayer,
) = with(drawScope) {
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive.asBrush())
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive)
contentLayer.alpha = alpha

// Finally draw the layer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(para
child("noise", NOISE_SHADER)
}

val progressiveShader = params.progressive?.toShader(params.contentSize)
val progressiveShader = params.progressive?.asBrush()?.toShader(params.contentSize)
val blur = if (progressiveShader != null) {
// If we've been provided with a progressive/gradient blur shader, we need to use
// our custom blur via a runtime shader
Expand Down

0 comments on commit 5c3967b

Please sign in to comment.