Logo

dev-resources.site

for different kinds of informations.

Using SVGs on Canvas with Compose Multiplatform

Published at
1/15/2025
Categories
android
mobile
ios
kotlin
Author
eevajonnapanula
Categories
4 categories in total
android
open
mobile
open
ios
open
kotlin
open
Author
15 person written this
eevajonnapanula
open
Using SVGs on Canvas with Compose Multiplatform

One thing that has continued to amaze me with building my Compose Multiplatform app is how easily everything has worked with Canvas. When I started building Neule.art, I assumed that Canvas would cause some problems, but it has worked smoothly on both Android and iOS.

I've been writing posts about creative coding on Canvas (see, for example, Not a Phase - Text with Compose and Canvas), and also about my First Impressions of Compose Multiplatform, and this post combines elements from both themes.

There are different ways to parse an SVG to be used with Compose, and in this blog post, I'm looking into using path data. This approach requires some manual work, but it also allows better flexibility for controlling, e.g., colors of the individual elements within the SVG.

What We're Building

Even though I'd love to show how I've built the shirt I'm using in Neule.art, simplifying the process into the form of a blog post is too difficult a task. I decided to create a smaller SVG, which we're going to convert into Canvas. It looks like this:

Eighth hand drawn hearts in yellow, white, purple and black.

The hearts I'm using in the SVG are from Sarah Laroche's Vector Heart Figma resource. And if you've ever seen the colors anywhere, you might recognize them as being from the non-binary flag.

Getting the Paths from an SVG

We first need something from the original SVG to draw it on Canvas: the paths of individual components within the SVG.

In an SVG, the path's d-attribute contains the commands for defining the shape being drawn, and we're using that to parse the SVG to the path on Canvas. If you're unfamiliar with SVGs, I recommend checking MDN's documentation on SVGs, as it contains much in-depth information about SVGs.

So, to prepare for drawing paths on Canvas, we'll need an SVG image as code. Then, we need to copy the path's d-attribute's content and finally store them somewhere inside our code. There are several options for opening the SVG's code - for example, open the file in an IDE or inspect it in the browser's developer tools.

In our example, we store the path strings in a list with a variable called pathStrings. We also want to attach the color information to each path to make the drawing phase easier, so we store the path string together with the color it will be drawn with.

The following example lists only two of the hearts' paths, as the complete list would take up too much space. You can find the complete list of paths in the snippet linked at the end of the blog post.

val pathStrings = listOf(
    Pair("M185.52 80.2313C172.773 67.8803 159.24 53.9097 154.36 36.3463C153.373 32.8076 152.633 29.1043 153.193 25.4737C154.813 14.8697 163.753 16.4547 168.74 23.722C175.593 33.7001 180.946 44.7021 184.58 56.2492C186.3 43.9382 188.04 31.5685 191.666 19.6794C192.786 16.0068 195.16 11.6456 198.98 12.0229C201.993 12.3195 203.8 15.5202 204.526 18.453C205.733 23.2988 205.426 28.3831 204.926 33.3509C202.806 52.8032 201.753 71.7397 198.673 91.1093C193.953 87.8965 189.62 84.204 185.52 80.2313", Colors.white),
    Pair("M32.5198 119.231C19.7732 106.88 6.2398 92.9097 1.3598 75.3463C0.373129 71.8076 -0.366871 68.1043 0.193129 64.4737C1.81313 53.8697 10.7531 55.4547 15.7398 62.722C22.5931 72.7001 27.9465 83.7021 31.5798 95.2492C33.2998 82.9382 35.0398 70.5685 38.6664 58.6794C39.7864 55.0068 42.1598 50.6456 45.9798 51.0229C48.9931 51.3195 50.7998 54.5202 51.5265 57.453C52.7331 62.2988 52.4265 67.3831 51.9265 72.3509C49.8065 91.8032 48.7532 110.74 45.6732 130.109C40.9532 126.897 36.6198 123.204 32.5198 119.231", Colors.black),
)
Enter fullscreen mode Exit fullscreen mode

Now that we have the path string and the color, we can move on to parsing the path strings into Compose's Path objects.

Parsing Paths

Compose has this great thing called PathParser, which is something we can use for, well, parsing paths. Inside a Canvas component's block, we map through the path strings, parse them, and then draw the path on canvas:

Canvas(...) {
    paths.map { (pathString, color) ->
        val parsedPath = PathParser()
          .parsePathString(pathString)
          .toPath()

        drawPath(
            path = parsedPath,
            color = color,
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

PathParser's method parsePathString takes care of the parsing and returns a PathParser-object. Then, we call the toPath conversion method to get the Path out of PathParser.

With these changes, we get the following image:

Eighth hand-drawn hearts in yellow, white, purple, and black, that take only the top left quarter of the image.

As you might notice, the image doesn't scale to the entire space available. The reason is that SVG code is hard-coded to fit a specific size. If you look at the path strings above, you can see that they contain numbers as coordinates - they're inside the (in our case) 278 x 270 area defined in the original SVG as size.

To fix this problem, we can add some scaling functions into the mix. Let's talk about that next.

Scaling

We'll need to convert the parsed path to PathNodes to scale the paths. This way, we can scale every moveTo, curveTo, and other SVG drawing functions to the correct size.

We'll need a couple of helper functions to scale the paths on Canvas. One is a function for transforming float values from one size to another, and the other is a function that handles PathNode's scaling.

This is how we define the float scaling:

private fun Float.scaleToSize(
    oldSize: Float,
    newSize: Float,
): Float {
    val ratio = newSize / oldSize
    return this * ratio
}
Enter fullscreen mode Exit fullscreen mode

The function takes in the old size (so, for example, the full old width), and new size (the new full width). With those values, we calculate a ratio to convert the float value by multiplying the value with the calculated ratio.

The PathNode's scaling requires a bit more code:

private fun PathNode.scaleTo(size: Size): PathNode {
    val originalWidth = 278f
    val originalHeight = 207f

    return when (this) {
        is PathNode.CurveTo ->
            this.copy(
                x1 = x1.scaleToSize(originalWidth, size.width),
                x2 = x2.scaleToSize(originalWidth, size.width),
                x3 = x3.scaleToSize(originalWidth, size.width),
                y1 = y1.scaleToSize(originalHeight, size.height),
                y2 = y2.scaleToSize(originalHeight, size.height),
                y3 = y3.scaleToSize(originalHeight, size.height),
            )
        is PathNode.MoveTo ->
            this.copy(
                x = x.scaleToSize(originalWidth, size.width),
                y = y.scaleToSize(originalHeight, size.height),
            )
        else -> this
    }
}
Enter fullscreen mode Exit fullscreen mode

We're scaling the values from the original size to the Canvas size for each type of PathNode. In the when-clause, we're handling only CurveTo and MoveTo, because those are the only commands our path strings contain. If there were others, they should be handled here too.

Scaling of each parameter utilizes the scaleToSize we defined previously. The parameters we're passing to the function depend on if the parameter is on the x or y-axis - if it's on the x, we pass in width (as that's the x-axis), and if it is on the y-axis, then we pass in height.

Now that we have the helper functions let's change the mapping of path strings a bit:

paths.map { (pathString, color) ->
    val parsedPath =
        PathParser()
          .parsePathString(pathString)
          .toNodes()
          .map { it.scaleTo(size) }
          .toPath()

    drawPath(
        path = parsedPath,
        color = color,
    )
}
Enter fullscreen mode Exit fullscreen mode

Here, we first parse the path, then convert the PathParser it returns with toNodes to a list of PathNodes. We then map through each PathNode, and scale it to size. Finally, we turn the scaled PathNode list into Path, which we can then draw.

After these changes, the picture scales nicely:

Eighth hand-drawn hearts in yellow, white, purple, and black, which now scale the whole image's area.

Wrapping up

In this blog post, we've looked into how to turn SVG into Paths that can be used in Compose's canvas. This approach works for both native Android development, and Compose Multiplatform projects.

You can find the complete code from this Github gist.

Links in the Blog Post

android Article's
30 articles in total
Favicon
Three Common Pitfalls in Modern Android Development
Favicon
Using SVGs on Canvas with Compose Multiplatform
Favicon
Kotlin Generics Simplified
Favicon
Understanding Process Management in Operating Systems
Favicon
Introduction to Operating Systems
Favicon
Why Should You Develop a Native Android App Over Flutter?
Favicon
Flutter Development for Low end PCs
Favicon
Day 13 of My Android Adventure: Crafting a Custom WishList App with Sir Denis Panjuta
Favicon
How to Integrate Stack, Bottom Tab, and Drawer Navigator in React Native
Favicon
Flutter Design Pattern Bussines Logic Component (BLOC)
Favicon
🌎 Seamless Multi-Language Support in React Native
Favicon
Morphing Geometric Shapes with SDF in GLSL Fragment Shaders and Visualization in Jetpack Compose
Favicon
FMWhatsApp - Enhanced WhatsApp Experience
Favicon
[Feedback Wanted]Meet Nora: A Desktop Plant Robot Companion 🌱
Favicon
GBWhatsApp - Advanced WhatsApp Alternative
Favicon
Android TreeView(Hierarchy based) with Kotlin
Favicon
The Role of Android in IoT Development
Favicon
PicsArt MOD APK: Unlock the Power of Creativity
Favicon
Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 3/3)
Favicon
How to Integrate Stack and Bottom Tab Navigator in React Native
Favicon
Day 11 Unlocking the Magic of Location Services!
Favicon
Creating M3U Playlists for xPola Player
Favicon
xPola Player: The Advanced Media Player for Android
Favicon
Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 2/3)
Favicon
How I wrote this technical post with Nebo: an Android gamechanger ✍️
Favicon
Understanding Room Database in Android: A Beginner's Guide
Favicon
hiya
Favicon
Android Lock Screen Widgets: Revolutionizing Content Consumption with Glance's Smart Features
Favicon
Top 10 Best Android App Development Companies in India
Favicon
Why Modded APKs Are Gaining Popularity in 2025: Understanding the Risks and Benefits

Featured ones: