简体   繁体   English

用JavaScript加载并绘制平铺图像到画布

[英]Load and draw tiled images to canvas in JavaScript

It seems clear to me how to dynamically load and draw an image with JavaScript. 对我来说似乎很清楚如何使用JavaScript动态加载和绘制图像。 I attach an onload function and set the image source: 我附加了onload函数并设置了图像源:

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

I want to do this for a 2D array of images (tiles) and update those tiles many times. 我想对图像(图块)的2D数组执行此操作,并多次更新这些图块。 However, as described later, I run into issues with implementing this. 但是,如稍后所述,在实现此功能时会遇到问题。

The function that I've written, and which I show below, contains some computations that are not relevant to my current problem. 我编写的函数(如下所示)包含一些与当前问题无关的计算。 Their purpose is to allow for the tiles to be panned/zoomed (simulating a camera). 它们的目的是允许平铺/缩放平铺(模拟摄像机)。 The tile chosen to be loaded depends on the location requested to render. 选择要加载的图块取决于请求渲染的位置。

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

As you can see, there is an array declared in the function to equal the size of the displayed grid of tiles. 如您所见,函数中声明了一个数组,该数组等于显示的图块网格的大小。 The elements of the array are ostensibly filled with images that are drawn once dynamically loaded. 表面上,数组的元素填充有动态加载后绘制的图像。 Unfortunately, imgArray[yIndex * imagesX + xIndex] is not considered an image by my browser, and I receive the error: 不幸的是,我的浏览器未将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)' 未捕获到的TypeError:无法在'CanvasRenderingContext2D'上执行'drawImage':提供的值不是'(HTMLImageElement或HTMLVideoElement或HTMLCanvasElement或ImageBitmap)类型的值

Also, I had originally thought the array wasn't necessary. 另外,我最初以为该数组不是必需的。 After all, why should I need to keep a handle to the image after I've finished drawing it if I'm just going to load a new one next time redraw is called? 毕竟,如果我下次要加载一个新的redraw画时,为什么我在画完图像后还需要保持该图像的句柄? In this scenario, the loop looked like this: 在这种情况下,循环如下所示:

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

I had better luck with this method since the last processed tile was rendered. 自从上次处理的磁贴被渲染以来,我对这种方法的运气更好。 Despite this, all other tiles were not drawn. 尽管如此,其他所有图块均未绘制。 I guess I might have been overwriting things so that only the last kept its values. 我想我可能一直在改写东西,以便只有最后一个保留它的值。

  1. So, what can I do so that I can render a 2D grid of tiles repeatedly such that the tiles are loaded all the time? 那么,我该怎么做才能重复渲染2D瓷砖网格,从而使瓷砖始终处于加载状态?

  2. And why is the array element not considered an image? 为何数组元素不被视为图像?

You've made the classic mistake that all JavaScript programmers make at one time or another. 您犯了所有JavaScript程序员一次或一次犯的经典错误。 The problem is here: 问题在这里:

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

You're defining a function in a loop and expecting each function to have its own copies of the variables. 您正在循环中定义一个函数,并期望每个函数都有自己的变量副本。 That's not what happens. 那不是那样 Every function you define uses the same variables yIndex , xIndex , viewX , and viewY . 您定义的每个函数都使用相同的变量yIndexxIndexviewXviewY Note that the functions use those variables, not the values of those variables. 请注意,函数使用这些变量,而不是这些变量的值。

This is very important: the functions you define in the loop do not use the value of yIndex (and xIndex , viewX , viewY ) at the time you defined the function. 这非常重要:在循环中定义的函数在定义函数时不使用yIndex的值(以及xIndexviewXviewY )。 The functions use the variables themselves. 这些函数使用变量本身。 Each function evaluates yIndex (and xIndex and the rest) at the time that the function is executed. 每个函数在执行该函数时都会评估yIndex (以及xIndex和其余的)。

We say that these variables have been bound in a closure . 我们说这些变量已经绑定在一个闭包中 When the loop ends, yIndex and xIndex are out of range, so the functions calculate a position beyond the end of the array. 循环结束时, yIndexxIndex超出范围,因此函数计算的位置超出数组的末尾。

The solution is to write another function to which you pass yIndex , xIndex , viewX , and viewY , and which makes a new function that captures the values that you want to use. 解决方案是编写另一个函数, xIndex传递yIndexxIndexviewXviewY ,并创建一个新函数以捕获要使用的值。 These values will be stored in new variables that are separate from the variables in the loop. 这些值将存储在与循环中的变量分开的新变量中。

You can put this function definition inside redraw() , before the loop: 您可以将此函数定义放在循环之前的redraw()

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

Now replace the problematic lines with a call to the function-making function: 现在,用对函数创建函数的调用来替换有问题的行:

imgArray[yIndex * imagesX + xIndex].onload = makeImageFunction(yIndex, xIndex, viewX, viewY);

Also, make sure that you're setting the source of the correct image: 另外,请确保您设置的是正确图像的来源:

imgArray[yIndex * imagesX + xIndex].src = imageName;

To make your code easier to read and easier to maintain, factor out the repeated calculation of yIndex * imagesX + xIndex . 为了使您的代码更易于阅读和维护,请排除重复计算yIndex * imagesX + xIndex It would be better to calculate the image index once and use it everywhere. 最好一次计算图像索引并在各处使用它。

You can simplify makeImageFunction like so: 您可以像这样简化makeImageFunction

function makeImageFunction(index, viewX, viewY) {
    return function () {
        ctx.drawImage(imgArray[index], viewX, viewY, imageSize * zoom, imageSize * zoom);
    };
}

Then write this inside the loop: 然后在循环中编写以下代码:

// Create image.
var index = yIndex * imagesX + xIndex;
imgArray[index] = new Image();
imgArray[index].onload = makeImageFunction(index, viewX, viewY);
imgArray[index].src = imageName;

You may wish to factor out imgArray[index] as well: 您可能还希望排除imgArray[index]

// Create image.
var index = yIndex * imagesX + xIndex;
var image = imgArray[index] = new Image();
image.onload = makeImageFunction(index, viewX, viewY);
image.src = imageName;

At this point we ask ourselves why we have an image array at all. 在这一点上,我们自问为什么我们要有一个图像阵列。 If, as you say in your question, you have no use for the array once you've loaded the image, we can get rid of it altogether. 如您在问题中所述,如果在加载图像后不再使用该数组,我们可以完全摆脱它。

Now the argument that we pass to makeImageFunction is the image: 现在,我们传递给makeImageFunction的参数是图像:

function makeImageFunction(image, viewX, viewY) {
    return function () {
        ctx.drawImage(image, viewX, viewY, imageSize * zoom, imageSize * zoom);
    };
}

And inside the loop we have: 在循环内部,我们有:

// Create image.
var image = new Image();
image.onload = makeImageFunction(image, viewX, viewY);
image.src = imageName;

This looks similar to the alternative scenario you mention in your question, except that it does not have the function definition inside the loop. 这看起来与您在问题中提到的替代方案类似,不同之处在于它在循环内没有函数定义。 Instead it calls a function-making function which captures the current values of image , viewX , and viewY in a new function. 而是调用一个函数制作函数,该函数在新函数中捕获imageviewXviewY的当前值。

Now you can work out why your original code only drew the last image. 现在您可以弄清楚为什么原始代码只绘制了最后一张图像。 Your loop defined a whole bunch of functions that all referred to the variable image . 您的循环定义了一堆函数,所有这些函数都引用了变量image When the loop was over, the value of image was the last image you made, so all of the functions referred to the last image. 循环结束后, image的值就是您制作的最后一张图像,因此所有功能都引用了最后一张图像。 Thus, they all drew the same image. 因此,他们都绘制了相同的图像。 And they drew it in the same location, because all of them were using the same variables viewX and viewY . 他们将其绘制在相同的位置,因为它们都使用相同的变量viewXviewY

Michael Laszlo has provided a great answer but I would like to add to it in this situation because this is in regards to a graphics application which has special considerations. Michael Laszlo提供了一个很好的答案,但是在这种情况下,我想补充一下,因为这是针对具有特殊考虑的图形应用程序。

When updating the content of the canvas you should keep it all in one function. 更新画布的内容时,应将其全部保留在一个功能中。 that function should be called when all the other page layout and rendering has been completed. 当所有其他页面布局和呈现均已完成时,应调用该函数。 This function should not be called more than 60 times a second. 每秒调用此函数的次数不应超过60次。 Accessing the canvas in an ad hock fashion is messy and will slow the whole page down. 以广告飞跃的方式访问画布很麻烦,并且会降低整个页面的速度。

Browsers provide a method for ensuring the timing of the canvas redraw is in sync with its own layout and rendering. 浏览器提供了一种方法,以确保画布重绘的时间与其自己的布局和渲染保持同步。 window.requestAnimationFrame (renderFunction); see https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame 参见https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame

There is no need to add the onload event to each image, it is a waste of resources. 无需将onload事件添加到每个图像,这是浪费资源。 You simply add the image to an array then on each animation frame you check if the image in the array has been loaded by inspecting the complete property of each image. 您只需将图像添加到数组中,然后在每个动画帧上通过检查每个图像的complete属性来检查数组中的图像是否已加载。

I am assuming that the tiles are part of a background so only needs to be rendered once. 我假设图块是背景的一部分,因此只需要渲染一次即可。 It is best to create a off page canvas object and render the tiles to it, then render that canvas to the on page canvas the user sees. 最好创建一个页面外画布对象并向其渲染图块,然后将该画布渲染为用户看到的页面上画布。

... // 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);
}

This is just an example showing the rendering of tiles and how I might imagine your game to function and not intended as functional code. 这只是一个示例,显示了磁贴的渲染以及我如何想象您的游戏正常运行,而不是用作功能代码。 This method simplifies the rendering process and only does it when required. 此方法简化了渲染过程,并且仅在需要时才执行。 It also keeps the GPU (Graphics Processing Unit) state changes to a minimum by not calling the draw function on an add hock (Image.onload) way. 通过不以添加指令(Image.onload)的方式调用draw函数,还可以使GPU(图形处理单元)状态的变化降至最低。

It also does away with the need to add onload function to each tiled image, which its self is an extra overhead. 它还消除了向每个平铺图像添加onload函数的需要,这本身就是额外的开销。 If as Michael Laszlo suggested you add a new function for each image you can run the risk of a memory leak if you do not delete the image reference or the function referenced by the onload property. 如果按照Michael Laszlo的建议为每个图像添加一个新功能,则如果不删除图像引用或onload属性所引用的功能,则可能会出现内存泄漏的风险。 It is always best to minimize all interaction with the DOM and DOM objects such as HTMLImageElement and if not needed always ensure you dereferance them so they dont hang around wasting memory. 始终最好最大程度地减少与DOM和DOM对象(例如HTMLImageElement所有交互,如果不需要,请始终确保取消引用它们,以免它们浪费内存。

requestAnimationFrame also stops being called if the browser window is not visible allowing other processes on the device more CPU time and thus being more user friendly. 如果浏览器窗口不可见,则requestAnimationFrame也会停止调用,从而使设备上的其他进程有更多的CPU时间,从而更加用户友好。

Writing games requires a careful eye on performance and every little bit helps. 编写游戏需要对性能有仔细的了解,每一点都有帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM