|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
position: relative; |
|
} |
|
|
|
svg { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
} |
|
|
|
.handle { |
|
stroke: #fff; |
|
stroke-width: 2px; |
|
} |
|
|
|
.line { |
|
stroke-width: 1.5px; |
|
stroke-linecap: square; |
|
} |
|
|
|
#expected .line { |
|
stroke: cyan; |
|
} |
|
|
|
#actual .line { |
|
stroke: magenta; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script src="numeric-solve.min.js"></script> |
|
<script> |
|
|
|
var margin = {top: 50, right: 280, bottom: 50, left: 280}, |
|
width = 960, |
|
height = 500; |
|
|
|
var transform = ["", "-webkit-", "-moz-", "-ms-", "-o-"].reduce(function(p, v) { return v + "transform" in document.body.style ? v : p; }) + "transform"; |
|
|
|
var sourcePoints = [[0, 0], [960, 0], [960, 500], [0, 500]], |
|
targetPoints = [[280, 50], [688, 33], [680, 450], [341, 371]]; |
|
|
|
var svg = d3.select("body").selectAll("svg") |
|
.data(["actual", "expected"]) |
|
.enter().append("svg") |
|
.attr("id", function(d) { return d; }) |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
svg.selectAll(".line") |
|
.data(d3.range(0, width + 1, width / 10).map(function(x) { return [[x, 0], [x, height]]; }) |
|
.concat(d3.range(0, height + 1, height / 10).map(function(y) { return [[0, y], [width, y]]; }))) |
|
.enter().append("path") |
|
.attr("class", "line") |
|
.attr("d", function(d) { return "M" + d[0] + "L" + d[1]; }); |
|
|
|
var svgActual = d3.select("#actual") |
|
.style(transform + "-origin", "0px 0px 0"); |
|
|
|
var svgExpected = d3.select("#expected"); |
|
|
|
svgExpected.selectAll(".handle") |
|
.data(targetPoints) |
|
.enter().append("circle") |
|
.attr("class", "handle") |
|
.attr("transform", function(d) { return "translate(" + d + ")"; }) |
|
.attr("r", 7) |
|
.call(d3.behavior.drag() |
|
.origin(function(d) { return {x: d[0], y: d[1]}; }) |
|
.on("drag", dragged)); |
|
|
|
var lineExpected = svgExpected.selectAll(".line"); |
|
|
|
transformed(); |
|
|
|
function dragged(d) { |
|
d3.select(this).attr("transform", "translate(" + (d[0] = d3.event.x) + "," + (d[1] = d3.event.y) + ")"); |
|
transformed(); |
|
} |
|
|
|
function transformed() { |
|
for (var a = [], b = [], i = 0, n = sourcePoints.length; i < n; ++i) { |
|
var s = sourcePoints[i], t = targetPoints[i]; |
|
a.push([s[0], s[1], 1, 0, 0, 0, -s[0] * t[0], -s[1] * t[0]]), b.push(t[0]); |
|
a.push([0, 0, 0, s[0], s[1], 1, -s[0] * t[1], -s[1] * t[1]]), b.push(t[1]); |
|
} |
|
|
|
var X = solve(a, b, true), matrix = [ |
|
X[0], X[3], 0, X[6], |
|
X[1], X[4], 0, X[7], |
|
0, 0, 1, 0, |
|
X[2], X[5], 0, 1 |
|
].map(function(x) { |
|
return d3.round(x, 6); |
|
}); |
|
|
|
svgActual.style(transform, "matrix3d(" + matrix + ")"); |
|
lineExpected.attr("d", function(d) { return "M" + project(matrix, d[0]) + "L" + project(matrix, d[1]); }); |
|
} |
|
|
|
// Given a 4x4 perspective transformation matrix, and a 2D point (a 2x1 vector), |
|
// applies the transformation matrix by converting the point to homogeneous |
|
// coordinates at z=0, post-multiplying, and then applying a perspective divide. |
|
function project(matrix, point) { |
|
point = multiply(matrix, [point[0], point[1], 0, 1]); |
|
return [point[0] / point[3], point[1] / point[3]]; |
|
} |
|
|
|
// Post-multiply a 4x4 matrix in column-major order by a 4x1 column vector: |
|
// [ m0 m4 m8 m12 ] [ v0 ] [ x ] |
|
// [ m1 m5 m9 m13 ] * [ v1 ] = [ y ] |
|
// [ m2 m6 m10 m14 ] [ v2 ] [ z ] |
|
// [ m3 m7 m11 m15 ] [ v3 ] [ w ] |
|
function multiply(matrix, vector) { |
|
return [ |
|
matrix[0] * vector[0] + matrix[4] * vector[1] + matrix[8 ] * vector[2] + matrix[12] * vector[3], |
|
matrix[1] * vector[0] + matrix[5] * vector[1] + matrix[9 ] * vector[2] + matrix[13] * vector[3], |
|
matrix[2] * vector[0] + matrix[6] * vector[1] + matrix[10] * vector[2] + matrix[14] * vector[3], |
|
matrix[3] * vector[0] + matrix[7] * vector[1] + matrix[11] * vector[2] + matrix[15] * vector[3] |
|
]; |
|
} |
|
|
|
</script> |