In my last post I had said I was trying out some libraries to automatically track dependencies for me. The one I'm playing with right now is Vue.js. It can track dependencies between data and the HTML/SVG elements. For example if I write SVG:

<circle :cx="center.x" :cy="center.y" r="30"/>

and I have it hooked up to data:

data: {
  center: {x: 100, y: 150}
}

then Vue will remember that the circle depends on those x and y properties. If I modify the data with center.x = 75; Vue will detect this and update the circle. This is quite convenient!

Vue only updates DOM elements: HTML and SVG. In some of my articles I use Canvas or WebGL, so I was looking for a way to make Vue work with those. After playing around with a few different approaches, I discovered a clever trick, and I'm writing this blog post to share it.

The first trick, thanks to @JoshWComeau, is to make a canvas component that takes a “draw function”. That way, instead of having to make a new component for each type of drawing, I can reuse the canvas component and pass in a different function each time. Here's an example:

<a-canvas width="500" height="200"
          :draw="(canvas, ctx) => { ctx.fillRect(x,y,1,1); }" />

The second trick, which I found through reading about how Vue works and also experimenting, is to make the draw function's dependencies trigger a redraw. Vue will automatically track dependencies in the DOM elements and also in computed values:

computed: {
    redraw() {
        let canvas = this.$el;
        this.draw(canvas, this.ctx);
    },
}

This will create a data dependency. Since the draw() function depends on x and y, whenever x or y changes, it might affect the value of the draw() function, which might affect the value of the redraw computed property. Great! This automatically sets up the dependency without me having to “subscribe” to x and y like I do with the Observer pattern.

Although I have successfully set up dependencies xredraw and yredraw, it's not enough. You see, Vue only looks at the dependencies needed for rendering the DOM. Since I had set up the DOM as <canvas width=… height=…/>, Vue sees that there's no need to compute redraw at all. And that means my draw function never gets called. Doh!

So the third trick is to put the result into the DOM, with <canvas width=… height=… :data-dummyvalue="redraw"/>. Now Vue has a dependency redraw → DOM. Combined with the previous dependencies, whenever x or y changes, it knows it needs to compute redraw, and in doing that it calls my draw() function.

This is great! It's all automatic. If the redraw function had been (canvas, ctx) ⇒ ctx.fillRect(100, 100, w, h) then Vue would redraw the canvas whenever w or h changed. The canvas component doesn't have to change and I never need to explicitly list the dependencies.

There are a few other details. Vue wants to create the canvas with the dummyvalue but to compute the dummyvalue you need to call draw() which needs the canvas. There's a cycle there that needs to be broken. And we also need to trigger a redraw whenever the size changes, so we need to add the widthredraw and heightredraw dependencies. These are handled in the code I put up on gist.

So far I've only used this in a very simple page and it's worked well. I looked around but didn't see any Vue components that worked this way; if you know of any, please post in the comments. [Update 2022-09: newer versions of Vue have watchEffect which is worth a look for this type of thing]

Labels: , ,

0 comments: