Trying to make a user-friendly UI for drawing SVG arcs.
A Pen by Andreas Borgen on CodePen.
<script> | |
var exports = exports || {}; | |
var module = module || {}; | |
</script> | |
<script __src='http://algebra.js.org/javascripts/algebra-0.2.6.min.js'></script> | |
<script src='https://cdn.rawgit.com/lovasoa/linear-solve/716f0f3c22fedc90f05d42f6d8dbc6bada4e4597/gauss-jordan.js'></script> | |
<h2>SVG arc path</h2> | |
<h3>Drag the blue and green dots</h3> | |
<div id="controls"> | |
<label> | |
<input type="radio" name="arc-mode" id="draw-circle" > | |
Circle | |
</label> | |
<br /> | |
<label> | |
<input type="radio" name="arc-mode" checked > | |
Ellipse | |
<label> | |
<input type="checkbox" name="ell-mode" id="draw-ell-large" /> | |
..large | |
</label> | |
<br /> | |
<label>(Mouse wheel to stretch)</label> | |
</label> | |
</div> | |
<svg width="500" height="500" _viewBox="-500,-500, 1000,1000" > | |
<path id="arc" /> | |
<circle id="start" class="dot" r="10" cx="200" cy="150" /> | |
<circle id="help" class="dot" r="10" cx="450" cy="300" /> | |
<circle id="end" class="dot" r="10" cx="250" cy="400" /> | |
<path id="end-arrow" d="M-25,-10 l25,10 -25,10" /> | |
<g id="helpers-ellipse"> | |
<circle id="stretch" class="dot" r="8" cx="300" cy="250" /> | |
<path id="arc-alternate" /> | |
<line class="tangent" /> | |
<line class="tangent" /> | |
<line class="tangent" id="stretch-path" /> | |
<circle id="temp1" class="dot temp" r="10" /> | |
<circle id="temp2" class="dot temp" r="10" /> | |
<circle class="dot debug" r="4" /> | |
<circle class="dot debug" r="4" /> | |
<circle class="dot debug" r="4" /> | |
<circle class="dot debug" r="4" /> | |
<circle class="dot debug" r="4" /> | |
<ellipse id="ell-temp" cx="100" cy="50" rx="60" ry="40" /> | |
</g> | |
</svg> | |
<pre><code></code></pre> |
//window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); }; | |
Array.from = Array.from || function(list) { return Array.prototype.slice.call(list); }; | |
Number.isFinite = Number.isFinite || function(value) { return (typeof value === 'number') && isFinite(value); } | |
const _drawCircle = document.querySelector('#draw-circle'), | |
_drawEllLarge = document.querySelector('#draw-ell-large'), | |
_svg = document.querySelector('svg'), | |
_pointStart = document.querySelector('#start'), | |
_pointHelp = document.querySelector('#help'), | |
_pointEnd = document.querySelector('#end'), | |
_arrow = document.querySelector('#end-arrow'), | |
_pointStretch = document.querySelector('#stretch'), | |
_stretchPath = document.querySelector('#stretch-path'), | |
_tangents = Array.from(document.querySelectorAll('.tangent')), | |
_arc = document.querySelector('#arc'), | |
_arcAlt = document.querySelector('#arc-alternate'), | |
_code = document.querySelector('code'); | |
const _ellTangentCloseness = .01, | |
_ellStretchMin = .1, | |
_ellStretchMax = .5, | |
_ellStretchStep = .01; | |
let _ellStretch = .4; | |
var _utils = { | |
circlePosition: function(circle, pos) { | |
if(pos) { | |
this.setSVGValue(circle.cx, pos.x); | |
this.setSVGValue(circle.cy, pos.y); | |
} | |
else { | |
return { x: circle.cx.baseVal.value, | |
y: circle.cy.baseVal.value }; | |
} | |
}, | |
drawLine: function(line, pStart, pEnd) { | |
this.setSVGValue(line.x1, pStart.x); | |
this.setSVGValue(line.y1, pStart.y); | |
this.setSVGValue(line.x2, pEnd.x); | |
this.setSVGValue(line.y2, pEnd.y); | |
}, | |
setSVGValue: function(property, value) { | |
property.baseVal.value = value; | |
}, | |
distance: function(p1, p2 = {x: 0, y: 0}) { | |
var dx = p2.x - p1.x, | |
dy = p2.y - p1.y; | |
return Math.sqrt(dx*dx + dy*dy); | |
}, | |
midPoint: function(p1, p2) { | |
var x = (p1.x + p2.x) / 2, | |
y = (p1.y + p2.y) / 2; | |
return {x, y}; | |
}, | |
angle: function(start, end = {x: 0, y: 0}, degrees = false) { | |
const dy = end.y - start.y, | |
dx = end.x - start.x; | |
let theta = Math.atan2(dy, dx); // range (-PI, PI] | |
if(degrees) { | |
theta *= 180 / Math.PI; // rads to degs, range (-180, 180] | |
} | |
//if (theta < 0) theta = 360 + theta; // range [0, 360) | |
return theta; | |
}, | |
// To find orientation of ordered triplet (p, q, r). | |
// The function returns following values | |
// 0 --> p, q and r are colinear | |
// 1 --> Clockwise | |
// 2 --> Counterclockwise | |
orientation: function(p, q, r) | |
{ | |
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/ | |
// for details of below formula. | |
const val = ((q.y - p.y) * (r.x - q.x)) - | |
((q.x - p.x) * (r.y - q.y)); | |
if (val === 0) return 0; // colinear | |
return (val > 0) ? 1 : 2; // clockwise or counterclockwise | |
}, | |
rotatePoint: function(point, radians, center = {x: 0, y: 0}) { | |
//The SVG label element is rotated clockwise around its top-left corner: | |
//http://stackoverflow.com/questions/17410809/how-to-calculate-rotation-in-2d-in-javascript | |
const x = point.x, | |
y = point.y, | |
cx = center.x, | |
cy = center.y, | |
cos = Math.cos(radians), | |
sin = Math.sin(radians); | |
const nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, | |
ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; | |
return {x: nx, y: ny}; | |
}, | |
//http://www.java2s.com/Code/Java/2D-Graphics-GUI/Returnsclosestpointonsegmenttopoint.htm | |
getClosestPointOnSegment: function(segmStart, segmEnd, point) | |
{ | |
function _pointOnSegment(sx1, sy1, sx2, sy2, px, py) | |
{ | |
const xDelta = sx2 - sx1, | |
yDelta = sy2 - sy1; | |
if ((xDelta == 0) && (yDelta == 0)) { throw 'Segment start equals segment end'; } | |
const u = ((px - sx1) * xDelta + (py - sy1) * yDelta) / (xDelta * xDelta + yDelta * yDelta); | |
let closestX, closestY; | |
if (u < 0) | |
{ | |
[closestX, closestY] = [sx1, sy1]; | |
} | |
else if (u > 1) | |
{ | |
[closestX, closestY] = [sx2, sy2]; | |
} | |
else | |
{ | |
[closestX, closestY] = [(sx1 + u * xDelta), (sy1 + u * yDelta)]; | |
} | |
return { x: closestX, y: closestY}; | |
} | |
return _pointOnSegment(segmStart.x, segmStart.y, | |
segmEnd.x, segmEnd.y, | |
point.x, point.y); | |
}, | |
arr2point: function(arr) { | |
return {x: arr[0], y: arr[1]}; | |
}, | |
point2arr: function(p) { | |
return [p.x, p.y]; | |
}, | |
bools2int: function(bools) { | |
return bools.map(b => b ? 1 : 0); | |
}, | |
stringRound: function(numStr) { | |
//Truncate numbers at two decimals, if number > 0 | |
return numStr.replace(/([1-9]\d*\.\d\d)\d+/g, '$1'); | |
}, | |
clamp: function(num, min, max) { | |
return (num <= min) ? min | |
: (num >= max) ? max : num; | |
}, | |
}; | |
function getPoints() { | |
var p1 = _utils.circlePosition(_pointStart), | |
p2 = _utils.circlePosition(_pointHelp), | |
p3 = _utils.circlePosition(_pointEnd); | |
return [p1, p2, p3]; | |
} | |
function updateTangents() { | |
const p = getPoints(), | |
origin = p[1], | |
overshoot = 1.4; | |
//_utils.drawLine(_tangents[0], p[0], p[1]); | |
//_utils.drawLine(_tangents[1], p[2], p[1]); | |
[p[0], p[2]].forEach((p, i) => { | |
const x = overshoot*(p.x-origin.x) + origin.x, | |
y = overshoot*(p.y-origin.y) + origin.y; | |
_utils.drawLine(_tangents[i], { x, y }, origin); | |
}) | |
//const endAngle = _utils.angle(p[1], p[2], true), | |
// endArr = _utils.point2arr(p[2]); | |
//_arrow.setAttribute('transform', `translate(${endArr}) rotate(${endAngle})`); | |
} | |
function getStretchPoint(stretch = _ellStretch) { | |
const [start, help, end] = getPoints(); | |
const tmp1 = { | |
x: start.x + (help.x-start.x)*stretch, | |
y: start.y + (help.y-start.y)*stretch, | |
}, | |
tmp2 = { | |
x: end.x + (help.x-end.x)*stretch, | |
y: end.y + (help.y-end.y)*stretch, | |
}, | |
stretchPoint = _utils.midPoint(tmp1, tmp2); | |
return stretchPoint; | |
} | |
function initUI() { | |
var dragged; | |
function touch2mouse(event) { | |
//Extract the Touch object and add the standard MouseEvent properties: | |
//http://www.javascriptkit.com/javatutors/touchevents.shtml | |
//https://developer.mozilla.org/en-US/docs/Web/API/Touch | |
var touch = event.changedTouches[0]; | |
touch.preventDefault = event.preventDefault.bind(event); | |
touch.buttons = 1; | |
return touch; | |
} | |
[_pointStart, _pointHelp, _pointEnd, _pointStretch].forEach(function(p) { | |
p.onmousedown = onDown.bind(p); | |
p.ontouchstart = function(e) { | |
onDown.call(p, touch2mouse(e)); | |
}; | |
}); | |
_svg.onmousemove = onMove; | |
_svg.ontouchmove = (e) => { | |
onMove(touch2mouse(e)); | |
}; | |
_svg.onmousewheel = onWheel; | |
function onDown(e) { | |
e.preventDefault(); | |
var mousePos = { x: e.clientX, y: e.clientY }; | |
var pointPos = _utils.circlePosition(this); | |
dragged = { | |
element: this, | |
offset: { x: pointPos.x-mousePos.x, y: pointPos.y-mousePos.y } | |
}; | |
} | |
function onMove(e) { | |
if(dragged && (e.buttons === 1)) { | |
e.preventDefault(); | |
var mousePos = { x: e.clientX, y: e.clientY }, | |
offset = dragged.offset, | |
point = dragged.element, | |
pointPos = { x: mousePos.x + offset.x, | |
y: mousePos.y + offset.y }; | |
_utils.circlePosition(point, pointPos); | |
updateTangents(); | |
if(point === _pointStretch) { | |
const a = getStretchPoint(_ellStretchMin), | |
b = getStretchPoint(_ellStretchMax), | |
stretchPos = _utils.getClosestPointOnSegment(a, b, pointPos); | |
const dx = Math.abs(a.x - b.x), | |
dy = Math.abs(a.y - b.y), | |
stretchFactor = (dx > dy) ? Math.abs(stretchPos.x - a.x)/dx | |
: Math.abs(stretchPos.y - a.y)/dy; | |
_ellStretch = (_ellStretchMax-_ellStretchMin)*stretchFactor + _ellStretchMin; | |
} | |
calculateArc(); | |
} | |
else { | |
dragged = null; | |
} | |
} | |
function onWheel(e) { | |
if(_drawCircle.checked) { | |
//Noop | |
} | |
else { | |
e.preventDefault(); | |
const incr = (e.deltaY > 0) ? 1 : -1; | |
_ellStretch += (incr * _ellStretchStep); | |
_ellStretch = _utils.clamp(_ellStretch, _ellStretchMin, _ellStretchMax); | |
//console.log('stretch', _ellStretch); | |
calculateArc(); | |
} | |
} | |
//Input controls | |
Array.from(document.querySelectorAll('#controls input')) | |
.forEach(input => { input.onchange = e => calculateArc(); }); | |
} | |
function calculateArc() { | |
_svg.classList.toggle('draw-circle', _drawCircle.checked); | |
if(_drawCircle.checked) { | |
calculateCircle(); | |
} | |
else { | |
const a = getStretchPoint(_ellStretchMin), | |
b = getStretchPoint(_ellStretchMax); | |
_utils.drawLine(_stretchPath, a, b); | |
_utils.circlePosition(_pointStretch, getStretchPoint()); | |
calculateEllipse(); | |
} | |
} | |
function calculateCircle() { | |
function calcNorm(pA, pB/*, chord, normal*/) { | |
//Avoid divide-by-zero on slopeNorm: | |
if(pA.y === pB.y) { | |
pA.y += .1; | |
} | |
//Get the linear function that goes through points A and B, | |
//and then the normal (perpendicular) line of that: | |
//https://en.wikibooks.org/wiki/Basic_Algebra/Lines_(Linear_Functions)/Find_the_Equation_of_the_Line_Using_Two_Points | |
var slopeChord = (pB.y - pA.y)/(pB.x - pA.x), | |
//The slope of the normal is the negative reciprocal of the original slope: | |
//http://www.mathwords.com/n/negative_reciprocal.htm | |
slopeNorm = -1/slopeChord, | |
//The normal crosses the center of the chord: | |
pointNorm = { x: (pB.x + pA.x)/2, | |
y: (pB.y + pA.y)/2 }; | |
//y = slope*x + b | |
//b = y - slope*x | |
var b = pointNorm.y - (slopeNorm*pointNorm.x), | |
result = { slope: slopeNorm, | |
b: b, | |
chordIntersect: pointNorm }; | |
/* | |
var pNorm1 = { x: 0, y: b }, | |
pNorm2 = { x: 500, y: 500*slopeNorm + b }; | |
_utils.drawLine(normal, pNorm1, pNorm2); | |
*/ | |
return result; | |
} | |
const [p1, p2, p3] = getPoints(), | |
n1 = calcNorm(p1, p2), | |
n2 = calcNorm(p2, p3); | |
//The center of the circle is where the two normals intersect: | |
if(n1.slope !== n2.slope) { | |
//Equation: | |
// y1 = y2 | |
// slope1*x + b1 = slope2*x + b2 | |
// x = b2 - b1 | |
// x = (b2 - b1)/(slope1-slope2) | |
// | |
var cx = (n2.b - n1.b)/(n1.slope - n2.slope), | |
cy = n1.slope*cx + n1.b, | |
c = { x: cx, y: cy }; | |
var r = _utils.distance(c, p1); | |
var helperOrientation = _utils.orientation(p1, p3, p2), | |
centerOrientation = _utils.orientation(p1, p3, c); | |
//console.log(helperOrientation, centerOrientation); | |
var large = (helperOrientation === centerOrientation), | |
sweep = (helperOrientation === 1); | |
drawArc([r,r], 0, [large,sweep]); | |
} | |
} | |
function calculateEllipse() { | |
// #1: Find the ellipse equation | |
// - 5 points: | |
// http://math.stackexchange.com/questions/632442/calculate-ellipse-from-points | |
// | |
// - 4 points and angle: | |
// http://mathforum.org/library/drmath/view/54485.html | |
// (From http://mathforum.org/library/drmath/sets/select/dm_ellipse.html) | |
// http://math.stackexchange.com/questions/891085/determine-ellipse-from-two-points-and-direction-vectors-at-those-points | |
// http://math.stackexchange.com/questions/109890/how-to-find-an-ellipse-given-2-passing-points-and-the-tangents-at-them | |
// | |
// #2: Find ellipse radii: | |
// http://www.dummies.com/education/math/calculus/how-to-graph-an-ellipse/ | |
// http://math.stackexchange.com/questions/1217796/compute-center-axes-and-rotation-from-equation-of-ellipse | |
/* | |
* Find the points we need to calculate an ellipse | |
*/ | |
const p = getPoints(), | |
start = p[0], | |
help = p[1], | |
end = p[2]; | |
//We need 5 points to calculate an ellipsis, i.e. start, end and then two more. | |
//Because start-help and end-help are tangents on the ellipse, | |
//we can approximate and extra point along each tangent, very close to start and end. | |
// | |
//The fifth point is a movable "stretch" point, close to the help handle: | |
const closeness = _ellTangentCloseness, | |
p3 = { | |
x: start.x + (help.x-start.x)*closeness, | |
y: start.y + (help.y-start.y)*closeness, | |
}, | |
p4 = { | |
x: end.x + (help.x-end.x)*closeness, | |
y: end.y + (help.y-end.y)*closeness, | |
}, | |
p5 = getStretchPoint(), | |
points = [start, end, p3, p4, p5]; | |
const debugDots = document.querySelectorAll('.dot.debug'); | |
points.forEach((p, i) => { | |
_utils.circlePosition(debugDots[i], p); | |
}); | |
/* | |
* Calculate the ellipse's properties | |
*/ | |
const data = LM_5P_Ellipse.apply(null, points.map(_utils.point2arr)); | |
if( !(data && Number.isFinite(data[40])) ) { return; } | |
const pCenter = _utils.arr2point(data[10]), | |
//Major axis length: | |
edge = _utils.arr2point(data[11]), | |
maxRad = _utils.distance(edge), | |
//Major axis inclination (rotation): | |
degs = _utils.angle(edge) * 180/Math.PI; | |
//console.log('angle2', degs.toFixed(2)); | |
/* | |
* Render the ellipse arc | |
*/ | |
const helperOrientation = _utils.orientation(p[0], p[2], p[1]), | |
large = _drawEllLarge.checked, | |
sweep = large ^/*xor*/ (helperOrientation === 1); | |
drawArc([maxRad, maxRad*data[40]], degs, [large, sweep]); | |
//DEBUG | |
// const ell = document.querySelector('#ell-temp'); | |
// _utils.circlePosition(ell, pCenter); | |
// _utils.setSVGValue(ell.rx, maxRad); | |
// _utils.setSVGValue(ell.ry, maxRad * data[40]); | |
// ell.setAttribute('transform', `rotate(${degs} ${data[10]})`); | |
//DEBUG | |
} | |
function drawArc(radii, angle, flags) { | |
var p = getPoints(), | |
flagsNum = _utils.bools2int(flags), | |
flagsNumAlt = _utils.bools2int(flags.map(f => !f)); | |
var arcData = `M${[p[0].x,p[0].y]} A${[radii[0],radii[1]]} ${angle} ${flagsNum} ${[p[2].x, p[2].y]}`; | |
_arc.setAttribute('d', arcData); | |
var arcAltData = `M${[p[0].x,p[0].y]} A${[radii[0],radii[1]]} ${angle} ${flagsNumAlt} ${[p[2].x, p[2].y]}`; | |
_arcAlt.setAttribute('d', arcAltData); | |
_code.textContent = _utils.stringRound(arcData); | |
} | |
initUI(); | |
updateTangents(); | |
calculateArc(); | |
// | |
// ellipse.c | |
// emptyExample | |
// | |
// Created by Oriol Ferrer Mesià on 21/02/13. | |
// https://github.com/armadillu/ofxEllipseSolver | |
// | |
// Ported to JS by Andreas Borgen. | |
// Removed handling of the 3rd (z index?) value in the input points (p0...4). | |
// | |
function toconic(p0, p1, p2, p3, p4) { | |
const L0 = [], L1 = [], L2 = [], L3 = []; | |
let A, B, C; | |
let a1, a2, b1, b2, c1, c2; | |
let x0, x4, y0, y4; | |
let y4y0, x4x0, y4x0, x4y0; | |
let a1a2, a1b2, a1c2, b1a2, b1b2, b1c2, c1a2, c1b2, c1c2; | |
let aa, bb, cc, dd, ee, ff; | |
function cross(a, b, ab) { | |
ab[0] = a[1] - b[1]; | |
ab[1] = b[0] - a[0]; | |
ab[2] = a[0]*b[1] - a[1]*b[0]; | |
} | |
cross(p0, p1, L0); | |
cross(p1, p2, L1); | |
cross(p2, p3, L2); | |
cross(p3, p4, L3); | |
A = L0[1]*L3[2] - L0[2]*L3[1]; | |
B = L0[2]*L3[0] - L0[0]*L3[2]; | |
C = L0[0]*L3[1] - L0[1]*L3[0]; | |
a1 = L1[0]; b1 = L1[1]; c1 = L1[2]; | |
a2 = L2[0]; b2 = L2[1]; c2 = L2[2]; | |
x0 = p0[0]; y0 = p0[1]; | |
x4 = p4[0]; y4 = p4[1]; | |
x4x0 = x4*x0; | |
x4y0 = x4*y0; | |
y4x0 = y4*x0; | |
y4y0 = y4*y0; | |
a1a2 = a1*a2; | |
a1b2 = a1*b2; | |
a1c2 = a1*c2; | |
b1a2 = b1*a2; | |
b1b2 = b1*b2; | |
b1c2 = b1*c2; | |
c1a2 = c1*a2; | |
c1b2 = c1*b2; | |
c1c2 = c1*c2; | |
aa = -A*a1a2*y4 | |
+ A*a1a2*y0 | |
- B*b1a2*y4 | |
- B*c1a2 | |
+ B*a1b2*y0 | |
+ B*a1c2 | |
+ C*b1a2*y4y0 | |
+ C*c1a2*y0 | |
- C*a1b2*y4y0 | |
- C*a1c2*y4; | |
cc = A*c1b2 | |
+ A*a1b2*x4 | |
- A*b1c2 | |
- A*b1a2*x0 | |
+ B*b1b2*x4 | |
- B*b1b2*x0 | |
+ C*b1c2*x4 | |
+ C*b1a2*x4x0 | |
- C*c1b2*x0 | |
- C*a1b2*x4x0; | |
ff = A*c1a2*y4x0 | |
+ A*c1b2*y4y0 | |
- A*a1c2*x4y0 | |
- A*b1c2*y4y0 | |
- B*c1a2*x4x0 | |
- B*c1b2*x4y0 | |
+ B*a1c2*x4x0 | |
+ B*b1c2*y4x0 | |
- C*c1c2*x4y0 | |
+ C*c1c2*y4x0; | |
bb = A*c1a2 | |
+ A*a1a2*x4 | |
- A*a1b2*y4 | |
- A*a1c2 | |
- A*a1a2*x0 | |
+ A*b1a2*y0 | |
+ B*b1a2*x4 | |
- B*b1b2*y4 | |
- B*c1b2 | |
- B*a1b2*x0 | |
+ B*b1b2*y0 | |
+ B*b1c2 | |
- C*b1c2*y4 | |
- C*b1a2*x4y0 | |
- C*b1a2*y4x0 | |
- C*c1a2*x0 | |
+ C*c1b2*y0 | |
+ C*a1b2*x4y0 | |
+ C*a1b2*y4x0 | |
+ C*a1c2*x4; | |
dd = -A*c1a2*y4 | |
+ A*a1a2*y4x0 | |
+ A*a1b2*y4y0 | |
+ A*a1c2*y0 | |
- A*a1a2*x4y0 | |
- A*b1a2*y4y0 | |
+ B*b1a2*y4x0 | |
+ B*c1a2*x0 | |
+ B*c1a2*x4 | |
+ B*c1b2*y0 | |
- B*a1b2*x4y0 | |
- B*a1c2*x0 | |
- B*a1c2*x4 | |
- B*b1c2*y4 | |
+ C*b1c2*y4y0 | |
+ C*c1c2*y0 | |
- C*c1a2*x4y0 | |
- C*c1b2*y4y0 | |
- C*c1c2*y4 | |
+ C*a1c2*y4x0; | |
ee = -A*c1a2*x0 | |
- A*c1b2*y4 | |
- A*c1b2*y0 | |
- A*a1b2*x4y0 | |
+ A*a1c2*x4 | |
+ A*b1c2*y4 | |
+ A*b1c2*y0 | |
+ A*b1a2*y4x0 | |
- B*b1a2*x4x0 | |
- B*b1b2*x4y0 | |
+ B*c1b2*x4 | |
+ B*a1b2*x4x0 | |
+ B*b1b2*y4x0 | |
- B*b1c2*x0 | |
- C*b1c2*x4y0 | |
+ C*c1c2*x4 | |
+ C*c1a2*x4x0 | |
+ C*c1b2*y4x0 | |
- C*c1c2*x0 | |
- C*a1c2*x4x0; | |
if (aa /*!= 0.0*/) { | |
bb /= aa; cc /= aa; dd /= aa; ee /= aa; ff /= aa; aa = 1.0; | |
} else if (bb /*!= 0.0*/) { | |
cc /= bb; dd /= bb; ee /= bb; ff /= bb; bb = 1.0; | |
} else if (cc /*!= 0.0*/) { | |
dd /= cc; ee /= cc; ff /= cc; cc = 1.0; | |
} else if (dd /*!= 0.0*/) { | |
ee /= dd; ff /= dd; dd = 1.0; | |
} else if (ee /*!= 0.0*/) { | |
ff /= ee; ee = 1.0; | |
} else { | |
return false; | |
} | |
return [aa, bb, cc, dd, ee, ff]; | |
} | |
//http://www.lee-mac.com/5pointellipse.html | |
// 5-Point Ellipse - Lee Mac | |
// Args: p1,p2,p3,p4,p5 - UCS points defining Ellipse | |
// Returns a list of: ((10 <WCS Center>) (11 <WCS Major Axis Endpoint from Center>) (40 . <Minor/Major Ratio>)) | |
// Version 1.1 - 2013-11-28 | |
//* | |
//(defun LM:5P-Ellipse ( p1 p2 p3 p4 p5 / a av b c cf cx cy d e f i m1 m2 rl v x ) | |
function LM_5P_Ellipse ( p1, p2, p3, p4, p5 ) { | |
//debugger | |
//console.log('LM_5P_Ellipse', p1, p2, p3, p4, p5); | |
/* | |
//(setq m1 | |
// (trp | |
// (mapcar | |
// (function | |
// (lambda ( p ) | |
// (list | |
// (* (car p) (car p)) | |
// (* (car p) (cadr p)) | |
// (* (cadr p) (cadr p)) | |
// (car p) | |
// (cadr p) | |
// 1.0 | |
// ) | |
// ) | |
// ) | |
// (list p1 p2 p3 p4 p5) | |
// ) | |
// ) | |
//) | |
const points = [p1, p2, p3, p4, p5], | |
matrix = points.map(p => [ | |
p[0] * p[0], | |
p[0] * p[1], | |
p[1] * p[1], | |
p[0], | |
p[1], | |
1 | |
]); | |
const m1 = trp(matrix); | |
//(setq i -1.0) | |
let i = -1; | |
//(repeat 6 | |
// (setq cf (cons (* (setq i (- i)) (detm (trp (append (reverse m2) (cdr m1))))) cf) | |
// m2 (cons (car m1) m2) | |
// m1 (cdr m1) | |
// ) | |
//) | |
let cf = [], m2 = []; | |
for(let x=0; x<6; x++) { | |
i = -i; | |
//Pop the first entry from the m1 matrix: | |
const m1First = m1.splice(0, 1)[0], | |
m2Rev = m2.slice().reverse(), | |
det = detm( trp(m2Rev.concat(m1)) ); | |
//console.log('det', det); | |
cf.unshift(i * det); | |
m2.unshift(m1First); | |
} | |
//console.log('cf', cf); | |
//(mapcar 'set '(f e d c b a) cf) ;; Coefficients of Conic equation ax^2 + bxy + cy^2 + dx + ey + f = 0 | |
const f = cf[0], | |
e = cf[1], | |
d = cf[2], | |
c = cf[3], | |
b = cf[4], | |
a = cf[5]; | |
*/ | |
//toconic() does the same as the above, and faster. | |
const [a, b, c, d, e, f] = toconic( p1, p2, p3, p4, p5 ); | |
const epsilon = 1e-8; | |
//(if (< 0 (setq x (- (* 4.0 a c) (* b b)))) | |
// (progn | |
const x = (4.0 * a * c) - (b * b); | |
//console.log('LM x', x); | |
if(0 < x) { | |
//(if (equal 0.0 b 1e-8) ;; Ellipse parallel to coordinate axes | |
// (setq av '((1.0 0.0) (0.0 1.0))) ;; Axis vectors | |
// (setq av | |
// (mapcar | |
// (function | |
// (lambda ( v / d ) | |
// (setq v (list (/ b 2.0) (- v a)) ;; Eigenvectors | |
// d (distance '(0.0 0.0) v) | |
// ) | |
// (mapcar '/ v (list d d)) | |
// ) | |
// ) | |
// (quad 1.0 (- (+ a c)) (- (* a c) (* 0.25 b b))) ;; Eigenvalues | |
// ) | |
// ) | |
//) | |
let av; | |
if(Math.abs(b) < epsilon) { | |
av = [[1.0, 0.0], [0.0, 1.0]]; | |
} | |
else { | |
av = quad( 1.0, -(a+c), (a*c) - (0.25*b*b) ).map(v => { | |
const vx = (b / 2.0), | |
vy = (v - a), | |
d = Math.sqrt(vx*vx + vy*vy); | |
return [vx/d, vy/d]; | |
}); | |
} | |
//(setq cx (/ (- (* b e) (* 2.0 c d)) x) ;; Ellipse Center | |
// cy (/ (- (* b d) (* 2.0 a e)) x) | |
//) | |
const cx = ((b * e) - (2.0 * c * d)) / x, | |
cy = ((b * d) - (2.0 * a * e)) / x; | |
//;; For radii, solve intersection of axis vectors with Conic Equation: | |
//;; ax^2 + bxy + cy^2 + dx + ey + f = 0 } | |
//;; x = cx + vx(t) }- solve for t | |
//;; y = cy + vy(t) } | |
//(setq rl | |
// (mapcar | |
// (function | |
// (lambda ( v / vv vx vy ) | |
// (setq vv (mapcar '* v v) | |
// vx (car v) | |
// vy (cadr v) | |
// ) | |
// (apply 'max | |
// (quad | |
// (+ (* a (car vv)) (* b vx vy) (* c (cadr vv))) | |
// (+ (* 2.0 a cx vx) (* b (+ (* cx vy) (* cy vx))) (* c 2.0 cy vy) (* d vx) (* e vy)) | |
// (+ (* a cx cx) (* b cx cy) (* c cy cy) (* d cx) (* e cy) f) | |
// ) | |
// ) | |
// ) | |
// ) | |
// av | |
// ) | |
//) | |
const rl = av.map(v => { | |
const //vv = v.map(x => x*x), | |
vx = v[0], | |
vy = v[1]; | |
const tempA = (a * vx*vx) + (b * vx * vy) + (c * vy*vy), | |
tempB = (2.0 * a * cx * vx) + (b * ((cx * vy) + (cy * vx))) + (c * 2.0 * cy * vy) + (d * vx) + (e * vy), | |
tempC = (a * cx * cx) + (b * cx * cy) + (c * cy * cy) + (d * cx) + (e * cy) + f, | |
q = quad(tempA, tempB, tempC); | |
//return Math.max.apply(Math, q); | |
return (q[0] > q[1]) ? q[0] : q[1]; | |
}); | |
//(if (apply '> rl) | |
// (setq rl (reverse rl) | |
// av (reverse av) | |
// ) | |
//) | |
if(rl[0] > rl[1]) { | |
rl.reverse(); | |
av.reverse(); | |
} | |
//(list | |
// (cons 10 (trans (list cx cy) 1 0)) ;; WCS Ellipse Center | |
// (cons 11 (trans (mapcar '(lambda ( v ) (* v (cadr rl))) (cadr av)) 1 0)) ;; WCS Major Axis Endpoint from Center | |
// (cons 40 (apply '/ rl)) ;; minor/major ratio | |
//) | |
function trans(list, from, to) { | |
//We don't operate on different coordinate systems, so we don't need to change anything here(?) | |
return list; | |
} | |
const center = [cx, cy], //trans([cx, cy], 1, 0), | |
av1 = av[1], | |
rl1 = rl[1], | |
axisEnd = [av1[0]*rl1, av1[1]*rl1], //trans(av[1].map(v => v*rl[1]), 1, 0), | |
axisRatio = rl[0] / rl1, | |
result = { | |
'10': center, | |
'11': axisEnd, | |
'40': axisRatio | |
}; | |
//console.log('result', JSON.stringify(result, null, 4)); | |
return result; | |
// ) | |
//) | |
} | |
//) | |
} | |
/* | |
*/ | |
//;; Matrix Determinant (Upper Triangular Form) - ElpanovEvgeniy | |
//;; Args: m - nxn matrix | |
//(defun detm ( m / d ) | |
// (cond | |
function detm(m) { | |
//debugger | |
return recursive_determinant(m); | |
/* | |
//( (null m) 1) | |
if(m.length == 0) { | |
return 1; | |
} | |
else { | |
//( (and (zerop (caar m)) | |
// (setq d (car (vl-member-if-not (function (lambda ( a ) (zerop (car a)))) (cdr m)))) | |
// ) | |
// (detm (cons (mapcar '+ (car m) d) (cdr m))) | |
//) | |
const mRest = m.slice(1), | |
notZeroStarters = mRest.filter(a => (a[0] !== 0)), | |
d = notZeroStarters.length ? notZeroStarters[0] : null; | |
if((m[0][0] === 0) && d) { | |
const newFirstRow = m[0].map((mm, i) => mm + d[i]), | |
m2 = [newFirstRow].concat(mRest); | |
return detm(m2); | |
} | |
//( (zerop (caar m)) 0) | |
else if(m[0][0] === 0) { | |
return 0; | |
} | |
//( (* (caar m) | |
// (detm | |
// (mapcar | |
// (function | |
// (lambda ( a / d ) | |
// (setq d (/ (car a) (float (caar m)))) | |
// (mapcar | |
// (function | |
// (lambda ( b c ) (- b (* c d))) | |
// ) | |
// (cdr a) (cdar m) | |
// ) | |
// ) | |
// ) | |
// (cdr m) | |
// ) | |
// ) | |
// ) | |
//) | |
else { | |
//https://forums.autodesk.com/t5/autocad-architecture/unknown-lisp-function-cdar/td-p/485574 | |
//CDAR is the same as (cdr (car x)) is it will take the first element of a list, and then return all but the first element of that list. | |
function cdar(x) { return x[0].slice(1); } | |
function innerLambda(b, c, d) { return (b - (c * d)); } | |
const m2 = mRest.map(a => { | |
const d = a[0] / m[0][0], | |
m1_1 = cdar(m); | |
//return a.map((b, i) => innerLambda(b, m1_1[i], d)); | |
return m1_1.map((c, i) => innerLambda(a[i], c, d)); | |
}); | |
return m[0][0] * detm(m2); | |
} | |
} | |
*/ | |
// ) | |
//) | |
} | |
/* | |
* https://github.com/VikParuchuri/vikparuchuri-affirm/blob/master/find-the-determinant-of-a-matrix.md | |
* Find the determinant in a recursive fashion. Very inefficient. | |
* X - Matrix object | |
*/ | |
function recursive_determinant(X) { | |
//#Must be a square matrix | |
//assert X.rows == X.cols | |
//#Must be at least 2x2 | |
//assert X.rows > 1 | |
//#If more than 2 rows, reduce and solve in a piecewise fashion | |
if (X.length > 2) { | |
const //termList = [], | |
cols = X[0].length; | |
let sum = 0; | |
for (let j = 0 ; j < cols; j++) { | |
//#Remove first row and column j | |
//new_x = deepcopy(X) | |
//del new_x[0] | |
//new_x.del_column(j) | |
// | |
const newX = X.slice(1).map(row => row.filter((x, i) => (i !== j))), | |
//#Find the multiplier | |
multiplier = X[0][j] * Math.pow(-1, (2+j)), | |
//#Recurse to find the determinant | |
det = recursive_determinant(newX); | |
//termList.push(multiplier * det); | |
sum += (multiplier * det); | |
} | |
return sum; //termList.reduce((a, b) => a + b); | |
} | |
else { | |
return (X[0][0]*X[1][1] - X[0][1]*X[1][0]); | |
} | |
} | |
//;; Matrix Transpose - Doug Wilson | |
//;; Args: m - nxn matrix | |
//(defun trp ( m ) | |
// (apply 'mapcar (cons 'list m)) | |
//) | |
function trp(m) { | |
//Normal matrix transpose? | |
//https://en.wikipedia.org/wiki/Transpose | |
const newRows = m[0].length, | |
newM = []; | |
for(let i=0; i<newRows; i++) { | |
newM.push(m.map(row => row[i])); | |
} | |
return newM; | |
} | |
//;; Quadratic Solution - Lee Mac | |
//;; Args: a,b,c - coefficients of ax^2 + bx + c = 0 | |
//(defun quad ( a b c / d r ) | |
// (if (<= 0 (setq d (- (* b b) (* 4.0 a c)))) | |
// (progn | |
// (setq r (sqrt d)) | |
// (list (/ (+ (- b) r) (* 2.0 a)) (/ (- (- b) r) (* 2.0 a))) | |
// ) | |
// ) | |
//) | |
function quad(a, b, c) { | |
const d = (b * b) - (4.0 * a * c); | |
if(0 <= d) { | |
const r = Math.sqrt(d); | |
return [ | |
((-b) + r) / (2.0 * a), | |
((-b) - r) / (2.0 * a) | |
]; | |
} | |
} |
//window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); }; | |
Array.from = Array.from || function(list) { return Array.prototype.slice.call(list); }; | |
var _drawCircle = document.querySelector('#draw-circle'), | |
_pointStart = document.querySelector('#start'), | |
_pointHelp = document.querySelector('#help'), | |
_pointEnd = document.querySelector('#end'), | |
_tangents = Array.from(document.querySelectorAll('.tangent')), | |
_arc = document.querySelector('#arc'), | |
_arcAlt = document.querySelector('#arc-alternate'), | |
_code = document.querySelector('code'); | |
var _utils = { | |
circlePosition: function(circle, pos) { | |
if(pos) { | |
this.setSVGValue(circle.cx, pos.x); | |
this.setSVGValue(circle.cy, pos.y); | |
} | |
else { | |
return { x: circle.cx.baseVal.value, | |
y: circle.cy.baseVal.value }; | |
} | |
}, | |
drawLine: function(line, pStart, pEnd) { | |
this.setSVGValue(line.x1, pStart.x); | |
this.setSVGValue(line.y1, pStart.y); | |
this.setSVGValue(line.x2, pEnd.x); | |
this.setSVGValue(line.y2, pEnd.y); | |
}, | |
setSVGValue: function(property, value) { | |
property.baseVal.value = value; | |
}, | |
distance: function(p1, p2) { | |
var dx = p2.x - p1.x, | |
dy = p2.y - p1.y; | |
return Math.sqrt(dx*dx + dy*dy); | |
}, | |
// To find orientation of ordered triplet (p, q, r). | |
// The function returns following values | |
// 0 --> p, q and r are colinear | |
// 1 --> Clockwise | |
// 2 --> Counterclockwise | |
orientation: function(p, q, r) | |
{ | |
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/ | |
// for details of below formula. | |
const val = ((q.y - p.y) * (r.x - q.x)) - | |
((q.x - p.x) * (r.y - q.y)); | |
if (val === 0) return 0; // colinear | |
return (val > 0) ? 1 : 2; // clockwise or counterclockwise | |
}, | |
bools2int: function(bools) { | |
return bools.map(b => b ? 1 : 0); | |
}, | |
}; | |
function getPoints() { | |
var p1 = _utils.circlePosition(_pointStart), | |
p2 = _utils.circlePosition(_pointHelp), | |
p3 = _utils.circlePosition(_pointEnd); | |
return [p1, p2, p3]; | |
} | |
function updateTangents() { | |
var p = getPoints(); | |
_utils.drawLine(_tangents[0], p[0], p[1]); | |
_utils.drawLine(_tangents[1], p[2], p[1]); | |
} | |
function initDrag() { | |
var svg = document.querySelector('svg'), | |
dragged; | |
function touch2mouse(event) { | |
//Extract the Touch object and add the standard MouseEvent properties: | |
//http://www.javascriptkit.com/javatutors/touchevents.shtml | |
//https://developer.mozilla.org/en-US/docs/Web/API/Touch | |
var touch = event.changedTouches[0]; | |
touch.preventDefault = event.preventDefault.bind(event); | |
touch.buttons = 1; | |
return touch; | |
} | |
[_pointStart, _pointHelp, _pointEnd].forEach(function(p) { | |
p.onmousedown = onDown.bind(p); | |
p.ontouchstart = function(e) { | |
onDown.call(p, touch2mouse(e)); | |
}; | |
}); | |
svg.onmousemove = onMove; | |
svg.ontouchmove = function(e) { | |
onMove(touch2mouse(e)); | |
}; | |
function onDown(e) { | |
e.preventDefault(); | |
var mousePos = { x: e.clientX, y: e.clientY }; | |
var pointPos = _utils.circlePosition(this); | |
dragged = { | |
element: this, | |
offset: { x: pointPos.x-mousePos.x, y: pointPos.y-mousePos.y } | |
}; | |
} | |
function onMove(e) { | |
if(dragged && (e.buttons === 1)) { | |
e.preventDefault(); | |
var mousePos = { x: e.clientX, y: e.clientY }, | |
offset = dragged.offset, | |
point = dragged.element; | |
_utils.circlePosition(point, { x: mousePos.x + offset.x, | |
y: mousePos.y + offset.y }); | |
updateTangents(); | |
calculateArc(); | |
} | |
else { | |
dragged = null; | |
} | |
} | |
} | |
function calculateArc() { | |
if(_drawCircle.checked) { | |
calculateCircle(); | |
} | |
else { | |
calculateEllipse(); | |
} | |
} | |
function calculateCircle() { | |
function calcNorm(pA, pB/*, chord, normal*/) { | |
//Avoid divide-by-zero on slopeNorm: | |
if(pA.y === pB.y) { | |
pA.y += .1; | |
} | |
//Get the linear function that goes through points A and B, | |
//and then the normal (perpendicular) line of that: | |
//https://en.wikibooks.org/wiki/Basic_Algebra/Lines_(Linear_Functions)/Find_the_Equation_of_the_Line_Using_Two_Points | |
var slopeChord = (pB.y - pA.y)/(pB.x - pA.x), | |
//The slope of the normal is the negative reciprocal of the original slope: | |
//http://www.mathwords.com/n/negative_reciprocal.htm | |
slopeNorm = -1/slopeChord, | |
//The normal crosses the center of the chord: | |
pointNorm = { x: (pB.x + pA.x)/2, | |
y: (pB.y + pA.y)/2 }; | |
//y = slope*x + b | |
//b = y - slope*x | |
var b = pointNorm.y - (slopeNorm*pointNorm.x), | |
result = { slope: slopeNorm, | |
b: b, | |
chordIntersect: pointNorm }; | |
/* | |
var pNorm1 = { x: 0, y: b }, | |
pNorm2 = { x: 500, y: 500*slopeNorm + b }; | |
_utils.drawLine(normal, pNorm1, pNorm2); | |
*/ | |
return result; | |
} | |
var p1 = _utils.circlePosition(_pointStart), | |
p2 = _utils.circlePosition(_pointHelp), | |
p3 = _utils.circlePosition(_pointEnd), | |
n1 = calcNorm(p1, p2), | |
n2 = calcNorm(p2, p3); | |
//The center of the circle is where the two normals intersect: | |
if(n1.slope !== n2.slope) { | |
//Equation: | |
// y1 = y2 | |
// slope1*x + b1 = slope2*x + b2 | |
// x = b2 - b1 | |
// x = (b2 - b1)/(slope1-slope2) | |
// | |
var cx = (n2.b - n1.b)/(n1.slope - n2.slope), | |
cy = n1.slope*cx + n1.b, | |
c = { x: cx, y: cy }; | |
var r = _utils.distance(c, p1); | |
var helperOrientation = _utils.orientation(p1, p3, p2), | |
centerOrientation = _utils.orientation(p1, p3, c); | |
//console.log(helperOrientation, centerOrientation); | |
var large = (helperOrientation === centerOrientation), | |
sweep = (helperOrientation === 1); | |
drawArc([r,r], 0, [large,sweep]); | |
} | |
} | |
function calculateEllipse() { | |
//TODO: | |
// 2 points w/tangents and an angle. | |
// | |
// #1: Find the ellipse equation | |
// http://mathforum.org/library/drmath/view/54485.html | |
// (From http://mathforum.org/library/drmath/sets/select/dm_ellipse.html) | |
// http://math.stackexchange.com/questions/891085/determine-ellipse-from-two-points-and-direction-vectors-at-those-points | |
// http://math.stackexchange.com/questions/109890/how-to-find-an-ellipse-given-2-passing-points-and-the-tangents-at-them | |
// | |
// #2: Find ellipse radii: | |
// http://www.dummies.com/education/math/calculus/how-to-graph-an-ellipse/ | |
// http://math.stackexchange.com/questions/1217796/compute-center-axes-and-rotation-from-equation-of-ellipse | |
drawArc([200,400], 30, [1,0]); | |
} | |
function drawArc(radii, angle, flags) { | |
var p = getPoints(), | |
flagsNum = _utils.bools2int(flags), | |
flagsNumAlt = _utils.bools2int(flags.map(f => !f)); | |
var arcData = `M${[p[0].x,p[0].y]} A${[radii[0],radii[1]]} ${angle} ${flagsNum} ${[p[2].x, p[2].y]}`; | |
_arc.setAttribute('d', arcData); | |
var arcAltData = `M${[p[0].x,p[0].y]} A${[radii[0],radii[1]]} ${angle} ${flagsNumAlt} ${[p[2].x, p[2].y]}`; | |
_arcAlt.setAttribute('d', arcAltData); | |
_code.textContent = arcData.replace(/(\d\.\d\d)\d+/g, '$1'); | |
} | |
initDrag(); | |
updateTangents(); | |
calculateArc(); |
body { | |
display: flex; | |
margin: 0; | |
min-height: 100vh; | |
font-family: Georgia, sans-serif; | |
flex-flow: column nowrap; | |
align-items: center; | |
justify-content: center; | |
h2, h3 { | |
margin: 0; | |
margin-bottom: .2em; | |
} | |
} | |
#controls { | |
margin: 1em 0; | |
label { | |
display: inline-block; | |
vertical-align: top; | |
cursor: pointer; | |
} | |
label:first-child { | |
margin-bottom: .5em; | |
} | |
label label { | |
margin-left: 1em; | |
font-size: .9em; | |
//opacity: .9; | |
} | |
} | |
svg { | |
//flex: 0 0 auto; | |
display: block; | |
border: 1px solid gainsboro; | |
background: lightyellow; | |
&.draw-circle #helpers-ellipse { | |
display: none; | |
} | |
#helpers-ellipse { | |
pointer-events: none; | |
} | |
#arc, #arc-alternate, #end-arrow { | |
stroke-width: 2; | |
stroke: black; | |
fill: none; | |
} | |
#arc-alternate { | |
opacity: .1; | |
stroke-dasharray: 10; | |
} | |
.tangent, #ell-temp { | |
stroke-width: 1; | |
stroke-dasharray: 6 4; | |
stroke: salmon; | |
fill: none; | |
} | |
#ell-temp, .dot.temp { | |
//stroke: yellow; | |
display: none; | |
} | |
.dot { | |
stroke-width: 10; | |
stroke: lime; | |
fill: transparent; | |
opacity: .6; | |
cursor: pointer; | |
&#help { | |
stroke: dodgerblue; | |
} | |
&#end { | |
fill: black; | |
} | |
&#stretch { | |
//display: none; | |
stroke: dodgerblue; | |
stroke-width: 6; | |
pointer-events: auto; | |
} | |
&.temp { | |
stroke: gold; | |
} | |
&.debug { | |
stroke: salmon; | |
stroke-width: 1; | |
&.help { | |
stroke: gray; | |
} | |
&.p5 { | |
//stroke: gray; | |
//stroke-width: 5; | |
} | |
} | |
} | |
} | |
code { | |
font-size: 22px; | |
} |
Trying to make a user-friendly UI for drawing SVG arcs.
A Pen by Andreas Borgen on CodePen.