Using canvas with React Native
React Native is the Wild West. There’s so much to discover and build, and it can be a fun journey. Sometimes, before you can start panning for gold, you have to make the tent you’re going to sleep in. And build the saloon you’re going to drink your sarsaparilla in. And commission the trader you’re going to sell your gold to.
This past week, I wanted to take a feed of images, and make minor changes to each. The web developer in me thought; “Let’s use canvas for this. There are already loads of resources explaining how to do the things I want to do. It’ll be easy!”
Turns out there isn’t really a good abstraction for canvas, in React Native. There’s an interesting hack, which involves using a web view to get access to the browser canvas API, but nothing which will abstract the process of pixel drawing in each native platform.
This is the approach I want to talk about, today.
Just to be clear: I think it’s an elegant solution to a problem that needn’t exist. I think a cross-platform canvas API needs to be a thing as much as react-native-svg does. Until that happens, web views will have to do…
So, how does one use canvas through web views? Let’s look at parts of the react-native-canvas library to find out.
Loading the Webview
The first step is to create a Webview decorator, which will give us access to the canvas:
This is the first point of contact, between a developer and the underlying canvas. It’s a React component, meant to be rendered as part of a larger component. Though one could render it directly to the document, the only way to draw on it is to provide a ref
function.
Decorating components
Apart from the normal React code, there are some new bits of Javascript that may be new to you.
The first is that methods are defined as name = parameters => body
. This is shorthand for binding the methods to this
, in the constructor.
The second is that the Canvas
component is being decorated by @defineMethods
and @defineProperties
. These help to add public methods and properties, which are proxied to the underlying canvas objects. They look like this:
defineMethods
loops through the provided methods and defines methods for them on the decorated class. In this case, it’s toDataUrl
, defined on the Canvas
component. These methods call postMessage
on the component they’re added to.
defineProperties
works similarly:
Each of the provided property names creates a getter and setter of the same name; so that they can be accessed as properties on the component..
Each setter stores its value in a private property, on the component; and also passed it to the underlying canvas or context. This means changes aren’t immediate or guaranteed — since the two operations aren’t connected at the point of success.
Creating the context
The context is much simpler. Most of the work is in defining which methods and properties should be made public in React Native:
Context
uses the postMessage
implementation, in Canvas
; so the bridge is shared and all the queueing logic is reused. So…What does the bridge look like?
This is a pretty naive bridge implementation. That is, it makes no attempt at linking sent and received messages. It serves two purposes:
- When the
Canvas
component is rendered, and before theWebview
component is fully loaded, it stores all the drawing commands. When theWebview
finishes loading, all those commands are sent to the underlying canvas. - When new actions are sent, to the underlying canvas, their results are returned via the same message API. These are sent to
onMessage
, via theWebview
property of the same name. When we call a method or set a property:canvas.webview.postMessage
sends a message to a canvas in theWebview
. TheWebview
sends a message back, which is picked up inbridge.onMessage
. At the same time, a consumer can awaitbridge.waitForMessage
, which will be resolved as soon asbridge.onMessage
receives a message.
I’m not entirely show how I would restructure this, to link sent and received messages, or to deal with the order dependency of
onMessage
andwaitForMessage
. Just because I can see these issues, doesn’t mean I know how to fix them or that I don’t think the library is still immensely useful.
The final piece of the puzzle is the Javascript that runs within the Webview
:
Most of the work is being done when the document receives a message. If we’re dealing with a set
operation, we set the appropriate property value on the target. The Canvas
component doesn’t expect a return message from this.
If we need to call a canvas or context method, we call the method and send the serialized return value back. Many of the methods don’t typically have useful return values, but that’s ok. All we need is a way to await the completed operations (which this gives us).
Extending the canvas
Let’s go back to the reason I discovered this library, in the first place. I needed to make small changes to images, in Javascript. This library has provided a way for me to access the canvas API, but I also need a way to load images from a remote source.
The canvas API provides a drawImage
method, which could accept another canvas (or a browser Image
object) as a source. Trouble is, Image
isn’t available (at least not with the same functionality as in the browser) in React Native. So, I patched the Webview
Javascript to allow for this:
This addition makes use of the browser image API. These images allow base64 data URIs as well as normal URLs. So, I can now use either to load an image, and then make changes to the image; using the browser canvas API.
That’s all, for now
I’ve enjoyed working with this library, so far. It’s certainly useful for small edits. I hope to write more about how I’m using it soon, so keep an eye out for that.