Raphael Morgan
Raphael Morgan

Reputation: 210

Is it more efficient to draw objects that aren't on the canvas or to check to see if they are?

I have an array of tiles and an array of objects. I don't want to remove elements from their arrays when they go offscreen because I want to be able to redraw them if they come back. Right now in my animation frame (although I'll probably end up moving the tiles to another canvas that only changes when the screen is moved or the window is resized etc), I update the tiles like this:

for (let t of tiles) {
  if (t.x < width/scale*2 + 5 && t.x > -5 && t.y < height/scale*2 + 5 && t.y > -5) t.draw();
}

because I realized there will always be more tiles than are actually on screen. This seems like a lot of extra checking before drawing each one (rather than just a one line for (let t of tiles) t.draw()), especially since I'll have to add offset variables in there once I add the pan functionality. Is it better to keep checking every time or to just draw all of the tiles? Or is there another option I'm missing that's less memory-intensive?

For context, here's my draw function in the Tile class:

draw () {
  let yOffset = - this.x%2 * scale/4;
  ctx.beginPath();
  ctx.moveTo(this.x*scale/2, (this.y-1)*scale/2 + yOffset)
  ctx.lineTo(this.x*scale/2+ scale/2, (this.y-1)*scale/2 + scale/4 + yOffset);
  ctx.lineTo(this.x*scale/2, (this.y-1)*scale/2 + scale/2 + yOffset);
  ctx.lineTo(this.x*scale/2- scale/2, (this.y-1)*scale/2 + scale/4 + yOffset);
  ctx.closePath();
  ctx.save();
  ctx.stroke();
  ctx.clip();
  ctx.drawImage(this.sprite,this.x*scale/2 - scale/2,(this.y-1)*scale/2 + yOffset, scale, scale);
  ctx.restore();
}

to draw a pattern that looks like this: A 2d HTML5 canvas with a grid of flattened diamond-shaped tiles, so that it looks like a grid of squares turned and made 3d. The tiles are green and roughly drawn to look like grass.

Upvotes: 0

Views: 168

Answers (1)

kuroi neko
kuroi neko

Reputation: 8661

EDIT: I finally mustered the courage to do a working example.
Sorry for the previous half-assed and buggy pseudo-code, that was really sloppy :).


I think the easiest way would be to draw just enough copies of your tile to cover the whole canvas, letting the clipping trim the overflowing bits.

The panning of the view will translate the repeating tile pattern relative to the canvas, so that only a portion of the tiles will be visible in the corners and on the borders.

You can compute precisely the coordinates of the top left tile to draw: it's the one that contains the top left corner of the canvas.

From there on you just have to draw tiles until you've covered the whole surface. The termination conditions are dead easy: you end a row and start the next one when the next tile would lie outside the canvas. Similarly, you end the whole process when the next row would lie outside the canvas.

See the fiddle below for a live demo (drag the background with left mouse button).

// =====================
// ancillary point class
// =====================
class Point {
    constructor (x, y)
    {
        this.x = x;
        this.y = y;
    }

    add (v) { return new Point (this.x + v.x, this.y + v.y);}
    sub (v) { return new Point (this.x - v.x, this.y - v.y);}
};

// ==============
// canvas handler
// ==============
class CanvasHandler {
    constructor (canvas_id, tile_src)
    {
        // get ready to paint
        this.report = document.getElementById ('report');
        this.canvas = document.getElementById (canvas_id);
        this.ctx = this.canvas.getContext("2d");

        // get canvas size
        this.canvas_size = new Point(this.canvas.width, this.canvas.height);

        // load tile from an external image
        this.tile = new Image();
        // initialize background once the image is loaded
        this.tile.onload = () => {
            this.tile_size = new Point (this.tile.naturalWidth, this.tile.naturalHeight);
            this.draw_background();
        }
        this.tile.src = tile_src;
        
        // setup mouse panning
        this.panning = new Point(0,0);
        this.canvas.onmouseup   = () => this.is_down = false;
        this.canvas.onmouseout  = () => this.is_down = false;
        this.canvas.onmousedown = (evt) => {
            this.is_down = true;
            this.pos = new Point(evt.x, evt.y);
        }   
        this.canvas.onmousemove = (evt) => {
            if (!this.is_down) return;
            let new_pos = new Point(evt.x, evt.y)
            let delta = new_pos.sub(this.pos);
            this.pos = new_pos;
            this.panning = this.panning.add(delta);
            this.draw_background();
        }
    }

    draw_background () {
        // nothing to draw until the tile is loaded
        if (this.tile_size == undefined) return;
        
        // compute the topmost and leftmost tile position
        const covering_tile_coord = (val, mod) => {
            let res = val % mod; // first tile that fits in the canvas from the top left corner
            if (res > 0) res -= mod; // go back one tile if we're not exactly at the top left
            return res;
        }
        let origin = new Point (
            covering_tile_coord (this.panning.x, this.tile_size.x),
            covering_tile_coord (this.panning.y, this.tile_size.y));
        
        // paint to cover the whole canvas
        let drawing = 0; // for statistics
        for (let x = origin.x ; x < this.canvas_size.x-1 ; x += this.tile_size.x)
        for (let y = origin.y ; y < this.canvas_size.y-1 ; y += this.tile_size.y) {
            this.ctx.drawImage(this.tile, x, y, this.tile_size.x, this.tile_size.y);
            drawing++;
        }
        // display tile count
        this.report.innerHTML = drawing + " tiles drawn";
    }
}

const canvas_panning_demo = [
    new CanvasHandler("canvas1", "https://lh3.googleusercontent.com/a-/AOh14Gg6t1BxnMNoJCH8t6NrnjtdFBsOyzr9PzjoK0UqRg=k-s64"),
    new CanvasHandler("canvas2", "https://i.sstatic.net/otSfG.jpg?s=48&g=1")];
body {background-color: #ddd;}
<canvas id='canvas1' width='290' height='135'></canvas>
<canvas id='canvas2' width='290' height='135'></canvas>
<p id='report'>Loading...</p>

Now if memory is not an issue (why should it be when your average browser needs a gigabyte just to launch and display about:blank?), you could also create a second, hidden canvas referencing an actual image containing a pre-render array of tiles just big enough to cover your visible canvas in the worst case, and simply copy it at the proper location with a single call to drawImage().

Upvotes: 1

Related Questions