Canvas drawing functions behaving in asynchronous way - javascript

What I am doing here is that I am covering the entire web page loaded with the layer of a canvas. Now on that canvas user clicks and that click creates a dot. At that point createDot() function gets called.
Inside createDot() the dot is drawn on to the canvas and then the screenshot request is sent to the background script.
Now the problem is that when I click on the canvas and the screenshot is taken, the dot does not appear in the screenshot.
Now what's weird is that when i click a 2nd dot onto the same canvas and when the screenshot comes, it has now the first dot that I clicked but not the 2nd one.
So what is happening is that screenshot is not capturing the current dot but only the previous dots.
I checked that the canvas drawing functions are all blocking so the request cannot be sent to the background script before the drawing is complete. I also confirmed the same by logging to the console.
content script:
function createDot(canvas) {
var context = canvas.getContext("2d");
var canvasOffset = $("#canvas").offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var pointX , pointY;
var point_X,point_Y;
function handleMouseDown(e) {
//coordinates of the click in dom.
pointX = parseInt(e.pageX - offsetX);
pointY = parseInt(e.pageY - offsetY);
//making the dot
var radius = 10;
context.beginPath();
context.arc(pointX, pointY, radius, 0, 2 * Math.PI);
context.fillStyle = 'red';
context.fill();
context.lineWidth = 2;
context.strokeStyle = '#003300';
context.stroke();
console.log("drawn everything");
takeDotScreenshot(); //gets called when the canvas has finished its job of drawing.
}
$("#canvas").mousedown(function (e) {
handleMouseDown(e);
});
}
function takeDotScreenshot() {
console.log("sending request to Background script");
chrome.runtime.sendMessage({"capture":true},function(response) {
var img_src=response.screenshot;
});
//logic for using that screenshot and appending it on to the content page...
}
Background script:
chrome.runtime.onMessage.addListener(function(request,sender,sendResponse) {
if(request.capture) {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.captureVisibleTab(null, {}, function(dataUrl) {
if(dataUrl) {
sendResponse({"screenshot":dataUrl});
}
});
});
}
return true;
});
UPDATE:
If I use setTimeout of 1ms for calling the takeDotScreenshot function, everything works fine. WHY!
setTimeout(takeDotScreenshot, 1);

There is a very detailed, great answer why setTimeout helps here:
https://stackoverflow.com/a/4575011/10574621
The DOM-Update-Event for your canvas (which actually make the dot visible) could be behind your take-screenshot-event, even if the canvas recieved all data needed from your stroke()-event.

Screen shot is a snap shot of what the user can see. When inside a rendering function the canvas is not presented for display until the execution stack is empty (returned all the way out of the functions)
Depending on how you render (using requestAnimationFrame or directly from mouse, keyboard, timer events) the canvas will still take time to be presented to the display.
When you set the timeout to zero you can add a call onto the call stack that runs before the canvas is presented to the display. Any code you run will block the page thus preventing the canvas from being displayed and hence no screen shot.
I would set the timeout from render to capture at at least 17ms (just over 60fps). Nobody can notice that delay but gives plenty of time for the canvas to be presented to the display

Related

How to draw other shapes or lines in `rot-js` canvas?

I'm using rot-js to draw a grid with hexagons and want to add triangles and other shapes to the canvas. I've tried acting on display.getContainer() but that's not working. What needs to be done to get this to work?
Setting ctx.globalCompositeOperation = "xor", I'm able to see the objects being drawn (but the colors are all wrong).
Setting ctx.globalAlpha = .8 also allows everything to be visible to an extent so I'm thinking this has something to do with layers.
If I work directly on an existing canvas element, drawing works fine.
Hard to tell for sure since you didn't provide anything about what you are actually doing, but given the description, I'll guess you are not waiting for the next frame before doing your own drawings over the ones made by the library.
The library stacks all its rendering operations in a requestAnimationFrame callback, so if you do it before, your drawings will be covered by the lib's ones.
To workaround this, simply wrap your own drawing operations in a requestAnimationFrame callback, it will get stacked after the ones of the lib, and will get drawn on top.
const display = new ROT.Display();
const canvas = display.getContainer();
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
// async calls
display.draw(5, 4, "#");
display.draw(15, 4, "%", "#0f0");
display.draw(25, 4, "#", "#f00", "#009");
// end async calls
// this will get covered
ctx.fillStyle = 'red';
ctx.fillRect(20,20,40,40);
// wait next frame
requestAnimationFrame(() => {
ctx.fillStyle = 'green';
ctx.fillRect(120,20,40,40);
})
<script src="https://cdn.jsdelivr.net/npm/rot-js"></script>

canvas toDataURL() returns transparent image

I am attempting to use a chrome extension to take a screenshot of the current page, and then draw some shapes on it. After I have done that to the image, I turn the whole thing into a canvas that way it is all together, like the divs I have drawn on it are now baked into the 'image', and they are one in the same. After doing this, I want to turn the canvas back into a png image I can use to push to a service I have, but when I go to use the canvas.toDataURL() in order to do so, the image source that it creates is completely transparent. If I do it as a jpeg, it is completely black.
I read something about the canvas being 'dirtied' because I have drawn an image to it, and that this won't work in Chrome, but that doesn't make sense to me as I have gotten it to work before, but I am unable to use my previous method. Below is the code snippet that isn't working. I am just making a canvas element, and then I am drawing an image before that.
var passes = rectangles.length;
var run = 0;
var context = hiDefCanvas.getContext('2d');
while (run < passes) {
var rect = rectangles[run];
// Set the stroke and fill color
context.strokeStyle = 'rgba(0,255,130,0.7)';
context.fillStyle = 'rgba(0,0,255,0.1)';
context.rect(rect.left, rect.top, rect.width, rect.height);
context.setLineDash([2,1]);
context.lineWidth = 2;
run++;
} // end of the while loop
screencapImage.className = 'hide';
context.fill();
context.stroke();
console.log(hiDefCanvas.toDataURL());
And the image data that it returns is: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACWAAAAVGCAYAAAAaGIAxAAAgAElEQ…ECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQBVKBUe32pNYAAAAAElFTkSuQmCC which is a blank, transparent image.
Is there something special I need to do with Chrome? Is there something that I am missing? Thanks, I appreciate the time and help.
Had the same problem, and found the solution here:
https://bugzilla.mozilla.org/show_bug.cgi?id=749824
"I can confirm, that it works if you set preserveDrawingBuffer.
var glContextAttributes = { preserveDrawingBuffer: true };
var gl = canvas.getContext("experimental-webgl", glContextAttributes);"
After getting the context with preserveDrawingBuffer, toDataURL just works as expected, no completely transparent or black image.
Having a similar problem I found a solution, thanks to the following post
https://github.com/iddan/react-native-canvas/issues/29.
These methods return a promise, so you would need to wait for it to resolve before the variable can be populated.
My solution was to set asyn function and await for the result:
const canvas = <HTMLCanvasElement>document.getElementById("myCanvas")[0];
let ctx = canvas.getContext('2d');
let img = new Image();
img.onload = async (e) => { ctx.drawImage(img, 0, 0); ctx.font = "165px Arial";ctx.fillStyle = "white"; b64Code = await (<any>canvas).toDataURL(); }

HTML5 Canvas - Draw on Canvas, Save Context and Restore it later

Requirement:
Now: Draw on a Canvas, and hit Save (store Canvas state/drawing offline - but NOT as image).
Later: Open up the Canvas with previously saved drawing showing, and continue to draw again.
For drawing we normally use code as follows:
canvas = document.getElementById('can');
ctx = canvas.getContext("2d");
...
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(currX, currY);
....
In order to restore Canvas state later - exporting to Image does not help.
I want to restore the Canvas to it's original state to continue editing the drawing at a later date.
I guess, the Canvas context has to be exported and stored offline - how?
Your best shot here is to use a Proxy that will both store the draw commands and perform the drawings.
Since the browser support for Proxy is very bad (only FF as of today), you'll have to build the Proxy yourself, either by using nosuchmethod, or by building a new brand new WatchedContext Class out of the Context2D.
I took the last solution (WatchedContext Class) for this short demo :
function WatchedContext(hostedCtx) {
this.commands= [];
Context2dPrototype = CanvasRenderingContext2D.prototype;
for (var p in Context2dPrototype ) {
this[p] = function(methodName) {
return function() {
this.commands.push(methodName, arguments);
return Context2dPrototype[methodName].apply(hostedCtx, arguments);
}
}(p);
}
this.replay=function() {
for (var i=0; i<this.commands.length; i+=2) {
var com = this.commands[i];
var args = this.commands[i+1];
Context2dPrototype[com].apply(hostedCtx, args);
}
}
}
Obviously you might need some other method (start/stop recording, clear, ...)
Just a small example of use :
var cv = document.getElementById('cv');
var ctx=cv.getContext('2d');
var watchedContext=new WatchedContext(ctx);
// do some drawings on the watched context
// --> they are performed also on the real context
watchedContext.beginPath();
watchedContext.moveTo(10, 10);
watchedContext.lineTo(100, 100);
watchedContext.stroke();
// clear context (not using the watched context to avoid recording)
ctx.clearRect(0,0,100,1000);
// replay what was recorded
watchedContext.replay();
You can see here :
http://jsbin.com/gehixavebe/2/edit?js,output
That the replay does work, and the line is re-drawn as a result of replaying the stored commands.
For storing offline you can either store the commands locally using localStorage or store them remotely on a server an use AJAX calls or similar.

Code suddenly works after clicking Run in JSFiddle. What?

I made a jsFiddle which demonstrates pixel manipulation in JavaScript. It works perfectly fine in Chrome. Then I moved to test it on Firefox.
It doesn't work and it threw an error:
IndexSizeError: Index or size is negative or greater than the allowed amount
This confuses me. But wait, there's more.
When I click Run again, the code suddenly works. I don't know what sorcery is this, or it's just some weird Firefox bug.
You can see the problem here: http://jsfiddle.net/DerekL/RdK7H/
When you are in jsFiddle, click on Select File and select a PNG file. You should see the code is not working. Then you click Run. Do the same thing again, and it suddenly works.
There are also some problems in some of the functions in Firefox which also frustrated me, however it is not part of this question.
If you need to know, I'm using Firefox 26.0.
It is because your image hasn't completed loading yet so the default width and height of the image is returned (both 0). As you cannot use 0 for the width and height of getImageData() you get an error.
When I click Run again, the code suddenly works. I don't know what
sorcery is this, or it's just some weird Firefox bug.
It's because the image is now in the cache and the browser happens to be able to provide it before you attempt to read its width and height (no, the bug is in your code :-) ).
Handling image loading with a busy loop and a timeout value is begging to fail.
Make sure you add an onload handler to the image (this may require you to refactor the code a bit to support a callback (or promise) and the return value won't be valid for the same reason as the error):
getRGBArray: function(uri, callback){ /// add parameter for callback here
var image = new Image();
image.onload = imageLoaded; /// add an onload handler here
image.src = uri;
function imageLoaded() {
//var t = Date.now();
//while(Date.now() - t < 3000 && !image.width);
var width = this.width, /// replace image with this to be sure you
height = this.height, /// ..are dealing with the correct image in
canvas = $("<canvas>").attr({ /// ..case you load several ones..
width: width,
height: height
}).appendTo("body"),
ctx = canvas[0].getContext("2d");
ctx.drawImage(this, 0, 0);
var imgData = ctx.getImageData(0, 0, width, height).data;
...
callback(imgData); /// example of callback
}
...
}
Optionally separate the image loading so you can call this function without relying on if the image has loaded or not.
Update
As briefly mentioned you can separate the image loading from the your main code. For example - instead of loading your image in the getRGBArray() function, pre-load it somewhere else in the code and pass the image as an argument instead (callback cannot be avoided but you can keep your original code synchronous after the loading point):
function loadImage(url, callback) {
var image = new Image();
image.onload = function() {
callback(this);
}
image.src = uri;
}
Then call it for example like this:
loadImage(url, readyToGo);
function readyToGo(image) {
var pixels = getRGBArray(image);
...
}
A small modification in the original function to make it use the passed image instead of url:
getRGBArray: function(image){
var width = image.width,
height = image.height,
canvas = $("<canvas>").attr({
width: width,
height: height
}).appendTo("body"),
ctx = canvas[0].getContext("2d");
ctx.drawImage(image, 0, 0);
var imgData = ctx.getImageData(0, 0, width, height).data;
...
return opt;
}
...
}
Hope this helps!

Multitouch Pinch, Pan, Zoom in HTML 5 Canvas

I have most of a module written to handle multitouch pinch, pan and zoom on an HTML 5 canvas element. I will share it below. I've been developing in JavaScript for some time now, and this one continues to boggle me. If anybody has any insights, I will post the final version up on stack for everybody to share once it is confirmed working on my iPad.
Here is what I am doing:
touchmove events trigger the changing of variables. I use these variables to change the way in which my image is painted onto the canvas. I have eight variables, each corresponding to the options that can be put into the drawImage() function. These eight variables get updated through functions that increment/decrement their values and keep them within a certain range. The variables are closure variables, so they are global throughout my module. To prevent over-processing, I make a call to this drawImage() function once every 40ms while the user has their finger pressed to the screen using a setInterval().
Here is the problem:
touchmove events seem to be causing a race condition where my variables get updated by many different instances of that same event. I can somewhat confirm this through my console output, that tracks one variable that is bounded to never reach below 20. When I swipe in one direction quickly, that variable dips down far below 20. Then when I release my finger, swipe slowly, it returns to 20. Another thing that points me in this direction, when I look at these variables while stepping through my program, they differ from what my console.log() pumps out.
Note: The code successfully draws the image the first time, but not anytime thereafter. A basic rendition of my code is below... The full version is on GitHub inside the Scripts folder. It is a Sencha Touch v1.1 app at heart
function PinchPanZoomFile(config)
{
/*
* Closure variable declaration here...
* Canvas Declaration here...
*/
function handleTouchStart(e) {
whatDown.oneDown = (e.originalEvent.targetTouches.length == 1) ? true : false;
whatDown.twoDown = (e.originalEvent.targetTouches.length >= 2) ? true : false;
drawInterval = setInterval(draw, 100);
}
function handleTouchEnd(e) {
whatDown.oneDown = (e.originalEvent.targetTouches.length == 1) ? true : false;
whatDown.twoDown = (e.originalEvent.targetTouches.length >= 2) ? true : false;
clearInterval(drawInterval);
}
function handleTouchMove(e) {
if(whatDown.twoDown) {
/*
* Do Panning & Zooming
*/
changeWindowXBy(deltaDistance); //deltaDistance
changeWindowYBy(deltaDistance); //deltaDistance
changeCanvasXBy(deltaX); //Pan
changeCanvasYBy(deltaY); //Pan
changeWindowDimsBy(deltaDistance*-1,deltaDistance*-1); //(deltaDistance)*-1 -- get smaller when zooming in.
changeCanvasWindowDimsBy(deltaDistance,deltaDistance); //deltaDistance -- get bigger when zooming in
} else if(whatDown.oneDown) {
/*
* Do Panning
*/
changeWindowXBy(0);
changeWindowYBy(0);
changeCanvasXBy(deltaX);
changeCanvasYBy(deltaY);
changeWindowDimsBy(0,0);
changeCanvasWindowDimsBy(0,0);
}
}
function draw() {
//Draw Image Off Screen
var offScreenCtx = offScreenCanvas[0].getContext('2d');
offScreenCtx.save();
offScreenCtx.clearRect(0, 0, canvasWidth, canvasHeight);
offScreenCtx.restore();
offScreenCtx.drawImage(base64Image,
parseInt(windowX),
parseInt(windowY),
parseInt(windowWidth),
parseInt(windowHeight),
parseInt(canvasX),
parseInt(canvasY),
parseInt(canvasWindowWidth),
parseInt(canvasWindowHeight)
);
//Draw Image On Screen
var offScreenImageData = offScreenCtx.getImageData(0, 0, canvasWidth, canvasHeight);
var onScreenCtx = canvas[0].getContext('2d');
onScreenCtx.putImageData(offScreenImageData, 0, 0);
}
}
I strongly recommend using Sencha Touch 2.0.1, as it supports a lot of the touch events you need. See http://dev.sencha.com/deploy/touch/examples/production/kitchensink/#demo/touchevents for examples.

Categories