Perspective effect with JavaScript Canvas

2

I need to fill this figure so that the image is slightly distorted, giving a perspective effect.

I believe it is possible with setTransform() , but I still do not understand how it works to construct a formula that calculates the transformation using the points reported in the addImage() function.

const canvas = document.getElementById('c')
const ctx = canvas.getContext('2d')

function pointAt([x, y]) {
  ctx.fillStyle = 'blue'
  ctx.beginPath()
  ctx.arc(x, y, 8, 0, 2 * Math.PI)
  ctx.fill()
  ctx.moveTo(x, y)
}

function lineTo([x1, y1], [x2, y2]) {
  ctx.moveTo(x1, y1)
  ctx.lineWidth = 2
  ctx.strokeStyle = 'red'
  ctx.lineTo(x2, y2)
  ctx.stroke()
}

function drawBox(a, b, c, d) {
  lineTo(a, b)
  lineTo(b, c)
  lineTo(c, d)
  lineTo(d, a)
  pointAt(a)
  pointAt(b)
  pointAt(c)
  pointAt(d)
}

function addImage(img, a, b, c, d) {
  const [x, y] = a
  const w = c[0] - x
  const h = c[1] - y
  
  ctx.drawImage(img, x, y, w, h)

  drawBox(a, b, c, d)
}

const img = new Image()

img.onload = () => {
  const a = [16, 40]
  const b = [480, 16]
  const c = [480, 280]
  const d = [16, 240]
  
  addImage(img, a, b, c, d)
}

img.src = 'https://i.stack.imgur.com/bt0xs.png'
<canvas id="c" width="800" height="600"></canvas>

link

    
asked by anonymous 20.08.2018 / 05:18

1 answer

1

Use Perspective.js

(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Perspective = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
// Copyright 2010 futomi  http://www.html5.jp/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// perspective.js v0.0.2
// 2010-08-28
//
// This file was modified by Fabien LOISON <http://www.flozz.fr/>


/* -------------------------------------------------------------------
 * define objects (name space) for this library.
 * ----------------------------------------------------------------- */

var html5jp = window.html5jp || {};

(function() {

    /* -------------------------------------------------------------------
     * constructor
     * ----------------------------------------------------------------- */
    html5jp.perspective = function(ctxd, image) {
        // check the arguments
        if( ! ctxd || ! ctxd.strokeStyle ) { return; }
        if( ! image || ! image.width || ! image.height ) { return; }
        // prepare a <canvas> for the image
        var cvso = document.createElement('canvas');
        cvso.width = parseInt(image.width);
        cvso.height = parseInt(image.height);
        var ctxo = cvso.getContext('2d');
        ctxo.drawImage(image, 0, 0, cvso.width, cvso.height);
        // prepare a <canvas> for the transformed image
        var cvst = document.createElement('canvas');
        cvst.width = ctxd.canvas.width;
        cvst.height = ctxd.canvas.height;
        var ctxt = cvst.getContext('2d');
        // parameters
        this.p = {
            ctxd: ctxd,
            cvso: cvso,
            ctxo: ctxo,
            ctxt: ctxt
        }
    };

    /* -------------------------------------------------------------------
     * prototypes
     * ----------------------------------------------------------------- */

    var proto = html5jp.perspective.prototype;

    /* -------------------------------------------------------------------
     * public methods
     * ----------------------------------------------------------------- */

    proto.draw = function(points) {
        var d0x = points[0][0];
        var d0y = points[0][1];
        var d1x = points[1][0];
        var d1y = points[1][1];
        var d2x = points[2][0];
        var d2y = points[2][1];
        var d3x = points[3][0];
        var d3y = points[3][1];
        // compute the dimension of each side
        var dims = [
            Math.sqrt( Math.pow(d0x-d1x, 2) + Math.pow(d0y-d1y, 2) ), // top side
            Math.sqrt( Math.pow(d1x-d2x, 2) + Math.pow(d1y-d2y, 2) ), // right side
            Math.sqrt( Math.pow(d2x-d3x, 2) + Math.pow(d2y-d3y, 2) ), // bottom side
            Math.sqrt( Math.pow(d3x-d0x, 2) + Math.pow(d3y-d0y, 2) )  // left side
                ];
        //
        var ow = this.p.cvso.width;
        var oh = this.p.cvso.height;
        // specify the index of which dimension is longest
        var base_index = 0;
        var max_scale_rate = 0;
        var zero_num = 0;
        for( var i=0; i<4; i++ ) {
            var rate = 0;
            if( i % 2 ) {
                rate = dims[i] / ow;
            } else {
                rate = dims[i] / oh;
            }
            if( rate > max_scale_rate ) {
                base_index = i;
                max_scale_rate = rate;
            }
            if( dims[i] == 0 ) {
                zero_num ++;
            }
        }
        if(zero_num > 1) { return; }
        //
        var step = 2;
        var cover_step = step * 5;
        //
        var ctxo = this.p.ctxo;
        var ctxt = this.p.ctxt;
        ctxt.clearRect(0, 0, ctxt.canvas.width, ctxt.canvas.height);
        if(base_index % 2 == 0) { // top or bottom side
            var ctxl = this.create_canvas_context(ow, cover_step);
            ctxl.globalCompositeOperation = "copy";
            var cvsl = ctxl.canvas;
            for( var y=0; y<oh; y+=step ) {
                var r = y / oh;
                var sx = d0x + (d3x-d0x) * r;
                var sy = d0y + (d3y-d0y) * r;
                var ex = d1x + (d2x-d1x) * r;
                var ey = d1y + (d2y-d1y) * r;
                var ag = Math.atan( (ey-sy) / (ex-sx) );
                var sc = Math.sqrt( Math.pow(ex-sx, 2) + Math.pow(ey-sy, 2) ) / ow;
                ctxl.setTransform(1, 0, 0, 1, 0, -y);
                ctxl.drawImage(ctxo.canvas, 0, 0);
                //
                ctxt.translate(sx, sy);
                ctxt.rotate(ag);
                ctxt.scale(sc, sc);
                ctxt.drawImage(cvsl, 0, 0);
                //
                ctxt.setTransform(1, 0, 0, 1, 0, 0);
            }
        } else if(base_index % 2 == 1) { // right or left side
            var ctxl = this.create_canvas_context(cover_step, oh);
            ctxl.globalCompositeOperation = "copy";
            var cvsl = ctxl.canvas;
            for( var x=0; x<ow; x+=step ) {
                var r =  x / ow;
                var sx = d0x + (d1x-d0x) * r;
                var sy = d0y + (d1y-d0y) * r;
                var ex = d3x + (d2x-d3x) * r;
                var ey = d3y + (d2y-d3y) * r;
                var ag = Math.atan( (sx-ex) / (ey-sy) );
                var sc = Math.sqrt( Math.pow(ex-sx, 2) + Math.pow(ey-sy, 2) ) / oh;
                ctxl.setTransform(1, 0, 0, 1, -x, 0);
                ctxl.drawImage(ctxo.canvas, 0, 0);
                //
                ctxt.translate(sx, sy);
                ctxt.rotate(ag);
                ctxt.scale(sc, sc);
                ctxt.drawImage(cvsl, 0, 0);
                //
                ctxt.setTransform(1, 0, 0, 1, 0, 0);
            }
        }
        // set a clipping path and draw the transformed image on the destination canvas.
        this.p.ctxd.save();
        this.p.ctxd.drawImage(ctxt.canvas, 0, 0);
        this._applyMask(this.p.ctxd, [[d0x, d0y], [d1x, d1y], [d2x, d2y], [d3x, d3y]]);
        this.p.ctxd.restore();
    }

    /* -------------------------------------------------------------------
     * private methods
     * ----------------------------------------------------------------- */

    proto.create_canvas_context = function(w, h) {
        var canvas = document.createElement('canvas');
        canvas.width = w;
        canvas.height = h;
        var ctx = canvas.getContext('2d');
        return ctx;
    };

    proto._applyMask = function(ctx, points) {
        ctx.beginPath();
        ctx.moveTo(points[0][0], points[0][1]);
        for( var i=1; i<points.length; i++ ) {
            ctx.lineTo(points[i][0], points[i][1]);
        }
        ctx.closePath();
        ctx.globalCompositeOperation = "destination-in";
        ctx.fill();
        ctx.globalCompositeOperation = "source-over";
    };

})();


module.exports = html5jp.perspective;

},{}]},{},[1])(1)
});
<!doctype html>

<html>

    <head>
        <title>Perspective.js demo</title>
    </head>

    <body>
        <canvas id="canvas"></canvas>
        <script>
            var canvas = document.getElementById("canvas");
            var image = new Image();
            image.onload = function() {
                canvas.width = image.width;
                canvas.height = image.height;
                var ctx = canvas.getContext("2d");
                var p = new Perspective(ctx, image);
                p.draw([
                        [50, 100],
                        [image.width - 20, 1],
                        [image.width - 10, image.height - 1],
                        [10, image.height]
                ]);
            }
            image.src = "https://i.stack.imgur.com/bt0xs.png";
        </script>
    </body>

</html>
    
20.08.2018 / 14:48