summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/doc/latex/figput/javascript/tikz.js
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/doc/latex/figput/javascript/tikz.js')
-rw-r--r--Master/texmf-dist/doc/latex/figput/javascript/tikz.js1040
1 files changed, 1040 insertions, 0 deletions
diff --git a/Master/texmf-dist/doc/latex/figput/javascript/tikz.js b/Master/texmf-dist/doc/latex/figput/javascript/tikz.js
new file mode 100644
index 00000000000..b5317702b0a
--- /dev/null
+++ b/Master/texmf-dist/doc/latex/figput/javascript/tikz.js
@@ -0,0 +1,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;
+ }
+}