Home | History | Annotate | Download | only in tools
      1 <!DOCTYPE html>
      2 
      3 <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
      4 <head>
      5     <meta charset="utf-8" />
      6     <title></title>
      7 <div style="height:0">
      8 
      9 <div id="cubics">
     10 {{{403.28299,497.196991}, {403.424011,497.243988}, {391.110992,495.556}, {391.110992,495.556}}}
     11 {{{398.375,501.976013}, {403.28299,497.196991}}}
     12 {{{403.42401123046875, 497.243988037109375}, {378.7979736328125, 493.868011474609375}}}, id=0
     13 {{{415.5605946972181641, 498.8838844379989155}, {378.8318672594865006, 493.8897667505245295}}}, id=1
     14 {{{403.2954048004600622, 497.117149457477467}, {403.2729768294798873, 497.2781265045034615}}}, id=2
     15 {{{403.42401123046875, 497.243988037109375}, {378.7979736328125, 493.868011474609375}}}, id=3
     16 
     17 </div>
     18 
     19 <div id="c1">
     20 {{{403.28299f, 497.196991f}, {403.284241f, 497.197388f}, {403.28418f, 497.197632f}}}
     21 {{{403.28418f, 497.197632f}, {403.280487f, 497.224304f}, {391.110992f, 495.556f}, {391.110992f, 495.556f}}}    
     22 
     23 </div>
     24     </div>
     25 
     26 <script type="text/javascript">
     27 
     28     var testDivs = [
     29     c1,
     30     cubics,
     31     ];
     32 
     33     var decimal_places = 3;
     34 
     35     var tests = [];
     36     var testTitles = [];
     37     var testIndex = 0;
     38     var ctx;
     39 
     40     var subscale = 1;
     41     var xmin, xmax, ymin, ymax;
     42     var scale;
     43     var initScale;
     44     var mouseX, mouseY;
     45     var mouseDown = false;
     46     var srcLeft, srcTop;
     47     var screenWidth, screenHeight;
     48     var drawnPts;
     49     var curveT = 0;
     50     var curveW = -1;
     51 
     52     var lastX, lastY;
     53     var activeCurve = [];
     54     var activePt;
     55     var ids = [];
     56 
     57     var focus_on_selection = 0;
     58     var draw_t = false;
     59     var draw_w = false;
     60     var draw_closest_t = false;
     61     var draw_cubic_red = false;
     62     var draw_derivative = false;
     63     var draw_endpoints = 2;
     64     var draw_id = 0;
     65     var draw_midpoint = 0;
     66     var draw_mouse_xy = false;
     67     var draw_order = false;
     68     var draw_point_xy = false;
     69     var draw_ray_intersect = false;
     70     var draw_quarterpoint = 0;
     71     var draw_tangents = 1;
     72     var draw_sortpoint = 0;
     73     var retina_scale = !!window.devicePixelRatio;
     74 
     75     function parse(test, title) {
     76         var curveStrs = test.split("{{");
     77         var pattern = /-?\d+\.*\d*e?-?\d*/g;
     78         var curves = [];
     79         for (var c in curveStrs) {
     80             var curveStr = curveStrs[c];
     81             var idPart = curveStr.split("id=");
     82             var id = -1;
     83             if (idPart.length == 2) {
     84                 id = parseInt(idPart[1]);
     85                 curveStr = idPart[0];
     86             }
     87             var points = curveStr.match(pattern);
     88             var pts = [];
     89             for (var wd in points) {
     90                 var num = parseFloat(points[wd]);
     91                 if (isNaN(num)) continue;
     92                 pts.push(num);
     93             }
     94             if (pts.length > 2) {
     95                 curves.push(pts);
     96             }
     97             if (id >= 0) {
     98                 ids.push(id);
     99                 ids.push(pts);
    100             }
    101         }
    102         if (curves.length >= 1) {
    103             tests.push(curves);
    104             testTitles.push(title);
    105         }
    106     }
    107 
    108     function init(test) {
    109         var canvas = document.getElementById('canvas');
    110         if (!canvas.getContext) return;
    111         ctx = canvas.getContext('2d');
    112         var resScale = retina_scale && window.devicePixelRatio ? window.devicePixelRatio : 1;
    113         var unscaledWidth = window.innerWidth - 20;
    114         var unscaledHeight = window.innerHeight - 20;
    115         screenWidth = unscaledWidth;
    116         screenHeight = unscaledHeight;
    117         canvas.width = unscaledWidth * resScale;
    118         canvas.height = unscaledHeight * resScale;
    119         canvas.style.width = unscaledWidth + 'px';
    120         canvas.style.height = unscaledHeight + 'px';
    121         if (resScale != 1) {
    122             ctx.scale(resScale, resScale);
    123         }
    124         xmin = Infinity;
    125         xmax = -Infinity;
    126         ymin = Infinity;
    127         ymax = -Infinity;
    128         for (var curves in test) {
    129             var curve = test[curves];
    130             var last = curve.length - (curve.length % 2 == 1 ? 1 : 0);
    131             for (var idx = 0; idx < last; idx += 2) {
    132                 xmin = Math.min(xmin, curve[idx]);
    133                 xmax = Math.max(xmax, curve[idx]);
    134                 ymin = Math.min(ymin, curve[idx + 1]);
    135                 ymax = Math.max(ymax, curve[idx + 1]);
    136             }
    137         }
    138         xmin -= Math.min(1, Math.max(xmax - xmin, ymax - ymin));
    139         var testW = xmax - xmin;
    140         var testH = ymax - ymin;
    141         subscale = 1;
    142         while (testW * subscale < 0.1 && testH * subscale < 0.1) {
    143             subscale *= 10;
    144         }
    145         while (testW * subscale > 10 && testH * subscale > 10) {
    146             subscale /= 10;
    147         }
    148         setScale(xmin, xmax, ymin, ymax);
    149         mouseX = (screenWidth / 2) / scale + srcLeft;
    150         mouseY = (screenHeight / 2) / scale + srcTop;
    151         initScale = scale;
    152     }
    153 
    154     function setScale(x0, x1, y0, y1) {
    155         var srcWidth = x1 - x0;
    156         var srcHeight = y1 - y0;
    157         var usableWidth = screenWidth;
    158         var xDigits = Math.ceil(Math.log(Math.abs(xmax)) / Math.log(10));
    159         var yDigits = Math.ceil(Math.log(Math.abs(ymax)) / Math.log(10));
    160         usableWidth -= (xDigits + yDigits) * 10;
    161         usableWidth -= decimal_places * 10;
    162         var hscale = usableWidth / srcWidth;
    163         var vscale = screenHeight / srcHeight;
    164         scale = Math.min(hscale, vscale);
    165         var invScale = 1 / scale;
    166         var sxmin = x0 - invScale * 5;
    167         var symin = y0 - invScale * 10;
    168         var sxmax = x1 + invScale * (6 * decimal_places + 10);
    169         var symax = y1 + invScale * 10;
    170         srcWidth = sxmax - sxmin;
    171         srcHeight = symax - symin;
    172         hscale = usableWidth / srcWidth;
    173         vscale = screenHeight / srcHeight;
    174         scale = Math.min(hscale, vscale);
    175         srcLeft = sxmin;
    176         srcTop = symin;
    177     }
    178 
    179 function dxy_at_t(curve, t) {
    180     var dxy = {};
    181     if (curve.length == 6) {
    182         var a = t - 1;
    183         var b = 1 - 2 * t;
    184         var c = t;
    185         dxy.x = a * curve[0] + b * curve[2] + c * curve[4];
    186         dxy.y = a * curve[1] + b * curve[3] + c * curve[5];
    187     } else if (curve.length == 7) {
    188         var p20x = curve[4] - curve[0];
    189         var p20y = curve[5] - curve[1];
    190         var p10xw = (curve[2] - curve[0]) * curve[6];
    191         var p10yw = (curve[3] - curve[1]) * curve[6];
    192         var coeff0x = curve[6] * p20x - p20x;
    193         var coeff0y = curve[6] * p20y - p20y;
    194         var coeff1x = p20x - 2 * p10xw;
    195         var coeff1y = p20y - 2 * p10yw;
    196         dxy.x = t * (t * coeff0x + coeff1x) + p10xw;
    197         dxy.y = t * (t * coeff0y + coeff1y) + p10yw;
    198     } else if (curve.length == 8) {
    199         var one_t = 1 - t;
    200         var a = curve[0];
    201         var b = curve[2];
    202         var c = curve[4];
    203         var d = curve[6];
    204         dxy.x = 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
    205         a = curve[1];
    206         b = curve[3];
    207         c = curve[5];
    208         d = curve[7];
    209         dxy.y = 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
    210     }
    211     return dxy;
    212 }
    213 
    214     var flt_epsilon = 1.19209290E-07;
    215 
    216     function approximately_zero(A) {
    217         return Math.abs(A) < flt_epsilon;
    218     }
    219 
    220     function approximately_zero_inverse(A) {
    221         return Math.abs(A) > (1 / flt_epsilon);
    222     }
    223 
    224     function quad_real_roots(A, B, C) {
    225         var s = [];
    226         var p = B / (2 * A);
    227         var q = C / A;
    228         if (approximately_zero(A) && (approximately_zero_inverse(p)
    229                 || approximately_zero_inverse(q))) {
    230             if (approximately_zero(B)) {
    231                 if (C == 0) {
    232                     s[0] = 0;
    233                 }
    234                 return s;
    235             }
    236             s[0] = -C / B;
    237             return s;
    238         }
    239         /* normal form: x^2 + px + q = 0 */
    240         var p2 = p * p;
    241         if (!approximately_zero(p2 - q) && p2 < q) {
    242             return s;
    243         }
    244         var sqrt_D = 0;
    245         if (p2 > q) {
    246             sqrt_D = Math.sqrt(p2 - q);
    247         }
    248         s[0] = sqrt_D - p;
    249         var flip = -sqrt_D - p;
    250         if (!approximately_zero(s[0] - flip)) {
    251             s[1] = flip;
    252         }
    253         return s;
    254     }
    255 
    256     function cubic_real_roots(A, B, C, D) {
    257         if (approximately_zero(A)) {  // we're just a quadratic
    258             return quad_real_roots(B, C, D);
    259         }
    260         if (approximately_zero(D)) {  // 0 is one root
    261             var s = quad_real_roots(A, B, C);
    262             for (var i = 0; i < s.length; ++i) {
    263                 if (approximately_zero(s[i])) {
    264                     return s;
    265                 }
    266             }
    267             s.push(0);
    268             return s;
    269         }
    270         if (approximately_zero(A + B + C + D)) {  // 1 is one root
    271             var s = quad_real_roots(A, A + B, -D);
    272             for (var i = 0; i < s.length; ++i) {
    273                 if (approximately_zero(s[i] - 1)) {
    274                     return s;
    275                 }
    276             }
    277             s.push(1);
    278             return s;
    279         }
    280         var a, b, c;
    281         var invA = 1 / A;
    282         a = B * invA;
    283         b = C * invA;
    284         c = D * invA;
    285         var a2 = a * a;
    286         var Q = (a2 - b * 3) / 9;
    287         var R = (2 * a2 * a - 9 * a * b + 27 * c) / 54;
    288         var R2 = R * R;
    289         var Q3 = Q * Q * Q;
    290         var R2MinusQ3 = R2 - Q3;
    291         var adiv3 = a / 3;
    292         var r;
    293         var roots = [];
    294         if (R2MinusQ3 < 0) {   // we have 3 real roots
    295             var theta = Math.acos(R / Math.sqrt(Q3));
    296             var neg2RootQ = -2 * Math.sqrt(Q);
    297             r = neg2RootQ * Math.cos(theta / 3) - adiv3;
    298             roots.push(r);
    299             r = neg2RootQ * Math.cos((theta + 2 * Math.PI) / 3) - adiv3;
    300             if (!approximately_zero(roots[0] - r)) {
    301                 roots.push(r);
    302             }
    303             r = neg2RootQ * Math.cos((theta - 2 * Math.PI) / 3) - adiv3;
    304             if (!approximately_zero(roots[0] - r) && (roots.length == 1
    305                         || !approximately_zero(roots[1] - r))) {
    306                 roots.push(r);
    307             }
    308         } else {  // we have 1 real root
    309             var sqrtR2MinusQ3 = Math.sqrt(R2MinusQ3);
    310             var A = Math.abs(R) + sqrtR2MinusQ3;
    311             A = Math.pow(A, 1/3);
    312             if (R > 0) {
    313                 A = -A;
    314             }
    315             if (A != 0) {
    316                 A += Q / A;
    317             }
    318             r = A - adiv3;
    319             roots.push(r);
    320             if (approximately_zero(R2 - Q3)) {
    321                 r = -A / 2 - adiv3;
    322                 if (!approximately_zero(roots[0] - r)) {
    323                     roots.push(r);
    324                 }
    325             }
    326         }
    327         return roots;
    328     }
    329 
    330     function approximately_zero_or_more(tValue) {
    331         return tValue >= -flt_epsilon;
    332     }
    333 
    334     function approximately_one_or_less(tValue) {
    335         return tValue <= 1 + flt_epsilon;
    336     }
    337 
    338     function approximately_less_than_zero(tValue) {
    339         return tValue < flt_epsilon;
    340     }
    341 
    342     function approximately_greater_than_one(tValue) {
    343         return tValue > 1 - flt_epsilon;
    344     }
    345 
    346     function add_valid_ts(s) {
    347         var t = [];
    348     nextRoot:
    349         for (var index = 0; index < s.length; ++index) {
    350             var tValue = s[index];
    351             if (approximately_zero_or_more(tValue) && approximately_one_or_less(tValue)) {
    352                 if (approximately_less_than_zero(tValue)) {
    353                     tValue = 0;
    354                 } else if (approximately_greater_than_one(tValue)) {
    355                     tValue = 1;
    356                 }
    357                 for (var idx2 = 0; idx2 < t.length; ++idx2) {
    358                     if (approximately_zero(t[idx2] - tValue)) {
    359                         continue nextRoot;
    360                     }
    361                 }
    362                 t.push(tValue);
    363             }
    364         }
    365         return t;
    366     }
    367 
    368     function quad_roots(A, B, C) {
    369         var s = quad_real_roots(A, B, C);
    370         var foundRoots = add_valid_ts(s);
    371         return foundRoots;
    372     }
    373 
    374     function cubic_roots(A, B, C, D) {
    375         var s = cubic_real_roots(A, B, C, D);
    376         var foundRoots = add_valid_ts(s);
    377         return foundRoots;
    378     }
    379 
    380     function ray_curve_intersect(startPt, endPt, curve) {
    381         var adj = endPt[0] - startPt[0];
    382         var opp = endPt[1] - startPt[1];
    383         var r = [];
    384         var len = (curve.length == 7 ? 6 : curve.length) / 2;
    385         for (var n = 0; n < len; ++n) {
    386             r[n] = (curve[n * 2 + 1] - startPt[1]) * adj - (curve[n * 2] - startPt[0]) * opp;
    387         }
    388         if (curve.length == 6) {
    389             var A = r[2];
    390             var B = r[1];
    391             var C = r[0];
    392             A += C - 2 * B;  // A = a - 2*b + c
    393             B -= C;  // B = -(b - c)
    394             return quad_roots(A, 2 * B, C);
    395         }
    396         if (curve.length == 7) {
    397             var A = r[2];
    398             var B = r[1] * curve[6];
    399             var C = r[0];
    400             A += C - 2 * B;  // A = a - 2*b + c
    401             B -= C;  // B = -(b - c)
    402             return quad_roots(A, 2 * B, C);
    403         }
    404         var A = r[3];       // d
    405         var B = r[2] * 3;   // 3*c
    406         var C = r[1] * 3;   // 3*b
    407         var D = r[0];       // a
    408         A -= D - C + B;     // A =   -a + 3*b - 3*c + d
    409         B += 3 * D - 2 * C; // B =  3*a - 6*b + 3*c
    410         C -= 3 * D;         // C = -3*a + 3*b
    411         return cubic_roots(A, B, C, D);
    412     }
    413 
    414     function x_at_t(curve, t) {
    415         var one_t = 1 - t;
    416         if (curve.length == 4) {
    417             return one_t * curve[0] + t * curve[2];
    418         }
    419         var one_t2 = one_t * one_t;
    420         var t2 = t * t;
    421         if (curve.length == 6) {
    422             return one_t2 * curve[0] + 2 * one_t * t * curve[2] + t2 * curve[4];
    423         }
    424         if (curve.length == 7) {
    425             var numer = one_t2 * curve[0] + 2 * one_t * t * curve[2] * curve[6]
    426                     + t2 * curve[4];
    427             var denom = one_t2            + 2 * one_t * t            * curve[6]
    428                     + t2;
    429             return numer / denom;
    430         }
    431         var a = one_t2 * one_t;
    432         var b = 3 * one_t2 * t;
    433         var c = 3 * one_t * t2;
    434         var d = t2 * t;
    435         return a * curve[0] + b * curve[2] + c * curve[4] + d * curve[6];
    436     }
    437 
    438     function y_at_t(curve, t) {
    439         var one_t = 1 - t;
    440         if (curve.length == 4) {
    441             return one_t * curve[1] + t * curve[3];
    442         }
    443         var one_t2 = one_t * one_t;
    444         var t2 = t * t;
    445         if (curve.length == 6) {
    446             return one_t2 * curve[1] + 2 * one_t * t * curve[3] + t2 * curve[5];
    447         }
    448         if (curve.length == 7) {
    449             var numer = one_t2 * curve[1] + 2 * one_t * t * curve[3] * curve[6]
    450                     + t2 * curve[5];
    451             var denom = one_t2            + 2 * one_t * t            * curve[6]
    452                     + t2;
    453             return numer / denom;
    454         }
    455         var a = one_t2 * one_t;
    456         var b = 3 * one_t2 * t;
    457         var c = 3 * one_t * t2;
    458         var d = t2 * t;
    459         return a * curve[1] + b * curve[3] + c * curve[5] + d * curve[7];
    460     }
    461 
    462     function drawPointAtT(curve) {
    463         var x = x_at_t(curve, curveT);
    464         var y = y_at_t(curve, curveT);
    465         drawPoint(x, y, false);
    466     }
    467 
    468     function drawLine(x1, y1, x2, y2) {
    469         ctx.beginPath();
    470         ctx.moveTo((x1 - srcLeft) * scale,
    471                 (y1 - srcTop) * scale);
    472         ctx.lineTo((x2 - srcLeft) * scale,
    473                 (y2 - srcTop) * scale);
    474         ctx.stroke();
    475     }
    476 
    477     function drawPoint(px, py, xend) {
    478         for (var pts = 0; pts < drawnPts.length; pts += 2) {
    479             var x = drawnPts[pts];
    480             var y = drawnPts[pts + 1];
    481             if (px == x && py == y) {
    482                 return;
    483             }
    484         }
    485         drawnPts.push(px);
    486         drawnPts.push(py);
    487         var _px = (px - srcLeft) * scale;
    488         var _py = (py - srcTop) * scale;
    489         ctx.beginPath();
    490         if (xend) {
    491             ctx.moveTo(_px - 3, _py - 3);
    492             ctx.lineTo(_px + 3, _py + 3);
    493             ctx.moveTo(_px - 3, _py + 3);
    494             ctx.lineTo(_px + 3, _py - 3);
    495         } else {
    496             ctx.arc(_px, _py, 3, 0, Math.PI * 2, true);
    497             ctx.closePath();
    498         }
    499         ctx.stroke();
    500         if (draw_point_xy) {
    501             var label = px.toFixed(decimal_places) + ", " + py.toFixed(decimal_places);
    502             ctx.font = "normal 10px Arial";
    503             ctx.textAlign = "left";
    504             ctx.fillStyle = "black";
    505             ctx.fillText(label, _px + 5, _py);
    506         }
    507     }
    508 
    509     function drawPointSolid(px, py) {
    510         drawPoint(px, py, false);
    511         ctx.fillStyle = "rgba(0,0,0, 0.4)";
    512         ctx.fill();
    513     }
    514 
    515     function crossPt(origin, pt1, pt2) {
    516         return ((pt1[0] - origin[0]) * (pt2[1] - origin[1])
    517               - (pt1[1] - origin[1]) * (pt2[0] - origin[0])) > 0 ? 0 : 1;
    518     }
    519 
    520     // may not work well for cubics
    521     function curveClosestT(curve, x, y) {
    522         var closest = -1;
    523         var closestDist = Infinity;
    524         var l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
    525         for (var i = 0; i < 16; ++i) {
    526             var testX = x_at_t(curve, i / 16);
    527             l = Math.min(testX, l);
    528             r = Math.max(testX, r);
    529             var testY = y_at_t(curve, i / 16);
    530             t = Math.min(testY, t);
    531             b = Math.max(testY, b);
    532             var dx = testX - x;
    533             var dy = testY - y;
    534             var dist = dx * dx + dy * dy;
    535             if (closestDist > dist) {
    536                 closestDist = dist;
    537                 closest = i;
    538             }
    539         }
    540         var boundsX = r - l;
    541         var boundsY = b - t;
    542         var boundsDist = boundsX * boundsX + boundsY * boundsY;
    543         if (closestDist > boundsDist) {
    544             return -1;
    545         }
    546         console.log("closestDist = " + closestDist + " boundsDist = " + boundsDist
    547                 + " t = " + closest / 16);
    548         return closest / 16;
    549     }
    550 
    551     var kMaxConicToQuadPOW2 = 5;
    552 
    553     function computeQuadPOW2(curve, tol) {
    554         var a = curve[6] - 1;
    555         var k = a / (4 * (2 + a));
    556         var x = k * (curve[0] - 2 * curve[2] + curve[4]);
    557         var y = k * (curve[1] - 2 * curve[3] + curve[5]);
    558 
    559         var error = Math.sqrt(x * x + y * y);
    560         var pow2;
    561         for (pow2 = 0; pow2 < kMaxConicToQuadPOW2; ++pow2) {
    562             if (error <= tol) {
    563                 break;
    564             }
    565             error *= 0.25;
    566         }
    567         return pow2;
    568     }
    569 
    570     function subdivide_w_value(w) {
    571         return Math.sqrt(0.5 + w * 0.5);
    572     }
    573 
    574     function chop(curve, part1, part2) {
    575         var w = curve[6];
    576         var scale = 1 / (1 + w);
    577         part1[0] = curve[0];
    578         part1[1] = curve[1];
    579         part1[2] = (curve[0] + curve[2] * w) * scale;
    580         part1[3] = (curve[1] + curve[3] * w) * scale;
    581         part1[4] = part2[0] = (curve[0] + (curve[2] * w) * 2 + curve[4]) * scale * 0.5;
    582         part1[5] = part2[1] = (curve[1] + (curve[3] * w) * 2 + curve[5]) * scale * 0.5;
    583         part2[2] = (curve[2] * w + curve[4]) * scale;
    584         part2[3] = (curve[3] * w + curve[5]) * scale;
    585         part2[4] = curve[4];
    586         part2[5] = curve[5];
    587         part1[6] = part2[6] = subdivide_w_value(w);
    588     }
    589 
    590     function subdivide(curve, level, pts) {
    591         if (0 == level) {
    592             pts.push(curve[2]);
    593             pts.push(curve[3]);
    594             pts.push(curve[4]);
    595             pts.push(curve[5]);
    596         } else {
    597             var part1 = [], part2 = [];
    598             chop(curve, part1, part2);
    599             --level;
    600             subdivide(part1, level, pts);
    601             subdivide(part2, level, pts);
    602         }
    603     }
    604 
    605     function chopIntoQuadsPOW2(curve, pow2, pts) {
    606         subdivide(curve, pow2, pts);
    607         return 1 << pow2;
    608     }
    609 
    610     function drawConic(curve, srcLeft, srcTop, scale) {
    611         var tol = 1 / scale;
    612         var pow2 = computeQuadPOW2(curve, tol);
    613         var pts = [];
    614         chopIntoQuadsPOW2(curve, pow2, pts);
    615         for (var i = 0; i < pts.length; i += 4) {
    616             ctx.quadraticCurveTo(
    617                 (pts[i + 0] - srcLeft) * scale, (pts[i + 1] - srcTop) * scale,
    618                 (pts[i + 2] - srcLeft) * scale, (pts[i + 3] - srcTop) * scale);
    619         }
    620     }
    621 
    622     function draw(test, title) {
    623         ctx.font = "normal 50px Arial";
    624         ctx.textAlign = "left";
    625         ctx.fillStyle = "rgba(0,0,0, 0.1)";
    626         ctx.fillText(title, 50, 50);
    627         ctx.font = "normal 10px Arial";
    628         //  ctx.lineWidth = "1.001"; "0.999";
    629         var hullStarts = [];
    630         var hullEnds = [];
    631         var midSpokes = [];
    632         var midDist = [];
    633         var origin = [];
    634         var shortSpokes = [];
    635         var shortDist = [];
    636         var sweeps = [];
    637         drawnPts = [];
    638         for (var curves in test) {
    639             var curve = test[curves];
    640             origin.push(curve[0]);
    641             origin.push(curve[1]);
    642             var startPt = [];
    643             startPt.push(curve[2]);
    644             startPt.push(curve[3]);
    645             hullStarts.push(startPt);
    646             var endPt = [];
    647             if (curve.length == 4) {
    648                 endPt.push(curve[2]);
    649                 endPt.push(curve[3]);
    650             } else if (curve.length == 6 || curve.length == 7) {
    651                 endPt.push(curve[4]);
    652                 endPt.push(curve[5]);
    653             } else if (curve.length == 8) {
    654                 endPt.push(curve[6]);
    655                 endPt.push(curve[7]);
    656             }
    657             hullEnds.push(endPt);
    658             var sweep = crossPt(origin, startPt, endPt);
    659             sweeps.push(sweep);
    660             var midPt = [];
    661             midPt.push(x_at_t(curve, 0.5));
    662             midPt.push(y_at_t(curve, 0.5));
    663             midSpokes.push(midPt);
    664             var shortPt = [];
    665             shortPt.push(x_at_t(curve, 0.25));
    666             shortPt.push(y_at_t(curve, 0.25));
    667             shortSpokes.push(shortPt);
    668             var dx = midPt[0] - origin[0];
    669             var dy = midPt[1] - origin[1];
    670             var dist = Math.sqrt(dx * dx + dy * dy);
    671             midDist.push(dist);
    672             dx = shortPt[0] - origin[0];
    673             dy = shortPt[1] - origin[1];
    674             dist = Math.sqrt(dx * dx + dy * dy);
    675             shortDist.push(dist);
    676         }
    677         var intersect = [];
    678         var useIntersect = false;
    679         var maxWidth = Math.max(xmax - xmin, ymax - ymin);
    680         for (var curves in test) {
    681             var curve = test[curves];
    682             if (curve.length >= 6 && curve.length <= 8) {
    683                 var opp = curves == 0 || curves == 1 ? 0 : 1;
    684                 var sects = ray_curve_intersect(origin, hullEnds[opp], curve);
    685                 intersect.push(sects);
    686                 if (sects.length > 1) {
    687                     var intersection = sects[0];
    688                     if (intersection == 0) {
    689                         intersection = sects[1];
    690                     }
    691                     var ix = x_at_t(curve, intersection) - origin[0];
    692                     var iy = y_at_t(curve, intersection) - origin[1];
    693                     var ex = hullEnds[opp][0] - origin[0];
    694                     var ey = hullEnds[opp][1] - origin[1];
    695                     if (ix * ex >= 0 && iy * ey >= 0) {
    696                         var iDist = Math.sqrt(ix * ix + iy * iy);
    697                         var eDist = Math.sqrt(ex * ex + ey * ey);
    698                         var delta = Math.abs(iDist - eDist) / maxWidth;
    699                         if (delta > (curve.length != 8 ? 1e-5 : 1e-4)) {
    700                             useIntersect ^= true;
    701                         }
    702                     }
    703                 }
    704             }
    705         }
    706         var midLeft = curves != 0 ? crossPt(origin, midSpokes[0], midSpokes[1]) : 0;
    707         var firstInside;
    708         if (useIntersect) {
    709             var sect1 = intersect[0].length > 1;
    710             var sIndex = sect1 ? 0 : 1;
    711             var sects = intersect[sIndex];
    712             var intersection = sects[0];
    713             if (intersection == 0) {
    714                 intersection = sects[1];
    715             }
    716             var curve = test[sIndex];
    717             var ix = x_at_t(curve, intersection) - origin[0];
    718             var iy = y_at_t(curve, intersection) - origin[1];
    719             var opp = sect1 ? 1 : 0;
    720             var ex = hullEnds[opp][0] - origin[0];
    721             var ey = hullEnds[opp][1] - origin[1];
    722             var iDist = ix * ix + iy * iy;
    723             var eDist = ex * ex + ey * ey;
    724             firstInside = (iDist > eDist) ^ (sIndex == 0) ^ sweeps[0];
    725 //            console.log("iDist=" + iDist + " eDist=" + eDist + " sIndex=" + sIndex
    726  //                   + " sweeps[0]=" + sweeps[0]);
    727         } else {
    728  //           console.log("midLeft=" + midLeft);
    729             firstInside = midLeft != 0;
    730         }
    731         var shorter = midDist[1] < midDist[0];
    732         var shortLeft = shorter ? crossPt(origin, shortSpokes[0], midSpokes[1])
    733                 : crossPt(origin, midSpokes[0], shortSpokes[1]);
    734         var startCross = crossPt(origin, hullStarts[0], hullStarts[1]);
    735         var disallowShort = midLeft == startCross && midLeft == sweeps[0]
    736                     && midLeft == sweeps[1];
    737 
    738   //      console.log("midLeft=" + midLeft + " startCross=" + startCross);
    739         var intersectIndex = 0;
    740         for (var curves in test) {
    741             var curve = test[draw_id != 2 ? curves : test.length - curves - 1];
    742             if (curve.length != 4 && curve.length != 6 && curve.length != 7 && curve.length != 8) {
    743                 continue;
    744             }
    745             ctx.lineWidth = 1;
    746             if (draw_tangents != 0) {
    747                 if (draw_cubic_red ? curve.length == 8 : firstInside == curves) {
    748                     ctx.strokeStyle = "rgba(255,0,0, 0.3)";
    749                 } else {
    750                     ctx.strokeStyle = "rgba(0,0,255, 0.3)";
    751                 }
    752                 drawLine(curve[0], curve[1], curve[2], curve[3]);
    753                 if (draw_tangents != 2) {
    754                     if (curve.length > 4) drawLine(curve[2], curve[3], curve[4], curve[5]);
    755                     if (curve.length == 8) drawLine(curve[4], curve[5], curve[6], curve[7]);
    756                 }
    757                 if (draw_tangents != 1) {
    758                     if (curve.length == 6 || curve.length == 7) {
    759                         drawLine(curve[0], curve[1], curve[4], curve[5]);
    760                     }
    761                     if (curve.length == 8) drawLine(curve[0], curve[1], curve[6], curve[7]);
    762                 }
    763             }
    764             ctx.beginPath();
    765             ctx.moveTo((curve[0] - srcLeft) * scale, (curve[1] - srcTop) * scale);
    766             if (curve.length == 4) {
    767                 ctx.lineTo((curve[2] - srcLeft) * scale, (curve[3] - srcTop) * scale);
    768             } else if (curve.length == 6) {
    769                 ctx.quadraticCurveTo(
    770                     (curve[2] - srcLeft) * scale, (curve[3] - srcTop) * scale,
    771                     (curve[4] - srcLeft) * scale, (curve[5] - srcTop) * scale);
    772             } else if (curve.length == 7) {
    773                 drawConic(curve, srcLeft, srcTop, scale);
    774             } else {
    775                 ctx.bezierCurveTo(
    776                     (curve[2] - srcLeft) * scale, (curve[3] - srcTop) * scale,
    777                     (curve[4] - srcLeft) * scale, (curve[5] - srcTop) * scale,
    778                     (curve[6] - srcLeft) * scale, (curve[7] - srcTop) * scale);
    779             }
    780             if (draw_cubic_red ? curve.length == 8 : firstInside == curves) {
    781                 ctx.strokeStyle = "rgba(255,0,0, 1)";
    782             } else {
    783                 ctx.strokeStyle = "rgba(0,0,255, 1)";
    784             }
    785             ctx.stroke();
    786             if (draw_endpoints > 0) {
    787                 drawPoint(curve[0], curve[1], false);
    788                 if (draw_endpoints > 1 || curve.length == 4) {
    789                     drawPoint(curve[2], curve[3], curve.length == 4 && draw_endpoints == 3);
    790                 }
    791                 if (curve.length == 6 || curve.length == 7 ||
    792                         (draw_endpoints > 1 && curve.length == 8)) {
    793                     drawPoint(curve[4], curve[5], (curve.length == 6 || curve.length == 7) && draw_endpoints == 3);
    794                 }
    795                 if (curve.length == 8) {
    796                     drawPoint(curve[6], curve[7], curve.length == 8 && draw_endpoints == 3);
    797                 }
    798             }
    799             if (draw_midpoint != 0) {
    800                 if ((curves == 0) == (midLeft == 0)) {
    801                     ctx.strokeStyle = "rgba(0,180,127, 0.6)";
    802                 } else {
    803                     ctx.strokeStyle = "rgba(127,0,127, 0.6)";
    804                 }
    805                 var midX = x_at_t(curve, 0.5);
    806                 var midY = y_at_t(curve, 0.5);
    807                 drawPointSolid(midX, midY);
    808                 if (draw_midpoint > 1) {
    809                     drawLine(curve[0], curve[1], midX, midY);
    810                 }
    811             }
    812             if (draw_quarterpoint != 0) {
    813                 if ((curves == 0) == (shortLeft == 0)) {
    814                     ctx.strokeStyle = "rgba(0,191,63, 0.6)";
    815                 } else {
    816                     ctx.strokeStyle = "rgba(63,0,191, 0.6)";
    817                 }
    818                 var midT = (curves == 0) == shorter ? 0.25 : 0.5;
    819                 var midX = x_at_t(curve, midT);
    820                 var midY = y_at_t(curve, midT);
    821                 drawPointSolid(midX, midY);
    822                 if (draw_quarterpoint > 1) {
    823                     drawLine(curve[0], curve[1], midX, midY);
    824                 }
    825             }
    826             if (draw_sortpoint != 0) {
    827                 if ((curves == 0) == ((disallowShort == -1 ? midLeft : shortLeft) == 0)) {
    828                     ctx.strokeStyle = "rgba(0,155,37, 0.6)";
    829                 } else {
    830                     ctx.strokeStyle = "rgba(37,0,155, 0.6)";
    831                 }
    832                 var midT = (curves == 0) == shorter && disallowShort != curves ? 0.25 : 0.5;
    833                 console.log("curves=" + curves + " disallowShort=" + disallowShort
    834                         + " midLeft=" + midLeft + " shortLeft=" + shortLeft
    835                         + " shorter=" + shorter + " midT=" + midT);
    836                 var midX = x_at_t(curve, midT);
    837                 var midY = y_at_t(curve, midT);
    838                 drawPointSolid(midX, midY);
    839                 if (draw_sortpoint > 1) {
    840                     drawLine(curve[0], curve[1], midX, midY);
    841                 }
    842             }
    843             if (draw_ray_intersect != 0) {
    844                 ctx.strokeStyle = "rgba(75,45,199, 0.6)";
    845                 if (curve.length >= 6 && curve.length <= 8) {
    846                     var intersections = intersect[intersectIndex];
    847                     for (var i in intersections) {
    848                         var intersection = intersections[i];
    849                         var x = x_at_t(curve, intersection);
    850                         var y = y_at_t(curve, intersection);
    851                         drawPointSolid(x, y);
    852                         if (draw_ray_intersect > 1) {
    853                             drawLine(curve[0], curve[1], x, y);
    854                         }
    855                     }
    856                 }
    857                 ++intersectIndex;
    858             }
    859             if (draw_order) {
    860                 var px = x_at_t(curve, 0.75);
    861                 var py = y_at_t(curve, 0.75);
    862                 var _px = (px - srcLeft) * scale;
    863                 var _py = (py - srcTop) * scale;
    864                 ctx.beginPath();
    865                 ctx.arc(_px, _py, 15, 0, Math.PI * 2, true);
    866                 ctx.closePath();
    867                 ctx.fillStyle = "white";
    868                 ctx.fill();
    869                 if (draw_cubic_red ? curve.length == 8 : firstInside == curves) {
    870                     ctx.strokeStyle = "rgba(255,0,0, 1)";
    871                     ctx.fillStyle = "rgba(255,0,0, 1)";
    872                 } else {
    873                     ctx.strokeStyle = "rgba(0,0,255, 1)";
    874                     ctx.fillStyle = "rgba(0,0,255, 1)";
    875                 }
    876                 ctx.stroke();
    877                 ctx.font = "normal 16px Arial";
    878                 ctx.textAlign = "center";
    879                 ctx.fillText(parseInt(curves) + 1, _px, _py + 5);
    880             }
    881             if (draw_closest_t) {
    882                 var t = curveClosestT(curve, mouseX, mouseY);
    883                 if (t >= 0) {
    884                     var x = x_at_t(curve, t);
    885                     var y = y_at_t(curve, t);
    886                     drawPointSolid(x, y);
    887                 }
    888             }
    889             if (!approximately_zero(scale - initScale)) {
    890                 ctx.font = "normal 20px Arial";
    891                 ctx.fillStyle = "rgba(0,0,0, 0.3)";
    892                 ctx.textAlign = "right";
    893                 ctx.fillText(scale.toFixed(decimal_places) + 'x',
    894                         screenWidth - 10, screenHeight - 5);
    895             }
    896             if (draw_t) {
    897                 drawPointAtT(curve);
    898             }
    899             if (draw_id != 0) {
    900                 var id = -1;
    901                 for (var i = 0; i < ids.length; i += 2) {
    902                     if (ids[i + 1] == curve) {
    903                         id = ids[i];
    904                         break;
    905                     }
    906                 }
    907                 if (id >= 0) {
    908                     var px = x_at_t(curve, 0.5);
    909                     var py = y_at_t(curve, 0.5);
    910                     var _px = (px - srcLeft) * scale;
    911                     var _py = (py - srcTop) * scale;
    912                     ctx.beginPath();
    913                     ctx.arc(_px, _py, 15, 0, Math.PI * 2, true);
    914                     ctx.closePath();
    915                     ctx.fillStyle = "white";
    916                     ctx.fill();
    917                     ctx.strokeStyle = "rgba(255,0,0, 1)";
    918                     ctx.fillStyle = "rgba(255,0,0, 1)";
    919                     ctx.stroke();
    920                     ctx.font = "normal 16px Arial";
    921                     ctx.textAlign = "center";
    922                     ctx.fillText(id, _px, _py + 5);
    923                 }
    924             }
    925         }
    926         if (draw_t) {
    927             drawCurveTControl();
    928         }
    929         if (draw_w) {
    930             drawCurveWControl();
    931         }
    932     }
    933 
    934     function drawCurveTControl() {
    935         ctx.lineWidth = 2;
    936         ctx.strokeStyle = "rgba(0,0,0, 0.3)";
    937         ctx.beginPath();
    938         ctx.rect(screenWidth - 80, 40, 28, screenHeight - 80);
    939         ctx.stroke();
    940         var ty = 40 + curveT * (screenHeight - 80);
    941         ctx.beginPath();
    942         ctx.moveTo(screenWidth - 80, ty);
    943         ctx.lineTo(screenWidth - 85, ty - 5);
    944         ctx.lineTo(screenWidth - 85, ty + 5);
    945         ctx.lineTo(screenWidth - 80, ty);
    946         ctx.fillStyle = "rgba(0,0,0, 0.6)";
    947         ctx.fill();
    948         var num = curveT.toFixed(decimal_places);
    949         ctx.font = "normal 10px Arial";
    950         ctx.textAlign = "left";
    951         ctx.fillText(num, screenWidth - 78, ty);
    952     }
    953 
    954     function drawCurveWControl() {
    955         var w = -1;
    956         var choice = 0;
    957         for (var curves in tests[testIndex]) {
    958             var curve = tests[testIndex][curves];
    959             if (curve.length != 7) {
    960                 continue;
    961             }
    962             if (choice == curveW) {
    963                 w = curve[6];
    964                 break;
    965             }
    966             ++choice;
    967         }
    968         if (w < 0) {
    969             return;
    970         }
    971         ctx.lineWidth = 2;
    972         ctx.strokeStyle = "rgba(0,0,0, 0.3)";
    973         ctx.beginPath();
    974         ctx.rect(screenWidth - 40, 40, 28, screenHeight - 80);
    975         ctx.stroke();
    976         var ty = 40 + w * (screenHeight - 80);
    977         ctx.beginPath();
    978         ctx.moveTo(screenWidth - 40, ty);
    979         ctx.lineTo(screenWidth - 45, ty - 5);
    980         ctx.lineTo(screenWidth - 45, ty + 5);
    981         ctx.lineTo(screenWidth - 40, ty);
    982         ctx.fillStyle = "rgba(0,0,0, 0.6)";
    983         ctx.fill();
    984         var num = w.toFixed(decimal_places);
    985         ctx.font = "normal 10px Arial";
    986         ctx.textAlign = "left";
    987         ctx.fillText(num, screenWidth - 38, ty);
    988     }
    989 
    990     function ptInTControl() {
    991         var e = window.event;
    992         var tgt = e.target || e.srcElement;
    993         var left = tgt.offsetLeft;
    994         var top = tgt.offsetTop;
    995         var x = (e.clientX - left);
    996         var y = (e.clientY - top);
    997         if (x < screenWidth - 80 || x > screenWidth - 50) {
    998             return false;
    999         }
   1000         if (y < 40 || y > screenHeight - 80) {
   1001             return false;
   1002         }
   1003         curveT = (y - 40) / (screenHeight - 120);
   1004         if (curveT < 0 || curveT > 1) {
   1005             throw "stop execution";
   1006         }
   1007         return true;
   1008     }
   1009 
   1010     function ptInWControl() {
   1011         var e = window.event;
   1012         var tgt = e.target || e.srcElement;
   1013         var left = tgt.offsetLeft;
   1014         var top = tgt.offsetTop;
   1015         var x = (e.clientX - left);
   1016         var y = (e.clientY - top);
   1017         if (x < screenWidth - 40 || x > screenWidth - 10) {
   1018             return false;
   1019         }
   1020         if (y < 40 || y > screenHeight - 80) {
   1021             return false;
   1022         }
   1023         var w = (y - 40) / (screenHeight - 120);
   1024         if (w < 0 || w > 1) {
   1025             throw "stop execution";
   1026         }
   1027         var choice = 0;
   1028         for (var curves in tests[testIndex]) {
   1029             var curve = tests[testIndex][curves];
   1030             if (curve.length != 7) {
   1031                 continue;
   1032             }
   1033             if (choice == curveW) {
   1034                 curve[6] = w;
   1035                 break;
   1036             }
   1037             ++choice;
   1038         }
   1039         return true;
   1040     }
   1041 
   1042     function drawTop() {
   1043         init(tests[testIndex]);
   1044         redraw();
   1045     }
   1046 
   1047     function redraw() {
   1048         if (focus_on_selection > 0) {
   1049             var focusXmin = focusYmin = Infinity;
   1050             var focusXmax = focusYmax = -Infinity;
   1051             var choice = 0;
   1052             for (var curves in tests[testIndex]) {
   1053                 if (++choice != focus_on_selection) {
   1054                     continue;
   1055                 }
   1056                 var curve = tests[testIndex][curves];
   1057                 var last = curve.length - (curve.length % 2 == 1 ? 1 : 0);
   1058                 for (var idx = 0; idx < last; idx += 2) {
   1059                     focusXmin = Math.min(focusXmin, curve[idx]);
   1060                     focusXmax = Math.max(focusXmax, curve[idx]);
   1061                     focusYmin = Math.min(focusYmin, curve[idx + 1]);
   1062                     focusYmax = Math.max(focusYmax, curve[idx + 1]);
   1063                 }
   1064             }
   1065             focusXmin -= Math.min(1, Math.max(focusXmax - focusXmin, focusYmax - focusYmin));
   1066             if (focusXmin < focusXmax && focusYmin < focusYmax) {
   1067                 setScale(focusXmin, focusXmax, focusYmin, focusYmax);
   1068             }
   1069         }
   1070         ctx.beginPath();
   1071         ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
   1072         ctx.fillStyle = "white";
   1073         ctx.fill();
   1074         draw(tests[testIndex], testTitles[testIndex]);
   1075     }
   1076 
   1077     function doKeyPress(evt) {
   1078         var char = String.fromCharCode(evt.charCode);
   1079         var focusWasOn = false;
   1080         switch (char) {
   1081             case '0':
   1082             case '1':
   1083             case '2':
   1084             case '3':
   1085             case '4':
   1086             case '5':
   1087             case '6':
   1088             case '7':
   1089             case '8':
   1090             case '9':
   1091                 decimal_places = char - '0';
   1092                 redraw();
   1093                 break;
   1094             case '-':
   1095                 focusWasOn = focus_on_selection;
   1096                 if (focusWasOn) {
   1097                     focus_on_selection = false;
   1098                     scale /= 1.2;
   1099                 } else {
   1100                     scale /= 2;
   1101                 }
   1102                 calcLeftTop();
   1103                 redraw();
   1104                 focus_on_selection = focusWasOn;
   1105                 break;
   1106             case '=':
   1107             case '+':
   1108                 focusWasOn = focus_on_selection;
   1109                 if (focusWasOn) {
   1110                     focus_on_selection = false;
   1111                     scale *= 1.2;
   1112                 } else {
   1113                     scale *= 2;
   1114                 }
   1115                 calcLeftTop();
   1116                 redraw();
   1117                 focus_on_selection = focusWasOn;
   1118                 break;
   1119             case 'b':
   1120                 draw_cubic_red ^= true;
   1121                 redraw();
   1122                 break;
   1123             case 'c':
   1124                 drawTop();
   1125                 break;
   1126             case 'd':
   1127                 var test = tests[testIndex];
   1128                 var testClone = [];
   1129                 for (var curves in test) {
   1130                     var c = test[curves];
   1131                     var cClone = [];
   1132                     for (var index = 0; index < c.length; ++index) {
   1133                         cClone.push(c[index]);
   1134                     }
   1135                     testClone.push(cClone);
   1136                 }
   1137                 tests.push(testClone);
   1138                 testTitles.push(testTitles[testIndex] + " copy");
   1139                 testIndex = tests.length - 1;
   1140                 redraw();
   1141                 break;
   1142             case 'e':
   1143                 draw_endpoints = (draw_endpoints + 1) % 4;
   1144                 redraw();
   1145                 break;
   1146             case 'f':
   1147                 draw_derivative ^= true;
   1148                 redraw();
   1149                 break;
   1150             case 'i':
   1151                 draw_ray_intersect = (draw_ray_intersect + 1) % 3;
   1152                 redraw();
   1153                 break;
   1154             case 'l':
   1155                 var test = tests[testIndex];
   1156                 console.log("<div id=\"" + testTitles[testIndex] + "\" >");
   1157                 for (var curves in test) {
   1158                     var c = test[curves];
   1159                     var s = "{{";
   1160                     for (var i = 0; i < c.length; i += 2) {
   1161                         s += "{";
   1162                         s += c[i] + "," + c[i + 1];
   1163                         s += "}";
   1164                         if (i + 2 < c.length) {
   1165                             s += ", ";
   1166                         }
   1167                     }
   1168                     console.log(s + "}},");
   1169                 }
   1170                 console.log("</div>");
   1171                 break;
   1172             case 'm':
   1173                 draw_midpoint = (draw_midpoint + 1) % 3;
   1174                 redraw();
   1175                 break;
   1176             case 'N':
   1177                 testIndex += 9;
   1178             case 'n':
   1179                 testIndex = (testIndex + 1) % tests.length;
   1180                 drawTop();
   1181                 break;
   1182             case 'o':
   1183                 draw_order ^= true;
   1184                 redraw();
   1185                 break;
   1186             case 'P':
   1187                 testIndex -= 9;
   1188             case 'p':
   1189                 if (--testIndex < 0)
   1190                     testIndex = tests.length - 1;
   1191                 drawTop();
   1192                 break;
   1193             case 'q':
   1194                 draw_quarterpoint = (draw_quarterpoint + 1) % 3;
   1195                 redraw();
   1196                 break;
   1197             case 'r':
   1198                 for (var i = 0; i < testDivs.length; ++i) {
   1199                     var title = testDivs[i].id.toString();
   1200                     if (title == testTitles[testIndex]) {
   1201                         var str = testDivs[i].firstChild.data;
   1202                         parse(str, title);
   1203                         var original = tests.pop();
   1204                         testTitles.pop();
   1205                         tests[testIndex] = original;
   1206                         break;
   1207                     }
   1208                 }
   1209                 redraw();
   1210                 break;
   1211             case 's':
   1212                 draw_sortpoint = (draw_sortpoint + 1) % 3;
   1213                 redraw();
   1214                 break;
   1215             case 't':
   1216                 draw_t ^= true;
   1217                 redraw();
   1218                 break;
   1219             case 'u':
   1220                 draw_closest_t ^= true;
   1221                 redraw();
   1222                 break;
   1223             case 'v':
   1224                 draw_tangents = (draw_tangents + 1) % 4;
   1225                 redraw();
   1226                 break;
   1227             case 'w':
   1228                 ++curveW;
   1229                 var choice = 0;
   1230                 draw_w = false;
   1231                 for (var curves in tests[testIndex]) {
   1232                     var curve = tests[testIndex][curves];
   1233                     if (curve.length != 7) {
   1234                         continue;
   1235                     }
   1236                     if (choice == curveW) {
   1237                         draw_w = true;
   1238                         break;
   1239                     }
   1240                     ++choice;
   1241                 }
   1242                 if (!draw_w) {
   1243                     curveW = -1;
   1244                 }
   1245                 redraw();
   1246                 break;
   1247             case 'x':
   1248                 draw_point_xy ^= true;
   1249                 redraw();
   1250                 break;
   1251             case 'y':
   1252                 draw_mouse_xy ^= true;
   1253                 redraw();
   1254                 break;
   1255             case '\\':
   1256                 retina_scale ^= true;
   1257                 drawTop();
   1258                 break;
   1259             case '`':
   1260                 ++focus_on_selection;
   1261                 if (focus_on_selection >= tests[testIndex].length) {
   1262                     focus_on_selection = 0;
   1263                 }
   1264                 setScale(xmin, xmax, ymin, ymax);
   1265                 redraw();
   1266                 break;
   1267             case '.':
   1268                 draw_id = (draw_id + 1) % 3;
   1269                 redraw();
   1270                 break;
   1271         }
   1272     }
   1273 
   1274     function doKeyDown(evt) {
   1275         var char = evt.keyCode;
   1276         var preventDefault = false;
   1277         switch (char) {
   1278             case 37: // left arrow
   1279                 if (evt.shiftKey) {
   1280                     testIndex -= 9;
   1281                 }
   1282                 if (--testIndex < 0)
   1283                     testIndex = tests.length - 1;
   1284                 if (evt.ctrlKey) {
   1285                     redraw();
   1286                 } else {
   1287                     drawTop();
   1288                 }
   1289                 preventDefault = true;
   1290                 break;
   1291             case 39: // right arrow
   1292                 if (evt.shiftKey) {
   1293                     testIndex += 9;
   1294                 }
   1295                 if (++testIndex >= tests.length)
   1296                     testIndex = 0;
   1297                 if (evt.ctrlKey) {
   1298                     redraw();
   1299                 } else {
   1300                     drawTop();
   1301                 }
   1302                 preventDefault = true;
   1303                 break;
   1304         }
   1305         if (preventDefault) {
   1306             evt.preventDefault();
   1307             return false;
   1308         }
   1309         return true;
   1310     }
   1311 
   1312     function calcXY() {
   1313         var e = window.event;
   1314         var tgt = e.target || e.srcElement;
   1315         var left = tgt.offsetLeft;
   1316         var top = tgt.offsetTop;
   1317         mouseX = (e.clientX - left) / scale + srcLeft;
   1318         mouseY = (e.clientY - top) / scale + srcTop;
   1319     }
   1320 
   1321     function calcLeftTop() {
   1322         srcLeft = mouseX - screenWidth / 2 / scale;
   1323         srcTop = mouseY - screenHeight / 2 / scale;
   1324     }
   1325 
   1326     function handleMouseClick() {
   1327         if ((!draw_t || !ptInTControl()) && (!draw_w || !ptInWControl())) {
   1328             calcXY();
   1329         } else {
   1330             redraw();
   1331         }
   1332     }
   1333 
   1334     function initDown() {
   1335         var test = tests[testIndex];
   1336         var bestDistance = 1000000;
   1337         activePt = -1;
   1338         for (var curves in test) {
   1339             var testCurve = test[curves];
   1340             if (testCurve.length != 4 && (testCurve.length < 6 || testCurve.length > 8)) {
   1341                 continue;
   1342             }
   1343             var testMax = testCurve.length == 7 ? 6 : testCurve.length;
   1344             for (var i = 0; i < testMax; i += 2) {
   1345                 var testX = testCurve[i];
   1346                 var testY = testCurve[i + 1];
   1347                 var dx = testX - mouseX;
   1348                 var dy = testY - mouseY;
   1349                 var dist = dx * dx + dy * dy;
   1350                 if (dist > bestDistance) {
   1351                     continue;
   1352                 }
   1353                 activeCurve = testCurve;
   1354                 activePt = i;
   1355                 bestDistance = dist;
   1356             }
   1357         }
   1358         if (activePt >= 0) {
   1359             lastX = mouseX;
   1360             lastY = mouseY;
   1361         }
   1362     }
   1363 
   1364     function handleMouseOver() {
   1365         calcXY();
   1366         if (draw_mouse_xy) {
   1367             var num = mouseX.toFixed(decimal_places) + ", " + mouseY.toFixed(decimal_places);
   1368             ctx.beginPath();
   1369             ctx.rect(300, 100, num.length * 6, 10);
   1370             ctx.fillStyle = "white";
   1371             ctx.fill();
   1372             ctx.font = "normal 10px Arial";
   1373             ctx.fillStyle = "black";
   1374             ctx.textAlign = "left";
   1375             ctx.fillText(num, 300, 108);
   1376         }
   1377         if (!mouseDown) {
   1378             activePt = -1;
   1379             return;
   1380         }
   1381         if (activePt < 0) {
   1382             initDown();
   1383             return;
   1384         }
   1385         var deltaX = mouseX - lastX;
   1386         var deltaY = mouseY - lastY;
   1387         lastX = mouseX;
   1388         lastY = mouseY;
   1389         if (activePt == 0) {
   1390             var test = tests[testIndex];
   1391             for (var curves in test) {
   1392                 var testCurve = test[curves];
   1393                 testCurve[0] += deltaX;
   1394                 testCurve[1] += deltaY;
   1395             }
   1396         } else {
   1397             activeCurve[activePt] += deltaX;
   1398             activeCurve[activePt + 1] += deltaY;
   1399         }
   1400         redraw();
   1401     }
   1402 
   1403     function start() {
   1404         for (var i = 0; i < testDivs.length; ++i) {
   1405             var title = testDivs[i].id.toString();
   1406             var str = testDivs[i].firstChild.data;
   1407             parse(str, title);
   1408         }
   1409         drawTop();
   1410         window.addEventListener('keypress', doKeyPress, true);
   1411         window.addEventListener('keydown', doKeyDown, true);
   1412         window.onresize = function () {
   1413             drawTop();
   1414         }
   1415     }
   1416 
   1417 </script>
   1418 </head>
   1419 
   1420 <body onLoad="start();">
   1421 
   1422 <canvas id="canvas" width="750" height="500"
   1423     onmousedown="mouseDown = true"
   1424     onmouseup="mouseDown = false"
   1425     onmousemove="handleMouseOver()"
   1426     onclick="handleMouseClick()"
   1427     ></canvas >
   1428 </body>
   1429 </html>