Reputation: 210
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:
Upvotes: 0
Views: 168
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