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
60var testDivs = [
61sect0,
62sect1,
63sect2,
64sect3,
65sect4,
66sect5,
67sect6,
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
216function 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>