Glitch Effect in Jetpack Compose

Glitch Effect in Jetpack Compose

Easily apply sci-fi, cyberpunk effects on to your app

Glitch effects are a staple in sci-fi movies and cyberpunk games. In this article, we will see how easy it is to implement this effect in Jetpack Compose.

0:00
/0:01

Final Glitch effect

This is way less intimidating than it looks. Plus it offers lots of opportunity for random creative additions.

Graphics Layer

Since we will be drawing multiple copies of our Composable, we need to find a way to do this cheaply, performance-wise.
For this, we can use a graphicsLayer.

val graphicsLayer = rememberGraphicsLayer()

After creating one, we can "record" our content in a drawWithContent Modifier.

modifier = Modifier  
    .drawWithContent {  
        graphicsLayer.record { this@drawWithContent.drawContent() }  
        drawLayer(graphicsLayer)
    }

This small but powerful lines of code draws the content on an offscreen buffer which we have to draw manually using drawLayer. The benefit of this is that we can call drawLayer multiple times, but with much less performance overhead than rendering multiple composables.
On top of this, we have all our regular canvas functions to manipulate our content.

Slicing

Now let's cut our view into multiple horizontal slices. We will later apply random translations and scale onto each slice.

graphicsLayer.record { this@drawWithContent.drawContent() }  
for (i in 0 until slices) {  
    clipRect(  
        top = (i / slices.toFloat()) * size.height,  
        bottom = (((i + 1) / slices.toFloat()) * size.height) + 1f,  
    ) {  
        drawLayer(graphicsLayer)  
    }  
}

Here we are creating slices by calling drawLayer multiple times, but with a different clipping.
Each clip's position and size is calculated based on the amount of slices we have.
I also added 1px to the height to avoid any weird gaps.

0:00
/0:03

Illustration of the composable being sliced

Adding Chaos

This is where you can get crazy with all the random mutations you can apply. In each slice, we will add a random translation (x-axis), scale (x-axis) and a random color overlay.
But you can of course experiment with many more variations.

translate(  
    left = if (Random.nextInt(5) < step)  
        Random.nextInt(-20..20).toFloat() * intensity  
    else  
        0f  
) {
	// ...
}

We apply these mutations by wrapping our clipRect with other functions. First, let's apply a translation along the x-axis using translate(). This will randomly apply an offset within the given range.

scale(  
    scaleY = 1f,  
    scaleX = if (Random.nextInt(10) < step)  
        1f + (1f * Random.nextFloat() * intensity)  
    else  
        1f  
) {
	// ...
}

Next we have scale() that scales along the x-axis as well.

drawLayer(graphicsLayer)  
if (Random.nextInt(5, 30) < step) {  
    drawRect(  
        color = glitchColors.random(),  
        blendMode = BlendMode.SrcAtop  
    )  
}

And finally, we can just randomly draw a rectangle using a random color.

Timing and easing

Wait, hold on! You might be asking yourself, what were those step and intensity variables that I used.
These are for timing the animation and giving it a more natural feel than just pure chaos.

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

The step variable is an integer that is animated from 10 to 0. We use an integer for this instead of a float so that we can have discrete changes in our glitch effect. Whenever step changes, there will be new mutations to our glitch effect.

But we also want our effect to start off in chaos, and then simmer down over its duration. That's why we use step to decide when a mutation should be applied.

left = if (Random.nextInt(5) < step)  
        Random.nextInt(-20..20).toFloat() * intensity  
    else  
        0f 

For example, as step decreases towards 0, there will be less instances this mutation will be applied.

Using step, we can also get the intensity that should be applied on each mutation.

val intensity = step / 10f

With these two variables applied, we can see the difference it makes by making the animation slow down as it comes to an end.

0:00
/0:01

Pure randomness without easing

0:00
/0:01

Easing applied for the animation to gradually simmer out

And with that, we now know how to create a glitch effect. For the full code as Modifier, grab it from my recent UI Recipe.

Download Sweeper

Also, check out my new game that inspired this effect. Sweeper is based on the classic game, minesweeper, but with lots of delightful animations made in our favorite UI framework, Jetpack Compose.

It's built on Compose Multiplatform, so it's available on both Android and iOS.

Thanks for reading and good luck!

Subscribe for UI recipes

Mastodon