I was trying to do a perspective grid on my canvas and I've changed the function from another website with this result:
function keystoneAndDisplayImage(ctx, img, x, y, pixelHeight, scalingFactor) {
var h = img.height,
w = img.width,
numSlices = Math.abs(pixelHeight),
sliceHeight = h / numSlices,
polarity = (pixelHeight > 0) ? 1 : -1,
heightScale = Math.abs(pixelHeight) / h,
widthScale = (1 - scalingFactor) / numSlices;
for(var n = 0; n < numSlices; n++) {
var sy = sliceHeight * n,
sx = 0,
sHeight = sliceHeight,
sWidth = w;
var dy = y + (sliceHeight * n * heightScale * polarity),
dx = x + ((w * widthScale * n) / 2),
dHeight = sliceHeight * heightScale,
dWidth = w * (1 - (widthScale * n));
ctx.drawImage(img, sx, sy, sWidth, sHeight,
dx, dy, dWidth, dHeight);
}
}
It creates almost-good perspective grid, but it isn't scaling the Height, so every square has got the same height. Here's a working jsFiddle and how it should look like, just below the canvas. I can't think of any math formula to distort the height in proportion to the "perspective distance" (top).
I hope you understand. Sorry for language errors. Any help would be greatly appreciatedRegards
There is sadly no proper way besides using a 3D approach. But luckily it is not so complicated.
The following will produce a grid that is rotatable by the X axis (as in your picture) so we only need to focus on that axis.
To understand what goes on: We define the grid in Cartesian coordinate space. Fancy word for saying we are defining our points as vectors and not absolute coordinates. That is to say one grid cell can go from 0,0 to 1,1 instead of for example 10,20 to 45, 45 just to take some numbers.
At the projection stage we project these Cartesian coordinates into our screen coordinates.
The result will be like this:
ONLINE DEMO
Ok, lets dive into it - first we set up some variables that we need for projection etc:
fov = 512, /// Field of view kind of the lense, smaller values = spheric
viewDist = 22, /// view distance, higher values = further away
w = ez.width / 2, /// center of screen
h = ez.height / 2,
angle = -27, /// grid angle
i, p1, p2, /// counter and two points (corners)
grid = 10; /// grid size in Cartesian
To adjust the grid we don't adjust the loops (see below) but alter the fov and viewDist as well as modifying the grid to increase or decrease the number of cells.
Lets say you want a more extreme view - by setting fov to 128 and viewDist to 5 you will get this result using the same grid and angle:
The "magic" function doing all the math is as follows:
function rotateX(x, y) {
var rd, ca, sa, ry, rz, f;
rd = angle * Math.PI / 180; /// convert angle into radians
ca = Math.cos(rd);
sa = Math.sin(rd);
ry = y * ca; /// convert y value as we are rotating
rz = y * sa; /// only around x. Z will also change
/// Project the new coords into screen coords
f = fov / (viewDist + rz);
x = x * f + w;
y = ry * f + h;
return [x, y];
}
And that's it. Worth to mention is that it is the combination of the new Y and Z that makes the lines smaller at the top (at this angle).
Now we can create a grid in Cartesian space like this and rotate those points directly into screen coordinate space:
/// create vertical lines
for(i = -grid; i <= grid; i++) {
p1 = rotateX(i, -grid);
p2 = rotateX(i, grid);
ez.strokeLine(p1[0], p1[1], p2[0], p2[1]); //from easyCanvasJS, see demo
}
/// create horizontal lines
for(i = -grid; i <= grid; i++) {
p1 = rotateX(-grid, i);
p2 = rotateX(grid, i);
ez.strokeLine(p1[0], p1[1], p2[0], p2[1]);
}
Also notice that position 0,0 is center of screen. This is why we use negative values to get out on the left side or upwards. You can see that the two center lines are straight lines.
And that's all there is to it. To color a cell you simply select the Cartesian coordinate and then convert it by calling rotateX() and you will have the coordinates you need for the corners.
For example - a random cell number is picked (between -10 and 10 on both X and Y axis):
c1 = rotateX(cx, cy); /// upper left corner
c2 = rotateX(cx + 1, cy); /// upper right corner
c3 = rotateX(cx + 1, cy + 1); /// bottom right corner
c4 = rotateX(cx, cy + 1); /// bottom left corner
/// draw a polygon between the points
ctx.beginPath();
ctx.moveTo(c1[0], c1[1]);
ctx.lineTo(c2[0], c2[1]);
ctx.lineTo(c3[0], c3[1]);
ctx.lineTo(c4[0], c4[1]);
ctx.closePath();
/// fill the polygon
ctx.fillStyle = 'rgb(200,0,0)';
ctx.fill();
An animated version that can help see what goes on.
Related
I am wondering how you can rotate an image by only using the transform function. From my understanding this is not possible, since the only things you can do with transform are the following:
Horizontal scaling
Horizontal skewing
Vertical skewing
Vertical scaling
Horizontal moving
Vertical moving
Source: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Transformations
And I don't see how any of these would be able to rotate the shape, is this even possible. I assume it must be possible, since rotate is in-fact a type transformation.
2D transform basics
6 values as 3 vectors
The transform is a set of 6 numbers. The 6 numbers as 3 pairs represent the direction and scale of the x axis, the direction and scale of the y axis, and the position of the origin.
Default transform AKA Identity matrix
The default transform (called the identity matrix) has the values ctx.setTransform(1, 0, 0, 1, 0, 0) meaning that
the x axis is 1 transformed pixel per CSS pixel in the direction {x: 1, y: 0} left to right
the y axis is 1 transformed pixel per CSS pixel in the direction {x: 0, y: 1} top to bottom
the origin is at pixel location {x: 0, y: 0} top left corner
Scaling
If we scale the transform we increase the length of the first two vectors. To scale by 2 the transform is ctx.setTransform(2, 0, 0, 2, 0, 0);
the x axis is 1 transformed pixel for every 2 CSS pixel in the x direction {x: 2, y: 0} left to right
the y axis is 1 transformed pixel for every 2 CSS pixel in the y direction {x: 0, y: 2} top to bottom
the origin is still top left {x: 0, y: 0}
Rotate and translation
If we want to rotate by 90deg a square 256 by 256 image then the transform is ctx.setTransform(0, 1, -1, 0, 256, 0)
the x axis is 1 transformed pixel per CSS pixel down in the y direction {x: 0, y: 1}
the y axis is 1 transformed pixel per CSS pixel across in the negative x direction {x: -1, y: 0} right to left
the origin (where the image 0, 0 will be on the canvas) is {x: 256, y: 0}
Thus if we run
ctx.setTransform(0, 1, -1, 0, 256, 0);
ctx.drawImage(myImage, 0, 0, 256, 256); // will draw image rotated 90deg CW
We get a rotated image.
A vector
A vector is two values that have a x and y value. The vector defines a direction and length.
Create a rotated unit vector
To convert a direction to a vector we use sin and cos
const myDirection = angle;
const myDirectionAsRadians = angle * (Math.PI / 180); // convert angle to radians
const x = Math.cos(myDirectionAsRadians)
const y = Math.sin(myDirectionAsRadians)
If we set myDirection to 90 (deg) then x = 0 and y = 1 pointing down the canvas
Using sin and cos creates a vector in any direction. It has a special property in that its length is always 1. We call such a vector a Unit vector. You may sometimes see a vector being normalized. This converts a vector of any length to a unit vector. It is done by dividing the vector x and y by its length.
function normalize(vector) {
const length = Math.hypot(vector.x, vector.y);
vector.x /= length;
vector.y /= length;
}
NOTE a vector with zero length eg x: 0, y:0 can not be normalized. Not because it has no length (the length is 0) but because it has no direction.
Scale a rotated vector
We can define an angle and a scale
const myDirection = -90;
const myDirectionAsRadians = -90 * (Math.PI / 180); // -90 as radians
const myScale = 2;
const x = Math.cos(myDirectionAsRadians) * myScale
const y = Math.sin(myDirectionAsRadians) * myScale
Now for -90 deg the vector is x = 0 and y = -2 pointing up and two CSS pixels long.
Quick rotate vector 90deg CW
For a uniform scale and rotation (the image is always square) all we need is a single vector. For example from the above. x = 0 and y = -2 (pointing up) can be rotated 90 CW by swapping the two components and negating the new x. eg xx = -y and y = x to get xx = 2 and y = 0 2 CSS pixels from left two right. Thus we have the direction and scale of both the x and y axis. With the y axis always 90 CW from the x.
Using the transform to draw
Create rotated and scaled transform
To create a transform that rotates any angle and scales by any amount
function scaleAndRotate(scale, rotate) { // rotate is in radians
// get direction and length of x axis
const xAX = Math.cos(rotate) * scale;
const xAY = Math.sin(rotate) * scale;
// get direction and length of y axis that is 90 deg CW of x axis and same length
const [yAX, yAY] = [-xAY, xAX]; // swap and negate new x
// set the transform
ctx.setTransform(xAX, xAY, yAX, yAY, 0, 0);
}
Drawing an image
Lets create a function that will draw an image anywhere on the canvas that is rotated and scaled uniformly. We will use the center of the image as the reference point
function drawImageScaleRotate(img, x, y, scale, rotate) {
// define the direction and scale of x axis
const xAX = Math.cos(rotate) * scale;
const xAY = Math.sin(rotate) * scale;
// create the transform with yaxis at 90 CW of x axis and origin at x, y
ctx.setTransform(xAX, xAY, -xAY, xAX, x, y);
// Draw the image so that its center is at the new origin x, y
ctx.drawImage(img, -img.width / 2, -img.height / 2);
}
There is much more
When we set the transform with ctx.setTranform we replace the existing transform. This transform remains current. If we use ctx.transform, ctx.rotate, ctx.scale, ctx.translate the transforms are applied to the current transform, you build a transform in stages.
The transform functions are relatively expensive in terms of CPU cycles. That is way using sin and cos to build the matrix is much faster than using ctx.scale, ctx.rotate, ctx.translate to do the same thing starting from default.
Building transforms can become tricky as we need to keep track of what stage we are at.
We generally only use these function not to transform a single image (text, path, or what ever) but to create linked transforms.
For example a game object like a tank. The body of the tank is transformed (rotated and positioned) then the turret which is rotated with the body but has an additional independent rotation by using ctx.rotate. Full explanation is beyond the scope of this question.
The final function
From all this we can create a simplified function that will draw an image with its center at any location, that is uniformly scaled and rotated
function drawImageScaleRotate(img, x, y, scale, rotate) {
const xAX = Math.cos(rotate) * scale;
const xAY = Math.sin(rotate) * scale;
ctx.setTransform(xAX, xAY, -xAY, xAX, x, y);
ctx.drawImage(img, -img.width / 2, -img.height / 2);
}
To reset the transform to the default use ctx.resetTransform NOTE not fully supported yet or use ctx.setTransform(1,0,0,1,0,0);
Animated transforms faster than CSS + HTML or SVG
Using the above function is the 2nd fastest way to draw animated rotated scaled images, faster than CSS + HTML or SVG. You can literally fill the screen with animated images.
Demo
var w,h;
var image = new Image;
image.src = "https://i.stack.imgur.com/C7qq2.png?s=328&g=1";
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
document.body.appendChild(canvas);
const resize = () => { w = canvas.width = innerWidth; h = canvas.height = innerHeight;}
const rand = (min,max) => Math.random() * (max ?(max-min) : min) + (max ? min : 0);
const DO = (count,callback) => { while (count--) { callback(count) } }
resize();
addEventListener("resize",resize);
const sprites = [];
DO(500,()=>{
sprites.push({
xr : rand(w), yr : rand(h),
x : 0, y : 0, // actual position of sprite
r : rand(Math.PI * 2),
scale : rand(0.1,0.25),
dx : rand(-2,2), dy : rand(-2,2),
dr : rand(-0.2,0.2),
});
});
function drawImage(image, spr){
const xAX = Math.cos(spr.r) * spr.scale;
const xAY = Math.sin(spr.r) * spr.scale;
ctx.setTransform(xAX, xAY, -xAY, xAX, spr.x, spr.y);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
function update(){
var ihM,iwM;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
if(image.complete){
var iw = image.width;
var ih = image.height;
for(var i = 0; i < sprites.length; i ++){
var spr = sprites[i];
spr.xr += spr.dx;
spr.yr += spr.dy;
spr.r += spr.dr;
// keeps images in canvas adds space to all sides so that image
// can move completely of the canvas befor warping to other side
// I do this to prevent images visualy popping in and out at edges
iwM = iw * spr.scale * 2 + w;
ihM = ih * spr.scale * 2 + h;
spr.x = ((spr.xr % iwM) + iwM) % iwM - iw * spr.scale;
spr.y = ((spr.yr % ihM) + ihM) % ihM - ih * spr.scale;
drawImage(image,spr);
}
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
If you are wondering which is the fastest way to draw animated content. That is via webGL. The above can draw 1000 scaled rotated images on most devices at a good frame rate. WebGL can easily draw 10000 (with extra features eg colored) in the same time.
You can rotate your image easily. You can specify angles by which you wants to apply rotation.
Source : https://www.w3schools.com/cssref/css3_pr_transform.asp
<style>
img.a {
transform: rotate(180deg);
}
</style>
<img class="a" src="https://picsum.photos/id/237/200/300"/>
use transform "rotate".and use it as below
p{
color:red;
font-size:12px;
text-align:center;
}
.rotate1{
transform:rotate(45deg);
margin-top:40px;
}
.rotate2{
transform:rotate(90deg);
margin-top:40px;
}
.rotate3{
transform:rotate(180deg);
margin-top:40px;
}
<p class="rotate1">ROTATE1</p>
<p class="rotate2">ROTATE2</p>
<p class="rotate3">ROTATE3</p>
You have ctx.rotate(radians) function.
Read below:
https://www.w3schools.com/tags/canvas_rotate.asp
I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.
Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.
This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms
I need to adapt the algorithm (or find the formula) to work in all three dimensions to find the new four coordinates. In the example i've obtained the new width and height of a transformed (rotate and perspective), but i don't know how to extract the angle and the new coordinates.
var x = Math.cos(angle) * origy;
var z = Math.sin(angle) * origy;
return x * p / (p + z);
Given an angle between 0° and 90°, generate a SVG gradient that fills entire rectangle.
SVG gradients accept two control points rather than angle. Here is the code of the first square on the picture above:
<linearGradient x1="0" y1="0" x2="1" y2="0.5">
The problem is that the gradient doesn’t cover the entire square. I want to extend the gradient just enough to fill the shape entirely so the red triangle would be not visible. Here is an interactive demo (tested in Chrome, Firefox and Safari) to give you a better idea.
Solution in JavaScript:
function angleToVector(angle) {
var od = Math.sqrt(2);
var op = Math.cos(Math.abs(Math.PI/4 - angle)) * od;
var x = op * Math.cos(angle);
var y = op * Math.sin(angle);
return {x: x, y: y};
}
For angle between -180° and 180°:
function angleToPoints(angle) {
var segment = Math.floor(angle / Math.PI * 2) + 2;
var diagonal = (1/2 * segment + 1/4) * Math.PI;
var op = Math.cos(Math.abs(diagonal - angle)) * Math.sqrt(2);
var x = op * Math.cos(angle);
var y = op * Math.sin(angle);
return {
x1: x < 0 ? 1 : 0,
y1: y < 0 ? 1 : 0,
x2: x >= 0 ? x : x + 1,
y2: y >= 0 ? y : y + 1
};
}
There might be a simpler solution for this.
So your question as I understand it is this: given a rectangle (whose top left corner is the origin O = (0, 0) and whose bottom right corner is D = (w, h)) and a line l through point O at angle a (with 0° <= a <= 90°), find the point P = (x2, y2) on l such that line DP makes a right angle with l.
If you draw the diagonal of the rectangle, OD, it completes a right triangle with the right angle at P. The angle of that diagonal is atan(h/w), and if you take the absolute difference of that from a (i.e. |atan(h/w) - a|), you'll get the angle in that right triangle at point O. Then you can take the cosine of that angle to get the distance between O and P along l as a proportion of the length of OD (the hypotenuse). You can multiply out the hypotenuse, and then just multiply that by cos(a) and sin(a) to get x2 and y2, respectively.
To summarize:
|OD| = sqrt(w*w + h*h)
|OP| = cos(|atan(h/w) - a|) * |OD|
x2 = |OP| * cos(a)
y2 = |OP| * sin(a)
I am writing a multitouch jigsaw puzzle using html5 canvas in which you can rotate the pieces around a point. Each piece has their own canvas the size of their bounding box. When the rotation occurs, the canvas size must change, which I am able to calculate and is working. What I can't figure out, is how to find the new x,y offsets if I am to have this appear to be rotating around the pivot (first touch point).
Here is an image to better explain what I'm trying to achieve. Note the pivot point is not always the center, otherwise I could just halve the difference between the new bounds and the old.
So I know the original x, y, width, height, rotation angle, new bounds(rotatedWidth, rotatedHeight), and the pivot X,Y relating to original object. What I can't figure out how to get is the x/y offset for the new bounds (to make it appear that the object rotated around the pivot point)
Thanks in advance!
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 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
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);
to rotate around the centre point of the canvas you can use this function:
function rotate(context, rotation, canvasWidth, canvasHeight) {
// Move registration point to the center of the canvas
context.translate(canvasWidth / 2, canvasHeight/ 2);
// Rotate 1 degree
context.rotate((rotation * Math.PI) / 180);
// Move registration point back to the top left corner of canvas
context.translate(-canvasWidth / 2, -canvasHeight/ 2);
}
Here is the way that worked best for me. First I calculate what is the new width and height of that image, then I translate it by half of that amount, then I apply the rotation and finally I go back by the original width and height amount to re center the image.
var canvas = document.getElementById("canvas")
const ctx = canvas.getContext('2d')
drawRectangle(30,30,40,40,30,"black")
function drawRectangle(x,y,width,height, angle,color) {
drawRotated(x,y,width,height,angle,ctx =>{
ctx.fillStyle = color
ctx.fillRect(0,0,width,height)
})
}
function drawRotated(x,y,width,height, angle,callback)
{
angle = angle * Math.PI / 180
const newWidth = Math.abs(width * Math.cos(angle)) + Math.abs(height * Math.sin(angle));
const newHeight = Math.abs(width * Math.sin(angle)) + Math.abs(height * Math.cos(angle));
var surface = document.createElement('canvas')
var sctx = surface.getContext('2d')
surface.width = newWidth
surface.height = newHeight
// START debug magenta square
sctx.fillStyle = "magenta"
sctx.fillRect(0,0,newWidth,newHeight)
// END
sctx.translate(newWidth/2,newHeight/2)
sctx.rotate(angle)
sctx.translate(-width/2,-height/2)
callback(sctx)
ctx.drawImage(surface,x-newWidth/2,y-newHeight/2)
}
#canvas{
border:1px solid black
}
<canvas id="canvas">
</canvas>