Haze

repository·main·Indexed 25 days ago

https://github.com/chrisbanes/haze

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

Tokens
21.7K
Snippets
56
Records
93
Agent score
81%

What's inside Haze

  1. Overview of Haze

    main
    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.
  2. Haze module overview

    main

    Haze 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.
  3. Configure GlassStyle for reusable appearances

    main

    A GlassStyle is an opaque, replayable appearance program. You can create styles using a block and compose them using the .then method. Styles can be shared across multiple hazeGlass nodes. 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.
    }
  4. Understand the performance cost of Haze

    main

    Haze 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%).

  5. How Glass rendering works via the shared retained stage graph

    main

    Glass 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:

    1. Capture: The source content is captured into a retained layer.
    2. Blur: An optional separable-blur layer is produced.
    3. 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.
    4. Optical Pass: The optical pass is applied to the selected depth input.
    5. Composition: Optional stages for refraction-detail, rim, interaction-lighting, and group-alpha are composed.

    The renderer only re-records stages that have been invalidated by changes to source content, parameters, topology, or resource availability.

  6. Configure HazeBlurStyle and use .then() for overrides

    main

    HazeBlurStyle is 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 an emptyList() to colorEffects().

    val base = HazeBlurStyle {
      blurRadius(20.dp)
      noiseFactor(0.15f)
    }
    
    val compact = base.then {
      blurRadius(12.dp)
    }
  7. How blur color effects work in Haze 2

    main

    In 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 to null means 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 @Immutable contract is maintained even if the original list is mutated later.

  8. Understand factors affecting Glass performance

    main

    Glass 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 RuntimeShader effects use a single-output renderer for one or many surfaces; sibling attachment does not change the rendering topology.

  9. Understand HazeEffectDrawScope and HazeEffectLayoutScope

    main

    Custom renderers interact with the drawing and layout phases through two specific scopes:

    HazeEffectDrawScope

    Extends DrawScope and provides:

    • modifierBounds: The bounds of the effect in the current layer.
    • drawInput(): Draws the selected input (either HazeInput.Sources or HazeInput.Content).
    • currentValueOf: For tracked access to composition-local values.
    • The structural HazeSampling value.

    HazeEffectLayoutScope

    Extends Density and 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 draw invalidates drawing. Reading them during calculateLayerBounds triggers a recalculation of bounds followed by a redraw.

  10. How interaction updates affect the Android fused renderer

    main

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