<script src="./d3.v3.min.js"></script>
<script src="./numeric-1.2.6.min.js"></script>
<p>
Drag points around to distort image.
<input type="checkbox" checked="1" id="drawSkeleton"></input>
<label for="drawSkeleton">Draw wireframe and control points.</label>
</p>
<div id="container">
<canvas id="canvasElement"></canvas>
<svg id="svgElement"></svg>
</div>
<style type="text/css">
#container {
position: relative;
width: 800px; height: 800px;
}
#container * {
position: absolute;
}
#screen {
visibility: hidden;
}
circle.control-point {
fill: red;
fill-opacity: 0.25;
}
circle.control-point:hover {
stroke: yellow;
stroke-width: 2px;
}
</style>
<script type="text/javascript">
var maximumTriangleCount = 200;
var maxAllowableBadness = 1e-2;
// The control points which represent the top-left, top-right and bottom
// right of the image.
var controlPoints = [
{ x: 100, y: 100 },
{ x: 300, y: 100 },
{ x: 300, y: 400 },
{ x: 100, y: 400 }
];
var backgroundImgElement = document.getElementById('background');
var imgElement = document.getElementById('screen');
var canvasElement = document.getElementById('canvasElement');
var svgElement = document.getElementById('svgElement');
var drawSkeletonElement = document.getElementById('drawSkeleton');
var containerElement = document.getElementById('container')
initialize();
function initialize()
{
drawSkeletonElement.onchange = redrawImg;
imgElement.onload = redrawImg;
backgroundImgElement.onload = resizeElements;
resizeElements();
setupDragging();
redrawImg();
}
function resizeElements() {
var w = backgroundImgElement.naturalWidth;
var h = backgroundImgElement.naturalHeight;
containerElement.style.width = w+'px';
containerElement.style.height = h+'px';
svgElement.style.width = w+'px'; svgElement.style.height = h+'px';
canvasElement.width = w; canvasElement.height = h;
redrawImg();
}
function redrawImg() {
var drawSkeleton = !!(drawSkeletonElement.checked);
var w = imgElement.naturalWidth, h = imgElement.naturalHeight;
var srcPoints = [
{ x: 0, y: 0 }, // top-left
{ x: w, y: 0 }, // top-right
{ x: w-120, y: h-5 }, // bottom-right
{ x: 120, y: h-5 } // bottom-left
];
var ctx = canvasElement.getContext('2d');
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
var projPoints = findQuadProjectiveDepths(controlPoints);
var triangles = [
{ src: [srcPoints[0], srcPoints[1], srcPoints[2]],
dst: [projPoints[0], projPoints[1], projPoints[2]] },
{ src: [srcPoints[2], srcPoints[3], srcPoints[0]],
dst: [projPoints[2], projPoints[3], projPoints[0]] }
];
// Keep sub-dividing until we're done
var triIdx = 0;
while((triIdx < triangles.length) && (triangles.length < maximumTriangleCount)) {
var newTris = subdivideTriangle(triangles[triIdx]);
if(newTris.length == 1) {
// no subdivision performed
triIdx++;
} else {
// remove original triangle and add new ones
triangles.splice(triIdx, 1);
triangles = triangles.concat(newTris);
}
}
// Draw affine-transformed triangles
for(var i=0; i<triangles.length; i++) {
var src = triangles[i].src;
var dstProj = triangles[i].dst;
var dst = [];
for(var j=0; j<dstProj.length; j++) {
var p = dstProj[j];
dst.push({ x: p.x/p.z, y: p.y/p.z });
}
var T = affineTransformationFromTriangleCorners(src, dst);
ctx.save();
// set clip
trianglePath(ctx, dst);
ctx.clip();
// draw image
ctx.transform(T[0], T[1], T[2], T[3], T[4], T[5]);
ctx.drawImage(imgElement, 0, 0);
ctx.restore();
if(drawSkeleton) {
trianglePath(ctx, dst);
ctx.lineWidth = 2;
ctx.strokeStyle = '#080';
ctx.stroke();
}
}
if(drawSkeleton) {
svgElement.style.visibility = 'visible';
} else {
svgElement.style.visibility = 'hidden';
}
}
function subdivideTriangle(inputTri)
{
// Work out badness of each edge
var worstEdge = { badness: -1, corners: null };
for(var cornerIdx=0; cornerIdx<inputTri.dst.length; cornerIdx++)
{
var corner1Idx = cornerIdx, corner2Idx = (cornerIdx+1) % inputTri.dst.length;
var dz = inputTri.dst[corner1Idx].z - inputTri.dst[corner2Idx].z;
var badness = Math.abs(dz);
if(badness > worstEdge.badness) {
worstEdge = { badness: badness, corners: [corner1Idx, corner2Idx] };
}
}
// If the maximum badness is OK, don't subdivide
if(worstEdge.badness < maxAllowableBadness) {
return [inputTri];
}
// Going to turn
//
// A _____ B A __D__ B
// \ / \/_\/
// \ / => F \ / E
// C C
var srcA = inputTri.src[0], dstA = inputTri.dst[0];
var srcB = inputTri.src[1], dstB = inputTri.dst[1];
var srcC = inputTri.src[2], dstC = inputTri.dst[2];
var srcD = { x: (srcA.x + srcB.x)/2, y: (srcA.y + srcB.y)/2 };
var dstD =
{ x: (dstA.x + dstB.x)/2, y: (dstA.y + dstB.y)/2, z: (dstA.z + dstB.z)/2 };
var srcE = { x: (srcB.x + srcC.x)/2, y: (srcB.y + srcC.y)/2 };
var dstE =
{ x: (dstB.x + dstC.x)/2, y: (dstB.y + dstC.y)/2, z: (dstB.z + dstC.z)/2 };
var srcF = { x: (srcC.x + srcA.x)/2, y: (srcC.y + srcA.y)/2 };
var dstF =
{ x: (dstC.x + dstA.x)/2, y: (dstC.y + dstA.y)/2, z: (dstC.z + dstA.z)/2 };
return [
{ src: [ srcA, srcD, srcF ], dst: [ dstA, dstD, dstF ] },
{ src: [ srcD, srcB, srcE ], dst: [ dstD, dstB, dstE ] },
{ src: [ srcC, srcF, srcE ], dst: [ dstC, dstF, dstE ] },
{ src: [ srcD, srcE, srcF ], dst: [ dstD, dstE, dstF ] },
]
}
function findQuadProjectiveDepths(corners)
{
// See http://www.reedbeta.com/blog/2012/05/26/quadrilateral-interpolation-part-1/
// Firstly, find the centre point:
var centre = intersectLines(
[corners[0], corners[2]], [corners[1], corners[3]]);
// Lengths of diagonals
var d02 = dist(corners[0], corners[2]);
var d13 = dist(corners[1], corners[3]);
// Find a projective homogeneous representation for each corner point
// with the correct projective co-ordinate. See the site referenced in
// the top comment for some more details.
var tl_z = d02 / dist(corners[0], centre);
var tl_hom = { x: tl_z * corners[0].x, y: tl_z * corners[0].y, z: tl_z };
var tr_z = d13 / dist(corners[1], centre);
var tr_hom = { x: tr_z * corners[1].x, y: tr_z * corners[1].y, z: tr_z };
var br_z = d02 / dist(corners[2], centre);
var br_hom = { x: br_z * corners[2].x, y: br_z * corners[2].y, z: br_z };
var bl_z = d13 / dist(corners[3], centre);
var bl_hom = { x: bl_z * corners[3].x, y: bl_z * corners[3].y, z: bl_z };
return [ tl_hom, tr_hom, br_hom, bl_hom ];
}
function trianglePath(ctx, points)
{
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.lineTo(points[2].x, points[2].y);
ctx.closePath();
}
function setupDragging()
{
// Use d3.js to provide user-draggable control points
var rectDragBehav = d3.behavior.drag().on('drag', rectDragDrag);
var dragT = d3