| 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 | })(); |