Create a gist now

Instantly share code, notes, and snippets.

@twolfson /.gitignore
Last active Oct 26, 2017

What would you like to do?
Interactive demo of distance to segment
node_modules/

gist-distance-to-segment

Interactive demo of distance to segment

Getting started

To get our repo working locally, run the following:

# Clone our repo
git clone git@gist.github.com:e196ea4ff1bcb3a6a56677e086b3c30f.git gist-distance-to-segment
cd gist-distance-to-segment

# Install our dependencies
npm install

# Start our development server
npm start
# serving /home/todd/github/gist-distance-to-segment on port 3000

Our development server will be running locally at http://localhost:3000/index.jade

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);
});
{
"name": "gist-distance-to-segment",
"version": "1.0.0",
"description": "Interactive demo of distance to segment",
"main": "index.js",
"scripts": {
"start": "serve .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Todd Wolfson <todd@twolfson.com> (http://twolfson.com/)",
"license": "Unlicense",
"devDependencies": {
"serve": "~1.4.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment