If you use the rotation plugin in CamanJS there is an issue when you are trying to revert changes. Caman is only implemented in a way that is working good when you crop or resize your image, but not when you rotate it. When you revert and the image is rotated the image reloads distorted, because it doesn't take under consideration that the canvas has rotated and changed size. Also the imageData.data of the canvas are different now. So I think i fixxed it by looking how he implemented the resize. Basicaly what I did (and he does too) is:
Create a canvas in the initial state
Update his pixelData from the initialState
create a new canvas
Rotate him with the initial image
get the ImageData and rerender them
So what I added. I needed to know how many angles was the image rotated so I can get the correct imageData when rotate the new canvas (step 4).
this.angle=0; //added it in the constructor
I also added a new boolean in the constructor to tell me if canvas was rotated
this.rotated = false;
In the rotated plugin:
Caman.Plugin.register("rotate", function(degrees) {
//....
//....
//....
this.angle += degrees;
this.rotated = true;
return this.replaceCanvas(canvas);
}
and on the originalVisiblePixels prototype:
else if (this.rotated){
canvas = document.createElement('canvas');//Canvas for initial state
canvas.width = this.originalWidth; //give it the original width
canvas.height = this.originalHeight; //and original height
ctx = canvas.getContext('2d');
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
pixelData = imageData.data;//get the pixelData (length equal to those of initial canvas
_ref = this.originalPixelData; //use it as a reference array
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
pixel = _ref[i];
pixelData[i] = pixel; //give pixelData the initial pixels
}
ctx.putImageData(imageData, 0, 0); //put it back on our canvas
rotatedCanvas = document.createElement('canvas'); //canvas to rotate from initial
rotatedCtx = rotatedCanvas.getContext('2d');
rotatedCanvas.width = this.canvas.width;//Our canvas was already rotated so it has been replaced. Caman's canvas attribute is allready rotated, So use that width
rotatedCanvas.height = this.canvas.height; //the same
x = rotatedCanvas.width / 2; //for translating
y = rotatedCanvas.width / 2; //same
rotatedCtx.save();
rotatedCtx.translate(x, y);
rotatedCtx.rotate(this.angle * Math.PI / 180); //rotation based on the total angle
rotatedCtx.drawImage(canvas, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height); //put the image back on canvas
rotatedCtx.restore(); //restore it
pixelData = rotatedCtx.getImageData(0, 0, rotatedCanvas.width, rotatedCanvas.height).data; //get the pixelData back
width = rotatedCanvas.width; //used for returning the pixels in revert function
}
You also need to add some resets in the reset prototype function. Basicaly reset angle and rotated
Caman.prototype.reset = function() {
//....
//....
this.angle = 0;
this.rotated = false;
}
and that's it.
I used it and works so far. What do you think?Hope it helps
Thanks for this, it worked after one slight change.
in the else if statement inside the originalVisiblePixels prototype I changed:
x = rotatedCanvas.width / 2; //for translating
y = rotatedCanvas.width / 2; //same
to:
x = rotatedCanvas.width / 2; //for translating
y = rotatedCanvas.height/ 2; //same
before this change my images where being cut.
Related
So I have this piece of code that I use for erasing and restoring parts of an image with a (for example) removed background. Erasing from the main canvas is simple and the user can erase a circular shape with a line between points.
if(removeMode) {
ctxs[index].globalCompositeOperation = 'destination-out';
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, 2 * Math.PI);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
}
The problem is with the restoring. Currently I am able to copy parts of the original image to the main canvas but only in a rectangular shape using the getImageData() and putImageData() functions.
ctxs[index].globalCompositeOperation = 'source-out';
ctxs[0].putImageData(ctxs[1].getImageData(x-radius, y-radius, 2*radius, 2*radius), x-radius, y-radius);
Ideally I would like to clip a part of the original image canvas to the main canvas with a shape similar to the erasing feature. I have tried the clip() function but honestly I am not sure how to go about it. Here is what I initially tried to clip a part of a canvas.
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, Math.PI * 2);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
ctxs[index].clip();
How do I copy a custom shape from a canvas to another canvas?
Thanks in advance,
Edit:
I have also thought of using a mask where I would create the mask as such (example using numpy in python):
Y, X = np.ogrid[:canvas_height, :canvas_width]
# Y, X are matrix values and x, y are coordinates of the cursor within the image
center_dist = np.sqrt((X - x)**2 + (Y-y)**2)
# create mask
mask = center_dist <= radius
# omit everything except circular shape from mask
circular_img = original_img.copy()
circular_img[~mask] = 0
# combine images
new_img = np.maximum(original_img, new_img)
Example of what I have now
Simpler solution
Every shape fits into a rectangle.
Proof Your canvas is a rectangle and already contains the shape.
As a result, you can determine the smallest possible rectangle that contains the full shape and store that. It will necessarily contain your shape. Upon reload you will need to know the shape's boundaries inside the copy though, so that info will also be needed.
Harder, but more precise solution
You can create a structure and store the content, point-by-point (yet, this will be not very performant):
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
let myShape = [];
for (let x = 0; x < canvas.width; x++) {
for (let y = 0; y < canvas.height; y++) {
if (inShape(x, y, canvas)) {
myShape.push({x, y, content: data});
}
}
}
The snippet above assumes that you have properly implemented inShape.
Homogeneous shape
If all the points inside the shape are similar, then you will need to only know where the boundaries of the shape were. If you have a convex polygon, for example, then you will need to know where its center is and what the boundaries are. If you have a filled circle, then you will only need its center and radius. The geometrical data you need largely depend on what shape you have.
Keep using composite operations.
"destination-out" will indeed remove the previous pixels that do overlap with the new ones.
If you use the inverse "destination-in", only the previous pixels that do overlap with the new ones are kept.
So you store your original image intact, and then use one of these modes to render it given the action you want to perform.
Here since it seems we are in a paint-like configuration, I guess it makes more sense to erase the final result and restore the original canvas. For this we need a third canvas, detached where we'll draw the "restoration" part on its own before drawing that back to the visible canvas:
(async () => {
// the main, visible canvas
const canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 250;
const ctx = canvas.getContext("2d");
// a detached canvas context to do the compositing
const detached = canvas.cloneNode().getContext("2d");
// the "source" canvas (here just an ImageBitmap)
const originalCanvas = await loadImage();
ctx.lineWidth = detached.lineWidth = 8;
// we store every drawing in its own Path2D object
const paths = [];
let down = false;
const checkbox = document.querySelector("input");
canvas.onmousedown = (evt) => {
down = true;
const newPath = new Path2D();
newPath.isEraser = !checkbox.checked;
paths.push(newPath);
};
canvas.onmouseup = (evt) => { down = false; };
canvas.onmousemove = (evt) => {
if (!down) { return; }
const {x, y} = parseMouseEvent(evt);
paths[paths.length - 1].lineTo(x, y);
redraw();
};
redraw();
function redraw() {
// clear the visible context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(originalCanvas, 0, 0);
paths.forEach((path) => {
if (path.isEraser) {
// erase the current content
ctx.globalCompositeOperation = "destination-out";
ctx.stroke(path);
}
else {
// to restore
// we do the compositing on the detached canvas
detached.globalCompositeOperation = "source-over";
detached.drawImage(originalCanvas, 0, 0);
detached.globalCompositeOperation = "destination-in";
detached.stroke(path);
// draw the result on the main context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(detached.canvas, 0, 0);
}
});
}
})().catch(console.error);
async function loadImage() {
const url = "https://picsum.photos/500/250";
const req = await fetch(url);
const blob = req.ok && await req.blob();
return createImageBitmap(blob);
}
function parseMouseEvent(evt) {
const rect = evt.target.getBoundingClientRect();
return {x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
canvas { border: 1px solid; vertical-align: top }
<label>erase/restore <input type="checkbox"></label>
<canvas></canvas>
Note that here I do create new paths every time, but you could very well use the same ones for both erasing and restoring (and even any other graphic source).
I am implementing a photo editor, where the image can be zoomed in, rotated and cropped. I am having trouble rotating a rectangular image 90° to the right and to the left. When rotating, the image does not maintain its aspect ratio and does not fill the canvas.
The crop area is square shaped. Upon loading, the image looks like this:
Image initially upright
When I rotate the image 90° to the right, it looks like this:
Image rotated 90°
The image is now out of the cropped area bounds and the aspect ratio is off. I can't figure it out.
This is my code.
function rotate (img) {
var imageCropper = document.querySelector ('.imageCropper');
var cropArea = imageCropper.querySelector ('.cropArea');
var originalCanvas = imageCropper.querySelector ('canvas.original');
var resizedCanvas = imageCropper.querySelector ('canvas.resized');
var cropAreaRect = cropArea.getBoundingClientRect();
var resizedCtx = resizedCanvas.getContext ('2d');
rotationAngle = img.getAttribute ('data-rotation-angle');
if (Math.abs(rotationAngle) == 90 || Math.abs(rotationAngle == 270)) {
resizedCanvas.width = img.height;
resizedCanvas.height = img.width;
} else {
resizedCanvas.width = img.width;
resizedCanvas.height = img.height;
}
if (img.matches('canvas')) { // canvas element as opposed to Image object
// The source image is the resized canvas itself, and drawing it onto itself won't work.
// Set the original canvas as source so that we can draw it on the resized canvas.
img = originalCanvas;
}
//######################################################### --- CALCULATE CENTER POSITION
var centerPosX = Math.abs (resizedCanvas.offsetLeft) + (cropAreaRect.width / 2);
var centerPosY = Math.abs (resizedCanvas.offsetTop) + (cropAreaRect.height / 2);
//######################################################### --- ROTATE
resizedCtx.translate (Math.floor (centerPosX), Math.floor (centerPosY));
resizedCtx.rotate(rotationAngle * Math.PI / 180);
resizedCtx.translate (Math.floor (-centerPosX), Math.floor (-centerPosY));
resizedCtx.drawImage (
img,
0, 0,
img.width, img.height,
0, 0,
resizedCanvas.width, resizedCanvas.height
);
}
if (node.matches ('.imageCropper button.rotateRight')) {
node.addEventListener ('click', function () {
var imageCropper = node.closest ('.imageCropper');
var cropArea = imageCropper.querySelector ('.cropArea');
var resizedCanvas = imageCropper.querySelector ('canvas.resized');
// Get the rotation angle value in the data-rotation-angle attribute
var rotationAngle = parseFloat(resizedCanvas.getAttribute ('data-rotation-angle'));
// If a rotation angle value does not exist, it means the image is in the original position
// and this will be the first rotation, so we set the rotationAngle as 0.
if (! rotationAngle) {
var rotationAngle = 0;
}
// We increment the rotation angle by 90 degrees, which will rotate the image in a clockwise direction
rotationAngle += 90;
// When the rotation angle reaches 270 and the next decrementation would take it to 360,
// that means the image has done a full rotation and is back to its original position,
// so we set it to 0.
// We don't want values bigger than 360.
if (rotationAngle > 270) {
rotationAngle = 0;
}
// We set the data-rotation-angle with the new rotation angle value
resizedCanvas.setAttribute('data-rotation-angle', rotationAngle);
// Call the rotate() function
rotate (resizedCanvas);
}, false);
}
How to draw outer and inner border around any canvas shape?
I'm drawing several stroke-only shapes on an html canvas, and I would like to draw an inner and outer border around them.
draft example:
Is there a generic why to do it for any shape (assuming it's a closed stroke-only shape)?
Two methods
There is no inbuilt way to do this and there are two programmatic ways that I use. The first is complicated and involves expanding and contracting the path then drawing along that path. This works for most situations but will fail in complex situation, and the solution has many variables and options to account for these complications and how to handle them.
The better of the two
The second and easiest way that I present below is by using the ctx.globalCompositeOperation setting to mask out what you want drawn or not. As the stroke is drawn along the center and the fill fills up to the center you can draw the stroke at twice the desired width and then either mask in or mask out the inner or outer part.
This does become problematic when you start to create very complex images as the masking (Global Composite Operation) will interfere with what has already been drawn.
To simplify the process you can create a second canvas the same size as the original as a scratch space. You can then draw the shape on he scratch canvas do the masking and then draw the scratch canvas onto the working one.
Though this method is not as fast as computing the expanded or shrunk path, it does not suffer from the ambiguities faced by moving points in the path. Nor does this method create the lines with the correct line join or mitering for the inside or outside edges, for that you must use a the other method. For most purposes the masking it is a good solution.
Below is a demo of the masking method to draw an inner or outer path. If you modify the mask by including drawing a stroke along with the fill you can also set an offset so that the outline or inline will be offset by a number of pixels. I have left that for you. (hint add stroke and set the line width to twice the offset distance when drawing the mask).
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = ( function () {
canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage = function(w,h){
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d");
return image;
}
/** CreateImage.js end **/
// define a shape for demo
var shape = [0.1,0.1,0.9,0.1,0.5,0.5,0.8,0.9,0.1,0.9];
// draws the shape as a stroke
var strokeShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] *w, shape[1] *h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i] * w, shape[i + 1] * h);
}
ctx.closePath();
ctx.stroke();
}
// draws the shape as filled
var fillShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] * w,shape[1] * h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i]*w,shape[i+1]*h);
}
ctx.closePath();
ctx.fill();
}
var drawInOutStroke = function(width,style,where){
// clear the workspace
workCtx.ctx.globalCompositeOperation ="source-over";
workCtx.ctx.clearRect(0, 0, workCtx.width, workCtx.height);
// set the width to double
workCtx.ctx.lineWidth = width*2;
workCtx.ctx.strokeStyle = style;
// fill colour does not matter here as its not seen
workCtx.ctx.fillStyle = "white";
// can use any join type
workCtx.ctx.lineJoin = "round";
// draw the shape outline at double width
strokeShape(workCtx.ctx);
// set comp to in.
// in means leave only pixel that are both in the source and destination
if (where.toLowerCase() === "in") {
workCtx.ctx.globalCompositeOperation ="destination-in";
} else {
// out means only pixels on the destination that are not part of the source
workCtx.ctx.globalCompositeOperation ="destination-out";
}
fillShape(workCtx.ctx);
ctx.drawImage(workCtx, 0, 0);
}
// clear in case of resize
ctx.globalCompositeOperation ="source-over";
ctx.clearRect(0,0,canvas.width,canvas.height);
// create the workspace canvas
var workCtx = createImage(canvas.width, canvas.height);
// draw the outer stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "black", "out");
// draw the inner stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "red", "in");
// draw the shape outline just to highlight the effect
ctx.strokeStyle = "white";
ctx.lineJoin = "round";
ctx.lineWidth = (canvas.width + canvas.height) / 140;
strokeShape(ctx);
};
// run the demo
demo();
// incase fullscreen redraw it all
window.addEventListener("resize",demo)
I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.
Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?
Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 150),
2, 2);
context.fillStyle = 'rgba(0,0,0,.02)';
context.fillRect(0, 0, 300, 150);
setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);
The problem is two-parted:
There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.
Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.
To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.
Update:
The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.
Online demo here
A simple spec object can look like this:
function Spec(ctx, speed) {
var me = this;
reset(); /// initialize object
this.update = function() {
ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing
this.alpha -= speed; /// update alpha
if (this.alpha <= 0) reset(); /// if black then reset again
/// draw the spec
ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
ctx.fillRect(me.x, me.y, 1, 1);
}
function reset() {
me.x = (ctx.canvas.width * Math.random())|0; /// random x rounded to int
me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int
if (me.alpha) { /// reset alpha
me.alpha = 1.0; /// set to 1 if existed
} else {
me.alpha = Math.random(); /// use random if not
}
}
}
Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.
The next step then is to generate a number of points:
/// create 100 specs with random speed
var i = 100, specs = [];
while(i--) {
specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}
Instead of messing with FPS you simply use the speed which can be set individually per spec.
Now it's simply a matter of updating each object in a loop:
function loop() {
/// iterate each object
var i = specs.length - 1;
while(i--) {
specs[i].update(); /// update each object
}
requestAnimationFrame(loop); /// loop synced to monitor
}
As you can see performance is not an issue and there is no residue left. Hope this helps.
I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:
EDIT
You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";
var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');
canvas2.width=canvas2.height=canvas.width;
window.draw = function(){
var W = canvas2.width;
var H = canvas2.height;
context2.fillStyle='rgba(255,255,255,1)';
context2.fillRect(
Math.floor(Math.random()*W),
Math.floor(Math.random()*H),
2,2);
context2.fillStyle='rgba(0,0,0,.02)';
context2.fillRect(0,0,W,H);
context.fillStyle='rgba(0,0,0,1)';
context.fillRect(0,0,300,150);
context.drawImage(canvas2,0,0,300,150);
setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);
window.thresholdFilter = function () {
var W = canvas2.width;
var H = canvas2.height;
var i, j, threshold = 30, rgb = []
, imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;
for (i = 0; i < Npixels; i += 4) {
rgb[0] = imgData.data[i];
rgb[1] = imgData.data[i+1];
rgb[2] = imgData.data[i+2];
if ( rgb[0] < threshold &&
rgb[1] < threshold &&
rgb[2] < threshold
) {
imgData.data[i] = 0;
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
}
}
context2.putImageData(imgData,0,0);
};
setInterval("thresholdFilter()", 1000);
Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/
To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 300),
2, 2);
setTimeout('draw()', 1000 / 20);
}
window.fadeToBlack = function () {
context.fillStyle = 'rgba(0,0,0,.1)';
context.fillRect(0, 0, 300, 300);
setTimeout('fadeToBlack()', 1000 / 4);
}
draw();
fadeToBlack();
Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/
I'm new into HTML5 programming and I wanted to know how to rotate each image when it is added into canvas. Should each of them be placed into a canvas and then rotated? If so how can i add multiple canvas into a single canvas context.
Fiddle : http://jsfiddle.net/G7ehG/
Code
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for(var src in sources) {
numImages++;
}
for(var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var sources = {
image1: 'http://farm3.static.flickr.com/2666/3686946460_0acfa289fa_m.jpg',
image2: 'http://farm4.static.flickr.com/3611/3686140905_cbf9824a49_m.jpg'
};
loadImages(sources, function(images) {
context.drawImage(images.image1, 100, 30, 200, 137);
context.drawImage(images.image2, 350, 55, 93, 104);
});
In your comment you mentioned that you know about context.rotate, but you don't want the context to stay rotated. That's not a problem at all. First, calling context.rotate only affects things which are drawn afterwards. Anything drawn before will stay were it was. Second, it can be easily reversed after drawing.
use context.save() to create a snapshot of all current context settings, including current rotation.
use context.rotate(angle) and draw your image. The angle is in Radian. That means a full 360° circle is Math.PI * 2. The point the image will be is rotated around is the current origin of the canvas (0:0). When you want to rotate the image around its center, use context.translate(x, y) to set the origin to where you want the center of the image to be, then rotate, and then draw the image at the coordinates -img.width/ 2, -img.height / 2
use context.restore() to return to your snapshot. Rotation and translation will now be like they were before.
Here is an example function which draws an image rotated by 45° at the coordinates 100,100:
function drawRotated(image, context) {
context.save();
context.translate(100, 100);
context.rotate(Math.PI / 4);
context.drawImage(image, -image.width / 2, -image.height / 2);
context.restore();
}