HTML5画布缩放和旋转后如何回到原始位置

3
我正在尝试使用canvas进行一些操作。首先,用户上传图像,如果图像过大,我需要将其缩小。该部分已经正常工作。最近,我们遇到了iPhone用户上传图像的问题。这些图像存在方向问题。我已经找出如何提取方向,但是当我在canvas中操作图像时会发生什么就成了我的问题。
这是我需要做的事情:获取图像,translate(),scale(),rotate(),translate() <- 将其恢复到原始位置,drawImage()。
当我这样做时,图像的某些部分会消失在深渊中。
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 */
}

我手动设置了方向,这样我就可以看到每个位置的外观,如果是纵向方向,我会翻转画布。
我尝试过save()和restore()。我尝试过translate(x,y)然后translate(-x,-y)。我的猜测是因为比例尺的缘故,网格偏离了,需要将x和y乘以比例尺。我尝试过使用scaleRatio进行计算,但仍然无效。
正如你所看到的,我手动设置了平移,但这只适用于我正在处理的图像大小,所以这不是一个好的解决方案!
以下是代码: JSFiddle 如果我正常旋转右侧,一切都正常。
谢谢!
3个回答

17

变换

如果您对如何进行变换不感兴趣,可以跳到底部找到解决问题的替代方法。所有内容都已注释说明,我猜测了一下您的需求。

如果您想了解我认为更简单的使用二维变换函数的方法,请继续阅读。

矩阵运算

当您通过画布2D API使用平移、缩放和旋转时,实际上是将现有矩阵与每个函数创建的矩阵相乘。

基本上当你执行

ctx.rotate(Math.PI); // rotate 180 deg

API 创建一个新的旋转矩阵并将其与现有矩阵相乘。
与普通数学乘法不同,矩阵乘法将根据您的乘法顺序更改结果。在普通数学乘法中,A * B = B * A,但是对于矩阵,这种情况不成立mA * mB != mB * mA(注意不等号)。
当您需要应用多个不同的变换时,这会变得更加复杂。
ctx.scale(2,2);
ctx.rotate(Math.PI/2);
ctx.translate(100,100);

不会得到相同的结果

ctx.scale(2,2);
ctx.translate(100,100);
ctx.rotate(Math.PI/2);

你需要应用转换的顺序取决于你想要实现的目标。使用API进行这种方式对于复杂的链接动画非常方便。不幸的是,如果你不了解矩阵数学,它也会成为无尽沮丧的来源。在某些情况下,它还会迫使许多人使用“save”和“restore”函数来恢复默认转换,这可能会对GPU性能造成很高的代价。
我们有福气了,因为2D API还有一个函数“ctx.setTransform(a, b, c, d, e, f)”,这就是你真正需要的一切。此函数将现有的变换替换为提供的变换。大部分文档都对“a,b,c,d,e,f”的含义含糊不清,但其中包含了旋转、缩放和平移。
该函数的一个方便用途是设置默认转换而不是使用save和restore。
我经常看到这种类型的东西。(参见下面进一步引用的示例1)
// 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

一种更简单的方法是只需删除保存和还原,并通过将其设置为恒等矩阵来手动重置变换。
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

现在我相信您仍然想知道传递给setTransform的这些数字是什么意思?
最简单的方法是将它们视为2个向量和1个坐标。这两个向量描述了单个像素的方向和比例,坐标仅仅是原点(绘制在画布上0,0位置的位置)的x,y像素位置。
一个像素有两个轴,X轴和Y轴。为了描述每个轴,我们需要两个数字(一个向量),这些数字描述了像素顶部和左侧的屏幕(未变换)方向和比例。因此,对于与屏幕像素匹配的普通像素,X轴从左到右横跨顶部,并且长度为一个像素。向量是(1,0)一个像素横跨,没有像素下降。对于沿着屏幕向下的Y轴,向量是(0,1)没有像素横跨,向下一个像素。原点是位于坐标(0,0)的屏幕顶部右侧像素。
因此,我们得到了恒等矩阵,这是2D API(以及许多其他API)的默认矩阵。X轴(1,0),Y轴(0,1)和原点(0,0)与setTransform(1,0,0,1,0,0)的六个参数相匹配。
现在假设我们想放大像素。我们所要做的就是增加X和Y轴的大小,setTransform(2,0,0,2,0,0)与scale(2,2)(从默认变换)相同。我们的像素顶部现在横跨两个像素,向左侧延伸两个像素。缩小setTransform(0.5,0,0,0.5,0,0),我们的像素现在横跨半个像素并向下半个像素。
这两个轴向量(a,b)和(c,d)可以指向任何方向,完全独立于彼此,它们不必相互垂直,因此可以扭曲像素,也不需要它们具有相同的长度,因此可以更改像素宽高比。原点也是独立的,只是画布原点的绝对坐标,并且可以设置在画布上或离画布任何位置。
现在假设我们要将变换顺时针旋转90度,同时将两个轴放大2倍,并将原点放置于画布中心位置。我们希望像素的X轴(顶部)长度为2个像素,并指向屏幕下方。该向量是(0,2)即横坐标为0,纵坐标为2。我们希望像素的左侧长度为2,指向屏幕左边(-2,0) 即横坐标为负2,纵坐标为0。而且,将原点放置于画布中心位置是(canvas.width / 2, canvas.height / 2)。最终矩阵为setTransform(0,2,-2,0,canvas.width / 2, canvas.height / 2)
逆时针旋转的方法是setTransform(0,-2,2,0,canvas.width / 2, canvas.height / 2)
容易发现,将向量交换并改变符号就可以将向量顺时针旋转90度。
  • 向量(x,y)顺时针旋转90度后为(-y,x)。
  • 向量(x,y)逆时针旋转90度后为(y,-x)。
对于顺时针旋转,交换x和y并取y的相反数;对于逆时针旋转,交换x和y并取x的相反数。
对于180度旋转,可以从0度开始,使用向量(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;

或者只用x和y来表示

  • 0度 (x,y)
  • 90度 (-y,x)
  • 180度 (-x,-y)
  • 270度 (y,-x)
  • 然后回到360度 (x,y)。

这是向量的一个非常方便的属性,我们可以利用它来简化我们的变换矩阵的创建。在大多数情况下,我们不想扭曲我们的图像,因此我们知道Y轴始终比X轴顺时针旋转90度。现在我们只需要描述X轴,通过将90度旋转应用于该向量,我们就有了Y轴。

因此,变量xy是我们像素顶部(X轴)的比例和方向,oxoy是画布上原点的位置(平移)。

var x = 1; // one pixel across
var y = 0; // none down
var ox = canvas.width / 2; // center of canvas
var oy = canvas.height / 2;

现在创建转换的方法是:
ctx.setTransform(x, y, -y, x, ox, oy);

请注意,Y轴与X轴垂直90度。
正弦和单位向量
当轴线与顶部和侧面对齐时,一切都很好,但如果轴线以任意角度对齐,比如由ctx.rotate(angle)提供的参数,那么我们该如何获得矢量呢?这就需要用到一点三角函数了。Math函数Math.cos(angle)返回角度的X分量,angle和Math.sin(angle)给出Y分量。对于0度,cos(0) = 1sin(0) = 0,对于90度(Math.PI/2弧度),cos(PI/2) = 0sin(PI/2) = 1
使用sin和cos的好处在于,我们得到的两个数字总是给出长度为1个单位(像素)的矢量(这称为标准化矢量或单位向量)。因此,cos(a)2 + sin(a)2 = 1。
这为什么很重要?因为它使缩放变得非常容易。假设我们总是保持长宽比相等,那么我们只需要一个数来进行缩放。要缩放一个向量,只需将其乘以比例即可。
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;

向量 x,y 现在长度为两个单位。

比使用保存、恢复、旋转、缩放、平移等更好 :(

现在将它们组合起来,创建一个具有任意旋转、缩放和平移(原点)的矩阵。

// 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);
}

现在将其应用于上述给出的示例(1)
// 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

这就是如何使用setTransform来简化canvas的转换,而不是猜测、试错、缩放、旋转和来回平移,在一堆保存和还原中。

使用它可以简化您的代码。

答案

现在让我们来看你的问题。

我不完全确定你想要什么,我猜想你不介意缩放画布来适应图像,图像始终位于中心,并且长宽比保持不变。由于旋转与屏幕对齐,因此我将手动设置转换。

    // 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.

您真是个天才!我仔细阅读了解释,现在完全明白为什么我无法理解它了,我不记得所有的三角函数和其他数学了!我确实遇到了ctx.setTransform(a,b,c,d,e,f),但像你说的那样,它模糊不清,并且它没有按照我想象的那样起作用。非常感谢您!这真是个头疼的事情! - Pablo
1
很棒的解释,但在更复杂的问题中会有一些注意事项。在这种情况下,保存/旋转/平移/恢复的成本将是drawImage成本的一小部分,使其成为微优化。避免使用save()和restore()的注意事项是您知道初始变换矩阵是单位矩阵。如果您正在使用辅助函数作为较大绘图操作的一部分,则这是一个巨大的假设。Canvas没有本地getTransform,并且将变换应用于其他变换比从单位矩阵开始更加复杂。 - mcurland
你的回答不仅回应了问题,还涵盖了使用画布进行绘图的全部内容。受益匪浅,非常感谢。我很乐意请你喝杯咖啡。 - asyncwait

1
尝试这个 https://jsfiddle.net/uLdf4paL/2/。你的代码是正确的,但当你尝试旋转和缩放图像时,你需要更改orientation变量(如果这是你想要的)。

抱歉...在这个例子中,方向被设置为触发代码(在我的完整代码中,它是由上传的图像设置的)。问题在于ctx.translate(-1380,-1055); // <-手动数字。我不想要手动数字。如果你注释掉那一行,并在drawImage之前放回这一行//ctx.translate(-halfScaledWidth,-halfScaledheight);,你会看到问题所在。我正在展示如何使用硬编码数字使其看起来正常,但我不想要这样。 - Pablo

0
我的解决方案是使用两个画布,一个用于旋转和缩放(围绕图像的中心点),然后使用一个新画布来平移并在其上绘制前一个画布。这对我完美地起作用了。

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>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接