Merge pull request #516 from danvk/jstd-coverage
[dygraphs.git] / dashed-canvas.js
1 /**
2 * @license
3 * Copyright 2012 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
5 */
6
7 (function() {
8 'use strict';
9
10 /**
11 * @fileoverview Adds support for dashed lines to the HTML5 canvas.
12 *
13 * Usage:
14 * var ctx = canvas.getContext("2d");
15 * ctx.installPattern([10, 5]) // draw 10 pixels, skip 5 pixels, repeat.
16 * ctx.beginPath();
17 * ctx.moveTo(100, 100); // start the first line segment.
18 * ctx.lineTo(150, 200);
19 * ctx.lineTo(200, 100);
20 * ctx.moveTo(300, 150); // start a second, unconnected line
21 * ctx.lineTo(400, 250);
22 * ...
23 * ctx.stroke(); // draw the dashed line.
24 * ctx.uninstallPattern();
25 *
26 * This is designed to leave the canvas untouched when it's not used.
27 * If you never install a pattern, or call uninstallPattern(), then the canvas
28 * will be exactly as it would have if you'd never used this library. The only
29 * difference from the standard canvas will be the "installPattern" method of
30 * the drawing context.
31 */
32
33 /**
34 * Change the stroking style of the canvas drawing context from a solid line to
35 * a pattern (e.g. dashes, dash-dot-dash, etc.)
36 *
37 * Once you've installed the pattern, you can draw with it by using the
38 * beginPath(), moveTo(), lineTo() and stroke() method calls. Note that some
39 * more advanced methods (e.g. quadraticCurveTo() and bezierCurveTo()) are not
40 * supported. See file overview for a working example.
41 *
42 * Side effects of calling this method include adding an "isPatternInstalled"
43 * property and "uninstallPattern" method to this particular canvas context.
44 * You must call uninstallPattern() before calling installPattern() again.
45 *
46 * @param {Array.<number>} pattern A description of the stroke pattern. Even
47 * indices indicate a draw and odd indices indicate a gap (in pixels). The
48 * array should have a even length as any odd lengthed array could be expressed
49 * as a smaller even length array.
50 */
51 CanvasRenderingContext2D.prototype.installPattern = function(pattern) {
52 if (typeof(this.isPatternInstalled) !== 'undefined') {
53 throw "Must un-install old line pattern before installing a new one.";
54 }
55 this.isPatternInstalled = true;
56
57 var dashedLineToHistory = [0, 0];
58
59 // list of connected line segements:
60 // [ [x1, y1], ..., [xn, yn] ], [ [x1, y1], ..., [xn, yn] ]
61 var segments = [];
62
63 // Stash away copies of the unmodified line-drawing functions.
64 var realBeginPath = this.beginPath;
65 var realLineTo = this.lineTo;
66 var realMoveTo = this.moveTo;
67 var realStroke = this.stroke;
68
69 /** @type {function()|undefined} */
70 this.uninstallPattern = function() {
71 this.beginPath = realBeginPath;
72 this.lineTo = realLineTo;
73 this.moveTo = realMoveTo;
74 this.stroke = realStroke;
75 this.uninstallPattern = undefined;
76 this.isPatternInstalled = undefined;
77 };
78
79 // Keep our own copies of the line segments as they're drawn.
80 this.beginPath = function() {
81 segments = [];
82 realBeginPath.call(this);
83 };
84 this.moveTo = function(x, y) {
85 segments.push([[x, y]]);
86 realMoveTo.call(this, x, y);
87 };
88 this.lineTo = function(x, y) {
89 var last = segments[segments.length - 1];
90 last.push([x, y]);
91 };
92
93 this.stroke = function() {
94 if (segments.length === 0) {
95 // Maybe the user is drawing something other than a line.
96 // TODO(danvk): test this case.
97 realStroke.call(this);
98 return;
99 }
100
101 for (var i = 0; i < segments.length; i++) {
102 var seg = segments[i];
103 var x1 = seg[0][0], y1 = seg[0][1];
104 for (var j = 1; j < seg.length; j++) {
105 // Draw a dashed line from (x1, y1) - (x2, y2)
106 var x2 = seg[j][0], y2 = seg[j][1];
107 this.save();
108
109 // Calculate transformation parameters
110 var dx = (x2-x1);
111 var dy = (y2-y1);
112 var len = Math.sqrt(dx*dx + dy*dy);
113 var rot = Math.atan2(dy, dx);
114
115 // Set transformation
116 this.translate(x1, y1);
117 realMoveTo.call(this, 0, 0);
118 this.rotate(rot);
119
120 // Set last pattern index we used for this pattern.
121 var patternIndex = dashedLineToHistory[0];
122 var x = 0;
123 while (len > x) {
124 // Get the length of the pattern segment we are dealing with.
125 var segment = pattern[patternIndex];
126 // If our last draw didn't complete the pattern segment all the way
127 // we will try to finish it. Otherwise we will try to do the whole
128 // segment.
129 if (dashedLineToHistory[1]) {
130 x += dashedLineToHistory[1];
131 } else {
132 x += segment;
133 }
134
135 if (x > len) {
136 // We were unable to complete this pattern index all the way, keep
137 // where we are the history so our next draw continues where we
138 // left off in the pattern.
139 dashedLineToHistory = [patternIndex, x-len];
140 x = len;
141 } else {
142 // We completed this patternIndex, we put in the history that we
143 // are on the beginning of the next segment.
144 dashedLineToHistory = [(patternIndex+1)%pattern.length, 0];
145 }
146
147 // We do a line on a even pattern index and just move on a odd
148 // pattern index. The move is the empty space in the dash.
149 if (patternIndex % 2 === 0) {
150 realLineTo.call(this, x, 0);
151 } else {
152 realMoveTo.call(this, x, 0);
153 }
154
155 // If we are not done, next loop process the next pattern segment, or
156 // the first segment again if we are at the end of the pattern.
157 patternIndex = (patternIndex+1) % pattern.length;
158 }
159
160 this.restore();
161 x1 = x2;
162 y1 = y2;
163 }
164 }
165 realStroke.call(this);
166 segments = [];
167 };
168 };
169
170 /**
171 * Removes the previously-installed pattern.
172 * You must call installPattern() before calling this. You can install at most
173 * one pattern at a time--there is no pattern stack.
174 */
175 CanvasRenderingContext2D.prototype.uninstallPattern = function() {
176 // This will be replaced by a non-error version when a pattern is installed.
177 throw "Must install a line pattern before uninstalling it.";
178 };
179
180 })();