HTML5 canvas rotation not centered at origin [duplicate] - html

Hi I want to rotate this shape around its center when I move my mouse, but currently it's rotating around (0, 0). How to change my code?
Source code (also see jsfiddle):
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
class Circle {
constructor(options) {
this.cx = options.x;
this.cy = options.y;
this.radius = options.radius;
this.color = options.color;
this.angle = 0;
this.toAngle = this.angle;
this.binding();
}
binding() {
const self = this;
window.addEventListener('mousemove', (e) => {
self.update(e.clientX, e.clientY);
});
}
update(nx, ny) {
this.toAngle = Math.atan2(ny - this.cy, nx - this.cx);
}
render() {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.beginPath();
ctx.lineWidth = 1;
if (this.toAngle !== this.angle) {
ctx.rotate(this.toAngle - this.angle);
}
ctx.strokeStyle = this.color;
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.fillRect(this.cx - this.radius / 4, this.cy - this.radius / 4, 20, 20);
ctx.closePath();
ctx.restore();
}
}
var rotatingCircle = new Circle({
x: 150,
y: 100,
radius: 40,
color: 'black'
});
function animate() {
rotatingCircle.render();
requestAnimationFrame(animate);
}
animate();

All good answers, well frustratingly no... they fail to mention that the solution only works if the current transform is at it default. They fail to mention how to get back to the default state and save and restore states.
To get the default transformation state
ctx.setTransform(1,0,0,1,0,0);
To save and restore all states
ctx.save();
ctx.transform(10,0,0,2,200,100); // set some transform state
ctx.globalAlpha = 0.4;
ctx.restore(); // each save must be followed by a restore at some point
and they can be nested
ctx.save(); // save default state
ctx.globalAlpha = 0.4;
ctx.save(); // save state with alpha = 0.4
ctx.transform(10,0,0,2,200,100); // set some transform state
ctx.restore(); // restore to alpha at 0.4
ctx.restore(); // restore to default.
setTransform completely replaces the current transformation. while transform, scale, rotate, translate, multiply the existing transform with the appropriate transform. This is handy if you have an object attached to another, and want the transformation of the first to apply to the second, and additional transforms to the second but not to the first.
ctx.rotate(Math.PI /2); // Rotates everything 90 clockwise
ctx.rotate(Math.PI /2); // Rotates everything another 90 clockwise so that
// everything is 180 from default
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 1,1
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 2,2
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 3,3
ctx.translate(1,1); // move diagonally down by 1. Origin is now at 4,4
ctx.scale(2,2); // scale by 2 everything twice as big
ctx.scale(2,2); // scale by 2 everything four times as big
And an alternative that does not require the default transform state of ctx
// scaleX, scaleY are scales along axis x,y
// posX, posY is position of center point
// rotate is in radians clockwise with 0 representing the x axis across the screen
// image is an image to draw.
ctx.setTransform(scaleX,0,0,scaleY, posX, posY);
ctx.rotate(rotate);
ctx.drawImage(image,-image.width / 2, -image.height / 2);
Or if not a image but a object
ctx.setTransform(scaleX,0,0,scaleY, posX, posY);
ctx.rotate(rotate);
ctx.translate(-object.width / 2, -object.height / 2);

You need to:
first translate to the point of rotation (pivot)
then rotate
then either:
A: draw in at (0,0) using (-width/2, -height/2) as relative coordinate (for centered drawings)
B: translate back and use the object's absolute position and subtract relative coordinates for centered drawing
Modified code:
ctx.beginPath();
ctx.lineWidth = 1;
ctx.translate(this.cx, this.cy); // translate to pivot
if (this.toAngle !== this.angle) {
ctx.rotate(this.toAngle - this.angle);
}
ctx.strokeStyle = this.color;
ctx.arc(0, 0, this.radius, 0, Math.PI * 2); // render at pivot
ctx.closePath(); // must come before stroke() btw.
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.fillRect(-this.radius / 4, -this.radius / 4, 20, 20); // render at pivot
Modified Fiddle
Bonus tip: you're currently using save()/restore() calls to maintain the transformation matrix. Another way could be to set the matrix using absolute values initially replacing the save()/restore() - so instead of the first translate():
ctx.setTranform(1,0,0,1,this.cx, this.cy); // translate to pivot
You can also set things like styles on an individual basis for each. Regardless, it doesn't change the core solution though.

You have to first translate to the circle centre, make the rotation and then translate back
Do this before rendering the circle and the square
ctx.translate(this.cx, this.cy);
ctx.rotate(this.toAngle - this.angle);
ctx.translate(-this.cx, -this.cy);
jsfiddle below:
https://jsfiddle.net/1st8Lbu8/2/

Related

HTML Canvas Rotating a character's gun to face mouse

I'm very new to Javascript and I've started a simple game. I want the character's gun to rotate to follow the mouse. So far, movement and everything else works fine, except that when I added the rotation functionality the character seems to rotate in a huge circle around the screen. Here's the jsfiddle: https://jsfiddle.net/jvwr8bug/#
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
var mouseX = evt.clientX - rect.top;
var mouseY = evt.clientY - rect.left;
return {
x: mouseX,
y: mouseY
};
}
window.addEventListener('load', function() {
canvas.addEventListener('mousemove', function(evt) {
var m = getMousePos(canvas, evt);
mouse.x = m.x;
mouse.y = m.y;
}, false);
}, false);
The error seems to be somewhere there but obviously it could be something else
**Edit: Thanks to Blindman67 for the fix.
You were rotating the current transform by rotation each frame. ctx.rotate(a) rotates the current transform so each time it is called you increase the rotation amount by a. Think of it as a relative rotation rather than setting the absolute rotation.
To fix your code replace the canon rendering with
//cannon
//ctx.rotate(rotation); // << you had
// setTransform overwrites the current transform with a new one
// The arguments represent the vectors for the X and Y axis
// And are simply the direction and length of one pixel for each axis
// And a coordinate for the origin.
// All values are in screen/canvas pixels coordinates
// setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY)
ctx.setTransform(1,0,0,1,x,y); // set center of rotation (origin) to center of gun
ctx.rotate(rotation); // rotate about that point.
ctx.fillStyle = "#989898";
ctx.fillRect(15, - 12.5, 25, 25); // draw relative to origin
ctx.lineWidth = 2;
ctx.strokeStyle = "#4f4f4f";
ctx.strokeRect( 15,- 12.5, 25, 25); // draw relative to origin
//body
ctx.fillStyle = "#5079c4";
ctx.beginPath();
ctx.arc(0, 0, size, 0, Math.PI * 2); // draw relative to origin
ctx.fill();
ctx.stroke();
// can't leave the transformed state as is because that will effect anything else
// that will be rendered. So reset to the default.
ctx.setTransform(1,0,0,1,0,0); // restore the origin to the default
And a few more problems to get it working
Just above rendering the canon get the direction to the mouse
// you had coordinates mixed up
// rotation = Math.atan2(mouse.x - y, mouse.y - x); // you had (similar)
rotation = Math.atan2(mouse.y - y, mouse.x - x);
And your mouse event listener is mixing up coordinates and not running very efficiently
Replace all your mouse code with. You don't need onload as the canvas already exists.
canvas.addEventListener('mousemove', function(evt) {
var rect = this.getBoundingClientRect();
mouse.x = evt.clientX - rect.left; // you had evt.clientX - rect.top
mouse.y = evt.clientY - rect.top; // you had evt.clientY - rect.left
}, false);

How to show part of element from other side of canvas

How to show part of element outside of canvas from opposite side canvas. Illustration:
You need to draw twice when the shape is outside canvas' boundaries. Draw the main part first, then the same part offset by width so it gives the illusion of showing on the other side.
Manually Draw twice
This draws a shape going from right to left, when the shape is outside the left edge it will be redrawn at the right edge representing the part that is non-visible on the left side. For the opposite way (left to right) the principle is just the same, just use x with canvas' width instead of 0.
var ctx = document.querySelector("canvas").getContext("2d"),
x = 100, // start position
w = 200; // shape width
ctx.fillStyle = "#777";
(function loop() {
ctx.clearRect(0, 0, 300, 150); // clear canvas
ctx.fillRect(x, 0, w, 150); // draw main part/image/shape
if (x < 0) { // should rotate? draw secondary
ctx.fillRect(ctx.canvas.width + x, 0, w, 150); // use canvas width + x (x<0)
}
x -= 7; // animate
if (x <= -w) x = ctx.canvas.width + x; // at some point reset x
requestAnimationFrame(loop)
})();
<canvas></canvas>
Translated Pattern
To simplify this a CanvasPattern can be used. The later version of canvas allows local transforms on the pattern itself, but since this is not currently widely spread I'll show an example using normal transforms and compensated x position:
var ctx = document.querySelector("canvas").getContext("2d"),
pattern,
x = 100, // start position
w = 200; // shape width
// create pattern
ctx.fillStyle = "#777";
ctx.fillRect(x, 0, w, 150); // draw main part/image/shape
pattern = ctx.createPattern(ctx.canvas, "repeat"); // use current canvas as pattern
ctx.fillStyle = pattern; // set pattern as fillStyle
(function loop() {
ctx.setTransform(1,0,0,1,0,0); // reset transforms
ctx.clearRect(0, 0, 300, 150); // clear canvas
ctx.setTransform(1,0,0,1,x,0); // translate absolute x
ctx.fillRect(-x, 0, 300, 150); // fill using pattern, compensate transform
x -= 7; // animate
requestAnimationFrame(loop)
})();
<canvas></canvas>

HTM5 draw a shape from multiple objects and then fill it and set opacity

For example lets say I am drawing some circle with arrow, transformatting it and rotating (code and jsfiddle below)... In given code, arrow is red... but when I make it gradient, the gradient is being rotaded during creation of whole object... so I need to apply gradient after everything is drawn... how do I do that?
Second thing... when applying global alpha opacity to context before drawing, arrow on circle is darker, because there are 2 layers in same place... how do I apply opacity to whole shape?
JSFiddle here
var x1 = 10;
var y1 = 10;
var x2 = 50;
var y2 = 50;
var dx = x2 - x1;
var dy = y2 - y1;
var radians = Math.atan2(dy, dx);
var length = Math.sqrt(dx * dx + dy * dy);
ctx.save();
ctx.translate(x1, y1);
ctx.rotate(radians);
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(length, 0);
ctx.lineTo(length - 7, -4);
ctx.lineTo(length - 7, 4);
ctx.lineTo(length, 0);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
ctx.strokeStyle = "red";
ctx.stroke();
ctx.restore();
Gradient rotation
When you createLinearGradient you specify the starting and ending xy's
(basically you specify the line along which the gradient will follow).
Therefore, you can adjust the angle of that line such that the gradient fits your design needs even after it's rotated.
Opacity of the combined circle+arrow
As you've discovered, if you overlap drawings then the resulting opacity will be darker on the overlap.
You have 2 solutions to the darkened opacity:
start drawing the arrow at on the circumference of the circle rather than staring the arrow at the circle's center. This way you have small or no overlap.
combine the arc and arrow into 1 single path.

Animate a Fill Circle using Canvas

Basically I want to be able to Fill a Circle using canvas, but it animate to a certain percentage.
I.e only have the circle fill up 80% of the way.
My canvas knowledge isn't amazing, Here is an image i made in photoshop to display what i want.
I want the circle to start empty and then Fill up to say 70% of the circle.
Is this possible with Canvas, if so? can anyone shed some light on how to do it?
Here is a fiddle of what I've managed
http://jsfiddle.net/6Vm67/
var canvas = document.getElementById('Circle');
var context = canvas.getContext('2d');
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var radius = 80;
context.beginPath();
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
context.fillStyle = '#13a8a4';
context.fill();
context.lineWidth = 10;
context.strokeStyle = '#ffffff';
context.stroke();
Any help would be massively appreciated
Clipping regions make this very easy. All you have to do is make a circular clipping region and then fill a rectangle of some size to get a "partial circle" worth of fill. Here's an example:
var canvas = document.getElementById('Circle');
var context = canvas.getContext('2d');
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var radius = 80;
var full = radius*2;
var amount = 0;
var amountToIncrease = 10;
function draw() {
context.save();
context.beginPath();
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
context.clip(); // Make a clipping region out of this path
// instead of filling the arc, we fill a variable-sized rectangle
// that is clipped to the arc
context.fillStyle = '#13a8a4';
// We want the rectangle to get progressively taller starting from the bottom
// There are two ways to do this:
// 1. Change the Y value and height every time
// 2. Using a negative height
// I'm lazy, so we're going with 2
context.fillRect(centerX - radius, centerY + radius, radius * 2, -amount);
context.restore(); // reset clipping region
context.beginPath();
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
context.lineWidth = 10;
context.strokeStyle = '#000000';
context.stroke();
// Every time, raise amount by some value:
amount += amountToIncrease;
if (amount > full) amount = 0; // restart
}
draw();
// Every second we'll fill more;
setInterval(draw, 1000);
http://jsfiddle.net/simonsarris/pby9r/
This is a little more dynamic, object-oriented version, so you can configure the options as the circle radius, border width, colors, duration and step of animation, you can also animate the circle to a certain percentage. It was quite fun to write this.
<canvas id="Circle" width="300" height="300"></canvas>
<script>
function Animation( opt ) {
var context = opt.canvas.getContext("2d");
var handle = 0;
var current = 0;
var percent = 0;
this.start = function( percentage ) {
percent = percentage;
// start the interval
handle = setInterval( draw, opt.interval );
}
// fill the background color
context.fillStyle = opt.backcolor;
context.fillRect( 0, 0, opt.width, opt.height );
// draw a circle
context.arc( opt.width / 2, opt.height / 2, opt.radius, 0, 2 * Math.PI, false );
context.lineWidth = opt.linewidth;
context.strokeStyle = opt.circlecolor;
context.stroke();
function draw() {
// make a circular clipping region
context.beginPath();
context.arc( opt.width / 2, opt.height / 2, opt.radius-(opt.linewidth/2), 0, 2 * Math.PI, false );
context.clip();
// draw the current rectangle
var height = ((100-current)*opt.radius*2)/100 + (opt.height-(opt.radius*2))/2;
context.fillStyle = opt.fillcolor;
context.fillRect( 0, height, opt.width, opt.radius*2 );
// clear the interval when the animation is over
if ( current < percent ) current+=opt.step;
else clearInterval(handle);
}
}
// create the new object, add options, and start the animation with desired percentage
var canvas = document.getElementById("Circle");
new Animation({
'canvas': canvas,
'width': canvas.width,
'height': canvas.height,
'radius': 100,
'linewidth': 10,
'interval': 20,
'step': 1,
'backcolor': '#666',
'circlecolor': '#fff',
'fillcolor': '#339999'
}).start( 70 );
</script>

how to display part of the canvas after scaling in html5

How to display part of canvas after scaling in html5
For ex:
var c =document.getElementById("mycanvas");
var canvas = c.getContext("2d");
canvas.scale(4,4);
canvas.drawImage(img,0,0);
canvas.drawImage(img,200,200);
img is some image.
Here i have scaled it some value, now it displays the top-left region of the canvas(with only the top-left image) but what if i want it to display bottom-right region(only the bottom-right image) or according to the coordinates i give to it. How can i do that?
Can someone plz help me on this? I will be very grateful.....
If you are scaling you must remember that the coordinates you use to position will also be scaled up, so if you are scaling by a factor of 4 than your coordinates will be 200 * 4 and not 200. To scale the image alone you can use the call drawImage(img,x,y,width,height) and use...
var c = document.getElementById("mycanvas");
var ctx = c.getContext("2d");
var scale = 4;
var width = img.width * scale;
var height = img.height * scale;
ctx.drawImage(img, 0, 0, width, height);
ctx.drawImage(img, 200, 200, width, height);
Or you will need to divide your coordinates by the scale factor...
var c = document.getElementById("mycanvas");
var ctx = c.getContext("2d");
var scale = 4;
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 200 / scale, 200 / scale);
I've put together a fiddle showing the latter approach using clipping to ensure that the image stays in its quadrant http://jsfiddle.net/ujtd2/
Edit using the state stack you can prevent having to do the conversion yourself.
var c = document.getElementById("mycanvas");
var ctx = c.getContext("2d");
var scale = 4;
// add a new item to the context state stack
ctx.save();
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0);
// discard the previous state be restoring the last state
// back to normal scale
ctx.restore();
// Set translation
ctx.translate(200, 200);
// Repeat for second image
ctx.save();
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0);
I follow now. To zoom in and show the part of the scene from a specific coordinate use translate.
ctx.scale(4, 4);
ctx.translate(-200, -200);
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 200, 200);
This zooms in by 4 and then moves the visible portion down and right by 200 pixels, by translating the drawing coordinates up and left by 200 pixels.
You can use drawImage the following way :
drawImage(
image,
sourceX,
sourceY,
sourceWidth,
sourceHeight,
destinationX,
destinationY,
destinationWidth,
destinationHeight
);
You determine the region of the source you want and then the place you want to put it on your canvas.
You can find some info here : MDN Draw image documentation