Rotate tiled images individually in HTML canvas - javascript

I have some code in my game that rotates an icon within its canvas as follows:
if (rotate > 0) {
context.translate(canvasWidth / 2, canvasHeight / 2);
context.rotate(rotate);
context.translate(-canvasWidth / 2, -canvasHeight / 2);
}
Nothing you haven't seen before. I've also added a function that tiles the icons within a larger canvas like so:
var x = 0;
var y = 0;
for (var i = 0; i < totalUnits; i++) {
context.drawImage(img, x, y);
if (i != 0 && (i + 1) % level == 0) {
x = 0;
y += 72;
}
else {
x += 72;
}
}
Note that the variable level can be any integer, and totalUnits is its square if it's greater than 1, so for example if I specify level as 2, then 4 images are drawn on my canvas, 2 across and 2 down. Note also that my images are always 72x72 pixels, hence the 72 above. Again, nothing particularly exciting.
My difficulty is trying to rotate the images within the canvas such that the individual image is rotated by the value passed in rotate, but not the whole canvas itself. I have tried adding the following code with many permutations replacing the above call to context.drawImage in the for loop, with no luck so far:
context.translate(72 / 2, 72 / 2);
context.rotate(rotate);
context.drawImage(img, x, y);
context.rotate(0);
context.translate(-72 / 2, -72 / 2);
To help visualise the effect I am trying to achieve, here is what is drawn when rotate is set to 0:
And here is what I'd like my tiled images on the canvas to look like when rotated by 45 degrees (for example):
I'd like to point out that I am not trying to rotate the entire canvas - I know how to do that, but it is not the effect I'm trying to achieve as I need the icons to stay in their individual x, y positions. Also, rotating the entire canvas presents cutoff corner challenges.
Any help would be much appreciated!

Easiest way is to overwrite the current transform
ctx.setTransform(scale, 0, 0, scale, x, y);
ctx.rotate(rotate);
ctx.drawImage(img, -img.width / 2, -img.height/ 2);
Draw image center at x, y, rotated by rotate around img center, and scales by scale. Scale of 1 is no scale.
To reset to default transform for example before clearing the canvas
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
Because under the hood the ctx.rotate requires at least 1 sin and 1 cos, 12 temp numbers, then 12 multiplications and 8 additions. If the scaling is uniform (same scale for x and y), it is quicker to use a simplified method to creating the matrix. Note if the scale is always 1 you don't need the scale`
Also when the image loads you can set the point of rotation as properties of the image.
When the image loads set the offset
img.offset_x = - img.naturalWidth / 2; // NOTE I use snake_case for the property names
img.offset_y = - img.naturalHeight / 2; // so that the names will never clash with
// future changes to the standard
To render that image
const ax = Math.cos(rotate) * scale;
const ay = Math.sin(rotate) * scale;
ctx.setTranform(ax, ay, -ay, ax, x, y);
ctx.drawImage(img, img.offset_x, img.offset_y);

Related

Resizing rotated rectangle on HTML canvas

I have an instance of HTML 5 canvas and a rectangle drawn on it.
My drawing function takes a resizing angle into account and uses relative coordinates.
Relative coordinates're based upon three variables: top left rectangle point, rectangle width and rectangle height.
Rectangle width and rectangle height're calculated using two points: top left rectangle point and bottom right rectangle point.
To sum up, drawing function depends on top left rectangle point, bottom right rectangle point and rotation. It's an important point for the following text!
Here's a code snippet:
var canvas = document.getElementById('imageCanvas');
var ctx = canvas.getContext('2d');
var xTopLeft = 550;
var yTopLeft = 200;
var xBottomRight = 750;
var yBottomRight = 450;
var w = Math.max(xTopLeft, xBottomRight) - Math.min(xTopLeft, xBottomRight);
var h = Math.max(yTopLeft, yBottomRight) - Math.min(yTopLeft, yBottomRight);
var r = 1;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save()
ctx.translate(xTopLeft + w / 2, yTopLeft + h / 2);
ctx.rotate(r);
ctx.fillStyle = "yellow";
ctx.fillRect(w / 2 * (-1), h / 2 * (-1), w, h);
ctx.restore()
}
Here's my rectangle with a bunch of controls: eight resizing handles (white) and one rotation handle (green):
Rotating works fine.
Resizing works fine.
And I also try to implement resizing after rotation. Here's my approach with a humble illustration:
Grab the coordinates of the red point (it's mouse cursor coordiantes)
Derotate the point using negative angle to get derotated coordinates
function rotatePoint(x, y, center_x, center_y, angle) {
var new_x = (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx;
var new_y = (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy;
return [new_x, new_y]
}
Update xTopLeft, yTopLeft and redraw
Done
The idea behind this approach is simple. Since my drawing function depeneds on top left rectangle point and bottom right rectangle point I just need to get their new coordinates.
For instance, here's a simplified code for B point:
if (point == 'B') {
var newPointB = rotatePoint(mouse.x, mouse.y, center_x, center_y, -r);
xBottomRight = newPointB[0];
yTopLeft = newPointB[1];
}
But it doesn't work as expected: while resizing my rotated rectangle shifts, jumps and totally misbehaves.
In search of insights I've stumbled upon this article. The article covers my problem, but I don't get author's approach and can't implement it.
Why should I always lock the coordinates of the A point? My top left handle is intended to resize the rectangle in a north-west direction, so it would be necessary to change the coordinates of the A point...
Why should we recalculate the center point before derotation? It breaks the idea of uniform matrix transformations...
What's the correct algorithm in my case?
I was also facing same problem. It turned out that the angle I was using was in degree. Try multiplying angle in rotatePoint function with (Math.PI / 180).

p5.js WebGL 3d graphics covered by 2d background when rotated

I have two things that I want to display with p5, one is a 2D background and the other is a 3D WebGL foreground, both generated by p5. What I noticed is that even if I draw the 2D background before the 3D stuff in the draw() function, the 3D stuff will still be partially covered by the background when rotateX() or rotateY() is called. It looks kind of like this:
I suspect what's happening is that the 2d and 3d stuff are both on the same z-plane, therefore when the foreground is rotated some of it gets covered by the background which now is in the front compared to the covered parts.
So my question is how can I keep the background completely in the back (i.e. not covering foreground regardless of the rotation)?
Below is my current implementation, the 2d background is generated in an offscreen canvas then put onto the main canvas with image() where the 3d stuff is generated, but I'll take any other approaches.
let bg;
p.setup = () => {
p.createCanvas(width,height,p.WEBGL);
bg = p.createGraphics(width,height);
}
p.draw = () => {
... // draw background bg
p.image(bg,x,y); // draw background on canvas
... // draw foreground
p.rotateX(degrees);//rotate
}
The best way to accomplish this is by clearing the WebGL depth buffer. This is buffer stores the depth for every pixel that has been draw so far so that as subsequent triangles are drawn they can be clipped if some or all of them is behind whatever was previously drawn at that location. This buffer is automatically cleared in between calls to draw() in p5.js but you can also call it yourself mid-frame:
let bg;
let zSlider;
let glContext;
function setup() {
let c = createCanvas(200, 200, WEBGL);
glContext = c.GL;
bg = createGraphics(width, height);
bg.background('red');
for (let y = 0; y < height; y += 20) {
for (let x = 0; x < width; x += 20) {
if ((x / 20 + y / 20) % 2 === 0) {
bg.fill('black');
} else {
bg.fill(
map(x + y, 0, width + height, 0, 360),
map(y, 0, height, 50, 100),
map(x, 0, width, 50, 100)
);
}
bg.square(x, y, 20)
}
}
zSlider = createSlider(0, width * 2, width);
zSlider.position(10, 10);
}
function draw() {
image(bg, -width / 2, -height / 2, width, height);
// Clear the z-buffer, subsequent drawing commands will not clip, even if they
// intersect with or are behind previously drawn elements (like our background
// image)
glContext.clear(glContext.DEPTH_BUFFER_BIT);
push();
translate(0, 0, (zSlider.value() - width) * 2);
rotateX(millis() / 1000 * PI / 4);
rotateY(millis() / 1000 * PI / 8);
box(100);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>
There is also kludgy solution that doesn't rely on calling WebGL internals, but I have only been able to make it work for square canvases:
Switch to an orthographic camera mode before drawing your background image.
Translate in the negative Z direction as far as possible without going beyond the "far" clipping plane.
Draw your background image.
Pop the state back to the normal perspective camera.
This uses orthographic projection to allow you to draw the background image behind the rest of the scene without diminishing size due to perspective. However I haven't come up with a fool proof way to determine what the perfect translation value is, nor how to reliably setup the orthographic project to control where the "far" clipping plane is.
let bg;
let zSlider;
function setup() {
createCanvas(200, 200, WEBGL);
bg = createGraphics(width, height);
bg.background('red');
for (let y = 0; y < height; y += 20) {
for (let x = 0; x < width; x += 20) {
if ((x / 20 + y / 20) % 2 === 0) {
bg.fill('black');
} else {
bg.fill(
map(x + y, 0, width + height, 0, 360),
map(y, 0, height, 50, 100),
map(x, 0, width, 50, 100)
);
}
bg.square(x, y, 20)
}
}
zSlider = createSlider(0, width * 2, width);
zSlider.position(10, 10);
}
function draw() {
push();
ortho();
translate(0, 0, min(width, height) * -0.13);
image(bg, -width / 2, -height / 2, width, height);
pop();
push();
translate(0, 0, (zSlider.value() - width) * 2);
rotateX(millis() / 1000 * PI / 4);
rotateY(millis() / 1000 * PI / 8);
box(100);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>

Image Manipulation - add image with corners in exact positions

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

HTML5 canvas translate back after scale and rotation

I'm trying to do a few thing with canvas. First I have a user upload an image, if the image is larger than I want I need to scale it down. That part is working just fine. Recently we ran into an issue with iPhone users uploading images. These have orientation issues. I've figured out how to get the orientation extracted, my issue is what happens when I manipulate the image in the canvas.
This is what I need to do: Get the image, translate(), scale(), rotate(), translate() <- get it back to its original position, drawImage().
When I do that part of the image is off in the abyss.
if (dimensions[0] > 480 || dimensions[1] > 853) {
// Scale the image.
var horizontal = width > height;
if (horizontal) {
scaledHeight = 480;
scaleRatio = scaledHeight / height;
scaledWidth = width * scaleRatio;
} else {
scaledWidth = 640;
scaleRatio = scaledWidth / width;
scaledHeight = height * scaleRatio;
}
canvas['width'] = scaledWidth;
canvas['height'] = scaledHeight;
ctx['drawImage'](image, 0, 0, width, height, 0, 0, scaledWidth, scaledHeight);
/* Rotate Image */
orientation = 8; //manual orientation -> on the site we use loadImage to get the orientation
if(orientation != 1){
switch(orientation){
case 8:
case 6:
canvas.width = scaledHeight;
canvas.height = scaledWidth;
break;
}
var halfScaledWidth = scaledWidth/2;
var halfScaledheight = scaledHeight/2;
ctx.save(); //<- SAVE
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.translate(halfScaledWidth,halfScaledheight);
switch(orientation){
case 8: //rotate left
ctx.scale(scaleRatio,scaleRatio);
ctx.rotate(-90*Math.PI/180);
ctx.translate(-1380,-1055); // <-Manuial numbers
break;
case 3: //Flip upside down
ctx.scale(scaleRatio,scaleRatio);
ctx.rotate(180*Math.PI/180);
ctx.translate(-925,-595); //<-Manuial numbers
break;
case 6: //rotate right
ctx.scale(scaleRatio,scaleRatio);
ctx.rotate(90*Math.PI/180);
ctx.translate(-462,-130); //<-Manuial numbers
break;
}
//re-translate and draw image
//ctx.translate(-halfScaledWidth,-halfScaledheight);
ctx.drawImage(image,-halfScaledWidth, -halfScaledheight);
ctx.restore(); //<- RESTORE
}
/* Rotate Image */
}
I have orientation set manually so I can see how it looks in each position Im worried about. If its a portrait orientation I flip the canvas.
I've tried save() and restore(). I've tried translate(x,y) then translate(-x,-y).. My guess is that because of the scale the grid is off and x and y need to be multiplied. I tried doing that against the scaleRatio and still didn't work.
As you can see I manually set the translate back but that only works with the image size I am working with, so not a good solution at all!
Here is the code:
JSFiddle If I do a normal rotate right it all works.
Thanks!
Transformations
For the easy answer if you are not interested in the how skip to the bottom where you will find an alternative approch to your problem. It is all commented. I have made a bit of a guess as to what you wanted.
If you are interested in what I consider a simpler way to use the 2D transformation functions read the rest.
Matrix Math
When you use translate, scale, and rotate via the canvas 2D API what you are doing is multiplying the existing matrix with one created with each function.
Basically when you do
ctx.rotate(Math.PI); // rotate 180 deg
the API creates a new rotation matrix and multiplies the existing matrix with it.
Unlike normal math multiplication matrix multiplication will change the result depending on what order you multiply. In normal math multiplication A * B = B * A but this does not hold true for matrices mA * mB != mB * mA (Note the not equals)
This becomes more problematic when you need to apply several different transformations.
ctx.scale(2,2);
ctx.rotate(Math.PI/2);
ctx.translate(100,100);
Does not give the same result as
ctx.scale(2,2);
ctx.translate(100,100);
ctx.rotate(Math.PI/2);
The order that you need to apply the tranforms depends on what you are trying to achieve. Using the API this way is very handy for complex linked animations. Unfortunately it is also a source of endless frustration if you are not aware of matrix math. It also forces many to use the save and restore functions to restore the default transformation, that in some situations can be very costly in GPU performance.
setTransform()
We are in luck though as the 2D API also has the function ctx.setTransform(a, b, c, d, e, f) which is all you really should ever need. This function replaces the existing transform with the one supplied. Most of the documentation is rather vague as to the meaning of the a,b,c,d,e,f but contained in them is the rotation, scale, and translation.
One handy use of the function is to set the default transform rather than use save and restore.
I see this type of thing a lot. (The Example 1, referenced further down)
// transform for image 1
ctx.save(); // save state
ctx.scale(2,2);
ctx.rotate(Math.PI/2);
ctx.translate(100,100);
// draw the image
ctx.drawImage(img1, -img1.width / 2, -img1.height / 2);
ctx.restore(); // restore saved state
// transform for image 2
ctx.save(); // save state agian
ctx.scale(1,1);
ctx.rotate(Math.PI);
ctx.translate(100,100);
// draw the image
ctx.drawImage(img2, -img2.width / 2, -img2.height / 2);
ctx.restore(); // restore saved state
An easier way is to just drop the save and restores and reset the transform manually by setting it to the Identity matrix
ctx.scale(2,2);
ctx.rotate(Math.PI/2);
ctx.translate(100,100);
// draw the image
ctx.drawImage(img1, -img1.width / 2, -img1.height / 2);
ctx.setTransform(1,0,0,1,0,0); // restore default transform
// transform for image 2
ctx.scale(1,1);
ctx.rotate(Math.PI);
ctx.translate(100,100);
// draw the image
ctx.drawImage(img2, -img2.width / 2, -img2.height / 2);
ctx.setTransform(1,0,0,1,0,0); // restore default transform
Now then I am sure you are still wondering what are these numbers being passed to setTransform and what do they mean?
The easiest way to remember them is as 2 vectors and 1 coordinate. The two vectors describe the direction and scale of a single pixel, the coordinate is simply the x,y pixel location of the origin (the location that drawing at 0,0 will be on the canvas).
A Pixel and its axis
Imagine a single pixel, this is the abstract transformed pixel that can be scaled and rotated by the current transformation. It has two axis, X and Y. To describe each axis we need two numbers ( a vector) these describes the screen (untransformed) direction and scale of the top and left side of the pixel. So for a normal pixel that matches the screen pixels the X axis is across the top from left to right and is one pixel long. The vector is (1,0) one pixel across, no pixels down. For the Y axis that goes down the screen the vector is (0,1) no pixels across, one pixel down. The origin is the top right screen pixel which is at coordinate (0,0).
Thus we get the Identity Matrix, the default matrix for the 2D API (and many other APIs) The X axis (1,0), Y axis (0,1) and the origin (0,0) which match the six arguments for setTransform(1,0,0,1,0,0).
Now say we want to scale the pixel up. All we do is increase the size of the X and Y Axis setTransform(2,0,0,2,0,0) is the same as scale(2,2) (from the default transform) Our pixel's top is now two pixels long across the top and two pixels long down the left side. To scale down setTransform(0.5,0,0,0.5,0,0) our pixel is now half a pixel across and down.
These two axis vectors (a,b) & (c,d) can point in any direction, are completely independent of each other , they don't have to be at 90 deg to each other so can skew the pixel, nor do they require that they be the same length so you can change the pixel aspect. The origin is also independent and is just the canvas absolute coordinates in pixels of the origin and can be set to anywhere on or off the canvas.
Now say we want to rotate the transform 90Deg clockwise, scale up both axes by 2 and position the origin at the center of the canvas. We want the X axis (top) of our pixel to be 2 pixels long and pointing down the screen. The vector is (0,2) 0 across and two down. We want the left side of our pixel to 2 long and point to the left of the screen (-2,0) Negative two across and none down. And the origin at the center is (canvas.width / 2, canvas.height / 2) to get the final matrix that is setTransform(0,2,-2,0,canvas.width / 2, canvas.height / 2)
Rotate the other way is setTransform(0,-2,2,0,canvas.width / 2, canvas.height / 2)
Easy Rotate 90deg
You may notice that rotating 90 degrees is just swapping the vectors and changing a sign.
The vector (x,y) rotated 90 degrees clockwise is (-y,x).
The vector (x,y) rotated 90 degrees anti-clockwise is (y,-x).
Swap the x, and y and negate the y for clockwise or negate the x for the anticlockwise rotation.
For 180 it is starting at 0 deg vector (1,0)
// input vector
var x = 1;
var y = 0;
// rotated vector
var rx90 = -y; // swap y to x and make it negative
var ry90 = x; // x to y as is
// rotate again same thing
var rx180 = -ry90;
var rx180 = rx90;
// Now for 270
var rx270 = -ry180; // swap y to x and make it negative
var rx270 = rx180;
Or all in terms of just x and y
0 deg (x,y)
90deg (-y,x)
180deg (-x,-y)
270deg (y,-x)
and back to 360 (x,y).
This is a very handy attribute of a vector that we can exploit to simplify the creation of our transformation matrix. In most situations we do not want to skew our image thus we know that the Y axis is always 90Deg clockwise from the x axis. Now we only need to describe the x axis and by applying the 90deg rotation to that vector we have the y axis.
So the vars x and y are the scale and direction of the top of our pixel (x axis), ox, oy are the location of the origin on the canvas (translation) .
var x = 1; // one pixel across
var y = 0; // none down
var ox = canvas.width / 2; // center of canvas
var oy = canvas.height / 2;
Now to create the transform is
ctx.setTransform(x, y, -y, x, ox, oy);
Note that the y axis is at 90 degs to the x axis.
Trig and the Unit vector
All well and easy when the axis are aligned to the top and sides, how do you get the vector for a axis at an arbitrary angle such as is supplied by the argument for ctx.rotate(angle) For that we need a tiny bit of trig. The Math function Math.cos(angle) returns the x component of the angle, angle and Math.sin(angle) gives us the Y component. For zero deg cos(0) = 1 and sin(0) = 0 for 90 deg (Math.PI/2 radians) cos(PI/2) = 0 and sin(PI/2) = 1.
The beauty of using sin and cos is that the two numbers that we get for our vector always give us a vector that is 1 unit (pixel) long (this is called a normalised vector or a unit vector) thus cos(a)2 + sin(a)2 = 1
Why does this matter? because it makes scaling very easy. Assuming that we always keep the aspect square we only need one number for the scale. To scale a vector you simply multiply it by the scale
var scale = 2; // scale of 2
var ang = Math.random() * 100; // any random angle
var x = Math.cos(ang); // get the x axis as a unit vector.
var y = Math.sin(ang);
// scale the axis
x *= scale;
y *= scale;
the vector x,y is now two units long.
Better than using save, restore, rotate, scale, translate... :(
Now put it all together to create a matrix with an arbitrary rotation, scale and translation (origin)
// ctx is the 2D context,
// originX, and originY is the origin, same as ctx.translate(originX,originY)
// rotation is the angle in radians same as ctx.rotate(rotation)
// scale is the scale of x and y axis same as ctx.scale(scale,scale)
function createTransform(ctx,originX,originY,rotation,scale){
var x, y;
x = Math.cos(rotation) * scale;
y = Math.sin(rotation) * scale;
ctx.setTransform(x, y, -y, x, originX, originY);
}
Now to apply that to the example (1) given above
// dont need ctx.save(); // save state
// dont need ctx.scale(2,2);
// dont need ctx.rotate(Math.PI/2);
// dont need ctx.translate(100,100);
createMatrix(ctx, 100, 100, Math.PI/2, 2)
// draw the image normally
ctx.drawImage(img1, -img1.width / 2, -img1.height / 2);
// dont need ctx.restore(); // restore saved state
// transform for image 2
// dont need ctx.save(); // save state agian
// dont need ctx.scale(1,1);
// dont need ctx.rotate(Math.PI);
// dont need ctx.translate(100,100);
// we don't have to reset the default transform because
// ctx.setTransform completely replaces the current transform
createMatrix(ctx, 100, 100, Math.PI/2, 2)
// draw the image
ctx.drawImage(img2, -img2.width / 2, -img2.height / 2);
// dont need ctx.restore(); // restore saved state
And that is how you use setTransform to simplify transforming the canvas, rather than guessing, trial and error, scale, rotates, and translates back and forth within a sea of save and restores.
Using that to simplify your code
The answer
And now to your question
I am not entirely sure what you are after, I presume you a dont mind scaling the canvas to accommodate the image, that the image is always in the center and that the aspect remains the same. As the rotations are aligned to the screen I will set the transforms manualy
// this is in set up code
const MAX_SIZE_WIDTH = 640;
const MAX_SIZE_HEIGHT = 480;
orientationData = [];
orientationData[6] = Math.PI/2; // xAxis pointing down
orientationData[8] = -Math.PI/2; // xAxis pointing up
orientationData[3] = Math.PI; //xAxis pointing to left
// in your code
var orient,w,h,iw,ih,scale,ax,ay; // w and h are canvas size
// assume image is the loaded image
iw = image.width; // get the image width and height so I dont have to type as much.
ih = image.height;
if(orientation != 1){
var orient = orientationData[orientation];
if(orient === undefined){
return; // bad data so return
}
// get scale and resize canvas to suit
// is the image on the side
if(orientation === 6 || orientation === 8){
// on side so swap width and height
// get the height and width scales for the image, dont scale
// if the image is smaller than the dimension
scale = Math.min(1,
MAX_SIZE_WIDTH / ih,
MAX_SIZE_HEIGHT / iw
);
w = canvas.width = scale * ih;
h = canvas.height = scale * iw;
}else{
// for normal orientation
scale = Math.min(1,
MAX_SIZE_WIDTH / iw,
MAX_SIZE_HEIGHT / ih
);
h = canvas.height = scale * ih;
w = canvas.width = scale * iw;
}
// Do you really need to clear the canvas if the image is filling it??
// ensure that the default transform is set
ctx.setTransform(1, 0, 0, 1, 0, 0);
// clear the canvas
ctx.clearRect(0, 0, w, h);
// now create the transformation matrix to
// position the image in the center of the screen
// first get the xAxis and scale it
ax = Math.cos(orient) * scale;
ay = Math.sin(orient) * scale;
// now set the transform, the origin is always the canvas center
// and the Y axis is 90 deg clockwise from the xAxis and same scale
ctx.setTransform(ax, ay, -ay, ax, w / 2, h / 2);
// now draw the image offset by half its width and height
// so that it is centered on the canvas
ctx.drawImage(image,-iw / 2, -ih / 2);
// restore the default transform
ctx.setTransform(1, 0, 0, 1, 0, 0);
} // done.
Try this one https://jsfiddle.net/uLdf4paL/2/. Your code is correct, but you have to change orientation variable when you try to rotate and scale image (if it is what you wanna get).
My solution for this was using two canvases
one for the rotation and scaling (around the center point of the image)
then using a new canvas for translating and drawing the prev canvas on it. it worked perfectly for me:
const root = document.querySelector("#root");
const img = document.querySelector('#img');
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext("2d");
const tempCanv = document.createElement('canvas')
const tempCtx = tempCanv.getContext('2d');
canvas.height = img.height
canvas.width = img.width
tempCanv.width = img.width
tempCanv.height = img.height
const position = [0.25, 0.25];
const rotation = -40;
const scale = [37.222, 37.222];
const x = img.width / 2;
const y = img.height / 2;
// Set the origin to the center
tempCtx.translate(x, y);
// now rotate and scale around the center
tempCtx.rotate(Math.PI / 180 * rotation);
tempCtx.scale(scale[0] / 100, scale[1] / 100);
// Translate back to origin
tempCtx.translate(-x, -y);
// Draw temp
tempCtx.drawImage(img,0,0);
// Translate the final canvas
ctx.translate(position[0] * canvas.width, position[1] * canvas.height);
ctx.drawImage(tempCanv, 0, 0);
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#root{
display: flex;
gap: 2rem;
padding: 1rem;
overflow:visible
position relative;
}
.image-container{
}
#img{
transform: rotate(-40deg) scale(0.37222, 0.37222);
top: 25%;
left: 25%;
position: relative;
}
</style>
</head>
<body>
<div id="root">
<div class="image-container">
<img id="img" src="https://media.istockphoto.com/photos/cat-world-picture-id1311993425?b=1&k=20&m=1311993425&s=170667a&w=0&h=vFvrS09vrSeKH_u2XZVmjuKeFiIEjTkwr9KQdyOfqvg=" alt="red">
</div>
<canvas id="canvas"></canvas>
</div>
</body>
</html>

Understanding HTML5 Canvas

I am trying to get to grips and understand how to use and create colliding balls with HTML5 canvas,examples I have looked at have a lot of JavaScript, but I need to break it down into much smaller chunks to get a better understanding of what's going on.
In my example what I understand so far is that I am redrawing the circles every 40 milliseconds onto the canvas, and calling the animate function each time. Every time this is called the position of the circle changes as I am changing it with
circles[0].x+=1;
circles[0].y+=-1.5;
So my circle objects are in an array, and there are 2 things I would like to achieve:
1) not to let the balls escape the canvas area
2) if the balls collide then bounce off each other and reverse in direction.
What I want to tackle first though is not letting the balls escape the canvas and how I would go about working that out.
I have access to the window.width and window.height, so it's a case of understanding how to get the position of each ball in the array, and ensure that it does not cross those boundaries.
I don't want to just have it work, would much prefer to understand what is happening.
This will check collisions on the bounds of the canvas. I updated your objects to store vx and vy (velocity) and the draw() function to move based on these properties. I added checkBounds() which reverses the velocity when the circle goes outside the bounds.
EDIT: modified so that it takes into account the radius of the circles too.
Doing a collision detect between the circles could follow a similar pattern
http://jsfiddle.net/3tfUN/5/
var canvas = document.getElementById('ball-canvas');
var context = canvas.getContext('2d')
var radius = 50;
var strokewidth = 2;
var strokestyle = '#666';
var frameCount = 0;
var w = canvas.width;
var h = canvas.height;
// Circle Objects
var yellowCircle = {
x: 50,
y: h / 2,
radius: radius,
color: 'yellow',
vx: 1,
vy: 1.5
}
var redCircle = {
x: 450,
y: h / 2,
radius: radius,
color: 'red',
vx: 1,
vy: -1
}
var blueCircle = {
x: 850,
y: h / 2,
radius: radius,
color: 'blue',
vx: -1,
vy: -1.5
}
// Create empty array and then push cirlce objects into array
var circles = [];
circles.push(yellowCircle, blueCircle, redCircle);
function checkBounds() {
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
if (c.x > w - c.radius || c.x < c.radius) {
c.vx = -c.vx;
}
if (c.y > h - c.radius || c.y < c.radius) {
c.vy = -c.vy;
}
}
}
// Clear last circle and draw again
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the circle from the from page
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
context.beginPath();
context.fillStyle = c.color // Set the color of the circle using key:valuecontext.fill();
context.lineWidth = strokewidth;
context.strokeStyle = strokestyle;
context.stroke();
context.arc(c.x, c.y, c.radius, 0, Math.PI * 2); // X-axis Position, y-axis Position, radius, % of fill, ?
context.closePath();
context.fill();
}
}
function animate() {
for (i = 0; i <= 2; i++) {
circles[i].x += circles[i].vx;
circles[i].y += circles[i].vy;
}
checkBounds();
draw();
}
var canvas = document.getElementById('ball-canvas');
var context = canvas.getContext('2d')
var radius = 50;
setInterval(animate, 40);
circles[0].x+=1;
circles[0].y+=-1.5;
That's pretty tough to maintain. Instead, I'd suggest you have properties for X and Y speeds (I used moveX and moveY in the example).
Next, you need to check whether the position of the ball + the radius compensation is touching the canvas edges, and if so, reverse the speed value. So, for example, the X speed of the ball is 4 and now it hits the left or the right canvas egde, the X speed now becomes -4.
This is it, in a nutshell:
var c = circles[i];
// check rebounds
if (c.x - c.radius <= 0 || c.x + c.radius >= canvas.width)
c.moveX = -c.moveX; // flip the horizontal speed component
if (c.y - c.radius <= 0 || c.y + c.radius >= canvas.height)
c.moveY = -c.moveY; // flip the vertical speed component
// Yellow Circle
c.x += c.moveX; // here we don't have to worry
c.y += c.moveY; // about directions anymore
See my example here: http://jsfiddle.net/3tfUN/8/
The same principle applies for collisions between balls. I'm assuming you want to do simple collisions without angle changes.
But if you wish to simulate real ball collisions, that would require some more serious trigonometry to calculate when exactly the pixel-perfect collision happens, and to calculate the new X and Y speed components.
UPDATE
An example featuring slightly improved collision detection and speed transfer between balls: http://jsfiddle.net/3tfUN/12/
The canvas is just a "canvas" where you draw the circles. What you need to accomplish what you want is to model a "world" where the circles are object with width and height dimensions and their current position, and where the bounds are well defined. Once you have the width and height of each circle and their position, you can calculate where they are in respect to the bounds you set and see if you need to change direction or keep going.
Collisions stem from the same principle but are somewhat harder to model if you want them to be "realistic" (in the bounds problem you are only interested in the width and height of the circles because the bounding area is box shaped and the circle will always collide in the furthest point from its center, while when two circles collide you should take into account the radius of each circle instead of the "bounding box" around them.
I don't have time right now to show you this concepts with examples, but hopefully I sent you in the right track :).

Categories