简体   繁体   English

如何像这样倾斜图像

[英]how to skew image like this

I want to skew an image like this what params I need to set for context.setTransform?我想像这样倾斜图像我需要为 context.setTransform 设置什么参数?在此处输入图像描述

You cannot achieve this with a single 2D transform.您无法通过单个 2D 变换来实现这一点。

A 2D transform allows you to skew the image "upwards" or "downwards" by passing the tangent of the skew angle in the second argument to setTransform() , but you want to perform both in a symmetrical manner (resulting in a "nearwards" and/or "farwards" deformation). 2D 变换允许您通过将第二个参数中的倾斜角的切线传递给setTransform()来“向上”或“向下”倾斜图像,但您希望以对称的方式执行这两种操作(导致“接近”和/或“向前”变形)。 You need a 3D transform to do that.你需要一个 3D 变换来做到这一点。

However, you can emulate the same result by slicing the image into several horizontal "bands" and applying a different transform when rendering each band.但是,您可以通过将图像切片成多个水平“波段”并在渲染每个波段时应用不同的变换来模拟相同的结果。 Bands further from the half of the image will be applied stronger skew angles.离图像一半更远的波段将应用更强的倾斜角度。 Something like:就像是:

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

Note the bands are two pixels high instead of one to avoid a moire effect.请注意,波段是两个像素而不是一个像素高,以避免出现莫尔效应。

You can see the results in this fiddle .你可以在这个 fiddle 中看到结果。

Here's a function I wrote when I was playing with rendering pseudo-3d perspective with JS.这是我在玩 JS 渲染伪 3d 透视图时编写的一个函数。

Unlike the stripe-based transformation functions (which, admittedly, are perfectly good enough for most standard use cases) this function uses a matrix of 4 corners to define a custom quadrilateral that the original rectangle should be transformed to.与基于条带的转换函数(不可否认,对于大多数标准用例来说已经足够好)不同,该函数使用 4 个角的矩阵来定义原始矩形应转换为的自定义四边形。 This adds some flexibility and can be used to render custom trapezoids for both "painting-on-the-wall" horizontal perspective and "carpet-on-the-floor" vertical perspective (as well as asymetrical quadrilaterals for even more 3d-like feel).这增加了一些灵活性,可用于为“墙上绘画”水平透视和“地板上的地毯”垂直透视(以及不对称的四边形,以获得更像 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);
}

Here's how to call it:调用方法如下:

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";
}

Despite all the per-pixel calculations it is actually quite efficient, and it gets the job done:尽管进行了每像素计算,但它实际上非常有效,并且可以完成工作:

transformed image变换图像

...but there may be more elegant ways to do it. ...但可能有更优雅的方法来做到这一点。

There is a method of transforming a rectangle to a trapezium see this stack overflow answer .有一种将矩形转换为梯形的方法,请参阅此堆栈溢出答案 However you would need to use this on each pixel.但是,您需要在每个像素上使用它。

You could also slice the image into vertical strips 1 pixel wide and then stretch each strip from its centre.您还可以将图像切成 1 像素宽的垂直条带,然后从其中心拉伸每个条带。

Suppose this leads to w strips and you want the left hand edge of the trapezium to be 80% of the right hand edge then假设这导致 w 条,并且您希望梯形的左手边是右手边的 80%,然后

for strip n the stretch whould be 1+n/(4w)对于条带 n,拉伸应该是 1+n/(4w)

That's still only for the future, but it's so cool that I can't refrain from adding it already.这仍然只是为了未来,但它太酷了,我已经忍不住要添加它了。

Chrome team is working on adding non-affine transforms to the 2D API . Chrome 团队正致力于向 2D API 添加非仿射变换
This would add a few methods to the 2D API such as perspective() , rotate3d() , rotateAxis() , and extend others to add a z-axis, as well as improve setTransform() and transform() into finally accepting a 3D DOMMatrix.这将向 2D API 添加一些方法,例如perspective()rotate3d()rotateAxis()并扩展其他方法以添加 z 轴,以及改进setTransform()transform()以最终接受 3D DOM矩阵。

This is still very experimental and may still change, but you can already try this in Chrome Canary with chrome://flags/#enable-experimental-web-platform-features switched on.这仍然是非常实验性的并且可能仍然会发生变化,但是您已经可以在 Chrome Canary 中尝试使用chrome://flags/#enable-experimental-web-platform-features打开。

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

Which in current Chrome Canary renders as在当前的 Chrome Canary 中呈现为

在此处输入图片说明

The question has been answered a long time ago but I would like to add one idea to @Frédéric.很久以前就已经回答了这个问题,但我想向@Frédéric 添加一个想法。

If we also need a perspective illusion, we just have to multiply it's width while drawing by the skew() angle cosinus, since we have the angle and we have the hypotenuse.如果我们还需要透视错觉,我们只需在绘制时将其宽度乘以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>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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