import BBox from './BBox'; const SVG_COMMANDS = { moveTo: 'M', lineTo: 'L', quadraticCurveTo: 'Q', bezierCurveTo: 'C', closePath: 'Z' }; /** * Path objects are returned by glyphs and represent the actual * vector outlines for each glyph in the font. Paths can be converted * to SVG path data strings, or to functions that can be applied to * render the path to a graphics context. */ export default class Path { constructor() { this.commands = []; this._bbox = null; this._cbox = null; } /** * Compiles the path to a JavaScript function that can be applied with * a graphics context in order to render the path. * @return {string} */ toFunction() { let cmds = this.commands.map(c => ` ctx.${c.command}(${c.args.join(', ')});`); return new Function('ctx', cmds.join('\n')); } /** * Converts the path to an SVG path data string * @return {string} */ toSVG() { let cmds = this.commands.map(c => { let args = c.args.map(arg => Math.round(arg * 100) / 100); return `${SVG_COMMANDS[c.command]}${args.join(' ')}`; }); return cmds.join(''); } /** * Gets the "control box" of a path. * This is like the bounding box, but it includes all points including * control points of bezier segments and is much faster to compute than * the real bounding box. * @type {BBox} */ get cbox() { if (!this._cbox) { let cbox = new BBox; for (let command of this.commands) { for (let i = 0; i < command.args.length; i += 2) { cbox.addPoint(command.args[i], command.args[i + 1]); } } this._cbox = Object.freeze(cbox); } return this._cbox; } /** * Gets the exact bounding box of the path by evaluating curve segments. * Slower to compute than the control box, but more accurate. * @type {BBox} */ get bbox() { if (this._bbox) { return this._bbox; } let bbox = new BBox; let cx = 0, cy = 0; let f = t => ( Math.pow(1 - t, 3) * p0[i] + 3 * Math.pow(1 - t, 2) * t * p1[i] + 3 * (1 - t) * Math.pow(t, 2) * p2[i] + Math.pow(t, 3) * p3[i] ); for (let c of this.commands) { switch (c.command) { case 'moveTo': case 'lineTo': let [x, y] = c.args; bbox.addPoint(x, y); cx = x; cy = y; break; case 'quadraticCurveTo': case 'bezierCurveTo': if (c.command === 'quadraticCurveTo') { // http://fontforge.org/bezier.html var [qp1x, qp1y, p3x, p3y] = c.args; var cp1x = cx + 2 / 3 * (qp1x - cx); // CP1 = QP0 + 2/3 * (QP1-QP0) var cp1y = cy + 2 / 3 * (qp1y - cy); var cp2x = p3x + 2 / 3 * (qp1x - p3x); // CP2 = QP2 + 2/3 * (QP1-QP2) var cp2y = p3y + 2 / 3 * (qp1y - p3y); } else { var [cp1x, cp1y, cp2x, cp2y, p3x, p3y] = c.args; } // http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html bbox.addPoint(p3x, p3y); var p0 = [cx, cy]; var p1 = [cp1x, cp1y]; var p2 = [cp2x, cp2y]; var p3 = [p3x, p3y]; for (var i = 0; i <= 1; i++) { let b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; let a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; c = 3 * p1[i] - 3 * p0[i]; if (a === 0) { if (b === 0) { continue; } let t = -c / b; if (0 < t && t < 1) { if (i === 0) { bbox.addPoint(f(t), bbox.maxY); } else if (i === 1) { bbox.addPoint(bbox.maxX, f(t)); } } continue; } let b2ac = Math.pow(b, 2) - 4 * c * a; if (b2ac < 0) { continue; } let t1 = (-b + Math.sqrt(b2ac)) / (2 * a); if (0 < t1 && t1 < 1) { if (i === 0) { bbox.addPoint(f(t1), bbox.maxY); } else if (i === 1) { bbox.addPoint(bbox.maxX, f(t1)); } } let t2 = (-b - Math.sqrt(b2ac)) / (2 * a); if (0 < t2 && t2 < 1) { if (i === 0) { bbox.addPoint(f(t2), bbox.maxY); } else if (i === 1) { bbox.addPoint(bbox.maxX, f(t2)); } } } cx = p3x; cy = p3y; break; } } return this._bbox = Object.freeze(bbox); } /** * Applies a mapping function to each point in the path. * @param {function} fn * @return {Path} */ mapPoints(fn) { let path = new Path; for (let c of this.commands) { let args = []; for (let i = 0; i < c.args.length; i += 2) { let [x, y] = fn(c.args[i], c.args[i + 1]); args.push(x, y); } path[c.command](...args); } return path; } /** * Transforms the path by the given matrix. */ transform(m0, m1, m2, m3, m4, m5) { return this.mapPoints((x, y) => { x = m0 * x + m2 * y + m4; y = m1 * x + m3 * y + m5; return [x, y]; }); } /** * Translates the path by the given offset. */ translate(x, y) { return this.transform(1, 0, 0, 1, x, y); } /** * Rotates the path by the given angle (in radians). */ rotate(angle) { let cos = Math.cos(angle); let sin = Math.sin(angle); return this.transform(cos, sin, -sin, cos, 0, 0); } /** * Scales the path. */ scale(scaleX, scaleY = scaleX) { return this.transform(scaleX, 0, 0, scaleY, 0, 0); } } for (let command of ['moveTo', 'lineTo', 'quadraticCurveTo', 'bezierCurveTo', 'closePath']) { Path.prototype[command] = function(...args) { this._bbox = this._cbox = null; this.commands.push({ command, args }); return this; }; }