1 // TODO fillStyle for lineSegment?
  2 // TODO lineOffset for flow maps?
  3 
  4 pv.SvgScene.line = function(scenes) {
  5   var e = scenes.$g.firstChild;
  6   if (scenes.length < 2) return e;
  7   var s = scenes[0];
  8 
  9   /* segmented */
 10   if (s.segmented) return this.lineSegment(scenes);
 11 
 12   /* visible */
 13   if (!s.visible) return e;
 14   var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle);
 15   if (!fill.opacity && !stroke.opacity) return e;
 16 
 17   /* points */
 18   var p = "";
 19   for (var i = 0; i < scenes.length; i++) {
 20     var si = scenes[i];
 21     p += si.left + "," + si.top + " ";
 22 
 23     /* interpolate (assume linear by default) */
 24     if (i < scenes.length - 1) {
 25       var sj = scenes[i + 1];
 26       switch (s.interpolate) {
 27         case "step-before": {
 28           p += si.left + "," + sj.top + " ";
 29           break;
 30         }
 31         case "step-after": {
 32           p += sj.left + "," + si.top + " ";
 33           break;
 34         }
 35       }
 36     }
 37   }
 38 
 39 
 40   e = this.expect("polyline", e);
 41   e.setAttribute("cursor", s.cursor);
 42   e.setAttribute("points", p);
 43   e.setAttribute("fill", fill.color);
 44   e.setAttribute("fill-opacity", fill.opacity);
 45   e.setAttribute("stroke", stroke.color);
 46   e.setAttribute("stroke-opacity", stroke.opacity);
 47   e.setAttribute("stroke-width", s.lineWidth);
 48   return this.append(e, scenes, 0);
 49 };
 50 
 51 pv.SvgScene.lineSegment = function(scenes) {
 52   var e = scenes.$g.firstChild;
 53   for (var i = 0, n = scenes.length - 1; i < n; i++) {
 54     var s1 = scenes[i], s2 = scenes[i + 1];
 55 
 56     /* visible */
 57     if (!s1.visible || !s2.visible) continue;
 58     var stroke = pv.color(s1.strokeStyle);
 59     if (!stroke.opacity) continue;
 60 
 61     /* Line-line intersection, per Akenine-Moller 16.16.1. */
 62     function intersect(o1, d1, o2, d2) {
 63       return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp())));
 64     }
 65 
 66     /*
 67      * P1-P2 is the current line segment. V is a vector that is perpendicular to
 68      * the line segment, and has length lineWidth / 2. ABCD forms the initial
 69      * bounding box of the line segment (i.e., the line segment if we were to do
 70      * no joins).
 71      */
 72     var p1 = pv.vector(s1.left, s1.top),
 73         p2 = pv.vector(s2.left, s2.top),
 74         p = p2.minus(p1),
 75         v = p.perp().norm(),
 76         w = v.times(s1.lineWidth / 2),
 77         a = p1.plus(w),
 78         b = p2.plus(w),
 79         c = p2.minus(w),
 80         d = p1.minus(w);
 81 
 82     /*
 83      * Start join. P0 is the previous line segment's start point. We define the
 84      * cutting plane as the average of the vector perpendicular to P0-P1, and
 85      * the vector perpendicular to P1-P2. This insures that the cross-section of
 86      * the line on the cutting plane is equal if the line-width is unchanged.
 87      * Note that we don't implement miter limits, so these can get wild.
 88      */
 89     if (i > 0) {
 90       var s0 = scenes[i - 1];
 91       if (s0.visible) {
 92         var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v);
 93         d = intersect(p1, v1, d, p);
 94         a = intersect(p1, v1, a, p);
 95       }
 96     }
 97 
 98     /* Similarly, for end join. */
 99     if (i < (n - 1)) {
100       var s3 = scenes[i + 2];
101       if (s3.visible) {
102         var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v);
103         c = intersect(p2, v2, c, p);
104         b = intersect(p2, v2, b, p);
105       }
106     }
107 
108     /* points */
109     var p = a.x + "," + a.y + " "
110       + b.x + "," + b.y + " "
111       + c.x + "," + c.y + " "
112       + d.x + "," + d.y;
113 
114     e = this.expect("polygon", e);
115     e.setAttribute("cursor", s1.cursor);
116     e.setAttribute("points", p);
117     e.setAttribute("fill", stroke.color);
118     e.setAttribute("fill-opacity", stroke.opacity);
119     e = this.append(e, scenes, i);
120   }
121   return e;
122 };
123