绘制不在 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=]
我有一组图块和一组对象。当他们离开屏幕时,我不想从他们的数组中删除元素,因为我希望能够在他们回来时重新绘制它们。 现在在我的动画帧中(尽管我最终可能会将图块移动到另一个 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=]