1 pv.SvgScene.line = function(scenes) {
  2   var e = scenes.$g.firstChild;
  3   if (scenes.length < 2) return e;
  4   var s = scenes[0];
  5 
  6   /* segmented */
  7   if (s.segmented) return this.lineSegment(scenes);
  8 
  9   /* visible */
 10   if (!s.visible) return e;
 11   var fill = s.fillStyle, stroke = s.strokeStyle;
 12   if (!fill.opacity && !stroke.opacity) return e;
 13 
 14   /* points */
 15   var d = "M" + s.left + "," + s.top;
 16 
 17   if (scenes.length > 2 && (s.interpolate == "basis" || s.interpolate == "cardinal" || s.interpolate == "monotone")) {
 18     switch (s.interpolate) {
 19       case "basis": d += this.curveBasis(scenes); break;
 20       case "cardinal": d += this.curveCardinal(scenes, s.tension); break;
 21       case "monotone": d += this.curveMonotone(scenes); break;
 22     }
 23   } else {
 24     for (var i = 1; i < scenes.length; i++) {
 25       d += this.pathSegment(scenes[i - 1], scenes[i]);
 26     }
 27   }
 28 
 29   e = this.expect(e, "path", {
 30       "shape-rendering": s.antialias ? null : "crispEdges",
 31       "pointer-events": s.events,
 32       "cursor": s.cursor,
 33       "d": d,
 34       "fill": fill.color,
 35       "fill-opacity": fill.opacity || null,
 36       "stroke": stroke.color,
 37       "stroke-opacity": stroke.opacity || null,
 38       "stroke-width": stroke.opacity ? s.lineWidth / this.scale : null,
 39       "stroke-linejoin": s.lineJoin
 40     });
 41   return this.append(e, scenes, 0);
 42 };
 43 
 44 pv.SvgScene.lineSegment = function(scenes) {
 45   var e = scenes.$g.firstChild;
 46 
 47   var s = scenes[0];
 48   var paths;
 49   switch (s.interpolate) {
 50     case "basis": paths = this.curveBasisSegments(scenes); break;
 51     case "cardinal": paths = this.curveCardinalSegments(scenes, s.tension); break;
 52     case "monotone": paths = this.curveMonotoneSegments(scenes); break;
 53   }
 54 
 55   for (var i = 0, n = scenes.length - 1; i < n; i++) {
 56     var s1 = scenes[i], s2 = scenes[i + 1];
 57 
 58     /* visible */
 59     if (!s1.visible || !s2.visible) continue;
 60     var stroke = s1.strokeStyle, fill = pv.Color.transparent;
 61     if (!stroke.opacity) continue;
 62 
 63     /* interpolate */
 64     var d;
 65     if ((s1.interpolate == "linear") && (s1.lineJoin == "miter")) {
 66       fill = stroke;
 67       stroke = pv.Color.transparent;
 68       d = this.pathJoin(scenes[i - 1], s1, s2, scenes[i + 2]);
 69     } else if(paths) {
 70       d = paths[i];
 71     } else {
 72       d = "M" + s1.left + "," + s1.top + this.pathSegment(s1, s2);
 73     }
 74 
 75     e = this.expect(e, "path", {
 76         "shape-rendering": s1.antialias ? null : "crispEdges",
 77         "pointer-events": s1.events,
 78         "cursor": s1.cursor,
 79         "d": d,
 80         "fill": fill.color,
 81         "fill-opacity": fill.opacity || null,
 82         "stroke": stroke.color,
 83         "stroke-opacity": stroke.opacity || null,
 84         "stroke-width": stroke.opacity ? s1.lineWidth / this.scale : null,
 85         "stroke-linejoin": s1.lineJoin
 86       });
 87     e = this.append(e, scenes, i);
 88   }
 89   return e;
 90 };
 91 
 92 /** @private Returns the path segment for the specified points. */
 93 pv.SvgScene.pathSegment = function(s1, s2) {
 94   var l = 1; // sweep-flag
 95   switch (s1.interpolate) {
 96     case "polar-reverse":
 97       l = 0;
 98     case "polar": {
 99       var dx = s2.left - s1.left,
100           dy = s2.top - s1.top,
101           e = 1 - s1.eccentricity,
102           r = Math.sqrt(dx * dx + dy * dy) / (2 * e);
103       if ((e <= 0) || (e > 1)) break; // draw a straight line
104       return "A" + r + "," + r + " 0 0," + l + " " + s2.left + "," + s2.top;
105     }
106     case "step-before": return "V" + s2.top + "H" + s2.left;
107     case "step-after": return "H" + s2.left + "V" + s2.top;
108   }
109   return "L" + s2.left + "," + s2.top;
110 };
111 
112 /** @private Line-line intersection, per Akenine-Moller 16.16.1. */
113 pv.SvgScene.lineIntersect = function(o1, d1, o2, d2) {
114   return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp())));
115 }
116 
117 /** @private Returns the miter join path for the specified points. */
118 pv.SvgScene.pathJoin = function(s0, s1, s2, s3) {
119   /*
120    * P1-P2 is the current line segment. V is a vector that is perpendicular to
121    * the line segment, and has length lineWidth / 2. ABCD forms the initial
122    * bounding box of the line segment (i.e., the line segment if we were to do
123    * no joins).
124    */
125   var p1 = pv.vector(s1.left, s1.top),
126       p2 = pv.vector(s2.left, s2.top),
127       p = p2.minus(p1),
128       v = p.perp().norm(),
129       w = v.times(s1.lineWidth / (2 * this.scale)),
130       a = p1.plus(w),
131       b = p2.plus(w),
132       c = p2.minus(w),
133       d = p1.minus(w);
134 
135   /*
136    * Start join. P0 is the previous line segment's start point. We define the
137    * cutting plane as the average of the vector perpendicular to P0-P1, and
138    * the vector perpendicular to P1-P2. This insures that the cross-section of
139    * the line on the cutting plane is equal if the line-width is unchanged.
140    * Note that we don't implement miter limits, so these can get wild.
141    */
142   if (s0 && s0.visible) {
143     var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v);
144     d = this.lineIntersect(p1, v1, d, p);
145     a = this.lineIntersect(p1, v1, a, p);
146   }
147 
148   /* Similarly, for end join. */
149   if (s3 && s3.visible) {
150     var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v);
151     c = this.lineIntersect(p2, v2, c, p);
152     b = this.lineIntersect(p2, v2, b, p);
153   }
154 
155   return "M" + a.x + "," + a.y
156        + "L" + b.x + "," + b.y
157        + " " + c.x + "," + c.y
158        + " " + d.x + "," + d.y;
159 };
160