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