在 JavaScript 中将平铺图像加载并绘制到 canvas
Load and draw tiled images to canvas in JavaScript
我似乎很清楚如何使用 JavaScript 动态加载和绘制图像。我附加了一个 onload
函数并设置了图像源:
var img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
}
img.src = "your_img_path";
我想对二维图像(图块)阵列执行此操作并多次更新这些图块。但是,如后所述,我 运行 遇到了实施此问题的问题。
我编写的函数(如下所示)包含一些与我当前问题无关的计算。它们的目的是让图块成为 panned/zoomed(模拟相机)。选择要加载的图块取决于请求渲染的位置。
setInterval(redraw, 35);
function redraw()
{
var canvas = document.getElementById("MyCanvas");
var ctx = canvas.getContext("2d");
//Make canvas full screen.
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
//Compute the range of in-game coordinates.
var lowestX = currentX - (window.innerWidth / 2) / zoom;
var highestX = currentX + (window.innerWidth / 2) / zoom;
var lowestY = currentY - (window.innerHeight / 2) / zoom;
var highestY = currentY + (window.innerHeight / 2) / zoom;
//Compute the amount of tiles that can be displayed in each direction.
var imagesX = Math.ceil((highestX - lowestX) / imageSize);
var imagesY = Math.ceil((highestY - lowestY) / imageSize);
//Compute viewport offsets for the grid of tiles.
var offsetX = -(mod(currentX, imageSize) * mapZoom);
var offsetY = -(mod(currentY, imageSize) * mapZoom);
//Create array to store 2D grid of tiles and iterate through.
var imgArray = new Array(imagesY * imagesX);
for (var yIndex = 0; yIndex < imagesY; yIndex++) {
for (var xIndex = 0; xIndex < imagesX; xIndex++) {
//Determine name of image to load.
var iLocX = (lowestX + (offsetX / mapZoom) + (xIndex * imageSize));
var iLocY = (lowestY + (offsetY / mapZoom) + (yIndex * imageSize));
var iNameX = Math.floor(iLocX / imageSize);
var iNameY = Math.floor(iLocY / imageSize);
//Construct image name.
var imageName = "Game/Tiles_" + iNameX + "x" + iNameY + ".png";
//Determine the viewport coordinates of the images.
var viewX = offsetX + (xIndex * imageSize * zoom);
var viewY = offsetY + (yIndex * imageSize * zoom);
//Create Image
imgArray[yIndex * imagesX + xIndex] = new Image();
imgArray[yIndex * imagesX + xIndex].onload = function() {
ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
imgArray[yIndex * imagesX + xIndex].src = imageName;
}
}
}
如您所见,函数中声明了一个数组,其大小等于显示的图块网格的大小。数组的元素表面上充满了动态加载后绘制的图像。不幸的是,imgArray[yIndex * imagesX + xIndex]
不被我的浏览器视为图像,我收到错误消息:
Uncaught TypeError: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(HTMLImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap)'
另外,我本来以为不需要数组。毕竟,如果我只是要在下次调用 redraw
时加载一个新图像,为什么我需要在完成绘制后保留图像的句柄?在这种情况下,循环如下所示:
for (var yIndex = 0; yIndex < imagesY; yIndex++) {
for (var xIndex = 0; xIndex < imagesX; xIndex++) {
//Determine name of image to load.
var iLocX = (lowestX + (offsetX / mapZoom) + (xIndex * imageSize));
var iLocY = (lowestY + (offsetY / mapZoom) + (yIndex * imageSize));
var iNameX = Math.floor(iLocX / imageSize);
var iNameY = Math.floor(iLocY / imageSize);
//Construct image name.
var imageName = "Game/Tiles_" + iNameX + "x" + iNameY + ".png";
//Determine the viewport coordinates of the images.
var viewX = offsetX + (xIndex * imageSize * zoom);
var viewY = offsetY + (yIndex * imageSize * zoom);
//Create Image
var img = new Image();
img.onload = function() {
ctx.drawImage(img, viewX, viewY, imageSize * zoom, imageSize * zoom);
};
img.src = imageName;
}
}
自从上次处理的图块被渲染后,我使用这种方法的运气就更好了。尽管如此,所有其他瓷砖都没有绘制。我想我可能一直在覆盖一些东西,所以只有最后一个保留了它的值。
那么,我该怎么做才能重复渲染 2D 网格图块,以便始终加载图块?
为什么数组元素不被视为图像?
您犯了所有 JavaScript 程序员都曾犯过的经典错误。问题在这里:
imgArray[yIndex * imagesX + xIndex].onload = function() {
ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
您在循环中定义一个函数,并期望每个函数都有自己的变量副本。事实并非如此。您定义的每个函数都使用相同的变量 yIndex
、xIndex
、viewX
和 viewY
。请注意,函数使用这些变量,而不是这些变量的值。
这很重要:你在循环中定义的函数在循环中不使用yIndex
(和xIndex
、viewX
、viewY
)的值您定义函数的时间。函数使用变量本身。每个函数在执行函数时计算 yIndex
(以及 xIndex
和其余)。
我们说这些变量已经绑定在一个closure中了。当循环结束时,yIndex
和 xIndex
超出范围,因此函数计算超出数组末尾的位置。
解决方案是编写另一个函数,将 yIndex
、xIndex
、viewX
和 viewY
传递给该函数,然后创建一个捕获您要使用的值。这些值将存储在与循环中的变量分开的新变量中。
您可以将此函数定义放在 redraw()
中,在循环之前:
function makeImageFunction(yIndex, xIndex, viewX, viewY) {
return function () {
ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
}
现在将有问题的行替换为对函数制作函数的调用:
imgArray[yIndex * imagesX + xIndex].onload = makeImageFunction(yIndex, xIndex, viewX, viewY);
此外,请确保您设置的图片来源正确:
imgArray[yIndex * imagesX + xIndex].src = imageName;
为了使您的代码更易读和更易于维护,请分解掉yIndex * imagesX + xIndex
的重复计算。最好计算一次图像索引,然后到处使用。
您可以像这样简化 makeImageFunction
:
function makeImageFunction(index, viewX, viewY) {
return function () {
ctx.drawImage(imgArray[index], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
}
然后在循环中写下:
// Create image.
var index = yIndex * imagesX + xIndex;
imgArray[index] = new Image();
imgArray[index].onload = makeImageFunction(index, viewX, viewY);
imgArray[index].src = imageName;
您可能还希望分解出 imgArray[index]
:
// Create image.
var index = yIndex * imagesX + xIndex;
var image = imgArray[index] = new Image();
image.onload = makeImageFunction(index, viewX, viewY);
image.src = imageName;
在这一点上,我们问自己为什么我们有一个图像数组。如果,正如您在问题中所说,一旦您加载了图像,您就不再需要数组,我们可以完全摆脱它。
现在我们传递给 makeImageFunction
的参数是图像:
function makeImageFunction(image, viewX, viewY) {
return function () {
ctx.drawImage(image, viewX, viewY, imageSize * zoom, imageSize * zoom);
};
}
在循环中我们有:
// Create image.
var image = new Image();
image.onload = makeImageFunction(image, viewX, viewY);
image.src = imageName;
这看起来与您在问题中提到的替代方案相似,只是它在循环内没有函数定义。相反,它调用一个函数制作函数,该函数在新函数中捕获 image
、viewX
和 viewY
的当前值。
现在你可以弄清楚为什么你的原始代码只绘制了最后一张图像。您的循环定义了一大堆函数,它们都引用了变量 image
。当循环结束时,image
的值是您制作的最后一张图像,因此所有函数都引用最后一张图像。因此,他们都画了同一个图像。他们把它画在同一个位置,因为他们都使用相同的变量 viewX
和 viewY
.
Michael Laszlo 提供了一个很好的答案,但我想在这种情况下补充它,因为这是关于具有特殊考虑的图形应用程序。
更新 canvas 的内容时,您应该将其全部保存在一个函数中。当所有其他页面布局和呈现都已完成时,应调用该函数。此函数每秒调用次数不应超过 60 次。以临时方式访问 canvas 很混乱,并且会减慢整个页面的速度。
浏览器提供了一种方法来确保 canvas 重绘的时间与其自身的布局和渲染同步。 window.requestAnimationFrame (renderFunction);
参见 https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
没必要给每张图片都加上onload事件,浪费资源。您只需将图像添加到数组,然后在每个动画帧上通过检查每个图像的 complete
属性 来检查数组中的图像是否已加载。
我假设图块是背景的一部分,因此只需要渲染一次。最好创建一个关闭页面 canvas 对象并向其呈现图块,然后将 canvas 呈现到用户看到的页面 canvas。
... // all in scope of your game object
// create a background canvas and get the 2d context
function setup(){
// populate your imageArray with the image tiles
// Now call requestAmimationFrame
window.requestAnimationFrame(myRenderTileSetup);
}
function myRenderTileSetup(){ // this renders the tiles as they are avalible
var renderedCount = 0;
for(y = 0; y < tilesHeight; y+=1 ){
for(x = 0; x < tilesWidth; x+=1 ){
arrayIndex = x+y*tilesWidth;
// check if the image is available and loaded.
if(imageArray[arrayIndex] && imageArray[arrayIndex].complete){
backGroundCTX.drawImage(imageArray[arrayIndex],... // draw the image
imageArray[arrayIndex] = undefined; // we dont need the image
// so unreference it
}else{ // the image is not there so must have been rendered
renderedCount += 1; // count rendered tiles.
}
}
}
// when all tiles have been loaded and rendered switch to the game render function
if(renderedCount === tilesWidth*tilesHeight){
// background completely rendered
// switch rendering to the main game render.
window.requestAnimationFrame(myRenderMain);
}else{
// if not all available keep doing this till all tiles complete/
window.requestAnimationFrame(myRenderTileSetup);
}
}
function myRenderMain(){
... // render the background tiles canvas
... // render game graphics.
// now ready for next frame.
window.requestAnimationFrame(myRenderMain);
}
这只是一个示例,显示了图块的渲染以及我如何想象您的游戏可以运行,而不是作为功能代码。这种方法简化了渲染过程,只在需要时才进行。它还通过不以添加 hock (Image.onload) 方式调用绘图函数,将 GPU(图形处理单元)状态更改保持在最低限度。
它还消除了为每个平铺图像添加 onload 函数的需要,这本身就是一个额外的开销。如果像 Michael Laszlo 建议的那样为每个图像添加一个新函数,那么如果您不删除图像引用或 onload 属性 引用的函数,则可能会 运行 内存泄漏的风险。最好尽量减少与 DOM 和 DOM 对象(例如 HTMLImageElement
)的所有交互,如果不需要,请始终确保取消引用它们,这样它们就不会浪费内存。
如果浏览器 window 不可见,requestAnimationFrame
也会停止调用,从而允许设备上的其他进程有更多 CPU 时间,从而更加用户友好。
编写游戏需要密切关注性能,一点一滴都会有所帮助。
我似乎很清楚如何使用 JavaScript 动态加载和绘制图像。我附加了一个 onload
函数并设置了图像源:
var img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
}
img.src = "your_img_path";
我想对二维图像(图块)阵列执行此操作并多次更新这些图块。但是,如后所述,我 运行 遇到了实施此问题的问题。
我编写的函数(如下所示)包含一些与我当前问题无关的计算。它们的目的是让图块成为 panned/zoomed(模拟相机)。选择要加载的图块取决于请求渲染的位置。
setInterval(redraw, 35);
function redraw()
{
var canvas = document.getElementById("MyCanvas");
var ctx = canvas.getContext("2d");
//Make canvas full screen.
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
//Compute the range of in-game coordinates.
var lowestX = currentX - (window.innerWidth / 2) / zoom;
var highestX = currentX + (window.innerWidth / 2) / zoom;
var lowestY = currentY - (window.innerHeight / 2) / zoom;
var highestY = currentY + (window.innerHeight / 2) / zoom;
//Compute the amount of tiles that can be displayed in each direction.
var imagesX = Math.ceil((highestX - lowestX) / imageSize);
var imagesY = Math.ceil((highestY - lowestY) / imageSize);
//Compute viewport offsets for the grid of tiles.
var offsetX = -(mod(currentX, imageSize) * mapZoom);
var offsetY = -(mod(currentY, imageSize) * mapZoom);
//Create array to store 2D grid of tiles and iterate through.
var imgArray = new Array(imagesY * imagesX);
for (var yIndex = 0; yIndex < imagesY; yIndex++) {
for (var xIndex = 0; xIndex < imagesX; xIndex++) {
//Determine name of image to load.
var iLocX = (lowestX + (offsetX / mapZoom) + (xIndex * imageSize));
var iLocY = (lowestY + (offsetY / mapZoom) + (yIndex * imageSize));
var iNameX = Math.floor(iLocX / imageSize);
var iNameY = Math.floor(iLocY / imageSize);
//Construct image name.
var imageName = "Game/Tiles_" + iNameX + "x" + iNameY + ".png";
//Determine the viewport coordinates of the images.
var viewX = offsetX + (xIndex * imageSize * zoom);
var viewY = offsetY + (yIndex * imageSize * zoom);
//Create Image
imgArray[yIndex * imagesX + xIndex] = new Image();
imgArray[yIndex * imagesX + xIndex].onload = function() {
ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
imgArray[yIndex * imagesX + xIndex].src = imageName;
}
}
}
如您所见,函数中声明了一个数组,其大小等于显示的图块网格的大小。数组的元素表面上充满了动态加载后绘制的图像。不幸的是,imgArray[yIndex * imagesX + xIndex]
不被我的浏览器视为图像,我收到错误消息:
Uncaught TypeError: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(HTMLImageElement or HTMLVideoElement or HTMLCanvasElement or ImageBitmap)'
另外,我本来以为不需要数组。毕竟,如果我只是要在下次调用 redraw
时加载一个新图像,为什么我需要在完成绘制后保留图像的句柄?在这种情况下,循环如下所示:
for (var yIndex = 0; yIndex < imagesY; yIndex++) {
for (var xIndex = 0; xIndex < imagesX; xIndex++) {
//Determine name of image to load.
var iLocX = (lowestX + (offsetX / mapZoom) + (xIndex * imageSize));
var iLocY = (lowestY + (offsetY / mapZoom) + (yIndex * imageSize));
var iNameX = Math.floor(iLocX / imageSize);
var iNameY = Math.floor(iLocY / imageSize);
//Construct image name.
var imageName = "Game/Tiles_" + iNameX + "x" + iNameY + ".png";
//Determine the viewport coordinates of the images.
var viewX = offsetX + (xIndex * imageSize * zoom);
var viewY = offsetY + (yIndex * imageSize * zoom);
//Create Image
var img = new Image();
img.onload = function() {
ctx.drawImage(img, viewX, viewY, imageSize * zoom, imageSize * zoom);
};
img.src = imageName;
}
}
自从上次处理的图块被渲染后,我使用这种方法的运气就更好了。尽管如此,所有其他瓷砖都没有绘制。我想我可能一直在覆盖一些东西,所以只有最后一个保留了它的值。
那么,我该怎么做才能重复渲染 2D 网格图块,以便始终加载图块?
为什么数组元素不被视为图像?
您犯了所有 JavaScript 程序员都曾犯过的经典错误。问题在这里:
imgArray[yIndex * imagesX + xIndex].onload = function() {
ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
您在循环中定义一个函数,并期望每个函数都有自己的变量副本。事实并非如此。您定义的每个函数都使用相同的变量 yIndex
、xIndex
、viewX
和 viewY
。请注意,函数使用这些变量,而不是这些变量的值。
这很重要:你在循环中定义的函数在循环中不使用yIndex
(和xIndex
、viewX
、viewY
)的值您定义函数的时间。函数使用变量本身。每个函数在执行函数时计算 yIndex
(以及 xIndex
和其余)。
我们说这些变量已经绑定在一个closure中了。当循环结束时,yIndex
和 xIndex
超出范围,因此函数计算超出数组末尾的位置。
解决方案是编写另一个函数,将 yIndex
、xIndex
、viewX
和 viewY
传递给该函数,然后创建一个捕获您要使用的值。这些值将存储在与循环中的变量分开的新变量中。
您可以将此函数定义放在 redraw()
中,在循环之前:
function makeImageFunction(yIndex, xIndex, viewX, viewY) {
return function () {
ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
}
现在将有问题的行替换为对函数制作函数的调用:
imgArray[yIndex * imagesX + xIndex].onload = makeImageFunction(yIndex, xIndex, viewX, viewY);
此外,请确保您设置的图片来源正确:
imgArray[yIndex * imagesX + xIndex].src = imageName;
为了使您的代码更易读和更易于维护,请分解掉yIndex * imagesX + xIndex
的重复计算。最好计算一次图像索引,然后到处使用。
您可以像这样简化 makeImageFunction
:
function makeImageFunction(index, viewX, viewY) {
return function () {
ctx.drawImage(imgArray[index], viewX, viewY, imageSize * zoom, imageSize * zoom);
};
}
然后在循环中写下:
// Create image.
var index = yIndex * imagesX + xIndex;
imgArray[index] = new Image();
imgArray[index].onload = makeImageFunction(index, viewX, viewY);
imgArray[index].src = imageName;
您可能还希望分解出 imgArray[index]
:
// Create image.
var index = yIndex * imagesX + xIndex;
var image = imgArray[index] = new Image();
image.onload = makeImageFunction(index, viewX, viewY);
image.src = imageName;
在这一点上,我们问自己为什么我们有一个图像数组。如果,正如您在问题中所说,一旦您加载了图像,您就不再需要数组,我们可以完全摆脱它。
现在我们传递给 makeImageFunction
的参数是图像:
function makeImageFunction(image, viewX, viewY) {
return function () {
ctx.drawImage(image, viewX, viewY, imageSize * zoom, imageSize * zoom);
};
}
在循环中我们有:
// Create image.
var image = new Image();
image.onload = makeImageFunction(image, viewX, viewY);
image.src = imageName;
这看起来与您在问题中提到的替代方案相似,只是它在循环内没有函数定义。相反,它调用一个函数制作函数,该函数在新函数中捕获 image
、viewX
和 viewY
的当前值。
现在你可以弄清楚为什么你的原始代码只绘制了最后一张图像。您的循环定义了一大堆函数,它们都引用了变量 image
。当循环结束时,image
的值是您制作的最后一张图像,因此所有函数都引用最后一张图像。因此,他们都画了同一个图像。他们把它画在同一个位置,因为他们都使用相同的变量 viewX
和 viewY
.
Michael Laszlo 提供了一个很好的答案,但我想在这种情况下补充它,因为这是关于具有特殊考虑的图形应用程序。
更新 canvas 的内容时,您应该将其全部保存在一个函数中。当所有其他页面布局和呈现都已完成时,应调用该函数。此函数每秒调用次数不应超过 60 次。以临时方式访问 canvas 很混乱,并且会减慢整个页面的速度。
浏览器提供了一种方法来确保 canvas 重绘的时间与其自身的布局和渲染同步。 window.requestAnimationFrame (renderFunction);
参见 https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
没必要给每张图片都加上onload事件,浪费资源。您只需将图像添加到数组,然后在每个动画帧上通过检查每个图像的 complete
属性 来检查数组中的图像是否已加载。
我假设图块是背景的一部分,因此只需要渲染一次。最好创建一个关闭页面 canvas 对象并向其呈现图块,然后将 canvas 呈现到用户看到的页面 canvas。
... // all in scope of your game object
// create a background canvas and get the 2d context
function setup(){
// populate your imageArray with the image tiles
// Now call requestAmimationFrame
window.requestAnimationFrame(myRenderTileSetup);
}
function myRenderTileSetup(){ // this renders the tiles as they are avalible
var renderedCount = 0;
for(y = 0; y < tilesHeight; y+=1 ){
for(x = 0; x < tilesWidth; x+=1 ){
arrayIndex = x+y*tilesWidth;
// check if the image is available and loaded.
if(imageArray[arrayIndex] && imageArray[arrayIndex].complete){
backGroundCTX.drawImage(imageArray[arrayIndex],... // draw the image
imageArray[arrayIndex] = undefined; // we dont need the image
// so unreference it
}else{ // the image is not there so must have been rendered
renderedCount += 1; // count rendered tiles.
}
}
}
// when all tiles have been loaded and rendered switch to the game render function
if(renderedCount === tilesWidth*tilesHeight){
// background completely rendered
// switch rendering to the main game render.
window.requestAnimationFrame(myRenderMain);
}else{
// if not all available keep doing this till all tiles complete/
window.requestAnimationFrame(myRenderTileSetup);
}
}
function myRenderMain(){
... // render the background tiles canvas
... // render game graphics.
// now ready for next frame.
window.requestAnimationFrame(myRenderMain);
}
这只是一个示例,显示了图块的渲染以及我如何想象您的游戏可以运行,而不是作为功能代码。这种方法简化了渲染过程,只在需要时才进行。它还通过不以添加 hock (Image.onload) 方式调用绘图函数,将 GPU(图形处理单元)状态更改保持在最低限度。
它还消除了为每个平铺图像添加 onload 函数的需要,这本身就是一个额外的开销。如果像 Michael Laszlo 建议的那样为每个图像添加一个新函数,那么如果您不删除图像引用或 onload 属性 引用的函数,则可能会 运行 内存泄漏的风险。最好尽量减少与 DOM 和 DOM 对象(例如 HTMLImageElement
)的所有交互,如果不需要,请始终确保取消引用它们,这样它们就不会浪费内存。
requestAnimationFrame
也会停止调用,从而允许设备上的其他进程有更多 CPU 时间,从而更加用户友好。
编写游戏需要密切关注性能,一点一滴都会有所帮助。