diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 1580500..31bdaef 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -11,6 +11,7 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index cfc0c81..b46d6f6 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,3 +1,4 @@
+
@@ -88,16 +89,4 @@
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 48c51d8..630f445 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,44 @@ Download the app [here](./app/release/app-release.apk).
You mustn't use the shaders in the project for commercial purposes, they are only for educational purposes.
+## Library
+
+The library is under construction, use on your own risk.
+
+It doesn't support nested glass effects for now.
+
+### Examples
+
+```kotlin
+val providerState = rememberLiquidGlassProviderState()
+
+CompositionLocalProvider(
+ LocalLiquidGlassProviderState provides providerState
+) {
+ Box(
+ Modifier
+ .liquidGlassProvider(providerState)
+ )
+
+ Box(
+ Modifier
+ .liquidGlass(
+ remember {
+ LiquidGlassStyle(
+ shape = CornerShape.extraLarge,
+ blurRadius = 8.dp,
+ refractionHeight = 24.dp,
+ refractionAmount = (-64).dp,
+ whitePoint = 0.25f,
+ chromaMultiplier = 1.5f
+ // ...
+ )
+ }
+ )
+ )
+}
+```
+
## Glass parameters
| Parameter | Availability | Verification |
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1244801..800b460 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -80,6 +80,7 @@ android {
}
dependencies {
+ implementation(project(":library"))
implementation(files("libs/expressa-core-release.aar"))
implementation(files("libs/expressa-components-release.aar"))
implementation(libs.androidx.core.ktx)
diff --git a/app/release/app-release.apk b/app/release/app-release.apk
index 67d76bb..c0a7178 100644
Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ
diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm
index 14df4e7..759c63c 100644
Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ
diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm
index 0c62357..9c8948c 100644
Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
deleted file mode 100644
index 1858e63..0000000
Binary files a/app/src/main/ic_launcher-playstore.png and /dev/null differ
diff --git a/app/src/main/java/com/kyant/glass/MainActivity.kt b/app/src/main/java/com/kyant/glass/MainActivity.kt
index 1a5d6c9..fe8cf17 100644
--- a/app/src/main/java/com/kyant/glass/MainActivity.kt
+++ b/app/src/main/java/com/kyant/glass/MainActivity.kt
@@ -24,7 +24,6 @@ import com.kyant.expressa.ripple.RippleConfiguration
import com.kyant.expressa.ripple.ripple
import com.kyant.expressa.ui.LocalContentColor
import com.kyant.glass.playground.Preview
-import com.kyant.glass.playground.PreviewState
class MainActivity : ComponentActivity() {
@@ -60,8 +59,7 @@ class MainActivity : ComponentActivity() {
view.rootView.background = background.toArgb().toDrawable()
}
- val previewState = remember { PreviewState() }
- Preview(previewState)
+ Preview()
}
}
}
diff --git a/app/src/main/java/com/kyant/glass/playground/ConfigurationBottomSheet.kt b/app/src/main/java/com/kyant/glass/playground/ConfigurationBottomSheet.kt
index c6023af..ed8ad97 100644
--- a/app/src/main/java/com/kyant/glass/playground/ConfigurationBottomSheet.kt
+++ b/app/src/main/java/com/kyant/glass/playground/ConfigurationBottomSheet.kt
@@ -9,136 +9,165 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kyant.expressa.components.button.Button
+import com.kyant.expressa.components.button.ButtonColors
import com.kyant.expressa.components.iconbutton.IconButton
+import com.kyant.expressa.components.iconbutton.IconButtonColors
+import com.kyant.expressa.m3.LocalColorScheme
import com.kyant.expressa.m3.motion.MotionScheme
import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.expressa.ui.Text
import com.kyant.glass.R
+import com.kyant.liquidglass.LiquidGlassStyle
+import com.kyant.liquidglass.liquidGlass
@Composable
fun ConfigurationBottomSheet(
state: PreviewState,
modifier: Modifier = Modifier
) {
- Column(
- modifier
- .pointerInput(Unit) {
- detectTapGestures {}
- }
- .shadow(
- elevation = 8.dp,
- shape = CornerShape.extraLarge,
- ambientColor = Color.Black.copy(alpha = 0.3f),
- spotColor = Color.Black.copy(alpha = 0.3f)
- )
- .background(surface)
- .padding(16.dp)
- .fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ // val providerState = rememberLiquidGlassProviderState()
+ CompositionLocalProvider(
+ // LocalLiquidGlassProviderState provides providerState
) {
- Row(
- modifier
- .fillMaxWidth()
- .padding(start = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- when (state.configurationMode) {
- ConfigurationMode.Colors -> stringResource(R.string.colors)
- ConfigurationMode.Advanced -> stringResource(R.string.advanced_settings)
- null -> ""
- },
- titleMedium,
- Modifier.weight(1f)
- )
-
- IconButton(
- { state.configurationMode = null }
- ) {
- Icon(
- painterResource(R.drawable.close_24px),
- stringResource(R.string.close)
- )
- }
- }
-
+ val isDark = LocalColorScheme.current.isDark
Column(
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- val retainedConfigurationMode = remember {
- Snapshot.withoutReadObservation { state.configurationMode }
- }
- when (retainedConfigurationMode) {
- ConfigurationMode.Colors -> {
- SliderChip(
- state.blurRadius,
- stringResource(R.string.blur_radius),
- { Icon(painterResource(R.drawable.lens_blur_24px)) },
- Modifier.fillMaxWidth()
- )
- SliderChip(
- state.contrast,
- stringResource(R.string.contrast),
- { Icon(painterResource(R.drawable.contrast_24px)) },
- Modifier.fillMaxWidth()
- )
- SliderChip(
- state.whitePoint,
- stringResource(R.string.white_point),
- { Icon(painterResource(R.drawable.wb_sunny_24px)) },
- Modifier.fillMaxWidth()
- )
- SliderChip(
- state.chromaMultiplier,
- stringResource(R.string.chroma_multiplier),
- { Icon(painterResource(R.drawable.palette_24px)) },
- Modifier.fillMaxWidth()
- )
- }
-
- ConfigurationMode.Advanced -> {
- Button({ state.unsafeMode = !state.unsafeMode }) {
- Text(
- if (state.unsafeMode) stringResource(R.string.disable_unsafe_mode)
- else stringResource(R.string.enable_unsafe_mode),
- Modifier.animateContentSize(MotionScheme.fastSpatial())
+ modifier
+ // .liquidGlassProvider(providerState)
+ .liquidGlass(
+ remember(isDark) {
+ LiquidGlassStyle(
+ shape = CornerShape.extraLarge,
+ blurRadius = 8.dp,
+ refractionHeight = 24.dp,
+ refractionAmount = (-64).dp,
+ whitePoint = if (isDark) -0.25f else 0.25f,
+ chromaMultiplier = 1.5f
)
}
- SliderChip(
- state.bleedBlurRadius,
- stringResource(R.string.bleed_blur_radius),
- { Icon(painterResource(R.drawable.lens_blur_24px)) },
- Modifier.fillMaxWidth()
+ )
+ .background(surface.copy(alpha = 0.6f))
+ .pointerInput(Unit) {
+ detectTapGestures {}
+ }
+ .padding(16.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(start = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ when (state.configurationMode) {
+ ConfigurationMode.Colors -> stringResource(R.string.colors)
+ ConfigurationMode.Advanced -> stringResource(R.string.advanced_settings)
+ null -> ""
+ },
+ titleMedium,
+ Modifier.weight(1f)
+ )
+
+ IconButton(
+ { state.configurationMode = null },
+ Modifier.liquidGlass(remember { LiquidGlassStyle(CornerShape.full) }),
+ colors = IconButtonColors.tonal(
+ containerColor = primaryContainer.copy(alpha = 0.6f),
+ contentColor = Color.White
)
- SliderChip(
- state.eccentricFactor,
- stringResource(R.string.eccentric_factor),
- { Icon(painterResource(R.drawable.all_out_24px)) },
- Modifier.fillMaxWidth()
- )
- SliderChip(
- state.dispersionHeight,
- stringResource(R.string.dispersion_height),
- { Icon(painterResource(R.drawable.star_shine_24px)) },
- Modifier.fillMaxWidth()
+ ) {
+ Icon(
+ painterResource(R.drawable.close_24px),
+ stringResource(R.string.close)
)
}
+ }
- null -> {}
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ val retainedConfigurationMode = remember {
+ Snapshot.withoutReadObservation { state.configurationMode }
+ }
+ when (retainedConfigurationMode) {
+ ConfigurationMode.Colors -> {
+ SliderChip(
+ state.blurRadius,
+ stringResource(R.string.blur_radius),
+ { Icon(painterResource(R.drawable.lens_blur_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ SliderChip(
+ state.contrast,
+ stringResource(R.string.contrast),
+ { Icon(painterResource(R.drawable.contrast_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ SliderChip(
+ state.whitePoint,
+ stringResource(R.string.white_point),
+ { Icon(painterResource(R.drawable.wb_sunny_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ SliderChip(
+ state.chromaMultiplier,
+ stringResource(R.string.chroma_multiplier),
+ { Icon(painterResource(R.drawable.palette_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ }
+
+ ConfigurationMode.Advanced -> {
+ Button(
+ { state.unsafeMode = !state.unsafeMode },
+ Modifier.liquidGlass(remember { LiquidGlassStyle(CornerShape.full) }),
+ colors = ButtonColors.filled(
+ containerColor = primary.copy(alpha = 0.6f)
+ )
+ ) {
+ Text(
+ if (state.unsafeMode) stringResource(R.string.disable_unsafe_mode)
+ else stringResource(R.string.enable_unsafe_mode),
+ Modifier.animateContentSize(MotionScheme.fastSpatial())
+ )
+ }
+ SliderChip(
+ state.bleedBlurRadius,
+ stringResource(R.string.bleed_blur_radius),
+ { Icon(painterResource(R.drawable.lens_blur_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ SliderChip(
+ state.eccentricFactor,
+ stringResource(R.string.eccentric_factor),
+ { Icon(painterResource(R.drawable.all_out_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ SliderChip(
+ state.dispersionHeight,
+ stringResource(R.string.dispersion_height),
+ { Icon(painterResource(R.drawable.star_shine_24px)) },
+ Modifier.fillMaxWidth()
+ )
+ }
+
+ null -> {}
+ }
}
}
}
diff --git a/app/src/main/java/com/kyant/glass/playground/Preview.kt b/app/src/main/java/com/kyant/glass/playground/Preview.kt
index a24805d..4910ab0 100644
--- a/app/src/main/java/com/kyant/glass/playground/Preview.kt
+++ b/app/src/main/java/com/kyant/glass/playground/Preview.kt
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -22,6 +23,7 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.asComposeRenderEffect
+import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
@@ -42,59 +44,56 @@ import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.util.fastCoerceAtMost
import com.kyant.expressa.prelude.*
import com.kyant.glass.R
+import com.kyant.liquidglass.LocalLiquidGlassProviderState
+import com.kyant.liquidglass.liquidGlassProvider
+import com.kyant.liquidglass.rememberLiquidGlassProviderState
import org.intellij.lang.annotations.Language
@Composable
-fun Preview(state: PreviewState) {
+fun Preview() {
+ val state = remember { PreviewState() }
+
val graphicsLayer = rememberGraphicsLayer()
val backgroundColor = surfaceContainerLowest
val containerSize = LocalWindowInfo.current.containerSize
-
var rect by remember { mutableStateOf(Rect.Zero) }
-
val painter =
state.imageBitmap?.let { BitmapPainter(it) }
?: painterResource(R.drawable.homescreen)
- Box(
- Modifier.fillMaxSize()
+ val providerState = rememberLiquidGlassProviderState()
+ CompositionLocalProvider(
+ LocalLiquidGlassProviderState provides providerState
) {
- // background
- Image(
- painter, null,
- Modifier
- .fillMaxSize()
- .drawWithContent {
- drawContent()
+ Box(Modifier.fillMaxSize()) {
+ // background
+ Image(
+ painter, null,
+ Modifier
+ .fillMaxSize()
+ .drawWithContent {
+ drawContent()
- graphicsLayer.record {
- drawRect(backgroundColor)
- this@drawWithContent.drawContent()
+ graphicsLayer.record {
+ drawRect(backgroundColor)
+ this@drawWithContent.drawContent()
+ }
}
- },
- contentScale = ContentScale.Crop
- )
+ .liquidGlassProvider(providerState),
+ contentScale = ContentScale.Crop
+ )
- @Language("AGSL")
- val colorShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
+ @Language("AGSL")
+ val colorShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
const half3 rgbToY = half3(0.2126, 0.7152, 0.0722);
float luma(half4 color) {
return dot(toLinearSrgb(color.rgb), rgbToY);
}
-
- half4 saturateColor(half4 color, float amount) {
- half3 linearSrgb = toLinearSrgb(color.rgb);
- float y = dot(linearSrgb, rgbToY);
- half3 gray = half3(y);
- half3 adjustedLinearSrgb = mix(gray, linearSrgb, amount);
- half3 adjustedSrgb = fromLinearSrgb(adjustedLinearSrgb);
- return half4(adjustedSrgb, color.a);
- }
""".trimIndent()
- @Language("AGSL")
- val refractionShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
+ @Language("AGSL")
+ val refractionShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
float circleMap(float x) {
return 1.0 - sqrt(1.0 - x * x);
}
@@ -147,24 +146,24 @@ fun Preview(state: PreviewState) {
}
""".trimIndent()
- // glass
- Box(
- Modifier
- .graphicsLayer { // transform and clip
- translationX = state.offset.x
- translationY = state.offset.y
- clip = true
- shape = RoundedCornerShape(state.cornerRadius.value)
- }
- .graphicsLayer { // white point & chroma boost
- val contrast = state.contrast.value
- val whitePoint = state.whitePoint.value
- val chromaMultiplier = state.chromaMultiplier.value
+ // glass
+ Box(
+ Modifier
+ .graphicsLayer { // transform and clip
+ translationX = state.offset.x
+ translationY = state.offset.y
+ clip = true
+ shape = RoundedCornerShape(state.cornerRadius.value)
+ }
+ .graphicsLayer { // white point & chroma boost
+ val contrast = state.contrast.value
+ val whitePoint = state.whitePoint.value
+ val chromaMultiplier = state.chromaMultiplier.value
- if (contrast != 0f || whitePoint != 0f || chromaMultiplier != 1f) {
- renderEffect = RenderEffect.createRuntimeShaderEffect(
- RuntimeShader(
- """// This file belongs to Kyant. You must not use it without permission.
+ if (contrast != 0f || whitePoint != 0f || chromaMultiplier != 1f) {
+ renderEffect = RenderEffect.createRuntimeShaderEffect(
+ RuntimeShader(
+ """// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float contrast;
@@ -173,6 +172,15 @@ fun Preview(state: PreviewState) {
$colorShaderUtils
+ half4 saturateColor(half4 color, float amount) {
+ half3 linearSrgb = toLinearSrgb(color.rgb);
+ float y = dot(linearSrgb, rgbToY);
+ half3 gray = half3(y);
+ half3 adjustedLinearSrgb = mix(gray, linearSrgb, amount);
+ half3 adjustedSrgb = fromLinearSrgb(adjustedLinearSrgb);
+ return half4(adjustedSrgb, color.a);
+ }
+
half4 main(float2 coord) {
half4 color = image.eval(coord);
@@ -185,19 +193,19 @@ fun Preview(state: PreviewState) {
return color;
}"""
- ).apply {
- setFloatUniform("contrast", contrast)
- setFloatUniform("whitePoint", whitePoint)
- setFloatUniform("chromaMultiplier", chromaMultiplier)
- },
- "image"
- ).asComposeRenderEffect()
+ ).apply {
+ setFloatUniform("contrast", contrast)
+ setFloatUniform("whitePoint", whitePoint)
+ setFloatUniform("chromaMultiplier", chromaMultiplier)
+ },
+ "image"
+ ).asComposeRenderEffect()
+ }
}
- }
- .graphicsLayer { // dispersion effect
- renderEffect = RenderEffect.createRuntimeShaderEffect(
- RuntimeShader(
- """// This file belongs to Kyant. You must not use it without permission.
+ .graphicsLayer { // dispersion effect
+ renderEffect = RenderEffect.createRuntimeShaderEffect(
+ RuntimeShader(
+ """// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
@@ -286,22 +294,22 @@ fun Preview(state: PreviewState) {
return color;
}
}"""
- ).apply {
- val cornerRadius =
- state.cornerRadius.value.toPx()
- .fastCoerceAtMost(size.minDimension / 2f)
+ ).apply {
+ val cornerRadius =
+ state.cornerRadius.value.toPx()
+ .fastCoerceAtMost(size.minDimension / 2f)
- setFloatUniform("size", size.width, size.height)
- setFloatUniform("cornerRadius", cornerRadius)
- setFloatUniform("dispersionHeight", state.dispersionHeight.value.toPx())
- },
- "image"
- ).asComposeRenderEffect()
- }
- .graphicsLayer { // refraction & bleed
- val refractionRenderEffect = RenderEffect.createRuntimeShaderEffect(
- RuntimeShader(
- """// This file belongs to Kyant. You must not use it without permission.
+ setFloatUniform("size", size.width, size.height)
+ setFloatUniform("cornerRadius", cornerRadius)
+ setFloatUniform("dispersionHeight", state.dispersionHeight.value.toPx())
+ },
+ "image"
+ ).asComposeRenderEffect()
+ }
+ .graphicsLayer { // refraction & bleed
+ val refractionRenderEffect = RenderEffect.createRuntimeShaderEffect(
+ RuntimeShader(
+ """// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
@@ -326,22 +334,22 @@ fun Preview(state: PreviewState) {
return color;
}
}"""
- ).apply {
- setFloatUniform("size", size.width, size.height)
- setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
+ ).apply {
+ setFloatUniform("size", size.width, size.height)
+ setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
- setFloatUniform("refractionHeight", state.refractionHeight.value.toPx())
- setFloatUniform("refractionAmount", state.refractionAmount.value.toPx())
- setFloatUniform("eccentricFactor", state.eccentricFactor.value)
+ setFloatUniform("refractionHeight", state.refractionHeight.value.toPx())
+ setFloatUniform("refractionAmount", state.refractionAmount.value.toPx())
+ setFloatUniform("eccentricFactor", state.eccentricFactor.value)
- setFloatUniform("bleedOpacity", state.bleedOpacity.value)
- },
- "image"
- )
+ setFloatUniform("bleedOpacity", state.bleedOpacity.value)
+ },
+ "image"
+ )
- val bleedRenderEffect = RenderEffect.createRuntimeShaderEffect(
- RuntimeShader(
- """// This file belongs to Kyant. You must not use it without permission.
+ val bleedRenderEffect = RenderEffect.createRuntimeShaderEffect(
+ RuntimeShader(
+ """// This file belongs to Kyant. You must not use it without permission.
uniform shader image;
uniform float2 size;
@@ -359,109 +367,110 @@ fun Preview(state: PreviewState) {
color.rgb = mix(color.rgb, half3(1.0), 0.5 * circleMap(1.0 - luma));
return color;
}"""
- ).apply {
- setFloatUniform("size", size.width, size.height)
- setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
+ ).apply {
+ setFloatUniform("size", size.width, size.height)
+ setFloatUniform("cornerRadius", state.cornerRadius.value.toPx())
- setFloatUniform("eccentricFactor", state.eccentricFactor.value)
- setFloatUniform("bleedAmount", state.bleedAmount.value.toPx())
- },
- "image"
- )
-
- renderEffect =
- if (state.bleedOpacity.value > 0f) {
- val bleedBlurRadiusPx = state.bleedBlurRadius.value.toPx()
- val blurredBleedRenderEffect =
- if (bleedBlurRadiusPx > 0f) {
- RenderEffect.createChainEffect(
- bleedRenderEffect,
- RenderEffect.createBlurEffect(
- bleedBlurRadiusPx,
- bleedBlurRadiusPx,
- Shader.TileMode.CLAMP
- )
- )
- } else {
- bleedRenderEffect
- }
-
- RenderEffect.createBlendModeEffect(
- blurredBleedRenderEffect,
- refractionRenderEffect,
- BlendMode.SRC_OVER
- ).asComposeRenderEffect()
- } else {
- refractionRenderEffect.asComposeRenderEffect()
- }
- }
- .graphicsLayer { // blur
- val blurRadiusPx = state.blurRadius.value.toPx()
- if (blurRadiusPx > 0f) {
- renderEffect =
- RenderEffect.createBlurEffect(
- blurRadiusPx,
- blurRadiusPx,
- Shader.TileMode.DECAL
- ).asComposeRenderEffect()
- }
- }
- .drawBehind {
- translate(-rect.left, -rect.top) {
- drawLayer(graphicsLayer)
- }
- }
- .onGloballyPositioned { layoutCoordinates ->
- rect = layoutCoordinates.boundsInParent()
- }
- .pointerInput(Unit) {
- detectTransformGestures { _, pan, zoom, _ ->
- state.offset += pan
- state.size = DpSize(
- (state.size.width * zoom).coerceIn(
- state.minSize.width,
- containerSize.width.toDp()
- ),
- (state.size.height * zoom).coerceIn(
- state.minSize.height,
- containerSize.height.toDp()
- )
+ setFloatUniform("eccentricFactor", state.eccentricFactor.value)
+ setFloatUniform("bleedAmount", state.bleedAmount.value.toPx())
+ },
+ "image"
)
- }
- }
- .layout { measurable, constraints ->
- val size = state.size.toSize().roundToIntSize()
- val width = size.width.fastCoerceAtMost(constraints.maxWidth)
- val height = size.height.fastCoerceAtMost(constraints.maxHeight)
- val placeable = measurable.measure(Constraints.fixed(width, height))
- layout(width, height) {
- placeable.place(0, 0)
- }
- }
- .align(Alignment.Center)
- )
+ renderEffect =
+ if (state.bleedOpacity.value > 0f) {
+ val bleedBlurRadiusPx = state.bleedBlurRadius.value.toPx()
+ val blurredBleedRenderEffect =
+ if (bleedBlurRadiusPx > 0f) {
+ RenderEffect.createChainEffect(
+ bleedRenderEffect,
+ RenderEffect.createBlurEffect(
+ bleedBlurRadiusPx,
+ bleedBlurRadiusPx,
+ Shader.TileMode.CLAMP
+ )
+ )
+ } else {
+ bleedRenderEffect
+ }
- BlurredAnimatedContent({ state.displayControls }) { visible ->
- if (visible) {
+ RenderEffect.createBlendModeEffect(
+ blurredBleedRenderEffect,
+ refractionRenderEffect,
+ BlendMode.SRC_OVER
+ ).asComposeRenderEffect()
+ } else {
+ refractionRenderEffect.asComposeRenderEffect()
+ }
+ }
+ .graphicsLayer { // blur
+ val blurRadiusPx = state.blurRadius.value.toPx()
+ if (blurRadiusPx > 0f) {
+ renderEffect =
+ RenderEffect.createBlurEffect(
+ blurRadiusPx,
+ blurRadiusPx,
+ Shader.TileMode.DECAL
+ ).asComposeRenderEffect()
+ }
+ }
+ .drawBehind {
+ clipRect(0f, 0f, rect.width, rect.height) {
+ translate(-rect.left, -rect.top) {
+ drawLayer(graphicsLayer)
+ }
+ }
+ }
+ .onGloballyPositioned { layoutCoordinates ->
+ rect = layoutCoordinates.boundsInParent()
+ }
+ .pointerInput(Unit) {
+ detectTransformGestures { _, pan, zoom, _ ->
+ state.offset += pan
+ state.size = DpSize(
+ (state.size.width * zoom).coerceIn(
+ state.minSize.width,
+ containerSize.width.toDp()
+ ),
+ (state.size.height * zoom).coerceIn(
+ state.minSize.height,
+ containerSize.height.toDp()
+ )
+ )
+ }
+ }
+ .layout { measurable, constraints ->
+ val size = state.size.toSize().roundToIntSize()
+ val width = size.width.fastCoerceAtMost(constraints.maxWidth)
+ val height = size.height.fastCoerceAtMost(constraints.maxHeight)
+ val placeable = measurable.measure(Constraints.fixed(width, height))
+
+ layout(width, height) {
+ placeable.place(0, 0)
+ }
+ }
+ .align(Alignment.Center)
+ )
+
+ if (state.displayControls) {
PreviewControls(
state,
{ rect }
)
}
- }
- BlurredAnimatedContent(
- { state.isInConfigurationMode },
- Modifier
- .padding(16.dp)
- .safeDrawingPadding()
- .align(Alignment.BottomCenter)
- ) { isInConfigurationMode ->
- if (!isInConfigurationMode) {
- ButtonGroup(state)
- } else {
- ConfigurationBottomSheet(state)
+ BlurredAnimatedContent(
+ { state.isInConfigurationMode },
+ Modifier
+ .padding(16.dp)
+ .safeDrawingPadding()
+ .align(Alignment.BottomCenter)
+ ) { isInConfigurationMode ->
+ if (!isInConfigurationMode) {
+ PreviewToolbar(state)
+ } else {
+ ConfigurationBottomSheet(state)
+ }
}
}
}
diff --git a/app/src/main/java/com/kyant/glass/playground/PreviewControls.kt b/app/src/main/java/com/kyant/glass/playground/PreviewControls.kt
index d3a3caa..b7f4edf 100644
--- a/app/src/main/java/com/kyant/glass/playground/PreviewControls.kt
+++ b/app/src/main/java/com/kyant/glass/playground/PreviewControls.kt
@@ -4,12 +4,13 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@@ -17,10 +18,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.LayoutDirection.Ltr
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import com.kyant.expressa.m3.shape.CornerShape
@@ -35,7 +39,11 @@ fun PreviewControls(
rect: () -> Rect,
modifier: Modifier = Modifier
) {
- Box(modifier.fillMaxSize()) {
+ val layoutDirection = LocalLayoutDirection.current
+
+ BoxWithConstraints(modifier.fillMaxSize()) {
+ val containerSize = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat())
+
// width
Icon(
painterResource(R.drawable.height_24px),
@@ -44,7 +52,9 @@ fun PreviewControls(
Modifier
.graphicsLayer {
val rect = rect()
- translationX = rect.right + 8.dp.toPx()
+ translationX =
+ if (layoutDirection == Ltr) rect.right + 8.dp.toPx()
+ else -containerSize.width + rect.left - 8.dp.toPx()
translationY = rect.center.y - size.height / 2
}
.border(
@@ -56,7 +66,7 @@ fun PreviewControls(
.background(tertiaryContainer)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
- val delta = dragAmount.x * 2
+ val delta = dragAmount.x * 2f * if (layoutDirection == Ltr) 1f else -1f
state.size =
state.size.copy(
width =
@@ -80,7 +90,9 @@ fun PreviewControls(
Modifier
.graphicsLayer {
val rect = rect()
- translationX = rect.center.x - size.width / 2
+ translationX =
+ if (layoutDirection == Ltr) rect.center.x - size.width / 2
+ else -containerSize.width + rect.center.x + size.width / 2
translationY = rect.bottom + 8.dp.toPx()
}
.border(
@@ -123,15 +135,20 @@ fun PreviewControls(
val rect = rect()
val cornerRadiusOffset =
(-state.cornerRadius.value.toPx() + 8.dp.toPx()) / (2 * sqrt(2f))
- translationX = rect.right + cornerRadiusOffset
+ translationX =
+ if (layoutDirection == Ltr) rect.right + cornerRadiusOffset
+ else -containerSize.width + rect.left - cornerRadiusOffset
translationY = rect.bottom + cornerRadiusOffset
}
.border(
2.dp,
primary,
CornerShape.full
- ),
- dragDelta = { dragAmount -> (-dragAmount.x - dragAmount.y) / sqrt(2f) }
+ )
+ .requiredWidth(IntrinsicSize.Max),
+ dragDelta = { dragAmount ->
+ -((dragAmount.x * if (layoutDirection == Ltr) 1f else -1f) + dragAmount.y) / sqrt(2f)
+ }
)
// refraction
@@ -140,7 +157,9 @@ fun PreviewControls(
.width(IntrinsicSize.Max)
.graphicsLayer {
val rect = rect()
- translationX = rect.center.x - size.width / 2
+ translationX =
+ if (layoutDirection == Ltr) rect.center.x - size.width / 2
+ else -containerSize.width + rect.center.x + size.width / 2
translationY = rect.top - size.height - 8.dp.toPx()
},
horizontalAlignment = Alignment.CenterHorizontally,
diff --git a/app/src/main/java/com/kyant/glass/playground/ButtonGroup.kt b/app/src/main/java/com/kyant/glass/playground/PreviewToolbar.kt
similarity index 72%
rename from app/src/main/java/com/kyant/glass/playground/ButtonGroup.kt
rename to app/src/main/java/com/kyant/glass/playground/PreviewToolbar.kt
index ca0a010..1fde35f 100644
--- a/app/src/main/java/com/kyant/glass/playground/ButtonGroup.kt
+++ b/app/src/main/java/com/kyant/glass/playground/PreviewToolbar.kt
@@ -11,9 +11,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -21,18 +23,23 @@ import androidx.compose.ui.unit.dp
import com.kyant.expressa.components.iconbutton.IconButton
import com.kyant.expressa.components.iconbutton.IconButtonColors
import com.kyant.expressa.components.iconbutton.IconButtonSizes
+import com.kyant.expressa.m3.shape.CornerShape
import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.Icon
import com.kyant.glass.R
+import com.kyant.liquidglass.LiquidGlassStyle
+import com.kyant.liquidglass.liquidGlass
import kotlinx.coroutines.launch
@Composable
-fun ButtonGroup(
+fun PreviewToolbar(
state: PreviewState,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
+ val iconButtonLiquidGlassStyle = remember { LiquidGlassStyle(CornerShape.full) }
+
Row(
modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -40,10 +47,11 @@ fun ButtonGroup(
) {
IconButton(
{ scope.launch { state.reset() } },
+ Modifier.liquidGlass(iconButtonLiquidGlassStyle),
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
- containerColor = tertiaryContainer,
- contentColor = onTertiaryContainer
+ containerColor = primary.copy(alpha = 0.6f),
+ contentColor = onPrimary
)
) {
Icon(
@@ -63,10 +71,15 @@ fun ButtonGroup(
) {
IconButton(
{ state.displayControls = !state.displayControls },
+ Modifier.liquidGlass(
+ iconButtonLiquidGlassStyle.copy
+ (whitePoint = if (state.displayControls) 0.5f else -0.5f)
+ ),
sizes = IconButtonSizes.medium,
- colors =
- if (state.displayControls) IconButtonColors.filled()
- else IconButtonColors.tonal()
+ colors = IconButtonColors.tonal(
+ containerColor = Color.Transparent,
+ contentColor = if (state.displayControls) Color.Black else Color.White
+ )
) {
Icon(
painterResource(
@@ -81,8 +94,12 @@ fun ButtonGroup(
}
IconButton(
{ state.configurationMode = ConfigurationMode.Colors },
+ Modifier.liquidGlass(iconButtonLiquidGlassStyle.copy(whitePoint = -0.5f)),
sizes = IconButtonSizes.medium,
- colors = IconButtonColors.tonal()
+ colors = IconButtonColors.tonal(
+ containerColor = Color.Transparent,
+ contentColor = Color.White
+ )
) {
Icon(
painterResource(R.drawable.colors_24px),
@@ -91,10 +108,11 @@ fun ButtonGroup(
}
IconButton(
{ state.configurationMode = ConfigurationMode.Advanced },
+ Modifier.liquidGlass(iconButtonLiquidGlassStyle.copy(whitePoint = -0.5f)),
sizes = IconButtonSizes.medium,
colors = IconButtonColors.tonal(
- containerColor = primaryContainer,
- contentColor = onPrimaryContainer
+ containerColor = Color.Transparent,
+ contentColor = Color.White
)
) {
Icon(
@@ -121,7 +139,12 @@ fun ButtonGroup(
)
)
},
- sizes = IconButtonSizes.medium
+ Modifier.liquidGlass(iconButtonLiquidGlassStyle),
+ sizes = IconButtonSizes.medium,
+ colors = IconButtonColors.tonal(
+ containerColor = primary.copy(alpha = 0.6f),
+ contentColor = onPrimary
+ )
) {
Icon(
painterResource(R.drawable.add_photo_alternate_24px),
diff --git a/app/src/main/java/com/kyant/glass/playground/SliderChip.kt b/app/src/main/java/com/kyant/glass/playground/SliderChip.kt
index 2576662..5595a7f 100644
--- a/app/src/main/java/com/kyant/glass/playground/SliderChip.kt
+++ b/app/src/main/java/com/kyant/glass/playground/SliderChip.kt
@@ -1,19 +1,24 @@
package com.kyant.glass.playground
import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+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.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.LayoutDirection.Ltr
import androidx.compose.ui.unit.dp
import com.kyant.expressa.m3.motion.MotionScheme
import com.kyant.expressa.m3.shape.CornerShape
@@ -21,6 +26,8 @@ import com.kyant.expressa.prelude.*
import com.kyant.expressa.ui.LocalTextStyle
import com.kyant.expressa.ui.ProvideTextStyle
import com.kyant.expressa.ui.Text
+import com.kyant.liquidglass.LiquidGlassStyle
+import com.kyant.liquidglass.liquidGlass
@Composable
fun > SliderChip(
@@ -30,19 +37,32 @@ fun > SliderChip(
modifier: Modifier = Modifier,
dragDelta: (dragAmount: Offset) -> Float = { it.x }
) {
- val containerColor = surfaceContainer
+ val layoutDirection = LocalLayoutDirection.current
+
val sliderColor = primaryContainer
val unsafeColors = yellowStaticColors
val errorColor = errorContainer
Row(
modifier
+ .liquidGlass(
+ remember {
+ LiquidGlassStyle(
+ shape = CornerShape.extraLarge,
+ chromaMultiplier = 1.5f
+ )
+ }
+ )
.clip(CornerShape.full)
+ .background(surfaceBright.copy(alpha = 0.6f))
.drawBehind {
if (value.isValid) {
- drawRect(containerColor)
+ // drawRect(containerColor)
drawRect(
if (value.isSafe) sliderColor else unsafeColors.accentContainer,
+ topLeft =
+ if (layoutDirection == Ltr) Offset.Zero
+ else Offset(size.width * (1f - value.progress), 0f),
size = size.copy(width = size.width * value.progress)
)
} else {
@@ -51,7 +71,7 @@ fun > SliderChip(
}
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
- val delta = dragDelta(dragAmount) / size.width
+ val delta = dragDelta(dragAmount) / size.width * if (layoutDirection == Ltr) 1f else -1f
value.setProgress(value.progress + delta)
}
}
@@ -74,20 +94,24 @@ fun > SliderChip(
if (label != null) labelMedium
else labelLarge
) {
- if (value.isValid) {
- Text(
- value.label,
- if (value.isSafe) primary else unsafeColors.accent
- )
- } else {
- Row {
+ CompositionLocalProvider(
+ LocalLayoutDirection provides Ltr
+ ) {
+ if (value.isValid) {
Text(
- value.unsafeLabel,
- LocalTextStyle.current.copy(textDecoration = TextDecoration.LineThrough),
- error
+ value.label,
+ if (value.isSafe) primary else unsafeColors.accent
)
- Text(" -> ")
- Text(value.label)
+ } else {
+ Row {
+ Text(
+ value.unsafeLabel,
+ LocalTextStyle.current.copy(textDecoration = TextDecoration.LineThrough),
+ error
+ )
+ Text(" -> ")
+ Text(value.label)
+ }
}
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 5c98ad0..5ca1837 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ab5c2af..1a0a91e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -20,5 +20,6 @@ androidx-graphics-shapes = { group = "androidx.graphics", name = "graphics-shape
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/library/.gitignore b/library/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/library/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
new file mode 100644
index 0000000..e6a95c0
--- /dev/null
+++ b/library/build.gradle.kts
@@ -0,0 +1,46 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "com.kyant.liquidglass"
+ 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 {
+ apiVersion = KotlinVersion.KOTLIN_2_3
+ languageVersion = KotlinVersion.KOTLIN_2_3
+ jvmTarget = JvmTarget.JVM_21
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+}
diff --git a/library/consumer-rules.pro b/library/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/library/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/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/library/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt b/library/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt
new file mode 100644
index 0000000..175f958
--- /dev/null
+++ b/library/src/main/java/com/kyant/liquidglass/LiquidGlassModifier.kt
@@ -0,0 +1,153 @@
+package com.kyant.liquidglass
+
+import android.graphics.BlendMode
+import android.graphics.RenderEffect
+import android.graphics.Shader
+import android.os.Build
+import androidx.compose.foundation.border
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asComposeRenderEffect
+import androidx.compose.ui.graphics.drawscope.withTransform
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Modifier.liquidGlass(
+ style: LiquidGlassStyle,
+ providerState: LiquidGlassProviderState = LocalLiquidGlassProviderState.current,
+ state: LiquidGlassState = rememberLiquidGlassState()
+): Modifier =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ this
+ .border(
+ width = 1.dp,
+ color = Color.White.copy(alpha = 0.2f),
+ shape = style.shape
+ )
+ .graphicsLayer {
+ clip = true
+ shape = style.shape
+ }
+ .drawWithCache {
+ val contentBlurRadiusPx = style.blurRadius.toPx()
+ val contentRenderEffect =
+ if (contentBlurRadiusPx > 0f) {
+ RenderEffect.createBlurEffect(
+ contentBlurRadiusPx,
+ contentBlurRadiusPx,
+ Shader.TileMode.DECAL
+ )
+ } else {
+ RenderEffect.createOffsetEffect(0f, 0f)
+ }
+
+ val cornerRadiusPx = style.shape.topStart.toPx(size, this)
+
+ val refractionRenderEffect =
+ RenderEffect.createChainEffect(
+ RenderEffect.createRuntimeShaderEffect(
+ state.refractionShader.apply {
+ setFloatUniform("size", size.width, size.height)
+ setFloatUniform("cornerRadius", cornerRadiusPx)
+
+ setFloatUniform("refractionHeight", style.refractionHeight.toPx())
+ setFloatUniform("refractionAmount", style.refractionAmount.toPx())
+ setFloatUniform("eccentricFactor", style.eccentricFactor)
+
+ setFloatUniform("bleedOpacity", style.bleedOpacity)
+ },
+ "image"
+ ),
+ contentRenderEffect
+ )
+
+ val refractionAndBleedRenderEffect =
+ if (style.bleedOpacity > 0f) {
+ val bleedRenderEffect =
+ RenderEffect.createChainEffect(
+ RenderEffect.createRuntimeShaderEffect(
+ state.bleedShader.apply {
+ setFloatUniform("size", size.width, size.height)
+ setFloatUniform("cornerRadius", cornerRadiusPx)
+
+ setFloatUniform("eccentricFactor", style.eccentricFactor)
+ setFloatUniform("bleedAmount", style.bleedAmount.toPx())
+ },
+ "image"
+ ),
+ contentRenderEffect
+ )
+
+ val bleedBlurRadiusPx = style.bleedBlurRadius.toPx()
+ val blurredBleedRenderEffect =
+ if (bleedBlurRadiusPx > 0f) {
+ RenderEffect.createChainEffect(
+ bleedRenderEffect,
+ RenderEffect.createBlurEffect(
+ bleedBlurRadiusPx,
+ bleedBlurRadiusPx,
+ Shader.TileMode.CLAMP
+ )
+ )
+ } else {
+ bleedRenderEffect
+ }
+
+ RenderEffect.createBlendModeEffect(
+ blurredBleedRenderEffect,
+ refractionRenderEffect,
+ BlendMode.SRC_OVER
+ )
+ } else {
+ refractionRenderEffect
+ }
+
+ val contrast = style.contrast
+ val whitePoint = style.whitePoint
+ val chromaMultiplier = style.chromaMultiplier
+ val renderEffect =
+ if (contrast != 0f || whitePoint != 0f || chromaMultiplier != 1f) {
+ RenderEffect.createChainEffect(
+ RenderEffect.createRuntimeShaderEffect(
+ state.colorManipulationShader.apply {
+ setFloatUniform("contrast", contrast)
+ setFloatUniform("whitePoint", whitePoint)
+ setFloatUniform("chromaMultiplier", chromaMultiplier)
+ },
+ "image"
+ ),
+ refractionAndBleedRenderEffect
+ ).asComposeRenderEffect()
+ } else {
+ refractionAndBleedRenderEffect.asComposeRenderEffect()
+ }
+
+ state.graphicsLayer.renderEffect = renderEffect
+
+ onDrawBehind {
+ val rect = state.rect ?: return@onDrawBehind
+ state.graphicsLayer.record {
+ withTransform({
+ clipRect(0f, 0f, size.width, size.height)
+ translate(-rect.left, -rect.top)
+ }) {
+ drawLayer(providerState.graphicsLayer)
+ }
+ }
+ drawLayer(state.graphicsLayer)
+ }
+ }
+ .onGloballyPositioned { layoutCoordinates ->
+ state.rect = providerState.rect?.let {
+ layoutCoordinates.boundsInRoot().translate(-it.topLeft)
+ }
+ }
+ } else {
+ this
+ }
diff --git a/library/src/main/java/com/kyant/liquidglass/LiquidGlassProviderState.kt b/library/src/main/java/com/kyant/liquidglass/LiquidGlassProviderState.kt
new file mode 100644
index 0000000..50549c6
--- /dev/null
+++ b/library/src/main/java/com/kyant/liquidglass/LiquidGlassProviderState.kt
@@ -0,0 +1,58 @@
+package com.kyant.liquidglass
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.rememberGraphicsLayer
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+
+val LocalLiquidGlassProviderState: ProvidableCompositionLocal =
+ staticCompositionLocalOf { error("CompositionLocal LocalLiquidGlassProviderState not present") }
+
+@Composable
+fun rememberLiquidGlassProviderState(): LiquidGlassProviderState {
+ val graphicsLayer = rememberGraphicsLayer()
+ return remember(graphicsLayer) {
+ LiquidGlassProviderState(graphicsLayer)
+ }
+}
+
+class LiquidGlassProviderState
+internal constructor(
+ internal val graphicsLayer: GraphicsLayer,
+) {
+
+ internal var rect: Rect? by mutableStateOf(null)
+}
+
+fun Modifier.liquidGlassProvider(
+ state: LiquidGlassProviderState,
+ backgroundColor: Color? = null
+): Modifier =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ this
+ .drawWithContent {
+ drawContent()
+
+ state.graphicsLayer.record {
+ backgroundColor?.let { drawRect(it) }
+ this@drawWithContent.drawContent()
+ }
+ }
+ .onGloballyPositioned { layoutCoordinates ->
+ state.rect = layoutCoordinates.boundsInRoot()
+ }
+ } else {
+ this
+ }
diff --git a/library/src/main/java/com/kyant/liquidglass/LiquidGlassShaders.kt b/library/src/main/java/com/kyant/liquidglass/LiquidGlassShaders.kt
new file mode 100644
index 0000000..cd5bb78
--- /dev/null
+++ b/library/src/main/java/com/kyant/liquidglass/LiquidGlassShaders.kt
@@ -0,0 +1,149 @@
+package com.kyant.liquidglass
+
+import org.intellij.lang.annotations.Language
+
+internal object LiquidGlassShaders {
+
+ @Language("AGSL")
+ private val colorShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
+ const half3 rgbToY = half3(0.2126, 0.7152, 0.0722);
+
+ float luma(half4 color) {
+ return dot(toLinearSrgb(color.rgb), rgbToY);
+ }
+ """.trimIndent()
+
+ @Language("AGSL")
+ private val refractionShaderUtils = """// This file belongs to Kyant. You must not use it without permission.
+ float circleMap(float x) {
+ return 1.0 - sqrt(1.0 - x * x);
+ }
+
+ float sdRectangle(float2 coord, float2 halfSize) {
+ float2 d = abs(coord) - halfSize;
+ float outside = length(max(d, 0.0));
+ float inside = min(max(d.x, d.y), 0.0);
+ return outside + inside;
+ }
+
+ float sdRoundedRectangle(float2 coord, float2 halfSize, float cornerRadius) {
+ float2 innerHalfSize = halfSize - float2(cornerRadius);
+ return sdRectangle(coord, innerHalfSize) - cornerRadius;
+ }
+
+ float2 gradSdRoundedRectangle(float2 coord, float2 halfSize, float cornerRadius) {
+ float2 innerHalfSize = halfSize - float2(cornerRadius);
+ float2 cornerCoord = abs(coord) - innerHalfSize;
+
+ if (cornerCoord.x >= 0.0 && cornerCoord.y >= 0.0) {
+ return sign(coord) * normalize(cornerCoord);
+ } else {
+ return sign(coord) * ((-cornerCoord.x < -cornerCoord.y) ? float2(1.0, 0.0) : float2(0.0, 1.0));
+ }
+ }
+
+ half4 refractionColor(float2 coord, float2 size, float cornerRadius, float eccentricFactor, float height, float amount) {
+ float2 halfSize = size * 0.5;
+ float2 centeredCoord = coord - halfSize;
+ float sd = sdRoundedRectangle(centeredCoord, halfSize, cornerRadius);
+ sd = min(sd, 0.0);
+
+ if (sd <= 0.0 && -sd <= height) {
+ float maxGradRadius = max(min(halfSize.x, halfSize.y), cornerRadius);
+ float gradRadius = min(cornerRadius * 1.5, maxGradRadius);
+ float2 normal = gradSdRoundedRectangle(centeredCoord, halfSize, gradRadius);
+
+ float refractedDistance = circleMap(1.0 - -sd / height) * amount;
+ float2 refractedDirection = normalize(normal + eccentricFactor * normalize(centeredCoord));
+ float2 refractedCoord = coord + refractedDistance * refractedDirection;
+ if (refractedCoord.x < 0.0 || refractedCoord.x >= size.x ||
+ refractedCoord.y < 0.0 || refractedCoord.y >= size.y) {
+ return half4(0.0, 0.0, 0.0, 1.0);
+ }
+
+ return image.eval(refractedCoord);
+ } else {
+ return image.eval(coord);
+ }
+ }
+ """.trimIndent()
+
+ @Language("AGSL")
+ val refractionShaderString = """// This file belongs to Kyant. You must not use it without permission.
+ uniform shader image;
+
+ uniform float2 size;
+ uniform float cornerRadius;
+
+ uniform float refractionHeight;
+ uniform float refractionAmount;
+ uniform float eccentricFactor;
+
+ uniform float bleedOpacity;
+
+ $colorShaderUtils
+ $refractionShaderUtils
+
+ half4 main(float2 coord) {
+ half4 color = refractionColor(coord, size, cornerRadius, eccentricFactor, refractionHeight, refractionAmount);
+ if (bleedOpacity <= 0.0) {
+ return color;
+ } else {
+ float luma = luma(color);
+ color *= 1.0 - bleedOpacity * luma;
+ return color;
+ }
+ }"""
+
+ @Language("AGSL")
+ val bleedShaderString = """// This file belongs to Kyant. You must not use it without permission.
+ uniform shader image;
+
+ uniform float2 size;
+ uniform float cornerRadius;
+
+ uniform float eccentricFactor;
+ uniform float bleedAmount;
+
+ $colorShaderUtils
+ $refractionShaderUtils
+
+ half4 main(float2 coord) {
+ half4 color = refractionColor(coord, size, cornerRadius, eccentricFactor, cornerRadius * 3.5, bleedAmount);
+ float luma = luma(color);
+ color.rgb = mix(color.rgb, half3(1.0), 0.5 * circleMap(1.0 - luma));
+ return color;
+ }"""
+
+ @Language("AGSL")
+ val colorManipulationShaderString = """// This file belongs to Kyant. You must not use it without permission.
+ uniform shader image;
+
+ uniform float contrast;
+ uniform float whitePoint;
+ uniform float chromaMultiplier;
+
+ $colorShaderUtils
+
+ half4 saturateColor(half4 color, float amount) {
+ half3 linearSrgb = toLinearSrgb(color.rgb);
+ float y = dot(linearSrgb, rgbToY);
+ half3 gray = half3(y);
+ half3 adjustedLinearSrgb = mix(gray, linearSrgb, amount);
+ half3 adjustedSrgb = fromLinearSrgb(adjustedLinearSrgb);
+ return half4(adjustedSrgb, color.a);
+ }
+
+ half4 main(float2 coord) {
+ half4 color = image.eval(coord);
+
+ color = saturateColor(color, chromaMultiplier);
+
+ float3 target = (whitePoint > 0.0) ? float3(1.0) : float3(0.0);
+ color.rgb = mix(color.rgb, target, abs(whitePoint));
+
+ color.rgb = (color.rgb - 0.5) * (1.0 + contrast) + 0.5;
+
+ return color;
+ }"""
+}
diff --git a/library/src/main/java/com/kyant/liquidglass/LiquidGlassState.kt b/library/src/main/java/com/kyant/liquidglass/LiquidGlassState.kt
new file mode 100644
index 0000000..2de3dc9
--- /dev/null
+++ b/library/src/main/java/com/kyant/liquidglass/LiquidGlassState.kt
@@ -0,0 +1,59 @@
+package com.kyant.liquidglass
+
+import android.graphics.RuntimeShader
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.rememberGraphicsLayer
+
+@Composable
+fun rememberLiquidGlassState(): LiquidGlassState {
+ val graphicsLayer = rememberGraphicsLayer()
+ return remember(graphicsLayer) {
+ LiquidGlassState(graphicsLayer)
+ }
+}
+
+class LiquidGlassState internal constructor(
+ internal val graphicsLayer: GraphicsLayer
+) {
+
+ internal var rect: Rect? by mutableStateOf(null)
+
+ private var _colorManipulationShader: RuntimeShader? = null
+ private var _refractionShader: RuntimeShader? = null
+ private var _bleedShader: RuntimeShader? = null
+
+ internal val colorManipulationShader: RuntimeShader
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ get() {
+ if (_colorManipulationShader == null) {
+ _colorManipulationShader = RuntimeShader(LiquidGlassShaders.colorManipulationShaderString)
+ }
+ return _colorManipulationShader!!
+ }
+
+ internal val refractionShader: RuntimeShader
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ get() {
+ if (_refractionShader == null) {
+ _refractionShader = RuntimeShader(LiquidGlassShaders.refractionShaderString)
+ }
+ return _refractionShader!!
+ }
+
+ internal val bleedShader: RuntimeShader
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ get() {
+ if (_bleedShader == null) {
+ _bleedShader = RuntimeShader(LiquidGlassShaders.bleedShaderString)
+ }
+ return _bleedShader!!
+ }
+}
diff --git a/library/src/main/java/com/kyant/liquidglass/LiquidGlassStyle.kt b/library/src/main/java/com/kyant/liquidglass/LiquidGlassStyle.kt
new file mode 100644
index 0000000..5ebaaaf
--- /dev/null
+++ b/library/src/main/java/com/kyant/liquidglass/LiquidGlassStyle.kt
@@ -0,0 +1,173 @@
+package com.kyant.liquidglass
+
+import androidx.annotation.FloatRange
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Suppress("FunctionName")
+fun LiquidGlassStyle(
+ shape: CornerBasedShape,
+ blurRadius: Dp = 2.dp,
+
+ refractionHeight: Dp = 8.dp,
+ refractionAmount: Dp = (-16).dp,
+ @FloatRange(from = 0.0, to = 1.0) eccentricFactor: Float = 1f,
+
+ bleedAmount: Dp = 0.dp,
+ bleedBlurRadius: Dp = 0.dp,
+ @FloatRange(from = 0.0, to = 1.0) bleedOpacity: Float = 0f,
+
+ @FloatRange(from = -1.0, to = 1.0) contrast: Float = 0f,
+ @FloatRange(from = -1.0, to = 1.0) whitePoint: Float = 0f,
+ @FloatRange(from = 0.5, to = 2.0) chromaMultiplier: Float = 1f
+): ImmutableLiquidGlassStyle =
+ ImmutableLiquidGlassStyle(
+ shape = shape,
+ blurRadius = blurRadius,
+ refractionHeight = refractionHeight,
+ refractionAmount = refractionAmount,
+ eccentricFactor = eccentricFactor,
+ bleedAmount = bleedAmount,
+ bleedBlurRadius = bleedBlurRadius,
+ bleedOpacity = bleedOpacity,
+ contrast = contrast,
+ whitePoint = whitePoint,
+ chromaMultiplier = chromaMultiplier
+ )
+
+fun MutableLiquidGlassState(
+ shape: CornerBasedShape,
+ blurRadius: Dp = 2.dp,
+
+ refractionHeight: Dp = 8.dp,
+ refractionAmount: Dp = (-16).dp,
+ @FloatRange(from = 0.0, to = 1.0) eccentricFactor: Float = 1f,
+
+ bleedAmount: Dp = 0.dp,
+ bleedBlurRadius: Dp = 0.dp,
+ @FloatRange(from = 0.0, to = 1.0) bleedOpacity: Float = 0f,
+
+ @FloatRange(from = -1.0, to = 1.0) contrast: Float = 0f,
+ @FloatRange(from = -1.0, to = 1.0) whitePoint: Float = 0f,
+ @FloatRange(from = 0.5, to = 2.0) chromaMultiplier: Float = 1f
+): MutableLiquidGlassState =
+ MutableLiquidGlassState(
+ initialStyle = ImmutableLiquidGlassStyle(
+ shape,
+ blurRadius,
+ refractionHeight,
+ refractionAmount,
+ eccentricFactor,
+ bleedAmount,
+ bleedBlurRadius,
+ bleedOpacity,
+ contrast,
+ whitePoint,
+ chromaMultiplier
+ )
+ )
+
+sealed interface LiquidGlassStyle {
+
+ val shape: CornerBasedShape
+
+ val blurRadius: Dp
+
+ val refractionHeight: Dp
+
+ val refractionAmount: Dp
+
+ @get:FloatRange(from = 0.0, to = 1.0)
+ val eccentricFactor: Float
+
+ val bleedAmount: Dp
+
+ val bleedBlurRadius: Dp
+
+ @get:FloatRange(from = 0.0, to = 1.0)
+ val bleedOpacity: Float
+
+ @get:FloatRange(from = -1.0, to = 1.0)
+ val contrast: Float
+
+ @get:FloatRange(from = -1.0, to = 1.0)
+ val whitePoint: Float
+
+ @get:FloatRange(from = 0.5, to = 2.0)
+ val chromaMultiplier: Float
+}
+
+class ImmutableLiquidGlassStyle
+internal constructor(
+ override val shape: CornerBasedShape,
+ override val blurRadius: Dp = 2.dp,
+
+ override val refractionHeight: Dp = 8.dp,
+ override val refractionAmount: Dp = (-16).dp,
+ @param:FloatRange(from = 0.0, to = 1.0) override val eccentricFactor: Float = 1f,
+
+ override val bleedAmount: Dp = 0.dp,
+ override val bleedBlurRadius: Dp = 0.dp,
+ @param:FloatRange(from = 0.0, to = 1.0) override val bleedOpacity: Float = 0f,
+
+ @param:FloatRange(from = -1.0, to = 1.0) override val contrast: Float = 0f,
+ @param:FloatRange(from = -1.0, to = 1.0) override val whitePoint: Float = 0f,
+ @param:FloatRange(from = 0.5, to = 2.0) override val chromaMultiplier: Float = 1f
+) : LiquidGlassStyle {
+
+ fun copy(
+ shape: CornerBasedShape = this.shape,
+ blurRadius: Dp = this.blurRadius,
+
+ refractionHeight: Dp = this.refractionHeight,
+ refractionAmount: Dp = this.refractionAmount,
+ @FloatRange(from = 0.0, to = 1.0) eccentricFactor: Float = this.eccentricFactor,
+
+ bleedAmount: Dp = this.bleedAmount,
+ bleedBlurRadius: Dp = this.bleedBlurRadius,
+ @FloatRange(from = 0.0, to = 1.0) bleedOpacity: Float = this.bleedOpacity,
+
+ @FloatRange(from = -1.0, to = 1.0) contrast: Float = this.contrast,
+ @FloatRange(from = -1.0, to = 1.0) whitePoint: Float = this.whitePoint,
+ @FloatRange(from = 0.5, to = 2.0) chromaMultiplier: Float = this.chromaMultiplier
+ ): ImmutableLiquidGlassStyle =
+ ImmutableLiquidGlassStyle(
+ shape = shape,
+ blurRadius = blurRadius,
+ refractionHeight = refractionHeight,
+ refractionAmount = refractionAmount,
+ eccentricFactor = eccentricFactor,
+ bleedAmount = bleedAmount,
+ bleedBlurRadius = bleedBlurRadius,
+ bleedOpacity = bleedOpacity,
+ contrast = contrast,
+ whitePoint = whitePoint,
+ chromaMultiplier = chromaMultiplier
+ )
+}
+
+class MutableLiquidGlassState
+internal constructor(
+ val initialStyle: LiquidGlassStyle
+) : LiquidGlassStyle {
+
+ override var shape: CornerBasedShape by mutableStateOf(initialStyle.shape)
+ override var blurRadius: Dp by mutableStateOf(initialStyle.blurRadius)
+
+ override var refractionHeight: Dp by mutableStateOf(initialStyle.refractionHeight)
+ override var refractionAmount: Dp by mutableStateOf(initialStyle.refractionAmount)
+ override var eccentricFactor: Float by mutableFloatStateOf(initialStyle.eccentricFactor)
+
+ override var bleedAmount: Dp by mutableStateOf(initialStyle.bleedAmount)
+ override var bleedBlurRadius: Dp by mutableStateOf(initialStyle.bleedBlurRadius)
+ override var bleedOpacity: Float by mutableFloatStateOf(initialStyle.bleedOpacity)
+
+ override var contrast: Float by mutableFloatStateOf(initialStyle.contrast)
+ override var whitePoint: Float by mutableFloatStateOf(initialStyle.whitePoint)
+ override var chromaMultiplier: Float by mutableFloatStateOf(initialStyle.chromaMultiplier)
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8ece87e..ff6489a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,4 +21,4 @@ dependencyResolutionManagement {
rootProject.name = "Glass"
include(":app")
-
\ No newline at end of file
+include(":library")