|
doctype |
|
html |
|
head |
|
meta(charset="utf-8") |
|
title gist-orthogonal-vector |
|
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--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; |
|
} |
|
body |
|
p Click and drag a circle to see the our projection and orthogonal 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") |
|
line#line--c-c-prime.line(x1="60", y1="260", x2="60", y2="170", |
|
stroke-width="10", stroke="limegreen") |
|
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") |
|
|
|
//- 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 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'); |
|
let lineCCPrimeEl = sceneEl.querySelector('#line--c-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 acPrimeVector = vec2.create(); |
|
let ccPrimeVector = 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); |
|
lineCCPrimeEl.setAttribute('x1', vertexCDraggie.position.x); |
|
lineCCPrimeEl.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); |
|
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); |
|
|
|
// Perform vector addition to find our orthogonal vector |
|
vec2.sub(ccPrimeVector, acVector, acPrimeVector); |
|
|
|
// Update our line and projection dot |
|
lineACPrimeEl.setAttribute('x2', aVertex[0] + acPrimeVector[0]); |
|
lineACPrimeEl.setAttribute('y2', aVertex[1] + acPrimeVector[1]); |
|
// DEV: We could use `acPrimeVector` but that defeats the point |
|
lineCCPrimeEl.setAttribute('x2', cVertex[0] - ccPrimeVector[0]); |
|
lineCCPrimeEl.setAttribute('y2', cVertex[1] - ccPrimeVector[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); |
|
}); |