绘制不在 canvas 上的对象或检查它们是否更有效?

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

我有一组图块和一组对象。当他们离开屏幕时,我不想从他们的数组中删除元素,因为我希望能够在他们回来时重新绘制它们。 现在在我的动画帧中(尽管我最终可能会将图块移动到另一个 canvas,它只会在屏幕移动或 window 调整大小时等时发生变化),我像这样更新图块:

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();
}

因为我意识到屏幕上的图块总是比实际多。这似乎是在绘制每一个(而不是仅仅一行 for (let t of tiles) t.draw())之前进行大量额外检查,特别是因为一旦我添加了平移功能,我就必须在其中添加偏移变量。是每次都继续检查还是只绘制所有瓷砖更好?还是我遗漏了另一种内存占用较少的选项?

对于上下文,这是我在 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();
}

绘制如下所示的图案:

编辑:我终于鼓起勇气做了一个工作示例。
抱歉之前的半途而废的伪代码,真的很草率:)。


我认为最简单的方法是绘制足够多的图块副本以覆盖整个 canvas,让裁剪 trim 溢出位。

视图的平移将相对于 canvas 平移重复的拼贴图案,因此只有一部分拼贴在角落和边框上可见。

您可以精确计算要绘制的左上图块的坐标:它包含 canvas 的左上角。

从那里开始,您只需绘制瓷砖,直到覆盖整个表面。终止条件非常简单:结束一行并在下一个图块位于 canvas 之外时开始下一个。同样,当下一行位于 canvas.

之外时,您将结束整个过程

查看下面的 fiddle 现场演示(用鼠标左键拖动背景)。

// =====================
// 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.stack.imgur.com/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>

现在,如果内存不是问题(当您的普通浏览器需要 1 GB 才能启动和显示时,为什么会成为问题 about:blank?),您还可以创建第二个隐藏的 canvas引用包含预渲染图块数组的实际图像,在最坏的情况下刚好足以覆盖可见的 canvas,只需调用 drawImage().[= 即可将其复制到正确的位置。 15=]