diff --git a/docs/architecture.md b/docs/architecture.md index f44bca34..701445b2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,7 +60,7 @@ haze (core) ``` Future effects will follow the same pattern: -- **haze-liquidglass** - Refraction-based liquid glass effect +- **haze-liquidglass** - Refraction-based liquid glass effect with rounded-shape SDF and optional chromatic aberration - Custom third-party effect modules ## Effect Registration Pattern diff --git a/docs/effects/liquid-glass.md b/docs/effects/liquid-glass.md new file mode 100644 index 00000000..fc9f1869 --- /dev/null +++ b/docs/effects/liquid-glass.md @@ -0,0 +1,47 @@ +# Liquid Glass + +A refraction-driven glass effect inspired by iOS “liquid” glass. It combines refraction, depth blur, tint, Fresnel/ambient lift, and specular highlights with optional rounded shapes and dispersion. + +## Parameters +- **tint**: Glass tint (defaults to white 12% alpha). +- **refractionStrength**: Distortion strength `0..1` (default 0.7). +- **refractionHeight**: Fraction of the shortest side that participates in refraction (default 0.25). Lower values pull the effect toward the edges; higher values push it deeper into the interior. +- **depth / blurRadius**: Blend refracted content with a blurred input to add depth (defaults 0.4 / 4.dp). +- **specularIntensity**: Highlight strength `0..1` (default 0.4). +- **ambientResponse**: Fresnel/edge lift `0..1` (default 0.5). +- **edgeSoftness**: Soft fade at the edges (default 12.dp). Set to 0.dp for hard edges. +- **shape (RoundedCornerShape)**: Rounded-rect boundary for refraction and masking (default all radii 0). +- **lightPosition**: Optional light source; defaults to the layer center. +- **chromaticAberrationStrength**: Simple dispersion strength `0..1` (default 0). TODO: expand to channel/spread controls. +- **alpha**: Overall opacity multiplier. + +## Fallbacks +- Runtime shader path: rounded SDF refraction, depth mix, tint/specular/Fresnel, chromatic aberration, and edge softness. +- Fallback path (when runtime shaders unavailable): tinted fill + radial highlight + edge falloff; respects rounded shapes and alpha. + +## Usage +```kotlin +Box( + Modifier + .size(180.dp) + .clip(RoundedCornerShape(20.dp)) + .hazeEffect(state = hazeState) { + liquidGlassEffect { + tint = Color.White.copy(alpha = 0.16f) + refractionStrength = 0.8f + refractionHeight = 0.32f + depth = 0.5f + specularIntensity = 0.7f + ambientResponse = 0.7f + edgeSoftness = 14.dp + shape = RoundedCornerShape(20.dp) + chromaticAberrationStrength = 0.2f // TODO: expand controls later + } + } +) +``` + +## Tips +- Lower `refractionHeight` for a pronounced edge “lens”; raise it for a fuller dome. +- Keep `chromaticAberrationStrength` modest; start at 0.1–0.25 to avoid rainbow artifacts. +- Combine `edgeSoftness` with rounded shapes for smooth clipping; set `edgeSoftness = 0.dp` to rely purely on the shape. diff --git a/haze-liquidglass/api/api.txt b/haze-liquidglass/api/api.txt new file mode 100644 index 00000000..b0be71ea --- /dev/null +++ b/haze-liquidglass/api/api.txt @@ -0,0 +1,104 @@ +// Signature format: 4.0 +package dev.chrisbanes.haze.liquidglass { + + public final class HazeEffectScopeKt { + method public static inline void liquidGlassEffect(dev.chrisbanes.haze.HazeEffectScope, optional kotlin.jvm.functions.Function1 block); + } + + public final class LiquidGlassDefaults { + method public androidx.compose.foundation.shape.RoundedCornerShape getShape(); + property public static float ambientResponse; + property public androidx.compose.ui.unit.Dp blurRadius; + property public static float chromaticAberrationStrength; + property public static float depth; + property public androidx.compose.ui.unit.Dp edgeSoftness; + property public static float refractionHeight; + property public static float refractionStrength; + property public androidx.compose.foundation.shape.RoundedCornerShape shape; + property public static float specularIntensity; + property public androidx.compose.ui.graphics.Color tint; + field public static final dev.chrisbanes.haze.liquidglass.LiquidGlassDefaults INSTANCE; + field public static final float ambientResponse = 0.5f; + field public static final float chromaticAberrationStrength = 0.0f; + field public static final float depth = 0.4f; + field public static final float refractionHeight = 0.25f; + field public static final float refractionStrength = 0.7f; + field public static final float specularIntensity = 0.4f; + } + + @androidx.compose.runtime.Immutable public final class LiquidGlassStyle { + ctor public LiquidGlassStyle(); + ctor @KotlinOnly public LiquidGlassStyle(optional androidx.compose.ui.graphics.Color tint, optional float refractionStrength, optional float specularIntensity, optional float depth, optional float ambientResponse, optional androidx.compose.ui.unit.Dp edgeSoftness, optional androidx.compose.ui.geometry.Offset lightPosition, optional androidx.compose.ui.unit.Dp blurRadius, optional float refractionHeight, optional float chromaticAberrationStrength, optional androidx.compose.foundation.shape.RoundedCornerShape? shape); + method public float component10(); + method public androidx.compose.foundation.shape.RoundedCornerShape? component11(); + method public float component2(); + method public float component3(); + method public float component4(); + method public float component5(); + method public float component9(); + method public dev.chrisbanes.haze.liquidglass.LiquidGlassStyle copy-Uuz26js(long tint, float refractionStrength, float specularIntensity, float depth, float ambientResponse, float edgeSoftness, long lightPosition, float blurRadius, float refractionHeight, float chromaticAberrationStrength, androidx.compose.foundation.shape.RoundedCornerShape? shape); + method public float getAmbientResponse(); + method public float getChromaticAberrationStrength(); + method public float getDepth(); + method public float getRefractionHeight(); + method public float getRefractionStrength(); + method public androidx.compose.foundation.shape.RoundedCornerShape? getShape(); + method public float getSpecularIntensity(); + property public float ambientResponse; + property public androidx.compose.ui.unit.Dp blurRadius; + property public float chromaticAberrationStrength; + property public float depth; + property public androidx.compose.ui.unit.Dp edgeSoftness; + property public androidx.compose.ui.geometry.Offset lightPosition; + property public float refractionHeight; + property public float refractionStrength; + property public androidx.compose.foundation.shape.RoundedCornerShape? shape; + property public float specularIntensity; + property public androidx.compose.ui.graphics.Color tint; + field public static final dev.chrisbanes.haze.liquidglass.LiquidGlassStyle.Companion Companion; + } + + public static final class LiquidGlassStyle.Companion { + method public dev.chrisbanes.haze.liquidglass.LiquidGlassStyle getUnspecified(); + property public dev.chrisbanes.haze.liquidglass.LiquidGlassStyle Unspecified; + } + + public final class LiquidGlassVisualEffect implements dev.chrisbanes.haze.VisualEffect { + ctor public LiquidGlassVisualEffect(); + method public void draw(androidx.compose.ui.graphics.drawscope.DrawScope, dev.chrisbanes.haze.VisualEffectContext context); + method public float getAlpha(); + method public float getAmbientResponse(); + method public float getChromaticAberrationStrength(); + method public float getDepth(); + method public float getRefractionHeight(); + method public float getRefractionStrength(); + method public androidx.compose.foundation.shape.RoundedCornerShape getShape(); + method public float getSpecularIntensity(); + method public dev.chrisbanes.haze.liquidglass.LiquidGlassStyle getStyle(); + method public void setAlpha(float); + method public void setAmbientResponse(float); + method public void setChromaticAberrationStrength(float); + method public void setDepth(float); + method public void setRefractionHeight(float); + method public void setRefractionStrength(float); + method public void setShape(androidx.compose.foundation.shape.RoundedCornerShape); + method public void setSpecularIntensity(float); + method public void setStyle(dev.chrisbanes.haze.liquidglass.LiquidGlassStyle); + property public float alpha; + property public float ambientResponse; + property public androidx.compose.ui.unit.Dp blurRadius; + property public float chromaticAberrationStrength; + property public float depth; + property public androidx.compose.ui.unit.Dp edgeSoftness; + property public androidx.compose.ui.geometry.Offset lightPosition; + property public float refractionHeight; + property public float refractionStrength; + property public androidx.compose.foundation.shape.RoundedCornerShape shape; + property public float specularIntensity; + property public dev.chrisbanes.haze.liquidglass.LiquidGlassStyle style; + property public androidx.compose.ui.graphics.Color tint; + field public static final String TAG = "LiquidGlassVisualEffect"; + } + +} + diff --git a/haze-liquidglass/build.gradle.kts b/haze-liquidglass/build.gradle.kts new file mode 100644 index 00000000..c7d6e67f --- /dev/null +++ b/haze-liquidglass/build.gradle.kts @@ -0,0 +1,63 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + + +import dev.chrisbanes.gradle.addDefaultHazeTargets + +plugins { + id("dev.chrisbanes.android.library") + id("dev.chrisbanes.kotlin.multiplatform") + id("dev.chrisbanes.compose") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish") + id("dev.chrisbanes.metalava") +} + +android { + namespace = "dev.chrisbanes.haze.liquidglass" +} + +kotlin { + addDefaultHazeTargets() + explicitApi() + + sourceSets { + commonMain { + dependencies { + api(projects.haze) + implementation(projects.hazeUtils) + implementation(compose.ui) + implementation(compose.foundation) + } + } + + val skikoMain by creating { + dependsOn(commonMain.get()) + } + + iosMain { + dependsOn(skikoMain) + } + + macosMain { + dependsOn(skikoMain) + } + + jvmMain { + dependsOn(skikoMain) + } + + wasmJsMain { + dependsOn(skikoMain) + } + + jsMain { + dependsOn(skikoMain) + } + } + + compilerOptions { + optIn.add("dev.chrisbanes.haze.ExperimentalHazeApi") + optIn.add("dev.chrisbanes.haze.InternalHazeApi") + } +} diff --git a/haze-liquidglass/gradle.properties b/haze-liquidglass/gradle.properties new file mode 100644 index 00000000..232dc7f7 --- /dev/null +++ b/haze-liquidglass/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=haze-liquidglass +POM_NAME=Haze Liquid Glass + diff --git a/haze-liquidglass/src/androidMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.android.kt b/haze-liquidglass/src/androidMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.android.kt new file mode 100644 index 00000000..69e0b2e0 --- /dev/null +++ b/haze-liquidglass/src/androidMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.android.kt @@ -0,0 +1,22 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.ui.graphics.drawscope.DrawScope +import dev.chrisbanes.haze.ExperimentalHazeApi +import dev.chrisbanes.haze.VisualEffectContext +import dev.chrisbanes.haze.isRuntimeShaderRenderEffectSupported + +@OptIn(ExperimentalHazeApi::class) +internal actual fun LiquidGlassVisualEffect.updateDelegate( + context: VisualEffectContext, + drawScope: DrawScope, +) { + val wantsRuntime = isRuntimeShaderRenderEffectSupported() + delegate = when { + wantsRuntime && delegate !is RuntimeShaderLiquidGlassDelegate -> RuntimeShaderLiquidGlassDelegate(this) + !wantsRuntime && delegate !is FallbackLiquidGlassDelegate -> FallbackLiquidGlassDelegate(this) + else -> delegate + } +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/Canvas.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/Canvas.kt new file mode 100644 index 00000000..4f177ba0 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/Canvas.kt @@ -0,0 +1,168 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.isSpecified +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import dev.chrisbanes.haze.ExperimentalHazeApi +import dev.chrisbanes.haze.VisualEffectContext +import dev.chrisbanes.haze.withGraphicsLayer +import kotlin.math.max +import kotlin.math.roundToInt + +@OptIn(ExperimentalHazeApi::class) +internal fun DrawScope.createAndDrawScaledContentLayer( + context: VisualEffectContext, + releaseLayerOnExit: Boolean = true, + block: DrawScope.(GraphicsLayer) -> Unit, +) { + val graphicsContext = context.requireGraphicsContext() + + val effect = context.visualEffect + val scaleFactor = effect.calculateInputScaleFactor(context.inputScale) + val clip = effect.shouldClip() + + val layer = createScaledContentLayer( + context = context, + scaleFactor = scaleFactor, + layerSize = context.layerSize, + layerOffset = context.layerOffset, + backgroundColor = Color.Transparent, + ) + + if (layer != null) { + layer.clip = clip + + drawScaledContent( + offset = -context.layerOffset, + scaledSize = size * scaleFactor, + clip = clip, + ) { + block(layer) + } + + if (releaseLayerOnExit) { + graphicsContext.releaseGraphicsLayer(layer) + } + } +} + +@OptIn(ExperimentalHazeApi::class) +internal fun DrawScope.createScaledContentLayer( + context: VisualEffectContext, + backgroundColor: Color, + scaleFactor: Float, + layerSize: Size, + layerOffset: Offset, +): GraphicsLayer? { + val scaledLayerSize = (layerSize * scaleFactor).roundToIntSize() + + if (scaledLayerSize.width <= 0 || scaledLayerSize.height <= 0) { + return null + } + + val graphicsContext = context.requireGraphicsContext() + val layer = graphicsContext.createGraphicsLayer() + + layer.record(size = scaledLayerSize) { + if (backgroundColor.alpha > 0f) { + drawRect(backgroundColor) + } + + scale(scale = scaleFactor, pivot = Offset.Zero) { + translate(layerOffset - context.position) { + for (area in context.areas) { + val position = Snapshot.withoutReadObservation { area.position } + val resolvedPosition = if (position.isSpecified) position else Offset.Zero + translate(resolvedPosition) { + val areaLayer = area.contentLayer + ?.takeUnless { it.isReleased } + ?.takeUnless { it.size.width <= 0 || it.size.height <= 0 } + + if (areaLayer != null) { + drawLayer(areaLayer) + } + } + } + } + } + } + + return layer +} + +internal fun DrawScope.drawScaledContent( + offset: Offset, + scaledSize: Size, + clip: Boolean = true, + block: DrawScope.() -> Unit, +) { + val scaleFactor = max(size.width / scaledSize.width, size.height / scaledSize.height) + optionalClipRect(enabled = clip) { + translate(offset) { + scale(scale = scaleFactor, pivot = Offset.Zero) { + block() + } + } + } +} + +internal inline fun DrawScope.withAlpha( + alpha: Float, + context: VisualEffectContext, + crossinline block: DrawScope.() -> Unit, +) { + if (alpha < 1f) { + context.withGraphicsLayer { layer -> + layer.alpha = alpha + layer.record { block() } + drawLayer(layer) + } + } else { + block() + } +} + +internal inline fun DrawScope.translate( + offset: Offset, + block: DrawScope.() -> Unit, +) { + if (offset != Offset.Zero) { + translate(offset.x, offset.y, block) + } else { + block() + } +} + +private inline fun DrawScope.optionalClipRect( + enabled: Boolean, + left: Float = 0.0f, + top: Float = 0.0f, + right: Float = size.width, + bottom: Float = size.height, + clipOp: ClipOp = ClipOp.Intersect, + block: DrawScope.() -> Unit, +) = withTransform( + transformBlock = { + if (enabled) { + clipRect(left, top, right, bottom, clipOp) + } + }, + drawBlock = block, +) + +private fun Size.roundToIntSize(): androidx.compose.ui.unit.IntSize { + return androidx.compose.ui.unit.IntSize(width.roundToInt(), height.roundToInt()) +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/CornerRadii.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/CornerRadii.kt new file mode 100644 index 00000000..db8ad3f5 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/CornerRadii.kt @@ -0,0 +1,65 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +internal data class CornerRadii( + val topLeft: Float, + val topRight: Float, + val bottomRight: Float, + val bottomLeft: Float, +) { + fun isZero(): Boolean = + topLeft == 0f && + topRight == 0f && + bottomRight == 0f && + bottomLeft == 0f +} + +internal fun RoundedCornerShape.toCornerRadiiPx( + layerSize: Size, + density: Density, + layoutDirection: LayoutDirection, +): CornerRadii { + val topStartPx = topStart.toPx(layerSize, density) + val topEndPx = topEnd.toPx(layerSize, density) + val bottomEndPx = bottomEnd.toPx(layerSize, density) + val bottomStartPx = bottomStart.toPx(layerSize, density) + + return if (layoutDirection == LayoutDirection.Ltr) { + CornerRadii( + topLeft = topStartPx, + topRight = topEndPx, + bottomRight = bottomEndPx, + bottomLeft = bottomStartPx, + ) + } else { + CornerRadii( + topLeft = topEndPx, + topRight = topStartPx, + bottomRight = bottomStartPx, + bottomLeft = bottomEndPx, + ) + } +} + +internal fun CornerRadii.toRoundRect(size: Size): RoundRect = RoundRect( + left = 0f, + top = 0f, + right = size.width, + bottom = size.height, + topLeftCornerRadius = CornerRadius(topLeft), + topRightCornerRadius = CornerRadius(topRight), + bottomRightCornerRadius = CornerRadius(bottomRight), + bottomLeftCornerRadius = CornerRadius(bottomLeft), +) + +internal fun CornerRadii.toPath(size: Size): Path = Path().apply { addRoundRect(toRoundRect(size)) } diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/FallbackLiquidGlassDelegate.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/FallbackLiquidGlassDelegate.kt new file mode 100644 index 00000000..a5fd50d4 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/FallbackLiquidGlassDelegate.kt @@ -0,0 +1,81 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.platform.LocalLayoutDirection +import dev.chrisbanes.haze.ExperimentalHazeApi +import dev.chrisbanes.haze.VisualEffectContext +import kotlin.math.max + +@OptIn(ExperimentalHazeApi::class) +internal class FallbackLiquidGlassDelegate( + private val effect: LiquidGlassVisualEffect, +) : LiquidGlassVisualEffect.Delegate { + override fun DrawScope.draw(context: VisualEffectContext) { + val tint = effect.tint + if (!tint.isSpecified) return + + val density = context.requireDensity() + val layoutDirection = context.currentValueOf(LocalLayoutDirection) + val edgeSoftnessPx = with(context.requireDensity()) { effect.edgeSoftness.toPx() } + val highlightCenter = effect.lightPosition.takeUnless { it == Offset.Unspecified } + ?: Offset(size.width / 2f, size.height / 3f) + + val radii = effect.shape.toCornerRadiiPx(layerSize = size, density = density, layoutDirection = layoutDirection) + val roundRect = radii.takeUnless { it.isZero() }?.toRoundRect(size) + val shapePath = roundRect?.let { Path().apply { addRoundRect(it) } } + + withAlpha(alpha = effect.alpha, context = context) { + if (shapePath != null) { + clipPath(shapePath) { + drawRect(color = tint) + } + } else { + drawRect(color = tint) + } + + // Specular-ish radial highlight + val highlightBrush = Brush.radialGradient( + colors = listOf(Color.White.copy(alpha = 0.25f), Color.Transparent), + center = highlightCenter, + radius = max(size.minDimension / 2f, edgeSoftnessPx * 4f), + ) + if (shapePath != null) { + clipPath(shapePath) { + drawCircle(brush = highlightBrush, radius = max(size.minDimension / 2f, edgeSoftnessPx * 4f), center = highlightCenter) + } + } else { + drawCircle( + brush = highlightBrush, + center = highlightCenter, + radius = max(size.minDimension / 2f, edgeSoftnessPx * 4f), + ) + } + + // Edge falloff + if (edgeSoftnessPx > 0f) { + val softness = edgeSoftnessPx + val stroke = Stroke(width = softness * 2f) + val edgeBrush = Brush.radialGradient( + colors = listOf(Color.White.copy(alpha = 0.12f), Color.Transparent), + center = Offset(size.width / 2f, size.height / 2f), + radius = size.maxDimension, + ) + if (shapePath != null) { + drawPath(path = shapePath, brush = edgeBrush, style = stroke) + } else { + drawRect(brush = edgeBrush, style = stroke) + } + } + } + } +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/HazeEffectScope.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/HazeEffectScope.kt new file mode 100644 index 00000000..4a063429 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/HazeEffectScope.kt @@ -0,0 +1,17 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import dev.chrisbanes.haze.HazeEffectScope + +/** + * Configures a [LiquidGlassVisualEffect] for this effect scope. + */ +public inline fun HazeEffectScope.liquidGlassEffect( + block: LiquidGlassVisualEffect.() -> Unit = {}, +) { + val effect = visualEffect as? LiquidGlassVisualEffect ?: LiquidGlassVisualEffect() + visualEffect = effect + effect.block() +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassDefaults.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassDefaults.kt new file mode 100644 index 00000000..86cf6196 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassDefaults.kt @@ -0,0 +1,24 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Suppress("ConstPropertyName", "ktlint:standard:property-naming") +public object LiquidGlassDefaults { + // Based on Apple's Liquid Glass and https://kube.io/blog/liquid-glass-css-svg/ + public const val refractionStrength: Float = 0.7f // Typical range: 0.7-1.0 + public const val specularIntensity: Float = 0.4f // Typical range: 0.2-0.5 + public const val depth: Float = 0.4f // Blur/depth mixing + public const val ambientResponse: Float = 0.5f // Fresnel edge glow + public val tint: Color = Color.White.copy(alpha = 0.12f) // Glass tint opacity + public val edgeSoftness: Dp = 12.dp // Smooth edge falloff + public val blurRadius: Dp = 4.dp // Blur radius for glass depth effect + public const val refractionHeight: Float = 0.25f // Fraction of min dimension used for refraction + public const val chromaticAberrationStrength: Float = 0f // 0 = off, 1 = strong dispersion + public val shape: RoundedCornerShape = RoundedCornerShape(0.dp) +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassDirtyFields.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassDirtyFields.kt new file mode 100644 index 00000000..232b82be --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassDirtyFields.kt @@ -0,0 +1,59 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import dev.chrisbanes.haze.Bitmask +import dev.chrisbanes.haze.InternalHazeApi + +@Suppress("ConstPropertyName", "ktlint:standard:property-naming") +@OptIn(InternalHazeApi::class) +internal object LiquidGlassDirtyFields { + const val RefractionStrength: Int = 0b1 + const val SpecularIntensity: Int = RefractionStrength shl 1 + const val Depth: Int = SpecularIntensity shl 1 + const val AmbientResponse: Int = Depth shl 1 + const val Tint: Int = AmbientResponse shl 1 + const val EdgeSoftness: Int = Tint shl 1 + const val LightPosition: Int = EdgeSoftness shl 1 + const val BlurRadius: Int = LightPosition shl 1 + const val RefractionHeight: Int = BlurRadius shl 1 + const val ChromaticAberration: Int = RefractionHeight shl 1 + const val Shape: Int = ChromaticAberration shl 1 + const val Alpha: Int = Shape shl 1 + const val Style: Int = Alpha shl 1 + + const val InvalidateFlags: Int = + RefractionStrength or + SpecularIntensity or + Depth or + AmbientResponse or + Tint or + EdgeSoftness or + LightPosition or + BlurRadius or + RefractionHeight or + ChromaticAberration or + Shape or + Alpha or + Style + + fun stringify(dirtyTracker: Bitmask): String { + val params = buildList { + if (RefractionStrength in dirtyTracker) add("RefractionStrength") + if (SpecularIntensity in dirtyTracker) add("SpecularIntensity") + if (Depth in dirtyTracker) add("Depth") + if (AmbientResponse in dirtyTracker) add("AmbientResponse") + if (Tint in dirtyTracker) add("Tint") + if (EdgeSoftness in dirtyTracker) add("EdgeSoftness") + if (LightPosition in dirtyTracker) add("LightPosition") + if (BlurRadius in dirtyTracker) add("BlurRadius") + if (RefractionHeight in dirtyTracker) add("RefractionHeight") + if (ChromaticAberration in dirtyTracker) add("ChromaticAberration") + if (Shape in dirtyTracker) add("Shape") + if (Alpha in dirtyTracker) add("Alpha") + if (Style in dirtyTracker) add("Style") + } + return params.joinToString(separator = ", ", prefix = "[", postfix = "]") + } +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassShaders.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassShaders.kt new file mode 100644 index 00000000..0e2114a3 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassShaders.kt @@ -0,0 +1,172 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +internal object LiquidGlassShaders { + /** + * SKSL that simulates a layered glass effect with refraction, specular highlights, + * Fresnel-based ambient response, and soft edges. + */ + val LIQUID_GLASS_SKSL: String by lazy(mode = LazyThreadSafetyMode.NONE) { + """ + uniform shader content; + uniform shader blurredContent; + uniform float2 layerSize; + uniform float refractionStrength; + uniform float specularIntensity; + uniform float depth; + uniform float ambientResponse; + uniform float edgeSoftness; + uniform float refractionHeight; + uniform float chromaticAberrationStrength; + uniform float2 lightPosition; + uniform vec4 cornerRadii; // topLeft, topRight, bottomRight, bottomLeft + uniform vec4 tintColor; + + float luma(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); } + + vec2 clampCoord(vec2 coord) { + return clamp(coord, vec2(0.5, 0.5), layerSize - vec2(0.5, 0.5)); + } + + float circleMap(float x) { + return 1.0 - sqrt(max(0.0, 1.0 - x * x)); + } + + float distanceToRoundedRect(vec2 coord) { + float width = layerSize.x; + float height = layerSize.y; + + float tl = cornerRadii.x; + float tr = cornerRadii.y; + float br = cornerRadii.z; + float bl = cornerRadii.w; + + if (coord.x < tl && coord.y < tl) { + vec2 c = vec2(tl, tl); + return max(tl - length(coord - c), 0.0); + } + if (coord.x > width - tr && coord.y < tr) { + vec2 c = vec2(width - tr, tr); + return max(tr - length(coord - c), 0.0); + } + if (coord.x > width - br && coord.y > height - br) { + vec2 c = vec2(width - br, height - br); + return max(br - length(coord - c), 0.0); + } + if (coord.x < bl && coord.y > height - bl) { + vec2 c = vec2(bl, height - bl); + return max(bl - length(coord - c), 0.0); + } + + float distLeft = coord.x; + float distRight = width - coord.x; + float distTop = coord.y; + float distBottom = height - coord.y; + return max(min(min(distLeft, distRight), min(distTop, distBottom)), 0.0); + } + + vec2 inwardNormal(vec2 coord) { + float width = layerSize.x; + float height = layerSize.y; + + float tl = cornerRadii.x; + float tr = cornerRadii.y; + float br = cornerRadii.z; + float bl = cornerRadii.w; + + if (coord.x < tl && coord.y < tl) { + vec2 c = vec2(tl, tl); + vec2 dir = c - coord; + return normalize(dir + vec2(1e-4)); + } + if (coord.x > width - tr && coord.y < tr) { + vec2 c = vec2(width - tr, tr); + vec2 dir = c - coord; + return normalize(dir + vec2(1e-4)); + } + if (coord.x > width - br && coord.y > height - br) { + vec2 c = vec2(width - br, height - br); + vec2 dir = c - coord; + return normalize(dir + vec2(1e-4)); + } + if (coord.x < bl && coord.y > height - bl) { + vec2 c = vec2(bl, height - bl); + vec2 dir = c - coord; + return normalize(dir + vec2(1e-4)); + } + + float dx = (width - 2.0 * coord.x); + float dy = (height - 2.0 * coord.y); + vec2 n = vec2(dx, dy); + if (dot(n, n) < 1e-8) n = vec2(0.0, 1.0); + return normalize(n); + } + + vec3 computeContentNormal(vec2 coord) { + float l = luma(content.eval(clampCoord(coord + vec2(1.0, 0.0))).rgb); + float r = luma(content.eval(clampCoord(coord - vec2(1.0, 0.0))).rgb); + float t = luma(content.eval(clampCoord(coord + vec2(0.0, 1.0))).rgb); + float b = luma(content.eval(clampCoord(coord - vec2(0.0, 1.0))).rgb); + vec2 grad = vec2(r - l, b - t); + return normalize(vec3(grad, 1.0)); + } + + vec3 computeNormal(vec2 coord, vec2 shapeNormal2D) { + vec3 shapeNormal = normalize(vec3(shapeNormal2D, 0.5)); + vec3 contentNormal = computeContentNormal(coord); + return normalize(mix(shapeNormal, contentNormal, 0.15)); + } + + float edgeMask(vec2 coord, float distToEdge) { + if (edgeSoftness <= 0.0) return 1.0; + float e = distToEdge / max(edgeSoftness, 0.0001); + return clamp(e, 0.0, 1.0); + } + + vec4 sampleChroma(vec2 coord, vec2 chromaOffset) { + if (chromaticAberrationStrength <= 0.0001) return content.eval(clampCoord(coord)); + vec2 forward = clampCoord(coord + chromaOffset); + vec2 backward = clampCoord(coord - chromaOffset); + vec4 base = content.eval(clampCoord(coord)); + return vec4(content.eval(forward).r, base.g, content.eval(backward).b, base.a); + } + + vec4 main(vec2 coord) { + vec4 base = content.eval(coord); + + float distToEdge = distanceToRoundedRect(coord); + vec2 normal2D = inwardNormal(coord); + + float refractionZone = max(refractionHeight, 0.0001); + float normalizedDist = clamp(distToEdge / refractionZone, 0.0, 1.0); + float displacementMagnitude = distToEdge >= refractionZone ? 0.0 : circleMap(1.0 - normalizedDist); + vec2 displacement = normal2D * displacementMagnitude * refractionZone * refractionStrength; + + vec2 refractCoord = clampCoord(coord + displacement); + + vec2 chromaOffset = displacement * chromaticAberrationStrength * 0.5; + vec4 refracted = sampleChroma(refractCoord, chromaOffset); + vec4 blurred = blurredContent.eval(refractCoord); + + vec3 normal = computeNormal(coord, normal2D); + + vec3 mixedColor = mix(base.rgb, blurred.rgb, clamp(depth, 0.0, 1.0)); + vec3 tinted = mix(mixedColor, tintColor.rgb, tintColor.a); + + vec2 lightDir2D = normalize(lightPosition - coord); + vec3 lightDir = normalize(vec3(lightDir2D, 1.0)); + float spec = pow(max(dot(normal, lightDir), 0.0), 24.0) * specularIntensity; + + float fresnel = pow(1.0 - max(dot(normal, vec3(0.0, 0.0, 1.0)), 0.0), 3.0); + float ambient = mix(1.0, 1.0 + fresnel, clamp(ambientResponse, 0.0, 1.0)); + + vec3 finalColor = mix(tinted, refracted.rgb, refractionStrength) * ambient + spec; + + float edge = edgeMask(coord, distToEdge); + return vec4(finalColor, base.a) * edge; + } + """ + } +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassStyle.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassStyle.kt new file mode 100644 index 00000000..8ffbf036 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassStyle.kt @@ -0,0 +1,30 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Dp.Companion.Unspecified + +@Immutable +public data class LiquidGlassStyle( + val tint: Color = Color.Unspecified, + val refractionStrength: Float = Float.NaN, + val specularIntensity: Float = Float.NaN, + val depth: Float = Float.NaN, + val ambientResponse: Float = Float.NaN, + val edgeSoftness: Dp = Dp.Unspecified, + val lightPosition: Offset = Offset.Unspecified, + val blurRadius: Dp = Dp.Unspecified, + val refractionHeight: Float = Float.NaN, + val chromaticAberrationStrength: Float = Float.NaN, + val shape: RoundedCornerShape? = null, +) { + public companion object { + public val Unspecified: LiquidGlassStyle = LiquidGlassStyle() + } +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.kt new file mode 100644 index 00000000..a55a9f88 --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.kt @@ -0,0 +1,327 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.takeOrElse +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.takeOrElse +import dev.chrisbanes.haze.Bitmask +import dev.chrisbanes.haze.ExperimentalHazeApi +import dev.chrisbanes.haze.HazeInputScale +import dev.chrisbanes.haze.HazeLogger +import dev.chrisbanes.haze.VisualEffect +import dev.chrisbanes.haze.VisualEffectContext + +/** + * A [VisualEffect] implementation that simulates the iOS-style liquid glass look: + * refraction, depth layering, specular highlights, and soft tinted glass. + */ +@OptIn(ExperimentalHazeApi::class) +public class LiquidGlassVisualEffect : VisualEffect { + + internal var dirtyTracker: Bitmask by mutableStateOf(Bitmask()) + private set + + internal var delegate: Delegate = FallbackLiquidGlassDelegate(this) + set(value) { + if (value != field) { + HazeLogger.d(TAG) { "delegate changed. Current $field. New: $value" } + value.attach() + field.detach() + field = value + } + } + + override fun update(context: VisualEffectContext) { + if (dirtyTracker.any(LiquidGlassDirtyFields.InvalidateFlags)) { + context.invalidateDraw() + } + } + + override fun DrawScope.draw(context: VisualEffectContext) { + updateDelegate(context, this) + + try { + with(delegate) { draw(context) } + } finally { + resetDirtyTracker() + } + } + + override fun shouldClip(): Boolean = edgeSoftness > 0.dp || !shape.hasZeroCornerRadii() + + override fun calculateInputScaleFactor(scale: HazeInputScale): Float = when (scale) { + is HazeInputScale.None -> 1f + is HazeInputScale.Fixed -> scale.scale + HazeInputScale.Auto -> 0.75f + } + + override fun requireInvalidation(): Boolean = dirtyTracker.any(LiquidGlassDirtyFields.InvalidateFlags) + + override fun calculateLayerBounds(rect: Rect, density: Density): Rect { + val softnessPx = with(density) { edgeSoftness.toPx() } + return if (softnessPx > 0f) rect.inflate(softnessPx) else rect + } + + /** + * Strength of refractive distortion, in the range `0f..1f`. + */ + public var refractionStrength: Float = Float.NaN + get() { + return field.takeOrElse { style.refractionStrength } + .takeOrElse { LiquidGlassDefaults.refractionStrength } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.RefractionStrength + } + } + + /** + * Intensity of specular highlights, in the range `0f..1f`. + */ + public var specularIntensity: Float = Float.NaN + get() { + return field.takeOrElse { style.specularIntensity } + .takeOrElse { LiquidGlassDefaults.specularIntensity } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.SpecularIntensity + } + } + + /** + * Depth perception factor (0 = flat, 1 = deep layered glass). + */ + public var depth: Float = Float.NaN + get() { + return field.takeOrElse { style.depth } + .takeOrElse { LiquidGlassDefaults.depth } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.Depth + } + } + + /** + * Strength of ambient lighting response and Fresnel accent. + */ + public var ambientResponse: Float = Float.NaN + get() { + return field.takeOrElse { style.ambientResponse } + .takeOrElse { LiquidGlassDefaults.ambientResponse } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.AmbientResponse + } + } + + /** + * Glass tint applied to the refracted content. + */ + public var tint: Color = Color.Unspecified + get() { + return field.takeOrElse { style.tint } + .takeOrElse { LiquidGlassDefaults.tint } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.Tint + } + } + + /** + * Softening distance for glass edges. + */ + public var edgeSoftness: Dp = Dp.Unspecified + get() { + return field.takeOrElse { style.edgeSoftness } + .takeOrElse { LiquidGlassDefaults.edgeSoftness } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.EdgeSoftness + } + } + + /** + * Position of the virtual light source. When unspecified, the center of the layer is used. + */ + public var lightPosition: Offset = Offset.Unspecified + get() { + return field.takeOrElse { style.lightPosition } + .takeOrElse { Offset.Unspecified } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.LightPosition + } + } + + /** + * Radius of the blur applied to create depth effect. + */ + public var blurRadius: Dp = Dp.Unspecified + get() { + return field.takeOrElse { style.blurRadius } + .takeOrElse { LiquidGlassDefaults.blurRadius } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.BlurRadius + } + } + + /** + * Height of the refraction zone expressed as a fraction of the smallest dimension (0f..1f). + */ + public var refractionHeight: Float = Float.NaN + get() { + return field.takeOrElse { style.refractionHeight } + .takeOrElse { LiquidGlassDefaults.refractionHeight } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.RefractionHeight + } + } + + /** + * Strength of chromatic aberration. TODO: expand to configurable channel/spread controls. + */ + public var chromaticAberrationStrength: Float = Float.NaN + get() { + return field.takeOrElse { style.chromaticAberrationStrength } + .takeOrElse { LiquidGlassDefaults.chromaticAberrationStrength } + } + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.ChromaticAberration + } + } + + /** + * Shape applied to the glass. Defaults to a rectangle (all radii zero). + */ + private var _shape: RoundedCornerShape? = null + + public var shape: RoundedCornerShape + get() = _shape ?: style.shape ?: LiquidGlassDefaults.shape + set(value) { + if (value != _shape) { + _shape = value + dirtyTracker += LiquidGlassDirtyFields.Shape + } + } + + /** + * Overall opacity for the effect. + */ + public var alpha: Float = 1f + set(value) { + if (value != field) { + field = value + dirtyTracker += LiquidGlassDirtyFields.Alpha + } + } + + /** + * Optional style container that can set multiple parameters at once. + */ + public var style: LiquidGlassStyle = LiquidGlassStyle.Unspecified + set(value) { + if (field != value) { + onStyleChanged(old = field, new = value) + field = value + dirtyTracker += LiquidGlassDirtyFields.Style + } + } + + override fun preferClipToAreaBounds(): Boolean = edgeSoftness <= 0.dp && shape.hasZeroCornerRadii() + + internal interface Delegate { + fun attach() = Unit + fun DrawScope.draw(context: VisualEffectContext) + fun detach() = Unit + } + + private fun resetDirtyTracker() { + dirtyTracker = Bitmask() + } + + private fun onStyleChanged(old: LiquidGlassStyle, new: LiquidGlassStyle) { + if (old.refractionStrength != new.refractionStrength) { + dirtyTracker += LiquidGlassDirtyFields.RefractionStrength + } + if (old.specularIntensity != new.specularIntensity) { + dirtyTracker += LiquidGlassDirtyFields.SpecularIntensity + } + if (old.depth != new.depth) { + dirtyTracker += LiquidGlassDirtyFields.Depth + } + if (old.ambientResponse != new.ambientResponse) { + dirtyTracker += LiquidGlassDirtyFields.AmbientResponse + } + if (old.tint != new.tint) { + dirtyTracker += LiquidGlassDirtyFields.Tint + } + if (old.edgeSoftness != new.edgeSoftness) { + dirtyTracker += LiquidGlassDirtyFields.EdgeSoftness + } + if (old.lightPosition != new.lightPosition) { + dirtyTracker += LiquidGlassDirtyFields.LightPosition + } + if (old.blurRadius != new.blurRadius) { + dirtyTracker += LiquidGlassDirtyFields.BlurRadius + } + if (old.refractionHeight != new.refractionHeight) { + dirtyTracker += LiquidGlassDirtyFields.RefractionHeight + } + if (old.chromaticAberrationStrength != new.chromaticAberrationStrength) { + dirtyTracker += LiquidGlassDirtyFields.ChromaticAberration + } + if (old.shape != new.shape) { + dirtyTracker += LiquidGlassDirtyFields.Shape + } + } + + internal companion object { + const val TAG = "LiquidGlassVisualEffect" + } +} + +internal expect fun LiquidGlassVisualEffect.updateDelegate( + context: VisualEffectContext, + drawScope: DrawScope, +) + +private fun RoundedCornerShape.hasZeroCornerRadii(): Boolean = this == LiquidGlassDefaults.shape + +private inline fun Float.takeOrElse(default: () -> Float): Float { + return if (this.isNaN()) default() else this +} diff --git a/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/RuntimeShaderLiquidGlassDelegate.kt b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/RuntimeShaderLiquidGlassDelegate.kt new file mode 100644 index 00000000..b17476be --- /dev/null +++ b/haze-liquidglass/src/commonMain/kotlin/dev/chrisbanes/haze/liquidglass/RuntimeShaderLiquidGlassDelegate.kt @@ -0,0 +1,129 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.geometry.takeOrElse +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import dev.chrisbanes.haze.ExperimentalHazeApi +import dev.chrisbanes.haze.InternalHazeApi +import dev.chrisbanes.haze.VisualEffectContext +import dev.chrisbanes.haze.asComposeRenderEffect +import dev.chrisbanes.haze.createBlurRenderEffect +import dev.chrisbanes.haze.createRuntimeEffect +import dev.chrisbanes.haze.createRuntimeShaderRenderEffect + +@OptIn(ExperimentalHazeApi::class, InternalHazeApi::class) +internal class RuntimeShaderLiquidGlassDelegate( + private val effect: LiquidGlassVisualEffect, +) : LiquidGlassVisualEffect.Delegate { + private var renderEffect: RenderEffect? = null + private var lastParams: RenderParams? = null + + override fun DrawScope.draw(context: VisualEffectContext) { + val density = context.requireDensity() + val layoutDirection = context.currentValueOf(LocalLayoutDirection) + val scaleFactor = effect.calculateInputScaleFactor(context.inputScale) + val layerSize = context.layerSize * scaleFactor + createAndDrawScaledContentLayer(context) { + val layerRadii = effect.shape.toCornerRadiiPx( + layerSize = layerSize, + density = density, + layoutDirection = layoutDirection, + ) + val params = RenderParams( + layerSize = layerSize, + refractionStrength = effect.refractionStrength.coerceIn(0f, 1f), + specularIntensity = effect.specularIntensity.coerceIn(0f, 1f), + depth = effect.depth.coerceIn(0f, 1f), + ambientResponse = effect.ambientResponse.coerceIn(0f, 1f), + tint = effect.tint, + edgeSoftnessPx = with(density) { effect.edgeSoftness.toPx() }, + blurRadiusPx = with(density) { effect.blurRadius.toPx() }, + refractionHeightPx = effect.refractionHeight.coerceIn(0f, 1f) * layerSize.minDimension, + chromaticAberrationStrength = effect.chromaticAberrationStrength.coerceIn(0f, 1f), + cornerRadii = layerRadii, + lightPosition = effect.lightPosition.takeOrElse { + context.layerSize.center * scaleFactor + }, + ) + + if (params != lastParams || renderEffect == null) { + renderEffect = buildRenderEffect(params) + lastParams = params + } + + it.renderEffect = renderEffect + it.alpha = effect.alpha + drawLayer(it) + } + } + + private fun buildRenderEffect(params: RenderParams): RenderEffect { + // Create blur effect for the blurred content input + val blurEffect = createBlurRenderEffect( + radiusX = params.blurRadiusPx, + radiusY = params.blurRadiusPx, + tileMode = TileMode.Clamp, + ) + + return createRuntimeShaderRenderEffect( + effect = LIQUID_GLASS_RUNTIME_EFFECT, + shaderNames = arrayOf("content", "blurredContent"), + inputs = arrayOf(null, blurEffect), + ) { + setFloatUniform("layerSize", params.layerSize.width, params.layerSize.height) + setFloatUniform("refractionStrength", params.refractionStrength) + setFloatUniform("specularIntensity", params.specularIntensity) + setFloatUniform("depth", params.depth) + setFloatUniform("ambientResponse", params.ambientResponse) + setFloatUniform("edgeSoftness", params.edgeSoftnessPx) + setFloatUniform("refractionHeight", params.refractionHeightPx) + setFloatUniform("chromaticAberrationStrength", params.chromaticAberrationStrength) + setFloatUniform( + "cornerRadii", + params.cornerRadii.topLeft, + params.cornerRadii.topRight, + params.cornerRadii.bottomRight, + params.cornerRadii.bottomLeft, + ) + setFloatUniform("lightPosition", params.lightPosition.x, params.lightPosition.y) + setFloatUniform( + "tintColor", + params.tint.red, + params.tint.green, + params.tint.blue, + params.tint.alpha, + ) + }.asComposeRenderEffect() + } + + private data class RenderParams( + val layerSize: Size, + val refractionStrength: Float, + val specularIntensity: Float, + val depth: Float, + val ambientResponse: Float, + val tint: Color, + val edgeSoftnessPx: Float, + val blurRadiusPx: Float, + val refractionHeightPx: Float, + val chromaticAberrationStrength: Float, + val cornerRadii: CornerRadii, + val lightPosition: Offset, + ) + + private companion object { + val LIQUID_GLASS_RUNTIME_EFFECT by lazy(LazyThreadSafetyMode.NONE) { + createRuntimeEffect(LiquidGlassShaders.LIQUID_GLASS_SKSL) + } + } +} diff --git a/haze-liquidglass/src/skikoMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.skiko.kt b/haze-liquidglass/src/skikoMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.skiko.kt new file mode 100644 index 00000000..69e0b2e0 --- /dev/null +++ b/haze-liquidglass/src/skikoMain/kotlin/dev/chrisbanes/haze/liquidglass/LiquidGlassVisualEffect.skiko.kt @@ -0,0 +1,22 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.liquidglass + +import androidx.compose.ui.graphics.drawscope.DrawScope +import dev.chrisbanes.haze.ExperimentalHazeApi +import dev.chrisbanes.haze.VisualEffectContext +import dev.chrisbanes.haze.isRuntimeShaderRenderEffectSupported + +@OptIn(ExperimentalHazeApi::class) +internal actual fun LiquidGlassVisualEffect.updateDelegate( + context: VisualEffectContext, + drawScope: DrawScope, +) { + val wantsRuntime = isRuntimeShaderRenderEffectSupported() + delegate = when { + wantsRuntime && delegate !is RuntimeShaderLiquidGlassDelegate -> RuntimeShaderLiquidGlassDelegate(this) + !wantsRuntime && delegate !is FallbackLiquidGlassDelegate -> FallbackLiquidGlassDelegate(this) + else -> delegate + } +} diff --git a/haze-screenshot-tests/build.gradle.kts b/haze-screenshot-tests/build.gradle.kts index 9eefa432..69cfb6fb 100644 --- a/haze-screenshot-tests/build.gradle.kts +++ b/haze-screenshot-tests/build.gradle.kts @@ -41,6 +41,7 @@ kotlin { commonMain { dependencies { api(projects.hazeBlur) + api(projects.hazeLiquidglass) api(compose.foundation) api(compose.material3) api(compose.components.resources) diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard.png new file mode 100644 index 00000000..12cd7b38 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard[28].png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard[32].png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_100.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_100.png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_100.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_40.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_40.png new file mode 100644 index 00000000..c573da48 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_40.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_70.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_70.png new file mode 100644 index 00000000..20af1d3c Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[28]_70.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_100.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_100.png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_100.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_40.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_40.png new file mode 100644 index 00000000..c573da48 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_40.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_70.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_70.png new file mode 100644 index 00000000..20af1d3c Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha[32]_70.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_100.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_100.png new file mode 100644 index 00000000..0cae32f1 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_100.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_40.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_40.png new file mode 100644 index 00000000..ca7b76f3 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_40.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_70.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_70.png new file mode 100644 index 00000000..0477e365 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_alpha_70.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_blue.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_blue.png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_blue.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_magenta.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_magenta.png new file mode 100644 index 00000000..de5924eb Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_magenta.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_orange.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_orange.png new file mode 100644 index 00000000..c79249ee Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[28]_orange.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_blue.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_blue.png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_blue.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_magenta.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_magenta.png new file mode 100644 index 00000000..de5924eb Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_magenta.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_orange.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_orange.png new file mode 100644 index 00000000..c79249ee Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange[32]_orange.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_blue.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_blue.png new file mode 100644 index 00000000..f077a8d2 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_blue.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_magenta.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_magenta.png new file mode 100644 index 00000000..cf89f52c Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_magenta.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_orange.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_orange.png new file mode 100644 index 00000000..17666c3b Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_orange.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic.png new file mode 100644 index 00000000..0ddb18e1 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic[28].png new file mode 100644 index 00000000..e526a194 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic[32].png new file mode 100644 index 00000000..e526a194 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions.png new file mode 100644 index 00000000..98bd27e3 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions[28].png new file mode 100644 index 00000000..ffca0968 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions[32].png new file mode 100644 index 00000000..ffca0968 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_chromatic_companions[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind.png new file mode 100644 index 00000000..65f033b0 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind[28].png new file mode 100644 index 00000000..275b9562 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind[32].png new file mode 100644 index 00000000..275b9562 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_bottomRight.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_bottomRight.png new file mode 100644 index 00000000..607c47b6 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_center.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_center.png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_center.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_topLeft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_topLeft.png new file mode 100644 index 00000000..516fd2ce Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[28]_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_bottomRight.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_bottomRight.png new file mode 100644 index 00000000..607c47b6 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_center.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_center.png new file mode 100644 index 00000000..076c9a9d Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_center.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_topLeft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_topLeft.png new file mode 100644 index 00000000..516fd2ce Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition[32]_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_bottomRight.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_bottomRight.png new file mode 100644 index 00000000..b0a54db0 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_center.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_center.png new file mode 100644 index 00000000..19f66336 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_center.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_topLeft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_topLeft.png new file mode 100644 index 00000000..81de2100 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_lightPosition_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint.png new file mode 100644 index 00000000..184f2348 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint[28].png new file mode 100644 index 00000000..9ba8e7ec Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint[32].png new file mode 100644 index 00000000..9ba8e7ec Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_noTint[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[28]_rounded.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[28]_rounded.png new file mode 100644 index 00000000..541b820a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[28]_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[28]_shallow.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[28]_shallow.png new file mode 100644 index 00000000..541b820a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[28]_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[32]_rounded.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[32]_rounded.png new file mode 100644 index 00000000..541b820a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[32]_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[32]_shallow.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[32]_shallow.png new file mode 100644 index 00000000..541b820a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight[32]_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_rounded.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_rounded.png new file mode 100644 index 00000000..0d34bb5f Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_shallow.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_shallow.png new file mode 100644 index 00000000..44e9bfc0 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style.png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style.png new file mode 100644 index 00000000..5268fb64 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style[28].png new file mode 100644 index 00000000..f7330e38 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style[32].png new file mode 100644 index 00000000..f7330e38 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassContentScreenshotTest.creditCard_style[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard.png new file mode 100644 index 00000000..5fcfa0de Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard[28].png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard[32].png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha.png new file mode 100644 index 00000000..55b95b14 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28].png new file mode 100644 index 00000000..dc5be480 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28]_15.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28]_15.png new file mode 100644 index 00000000..9154184e Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28]_15.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28]_45.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28]_45.png new file mode 100644 index 00000000..c7b7238a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[28]_45.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32].png new file mode 100644 index 00000000..dc5be480 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32]_15.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32]_15.png new file mode 100644 index 00000000..9154184e Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32]_15.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32]_45.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32]_45.png new file mode 100644 index 00000000..c7b7238a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha[32]_45.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha_15.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha_15.png new file mode 100644 index 00000000..b4b510f7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha_15.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha_45.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha_45.png new file mode 100644 index 00000000..4a083d78 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_alpha_45.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[28]_off.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[28]_off.png new file mode 100644 index 00000000..cfe75fca Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[28]_off.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[28]_on.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[28]_on.png new file mode 100644 index 00000000..cfe75fca Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[28]_on.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[32]_off.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[32]_off.png new file mode 100644 index 00000000..cfe75fca Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[32]_off.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[32]_on.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[32]_on.png new file mode 100644 index 00000000..cfe75fca Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration[32]_on.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration_off.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration_off.png new file mode 100644 index 00000000..eb4b5876 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration_off.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration_on.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration_on.png new file mode 100644 index 00000000..00308955 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_chromaticAberration_on.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions.png new file mode 100644 index 00000000..b1b71a21 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions[28].png new file mode 100644 index 00000000..74f4c7ef Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions[32].png new file mode 100644 index 00000000..74f4c7ef Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_companions[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_disabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_disabled.png new file mode 100644 index 00000000..05f2d676 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_disabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_enabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_enabled.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_re_enabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_re_enabled.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[28]_re_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_disabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_disabled.png new file mode 100644 index 00000000..05f2d676 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_disabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_enabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_enabled.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_re_enabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_re_enabled.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled[32]_re_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_disabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_disabled.png new file mode 100644 index 00000000..3ed2c7aa Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_disabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_enabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_enabled.png new file mode 100644 index 00000000..5fcfa0de Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_re_enabled.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_re_enabled.png new file mode 100644 index 00000000..5fcfa0de Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_conditional_enabled_re_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[28]_sharp.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[28]_sharp.png new file mode 100644 index 00000000..ff68a9fd Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[28]_sharp.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[28]_soft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[28]_soft.png new file mode 100644 index 00000000..170a9b46 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[28]_soft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[32]_sharp.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[32]_sharp.png new file mode 100644 index 00000000..ff68a9fd Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[32]_sharp.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[32]_soft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[32]_soft.png new file mode 100644 index 00000000..170a9b46 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness[32]_soft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness_sharp.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness_sharp.png new file mode 100644 index 00000000..ef636fb5 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness_sharp.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness_soft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness_soft.png new file mode 100644 index 00000000..d0e1be36 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_edgeSoftness_soft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_bottomRight.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_bottomRight.png new file mode 100644 index 00000000..0f21c727 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_center.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_center.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_center.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_topLeft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_topLeft.png new file mode 100644 index 00000000..3a193baf Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[28]_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_bottomRight.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_bottomRight.png new file mode 100644 index 00000000..0f21c727 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_center.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_center.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_center.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_topLeft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_topLeft.png new file mode 100644 index 00000000..3a193baf Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition[32]_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_bottomRight.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_bottomRight.png new file mode 100644 index 00000000..ba775681 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_center.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_center.png new file mode 100644 index 00000000..0d352026 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_center.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_topLeft.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_topLeft.png new file mode 100644 index 00000000..37be729a Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_lightPosition_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple.png new file mode 100644 index 00000000..4ea27618 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple[28].png new file mode 100644 index 00000000..f0a831b7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple[32].png new file mode 100644 index 00000000..f0a831b7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_multiple[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint.png new file mode 100644 index 00000000..2db314f5 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint[28].png new file mode 100644 index 00000000..bd854acf Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint[32].png new file mode 100644 index 00000000..bd854acf Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_noTint[32].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[28]_high.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[28]_high.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[28]_high.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[28]_low.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[28]_low.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[28]_low.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[32]_high.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[32]_high.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[32]_high.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[32]_low.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[32]_low.png new file mode 100644 index 00000000..cca10366 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth[32]_low.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth_high.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth_high.png new file mode 100644 index 00000000..58b63b18 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth_high.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth_low.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth_low.png new file mode 100644 index 00000000..7f878b34 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_refraction_depth_low.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[28]_rounded.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[28]_rounded.png new file mode 100644 index 00000000..7333ebe7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[28]_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[28]_shallow.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[28]_shallow.png new file mode 100644 index 00000000..7333ebe7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[28]_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[32]_rounded.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[32]_rounded.png new file mode 100644 index 00000000..7333ebe7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[32]_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[32]_shallow.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[32]_shallow.png new file mode 100644 index 00000000..7333ebe7 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight[32]_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_rounded.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_rounded.png new file mode 100644 index 00000000..459b84c9 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_shallow.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_shallow.png new file mode 100644 index 00000000..964e0b0c Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style.png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style.png new file mode 100644 index 00000000..33192549 Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style.png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style[28].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style[28].png new file mode 100644 index 00000000..c6ccdc9b Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style[28].png differ diff --git a/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style[32].png b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style[32].png new file mode 100644 index 00000000..c6ccdc9b Binary files /dev/null and b/haze-screenshot-tests/screenshots/android/LiquidGlassScreenshotTest.creditCard_style[32].png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard.png new file mode 100644 index 00000000..43c5cb67 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_100.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_100.png new file mode 100644 index 00000000..a576c16b Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_100.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_40.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_40.png new file mode 100644 index 00000000..3ee56216 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_40.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_70.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_70.png new file mode 100644 index 00000000..0c9cf829 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_alpha_70.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_blue.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_blue.png new file mode 100644 index 00000000..43c5cb67 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_blue.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_magenta.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_magenta.png new file mode 100644 index 00000000..ae908f82 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_magenta.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_orange.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_orange.png new file mode 100644 index 00000000..10a0cbab Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_backgroundChange_orange.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_chromatic.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_chromatic.png new file mode 100644 index 00000000..b5514214 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_chromatic.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind.png new file mode 100644 index 00000000..2748a9c1 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_drawContentBehind.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_bottomRight.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_bottomRight.png new file mode 100644 index 00000000..7642e5f5 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_center.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_center.png new file mode 100644 index 00000000..0f00135d Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_center.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_topLeft.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_topLeft.png new file mode 100644 index 00000000..3c6d9d94 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_lightPosition_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_noTint.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_noTint.png new file mode 100644 index 00000000..f2126c73 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_noTint.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_rounded.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_rounded.png new file mode 100644 index 00000000..0eb9d9e4 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_shallow.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_shallow.png new file mode 100644 index 00000000..5ea420a9 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_shape_refractionHeight_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_style.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_style.png new file mode 100644 index 00000000..76643dd4 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassContentScreenshotTest.creditCard_style.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard.png new file mode 100644 index 00000000..e369d3f9 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha.png new file mode 100644 index 00000000..4750f4f3 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha_15.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha_15.png new file mode 100644 index 00000000..a04cbeaf Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha_15.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha_45.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha_45.png new file mode 100644 index 00000000..304442c9 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_alpha_45.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_chromaticAberration_off.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_chromaticAberration_off.png new file mode 100644 index 00000000..c255a664 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_chromaticAberration_off.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_chromaticAberration_on.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_chromaticAberration_on.png new file mode 100644 index 00000000..c255a664 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_chromaticAberration_on.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_disabled.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_disabled.png new file mode 100644 index 00000000..b16e76af Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_disabled.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_enabled.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_enabled.png new file mode 100644 index 00000000..e369d3f9 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_re_enabled.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_re_enabled.png new file mode 100644 index 00000000..e369d3f9 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_conditional_enabled_re_enabled.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_edgeSoftness_sharp.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_edgeSoftness_sharp.png new file mode 100644 index 00000000..494fffc5 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_edgeSoftness_sharp.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_edgeSoftness_soft.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_edgeSoftness_soft.png new file mode 100644 index 00000000..494569af Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_edgeSoftness_soft.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_bottomRight.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_bottomRight.png new file mode 100644 index 00000000..711d2d26 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_bottomRight.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_center.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_center.png new file mode 100644 index 00000000..f44a13c5 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_center.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_topLeft.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_topLeft.png new file mode 100644 index 00000000..f592536e Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_lightPosition_topLeft.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_multiple.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_multiple.png new file mode 100644 index 00000000..f8645a2a Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_multiple.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_noTint.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_noTint.png new file mode 100644 index 00000000..1f690ea6 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_noTint.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_refraction_depth_high.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_refraction_depth_high.png new file mode 100644 index 00000000..c316412e Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_refraction_depth_high.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_refraction_depth_low.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_refraction_depth_low.png new file mode 100644 index 00000000..a13bfcc8 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_refraction_depth_low.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_rounded.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_rounded.png new file mode 100644 index 00000000..904e49d1 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_rounded.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_shallow.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_shallow.png new file mode 100644 index 00000000..8d56bf7a Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_shape_refractionHeight_shallow.png differ diff --git a/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_style.png b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_style.png new file mode 100644 index 00000000..f5433d50 Binary files /dev/null and b/haze-screenshot-tests/screenshots/desktop/LiquidGlassScreenshotTest.creditCard_style.png differ diff --git a/haze-screenshot-tests/src/commonMain/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt b/haze-screenshot-tests/src/commonMain/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt index 77b9d603..d89f666c 100644 --- a/haze-screenshot-tests/src/commonMain/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt +++ b/haze-screenshot-tests/src/commonMain/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import dev.chrisbanes.haze.blur.BlurVisualEffect import haze_root.haze_screenshot_tests.generated.resources.Res import haze_root.haze_screenshot_tests.generated.resources.photo import kotlin.math.roundToInt @@ -42,7 +41,7 @@ import org.jetbrains.compose.resources.painterResource @Composable internal fun CreditCardSample( - visualEffect: BlurVisualEffect, + visualEffect: VisualEffect, backgroundColors: List = listOf(Color.Blue, Color.Cyan), shape: RoundedCornerShape = RoundedCornerShape(16.dp), enabled: Boolean = true, @@ -79,7 +78,7 @@ internal fun CreditCardSample( @Composable internal fun CreditCardContentBlurring( - visualEffect: BlurVisualEffect, + visualEffect: VisualEffect, backgroundColors: List = listOf(Color.Blue, Color.Cyan), drawContentBehind: Boolean = false, ) { @@ -99,7 +98,7 @@ internal fun CreditCardContentBlurring( @Composable internal fun CreditCardPagerSample( - visualEffect: BlurVisualEffect, + visualEffect: VisualEffect, pagerPosition: Float, backgroundColors: List = listOf(Color.Blue, Color.Cyan), shape: RoundedCornerShape = RoundedCornerShape(16.dp), @@ -167,7 +166,7 @@ private fun CreditCard( index: Int, shape: RoundedCornerShape, enabled: Boolean, - visualEffect: BlurVisualEffect, + visualEffect: VisualEffect, modifier: Modifier = Modifier, baseWidth: Float = .7f, ) { @@ -197,7 +196,7 @@ private fun CreditCard( @Composable fun OverlayingContent( - visualEffect: BlurVisualEffect, + visualEffect: VisualEffect, topOffset: DpOffset = DpOffset.Zero, ) { val hazeState = rememberHazeState() @@ -254,7 +253,7 @@ fun OverlayingContent( } @Composable -fun ContentAtEdges(visualEffect: BlurVisualEffect) { +fun ContentAtEdges(visualEffect: VisualEffect) { val hazeState = rememberHazeState() Box(modifier = Modifier.fillMaxSize()) { diff --git a/haze-screenshot-tests/src/commonTest/kotlin/dev/chrisbanes/haze/LiquidGlassContentScreenshotTest.kt b/haze-screenshot-tests/src/commonTest/kotlin/dev/chrisbanes/haze/LiquidGlassContentScreenshotTest.kt new file mode 100644 index 00000000..945230c4 --- /dev/null +++ b/haze-screenshot-tests/src/commonTest/kotlin/dev/chrisbanes/haze/LiquidGlassContentScreenshotTest.kt @@ -0,0 +1,217 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.liquidglass.LiquidGlassStyle +import dev.chrisbanes.haze.liquidglass.LiquidGlassVisualEffect +import dev.chrisbanes.haze.test.ScreenshotTest +import dev.chrisbanes.haze.test.ScreenshotTheme +import dev.chrisbanes.haze.test.runScreenshotTest +import kotlin.test.Test + +class LiquidGlassContentScreenshotTest : ScreenshotTest() { + + @Test + fun creditCard() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + } + setContent { + ScreenshotTheme { + CreditCardContentBlurring(visualEffect = visualEffect) + } + } + captureRoot() + } + + @Test + fun creditCard_noTint() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = Color.Transparent + } + setContent { + ScreenshotTheme { + CreditCardContentBlurring(visualEffect = visualEffect) + } + } + captureRoot() + } + + @Test + fun creditCard_style() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + style = VibrantStyle + } + setContent { + ScreenshotTheme { + CreditCardContentBlurring(visualEffect = visualEffect) + } + } + captureRoot() + } + + @Test + fun creditCard_drawContentBehind() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + edgeSoftness = 16.dp + } + setContent { + ScreenshotTheme { + CreditCardContentBlurring( + visualEffect = visualEffect, + drawContentBehind = true, + ) + } + } + captureRoot() + } + + @Test + fun creditCard_alpha() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + alpha = 0.7f + refractionStrength = 0.45f + } + setContent { + ScreenshotTheme { + CreditCardContentBlurring(visualEffect = visualEffect) + } + } + + captureRoot("70") + + visualEffect.alpha = 0.4f + waitForIdle() + captureRoot("40") + + visualEffect.alpha = 1f + waitForIdle() + captureRoot("100") + } + + @Test + fun creditCard_lightPosition() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + specularIntensity = 0.65f + } + setContent { + ScreenshotTheme { + CreditCardContentBlurring(visualEffect = visualEffect) + } + } + + captureRoot("center") + + visualEffect.lightPosition = Offset(-96f, -64f) + waitForIdle() + captureRoot("topLeft") + + visualEffect.lightPosition = Offset(120f, 80f) + waitForIdle() + captureRoot("bottomRight") + } + + @Test + fun creditCard_backgroundChange() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + edgeSoftness = 12.dp + } + var backgroundColors by mutableStateOf(listOf(Color.Blue, Color.Cyan)) + + setContent { + ScreenshotTheme { + CreditCardContentBlurring( + visualEffect = visualEffect, + backgroundColors = backgroundColors, + ) + } + } + + captureRoot("blue") + + backgroundColors = listOf(Color.Magenta, Color(0xFF7CF7C8)) + waitForIdle() + captureRoot("magenta") + + backgroundColors = listOf(Color(0xFFFBA045), Color(0xFFF25555)) + waitForIdle() + captureRoot("orange") + } + + @Test + fun creditCard_shape_refractionHeight() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + refractionStrength = 0.7f + refractionHeight = 0.3f + depth = 0.45f + specularIntensity = 0.6f + edgeSoftness = 10.dp + shape = androidx.compose.foundation.shape.RoundedCornerShape(22.dp) + } + + setContent { + ScreenshotTheme { + CreditCardContentBlurring( + visualEffect = visualEffect, + backgroundColors = listOf(Color(0xFF1E88E5), Color(0xFF00ACC1)), + ) + } + } + + captureRoot("rounded") + + visualEffect.refractionHeight = 0.16f + waitForIdle() + captureRoot("shallow") + } + + @Test + fun creditCard_chromatic() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + refractionStrength = 0.8f + chromaticAberrationStrength = 0.22f + depth = 0.5f + ambientResponse = 0.7f + edgeSoftness = 14.dp + shape = androidx.compose.foundation.shape.RoundedCornerShape(20.dp) + } + + setContent { + ScreenshotTheme { + CreditCardContentBlurring( + visualEffect = visualEffect, + backgroundColors = listOf(Color(0xFF7E57C2), Color(0xFF26C6DA)), + ) + } + } + + captureRoot() + } + + companion object { + val DefaultTint = Color.White.copy(alpha = 0.1f) + + val VibrantStyle = LiquidGlassStyle( + tint = Color(0xFF49E1FF).copy(alpha = 0.35f), + refractionStrength = 0.5f, + specularIntensity = 0.75f, + depth = 0.35f, + ambientResponse = 0.75f, + edgeSoftness = 12.dp, + lightPosition = Offset(48f, -32f), + ) + } +} diff --git a/haze-screenshot-tests/src/commonTest/kotlin/dev/chrisbanes/haze/LiquidGlassScreenshotTest.kt b/haze-screenshot-tests/src/commonTest/kotlin/dev/chrisbanes/haze/LiquidGlassScreenshotTest.kt new file mode 100644 index 00000000..f16772ee --- /dev/null +++ b/haze-screenshot-tests/src/commonTest/kotlin/dev/chrisbanes/haze/LiquidGlassScreenshotTest.kt @@ -0,0 +1,263 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.liquidglass.LiquidGlassStyle +import dev.chrisbanes.haze.liquidglass.LiquidGlassVisualEffect +import dev.chrisbanes.haze.test.ScreenshotTest +import dev.chrisbanes.haze.test.ScreenshotTheme +import dev.chrisbanes.haze.test.runScreenshotTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LiquidGlassScreenshotTest : ScreenshotTest() { + + @BeforeTest + fun before() { + HazeLogger.enabled = true + } + + @Test + fun creditCard() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + captureRoot() + } + + @Test + fun creditCard_noTint() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = Color.Transparent + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + captureRoot() + } + + @Test + fun creditCard_multiple() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + refractionStrength = 0.45f + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect, numberCards = 3) + } + } + captureRoot() + } + + @Test + fun creditCard_style() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + style = VibrantStyle + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + captureRoot() + } + + @Test + fun creditCard_alpha() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + alpha = 0.85f + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + + captureRoot() + + visualEffect.alpha = 0.45f + waitForIdle() + captureRoot("45") + + visualEffect.alpha = 0.15f + waitForIdle() + captureRoot("15") + } + + @Test + fun creditCard_edgeSoftness() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + edgeSoftness = 0.dp + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + + captureRoot("sharp") + + visualEffect.edgeSoftness = 18.dp + waitForIdle() + captureRoot("soft") + } + + @Test + fun creditCard_refraction_depth() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + refractionStrength = 0.25f + depth = 0.15f + specularIntensity = 0.35f + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + + captureRoot("low") + + visualEffect.refractionStrength = 0.6f + visualEffect.depth = 0.5f + visualEffect.specularIntensity = 0.7f + waitForIdle() + captureRoot("high") + } + + @Test + fun creditCard_lightPosition() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + lightPosition = Offset.Unspecified + specularIntensity = 0.55f + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect) + } + } + + captureRoot("center") + + visualEffect.lightPosition = Offset(-120f, -80f) + waitForIdle() + captureRoot("topLeft") + + visualEffect.lightPosition = Offset(140f, 120f) + waitForIdle() + captureRoot("bottomRight") + } + + @Test + fun creditCard_conditional_enabled() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + } + var enabled by mutableStateOf(true) + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect, enabled = enabled) + } + } + + captureRoot("enabled") + + enabled = false + waitForIdle() + captureRoot("disabled") + + enabled = true + waitForIdle() + captureRoot("re_enabled") + } + + @Test + fun creditCard_shape_refractionHeight() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + refractionStrength = 0.7f + refractionHeight = 0.32f + depth = 0.45f + specularIntensity = 0.6f + shape = RoundedCornerShape(24.dp) + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect, shape = RoundedCornerShape(24.dp)) + } + } + + captureRoot("rounded") + + visualEffect.refractionHeight = 0.18f + waitForIdle() + captureRoot("shallow") + } + + @Test + fun creditCard_chromaticAberration() = runScreenshotTest { + val visualEffect = LiquidGlassVisualEffect().apply { + tint = DefaultTint + refractionStrength = 0.8f + chromaticAberrationStrength = 0.0f + depth = 0.4f + edgeSoftness = 14.dp + shape = RoundedCornerShape(20.dp) + } + + setContent { + ScreenshotTheme { + CreditCardSample(visualEffect = visualEffect, shape = RoundedCornerShape(20.dp)) + } + } + + captureRoot("off") + + visualEffect.chromaticAberrationStrength = 0.24f + waitForIdle() + captureRoot("on") + } + + companion object { + val DefaultTint = Color.White.copy(alpha = 0.1f) + + val VibrantStyle = LiquidGlassStyle( + tint = Color(0xFF3F8CFF).copy(alpha = 0.35f), + refractionStrength = 0.55f, + specularIntensity = 0.75f, + depth = 0.4f, + ambientResponse = 0.8f, + edgeSoftness = 14.dp, + lightPosition = Offset(64f, -48f), + ) + } +} diff --git a/haze-utils/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt b/haze-utils/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt index ca1ab552..c6e7be3d 100644 --- a/haze-utils/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt +++ b/haze-utils/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt @@ -73,6 +73,7 @@ public actual fun createBlurRenderEffect( } return try { + // On Android we use the native blur effect directly for better performance val blurEffect = RenderEffect.createBlurEffect(radiusX, radiusY, edgeTreatment) if (input != null) { RenderEffect.createChainEffect(blurEffect, input) diff --git a/kotlin-js-store/package-lock.json b/kotlin-js-store/package-lock.json index 88047fe8..882fc494 100644 --- a/kotlin-js-store/package-lock.json +++ b/kotlin-js-store/package-lock.json @@ -12,6 +12,8 @@ "packages/haze-root-haze-test", "packages/haze-root-haze-blur", "packages/haze-root-haze-blur-test", + "packages/haze-root-haze-liquidglass", + "packages/haze-root-haze-liquidglass-test", "packages/haze-root-haze-materials", "packages/haze-root-haze-materials-test", "packages/haze-root-haze-utils", @@ -2972,6 +2974,14 @@ "resolved": "packages/haze-root-haze-blur-test", "link": true }, + "node_modules/haze-root-haze-liquidglass": { + "resolved": "packages/haze-root-haze-liquidglass", + "link": true + }, + "node_modules/haze-root-haze-liquidglass-test": { + "resolved": "packages/haze-root-haze-liquidglass-test", + "link": true + }, "node_modules/haze-root-haze-materials": { "resolved": "packages/haze-root-haze-materials", "link": true @@ -6940,6 +6950,25 @@ "version": "0.0.0-unspecified", "devDependencies": {} }, + "packages/haze-root-haze-liquidglass": { + "version": "0.0.0-unspecified", + "devDependencies": {} + }, + "packages/haze-root-haze-liquidglass-test": { + "version": "0.0.0-unspecified", + "devDependencies": { + "karma": "6.4.4", + "karma-chrome-launcher": "3.2.0", + "karma-mocha": "2.0.1", + "karma-sourcemap-loader": "0.4.0", + "karma-webpack": "5.0.1", + "kotlin-web-helpers": "2.1.0", + "mocha": "11.7.1", + "source-map-loader": "5.0.0", + "webpack": "5.100.2", + "webpack-cli": "6.0.1" + } + }, "packages/haze-root-haze-materials": { "version": "0.0.0-unspecified", "dependencies": { diff --git a/kotlin-js-store/wasm/package-lock.json b/kotlin-js-store/wasm/package-lock.json index a1dba620..d2b75778 100644 --- a/kotlin-js-store/wasm/package-lock.json +++ b/kotlin-js-store/wasm/package-lock.json @@ -12,6 +12,8 @@ "packages/haze-root-haze-test", "packages/haze-root-haze-blur", "packages/haze-root-haze-blur-test", + "packages/haze-root-haze-liquidglass", + "packages/haze-root-haze-liquidglass-test", "packages/haze-root-haze-materials", "packages/haze-root-haze-materials-test", "packages/haze-root-haze-utils", @@ -77,6 +79,14 @@ "resolved": "packages/haze-root-haze-blur-test", "link": true }, + "node_modules/haze-root-haze-liquidglass": { + "resolved": "packages/haze-root-haze-liquidglass", + "link": true + }, + "node_modules/haze-root-haze-liquidglass-test": { + "resolved": "packages/haze-root-haze-liquidglass-test", + "link": true + }, "node_modules/haze-root-haze-materials": { "resolved": "packages/haze-root-haze-materials", "link": true @@ -303,6 +313,14 @@ "version": "0.0.0-unspecified", "devDependencies": {} }, + "packages/haze-root-haze-liquidglass": { + "version": "0.0.0-unspecified", + "devDependencies": {} + }, + "packages/haze-root-haze-liquidglass-test": { + "version": "0.0.0-unspecified", + "devDependencies": {} + }, "packages/haze-root-haze-materials": { "version": "0.0.0-unspecified", "dependencies": { diff --git a/mkdocs.yml b/mkdocs.yml index 30ee6002..e6fbf5ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,8 @@ nav: - "Materials": blur/materials.md - "FAQs": blur/faq.md - "Platforms": blur/platforms.md + - "Effects": + - "Liquid Glass": effects/liquid-glass.md - "Custom Effects": custom-effects.md - "Performance": performance.md - "Recipes": recipes.md diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index b5b90841..c1179db4 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { dependencies { api(projects.haze) api(projects.hazeBlur) + api(projects.hazeLiquidglass) api(projects.hazeMaterials) api(libs.androidx.navigation.compose) diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/LiquidGlassDebugSample.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/LiquidGlassDebugSample.kt new file mode 100644 index 00000000..bd199b15 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/LiquidGlassDebugSample.kt @@ -0,0 +1,198 @@ +// Copyright 2025, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.sample + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.liquidglass.liquidGlassEffect + +@Composable +fun LiquidGlassDebugSample(navController: NavHostController) { + val hazeState = remember { HazeState() } + + Box(modifier = Modifier.fillMaxSize()) { + // Background + Box( + modifier = Modifier + .fillMaxSize() + .hazeSource(state = hazeState) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFF1E88E5), // Bright blue + Color(0xFF00ACC1), // Cyan + ), + ), + ), + ) { + // High contrast text pattern to make refraction visible + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + .align(Alignment.BottomCenter), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + repeat(20) { i -> + Text( + text = "█████ Line ${i + 1} █████████████████", + color = Color.White.copy(alpha = 0.3f), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ) + } + } + } + + // Test cards - each with different settings + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "Check console for delegate info. If shader works, you should see OBVIOUS warping on Card 2.", + color = Color.White, + fontSize = 14.sp, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Card 1: Just tint (baseline) + TestCard( + hazeState = hazeState, + title = "1. Tint Only", + modifier = Modifier.weight(1f), + ) { + tint = Color.White.copy(alpha = 0.2f) + refractionStrength = 0f + specularIntensity = 0f + ambientResponse = 0f + depth = 0f + } + + // Card 2: Tint + Refraction + TestCard( + hazeState = hazeState, + title = "2. + Refraction", + modifier = Modifier.weight(1f), + ) { + tint = Color.White.copy(alpha = 0.2f) + refractionStrength = 0.8f // Strong to make it obvious + specularIntensity = 0f + ambientResponse = 0f + depth = 0f + } + + // Card 3: + Depth/Blur + TestCard( + hazeState = hazeState, + title = "3. + Blur", + modifier = Modifier.weight(1f), + ) { + tint = Color.White.copy(alpha = 0.2f) + refractionStrength = 0.8f + specularIntensity = 0f + ambientResponse = 0f + depth = 0.6f // Enable depth mixing + } + + // Card 4: Full effect + TestCard( + hazeState = hazeState, + title = "4. Full", + modifier = Modifier.weight(1f), + ) { + tint = Color.White.copy(alpha = 0.15f) + refractionStrength = 0.8f + specularIntensity = 0.8f + ambientResponse = 0.8f + depth = 0.6f + edgeSoftness = 16.dp + refractionHeight = 0.28f + shape = RoundedCornerShape(16.dp) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + TestCard( + hazeState = hazeState, + title = "5. Rounded + Chroma", + modifier = Modifier.weight(1f), + ) { + tint = Color.White.copy(alpha = 0.18f) + refractionStrength = 0.85f + specularIntensity = 0.75f + ambientResponse = 0.7f + depth = 0.55f + edgeSoftness = 14.dp + shape = RoundedCornerShape(24.dp) + refractionHeight = 0.35f + chromaticAberrationStrength = 0.22f + } + } + } + } +} + +@Composable +private fun TestCard( + hazeState: HazeState, + title: String, + modifier: Modifier = Modifier, + config: dev.chrisbanes.haze.liquidglass.LiquidGlassVisualEffect.() -> Unit, +) { + Card( + modifier = modifier + .size(width = 120.dp, height = 160.dp) + .clip(RoundedCornerShape(12.dp)) + .hazeEffect(state = hazeState) { + liquidGlassEffect(config) + }, + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = title, + color = Color.Black, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + ) + } + } +} diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/LiquidGlassSample.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/LiquidGlassSample.kt new file mode 100644 index 00000000..fb2e3121 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/LiquidGlassSample.kt @@ -0,0 +1,130 @@ +// Copyright 2023, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.sample + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.liquidglass.liquidGlassEffect +import dev.chrisbanes.haze.rememberHazeState + +@Composable +fun LiquidGlassCreditCardSample(navController: NavHostController) { + val hazeState = rememberHazeState() + + Box { + // Background content + Box( + modifier = Modifier + .fillMaxSize() + .hazeSource(state = hazeState), + ) { + Spacer( + Modifier + .fillMaxSize() + .background(brush = Brush.linearGradient(colors = listOf(Color.Black, Color.DarkGray))), + ) + + Text( + text = LorumIspum, + color = Color.White.copy(alpha = 0.2f), + modifier = Modifier.padding(24.dp), + ) + } + + // Card 1 + + repeat(3) { index -> + // Our card + val reverseIndex = (2 - index) + val cardOffset = remember { mutableFloatStateOf(0f) } + val draggableState = rememberDraggableState { cardOffset.value += it } + + val shape = RoundedCornerShape(16.dp) + + Box( + modifier = Modifier + .testTag("credit_card_$index") + .fillMaxWidth(0.7f - (reverseIndex * 0.05f)) + .aspectRatio(16 / 9f) + .align(Alignment.Center) + .offset { IntOffset(x = 0, y = reverseIndex * -100) } + .offset { IntOffset(x = 0, y = cardOffset.value.toInt()) } + .draggable( + state = draggableState, + orientation = Orientation.Vertical, + onDragStopped = { velocity -> + animate( + initialValue = cardOffset.value, + targetValue = 0f, + initialVelocity = velocity, + animationSpec = spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow), + ) { value, _ -> + cardOffset.value = value + } + }, + ) + // We add 1 to the zIndex as the background content is zIndex 0f + .hazeSource(hazeState, zIndex = 1f + index) + .clip(shape) + .hazeEffect(state = hazeState) { + liquidGlassEffect { + this.shape = shape + } + }, + ) { + Column(Modifier.padding(32.dp)) { + Text("Bank of Haze") + } + } + } + + FloatingActionButton( + onClick = navController::navigateUp, + modifier = Modifier + .windowInsetsPadding(WindowInsets.statusBars) + .padding(24.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + } +} diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt index 9734a0d8..69c6b14b 100644 --- a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt @@ -72,6 +72,8 @@ val CommonSamples: List = listOf( Sample.BottomSheet, Sample.ContentBlurring, Sample.LayerTransformations, + Sample.LiquidGlass, + Sample.LiquidGlassDebug, ) @OptIn(ExperimentalHazeApi::class) @@ -272,6 +274,26 @@ interface Sample { // We should seal this interface, but KMP doesn't support it LayerTransformations(blurEnabled = blurEnabled) } } + + @Serializable + data object LiquidGlass : Sample { + override val title: String = "Liquid Glass" + + @Composable + override fun Content(navController: NavHostController, blurEnabled: Boolean) { + LiquidGlassCreditCardSample(navController = navController) + } + } + + @Serializable + data object LiquidGlassDebug : Sample { + override val title: String = "Liquid Glass (Debug)" + + @Composable + override fun Content(navController: NavHostController, blurEnabled: Boolean) { + LiquidGlassDebugSample(navController = navController) + } + } } @Composable diff --git a/settings.gradle.kts b/settings.gradle.kts index b71cd8ca..19001756 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -58,6 +58,7 @@ rootProject.name = "haze-root" include( ":haze", ":haze-blur", + ":haze-liquidglass", ":haze-utils", ":haze-materials", ":haze-screenshot-tests",