Commit | Line | Data |
---|---|---|
fb63bf1b DV |
1 | /** |
2 | * @license | |
3 | * Copyright 2012 Dan Vanderkam (danvdk@gmail.com) | |
4 | * MIT-licensed (http://opensource.org/licenses/MIT) | |
5 | */ | |
6 | ||
3ce712e6 DV |
7 | (function() { |
8 | 'use strict'; | |
9 | ||
fb63bf1b DV |
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 | ||
7e1f9ca3 DV |
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 | * | |
0bb99c82 | 46 | * @param {Array.<number>} pattern A description of the stroke pattern. Even |
7e1f9ca3 DV |
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 | */ | |
fb63bf1b DV |
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 | ||
0bb99c82 | 69 | /** @type {function()|undefined} */ |
fb63bf1b DV |
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]; | |
ddc5f0ee | 122 | var x = 0; |
fb63bf1b DV |
123 | while (len > x) { |
124 | // Get the length of the pattern segment we are dealing with. | |
ddc5f0ee | 125 | var segment = pattern[patternIndex]; |
fb63bf1b DV |
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(); | |
46fd9089 DV |
161 | x1 = x2; |
162 | y1 = y2; | |
fb63bf1b DV |
163 | } |
164 | } | |
165 | realStroke.call(this); | |
166 | segments = []; | |
167 | }; | |
168 | }; | |
8458f353 DV |
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."; | |
46fd9089 | 178 | }; |
3ce712e6 DV |
179 | |
180 | })(); |