Saving canvas to image via canvas.toDataURL results in black rectangle - javascript

Im using Pixi.js and trying to save a frame of the animation to an image. canvas.toDataUrl should work, but all i get is a black rectangle. See live example here
the code I use to extract the image data and set the image is:
var canvas = $('canvas')[0];
var context = canvas.getContext('2d');
$('button').click(function() {
var data = renderer.view.toDataURL("image/png", 1);
//tried var data = canvas.toDataURL();
$('img').attr('src', data);
})

I know this has been answered at least 5 other times on SO but ...
What Kaiido mentioned will work but the real issue is that canvas, when used with WebGL, by default has 2 buffers. The buffer you are drawing to and the buffer being displayed.
When you start drawing into a WebGL canvas, as soon as you exit the current event, for example your requestAnimationFrame callback, the canvas is marked for swapping those 2 buffers. When the browser re-draws the page it does the swap. The buffer that you were drawing to is swapped with the one that was being displayed. You're now drawing to other buffer. That buffer is cleared.
The reason it's cleared instead of just left alone is that whether the browser actually swaps buffers or does something else is up to the browser. For example if antialiasing is on (which is the default) then it doesn't actually do a swap. It does a "resolve". It converts the highres buffer you just drew to a normal res anti-aliased copy into the display buffer.
So, to make it more consistent, regardless of which way the browser does its default it just always clears whatever buffer you're about to draw to. Otherwise you'd have no idea if it had 1 frame old data or 2 frame old data.
Setting preserveDrawingBuffer: true tells the browser "always copy, never swap". In this case it doesn't have to clear the drawing buffer because what's in the drawing buffer is always known. No swapping.
What is the point of all that? The point is, if you want to call toDataURL or gl.readPixels you need to call it IN THE SAME EVENT.
So for example your code could work something like this
var capture = false;
$('button').click(function() {
capture = true;
});
function render() {
renderer.render(...);
if (capture) {
capture = false;
var data = renderer.view.toDataURL("image/png", 1);
$('img').attr('src', data);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
In that case because you call toDataURL in the same javascript event as you rendered to it you'll get the correct results always regardless of wither or not preserveDrawingBuffer is true or false.
If you're writing app that is not constantly rendering you could also do something like
$('button').click(function() {
// render right now
renderer.render(...);
// capture immediately
var data = renderer.view.toDataURL("image/png", 1);
$('img').attr('src', data);
});
The reason preserveDrawingBuffer is false by default is because swapping is faster than copying so this allows the browser to go as fast as possible.
Also see this answer for one other detail

[NOTE]
While this answer is the accepted one, please do read the one by #gman just below, it does contain a way better way of doing.
Your problem is that you are using webGL context, then you need to set the preserveDrawingBuffer property of the webGL context to true in order to be able to call toDataURL() method.
Or alternatively, you can force pixi to use the 2D context, by using the CanvasRenderer Class

Related

How to get usable canvas from Mapbox GL JS

I'm using Mapbox GL, and trying to get a snapshot of it, and merge the snapshot with another image overlaid for output.
I have a HTMLCanvasElement off screen, and I'm first writing the canvas returned from Map.getCanvas() to it, then writing the second (alpha transparent) canvas over that.
The problem is that, though I clearly see elements onscreen in the Map instance, the result only shows the second image/canvas written, and the rest is blank.
So I export just the map's canvas, and I see it is because the map canvas is blank, although a console.log() shows the image data from it to be a large chunk of information.
Here's my export function:
onExport(annotationCanvas: HTMLCanvasElement) {
const mergeCanvas: HTMLCanvasElement = document.createElement('canvas');
const mapCanvas: HTMLCanvasElement = this.host.map.getCanvas();
const mergeCtx: CanvasRenderingContext2D = mergeCanvas.getContext('2d');
mergeCanvas.height = annotationCanvas.height;
mergeCanvas.width = annotationCanvas.width;
mergeCtx.drawImage(mapCanvas, 0, 0);
mergeCtx.drawImage(annotationCanvas, 0, 0);
const mergedDataURL = mergeCanvas.toDataURL();
const mapDataURL = mapCanvas.toDataURL();
const annotationDataURL = annotationCanvas.toDataURL();
console.log(mapDataURL); // Lots of data
download(mapDataURL, 'map-data-only.png'); // Blank image # 1920x1080
download(mergedDataURL, 'annotation.png'); // Only shows annotation (the second layer/canvas) data
}
Is this a bug, or am I missing something?
UPDATE: I sort of figured out what this is about, and have possible options.
Upon stumbling upon a Mapbox feature request, I learned that if you instantiate your Map with the preserveDrawingBuffer option set to false (the default), you wont be able to get a canvas with usable image data. But setting this option to true degrades performance. But you can't change this setting after a Map is instantiated...
I want the Map to perform the best it possibly can!!!!
So, on this answer I stumbled on, regarding a question about three.js, I learned that if I take the screenshot immediately after rendering, I will get the canvas/data that I need.
I tried just calling this.host.map['_rerender']() right before I capture the canvas, but it still returned blankness.
Then searching around in the source code, I found a function called _requestRenderFrame, that looks like it might be what I need, because I can ask the Map to run a function immediately after the next render cycle. But as I come to find out, for some reason, that function is omitted in the compiled code, whilst present in the source, apparently because it is only in the master, and not part of the release.
So I don't really have a satisfactory solution yet, so please let me know of any insights.
As you mentioned in your updated question the solution is to set preserveDrawingBuffer: true upon Map initialisation.
To answer your updated question I think #jfirebaugh's answer at https://github.com/mapbox/mapbox-gl-js/issues/6448#issuecomment-378307311 sums it up very well:
preserveDrawingBuffer can't be modified on the fly. It has to be set at the time the WebGL context is created, and that can have a negative effect on performance.
It's rumored that you can grab the the canvas data URL immediately after rendering, without needing preserveDrawingBuffer, but I haven't verified that, and I suspect it's not guaranteed by the spec.
So although it might be possible to grab the canvas data URL immediately after rendering, it's not guaranteed by the spec.

three.js: capture video frame for later use

I am using three.js r73. I have a fragment shader that display video frames with a line like this:
gl_FragColor = texture2D( iChannel0, uv);
I would like another uniform (say iChannel1) to contain the video frame that was displayed at some earlier point. (Then I'll do some masking between iChannel0 and iChannel1 to do motion capture/edge detection etc).
I've tried various approaches to this, but no luck.
I figure that I need to clone the video texture somehow in javascript and then assign it to iChannel1, but I don't know how to capture a frame.
I guess I could capture the canvas content but there might be other noise on the canvas that I don't want. I truly want the video frame not the canvas. Also going via canvas capture seems very roundabout. I feel like I'm just missing some API call to capture the current video frame in a way that I can make a texture that iChannel1 will use.
I looked at UniformsUtils but that doesn't seem to do the trick either.
I found a way to do this shortly after posting my question. D'oh.
Here is the way, for future reference:
if (timeToCaptureFrame) {
that.uniforms.iChannel1.value.image = that.uniforms.iChannel0.value.image;
that.uniforms.iChannel1.value.needsUpdate = true;
}
Note that iChannel1 needs to be initialized correctly for this to work. Something like this works:
var pathToSubtractionTexture = 'aStillImage.jpg';
(new THREE.TextureLoader()).load(pathToSubtractionTexture, function ( texture ) {
that.uniforms.iChannel1.value = texture;
});

Why is there no reference on image data?

Yes, I know about .getImageData()
I mean, let's say, I have to change some pixels:
var imageData = ctx.getImageData(...);
Seems, this method give me completely new copy of "real" (hiden somewhere deep from me) image-data.
I say that, because if you create new one:
var imgData2 = ctx.getImageData(.../*same parameters as before*/);
and compare two buffers:
imageData.data.buffer === imgData2.data.buffer; //false
So, each time it create new copy from it's bitmap. Oh my Gosh, Why? Okay, go further:
/*...apply some new changes to the imageData in a loop...*/
Nothing special above. But now, it's time to put this back:
ctx.putImageData(imageData, ...);
And this one inside itself run new loop and copy my imageData.
So much extra work! Is there a way to get actual imageData and manipulate it without get/put? And if no - I'm ask again - WHY? Is it security reasons? What they afraid I can do with that pixels?
Thank you!
Short answer :
No.
Not so long Answer :
You may be able to achieve it with some hacks but that would be such a pain.
Explanations :
According to the specs, getImageData returns a TypedArray which contains the canvas' image data, not a pointer to the live imageData as a modifiable object.
To paint on a canvas, you have to use methods such as fill, stroke, drawImage or putImageData ; that would make even less sense that each time you iterate through the array, the actual canvas image get modified.
Each time you call getImageData, a new TypedArray (note that the choice of the actual type is up to the UA) is created and filled with the current data of the canvasImage. This way, you can call this method, make different alterations on the ArrayBuffer, without modifying the actual image into the canvas (so you can store it, or call the method again).
As to why the buffer of the returned ImageData is not the same on each call, I think it is because "Pixels must be returned as non-premultiplied alpha values", but for performances, they do store it as premultiplied. You can see here the de-premultiplication operation from Firefox source code, which actually fills a new Uint8ClampedArray.
Also, it avoids to check if the canvasImage as been modified since the last call, and ensure you always get its current ImageData.

Collision detection using a collision array inside html5 canvas

I am trying to detect if a character and an object inside an image collide. I am using a function that can parse the image and creates a collision array and another function that can detect if there is a collision or not in a specific location. My problem is that the isCollision function is never executed that's my jsfiddle : http://jsfiddle.net/AbdiasSoftware/UNWWq/1/
if (isCollision(character.x, character.y)) {
alert("Collision");
}
Please help me to fix my problem.
Add this in the top of your init() method and it should work:
FieldImg.crossOrigin = '';
As you are loading the image from a different origin CORS kicks in and you need to request cross-origin usage when using getImageData() (or toDataURL()).
See modified fiddle here.
Note: in you final code this is probably not gonna be necessary though as you probably want to include the images in the same domain as the page itself - in these cases you need to remove the cross-origin request unless your server is setup to handle this. Just something to have in mind for later if what worked suddenly don't...
Ok i found it :
You are loading a huge background image, and you draw part of it on a much smaller canvas.
You do visual collision detection using a binary view on the display canvas that you build in the process() function.
The issue comes when you want to test : to compute the pixel position of the player within the binary view, you are using y*width + x, but with the wrong width, that of the background image when it should be that of the view (cw).
function isCollision(x, y) {
return (buffer8[y * cw + x] === 1);
}
.
move right and look in the console with this fiddle :
http://jsfiddle.net/gamealchemist/UNWWq/9/

Canvas draw calls are rendering out of sequence

I have the following code for writing draw calls to a "back buffer" canvas, then placing those in a main canvas using drawImage. This is for optimization purposes and to ensure all images get placed in sequence.
Before placing the buffer canvas on top of the main one, I'm using fillRect to create a dark-blue background on the main canvas.
However, the blue background is rendering after the sprites. This is unexpected, as I am making its fillRect call first.
Here is my code:
render: function() {
this.buffer.clearRect(0,0,this.w,this.h);
this.context.fillStyle = "#000044";
this.context.fillRect(0,0,this.w,this.h);
for (var i in this.renderQueue) {
for (var ii = 0; ii < this.renderQueue[i].length; ii++) {
sprite = this.renderQueue[i][ii];
// Draw it!
this.buffer.fillStyle = "green";
this.buffer.fillRect(sprite.x, sprite.y, sprite.w, sprite.h);
}
}
this.context.drawImage(this.bufferCanvas,0,0);
}
This also happens when I use fillRect on the buffer canvas, instead of the main one.
Changing the globalCompositeOperation between 'source-over' and 'destination-over' (for both contexts) does nothing to change this.
Paradoxically, if I instead place the blue fillRect inside the nested for loops with the other draw calls, it works as expected...
Thanks in advance!
Addenum: Changing the composite operation does behave as expected, but not for remedying this specific issue. Sorry for the ambiguity.
There's a lot that's suspect here.
First off double buffering a canvas does nothing but hurt performance by adding complication, all browsers do double buffering automatically, so if that's your goal here you shouldn't be drawing to a buffer.
Here's an example of why you don't need double buffering: http://jsfiddle.net/simonsarris/XzAjv/
So getting to the meat of the matter, lines of javascript inside a discrete function don't simply run out of order. Something else is wrong here.
Setting a breakpoint on the drawImage would solve this pretty much instantly, so if you aren't familiar with firebug or chrome developer tools I'd highly recommend giving them a look.
I'm guessing that the "blue" you're seeing is actually the only thing drawn to your "buffer" canvas and perhaps this.buffer is not actually the buffer context.
Another possibility is that this.w and this.h are accidentally very small, so that your initial clearRect and fillRect at the start of the method are doing nothing.
In any case speculation is nowhere near as good as opening up developer tools and actually looking at what's happening.
Generally speaking if you need things to be in order use an array not an object. Iterating over an object is not guaranteed to be in any particular order.
Use an array and for (var i=0; i

Categories

Resources