https://kotlinlang.org logo
#compose-web
Title
# compose-web
a

agrosner

08/18/2022, 12:07 PM
Hey , anyone have a good image loading library like Coil that supports compose canvas for web?
d

deviant

08/18/2022, 12:46 PM
did you try to load into the coil painter and then use it with built-in
Image
composable?
a

agrosner

08/18/2022, 1:11 PM
i dont believe coil is set up for multiplatform
c

Casey Brooks

08/18/2022, 1:51 PM
Yeah, Coil depends on Android/JVM-specifc APIs, and I don’t think there’s any plan to make it support multiplatform. There was a thread the other day discussing image loaders for Compose Desktop, but I’m not sure if any of them support JS targets for Web usage https://kotlinlang.slack.com/archives/C01D6HTPATV/p1660433149092459
But the browser itself has APIs for fetching images such that you might not need a dedicated image loader library if you’re only targeting Web. I’m doing something like this in a Compose/DOM app
Copy code
val image = Image()
image.src = backgroundImageUrl
image.onload = { doSomethingWithDownloadedImage(image) }
m

Michael Paus

08/18/2022, 2:12 PM
@Casey Brooks Could you elaborate on that a little bit more? E.g., how do you get the JS Image into the Compose world as an ImageBitmap? This is probably not an issue in the Compose/DOM world.
c

Casey Brooks

08/18/2022, 2:14 PM
I haven’t tried specifically with Web using the Material Canvas target, I’m using Web with DOM to create regular HTML. But here’s the full function where I’m fetching an image to draw it manually to a standard HTML `<canvas>`:
Copy code
@Composable
fun ComposableCanvas(
    backgroundImageUrl: String,
    vararg keys: Any?,
    onDraw: CanvasRenderingContext2D.(width: Double, height: Double) -> Unit
) {
    val imageBitmap: Image? by produceState<Image?>(null, backgroundImageUrl) {
        // I had to use a `callbackFlow` here because of a weird scoping issue where I couldn't assign `.value = ` from the callback as normal
        val flow = callbackFlow<Image?> {
            val image = Image()
            image.src = backgroundImageUrl
            image.onload = { trySend(image) }
            awaitClose { image.onload = null }
        }

        flow
            .onEach { value = it }
            .launchIn(this)
    }
    var canvasEl: HTMLCanvasElement? by remember { mutableStateOf(null) }
    var drawScope: CanvasRenderingContext2D? by remember { mutableStateOf(null) }

    LaunchedEffect(drawScope, imageBitmap, canvasEl, *keys) {
        imageBitmap?.let { bmp ->
            canvasEl?.let { canvas ->
                drawScope?.apply {
                    canvas.width = bmp.naturalWidth
                    canvas.height = bmp.naturalHeight
                    drawImage(bmp, 0.0, 0.0, bmp.width.toDouble(), bmp.height.toDouble())
                    onDraw(canvas.width.toDouble(), canvas.height.toDouble())
                }
            }
        }
    }

    Canvas(
        {
            ref {
                canvasEl = it
                drawScope = it.getContext("2d") as CanvasRenderingContext2D

                onDispose {
                    canvasEl = null
                    drawScope = null
                }
            }
        }
    )
}
If you can find a way to get the
Image
as a
ByteArray
, you should be able to feed that into the JS Canvas APIs. If it’s using skiko for the canvas the same as Desktop, you might be able to use this function to convert the ByteArray to an Image that can be drawn in Compose
Copy code
fun ByteArray.asImage(): ImageBitmap {
    return Image.makeFromEncoded(this).toComposeImageBitmap()
}
m

Michael Paus

08/18/2022, 2:21 PM
Interesting that you call the value
imageBitmap
although it actually is just a JS Image 😉. But I guess in a real Compose Canvas-Web application you also want to render into your Compose Canvas.
c

Casey Brooks

08/18/2022, 2:25 PM
This is the first time I’ve tried working with an HTML canvas, and I’m sure there are things to be improved here.
imageBitmap
in that snippet is this, which is both a bitmap, but also an HTML node, I guess? JS makes no sense.
m

Michael Paus

08/18/2022, 2:30 PM
Yes, JS is the wrong term. It should have been HTML, which I actually meant 😉 I obviously associate JS with HTML/Browser usage too much 🙃.
I have not tried it myself yet, but I think this should be doable with a combination of the Ktor client (using the JS engine) and the ByteArray to ImageBitmap conversion. See https://ktor.io/docs/response.html and https://ktor.io/docs/http-client-engines.html#minimal-version
I could not resist. It works indeed. This quick and dirty code
Copy code
fun main() {
    onWasmReady {
        BrowserViewportWindow("MapDemoMpp") {
            MaterialTheme {
                var imageBitmap: ImageBitmap? by remember { mutableStateOf(null) }

                LaunchedEffect("K1") {
                    val client = HttpClient(Js)
                    val httpResponse: HttpResponse = client.get("<https://pbs.twimg.com/media/FP_EJdPXwAAhoSR?format=jpg&name=360x360>")
                    client.close()
                    val encodedImageData: ByteArray = httpResponse.body()
                    val loadedImageBitmap: ImageBitmap = imageBitmapFromBytes(encodedImageData)
                    imageBitmap = loadedImageBitmap
                }

                App(imageBitmap)
            }
        }
    }
}

fun imageBitmapFromBytes(encodedImageData: ByteArray): ImageBitmap {
    return Image.makeFromEncoded(encodedImageData).toComposeImageBitmap()
}
loads an image from an URL via the Ktor client, converts it into a Compose ImageBitmap and then the App renders into into a normal Compose Canvas. Here is the result:
a

agrosner

08/19/2022, 2:39 PM
this worked! thank you. will def work around cleaning it up later, but im doing a hackathon , so good enough!
14 Views