Featured image of post Is it vaporwave? - Compose Hot reload

Is it vaporwave? - Compose Hot reload

Why

I want to play with the recently released Compose Hot reload feature by JetBrains. To do this I setup a Desktop compose project and picked a topic to draw and explore: Vaporwave Why? because Vaporwave is a curious Internet thing. It has a strong visual aesthetic that I wanted to recreate. It is intriguing. It is seems to be a fleeting transient phenomena: it is in the name vapor and wave and yet… and yet we see it pop up in unexpected places. It is a form of hauntology: a cultural idea that returns from the past to haunt the present.

There will be ramblings interspersed and remixed with code, I will put the code at the beginning of the sections, so that you can easily skim if remix-rambling is not your thing. Yes, there is a matching repo. Yes, it is a mix of organic hand coding and AI enhancements. I will explain my process.

But first

Setting up Hot Reload

You need:

  1. A project that has a Compose desktop part. It can be stand alone or part of a multiplatform project
  2. Use a Kotlin version 2.1.20 or later
  3. Get the plugin and the latest version information and add it the place you keep your versions, like a toml file
  4. Setup the plugin in your build.gradle.kts
  5. Setup the foojay resolver
  6. Run the main application from the gutter icon or command line. something like ./gradlew runHot --mainClass MainKt --auto
  7. Optional extra make a run target for your particular main application if you have more than one See and example of each line in the sample repo

Wow these instructions are vague! indeed things change quickly. Please follow the original repo. This post will date.

Origins and Sound track

Vaporwave appeared as a music genre with a strong aesthetic, some say in 2010. The iconic album Floral Shoppe by Macintosh Plus is still available and makes a perfect sound track for inspecting the code or reading this article.

Go ahead click the link and start the music. I’ll wait.

We are on the cusp of widespread changes in how we interact with technology. Will we have nostalgia for the way we did thing in the olden days? Will we even remember how it really was?

A-E-S-T-H-E-T-I-C

Vaporwave is known for a strong aesthetic. Here is a jumpstart table with the technique and a note. Click on the image to see the code or scroll for more detail

ImageTechniquesNotes
triangle codePath on Canvas, Linear gradient brushTriangles are geometric shapes reminiscent of corporate logos of the eighties: an abstract shape that becomes a place holder for ambiguous concepts like digital consumerism
floorgrid codeCanvas, Offset and Path, conversion of Offset to create perspectivea magenta grid looks like something from a pixelated game: a technology that we recognise and remember imperfectly with nostalgia. It also has a black and white tiled version.
colorshift codeImage with a BitmapPainter, ColorFilter, colorMatrix, blur,The choice of pngs, dolphins and old tech cassettes. Greek sculptures to remind us of beauty ideals. Having these elements floating makes them look surreal. Using blur and color filters change what they look like to simulate old VHS errors. Remembering past technology imperfectly. Human and digital memory is fallable.
sunsetcodedrawCircle filled with gradients with color stops and Color.Transparenta retro sunset in a perfect world remembered imperfectly
palm codesvg string converted to Path, transformation matrix to convert from original points to new composablereferences to idealised worlds of palm trees. Unreleastic and rendered in neon
mesh codemesh gradient by sinasamakipastel colors dreamy, sureal, floaty - very vaporwave-y
glitch codeSkia Shader language, RuntimeShader, composeRenderEffect in a modifierdigital artefacts degrade, bitrot and imperfect memories, glitches and remixes

Fonts and Colours

Pick some colours, not too many and stick with them. Pick some fonts, not too many and stick with them. I chose pastels for a dreamy, hazy vibe. There needs to be a pink and a teal, in my case Mulberry and RobinEggBlue.

Palette

The fonts were retro, reminiscent of arcade games but also glitched and damaged because digital artefacts degrade, nothing is immune to bitrot. The fonts are found on Google Fonts

Fonts

Let’s dig into a few of the details, shall we?

Vanishing Floor

Floorgrid Floortiles

To get the ghostly magenta grid or the black and white tiling my inital prompt had Junie just drawing a bunch of squares. Not what I wanted at all, because as you will notice, the squares are slanted because of the one point perspective. With a bit of research I found a way to do a projection. Each point that starts a line or is a corner of a tile is converted into a point projected onto the canvas. The same projection function can be used whether we are drawing squares or lines. More info on one point perspective.

Cassettes, dolphins, statues and color shifts

Cassettes DolphinColorshift

A selection of pngs. This is nothing new. What makes this part interesting is you can apply a filter to the image to modify any of the color or alpha channel. How it works is you provide a matrix of values. This matrix will then be applied to each pixel.

The matrix looks like this:

[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]

If you take a color [R, G, B, A] and apply the matrix you get this

R' = a*R + b*G + c*B + d*A + e
G' = f*R + g*G + h*B + i*A + j
B' = k*R + l*G + m*B + n*A + o
A' = p*R + q*G + r*B + s*A + t

A matrix that inverse the image looks like this:

[ -1, 0, 0, 0, 255,
  0, -1, 0, 0, 255,
  0, 0, -1, 0, 255,
  0, 0, 0, 1, 0 ]

This matrix gives a lot of power to change the original image.

We can also add a blur modifier and flip the images horizontally or vertically. Horizontal or vertical flip is achieved by adjusting the scale.

More in the official docs.

Sunset

Sunset

The clue to the retro sunset is in a combination of gradients with color stops and using Color.Transparent.

val brushRetro = Brush.verticalGradient(
            colorStops = arrayOf(
                0.0f to topColor,
                0.43f to topColor,
                0.43f to Color.Transparent,
                0.45f to Color.Transparent,
                0.45f to topColor,
                0.57f to middleColor,
                0.57f to Color.Transparent,
                0.6f to Color.Transparent,
                0.6f to middleColor,
                0.7f to middleColor,
                0.7f to Color.Transparent,
                0.74f to Color.Transparent,
                0.74f to middleColor,
                0.84f to bottomColor,
                0.84f to Color.Transparent,
                0.9f to Color.Transparent,
                0.9f to bottomColor,
                1.0f to bottomColor
            ),
            startY = 0f,
            endY = height
        )

Palm tree svg

Sunset

It is possible to extract the points of a simple svg. Looking at the sample palm tree file find the following line.

Sunset

Copy the string into the code and convert it to a Path. The problem is the points are relative to the coordinate system and size of the svg image and not the resulting composable view. It needs to be transformed. The conversion is based on the original path and the destination composable.

The conversion code looks like this

fun fromBoundsToComposeView(
    bounds: Rect = Rect(-1f, -1f, 1f, 1f),
    width: Float,
    height: Float
): Matrix {
    val originalWidth = bounds.right - bounds.left
    val originalHeight = bounds.bottom - bounds.top
    val scale = min(width / originalWidth, height / originalHeight)
    val newLeft = bounds.left - (width / scale - originalWidth) / 2
    val newTop = bounds.top - (height / scale - originalHeight) / 2
    val matrix = Matrix()
    matrix.translate(-newLeft, -newTop)
    matrix.scale(scale, scale)
    return matrix
}

Apply the resulting matrix to the image. path.transform(matrix)

Mesh Gradient

Mesh gradient

There is a really useful mesh gradient modifier created by sinasamaki with a great writeup on how it works. Hot reload is useful for tuning the colors of the gradients. There is also a visual tool to set the gradient up.

Mesh gradient

Glitches

Glitch

It is possible to use Skia Shaders with Compose. The setup is easy enough and the shader is copied into a string. I found a small glitch shader by Coolok on ShaderToy. Integration is straight forward by creating a runtime Shader.

Understanding shader code is something else entirely. The core understanding is that shader code runs on a single pixel position not on the whole image. So the code has to make changes from the pixel perspective. The main entry point is mainImage. You get the pixel coordinates in a two dimensional vector, fragCoord and the code has to return the color of the pixel at that position in a four dimensional vector (RGBA) in fragColor.

Hot reload is a useful tool to tweak and experiment with shaders to learn how they work. This playlist, on learning how to build shaders on ShaderToy, is a good resource to get started.

Enter the AIs

Of course I had help - Junie and AI assistent as well as Gemini for research.

Research with Gemini

To orient me and to be sure I understood vaporwave, Gemini did a deep research report on the topic for me. I AI-slop created the pixelated cassettes, palm outlines and the marble statue image in Gemini.

Plans and execution with Junie

  1. make a list of compose techniques to explore
  2. make a list of vaporwave aesthetic elements
  3. create a readme and a guidelines file
  4. collect visual references, blog posts with code snippets, bitmaps, images, fonts and colours in a docs folder
  5. take each of the aesthetic elements and ask Junie to build one sample element and use it in a specific composable e.g. Build an equilateral triangle composable that draws on the Canvas and uses a gradient brush. Show and example of this composable in the SoloTriangle composable
  6. check, commit, tag and goto step 5

I didn’t let Junie just do the whole list. I prompted each element separately so that I could check it out and tweak it before I commited and tagged. I also started a fresh context and a new task for each step. This worked well because the guidelines.md and docs/look folder kept the continuity.

Inbetween questions with AI assistant

While Junie was busy building things, I use the AI assistant to research info with oneshot questions for the next task. e.g. How do I draw a perspective grid that has a single vanishing point on a flat surface The answer was python code. I asked it to explain the python code. The chat session didn’t interfere with what Junie was doing. In my next prompt to Junie, I asked it to use the research I captured in a markdown file.

Understanding everything with DeepWiki

For more detailed understanding of the whole project, I fed it to DeepWiki. This helped to make sense of some opaque parts such as the glitch shader code.

All the things

The final composition of all the pieces is a quick and dirty Box or two with everything inside.

A fun way to showcase hot reload is to run the app, in my case

./gradlew runHot --mainClass MainKt --auto

Then in a separate terminal run the speedrun script. This script checks out a git tag in a series of git tags when you type enter in the terminal. Because the app is running auto updating hot reload, the changes appear progressively.

Tada! All the things

Live demo video coming to a browser near you soon.