Sitemap
ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

How a holographic foil effect works in Jetpack Compose

7 min readOct 26, 2026

--

Thin-film interference, glitter flakes and the tilt sensor, in an AGSL shader for a scratch-art app

Press enter or click to view image in full size
The same Playink drawing with three backgrounds: holographic, gold and silver

What you’ll find in this article

Some time ago I published Playink on Google Play, a scratch-art app for kids. I built the first version on my own, without AI, and the idea was simple: a black layer you scratch with your finger and, underneath, a colour gradient. Like the scratch-art paper we all had as kids, without the mess.

It worked, but the colours underneath were static, just like on paper. I wanted more: a holographic foil underneath, like those shiny stickers, with the light moving across the drawing as you tilt the phone.

For that part I did use AI. I worked with spec-driven development: I started from a spec describing what I wanted, several AI agents worked on it in parallel, and I decided how it had to look, tested it on the phone and approved every change.

In this article I’ll walk you through how the effect works: why the scratch surface had to change, the AGSL shader layer by layer, and how the tilt of the phone reaches the shader without a single recomposition. The full code is in the gists along the way.

The problem: a background that changes every frame

In the first version, every stroke was painted with the colour of the background at that point. That’s fine for a gradient that never changes, but a holographic foil changes every frame, so a stroke can’t have “a colour”.

The solution was to turn it around. The background is painted in full, and on top of it there’s a black layer where every stroke cuts a hole. In Compose, that’s an offscreen layer and a Clear blend mode (simplified):

drawIntoCanvas { it.saveLayer(surfaceSize.toRect(), Paint()) }
drawRect(surfaceColor, size = surfaceSize)
strokes.forEach { drawPath(it, Color.Black, blendMode = BlendMode.Clear) }
drawIntoCanvas { it.restore() }

The strokes only clear the black layer, not the background below it. From the outside nothing changed, but now the background can be anything that fills a rectangle, including a shader.

A shader runs once per pixel

Picture a stadium where thousands of people hold up coloured cards and, together, form a giant image. Nobody moves from their seat: everyone follows the same simple rule and the image changes on its own.

A shader works the same way. The GPU runs the same function for every pixel, in parallel, every frame, and each pixel works out its colour from just two inputs: where it is on the screen and how much the phone is tilted. Nothing moves; everything is recalculated from scratch.

On Android 13 and later, that function can be written in AGSL and wrapped in a RuntimeShader. Playink paints it with a ShaderBrush that keeps the same shader while the canvas size doesn’t change, so a tilt change only updates a uniform:

The foil itself is four layers. Let’s build it one at a time.

Layer 1: the relief

A real holographic foil has an irregular relief, with liquid-looking blobs rather than straight stripes. The shader gets it from fractal value noise (fbm, four octaves), distorted by more noise. That’s called domain warping, and it’s like stirring paint with a stick (simplified):

float2 n = uv + seed;
float2 q = float2(fbm(n * 1.2), fbm(n * 1.2 + float2(5.2, 1.3)));
float warp = fbm(n * 1.6 + 1.6 * q);
Press enter or click to view image in full size
The relief that decides the film thickness at each point, still without colour

Layer 2: a soap bubble

The colours of a soap bubble aren’t paint. Light bounces off both sides of a very thin film and, depending on how thick the film is at each point, some colours add up and others cancel out. That’s thin-film interference, and it’s what the shader imitates.

Get Manuel Mato’s stories in your inbox

Join Medium for free to get updates from this writer.

Each pixel gets a “thickness” from the relief, its position and the tilt. Then red, green and blue are cosines of that thickness, each one at a frequency proportional to one over its wavelength (650, 530 and 450 nm):

float3 film = 0.5 + 0.5 * cos(6.2831853 * (thickness * float3(1.0, 1.226, 1.444)
+ float3(0.0, 0.08, 0.16) + palettePhase));

Because the three waves move at different speeds, they drift in and out of phase as the thickness changes, and that’s where the rainbow comes from. A small change gives a big jump in colour: a thickness of 1.0 is orange, 1.1 is already magenta. That’s why the foil reacts to the slightest movement of your hand.

The thickness is kept between 0.45 and 1.95, the first interference orders, which are the vivid ones: below that the film turns flat silver, and above it the colours get muddy. After that, the colour is lifted into a bright range, because metal foil is never dark.

Press enter or click to view image in full size
The app’s shader with only the soap-bubble colours

Layer 3: the light band

When you tilt a holographic sticker, at some point a flash of light reaches you. In the shader, that’s a diagonal band whose position depends on the tilt: a Gaussian core plus a wider sheen. It’s mixed with a screen blend, 1 − (1 − a)(1 − b), which brightens the colours without burning them to white.

Press enter or click to view image in full size
The light band over the soap-bubble colours

Layer 4: glitter

Last, the glitter. The screen is split into a grid of tiny cells, and each cell uses a hash to decide whether it has a flake and which way that flake faces. A flake only flashes when the tilt lines it up with the light, just like real glitter, and it’s drawn as a small four-point star. On top of everything there’s a fine diagonal grain, like the brushed texture of a metal sheet.

The four layers together, with the light moving as if someone were tilting the phone

Here’s the whole shader, around a hundred lines of AGSL, with the two presets the app uses:

Getting the tilt to the shader without recomposing

The tilt comes from TYPE_GAME_ROTATION_VECTOR (gyroscope and accelerometer, without the compass), with the accelerometer alone as a fallback. Sensors are noisy, and a foil that shakes looks broken, so every reading goes through a low-pass filter. The filter also drops changes too small to be seen, so a phone lying on a table doesn’t redraw the screen at all:

smoothed += (target - smoothed) * alpha
if ((smoothed - lastEmitted).getDistance() < MIN_VISIBLE_CHANGE) return null

The orientation when the screen becomes visible is the neutral one, so it doesn’t matter whether you’re lying on the sofa or sitting at a table. And if the system animations are off, the foil stays still.

The key part for performance is where the tilt is read. It’s a Compose State, but it’s only read inside drawBehind, so a new value invalidates the draw phase and nothing else:

.drawBehind {
backgroundBrush.tilt = tilt.value
drawRect(backgroundBrush)
}

No recomposition, no relayout: the brush updates one uniform and the background is redrawn.

What didn’t make it

A fallback for older phones. AGSL needs Android 13. For older versions we tried an imitation with moving gradients, but it looked pastel and barely reacted to the tilt, so I decided to remove it. On Android 7 to 12, every drawing reveals a colour gradient, as in the first version, instead of a foil that doesn’t look like one.

Sharing as a GIF. A GIF only has 256 colours, and the foil gradients turned into steps. So, when you share a holographic drawing, the app renders a short looping MP4 in which the light follows a figure-eight tilt.

Conclusion

The holographic foil isn’t a video or an animation: it’s a single function that every pixel solves on its own. A warped-noise relief, thin-film colours, a light band and a layer of glitter, driven by a filtered tilt that only ever touches the draw phase. Around a hundred lines of AGSL, written by AI from the spec; my part was deciding how it had to look and testing it on the phone until the screen looked like a sticker.

First you scratch the drawing; then, as you tilt the phone, the light moves across it and the colours change

If you want to try it, Playink is on Google Play: it’s free, with no ads and no in-app purchases, and the shiny backgrounds need Android 13 or later.

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.