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