Glitch effect

Glitch effect

Subscribe for Live Previews, in your browser

$3 / month

Code

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.graphics.withSaveLayer
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.example.web_demos.generated.resources.Res
import com.example.web_demos.generated.resources.space_mono_regular
import org.jetbrains.compose.resources.Font
import theme.Colors
import theme.Colors.Green500
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextInt

@Composable
fun Modifier.glitchEffect(
    key: Any? = null,
    glitchColors: List<Color> = listOf(Green500),
    slices: Int = 20,
): Modifier {

    val graphicsLayer = rememberGraphicsLayer()
    var step by remember { mutableStateOf(0) }

    LaunchedEffect(key) {
        Animatable(10f)
            .animateTo(
                targetValue = 0f,
                animationSpec = tween(
                    durationMillis = 500,
                    easing = LinearEasing,
                )
            ) {
                step = this.value.roundToInt()
            }
    }

    return drawWithContent {
        if (step == 0) {
            drawContent()
            return@drawWithContent
        }
        graphicsLayer.record { this@drawWithContent.drawContent() }

        val intensity = step / 10f
        for (i in 0 until slices) {
            translate(
                left = if (Random.nextInt(5) < step)
                    Random.nextInt(-20..20).toFloat() * intensity
                else
                    0f
            ) {
                scale(
                    scaleY = 1f,
                    scaleX = if (Random.nextInt(10) < step)
                        1f + (1f * Random.nextFloat() * intensity)
                    else
                        1f
                ) {
                    clipRect(
                        top = (i / slices.toFloat()) * size.height,
                        bottom = (((i + 1) / slices.toFloat()) * size.height) + 1f,
                    ) {
                        layer {
                            drawLayer(graphicsLayer)
                            if (Random.nextInt(5, 30) < step) {
                                drawRect(
                                    color = glitchColors.random(),
                                    blendMode = BlendMode.SrcAtop
                                )
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun GlitchEffectImpl() {

    var key by remember { mutableStateOf(0) }
    val interaction = remember { MutableInteractionSource() }
    val isHovered by interaction.collectIsHoveredAsState()

    Box(
        modifier = Modifier
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = {
                        key = Random.nextInt()
                    }
                )
            }
            .pointerHoverIcon(PointerIcon.Hand)
            .hoverable(interaction)
            .glitchEffect(
                key,
                remember { listOf(Colors.Cyan500, Colors.Yellow500, Colors.Pink500) })
            .padding(4.dp)
            .rings(
                ringSpace = if (isHovered) 4.dp else 2.dp
            )
            .background(
                brush = Brush.verticalGradient(
                    colors = listOf(Colors.Zinc950, Colors.Zinc900)
                ),
                shape = CutCornerShape(20),
            )
            .padding(horizontal = 32.dp, vertical = 16.dp)
    ) {
        Text(
            text = "Tap to Glitch",
            style = TextStyle(
                color = Colors.Red500,
                fontFamily = FontFamily(
                    Font(
                        resource = Res.font.space_mono_regular,
                        weight = FontWeight.Normal,
                        style = FontStyle.Normal,
                    )
                )
            )
        )
    }

}

@Composable
fun Modifier.rings(
    ringColor: Color = Colors.Red500,
    ringCount: Int = 6,
    ringSpace: Dp = 2.dp
): Modifier {

    val animatedRingSpace by animateDpAsState(
        targetValue = ringSpace,
        animationSpec = tween()
    )

    return (1..ringCount).map { index ->
        Modifier.border(
            width = 0.dp,
            color = ringColor.copy(alpha = index / ringCount.toFloat()),
            shape = CutCornerShape(20),
        )
            .padding(animatedRingSpace)
    }.fold(initial = this) { acc, item -> acc.then(item) }
}

private fun DrawScope.layer(block: DrawScope.() -> Unit) =
    drawIntoCanvas { canvas ->
        canvas.withSaveLayer(
            bounds = size.toRect(),
            paint = Paint(),
        ) { block() }
    }
Mastodon