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;