|
doctype |
|
html |
|
head |
|
meta(charset="utf-8") |
|
title gist-distance-to-segment |
|
style. |
|
body { |
|
background: linen; |
|
/* https://github.com/corysimmons/typographic/blob/2.9.3/scss/typographic.scss#L34 */ |
|
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'; |
|
} |
|
|
|
#scene { |
|
border: 1px solid #000; |
|
} |
|
|
|
.vertex { |
|
cursor: pointer; |
|
stroke: #000; |
|
stroke-width: 5px; |
|
} |
|
|
|
#vertex--a { |
|
fill: orange; |
|
} |
|
#vertex--b { |
|
fill: blue; |
|
} |
|
#vertex--c { |
|
fill: limegreen; |
|
} |
|
body |
|
p Click and drag a circle to see the our distance to segment update! |
|
svg#scene(width="400", height="400", viewBox="0 0 400 400", xmlns="https://www.w3.org/2000/svg") |
|
//- DEV: We put our line first so it's hidden behind our vertex z-indicies |
|
line#line--ab.line(x1="120", y1="170", x2="280", y2="170", |
|
stroke-width="10", stroke="#000") |
|
line#line--cx.line(x1="60", y1="260", x2="120", y2="170", |
|
stroke-width="5", stroke="#999", stroke-dasharray="5,5") |
|
circle#vertex--a.vertex(cx="120", cy="170", r="20") |
|
circle#vertex--b.vertex(cx="280", cy="170", r="20") |
|
circle#vertex--c.vertex(cx="60", cy="260", r="20") |
|
|
|
//- Load in our dependencies |
|
script(src="https://unpkg.com/draggabilly@2.1.1/dist/draggabilly.pkgd.js") |
|
script(src="https://unpkg.com/gl-matrix@2.4.0/dist/gl-matrix.js") |
|
|
|
//- Define our scripts/bindings |
|
script. |
|
const Draggabilly = window.Draggabilly; |
|
document.addEventListener('DOMContentLoaded', function handleReady () { |
|
// Resolve our circle |
|
// TODO: Add assertion for element resolve |
|
let sceneEl = document.querySelector('#scene'); |
|
let vertexAEl = sceneEl.querySelector('#vertex--a'); |
|
let vertexBEl = sceneEl.querySelector('#vertex--b'); |
|
let vertexCEl = sceneEl.querySelector('#vertex--c'); |
|
let lineABEl = sceneEl.querySelector('#line--ab'); |
|
let lineCXEl = sceneEl.querySelector('#line--cx'); |
|
|
|
// Add our drag bindings |
|
function bindCircleDraggabilly(circleEl) { |
|
// Initialize our bindings |
|
let draggie = new Draggabilly(circleEl); |
|
|
|
// Update our bindings to support SVG |
|
// https://github.com/desandro/draggabilly/blob/v2.1.1/draggabilly.js#L266 |
|
// https://github.com/desandro/draggabilly/blob/v2.1.1/draggabilly.js#L184-L194 |
|
draggie._getPosition = function() { |
|
let x = parseFloat(this.element.getAttribute('cx'), 10); |
|
let y = parseFloat(this.element.getAttribute('cy'), 10); |
|
// Clean up 'auto' or other non-integer values |
|
this.position.x = isNaN(x) ? 0 : x; |
|
this.position.y = isNaN(y) ? 0 : y; |
|
}; |
|
// https://github.com/desandro/draggabilly/blob/v2.1.1/draggabilly.js#L424-L427 |
|
draggie.positionDrag = function () { |
|
this.element.setAttribute('cx', this.startPosition.x + this.dragPoint.x); |
|
this.element.setAttribute('cy', this.startPosition.y + this.dragPoint.y); |
|
}; |
|
// https://github.com/desandro/draggabilly/blob/v2.1.1/draggabilly.js#L418-L422 |
|
draggie.setLeftTop = function () { |
|
this.element.setAttribute('cx', this.position.x); |
|
this.element.setAttribute('cy', this.position.y); |
|
}; |
|
|
|
// Return our overridden binding |
|
return draggie; |
|
} |
|
let vertexADraggie = bindCircleDraggabilly(vertexAEl); |
|
let vertexBDraggie = bindCircleDraggabilly(vertexBEl); |
|
let vertexCDraggie = bindCircleDraggabilly(vertexCEl); |
|
|
|
// Eagerly calculate our positions so we can have a shared redraw |
|
vertexADraggie._getPosition(); |
|
vertexBDraggie._getPosition(); |
|
vertexCDraggie._getPosition(); |
|
|
|
// When our vertices move, update our scene |
|
// DEV: We could be more intelligent about what updated but it's saner to perform a declarative render |
|
let aVertex = vec2.create(); |
|
let bVertex = vec2.create(); |
|
let cVertex = vec2.create(); |
|
let abVector = vec2.create(); |
|
let acVector = vec2.create(); |
|
function updateScene() { |
|
// Update our lines |
|
lineABEl.setAttribute('x1', vertexADraggie.position.x); |
|
lineABEl.setAttribute('y1', vertexADraggie.position.y); |
|
lineABEl.setAttribute('x2', vertexBDraggie.position.x); |
|
lineABEl.setAttribute('y2', vertexBDraggie.position.y); |
|
lineCXEl.setAttribute('x1', vertexCDraggie.position.x); |
|
lineCXEl.setAttribute('y1', vertexCDraggie.position.y); |
|
|
|
// Unwrap our positions into vectors |
|
vec2.set(aVertex, vertexADraggie.position.x, vertexADraggie.position.y); |
|
vec2.set(bVertex, vertexBDraggie.position.x, vertexBDraggie.position.y); |
|
vec2.set(cVertex, vertexCDraggie.position.x, vertexCDraggie.position.y); |
|
|
|
// Perform our distance to segment calculations |
|
// Resolve the vectors between our vertices and our point |
|
// DEV: This overwrites our reusable variable values |
|
vec2.sub(abVector, bVertex, aVertex); |
|
vec2.sub(acVector, cVertex, aVertex); |
|
|
|
// Compare how much our vectors overlap |
|
/* |
|
3 scenarios (simplified to exclude multiple axises): |
|
* | are projections, \ | / are closest lines |
|
|
|
"Behind" A |
|
C |
|
\ |
|
**A--------B |
|
|
|
Between A and B |
|
C |
|
| |
|
A****----B |
|
|
|
"Ahead of" B |
|
C |
|
/ |
|
A--------B** |
|
*/ |
|
// cos(θ) = percentage of AC that projects onto AB |
|
// https://en.wikipedia.org/wiki/Dot_product#/media/File:Dot_Product.svg |
|
// cos(θ) * |AC|/|AB| = ratio for how much AC overlaps AB |
|
// acAbRatio = |AC|*|AB|*cos(θ) / |AB|^2 = |AC|*cos(θ)/|AB| = cos(θ) * |AC|/|AB| |
|
// See deeper proof in https://gist.github.com/twolfson/207556d7ac0cd5d04fa283f6062841ab#finding-the-shortest-distance-from-a-point-to-asegment |
|
let baSquaredLength = vec2.squaredLength(abVector); |
|
let acAbRatio = vec2.dot(acVector, abVector) / baSquaredLength; |
|
|
|
// If our ratio/projection is "behind" AB, then update our line to point to A |
|
if (acAbRatio < 0.0) { |
|
lineCXEl.setAttribute('x2', vertexADraggie.position.x); |
|
lineCXEl.setAttribute('y2', vertexADraggie.position.y); |
|
// Otherwise, if our ratio/projection is "ahead of" AB, then update our line to point to B |
|
} else if (acAbRatio > 1.0) { |
|
lineCXEl.setAttribute('x2', vertexBDraggie.position.x); |
|
lineCXEl.setAttribute('y2', vertexBDraggie.position.y); |
|
// Otherwise (we're projecting between A and B), perform vector addition to find our projection |
|
} else { |
|
let cxX = aVertex[0] + acAbRatio * abVector[0]; |
|
let cxY = aVertex[1] + acAbRatio * abVector[1]; |
|
lineCXEl.setAttribute('x2', cxX); |
|
lineCXEl.setAttribute('y2', cxY); |
|
} |
|
} |
|
vertexADraggie.on('dragMove', updateScene); |
|
vertexBDraggie.on('dragMove', updateScene); |
|
vertexCDraggie.on('dragMove', updateScene); |
|
}); |