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