Skip to content

Commit 4b36e4d

Browse files
committed
Add radial progressive
1 parent 122222d commit 4b36e4d

File tree

15 files changed

+125
-30
lines changed

15 files changed

+125
-30
lines changed

docs/usage.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ LargeTopAppBar(
100100
)
101101
```
102102

103+
There is two types of progressive effect support in Haze:
104+
105+
- `HazeProgressive.LinearGradient`: Linear gradients, usually vertical or horizontal but you can set any angle.
106+
- `HazeProgressive.RadialGradient`: Radial gradients, with a defined center.
107+
103108
!!! warning "Performance of Progressive"
104109

105110
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.

haze/api/api.txt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ package dev.chrisbanes.haze {
155155
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier hazeSource(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional float zIndex, optional Object? key);
156156
}
157157

158-
public sealed interface HazeProgressive {
158+
@androidx.compose.runtime.Immutable public sealed interface HazeProgressive {
159159
field public static final dev.chrisbanes.haze.HazeProgressive.Companion Companion;
160160
}
161161

@@ -187,6 +187,20 @@ package dev.chrisbanes.haze {
187187
property public final float startIntensity;
188188
}
189189

190+
@dev.chrisbanes.haze.Poko public static final class HazeProgressive.RadialGradient implements dev.chrisbanes.haze.HazeProgressive {
191+
ctor public HazeProgressive.RadialGradient(optional androidx.compose.animation.core.Easing easing, optional long center, optional float centerIntensity, optional float radius, optional float radiusIntensity);
192+
method public long getCenter();
193+
method public float getCenterIntensity();
194+
method public androidx.compose.animation.core.Easing getEasing();
195+
method public float getRadius();
196+
method public float getRadiusIntensity();
197+
property public final long center;
198+
property public final float centerIntensity;
199+
property public final androidx.compose.animation.core.Easing easing;
200+
property public final float radius;
201+
property public final float radiusIntensity;
202+
}
203+
190204
@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 {
191205
ctor public HazeSourceNode(dev.chrisbanes.haze.HazeState state, optional float zIndex, optional Object? key);
192206
method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
@@ -273,5 +287,8 @@ package dev.chrisbanes.haze {
273287
property public final dev.chrisbanes.haze.HazeTint Unspecified;
274288
}
275289

290+
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Poko {
291+
}
292+
276293
}
277294

haze/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach
124124
}
125125
}
126126

127+
poko {
128+
pokoAnnotation.set("dev/chrisbanes/haze/Poko")
129+
}
130+
127131
baselineProfile {
128132
filter { include("dev.chrisbanes.haze.*") }
129133
}
Loading
Loading
Loading
Loading

haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeChildNode.android.kt

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,36 @@ import kotlin.math.min
2222
private const val USE_RUNTIME_SHADER = true
2323

2424
@RequiresApi(31)
25-
internal actual fun HazeEffectNode.drawLinearGradientProgressiveEffect(
25+
internal actual fun HazeEffectNode.drawProgressiveEffect(
2626
drawScope: DrawScope,
27-
progressive: HazeProgressive.LinearGradient,
27+
progressive: HazeProgressive,
2828
contentLayer: GraphicsLayer,
2929
) {
3030
if (USE_RUNTIME_SHADER && Build.VERSION.SDK_INT >= 33) {
3131
with(drawScope) {
32-
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive.asBrush())
32+
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive)
3333
contentLayer.alpha = alpha
3434

3535
// Finally draw the layer
3636
drawLayer(contentLayer)
3737
}
38-
} else if (progressive.preferPerformance) {
39-
// When the 'prefer performance' flag is enabled, we switch to using a mask instead
38+
} else if (progressive is HazeProgressive.LinearGradient && !progressive.preferPerformance) {
39+
// If it's a linear gradient, and the 'preferPerformance' flag is not enabled, we can use
40+
// our slow approximated version
41+
drawLinearGradientProgressiveEffectUsingLayers(
42+
drawScope = drawScope,
43+
progressive = progressive,
44+
contentLayer = contentLayer,
45+
)
46+
} else {
47+
// Otherwise we convert it to a mask
4048
with(drawScope) {
4149
contentLayer.renderEffect = getOrCreateRenderEffect(mask = progressive.asBrush())
4250
contentLayer.alpha = alpha
4351

4452
// Finally draw the layer
4553
drawLayer(contentLayer)
4654
}
47-
} else {
48-
drawLinearGradientProgressiveEffectUsingLayers(
49-
drawScope = drawScope,
50-
progressive = progressive,
51-
contentLayer = contentLayer,
52-
)
5355
}
5456
}
5557

haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(para
4040

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

43-
val progressiveShader = params.progressive?.toShader(params.contentSize)
43+
val progressiveShader = params.progressive?.asBrush()?.toShader(params.contentSize)
4444

4545
val blur = when {
4646
params.blurRadius <= 0.dp -> AndroidRenderEffect.createOffsetEffect(0f, 0f)

haze/src/commonMain/kotlin/dev/chrisbanes/haze/Gradient.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,28 @@ package dev.chrisbanes.haze
66
import androidx.compose.ui.graphics.Brush
77
import androidx.compose.ui.graphics.Color
88

9-
internal fun HazeProgressive.LinearGradient.asBrush(numStops: Int = 20): Brush {
10-
return Brush.linearGradient(
9+
internal fun HazeProgressive.asBrush(numStops: Int = 20): Brush? = when (this) {
10+
is HazeProgressive.LinearGradient -> asBrush(numStops)
11+
is HazeProgressive.RadialGradient -> asBrush(numStops)
12+
else -> null
13+
}
14+
15+
private fun HazeProgressive.LinearGradient.asBrush(numStops: Int = 20): Brush =
16+
Brush.linearGradient(
1117
colors = List(numStops) { i ->
1218
val x = i * 1f / (numStops - 1)
1319
Color.Magenta.copy(alpha = lerp(startIntensity, endIntensity, easing.transform(x)))
1420
},
1521
start = start,
1622
end = end,
1723
)
18-
}
24+
25+
private fun HazeProgressive.RadialGradient.asBrush(numStops: Int = 20): Brush =
26+
Brush.radialGradient(
27+
colors = List(numStops) { i ->
28+
val x = i * 1f / (numStops - 1)
29+
Color.Magenta.copy(alpha = lerp(centerIntensity, radiusIntensity, easing.transform(x)))
30+
},
31+
center = center,
32+
radius = radius,
33+
)

haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package dev.chrisbanes.haze
77

88
import androidx.compose.animation.core.EaseIn
99
import androidx.compose.animation.core.Easing
10+
import androidx.compose.runtime.Immutable
1011
import androidx.compose.runtime.snapshots.Snapshot
1112
import androidx.compose.ui.Modifier
1213
import androidx.compose.ui.geometry.Offset
@@ -47,7 +48,6 @@ import androidx.compose.ui.unit.dp
4748
import androidx.compose.ui.unit.roundToIntSize
4849
import androidx.compose.ui.unit.takeOrElse
4950
import androidx.compose.ui.unit.toSize
50-
import dev.drewhamilton.poko.Poko
5151

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

@@ -381,8 +381,8 @@ class HazeEffectNode(
381381
clipRect {
382382
scale(1f / scaleFactor, Offset.Zero) {
383383
val p = progressive
384-
if (p is HazeProgressive.LinearGradient) {
385-
drawLinearGradientProgressiveEffect(
384+
if (p != null) {
385+
drawProgressiveEffect(
386386
drawScope = this,
387387
progressive = p,
388388
contentLayer = layer,
@@ -416,7 +416,7 @@ class HazeEffectNode(
416416
if (tint.brush != null) {
417417
val maskingShader = when {
418418
m is ShaderBrush -> m.createShader(size)
419-
p is HazeProgressive.LinearGradient -> (p.asBrush() as ShaderBrush).createShader(size)
419+
p != null -> (p.asBrush() as? ShaderBrush)?.createShader(size)
420420
else -> null
421421
}
422422

@@ -437,10 +437,11 @@ class HazeEffectNode(
437437
}
438438
} else {
439439
// This must be a color
440+
val progressiveBrush = p?.asBrush()
440441
if (m != null) {
441442
drawRect(brush = m, colorFilter = ColorFilter.tint(tint.color))
442-
} else if (p is HazeProgressive.LinearGradient) {
443-
drawRect(brush = p.asBrush(), colorFilter = ColorFilter.tint(tint.color))
443+
} else if (progressiveBrush != null) {
444+
drawRect(brush = progressiveBrush, colorFilter = ColorFilter.tint(tint.color))
444445
} else {
445446
drawRect(color = tint.color, blendMode = tint.blendMode)
446447
}
@@ -488,6 +489,7 @@ class HazeEffectNode(
488489
/**
489490
* Parameters for applying a progressive blur effect.
490491
*/
492+
@Immutable
491493
sealed interface HazeProgressive {
492494

493495
/**
@@ -523,6 +525,35 @@ sealed interface HazeProgressive {
523525
val preferPerformance: Boolean = false,
524526
) : HazeProgressive
525527

528+
/**
529+
* A radial gradient effect.
530+
*
531+
* Platform support:
532+
* - Skia backed platforms (iOS, Desktop, etc): ✅
533+
* - Android SDK Level 33+: ✅
534+
* - Android SDK Level 31-32: Falls back to a mask
535+
* - Android SDK Level < 31: Falls back to a scrim
536+
*
537+
* @param easing - The easing function to use when applying the effect. Defaults to a
538+
* linear easing effect.
539+
* @param center Center position of the radial gradient circle. If this is set to
540+
* [Offset.Unspecified] then the center of the drawing area is used as the center for
541+
* the radial gradient. [Float.POSITIVE_INFINITY] can be used for either [Offset.x] or
542+
* [Offset.y] to indicate the far right or far bottom of the drawing area respectively.
543+
* @param centerIntensity - The intensity of the haze effect at the [center], in the range `0f`..`1f`.
544+
* @param radius Radius for the radial gradient. Defaults to positive infinity to indicate
545+
* the largest radius that can fit within the bounds of the drawing area.
546+
* @param radiusIntensity - The intensity of the haze effect at the [radius], in the range `0f`..`1f`
547+
*/
548+
@Poko
549+
class RadialGradient(
550+
val easing: Easing = EaseIn,
551+
val center: Offset = Offset.Unspecified,
552+
val centerIntensity: Float = 1f,
553+
val radius: Float = Float.POSITIVE_INFINITY,
554+
val radiusIntensity: Float = 0f,
555+
) : HazeProgressive
556+
526557
companion object {
527558
/**
528559
* A vertical gradient effect.
@@ -598,7 +629,7 @@ internal class RenderEffectParams(
598629
val tintAlphaModulate: Float = 1f,
599630
val contentSize: Size,
600631
val mask: Brush? = null,
601-
val progressive: Brush? = null,
632+
val progressive: HazeProgressive? = null,
602633
)
603634

604635
@ExperimentalHazeApi
@@ -630,7 +661,7 @@ internal fun HazeEffectNode.getOrCreateRenderEffect(
630661
tintAlphaModulate: Float = 1f,
631662
contentSize: Size = this.size * inputScale,
632663
mask: Brush? = this.mask,
633-
progressive: Brush? = null,
664+
progressive: HazeProgressive? = null,
634665
): RenderEffect? = getOrCreateRenderEffect(
635666
RenderEffectParams(
636667
blurRadius = blurRadius,
@@ -658,9 +689,9 @@ internal fun CompositionLocalConsumerModifierNode.getOrCreateRenderEffect(params
658689

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

661-
internal expect fun HazeEffectNode.drawLinearGradientProgressiveEffect(
692+
internal expect fun HazeEffectNode.drawProgressiveEffect(
662693
drawScope: DrawScope,
663-
progressive: HazeProgressive.LinearGradient,
694+
progressive: HazeProgressive,
664695
contentLayer: GraphicsLayer,
665696
)
666697

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2025, Christopher Banes and the Haze project contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package dev.chrisbanes.haze
5+
6+
@Retention(AnnotationRetention.SOURCE)
7+
@Target(AnnotationTarget.CLASS)
8+
annotation class Poko

haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ class HazeScreenshotTest : ScreenshotTest() {
189189
captureRoot()
190190
}
191191

192+
@Test
193+
fun creditCard_progressive_radial() = runScreenshotTest {
194+
setContent {
195+
ScreenshotTheme {
196+
CreditCardSample(
197+
tint = DefaultTint,
198+
progressive = HazeProgressive.RadialGradient(),
199+
)
200+
}
201+
}
202+
captureRoot()
203+
}
204+
192205
@Test
193206
fun creditCard_childTint() = runScreenshotTest {
194207
var tint by mutableStateOf(

haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeChildNode.skiko.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
77
import androidx.compose.ui.graphics.layer.GraphicsLayer
88
import androidx.compose.ui.graphics.layer.drawLayer
99

10-
internal actual fun HazeEffectNode.drawLinearGradientProgressiveEffect(
10+
internal actual fun HazeEffectNode.drawProgressiveEffect(
1111
drawScope: DrawScope,
12-
progressive: HazeProgressive.LinearGradient,
12+
progressive: HazeProgressive,
1313
contentLayer: GraphicsLayer,
1414
) = with(drawScope) {
15-
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive.asBrush())
15+
contentLayer.renderEffect = getOrCreateRenderEffect(progressive = progressive)
1616
contentLayer.alpha = alpha
1717

1818
// Finally draw the layer

haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(para
3838
child("noise", NOISE_SHADER)
3939
}
4040

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

0 commit comments

Comments
 (0)