Haze
repository·main·Indexed 25 days ago
https://github.com/chrisbanes/hazeA library providing hardware-accelerated visual effects, such as blur and tint, for Compose Multiplatform applications. It supports Android, Desktop (JVM), iOS, and Wasm/JS. Haze 2 introduces a modular pluggable visual effects system with specific modules for blur behavior and material presets.
What's inside Haze
- Haze is a library providing hardware-accelerated visual effects (such as blur and tint) for Compose Multiplatform. It is built on a modular effect system that allows you to apply effects to any composable using a single modifier. It supports Android, Desktop (JVM), iOS, and Wasm/JS.
Platform support for Haze
mainHaze is compatible with all platforms supported by Compose Multiplatform.
Note on MacOS: While Kotlin Multiplatform supports MacOS, Compose Multiplatform does not support it directly. Therefore, Haze does not currently support MacOS.
Supported platforms for Haze
mainHaze is built with Compose Multiplatform and supports the following platforms:
Platform Supported Android ✅ Desktop (JVM) ✅ iOS ✅ Wasm ✅ JS/Canvas ✅ Haze module overview
mainHaze is organized into several modules to separate core orchestration from specific effect implementations:
- haze: Contains core state, source capture, typed custom-effect orchestration, and a temporary legacy path.
- haze-blur: Implementation of the blur effect.
- haze-blur-materials: Reusable blur presets.
- haze-glass: Implementation of the Glass effect.
- haze-utils: Shared platform rendering utilities.
Configure GlassStyle for reusable appearances
mainA
GlassStyleis an opaque, replayable appearance program. You can create styles using a block and compose them using the.thenmethod. Styles can be shared across multiplehazeGlassnodes. When replacing a style via recomposition, any properties or interaction blocks omitted by the new style will be removed.val baseStyle = GlassStyle { tint(Color.White.copy(alpha = 0.16f)) optics(GlassOptics.Absolute(refractionStrength = 0.8f)) shape(RoundedCornerShape(20.dp)) } val emphasizedStyle = baseStyle.then { specularIntensity(0.7f) } CompositionLocalProvider(LocalGlassStyle provides baseStyle) { // Each node gets a fresh snapshot; an explicit Style is applied last. }Understand the performance cost of Haze
mainHaze introduces an additional cost to your application's frame duration. While the exact impact depends on your specific implementation, benchmarks on Android show the following approximate increases in frame duration when Haze is enabled:
| Scenario | Approximate Increase in Haze Cost | | :--- | : | | Scaffold (Simple rectangular areas) | +29% | | Images List (Multiple
RenderNodes, rounded rectangles) | +45% | | Credit Card (High invalidation frequency via dragging) | +98% |Note: These percentages represent the increase in the cost of Haze itself, not the total frame duration. The impact on total frame duration is typically much smaller (e.g., in the range of 3-5%).
How Glass rendering works via the shared retained stage graph
mainGlass effects are composed using a shared retained stage graph that ensures consistent depth semantics and stage ordering across both Android and Skiko platforms. Instead of rebuilding the entire effect graph on every frame, the renderer uses a retained approach to reuse unchanged stages, which is critical for performance during animations.
The rendering pipeline follows these steps:
- Capture: The source content is captured into a retained layer.
- Blur: An optional separable-blur layer is produced.
- Depth Selection: The depth input is selected or recorded:
depth 0: Uses the original source content.depth 1: Uses the blurred layer.- Intermediate depth: Records a retained mix of both source and blurred content.
- Optical Pass: The optical pass is applied to the selected depth input.
- Composition: Optional stages for
refraction-detail,rim,interaction-lighting, andgroup-alphaare composed.
The renderer only re-records stages that have been invalidated by changes to source content, parameters, topology, or resource availability.
Configure HazeBlurStyle and use .then() for overrides
mainHazeBlurStyleis a replayable record of Blur-specific writes rather than a value patch. You cannot use.copy()to modify an existing style. Instead, use the.then { ... }method to create a new style that applies additional writes on top of a base style.Resolution follows this priority: defaults $\rightarrow$
LocalHazeBlurStyle$\rightarrow$ the explicit modifier Style. The last write wins. To clear inherited effects (like color effects), pass anemptyList()tocolorEffects().val base = HazeBlurStyle { blurRadius(20.dp) noiseFactor(0.15f) } val compact = base.then { blurRadius(12.dp) }How blur color effects work in Haze 2
mainIn Haze 2, the blur style contract uses specific list semantics to manage color effects. This allows for predictable inheritance and overriding of color effects across different style-precedence tiers.
- Unspecified (
null): Setting the color effects tonullmeans they are unspecified. This allows the next style-precedence tier to supply the color effects. - Specified Empty (
emptyList()): Providing an empty list means the effects are explicitly specified as empty. This clears any inherited color effects. - Specified Non-empty: Providing a list of effects applies those specific effects.
Additionally, style objects defensively snapshot caller-owned lists to ensure their
@Immutablecontract is maintained even if the original list is mutated later.- Unspecified (
Understand factors affecting Glass performance
mainGlass is designed for real-time UI effects, but its performance cost is influenced by several factors. When designing layouts with Glass, consider the following:
- Surface area: Larger Glass surfaces process more pixels.
- Number of effects: Increasing the number of independent surfaces adds rendering and submission work.
- Changing content: Moving or updating the captured source content invalidates more retained work than redrawing an unchanged effect.
- Dynamic optics: Features like progressive blur and Full chromatic aberration increase sampling within the output effect graph.
- Device and display: Performance is dependent on GPU capability, resolution, refresh rate, and thermal state.
Note that Android
RuntimeShadereffects use a single-output renderer for one or many surfaces; sibling attachment does not change the rendering topology.Understand HazeEffectDrawScope and HazeEffectLayoutScope
mainCustom renderers interact with the drawing and layout phases through two specific scopes:
HazeEffectDrawScope
Extends
DrawScopeand provides:modifierBounds: The bounds of the effect in the current layer.drawInput(): Draws the selected input (eitherHazeInput.SourcesorHazeInput.Content).currentValueOf: For tracked access to composition-local values.- The structural
HazeSamplingvalue.
HazeEffectLayoutScope
Extends
Densityand provides:modifierBounds: The bounds of the modifier.currentValueOf: For tracked access to composition-local values.
Note on Side Effects: Reading snapshot state or composition locals during
drawinvalidates drawing. Reading them duringcalculateLayerBoundstriggers a recalculation of bounds followed by a redraw.How interaction updates affect the Android fused renderer
mainWhen using the fused renderer on Android (API 33+), live interaction values (such as press, hover, or focus) update the retained shader providers.
Crucially, interaction-only optical changes do not change the retained-layer topology or re-use previous pixels; instead, they re-record the fused output pixels. This design choice was made to avoid the performance cost of replaying additional layers for local interaction patches, ensuring that interaction updates remain within the frame budget.