|  | <!DOCTYPE html> | 
|  |  | 
|  | <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> | 
|  | <head> | 
|  | <meta charset="utf-8" /> | 
|  | <title></title> | 
|  | <div style="height:0"> | 
|  |  | 
|  | <div id="cubics"> | 
|  | {{{fX=124.70011901855469 fY=9.3718261718750000 } {fX=124.66775026544929 fY=9.3744316215161234 } {fX=124.63530969619751 fY=9.3770743012428284 }{fX=124.60282897949219 fY=9.3797206878662109 } id=10 | 
|  | {{{fX=124.70011901855469 fY=9.3718004226684570 } {fX=124.66775026544929 fY=9.3744058723095804 } {fX=124.63530969619751 fY=9.3770485520362854 } {fX=124.60282897949219 fY=9.3796949386596680 } id=1 | 
|  | {{{fX=124.70011901855469 fY=9.3718004226684570 } {fX=124.66786243087600 fY=9.3743968522034287 } {fX=124.63553249625420 fY=9.3770303056986286 } {fX=124.60316467285156 fY=9.3796672821044922 } id=2 | 
|  | </div> | 
|  |  | 
|  | </div> | 
|  |  | 
|  | <script type="text/javascript"> | 
|  |  | 
|  | var testDivs = [ | 
|  | cubics, | 
|  | ]; | 
|  |  | 
|  | var decimal_places = 3; | 
|  |  | 
|  | var tests = []; | 
|  | var testTitles = []; | 
|  | var testIndex = 0; | 
|  | var ctx; | 
|  |  | 
|  | var subscale = 1; | 
|  | var xmin, xmax, ymin, ymax; | 
|  | var hscale, vscale; | 
|  | var hinitScale, vinitScale; | 
|  | var uniformScale = true; | 
|  | var mouseX, mouseY; | 
|  | var mouseDown = false; | 
|  | var srcLeft, srcTop; | 
|  | var screenWidth, screenHeight; | 
|  | var drawnPts; | 
|  | var curveT = 0; | 
|  | var curveW = -1; | 
|  |  | 
|  | var lastX, lastY; | 
|  | var activeCurve = []; | 
|  | var activePt; | 
|  | var ids = []; | 
|  |  | 
|  | var focus_on_selection = 0; | 
|  | var draw_t = false; | 
|  | var draw_w = false; | 
|  | var draw_closest_t = false; | 
|  | var draw_cubic_red = false; | 
|  | var draw_derivative = false; | 
|  | var draw_endpoints = 2; | 
|  | var draw_id = 0; | 
|  | var draw_midpoint = 0; | 
|  | var draw_mouse_xy = false; | 
|  | var draw_order = false; | 
|  | var draw_point_xy = false; | 
|  | var draw_ray_intersect = false; | 
|  | var draw_quarterpoint = 0; | 
|  | var draw_tangents = 1; | 
|  | var draw_sortpoint = 0; | 
|  | var retina_scale = !!window.devicePixelRatio; | 
|  |  | 
|  | function parse(test, title) { | 
|  | var curveStrs = test.split("{{"); | 
|  | var pattern = /-?\d+\.*\d*e?-?\d*/g; | 
|  | var curves = []; | 
|  | for (var c in curveStrs) { | 
|  | var curveStr = curveStrs[c]; | 
|  | var idPart = curveStr.split("id="); | 
|  | var id = -1; | 
|  | if (idPart.length == 2) { | 
|  | id = parseInt(idPart[1]); | 
|  | curveStr = idPart[0]; | 
|  | } | 
|  | var points = curveStr.match(pattern); | 
|  | var pts = []; | 
|  | for (var wd in points) { | 
|  | var num = parseFloat(points[wd]); | 
|  | if (isNaN(num)) continue; | 
|  | pts.push(num); | 
|  | } | 
|  | if (pts.length > 2) { | 
|  | curves.push(pts); | 
|  | } | 
|  | if (id >= 0) { | 
|  | ids.push(id); | 
|  | ids.push(pts); | 
|  | } | 
|  | } | 
|  | if (curves.length >= 1) { | 
|  | tests.push(curves); | 
|  | testTitles.push(title); | 
|  | } | 
|  | } | 
|  |  | 
|  | function init(test) { | 
|  | var canvas = document.getElementById('canvas'); | 
|  | if (!canvas.getContext) return; | 
|  | ctx = canvas.getContext('2d'); | 
|  | var resScale = retina_scale && window.devicePixelRatio ? window.devicePixelRatio : 1; | 
|  | var unscaledWidth = window.innerWidth - 20; | 
|  | var unscaledHeight = window.innerHeight - 20; | 
|  | screenWidth = unscaledWidth; | 
|  | screenHeight = unscaledHeight; | 
|  | canvas.width = unscaledWidth * resScale; | 
|  | canvas.height = unscaledHeight * resScale; | 
|  | canvas.style.width = unscaledWidth + 'px'; | 
|  | canvas.style.height = unscaledHeight + 'px'; | 
|  | if (resScale != 1) { | 
|  | ctx.scale(resScale, resScale); | 
|  | } | 
|  | xmin = Infinity; | 
|  | xmax = -Infinity; | 
|  | ymin = Infinity; | 
|  | ymax = -Infinity; | 
|  | for (var curves in test) { | 
|  | var curve = test[curves]; | 
|  | var last = curve.length - (curve.length % 2 == 1 ? 1 : 0); | 
|  | for (var idx = 0; idx < last; idx += 2) { | 
|  | xmin = Math.min(xmin, curve[idx]); | 
|  | xmax = Math.max(xmax, curve[idx]); | 
|  | ymin = Math.min(ymin, curve[idx + 1]); | 
|  | ymax = Math.max(ymax, curve[idx + 1]); | 
|  | } | 
|  | } | 
|  | xmin -= Math.min(1, Math.max(xmax - xmin, ymax - ymin)); | 
|  | var testW = xmax - xmin; | 
|  | var testH = ymax - ymin; | 
|  | subscale = 1; | 
|  | while (testW * subscale < 0.1 && testH * subscale < 0.1) { | 
|  | subscale *= 10; | 
|  | } | 
|  | while (testW * subscale > 10 && testH * subscale > 10) { | 
|  | subscale /= 10; | 
|  | } | 
|  | setScale(xmin, xmax, ymin, ymax); | 
|  | mouseX = (screenWidth / 2) / hscale + srcLeft; | 
|  | mouseY = (screenHeight / 2) / vscale + srcTop; | 
|  | hinitScale = hscale; | 
|  | vinitScale = vscale; | 
|  | } | 
|  |  | 
|  | function setScale(x0, x1, y0, y1) { | 
|  | var srcWidth = x1 - x0; | 
|  | var srcHeight = y1 - y0; | 
|  | var usableWidth = screenWidth; | 
|  | var xDigits = Math.ceil(Math.log(Math.abs(xmax)) / Math.log(10)); | 
|  | var yDigits = Math.ceil(Math.log(Math.abs(ymax)) / Math.log(10)); | 
|  | usableWidth -= (xDigits + yDigits) * 10; | 
|  | usableWidth -= decimal_places * 10; | 
|  | hscale = usableWidth / srcWidth; | 
|  | vscale = screenHeight / srcHeight; | 
|  | if (uniformScale) { | 
|  | hscale = Math.min(hscale, vscale); | 
|  | vscale = hscale; | 
|  | } | 
|  | var hinvScale = 1 / hscale; | 
|  | var vinvScale = 1 / vscale; | 
|  | var sxmin = x0 - hinvScale * 5; | 
|  | var symin = y0 - vinvScale * 10; | 
|  | var sxmax = x1 + hinvScale * (6 * decimal_places + 10); | 
|  | var symax = y1 + vinvScale * 10; | 
|  | srcWidth = sxmax - sxmin; | 
|  | srcHeight = symax - symin; | 
|  | hscale = usableWidth / srcWidth; | 
|  | vscale = screenHeight / srcHeight; | 
|  | if (uniformScale) { | 
|  | hscale = Math.min(hscale, vscale); | 
|  | vscale = hscale; | 
|  | } | 
|  | srcLeft = sxmin; | 
|  | srcTop = symin; | 
|  | } | 
|  |  | 
|  | function dxy_at_t(curve, t) { | 
|  | var dxy = {}; | 
|  | if (curve.length == 6) { | 
|  | var a = t - 1; | 
|  | var b = 1 - 2 * t; | 
|  | var c = t; | 
|  | dxy.x = a * curve[0] + b * curve[2] + c * curve[4]; | 
|  | dxy.y = a * curve[1] + b * curve[3] + c * curve[5]; | 
|  | } else if (curve.length == 7) { | 
|  | var p20x = curve[4] - curve[0]; | 
|  | var p20y = curve[5] - curve[1]; | 
|  | var p10xw = (curve[2] - curve[0]) * curve[6]; | 
|  | var p10yw = (curve[3] - curve[1]) * curve[6]; | 
|  | var coeff0x = curve[6] * p20x - p20x; | 
|  | var coeff0y = curve[6] * p20y - p20y; | 
|  | var coeff1x = p20x - 2 * p10xw; | 
|  | var coeff1y = p20y - 2 * p10yw; | 
|  | dxy.x = t * (t * coeff0x + coeff1x) + p10xw; | 
|  | dxy.y = t * (t * coeff0y + coeff1y) + p10yw; | 
|  | } else if (curve.length == 8) { | 
|  | var one_t = 1 - t; | 
|  | var a = curve[0]; | 
|  | var b = curve[2]; | 
|  | var c = curve[4]; | 
|  | var d = curve[6]; | 
|  | dxy.x = 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t); | 
|  | a = curve[1]; | 
|  | b = curve[3]; | 
|  | c = curve[5]; | 
|  | d = curve[7]; | 
|  | dxy.y = 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t); | 
|  | } | 
|  | return dxy; | 
|  | } | 
|  |  | 
|  | var flt_epsilon = 1.19209290E-07; | 
|  |  | 
|  | function approximately_zero(A) { | 
|  | return Math.abs(A) < flt_epsilon; | 
|  | } | 
|  |  | 
|  | function approximately_zero_inverse(A) { | 
|  | return Math.abs(A) > (1 / flt_epsilon); | 
|  | } | 
|  |  | 
|  | function quad_real_roots(A, B, C) { | 
|  | var s = []; | 
|  | var p = B / (2 * A); | 
|  | var q = C / A; | 
|  | if (approximately_zero(A) && (approximately_zero_inverse(p) | 
|  | || approximately_zero_inverse(q))) { | 
|  | if (approximately_zero(B)) { | 
|  | if (C == 0) { | 
|  | s[0] = 0; | 
|  | } | 
|  | return s; | 
|  | } | 
|  | s[0] = -C / B; | 
|  | return s; | 
|  | } | 
|  | /* normal form: x^2 + px + q = 0 */ | 
|  | var p2 = p * p; | 
|  | if (!approximately_zero(p2 - q) && p2 < q) { | 
|  | return s; | 
|  | } | 
|  | var sqrt_D = 0; | 
|  | if (p2 > q) { | 
|  | sqrt_D = Math.sqrt(p2 - q); | 
|  | } | 
|  | s[0] = sqrt_D - p; | 
|  | var flip = -sqrt_D - p; | 
|  | if (!approximately_zero(s[0] - flip)) { | 
|  | s[1] = flip; | 
|  | } | 
|  | return s; | 
|  | } | 
|  |  | 
|  | function cubic_real_roots(A, B, C, D) { | 
|  | if (approximately_zero(A)) {  // we're just a quadratic | 
|  | return quad_real_roots(B, C, D); | 
|  | } | 
|  | if (approximately_zero(D)) {  // 0 is one root | 
|  | var s = quad_real_roots(A, B, C); | 
|  | for (var i = 0; i < s.length; ++i) { | 
|  | if (approximately_zero(s[i])) { | 
|  | return s; | 
|  | } | 
|  | } | 
|  | s.push(0); | 
|  | return s; | 
|  | } | 
|  | if (approximately_zero(A + B + C + D)) {  // 1 is one root | 
|  | var s = quad_real_roots(A, A + B, -D); | 
|  | for (var i = 0; i < s.length; ++i) { | 
|  | if (approximately_zero(s[i] - 1)) { | 
|  | return s; | 
|  | } | 
|  | } | 
|  | s.push(1); | 
|  | return s; | 
|  | } | 
|  | var a, b, c; | 
|  | var invA = 1 / A; | 
|  | a = B * invA; | 
|  | b = C * invA; | 
|  | c = D * invA; | 
|  | var a2 = a * a; | 
|  | var Q = (a2 - b * 3) / 9; | 
|  | var R = (2 * a2 * a - 9 * a * b + 27 * c) / 54; | 
|  | var R2 = R * R; | 
|  | var Q3 = Q * Q * Q; | 
|  | var R2MinusQ3 = R2 - Q3; | 
|  | var adiv3 = a / 3; | 
|  | var r; | 
|  | var roots = []; | 
|  | if (R2MinusQ3 < 0) {   // we have 3 real roots | 
|  | var theta = Math.acos(R / Math.sqrt(Q3)); | 
|  | var neg2RootQ = -2 * Math.sqrt(Q); | 
|  | r = neg2RootQ * Math.cos(theta / 3) - adiv3; | 
|  | roots.push(r); | 
|  | r = neg2RootQ * Math.cos((theta + 2 * Math.PI) / 3) - adiv3; | 
|  | if (!approximately_zero(roots[0] - r)) { | 
|  | roots.push(r); | 
|  | } | 
|  | r = neg2RootQ * Math.cos((theta - 2 * Math.PI) / 3) - adiv3; | 
|  | if (!approximately_zero(roots[0] - r) && (roots.length == 1 | 
|  | || !approximately_zero(roots[1] - r))) { | 
|  | roots.push(r); | 
|  | } | 
|  | } else {  // we have 1 real root | 
|  | var sqrtR2MinusQ3 = Math.sqrt(R2MinusQ3); | 
|  | var A = Math.abs(R) + sqrtR2MinusQ3; | 
|  | A = Math.pow(A, 1/3); | 
|  | if (R > 0) { | 
|  | A = -A; | 
|  | } | 
|  | if (A != 0) { | 
|  | A += Q / A; | 
|  | } | 
|  | r = A - adiv3; | 
|  | roots.push(r); | 
|  | if (approximately_zero(R2 - Q3)) { | 
|  | r = -A / 2 - adiv3; | 
|  | if (!approximately_zero(roots[0] - r)) { | 
|  | roots.push(r); | 
|  | } | 
|  | } | 
|  | } | 
|  | return roots; | 
|  | } | 
|  |  | 
|  | function approximately_zero_or_more(tValue) { | 
|  | return tValue >= -flt_epsilon; | 
|  | } | 
|  |  | 
|  | function approximately_one_or_less(tValue) { | 
|  | return tValue <= 1 + flt_epsilon; | 
|  | } | 
|  |  | 
|  | function approximately_less_than_zero(tValue) { | 
|  | return tValue < flt_epsilon; | 
|  | } | 
|  |  | 
|  | function approximately_greater_than_one(tValue) { | 
|  | return tValue > 1 - flt_epsilon; | 
|  | } | 
|  |  | 
|  | function add_valid_ts(s) { | 
|  | var t = []; | 
|  | nextRoot: | 
|  | for (var index = 0; index < s.length; ++index) { | 
|  | var tValue = s[index]; | 
|  | if (approximately_zero_or_more(tValue) && approximately_one_or_less(tValue)) { | 
|  | if (approximately_less_than_zero(tValue)) { | 
|  | tValue = 0; | 
|  | } else if (approximately_greater_than_one(tValue)) { | 
|  | tValue = 1; | 
|  | } | 
|  | for (var idx2 = 0; idx2 < t.length; ++idx2) { | 
|  | if (approximately_zero(t[idx2] - tValue)) { | 
|  | continue nextRoot; | 
|  | } | 
|  | } | 
|  | t.push(tValue); | 
|  | } | 
|  | } | 
|  | return t; | 
|  | } | 
|  |  | 
|  | function quad_roots(A, B, C) { | 
|  | var s = quad_real_roots(A, B, C); | 
|  | var foundRoots = add_valid_ts(s); | 
|  | return foundRoots; | 
|  | } | 
|  |  | 
|  | function cubic_roots(A, B, C, D) { | 
|  | var s = cubic_real_roots(A, B, C, D); | 
|  | var foundRoots = add_valid_ts(s); | 
|  | return foundRoots; | 
|  | } | 
|  |  | 
|  | function ray_curve_intersect(startPt, endPt, curve) { | 
|  | var adj = endPt[0] - startPt[0]; | 
|  | var opp = endPt[1] - startPt[1]; | 
|  | var r = []; | 
|  | var len = (curve.length == 7 ? 6 : curve.length) / 2; | 
|  | for (var n = 0; n < len; ++n) { | 
|  | r[n] = (curve[n * 2 + 1] - startPt[1]) * adj - (curve[n * 2] - startPt[0]) * opp; | 
|  | } | 
|  | if (curve.length == 6) { | 
|  | var A = r[2]; | 
|  | var B = r[1]; | 
|  | var C = r[0]; | 
|  | A += C - 2 * B;  // A = a - 2*b + c | 
|  | B -= C;  // B = -(b - c) | 
|  | return quad_roots(A, 2 * B, C); | 
|  | } | 
|  | if (curve.length == 7) { | 
|  | var A = r[2]; | 
|  | var B = r[1] * curve[6]; | 
|  | var C = r[0]; | 
|  | A += C - 2 * B;  // A = a - 2*b + c | 
|  | B -= C;  // B = -(b - c) | 
|  | return quad_roots(A, 2 * B, C); | 
|  | } | 
|  | var A = r[3];       // d | 
|  | var B = r[2] * 3;   // 3*c | 
|  | var C = r[1] * 3;   // 3*b | 
|  | var D = r[0];       // a | 
|  | A -= D - C + B;     // A =   -a + 3*b - 3*c + d | 
|  | B += 3 * D - 2 * C; // B =  3*a - 6*b + 3*c | 
|  | C -= 3 * D;         // C = -3*a + 3*b | 
|  | return cubic_roots(A, B, C, D); | 
|  | } | 
|  |  | 
|  | function x_at_t(curve, t) { | 
|  | var one_t = 1 - t; | 
|  | if (curve.length == 4) { | 
|  | return one_t * curve[0] + t * curve[2]; | 
|  | } | 
|  | var one_t2 = one_t * one_t; | 
|  | var t2 = t * t; | 
|  | if (curve.length == 6) { | 
|  | return one_t2 * curve[0] + 2 * one_t * t * curve[2] + t2 * curve[4]; | 
|  | } | 
|  | if (curve.length == 7) { | 
|  | var numer = one_t2 * curve[0] + 2 * one_t * t * curve[2] * curve[6] | 
|  | + t2 * curve[4]; | 
|  | var denom = one_t2            + 2 * one_t * t            * curve[6] | 
|  | + t2; | 
|  | return numer / denom; | 
|  | } | 
|  | var a = one_t2 * one_t; | 
|  | var b = 3 * one_t2 * t; | 
|  | var c = 3 * one_t * t2; | 
|  | var d = t2 * t; | 
|  | return a * curve[0] + b * curve[2] + c * curve[4] + d * curve[6]; | 
|  | } | 
|  |  | 
|  | function y_at_t(curve, t) { | 
|  | var one_t = 1 - t; | 
|  | if (curve.length == 4) { | 
|  | return one_t * curve[1] + t * curve[3]; | 
|  | } | 
|  | var one_t2 = one_t * one_t; | 
|  | var t2 = t * t; | 
|  | if (curve.length == 6) { | 
|  | return one_t2 * curve[1] + 2 * one_t * t * curve[3] + t2 * curve[5]; | 
|  | } | 
|  | if (curve.length == 7) { | 
|  | var numer = one_t2 * curve[1] + 2 * one_t * t * curve[3] * curve[6] | 
|  | + t2 * curve[5]; | 
|  | var denom = one_t2            + 2 * one_t * t            * curve[6] | 
|  | + t2; | 
|  | return numer / denom; | 
|  | } | 
|  | var a = one_t2 * one_t; | 
|  | var b = 3 * one_t2 * t; | 
|  | var c = 3 * one_t * t2; | 
|  | var d = t2 * t; | 
|  | return a * curve[1] + b * curve[3] + c * curve[5] + d * curve[7]; | 
|  | } | 
|  |  | 
|  | function drawPointAtT(curve) { | 
|  | var x = x_at_t(curve, curveT); | 
|  | var y = y_at_t(curve, curveT); | 
|  | drawPoint(x, y, false); | 
|  | } | 
|  |  | 
|  | function drawLine(x1, y1, x2, y2) { | 
|  | ctx.beginPath(); | 
|  | ctx.moveTo((x1 - srcLeft) * hscale, | 
|  | (y1 - srcTop) * vscale); | 
|  | ctx.lineTo((x2 - srcLeft) * hscale, | 
|  | (y2 - srcTop) * vscale); | 
|  | ctx.stroke(); | 
|  | } | 
|  |  | 
|  | function drawPoint(px, py, xend) { | 
|  | for (var pts = 0; pts < drawnPts.length; pts += 2) { | 
|  | var x = drawnPts[pts]; | 
|  | var y = drawnPts[pts + 1]; | 
|  | if (px == x && py == y) { | 
|  | return; | 
|  | } | 
|  | } | 
|  | drawnPts.push(px); | 
|  | drawnPts.push(py); | 
|  | var _px = (px - srcLeft) * hscale; | 
|  | var _py = (py - srcTop) * vscale; | 
|  | ctx.beginPath(); | 
|  | if (xend) { | 
|  | ctx.moveTo(_px - 3, _py - 3); | 
|  | ctx.lineTo(_px + 3, _py + 3); | 
|  | ctx.moveTo(_px - 3, _py + 3); | 
|  | ctx.lineTo(_px + 3, _py - 3); | 
|  | } else { | 
|  | ctx.arc(_px, _py, 3, 0, Math.PI * 2, true); | 
|  | ctx.closePath(); | 
|  | } | 
|  | ctx.stroke(); | 
|  | if (draw_point_xy) { | 
|  | var label = px.toFixed(decimal_places) + ", " + py.toFixed(decimal_places); | 
|  | ctx.font = "normal 10px Arial"; | 
|  | ctx.textAlign = "left"; | 
|  | ctx.fillStyle = "black"; | 
|  | ctx.fillText(label, _px + 5, _py); | 
|  | } | 
|  | } | 
|  |  | 
|  | function drawPointSolid(px, py) { | 
|  | drawPoint(px, py, false); | 
|  | ctx.fillStyle = "rgba(0,0,0, 0.4)"; | 
|  | ctx.fill(); | 
|  | } | 
|  |  | 
|  | function crossPt(origin, pt1, pt2) { | 
|  | return ((pt1[0] - origin[0]) * (pt2[1] - origin[1]) | 
|  | - (pt1[1] - origin[1]) * (pt2[0] - origin[0])) > 0 ? 0 : 1; | 
|  | } | 
|  |  | 
|  | // may not work well for cubics | 
|  | function curveClosestT(curve, x, y) { | 
|  | var closest = -1; | 
|  | var closestDist = Infinity; | 
|  | var l = Infinity, t = Infinity, r = -Infinity, b = -Infinity; | 
|  | for (var i = 0; i < 16; ++i) { | 
|  | var testX = x_at_t(curve, i / 16); | 
|  | l = Math.min(testX, l); | 
|  | r = Math.max(testX, r); | 
|  | var testY = y_at_t(curve, i / 16); | 
|  | t = Math.min(testY, t); | 
|  | b = Math.max(testY, b); | 
|  | var dx = testX - x; | 
|  | var dy = testY - y; | 
|  | var dist = dx * dx + dy * dy; | 
|  | if (closestDist > dist) { | 
|  | closestDist = dist; | 
|  | closest = i; | 
|  | } | 
|  | } | 
|  | var boundsX = r - l; | 
|  | var boundsY = b - t; | 
|  | var boundsDist = boundsX * boundsX + boundsY * boundsY; | 
|  | if (closestDist > boundsDist) { | 
|  | return -1; | 
|  | } | 
|  | console.log("closestDist = " + closestDist + " boundsDist = " + boundsDist | 
|  | + " t = " + closest / 16); | 
|  | return closest / 16; | 
|  | } | 
|  |  | 
|  | var kMaxConicToQuadPOW2 = 5; | 
|  |  | 
|  | function computeQuadPOW2(curve, tol) { | 
|  | var a = curve[6] - 1; | 
|  | var k = a / (4 * (2 + a)); | 
|  | var x = k * (curve[0] - 2 * curve[2] + curve[4]); | 
|  | var y = k * (curve[1] - 2 * curve[3] + curve[5]); | 
|  |  | 
|  | var error = Math.sqrt(x * x + y * y); | 
|  | var pow2; | 
|  | for (pow2 = 0; pow2 < kMaxConicToQuadPOW2; ++pow2) { | 
|  | if (error <= tol) { | 
|  | break; | 
|  | } | 
|  | error *= 0.25; | 
|  | } | 
|  | return pow2; | 
|  | } | 
|  |  | 
|  | function subdivide_w_value(w) { | 
|  | return Math.sqrt(0.5 + w * 0.5); | 
|  | } | 
|  |  | 
|  | function chop(curve, part1, part2) { | 
|  | var w = curve[6]; | 
|  | var scale = 1 / (1 + w); | 
|  | part1[0] = curve[0]; | 
|  | part1[1] = curve[1]; | 
|  | part1[2] = (curve[0] + curve[2] * w) * scale; | 
|  | part1[3] = (curve[1] + curve[3] * w) * scale; | 
|  | part1[4] = part2[0] = (curve[0] + (curve[2] * w) * 2 + curve[4]) * scale * 0.5; | 
|  | part1[5] = part2[1] = (curve[1] + (curve[3] * w) * 2 + curve[5]) * scale * 0.5; | 
|  | part2[2] = (curve[2] * w + curve[4]) * scale; | 
|  | part2[3] = (curve[3] * w + curve[5]) * scale; | 
|  | part2[4] = curve[4]; | 
|  | part2[5] = curve[5]; | 
|  | part1[6] = part2[6] = subdivide_w_value(w); | 
|  | } | 
|  |  | 
|  | function subdivide(curve, level, pts) { | 
|  | if (0 == level) { | 
|  | pts.push(curve[2]); | 
|  | pts.push(curve[3]); | 
|  | pts.push(curve[4]); | 
|  | pts.push(curve[5]); | 
|  | } else { | 
|  | var part1 = [], part2 = []; | 
|  | chop(curve, part1, part2); | 
|  | --level; | 
|  | subdivide(part1, level, pts); | 
|  | subdivide(part2, level, pts); | 
|  | } | 
|  | } | 
|  |  | 
|  | function chopIntoQuadsPOW2(curve, pow2, pts) { | 
|  | subdivide(curve, pow2, pts); | 
|  | return 1 << pow2; | 
|  | } | 
|  |  | 
|  | function drawConic(curve, srcLeft, srcTop, hscale, vscale) { | 
|  | var tol = 1 / Math.min(hscale, vscale); | 
|  | var pow2 = computeQuadPOW2(curve, tol); | 
|  | var pts = []; | 
|  | chopIntoQuadsPOW2(curve, pow2, pts); | 
|  | for (var i = 0; i < pts.length; i += 4) { | 
|  | ctx.quadraticCurveTo( | 
|  | (pts[i + 0] - srcLeft) * hscale, (pts[i + 1] - srcTop) * vscale, | 
|  | (pts[i + 2] - srcLeft) * hscale, (pts[i + 3] - srcTop) * vscale); | 
|  | } | 
|  | } | 
|  |  | 
|  | function draw(test, title) { | 
|  | ctx.font = "normal 50px Arial"; | 
|  | ctx.textAlign = "left"; | 
|  | ctx.fillStyle = "rgba(0,0,0, 0.1)"; | 
|  | ctx.fillText(title, 50, 50); | 
|  | ctx.font = "normal 10px Arial"; | 
|  | //  ctx.lineWidth = "1.001"; "0.999"; | 
|  | var hullStarts = []; | 
|  | var hullEnds = []; | 
|  | var midSpokes = []; | 
|  | var midDist = []; | 
|  | var origin = []; | 
|  | var shortSpokes = []; | 
|  | var shortDist = []; | 
|  | var sweeps = []; | 
|  | drawnPts = []; | 
|  | for (var curves in test) { | 
|  | var curve = test[curves]; | 
|  | origin.push(curve[0]); | 
|  | origin.push(curve[1]); | 
|  | var startPt = []; | 
|  | startPt.push(curve[2]); | 
|  | startPt.push(curve[3]); | 
|  | hullStarts.push(startPt); | 
|  | var endPt = []; | 
|  | if (curve.length == 4) { | 
|  | endPt.push(curve[2]); | 
|  | endPt.push(curve[3]); | 
|  | } else if (curve.length == 6 || curve.length == 7) { | 
|  | endPt.push(curve[4]); | 
|  | endPt.push(curve[5]); | 
|  | } else if (curve.length == 8) { | 
|  | endPt.push(curve[6]); | 
|  | endPt.push(curve[7]); | 
|  | } | 
|  | hullEnds.push(endPt); | 
|  | var sweep = crossPt(origin, startPt, endPt); | 
|  | sweeps.push(sweep); | 
|  | var midPt = []; | 
|  | midPt.push(x_at_t(curve, 0.5)); | 
|  | midPt.push(y_at_t(curve, 0.5)); | 
|  | midSpokes.push(midPt); | 
|  | var shortPt = []; | 
|  | shortPt.push(x_at_t(curve, 0.25)); | 
|  | shortPt.push(y_at_t(curve, 0.25)); | 
|  | shortSpokes.push(shortPt); | 
|  | var dx = midPt[0] - origin[0]; | 
|  | var dy = midPt[1] - origin[1]; | 
|  | var dist = Math.sqrt(dx * dx + dy * dy); | 
|  | midDist.push(dist); | 
|  | dx = shortPt[0] - origin[0]; | 
|  | dy = shortPt[1] - origin[1]; | 
|  | dist = Math.sqrt(dx * dx + dy * dy); | 
|  | shortDist.push(dist); | 
|  | } | 
|  | var intersect = []; | 
|  | var useIntersect = false; | 
|  | var maxWidth = Math.max(xmax - xmin, ymax - ymin); | 
|  | for (var curves in test) { | 
|  | var curve = test[curves]; | 
|  | if (curve.length >= 6 && curve.length <= 8) { | 
|  | var opp = curves == 0 || curves == 1 ? 0 : 1; | 
|  | var sects = ray_curve_intersect(origin, hullEnds[opp], curve); | 
|  | intersect.push(sects); | 
|  | if (sects.length > 1) { | 
|  | var intersection = sects[0]; | 
|  | if (intersection == 0) { | 
|  | intersection = sects[1]; | 
|  | } | 
|  | var ix = x_at_t(curve, intersection) - origin[0]; | 
|  | var iy = y_at_t(curve, intersection) - origin[1]; | 
|  | var ex = hullEnds[opp][0] - origin[0]; | 
|  | var ey = hullEnds[opp][1] - origin[1]; | 
|  | if (ix * ex >= 0 && iy * ey >= 0) { | 
|  | var iDist = Math.sqrt(ix * ix + iy * iy); | 
|  | var eDist = Math.sqrt(ex * ex + ey * ey); | 
|  | var delta = Math.abs(iDist - eDist) / maxWidth; | 
|  | if (delta > (curve.length != 8 ? 1e-5 : 1e-4)) { | 
|  | useIntersect ^= true; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | var midLeft = curves != 0 ? crossPt(origin, midSpokes[0], midSpokes[1]) : 0; | 
|  | var firstInside; | 
|  | if (useIntersect) { | 
|  | var sect1 = intersect[0].length > 1; | 
|  | var sIndex = sect1 ? 0 : 1; | 
|  | var sects = intersect[sIndex]; | 
|  | var intersection = sects[0]; | 
|  | if (intersection == 0) { | 
|  | intersection = sects[1]; | 
|  | } | 
|  | var curve = test[sIndex]; | 
|  | var ix = x_at_t(curve, intersection) - origin[0]; | 
|  | var iy = y_at_t(curve, intersection) - origin[1]; | 
|  | var opp = sect1 ? 1 : 0; | 
|  | var ex = hullEnds[opp][0] - origin[0]; | 
|  | var ey = hullEnds[opp][1] - origin[1]; | 
|  | var iDist = ix * ix + iy * iy; | 
|  | var eDist = ex * ex + ey * ey; | 
|  | firstInside = (iDist > eDist) ^ (sIndex == 0) ^ sweeps[0]; | 
|  | //            console.log("iDist=" + iDist + " eDist=" + eDist + " sIndex=" + sIndex | 
|  | //                   + " sweeps[0]=" + sweeps[0]); | 
|  | } else { | 
|  | //           console.log("midLeft=" + midLeft); | 
|  | firstInside = midLeft != 0; | 
|  | } | 
|  | var shorter = midDist[1] < midDist[0]; | 
|  | var shortLeft = shorter ? crossPt(origin, shortSpokes[0], midSpokes[1]) | 
|  | : crossPt(origin, midSpokes[0], shortSpokes[1]); | 
|  | var startCross = crossPt(origin, hullStarts[0], hullStarts[1]); | 
|  | var disallowShort = midLeft == startCross && midLeft == sweeps[0] | 
|  | && midLeft == sweeps[1]; | 
|  |  | 
|  | //      console.log("midLeft=" + midLeft + " startCross=" + startCross); | 
|  | var intersectIndex = 0; | 
|  | for (var curves in test) { | 
|  | var curve = test[draw_id != 2 ? curves : test.length - curves - 1]; | 
|  | if (curve.length != 4 && curve.length != 6 && curve.length != 7 && curve.length != 8) { | 
|  | continue; | 
|  | } | 
|  | ctx.lineWidth = 1; | 
|  | if (draw_tangents != 0) { | 
|  | if (draw_cubic_red ? curve.length == 8 : firstInside == curves) { | 
|  | ctx.strokeStyle = "rgba(255,0,0, 0.3)"; | 
|  | } else { | 
|  | ctx.strokeStyle = "rgba(0,0,255, 0.3)"; | 
|  | } | 
|  | drawLine(curve[0], curve[1], curve[2], curve[3]); | 
|  | if (draw_tangents != 2) { | 
|  | if (curve.length > 4) drawLine(curve[2], curve[3], curve[4], curve[5]); | 
|  | if (curve.length == 8) drawLine(curve[4], curve[5], curve[6], curve[7]); | 
|  | } | 
|  | if (draw_tangents != 1) { | 
|  | if (curve.length == 6 || curve.length == 7) { | 
|  | drawLine(curve[0], curve[1], curve[4], curve[5]); | 
|  | } | 
|  | if (curve.length == 8) drawLine(curve[0], curve[1], curve[6], curve[7]); | 
|  | } | 
|  | } | 
|  | ctx.beginPath(); | 
|  | ctx.moveTo((curve[0] - srcLeft) * hscale, (curve[1] - srcTop) * vscale); | 
|  | if (curve.length == 4) { | 
|  | ctx.lineTo((curve[2] - srcLeft) * hscale, (curve[3] - srcTop) * vscale); | 
|  | } else if (curve.length == 6) { | 
|  | ctx.quadraticCurveTo( | 
|  | (curve[2] - srcLeft) * hscale, (curve[3] - srcTop) * vscale, | 
|  | (curve[4] - srcLeft) * hscale, (curve[5] - srcTop) * vscale); | 
|  | } else if (curve.length == 7) { | 
|  | drawConic(curve, srcLeft, srcTop, hscale, vscale); | 
|  | } else { | 
|  | ctx.bezierCurveTo( | 
|  | (curve[2] - srcLeft) * hscale, (curve[3] - srcTop) * vscale, | 
|  | (curve[4] - srcLeft) * hscale, (curve[5] - srcTop) * vscale, | 
|  | (curve[6] - srcLeft) * hscale, (curve[7] - srcTop) * vscale); | 
|  | } | 
|  | if (draw_cubic_red ? curve.length == 8 : firstInside == curves) { | 
|  | ctx.strokeStyle = "rgba(255,0,0, 1)"; | 
|  | } else { | 
|  | ctx.strokeStyle = "rgba(0,0,255, 1)"; | 
|  | } | 
|  | ctx.stroke(); | 
|  | if (draw_endpoints > 0) { | 
|  | drawPoint(curve[0], curve[1], false); | 
|  | if (draw_endpoints > 1 || curve.length == 4) { | 
|  | drawPoint(curve[2], curve[3], curve.length == 4 && draw_endpoints == 3); | 
|  | } | 
|  | if (curve.length == 6 || curve.length == 7 || | 
|  | (draw_endpoints > 1 && curve.length == 8)) { | 
|  | drawPoint(curve[4], curve[5], (curve.length == 6 || curve.length == 7) && draw_endpoints == 3); | 
|  | } | 
|  | if (curve.length == 8) { | 
|  | drawPoint(curve[6], curve[7], curve.length == 8 && draw_endpoints == 3); | 
|  | } | 
|  | } | 
|  | if (draw_midpoint != 0) { | 
|  | if ((curves == 0) == (midLeft == 0)) { | 
|  | ctx.strokeStyle = "rgba(0,180,127, 0.6)"; | 
|  | } else { | 
|  | ctx.strokeStyle = "rgba(127,0,127, 0.6)"; | 
|  | } | 
|  | var midX = x_at_t(curve, 0.5); | 
|  | var midY = y_at_t(curve, 0.5); | 
|  | drawPointSolid(midX, midY); | 
|  | if (draw_midpoint > 1) { | 
|  | drawLine(curve[0], curve[1], midX, midY); | 
|  | } | 
|  | } | 
|  | if (draw_quarterpoint != 0) { | 
|  | if ((curves == 0) == (shortLeft == 0)) { | 
|  | ctx.strokeStyle = "rgba(0,191,63, 0.6)"; | 
|  | } else { | 
|  | ctx.strokeStyle = "rgba(63,0,191, 0.6)"; | 
|  | } | 
|  | var midT = (curves == 0) == shorter ? 0.25 : 0.5; | 
|  | var midX = x_at_t(curve, midT); | 
|  | var midY = y_at_t(curve, midT); | 
|  | drawPointSolid(midX, midY); | 
|  | if (draw_quarterpoint > 1) { | 
|  | drawLine(curve[0], curve[1], midX, midY); | 
|  | } | 
|  | } | 
|  | if (draw_sortpoint != 0) { | 
|  | if ((curves == 0) == ((disallowShort == -1 ? midLeft : shortLeft) == 0)) { | 
|  | ctx.strokeStyle = "rgba(0,155,37, 0.6)"; | 
|  | } else { | 
|  | ctx.strokeStyle = "rgba(37,0,155, 0.6)"; | 
|  | } | 
|  | var midT = (curves == 0) == shorter && disallowShort != curves ? 0.25 : 0.5; | 
|  | console.log("curves=" + curves + " disallowShort=" + disallowShort | 
|  | + " midLeft=" + midLeft + " shortLeft=" + shortLeft | 
|  | + " shorter=" + shorter + " midT=" + midT); | 
|  | var midX = x_at_t(curve, midT); | 
|  | var midY = y_at_t(curve, midT); | 
|  | drawPointSolid(midX, midY); | 
|  | if (draw_sortpoint > 1) { | 
|  | drawLine(curve[0], curve[1], midX, midY); | 
|  | } | 
|  | } | 
|  | if (draw_ray_intersect != 0) { | 
|  | ctx.strokeStyle = "rgba(75,45,199, 0.6)"; | 
|  | if (curve.length >= 6 && curve.length <= 8) { | 
|  | var intersections = intersect[intersectIndex]; | 
|  | for (var i in intersections) { | 
|  | var intersection = intersections[i]; | 
|  | var x = x_at_t(curve, intersection); | 
|  | var y = y_at_t(curve, intersection); | 
|  | drawPointSolid(x, y); | 
|  | if (draw_ray_intersect > 1) { | 
|  | drawLine(curve[0], curve[1], x, y); | 
|  | } | 
|  | } | 
|  | } | 
|  | ++intersectIndex; | 
|  | } | 
|  | if (draw_order) { | 
|  | var px = x_at_t(curve, 0.75); | 
|  | var py = y_at_t(curve, 0.75); | 
|  | var _px = (px - srcLeft) * hscale; | 
|  | var _py = (py - srcTop) * vscale; | 
|  | ctx.beginPath(); | 
|  | ctx.arc(_px, _py, 15, 0, Math.PI * 2, true); | 
|  | ctx.closePath(); | 
|  | ctx.fillStyle = "white"; | 
|  | ctx.fill(); | 
|  | if (draw_cubic_red ? curve.length == 8 : firstInside == curves) { | 
|  | ctx.strokeStyle = "rgba(255,0,0, 1)"; | 
|  | ctx.fillStyle = "rgba(255,0,0, 1)"; | 
|  | } else { | 
|  | ctx.strokeStyle = "rgba(0,0,255, 1)"; | 
|  | ctx.fillStyle = "rgba(0,0,255, 1)"; | 
|  | } | 
|  | ctx.stroke(); | 
|  | ctx.font = "normal 16px Arial"; | 
|  | ctx.textAlign = "center"; | 
|  | ctx.fillText(parseInt(curves) + 1, _px, _py + 5); | 
|  | } | 
|  | if (draw_closest_t) { | 
|  | var t = curveClosestT(curve, mouseX, mouseY); | 
|  | if (t >= 0) { | 
|  | var x = x_at_t(curve, t); | 
|  | var y = y_at_t(curve, t); | 
|  | drawPointSolid(x, y); | 
|  | } | 
|  | } | 
|  | if (!approximately_zero(hscale - hinitScale)) { | 
|  | ctx.font = "normal 20px Arial"; | 
|  | ctx.fillStyle = "rgba(0,0,0, 0.3)"; | 
|  | ctx.textAlign = "right"; | 
|  | var scaleTextOffset = hscale != vscale ? -25 : -5; | 
|  | ctx.fillText(hscale.toFixed(decimal_places) + 'x', | 
|  | screenWidth - 10, screenHeight - scaleTextOffset); | 
|  | if (hscale != vscale) { | 
|  | ctx.fillText(vscale.toFixed(decimal_places) + 'y', | 
|  | screenWidth - 10, screenHeight - 5); | 
|  | } | 
|  | } | 
|  | if (draw_t) { | 
|  | drawPointAtT(curve); | 
|  | } | 
|  | if (draw_id != 0) { | 
|  | var id = -1; | 
|  | for (var i = 0; i < ids.length; i += 2) { | 
|  | if (ids[i + 1] == curve) { | 
|  | id = ids[i]; | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (id >= 0) { | 
|  | var px = x_at_t(curve, 0.5); | 
|  | var py = y_at_t(curve, 0.5); | 
|  | var _px = (px - srcLeft) * hscale; | 
|  | var _py = (py - srcTop) * vscale; | 
|  | ctx.beginPath(); | 
|  | ctx.arc(_px, _py, 15, 0, Math.PI * 2, true); | 
|  | ctx.closePath(); | 
|  | ctx.fillStyle = "white"; | 
|  | ctx.fill(); | 
|  | ctx.strokeStyle = "rgba(255,0,0, 1)"; | 
|  | ctx.fillStyle = "rgba(255,0,0, 1)"; | 
|  | ctx.stroke(); | 
|  | ctx.font = "normal 16px Arial"; | 
|  | ctx.textAlign = "center"; | 
|  | ctx.fillText(id, _px, _py + 5); | 
|  | } | 
|  | } | 
|  | } | 
|  | if (draw_t) { | 
|  | drawCurveTControl(); | 
|  | } | 
|  | if (draw_w) { | 
|  | drawCurveWControl(); | 
|  | } | 
|  | } | 
|  |  | 
|  | function drawCurveTControl() { | 
|  | ctx.lineWidth = 2; | 
|  | ctx.strokeStyle = "rgba(0,0,0, 0.3)"; | 
|  | ctx.beginPath(); | 
|  | ctx.rect(screenWidth - 80, 40, 28, screenHeight - 80); | 
|  | ctx.stroke(); | 
|  | var ty = 40 + curveT * (screenHeight - 80); | 
|  | ctx.beginPath(); | 
|  | ctx.moveTo(screenWidth - 80, ty); | 
|  | ctx.lineTo(screenWidth - 85, ty - 5); | 
|  | ctx.lineTo(screenWidth - 85, ty + 5); | 
|  | ctx.lineTo(screenWidth - 80, ty); | 
|  | ctx.fillStyle = "rgba(0,0,0, 0.6)"; | 
|  | ctx.fill(); | 
|  | var num = curveT.toFixed(decimal_places); | 
|  | ctx.font = "normal 10px Arial"; | 
|  | ctx.textAlign = "left"; | 
|  | ctx.fillText(num, screenWidth - 78, ty); | 
|  | } | 
|  |  | 
|  | function drawCurveWControl() { | 
|  | var w = -1; | 
|  | var choice = 0; | 
|  | for (var curves in tests[testIndex]) { | 
|  | var curve = tests[testIndex][curves]; | 
|  | if (curve.length != 7) { | 
|  | continue; | 
|  | } | 
|  | if (choice == curveW) { | 
|  | w = curve[6]; | 
|  | break; | 
|  | } | 
|  | ++choice; | 
|  | } | 
|  | if (w < 0) { | 
|  | return; | 
|  | } | 
|  | ctx.lineWidth = 2; | 
|  | ctx.strokeStyle = "rgba(0,0,0, 0.3)"; | 
|  | ctx.beginPath(); | 
|  | ctx.rect(screenWidth - 40, 40, 28, screenHeight - 80); | 
|  | ctx.stroke(); | 
|  | var ty = 40 + w * (screenHeight - 80); | 
|  | ctx.beginPath(); | 
|  | ctx.moveTo(screenWidth - 40, ty); | 
|  | ctx.lineTo(screenWidth - 45, ty - 5); | 
|  | ctx.lineTo(screenWidth - 45, ty + 5); | 
|  | ctx.lineTo(screenWidth - 40, ty); | 
|  | ctx.fillStyle = "rgba(0,0,0, 0.6)"; | 
|  | ctx.fill(); | 
|  | var num = w.toFixed(decimal_places); | 
|  | ctx.font = "normal 10px Arial"; | 
|  | ctx.textAlign = "left"; | 
|  | ctx.fillText(num, screenWidth - 38, ty); | 
|  | } | 
|  |  | 
|  | function ptInTControl() { | 
|  | var e = window.event; | 
|  | var tgt = e.target || e.srcElement; | 
|  | var left = tgt.offsetLeft; | 
|  | var top = tgt.offsetTop; | 
|  | var x = (e.clientX - left); | 
|  | var y = (e.clientY - top); | 
|  | if (x < screenWidth - 80 || x > screenWidth - 50) { | 
|  | return false; | 
|  | } | 
|  | if (y < 40 || y > screenHeight - 80) { | 
|  | return false; | 
|  | } | 
|  | curveT = (y - 40) / (screenHeight - 120); | 
|  | if (curveT < 0 || curveT > 1) { | 
|  | throw "stop execution"; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | function ptInWControl() { | 
|  | var e = window.event; | 
|  | var tgt = e.target || e.srcElement; | 
|  | var left = tgt.offsetLeft; | 
|  | var top = tgt.offsetTop; | 
|  | var x = (e.clientX - left); | 
|  | var y = (e.clientY - top); | 
|  | if (x < screenWidth - 40 || x > screenWidth - 10) { | 
|  | return false; | 
|  | } | 
|  | if (y < 40 || y > screenHeight - 80) { | 
|  | return false; | 
|  | } | 
|  | var w = (y - 40) / (screenHeight - 120); | 
|  | if (w < 0 || w > 1) { | 
|  | throw "stop execution"; | 
|  | } | 
|  | var choice = 0; | 
|  | for (var curves in tests[testIndex]) { | 
|  | var curve = tests[testIndex][curves]; | 
|  | if (curve.length != 7) { | 
|  | continue; | 
|  | } | 
|  | if (choice == curveW) { | 
|  | curve[6] = w; | 
|  | break; | 
|  | } | 
|  | ++choice; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | function drawTop() { | 
|  | init(tests[testIndex]); | 
|  | redraw(); | 
|  | } | 
|  |  | 
|  | function redraw() { | 
|  | if (focus_on_selection > 0) { | 
|  | var focusXmin = focusYmin = Infinity; | 
|  | var focusXmax = focusYmax = -Infinity; | 
|  | var choice = 0; | 
|  | for (var curves in tests[testIndex]) { | 
|  | if (++choice != focus_on_selection) { | 
|  | continue; | 
|  | } | 
|  | var curve = tests[testIndex][curves]; | 
|  | var last = curve.length - (curve.length % 2 == 1 ? 1 : 0); | 
|  | for (var idx = 0; idx < last; idx += 2) { | 
|  | focusXmin = Math.min(focusXmin, curve[idx]); | 
|  | focusXmax = Math.max(focusXmax, curve[idx]); | 
|  | focusYmin = Math.min(focusYmin, curve[idx + 1]); | 
|  | focusYmax = Math.max(focusYmax, curve[idx + 1]); | 
|  | } | 
|  | } | 
|  | focusXmin -= Math.min(1, Math.max(focusXmax - focusXmin, focusYmax - focusYmin)); | 
|  | if (focusXmin < focusXmax && focusYmin < focusYmax) { | 
|  | setScale(focusXmin, focusXmax, focusYmin, focusYmax); | 
|  | } | 
|  | } | 
|  | ctx.beginPath(); | 
|  | ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height); | 
|  | ctx.fillStyle = "white"; | 
|  | ctx.fill(); | 
|  | draw(tests[testIndex], testTitles[testIndex]); | 
|  | } | 
|  |  | 
|  | function doKeyPress(evt) { | 
|  | var char = String.fromCharCode(evt.charCode); | 
|  | var focusWasOn = false; | 
|  | switch (char) { | 
|  | case '0': | 
|  | case '1': | 
|  | case '2': | 
|  | case '3': | 
|  | case '4': | 
|  | case '5': | 
|  | case '6': | 
|  | case '7': | 
|  | case '8': | 
|  | case '9': | 
|  | decimal_places = char - '0'; | 
|  | redraw(); | 
|  | break; | 
|  | case '-': | 
|  | focusWasOn = focus_on_selection; | 
|  | if (focusWasOn) { | 
|  | focus_on_selection = false; | 
|  | hscale /= 1.2; | 
|  | vscale /= 1.2; | 
|  | } else { | 
|  | hscale /= 2; | 
|  | vscale /= 2; | 
|  | } | 
|  | calcLeftTop(); | 
|  | redraw(); | 
|  | focus_on_selection = focusWasOn; | 
|  | break; | 
|  | case '=': | 
|  | case '+': | 
|  | focusWasOn = focus_on_selection; | 
|  | if (focusWasOn) { | 
|  | focus_on_selection = false; | 
|  | hscale *= 1.2; | 
|  | vscale *= 1.2; | 
|  | } else { | 
|  | hscale *= 2; | 
|  | vscale *= 2; | 
|  | } | 
|  | calcLeftTop(); | 
|  | redraw(); | 
|  | focus_on_selection = focusWasOn; | 
|  | break; | 
|  | case 'b': | 
|  | draw_cubic_red ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case 'c': | 
|  | drawTop(); | 
|  | break; | 
|  | case 'd': | 
|  | var test = tests[testIndex]; | 
|  | var testClone = []; | 
|  | for (var curves in test) { | 
|  | var c = test[curves]; | 
|  | var cClone = []; | 
|  | for (var index = 0; index < c.length; ++index) { | 
|  | cClone.push(c[index]); | 
|  | } | 
|  | testClone.push(cClone); | 
|  | } | 
|  | tests.push(testClone); | 
|  | testTitles.push(testTitles[testIndex] + " copy"); | 
|  | testIndex = tests.length - 1; | 
|  | redraw(); | 
|  | break; | 
|  | case 'e': | 
|  | draw_endpoints = (draw_endpoints + 1) % 4; | 
|  | redraw(); | 
|  | break; | 
|  | case 'f': | 
|  | draw_derivative ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case 'g': | 
|  | hscale *= 1.2; | 
|  | calcLeftTop(); | 
|  | redraw(); | 
|  | break; | 
|  | case 'G': | 
|  | hscale /= 1.2; | 
|  | calcLeftTop(); | 
|  | redraw(); | 
|  | break; | 
|  | case 'h': | 
|  | vscale *= 1.2; | 
|  | calcLeftTop(); | 
|  | redraw(); | 
|  | break; | 
|  | case 'H': | 
|  | vscale /= 1.2; | 
|  | calcLeftTop(); | 
|  | redraw(); | 
|  | break; | 
|  | case 'i': | 
|  | draw_ray_intersect = (draw_ray_intersect + 1) % 3; | 
|  | redraw(); | 
|  | break; | 
|  | case 'l': | 
|  | var test = tests[testIndex]; | 
|  | console.log("<div id=\"" + testTitles[testIndex] + "\" >"); | 
|  | for (var curves in test) { | 
|  | var c = test[curves]; | 
|  | var s = "{{"; | 
|  | for (var i = 0; i < c.length; i += 2) { | 
|  | s += "{"; | 
|  | s += c[i] + "," + c[i + 1]; | 
|  | s += "}"; | 
|  | if (i + 2 < c.length) { | 
|  | s += ", "; | 
|  | } | 
|  | } | 
|  | console.log(s + "}},"); | 
|  | } | 
|  | console.log("</div>"); | 
|  | break; | 
|  | case 'm': | 
|  | draw_midpoint = (draw_midpoint + 1) % 3; | 
|  | redraw(); | 
|  | break; | 
|  | case 'N': | 
|  | testIndex += 9; | 
|  | case 'n': | 
|  | testIndex = (testIndex + 1) % tests.length; | 
|  | drawTop(); | 
|  | break; | 
|  | case 'o': | 
|  | draw_order ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case 'P': | 
|  | testIndex -= 9; | 
|  | case 'p': | 
|  | if (--testIndex < 0) | 
|  | testIndex = tests.length - 1; | 
|  | drawTop(); | 
|  | break; | 
|  | case 'q': | 
|  | draw_quarterpoint = (draw_quarterpoint + 1) % 3; | 
|  | redraw(); | 
|  | break; | 
|  | case 'r': | 
|  | for (var i = 0; i < testDivs.length; ++i) { | 
|  | var title = testDivs[i].id.toString(); | 
|  | if (title == testTitles[testIndex]) { | 
|  | var str = testDivs[i].firstChild.data; | 
|  | parse(str, title); | 
|  | var original = tests.pop(); | 
|  | testTitles.pop(); | 
|  | tests[testIndex] = original; | 
|  | break; | 
|  | } | 
|  | } | 
|  | redraw(); | 
|  | break; | 
|  | case 's': | 
|  | draw_sortpoint = (draw_sortpoint + 1) % 3; | 
|  | redraw(); | 
|  | break; | 
|  | case 't': | 
|  | draw_t ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case 'u': | 
|  | draw_closest_t ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case 'v': | 
|  | draw_tangents = (draw_tangents + 1) % 4; | 
|  | redraw(); | 
|  | break; | 
|  | case 'w': | 
|  | ++curveW; | 
|  | var choice = 0; | 
|  | draw_w = false; | 
|  | for (var curves in tests[testIndex]) { | 
|  | var curve = tests[testIndex][curves]; | 
|  | if (curve.length != 7) { | 
|  | continue; | 
|  | } | 
|  | if (choice == curveW) { | 
|  | draw_w = true; | 
|  | break; | 
|  | } | 
|  | ++choice; | 
|  | } | 
|  | if (!draw_w) { | 
|  | curveW = -1; | 
|  | } | 
|  | redraw(); | 
|  | break; | 
|  | case 'x': | 
|  | draw_point_xy ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case 'y': | 
|  | draw_mouse_xy ^= true; | 
|  | redraw(); | 
|  | break; | 
|  | case '\\': | 
|  | retina_scale ^= true; | 
|  | drawTop(); | 
|  | break; | 
|  | case '`': | 
|  | ++focus_on_selection; | 
|  | if (focus_on_selection >= tests[testIndex].length) { | 
|  | focus_on_selection = 0; | 
|  | } | 
|  | setScale(xmin, xmax, ymin, ymax); | 
|  | redraw(); | 
|  | break; | 
|  | case '.': | 
|  | draw_id = (draw_id + 1) % 3; | 
|  | redraw(); | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | function doKeyDown(evt) { | 
|  | var char = evt.keyCode; | 
|  | var preventDefault = false; | 
|  | switch (char) { | 
|  | case 37: // left arrow | 
|  | if (evt.shiftKey) { | 
|  | testIndex -= 9; | 
|  | } | 
|  | if (--testIndex < 0) | 
|  | testIndex = tests.length - 1; | 
|  | if (evt.ctrlKey) { | 
|  | redraw(); | 
|  | } else { | 
|  | drawTop(); | 
|  | } | 
|  | preventDefault = true; | 
|  | break; | 
|  | case 39: // right arrow | 
|  | if (evt.shiftKey) { | 
|  | testIndex += 9; | 
|  | } | 
|  | if (++testIndex >= tests.length) | 
|  | testIndex = 0; | 
|  | if (evt.ctrlKey) { | 
|  | redraw(); | 
|  | } else { | 
|  | drawTop(); | 
|  | } | 
|  | preventDefault = true; | 
|  | break; | 
|  | } | 
|  | if (preventDefault) { | 
|  | evt.preventDefault(); | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | function calcXY() { | 
|  | var e = window.event; | 
|  | var tgt = e.target || e.srcElement; | 
|  | var left = tgt.offsetLeft; | 
|  | var top = tgt.offsetTop; | 
|  | mouseX = (e.clientX - left) / hscale + srcLeft; | 
|  | mouseY = (e.clientY - top) / vscale + srcTop; | 
|  | } | 
|  |  | 
|  | function calcLeftTop() { | 
|  | srcLeft = mouseX - screenWidth / 2 / hscale; | 
|  | srcTop = mouseY - screenHeight / 2 / vscale; | 
|  | } | 
|  |  | 
|  | function handleMouseClick() { | 
|  | if ((!draw_t || !ptInTControl()) && (!draw_w || !ptInWControl())) { | 
|  | calcXY(); | 
|  | } else { | 
|  | redraw(); | 
|  | } | 
|  | } | 
|  |  | 
|  | function initDown() { | 
|  | var test = tests[testIndex]; | 
|  | var bestDistance = 1000000; | 
|  | activePt = -1; | 
|  | for (var curves in test) { | 
|  | var testCurve = test[curves]; | 
|  | if (testCurve.length != 4 && (testCurve.length < 6 || testCurve.length > 8)) { | 
|  | continue; | 
|  | } | 
|  | var testMax = testCurve.length == 7 ? 6 : testCurve.length; | 
|  | for (var i = 0; i < testMax; i += 2) { | 
|  | var testX = testCurve[i]; | 
|  | var testY = testCurve[i + 1]; | 
|  | var dx = testX - mouseX; | 
|  | var dy = testY - mouseY; | 
|  | var dist = dx * dx + dy * dy; | 
|  | if (dist > bestDistance) { | 
|  | continue; | 
|  | } | 
|  | activeCurve = testCurve; | 
|  | activePt = i; | 
|  | bestDistance = dist; | 
|  | } | 
|  | } | 
|  | if (activePt >= 0) { | 
|  | lastX = mouseX; | 
|  | lastY = mouseY; | 
|  | } | 
|  | } | 
|  |  | 
|  | function handleMouseOver() { | 
|  | calcXY(); | 
|  | if (draw_mouse_xy) { | 
|  | var num = mouseX.toFixed(decimal_places) + ", " + mouseY.toFixed(decimal_places); | 
|  | ctx.beginPath(); | 
|  | ctx.rect(300, 100, num.length * 6, 10); | 
|  | ctx.fillStyle = "white"; | 
|  | ctx.fill(); | 
|  | ctx.font = "normal 10px Arial"; | 
|  | ctx.fillStyle = "black"; | 
|  | ctx.textAlign = "left"; | 
|  | ctx.fillText(num, 300, 108); | 
|  | } | 
|  | if (!mouseDown) { | 
|  | activePt = -1; | 
|  | return; | 
|  | } | 
|  | if (activePt < 0) { | 
|  | initDown(); | 
|  | return; | 
|  | } | 
|  | var deltaX = mouseX - lastX; | 
|  | var deltaY = mouseY - lastY; | 
|  | lastX = mouseX; | 
|  | lastY = mouseY; | 
|  | if (activePt == 0) { | 
|  | var test = tests[testIndex]; | 
|  | for (var curves in test) { | 
|  | var testCurve = test[curves]; | 
|  | testCurve[0] += deltaX; | 
|  | testCurve[1] += deltaY; | 
|  | } | 
|  | } else { | 
|  | activeCurve[activePt] += deltaX; | 
|  | activeCurve[activePt + 1] += deltaY; | 
|  | } | 
|  | redraw(); | 
|  | } | 
|  |  | 
|  | function start() { | 
|  | for (var i = 0; i < testDivs.length; ++i) { | 
|  | var title = testDivs[i].id.toString(); | 
|  | var str = testDivs[i].firstChild.data; | 
|  | parse(str, title); | 
|  | } | 
|  | drawTop(); | 
|  | window.addEventListener('keypress', doKeyPress, true); | 
|  | window.addEventListener('keydown', doKeyDown, true); | 
|  | window.onresize = function () { | 
|  | drawTop(); | 
|  | } | 
|  | } | 
|  |  | 
|  | </script> | 
|  | </head> | 
|  |  | 
|  | <body onLoad="start();"> | 
|  |  | 
|  | <canvas id="canvas" width="750" height="500" | 
|  | onmousedown="mouseDown = true" | 
|  | onmouseup="mouseDown = false" | 
|  | onmousemove="handleMouseOver()" | 
|  | onclick="handleMouseClick()" | 
|  | ></canvas > | 
|  | </body> | 
|  | </html> |