Can't get a canvas element to rotate properly - javascript

I'm trying to animate a rotating repeat icon on a canvas but it's not spinning around the center of the image. I'm not sure what to do to make it spin properly.
setInterval(draw, 30);
var degrees = 0.0;
function draw() {
var canvas = document.getElementById('tutorial');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 16, 16);
degrees += 0.10;
ctx.save();
ctx.translate(8,8);
ctx.rotate(degrees);
// Draw half open circle
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(8, 8, 6, 0, 1.75 * Math.PI);
ctx.stroke();
// Draw arrowhead
ctx.lineWidth = 2;
ctx.moveTo(13, 1);
ctx.lineTo(9, 5);
ctx.lineTo(13, 5);
ctx.lineTo(13, 1);
ctx.stroke();
ctx.restore();
}
}
Fiddle: http://jsfiddle.net/xTFkU/

You were rotating the canvas and translating, but drawing then performing your drawing functions as if the canvas wasn't translated.
Basically what you need to do is translate the canvas to half of the width and height of the image you're drawing. This will make 0,0 the center of the image. Then you subtract the offset from all of your coordinates.
What I did was add an offset value that is your width and height divided by 2. I then translated the canvas by that offset, and subtracted that offset from all of the coordinates.
Working Demo
(function() {
setInterval(draw, 30);
var degrees = 0.0;
var offset = 8;
function draw() {
var canvas = document.getElementById('tutorial');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 16, 16);
degrees += 0.10;
ctx.save();
ctx.translate(offset, offset);
ctx.rotate(degrees);
// Draw half open circle
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(8-offset , 8-offset, 6, 0, 1.75 * Math.PI);
ctx.stroke();
// Draw arrowhead
ctx.lineWidth = 2;
ctx.moveTo(13-offset , 1-offset);
ctx.lineTo(9-offset , 5-offset);
ctx.lineTo(13-offset , 5-offset);
ctx.lineTo(13-offset , 1-offset);
ctx.stroke();
ctx.restore();
}
}
})();​

With ctx.translate(8,8); you moved the coordinate origin into the center of your canvas, then rotated. You now will need to paint a circle around the center (0/0), not around the point (8/8): http://jsfiddle.net/xTFkU/4/. Alternatively, you can move the rotated coordinate system back by ctx.translate(-8,-8);: http://jsfiddle.net/xTFkU/5/

Related

Rotate canvas image to anticlockwise in the same canvas

Say we have a canvas:
<canvas id="one" width="100" height="200"></canvas>
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
// Sample graphic
context.beginPath();
context.rect(10, 10, 20, 50);
context.fillStyle = 'yellow';
context.fill();
context.lineWidth = 7;
context.strokeStyle = 'black';
context.stroke();
// create button
var button = document.getElementById("rotate");
button.onclick = function () {
// rotate the canvas 90 degrees each time the button is pressed
rotate();
}
var myImageData, rotating = false;
var rotate = function () {
if (!rotating) {
rotating = true;
// store current data to an image
myImageData = new Image();
myImageData.src = canvas.toDataURL();
myImageData.onload = function () {
// reset the canvas with new dimensions
canvas.width = ch;
canvas.height = cw;
cw = canvas.width;
ch = canvas.height;
context.save();
// translate and rotate
context.translate(cw, ch / cw);
context.rotate(Math.PI / 2);
// draw the previows image, now rotated
context.drawImage(myImageData, 0, 0);
context.restore();
// clear the temporary image
myImageData = null;
rotating = false;
}
}
}
And on a button click the canvas gets rotated -90 degrees anticlockwise (around the centre) and the dimensions of the canvas get also updated, so in a sense, it looks like this afterwards:
I want to rotate a canvas element to the anticlockwise rotation. I have used this code but it's not working as I want.
JavaScript has a built-in rotate() function for canvas context:
context.rotate( angle * Math.PI / 180);
The problem is that the rotation will only affect drawings made AFTER the rotation is done, which means you will need to:
Clear the canvas first: context.clearRect(0, 0, canvas.width, canvas.height);
Rotate the context context.rotate( 270 * Math.PI / 180);
Redraw the graphics
Thus, I recommend wrapping the graphics we want to draw in a function to make it easier to call after every rotation:
function drawGraphics() {
context.beginPath();
context.rect(10, 10, 20, 50);
context.fillStyle = 'yellow';
context.fill();
context.lineWidth = 7;
context.strokeStyle = 'black';
context.stroke();
}

Using Clip in canvas causing Pixels

I am trying to using context.clip() to clip a draw arc from other one and fill the clipped result.
But when i clip section & fill it, it gives pixelated fill .
var ctx = document.getElementById("canvas").getContext("2d");
var x = 150 ;
var y = 150 ;
var r = 100 ;
ctx.save() ;
ctx.translate(x,y) ;
ctx.beginPath() ;
ctx.arc(0,0,r,0,2*Math.PI);
ctx.closePath() ;
ctx.fillStyle = "cyan" ;
ctx.fill() ;
ctx.lineWidth = 10;
ctx.stroke();
ctx.restore() ;
ctx.save() ;
ctx.clip() ;
ctx.translate(x,y);
ctx.beginPath();
ctx.moveTo(r,-r-10);
ctx.arc(0,-r-10,r,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle = "#f2f2f2";
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
https://jsfiddle.net/x0d0n40z/1/
An alternative approach which eliminates the need for clip()/save()/restore() is to use a few steps of compositing.
Clipping mask is anti-aliased in some browsers while in other not. To obtain consistency (and in some cases also performance since save-clip-restore are relative expensive operations) using composition is preferred if possible.
In this case:
Fill main arc in target color
Define a clipping arc
Change composite mode to destination-out and fill (will cut main)
Change composite mode to source-atop and stroke (will outline cut)
Change composite mode to source-over and stroke outline of main circle
Example
Update: Simplified steps (with the last step merged into the process, ref. comments). I also chose to demonstrate use of the Path2D since we can reuse the object without interfering with the path on the ordinary context -
var ctx = c.getContext("2d"),
p = new Path2D(), // this will store main shape for reuse
x = 75, y = 75, radius = 70;
// main arc
p.arc(x, y, radius, 0, 6.28); // store to path object
ctx.fillStyle = "cyan";
ctx.fill(p); // fill path object
// clip top arc
ctx.globalCompositeOperation = "source-atop";
ctx.arc(x, y - radius, radius, 0, 6.28);
ctx.fillStyle = "#09f";
ctx.fill();
ctx.lineWidth = 5;
ctx.stroke();
// stroke main arc
ctx.globalCompositeOperation = "source-over";
ctx.stroke(p); // stroke path object
body {background:#e9e9e9}
<canvas id=c></canvas>
Old version:
var ctx = c.getContext("2d"),
x = 75, y = 75, radius = 70;
// main arc
ctx.arc(x, y, radius, 0, 6.28);
ctx.fillStyle = "cyan";
ctx.fill();
// clipping arc
ctx.beginPath();
ctx.arc(x, y - radius, radius, 0, 6.28);
// cut step
ctx.globalCompositeOperation = "destination-out";
ctx.fill();
// stroke gap step
ctx.globalCompositeOperation = "source-atop";
ctx.lineWidth = 10;
ctx.stroke();
// stroke whole outline
ctx.globalCompositeOperation = "source-over";
ctx.beginPath();
ctx.arc(x, y, radius, 0, 6.28);
ctx.lineWidth = 5;
ctx.stroke();
// if you want to color the clip then use this:
ctx.globalCompositeOperation = "destination-atop";
ctx.fillStyle = "#09f";
ctx.fill();
body {background:#e9e9e9}
<canvas id=c></canvas>
The problem is that the clip boundary is not being anti alised.
To solve you can render the shape without using the clip. The ctx.arc method lets you set the start and end angles so you can get the inset by filling two arcs.
You will need to get the angles where the clip circle and the inset circle intercept.
For this case it is very simple. First get the distance between the circles, and the angle from one to the other. This works only for two circles of same radius.
var c = {x:?,y:?}; // circle one location
var c1 = {x:?,y:?}; // circle two location
var radius = ?; // radius of both
var angle = Math.atan2(c1.y - c.y, c1.x - c.x); // get the angle from one to the next
var dist = Math.hypot(c1.x - c.x, c1.y - c.y); // get the distance. NOTE IE does not have hypot so do it the normal way with Math.sqrt....
Now you have the angle and distance the intercepts are a simple relationship between the distance and the radius
var iAngle = Math.acos(dist / 2 / radius); // the angle from the line between the circles
// to the intercepts
Now you have that angle you can draw the two arcs
ctx.beginPath();
ctx.arc(c.x,c.y,radius,angle - iAngle , angle + iAngle); // first arc
ctx.arc(c1.x,c1.y, radius, angle + Math.PI - iAngle, angle + Math.PI + iAngle); // second arc
ctx.fill();
ctx.stroke();
There is not much you can do to prevent the jaggies from effecting the clip area. Another way to achieve clipping is to use ctx.globalCompositeOperation to render a mask. You can mask in and out, and many more options. This will be a better solution when the clipping area becomes more complex.
I finally figured the right way to correct the bug .
Heres the clean result of what i wanted https://jsfiddle.net/x0d0n40z/6/
Code :
var ctx = document.getElementById("canvas").getContext("2d");
var r = 50
x = ctx.canvas.width/2;
y = ctx.canvas.height/2;
var offset = 60;
ctx.save();
ctx.setTransform(1,0,0,1.5,x,y);
ctx.beginPath();
ctx.arc(0,0,r,0,2*Math.PI);
ctx.stroke();
ctx.clip();
ctx.beginPath();
ctx.arc(0,0,r,0,2*Math.PI,false);
ctx.fillStyle = "cyan";
ctx.fill();
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.beginPath();
ctx.arc(0,-offset,r,0,2*Math.PI,false);
ctx.fillStyle = "#f2f2f2";
ctx.fill();
ctx.lineWidth = 1 ;
ctx.stroke();
ctx.setTransform(1,0,0,1.5,x,y);
ctx.beginPath();
ctx.arc(0,0,r,0,2*Math.PI,false);
ctx.lineWidth = 3 ;
ctx.stroke();
ctx.restore();
Source from were i learned to use clip : http://www.html5canvastutorials.com/advanced/html5-canvas-clipping-region-tutorial/

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.

Drag Move an Image inside a Circle (circular movement)

I drawn a circle in canvas and put an image near the border. Now I have absolutely no idea..I want to drag the image around the circle but the top of the arrow image should always be on the border.
For Example: I drag the arrow from the top to the left at nine o'clock. Now the arrow image needs to be rotated 90 degrees.
http://jsfiddle.net/L5twk3ak/1/
canvas = document.getElementById('test');
var context = canvas.getContext("2d");
var points = [];
var radius = 55;
imageBG = new Image();
imageBG.onload = function() {context.drawImage(imageBG, 148, 100, 15, 15);};
imageBG.src = 'https://www.nanamee.com/upload/images/5945/5945_p.jpg';
for(var degree = 0; degree < 360; degree++)
{
var radians = degree * Math.PI / 179;
var x = 150 + radius * Math.cos(radians);
var y = 150 + radius * Math.sin(radians);
points.push({x : x, y : y});
}
context.beginPath();
context.moveTo(points[0].x + 4, points[0].y + 4)
for(var i = 1; i < points.length; i++)
{
var pt = points[i];
context.lineTo(pt.x + 4, pt.y + 4);
}
context.strokeStyle = "black";
context.lineWidth = 1;
context.stroke();
context.closePath();
<canvas id="test" width="400" height="400">Your browser does not support the HTML5 canvas tag.</canvas>
You need to :
Draw your Arc as we're supposed to (unless you have better plans with lineTo() )
calculate the mouse position inside the canvas - on mousemove.
calculate the resultant degree depending on Mouse Position vs. the Arc center.
cache your image for reuse
create draw functions (one for the Arc, the other for drawing the Image after translating the canvas context). That way on (click+drag) mousemove you can simply reuse them to draw your objects into Canvas.
I'll not show you how to implement the click+drag cause it's pretty trivial: you simply need to apply your draw functions if both CLICK+MOUSEMOVE are registered.
Here's the interesting calculations part:
var canvas = document.getElementById('test'); // Store in variable!
var context = canvas.getContext("2d");
var circle = {rad: 55, x:100, y:100}; // Object for ease of use
var img = {src:'//placehold.it/13x13/000', x:0 ,y:0, w:13, h:13};
var arrowImg; // Store for later Image reference
function drawArc(){
context.beginPath();
context.arc(circle.x, circle.y, circle.rad, 0, Math.PI*2, true);
context.strokeStyle = "#000";
context.lineWidth = 1;
context.stroke();
context.closePath();
}
function drawImg( deg ){
context.save(); // save before we mess with ctx translations
context.translate(circle.y, circle.x); // temporarily translate the ctx
// to the Arc center coordinates.
context.rotate(deg*Math.PI/180); // we need Radians so deg*Math.PI/180
context.drawImage(arrowImg, circle.rad-img.w, -img.h/2);
context.restore(); // restore to default
}
function calcDeg(e){ // Retrieve degree from mouse position vs. arc center
var mPos = {
x : e.pageX-canvas.offsetLeft-circle.x,
y : e.pageY-canvas.offsetTop-circle.y
};
var getAtan = Math.atan2(mPos.y, mPos.x);
return getAtan*180/Math.PI;
}
drawArc(); // Draw the ARc
arrowImg = new Image(); // Create Image Obj
arrowImg.onload = function(){ drawImg(-90) }; // onload draw the Image
arrowImg.src = img.src;
canvas.addEventListener("mousemove", function(evt){
canvas.width = canvas.width; // clear the canvas
drawArc(); // Draw Arc
drawImg( calcDeg(evt) ); // Draw Image at the calculated degree
}, false);
canvas{background:#eee;}
<canvas id="test" width="400" height="400">Your browser does not support the HTML5 canvas tag.</canvas>
Not clear? Goog, than ask

Canvas - Rotating Paths

I want to know how to rotate a line in canvas.
Say I have the canvas set-up.
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
ctx.closePath();
How do I rotate this line?
Thanks,
Alex
Here's how to rotate a line segment between p1 & p2 around that segment's midpoint:
The idea is to:
Save the unrotated state of the context using context.save
Set the rotation point at the midpoint of the line using context.translate
Rotate to a specified radian angle using context.rotate
Draw the line. This is the tricky part...Since the canvas is already rotated and since the canvas origin is now the line's midpoint, you must moveTo minus the line's length/2 and lineTo the lines length/2: context.moveTo(-length/2,0); and context.lineTo(length/2,0);
Restore the context to its unrotated state with context.restore
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var p1={x:75,y:75};
var p2={x:150,y:150};
var dx=p2.x-p1.x;
var dy=p2.y-p1.y;
var length=Math.sqrt(dx*dx+dy*dy);
var angle=Math.atan2(dy,dx);
var midX=(p2.x+p1.x)/2;
var midY=(p2.y+p1.y)/2;
console.log(midX,midY);
draw(angle);
requestAnimationFrame(animate);
function animate(time){
requestAnimationFrame(animate);
draw(angle);
angle+=Math.PI/30;
}
function draw(radianAngle){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.translate(midX,midY);
ctx.rotate(radianAngle);
ctx.beginPath();
ctx.strokeStyle='red';
ctx.moveTo(-length/2,0);
ctx.lineTo(length/2,0);
ctx.stroke();
ctx.restore();
}
body{ background-color: ivory; }
canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>
Note: This code shows rotation around your line's midpoint, but you can rotate around any point by using context.translate(anyRotationPointX,anyRotationPointY);
To rotate anything you can use context method - rotate().
We can't rotate line like object (like in SVG), but we can - redraw canvas with new rotated line.
var canvas = document.getElementById("example"),
ctx = example.getContext('2d'),
canvasWidth = canvas.width,
canvasHeight = canvas.height,
p1 = {x:canvasWidth/2+50,y:canvasHeight/2},
p2 = {x:p1.x,y:p1.y+100},
middlePoint = {x:(p1.x+p2.x)/2,y:(p1.y+p2.y)/2};
function rotate(degree,rotatePoint,drFunc) {
rotatePoint = rotatePoint || {x:canvasWidth/2,y:canvasHeight/2};
// Clear the canvas
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Move registration point to the center of the canvas
ctx.translate(rotatePoint.x, rotatePoint.y);
// Rotate 1 degree
ctx.rotate((Math.PI / 180)*degree);
// Move registration point back to the top left corner of canvas
ctx.translate(-rotatePoint.x, -rotatePoint.y);
drFunc();
}
function drFunc(){
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
ctx.closePath();
}
rotate(1,middlePoint,drFunc);
Fiddle

Categories