A perspective projection can be precisely specified through four pairs of corresponding points. By dragging the four corners of the sailboat image above, you can adjust the perspective projection interactively and place the image anywhere you like in the scene.
The image is transformed using a matrix3d CSS transform. The transformation matrix is computed by solving a series of linear equations derived from the four point-pairs using LU decomposition as implemented by numeric.js.
This technique can also be used to apply a perspective transform manually for simple 2D shapes, such as lines and circles.
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
position: relative;
width: 960px;
height: 500px;
}
#background {
width: 960px;
height: 500px;
background: url(mosque.jpg);
}
svg {
position: absolute;
top: 0;
left: 0;
}
.line {
stroke: #000;
stroke-opacity: .5;
stroke-width: 1px;
stroke-linecap: square;
}
.handle {
fill: none;
pointer-events: all;
stroke: #fff;
stroke-width: 2px;
cursor: move;
}
#buttons {
position: absolute;
top: 20px;
left: 20px;
z-index: 2;
}
button {
display: block;
width: 10em;
}
button:focus {
outline: none;
}
</style>
<div id="background"></div>
<div id="buttons">
<button data-targets="[[492,329],[542,330],[569,434],[424,424]]">Floor</button>
<button data-targets="[[-28,287],[74,288],[72,413],[-31,404]]">Near Wall</button>
<button data-targets="[[-194,282],[-95,282],[-100,354],[-200,365]]">Far Wall</button>
<button data-targets="[[0,0],[400,0],[400,400],[0,400]]">Reset</button>
</div>
<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 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var transform = ["", "-webkit-", "-moz-", "-ms-", "-o-"].reduce(function(p, v) { return v + "transform" in document.body.style ? v : p; }) + "transform";
var sourcePoints = [[0, 0], [width, 0], [width, height], [0, height]],
targetPoints = [[0, 0], [width, 0], [width, height], [0, height]];
d3.select("body").selectAll("svg")
.data(["transform", "flat"])
.enter().append("svg")
.attr("id", function(d) { return d; })
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var svgTransform = d3.select("#transform")
.style(transform + "-origin", margin.left + "px " + margin.top + "px 0");
var svgFlat = d3.select("#flat");
svgTransform.select("g").append("image")
.attr("xlink:href", "sailboat.png")
.attr("width", width)
.attr("height", height);
svgTransform.select("g").selectAll(".line--x")
.data(d3.range(0, width + 1, 40))
.enter().append("line")
.attr("class", "line line--x")
.attr("x1", function(d) { return d; })
.attr("x2", function(d) { return d; })
.attr("y1", 0)
.attr("y2", height);
svgTransform.select("g").selectAll(".line--y")
.data(d3.range(0, height + 1, 40))
.enter().append("line")
.attr("class", "line line--y")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", function(d) { return d; })
.attr("y2", function(d) { return d; });
var handle = svgFlat.select("g").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));
d3.selectAll("button")
.datum(function(d) { return JSON.parse(this.getAttribute("data-targets")); })
.on("click", clicked)
.call(transformed);
function clicked(d) {
d3.transition()
.duration(750)
.tween("points", function() {
var i = d3.interpolate(targetPoints, d);
return function(t) {
handle.data(targetPoints = i(t)).attr("transform", function(d) { return "translate(" + d + ")"; });
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);
});
svgTransform.style(transform, "matrix3d(" + matrix + ")");
}
</script>
https://d3js.org/d3.v3.min.js