Is CanvasRenderingContext2D#drawImage() an async method in Chrome70 - javascript

I want to render a video in a canvas with 25fps or more. So I use CanvasRenderingContext2D#drawImage() to render every frame in the canvas. It works in chrome69 and FireFox. But it does not work in chrome70.
Here is the code fragment:
if (frame >= this.inFrame && frame <= this.outFrame) {
// this.ctx.drawImage(this.video,
// this.sourceRect.x, this.sourceRect.y, this.sourceRect.width, this.sourceRect.height,
// this.rect.x, this.rect.y, this.rect.width, this.rect.height);
this.frame.init(this.canvas); // line 6, breakpoint here.
this.frame.isPlaying = this.isPlaying;
let image = this.frame;
for (let i = 0; i<this.filters.length;i++) {
image = this.filters[i].getImageWithFilter(frame, image);
}
return image;
}
I put a breakpoint at line 6. At this time, The video is loaded.
this.video.readyState=4
And I execute this command in dev tools.
document.getElementById("test_canvas").getContext('2d').drawImage(this.video, 0, 0);
Sometimes the test canvas shows the correct video frame, sometimes not, just nothing to show.
Let's keep the program going on, the canvas will show the correct video frame finally.
So I doubt that the CanvasRenderingContext2D#drawImage() is an async method in Chrome70. But I find nothing in Chrome website.
Could anyone help me with this question or help me render correctly in every frames.

You can check that by imply calling getImageData() or even just fillRect() right after your drawImage() call.
If the returned ImageData is empty, or if there is no rect drawn over your video frame, then, yes, it might be async, which would be a terrible bug that you should report right away.
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var vid = document.createElement('video');
vid.muted = true;
vid.crossOrigin = 'anonymous';
vid.src = 'https://upload.wikimedia.org/wikipedia/commons/transcoded/a/a4/BBH_gravitational_lensing_of_gw150914.webm/BBH_gravitational_lensing_of_gw150914.webm.480p.webm'
vid.play().then(() => {
ctx.drawImage(vid, 0, 0);
console.log(
"imageData empty",
ctx.getImageData(0, 0, canvas.width, canvas.height).data.some(d => !!d) === false
);
ctx.fillStyle = 'green';
ctx.fillRect(0, 0, 50, 50);
});
document.body.append(canvas);
So now, to give a better explanation as to what you saw, the debugger will stop the whole event loop, and Chrome might have decided to not honor the next CSS frame drawing when the debugger has been called. Hence, what is painted on your screen is still the frame from when your script got called (when no image was drawn).
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var vid = document.createElement('video');
vid.muted = true;
vid.crossOrigin = 'anonymous';
vid.src = 'https://upload.wikimedia.org/wikipedia/commons/transcoded/a/a4/BBH_gravitational_lensing_of_gw150914.webm/BBH_gravitational_lensing_of_gw150914.webm.480p.webm'
vid.play().then(() => {
ctx.drawImage(vid, 0, 0);
const hasPixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data.some(d => !!d);
alert('That should also block the Event loop and css painting. But drawImage already happened: ' + hasPixels)
});
document.body.append(canvas);
canvas{
border:1px solid;
}

Related

How to fix the black screen with canvas.toDataURL() on Safari browser?

I have a video player (video.js) in my application, and a function which takes a snapshot of the video.
The function create a canvas which draws the snapshot and convert the canvas into a DataURL.
But this doesn't work in Safari.
I'm getting a black screen on Safari.
the video tag looks like:
<video crossorigin="anonymous" playsinline="playsinline" id="player_2140_html5_api" class="vjs-tech" data-setup="{ "inactivityTimeout": 0 }" tabindex="-1" autoplay="autoplay" loop="loop"></video>
This is the function which creates the snapshot:
var canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
var ctx = canvas.getContext('2d');
var video = document.querySelector('video');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
this.snapshotVideo = canvas.toDataURL('image/jpeg');
The generated dataUrl in Chrome is an image.
The generated dataUrl in Safari browser is a black rectangle.
In Safari the output is black instead of an image of the taken snapshot via the video, how can I solve this?
I resolved this problem on iOS...
I used setTimeout function:
setTimeout(() => {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(() => {
this.snapshotVideo = canvas.toDataURL('image/jpeg');
});
},100);
Same here.
The problem seen first time at Safari 15. Before it works well with your code snipped.
See last comments here:
https://bugs.webkit.org/show_bug.cgi?id=181663
or here:
https://bugs.webkit.org/show_bug.cgi?id=229611
I have the same issue. Calling toDataURL on safari 15 up (mac, iPad or iPhone) seems to occasionally return "" blank string. Previous versions of safari work fine. I've checked that it is not exceeding the browser canvas size limits which would return "data:," but that is not causing the issue and is returning a blank string.
I got the same problem on Safari iOS 15.5 & 15.6 for handling image drawing.
After calling drawImage, the toDataURL() always return blank image.
I changed some code to avoid using canvas in the drawImage and use image object as the first parameter.
var scrollTop = 100;
var oc2 = document.createElement('canvas');
if (oc2.getContext) {
var octx2 = oc2.getContext('2d');
oc2.width = 200;
oc2.height = 300;
var img1 = new Image();
img1.onload = function() {
octx2.drawImage(img1, 0, scrollTop, 200, 300, 0, 0, 200, 300);
var bg_data = oc2.toDataURL('image/png');
if (bg_data) {
drawing_board.setImg(bg_data);
drawing_board.initHistory();
}
};
img1.src = oc.toDataURL('image/png');
}

Canvas flickers when trying to draw image with updated src

get image of canvas whenever there is change in canvas
var canvas = document.getElementById('myCanvas');
socket.emit('updateCanvasImage', canvas.toDataURL());
draw image on new canvas somewhere else
var canvas = document.getElementById('myCanvasImg');
var context = canvas.getContext('2d');
var image = new Image();
image.onload = function() {
context.drawImage(this, 0, 0, canvas.width, canvas.height);
};
socket.on('updateCanvasImage', function (img) {
image.src = img;
});
The canvas flickers when socket changes the image.src
There are lots of questions like these here, but none of the solutions seem to work for me.
How to solve this problem?
Do not use events to render content
Do not use events to redraw the canvas. Image content is presented to the display at a fixed rate, while most events are not synced to the display rate, the mismatch between display rate and event rates can cause flicker.
requestAnimationFrame
When you repeatedly update any visual content, be that the canvas or other DOM content, you should use requestAnimationFrame to call a render function. This function should then render all the content ready for the next display frame.
When the render function returns the changes will be held in a backbuffer until the display hardware is ready to display the next frame.
Removing flicker
Thus to fix your problem create a render function that is tied to the display rate.
var image = new Image();
var update = true; // if true redraw
function renderFunction(){
if(update){ // only raw if needed
update = false;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
}
requestAnimationFrame(renderFunction);
}
requestAnimationFrame(renderFunction);
Then in the events just get the new image state and flag update when ready to draw
image.onload = () => update = true;
socket.on('updateCanvasImage', src => {update = false; image.src = src});
Do the same with the drag events
This will ensure you never have any flicker, and also you can check to see if the image updates are arriving faster than can be delayed and thus throttle back the image update rate.
Double buffering the canvas
There are many times where the canvas content is updated from one or more different sources, from a video, camera, a draw command (from mouse, touch, code), or from a stream of images.
In these cases it is best to use a second canvas that you keep offscreen (in RAM) and use as the source for display. This makes the display canvas just a view, that is independent of the content.
To create a second canvas;
function createCanvas(width, height){
const myOffScreenCanvas = document.createElement("canvas");
myOffScreenCanvas.width = width;
myOffScreenCanvas.height = height;
// attach the context to the canvas for easy access and to reduce complexity.
myOffScreenCanvas.ctx = myOffScreenCanvas.getContext("2d");
return myOffScreenCanvas;
}
Then in the render function you can display it
var background = createCanvas(1024,1024);
var scale = 1; // the current scale
var origin = {x : 0, y : 0}; // the current origin
function renderFunction(){
// set default transform
ctx.setTransform(1,0,0,1,0,0);
// clear
ctx.clearRect(0,0,canvas.width,canvas.height);
// set the current view
ctx.setTransform(scale,0,0,scale,origin.x,origin.y);
// draw the offscreen canvas
ctx.drawImage(background, 0, 0);
requestAnimationFrame(renderFunction);
}
requestAnimationFrame(renderFunction);
Thus your image load draws to the offscreen canvas
image.onload = () => background.ctx.drawImage(0, 0, background.width, background.height);
socket.on('updateCanvasImage', src => image.src = src);
And your mouse drag events need only update the canvas view. The render function will render the next frame using the updated view. You can also add zoom and rotation.
const mouse = {x : 0, y : 0, oldX : 0, oldY : 0, button : false}
function mouseEvents(e){
mouse.oldX = mouse.x;
mouse.oldY = mouse.y;
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if(mouse.button){
origin.x += mouse.x - mouse.oldX;
origin.y += mouse.y - mouse.oldY;
}
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
Whenever you change the src of an HTMLImageElement, its content is cleared, and when the canvas tries to render it, it can't.
Because of this, you will experience frames without any image (flickers), until the newly set media is loaded and parsed (fiddle reproducing the issue).
Without seeing your code it's quite hard to offer you a correct solution, but a simple structure could be:
let current = the currently loaded image, accessible to animation-loop/drag-event.
on( socket.updateCanvasImage, let newImage = a new Image on which you set the new src).
on( newImage.load, current = new Image ).
With this simple structure, you will avoid the flickers.
var ctx = canvas.getContext('2d');
function animLoop(time){ // draws continously
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(current, 0,0, canvas.width, canvas.height);
ctx.fillText(time, 20,20);
requestAnimationFrame(animLoop);
}
var current = new Image();
function loadImage(){
var img = new Image(); // if you really want to optimize your code for memory impact, you could declare it only once out of the function...
img.onload = function(){
current = this; // update the image to be rendered with the new & loaded one
setTimeout(loadImage, 2000); // start loading a new one in 2 sec (will be rendered even later)
}
img.onerror = loadImage;
img.src = 'https://upload.wikimedia.org/wikipedia/commons' + urls[++url_index % urls.length]+'?'+Math.random();
}
var url_index = 0;
var urls = [
//Martin Falbisoner [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons
'/2/2d/Okayama_Castle%2C_November_2016_-02.jpg',
//Diego Delso [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons
'/9/9b/Gran_Mezquita_de_Isfah%C3%A1n%2C_Isfah%C3%A1n%2C_Ir%C3%A1n%2C_2016-09-20%2C_DD_34-36_HDR.jpg',
//Dietmar Rabich / Wikimedia Commons / “Münster, LVM, Skulptur -Körper und Seele- -- 2016 -- 5920-6” / CC BY-SA 4.0, via Wikimedia Commons
'/5/53/M%C3%BCnster%2C_LVM%2C_Skulptur_-K%C3%B6rper_und_Seele-_--_2016_--_5920-6.jpg',
//By Charlesjsharp (Own work, from Sharp Photography, sharpphotography) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons
'/4/4b/Campo_flicker_(Colaptes_campestris)_female.JPG'
];
loadImage();
animLoop();
<canvas id="canvas" width="500" height="500"></canvas>
Edit:
This is only true for chrome, Firefox doesn't behave like that and actually only starts the parsing of the image when we call drawImage. This will hold the canvas' drawing during this time. If this is a problem, you can try to lower this with an ImageBitmap Object, but with the big images I used in demo, this halt is still there...
var ctx = canvas.getContext('2d');
function animLoop(time){ // draws continously
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(current, 0,0, canvas.width, canvas.height);
ctx.fillText(time, 20,20);
requestAnimationFrame(animLoop);
}
var current = new Image();
function loadImage(){
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function(){
createImageBitmap(this, 0,0,this.width, this.height).then(function(bmp){
current = bmp; // update the image to be rendered with an ImageBitmap
}).catch(e=>console.log(e))
setTimeout(loadImage, 2000); // start loading a new one in 2 sec (will be rendered even later)
}
img.onerror = loadImage;
img.src = 'https://upload.wikimedia.org/wikipedia/commons' + urls[++url_index % urls.length]+'?'+Math.random();
}
var url_index = 0;
var urls = [
//Martin Falbisoner [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons
'/2/2d/Okayama_Castle%2C_November_2016_-02.jpg',
//Diego Delso [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons
'/9/9b/Gran_Mezquita_de_Isfah%C3%A1n%2C_Isfah%C3%A1n%2C_Ir%C3%A1n%2C_2016-09-20%2C_DD_34-36_HDR.jpg',
//Dietmar Rabich / Wikimedia Commons / “Münster, LVM, Skulptur -Körper und Seele- -- 2016 -- 5920-6” / CC BY-SA 4.0, via Wikimedia Commons
'/5/53/M%C3%BCnster%2C_LVM%2C_Skulptur_-K%C3%B6rper_und_Seele-_--_2016_--_5920-6.jpg',
//By Charlesjsharp (Own work, from Sharp Photography, sharpphotography) [CC BY-SA 4.0 (http://creativecommons.org/licenses/by-sa/4.0)], via Wikimedia Commons
'/4/4b/Campo_flicker_(Colaptes_campestris)_female.JPG'
];
loadImage();
animLoop();
<canvas id="canvas" width="500" height="500"></canvas>
Re-Edit:
Since what you do is screen sharing you might also want to consider WebRTC along with canvas.captureStream instead of sending still images.

How to create an image generator for combining multiple images?

I am working on an image generator using HTML5 canvas and jQuery/JS. What I want to accomplish is the following.
The user can upload 2 or max 3 images (type should be png or jpg) to the canvas. The generated images should always be 1080x1920. If the hart uploads only 2 images, the images are 1080x960. If 3 images are uploaded, the size of each image should be 1080x640.
After they upload 2 or 3 images, the user can click on the download button to get the merged image, with a format of 1080x1920px.
It should make use of html canvas to get this done.
I came up with this:
HTML:
<canvas id="canvas">
Sorry, canvas not supported
</canvas><!-- /canvas.offers -->
<input id="fileInput" type="file" />
Generate
jQuery:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.height = 400;
canvas.width = 800;
var img1 = loadImage('http://www.shsu.edu/dotAsset/0e829093-971c-4037-9c1b-864a7be1dbe8.png', main);
var img2 = loadImage('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Ikea_logo.svg/266px-Ikea_logo.svg.png', main);
var minImages = 2;
var imagesLoaded = 0;
function main() {
imagesLoaded += 1;
if(imagesLoaded >= minImages) {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.drawImage(img1, 0, 0);
// ctx.translate(canvas.height/2,canvas.width/2); // move to the center of the canvas
// ctx.rotate(270*Math.PI/180); // rotate the canvas to the specified degrees
// ctx.drawImage(img2,0,canvas.height/2);
ctx.translate(-canvas.height/2,canvas.width/2); // move to the center of the canvas
ctx.rotate(90*Math.PI/180); // rotate the canvas to the specified degrees
ctx.drawImage(img2,-img2.width/2,-img2.width/2);
ctx.restore(); // restore the unrotated context
}
}
function loadImage(src, onload) {
var img = new Image();
img.onload = onload;
img.src = src;
console.log(img);
return img;
}
Above code will create the canvas and place both images (that are now hard-coded in JS) to the created canvas. It will rotate 90 degrees, but it will not position to the right corner. Also the second image should be position beside the first one.
How can I do the rotation and positioning of each image side by side?
Working Fiddle: https://jsfiddle.net/8ww1x4eq/2/
Have a look at the updated jsFiddle, is that what you wanted?
Have a look here regarding image rotation
Updated jsFiddle, drawing multiple images.
Notice:
The save script was just a lazy way to make sure I've got the
external scripts loaded before I save the merged_image...
There is no synchornisation in the sample script, notice that addToCanvas
was called on image loaded event, there could be a race condition
here (but I doubt it, since the image is loaded to memory on
client-side)
function addToCanvas(img) {
// resize canvas to fit the image
// height should be the max width of the images added, since we rotate -90 degree
// width is just a sum of all images' height
canvas.height = max(lastHeight, img.width);
canvas.width = lastWidth + img.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (lastImage) {
ctx.drawImage(lastImage, 0, canvas.height - lastImage.height);
}
ctx.rotate(270 * Math.PI / 180); // rotate the canvas to the specified degrees
ctx.drawImage(img, -canvas.height, lastWidth);
lastImage = new Image();
lastImage.src = canvas.toDataURL();
lastWidth += img.height;
lastHeight = canvas.height;
imagesLoaded += 1;
}
PS: I've added some script to download the merged image, but it would fail. The error message was: "Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported."
I've done a quick Google search and it seemed to be related to Cross-origin resources. I assumed that it wouldn't be an issue with FileReader. I haven't had time to test that so please test it (and please let me know :) It works with FileReader!
You can use toDataURL. But in this way user must do something like Save image as...
var img = canvas.toDataURL("image/png");
And then set for example img result src:
$("#result").attr("src",img);
Canvas is already an Image.
The canvas and img are interchangeable so there is no need to add the risky step of canvas.toDataURL which can fail depending on the image source domain. Just treat the canvas as if it were and img and put it in the DOM. Converting to a jpg does not save space (actually a resource hungry operation) as the an img needs to be decoded before it can be displayed.
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.height = 400;
canvas.width = 800;
document.body.appendChild(canvas); // add to the end of the document
// or add it to a containing element
var container = document.getElementById("containerID"); // or use JQuery
if(container !== null){
container.appendChild(canvas);
}

HTML5 Frame by frame animation (multiple images)

I've been losing my mind over this. I spent 3 hours trying different methods and finding a solution online, and I still haven't fixed it.
I have two separate images(not a spritesheet) and they need to be displayed one after the other, as an animation, infinitely. Here's is my latest code:
var canvas, context, imageOne, imageTwo, animation;
function init(){
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
imageOne = new Image();
imageTwo = new Image();
imageOne.src = "catone.png";
imageTwo.src = "cattwo.png";
// Just to make sure both images are loaded
setTimeout(function() { requestAnimationFrame(main);}, 3000);
}
function main(){
animation = {
clearCanvas: function(){
context.clearRect(0, 0, canvas.width, canvas.height);
},
renderImageOne: function(){
context.drawImage(imageOne, 100, 100);
},
renderImageTwo: function(){
context.drawImage(imageTwo, 100, 100);
}
};
animation.renderImageOne();
// I also tried calling animation.clearCanvas();
context.clearRect(0, 0, canvas.width, canvas.height);
animation.renderImageTwo();
// I put this here to confirm that the browser has entered the function, and that it hasn't stopped after animation.renderImageTwo();
console.log("cats");
requestAnimationFrame(main);
}
init();
But the problem is that the only one image is displayed, and it's not moving. I can't see any errors or warnings in the console. I'm also sure HTML and JavaScript are connected properly and the images are in the right path. So in any case, only the image in the first function is displayed. Example: animation.renderImageOne(); displays catone, but if I replace it with animation.renderImageTwo(); it displays cattwo.
The problem is here:
animation.renderImageOne();
// I also tried calling animation.clearCanvas();
context.clearRect(0, 0, canvas.width, canvas.height);
animation.renderImageTwo();
Is it's drawing the first image, clearing the canvas, then drawing the second image, then after all that it draws to the screen. Leaving you with only seeing the second image. You will need a variable that alternates values, and use that to determine which picture you should draw:
var canvas, context, imageOne, imageTwo, animation;
var imageToDraw = "one";
And then:
function main() {
...
if(imageToDraw == "one") {
animation.renderImageOne();
imageToDraw = "two";
}
else if(imageToDraw == "two") {
animation.renderImageTwo();
imageToDraw = "one";
}
...
}
Note: You don't need to define animation inside main(), you can move it into global scope. That way you don't redefine it each time you call main().

context.drawImage behaving weirdly

I have:
<canvas id='canvas' width="300" height="409" style="border:2px solid darkblue" >
</canvas>
And then:
<script type="text/javascript">
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image();
image.src = 'http://4.bp.blogspot.com/...-21+Kingfisher.JPG';
alert(image.src);
context.drawImage(image, 0, 0, 300, 400);
</script>
In IE 10, the image is painted as "to be expected". However, when I remove that alert statement, the picture is not painted!
In Chrome, no image is painted on my local PC, whether with or without the alert statement.
What could be happening? The fiddle is here
That is because loading images is an asynchronous operation. The alert call helps the browser to wait a bit so the image loading can finish. Therefor the image will be available at drawImage that follows.
The correct way to implement this is to use the code this way:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var image = new Image(); //document.createElement('img'); for Chrome due to issue
// add a onload handler that gets called when image is ready
image.onload = function () {
context.drawImage(this, 0, 0, 300, 400);
}
// set source last so onload gets properly initialized
image.src = 'http://4.bp.blogspot.com/...-21+Kingfisher.JPG';
The draw operation inside the callback for onload could just as easily have been a function call:
image.onload = nextStep;
// ...
function nextStep() {
/// draw image and other things...
}

Categories