Skip to content

6xingyv/gaze-glassy

Repository files navigation

Gaze Glassy

Maven Central Telegram

📦 Repository

Gaze released a group of artifacts, including:

  • glassy: Liquid Glass effect library for Compose Multiplatform.

  • capsule: G2 continuous rounded rectangles for Compose Multiplatform.

This repository hosts the glassy code.


🧩 Modules

:core

Provides cross-platform RuntimeShader & RenderEffect implementation.

With gaze-glassy-core, you can write shader code once and have it run on all supported platforms, including Android, iOS, Desktop, and Web. This is achieved by leveraging platform-specific APIs under the hood while providing a unified interface for developers.

:liquid:effect

The Liquid Glass effect library.

This module is the direct downstream project of Kyant's Backdrop (a.k.a. AndroidLiquidGlass). It migrates the original Jetpack Compose implementation to Compose Multiplatform.

:liquid:settings

Settings management for the liquid effect.

  • :liquid:settings:core: Core logic and data models for settings.
  • :liquid:settings:client: Client-side implementation for applying settings.
  • :liquid:settings:configurator: UI/Tooling for configuring the effect parameters.

Example usage of RuntimeShader and RenderEffect:

private const val RippleShaderString = """
uniform shader content;
uniform float2 iResolution;
uniform float rippleData[40];
uniform int rippleCount;
uniform float amplitude;
uniform float frequency;
uniform float decay;
uniform float speed;

float2 calculateRippleOffset(float2 position, float2 origin, float time) {
    float distance = length(position - origin);
    float delay = distance / speed;
    float adjustedTime = max(0.0, time - delay);
    float rippleAmount = amplitude * sin(frequency * adjustedTime) * exp(-decay * adjustedTime);
    return rippleAmount * normalize(position - origin);
}

float calculateBrightness(float2 position, float2 origin, float time) {
    float distance = length(position - origin);
    float delay = distance / speed;
    float adjustedTime = max(0.0, time - delay);
    float rippleAmount = amplitude * sin(frequency * adjustedTime) * exp(-decay * adjustedTime);
    return 0.3 * (rippleAmount / amplitude) * exp(-decay * adjustedTime);
}

half4 main(float2 fragCoord) {
    float2 position = fragCoord;
    float2 totalOffset = float2(0.0, 0.0);
    float totalBrightness = 0.0;

    if (rippleCount > 0) {
        float2 origin0 = float2(rippleData[0], rippleData[1]);
        totalOffset += calculateRippleOffset(position, origin0, rippleData[2]);
        totalBrightness += calculateBrightness(position, origin0, rippleData[2]);
    }
    if (rippleCount > 1) {
        float2 origin1 = float2(rippleData[4], rippleData[5]);
        totalOffset += calculateRippleOffset(position, origin1, rippleData[6]);
        totalBrightness += calculateBrightness(position, origin1, rippleData[6]);
    }
    if (rippleCount > 2) {
        float2 origin2 = float2(rippleData[8], rippleData[9]);
        totalOffset += calculateRippleOffset(position, origin2, rippleData[10]);
        totalBrightness += calculateBrightness(position, origin2, rippleData[10]);
    }
    if (rippleCount > 3) {
        float2 origin3 = float2(rippleData[12], rippleData[13]);
        totalOffset += calculateRippleOffset(position, origin3, rippleData[14]);
        totalBrightness += calculateBrightness(position, origin3, rippleData[14]);
    }
    if (rippleCount > 4) {
        float2 origin4 = float2(rippleData[16], rippleData[17]);
        totalOffset += calculateRippleOffset(position, origin4, rippleData[18]);
        totalBrightness += calculateBrightness(position, origin4, rippleData[18]);
    }
    if (rippleCount > 5) {
        float2 origin5 = float2(rippleData[20], rippleData[21]);
        totalOffset += calculateRippleOffset(position, origin5, rippleData[22]);
        totalBrightness += calculateBrightness(position, origin5, rippleData[22]);
    }
    if (rippleCount > 6) {
        float2 origin6 = float2(rippleData[24], rippleData[25]);
        totalOffset += calculateRippleOffset(position, origin6, rippleData[26]);
        totalBrightness += calculateBrightness(position, origin6, rippleData[26]);
    }
    if (rippleCount > 7) {
        float2 origin7 = float2(rippleData[28], rippleData[29]);
        totalOffset += calculateRippleOffset(position, origin7, rippleData[30]);
        totalBrightness += calculateBrightness(position, origin7, rippleData[30]);
    }
    if (rippleCount > 8) {
        float2 origin8 = float2(rippleData[32], rippleData[33]);
        totalOffset += calculateRippleOffset(position, origin8, rippleData[34]);
        totalBrightness += calculateBrightness(position, origin8, rippleData[34]);
    }
    if (rippleCount > 9) {
        float2 origin9 = float2(rippleData[36], rippleData[37]);
        totalOffset += calculateRippleOffset(position, origin9, rippleData[38]);
        totalBrightness += calculateBrightness(position, origin9, rippleData[38]);
    }

    float2 newPosition = position + totalOffset;
    half4 color = content.eval(newPosition);
    color.rgb += totalBrightness * color.a;

    return color;
}
"""

private data class RippleState(
    val position: Offset,
    val animatable: Animatable<Float, *>
)

private class RippleIndicationNode(
    private val interactionSource: InteractionSource
) : Modifier.Node(), DrawModifierNode {

    override val shouldAutoInvalidate: Boolean = false

    private val activeRipples = mutableListOf<RippleState>()

    private val amplitude: Float = 10f
    private val frequency: Float = 8f
    private val decay: Float = 1.5f
    private val speed: Float = 800f
    private val maxRipples: Int = 10

    private var contentLayer: GraphicsLayer? = null
    private var runtimeShader = if (PlatformVersion.supportsRuntimeShader()) {
        try {
            val shader = createRuntimeShader(RippleShaderString)
            shader
        } catch (e: Exception) {
            null
        }
    } else {
        null
    }

    private var currentSize: androidx.compose.ui.geometry.Size? = null

    private fun calculateDuration(componentSize: androidx.compose.ui.geometry.Size, pressPosition: Offset): Int {
        val corners = listOf(
            Offset(0f, 0f),
            Offset(componentSize.width, 0f),
            Offset(0f, componentSize.height),
            Offset(componentSize.width, componentSize.height)
        )

        val maxDistance = corners.maxOf { corner ->
            kotlin.math.sqrt(
                (corner.x - pressPosition.x) * (corner.x - pressPosition.x) +
                        (corner.y - pressPosition.y) * (corner.y - pressPosition.y)
            )
        }

        val propagationTime = (maxDistance / speed) * 1000
        val decayTime = (3 / decay) * 1000
        return (propagationTime + decayTime).toInt().coerceAtLeast(800)
    }

    private suspend fun animateRipple(pressPosition: Offset, componentSize: androidx.compose.ui.geometry.Size) {
        val duration = calculateDuration(componentSize, pressPosition)
        val animatable = Animatable(0f)
        val ripple = RippleState(pressPosition, animatable)

        if (activeRipples.size >= maxRipples) {
            activeRipples.removeAt(0)
        }
        activeRipples.add(ripple)

        animatable.animateTo(
            targetValue = duration / 1000f,
            animationSpec = tween(durationMillis = duration, easing = LinearEasing)
        ) {
            invalidateDraw()
        }

        activeRipples.remove(ripple)
        invalidateDraw()
    }

    override fun onAttach() {
        val graphicsContext = requireGraphicsContext()
        contentLayer = graphicsContext.createGraphicsLayer()

        coroutineScope.launch {
            interactionSource.interactions.collect { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> {
                        currentSize?.let { size ->
                            launch {
                                animateRipple(interaction.pressPosition, size)
                            }
                        }
                    }
                    is PressInteraction.Release -> {}
                    is PressInteraction.Cancel -> {}
                }
            }
        }
    }

    override fun onDetach() {
        val graphicsContext = requireGraphicsContext()
        contentLayer?.let { layer ->
            graphicsContext.releaseGraphicsLayer(layer)
            contentLayer = null
        }
    }

    override fun ContentDrawScope.draw() {
        currentSize = size

        val layer = contentLayer
        val shader = runtimeShader

        if (layer == null || shader == null || !PlatformVersion.supportsRuntimeShader()) {
            drawContent()
            return
        }

        layer.record(size = size.toIntSize()) {
            this@draw.drawContent()
        }

        if (activeRipples.isNotEmpty()) {
            val rippleData = FloatArray(40)
            activeRipples.take(10).forEachIndexed { index, ripple ->
                val baseIndex = index * 4
                rippleData[baseIndex] = ripple.position.x
                rippleData[baseIndex + 1] = ripple.position.y
                rippleData[baseIndex + 2] = ripple.animatable.value
            }

            shader.apply {
                setFloatUniform("iResolution", size.width, size.height)
                setIntUniform("rippleCount", activeRipples.size.coerceAtMost(10))
                setFloatUniform("amplitude", amplitude)
                setFloatUniform("frequency", frequency)
                setFloatUniform("decay", decay)
                setFloatUniform("speed", speed)
                setFloatUniform("rippleData", rippleData)
            }

            val effect = createRuntimeShaderEffect(shader, "content")
            if (effect != null) {
                val composeEffect = convertToComposeRenderEffect(effect)
                if (composeEffect != null) {
                    layer.renderEffect = composeEffect
                }
            }
        } else {
            layer.renderEffect = null
        }

        drawLayer(layer)
    }

}

object RippleIndication : IndicationNodeFactory {
    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return RippleIndicationNode(interactionSource)
    }

    override fun hashCode(): Int = -1

    override fun equals(other: Any?) = other === this

}

About

Liquid glass for Compose multiplatform, and unified code for shader

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages