I have two canvases that are different sizes. My goal is copy the user's drawing from the main canvas to a second canvas as a scaled down version. So far the drawImage() and scale appear to be working, but the second canvas keeps the old version of the main drawing along with the new copy. I tried clearing it each time before calling drawImage(), but that doesn't appear to do anything. How can I copy just the current image to my secondary canvas each time the function runs?
$('#hand').dblclick(function(){
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
//var imageData = context.getImageData(0, 0, 100, 100);
var newCanvas = document.getElementById('scaledCanvas');
var destCtx = newCanvas.getContext('2d');
destCtx.clearRect(0, 0, newCanvas.width, newCanvas.height);
destCtx.scale(.5,.5);
destCtx.drawImage(canvas, 0, 0);
});
I can include more code if necessary. I also just realized that scale keeps getting called; this explains why the new copied image would get smaller each time as well, so that might be another problem.
It's quite simple actually, you're using what's called a transform (translate, rotate, or scale).
In order to use them "freshly" each time you must save and restore the canvas state each time.
$('#hand').dblclick(function(){
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
//var imageData = context.getImageData(0, 0, 100, 100);
var newCanvas = document.getElementById('scaledCanvas');
var destCtx = newCanvas.getContext('2d');
destCtx.clearRect(0, 0, newCanvas.width, newCanvas.height);
//save the current state of this canvas' drawing mode
destCtx.save();
destCtx.scale(.5,.5);
destCtx.drawImage(canvas, 0, 0);
//restore destCtx to a 1,1 scale (and also 0,0 origin and 0 rotation)
destCtx.restore();
});
It's also important to note you can push several times before calling restore, in order to perform many cool geometric tricks using recursive functions etc...
Take a look at this explanation of states and transformations:
https://developer.mozilla.org/en-US/docs/HTML/Canvas/Tutorial/Transformations
Hope this helps you understand canvas transforms a bit better.
Related
I've noticed if I have a large number of canvases in memory, modifying each canvas before drawing them to the screen drastically reduces performance on my machine. This occurs even when the canvases are small and the modifications are minor.
Here is the most contrived example I could come up with:
var { canvas, ctx } = generateCanvas();
ctx.strokeStyle = "#000";
var images = [];
for (var i = 0; i < 500; i++) {
images.push(generateCanvas(50, "red"));
}
var fps = 0,
lastFps = new Date().getTime();
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
var modRects = document.getElementById("mod-rects").checked;
var drawRects = document.getElementById("draw-rects").checked;
ctx.clearRect(0, 0, 500, 500);
ctx.strokeRect(0, 0, 500, 500);
fps++;
if (new Date().getTime() - lastFps > 1000) {
console.clear();
console.log(fps);
fps = 0;
lastFps = new Date().getTime();
}
images.forEach(img => {
img.ctx.fillStyle = "yellow";
if (modRects) img.ctx.fillRect(20, 20, 10, 10);
if (drawRects) ctx.drawImage(img.canvas, 225, 225);
});
}
function generateCanvas(size = 500, color = "black") {
var canvas = document.createElement("canvas");
canvas.width = canvas.height = size;
var ctx = canvas.getContext("2d");
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
return {
canvas,
ctx
};
}
function generateCheckbox(name) {
var div = document.createElement("div");
var check = document.createElement("input");
check.type = "checkbox";
check.id = name;
var label = document.createElement("label");
label.for = name;
label.innerHTML = name;
div.appendChild(check);
div.appendChild(label);
return div;
}
document.body.appendChild(canvas);
document.body.appendChild(generateCheckbox("mod-rects"));
document.body.appendChild(generateCheckbox("draw-rects"));
canvas+div+div { margin-bottom: 20px; }
In this example we create 500 canvases of size 50x50. There are two checkboxes underneath the larger onscreen canvas. The first causes a small yellow square to be drawn on each of those 500 canvases. The 2nd causes the canvases to be drawn to the larger canvas. FPS is posted to the console once per second. I see no performance issues when one or the other checkbox is checked, but when both are checked, performance drops drastically.
My first thought is that it has something to do with sending in-memory canvas to the gfx card every frame when they are modified.
Here's the actual effect I'm trying to create.
Video: https://youtu.be/Vr6v2oF3G-8
Code: https://github.com/awhipple/base-command-dev/blob/e2c38946cdaf573abff5ded5399c90687ffa76a5/engine/gfx/shapes/Particle.js
My ultimate goal is to be able to smoothly transition the colors of the canvas. I'm using globalCompositeOperation = "source-in" and fillRect() to do this in the code link above.
As has been stated before, this is an issue with the overhead of sending hundreds of canvases to the GPU every single frame. When a canvas is modified in CPU it gets marked as "dirty" and is re sent to the GPU next time it's used.
The workaround I found was to create a large canvas containing a grid of my particle images. Every particle object makes its modification to its assigned section of the grid. Then once all modifications are made, we begin making draw image calls, cutting up the larger canvas as needed
I also needed to switch to globalCompositeOperation = "source-atop" to prevent all other particles from getting trashed each time I tried to change one.
Code: https://github.com/awhipple/base-command-dev/blob/2514327c6c30cb9914962d2c8d604f04bfbdbed5/engine/gfx/shapes/Particle.js
Examples: http://avocado.whipple.life/
You can see here, when this.newRender === true in draw, it queues up to be drawn later.
Then static drawQueuedParticles is called once every particle has had a chance to queue itself up.
The end result is that this larger canvas is only sent to the GPU once per frame. I saw a performance increase from 15 FPS to 60 FPS on my Razorblade Pro running a 2700 RTX GPU with 1500 on screen particles.
I expect browsers are optimized to display 1, or at most a few canvases at a time. I'm betting each canvas is uploaded to the GPU individually, which would have way more overhead than a single canvas. The GPU has a limited number of resources, and using a lot of canvases could cause a lot of churn if textures and buffers are repeatedly cleared for each canvas. This answer WebGL VS Canvas 2D hardware acceleration also claims that Chrome didn't hardware accelerate canvases under 256px.
Since you're trying to do a particle effect with sprites, you'd be better off using a webgl library that's built for this kind of thing. I've had a good experience with https://www.pixijs.com/. If you're doing 3d, https://threejs.org/ is also popular. It is possible to build your own webgl engine, but it's very complicated and a lot of work. You have to worry about things like vector math, vertex buffers, supporting mobile GPU's, batching draw calls, etc. You'd be better off using an existing library unless you really have a strong need for something unique.
I am wondering how I could alter my Javascript to only clear the falling sprites, and not the entire canvas (as it does currently).
I hope to place multiple other (animated) sprites on the canvas, which do not appear with the way my function animate is structured.
Is there a way so that if there was another image/sprite was on the canvas, it would not be affected by the function animate.
I'm thinking that this line needs to change:
ctx.clearRect(0, 0, canvas.width, canvas.height);
Though I have no idea what parameters I would need to place inside.
The falling sprites draw at a size of 60x60, but as they fall downwards this is where I am a bit stuck with clearing the only the sprite path.
Any help would be appreciated :)
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
canvas.width = 1408;
canvas.height = 640;
canvasWidth = canvas.width;
canvasHeight = canvas.height;
var orangeEnemy = new Image();
orangeEnemy.src = "http://www.catholicsun.org/wp-content/uploads/2016/09/cropped-sun-favicon-512x512-270x270.png";
var yellowEnemy = new Image();
yellowEnemy.src = "http://www.clker.com/cliparts/o/S/R/S/h/9/transparent-red-circle-hi.png";
var srcX;
var srcY;
var enemySpeed = 2.75;
var images = [orangeEnemy, yellowEnemy];
var spawnLineY=-50;
var spawnRate=2500;
var spawnRateOfDescent=1.50;
var lastSpawn=-1;
var objects=[];
var startTime=Date.now();
animate();
function spawnRandomObject() {
var object = {
x: Math.random() * (canvas.width - 15),
y: spawnLineY,
image: images[Math.floor(Math.random() * images.length)]
}
objects.push(object);
}
function animate(){
var time=Date.now();
if(time>(lastSpawn+spawnRate)){
lastSpawn=time;
spawnRandomObject();
}
requestAnimationFrame(animate);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// move each object down the canvas
for(var i=0;i<objects.length;i++){
var object=objects[i];
object.y += enemySpeed;
ctx.drawImage(object.image, object.x, object.y, 60, 60);
}
}
<html>
<canvas id="canvas" style="border:3px solid"></canvas>
</html>
The easiest and quickest way would be to overlay another canvas, specifically for your sprites, atop your current canvas (requires a bit of CSS). Put all your sprites in one, everything else in the other. The clearRect() in your animate() function will then only apply to your sprite canvas, and not the other.
Otherwise, you will have to keep track of the positions of the sprites, and clear each programatically with 60x60 rectangles using clearRect(offsetX, offsetY, 60, 60).
P.S. excuse the non-formatted answer... still figuring SO out
Clear once for performance.
You are much better off clearing the whole canvas and redrawing the sprites. Using the previous position, checking for overlap and then clearing each sprite in turn, making sure you don't clear an existing sprite will take many more CPU cycles than clearing the screen once.
The clear screen function is very fast and is done in hardware, the following is the results of a performance test on Firefox (currently the quickest renderer) of clearing 65K pixels using just one call for whole area then 4 calls each a quarter, then 16 calls each clearing a 16th. (µs is 1/1,000,000th second)
Each test clears 256*256 pixels Each sample is 100 tests
'Clear 1/1' Mean time: 213µs ±4µs 1396 samples
'Clear 4/4' Mean time: 1235µs ±14µs 1390 samples
'Clear 16/16' Mean time: 4507µs ±42µs 1405 samples
As you can see clearing 65K pixels is best done in one call with the actual javascript call adding about 100µs to do.
On Firefox the number of pixels to clear does not affect the execution time of the call to clearRect with a call clearRect(0,0,256,256) and clearRect(0,0,16,16) both taking ~2µs
Apart from the speed, trying to clear overlapping animated sprites per sprite becomes extremely complicated and can result in 100s of clear calls with only a dozen sprites. This is due to the overlapping.
In my application I need to get some images, process them, and save for later use. So I'm drawing them to a temporary canvas, then getting with getImageData function. But in output transparency is lost...
Here is my code:
var tempCanvas = document.createElement('canvas');
var tempContext = tempCanvas.getContext('2d');
tempContext.drawImage(image, 0, 0);
var imageData = tempContext.getImageData(0, 0, image.width, image.height);
My image has some transparent pixels but after this there are no transparent pixels in imageData how can I solve this issue?
Is there any way to convert Html Image to ImageData, so I can process it and then draw to canvas?
your imageData should contain alpha channel.
However, putImageData will replace the pixel value in the context.
It won't merge it with the previous value of the pixel in the context, it will replace it. So, what you should see is the pixel behind the canvas (in most of cases, the pixel color of the body tag of your html page).
What you have to do:
use a temporaty canvas to get image data is the good way
modifiy the imageData as you need
don't try to put this imageData back in the conetext with a putImageData, it won't behave as you wish
but create a new Image object and give it the imageData as source (yes, it works :))
use drawImage to draw back the image
example of code:
var tempCanvas = document.createElement('canvas');
var tempContext = tempCanvas.getContext('2d');
tempContext.drawImage(image, 0, 0);
var imageData = tempContext.getImageData(0, 0, image.width, image.height);
//modify here the imageData as you need
var img = new Image();
img.src = imageData;
context.drawImage(img, 0, 0); //the true context of the canvas
It should works.
I'm trying to use JavaScript drawImage to draw from a buffer canvas to another in Firefox; I'm calling the draw multiple times per frame using a fairly large canvas. My memory usage shoots through the roof in Firefox, but barely peaks in Chrome. I'm curious about the reason for this behavior and if there's a workaround to free the memory used (I'm assuming) by drawn images after they're no longer needed.
I need to render using globalCompositeOperation = 'source-in', so that's why I'm using this method.
Here's the basic idea:
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
//set height and width of canvas to browser window
var dummyCanvas = document.createElement('canvas');
var dummyctx = dummyCanvas.getContext('2d');
dummyCanvas.width = canvas.width;
dummyCanvas.height = canvas.height;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
//draw some stuff on normal canvas
dummyCtx.clearRect(0, 0, canvas.width, canvas.height);
//draw a polygon on buffer canvas
dummyctx.globalCompositeOperation = 'source-in';
//draw another polygon on buffer canvas
ctx.drawImage(dummyctx.canvas, 0, 0);
//draw some more stuff on normal canvas
}
Is this memory problem just a bug in Firefox? Am I doing something wrong? Are there any workarounds?
Thank you so much for any help!
I notixed that images are somehow piled on each other in a canvas, when I dra more than one. Maybe it would help clearing te canvas, before draing to it again:
context.clearRect(0, 0, canvas.width, canvas.height);
var w = canvas.width;
canvas.width = 1;
canvas.width = w;
I took this from a image resize that I dod not so long ago: http://boxed.hu/articles/html5-client-side-image-resize/
But, this is only a tip - so let me know how it worked.
I had a memory leak problem with the jquery.rotate plugin increasing the memory use on IE and found that although drawing takes up memory, the problem was when the original image was replaced by the manipulated image. Apparently the images were just piling up in memory. The line was:
p.parentNode.replaceChild(canvas, p);
so I changed to use the jQuery function replaceWith() and the memory stopped stacking up after rotating several images:
$(p).replaceWith(canvas);
Looking at the replaceWith function, it actually removes the object (probably in the end uses removeChild and appendChild) and appends the new one to the DOM. My guess is that there's a difference on how the browsers implement replaceChild().
Is there any way to create a deep copy of a canvas element with all drawn content?
Actually the correct way to copy the canvas data is to pass the old canvas to the new blank canvas. Try this function.
function cloneCanvas(oldCanvas) {
//create a new canvas
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
//set dimensions
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
//apply the old canvas to the new one
context.drawImage(oldCanvas, 0, 0);
//return the new canvas
return newCanvas;
}
Using getImageData is for pixel data access, not for copying canvases. Copying with it is very slow and hard on the browser. It should be avoided.
You can call
context.getImageData(0, 0, context.canvas.width, context.canvas.height);
which will return an ImageData object. This has a property named data of type CanvasPixelArray which contains the rgb and transparency values of all the pixels. These values are not references to the canvas so can be changed without affecting the canvas.
If you also want a copy of the element, you could create a new canvas element and then copy all attributes to the new canvas element. After that you can use the
context.putImageData(imageData, 0, 0);
method to draw the ImageData object onto the new canvas element.
See this answer for more detail getPixel from HTML Canvas? on manipulating the pixels.
You might find this mozilla article useful as well https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Drawing_shapes