/////////////////////////////////////////////////////////////////////// // Text Path /////////////////////////////////////////////////////////////////////// /** * Path constructor. * @author Jason Follas * @constructor * @augments Kinetic.Shape * @param {Object} config */ Kinetic.Plugins.TextPath = Kinetic.Shape.extend({ init : function (config) { this.setDefaultAttrs({ fontFamily : 'Calibri', fontSize : 12, fontStyle : 'normal', detectionType : 'path', text : '' }); this.dummyCanvas = document.createElement('canvas'); this.shapeType = "TextPath"; this.dataArray = []; var that = this; config.drawFunc = function (context) { var charArr = this.charArr; context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; context.textBaseline = 'middle'; context.textAlign = 'left'; context.save(); var glyphInfo = this.glyphInfo; for (var i = 0; i < glyphInfo.length; i++) { context.save(); var p0 = glyphInfo[i].p0; var p1 = glyphInfo[i].p1; var ht = parseFloat(this.attrs.fontSize); context.translate(p0.x, p0.y); context.rotate(glyphInfo[i].rotation); this.fillText(context, glyphInfo[i].text); this.strokeText(context, glyphInfo[i].text); context.restore(); //// To assist with debugging visually, uncomment following // context.beginPath(); // if (i % 2) // context.strokeStyle = 'cyan'; // else // context.strokeStyle = 'green'; // context.moveTo(p0.x, p0.y); // context.lineTo(p1.x, p1.y); // context.stroke(); } context.restore(); }; // call super constructor this._super(config); this.dataArray = Kinetic.Plugins.PathHelper.parsePathData(this.attrs.data); this.on('dataChange', function () { that.dataArray = Kinetic.Plugins.PathHelper.parsePathData(that.attrs.data); }); // update text data for certain attr changes var attrs = ['text', 'textStroke', 'textStrokeWidth']; for (var n = 0; n < attrs.length; n++) { var attr = attrs[n]; this.on(attr + 'Change', that._setTextData); } that._setTextData(); }, /** * get text width in pixels * @name getTextWidth * @methodOf Kinetic.Plugins.TextPath.prototype */ getTextWidth : function () { return this.textWidth; }, /** * get text height in pixels * @name getTextHeight * @methodOf Kinetic.Plugins.TextPath.prototype */ getTextHeight : function () { return this.textHeight; }, _getTextSize : function (text) { var dummyCanvas = this.dummyCanvas; var context = dummyCanvas.getContext('2d'); context.save(); context.font = this.attrs.fontStyle + ' ' + this.attrs.fontSize + 'pt ' + this.attrs.fontFamily; var metrics = context.measureText(text); context.restore(); return { width : metrics.width, height : parseInt(this.attrs.fontSize, 10) }; }, /** * set text data. */ _setTextData : function () { var that = this; var size = this._getTextSize(this.attrs.text); this.textWidth = size.width; this.textHeight = size.height; this.glyphInfo = []; var charArr = this.attrs.text.split(''); var p0, p1, pathCmd; var pIndex = -1; var currentT = 0; var getNextPathSegment = function () { currentT = 0; var pathData = that.dataArray; for (var i = pIndex + 1; i < pathData.length; i++) { if (pathData[i].pathLength > 0) { pIndex = i; return pathData[i]; } else if (pathData[i].command == 'M') { p0 = { x : pathData[i].points[0], y : pathData[i].points[1] }; } } return {}; }; var findSegmentToFitCharacter = function (c, before) { var glyphWidth = that._getTextSize(c).width; var currLen = 0; var attempts = 0; var needNextSegment = false; p1 = undefined; while (Math.abs(glyphWidth - currLen) / glyphWidth > 0.01 && attempts < 25) { attempts++; var cumulativePathLength = currLen; while (pathCmd === undefined) { pathCmd = getNextPathSegment(); if (pathCmd && cumulativePathLength + pathCmd.pathLength < glyphWidth) { cumulativePathLength += pathCmd.pathLength; pathCmd = undefined; } } if (pathCmd === {} || p0 === undefined) return undefined; var needNewSegment = false; switch (pathCmd.command) { case 'L': if (Kinetic.Plugins.PathHelper.getLineLength(p0.x, p0.y, pathCmd.points[0], pathCmd.points[1]) > glyphWidth) { p1 = Kinetic.Plugins.PathHelper.getPointOnLine(glyphWidth, p0.x, p0.y, pathCmd.points[0], pathCmd.points[1], p0.x, p0.y); } else pathCmd = undefined; break; case 'A': var start = pathCmd.points[4]; // 4 = theta var dTheta = pathCmd.points[5]; // 5 = dTheta var end = pathCmd.points[4] + dTheta; if (currentT === 0) currentT = start + 0.00000001; // Just in case start is 0 else if (glyphWidth > currLen) currentT += (Math.PI / 180.0) * dTheta / Math.abs(dTheta); else currentT -= Math.PI / 360.0 * dTheta / Math.abs(dTheta); if (Math.abs(currentT) > Math.abs(end)) { currentT = end; needNewSegment = true; } p1 = Kinetic.Plugins.PathHelper.getPointOnEllipticalArc(pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], currentT, pathCmd.points[6]); break; case 'C': if (currentT === 0) { if (glyphWidth > pathCmd.pathLength) currentT = 0.00000001; else currentT = glyphWidth / pathCmd.pathLength; } else if (glyphWidth > currLen) currentT += (glyphWidth - currLen) / pathCmd.pathLength; else currentT -= (currLen - glyphWidth) / pathCmd.pathLength; if (currentT > 1.0) { currentT = 1.0; needNewSegment = true; } p1 = Kinetic.Plugins.PathHelper.getPointOnCubicBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3], pathCmd.points[4], pathCmd.points[5]); break; case 'Q': if (currentT === 0) currentT = glyphWidth / pathCmd.pathLength; else if (glyphWidth > currLen) currentT += (glyphWidth - currLen) / pathCmd.pathLength; else currentT -= (currLen - glyphWidth) / pathCmd.pathLength; if (currentT > 1.0) { currentT = 1.0; needNewSegment = true; } p1 = Kinetic.Plugins.PathHelper.getPointOnQuadraticBezier(currentT, pathCmd.start.x, pathCmd.start.y, pathCmd.points[0], pathCmd.points[1], pathCmd.points[2], pathCmd.points[3]); break; } if (p1 !== undefined) { currLen = Kinetic.Plugins.PathHelper.getLineLength(p0.x, p0.y, p1.x, p1.y); } if (needNewSegment) { needNewSegment = false; pathCmd = undefined; } } }; for (var i = 0; i < charArr.length; i++) { // Find p1 such that line segment between p0 and p1 is approx. width of glyph findSegmentToFitCharacter(charArr[i]); if (p0 === undefined || p1 === undefined) break; var width = Kinetic.Plugins.PathHelper.getLineLength(p0.x, p0.y, p1.x, p1.y); // Note: Since glyphs are rendered one at a time, any kerning pair data built into the font will not be used. // Can foresee having a rough pair table built in that the developer can override as needed. var kern = 0; // placeholder for future implementation var midpoint = Kinetic.Plugins.PathHelper.getPointOnLine(kern + width / 2.0, p0.x, p0.y, p1.x, p1.y); var rotation = Math.atan2((p1.y - p0.y), (p1.x - p0.x)); this.glyphInfo.push({ transposeX : midpoint.x, transposeY : midpoint.y, text : charArr[i], rotation : rotation, p0 : p0, p1 : p1 }); p0 = p1; } } }); // add setters and getters Kinetic.Node.addGettersSetters(Kinetic.Plugins.TextPath, ['fontFamily', 'fontSize', 'fontStyle', 'textFill', 'textStroke', 'textStrokeWidth', 'text']); /** * set font family * @name setFontFamily * @methodOf Kinetic.Plugins.TextPath.prototype * @param {String} fontFamily */ /** * set font size * @name setFontSize * @methodOf Kinetic.Plugins.TextPath.prototype * @param {int} fontSize */ /** * set font style. Can be "normal", "italic", or "bold". "normal" is the default. * @name setFontStyle * @methodOf Kinetic.Plugins.TextPath.prototype * @param {String} fontStyle */ /** * set text fill color * @name setTextFill * @methodOf Kinetic.Plugins.TextPath.prototype * @param {String} textFill */ /** * set text stroke color * @name setFontStroke * @methodOf Kinetic.Plugins.TextPath.prototype * @param {String} textStroke */ /** * set text stroke width * @name setTextStrokeWidth * @methodOf Kinetic.Plugins.TextPath.prototype * @param {int} textStrokeWidth */ /** * set text * @name setText * @methodOf Kinetic.Plugins.TextPath.prototype * @param {String} text */ /** * get font family * @name getFontFamily * @methodOf Kinetic.Plugins.TextPath.prototype */ /** * get font size * @name getFontSize * @methodOf Kinetic.Plugins.TextPath.prototype */ /** * get font style * @name getFontStyle * @methodOf Kinetic.Plugins.TextPath.prototype */ /** * get text fill color * @name getTextFill * @methodOf Kinetic.Plugins.TextPath.prototype */ /** * get text stroke color * @name getTextStroke * @methodOf Kinetic.Plugins.TextPath.prototype */ /** * get text stroke width * @name getTextStrokeWidth * @methodOf Kinetic.Plugins.TextPath.prototype */ /** * get text * @name getText * @methodOf Kinetic.Plugins.TextPath.prototype */