Do you code? Toggl is looking for developers – you can work from anywhere! Check out our open positions here.

Recently, we completely overhauled Teamweek’s main user interface. In addition to a slicker design, the new UI also provides support for mobile devices and is built on HTML5 canvas using the CreateJS suite.

This blog post discusses the various ways to get the most performance out of the canvas application. The techniques described here are EaselJS-specific but the general idea behind them applies to all canvas applications.

Html5 Canvas and CreateJS

HTML5 canvas is basically a primitive pixel buffer and it takes a lot of effort to build a large application on it. It does provide a nice drawing API and even some more advanced visual effects like drop shadow, but it’s still tough to build an interactive user interface using just a drawing API. That’s where libraries like EaselJS come in. EaselJS (part of the CreateJS suite) provides a very nice Flash-like API for displaying and manipulating objects and handles the drawing part itself.

If you’re building a touch-enabled interface using canvas, you can’t help but compare your canvas app to native apps. Getting the same amount kind of smoothness out of a canvas application, however, requires some trickery. Fortunately, EaselJS is a rather thin framework so you can just dive in, see what it’s doing and figure out how to bend it to you needs.

Tip #1. Cache It!

EaselJS lets you lay out your UI in a hierarchical structure of DisplayObject instances. An instance of Stage is the root for all DisplayObjects and draws everything onto the canvas when you call Stage.update(). Obviously the more elements you have, the longer this operation takes. EaselJS provides a way to cache your objects using the DisplayObject.cache() method. This draws the object onto a separate off-screen canvas and uses to drawImage() to copy pixels from the cache canvas onto the real one when you call update(), so the next time you call update(), you’re drawing 1 object instead of the 1722 objects that are its descendants.

With Teamweek, one of the ways we use caching is that when you drag a task, we cache the rest of the timeline, so virtually only two objects are drawn onto the canvas during dragging. This makes dragging tasks much smoother.

Tip #2. Use Multiple Canvases

This follows the same idea as tip #1, but instead of manually caching objects, you can layer multiple canvases on top of each other and redraw only the one that changes. EaselJS doesn’t have any kind of built-in support for this but you can simply use multiple Stage instances or extend the Stage class’s functionality to achieve that. Keep in mind that you want to draw onto as few of these layers as possible at a time. It’s no use layering your application onto multiple canvases if you’re going to call Stage.update() on all of them if anything changes.

Tip #3. Avoid Expensive Hittest Calculations

EaselJS offers pixel-perfect hittest calculation, to determine whether the mouse (or finger) is over an object or not. It does this by drawing the object onto a 1×1 canvas, that represents the mouse pointer, and checking its alpha value to see if anything was drawn (note: as a side effect, items with an alpha value of 0 cannot receive mouse events). Not only is this slow as molasses, but it does this on every mousemove/touchmove event, which makes it very noticeable when you’re dragging objects around for example.

Update: EaselJS has already thought of this and let’s you specify the time interval, for mousemove hittest calculations.

Unless you’re making a game, you probably don’t need pixel-perfect hit-testing. A bounding-box based method works well enough for most people (like the good ol’ HTML&CSS days, remember?). EaselJS doesn’t offer a documented way of supplying your own hittest logic, so we’re left with just monkey patching the _getObjectsUnderPoint method of the Stage class. What’s more, EaselJS doesn’t have a concept of a DisplayObject’s size (unlike Flash, where we have the automatically-calculated width and height properties), so the bounding box needs to be defined manually.

Getting down and dirty: the code.

BoundingBoxHitTest = {
  /* Mixin class to override _getObjectsUnderPoint with a more efficient method of only
   * checking the bounding box of the children that have extended the BoundingBox mixin class.
   * This does not offer pixel-precision but more like a html/css style approach, which is sufficient for our needs.
   * This should be replaced by canvas's addHitRegion API when support arrives for it, since it improves accessibility.
   * NB! does not modify children so all childrens' _getObjectsUnderPoint methods are still the old one.
   */
  _getObjectsUnderPoint: function(x, y, arr, mouseEvents, self) {
    /* mouseEvents parameter is ignored
     * if arr is given, it is populated by all the the inner-most children that are under the point (x,y)
     * otherwise, only the front-most child is returned.
     * NB! If a child extends BoundingBox and matches, its children are not checked for a match
     */
    var bb, child, match, result, i;
    if (!self) {
      self = this;
    }
    for (i = this.children.length-1; i >=0; i--) {
      child = this.children[i];
      bb = child._bounding_box;
      if (bb && child.visible) {
        match = (child.x + bb.x <= x && x < child.x + bb.x + bb.width) && (child.y + bb.y <= y && y < child.y + bb.y + bb.height);
        if (match) {
          if (arr) {
            arr.push(child);
            continue;
          } else {
            return child;
          }
        }
      }
      if (child instanceof createjs.Container && child.visible && !bb) {
        result = self._getObjectsUnderPoint.call(child, x - child.x - child.regX, y - child.y - child.regY, arr, mouseEvents, self);
        if (result) {
          if (arr) {
            arr.push(result);
            continue;
          } else {
            return result;
          }
        }
      }
    }
    return null;
  }
};

BoundingBox = {
  /* The mixin class that all DisplayObjects should extend if they want to emit Mouse Events
   * The bounding box is defined relative to the object itself and must be updated manually as needed.
   */
  _bounding_box: null,
  setBoundingBox: function(x, y, w, h) {
    return this._bounding_box = new createjs.Rectangle(x, y, w, h);
  }
};

To apply these mixins you can just copy the methods to an object, using underscore’s _.extend or a similar function. e.g.

var myStage = _.extend(new createjs.Stage(myCanvas), BoundingBoxHitTest);

Keep in mind, that you need to call setBoundingBox() manually, every time a DisplayObject changes its size.

In the case of Teamweek, this can be up to 300 times faster than the original hittest, which brings scrolling the timeline from “completely unacceptable” to what we consider “smooth”.

Tip #4. Watch The Size of Your Canvas

Obviously, the larger the canvas the more costly the drawing operation, but if you’re targeting mobile devices, there are some size limits you must keep in mind.

From Safari Web Content Guide:

The maximum size for a canvas element is 3 megapixels for devices with less than 256 MB RAM and 5 megapixels for devices with greater or equal than 256 MB RAM

So if you want to support Apple’s older hardware, the size of your canvas cannot exceed 2048×1464.

But that’s not all! Even with smaller sizes, you have to keep your canvas’s aspect ratio between ~3/4 and ~4/3. If you step outside those boundaries, webkit seems to switch to a totally different rendering mode splitting the canvas into multiple fixed-size areas and rendering them separately with a noticeable delay between them.

There doesn’t seem to be any documentation on this but I have confirmed this happens on both Chrome and Safari on iOS versions 6.0.1 and 5.1.1.

Tip #5. Consider Implementing Redraw Regions

EaselJS does a good job at replicating the Flash API but one of the things we used to get for free with Flash is redraw regions. This basically means drawing only the parts of the screen that have changed since the last frame. EaselJS’s Stage.update() simply clears the whole canvas and draws the whole thing again.

Depending on the application, one could significantly increase the drawing speed by overriding the default drawing pipeline and implementing redraw regions in javascript.

Update: As pointed out below, the cost of calculating redraw regions in javascript is huge, so you might want to try other methods before going down this road.

Tip #6. Don’t Draw Anything At All

If you’re using EaselJS, don’t do this:

createjs.Ticker.addEventListener("tick", myStage.update);

Unless there is something always happening in your canvas, this just makes sure that your battery runs dry right before you can plug it in. Additionally, if your application is a canvas/HTML hybrid, this can significantly decrease the responsiveness of the HTML-side of your application.

Make sure you have some way of pausing/resuming your redraws operations.

Conclusion

Html5 canvas’s performance is certainly above that of traditional html, especially if you’re holding your redraw operations on a tight leash, but there are a lot of hoops to be jumped through if you’re trying to make your canvas application feel like a native one.

Hopefully, this article has given you some ideas about how to make your application perform well across various devices.

If you have more ideas about how to make canvas apps perform well, feel free to share in the comments!

Teamweek touch interface

TeamWeek is an online Project Planning Tool with a Team CalendarBeing an antidote to clumsy Gantt charts, it allows managers to respond to change faster.