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