Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Small Delay on Drawing Large Images for the First Time in Chrome

I'm working on a simple JavaScript based game using the canvas tag. As part of the game I have several large sprite sheets (e.g. 2816x768 and 4096x4864) that each contain related animations for the on screen character. When the game starts, the game simply has the character's idle animation playing. When the user presses space, I start playing another animation from an entirely different spite sheet.

Here is the code that draws the sprites:

Sprite.prototype.drawFrame = function(x, y)
{
    ctx.drawImage(this.image,
        x*this.width, y*this.height,
        this.width, this.height,

        this.position[0], this.position[1],
        this.width, this.height);
};

And here is the code that loads images:

Stage.prototype.loadImage = function(src)
{
    var image = new Image();
    this.incAssets();
    var stage = this;
    image.onload = function()
    {
        stage.decAssets();
    }
    image.src = src;
    return image;
}

The problem is that there is a 1.5 second delay from when the user presses space and when the frame from the new sprite sheet actually gets drawn. This is one time only and does not effect the smoothness of the following animation. I already have the sprite sheets preloaded using new Image and the game won't even start until all the corresponding image.onload events have fired, so I know the browser isn't waiting for them to load. I've stepped through the JavaScript using the debugger in Chrome 17.0 and have narrowed the delay to the drawImage call on the context. The most baffling thing is this delay is not present in Firefox 10.0.2, so it's a Chrome specific problem. It's very disrupting to the game play.

What am I doing wrong here? Is there anyway I can reduce this delay in Chrome?

Edit: I tried drawing the entire next frame as soon as it loads as suggested by Peter Wishart, but that had minimal effect. I also tried modifying the loadImage as follows:

Stage.prototype.loadImage = function(src)
{
    var image = new Image();
    this.incAssets();
    var stage = this;
    image.onload = function()
    {
        ctx.drawImage(image, 0, 0);
        stage.decAssets();
    }
    image.src = src;
    return image;
};

This too showed no effect.

In fact, I did find a solution, but it's terribly inefficient. It occurred to me that Chrome might be trying to do smart things with the image memory after it's decoded. If an image goes unused for long enough, and again this is only a guess, Chrome will delete the decoded data from memory, and pull it back in if it's needed again. Normally the decoding process takes an unnoticeable amount of time, but the large images I use cause a very drastic blip in performance. Using this I changed the draw loop to:

function draw()
{
    var currentTime = new Date().getTime();
    var deltaTime = currentTime - lastTime;
    lastTime = currentTime;

    var dt = deltaTime / 1000.0;

    // The hack that keeps all decoded image data in memory is as following.
    if (this.stage.nextStage != undefined) 
        this.stage.nextStage.draw(0); // The 0 here means the animations advance by 0 milliseconds, thereby keeping the state static.

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
    if (stage != undefined && stage.loaded)
    {
        stage.draw(dt);
    }
}

This solution does work, but like I said, it seems like a terrible waste. I have to draw the entire frame of the next set of animations, just to keep the decoded data from getting stale in Chrome.

Are there any less wasteful and less hacky alternatives to this strategy?

like image 529
zach297 Avatar asked Oct 09 '22 12:10

zach297


2 Answers

With the idea that Chrome was just throwing away the decoded image data after some time, I tried just copying the images into an offscreen canvas, under the assumption that Chrome would not bother with taking that canvas out of memory. The code to do that fit nicely into the loadImage function.

Stage.prototype.loadImage = function(src)
{
    var useCanvasCache = Prototype.Browser.WebKit; // If we are in a WebKit browser (e.g. Chrome)
    var decodeCanvas;
    var dectodeCtx;
    if (useCanvasCache)
    {
        // Creates a canvas to store the decoded image.
        decodeCanvas = document.createElement('canvas');
        dectodeCtx = decodeCanvas.getContext('2d');
    }
    var image = new Image();
    this.incAssets();
    var stage = this;
    image.onload = function()
    {
        stage.decAssets();
        // Simply transfer the image to the canvas to keep the data unencoded in memory.
        if (useCanvasCache)
        {
            decodeCanvas.width = image.width;
            decodeCanvas.height = image.height;
            dectodeCtx.drawImage(image, 0, 0);
        }

    }
    image.src = src;
    // Canvas works the same as an image in a drawImage call, so we can decide which to return.
    if (useCanvasCache)
    {
        return decodeCanvas;
    }
    else
    {
        return image;
    }
};

It works too. There is a small initial penalty when the page does it loading, and it likely uses more memory, but it's a workable solution, as speed is more important than memory in this application.

like image 67
zach297 Avatar answered Oct 13 '22 05:10

zach297


It seems like chrome has added to the decode function for precisely this purpose, preventing decoding delay on first add to DOM.

Not implemented in firefox or IE it seems.

const img = new Image();
img.src = "bigImage.jpg";
img.decode().then(() => {
     document.body.appendChild(img);
}).catch(() => {
    throw new Error('Could not load/decode big image.');
});

More information: https://medium.com/dailyjs/image-loading-with-image-decode-b03652e7d2d2

like image 30
Simon Epskamp Avatar answered Oct 13 '22 05:10

Simon Epskamp