如何扭曲图像像这样

20

我想像这样扭曲一张图片,那么我需要为 context.setTransform 设置哪些参数呢?

在此输入图片描述

1
已经有答案了 https://dev59.com/F2435IYBdhLWcg3wpx2x 它是相反的,但是是一样的 http://jsfiddle.net/rudiedirkx/349x9/ - GillesC
3
@gillesc,这不在Canvas里。 - katspaugh
好的,没注意到“上下文”的问题。微软有一篇文章应该可以解释清楚 http://msdn.microsoft.com/en-us/hh969244 (与下面的答案使用相同的方法)。 - GillesC
5个回答

24

单纯的二维变换无法实现此效果。

使用二维变换可以通过将倾斜角度的正切值传递给setTransform()的第二个参数来使图像向上或向下倾斜,但是您希望以对称的方式执行这两种变形(导致“靠近”和/或“远离”变形)。您需要使用三维变换才能实现。

但是,您可以通过将图像切成若干水平的“条带”,并在渲染每个条带时应用不同的变换来模拟相同的结果。越靠近图像中心的条带将会应用更强的倾斜角度。例如:

var width = image.width,
    height = image.height,
    context = $("canvas")[0].getContext("2d");
for (var i = 0; i <= height / 2; ++i) {
    context.setTransform(1, -0.4 * i / height, 0, 1, 0, 60);
    context.drawImage(image,
        0, height / 2 - i, width, 2,
        0, height / 2 - i, width, 2);
    context.setTransform(1, 0.4 * i / height, 0, 1, 0, 60);
    context.drawImage(image,
        0, height / 2 + i, width, 2,
        0, height / 2 + i, width, 2);
}
注意,这些条纹的高度是两个像素而不是一个像素,以避免出现莫尔纹效应。
您可以在此代码片段中查看结果。

嗨,Frédéric,你能解释一下如何应用变换来获得问题中图像的镜像,即左侧高度更大,右侧高度更低。谢谢。 - Chaitanya Munipalle
2
请注意,如果您使用垂直方向上为1像素宽的带状图案,也可以避免出现moire效应。 - Eric
@Eric,你说得完全正确,因为我一直想着在水平轴周围迭代,所以没有看到这一点。在垂直轴周围迭代确实可以更有效率。感谢你的评论 :) - Frédéric Hamidi

5
这里是一个我写的函数,当我使用JS渲染伪3D透视时编写的。
与基于条纹的转换函数不同(诚然,对于大多数标准用例而言,它们已经足够好了),该函数使用一个四个角落的矩阵来定义原始矩形应该转换到的自定义梯形。这增加了一些灵活性,并可用于渲染"贴在墙上"的水平透视和"地毯上"的垂直透视(以及不对称四边形,以获得更多的3D感觉)。
function drawImageInPerspective(
        srcImg,
        targetCanvas,
        //Define where on the canvas the image should be drawn:  
        //coordinates of the 4 corners of the quadrilateral that the original rectangular image will be transformed onto:
        topLeftX, topLeftY,
        bottomLeftX, bottomLeftY,
        topRightX, topRightY,
        bottomRightX, bottomRightY,
        //optionally flip the original image horizontally or vertically *before* transforming the original rectangular image to the custom quadrilateral:
        flipHorizontally,
        flipVertically
    ) {

    var srcWidth=srcImg.naturalWidth;
    var srcHeight=srcImg.naturalHeight;

    var targetMarginX=Math.min(topLeftX, bottomLeftX, topRightX, bottomRightX);
    var targetMarginY=Math.min(topLeftY, bottomLeftY, topRightY, bottomRightY);

    var targetTopWidth=(topRightX-topLeftX);
    var targetTopOffset=topLeftX-targetMarginX;
    var targetBottomWidth=(bottomRightX-bottomLeftX);
    var targetBottomOffset=bottomLeftX-targetMarginX;

    var targetLeftHeight=(bottomLeftY-topLeftY);
    var targetLeftOffset=topLeftY-targetMarginY;
    var targetRightHeight=(bottomRightY-topRightY);
    var targetRightOffset=topRightY-targetMarginY;

    var tmpWidth=Math.max(targetTopWidth+targetTopOffset, targetBottomWidth+targetBottomOffset);
    var tmpHeight=Math.max(targetLeftHeight+targetLeftOffset, targetRightHeight+targetRightOffset);

    var tmpCanvas=document.createElement('canvas');
    tmpCanvas.width=tmpWidth;
    tmpCanvas.height=tmpHeight;
    var tmpContext = tmpCanvas.getContext('2d');

    tmpContext.translate(
        flipHorizontally ? tmpWidth : 0,
        flipVertically ? tmpHeight : 0
    );
     tmpContext.scale(
        (flipHorizontally ? -1 : 1)*(tmpWidth/srcWidth),
        (flipVertically? -1 : 1)*(tmpHeight/srcHeight)
    );

    tmpContext.drawImage(srcImg, 0, 0);  

    var tmpMap=tmpContext.getImageData(0,0,tmpWidth,tmpHeight);
    var tmpImgData=tmpMap.data;

    var targetContext=targetCanvas.getContext('2d');
    var targetMap = targetContext.getImageData(targetMarginX,targetMarginY,tmpWidth,tmpHeight);
    var targetImgData = targetMap.data;

    var tmpX,tmpY,
        targetX,targetY,
        tmpPoint, targetPoint;

    for(var tmpY = 0; tmpY < tmpHeight; tmpY++) {
        for(var tmpX = 0;  tmpX < tmpWidth; tmpX++) {

            //Index in the context.getImageData(...).data array.
            //This array is a one-dimensional array which reserves 4 values for each pixel [red,green,blue,alpha) stores all points in a single dimension, pixel after pixel, row after row:
            tmpPoint=(tmpY*tmpWidth+tmpX)*4;

            //calculate the coordinates of the point on the skewed image.
            //
            //Take the X coordinate of the original point and translate it onto target (skewed) coordinate:
            //Calculate how big a % of srcWidth (unskewed x) tmpX is, then get the average this % of (skewed) targetTopWidth and targetBottomWidth, weighting the two using the point's Y coordinate, and taking the skewed offset into consideration (how far topLeft and bottomLeft of the transformation trapezium are from 0).   
            targetX=(
                       targetTopOffset
                       +targetTopWidth * tmpX/tmpWidth
                   )
                   * (1- tmpY/tmpHeight)
                   + (
                       targetBottomOffset
                       +targetBottomWidth * tmpX/tmpWidth
                   )
                   * (tmpY/tmpHeight)
            ;
            targetX=Math.round(targetX);

            //Take the Y coordinate of the original point and translate it onto target (skewed) coordinate:
            targetY=(
                       targetLeftOffset
                       +targetLeftHeight * tmpY/tmpHeight
                   )
                   * (1-tmpX/tmpWidth)
                   + (
                       targetRightOffset
                       +targetRightHeight * tmpY/tmpHeight
                   )
                   * (tmpX/tmpWidth)
            ;
            targetY=Math.round(targetY);

            targetPoint=(targetY*tmpWidth+targetX)*4;

            targetImgData[targetPoint]=tmpImgData[tmpPoint];  //red
            targetImgData[targetPoint+1]=tmpImgData[tmpPoint+1]; //green
            targetImgData[targetPoint+2]=tmpImgData[tmpPoint+2]; //blue
            targetImgData[targetPoint+3]=tmpImgData[tmpPoint+3]; //alpha
        }
    }

    targetContext.putImageData(targetMap,targetMarginX,targetMarginY);
}

以下是如何调用它:

function onLoad() {
    var canvas = document.createElement("canvas");
    canvas.id = 'canvas';
    canvas.width=800;
    canvas.height=800;
    document.body.appendChild(canvas);

    var img = new Image();
    img.onload = function(){ 
        //draw the original rectangular image as a 300x300 quadrilateral with its bottom-left and top-right corners skewed a bit:
        drawImageInPerspective(
         img, canvas,
         //coordinates of the 4 corners of the quadrilateral that the original rectangular image will be transformed onto:
         0, 0, //top left corner: x, y
         50, 300, //bottom left corner: x, y - position it 50px more to the right than the top right corner
         300, 50, //top right corner: x, y - position it 50px below the top left corner 
         300, 300, //bottom right corner: x,y
         false, //don't flip the original image horizontally
         false //don't flip the original image vertically
        );
    }
    img.src="img/rectangle.png";
}

尽管进行了每个像素的计算,但实际上效率相当高,并且可以完成工作:

变换后的图像

...但也可能有更优雅的方法来完成它。


2
问题早已得到解答,但我想向@Frédéric的观点添加一个想法。
如果我们还需要一种透视错觉,只需将其宽度在绘制时乘以skew()角度的余弦值,因为我们已经知道角度和斜边长度。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var alpha = 0;
var angle;
var opening = false;

window.addEventListener('mousedown', openDoor);

function openDoor() {
  opening = true;
}

var image = new Image();
var image2 = new Image();
image2.src = "https://i.ibb.co/7b19d5m/road-jpeg.png";

image.onload = function () {
  var width = image.width;
  var height = image.height;

  function animate() {
    if (opening && alpha < 60) alpha += 1;
    ctx.clearRect(0, 0, 400, 400);
    ctx.fillStyle = "black";
    ctx.fillRect(0,0,400,400);
    
    ctx.drawImage(image2,20, 60);
    angle = (alpha * Math.PI) / 180;

    ctx.save();
    for (var i = 0; i <= height / 2; ++i) {
      ctx.setTransform(1, (angle * i) / height, 0, 1, 20, 60);
      ctx.drawImage(
        image,
        0,
        height / 2 - i,
        width,
        2,
        0,
        height / 2 - i,        
        width * Math.cos(angle),
        2
      );
      ctx.setTransform(1, (-angle * i) / height, 0, 1, 20, 60);
      ctx.drawImage(
        image,
        0,
        height / 2 + i,
        width,
        2,
        0,
        height / 2 + i,
        width * Math.cos(angle),
        2
      );
    }

    ctx.restore();

    requestAnimationFrame(animate);
  }

  animate();
};
image.src = 'https://i.ibb.co/thZfnYh/door-jpg.png';
<canvas id="canvas" width="200" height="300"> </canvas>

<h3>Click on the door to open it</h3>


1

这只是未来的事情,但它非常酷,我忍不住要添加它。

Chrome团队正在努力将非仿射变换添加到2D API
这将向2D API添加一些方法,例如perspective()rotate3d()rotateAxis(),并扩展其他方法以添加z轴,同时改进setTransform()transform()以最终接受3D DOMMatrix。

这仍然是非常实验性的,可能仍会发生变化,但您已经可以在启用chrome://flags/#enable-experimental-web-platform-features的Chrome Canary中尝试它。

if( CanvasRenderingContext2D.prototype.rotate3d ) {
  onload = (evt) => {
    const img = document.getElementById("img");
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    ctx.translate(0, canvas.height/2);
    ctx.perspective(705); // yeah, magic numbers...
    ctx.rotate3d(0, (Math.PI/180) * 321, 0); // and more
    ctx.translate(0, -canvas.height/2);
    const ratio = img.naturalHeight / canvas.height;
    ctx.drawImage(img, 0, canvas.height/2 - img.naturalHeight/2);
  };
}else {
  console.error( "Your browser doesn't support affine transforms yet" );
}
body { margin: 0 }
canvas, img {
  max-height: 100vh; 
}
<canvas id="canvas" width="330" height="426"></canvas>
<img id="img" src="https://upload.wikimedia.org/wikipedia/en/f/f8/Only_By_the_Night_%28Kings_of_Leon_album_-_cover_art%29.jpg">

在当前的Chrome Canary中呈现为

enter image description here


0

有一种方法可以将矩形转换为梯形,参见这个Stack Overflow答案。但是你需要对每个像素使用此方法。

你还可以将图像切成垂直条带,每个条带宽度为1像素,然后从中心拉伸每个条带。

假设这导致w个条带,并且您希望梯形的左边缘占右边缘的80%,则

对于条带n,拉伸应为1 + n /(4w)


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