Horrible Canvas GetImageData() / PutImageData() performance on mobile
Asked Answered
B

2

8

I'm doing a little HTML5 game and, while loading my sprites at the beginning of the map, I do some processing with GetImageData() / looping over all the image / PutImageData().

This works fantastically great on my PC, however, on my cell phones it's horrendously slow.

PC: 5-6 ms
iPhone 4: 300-600 ms
Android HTC Desire S: 2500-3000 ms

I've been doing some VERY basic benchmarking, and both GetImageData and PutImageData run very fast, what's taking long is the looping through the contents.

Now, I obviously expect a slowdown on the phone, but 1000x sounds a bit excessive, and the loading takes about 4 minutes on my HTC, so that's not going to work. Also, everything else in the game works at very reasonable speed (mainly because the screen is ridiculously smaller, but still, it works surprisingly fine for JS on a cell phone)


What I'm doing in this processing is basically "darkening" the sprite to a certain level. I simply loop through all the pixels, and multiply them by a value < 1. That's all.

Since this is too slow... Is there a better way of doing the same thing, using the Canvas functionality, (compositing, opacity, whatever), without looping through all the pixels one by one?

NOTE: This layer has some 100% transparent pixels, and some 100% opaque pixels. Both need to remain either 100% opaque or 100% transparent.

Things I've thought of that wouldn't work:
1) Painting the sprites in a new canvas, with lower opacity. This won't work because i need the sprites to remain opaque, just darker.
2) Painting the sprites, and painting a semi-transparent black rect on top of them. This will make them darker, but it'll also make my transparent pixels not transparent anymore...

Any ideas?

This is the code I'm using, just in case you see something terribly idiotic in it:

function DarkenCanvas(baseImage, ratio) {
    var tmpCanvas = document.createElement("canvas");
    tmpCanvas.width = baseImage.width;
    tmpCanvas.height = baseImage.height;
    var ctx = tmpCanvas.getContext("2d");
    ctx.drawImage(baseImage, 0, 0);

    var pixelData = ctx.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height);
    var length = pixelData.data.length;
    for (var i = 0; i < length; i+= 4) {
        pixelData.data[i] = pixelData.data[i] * ratio;
        pixelData.data[i + 1] = pixelData.data[i + 1] * ratio;
        pixelData.data[i + 2] = pixelData.data[i + 2] * ratio;
    }

    ctx.putImageData(pixelData, 0, 0);
    return tmpCanvas
}

EDIT: This is an example of what i'm trying to do with the image:
Original: http://www.crystalgears.com/isoengine/sprites-ground.png
Darkened: http://www.crystalgears.com/isoengine/sprites-ground_darkened.png

Thanks!
Daniel

Boehmenist answered 10/12, 2011 at 1:37 Comment(4)
please show us before and after photos of the effect you are trying to achieve?Unhandled
Just added links at the bottom of the question. Thanks!Boehmenist
You're just trying to darken some sprites? For goodness sakes, don't use per-pixel image data for that. Just draw semi-transparent black overtop (with a different globalCompositeOperation to preserve alpha).Nocturnal
Thank you! That's the exact kind of answer i'm looking for! I'm doing it this way because it's how I used to do it in the pre-web times, so it's all I know. Which composition should I use? Also, could you post this as an answer so I can upvote you as much as I can? Thanks!Boehmenist
U
12

Phrogz has the right idea. You really just want to paint a half-transparent (or ratio-transparent) black over the whole thing with 'source-atop' globalCompositeOperation.

Like this: http://jsfiddle.net/F4cNg/

There was actually a good question on sophisticated darkening like this but the author deleted it which is a real shame. It's certainly possible to make dark-masks on drawn stuff, even with sophisticated shapes. They may not help you, but for the sake of completeness and for anyone searching for "darken canvas" stuff where some parts are kept light (exclusion zones), that can be done with the 'xor' globalCompositeOperation, like this:

http://jsfiddle.net/k6Xwy/1/

Unhandled answered 10/12, 2011 at 4:14 Comment(1)
Thank you so much, this is exactly what I needed. I considered darkening out the final result of the drawing. The XOR trick is cool, much simpler than that I was planning to do, but even without it I could just paint a rect with "holes". At that point, I don't care about my transparent pixels anymore. I'm not taking that approach, however, because i'm using an ISO perspective, and my method, while way slower, lets me have a bit more accuracy on a few things I care about. Thanks for the tip, though, i'll definitely come in handy sometime. THANKS!!!Boehmenist
H
11

Have you seen this performance tip?: http://ajaxian.com/archives/canvas-image-data-optimization-tip

They talk about reducing calls to the DOM to increase performance.

So, based on this tip you might try:

var pixel = ctx.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height);
    pixelData=pixel.data; // detach the pixel array from DOM
    var length = pixelData.length;
    for (var i = 0; i < length; i+= 4) {
        pixelData[i] = pixelData[i] * ratio;
        pixelData[i + 1] = pixelData[i + 1] * ratio;
        pixelData[i + 2] = pixelData[i + 2] * ratio;
    }

Your .data calls may be slowing you down.

Handicapped answered 6/10, 2012 at 2:20 Comment(1)
Absolutely true. Not a big difference though, but sure saves some milliseconds.👍Vastah

© 2022 - 2024 — McMap. All rights reserved.