Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active March 15, 2017 00:56
Show Gist options
  • Save Sphinxxxx/740f6ed7da9b3329546fa76b4a7c0a65 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/740f6ed7da9b3329546fa76b4a7c0a65 to your computer and use it in GitHub Desktop.
SVG arc path UI
<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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment