<!-- Original code here https://gist.github.com/twolfson/5b1a1ed21a77bc0fdd215e80ee7225cb -->
.container
  p Click and drag a circle to see the our projection vector 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--a-b.line(x1="120", y1="170", x2="280", y2="170",
      stroke-width="10", stroke="#000")
    line#line--a-c.line(x1="60", y1="260", x2="120", y2="170",
      stroke-width="10", stroke="#000")
    line#line--a-c-prime.line(x1="60", y1="170", x2="120", y2="170",
      stroke-width="10", stroke="#F00")
    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")
    circle#vertex--c-prime.vertex.vertex--disabled(cx="60", cy="170", r="20")
View Compiled
body {
  /* https://github.com/corysimmons/typographic/blob/2.9.3/scss/typographic.scss#L34 */
  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif';
}

/* 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;
    padding-top: 51px; /* 43px for Codepen text + 8px gutter width */
  } 
}

#scene {
  border: 1px solid #000;
}

.vertex {
  cursor: pointer;
  stroke: #000;
  stroke-width: 5px;
}
.vertex--disabled {
  cursor: not-allowed;
}

#vertex--a {
  fill: orange;
}
#vertex--b {
  fill: blue;
}
#vertex--c,
#vertex--c-prime {
  fill: limegreen;
}
#vertex--c-prime {
  fill: grey;
  stroke: grey;
}
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 vertexCPrimeEl = sceneEl.querySelector('#vertex--c-prime');
  let lineABEl = sceneEl.querySelector('#line--a-b');
  let lineACEl = sceneEl.querySelector('#line--a-c');
  let lineACPrimeEl = sceneEl.querySelector('#line--a-c-prime');

  // 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();
  let abUnitVector = vec2.create();
  let acPrimeVector = 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);
    lineACEl.setAttribute('x1', vertexADraggie.position.x);
    lineACEl.setAttribute('y1', vertexADraggie.position.y);
    lineACEl.setAttribute('x2', vertexCDraggie.position.x);
    lineACEl.setAttribute('y2', vertexCDraggie.position.y);
    lineACPrimeEl.setAttribute('x1', vertexADraggie.position.x);
    lineACPrimeEl.setAttribute('y1', vertexADraggie.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);
    vec2.sub(abVector, bVertex, aVertex);
    vec2.sub(acVector, cVertex, aVertex);

    // Lay out our mathematical logic
    /*
      axis
      |
      x offshoot
      x/
      origin

      x's = axisProjection (from offshoot)
    */
    // Knowns:
    //   cos(θ) = direction * |axisProjection| / |offshoot|
    //   axisUnitVector = direction * axisProjectionUnitVector
    //   axisUnitVector = axisVector / |axis|
    //   axisProjectionVector = axisProjectionUnitVector * |axisProjection|
    //   dot = |axis||offshoot|cos(θ)
    // Formula:
    //   dot = |axis||offshoot|cos(θ)
    //     = |axis||offshoot|(direction * |axisProjection|/|offshoot|)
    //     = |axis||axisProjection| * direction
    //   Therefore: |axisProjection| = dot/(|axis|*direction)
    //   goal: axisProjectionVector
    //     = axisProjectionUnitVector * |axisProjection|
    //     = direction * axisUnitVector * dot/(|axis| * direction)
    //     = axisVector/|axis| * dot/|axis|
    //     = axisVector * (dot/|axis|^2)

    // Perform our calculation of the projection vector
    // axisVector = abVector
    // offshootVector = acVector
    let abAcDotProduct = vec2.dot(abVector, acVector);
    // DEV: We prefer squared length as there's no square root during calculation so more precision
    let abSquaredLength = vec2.squaredLength(abVector);
    vec2.scale(acPrimeVector, abVector, abAcDotProduct / abSquaredLength);

    // Update our line and projection dot
    lineACPrimeEl.setAttribute('x2', aVertex[0] + acPrimeVector[0]);
    lineACPrimeEl.setAttribute('y2', aVertex[1] + acPrimeVector[1]);
    vertexCPrimeEl.setAttribute('cx', aVertex[0] + acPrimeVector[0]);
    vertexCPrimeEl.setAttribute('cy', aVertex[1] + acPrimeVector[1]);
  }
  vertexADraggie.on('dragMove', updateScene);
  vertexBDraggie.on('dragMove', updateScene);
  vertexCDraggie.on('dragMove', updateScene);
});
Rerun