diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 6be2029..e84a113 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,6 +12,7 @@
+
diff --git a/backdrop/src/main/java/com/kyant/backdrop/SimpleColorFilter.kt b/backdrop/src/main/java/com/kyant/backdrop/SimpleColorFilter.kt
new file mode 100644
index 0000000..f6fdc85
--- /dev/null
+++ b/backdrop/src/main/java/com/kyant/backdrop/SimpleColorFilter.kt
@@ -0,0 +1,37 @@
+package com.kyant.backdrop
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ColorMatrix
+import androidx.compose.ui.graphics.ColorMatrixColorFilter
+
+@Stable
+fun simpleColorFilter(
+ brightness: Float = 0f,
+ contrast: Float = 1f,
+ saturation: Float = 1f
+): ColorFilter {
+ val invSat = 1f - saturation
+ val r = 0.213f * invSat
+ val g = 0.715f * invSat
+ val b = 0.072f * invSat
+
+ val c = contrast
+ val t = (0.5f - c * 0.5f + brightness) * 255f
+ val s = saturation
+
+ val cr = c * r
+ val cg = c * g
+ val cb = c * b
+ val cs = c * s
+
+ val colorMatrix = ColorMatrix(
+ floatArrayOf(
+ cr + cs, cg, cb, 0f, t,
+ cr, cg + cs, cb, 0f, t,
+ cr, cg, cb + cs, 0f, t,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ )
+ return ColorMatrixColorFilter(colorMatrix)
+}
diff --git a/blur/.gitignore b/blur/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/blur/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/blur/build.gradle.kts b/blur/build.gradle.kts
new file mode 100644
index 0000000..f014139
--- /dev/null
+++ b/blur/build.gradle.kts
@@ -0,0 +1,48 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ id("dev.shreyaspatil.compose-compiler-report-generator")
+}
+
+android {
+ namespace = "com.kyant.blur"
+ compileSdk = 36
+ buildToolsVersion = "36.0.0"
+
+ defaultConfig {
+ minSdk = 21
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_21
+ freeCompilerArgs.addAll(
+ "-jvm-default=no-compatibility",
+ )
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ api(project(":backdrop"))
+ implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+}
diff --git a/blur/consumer-rules.pro b/blur/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/blur/proguard-rules.pro b/blur/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/blur/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/blur/src/main/AndroidManifest.xml b/blur/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/blur/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/blur/src/main/java/com/kyant/blur/BlurMaterial.kt b/blur/src/main/java/com/kyant/blur/BlurMaterial.kt
new file mode 100644
index 0000000..648b2b2
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/BlurMaterial.kt
@@ -0,0 +1,30 @@
+package com.kyant.blur
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import com.kyant.backdrop.simpleColorFilter
+
+@Immutable
+data class BlurMaterial(
+ val colorFilter: ColorFilter? = DefaultColorFilter,
+ val brush: Brush? = null,
+ val alpha: Float = 1f,
+ val blendMode: BlendMode = DrawScope.DefaultBlendMode
+) {
+
+ companion object {
+
+ @Stable
+ val DefaultColorFilter: ColorFilter = simpleColorFilter(saturation = 1.5f)
+
+ @Stable
+ val Default: BlurMaterial = BlurMaterial()
+
+ @Stable
+ val None: BlurMaterial = BlurMaterial(colorFilter = null)
+ }
+}
diff --git a/blur/src/main/java/com/kyant/blur/BlurModifier.kt b/blur/src/main/java/com/kyant/blur/BlurModifier.kt
new file mode 100644
index 0000000..77a4470
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/BlurModifier.kt
@@ -0,0 +1,377 @@
+package com.kyant.blur
+
+import android.graphics.RenderEffect
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.CacheDrawModifierNode
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.asAndroidColorFilter
+import androidx.compose.ui.graphics.asComposeRenderEffect
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.graphics.toAndroidTileMode
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.GlobalPositionAwareModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ObserverModifierNode
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateLayer
+import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import com.kyant.backdrop.Backdrop
+import com.kyant.backdrop.BackdropDrawScope
+import com.kyant.backdrop.SimpleBackdropDrawScope
+
+private val DefaultOnDrawBackdrop: BackdropDrawScope.() -> Unit = { drawBackdrop() }
+
+fun Modifier.blur(
+ backdrop: Backdrop,
+ style: BlurStyle,
+ compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
+ onDrawBackdrop: BackdropDrawScope.() -> Unit = DefaultOnDrawBackdrop
+): Modifier =
+ this
+ .then(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ SimpleBlurElement(
+ backdrop = backdrop,
+ style = style,
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ } else {
+ Modifier
+ }
+ )
+
+fun Modifier.blur(
+ backdrop: Backdrop,
+ compositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
+ onDrawBackdrop: BackdropDrawScope.() -> Unit = DefaultOnDrawBackdrop,
+ style: () -> BlurStyle
+): Modifier =
+ this
+ .then(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ BlurElement(
+ backdrop = backdrop,
+ style = style,
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ } else {
+ Modifier
+ }
+ )
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class SimpleBlurElement(
+ val backdrop: Backdrop,
+ val style: BlurStyle,
+ val compositingStrategy: CompositingStrategy,
+ val onDrawBackdrop: BackdropDrawScope.() -> Unit
+) : ModifierNodeElement() {
+
+ override fun create(): SimpleBlurNode {
+ return SimpleBlurNode(
+ backdrop = backdrop,
+ style = style,
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ }
+
+ override fun update(node: SimpleBlurNode) {
+ node.update(
+ backdrop = backdrop,
+ style = style,
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "blur"
+ properties["backdrop"] = backdrop
+ properties["style"] = style
+ properties["compositingStrategy"] = compositingStrategy
+ properties["onDrawBackdrop"] = onDrawBackdrop
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SimpleBlurElement) return false
+
+ if (backdrop != other.backdrop) return false
+ if (style != other.style) return false
+ if (compositingStrategy != other.compositingStrategy) return false
+ if (onDrawBackdrop != other.onDrawBackdrop) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = backdrop.hashCode()
+ result = 31 * result + style.hashCode()
+ result = 31 * result + compositingStrategy.hashCode()
+ result = 31 * result + onDrawBackdrop.hashCode()
+ return result
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class BlurElement(
+ val backdrop: Backdrop,
+ val style: () -> BlurStyle,
+ val compositingStrategy: CompositingStrategy,
+ val onDrawBackdrop: BackdropDrawScope.() -> Unit
+) : ModifierNodeElement() {
+
+ override fun create(): BlurNode {
+ return BlurNode(
+ backdrop = backdrop,
+ style = style,
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ }
+
+ override fun update(node: BlurNode) {
+ node.update(
+ backdrop = backdrop,
+ style = style,
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "blur"
+ properties["backdrop"] = backdrop
+ properties["style"] = style
+ properties["compositingStrategy"] = compositingStrategy
+ properties["onDrawBackdrop"] = onDrawBackdrop
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BlurElement) return false
+
+ if (backdrop != other.backdrop) return false
+ if (style != other.style) return false
+ if (compositingStrategy != other.compositingStrategy) return false
+ if (onDrawBackdrop != other.onDrawBackdrop) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = backdrop.hashCode()
+ result = 31 * result + style.hashCode()
+ result = 31 * result + compositingStrategy.hashCode()
+ result = 31 * result + onDrawBackdrop.hashCode()
+ return result
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class SimpleBlurNode(
+ var backdrop: Backdrop,
+ var style: BlurStyle,
+ var compositingStrategy: CompositingStrategy,
+ var onDrawBackdrop: BackdropDrawScope.() -> Unit
+) : LayoutModifierNode, GlobalPositionAwareModifierNode, DelegatingNode() {
+
+ override val shouldAutoInvalidate: Boolean = false
+
+ private var graphicsLayer: GraphicsLayer? = null
+
+ private var drawBackdropBlock: DrawScope.() -> Unit = {}
+
+ private val drawNode = delegate(CacheDrawModifierNode {
+ val style = style
+
+ val colorFilter = style.material.colorFilter
+ val colorFilterEffect =
+ if (colorFilter != null) {
+ RenderEffect.createColorFilterEffect(colorFilter.asAndroidColorFilter())
+ } else {
+ null
+ }
+
+ val blurRadiusPx = style.blurRadius.toPx()
+ val blurRenderEffect =
+ if (blurRadiusPx > 0f) {
+ val tileMode = style.tileMode.toAndroidTileMode()
+ if (colorFilterEffect != null) {
+ RenderEffect.createBlurEffect(
+ blurRadiusPx,
+ blurRadiusPx,
+ colorFilterEffect,
+ tileMode
+ )
+ } else {
+ RenderEffect.createBlurEffect(
+ blurRadiusPx,
+ blurRadiusPx,
+ tileMode
+ )
+ }
+ } else {
+ colorFilterEffect
+ }
+
+ val renderEffect = blurRenderEffect?.asComposeRenderEffect()
+
+ val graphicsLayer = graphicsLayer
+ graphicsLayer?.renderEffect = renderEffect
+
+ onDrawWithContent {
+ if (graphicsLayer != null) {
+ graphicsLayer.record {
+ val backdropDrawScope = SimpleBackdropDrawScope(this, drawBackdropBlock)
+ onDrawBackdrop(backdropDrawScope)
+ }
+
+ drawLayer(graphicsLayer)
+ }
+
+ style.material.brush?.let { brush ->
+ drawRect(
+ brush = brush,
+ alpha = style.material.alpha,
+ blendMode = style.material.blendMode
+ )
+ }
+
+ drawContent()
+ }
+ })
+
+ private val layerBlock: GraphicsLayerScope.() -> Unit = {
+ clip = true
+ shape = style.shape
+ compositingStrategy = this@SimpleBlurNode.compositingStrategy
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+
+ return layout(placeable.width, placeable.height) {
+ placeable.placeWithLayer(IntOffset.Zero, layerBlock = layerBlock)
+ }
+ }
+
+ override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+ if (coordinates.isAttached) {
+ drawBackdropBlock = { with(backdrop) { drawChild(coordinates) } }
+ drawNode.invalidateDraw()
+ }
+ }
+
+ override fun onAttach() {
+ val graphicsContext = requireGraphicsContext()
+ graphicsLayer =
+ graphicsContext.createGraphicsLayer().apply {
+ compositingStrategy = androidx.compose.ui.graphics.layer.CompositingStrategy.Offscreen
+ }
+ }
+
+ override fun onDetach() {
+ val graphicsContext = requireGraphicsContext()
+ graphicsLayer?.let { layer ->
+ graphicsContext.releaseGraphicsLayer(layer)
+ graphicsLayer = null
+ }
+ }
+
+ fun update(
+ backdrop: Backdrop,
+ style: BlurStyle,
+ compositingStrategy: CompositingStrategy,
+ onDrawBackdrop: BackdropDrawScope.() -> Unit
+ ) {
+ if (this.style != style ||
+ this.compositingStrategy != compositingStrategy
+ ) {
+ this.style = style
+ this.compositingStrategy = compositingStrategy
+ invalidateLayer()
+ }
+ if (this.backdrop != backdrop ||
+ this.onDrawBackdrop != onDrawBackdrop
+ ) {
+ this.backdrop = backdrop
+ this.onDrawBackdrop = onDrawBackdrop
+ drawNode.invalidateDrawCache()
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class BlurNode(
+ var backdrop: Backdrop,
+ var style: () -> BlurStyle,
+ var compositingStrategy: CompositingStrategy,
+ var onDrawBackdrop: BackdropDrawScope.() -> Unit
+) : ObserverModifierNode, DelegatingNode() {
+
+ override val shouldAutoInvalidate: Boolean = false
+
+ private val glassNode = delegate(
+ SimpleBlurNode(
+ backdrop = backdrop,
+ style = style(),
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ )
+
+ override fun onObservedReadsChanged() {
+ updateStyle()
+ }
+
+ fun update(
+ backdrop: Backdrop,
+ style: () -> BlurStyle,
+ compositingStrategy: CompositingStrategy,
+ onDrawBackdrop: BackdropDrawScope.() -> Unit
+ ) {
+ if (this.backdrop != backdrop ||
+ this.style != style ||
+ this.compositingStrategy != compositingStrategy ||
+ this.onDrawBackdrop != onDrawBackdrop
+ ) {
+ this.backdrop = backdrop
+ this.style = style
+ this.compositingStrategy = compositingStrategy
+ this.onDrawBackdrop = onDrawBackdrop
+ updateStyle()
+ }
+ }
+
+ private fun updateStyle() {
+ observeReads {
+ glassNode.update(
+ backdrop = backdrop,
+ style = style(),
+ compositingStrategy = compositingStrategy,
+ onDrawBackdrop = onDrawBackdrop
+ )
+ }
+ }
+}
diff --git a/blur/src/main/java/com/kyant/blur/BlurShaders.kt b/blur/src/main/java/com/kyant/blur/BlurShaders.kt
new file mode 100644
index 0000000..51815db
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/BlurShaders.kt
@@ -0,0 +1,38 @@
+package com.kyant.blur
+
+import org.intellij.lang.annotations.Language
+
+@Suppress("ConstPropertyName")
+internal object BlurShaders {
+
+ @Language("AGSL")
+ const val dynamicHighlightStyleShaderString = """
+uniform shader image;
+
+uniform float2 size;
+
+uniform float angle;
+uniform float falloff;
+
+float2 gradSdRoundedRectangle(float2 coord, float2 halfSize, float cornerRadius) {
+ float2 innerHalfSize = halfSize - float2(cornerRadius);
+ float2 cornerCoord = abs(coord) - innerHalfSize;
+
+ float insideCorner = step(0.0, min(cornerCoord.x, cornerCoord.y)); // 1 if in corner
+ float xMajor = step(cornerCoord.y, cornerCoord.x); // 1 if x is major
+ float2 gradEdge = float2(xMajor, 1.0 - xMajor);
+ float2 gradCorner = normalize(cornerCoord);
+ return sign(coord) * mix(gradEdge, gradCorner, insideCorner);
+}
+
+half4 main(float2 coord) {
+ float2 halfSize = size * 0.5;
+ float2 centeredCoord = coord - halfSize;
+
+ float2 grad = gradSdRoundedRectangle(centeredCoord, halfSize, min(halfSize.x, halfSize.y));
+ float2 normal = float2(-cos(angle), -sin(angle));
+ float intensity = pow(abs(dot(normal, grad)), falloff);
+
+ return image.eval(coord) * intensity;
+}"""
+}
diff --git a/blur/src/main/java/com/kyant/blur/BlurStyle.kt b/blur/src/main/java/com/kyant/blur/BlurStyle.kt
new file mode 100644
index 0000000..e7fe940
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/BlurStyle.kt
@@ -0,0 +1,14 @@
+package com.kyant.blur
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TileMode
+import androidx.compose.ui.unit.Dp
+
+@Immutable
+data class BlurStyle(
+ val shape: Shape,
+ val blurRadius: Dp,
+ val tileMode: TileMode = TileMode.Clamp,
+ val material: BlurMaterial = BlurMaterial.Default
+)
diff --git a/blur/src/main/java/com/kyant/blur/Highlight.kt b/blur/src/main/java/com/kyant/blur/Highlight.kt
new file mode 100644
index 0000000..d56c29e
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/Highlight.kt
@@ -0,0 +1,17 @@
+package com.kyant.blur
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Immutable
+data class Highlight(
+ val shape: Shape,
+ val width: Dp = 1f.dp,
+ val color: Color = Color.White.copy(alpha = 0.3f),
+ val blendMode: BlendMode = BlendMode.Plus,
+ val style: HighlightStyle = HighlightStyle.Default
+)
diff --git a/blur/src/main/java/com/kyant/blur/HighlightModifier.kt b/blur/src/main/java/com/kyant/blur/HighlightModifier.kt
new file mode 100644
index 0000000..8382c41
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/HighlightModifier.kt
@@ -0,0 +1,216 @@
+package com.kyant.blur
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.CacheDrawModifierNode
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.asComposeRenderEffect
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.draw
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.graphics.layer.CompositingStrategy
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ObserverModifierNode
+import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.isSpecified
+import androidx.compose.ui.unit.toIntSize
+import kotlin.math.ceil
+import kotlin.math.min
+
+fun Modifier.highlight(highlight: Highlight): Modifier =
+ this then SimpleHighlightElement(highlight)
+
+internal class SimpleHighlightElement(
+ val highlight: Highlight
+) : ModifierNodeElement() {
+
+ override fun create(): SimpleHighlightNode {
+ return SimpleHighlightNode(highlight)
+ }
+
+ override fun update(node: SimpleHighlightNode) {
+ node.update(highlight)
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "highlight"
+ properties["highlight"] = highlight
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is SimpleHighlightElement) return false
+
+ if (highlight != other.highlight) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return highlight.hashCode()
+ }
+}
+
+internal class HighlightElement(
+ val highlight: () -> Highlight
+) : ModifierNodeElement() {
+
+ override fun create(): HighlightNode {
+ return HighlightNode(highlight)
+ }
+
+ override fun update(node: HighlightNode) {
+ node.update(highlight)
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "highlight"
+ properties["highlight"] = highlight
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is HighlightElement) return false
+
+ if (highlight != other.highlight) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return highlight.hashCode()
+ }
+}
+
+internal class SimpleHighlightNode(
+ var highlight: Highlight
+) : DelegatingNode() {
+
+ override val shouldAutoInvalidate: Boolean = false
+
+ private var graphicsLayer: GraphicsLayer? = null
+
+ private val drawNode = delegate(CacheDrawModifierNode {
+ val highlight = highlight
+ val width = highlight.width
+ val color = highlight.color
+
+ val strokeWidthPx =
+ min(
+ if (width == Dp.Hairline) 1f else ceil(width.toPx()),
+ ceil(size.minDimension / 2f),
+ )
+ val halfStroke = strokeWidthPx / 2
+ val borderTopLeft = Offset(halfStroke, halfStroke)
+ val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
+ val outline =
+ if (width.isSpecified && color.isSpecified) {
+ highlight.shape.createOutline(borderSize, layoutDirection, this)
+ } else {
+ null
+ }
+
+ if (outline != null) {
+ graphicsLayer?.let { layer ->
+ val borderRenderEffect = highlight.style.createRenderEffect(size, strokeWidthPx)
+ layer.renderEffect = borderRenderEffect?.asComposeRenderEffect()
+ layer.blendMode = highlight.blendMode
+
+ layer.record(this, layoutDirection, size.toIntSize()) {
+ draw(
+ this@CacheDrawModifierNode,
+ layoutDirection,
+ drawContext.canvas,
+ drawContext.size,
+ drawContext.graphicsLayer
+ ) {
+ translate(borderTopLeft.x, borderTopLeft.y) {
+ drawOutline(
+ outline = outline,
+ brush = SolidColor(color),
+ style = Stroke(strokeWidthPx)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ onDrawBehind {
+ if (outline != null) {
+ graphicsLayer?.let { layer ->
+ drawLayer(layer)
+ }
+ }
+ }
+ })
+
+ override fun onAttach() {
+ val graphicsContext = requireGraphicsContext()
+ graphicsLayer =
+ graphicsContext.createGraphicsLayer().apply {
+ compositingStrategy = CompositingStrategy.Offscreen
+ }
+ }
+
+ override fun onDetach() {
+ val graphicsContext = requireGraphicsContext()
+ graphicsLayer?.let { layer ->
+ graphicsContext.releaseGraphicsLayer(layer)
+ graphicsLayer = null
+ }
+ }
+
+ fun update(
+ highlight: Highlight
+ ) {
+ if (this.highlight != highlight) {
+ this.highlight = highlight
+ drawNode.invalidateDrawCache()
+ }
+ }
+}
+
+internal class HighlightNode(
+ var highlight: () -> Highlight
+) : ObserverModifierNode, DelegatingNode() {
+
+ override val shouldAutoInvalidate: Boolean = false
+
+ private val drawNode = delegate(
+ SimpleHighlightNode(highlight())
+ )
+
+ override fun onObservedReadsChanged() {
+ updateHighlight()
+ }
+
+ override fun onAttach() {
+ updateHighlight()
+ }
+
+ fun update(
+ highlight: () -> Highlight
+ ) {
+ if (this.highlight != highlight) {
+ this.highlight = highlight
+ updateHighlight()
+ }
+ }
+
+ private fun updateHighlight() {
+ observeReads {
+ val highlight = highlight()
+ drawNode.update(highlight)
+ }
+ }
+}
diff --git a/blur/src/main/java/com/kyant/blur/HighlightStyle.kt b/blur/src/main/java/com/kyant/blur/HighlightStyle.kt
new file mode 100644
index 0000000..76199ec
--- /dev/null
+++ b/blur/src/main/java/com/kyant/blur/HighlightStyle.kt
@@ -0,0 +1,110 @@
+package com.kyant.blur
+
+import android.graphics.RenderEffect
+import android.graphics.RuntimeShader
+import android.graphics.Shader
+import android.os.Build
+import androidx.annotation.FloatRange
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Size
+import kotlin.math.PI
+
+@Immutable
+interface HighlightStyle {
+
+ @Stable
+ fun createRenderEffect(size: Size, width: Float): RenderEffect? {
+ return null
+ }
+
+ /**
+ * Solid highlight effect.
+ */
+ @Immutable
+ data object Solid : HighlightStyle
+
+ /**
+ * Soft highlight effect with blur.
+ */
+ @Immutable
+ data object Soft : HighlightStyle {
+
+ override fun createRenderEffect(size: Size, width: Float): RenderEffect? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val blurRadiusPx = width / 2f
+ if (blurRadiusPx > 0f) {
+ RenderEffect.createBlurEffect(
+ blurRadiusPx,
+ blurRadiusPx,
+ Shader.TileMode.DECAL
+ )
+ } else {
+ null
+ }
+ } else {
+ null
+ }
+ }
+ }
+
+ /**
+ * Dynamic highlight effect with shimmering effect.
+ *
+ * @param angle
+ * The angle of the highlight in degrees.
+ *
+ * @param falloff
+ * The falloff of the highlight. Higher value results in a sharper highlight.
+ */
+ @Immutable
+ data class Dynamic(
+ val angle: Float = 45f,
+ @param:FloatRange(from = 0.0) val falloff: Float = 1f
+ ) : HighlightStyle {
+
+ override fun createRenderEffect(size: Size, width: Float): RenderEffect? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val blurRadiusPx = width / 2f
+ val blurRenderEffect =
+ if (blurRadiusPx > 0f) {
+ RenderEffect.createBlurEffect(
+ blurRadiusPx,
+ blurRadiusPx,
+ Shader.TileMode.DECAL
+ )
+ } else {
+ null
+ }
+
+ val highlightRenderEffect =
+ RenderEffect.createRuntimeShaderEffect(
+ RuntimeShader(BlurShaders.dynamicHighlightStyleShaderString).apply {
+ setFloatUniform("size", size.width, size.height)
+
+ setFloatUniform("angle", angle * (PI / 180f).toFloat())
+ setFloatUniform("falloff", falloff)
+ },
+ "image"
+ )
+
+ if (blurRenderEffect != null) {
+ RenderEffect.createChainEffect(
+ highlightRenderEffect,
+ blurRenderEffect
+ )
+ } else {
+ highlightRenderEffect
+ }
+ } else {
+ null
+ }
+ }
+ }
+
+ companion object {
+
+ @Stable
+ val Default: Dynamic = Dynamic()
+ }
+}
diff --git a/glassmusic/build.gradle.kts b/glassmusic/build.gradle.kts
index b30b8a2..38df09d 100644
--- a/glassmusic/build.gradle.kts
+++ b/glassmusic/build.gradle.kts
@@ -80,6 +80,7 @@ android {
}
dependencies {
+ implementation(project(":blur"))
implementation(project(":liquid-glass"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
diff --git a/glassmusic/src/main/java/com/kyant/glassmusic/MainContent.kt b/glassmusic/src/main/java/com/kyant/glassmusic/MainContent.kt
index 5613958..0ab1041 100644
--- a/glassmusic/src/main/java/com/kyant/glassmusic/MainContent.kt
+++ b/glassmusic/src/main/java/com/kyant/glassmusic/MainContent.kt
@@ -25,6 +25,10 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
+import com.kyant.blur.BlurStyle
+import com.kyant.blur.Highlight
+import com.kyant.blur.blur
+import com.kyant.blur.highlight
import com.kyant.liquidglass.GlassStyle
import com.kyant.liquidglass.highlight.GlassHighlight
import com.kyant.liquidglass.liquidGlass
@@ -114,6 +118,20 @@ fun MainContent() {
.align(Alignment.BottomCenter),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
+ Box(
+ Modifier
+ .highlight(Highlight(CircleShape))
+ .blur(
+ liquidGlassProviderState.backdrop,
+ BlurStyle(
+ CircleShape,
+ 24.dp
+ )
+ )
+ .height(56.dp)
+ .fillMaxWidth()
+ )
+
Box(
Modifier
.liquidGlass(
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/Deprecated.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/Deprecated.kt
index 2ddcaf5..d07ffa7 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/Deprecated.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/Deprecated.kt
@@ -153,7 +153,7 @@ class LiquidGlassProviderState internal constructor(
internal val graphicsLayer: GraphicsLayer
) {
- internal val backdrop = LayerBackdrop(graphicsLayer, backgroundColor)
+ val backdrop: LayerBackdrop = LayerBackdrop(graphicsLayer, backgroundColor)
internal var position: Offset
get() = backdrop.backdropPosition
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/GlassShapeElement.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/GlassShapeElement.kt
index 0dd4f63..bb557d5 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/GlassShapeElement.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/GlassShapeElement.kt
@@ -40,7 +40,7 @@ internal class SimpleGlassShapeElement(
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is GlassShapeElement) return false
+ if (other !is SimpleGlassShapeElement) return false
if (style != other.style) return false
if (compositingStrategy != other.compositingStrategy) return false
@@ -125,7 +125,9 @@ internal class SimpleGlassShapeNode(
style: GlassStyle,
compositingStrategy: CompositingStrategy
) {
- if (this.style != style || this.compositingStrategy != compositingStrategy) {
+ if (this.style != style ||
+ this.compositingStrategy != compositingStrategy
+ ) {
this.style = style
this.compositingStrategy = compositingStrategy
invalidateLayer()
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt
index da86410..d839c77 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt
@@ -114,7 +114,7 @@ private class SimpleLiquidGlassElement(
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is LiquidGlassElement) return false
+ if (other !is SimpleLiquidGlassElement) return false
if (backdrop != other.backdrop) return false
if (style != other.style) return false
@@ -126,7 +126,7 @@ private class SimpleLiquidGlassElement(
override fun hashCode(): Int {
var result = backdrop.hashCode()
result = 31 * result + style.hashCode()
- result = 31 * result + (onDrawBackdrop?.hashCode() ?: 0)
+ result = 31 * result + onDrawBackdrop.hashCode()
return result
}
}
@@ -175,7 +175,7 @@ private class LiquidGlassElement(
override fun hashCode(): Int {
var result = backdrop.hashCode()
result = 31 * result + style.hashCode()
- result = 31 * result + (onDrawBackdrop?.hashCode() ?: 0)
+ result = 31 * result + onDrawBackdrop.hashCode()
return result
}
}
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/highlight/GlassHighlightElement.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/highlight/GlassHighlightElement.kt
index 0203233..2bf33a7 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/highlight/GlassHighlightElement.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/highlight/GlassHighlightElement.kt
@@ -46,7 +46,7 @@ internal class SimpleGlassHighlightElement(
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is GlassHighlightElement) return false
+ if (other !is SimpleGlassHighlightElement) return false
if (style != other.style) return false
@@ -205,105 +205,18 @@ internal class GlassHighlightNode(
override val shouldAutoInvalidate: Boolean = false
- private var highlight: GlassHighlight? = style().highlight
- set(value) {
- if (field != value) {
- field = value
- drawNode.invalidateDrawCache()
- }
- }
-
- private var shape: CornerBasedShape = style().shape
- set(value) {
- if (field != value) {
- field = value
- drawNode.invalidateDrawCache()
- }
- }
-
- private var graphicsLayer: GraphicsLayer? = null
-
- private val drawNode = delegate(CacheDrawModifierNode {
- val highlight = highlight
- if (highlight == null) {
- return@CacheDrawModifierNode onDrawWithContent { drawContent() }
- }
-
- val width = highlight.width
- val color = highlight.color
-
- val strokeWidthPx =
- min(
- if (width == Dp.Hairline) 1f else ceil(width.toPx()),
- ceil(size.minDimension / 2f),
- )
- val halfStroke = strokeWidthPx / 2
- val borderTopLeft = Offset(halfStroke, halfStroke)
- val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
- val outline =
- if (width.isSpecified && color.isSpecified) {
- shape.createOutline(borderSize, layoutDirection, this)
- } else {
- null
- }
-
- if (outline != null) {
- graphicsLayer?.let { layer ->
- val borderRenderEffect = highlight.style.createRenderEffect(size, strokeWidthPx)
- layer.renderEffect = borderRenderEffect?.asComposeRenderEffect()
- layer.blendMode = highlight.blendMode
-
- layer.record(this, layoutDirection, size.toIntSize()) {
- draw(
- this@CacheDrawModifierNode,
- layoutDirection,
- drawContext.canvas,
- drawContext.size,
- drawContext.graphicsLayer
- ) {
- translate(borderTopLeft.x, borderTopLeft.y) {
- drawOutline(
- outline = outline,
- brush = SolidColor(color),
- style = Stroke(strokeWidthPx)
- )
- }
- }
- }
- }
- }
-
- onDrawBehind {
- if (outline != null) {
- graphicsLayer?.let { layer ->
- drawLayer(layer)
- }
- }
- }
- })
+ private val drawNode = delegate(
+ SimpleGlassHighlightNode(style())
+ )
override fun onObservedReadsChanged() {
updateHighlight()
}
override fun onAttach() {
- val graphicsContext = requireGraphicsContext()
- graphicsLayer =
- graphicsContext.createGraphicsLayer().apply {
- compositingStrategy = CompositingStrategy.Offscreen
- }
-
updateHighlight()
}
- override fun onDetach() {
- val graphicsContext = requireGraphicsContext()
- graphicsLayer?.let { layer ->
- graphicsContext.releaseGraphicsLayer(layer)
- graphicsLayer = null
- }
- }
-
fun update(
style: () -> GlassStyle
) {
@@ -316,8 +229,7 @@ internal class GlassHighlightNode(
private fun updateHighlight() {
observeReads {
val style = style()
- highlight = style.highlight
- shape = style.shape
+ drawNode.update(style)
}
}
}
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassBrushElement.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassBrushElement.kt
index 108a455..6475652 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassBrushElement.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassBrushElement.kt
@@ -29,7 +29,7 @@ internal class SimpleGlassBrushElement(
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is GlassBrushElement) return false
+ if (other !is SimpleGlassBrushElement) return false
if (style != other.style) return false
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassMaterial.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassMaterial.kt
index 4f44b9a..98e93ad 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassMaterial.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/material/GlassMaterial.kt
@@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import com.kyant.backdrop.simpleColorFilter
/**
* The material properties of the liquid glass.
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/material/SimpleColorFilter.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/material/SimpleColorFilter.kt
index f3c9ab4..6e43efd 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/material/SimpleColorFilter.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/material/SimpleColorFilter.kt
@@ -2,36 +2,22 @@ package com.kyant.liquidglass.material
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.ColorMatrix
-import androidx.compose.ui.graphics.ColorMatrixColorFilter
+import com.kyant.backdrop.simpleColorFilter
+@Deprecated(
+ message = "Use the new package com.kyant.backdrop",
+ replaceWith = ReplaceWith(
+ "simpleColorFilter(brightness, contrast, saturation)",
+ "com.kyant.backdrop.simpleColorFilter"
+ )
+)
@Stable
fun simpleColorFilter(
brightness: Float = 0f,
contrast: Float = 1f,
saturation: Float = 1f
-): ColorFilter {
- val invSat = 1f - saturation
- val r = 0.213f * invSat
- val g = 0.715f * invSat
- val b = 0.072f * invSat
-
- val c = contrast
- val t = (0.5f - c * 0.5f + brightness) * 255f
- val s = saturation
-
- val cr = c * r
- val cg = c * g
- val cb = c * b
- val cs = c * s
-
- val colorMatrix = ColorMatrix(
- floatArrayOf(
- cr + cs, cg, cb, 0f, t,
- cr, cg + cs, cb, 0f, t,
- cr, cg, cb + cs, 0f, t,
- 0f, 0f, 0f, 1f, 0f
- )
- )
- return ColorMatrixColorFilter(colorMatrix)
-}
+): ColorFilter = simpleColorFilter(
+ brightness = brightness,
+ contrast = contrast,
+ saturation = saturation
+)
diff --git a/liquid-glass/src/main/java/com/kyant/liquidglass/shadow/GlassShadowElement.kt b/liquid-glass/src/main/java/com/kyant/liquidglass/shadow/GlassShadowElement.kt
index fc2356a..422da34 100644
--- a/liquid-glass/src/main/java/com/kyant/liquidglass/shadow/GlassShadowElement.kt
+++ b/liquid-glass/src/main/java/com/kyant/liquidglass/shadow/GlassShadowElement.kt
@@ -48,7 +48,7 @@ internal class SimpleGlassShadowElement(
override fun equals(other: Any?): Boolean {
if (this === other) return true
- if (other !is GlassShadowElement) return false
+ if (other !is SimpleGlassShadowElement) return false
if (style != other.style) return false
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8fc0aab..aa30ce6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,6 +21,7 @@ dependencyResolutionManagement {
rootProject.name = "Glass"
include(":backdrop")
+include(":blur")
include(":liquid-glass")
include(":app")
include(":glassmusic")