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.