简体   繁体   中英

How do I rotate a html canvas shape by only using a transform?

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 ax 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM