summaryrefslogtreecommitdiff
path: root/graphics/figput/javascript/tikz.js
blob: b5317702b0a693597c0ae7bfd07082400fb366f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
"use strict";
/*
Code necessary to output tikz for figures. It spoofs the normal canvas
drawing code so that the output comes here, and is then converted to
tizk output.

One problem with JS is that there is no path interator. You can't obtain
the segments that make up an entire path. I don't see any way around this
other than writing my own wrapper around Path2D. It might be possible to
somehow hack the internals of Path2D, but that would be brittle, even if
it works.

BUG: The long-term solution is to eliminate use of the JS Path2D class,
but it's not possible to entirely eliminate it since it's the only way to
draw to the browser window. What I *could* do is sub-class
CanvasRenderingContext2D so that this sub-class take my own path class
objects and converts them to Path2D for drawing. Another hurdle is
isPointInPath(), which is used in a few places. I could provide a separate
implementation of that, but it's fiddly. isPointInStroke() is a bit harder.

BUG: It's tempting to come up with a framework under which a "path"
is closer to our intuition of something that can be drawn as a continuous
thing, without lifting your pencil. Then, have a second-order thing that
may hold several of these continuous paths. Intuitively one wants a "path"
to have a clear start-point and end-point, but you also need to be able
to handle things like winding number for multiple paths when filling.

BUG: There are many cases where you might want the online version to be
different from what is printed. I just gave the example of filling a path,
and color is similar. There's lots of things that might make sense on a
computer screen, but wouldn't work well on printed paper.

*/
// Turns out that Point used to be part of js, but was deprecated, and
// seems not to exist any longer.
class Point2D {
    constructor(x, y) {
        this._x = x;
        this._y = y;
    }
    toString() {
        // May be handy for debugging.
        return "( " + this._x.toFixed(2) + "," + this._y.toFixed(2) + ")";
    }
    get x() {
        return this._x;
    }
    get y() {
        return this._y;
    }
    copy() {
        return new Point2D(this._x, this._y);
    }
    negate() {
        // return -this.
        return new Point2D(-this._x, -this.y);
    }
    negateSelf() {
        this._x = -this.x;
        this._y = -this.y;
    }
    minus(p) {
        // return this - p. Redunant since it's just a form of translation.
        return new Point2D(this._x - p.x, this._y - p.y);
    }
    minusSelf(p) {
        this._x -= p._x;
        this._y -= p._y;
    }
    translate2(u, v) {
        return new Point2D(u + this._x, v + this._y);
    }
    translate(p) {
        return new Point2D(p.x + this._x, p.y + this._y);
    }
    translateSelf2(u, v) {
        this._x += u;
        this._y += v;
    }
    translateSelf(p) {
        this._x += p.x;
        this._y += p.y;
    }
    scale(s) {
        return new Point2D(s * this._x, s * this._y);
    }
    scaleSelf(s) {
        // As above, but it's done in-place rather than returning a copy.
        this._x *= s;
        this._y *= s;
    }
    rotate(theta) {
        // Apply rotation matrix in the usual (RH) way. theta in radians.
        let c = Math.cos(theta);
        let s = Math.sin(theta);
        return new Point2D(c * this._x - s * this._y, s * this._x + c * this._y);
    }
    rotateSelf(theta) {
        let c = Math.cos(theta);
        let s = Math.sin(theta);
        let u = c * this._x - s * this._y;
        let v = s * this._x + c * this._y;
        this._x = u;
        this._y = v;
    }
    rotateAbout(c, theta) {
        // Rotate this about c by angle theta, returning the result.
        let answer = new Point2D(this.x - c.x, this.y - c.y);
        answer = answer.rotate(theta);
        answer._x += c.x;
        answer._y += c.y;
        return answer;
    }
    dot(a) {
        // Return this dot a.
        return a.x * this._x + a.y * this._y;
    }
    length() {
        return Math.sqrt(this._x ** 2 + this._y ** 2);
    }
    static dot(a, b) {
        // This looks like Java-style overloading, but it's not. One dot()
        // is static and the other is not.
        return a.dot(b);
    }
    angleBetween(a) {
        // Return angle between this and a, based on
        // this dot a = |this| |a| cos angle
        // This is the angle between the two, without any orientation.
        let cos = this.dot(a) / (this.length() * a.length());
        return Math.acos(cos);
    }
    cliffordBetween(a) {
        // The "clifford angle," which is like angleBetween(), but it takes
        // orientation into account. The angle is given relative to ("from") this.
        return Math.atan2(this.x * a.y - a.x * this.y, this.x * a.x + this.y * a.y);
    }
}
// Almost everything here is static because this is essentially a factory for the
// various segment types.
class PathSegment {
    constructor(kind, d) {
        // One of the values above.
        this.type = PathSegment.UNKNOWN;
        this.type = kind;
        this.s = d;
    }
    static getClose() {
        let d = {};
        return new PathSegment(PathSegment.CLOSE, d);
    }
    static getMoveTo(x, y) {
        let d = { x: x, y: y };
        return new PathSegment(PathSegment.MOVE_TO, d);
    }
    static getLineTo(x, y) {
        let d = { x: x, y: y };
        return new PathSegment(PathSegment.LINE_TO, d);
    }
    static getBezier(cx1, cy1, cx2, cy2, x, y) {
        let d = { cx1: cx1, cy1: cy1, cx2: cx2, cy2: cy2, x: x, y: y };
        return new PathSegment(PathSegment.BEZIER, d);
    }
    static getQuadratic(cx, cy, x, y) {
        let d = { cx: cx, cy: cy, x: x, y: y };
        return new PathSegment(PathSegment.QUADRATIC, d);
    }
    static getArc(x, y, r, a0, a1, ccw) {
        let d = { x: x, y: y, r: r, a0: a0, a1: a1, ccw: ccw };
        return new PathSegment(PathSegment.ARC, d);
    }
    static getArcTo(x1, y1, x2, y2, r) {
        let d = { x1: x1, y1: y1, x2: x2, y2: y2, r: r };
        return new PathSegment(PathSegment.ARC_TO, d);
    }
    static getEllipse(x, y, rx, ry, rot, a0, a1, ccw) {
        // BUG: If I convert everything to bezier, then many of these static
        // methods can be eliminated.
        // YES. THIS IS A BAD IDEA IN EVERY WAY. ONLY BEZIER CURVES SHOULD
        // BE ALLOWED INTERNALLY.
        // OTOH, there are certain shapes, like an ellipse or rectangle, that
        // should be treated as a single unitary thing.
        // What I should probably do is sub-class FPath for these. Interally, they
        // can be represented as a messy bezier thing, but that would be hidden from the user.
        // At the same time, one might want to add an ellipse or rect to an existing path to
        // obtain various fill effects. So, the ellipse sub-class will need something like
        // a toFPath() method so that it can be added to a normal FPath.
        let d = { x: x, y: y, rx: rx, ry: ry, rot: rot, a0: a0,
            a1: a1, ccw: ccw };
        return new PathSegment(PathSegment.ELLIPSE, d);
    }
    static getRect(x, y, w, h) {
        let d = { x: x, y: y, w: w, h: h };
        return new PathSegment(PathSegment.RECT, d);
    }
}
// The various kinds of segment.
// I'm being sloppy about typing here since the use is uncomplicated and
// private. Some kind of enumerated type would be better in the abstract.
// BUG: I should get rid of anything after the QUADRATIC type. The
// mathematically clean way to do this is to convert *everything* to beziers,
// including ellipses. In fact (?) is a quadratic just a cubic where the
// control points coincide? If so, I could get rid of QUADRATIC too.
PathSegment.MOVE_TO = 1;
PathSegment.LINE_TO = 2;
PathSegment.BEZIER = 3;
PathSegment.QUADRATIC = 4;
PathSegment.ARC = 5;
PathSegment.ARC_TO = 6;
PathSegment.ELLIPSE = 7;
PathSegment.RECT = 8;
PathSegment.CLOSE = 9;
PathSegment.UNKNOWN = -1;
// BUG: Add some flags so that things could be drawn or not drawn based
// on whether the output is going to tikz or to the screen. 
class FPath extends Path2D {
    constructor() {
        super();
        // An array of PathSegments.
        this.segs = [];
    }
    addPath(p) {
        // Append the elements of p to this. 
        for (let i = 0; i < p.segs.length; i++)
            this.segs.push(p.segs[i]);
    }
    closePath() {
        super.closePath();
        this.segs.push(PathSegment.getClose());
    }
    moveTo(x, y) {
        super.moveTo(x, y);
        this.segs.push(PathSegment.getMoveTo(x, y));
    }
    frontLineTo(x, y) {
        // To tack a line segment to the *begining* of an existing path.
        // This assumes that segs[0] is a moveTo() -- as I think (?) it must be
        // in any reasonable case. 
        // So, you start with a path that looks like
        // moveTo(a,b) ...whatever
        // and it becomes
        // moveTo(x,y) lineTo(a,b) ...whatever.
        // You're basically drawing as usual, but "from the wrong end."
        let s = this.segs[0];
        if (s.type != PathSegment.MOVE_TO)
            console.log("ERROR: frontLineTo() doesn't start with moveTo(): " + s.type);
        let newfirst = PathSegment.getMoveTo(x, y);
        // Convert the initial moveto to a lineto.
        // In fact, this is sort of pointless, and is only done this way to respect
        // the type-checker. It's (x,y) whether it's a lineto or a moveto.
        let m = s.s;
        let newsecond = PathSegment.getLineTo(m.x, m.y);
        this.segs[0] = newsecond;
        this.segs.unshift(newfirst);
    }
    lineTo(x, y) {
        super.lineTo(x, y);
        this.segs.push(PathSegment.getLineTo(x, y));
    }
    bezierCurveTo(cx1, cy1, cx2, cy2, x, y) {
        super.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
        this.segs.push(PathSegment.getBezier(cx1, cy1, cx2, cy2, x, y));
    }
    quadraticCurveTo(cx, cy, x, y) {
        super.quadraticCurveTo(cx, cy, x, y);
        this.segs.push(PathSegment.getQuadratic(cx, cy, x, y));
    }
    translate(p) {
        // Translate this entire path by the given point.
        // BUG: Not implemented for every possible type of segment.
        let answer = new FPath();
        for (let i = 0; i < this.segs.length; i++) {
            let s = this.segs[i];
            if (s.type == PathSegment.MOVE_TO) {
                let m = s.s;
                answer.moveTo(m.x + p.x, m.y + p.y);
            }
            else if (s.type == PathSegment.LINE_TO) {
                let m = s.s;
                answer.lineTo(m.x + p.x, m.y + p.y);
            }
            else if (s.type == PathSegment.BEZIER) {
                let m = s.s;
                answer.bezierCurveTo(m.cx1 + p.x, m.cy1 + p.y, m.cx2 + p.x, m.cy2 + p.y, m.x + p.x, m.y + p.y);
            }
            else if (s.type == PathSegment.ELLIPSE) {
                let m = s.s;
                answer.ellipse(m.x + p.x, m.y + p.y, m.rx, m.ry, m.rot, m.a0, m.a1, m.ccw);
            }
            else {
                console.log("whatever translattion you want, it's not done.");
            }
        }
        return answer;
    }
    rotate(a) {
        // Rotate this entire path about the origin and return the result.
        // BUG: I have only implemented this for bezier curves and lines.
        // Expanding this probably doesn't make sense until I settle on a
        // framework to more fully replace Path2D.
        let answer = new FPath();
        for (let i = 0; i < this.segs.length; i++) {
            let s = this.segs[i];
            if (s.type == PathSegment.MOVE_TO) {
                let m = s.s;
                let p = new Point2D(m.x, m.y).rotate(a);
                answer.moveTo(p.x, p.y);
            }
            else if (s.type == PathSegment.LINE_TO) {
                let m = s.s;
                let p = new Point2D(m.x, m.y).rotate(a);
                answer.lineTo(p.x, p.y);
            }
            else if (s.type == PathSegment.BEZIER) {
                let m = s.s;
                let c1 = new Point2D(m.cx1, m.cy1).rotate(a);
                let c2 = new Point2D(m.cx2, m.cy2).rotate(a);
                let e = new Point2D(m.x, m.y).rotate(a);
                answer.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, e.x, e.y);
            }
            else {
                console.log("whatever rotation you want, it's not done.");
            }
        }
        return answer;
    }
    scale(r) {
        // Scale this entire path about the origin and return the result.
        // BUG: I have only implemented this for bezier curves and lines.
        let answer = new FPath();
        for (let i = 0; i < this.segs.length; i++) {
            let s = this.segs[i];
            if (s.type == PathSegment.MOVE_TO) {
                let m = s.s;
                let p = new Point2D(r * m.x, r * m.y);
                answer.moveTo(p.x, p.y);
            }
            else if (s.type == PathSegment.LINE_TO) {
                let m = s.s;
                // BUG:
                console.log("scale not done for lines");
            }
            else if (s.type == PathSegment.BEZIER) {
                let m = s.s;
                let c1 = new Point2D(r * m.cx1, r * m.cy1);
                let c2 = new Point2D(r * m.cx2, r * m.cy2);
                let p = new Point2D(r * m.x, r * m.y);
                answer.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, p.x, p.y);
            }
            else {
                console.log("whatever scale you want, it's not done.");
            }
        }
        return answer;
    }
    reflectX() {
        // Reflect this entire path about the x-axis and return the result.
        // BUG: not implemented for every case.
        let answer = new FPath();
        for (let i = 0; i < this.segs.length; i++) {
            let s = this.segs[i];
            if (s.type == PathSegment.MOVE_TO) {
                let m = s.s;
                let p = new Point2D(m.x, -m.y);
                answer.moveTo(p.x, p.y);
            }
            else if (s.type == PathSegment.LINE_TO) {
                let m = s.s;
                let p = new Point2D(m.x, -m.y);
                answer.lineTo(p.x, p.y);
            }
            else if (s.type == PathSegment.BEZIER) {
                let m = s.s;
                let c1 = new Point2D(m.cx1, -m.cy1);
                let c2 = new Point2D(m.cx2, -m.cy2);
                let p = new Point2D(m.x, -m.y);
                answer.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, p.x, p.y);
            }
            else if (s.type == PathSegment.ELLIPSE) {
                let m = s.s;
                answer.ellipse(m.x, -m.y, m.rx, m.ry, m.rot, m.a0, m.a1, m.ccw);
            }
            else {
                console.log("whatever reflect you want, it's not done.");
            }
        }
        return answer;
    }
    reflectXY() {
        // Reflect this entire path about the x-axis AND y-axis.
        // BUG: not implemented for every case.
        // BUG: Also, what about reflectY()?
        let answer = new FPath();
        for (let i = 0; i < this.segs.length; i++) {
            let s = this.segs[i];
            if (s.type == PathSegment.MOVE_TO) {
                let m = s.s;
                let p = new Point2D(-m.x, -m.y);
                answer.moveTo(p.x, p.y);
            }
            else if (s.type == PathSegment.LINE_TO) {
                let m = s.s;
                // BUG:
                console.log("reflect not done for lines");
            }
            else if (s.type == PathSegment.BEZIER) {
                let m = s.s;
                let c1 = new Point2D(-m.cx1, -m.cy1);
                let c2 = new Point2D(-m.cx2, -m.cy2);
                let p = new Point2D(-m.x, -m.y);
                answer.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, p.x, p.y);
            }
            else {
                console.log("whatever reflect you want, it's not done.");
            }
        }
        return answer;
    }
    rotateAbout(a, p) {
        // Rotate this entire path about p and return the result.
        let t1 = this.translate(new Point2D(-p.x, -p.y));
        let t2 = t1.rotate(a);
        return t2.translate(p);
    }
    static arcToBezierNEW(r, a0, a1) {
        // Generate a series of bezier curves to represent an arc. The result
        // represents an arc of a circle of radius r, centered 
        // at (0,0), going from angle a0 to a1, in radians. 
        // Each step should subtend no more than pi/4 radians. Most of the
        // time pi/2 would be accurate enough, but pi/4 is better, and not that 
        // much extra work.
        let totalAngle = a1 - a0;
        if (totalAngle < 0)
            totalAngle += 2 * Math.PI;
        let numCurves = Math.ceil(4 * totalAngle / Math.PI);
        let subtend = totalAngle / numCurves;
        // See the manual for where this comes from. It's the crucial constant
        // for approximating arcs of circles by cubics.
        let k = (4 / 3) * Math.tan(subtend / 4);
        // Everything is built out of a single arc for a circle of radius r,
        // going cw, starting at (1,0) and angle subtend.
        let s = Math.sin(subtend);
        let c = Math.cos(subtend);
        let p1 = new Point2D(r, 0);
        let p2 = new Point2D(r, r * k);
        let p3 = new Point2D(r * (c + k * s), r * (s - k * c));
        let p4 = new Point2D(r * c, r * s);
        // The arc determined by the p_i above must be rotated to create
        // a series of sub-arcs to get the total arc we want.
        let answer = new FPath();
        answer.moveTo(p1.x, p1.y);
        for (let i = 0; i < numCurves; i++) {
            answer.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, p4.x, p4.y);
            p2.rotateSelf(subtend);
            p3.rotateSelf(subtend);
            p4.rotateSelf(subtend);
        }
        // Rotate the entire thing so that it starts at a0.
        answer = answer.rotate(a0);
        return answer;
    }
    arc(x, y, r, a0, a1, ccw) {
        // BUG: I am pretty sure this isn't right. There are things about 
        // being cw/cww and things like that. It needs to be tested.
        // A circular arc, centered at (x,y) and radius r, from angle
        // a0 to angle a1 (in radians), going cw or ccw. Like ellipse, this 
        // is basically independent of the surrounding segments. Actually,
        // the documentation I've found is a little vague on this point, 
        // but it looks like that is how it works.
        //
        // Another issue is the fact that these angles, a0 and a1, may
        // have "extra" multiples of 2pi in them and whether a1>a0.
        // The first thing this does is reduce the angles to be in [0,2pi).
        //
        // The whole cw versus ccw issue amounts to whether you're getting
        // the "large" arc or the "small" arc. If a1 > a0, then the ccw arc
        // is the small arc and the cw arc is the large arc. If a1 < a0,
        // then the ccw arc is the large arc and the cw arc is the small arc.
        // To untangle this, assume that the arc will be treated ccw, and
        // swap a1 and a0, if necessary, to make that the case.
        //
        // Finally, there's the issue of what cw and ccw mean when in
        // left or right handed coordinate systems. Ugh.
        //
        // See 
        // https://pomax.github.io/bezierinfo/#circles_cubic
        // for an explanation of the math, which is just HS algrbra.
        // 
        // 
        // BUG: Seems like this is a special case of an arc of an ellipse.
        if (ccw === undefined)
            ccw = false;
        // Reduce angles to be in [0,2pi).
        while (a0 < 0)
            a0 += 2 * Math.PI;
        while (a1 < 0)
            a1 += 2 * Math.PI;
        while (a0 >= 2 * Math.PI)
            a0 -= 2 * Math.PI;
        while (a1 >= 2 * Math.PI)
            a1 -= 2 * Math.PI;
        // If the user asked for cw, then swap the angles so that we only
        // need to consider the ccw case below.
        if (ccw === true) {
            let temp = a1;
            a1 = a0;
            a0 = temp;
        }
        // Get the various arcs for a circle centered at zero.
        let arcs = FPath.arcToBezierNEW(r, a0, a1);
        // Translate them all by (x,y).
        arcs = arcs.translate(new Point2D(x, y));
        this.addPath(arcs);
    }
    ellipse(x, y, rx, ry, rot, a0, a1, ccw) {
        // BUG: Long-term, the right thing to do here is convert it (internally)
        // to a series of bezier curves.
        if (ccw === undefined)
            ccw = false;
        super.ellipse(x, y, rx, ry, rot, a0, a1, ccw);
        this.segs.push(PathSegment.getEllipse(x, y, rx, ry, rot, a0, a1, ccw));
    }
    rect(x, y, w, h) {
        // BUG: As above, make into a series of line segments. This one is easy.
        super.rect(x, y, w, h);
        this.segs.push(PathSegment.getRect(x, y, w, h));
    }
    static circArcToBezier(r, a0, a1) {
        // Return a series of n bezier curves for a circle of radius r, extending
        // from r(cos a0,sin a0) to r (cos a1,sin a1).
        //  
        // BUG: This should really be part of ellipse().
        let answer = FPath.arcToBezierNEW(r, a0, a1);
        return answer;
    }
    static parametricToBezier(f, t0, t1, n) {
        // Given a 2D parametric curve, f(t) = x(t),y(t)), this returns a bezier
        // approximation from t=t0 to t=t1 by taking n time-steps. Obviously,
        // f must be a function returning f.x and f.y. 
        //
        // This works by sampling f (n+1) times, plus n times at the in
        // between points, and fitting a Bezier to each trio of points. It
        // follows the notes in the "main" manual. This is a hard problem -- or
        // a messy one. There are various strategies. The one used here is to
        // choose the tangent at the intermediate point (which I call B) to be
        // parallel to the line between the two end-points of the Bezier segment.
        // This is relatively straightforward, but one problem with this is that
        // the slopes where these segments meet need not be the same -- the
        // resulting curve is not G_1. I'm pretty sure that I worked out a method
        // once that was based (somehow?) on the way MetaPost works, but it's
        // complicated and messy and uses complex numbers.
        let p = new FPath();
        let p1 = f(t0);
        p.moveTo(p1.x, p1.y);
        for (let i = 0; i < n; i++) {
            let p4 = f(t0 + (i + 1) * (t1 - t0) / n);
            let B = f(t0 + (i + 0.5) * (t1 - t0) / n);
            // Work out an appropriate value for t based on relative distances.
            let d1 = Math.sqrt((B.x - p1.x) ** 2 + (B.y - p1.y) ** 2);
            let d2 = Math.sqrt((B.x - p4.x) ** 2 + (B.y - p4.y) ** 2);
            let t = d1 / (d1 + d2);
            // The p4 to p1 vector:
            let V = new Point2D(p4.x - p1.x, p4.y - p1.y);
            // e1 = B - (1-t)(p4-p1)/3 and e2 = B + t(p4-p1)/3.
            let e1 = new Point2D(B.x - (1 - t) * V.x / 3, B.y - (1 - t) * V.y / 3);
            let e2 = new Point2D(B.x + t * V.x / 3, B.y + t * V.y / 3);
            // Run de Casteljau's algorithm backwards. I call this alpha too,
            // but r is a better name since it's a ratio.
            let r = 1 - 1 / (t ** 3 + (1 - t) ** 3);
            let u = (1 - r) * (1 - t) ** 3;
            let C = new Point2D(p1.x * u + p4.x * (1 - u), p1.y * u + p4.y * (1 - u));
            let A = new Point2D(B.x + (C.x - B.x) / r, B.y + (C.y - B.y) / r);
            let v1 = new Point2D((e1.x - A.x * t) / (1 - t), (e1.y - A.y * t) / (1 - t));
            let v2 = new Point2D((e2.x - A.x * (1 - t)) / t, (e2.y - A.y * (1 - t)) / t);
            let p2 = new Point2D((v1.x - p1.x * (1 - t)) / t, (v1.y - p1.y * (1 - t)) / t);
            let p3 = new Point2D((v2.x - p4.x * t) / (1 - t), (v2.y - p4.y * t) / (1 - t));
            p.bezierCurveTo(p2.x, p2.y, p3.x, p3.y, p4.x, p4.y);
            p1 = p4.copy();
        }
        return p;
    }
}
// Text is a special case because it expects a LH coordinate system, but 
// everything else is set up for a RH coordinate system. The end-user
// shouldn't make a direct call to ctx.fillText(). If he does, then the
// the text will be upside-down. So, call this instead.
//
// Getting the placement of the js to match the placement of the tikz exactly
// is difficult because they're using two different fonts. So the tikz
// is drawn at (x+dx,y+dy). The dx and dy are optional and default to zero.
//
// BUG: I *could* create a class, something like FPath, to handle all drawing 
// of text, which may be more natural to the user. But I would probably have 
// to extend CanvasRenderingContext2D somehow and use that everywhere, not 
// just when creating TikZ. For now, this is a sufficient solution.
// Another approach would be to overwrite the existing 
// CanvasRenderingContext2D.fillText method to call the function below.
// In some ways, that's the "right" thing to do, but my gut is that
// it could lead to various problems and make the code generally brittle.
function drawText(ctx, txt, x, y, dx = 0, dy = 0) {
    let saveT = ctx.getTransform();
    if (ctx instanceof CTX) {
        // Don't fool around. Just write it to the .tikz file.
        // BUG: The ts compiler complains about this, but it works fine.
        ctx.fillText(txt, x + dx, y + dy);
        ctx.setTransform(saveT);
        return;
    }
    // Get the measurements -- all we really care about is the baseline. 
    // Transform the ctx so that the horizontal line at y becomes the 
    // origin, flip the scale, and draw at (x,0).
    //
    // Recapitulating the info on MDN, we care about m.actualBoundingBoxAscent
    // and m.actualBoundBoxDescent, which give distances from ctx.textBaseline
    // to the relevant side of the bounding box of the text. The baseline 
    // defaults to the 'alphabetic' setting, which puts the baseline just
    // under where you normally draw the letter -- B sits on the baseline,
    // while p hangs below it.
    let m = ctx.measureText(txt);
    ctx.translate(0, y);
    ctx.scale(1, -1);
    ctx.textBaseline = 'bottom';
    ctx.fillText(txt, x, 0);
    ctx.setTransform(saveT);
}
// As above, but these to draw the text in only one scenario or
// the other. This could be handled with boolean arguments to the
// above, but this seems clearer for the user.
function drawTextBrowserOnly(ctx, txt, x, y, dx = 0, dy = 0) {
    if (ctx instanceof CTX)
        // Skip it.
        return;
    drawText(ctx, txt, x, y, dx, dy);
}
function drawTextTikZOnly(ctx, txt, x, y, dx = 0, dy = 0) {
    if (ctx instanceof CTX)
        ctx.fillText(txt, x + dx, y + dy);
}
// This is to act much like the object returned from 
// canvas.getContext('2d').
// Only a few elements of the standard context class are needed.
// I purposely did *not* make this extend CanvasRenderingContext2D.
// By not extending, you can't accidentally make use of some feature
// of the normal ctx framework and have it silently fail.
// 
// This is one major difference. Each time you want to render a figure,
// you need a new one of these since the tikz text goes out to a file
// with a different name. In principle, it would be possible to allow
// reusing these, but there's no value in allowing for that. 
// I had hoped not to need to deal with transformation matricies, and
// just (implicitly) use the identity matrix. However, certain things
// are easier for the user if they are permitted. See
// www.alanzucconi.com/2016/02/10/tranfsormation-matrix
// for a brief summary of how these work.
// 
// Think of the matrix as R in the upper left, for rotation etc, and 
// (tx,ty,1) in the right column for translation, with M as the overall
// matrix. The bottom row is always (0 0 1). If the user gives (x y) as
// some position relative to M, then the "real" position is M(x y 1).
// By "real" I mean that position relative to the identity matrix.
//
// BUG: I think I am doing this the wrong way. As things stand, I store
// the thing the user does (the path and any points or whatever that
// specify) in terms given by the user. Then I convert those values to
// their "unadjusted" values when written to tikz output. Instead, I should
// convert things as they come in. For one thing, as things stand, if
// the user adjusts the t-matrix as things are drawn, it would mess up
// everything. This would also side-step certain questions like what
// a shear transformation should mean for something like an ellipse. If
// we correct things as just described, then an ellipse is an ellipse,
// and it is not shear-transformed, although the points where ellipse
// is located would be shear-transformed.
//
// BUG: Add a flag, like CTX.paper, and set is to true here.
// That way, the rendering process can output something different on paper.
// This flag will be undefined when run in a browser.
class CTX {
    constructor(name) {
        // Transformation matrix.
        // BUG: Try to get rid of this. I think that, now that all drawing is
        // done with a RH system, this is unnecessary. Everything related to
        // tmatrix is private and I think it's effectively unused.
        this.tmatrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
        // BUG: This is *not* the right way to do things, but it's easier.
        // The problem is in scaling lengths, which are not points. This is a
        // particular problem with radii. The proper solution is to work this value
        // out from the tmatrix, but that's messy.
        // This is why things like ellipses should be treated as beziers.
        this.netScale = 1.0;
        // To allow the user to set the linewidth. Otherwise, tikz uses a 
        // default value of 0.4pt. Tikz has certained named line widths, like
        // 'semithick' and 'ultra thin' but I don't care about those. It's better
        // to stick with numerical values to be consistent with js.
        // This name matches what's used in a "normal" ctx.
        // The way to specify line width in tikz is as an option to \draw:
        // \draw[line width = 1mm]  ...whatever...
        // for example.
        this.lineWidth = 1.0;
        // File name (without the '.tizk') for the figure.
        this.figureName = "";
        // This holds the output as it is generated.
        this.tikzstr = "";
        // Provide the name of the figure whose tikz is being generated.
        // This goes to a file, which is fiddly with js. The contents of the
        // file will be sent to the server, and it is assumed that the server
        // knows what to do. A normal HTTP server will choke on it (really,
        // it will just ignore it).
        //
        // As each call to stroke(),  fill(), and so forth is made, the corresponding
        // tikz is noted.  When all these are calls are done, call close() to write it out.
        this.figureName = name;
        this.tikzstr = "";
        // The tikz file needs a bit of a heading. 
        this.tikzstr += "\\begin{tikzpicture}\n";
        // And everything is clipped to the permitted drawing area. To obtain
        // that area, we need to look at the figure specification.
        let myFunc = getAugmentedFunction(name);
        let fpc = myFunc.figurePanelClass;
        // Neither one really seems to give the right thing.
        // Maybe use \clip as an option to 
        // \begin{tikzpicture}[\clip something?]
        //this.tikzstr += "\\clip (0bp,0bp) rectangle (" +fpc.textWidth+ 
        //    "bp," + fpc.h+ "bp);\n";
        this.tikzstr += "\\useasboundingbox (0bp,0bp) rectangle (" + fpc.textWidth.toFixed(2) +
            "bp," + (fpc.h - fpc.lowerPadding - fpc.upperPadding).toFixed(2) + "bp);\n";
    }
    close() {
        // Finalize the tikz specification, and write it out.
        // 
        // Note that, under Firefox, this generates an error on the console:
        //
        // XML Parsing Error: no root element found
        // Location: http://localhost:8000/geartest01.tikz
        // Line Number 1, Column 1:
        //
        // or whatever the file name is that's saved. Apparently this is a
        // "known issue" (aka, a bug) with Firefox. No such message appears
        // with MS Edge. It works the same either way.
        this.tikzstr += "\\end{tikzpicture}\n";
        // BUG: No doubt there is a more modern fetch() way to do this.
        let req = new XMLHttpRequest();
        // This is *really* not the standard way to do things.
        // Pass the file name to save under, then the text to save.
        // I *should* be passing some cgi script that takes input, but
        // I've tweaked the http server so that it's non-standard,
        // and does what I want instead of what it is supposed to do.
        let fname = this.figureName + ".tikz";
        req.open("POST", fname);
        // I have no idea whether this is really necessary.
        req.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
        req.send(this.tikzstr);
    }
    static clone3x3Matrix(m) {
        // JS seems not to have a standard way of creating a copy of a matrix.
        // This does it for a 3x3 matrix and returns the result.
        let a = [];
        a[0] = [];
        a[0][0] = m[0][0];
        a[0][1] = m[0][1];
        a[0][2] = m[0][2];
        a[1] = [];
        a[1][0] = m[1][0];
        a[1][1] = m[1][1];
        a[1][2] = m[1][2];
        a[2] = [];
        a[2][0] = m[2][0];
        a[2][1] = m[2][1];
        a[2][2] = m[2][2];
        return a;
    }
    getTransform() {
        return CTX.clone3x3Matrix(this.tmatrix);
    }
    setTransform(t) {
        this.tmatrix = CTX.clone3x3Matrix(t);
    }
    translate(tx, ty) {
        // Adjust the transformation matrix. Going forward, this will have the
        // effect of converting (x,y) to (tx + x,ty+y) whenever the user
        // refers to (x,y).  
        this.tmatrix[0][2] += tx;
        this.tmatrix[1][2] += ty;
    }
    scale(sx, sy) {
        // Scale the transformation matrix.
        // Let S = diag(sx,sy,1). The new t-matrix is the old t-matrix times S.
        // 
        // BUG: I am not sure. Maybe it should be S times old t-matrix, and
        // I have the order wrong. For the time being it doesn't matter since
        // every case I care about has sx=sy and the matrices commute in that
        // special case.
        this.tmatrix[0][0] *= sx;
        this.tmatrix[0][1] *= sy;
        this.tmatrix[1][0] *= sx;
        this.tmatrix[1][1] *= sy;
        // Track this here too.
        // BUG: This assumes that sx = xy.
        this.netScale *= sx;
    }
    applyTMatrix(x, y) {
        // Return tmatrix times (x,y). As a matrix operation, this is
        // tmatrix x (x y 1), but we only return the first two entries.
        let ax = this.tmatrix[0][0] * x + this.tmatrix[0][1] * y + this.tmatrix[0][2];
        let ay = this.tmatrix[1][0] * x + this.tmatrix[1][1] * y + this.tmatrix[1][2];
        return { x: ax, y: ay };
    }
    handlePath(path) {
        // Called by either fill() or stroke().
        var segs = path.segs;
        for (let i = 0; i < segs.length; i++) {
            // s is a PathSegment object.
            let s = segs[i];
            if (s.type == PathSegment.MOVE_TO) {
                let m = s.s;
                let t = this.applyTMatrix(m.x, m.y);
                //this.tikzstr += "(" +s.x+ "pt, " + s.y+ "pt) ";
                this.tikzstr += "(" + t.x.toFixed(2) + "bp, " + t.y.toFixed(2) + "bp) ";
            }
            else if (s.type == PathSegment.LINE_TO) {
                // Lines are drawn with the tikz \draw command. It takes the form
                // \draw [options] (x1,y1) -- (x2,y2);
                // Note that I include "bp" for the dimensions. I think that tikz
                // defaults to cm if no dimension is given, so I should specify 
                // something. Note also that I use bp, not pt.
                // 
                // BUG: I am not sure whether the tikz point is 72ppi or 72.27 ppi
                // to match latex.
                // 
                // BUG: For the time being, I will ignore these options, but they 
                // can be things like fill or dashed, or to set the color or line
                // width, and probably a mess of other stuff. 
                let m = s.s;
                let t = this.applyTMatrix(m.x, m.y);
                this.tikzstr += "-- (" + t.x.toFixed(2) + "bp, " + t.y.toFixed(2) + "bp) ";
            }
            else if (s.type == PathSegment.BEZIER) {
                // The sources I found aren't very explicit about exactly how
                // this is implemented. I assume it's done in the usual way.
                // We have
                // P(t) = B(3,0)*CP + B(3,1)*P1 + B(3,2)*P2 + B(3,3)*P3,
                // where t\in [0,1] and B are the usual Bernstein polynomials (the
                // functions of t):
                // B(n,m) = C(n,m) t^m (1-t)^(n-m),
                // and C is the choice function.
                // See also the tikz/pgf manual (v3.1.9a), p. 156, for the output.
                // However the manual is wrong, or not clear. Use 'and' between
                // the control points.
                //
                // The gist is that CP is fixed point where the curve starts;
                // it's implicit for both Java and tikz. P1 and P2 are the control
                // points and P3 is where the curve terminates. Fortunately this
                // matches up nicely with the tikz syntax.
                let m = s.s;
                let t1 = this.applyTMatrix(m.cx1, m.cy1);
                let t2 = this.applyTMatrix(m.cx2, m.cy2);
                let t3 = this.applyTMatrix(m.x, m.y);
                this.tikzstr +=
                    ".. controls (" + t1.x.toFixed(2) + "bp, " + t1.y.toFixed(2) + "bp) and (" +
                        t2.x.toFixed(2) + "bp, " + t2.y.toFixed(2) + "bp) .. (" +
                        t3.x.toFixed(2) + "bp, " + t3.y.toFixed(2) + "bp)";
            }
            else if (s.type == PathSegment.QUADRATIC) {
                // BUG: Put this back.
                console.log("quadratic does not work");
                // Tikz has this too (whew). Oddly, it's part of the pgf stuff.
                // Everything else is drawn with \draw (or \fill), but this
                // uses \pgfpathquadraticcurveto. I'm not sure if that matters,
                // and I hope that you can mix these freely in the middle of 
                // a \draw command. The tikz/pgf manual isn't very clear on
                // mixing these. 
                // BUG: I wonder if I should be using commands like \pgflineto, 
                // \pgfcurveto, and so forth, throughout what I've done.
                // See the tikz/pgf manul (v3.1.9a), p. 1095.
                //
                // BUG: I'm going to code this hoping that it works, but I suspect
                // that it will not, and will need to go back and change to 
                // something other than \draw or \fill to start with.
                // Maybe I need to define an entire path and then \draw or \fill 
                // it? It looks like you define that path, then say
                // \pgfusepath{fill} or whatever.
                // 
                // BUG: Maybe I could convert this to a cubic here and avoid this
                // entire messy issue?
                //
                // NOTE: pgf has some nice commands for drawing only *part* of 
                // a Bezier curve. See p. 1097 for \pgfpathcurvebetweentime.
                // BUG: Maybe I'll be lucky and this is never called.
                // I think (?) it must be that the only time this type of
                // segment ever arises is if the user users a QuadCurve2D, which
                // seems (?) unlikely.
                /*
                let t1 = this.applyTMatrix(s.cx,s.cy);
                let t2 = this.applyTMatrix(s.x,s.y);
                
                //this.tikzstr +=
                //  "\\pgfpathquadraticcurveto {\\pgfpoint{" +
                //    s.cx+ "pt}{" +x.cy+ "pt}}{\\pgfpoint{" +
                //    s.x+ "pt}{" +s.y+ "pt}}";
                this.tikzstr +=
                  "\\pgfpathquadraticcurveto {\\pgfpoint{" +
                    t1.x+ "bp}{" +t1.y+ "bp}}{\\pgfpoint{" +
                    t2.x+ "bp}{" +t2.y+ "bp}}";
                  */
            }
            else if (s.type == PathSegment.ARC) {
                console.log("arc");
                // This is a circular arc of a circle
                // centered at (x,y) over a given range of angles (cw or ccw).
                //
                // BUG: For the remaining cases, I may need to do something
                // special. It's not clear exactly what the browser is doing
                // with these. Are they converted, internally, to bezier
                // curves or are they somehow rendered more directly. 
                this.tikzstr += "no arc implemented";
            }
            else if (s.type == PathSegment.ARC_TO) {
                console.log("arc to not done");
                // This is essentially a bezier curve.
                // You have two control points and a radius. It is not
                // clear exactly how it works.
                this.tikzstr += "no arcTo implemented";
            }
            else if (s.type == PathSegment.ELLIPSE) {
                // BUG: This will only draw a complete ellipse, not an arc of
                // an ellipse.
                // BUG: The foolishness with netScale is another reason not
                // to allow an ellipse type. If an ellipse were really a series
                // of bezier curves, then this would be a non-issue.
                let m = s.s;
                let c = this.applyTMatrix(m.x, m.y);
                //console.log(s.x+ "," +s.y+ " becomes " +c.x+ " " +c.y);
                //this.tikzstr += "(" +c.x+ "pt," +c.y+ 
                //  "pt) ellipse [x radius=" +s.rx*this.netScale+ 
                //  "pt,y radius =" + s.ry*this.netScale+ "pt]";
                this.tikzstr += "(" + c.x.toFixed(2) + "bp," + c.y.toFixed(2) +
                    "bp) ellipse [x radius=" + (m.rx * this.netScale).toFixed(2) +
                    "bp,y radius =" + (m.ry * this.netScale).toFixed(2) + "bp]";
            }
            else if (s.type == PathSegment.RECT) {
                console.log("rect not done");
                this.tikzstr += "no rect implemented";
            }
            else if (s.type == PathSegment.CLOSE) {
                this.tikzstr += "-- cycle";
            }
            else {
                console.log("unknown FPath: " + s.type);
            }
        }
        this.tikzstr += ";\n";
    }
    stroke(path) {
        let segs = path.segs;
        if (segs.length === 0)
            return;
        this.tikzstr += "\\draw[line width=" + this.lineWidth.toFixed(2) + "bp] ";
        this.handlePath(path);
    }
    fill(path) {
        let segs = path.segs;
        if (segs.length === 0)
            return;
        this.tikzstr += "\\fill ";
        this.handlePath(path);
    }
    fillText(s, x, y) {
        // BUG: This is now done with top-level functions now and shouldn't
        // be called (or callable) by outside code.
        // 
        // BUG: I have my doubts about including this one. It needs to be done
        // *somehow*, but I am concerned about a mismatch between the JS
        // font and the fonts used by latex.
        // 
        // BUG: I am ignoring the ctx.font setting. It does seem that if you
        // set it to '10px san-serif' you get something reasonable for the
        // browswer that doesn't look too different than latex.
        //
        // BUG: This is so fussy that I suspect that any drawing that is at
        // all tricky will require that the user provide different placement for
        // text on the browser and text on the page. Getting things to match
        // up *exactly* may be impossible. 
        let t = this.applyTMatrix(x, y);
        // I had this as 'anchor=south west', but 'base west' seems closer
        // to what latex does. 
        // BUG: It's all a mystery.
        this.tikzstr +=
            "\\node [anchor=base west] at (" + t.x.toFixed(2) + "pt, " + t.y.toFixed(2) + "pt) {" + s + "};\n";
    }
}
class Numerical {
    static newton(f, g, a, b, y, e) {
        // Given a function, f, and an initial guess, g, bracketed between a and b,
        // for the argument to f, and a target value, y, this returns x such that
        // f(x) = y to within error, e.
        //  
        // A crude off-the-cuff implementation of Newton-Raphson.
        // This will only work in the tamest situations.
        //
        // Recall that the idea is that
        // f(x0 + dx) ~ f(x0) + f'(x0) dx
        // We want y = f(x + dx) and that is approximately equivalent to
        // y = f(x0) + f'(x0) dx or dx = ( y - f(x0) ) / f'(x0)
        // so that x0 becomes x1 = x0 + dx = x0 + ( y - f(x0) ) / f'(x0)
        //
        // NOTE: I had hoped to avoid the need to bracket entirely, and for
        // some functions (and sufficiently good initial guesses), you could,
        // but it's too easy for the algorithm to get lost among local extrema
        // if there is no bracket.
        //
        // In fact, here is a good example of why bracketing is needed.
        // Let f = cos x + x sin x, which happens to be the x-coordinate for
        // the parameterization of the unit involute. Suppose that you want
        // to find x for which f(x) = 1.5, and you start off with a guess of
        // x = 0.5. The slope of f at 0.5 is small so that Newton-Raphson
        // sends x1 to a value that is beyond the inflection point near x = 3.
        // At that point things go haywire.
        let x0 = g;
        let y0 = f(x0);
        let i = 0;
        while (Math.abs(y - y0) > e) {
            let fplus = f(x0 + e);
            let fminus = f(x0 - e);
            let fprime = (fplus - fminus) / (2 * e);
            let dx = (y - y0) / fprime;
            let x1 = x0 + dx;
            // Make sure we haven't passed a bracket. Just subdivide if we have.
            if (x1 > b)
                x1 = (x1 - x0) / 2;
            if (x1 < a)
                x1 = (x0 - x1) / 2;
            x0 = x1;
            y0 = f(x0);
            // Don't allow an infinite loop
            ++i;
            if (i > 100)
                return x0;
        }
        return x0;
    }
}