javascripthtmlcanvasdesign-patternshtml5-canvas

What are some typical design patterns when using the Canvas API?


I'm starting to get into graphical programming using the Canvas API, but I haven't seen any design patterns for reusing canvas code. So far I've only seen examples using a global canvas object and a single draw function.

I'm coming from a Python tkinter background where the primary pattern is to have a class for each graphical object, and pass the canvas context into the constructor so the object can draw itself. I was thinking about using the module pattern below to achieve the same thing, but I'm looking for more of a JavaScript way if there is one.

var MyShape = function (ctx) {
    // Assign the canvas context along with any other properties,
    // then draw the object.
    this.ctx = ctx;
    this.draw();
}

MyShape.prototype.draw = function () {
    // Use this.ctx to access the Canvas API and draw the object.
}

Solution

  • You have the right idea so far. In addition to what you have, i suggest you include an update function to each defined renderable object. So long as there's a possibility that these shapes may be moving over time.

    Also, if you find yourself needing more complex shapes, I suggest you compose your complex shapes out of other shape classes you have already defined. Such as a smiley face shape that, in its draw function, uses three predefined circle classes for the eyes and head, and perhaps just use the canvas context to generate the smile.

    You would call the draw function of each child shape from the parent shape in a cascade of draw calls.

    Edit: Here's a little example, i forgot to mention the importance of using a reference type to store the position of the parent of the shapes.

    var canvas = document.getElementById("game");
    var ctx = canvas.getContext("2d");
    
    var thing = new Thing(100, 100, 30, 40);
    
    function Vector(x, y) {
        this.x = x;
        this.y = y;
    }
    
    function Rectangle(pos, w, h, color, parent) {
        // use a vector2 'pos' so that the position of this rectangle matches the
        // position of the parent because pos is a reference type
        this.pos = pos;
        this.w = w;
        this.h = h;
        this.color = color || "#000";
    }
    
    Rectangle.prototype.render = function () {
        ctx.fillStyle = this.color;
        ctx.fillRect(this.pos.x, this.pos.y, this.w, this.h);
    };
    
    function Circle(pos, r, color, parent) {
        // use a vector2 'pos' so that the position of this circle matches the
        // position of the parent because pos is a reference type
        this.pos = pos;
        this.r = r;
        this.color = color || "#000";
    }
    
    Circle.prototype.render = function () {
        ctx.beginPath();
        ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2 * Math.PI, false);
        ctx.fillStyle = this.color;
        ctx.fill();
    };
    
    function Thing(x, y, w, h) {
        //create pos reference
        this.pos = new Vector(x, y);
    
        //use pos reference
        this.rect = new Rectangle(this.pos, w, h, "#00F");
        this.circle = new Circle(this.pos, w, "#F00");
    
        //for update() demonstration
        this.rotation = 0;
    }
    
    Thing.prototype.render = function () {
        this.circle.render();
        this.rect.render();
    };
    
    Thing.prototype.update = function () {
        // notice here i only have to modify the values of pos.x and pox.y, this is
        // important because if i had added fields x and y to the Thing class, they
        // would not be reference types and i could not have moved both the
        // rectangle and the circle if that was the case.
        this.pos.x = this.pos.x + Math.cos(this.rotation);
        this.pos.y = this.pos.y + Math.sin(this.rotation);
        this.rotation += 0.02;
    };
    
    function update() {
        thing.update();
    }
    
    function render() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        thing.render();
    }
    
    function main() {
        update();
        render();
        window.requestAnimationFrame(main);
    }
    
    main();
    #game {
        border: 1px solid black;
    }
    <canvas id="game" width="500px" height="500px"></canvas>