javascript - 在 JavaScript 中加载平铺图像并将其绘制到 Canvas 上

标签 javascript image canvas game-engine

我似乎很清楚如何使用 JavaScript 动态加载和绘制图像。我附加一个 onload 函数并设置图像源:

var img = new Image();
img.onload = function() {
    ctx.drawImage(img, 0, 0);
}
img.src = "your_img_path";

我想对二维图像数组(图 block )执行此操作并多次更新这些图 block 。但是,正如稍后所述,我在实现此操作时遇到了问题。

我编写的函数(如下所示)包含一些与我当前问题无关的计算。它们的目的是允许平移/缩放图 block (模拟相机)。选择加载的图 block 取决于请求渲染的位置。

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

如您所见,函数中声明了一个数组,该数组等于显示的图 block 网格的大小。数组的元素表面上充满了动态加载后绘制的图像。不幸的是,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;
    }   
}

自从渲染了最后处理的图 block 以来,我使用此方法的运气更好。尽管如此,所有其他图 block 都没有绘制。我想我可能已经覆盖了一些东西,所以只有最后一个保留了它的值。

  1. 那么,我该怎么做才能重复渲染图 block 的 2D 网格,以便始终加载图 block ?

  2. 为什么数组元素不被视为图像?

最佳答案

你犯了所有 JavaScript 程序员都曾犯过的经典错误。问题出在这里:

imgArray[yIndex * imagesX + xIndex].onload = function() {
    ctx.drawImage(imgArray[yIndex * imagesX + xIndex], viewX, viewY, imageSize * zoom, imageSize * zoom);
};

您在循环中定义一个函数,并期望每个函数都有自己的变量副本。事实并非如此。您定义的每个函数都使用相同的变量yIndexxIndexviewXviewY。请注意,函数使用这些变量,而不是这些变量的值。

这非常重要:您在循环中定义的函数不使用 yIndex 的值(以及 xIndexviewXviewY) 在您定义函数时。这些函数本身使用变量。每个函数在执行函数时都会计算 yIndex(以及 xIndex 和其余部分)。

我们说这些变量已绑定(bind)在 closure 中。当循环结束时,yIndexxIndex 超出范围,因此函数计算的位置超出了数组末尾。

解决方案是编写另一个函数,向其传递 yIndexxIndexviewXviewY ,这会创建一个新函数来捕获您要使用的值。这些值将存储在与循环中的变量分开的新变量中。

您可以将此函数定义放在 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;

这看起来与您在问题中提到的替代方案类似,只是它在循环内没有函数定义。相反,它调用一个函数创建函数,该函数在新函数中捕获 imageviewXviewY 的当前值。

现在您可以弄清楚为什么原始代码只绘制最后一张图像了。您的循环定义了一大堆函数,它们都引用变量image。当循环结束时,image 的值是您制作的最后一个图像,因此所有函数都引用最后一个图像。因此,他们都画了同一个图像。他们将其绘制在同一位置,因为它们都使用相同的变量 viewXviewY

关于javascript - 在 JavaScript 中加载平铺图像并将其绘制到 Canvas 上,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32153304/

相关文章:

css - svg, Canvas 在一起而不使用绝对位置

javascript - 是否有可能通过 jQuery 中的索引号获取父元素?或者还有其他办法吗?

javascript - 使用(表)执行多选列表的 jquery 程序时出现问题

javascript - 如果至少一个字段已填充值/或选定的下拉列表,则启用提交按钮

javascript - 在 JavaScript 中使用异步 AJAX 请求管理异步 IndexedDB

image - 在不改变图像尺寸的情况下减小非常大图像的文件大小

javascript - JavaScript 的 2D 引擎

javascript - 在 Canvas 中使用 Clip 会导致像素

java - 如何在 Swing 中有效地绘制图像的子部分

ios - 使用swift在图像上叠加文字