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="sect0">
     10 {{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
     11 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     12 </div>
     13 
     14 <div id="sect1">
     15 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     16 {{{{306.58801299999999, -227.983994}, {212.46499600000001, -262.24200400000001}, {95.551200899999998, 58.976398500000002}}}, 0.707107008f} id=1
     17 {{{{377.21899400000001, -141.98100299999999}, {237.77799285476553, -166.56830755921084}, {134.08399674208422, -155.06258330544892}}}, 0.788580656f} id=2
     18 {{{{134.08399674208422, -155.06258330544892}, {30.390000629402859, -143.55685905168704}, {23.185499199999999, -102.697998}}}, 0.923879623f} id=4
     19 </div>
     20 
     21 <div id="sect2">
     22 {{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
     23 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     24 {{{{205.78973252799028, -158.12538713371103}, {143.97848953841861, -74.076645245042371}, {95.551200899999998, 58.976398500000002}}}, 0.923879623f} id=3
     25 {{{{377.21899400000001, -141.98100299999999}, {237.77799285476553, -166.56830755921084}, {134.08399674208422, -155.06258330544892}}}, 0.788580656f} id=2
     26 </div>
     27 
     28 <div id="sect3">
     29 {{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
     30 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     31 {{{{205.78973252799028, -158.12538713371103}, {143.97848953841861, -74.076645245042371}, {95.551200899999998, 58.976398500000002}}}, 0.923879623f} id=3
     32 {{{{252.08225670812539, -156.90491625851064}, {185.93099479842493, -160.81544543232982}, {134.08399674208422, -155.06258330544892}}}, 0.835816324f} id=6
     33 </div>
     34 
     35 <div id="sect4">
     36 {{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
     37 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     38 {{{{205.78973252799028, -158.12538713371103}, {174.88411103320448, -116.10101618937664}, {145.19509369736275, -56.857102571363754}}}, 0.871667147f} id=3
     39 {{{{252.08225670812539, -156.90491625851064}, {185.93099479842493, -160.81544543232982}, {134.08399674208422, -155.06258330544892}}}, 0.835816324f} id=6
     40 </div>
     41 
     42 <div id="sect5">
     43 {{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
     44 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     45 {{{{205.78973252799028, -158.12538713371103}, {174.88411103320448, -116.10101618937664}, {145.19509369736275, -56.857102571363754}}}, 0.871667147f} id=3
     46 {{{{252.08225670812539, -156.90491625851064}, {219.70109133058406, -158.81912754088933}, {190.17095392508796, -158.38373974664466}}}, 0.858306944f} id=6
     47 </div>
     48 
     49 <div id="sect6">
     50 {{{{306.588013,-227.983994}, {212.464996,-262.242004}, {95.5512009,58.9763985}}}, 0.707107008f},
     51 {{{{377.218994,-141.981003}, {40.578701,-201.339996}, {23.1854992,-102.697998}}}, 0.707107008f},
     52 {{{{205.78973252799028, -158.12538713371103}, {190.33692178059735, -137.11320166154385}, {174.87004877564593, -111.2132534799228}}}, 0.858117759f} id=3
     53 {{{{252.08225670812539, -156.90491625851064}, {219.70109133058406, -158.81912754088933}, {190.17095392508796, -158.38373974664466}}}, 0.858306944f} id=6
     54 </div>
     55 
     56 </div>
     57 
     58 <script type="text/javascript">
     59 
     60 var testDivs = [
     61 sect0,
     62 sect1,
     63 sect2,
     64 sect3,
     65 sect4,
     66 sect5,
     67 sect6,
     68 ];
     69 
     70     var decimal_places = 3;
     71 
     72     var tests = [];
     73     var testTitles = [];
     74     var testIndex = 0;
     75     var ctx;
     76 
     77     var subscale = 1;
     78     var xmin, xmax, ymin, ymax;
     79     var scale;
     80     var initScale;
     81     var mouseX, mouseY;
     82     var mouseDown = false;
     83     var srcLeft, srcTop;
     84     var screenWidth, screenHeight;
     85     var drawnPts;
     86     var curveT = 0;
     87     var curveW = -1;
     88 
     89     var lastX, lastY;
     90     var activeCurve = [];
     91     var activePt;
     92     var ids = [];
     93 
     94     var focus_on_selection = 0;
     95     var draw_t = false;
     96     var draw_w = false;
     97     var draw_closest_t = false;
     98     var draw_cubic_red = false;
     99     var draw_derivative = false;
    100     var draw_endpoints = 2;
    101     var draw_id = 0;
    102     var draw_midpoint = 0;
    103     var draw_mouse_xy = false;
    104     var draw_order = false;
    105     var draw_point_xy = false;
    106     var draw_ray_intersect = false;
    107     var draw_quarterpoint = 0;
    108     var draw_tangents = 1;
    109     var draw_sortpoint = 0;
    110     var retina_scale = !!window.devicePixelRatio;
    111 
    112     function parse(test, title) {
    113         var curveStrs = test.split("{{");
    114         var pattern = /-?\d+\.*\d*e?-?\d*/g;
    115         var curves = [];
    116         for (var c in curveStrs) {
    117             var curveStr = curveStrs[c];
    118             var idPart = curveStr.split("id=");
    119             var id = -1;
    120             if (idPart.length == 2) {
    121                 id = parseInt(idPart[1]);
    122                 curveStr = idPart[0];
    123             }
    124             var points = curveStr.match(pattern);
    125             var pts = [];
    126             for (var wd in points) {
    127                 var num = parseFloat(points[wd]);
    128                 if (isNaN(num)) continue;
    129                 pts.push(num);
    130             }
    131             if (pts.length > 2) {
    132                 curves.push(pts);
    133             }
    134             if (id >= 0) {
    135                 ids.push(id);
    136                 ids.push(pts);
    137             }
    138         }
    139         if (curves.length >= 1) {
    140             tests.push(curves);
    141             testTitles.push(title);
    142         }
    143     }
    144 
    145     function init(test) {
    146         var canvas = document.getElementById('canvas');
    147         if (!canvas.getContext) return;
    148         ctx = canvas.getContext('2d');
    149         var resScale = retina_scale && window.devicePixelRatio ? window.devicePixelRatio : 1;
    150         var unscaledWidth = window.innerWidth - 20;
    151         var unscaledHeight = window.innerHeight - 20;
    152         screenWidth = unscaledWidth;
    153         screenHeight = unscaledHeight;
    154         canvas.width = unscaledWidth * resScale;
    155         canvas.height = unscaledHeight * resScale;
    156         canvas.style.width = unscaledWidth + 'px';
    157         canvas.style.height = unscaledHeight + 'px';
    158         if (resScale != 1) {
    159             ctx.scale(resScale, resScale);
    160         }
    161         xmin = Infinity;
    162         xmax = -Infinity;
    163         ymin = Infinity;
    164         ymax = -Infinity;
    165         for (var curves in test) {
    166             var curve = test[curves];
    167             var last = curve.length - (curve.length % 2 == 1 ? 1 : 0);
    168             for (var idx = 0; idx < last; idx += 2) {
    169                 xmin = Math.min(xmin, curve[idx]);
    170                 xmax = Math.max(xmax, curve[idx]);
    171                 ymin = Math.min(ymin, curve[idx + 1]);
    172                 ymax = Math.max(ymax, curve[idx + 1]);
    173             }
    174         }
    175         xmin -= Math.min(1, Math.max(xmax - xmin, ymax - ymin));
    176         var testW = xmax - xmin;
    177         var testH = ymax - ymin;
    178         subscale = 1;
    179         while (testW * subscale < 0.1 && testH * subscale < 0.1) {
    180             subscale *= 10;
    181         }
    182         while (testW * subscale > 10 && testH * subscale > 10) {
    183             subscale /= 10;
    184         }
    185         setScale(xmin, xmax, ymin, ymax);
    186         mouseX = (screenWidth / 2) / scale + srcLeft;
    187         mouseY = (screenHeight / 2) / scale + srcTop;
    188         initScale = scale;
    189     }
    190 
    191     function setScale(x0, x1, y0, y1) {
    192         var srcWidth = x1 - x0;
    193         var srcHeight = y1 - y0;
    194         var usableWidth = screenWidth;
    195         var xDigits = Math.ceil(Math.log(Math.abs(xmax)) / Math.log(10));
    196         var yDigits = Math.ceil(Math.log(Math.abs(ymax)) / Math.log(10));
    197         usableWidth -= (xDigits + yDigits) * 10;
    198         usableWidth -= decimal_places * 10;
    199         var hscale = usableWidth / srcWidth;
    200         var vscale = screenHeight / srcHeight;
    201         scale = Math.min(hscale, vscale);
    202         var invScale = 1 / scale;
    203         var sxmin = x0 - invScale * 5;
    204         var symin = y0 - invScale * 10;
    205         var sxmax = x1 + invScale * (6 * decimal_places + 10);
    206         var symax = y1 + invScale * 10;
    207         srcWidth = sxmax - sxmin;
    208         srcHeight = symax - symin;
    209         hscale = usableWidth / srcWidth;
    210         vscale = screenHeight / srcHeight;
    211         scale = Math.min(hscale, vscale);
    212         srcLeft = sxmin;
    213         srcTop = symin;
    214     }
    215 
    216 function dxy_at_t(curve, t) {
    217     var dxy = {};
    218     if (curve.length == 6) {
    219         var a = t - 1;
    220         var b = 1 - 2 * t;
    221         var c = t;
    222         dxy.x = a * curve[0] + b * curve[2] + c * curve[4];
    223         dxy.y = a * curve[1] + b * curve[3] + c * curve[5];
    224     } else if (curve.length == 7) {
    225         var p20x = curve[4] - curve[0];
    226         var p20y = curve[5] - curve[1];
    227         var p10xw = (curve[2] - curve[0]) * curve[6];
    228         var p10yw = (curve[3] - curve[1]) * curve[6];
    229         var coeff0x = curve[6] * p20x - p20x;
    230         var coeff0y = curve[6] * p20y - p20y;
    231         var coeff1x = p20x - 2 * p10xw;
    232         var coeff1y = p20y - 2 * p10yw;
    233         dxy.x = t * (t * coeff0x + coeff1x) + p10xw;
    234         dxy.y = t * (t * coeff0y + coeff1y) + p10yw;
    235     } else if (curve.length == 8) {
    236         var one_t = 1 - t;
    237         var a = curve[0];
    238         var b = curve[2];
    239         var c = curve[4];
    240         var d = curve[6];
    241         dxy.x = 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
    242         a = curve[1];
    243         b = curve[3];
    244         c = curve[5];
    245         d = curve[7];
    246         dxy.y = 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
    247     }
    248     return dxy;
    249 }
    250 
    251     var flt_epsilon = 1.19209290E-07;
    252 
    253     function approximately_zero(A) {
    254         return Math.abs(A) < flt_epsilon;
    255     }
    256 
    257     function approximately_zero_inverse(A) {
    258         return Math.abs(A) > (1 / flt_epsilon);
    259     }
    260 
    261     function quad_real_roots(A, B, C) {
    262         var s = [];
    263         var p = B / (2 * A);
    264         var q = C / A;
    265         if (approximately_zero(A) && (approximately_zero_inverse(p)
    266                 || approximately_zero_inverse(q))) {
    267             if (approximately_zero(B)) {
    268                 if (C == 0) {
    269                     s[0] = 0;
    270                 }
    271                 return s;
    272             }
    273             s[0] = -C / B;
    274             return s;
    275         }
    276         /* normal form: x^2 + px + q = 0 */
    277         var p2 = p * p;
    278         if (!approximately_zero(p2 - q) && p2 < q) {
    279             return s;
    280         }
    281         var sqrt_D = 0;
    282         if (p2 > q) {
    283             sqrt_D = Math.sqrt(p2 - q);
    284         }
    285         s[0] = sqrt_D - p;
    286         var flip = -sqrt_D - p;
    287         if (!approximately_zero(s[0] - flip)) {
    288             s[1] = flip;
    289         }
    290         return s;
    291     }
    292 
    293     function cubic_real_roots(A, B, C, D) {
    294         if (approximately_zero(A)) {  // we're just a quadratic
    295             return quad_real_roots(B, C, D);
    296         }
    297         if (approximately_zero(D)) {  // 0 is one root
    298             var s = quad_real_roots(A, B, C);
    299             for (var i = 0; i < s.length; ++i) {
    300                 if (approximately_zero(s[i])) {
    301                     return s;
    302                 }
    303             }
    304             s.push(0);
    305             return s;
    306         }
    307         if (approximately_zero(A + B + C + D)) {  // 1 is one root
    308             var s = quad_real_roots(A, A + B, -D);
    309             for (var i = 0; i < s.length; ++i) {
    310                 if (approximately_zero(s[i] - 1)) {
    311                     return s;
    312                 }
    313             }
    314             s.push(1);
    315             return s;
    316         }
    317         var a, b, c;
    318         var invA = 1 / A;
    319         a = B * invA;
    320         b = C * invA;
    321         c = D * invA;
    322         var a2 = a * a;
    323         var Q = (a2 - b * 3) / 9;
    324         var R = (2 * a2 * a - 9 * a * b + 27 * c) / 54;
    325         var R2 = R * R;
    326         var Q3 = Q * Q * Q;
    327         var R2MinusQ3 = R2 - Q3;
    328         var adiv3 = a / 3;
    329         var r;
    330         var roots = [];
    331         if (R2MinusQ3 < 0) {   // we have 3 real roots
    332             var theta = Math.acos(R / Math.sqrt(Q3));
    333             var neg2RootQ = -2 * Math.sqrt(Q);
    334             r = neg2RootQ * Math.cos(theta / 3) - adiv3;
    335             roots.push(r);
    336             r = neg2RootQ * Math.cos((theta + 2 * Math.PI) / 3) - adiv3;
    337             if (!approximately_zero(roots[0] - r)) {
    338                 roots.push(r);
    339             }
    340             r = neg2RootQ * Math.cos((theta - 2 * Math.PI) / 3) - adiv3;
    341             if (!approximately_zero(roots[0] - r) && (roots.length == 1
    342                         || !approximately_zero(roots[1] - r))) {
    343                 roots.push(r);
    344             }
    345         } else {  // we have 1 real root
    346             var sqrtR2MinusQ3 = Math.sqrt(R2MinusQ3);
    347             var A = Math.abs(R) + sqrtR2MinusQ3;
    348             A = Math.pow(A, 1/3);
    349             if (R > 0) {
    350                 A = -A;
    351             }
    352             if (A != 0) {
    353                 A += Q / A;
    354             }
    355             r = A - adiv3;
    356             roots.push(r);
    357             if (approximately_zero(R2 - Q3)) {
    358                 r = -A / 2 - adiv3;
    359                 if (!approximately_zero(roots[0] - r)) {
    360                     roots.push(r);
    361                 }
    362             }
    363         }
    364         return roots;
    365     }
    366 
    367     function approximately_zero_or_more(tValue) {
    368         return tValue >= -flt_epsilon;
    369     }
    370 
    371     function approximately_one_or_less(tValue) {
    372         return tValue <= 1 + flt_epsilon;
    373     }
    374 
    375     function approximately_less_than_zero(tValue) {
    376         return tValue < flt_epsilon;
    377     }
    378 
    379     function approximately_greater_than_one(tValue) {
    380         return tValue > 1 - flt_epsilon;
    381     }
    382 
    383     function add_valid_ts(s) {
    384         var t = [];
    385     nextRoot:
    386         for (var index = 0; index < s.length; ++index) {
    387             var tValue = s[index];
    388             if (approximately_zero_or_more(tValue) && approximately_one_or_less(tValue)) {
    389                 if (approximately_less_than_zero(tValue)) {
    390                     tValue = 0;
    391                 } else if (approximately_greater_than_one(tValue)) {
    392                     tValue = 1;
    393                 }
    394                 for (var idx2 = 0; idx2 < t.length; ++idx2) {
    395                     if (approximately_zero(t[idx2] - tValue)) {
    396                         continue nextRoot;
    397                     }
    398                 }
    399                 t.push(tValue);
    400             }
    401         }
    402         return t;
    403     }
    404 
    405     function quad_roots(A, B, C) {
    406         var s = quad_real_roots(A, B, C);
    407         var foundRoots = add_valid_ts(s);
    408         return foundRoots;
    409     }
    410 
    411     function cubic_roots(A, B, C, D) {
    412         var s = cubic_real_roots(A, B, C, D);
    413         var foundRoots = add_valid_ts(s);
    414         return foundRoots;
    415     }
    416 
    417     function ray_curve_intersect(startPt, endPt, curve) {
    418         var adj = endPt[0] - startPt[0];
    419         var opp = endPt[1] - startPt[1];
    420         var r = [];
    421         var len = (curve.length == 7 ? 6 : curve.length) / 2;
    422         for (var n = 0; n < len; ++n) {
    423             r[n] = (curve[n * 2 + 1] - startPt[1]) * adj - (curve[n * 2] - startPt[0]) * opp;
    424         }
    425         if (curve.length == 6) {
    426             var A = r[2];
    427             var B = r[1];
    428             var C = r[0];
    429             A += C - 2 * B;  // A = a - 2*b + c
    430             B -= C;  // B = -(b - c)
    431             return quad_roots(A, 2 * B, C);
    432         }
    433         if (curve.length == 7) {
    434             var A = r[2];
    435             var B = r[1] * curve[6];
    436             var C = r[0];
    437             A += C - 2 * B;  // A = a - 2*b + c
    438             B -= C;  // B = -(b - c)
    439             return quad_roots(A, 2 * B, C);
    440         }
    441         var A = r[3];       // d
    442         var B = r[2] * 3;   // 3*c
    443         var C = r[1] * 3;   // 3*b
    444         var D = r[0];       // a
    445         A -= D - C + B;     // A =   -a + 3*b - 3*c + d
    446         B += 3 * D - 2 * C; // B =  3*a - 6*b + 3*c
    447         C -= 3 * D;         // C = -3*a + 3*b
    448         return cubic_roots(A, B, C, D);
    449     }
    450 
    451     function x_at_t(curve, t) {
    452         var one_t = 1 - t;
    453         if (curve.length == 4) {
    454             return one_t * curve[0] + t * curve[2];
    455         }
    456         var one_t2 = one_t * one_t;
    457         var t2 = t * t;
    458         if (curve.length == 6) {
    459             return one_t2 * curve[0] + 2 * one_t * t * curve[2] + t2 * curve[4];
    460         }
    461         if (curve.length == 7) {
    462             var numer = one_t2 * curve[0] + 2 * one_t * t * curve[2] * curve[6]
    463                     + t2 * curve[4];
    464             var denom = one_t2            + 2 * one_t * t            * curve[6]
    465                     + t2;
    466             return numer / denom;
    467         }
    468         var a = one_t2 * one_t;
    469         var b = 3 * one_t2 * t;
    470         var c = 3 * one_t * t2;
    471         var d = t2 * t;
    472         return a * curve[0] + b * curve[2] + c * curve[4] + d * curve[6];
    473     }
    474 
    475     function y_at_t(curve, t) {
    476         var one_t = 1 - t;
    477         if (curve.length == 4) {
    478             return one_t * curve[1] + t * curve[3];
    479         }
    480         var one_t2 = one_t * one_t;
    481         var t2 = t * t;
    482         if (curve.length == 6) {
    483             return one_t2 * curve[1] + 2 * one_t * t * curve[3] + t2 * curve[5];
    484         }
    485         if (curve.length == 7) {
    486             var numer = one_t2 * curve[1] + 2 * one_t * t * curve[3] * curve[6]
    487                     + t2 * curve[5];
    488             var denom = one_t2            + 2 * one_t * t            * curve[6]
    489                     + t2;
    490             return numer / denom;
    491         }
    492         var a = one_t2 * one_t;
    493         var b = 3 * one_t2 * t;
    494         var c = 3 * one_t * t2;
    495         var d = t2 * t;
    496         return a * curve[1] + b * curve[3] + c * curve[5] + d * curve[7];
    497     }
    498 
    499     function drawPointAtT(curve) {
    500         var x = x_at_t(curve, curveT);
    501         var y = y_at_t(curve, curveT);
    502         drawPoint(x, y);
    503     }
    504 
    505     function drawLine(x1, y1, x2, y2) {
    506         ctx.beginPath();
    507         ctx.moveTo((x1 - srcLeft) * scale,
    508                 (y1 - srcTop) * scale);
    509         ctx.lineTo((x2 - srcLeft) * scale,
    510                 (y2 - srcTop) * scale);
    511         ctx.stroke();
    512     }
    513 
    514     function drawPoint(px, py) {
    515         for (var pts = 0; pts < drawnPts.length; pts += 2) {
    516             var x = drawnPts[pts];
    517             var y = drawnPts[pts + 1];
    518             if (px == x && py == y) {
    519                 return;
    520             }
    521         }
    522         drawnPts.push(px);
    523         drawnPts.push(py);
    524         var _px = (px - srcLeft) * scale;
    525         var _py = (py - srcTop) * scale;
    526         ctx.beginPath();
    527         ctx.arc(_px, _py, 3, 0, Math.PI * 2, true);
    528         ctx.closePath();
    529         ctx.stroke();
    530         if (draw_point_xy) {
    531             var label = px.toFixed(decimal_places) + ", " + py.toFixed(decimal_places);
    532             ctx.font = "normal 10px Arial";
    533             ctx.textAlign = "left";
    534             ctx.fillStyle = "black";
    535             ctx.fillText(label, _px + 5, _py);
    536         }
    537     }
    538 
    539     function drawPointSolid(px, py) {
    540         drawPoint(px, py);
    541         ctx.fillStyle = "rgba(0,0,0, 0.4)";
    542         ctx.fill();
    543     }
    544 
    545     function crossPt(origin, pt1, pt2) {
    546         return ((pt1[0] - origin[0]) * (pt2[1] - origin[1])
    547               - (pt1[1] - origin[1]) * (pt2[0] - origin[0])) > 0 ? 0 : 1;
    548     }
    549 
    550     // may not work well for cubics
    551     function curveClosestT(curve, x, y) {
    552         var closest = -1;
    553         var closestDist = Infinity;
    554         var l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
    555         for (var i = 0; i < 16; ++i) {
    556             var testX = x_at_t(curve, i / 16);
    557             l = Math.min(testX, l);
    558             r = Math.max(testX, r);
    559             var testY = y_at_t(curve, i / 16);
    560             t = Math.min(testY, t);
    561             b = Math.max(testY, b);
    562             var dx = testX - x;
    563             var dy = testY - y;
    564             var dist = dx * dx + dy * dy;
    565             if (closestDist > dist) {
    566                 closestDist = dist;
    567                 closest = i;
    568             }
    569         }
    570         var boundsX = r - l;
    571         var boundsY = b - t;
    572         var boundsDist = boundsX * boundsX + boundsY * boundsY;
    573         if (closestDist > boundsDist) {
    574             return -1;
    575         }
    576         console.log("closestDist = " + closestDist + " boundsDist = " + boundsDist
    577                 + " t = " + closest / 16);
    578         return closest / 16;
    579     }
    580 
    581     var kMaxConicToQuadPOW2 = 5;
    582 
    583     function computeQuadPOW2(curve, tol) {
    584         var a = curve[6] - 1;
    585         var k = a / (4 * (2 + a));
    586         var x = k * (curve[0] - 2 * curve[2] + curve[4]);
    587         var y = k * (curve[1] - 2 * curve[3] + curve[5]);
    588 
    589         var error = Math.sqrt(x * x + y * y);
    590         var pow2;
    591         for (pow2 = 0; pow2 < kMaxConicToQuadPOW2; ++pow2) {
    592             if (error <= tol) {
    593                 break;
    594             }
    595             error *= 0.25;
    596         }
    597         return pow2;
    598     }
    599 
    600     function subdivide_w_value(w) {
    601         return Math.sqrt(0.5 + w * 0.5);
    602     }
    603 
    604     function chop(curve, part1, part2) {
    605         var w = curve[6];
    606         var scale = 1 / (1 + w);
    607         part1[0] = curve[0];
    608         part1[1] = curve[1];
    609         part1[2] = (curve[0] + curve[2] * w) * scale;
    610         part1[3] = (curve[1] + curve[3] * w) * scale;
    611         part1[4] = part2[0] = (curve[0] + (curve[2] * w) * 2 + curve[4]) * scale * 0.5;
    612         part1[5] = part2[1] = (curve[1] + (curve[3] * w) * 2 + curve[5]) * scale * 0.5;
    613         part2[2] = (curve[2] * w + curve[4]) * scale;
    614         part2[3] = (curve[3] * w + curve[5]) * scale;
    615         part2[4] = curve[4];
    616         part2[5] = curve[5];
    617         part1[6] = part2[6] = subdivide_w_value(w);
    618     }
    619 
    620     function subdivide(curve, level, pts) {
    621         if (0 == level) {
    622             pts.push(curve[2]);
    623             pts.push(curve[3]);
    624             pts.push(curve[4]);
    625             pts.push(curve[5]);
    626         } else {
    627             var part1 = [], part2 = [];
    628             chop(curve, part1, part2);
    629             --level;
    630             subdivide(part1, level, pts);
    631             subdivide(part2, level, pts);
    632         }
    633     }
    634 
    635     function chopIntoQuadsPOW2(curve, pow2, pts) {
    636         subdivide(curve, pow2, pts);
    637         return 1 << pow2;
    638     }
    639 
    640     function drawConic(curve, srcLeft, srcTop, scale) {
    641         var tol = 1 / scale;
    642         var pow2 = computeQuadPOW2(curve, tol);
    643         var pts = [];
    644         chopIntoQuadsPOW2(curve, pow2, pts);
    645         for (var i = 0; i < pts.length; i += 4) {
    646             ctx.quadraticCurveTo(
    647                 (pts[i + 0] - srcLeft) * scale, (pts[i + 1] - srcTop) * scale,
    648                 (pts[i + 2] - srcLeft) * scale, (pts[i + 3] - srcTop) * scale);
    649         }
    650     }
    651 
    652     function draw(test, title) {
    653         ctx.font = "normal 50px Arial";
    654         ctx.textAlign = "left";
    655         ctx.fillStyle = "rgba(0,0,0, 0.1)";
    656         ctx.fillText(title, 50, 50);
    657         ctx.font = "normal 10px Arial";
    658         //  ctx.lineWidth = "1.001"; "0.999";
    659         var hullStarts = [];
    660         var hullEnds = [];
    661         var midSpokes = [];
    662         var midDist = [];
    663         var origin = [];
    664         var shortSpokes = [];
    665         var shortDist = [];
    666         var sweeps = [];
    667         drawnPts = [];
    668         for (var curves in test) {
    669             var curve = test[curves];
    670             origin.push(curve[0]);
    671             origin.push(curve[1]);
    672             var startPt = [];
    673             startPt.push(curve[2]);
    674             startPt.push(curve[3]);
    675             hullStarts.push(startPt);
    676             var endPt = [];
    677             if (curve.length == 4) {
    678                 endPt.push(curve[2]);
    679                 endPt.push(curve[3]);
    680             } else if (curve.length == 6 || curve.length == 7) {
    681                 endPt.push(curve[4]);
    682                 endPt.push(curve[5]);
    683             } else if (curve.length == 8) {
    684                 endPt.push(curve[6]);
    685                 endPt.push(curve[7]);
    686             }
    687             hullEnds.push(endPt);
    688             var sweep = crossPt(origin, startPt, endPt);
    689             sweeps.push(sweep);
    690             var midPt = [];
    691             midPt.push(x_at_t(curve, 0.5));
    692             midPt.push(y_at_t(curve, 0.5));
    693             midSpokes.push(midPt);
    694             var shortPt = [];
    695             shortPt.push(x_at_t(curve, 0.25));
    696             shortPt.push(y_at_t(curve, 0.25));
    697             shortSpokes.push(shortPt);
    698             var dx = midPt[0] - origin[0];
    699             var dy = midPt[1] - origin[1];
    700             var dist = Math.sqrt(dx * dx + dy * dy);
    701             midDist.push(dist);
    702             dx = shortPt[0] - origin[0];
    703             dy = shortPt[1] - origin[1];
    704             dist = Math.sqrt(dx * dx + dy * dy);
    705             shortDist.push(dist);
    706         }
    707         var intersect = [];
    708         var useIntersect = false;
    709         var maxWidth = Math.max(xmax - xmin, ymax - ymin);
    710         for (var curves in test) {
    711             var curve = test[curves];
    712             if (curve.length >= 6 && curve.length <= 8) {
    713                 var opp = curves == 0 || curves == 1 ? 0 : 1;
    714                 var sects = ray_curve_intersect(origin, hullEnds[opp], curve);
    715                 intersect.push(sects);
    716                 if (sects.length > 1) {
    717                     var intersection = sects[0];
    718                     if (intersection == 0) {
    719                         intersection = sects[1];
    720                     }
    721                     var ix = x_at_t(curve, intersection) - origin[0];
    722                     var iy = y_at_t(curve, intersection) - origin[1];
    723                     var ex = hullEnds[opp][0] - origin[0];
    724                     var ey = hullEnds[opp][1] - origin[1];
    725                     if (ix * ex >= 0 && iy * ey >= 0) {
    726                         var iDist = Math.sqrt(ix * ix + iy * iy);
    727                         var eDist = Math.sqrt(ex * ex + ey * ey);
    728                         var delta = Math.abs(iDist - eDist) / maxWidth;
    729                         if (delta > (curve.length != 8 ? 1e-5 : 1e-4)) {
    730                             useIntersect ^= true;
    731                         }
    732                     }
    733                 }
    734             }
    735         }
    736         var midLeft = curves != 0 ? crossPt(origin, midSpokes[0], midSpokes[1]) : 0;
    737         var firstInside;
    738         if (useIntersect) {
    739             var sect1 = intersect[0].length > 1;
    740             var sIndex = sect1 ? 0 : 1;
    741             var sects = intersect[sIndex];
    742             var intersection = sects[0];
    743             if (intersection == 0) {
    744                 intersection = sects[1];
    745             }
    746             var curve = test[sIndex];
    747             var ix = x_at_t(curve, intersection) - origin[0];
    748             var iy = y_at_t(curve, intersection) - origin[1];
    749             var opp = sect1 ? 1 : 0;
    750             var ex = hullEnds[opp][0] - origin[0];
    751             var ey = hullEnds[opp][1] - origin[1];
    752             var iDist = ix * ix + iy * iy;
    753             var eDist = ex * ex + ey * ey;
    754             firstInside = (iDist > eDist) ^ (sIndex == 0) ^ sweeps[0];
    755 //            console.log("iDist=" + iDist + " eDist=" + eDist + " sIndex=" + sIndex
    756  //                   + " sweeps[0]=" + sweeps[0]);
    757         } else {
    758  //           console.log("midLeft=" + midLeft);
    759             firstInside = midLeft != 0;
    760         }
    761         var shorter = midDist[1] < midDist[0];
    762         var shortLeft = shorter ? crossPt(origin, shortSpokes[0], midSpokes[1])
    763                 : crossPt(origin, midSpokes[0], shortSpokes[1]);
    764         var startCross = crossPt(origin, hullStarts[0], hullStarts[1]);
    765         var disallowShort = midLeft == startCross && midLeft == sweeps[0]
    766                     && midLeft == sweeps[1];
    767 
    768   //      console.log("midLeft=" + midLeft + " startCross=" + startCross);
    769         var intersectIndex = 0;
    770         for (var curves in test) {
    771             var curve = test[draw_id != 2 ? curves : test.length - curves - 1];
    772             if (curve.length != 4 && curve.length != 6 && curve.length != 7 && curve.length != 8) {
    773                 continue;
    774             }
    775             ctx.lineWidth = 1;
    776             if (draw_tangents != 0) {
    777                 if (draw_cubic_red ? curve.length == 8 : firstInside == curves) {
    778                     ctx.strokeStyle = "rgba(255,0,0, 0.3)";
    779                 } else {
    780                     ctx.strokeStyle = "rgba(0,0,255, 0.3)";
    781                 }
    782                 drawLine(curve[0], curve[1], curve[2], curve[3]);
    783                 if (draw_tangents != 2) {
    784                     if (curve.length > 4) drawLine(curve[2], curve[3], curve[4], curve[5]);
    785                     if (curve.length == 8) drawLine(curve[4], curve[5], curve[6], curve[7]);
    786                 }
    787                 if (draw_tangents != 1) {
    788                     if (curve.length == 6 || curve.length == 7) {
    789                         drawLine(curve[0], curve[1], curve[4], curve[5]);
    790                     }
    791                     if (curve.length == 8) drawLine(curve[0], curve[1], curve[6], curve[7]);
    792                 }
    793             }
    794             ctx.beginPath();
    795             ctx.moveTo((curve[0] - srcLeft) * scale, (curve[1] - srcTop) * scale);
    796             if (curve.length == 4) {
    797                 ctx.lineTo((curve[2] - srcLeft) * scale, (curve[3] - srcTop) * scale);
    798             } else if (curve.length == 6) {
    799                 ctx.quadraticCurveTo(
    800                     (curve[2] - srcLeft) * scale, (curve[3] - srcTop) * scale,
    801                     (curve[4] - srcLeft) * scale, (curve[5] - srcTop) * scale);
    802             } else if (curve.length == 7) {
    803                 drawConic(curve, srcLeft, srcTop, scale);
    804             } else {
    805                 ctx.bezierCurveTo(
    806                     (curve[2] - srcLeft) * scale, (curve[3] - srcTop) * scale,
    807                     (curve[4] - srcLeft) * scale, (curve[5] - srcTop) * scale,
    808                     (curve[6] - srcLeft) * scale, (curve[7] - srcTop) * scale);
    809             }
    810             if (draw_cubic_red ? curve.length == 8 : firstInside == curves) {
    811                 ctx.strokeStyle = "rgba(255,0,0, 1)";
    812             } else {
    813                 ctx.strokeStyle = "rgba(0,0,255, 1)";
    814             }
    815             ctx.stroke();
    816             if (draw_endpoints > 0) {
    817                 drawPoint(curve[0], curve[1]);
    818                 if (draw_endpoints > 1 || curve.length == 4) {
    819                     drawPoint(curve[2], curve[3]);
    820                 }
    821                 if (curve.length == 6 || curve.length == 7 ||
    822                         (draw_endpoints > 1 && curve.length == 8)) {
    823                     drawPoint(curve[4], curve[5]);
    824                 }
    825                 if (curve.length == 8) drawPoint(curve[6], curve[7]);
    826             }
    827             if (draw_midpoint != 0) {
    828                 if ((curves == 0) == (midLeft == 0)) {
    829                     ctx.strokeStyle = "rgba(0,180,127, 0.6)";
    830                 } else {
    831                     ctx.strokeStyle = "rgba(127,0,127, 0.6)";
    832                 }
    833                 var midX = x_at_t(curve, 0.5);
    834                 var midY = y_at_t(curve, 0.5);
    835                 drawPointSolid(midX, midY);
    836                 if (draw_midpoint > 1) {
    837                     drawLine(curve[0], curve[1], midX, midY);
    838                 }
    839             }
    840             if (draw_quarterpoint != 0) {
    841                 if ((curves == 0) == (shortLeft == 0)) {
    842                     ctx.strokeStyle = "rgba(0,191,63, 0.6)";
    843                 } else {
    844                     ctx.strokeStyle = "rgba(63,0,191, 0.6)";
    845                 }
    846                 var midT = (curves == 0) == shorter ? 0.25 : 0.5;
    847                 var midX = x_at_t(curve, midT);
    848                 var midY = y_at_t(curve, midT);
    849                 drawPointSolid(midX, midY);
    850                 if (draw_quarterpoint > 1) {
    851                     drawLine(curve[0], curve[1], midX, midY);
    852                 }
    853             }
    854             if (draw_sortpoint != 0) {
    855                 if ((curves == 0) == ((disallowShort == -1 ? midLeft : shortLeft) == 0)) {
    856                     ctx.strokeStyle = "rgba(0,155,37, 0.6)";
    857                 } else {
    858                     ctx.strokeStyle = "rgba(37,0,155, 0.6)";
    859                 }
    860                 var midT = (curves == 0) == shorter && disallowShort != curves ? 0.25 : 0.5;
    861                 console.log("curves=" + curves + " disallowShort=" + disallowShort
    862                         + " midLeft=" + midLeft + " shortLeft=" + shortLeft
    863                         + " shorter=" + shorter + " midT=" + midT);
    864                 var midX = x_at_t(curve, midT);
    865                 var midY = y_at_t(curve, midT);
    866                 drawPointSolid(midX, midY);
    867                 if (draw_sortpoint > 1) {
    868                     drawLine(curve[0], curve[1], midX, midY);
    869                 }
    870             }
    871             if (draw_ray_intersect != 0) {
    872                 ctx.strokeStyle = "rgba(75,45,199, 0.6)";
    873                 if (curve.length >= 6 && curve.length <= 8) {
    874                     var intersections = intersect[intersectIndex];
    875                     for (var i in intersections) {
    876                         var intersection = intersections[i];
    877                         var x = x_at_t(curve, intersection);
    878                         var y = y_at_t(curve, intersection);
    879                         drawPointSolid(x, y);
    880                         if (draw_ray_intersect > 1) {
    881                             drawLine(curve[0], curve[1], x, y);
    882                         }
    883                     }
    884                 }
    885                 ++intersectIndex;
    886             }
    887             if (draw_order) {
    888                 var px = x_at_t(curve, 0.75);
    889                 var py = y_at_t(curve, 0.75);
    890                 var _px = (px - srcLeft) * scale;
    891                 var _py = (py - srcTop) * scale;
    892                 ctx.beginPath();
    893                 ctx.arc(_px, _py, 15, 0, Math.PI * 2, true);
    894                 ctx.closePath();
    895                 ctx.fillStyle = "white";
    896                 ctx.fill();
    897                 if (draw_cubic_red ? curve.length == 8 : firstInside == curves) {
    898                     ctx.strokeStyle = "rgba(255,0,0, 1)";
    899                     ctx.fillStyle = "rgba(255,0,0, 1)";
    900                 } else {
    901                     ctx.strokeStyle = "rgba(0,0,255, 1)";
    902                     ctx.fillStyle = "rgba(0,0,255, 1)";
    903                 }
    904                 ctx.stroke();
    905                 ctx.font = "normal 16px Arial";
    906                 ctx.textAlign = "center";
    907                 ctx.fillText(parseInt(curves) + 1, _px, _py + 5);
    908             }
    909             if (draw_closest_t) {
    910                 var t = curveClosestT(curve, mouseX, mouseY);
    911                 if (t >= 0) {
    912                     var x = x_at_t(curve, t);
    913                     var y = y_at_t(curve, t);
    914                     drawPointSolid(x, y);
    915                 }
    916             }
    917             if (!approximately_zero(scale - initScale)) {
    918                 ctx.font = "normal 20px Arial";
    919                 ctx.fillStyle = "rgba(0,0,0, 0.3)";
    920                 ctx.textAlign = "right";
    921                 ctx.fillText(scale.toFixed(decimal_places) + 'x',
    922                         screenWidth - 10, screenHeight - 5);
    923             }
    924             if (draw_t) {
    925                 drawPointAtT(curve);
    926             }
    927             if (draw_id != 0) {
    928                 var id = -1;
    929                 for (var i = 0; i < ids.length; i += 2) {
    930                     if (ids[i + 1] == curve) {
    931                         id = ids[i];
    932                         break;
    933                     }
    934                 }
    935                 if (id >= 0) {
    936                     var px = x_at_t(curve, 0.5);
    937                     var py = y_at_t(curve, 0.5);
    938                     var _px = (px - srcLeft) * scale;
    939                     var _py = (py - srcTop) * scale;
    940                     ctx.beginPath();
    941                     ctx.arc(_px, _py, 15, 0, Math.PI * 2, true);
    942                     ctx.closePath();
    943                     ctx.fillStyle = "white";
    944                     ctx.fill();
    945                     ctx.strokeStyle = "rgba(255,0,0, 1)";
    946                     ctx.fillStyle = "rgba(255,0,0, 1)";
    947                     ctx.stroke();
    948                     ctx.font = "normal 16px Arial";
    949                     ctx.textAlign = "center";
    950                     ctx.fillText(id, _px, _py + 5);
    951                 }
    952             }
    953         }
    954         if (draw_t) {
    955             drawCurveTControl();
    956         }
    957         if (draw_w) {
    958             drawCurveWControl();
    959         }
    960     }
    961 
    962     function drawCurveTControl() {
    963         ctx.lineWidth = 2;
    964         ctx.strokeStyle = "rgba(0,0,0, 0.3)";
    965         ctx.beginPath();
    966         ctx.rect(screenWidth - 80, 40, 28, screenHeight - 80);
    967         ctx.stroke();
    968         var ty = 40 + curveT * (screenHeight - 80);
    969         ctx.beginPath();
    970         ctx.moveTo(screenWidth - 80, ty);
    971         ctx.lineTo(screenWidth - 85, ty - 5);
    972         ctx.lineTo(screenWidth - 85, ty + 5);
    973         ctx.lineTo(screenWidth - 80, ty);
    974         ctx.fillStyle = "rgba(0,0,0, 0.6)";
    975         ctx.fill();
    976         var num = curveT.toFixed(decimal_places);
    977         ctx.font = "normal 10px Arial";
    978         ctx.textAlign = "left";
    979         ctx.fillText(num, screenWidth - 78, ty);
    980     }
    981 
    982     function drawCurveWControl() {
    983         var w = -1;
    984         var choice = 0;
    985         for (var curves in tests[testIndex]) {
    986             var curve = tests[testIndex][curves];
    987             if (curve.length != 7) {
    988                 continue;
    989             }
    990             if (choice == curveW) {
    991                 w = curve[6];
    992                 break;
    993             }
    994             ++choice;
    995         }
    996         if (w < 0) {
    997             return;
    998         }
    999         ctx.lineWidth = 2;
   1000         ctx.strokeStyle = "rgba(0,0,0, 0.3)";
   1001         ctx.beginPath();
   1002         ctx.rect(screenWidth - 40, 40, 28, screenHeight - 80);
   1003         ctx.stroke();
   1004         var ty = 40 + w * (screenHeight - 80);
   1005         ctx.beginPath();
   1006         ctx.moveTo(screenWidth - 40, ty);
   1007         ctx.lineTo(screenWidth - 45, ty - 5);
   1008         ctx.lineTo(screenWidth - 45, ty + 5);
   1009         ctx.lineTo(screenWidth - 40, ty);
   1010         ctx.fillStyle = "rgba(0,0,0, 0.6)";
   1011         ctx.fill();
   1012         var num = w.toFixed(decimal_places);
   1013         ctx.font = "normal 10px Arial";
   1014         ctx.textAlign = "left";
   1015         ctx.fillText(num, screenWidth - 38, ty);
   1016     }
   1017 
   1018     function ptInTControl() {
   1019         var e = window.event;
   1020         var tgt = e.target || e.srcElement;
   1021         var left = tgt.offsetLeft;
   1022         var top = tgt.offsetTop;
   1023         var x = (e.clientX - left);
   1024         var y = (e.clientY - top);
   1025         if (x < screenWidth - 80 || x > screenWidth - 50) {
   1026             return false;
   1027         }
   1028         if (y < 40 || y > screenHeight - 80) {
   1029             return false;
   1030         }
   1031         curveT = (y - 40) / (screenHeight - 120);
   1032         if (curveT < 0 || curveT > 1) {
   1033             throw "stop execution";
   1034         }
   1035         return true;
   1036     }
   1037 
   1038     function ptInWControl() {
   1039         var e = window.event;
   1040         var tgt = e.target || e.srcElement;
   1041         var left = tgt.offsetLeft;
   1042         var top = tgt.offsetTop;
   1043         var x = (e.clientX - left);
   1044         var y = (e.clientY - top);
   1045         if (x < screenWidth - 40 || x > screenWidth - 10) {
   1046             return false;
   1047         }
   1048         if (y < 40 || y > screenHeight - 80) {
   1049             return false;
   1050         }
   1051         var w = (y - 40) / (screenHeight - 120);
   1052         if (w < 0 || w > 1) {
   1053             throw "stop execution";
   1054         }
   1055         var choice = 0;
   1056         for (var curves in tests[testIndex]) {
   1057             var curve = tests[testIndex][curves];
   1058             if (curve.length != 7) {
   1059                 continue;
   1060             }
   1061             if (choice == curveW) {
   1062                 curve[6] = w;
   1063                 break;
   1064             }
   1065             ++choice;
   1066         }
   1067         return true;
   1068     }
   1069 
   1070     function drawTop() {
   1071         init(tests[testIndex]);
   1072         redraw();
   1073     }
   1074 
   1075     function redraw() {
   1076         if (focus_on_selection > 0) {
   1077             var focusXmin = focusYmin = Infinity;
   1078             var focusXmax = focusYmax = -Infinity;
   1079             var choice = 0;
   1080             for (var curves in tests[testIndex]) {
   1081                 if (++choice != focus_on_selection) {
   1082                     continue;
   1083                 }
   1084                 var curve = tests[testIndex][curves];
   1085                 var last = curve.length - (curve.length % 2 == 1 ? 1 : 0);
   1086                 for (var idx = 0; idx < last; idx += 2) {
   1087                     focusXmin = Math.min(focusXmin, curve[idx]);
   1088                     focusXmax = Math.max(focusXmax, curve[idx]);
   1089                     focusYmin = Math.min(focusYmin, curve[idx + 1]);
   1090                     focusYmax = Math.max(focusYmax, curve[idx + 1]);
   1091                 }
   1092             }
   1093             focusXmin -= Math.min(1, Math.max(focusXmax - focusXmin, focusYmax - focusYmin));
   1094             if (focusXmin < focusXmax && focusYmin < focusYmax) {
   1095                 setScale(focusXmin, focusXmax, focusYmin, focusYmax);
   1096             }
   1097         }
   1098         ctx.beginPath();
   1099         ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
   1100         ctx.fillStyle = "white";
   1101         ctx.fill();
   1102         draw(tests[testIndex], testTitles[testIndex]);
   1103     }
   1104 
   1105     function doKeyPress(evt) {
   1106         var char = String.fromCharCode(evt.charCode);
   1107         var focusWasOn = false;
   1108         switch (char) {
   1109             case '0':
   1110             case '1':
   1111             case '2':
   1112             case '3':
   1113             case '4':
   1114             case '5':
   1115             case '6':
   1116             case '7':
   1117             case '8':
   1118             case '9':
   1119                 decimal_places = char - '0';
   1120                 redraw();
   1121                 break;
   1122             case '-':
   1123                 focusWasOn = focus_on_selection;
   1124                 if (focusWasOn) {
   1125                     focus_on_selection = false;
   1126                     scale /= 1.2;
   1127                 } else {
   1128                     scale /= 2;
   1129                 }
   1130                 calcLeftTop();
   1131                 redraw();
   1132                 focus_on_selection = focusWasOn;
   1133                 break;
   1134             case '=':
   1135             case '+':
   1136                 focusWasOn = focus_on_selection;
   1137                 if (focusWasOn) {
   1138                     focus_on_selection = false;
   1139                     scale *= 1.2;
   1140                 } else {
   1141                     scale *= 2;
   1142                 }
   1143                 calcLeftTop();
   1144                 redraw();
   1145                 focus_on_selection = focusWasOn;
   1146                 break;
   1147             case 'b':
   1148                 draw_cubic_red ^= true;
   1149                 redraw();
   1150                 break;
   1151             case 'c':
   1152                 drawTop();
   1153                 break;
   1154             case 'd':
   1155                 var test = tests[testIndex];
   1156                 var testClone = [];
   1157                 for (var curves in test) {
   1158                     var c = test[curves];
   1159                     var cClone = [];
   1160                     for (var index = 0; index < c.length; ++index) {
   1161                         cClone.push(c[index]);
   1162                     }
   1163                     testClone.push(cClone);
   1164                 }
   1165                 tests.push(testClone);
   1166                 testTitles.push(testTitles[testIndex] + " copy");
   1167                 testIndex = tests.length - 1;
   1168                 redraw();
   1169                 break;
   1170             case 'e':
   1171                 draw_endpoints = (draw_endpoints + 1) % 3;
   1172                 redraw();
   1173                 break;
   1174             case 'f':
   1175                 draw_derivative ^= true;
   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) / scale + srcLeft;
   1346         mouseY = (e.clientY - top) / scale + srcTop;
   1347     }
   1348 
   1349     function calcLeftTop() {
   1350         srcLeft = mouseX - screenWidth / 2 / scale;
   1351         srcTop = mouseY - screenHeight / 2 / scale;
   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>