path.js
1 // Geometric objects 2 3 import BoundingBox from './bbox'; 4 5 /** 6 * A bézier path containing a set of path commands similar to a SVG path. 7 * Paths can be drawn on a context using `draw`. 8 * @exports opentype.Path 9 * @class 10 * @constructor 11 */ 12 function Path() { 13 this.commands = []; 14 this.fill = 'black'; 15 this.stroke = null; 16 this.strokeWidth = 1; 17 } 18 19 /** 20 * @param {number} x 21 * @param {number} y 22 */ 23 Path.prototype.moveTo = function(x, y) { 24 this.commands.push({ 25 type: 'M', 26 x: x, 27 y: y 28 }); 29 }; 30 31 /** 32 * @param {number} x 33 * @param {number} y 34 */ 35 Path.prototype.lineTo = function(x, y) { 36 this.commands.push({ 37 type: 'L', 38 x: x, 39 y: y 40 }); 41 }; 42 43 /** 44 * Draws cubic curve 45 * @function 46 * curveTo 47 * @memberof opentype.Path.prototype 48 * @param {number} x1 - x of control 1 49 * @param {number} y1 - y of control 1 50 * @param {number} x2 - x of control 2 51 * @param {number} y2 - y of control 2 52 * @param {number} x - x of path point 53 * @param {number} y - y of path point 54 */ 55 56 /** 57 * Draws cubic curve 58 * @function 59 * bezierCurveTo 60 * @memberof opentype.Path.prototype 61 * @param {number} x1 - x of control 1 62 * @param {number} y1 - y of control 1 63 * @param {number} x2 - x of control 2 64 * @param {number} y2 - y of control 2 65 * @param {number} x - x of path point 66 * @param {number} y - y of path point 67 * @see curveTo 68 */ 69 Path.prototype.curveTo = Path.prototype.bezierCurveTo = function(x1, y1, x2, y2, x, y) { 70 this.commands.push({ 71 type: 'C', 72 x1: x1, 73 y1: y1, 74 x2: x2, 75 y2: y2, 76 x: x, 77 y: y 78 }); 79 }; 80 81 /** 82 * Draws quadratic curve 83 * @function 84 * quadraticCurveTo 85 * @memberof opentype.Path.prototype 86 * @param {number} x1 - x of control 87 * @param {number} y1 - y of control 88 * @param {number} x - x of path point 89 * @param {number} y - y of path point 90 */ 91 92 /** 93 * Draws quadratic curve 94 * @function 95 * quadTo 96 * @memberof opentype.Path.prototype 97 * @param {number} x1 - x of control 98 * @param {number} y1 - y of control 99 * @param {number} x - x of path point 100 * @param {number} y - y of path point 101 */ 102 Path.prototype.quadTo = Path.prototype.quadraticCurveTo = function(x1, y1, x, y) { 103 this.commands.push({ 104 type: 'Q', 105 x1: x1, 106 y1: y1, 107 x: x, 108 y: y 109 }); 110 }; 111 112 /** 113 * Closes the path 114 * @function closePath 115 * @memberof opentype.Path.prototype 116 */ 117 118 /** 119 * Close the path 120 * @function close 121 * @memberof opentype.Path.prototype 122 */ 123 Path.prototype.close = Path.prototype.closePath = function() { 124 this.commands.push({ 125 type: 'Z' 126 }); 127 }; 128 129 /** 130 * Add the given path or list of commands to the commands of this path. 131 * @param {Array} pathOrCommands - another opentype.Path, an opentype.BoundingBox, or an array of commands. 132 */ 133 Path.prototype.extend = function(pathOrCommands) { 134 if (pathOrCommands.commands) { 135 pathOrCommands = pathOrCommands.commands; 136 } else if (pathOrCommands instanceof BoundingBox) { 137 const box = pathOrCommands; 138 this.moveTo(box.x1, box.y1); 139 this.lineTo(box.x2, box.y1); 140 this.lineTo(box.x2, box.y2); 141 this.lineTo(box.x1, box.y2); 142 this.close(); 143 return; 144 } 145 146 Array.prototype.push.apply(this.commands, pathOrCommands); 147 }; 148 149 /** 150 * Calculate the bounding box of the path. 151 * @returns {opentype.BoundingBox} 152 */ 153 Path.prototype.getBoundingBox = function() { 154 const box = new BoundingBox(); 155 156 let startX = 0; 157 let startY = 0; 158 let prevX = 0; 159 let prevY = 0; 160 for (let i = 0; i < this.commands.length; i++) { 161 const cmd = this.commands[i]; 162 switch (cmd.type) { 163 case 'M': 164 box.addPoint(cmd.x, cmd.y); 165 startX = prevX = cmd.x; 166 startY = prevY = cmd.y; 167 break; 168 case 'L': 169 box.addPoint(cmd.x, cmd.y); 170 prevX = cmd.x; 171 prevY = cmd.y; 172 break; 173 case 'Q': 174 box.addQuad(prevX, prevY, cmd.x1, cmd.y1, cmd.x, cmd.y); 175 prevX = cmd.x; 176 prevY = cmd.y; 177 break; 178 case 'C': 179 box.addBezier(prevX, prevY, cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); 180 prevX = cmd.x; 181 prevY = cmd.y; 182 break; 183 case 'Z': 184 prevX = startX; 185 prevY = startY; 186 break; 187 default: 188 throw new Error('Unexpected path command ' + cmd.type); 189 } 190 } 191 if (box.isEmpty()) { 192 box.addPoint(0, 0); 193 } 194 return box; 195 }; 196 197 /** 198 * Draw the path to a 2D context. 199 * @param {CanvasRenderingContext2D} ctx - A 2D drawing context. 200 */ 201 Path.prototype.draw = function(ctx) { 202 ctx.beginPath(); 203 for (let i = 0; i < this.commands.length; i += 1) { 204 const cmd = this.commands[i]; 205 if (cmd.type === 'M') { 206 ctx.moveTo(cmd.x, cmd.y); 207 } else if (cmd.type === 'L') { 208 ctx.lineTo(cmd.x, cmd.y); 209 } else if (cmd.type === 'C') { 210 ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); 211 } else if (cmd.type === 'Q') { 212 ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y); 213 } else if (cmd.type === 'Z') { 214 ctx.closePath(); 215 } 216 } 217 218 if (this.fill) { 219 ctx.fillStyle = this.fill; 220 ctx.fill(); 221 } 222 223 if (this.stroke) { 224 ctx.strokeStyle = this.stroke; 225 ctx.lineWidth = this.strokeWidth; 226 ctx.stroke(); 227 } 228 }; 229 230 /** 231 * Convert the Path to a string of path data instructions 232 * See http://www.w3.org/TR/SVG/paths.html#PathData 233 * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 234 * @return {string} 235 */ 236 Path.prototype.toPathData = function(decimalPlaces) { 237 decimalPlaces = decimalPlaces !== undefined ? decimalPlaces : 2; 238 239 function floatToString(v) { 240 if (Math.round(v) === v) { 241 return '' + Math.round(v); 242 } else { 243 return v.toFixed(decimalPlaces); 244 } 245 } 246 247 function packValues() { 248 let s = ''; 249 for (let i = 0; i < arguments.length; i += 1) { 250 const v = arguments[i]; 251 if (v >= 0 && i > 0) { 252 s += ' '; 253 } 254 255 s += floatToString(v); 256 } 257 258 return s; 259 } 260 261 let d = ''; 262 for (let i = 0; i < this.commands.length; i += 1) { 263 const cmd = this.commands[i]; 264 if (cmd.type === 'M') { 265 d += 'M' + packValues(cmd.x, cmd.y); 266 } else if (cmd.type === 'L') { 267 d += 'L' + packValues(cmd.x, cmd.y); 268 } else if (cmd.type === 'C') { 269 d += 'C' + packValues(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); 270 } else if (cmd.type === 'Q') { 271 d += 'Q' + packValues(cmd.x1, cmd.y1, cmd.x, cmd.y); 272 } else if (cmd.type === 'Z') { 273 d += 'Z'; 274 } 275 } 276 277 return d; 278 }; 279 280 /** 281 * Convert the path to an SVG <path> element, as a string. 282 * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 283 * @return {string} 284 */ 285 Path.prototype.toSVG = function(decimalPlaces) { 286 let svg = '<path d="'; 287 svg += this.toPathData(decimalPlaces); 288 svg += '"'; 289 if (this.fill && this.fill !== 'black') { 290 if (this.fill === null) { 291 svg += ' fill="none"'; 292 } else { 293 svg += ' fill="' + this.fill + '"'; 294 } 295 } 296 297 if (this.stroke) { 298 svg += ' stroke="' + this.stroke + '" stroke-width="' + this.strokeWidth + '"'; 299 } 300 301 svg += '/>'; 302 return svg; 303 }; 304 305 /** 306 * Convert the path to a DOM element. 307 * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values 308 * @return {SVGPathElement} 309 */ 310 Path.prototype.toDOMElement = function(decimalPlaces) { 311 const temporaryPath = this.toPathData(decimalPlaces); 312 const newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 313 314 newPath.setAttribute('d', temporaryPath); 315 316 return newPath; 317 }; 318 319 export default Path;