I like following inspiring individuals on the web and seeing what they make — one such particular person is Cassie Codes, she makes unimaginable animations for the net. One in all her inspiring examples is that this cute animated Jellyfish.
After seeing this and obsessing over it for some time, I saved considering to myself that this cute little creature wants to return to life in Compose too. So this weblog put up describes how I went about making this in Jetpack Compose, the ultimate code might be discovered right here. The strategies in right here aren’t solely related for jellyfish in fact… another fish will do too! Simply kidding — this weblog put up will cowl:
- Customized ImageVectors
- Animating ImageVector Paths or Teams
- Making use of a distortion noise impact on a Composable with AGSL RenderEffect.
Let’s dive in! 🤿
To implement this jellyfish, we have to see what the SVG is made up of first — and attempt to replicate the totally different components of it. One of the simplest ways to determine what an SVG is drawing, is to remark out varied components of it and see the visible results of what every part of the svg renders. To do that, you may both change it within the codepen linked above, or obtain and open an SVG in a textual content editor (it’s a textual content readable format).
So let’s take an outline have a look at this SVG:
<!--
Jellyfish SVG, path knowledge eliminated for brevity
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
<defs>
<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" peak="100%">
<feTurbulence data-filterId="3" baseFrequency="0.02 0.03" consequence="turbulence" id="feturbulence" kind="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
<feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
</filter>
</defs>
<g class="jellyfish" filter="url(#turbulence)">
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle" />
<path class="tentacle" />
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="face" />
<path class="outerJelly"/>
<path id="freckle" />
<path id="freckle"/>
<path id="freckle-4"/>
</g>
<g id="bubbles" fill="#fff">
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble" />
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble" />
</g>
<g class="jellyfish face">
<path class="eye lefteye" fill="#b4bebf" d=""/>
<path class="eye righteye" fill="#b4bebf" d=""/>
<path class="mouth" fill="#d3d3d3" opacity=".72"/>
</g>
</svg>
The SVG consists of the next parts:
- Paths and Teams of paths that make up the SVG:
- Tentacles
- Face — blob and outer jelly
- Eyes — the animate open and closed
- Bubbles — animate randomly across the the jellyfish — the dimensions and alpha animates
2. Total the jelly fish physique additionally has feTurbulence
(noise) utilized as a feDisplacementMap
, this offers the wobbly look to it.
Now that we perceive what this SVG is made up of, let’s go about rendering the static model in Compose.
Compose has an idea of an ImageVector, the place you may construct up a vector programmatically — much like SVG. For vectors/SVGs that you simply simply wish to render with out altering, you too can load up a VectorDrawable utilizing painterResource(R.drawable.vector_image). This can deal with changing it into an ImageVector that Compose will render.
Now you is likely to be asking your self — why not simply import the jellyfish as an SVG into an xml file and cargo it up utilizing painterResource(R.drawable.jelly_fish)
?
That could be a nice query — and it’s attainable to load up the jellyfish on this means, eradicating the turbulence facet of the SVG and the picture will render with an XML loaded up (as defined within the documentation right here). However we wish to do a bit extra with the person components of the trail, reminiscent of animating components on click on and making use of a noise impact to the physique, so we’ll construct up our ImageVector
programmatically.
To be able to render this jellyfish in Compose, we are able to copy the trail knowledge (or the “d
” tag on the trail) that make up the fish, for instance, the primary tentacle has the next path knowledge:
M226.31 258.64c.77 8.68 2.71 16.48 1.55 25.15-.78 8.24-5 15.18-7.37 23-3.1 10.84-4.65 22.55 1.17 32.52 4.65 7.37 7.75 11.71 5.81 21.25-2.33 8.67-7.37 16.91-2.71 26 4.26 8.68 7.75 4.34 8.14-3 .39-12.14 0-24.28.77-36 .78-16.91-12-27.75-2.71-44.23 7-12.15 11.24-33 7.76-46.83z
If you’re new to paths / vectors / SVGs, the above is likely to be a bit overwhelming. However don’t fear, these are simply instructions that specify mathematical directions on how to attract one thing. As an example, M is an command to maneuver the digital cursor to a brand new place with out drawing, and L is a command to attract a line to the desired place, there are a couple of different instructions reminiscent of:
- M, m: Transfer to
- L, l, H, h, V, v: Line to
- C, c, S, s: Cubic Bézier curve to
- Q, q, T, t: Quadratic Bézier curve to
- A, a: Elliptical arc curve to
- Z, z — Shut the trail
The instructions are case delicate, an uppercase letter signifies absolute coordinates within the viewport house, whereas lowercase letter signifies that the command is relative to the present place.
Now you might be in all probability considering — do I’ve to attract in my head and know all of the positions and instructions by hand? No — under no circumstances. You may create a vector in most design applications — reminiscent of Figma or Inkscape, and export the results of your drawing to an SVG to get this data for your self. Whew! 😅
To create the vector in Compose: we name rememberVectorPainter
, which creates an ImageVector
, and we create a Group
known as jellyfish
, then one other Group
known as tentacles
and we place the primary Path
inside it for the primary tentacle. We additionally set a RadialGradient
because the background for the entire jellyfish.
And the results of the next is a small tentacle drawn on display screen with a radial gradient background!
We repeat this course of for all the weather of the SVG — taking the bits of the trail from the SVG file and making use of the colour and alpha to the trail that shall be drawn, we additionally logically group the paths into the tentacles, the face, the bubbles and so on:
We now have our total jellyfish rendering with the above ImageVector
:
We wish to animate components of this vector:
- The jellyfish ought to transfer up and down slowly
- The eyes ought to blink on click on of the jellyfish
- The jellyfish physique ought to have a wobbly/noise impact utilized to it.
Doing this with a XML file is tough: it’s tough to animate, we have to work with XML and we are able to’t apply different results to components contained in the file with out changing it to an AnimatedVectorDrawable
. For instance, we wish to introduce an interplay the place the eyes blink on click on of the jellyfish. We’re capable of get finer grain management of our parts and create customized animations programmatically.
So let’s see how we are able to animate particular person bits of the ImageVector
.
Trying on the codepen, we are able to see that the jellyfish is transferring with a translation up and down (y translation). To do that in compose, we create an infinite transition and a translationY
that’ll be animated over 3000 millis, we then set the group containing the jellyfish, and the face to have a translationY
, this may produce the up and down animation.
Nice — a part of the ImageVector
is now animating up and down, you’ll discover that the bubbles stay in the identical place.
Trying on the codepen, we are able to see that there’s a scaleY
and opacity
animation on every of the eyes. Let’s create these two variables and apply the dimensions to the Group
and the alpha on the Path
. We will even solely apply these on click on of the jellyfish, to make this a extra interactive animation.
We create two Animatables which is able to maintain the animation state, and a droop perform that we are going to name on click on on the jellyfish — we animate these properties to scale and fade the eyes.
We now have a cute blinking animation on click on — and our jellyfish is nearly full!
So we’ve received a lot of the issues we wish to have animated — the motion up and down, and the blinking. Let’s have a look at how the jellyfish’s physique has that wobbly impact utilized to it, the physique and tentacles are transferring with noise utilized to them to offer it a way of motion on it.
Trying on the SVG and the animation code, we are able to see that it makes use of feTurbulence
to generate noise that’s then utilized to the SVG as a feDisplacementMap
.
<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" peak="100%">
<feTurbulence data-filterId="3" baseFrequency="0.02 0.03" consequence="turbulence" id="feturbulence" kind="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
<feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
</filter>
</defs>
<g class="jellyfish" filter="url(#turbulence)">
Sadly these primitives aren’t supported in Android for the time being (see this open bug), however we do produce other instruments up our sleeve that may assist with this. feTurbulence
is producing noise that’s then used as a displacement map to maneuver the SVG round.
We will use AGSL shaders to attain this, it’s value noting that that is solely supported on Tiramisu and up (API 33+). First we have to create a shader that’ll act as a wobble, we received’t use noise at first — only a mapping perform as a substitute for simplicity.
The way in which the shaders work is that they act on particular person pixels — we get a coordinate (fragCoord
) and we’re anticipated to supply a shade consequence that’ll be rendered at that coordinate. Under is the preliminary shader we’ll use for reworking the composable:
In our case, the enter that we are going to be utilizing is our presently rendered pixels on display screen. We get entry to this by way of the uniform shader contents;
variable that we are going to ship as enter. We take the enter coord (fragCoord
), and we apply some transformations on this coordinate — transferring it with time and customarily performing some math on it to maneuver it round.
This produces a brand new coordinate, so as a substitute of returning the precise shade on the fragCoord
place, we shift the place we get the enter pixel from. For instance, if we had return contents.eval(fragCoord)
, it could produce no change — it could be a pass-through. We now get the pixel shade from a unique level of the composable — which is able to create a wobbly distortion impact on the content material of the composable.
To make use of this on our composable, we are able to apply this shader as a RenderEffect
to the contents of the composable:
We use createRuntimeShaderEffect
, passing within the WOBBLE_SHADER
as enter. This takes the present contents of the composable, and supplies it as enter into the shader, with the parameter title “contents
”. We then question the contents contained in the WOBBLE_SHADER
. The time
variable adjustments the wobble over time (creating the animation).
Operating this, we are able to see the entire Picture
is now distorted and appears a bit extra wobbly — similar to a jellyfish.
If we needed to not have the impact apply to the face and bubbles, we are able to extract these into separate ImageVectors
, and skip out on making use of the render impact to these vectors:
The shader we specified above isn’t utilizing a noise perform to use a displacement to the content material of the composable. Noise is a solution to apply a displacement, with a extra structured random perform. One such kind of noise is Perlin noise (which is what feTurbulence
makes use of underneath the hood), that is what it could appear to be if we render the results of working the Perlin noise perform:
We use the noise worth for every coordinate within the house, and use it to question a brand new coordinate within the “contents
” shader.
Let’s replace our shader to make use of a Perlin noise perform (tailored from this Github repo). We’ll then use it to find out the coordinate mapping from enter coordinate to output coordinate (i.e. a displacement map).
Making use of this noise perform, we get a a lot better consequence! The jellyfish seems as whether it is transferring contained in the water.
At this level you is likely to be questioning, that is cool — however very area of interest in its use case, Rebecca. Positive — possibly you aren’t making an animated jellyfish every single day at work (we are able to dream proper?). However RenderEffects
might be utilized to any composable tree — permitting you to use results to absolutely anything you need.
For instance, why wouldn’t you need your gradient textual content or complete composable display screen to have a noise impact or another AGSL impact your coronary heart needs?
So we’ve coated many fascinating ideas on this weblog put up — creating customized ImageVectors
from SVGs, animating components of an ImageVector
and making use of AGSL shaders as RenderEffects
to our UI in Compose.
For the complete code of the Jellyfish — take a look at the complete gist right here. For extra data on AGSL RenderEffects — take a look at the documentation, or the JetLagged Pattern for an additional instance utilization of it.
You probably have any questions — be happy to achieve out on Mastodon androiddev.social/@riggaroo or Twitter.