May 25th, 2023

Blossoming love for Compose animation

Kristen Halper
SW/FW Engineer

Hello Jetpack Compose developers,

Today we’ll be finishing up our blog series on animations in Jetpack Compose! This content was inspired by Nicole Terc’s Composable Sheep talk from droidcon NYC.

Over the past two weeks, we covered some basics graphics, animation canvases, and basic animations. This week, we’ll polish up our garden with some more complex animation combos and background shaders.

Creating more complex animations

We’ll be continuing to use our drawSunflower method and Sketch composable to build animations in this post, so if you haven’t already, check out the previous blog posts to see how we built those functions!

So far, we’ve built a basic sunflower, then animated its rotation, size, and color individually. Now, to create more complex animations, let’s try animated multiple properties at once! We can combine petal color animations with rotation to create a rainbow spinner effect, and we can also combine the color, size, and rotation animations together for an exciting effect:

  @Composable
  fun AnimatedPetalColorAndRotationSunflower() {
      Sketch(
          modifier = Modifier.size(100.dp),
          targetValue = 360f,
          animationSpec = infiniteRepeatable(tween(durationMillis = 3000, easing = LinearEasing))
      ) { animationState ->
        drawSunflower(hue = animationState, rotation = animationState)
      }
} @Composable fun AnimatedEverythingSunflower() { Sketch( modifier = Modifier.size(100.dp), targetValue = 360f, animationSpec = infiniteRepeatable( tween(durationMillis = 800, delayMillis = 50, easing = LinearOutSlowInEasing), repeatMode = RepeatMode.Reverse
) ) { animationState ->
drawSunflower( sizePct = animationState / 360f, rotation = animationState, color = Color.hsv(hue = animationState, saturation = 1f, value = 1f) ) } }

Our garden looks colorful and dynamic now:

[video-to-gif output image]

Adding a background shader

The last step to complete our garden is to surround the flowers with some vibrant grass, which we can do by adding a dynamic shader background. I started by checking out BackgroundShaderScreen.kt and GradientShaderScreen.kt to see how shaders were incorporated into the composable sheep project. For Android apps, you can use the Android Graphics Shading Language (AGSL), which is very similar to the popular OpenGL Shading Languade (GLSL) if you’re already familiar with shaders. Since our project is built with Compose Multiplatform, though, we also need to support the desktop version of the app. Instead of AGSL, for desktop we can use Skia’s Shading Language (SKSL).

Let’s first try to modify the GradientShaderScreen.kt code to show a nice blend of greens! Throughout this process, we can use the Skia Shaders Playground or the Shdr Editor (for GLSL) tools to help visualize the results.

Basic green gradient shader

Here’s what the shader code currently looks like (and literally looks like):

  uniform float2 iResolution;
  uniform float iTime;

  vec4 main(in float2 fragCoord) {
    // Normalized pixel coordinates (from 0 to 1)
    vec2 uv = fragCoord/iResolution.xy;

    // Time varying pixel color
    vec3 col = 0.8 + 0.2*cos(iTime*2.0+uv.xxx*2.0+vec3(1,2,4));
      
    // Output to screen
    return vec4(col,1.0);
  }

The line where col is assigned is where the time varying color is decided, so to make the gradient only show shades of green, this is the line we’ll have to change. This vec3 represents the RGB values of the shader color, so the first step to building a new gradient was to pick a few shades of green and note their RGB values.

Once the colors were chosen, all I had to do was experiment a bit in the shader preview tool and dust off my trigonometry knowledge to get the RGB values to vary between those of my chosen shades of green; this resulted in the following updated shader code:

  uniform float2 iResolution;
  uniform float iTime;

  vec4 main(in float2 fragCoord) {
      // Normalized pixel coordinates (from 0 to 1)
      vec2 uv = iTime/iResolution.xy;

      // Time varying pixel color
      float r = 0.3 + 0.5*sin(iTime*2.0+uv.x*3.0);
      float g = 0.9 + 0.1*cos(iTime*2.0+uv.x*3.0);
      float b = 0.3 + 0.4*cos(iTime*2.0+uv.x*3.0);

      // Output to screen
      return vec4(r, g, b, 1.0);
  }

Now that we’re happy with the shader code, all that’s left to do is use it in the app as our garden background. Since our desktop and mobile implementations will be different, we can add a backgroundBrush parameter to our main composable and write the appropriate code in each module. We will again be using LaunchedEffect to create a changing time value (like we did previously with Sketch) that can be passed in as the iTime float for the shader code. And, since we don’t want to cause unnecessary recomposition, we will make sure to use the lambda Modifier drawBehind. For the desktop app, we will be using Skia’s RuntimeEffect to build a shader, as covered in this article on Skia shaders for Compose desktop. For the Android app, we’ll use RuntimeShader, as described by the AGSL documentation. Both of these versions can then be converted to an Android ShaderBrush and passed into the common main composable. Check out the GitHub repo for the fully updated MyGarden code .

Here’s the result:

[video-to-gif output image]

Complex shader

Now that we have the shader infrastructure set up, we can play around more with creating different shader effects. The noodleZoom shader in GradientShaderScreen.kt, originally from Twitter user @notargs, creates an awesome zooming web-like effect that I think would look pretty cool behind our flowers. But it’s not green! So, let’s try using our RGB and trig knowledge again to change the original shader code, which is shown here:

  uniform float2 iResolution;
  uniform float iTime;
    // Source: @notargs https://twitter.com/notargs/status/1250468645030858753
    float f(vec3 p) {
       p.z -= iTime * 10.;
       float a = p.z * .1;
       p.xy *= mat2(cos(a), sin(a), -sin(a), cos(a));
       return .1 - length(cos(p.xy) + sin(p.yz));
    }
          
    half4 main(vec2 fragcoord) { 
       vec3 d = .5 - fragcoord.xy1 / iResolution.y;
       vec3 p=vec3(0);
       for (int i = 0; i < 32; i++) {
          p += f(p) * d;
       }
       return ((sin(p) + vec3(2, 5, 12)) / length(p)).xyz1;

This, the RGB values are determined by the vec3(2, 5, 12) in the return statement. The brightness of the color also depends on this vector, so we’ll also want to make sure we don’t make the values too high or low. After some experimenting in the shader playground again, I settled on vec3(0.95, 3.725, 2.05). Here’s how the final iteration of our garden looks:

[video-to-gif output image]

Resources and feedback

The content covered today is part of PR #3 in the animated-garden repo.

To learn more about Jetpack Compose animations and creative coding, check out these resources:

If you have any questions, use the feedback forum or message us on Twitter @surfaceduodev.

Since this is the end of the animated garden series, we will be livestreaming this week! You can also check out the archives on YouTube.

Author

Kristen Halper
SW/FW Engineer

Works in the Surface Duo Developer Experience team to help with all aspects of dual-screen SDK development and customer engagement.

0 comments

Discussion are closed.