1

I've been scouring the (still scant) discussions and documentation of famo.us looking for what I hoped would be an obvious thing: Animate a canvas in a CanvasSurface. I am really surprised that I have found not one single example.

I have made some progress, so I hope this post will help someone get their foot on the ladder.

Here is my bare bones code (previously using window.requestAnimationFrame but now adjusted to extend the built-in render method), which currently works, but I am puzzled.

My question follows the code:

define(function (require) {
    "use strict";
    var Engine = require('famous/core/Engine'),
        View = require('famous/core/View'),
        CanvasSurface = require('famous/surfaces/CanvasSurface'),
        context = Engine.createContext(),
        //
        VividCanvas = function () {
            var v = new View(),
                cw = 320,
                ch = 240,
                c = 0,
                surface = new CanvasSurface({size: [cw, ch]}),
                ctxt = surface.getContext('2d'),
                //
                redraw = function () {
                    ctxt = surface.getContext('2d'); // WHY IS THIS LINE NECESSARY?
                    c += 1;
                    c = c % 360;
                    ctxt.fillStyle = "hsl(" + c + ", 100%" + ", 50%)";
                    ctxt.fillRect(0, 0, cw, ch);
                    window.requestAnimationFrame(redraw);
                    return surface.id; // i.e. a valid renderSpec
                };
            surface.render = redraw;
            v.add(surface);
            return v;
        };
    //
    context.add(new VividCanvas());
});

What puzzles me is that the line ctxt = surface.getContext('2d') (the first line in the redraw function) is necessary. I would have thought that ctxt was already defined and in scope (see immediately before the redraw function is declared).

Indeed, if you log ctxt, you will get a 2d Canvas Context in both cases. But for some reason, the canvas context which is made in the redraw function is a different instance to the one made before the first redraw.

This may be demonstrated by inserting console.log(ctxt === surface.getContext('2d')) as the first line of the redraw function (before ctxt is redefined). It logs false. I do not understand why. Can someone explain?

If feel that ctxt should point to the right thing throughout, and should not need to be adjusted, so this second call to getContext should be unnecessary. However, if I omit it, the canvas draws once only.

So why is that?

Originally, I had a second, but perhaps related question was about requestAnimationFrame. From the comments I was able to replace this with the line surface.render = redraw, and making sure my redraw method returns the surface id, thus allowing Famo.us to handle the animation sync. Thanks to Andrew for his suggestion in the comments.

brennanyoung
  • 6,243
  • 3
  • 26
  • 44
  • 1
    I'll have to get back to this one, but for starters, I don't know if requestAnimationFrame is the right approach.. Famo.us allows you to use Engine.on('prerender',fn) which seems like a sure candidate for continuously updating a canvas.. – johntraver Jun 19 '14 at 15:20
  • 1
    Inherit from the CanvasSurface and then implement the `render` method where you perform all the updating work. – Andrew De Andrade Jun 19 '14 at 21:19
  • I've now added a render method, as per Andrew's suggestion. The tricky part was getting it to return a meaningful renderSpec, but a meaningful renderSpec is just the id of the surface. I still have the odd question about having to call getContext both before and after the redraw unresolved, however. Any takers? – brennanyoung Jun 24 '14 at 09:29

2 Answers2

2

When looking at the code of CanvasSurface, it seems to be using two contexts. Probably one you use to render to, while the other one is displayed. And when the next AnimationFrame begins, it flips the contexts.

/**
 * Returns the canvas element's context
 *
 * @method getContext
 * @param {string} contextId context identifier
 */
CanvasSurface.prototype.getContext = function getContext(contextId) {
    this._contextId = contextId;
    return this._currTarget ? this._currTarget.getContext(contextId) : this._backBuffer.getContext(contextId);
};
IjzerenHein
  • 2,690
  • 1
  • 17
  • 13
1

I did Canvas animation in the famous-lagometer project: https://github.com/IjzerenHein/famous-lagometer

Basically I do the canvas rendering in View.render(). The multiplication by two of the size, is to make it extra crips on retina displays.

/**
 * Renders the view.
 */
MyView.prototype.render = function render() {
    var context = this.canvas.getContext('2d');
    var size = this.getSize();
    var canvasSize = [size[0] * 2, size[1] * 2];
    this.canvas.setSize(size, canvasSize);

    // Do canvas drawing here...
    context.clearRect(0, 0, canvasSize[0], canvasSize[1]);
    context.fillStyle = this.options.backgroundColor;
    context.fillRect(0, 0, canvasSize[0], canvasSize[1]);
    context.lineWidth = 1;
    context.strokeStyle = this.options.borderColor;
    context.strokeRect(0, 0, canvasSize[0], canvasSize[1]);
    ...

    // Call super
    return this._node.render();
};
IjzerenHein
  • 2,690
  • 1
  • 17
  • 13
  • Useful example for prototype approach, thanks. (I prefer to avoid prototype and go with module pattern instead, as shown in my example above). But the main point of my question is still unresolved. I notice you also call getContext every time render is called. I am still curious why this is necessary, and why we can't just call it once, store the returned value when initialising the view and use the stored value in render(). – brennanyoung Jun 29 '14 at 14:44
  • 1
    I'm sorry, I did not read your entire question before posting. I would expect getContext() to not work until the actual HTML-element has been added to the DOM. This should be after the next render-cycle. I think I know the answer, it has to do with the CanvasSurface using a back-buffer. I will post it as a new question. Cheers – IjzerenHein Jun 29 '14 at 20:38