diff --git a/src/path/CurveFlattener.js b/src/path/CurveFlattener.js index d786b260..b1a771b9 100644 --- a/src/path/CurveFlattener.js +++ b/src/path/CurveFlattener.js @@ -1,9 +1,12 @@ var CurveFlattener = Base.extend({ initialize: function(curve) { - this.curve = curve; - this.segments = []; + this.parts = []; this.length = 0; - this._computeSegments(curve.getCurveValues(), 0, 1); + // Keep a current index from where we last where in getParameter(), to + // optimise for iterator-like usage of the flattener. + this.index = 0; + this.curve = curve.getCurveValues(); + this._computeSegments(this.curve, 0, 1); }, _computeSegments: function(values, minT, maxT) { @@ -13,7 +16,7 @@ var CurveFlattener = Base.extend({ && !Curve.isSufficientlyFlat.apply(Curve, values)) { var curves = Curve.subdivide.apply(Curve, values); var halfT = (minT + maxT) / 2; - // Recursively subdive and compute segments again. + // Recursively subdive and compute parts again. this._computeSegments(curves[0], minT, halfT); this._computeSegments(curves[1], halfT, maxT); } else { @@ -23,7 +26,7 @@ var CurveFlattener = Base.extend({ dist = Math.sqrt(x * x + y * y); if (dist > Numerical.TOLERANCE) { this.length += dist; - this.segments.push({ + this.parts.push({ length: this.length, value: maxT }); @@ -32,12 +35,21 @@ var CurveFlattener = Base.extend({ }, getParameter: function(length) { + // Make sure we're not beyond the requested length already. Search the + // start position backwards from where to then process the loop below. + var i, j = this.index; + for (;;) { + i = j; + if (j == 0 || this.parts[--j].length < length) + break; + } // Find the segment that succeeds the given length, then interpolate // with the previous segment - for (var i = 0, l = this.segments.length; i < l; i++) { - var segment = this.segments[i]; + for (var l = this.parts.length; i < l; i++) { + var segment = this.parts[i]; if (segment.length >= length) { - var prev = this.segments[i - 1], + this.index = i; + var prev = this.parts[i - 1], prevValue = prev ? prev.value : 0, prevLength = prev ? prev.length : 0; // Interpolate @@ -46,5 +58,27 @@ var CurveFlattener = Base.extend({ } } return 1; + }, + + drawDash: function(ctx, from, to, moveTo) { + from = this.getParameter(from); + to = this.getParameter(to); + var curve = this.curve; + if (from > 0) { + // 8th argument of Curve.subdivide() == t, and values can be + // directly used as arguments list for apply(). + curve[8] = from; // See above + curve = Curve.subdivide.apply(Curve, curve)[1]; // right + } + if (moveTo) { + ctx.moveTo(curve[0], curve[1]); + } + if (to < 1) { + // Se above about curve[8]. + // Interpolate the parameter at 'to' in the new curve and cut there + curve[8] = (to - from) / (1 - from); + curve = Curve.subdivide.apply(Curve, curve)[0]; // left + } + ctx.bezierCurveTo.apply(ctx, curve.slice(2)); } }); diff --git a/src/path/Path.js b/src/path/Path.js index 6304e777..4a2387ed 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -717,8 +717,7 @@ var Path = this.Path = PathItem.extend({ } function drawSegments(ctx, segments) { - var length = segments.length, - handleOut, outX, outY; + var handleOut, outX, outY; function drawSegment(i) { var segment = segments[i], @@ -741,22 +740,56 @@ var Path = this.Path = PathItem.extend({ outY = handleOut._y + y; } - for (var i = 0; i < length; i++) + for (var i = 0, l = segments.length; i < l; i++) drawSegment(i); // Close path by drawing first segment again if (this._closed && length > 1) drawSegment(0); } + function drawDashes(ctx, curves, dashArray, dashOffset) { + var length = 0, + from = dashOffset, to, + open = false; + for (var i = 0, j = 0, l = curves.length; i < l; i++) { + var curve = curves[i]; + var flattener = new CurveFlattener(curve); + length = flattener.length; + while (true) { + if (open) { + flattener.drawDash(ctx, from, to, false); + from = to + dashArray[(j++) % dashArray.length]; + open = false; + } + to = from + dashArray[(j++) % dashArray.length]; + flattener.drawDash(ctx, from, to, true); + if (to > length) { + from = 0; + to -= length; + open = true; + break; + } + from = to + dashArray[(j++) % dashArray.length]; + if (from >= length) { + from -= length; + break; + } + } + } + } + return { draw: function(ctx, param) { if (!param.compound) ctx.beginPath(); var fillColor = this.getFillColor(), - strokeColor = this.getStrokeColor(); + strokeColor = this.getStrokeColor(), + dashArray = this.getDashArray() || [], // TODO: Always defined? + hasDash = !!dashArray.length; - if (param.compound || param.selection || param.clip || fillColor || strokeColor) { + if (param.compound || param.selection || param.clip || fillColor + || strokeColor && !hasDash) { drawSegments(ctx, this._segments); } @@ -784,6 +817,13 @@ var Path = this.Path = PathItem.extend({ } if (strokeColor) { ctx.strokeStyle = strokeColor.getCanvasStyle(ctx); + if (hasDash) { + // We cannot use the path created by drawSegments above + // Use CurveFlatteners to draw dashed paths: + ctx.beginPath(); + var curves = this.getCurves(); + drawDashes(ctx, curves, dashArray, this.getDashOffset()); + } ctx.stroke(); } ctx.restore();