Interactive version of http://s3.amazonaws.com/canvas-warp/2009-11-01/index.html
From http://mike.teczno.com/notes/canvas-warp.html
A Pen by Andreas Borgen on CodePen.
<h2>Drag the round handles to warp the image</h2> | |
<img id="i" /> | |
<div id="drag-wrapper"> | |
<canvas id="c"></canvas> | |
<div class="drag-handle" data-corner="0"></div> | |
<div class="drag-handle" data-corner="1"></div> | |
<div class="drag-handle" data-corner="2"></div> | |
<div class="drag-handle" data-corner="3"></div> | |
<div class="drag-handle" data-corner="4"></div> | |
</div> |
"use strict"; | |
console.clear(); | |
(function() { | |
//http://mike.teczno.com/notes/canvas-warp.html | |
//http://s3.amazonaws.com/canvas-warp/2009-11-01/index.html | |
const utils = { | |
rndInt(max, override) { | |
//if(override !== undefined) { return override; } | |
return Math.round(Math.random() * max); | |
}, | |
/** | |
* https://en.wikipedia.org/wiki/Incircle_and_excircles_of_a_triangle | |
* https://math.stackexchange.com/questions/1413372/find-cartesian-coordinates-of-the-incenter | |
* https://www.mathopenref.com/coordincenter.html | |
*/ | |
calcIncircle(A, B, C) { | |
function lineLen(p1, p2) { | |
const dx = p2[0] - p1[0], | |
dy = p2[1] - p1[1]; | |
return Math.sqrt(dx*dx + dy*dy); | |
} | |
//Side lengths, perimiter p and semiperimiter s: | |
const a = lineLen(B, C), | |
b = lineLen(C, A), | |
c = lineLen(A, B), | |
p = (a + b + c), | |
s = p/2; | |
//Heron's formula | |
//https://www.wikihow.com/Calculate-the-Area-of-a-Triangle#Using_Side_Lengths | |
const area = Math.sqrt(s * (s-a) * (s-b) * (s-c)); | |
//Faster(?) alternative: | |
//http://geomalgorithms.com/a01-_area.html#Modern-Triangles | |
//const area = Math.abs( (B[0]-A[0])*(C[1]-A[1]) - (C[0]-A[0])*(B[1]-A[1]) )/2; | |
//Incircle radius r | |
// https://en.wikipedia.org/wiki/Incircle_and_excircles_of_a_triangle#Relation_to_area_of_the_triangle | |
//..and center [cx, cy] | |
// https://en.wikipedia.org/wiki/Incircle_and_excircles_of_a_triangle#Cartesian_coordinates | |
// https://www.mathopenref.com/coordincenter.html | |
const r = area/s, | |
cx = (a*A[0] + b*B[0] + c*C[0]) / p, | |
cy = (a*A[1] + b*B[1] + c*C[1]) / p; | |
return { | |
r, | |
c: [cx, cy], | |
} | |
}, | |
/* | |
* https://math.stackexchange.com/questions/17561/how-to-shrink-a-triangle | |
*/ | |
expandTriangle(A, B, C, amount) { | |
const incircle = this.calcIncircle(A, B, C), | |
c = incircle.c, | |
factor = (incircle.r + amount)/(incircle.r); | |
function extendPoint(p) { | |
const dx = p[0] - c[0], | |
dy = p[1] - c[1], | |
x2 = (dx * factor) + c[0], | |
y2 = (dy * factor) + c[1]; | |
return [x2, y2]; | |
} | |
const A2 = extendPoint(A), | |
B2 = extendPoint(B), | |
C2 = extendPoint(C); | |
return[A2, B2, C2]; | |
}, | |
/** | |
* Solves a system of linear equations. | |
* | |
* t1 = (a * r1) + (b + s1) + c | |
* t2 = (a * r2) + (b + s2) + c | |
* t3 = (a * r3) + (b + s3) + c | |
* | |
* r1 - t3 are the known values. | |
* a, b, c are the unknowns to be solved. | |
* returns the a, b, c coefficients. | |
*/ | |
linearSolution(r1, s1, t1, r2, s2, t2, r3, s3, t3) | |
{ | |
var a = (((t2 - t3) * (s1 - s2)) - ((t1 - t2) * (s2 - s3))) / (((r2 - r3) * (s1 - s2)) - ((r1 - r2) * (s2 - s3))); | |
var b = (((t2 - t3) * (r1 - r2)) - ((t1 - t2) * (r2 - r3))) / (((s2 - s3) * (r1 - r2)) - ((s1 - s2) * (r2 - r3))); | |
var c = t1 - (r1 * a) - (s1 * b); | |
return [a, b, c]; | |
}, | |
/** | |
* This draws a triangular area from an image onto a canvas, | |
* similar to how ctx.drawImage() draws a rectangular area from an image onto a canvas. | |
* | |
* s1-3 are the corners of the triangular area on the source image, and | |
* d1-3 are the corresponding corners of the area on the destination canvas. | |
* | |
* Those corner coordinates ([x, y]) can be given in any order, | |
* just make sure s1 corresponds to d1 and so forth. | |
*/ | |
drawImageTriangle(img, ctx, s1, s2, s3, d1, d2, d3) { | |
//I assume the "m" is for "magic"... | |
const xm = this.linearSolution(s1[0], s1[1], d1[0], s2[0], s2[1], d2[0], s3[0], s3[1], d3[0]), | |
ym = this.linearSolution(s1[0], s1[1], d1[1], s2[0], s2[1], d2[1], s3[0], s3[1], d3[1]); | |
ctx.save(); | |
ctx.setTransform(xm[0], ym[0], xm[1], ym[1], xm[2], ym[2]); | |
ctx.beginPath(); | |
ctx.moveTo(s1[0], s1[1]); | |
ctx.lineTo(s2[0], s2[1]); | |
ctx.lineTo(s3[0], s3[1]); | |
ctx.closePath(); | |
//Leaves a faint black (or whatever .fillStyle) border around the drawn triangle | |
// ctx.fill(); | |
ctx.clip(); | |
ctx.drawImage(img, 0, 0, img.width, img.height); | |
ctx.restore(); | |
return; | |
//DEBUG - https://en.wikipedia.org/wiki/Incircle_and_excircles_of_a_triangle | |
const incircle = this.calcIncircle(d1, d2, d3), | |
c = incircle.c; | |
//console.log(incircle); | |
ctx.beginPath(); | |
ctx.arc(c[0], c[1], incircle.r, 0, 2*Math.PI, false); | |
ctx.moveTo(d1[0], d1[1]); | |
ctx.lineTo(d2[0], d2[1]); | |
ctx.lineTo(d3[0], d3[1]); | |
ctx.closePath(); | |
//ctx.fillStyle = 'rgba(0,0,0, .3)'; | |
//ctx.fill(); | |
ctx.lineWidth = 2; | |
ctx.strokeStyle = 'rgba(255,0,0, .4)'; | |
ctx.stroke(); | |
}, | |
}; | |
const canv = document.querySelector('#c'), | |
ctx = canv.getContext('2d'), | |
img = document.querySelector('#i'), //new Image(), | |
handles = document.querySelectorAll('.drag-handle'); | |
let w, h, corners; | |
function updateUI() { | |
function drawTriangle(s1, s2, s3, d1, d2, d3) { | |
function movePoint(p, exampleSource, exampleTarget) { | |
const dx = exampleTarget[0]/exampleSource[0], | |
dy = exampleTarget[1]/exampleSource[1], | |
p2 = [p[0] * dx, p[1] * dy]; | |
return p2; | |
} | |
//Overlap the destination areas a little | |
//to avoid hairline cracks when drawing mulitiple connected triangles. | |
const [d1x, d2x, d3x] = utils.expandTriangle(d1, d2, d3, .3), | |
[s1x, s2x, s3x] = utils.expandTriangle(s1, s2, s3, .3); | |
//s1x = movePoint(s1, d1, d1x), | |
//s2x = movePoint(s2, d2, d2x), | |
//s3x = movePoint(s3, d3, d3x); | |
utils.drawImageTriangle(img, ctx, | |
s1x, s2x, s3x, | |
d1x, d2x, d3x); | |
} | |
ctx.clearRect(0,0, w,h); | |
drawTriangle([0, 0], [w/2, h/2], [0, h], | |
corners[0], corners[2], corners[3]); | |
//* | |
drawTriangle([0, 0], [w/2, h/2], [w, 0], | |
corners[0], corners[2], corners[1]); | |
drawTriangle([w, 0], [w/2, h/2], [w, h], | |
corners[1], corners[2], corners[4]); | |
drawTriangle([0, h], [w/2, h/2], [w, h], | |
corners[3], corners[2], corners[4]); | |
//*/ | |
corners.forEach((c, i) => { | |
const s = handles[i].style; | |
s.left = c[0] + 'px'; | |
s.top = c[1] + 'px'; | |
}); | |
} | |
img.onload = function() | |
{ | |
const rnd = utils.rndInt; | |
w = canv.width = img.width; | |
h = canv.height = img.height; | |
//Put the four corners (and center) of the source image at semi-random places on the canvas: | |
corners = [[rnd(w*.33), rnd(h*.33)], | |
[rnd(w*.33) + w*.67, rnd(h*.33)], | |
[rnd(w*.33) + w*.33, rnd(h*.33) + h*.33], //center | |
[rnd(w*.33), rnd(h*.33) + h*.67], | |
[rnd(w*.33) + w*.67, rnd(h*.33) + h*.67]]; | |
updateUI(); | |
dragTracker({ | |
container: document.querySelector('#drag-wrapper'), | |
selector: '.drag-handle', | |
handleOffset: 'center', | |
callback: (box, pos) => { | |
//console.log(corners[box.dataset.corner], pos); | |
corners[box.dataset.corner] = pos; | |
updateUI(); | |
}, | |
}); | |
}; | |
const imgW = Math.min(window.innerWidth - 10, 700); | |
img.src = `https://picsum.photos/${imgW}/${Math.round(imgW*2/3)}?image=1072`; | |
})(); | |
<script src="https://unpkg.com/[email protected]"></script> |
body { | |
display: flex; | |
flex-flow: column nowrap; | |
margin: 0; | |
height: 100vh; | |
align-items: center; | |
justify-content: center; | |
background-color: #ccc; | |
font-family: Georgia, sans-serif; | |
text-align: center; | |
} | |
#i { | |
display: none; | |
} | |
#c { | |
display: block; | |
background-color: #eee; | |
box-shadow: 2px 2px 6px 0 rgba(black, .5); | |
} | |
#drag-wrapper { | |
position: relative; | |
.drag-handle { | |
position: absolute; | |
width: 2em; | |
height: 2em; | |
margin: -1em; | |
border-radius: 100%; | |
box-sizing: border-box; | |
background: tomato; | |
border: 1px solid #444; | |
opacity: .7; | |
cursor: move; | |
&:nth-of-type(2) { background: limegreen; } | |
&:nth-of-type(3) { background: cyan; } | |
&:nth-of-type(4) { background: dodgerblue; } | |
&:nth-of-type(5) { background: gold; } | |
&::after { | |
//content: attr(data-corner); | |
} | |
} | |
} |
Interactive version of http://s3.amazonaws.com/canvas-warp/2009-11-01/index.html
From http://mike.teczno.com/notes/canvas-warp.html
A Pen by Andreas Borgen on CodePen.