Comparing methods for warping a quadrilateral based on a beginning and ending set of corners. Based on Projective Mappings for Image Warping and Four Corner Image Warping.
See also: Warp speed
Comparing methods for warping a quadrilateral based on a beginning and ending set of corners. Based on Projective Mappings for Image Warping and Four Corner Image Warping.
See also: Warp speed
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <style> | |
| .quad { | |
| stroke-linejoin: round; | |
| stroke-width: 3px; | |
| stroke: #000; | |
| fill: #fff; | |
| } | |
| .grid { | |
| stroke: #0eb8ba; | |
| stroke-width: 1px; | |
| fill: none; | |
| } | |
| text { | |
| fill: #000; | |
| text-transform: uppercase; | |
| text-anchor: middle; | |
| font: 600 36px sans-serif; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div></div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> | |
| <script src="warper.js"></script> | |
| <script> | |
| var width = 480, | |
| height = 480; | |
| var svg = d3.select("body").append("svg") | |
| .attr("width", 960) | |
| .attr("height", 500); | |
| var quads = svg.selectAll("g") | |
| .data(["bilinear", "projective"]) | |
| .enter() | |
| .append("g") | |
| .attr("transform", function(d, i){ | |
| return "translate(" + (i * width) + " 10)"; | |
| }); | |
| quads.append("text") | |
| .text(function(d){ | |
| return d; | |
| }) | |
| .attr("x", width / 2) | |
| .attr("y", height) | |
| .attr("dy", "-0.5em"); | |
| var square = getCorners(true), | |
| grid = getGrid(12); | |
| quads.append("path") | |
| .datum(square) | |
| .attr("class", "quad") | |
| .call(updateQuad); | |
| quads.append("path") | |
| .datum(grid) | |
| .attr("class", "grid") | |
| .call(updateGrid); | |
| warp(square); | |
| function warp(current) { | |
| var next = current === square ? getCorners() : square; | |
| quads.each(function(type, i){ | |
| var lines = grid, | |
| fn = warper[type](current, next); | |
| if (current === square) { | |
| lines = grid.map(function(linestring){ | |
| return linestring.map(fn); | |
| }); | |
| } | |
| var quad = d3.select(this); | |
| quad.select(".grid") | |
| .datum(lines) | |
| .transition() | |
| .delay(500) | |
| .duration(600) | |
| .call(updateGrid); | |
| quad.select(".quad") | |
| .datum(next) | |
| .transition() | |
| .delay(500) | |
| .duration(600) | |
| .call(updateQuad) | |
| .each("end",i && warp); | |
| }); | |
| } | |
| function updateQuad(sel) { | |
| sel.attr("d",function(d){ | |
| return "M" + d.join("L") + "Z"; | |
| }); | |
| } | |
| function updateGrid(sel) { | |
| sel.attr("d",function(d){ | |
| return d.map(function(linestring){ | |
| return "M" + linestring.join("L"); | |
| }); | |
| }); | |
| } | |
| function getGrid(dim) { | |
| var cols = d3.range(dim).map(function(i){ | |
| var x = width / 4 + (width / 2) * (1 + i) / (dim + 1); | |
| return d3.range(100).map(function(j){ | |
| return [x, height / 4 + (j / 99) * height / 2]; | |
| }) | |
| }); | |
| var rows = cols.map(function(col){ | |
| return col.map(function(point){ | |
| return [point[1], point[0]]; | |
| }); | |
| }); | |
| return rows.concat(cols); | |
| } | |
| function getCorners(sq) { | |
| return d3.range(4).map(function(i){ | |
| return [ | |
| ((i % 3 ? 1 : 0) + (sq ? 0.5 : 0.1 + Math.random() * 0.8)) * width / 2, | |
| (Math.floor(i / 2) + (sq ? 0.5 : 0.1 + Math.random() * 0.8)) * height / 2 | |
| ]; | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |
| var warper = {}; | |
| warper.bilinear = function(start,end) { | |
| var u0 = start[0][0], | |
| v0 = start[0][1], | |
| u1 = start[1][0], | |
| v1 = start[1][1], | |
| u2 = start[2][0], | |
| v2 = start[2][1], | |
| u3 = start[3][0], | |
| v3 = start[3][1], | |
| x0 = end[0][0], | |
| y0 = end[0][1], | |
| x1 = end[1][0], | |
| y1 = end[1][1], | |
| x2 = end[2][0], | |
| y2 = end[2][1], | |
| x3 = end[3][0], | |
| y3 = end[3][1]; | |
| var square = [ | |
| [1,u0,v0,u0 * v0,0,0,0,0], | |
| [1,u1,v1,u1 * v1,0,0,0,0], | |
| [1,u2,v2,u2 * v2,0,0,0,0], | |
| [1,u3,v3,u3 * v3,0,0,0,0], | |
| [0,0,0,0,1,u0,v0,u0 * v0], | |
| [0,0,0,0,1,u1,v1,u1 * v1], | |
| [0,0,0,0,1,u2,v2,u2 * v2], | |
| [0,0,0,0,1,u3,v3,u3 * v3] | |
| ]; | |
| var inverted = invert(square); | |
| var s = multiply(inverted,[x0,x1,x2,x3,y0,y1,y2,y3]); | |
| return function(p) { | |
| return [ | |
| s[0] + s[1] * p[0] + s[2] * p[1] + s[3] * p[0] * p[1], | |
| s[4] + s[5] * p[0] + s[6] * p[1] + s[7] * p[0] * p[1], | |
| ]; | |
| }; | |
| }; | |
| warper.projective = function(start,end) { | |
| var u0 = start[0][0], | |
| v0 = start[0][1], | |
| u1 = start[1][0], | |
| v1 = start[1][1], | |
| u2 = start[2][0], | |
| v2 = start[2][1], | |
| u3 = start[3][0], | |
| v3 = start[3][1], | |
| x0 = end[0][0], | |
| y0 = end[0][1], | |
| x1 = end[1][0], | |
| y1 = end[1][1], | |
| x2 = end[2][0], | |
| y2 = end[2][1], | |
| x3 = end[3][0], | |
| y3 = end[3][1]; | |
| var square = [ | |
| [u0,v0,1,0,0,0,-u0 * x0,-v0 * x0], | |
| [u1,v1,1,0,0,0,-u1 * x1,-v1 * x1], | |
| [u2,v2,1,0,0,0,-u2 * x2,-v2 * x2], | |
| [u3,v3,1,0,0,0,-u3 * x3,-v3 * x3], | |
| [0,0,0,u0,v0,1,-u0 * y0,-v0 * y0], | |
| [0,0,0,u1,v1,1,-u1 * y1,-v1 * y1], | |
| [0,0,0,u2,v2,1,-u2 * y2,-v2 * y2], | |
| [0,0,0,u3,v3,1,-u3 * y3,-v3 * y3] | |
| ]; | |
| var inverted = invert(square); | |
| var s = multiply(inverted,[x0,x1,x2,x3,y0,y1,y2,y3]); | |
| return function(p) { | |
| return [ | |
| (s[0] * p[0] + s[1] * p[1] + s[2]) / (s[6] * p[0] + s[7] * p[1] + 1), | |
| (s[3] * p[0] + s[4] * p[1] + s[5]) / (s[6] * p[0] + s[7] * p[1] + 1) | |
| ]; | |
| }; | |
| } | |
| function multiply(matrix,vector) { | |
| return matrix.map(function(row){ | |
| var sum = 0; | |
| row.forEach(function(c,i){ | |
| sum += c * vector[i]; | |
| }); | |
| return sum; | |
| }); | |
| } | |
| function invert(matrix) { | |
| var size = matrix.length, | |
| base, | |
| swap, | |
| augmented; | |
| // Augment w/ identity matrix | |
| augmented = matrix.map(function(row,i){ | |
| return row.slice(0).concat(row.slice(0).map(function(d,j){ | |
| return j === i ? 1 : 0; | |
| })); | |
| }); | |
| // Process each row | |
| for (var r = 0; r < size; r++) { | |
| base = augmented[r][r]; | |
| // Zero on diagonal, swap with a lower row | |
| if (!base) { | |
| for (var rr = r + 1; rr < size; rr++) { | |
| if (augmented[rr][r]) { | |
| // swap | |
| swap = augmented[rr]; | |
| augmented[rr] = augmented[r]; | |
| augmented[r] = swap; | |
| base = augmented[r][r]; | |
| break; | |
| } | |
| } | |
| if (!base) { | |
| throw new Error("Not invertable :("); | |
| } | |
| } | |
| // 1 on the diagonal | |
| for (var c = 0; c < size * 2; c++) { | |
| augmented[r][c] = augmented[r][c] / base; | |
| } | |
| // Zeroes elsewhere | |
| for (var q = 0; q < size; q++) { | |
| if (q !== r) { | |
| base = augmented[q][r]; | |
| for (var p = 0; p < size * 2; p++) { | |
| augmented[q][p] -= base * augmented[r][p]; | |
| } | |
| } | |
| } | |
| } | |
| return augmented.map(function(row){ | |
| return row.slice(size); | |
| }); | |
| } |