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