Copying between two canvas causes blur - javascript

I have two canvases. When I use drawImage() to copy from one canvas to the other, it is blurring the image slightly.
Why is this happening?
This seems like the kind of thing that occurs when theres some sub-pixel rounding. Maybe this is caused by the 45 degree 'rotation'?
Here is an example showing it occur:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var tempCanvas = document.getElementById("tmpCanvas");
var tempCtx = tempCanvas.getContext("2d");
canvas.width = canvas.height = 200;
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
// translate origins
ctx.translate(canvas.width / 2, canvas.height / 2);
tempCtx.translate(canvas.width / 2, canvas.height / 2);
// Create a red square
ctx.fillStyle = "rgba(255,0,0, 0.1)";
ctx.fillRect(-50, -50, 100, 100);
var angle = 0;
// Each draw we copy the current canvas to the tmpCanvas. Then copy it back to the original canvas.
function draw() {
angle += 45;
tempCtx.save();
tempCtx.rotate(angle * Math.PI / 180);
tempCtx.drawImage(
canvas,
0, // sourceX
0, // sourceY - note that source ignores translation. It's not a canvas context, so we choose top left corner of the canvas to start copying pixels.
canvas.width, // sourceWidth
canvas.height, // sourceHeight
-0.5 * canvas.width, // destinationX
-0.5 * canvas.height, // destinationY
canvas.width, // destinationWidth
canvas.height // destinationHeight
);
tempCtx.restore();
ctx.drawImage(
tempCanvas,
0,
0,
canvas.width,
canvas.height,
-0.5 * canvas.width,
-0.5 * canvas.height,
canvas.width,
canvas.height
);
// requestAnimationFrame(draw);
}
document.addEventListener("click", draw);
canvas {
border: 1px solid blue;
}
<p>
Click to trigger a "draw".
<br/>
A draw will do this:<br/>
1. rotate the bottom canvas by 45 degrees.<br/>
2. copy the top canvas to the bottom canvas<br/>
3. copy the bottom canvas to the top canvas<br/>
</p>
<br/>
<p>
Clicking repeatedly will "blur" the squares. Why?
</p>
<br/>
</p>
<canvas id="canvas"></canvas>
<canvas id="tmpCanvas"></canvas>

This is just antialiasing in action. When rotating twice by 45 degrees, the antialised edges fall slightly outside the original square, and these add up over time.
My goal is to make a canvas where you can draw on it, as the existing contents rotate about the origin.
You can make the drawing actions happen on the original canvas (apply the inverse rotation to the position of the mouse), and then repeatedly draw the original canvas rotated to the output canvas. Data flows in just one direction, from the original to the output, so there's no degradation.

Related

Clearing canvas "layers" separately?

I've been battling with <canvas> for a while. I want to create an animation/game with lots of different units on different layers.
Due to <canvas> limitation to just one context my approach is as follows:
have one <canvas> on the page,
create multiple "layers" using document.createElement("canvas"),
animate/rerender "layers" separately.
But this approach does not seem to work properly due to one quirk - in order to stack "layers" on my main canvas I'm doing realCanvas.drawImage(layerCanvas, 0, 0);. Otherwise the layers are not being rendered.
The issue here is ultimately it does not change a thing as everything is in being drawn on my main <canvas> and when I do clearRect on one of my layers it does nothing as the pixels are also drawn on the main canvas in addition to given layer. If I run clearRect on main canvas then the layers are useless as every layer is on main canvas so I'm back to starting point as I'm clearing the whole canvas and layers are not separated at all.
Is there a way to fix it easily? I feel like I'm missing something obvious.
Here's an example, how do I clear blue ball trail without touching background rectangles here? There should be only one blue ball under your cursor. Note it's a very simplified example, I'll have multiple blue balls and multiple other layers. I just want to know how the heck do I clear only one layer in canvas. Note I don't want to use multiple <canvas> elements and don't want to use any libs/engines as I'm trying to learn canvas by this. I know many apps use just one canvas html element, many layers and somehow animate them separately.
Source: https://jsfiddle.net/rpmf4tsb/
Try adding canvas2ctx.clearRect(0,0, canvas.width, canvas.height); under ctx.clearRect(0,0, canvas.width, canvas.height); and it works as supposed but all the layers are being cleared, not only the one with the ball...
If you look at things from a performance point-of-view, things are better if you use a single visible <canvas> element for your visual output.
Nothing is stopping you from doing things on seperate canvases you stack on top of each other though. Maybe there's just a basic misunderstanding here.
You say:
and when I do clearRect on one of my layers it does nothing as the
pixels are also drawn on the main canvas in addition to given layer
Well that's not true. If you draw the contents of a freshly cleared canvas onto another canvas it won't overwrite the target canvas with 'nothing'.
Take a look at this example:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
Our main canvas contains a green background with a black circle and we're utilizing the drawImage() method to draw a dynamically created, freshly cleared canvas onto, which results in a green background with a black circle as the new canvas element did not contain any data to draw. It did not erase the main canvas.
If we change the example a bit, so the second canvas contains a rectangle things will work as expected:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempContext.strokeRect(tempCanvas.width / 2 - 60, tempCanvas.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
Now if we assume the green background with the circle (tempCanvasA) and the rectangle (tempCanvasB) are two separate canvases we ultimately want to draw to a main canvas it will bring up an important point: the order of drawing.
So this will work:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();
let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
while this fails:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();
let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
The rectangle is missing! Why does it fail? Because we changed the order we draw the canvases onto the main canvas. In the latter example:
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
We first draw tempCanvasB which contains a transparent background & the rectangle and afterwards tempCanvasA with the solid green background - which covers the entire canvas - and the circle. As there are no transparent pixels it will overwrite the rectangle which we've drawn first.
To get to your example with the ball. The problem is that you're drawing the ball to the wrong canvas. Inside your draw function you're doing this:
ctx.clearRect(0, 0, canvas.width, canvas.height);
ball.draw();
ball.x = e.clientX;
ball.y = e.clientY;
ctx.drawImage(canvas2, 0, 0);
So first you clear ctx, afterwards call ball's draw method which draws onto canvas2ctx and finally drawImage onto ctx with the contents of canvas2ctx.
Instead draw the ball onto the main ctx after using drawImage()
e.g.
// helper functions
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
// canvas
let firstRender = true;
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth - 50;
canvas.height = window.innerHeight - 50;
let ctx = canvas.getContext('2d');
// virtual canvas for rectangles layer
let canvas2 = document.createElement("canvas");
canvas2.width = window.innerWidth - 50;
canvas2.height = window.innerHeight - 5;
let canvas2ctx = canvas2.getContext("2d");
let ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function draw(e) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvas2, 0, 0);
ball.draw();
ball.x = e.clientX;
ball.y = e.clientY;
if (firstRender) {
drawRandomRectangles()
firstRender = false;
}
}
function drawRandomRectangles() {
for (i = 0; i < 50; i++) {
canvas2ctx.beginPath();
canvas2ctx.rect(randomInt(0, window.innerWidth - 50), randomInt(0, window.innerWidth - 50), randomInt(5, 20), randomInt(5, 20));
canvas2ctx.stroke();
}
}
canvas.addEventListener('mousemove', function(e) {
draw(e);
});
ball.draw();
<canvas id="canvas"></canvas>
Thinking about your approach of multiple canvas stacking above each other sounds like an interesting approach to get things done. I would not recommend doing this in that way and therefore handle multiple layers through JavaScript and then still render every time everything new. Especially if you will use animations, then I believe that multiple not synchronized canvases will give you another sort of headache.
Then you would do the following:
Clear your canvas with clearRect.
Draw in an iteration each layer above each other
I hope this theoretical explanation helps.
Now to your code: At the end of the day your ctx and canvas2ctx are in the very same context, because they are from the same canvas. That makes anyway not much sense.

HTML5 canvas fill image with a circle

I was reading this post HTML5 canvas fill circle with image and was curious about doing something a little different.
Step by step I'd like to:
1. Take this image I have of a shield.
2. Trace the outline of the shield to get the shape.
3. Use a second, larger, image to fill the outline of the shield. I don't want it to fit. I just want it to observe the boundaries set by the outline of the shield and not draw outside them.
Here's what I have so far: JSFiddle - outline shape.
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = function () {
// draw the image
// (this time to grab the image's pixel data
ctx.drawImage(img, canvas.width / 2 - img.width / 2, canvas.height / 2 - img.height / 2);
// grab the image's pixel data
imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
data = imgData.data;
// call the marching ants algorithm
// to get the outline path of the image
// (outline=outside path of transparent pixels
points = geom.contour(defineNonTransparent);
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
redraw();
}
img.src = "https://cdn1.iconfinder.com/data/icons/shield-4/744/1-512.png";
// redraw the canvas
// user determines if original-image or outline path or both are visible
function redraw() {
// clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw the image
ctx.drawImage(img, canvas.width / 2 - img.width / 2, canvas.height / 2 - img.height / 2);
// draw the path (consisting of connected points)
// draw outline path
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
var point = points[i];
ctx.lineTo(point[0], point[1]);
}
ctx.closePath();
ctx.stroke();
ctx.fillStyle="yellow"; // Just filling with yellow temporarily but would like to use an image
ctx.fill();
}
I'm not sure how to draw inside the outline without overwriting it.
I figured it out using context save, clip, and restore. new jsfiddle
ctx.save();
ctx.clip();
loadnewimage();
ctx.restore();

How to Style Images in a canvas?

I'm using node-canvas and I was wonder how style an imported image in canvas similar to how you would an image in CSS.
For example, how would I crop a square image in canvas to a circle. In CSS, all you need to do is set border radius to 50%.
Well obviously you cannot use CSS in this case since CSS is applied to the DOM and not the the pixel based content of a Canvas element.
However the Canvas element has its own set of draw functions which allow to you replicate or at least approximate CSS rules.
Since you mentioned cropping an image to a circle I'll focus on this example. To achieve this effect you want to specify a clipping region before drawing the image. Every pixel outside of the clipped region will not be drawn. Effectively this will crop the image to the clipped region.
In code:
// Retrieve canvas and get context
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
// Save the context so we can undo the clipping region at a later time
context.save();
// Define the clipping region as an 360 degrees arc at point x and y
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
// Clip!
context.clip();
// Draw the image at imageX, imageY.
context.drawImage(image, imageX, imageY);
// Restore context to undo the clipping
context.restore();
I'd advice taking a look at this page to give you an idea of what you can do with the Canvas element and the 2D rendering context.
I don't know if this would work in node, However you can do this with canvas;
The simplest way of doing it is using, as you intended, border-radius:
canvas{border-radius:50%;}
An other way of doing it is by using the ctx.clip() method.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(125,120,100,0,2*Math.PI);
// you clip the context
ctx.clip();
let img = document.querySelector("#testImg");
ctx.drawImage(img, 0, 20);
<canvas width="250" height="240" >
<img id="testImg" src="theImage.jpg">
</canvas>
Yet an other way of doing it is by using ctx.globalCompositeOperation = "destination-atop"in this way:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 400,
cx = cw / 2;
let ch = canvas.height = 400,
cy = ch / 2;
ctx.globalCompositeOperation = "destination-atop";
let img = document.querySelector("#testImg");
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.fillStyle = "#f00";
ctx.arc(cx, cx, 100, 0, 2 * Math.PI);
ctx.fill();

Crop image like pie chart

I want to crop image over another image like a pie-chart to create a loading animation. I was thinking of using raphaeljs, but couldn't find any information about image cropping in pie-chart style.
Here are the sample images:
Start state:
End state:
What it should look like:
Just draw a semi-transparent filled arc on top of the image (adjust alpha value to your pleasing):
var ctx = document.querySelector("canvas").getContext("2d"),
img = new Image;
img.onload = draw;
img.src = "http://i.imgur.com/hQ5Pljv.png";
function draw(){
var cx = 157, cy = 159, r = 150,
pst = 0,
ang = Math.PI * 2 * (pst/100),
dlt = 2;
// animate the following part
(function loop() {
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, 0, ang);
ctx.fillStyle = "rgba(0,0,0,0.33)"; // adjust alpha here
ctx.fill();
pst += dlt;
if (pst <= 0 || pst >= 100) dlt = -dlt;
ang = Math.PI * 2 * (pst/100);
requestAnimationFrame(loop)
})()
}
<canvas width=320 height=320></canvas>
Method two - compositing
Use two steps to clip the same arc above to use images instead:
Draw arc, this will be the composite data
Change comp. mode to source-atop - next drawing replaces the drawn arc
Draw secondary image in
Change comp. mode to destination-atop - next drawing will fill all non-pixels
Draw main image in
Demo:
var ctx = document.querySelector("canvas").getContext("2d"),
img1 = new Image, img2 = new Image, cnt=2;
img1.onload = img2.onload = loader;
img1.src = "http://i.imgur.com/hQ5Pljv.png";
img2.src = "http://i.imgur.com/k70j3qp.jpg";
function loader(){if (!--cnt) draw()};
function draw(){
var cx = 157, cy = 159, r = 150,
pst = 0, ang = Math.PI * 2 * (pst/100), dlt = 2;
// animate the following part
(function loop() {
ctx.clearRect(0, 0, 320, 320); // clear canvas, or set last comp mode to "copy"
// first arc
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, 0, ang);
ctx.fill(); // this will be comp. basis for the next steps
// comp mode secondary image
ctx.globalCompositeOperation = "source-atop"; // replaces filled arc
ctx.drawImage(img2, 0, 0);
// comp mode main image
ctx.globalCompositeOperation = "destination-atop"; // fills all non-pixels
ctx.drawImage(img1, 0, 0);
pst += dlt; if (pst <= 0 || pst >= 100) dlt = -dlt; ang = Math.PI * 2 * (pst/100);
ctx.globalCompositeOperation = "source-over"; // reset comp. mode
requestAnimationFrame(loop)
})()
}
<canvas width=320 height=320></canvas>
You'll want an algorithm along the lines of:
Draw image A onto canvas 1
Clear canvas 2
Draw a partial circle on canvas 2, for the current state of the spinner, filled with white
Blit image B onto canvas 2, using the multiplicative blending mode
Blit canvas 2 onto canvas 1, using standard (replace) blending
Canvas 2 should contain the second image, masked by the section you want to use. Overlaying that onto canvas 1, provided you handle transparency properly, should give the effect you want.
You can also use two SVG circles with image backgrounds and do this trivially, assuming your target browsers support SVG.

HTML Canvas: How to draw a flipped/mirrored image?

I'm trying to flip/mirror an image as I paint it on an HTML canvas; I found a game tutorial showing a sprite sheet per direction a character has to face, but this doesn't seem quite right to me. Especially since each frame has a different size.
What would be the best technique to reach this goal?
I tried to call the setScale(-1, 1); on my canvas with no success. Maybe that isn't meant for this.
Thanks
You can do this by transforming the context with myContext.scale(-1,1) before drawing your image, however
This is going to slow down your game. It's a better idea to have a separate, reversed sprite.
You need to set the scale of the canvas as well as inverting the width.
drawToCanvas : function(v, context, width, height){
context.save();
context.scale(-1, 1);
context.drawImage(v, 0, 0, width*-1, height);
context.restore();
}
There are probably some performance issues with this but for me that was not an issue.
If you just flip it horizontally it will get off of bounds... so use translate to adjust its position:
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, 0, 0);
For a shorter code you can remove the translate and use the image size as negative offset in the second parameter of the drawImage (x coordinate) instead:
ctx.scale(-1, 1);
ctx.drawImage(img, canvas.width * -1, 0);
If you want to restore the context later, add save/restore before and after it all:
ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(img, canvas.width * -1, 0);
ctx.restore();
You don't need to redraw the entire image when creating a reflection. An original reflection simply shows the bottom part of the image. This way you are redrawing a smaller part of the image which provides better performance and also you don't need to create linear gradient to hide the lower part of the image (since you never draw it).
var img = new Image();
img.src = "//vignette2.wikia.nocookie.net/tomandjerryfan/images/9/99/Jerry_Mouse.jpg/revision/latest?cb=20110522075610";
img.onload = function() {
var thumbWidth = 250;
var REFLECTION_HEIGHT = 50;
var c = document.getElementById("output");
var ctx = c.getContext("2d");
var x = 1;
var y = 1;
//draw the original image
ctx.drawImage(img, x, y, thumbWidth, thumbWidth);
ctx.save();
//translate to a point from where we want to redraw the new image
ctx.translate(0, y + thumbWidth + REFLECTION_HEIGHT + 10);
ctx.scale(1, -1);
ctx.globalAlpha = 0.25;
//redraw only bottom part of the image
//g.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
ctx.drawImage(img, 0, img.height - REFLECTION_HEIGHT, img.width, REFLECTION_HEIGHT, x, y, thumbWidth, REFLECTION_HEIGHT);
// Revert transform and scale
ctx.restore();
};
body {
background-color: #FFF;
text-align: center;
padding-top: 10px;
}
<canvas id="output" width="500" height="500"></canvas>

Categories