344 lines
14 KiB
JavaScript
344 lines
14 KiB
JavaScript
import { Util } from '../Util.js';
|
|
import { Factory } from '../Factory.js';
|
|
import { Shape } from '../Shape.js';
|
|
import { Path } from './Path.js';
|
|
import { Text, stringToArray } from './Text.js';
|
|
import { getNumberValidator } from '../Validators.js';
|
|
import { _registerNode } from '../Global.js';
|
|
var EMPTY_STRING = '', NORMAL = 'normal';
|
|
function _fillFunc(context) {
|
|
context.fillText(this.partialText, 0, 0);
|
|
}
|
|
function _strokeFunc(context) {
|
|
context.strokeText(this.partialText, 0, 0);
|
|
}
|
|
export class TextPath extends Shape {
|
|
constructor(config) {
|
|
super(config);
|
|
this.dummyCanvas = Util.createCanvasElement();
|
|
this.dataArray = [];
|
|
this.dataArray = Path.parsePathData(this.attrs.data);
|
|
this.on('dataChange.konva', function () {
|
|
this.dataArray = Path.parsePathData(this.attrs.data);
|
|
this._setTextData();
|
|
});
|
|
this.on('textChange.konva alignChange.konva letterSpacingChange.konva kerningFuncChange.konva fontSizeChange.konva', this._setTextData);
|
|
this._setTextData();
|
|
}
|
|
_sceneFunc(context) {
|
|
context.setAttr('font', this._getContextFont());
|
|
context.setAttr('textBaseline', this.textBaseline());
|
|
context.setAttr('textAlign', 'left');
|
|
context.save();
|
|
var textDecoration = this.textDecoration();
|
|
var fill = this.fill();
|
|
var fontSize = this.fontSize();
|
|
var glyphInfo = this.glyphInfo;
|
|
if (textDecoration === 'underline') {
|
|
context.beginPath();
|
|
}
|
|
for (var i = 0; i < glyphInfo.length; i++) {
|
|
context.save();
|
|
var p0 = glyphInfo[i].p0;
|
|
context.translate(p0.x, p0.y);
|
|
context.rotate(glyphInfo[i].rotation);
|
|
this.partialText = glyphInfo[i].text;
|
|
context.fillStrokeShape(this);
|
|
if (textDecoration === 'underline') {
|
|
if (i === 0) {
|
|
context.moveTo(0, fontSize / 2 + 1);
|
|
}
|
|
context.lineTo(fontSize, fontSize / 2 + 1);
|
|
}
|
|
context.restore();
|
|
}
|
|
if (textDecoration === 'underline') {
|
|
context.strokeStyle = fill;
|
|
context.lineWidth = fontSize / 20;
|
|
context.stroke();
|
|
}
|
|
context.restore();
|
|
}
|
|
_hitFunc(context) {
|
|
context.beginPath();
|
|
var glyphInfo = this.glyphInfo;
|
|
if (glyphInfo.length >= 1) {
|
|
var p0 = glyphInfo[0].p0;
|
|
context.moveTo(p0.x, p0.y);
|
|
}
|
|
for (var i = 0; i < glyphInfo.length; i++) {
|
|
var p1 = glyphInfo[i].p1;
|
|
context.lineTo(p1.x, p1.y);
|
|
}
|
|
context.setAttr('lineWidth', this.fontSize());
|
|
context.setAttr('strokeStyle', this.colorKey);
|
|
context.stroke();
|
|
}
|
|
getTextWidth() {
|
|
return this.textWidth;
|
|
}
|
|
getTextHeight() {
|
|
Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.');
|
|
return this.textHeight;
|
|
}
|
|
setText(text) {
|
|
return Text.prototype.setText.call(this, text);
|
|
}
|
|
_getContextFont() {
|
|
return Text.prototype._getContextFont.call(this);
|
|
}
|
|
_getTextSize(text) {
|
|
var dummyCanvas = this.dummyCanvas;
|
|
var _context = dummyCanvas.getContext('2d');
|
|
_context.save();
|
|
_context.font = this._getContextFont();
|
|
var metrics = _context.measureText(text);
|
|
_context.restore();
|
|
return {
|
|
width: metrics.width,
|
|
height: parseInt(this.attrs.fontSize, 10),
|
|
};
|
|
}
|
|
_setTextData() {
|
|
var that = this;
|
|
var size = this._getTextSize(this.attrs.text);
|
|
var letterSpacing = this.letterSpacing();
|
|
var align = this.align();
|
|
var kerningFunc = this.kerningFunc();
|
|
this.textWidth = size.width;
|
|
this.textHeight = size.height;
|
|
var textFullWidth = Math.max(this.textWidth + ((this.attrs.text || '').length - 1) * letterSpacing, 0);
|
|
this.glyphInfo = [];
|
|
var fullPathWidth = 0;
|
|
for (var l = 0; l < that.dataArray.length; l++) {
|
|
if (that.dataArray[l].pathLength > 0) {
|
|
fullPathWidth += that.dataArray[l].pathLength;
|
|
}
|
|
}
|
|
var offset = 0;
|
|
if (align === 'center') {
|
|
offset = Math.max(0, fullPathWidth / 2 - textFullWidth / 2);
|
|
}
|
|
if (align === 'right') {
|
|
offset = Math.max(0, fullPathWidth - textFullWidth);
|
|
}
|
|
var charArr = stringToArray(this.text());
|
|
var spacesNumber = this.text().split(' ').length - 1;
|
|
var p0, p1, pathCmd;
|
|
var pIndex = -1;
|
|
var currentT = 0;
|
|
var getNextPathSegment = function () {
|
|
currentT = 0;
|
|
var pathData = that.dataArray;
|
|
for (var j = pIndex + 1; j < pathData.length; j++) {
|
|
if (pathData[j].pathLength > 0) {
|
|
pIndex = j;
|
|
return pathData[j];
|
|
}
|
|
else if (pathData[j].command === 'M') {
|
|
p0 = {
|
|
x: pathData[j].points[0],
|
|
y: pathData[j].points[1],
|
|
};
|
|
}
|
|
}
|
|
return {};
|
|
};
|
|
var findSegmentToFitCharacter = function (c) {
|
|
var glyphWidth = that._getTextSize(c).width + letterSpacing;
|
|
if (c === ' ' && align === 'justify') {
|
|
glyphWidth += (fullPathWidth - textFullWidth) / spacesNumber;
|
|
}
|
|
var currLen = 0;
|
|
var attempts = 0;
|
|
p1 = undefined;
|
|
while (Math.abs(glyphWidth - currLen) / glyphWidth > 0.01 &&
|
|
attempts < 20) {
|
|
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 (Path.getLineLength(p0.x, p0.y, pathCmd.points[0], pathCmd.points[1]) > glyphWidth) {
|
|
p1 = Path.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];
|
|
var dTheta = pathCmd.points[5];
|
|
var end = pathCmd.points[4] + dTheta;
|
|
if (currentT === 0) {
|
|
currentT = start + 0.00000001;
|
|
}
|
|
else if (glyphWidth > currLen) {
|
|
currentT += ((Math.PI / 180.0) * dTheta) / Math.abs(dTheta);
|
|
}
|
|
else {
|
|
currentT -= ((Math.PI / 360.0) * dTheta) / Math.abs(dTheta);
|
|
}
|
|
if ((dTheta < 0 && currentT < end) ||
|
|
(dTheta >= 0 && currentT > end)) {
|
|
currentT = end;
|
|
needNewSegment = true;
|
|
}
|
|
p1 = Path.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 / 2;
|
|
}
|
|
else {
|
|
currentT = Math.max(currentT - (currLen - glyphWidth) / pathCmd.pathLength / 2, 0);
|
|
}
|
|
if (currentT > 1.0) {
|
|
currentT = 1.0;
|
|
needNewSegment = true;
|
|
}
|
|
p1 = Path.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 = Path.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 = Path.getLineLength(p0.x, p0.y, p1.x, p1.y);
|
|
}
|
|
if (needNewSegment) {
|
|
needNewSegment = false;
|
|
pathCmd = undefined;
|
|
}
|
|
}
|
|
};
|
|
var testChar = 'C';
|
|
var glyphWidth = that._getTextSize(testChar).width + letterSpacing;
|
|
var lettersInOffset = offset / glyphWidth - 1;
|
|
for (var k = 0; k < lettersInOffset; k++) {
|
|
findSegmentToFitCharacter(testChar);
|
|
if (p0 === undefined || p1 === undefined) {
|
|
break;
|
|
}
|
|
p0 = p1;
|
|
}
|
|
for (var i = 0; i < charArr.length; i++) {
|
|
findSegmentToFitCharacter(charArr[i]);
|
|
if (p0 === undefined || p1 === undefined) {
|
|
break;
|
|
}
|
|
var width = Path.getLineLength(p0.x, p0.y, p1.x, p1.y);
|
|
var kern = 0;
|
|
if (kerningFunc) {
|
|
try {
|
|
kern = kerningFunc(charArr[i - 1], charArr[i]) * this.fontSize();
|
|
}
|
|
catch (e) {
|
|
kern = 0;
|
|
}
|
|
}
|
|
p0.x += kern;
|
|
p1.x += kern;
|
|
this.textWidth += kern;
|
|
var midpoint = Path.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;
|
|
}
|
|
}
|
|
getSelfRect() {
|
|
if (!this.glyphInfo.length) {
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
}
|
|
var points = [];
|
|
this.glyphInfo.forEach(function (info) {
|
|
points.push(info.p0.x);
|
|
points.push(info.p0.y);
|
|
points.push(info.p1.x);
|
|
points.push(info.p1.y);
|
|
});
|
|
var minX = points[0] || 0;
|
|
var maxX = points[0] || 0;
|
|
var minY = points[1] || 0;
|
|
var maxY = points[1] || 0;
|
|
var x, y;
|
|
for (var i = 0; i < points.length / 2; i++) {
|
|
x = points[i * 2];
|
|
y = points[i * 2 + 1];
|
|
minX = Math.min(minX, x);
|
|
maxX = Math.max(maxX, x);
|
|
minY = Math.min(minY, y);
|
|
maxY = Math.max(maxY, y);
|
|
}
|
|
var fontSize = this.fontSize();
|
|
return {
|
|
x: minX - fontSize / 2,
|
|
y: minY - fontSize / 2,
|
|
width: maxX - minX + fontSize,
|
|
height: maxY - minY + fontSize,
|
|
};
|
|
}
|
|
}
|
|
TextPath.prototype._fillFunc = _fillFunc;
|
|
TextPath.prototype._strokeFunc = _strokeFunc;
|
|
TextPath.prototype._fillFuncHit = _fillFunc;
|
|
TextPath.prototype._strokeFuncHit = _strokeFunc;
|
|
TextPath.prototype.className = 'TextPath';
|
|
TextPath.prototype._attrsAffectingSize = ['text', 'fontSize', 'data'];
|
|
_registerNode(TextPath);
|
|
Factory.addGetterSetter(TextPath, 'data');
|
|
Factory.addGetterSetter(TextPath, 'fontFamily', 'Arial');
|
|
Factory.addGetterSetter(TextPath, 'fontSize', 12, getNumberValidator());
|
|
Factory.addGetterSetter(TextPath, 'fontStyle', NORMAL);
|
|
Factory.addGetterSetter(TextPath, 'align', 'left');
|
|
Factory.addGetterSetter(TextPath, 'letterSpacing', 0, getNumberValidator());
|
|
Factory.addGetterSetter(TextPath, 'textBaseline', 'middle');
|
|
Factory.addGetterSetter(TextPath, 'fontVariant', NORMAL);
|
|
Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING);
|
|
Factory.addGetterSetter(TextPath, 'textDecoration', null);
|
|
Factory.addGetterSetter(TextPath, 'kerningFunc', null);
|