Using HTML5 Canvas - Rotate Image About Arbitrary Point

using HTML5 Canvas - rotate image about arbitrary point

In general, what you want to do is:

  1. Transform the context to the point on the canvas that the object should rotate about.
  2. Rotate the context.
  3. Transform the context by the negative offset within the object for the center of rotation.
  4. Draw the object at 0,0.

In code:

ctx.save();
ctx.translate( canvasRotationCenterX, canvasRotationCenterY );
ctx.rotate( rotationAmountInRadians );
ctx.translate( -objectRotationCenterX, -objectRotationCenterY );
ctx.drawImage( myImageOrCanvas, 0, 0 );
ctx.restore();

Here's a working example showing this in action. (The math for the rotation was just experimentally hacked to find a swing amount and offset in radians that fit the quickly-sketched gauge dial.)

As may be evident, you can substitute the translate call in step #3 above with offsets to the drawImage() call. For example:

ctx.drawImage( myImageOrCanvas, -objectRotationCenterX, -objectRotationCenterY );

Using context translation is recommended when you have multiple objects to draw at the same location.

Rotating around an arbitrary point: HTML5 Canvas

To rotate around a point you need to do 3 steps.

  • First translate the context to the center you wish to rotate around.
  • Then do the actual rotation.
  • Then translate the context back.

Like this:

var canvas = document.getElementById("testCanvas");
var dc = canvas.getContext("2d");
var angle = 0;
window.setInterval(function(){
angle = (angle + 1) % 360;
dc.clearRect(0, 0, canvas.width, canvas.height);

dc.save();
dc.fillStyle = "#FF0000";

dc.translate(150,200); // First translate the context to the center you wish to rotate around.
dc.rotate( angle*Math.PI/180 ); // Then do the actual rotation.
dc.translate(-150,-200); // Then translate the context back.

dc.beginPath();
dc.moveTo(100, 100);
dc.lineTo(200, 100);
dc.lineTo(200,300);
dc.lineTo(100,300);
dc.closePath();
dc.fill();

dc.restore();
}, 5);

Canvas rotate from bottom center image angle?

First you have to translate to the point around which you would like to rotate. In this case the image dimensions are 64 x 120. To rotate around the bottom center you want to translate to 32, 120.

ctx.translate(32, 120);

That brings you to the bottom center of the image. Then rotate the canvas:

ctx.rotate(90 * Math.PI/180);

Which rotate by 90 degrees.

Then when you draw the image try this:

ctx.drawImage(img, -32, -120, 64, 120);

? Does that work?

HTML5 canvas drawImage with at an angle

You need to modify the transformation matrix before drawing the image that you want rotated.

Assume image points to an HTMLImageElement object.

var x = canvas.width / 2;
var y = canvas.height / 2;
var width = image.width;
var height = image.height;

context.translate(x, y);
context.rotate(angleInRadians);
context.drawImage(image, -width / 2, -height / 2, width, height);
context.rotate(-angleInRadians);
context.translate(-x, -y);

The x, y coordinates is the center of the image on the canvas.

How can I rotate any shape or point on an HTML5 canvas around an arbitrary point?

It turns out the answer is pretty simple but involves a bit of math that may put some folks off. I'm using the Konvajs HTML5 canvas library but the code is easily transportable to your own lib. Also, this example is described as rotating a shape, but it's really rotating a point - the origin of the shape - so you could use it for any point-rotation-around-a-point case.

The rotateAroundPoint() function does the work - the rest of the code in the snippet is there to make it a working example.

Lifting this function out we can see that the inputs are the shape - although this could be any object with x, y and rotation properties, the rotation angle in degrees, and the rotation point - again an object with x & y values.

When we rotate around the point we are carrying out the equivalent of a rotation-in-place, followed by a translation (or move). These must be done in this sequence. Also, because of how 2d-drawing works, we have to work out the new position for the move and this depends on the drawing origin of the shape.

The calculation of the new x & y positions requires the use of sine & cosine functions which require radians - not degrees. So we multiply degrees by PI / 180 to get that.

// Rotate a shape around any point.
// shape is a Konva shape
// angleDegrees is the angle to rotate by, in degrees
// point is an object {x: posX, y: posY}
function rotateAroundPoint(shape, angleDegrees, point) {
let angleRadians = angleDegrees * Math.PI / 180; // sin + cos require radians

const x =
point.x +
(shape.x() - point.x) * Math.cos(angleRadians) -
(shape.y() - point.y) * Math.sin(angleRadians);
const y =
point.y +
(shape.x() - point.x) * Math.sin(angleRadians) +
(shape.y() - point.y) * Math.cos(angleRadians);

shape.rotation(shape.rotation() + angleDegrees); // rotate the shape in place
shape.x(x); // move the rotated shape in relation to the rotation point.
shape.y(y);

}

That's it! Have a play with the snippet - best viewed full-screen. Select a shape to rotate, then click the rotate button a few times to watch it spin around its origin (the natural point of rotation if we just change the rotation angle and nothing else). Then click the reset button, and click the canvas to move the blue target somewhere else on the canvas or shape, and rotate some more to see the effect.

There's also a codepen version here.

// Code to illustrate rotation of a shape around any given point. The important functions here is rotateAroundPoint() which does the rotation and movement math ! 

let
angle = 0, // display value of angle
startPos = {x: 80, y: 45},
shapes = [], // array of shape ghosts / tails
rotateBy = 20, // per-step angle of rotation
shapeName = $('#shapeName').val(), // what shape are we drawing
shape = null,
ghostLimit = 10,

// Set up a stage
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
}),


// add a layer to draw on
layer = new Konva.Layer(),

// create the rotation target point cross-hair marker
lineV = new Konva.Line({points: [0, -20, 0, 20], stroke: 'cyan', strokeWidth: 1}),
lineH = new Konva.Line({points: [-20, 0, 20, 0], stroke: 'cyan', strokeWidth: 1}),
circle = new Konva.Circle({x: 0, y: 0, radius: 10, fill: 'transparent', stroke: 'cyan', strokeWidth: 1}),
cross = new Konva.Group({draggable: true, x: startPos.x, y: startPos.y});

// Add the elements to the cross-hair group
cross.add(lineV, lineH, circle);
layer.add(cross);

// Add the layer to the stage
stage.add(layer);


$('#shapeName').on('change', function(){
shapeName = $('#shapeName').val();
shape.destroy();
shape = null;
reset();
})


// Draw whatever shape the user selected
function drawShape(){

// Add a shape to rotate
if (shape !== null){
shape.destroy();
}

switch (shapeName){
case "rectangle":
shape = new Konva.Rect({x: startPos.x, y: startPos.y, width: 120, height: 80, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;

case "hexagon":
shape = new Konva.RegularPolygon({x: startPos.x, y: startPos.y, sides: 6, radius: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;

case "ellipse":
shape = new Konva.Ellipse({x: startPos.x, y: startPos.y, radiusX: 40, radiusY: 20, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;

case "circle":
shape = new Konva.Ellipse({x: startPos.x, y: startPos.y, radiusX: 40, radiusY: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;

case "star":
shape = new Konva.Star({x: startPos.x, y: startPos.y, numPoints: 5, innerRadius: 20, outerRadius: 40, fill: 'magenta', stroke: 'black', strokeWidth: 4});
break;
};
layer.add(shape);

cross.moveToTop();

}



// Reset the shape position etc.
function reset(){

drawShape(); // draw the current shape

// Set to starting position, etc.
shape.position(startPos)
cross.position(startPos);
angle = 0;
$('#angle').html(angle);
$('#position').html('(' + shape.x() + ', ' + shape.y() + ')');

clearTails(); // clear the tail shapes

stage.draw(); // refresh / draw the stage.
}




// Click the stage to move the rotation point
stage.on('click', function (e) {
cross.position(stage.getPointerPosition());
stage.draw();
});

// Rotate a shape around any point.
// shape is a Konva shape
// angleRadians is the angle to rotate by, in radians
// point is an object {x: posX, y: posY}
function rotateAroundPoint(shape, angleDegrees, point) {
let angleRadians = angleDegrees * Math.PI / 180; // sin + cos require radians

const x =
point.x +
(shape.x() - point.x) * Math.cos(angleRadians) -
(shape.y() - point.y) * Math.sin(angleRadians);
const y =
point.y +
(shape.x() - point.x) * Math.sin(angleRadians) +
(shape.y() - point.y) * Math.cos(angleRadians);

shape.rotation(shape.rotation() + angleDegrees); // rotate the shape in place
shape.x(x); // move the rotated shape in relation to the rotation point.
shape.y(y);

shape.moveToTop(); //
}



$('#rotate').on('click', function(){

let newShape = shape.clone();
shapes.push(newShape);
layer.add(newShape);

// This ghost / tails stuff is just for fun.
if (shapes.length >= ghostLimit){
shapes[0].destroy();
shapes = shapes.slice(1);
}
for (var i = shapes.length - 1; i >= 0; i--){
shapes[i].opacity((i + 1) * (1/(shapes.length + 2)))
};

// This is the important call ! Cross is the rotation point as illustrated by crosshairs.
rotateAroundPoint(shape, rotateBy, {x: cross.x(), y: cross.y()});

cross.moveToTop();

stage.draw();

angle = angle + 10;
$('#angle').html(angle);
$('#position').html('(' + Math.round(shape.x() * 10) / 10 + ', ' + Math.round(shape.y() * 10) / 10 + ')');
})



// Function to clear the ghost / tail shapes
function clearTails(){

for (var i = shapes.length - 1; i >= 0; i--){
shapes[i].destroy();
};
shapes = [];

}

// User cicks the reset button.
$('#reset').on('click', function(){

reset();

})

// Force first draw!
reset();
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva@^3/konva.min.js"></script>
<p>1. Click the rotate button to see what happens when rotating around shape origin.</p>
<p>2. Reset then click stage to move rotation point and click rotate button again - rinse & repeat</p>
<p>
<button id = 'rotate'>Rotate</button>
<button id = 'reset'>Reset</button>
<select id='shapeName'>
<option value='rectangle'>Rectangle</option>
<option value='hexagon'>Polygon</option>
<option value='ellipse' >Ellipse</option>
<option value='circle' >Circle</option>
<option value='star' selected='selected'>Star</option>

</select>
Angle : <span id='angle'>0</span>
Position : <span id='position'></span>
</p>
<div id="container"></div>

Handling a rotated canvas image dragable relative to the canvas not the image

Through some more investigation, trial and error, and a slice of luck, found it! Have updated the fiddle - http://jsfiddle.net/jamesinealing/3chy076x/ - but here's the important bit of code for anyone who finds themselves with the same problem! The crucial bit was breaking it up so that any horizontal or verticle mouse movement could actually contribute to a proportionate x and y shift on teh rotated image, which varied according to the angle (so a 90-degree angle means an x mouse movement is translated 100% into a y image movement etc)

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate((canvas.width - pic.width) / 2, (canvas.height - pic.height) / 2);
ctx.rotate(angle * Math.PI / 180);
// now set the correct pan values based on the angle
panXX = (panX*(Math.cos(angle * Math.PI / 180)));
panXY = (panY*(Math.sin(angle * Math.PI / 180)));
panYY = (panY*(Math.cos(angle * Math.PI / 180)));
panYX = (panX*(Math.sin(angle * Math.PI / 180)));
panXTransformed = panXX+panXY;
panYTransformed = panYY-panYX;
ctx.drawImage(pic, panXTransformed, panYTransformed, pic.width, pic.height);
ctx.fillStyle="#0000FF";
ctx.fillRect(0,0,5,5); // place marker at 0,0 for reference
ctx.fillStyle="#FF0000";
ctx.fillRect(panXTransformed, panYTransformed,5,5); // place marker at current tranform point
ctx.restore();

Rotating canvas around a point and getting new x,y offest

  • First we need to find the distance from pivot point to the corner.
  • Then calculate the angle between pivot and corner
  • Then calculate the absolute angle based on previous angle + new angle.
  • And finally calculate the new corner.

Snapshot from demo

Snapshot from demo below showing a line from pivot to corner.

The red dot is calculated while the rectangle is rotated using

translations.

Here is an example using an absolute angle, but you can easily convert this into converting the difference between old and new angle for example. I kept the angles as degrees rather than radians for simplicity.

The demo first uses canvas' internal translation and rotation to rotate the rectangle. Then we use pure math to get to the same point as evidence that we have calculated the correct new x and y point for corner.

/// find distance from pivot to corner
diffX = rect[0] - mx; /// mx/my = center of rectangle (in demo of canvas)
diffY = rect[1] - my;
dist = Math.sqrt(diffX * diffX + diffY * diffY); /// Pythagoras

/// find angle from pivot to corner
ca = Math.atan2(diffY, diffX) * 180 / Math.PI; /// convert to degrees for demo

/// get new angle based on old + current delta angle
na = ((ca + angle) % 360) * Math.PI / 180; /// convert to radians for function

/// get new x and y and round it off to integer
x = (mx + dist * Math.cos(na) + 0.5)|0;
y = (my + dist * Math.sin(na) + 0.5)|0;

Initially you can verify the printed x and y by seeing that the they are exact the same value as the initial corner defined for the rectangle (50, 100).

UPDATE

It seems as I missed the word in: offset for the new bounds... sorry about that, but what you can do instead is to calculate the distance to each corner instead.

That will give you the outer limits of the bound and you just "mix and match" the corner base on those distance values using min and max.

New Live demo here

Bounds

The new parts consist of a function that will give you the x and y of a corner:

///mx, my = pivot, cx, cy = corner, angle in degrees
function getPoint(mx, my, cx, cy, angle) {

var x, y, dist, diffX, diffY, ca, na;

/// get distance from center to point
diffX = cx - mx;
diffY = cy - my;
dist = Math.sqrt(diffX * diffX + diffY * diffY);

/// find angle from pivot to corner
ca = Math.atan2(diffY, diffX) * 180 / Math.PI;

/// get new angle based on old + current delta angle
na = ((ca + angle) % 360) * Math.PI / 180;

/// get new x and y and round it off to integer
x = (mx + dist * Math.cos(na) + 0.5)|0;
y = (my + dist * Math.sin(na) + 0.5)|0;

return {x:x, y:y};
}

Now it's just to run the function for each corner and then do a min/max to find the bounds:

/// offsets
c2 = getPoint(mx, my, rect[0], rect[1], angle);
c2 = getPoint(mx, my, rect[0] + rect[2], rect[1], angle);
c3 = getPoint(mx, my, rect[0] + rect[2], rect[1] + rect[3], angle);
c4 = getPoint(mx, my, rect[0], rect[1] + rect[3], angle);

/// bounds
bx1 = Math.min(c1.x, c2.x, c3.x, c4.x);
by1 = Math.min(c1.y, c2.y, c3.y, c4.y);
bx2 = Math.max(c1.x, c2.x, c3.x, c4.x);
by2 = Math.max(c1.y, c2.y, c3.y, c4.y);

Rotating a star around one of its points on an HTML5 canvas

Just translate to the the point you want to rotate around, rotate and then translate back continuing the drawing of the star.

However, with the function you're using this may turn out to be a little more complex than just that as you would need the absolute position of the point for the star "spikes".

Here is an alternative implementation I wrote to draw deal with this. Here we call a function to get an array with the points in order to build a star.

We can then use that point array to chose a pivot point for rotation as well as rendering the star itself. The function looks like this:

// cx = center x
// cy = center y
// r1 = outer radius
// r2 = inner radius
// spikes = number of star "spikes"
function getStarPoints(cx, cy, r1, r2, spikes) {

var i = 0,
deltaAngle = (2 * Math.PI) / spikes,
x, y,
points = [];

// calc rest of points
for (; i < spikes; i++) {
points.push(
{
x: cx + r2 * Math.cos(deltaAngle * i), // calc inner point
y: cy + r2 * Math.sin(deltaAngle * i)
}, {
x: cx + r1 * Math.cos(deltaAngle * i + deltaAngle * 0.5), // outer point
y: cy + r1 * Math.sin(deltaAngle * i + deltaAngle * 0.5)
}
);
}

return points;
}

Now all we need to do is to pick a point from the array which would be a point object, and use that for center of rotation, here point index 1:

ctx.translate(points[1].x, points[1].y);   // translate to origin for rotation
ctx.rotate(angle); // rotate angle (radians)
ctx.translate(-points[1].x, -points[1].y); // translate back

Then render the star

ctx.beginPath();                           // start a new path
ctx.moveTo(points[0].x, points[0].y); // starting point
for(var i = 1, p; p = points[i++];)
ctx.lineTo(p.x, p.y); // add points to path
ctx.fill(); // close and fill

Note: if you want to stroke the star you would need a closePath() before stroking.

That's it. See snippet below for an animated version of this as well as full source.

Performance wise this would be faster as well as we calculate the star only once, and in this case we do not use save/restore (which are somewhat costly operations) for the animation.

Hope this helps!

var ctx = document.getElementById('canvas').getContext('2d'),    points = [],    angleStep = 0.025;
ctx.fillStyle = 'orange';
// draw first star at angle 0 and get point arraypoints = getStarPoints(150, 90, 80, 40, 5);
// now that we know the point for each part, lets animateloop();
function loop() {
// clear canvas ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// rotate around one of the points (or anywhere if wanted..) ctx.translate(points[1].x, points[1].y); ctx.rotate(angleStep); ctx.translate(-points[1].x, -points[1].y);
// draw start ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for(var i = 1, p; p = points[i++];) ctx.lineTo(p.x, p.y); ctx.fill();
requestAnimationFrame(loop);}
// Star by Ken Fyrstenberg/CC3.0-Attr// cx = center x// cy = center y// r1 = outer radius// r2 = inner radius// spikes = number of star "spikes"function getStarPoints(cx, cy, r1, r2, spikes) {
var i = 0, deltaAngle = (2 * Math.PI) / spikes, x, y, points = [];
// calc rest of points for (; i < spikes; i++) { points.push( { x: cx + r2 * Math.cos(deltaAngle * i), y: cy + r2 * Math.sin(deltaAngle * i) }, { x: cx + r1 * Math.cos(deltaAngle * i + deltaAngle * 0.5), y: cy + r1 * Math.sin(deltaAngle * i + deltaAngle * 0.5) } ); }
return points;}
<canvas id="canvas" width=350 height=180></canvas>


Related Topics



Leave a reply



Submit