JavaScript preprocessors can help make authoring JavaScript easier and more convenient. For instance, CoffeeScript can help prevent easytomake mistakes and offer a cleaner syntax and Babel can bring ECMAScript 6 features to browsers that only support ECMAScript 5.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
<! Original code here https://gist.github.com/twolfson/5b1a1ed21a77bc0fdd215e80ee7225cb >
.container
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 zindicies
line#lineab.line(x1="120", y1="170", x2="280", y2="170",
strokewidth="10", stroke="#000")
line#linecx.line(x1="60", y1="260", x2="120", y2="170",
strokewidth="5", stroke="#999", strokedasharray="5,5")
circle#vertexa.vertex(cx="120", cy="170", r="20")
circle#vertexb.vertex(cx="280", cy="170", r="20")
circle#vertexc.vertex(cx="60", cy="260", r="20")
body {
/* https://github.com/corysimmons/typographic/blob/2.9.3/scss/typographic.scss#L34 */
fontfamily: 'Helvetica Neue', 'Helvetica', 'Arial', 'sansserif';
}
/* Medium screenshot specific offsets, no height seems to be present */
@media screen and (width: 1024px) {
.container {
width: 768px; /* 1024  2*(8  120  ~6)  visual inspection of orange dot in Medium */
margin: 0 auto;
paddingtop: 51px; /* 43px for Codepen text + 8px gutter width */
}
}
#scene {
border: 1px solid #000;
}
.vertex {
cursor: pointer;
stroke: #000;
strokewidth: 5px;
}
#vertexa {
fill: orange;
}
#vertexb {
fill: blue;
}
#vertexc {
fill: limegreen;
}
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('#vertexa');
let vertexBEl = sceneEl.querySelector('#vertexb');
let vertexCEl = sceneEl.querySelector('#vertexc');
let lineABEl = sceneEl.querySelector('#lineab');
let lineCXEl = sceneEl.querySelector('#linecx');
// 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#L184L194
draggie._getPosition = function() {
let x = parseFloat(this.element.getAttribute('cx'), 10);
let y = parseFloat(this.element.getAttribute('cy'), 10);
// Clean up 'auto' or other noninteger 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#L424L427
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#L418L422
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
\
**AB
Between A and B
C

A****B
"Ahead of" B
C
/
AB**
*/
// 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#findingtheshortestdistancefromapointtoasegment
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);
});
Also see: Tab Triggers