cómo sesgar una imagen como esta

Quiero sesgar una imagen como esta, ¿qué parámetros necesito configurar para context.setTransform? enter image description here

preguntado el 03 de mayo de 12 a las 08:05

@gillesc, no está en Canvas. -

El buen punto no notó el "contexto". Microsoft tiene una publicación que debería explicarlo. msdn.microsoft.com/en-us/hh969244 (mismo método utilizado como respuesta a continuación) -

4 Respuestas

No puede lograr esto con una sola transformación 2D.

Una transformación 2D le permite sesgar la imagen "hacia arriba" o "hacia abajo" al pasar la tangente del ángulo de sesgo en el segundo argumento a setTransform(), pero desea realizar ambos de manera simétrica (lo que resulta en una deformación "cercana" y/o "lejana"). Necesitas una transformación 3D para hacer eso.

Sin embargo, puede emular el mismo resultado cortando la imagen en varias "bandas" horizontales y aplicando una transformación diferente al renderizar cada banda. A las bandas más alejadas de la mitad de la imagen se les aplicarán ángulos de inclinación más fuertes. Algo como:

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

Tenga en cuenta que las bandas tienen dos píxeles de alto en lugar de uno para evitar un efecto muaré.

Puedes ver los resultados en este violín.

contestado el 03 de mayo de 12 a las 19:05

Hola Frédéric, ¿Puedes explicar cómo aplicar la transformación para obtener la imagen especular de la imagen en la pregunta? es decir, mayor altura en el lado izquierdo y menor altura en el lado derecho. Gracias - Chaitanya Munipalle

Tenga en cuenta que si usa bandas de 1 píxel de ancho que van verticalmente, también evita un efecto muaré: Eric

@Eric, tienes toda la razón, estaba ciego a eso ya que tenía la mente puesta en iterar alrededor del eje horizontal. De hecho, iterar alrededor del eje vertical puede ser más eficiente. Gracias por tu comentario :) - Frederic Hamidi

Aquí hay una función que escribí cuando estaba jugando con la renderización de una perspectiva pseudo-3d con JS.

A diferencia de las funciones de transformación basadas en franjas (que, sin duda, son lo suficientemente buenas para la mayoría de los casos de uso estándar), esta función utiliza una matriz de 4 esquinas para definir un cuadrilátero personalizado en el que se debe transformar el rectángulo original. Esto agrega algo de flexibilidad y se puede usar para renderizar trapezoides personalizados tanto para la perspectiva horizontal de "pintura en la pared" como para la perspectiva vertical de "alfombra en el piso" (así como cuadriláteros asimétricos para una sensación aún más similar a la de 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);
}

He aquí cómo llamarlo:

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

A pesar de todos los cálculos por píxel, en realidad es bastante eficiente y hace el trabajo:

imagen transformada

...pero puede haber formas más elegantes de hacerlo.

contestado el 15 de mayo de 16 a las 13:05

Hay un método para transformar un rectángulo en un trapecio ver esta respuesta de desbordamiento de pila. Sin embargo, necesitaría usar esto en cada píxel.

También puede dividir la imagen en tiras verticales de 1 píxel de ancho y luego estirar cada tira desde su centro.

Suponga que esto conduce a w tiras y desea que el borde izquierdo del trapecio sea el 80% del borde derecho, entonces

para la tira n el tramo debería ser 1+n/(4w)

contestado el 23 de mayo de 17 a las 11:05

Eso sigue siendo solo para el futuro, pero es tan genial que no puedo evitar agregarlo ya.

El equipo de Chrome está trabajando en agregar transformaciones no afines a la API 2D.
Esto agregaría algunos métodos a la API 2D, como perspective(), rotate3d(), rotateAxis()y extender otros para agregar un eje z, así como mejorar setTransform() y transform() en finalmente aceptar un DOMMatrix 3D.

Esto todavía es muy experimental y aún puede cambiar, pero ya puedes probar esto en Chrome Canary con chrome://flags/#enable-experimental-web-platform-features encendido.

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

Que en Chrome Canary actual se representa como

enter image description here

Respondido 01 Abr '21, 03:04

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.