Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active July 3, 2016 05:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veltman/8f5a157276b1dc18ce2fba1bc06dfb48 to your computer and use it in GitHub Desktop.
Save veltman/8f5a157276b1dc18ce2fba1bc06dfb48 to your computer and use it in GitHub Desktop.
Warp-off
<!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);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment