mirror of
https://github.com/konvajs/konva.git
synced 2025-04-24 19:03:56 +08:00
596 lines
16 KiB
TypeScript
596 lines
16 KiB
TypeScript
import { Util } from '../Util';
|
|
import { Factory } from '../Factory';
|
|
import { Context } from '../Context';
|
|
import { Shape, ShapeConfig } from '../Shape';
|
|
import { Path } from './Path';
|
|
import { Text, stringToArray } from './Text';
|
|
import { getNumberValidator } from '../Validators';
|
|
import { _registerNode } from '../Global';
|
|
|
|
import { GetSet, Vector2d } from '../types';
|
|
|
|
export interface TextPathConfig extends ShapeConfig {
|
|
text?: string;
|
|
data?: string;
|
|
fontFamily?: string;
|
|
fontSize?: number;
|
|
fontStyle?: string;
|
|
letterSpacing?: number;
|
|
}
|
|
|
|
var EMPTY_STRING = '',
|
|
NORMAL = 'normal';
|
|
|
|
function _fillFunc(context) {
|
|
context.fillText(this.partialText, 0, 0);
|
|
}
|
|
function _strokeFunc(context) {
|
|
context.strokeText(this.partialText, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Path constructor.
|
|
* @author Jason Follas
|
|
* @constructor
|
|
* @memberof Konva
|
|
* @augments Konva.Shape
|
|
* @param {Object} config
|
|
* @param {String} [config.fontFamily] default is Arial
|
|
* @param {Number} [config.fontSize] default is 12
|
|
* @param {String} [config.fontStyle] can be normal, bold, or italic. Default is normal
|
|
* @param {String} [config.fontVariant] can be normal or small-caps. Default is normal
|
|
* @param {String} [config.textBaseline] Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'. Default is middle
|
|
* @param {String} config.text
|
|
* @param {String} config.data SVG data string
|
|
* @param {Function} config.kerningFunc a getter for kerning values for the specified characters
|
|
* @@shapeParams
|
|
* @@nodeParams
|
|
* @example
|
|
* var kerningPairs = {
|
|
* 'A': {
|
|
* ' ': -0.05517578125,
|
|
* 'T': -0.07421875,
|
|
* 'V': -0.07421875
|
|
* }
|
|
* 'V': {
|
|
* ',': -0.091796875,
|
|
* ":": -0.037109375,
|
|
* ";": -0.037109375,
|
|
* "A": -0.07421875
|
|
* }
|
|
* }
|
|
* var textpath = new Konva.TextPath({
|
|
* x: 100,
|
|
* y: 50,
|
|
* fill: '#333',
|
|
* fontSize: '24',
|
|
* fontFamily: 'Arial',
|
|
* text: 'All the world\'s a stage, and all the men and women merely players.',
|
|
* data: 'M10,10 C0,0 10,150 100,100 S300,150 400,50',
|
|
* kerningFunc(leftChar, rightChar) {
|
|
* return kerningPairs.hasOwnProperty(leftChar) ? pairs[leftChar][rightChar] || 0 : 0
|
|
* }
|
|
* });
|
|
*/
|
|
export class TextPath extends Shape<TextPathConfig> {
|
|
dummyCanvas = Util.createCanvasElement();
|
|
dataArray = [];
|
|
path: SVGPathElement | undefined;
|
|
glyphInfo: Array<{
|
|
transposeX: number;
|
|
transposeY: number;
|
|
text: string;
|
|
rotation: number;
|
|
p0: Vector2d;
|
|
p1: Vector2d;
|
|
}>;
|
|
partialText: string;
|
|
pathLength: number;
|
|
textWidth: number;
|
|
textHeight: number;
|
|
|
|
constructor(config?: TextPathConfig) {
|
|
// call super constructor
|
|
super(config);
|
|
|
|
this._readDataAttribute();
|
|
|
|
this.on('dataChange.konva', function () {
|
|
this._readDataAttribute();
|
|
this._setTextData();
|
|
});
|
|
|
|
// update text data for certain attr changes
|
|
this.on(
|
|
'textChange.konva alignChange.konva letterSpacingChange.konva kerningFuncChange.konva fontSizeChange.konva fontFamilyChange.konva',
|
|
this._setTextData
|
|
);
|
|
|
|
this._setTextData();
|
|
}
|
|
|
|
_getTextPathLength() {
|
|
// defines the length of the path
|
|
// if possible use native browser method, otherwise use KonvaJS implementation
|
|
if (typeof window !== 'undefined' && this.attrs.data) {
|
|
try {
|
|
if (!this.path) {
|
|
this.path = document.createElementNS(
|
|
'http://www.w3.org/2000/svg',
|
|
'path'
|
|
) as SVGPathElement;
|
|
this.path.setAttribute('d', this.attrs.data);
|
|
}
|
|
return this.path.getTotalLength();
|
|
} catch (e) {
|
|
console.warn(e);
|
|
return Path.getPathLength(this.dataArray);
|
|
}
|
|
}
|
|
|
|
return Path.getPathLength(this.dataArray);
|
|
}
|
|
_getPointAtLength(length: number) {
|
|
// if path is not defined yet, do nothing
|
|
if (!this.attrs.data) {
|
|
return null;
|
|
}
|
|
|
|
// if possible use native browser method, otherwise use KonvaJS implementation
|
|
if (typeof window !== 'undefined' && this.attrs.data && this.path) {
|
|
try {
|
|
return this.path.getPointAtLength(length);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
// try using KonvaJS implementation as a backup
|
|
return Path.getPointAtLengthOfDataArray(length, this.dataArray);
|
|
}
|
|
} else {
|
|
return Path.getPointAtLengthOfDataArray(length, this.dataArray);
|
|
}
|
|
}
|
|
|
|
_readDataAttribute() {
|
|
this.dataArray = Path.parsePathData(this.attrs.data);
|
|
this.path = undefined;
|
|
this.pathLength = this._getTextPathLength();
|
|
}
|
|
|
|
_sceneFunc(context: 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();
|
|
|
|
//// To assist with debugging visually, uncomment following
|
|
//
|
|
// if (i % 2) context.strokeStyle = 'cyan';
|
|
// else context.strokeStyle = 'green';
|
|
// var p1 = glyphInfo[i].p1;
|
|
// context.moveTo(p0.x, p0.y);
|
|
// context.lineTo(p1.x, p1.y);
|
|
// context.stroke();
|
|
}
|
|
if (textDecoration === 'underline') {
|
|
context.strokeStyle = fill;
|
|
context.lineWidth = fontSize / 20;
|
|
context.stroke();
|
|
}
|
|
|
|
context.restore();
|
|
}
|
|
_hitFunc(context: 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();
|
|
}
|
|
/**
|
|
* get text width in pixels
|
|
* @method
|
|
* @name Konva.TextPath#getTextWidth
|
|
*/
|
|
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: string) {
|
|
return Text.prototype.setText.call(this, text);
|
|
}
|
|
|
|
_getContextFont() {
|
|
return Text.prototype._getContextFont.call(this);
|
|
}
|
|
|
|
_getTextSize(text: string) {
|
|
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.fontSize()}`, 10),
|
|
};
|
|
}
|
|
_setTextData() {
|
|
const { width, height } = this._getTextSize(this.attrs.text);
|
|
this.textWidth = width;
|
|
this.textHeight = height;
|
|
this.glyphInfo = [];
|
|
|
|
if (!this.attrs.data) {
|
|
return null;
|
|
}
|
|
|
|
const letterSpacing = this.letterSpacing();
|
|
const align = this.align();
|
|
const kerningFunc = this.kerningFunc();
|
|
|
|
// defines the width of the text on a straight line
|
|
const textWidth = Math.max(
|
|
this.textWidth + ((this.attrs.text || '').length - 1) * letterSpacing,
|
|
0
|
|
);
|
|
|
|
let offset = 0;
|
|
if (align === 'center') {
|
|
offset = Math.max(0, this.pathLength / 2 - textWidth / 2);
|
|
}
|
|
if (align === 'right') {
|
|
offset = Math.max(0, this.pathLength - textWidth);
|
|
}
|
|
|
|
const charArr = stringToArray(this.text());
|
|
|
|
// Algorithm for calculating glyph positions:
|
|
// 1. Get the begging point of the glyph on the path using the offsetToGlyph,
|
|
// 2. Get the ending point of the glyph on the path using the offsetToGlyph plus glyph width,
|
|
// 3. Calculate the rotation, width, and midpoint of the glyph using the start and end points,
|
|
// 4. Add glyph width to the offsetToGlyph and repeat
|
|
let offsetToGlyph = offset;
|
|
for (var i = 0; i < charArr.length; i++) {
|
|
const charStartPoint = this._getPointAtLength(offsetToGlyph);
|
|
if (!charStartPoint) return;
|
|
|
|
let glyphWidth = this._getTextSize(charArr[i]).width + letterSpacing;
|
|
if (charArr[i] === ' ' && align === 'justify') {
|
|
const numberOfSpaces = this.text().split(' ').length - 1;
|
|
glyphWidth += (this.pathLength - textWidth) / numberOfSpaces;
|
|
}
|
|
|
|
const charEndPoint = this._getPointAtLength(offsetToGlyph + glyphWidth);
|
|
|
|
const width = Path.getLineLength(
|
|
charStartPoint.x,
|
|
charStartPoint.y,
|
|
charEndPoint.x,
|
|
charEndPoint.y
|
|
);
|
|
|
|
let kern = 0;
|
|
if (kerningFunc) {
|
|
try {
|
|
// getKerning is a user provided getter. Make sure it never breaks our logic
|
|
kern = kerningFunc(charArr[i - 1], charArr[i]) * this.fontSize();
|
|
} catch (e) {
|
|
kern = 0;
|
|
}
|
|
}
|
|
|
|
charStartPoint.x += kern;
|
|
charEndPoint.x += kern;
|
|
this.textWidth += kern;
|
|
|
|
const midpoint = Path.getPointOnLine(
|
|
kern + width / 2.0,
|
|
charStartPoint.x,
|
|
charStartPoint.y,
|
|
charEndPoint.x,
|
|
charEndPoint.y
|
|
);
|
|
|
|
const rotation = Math.atan2(
|
|
charEndPoint.y - charStartPoint.y,
|
|
charEndPoint.x - charStartPoint.x
|
|
);
|
|
this.glyphInfo.push({
|
|
transposeX: midpoint.x,
|
|
transposeY: midpoint.y,
|
|
text: charArr[i],
|
|
rotation: rotation,
|
|
p0: charStartPoint,
|
|
p1: charEndPoint,
|
|
});
|
|
|
|
offsetToGlyph += glyphWidth;
|
|
}
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
destroy(): this {
|
|
Util.releaseCanvas(this.dummyCanvas);
|
|
return super.destroy();
|
|
}
|
|
|
|
fontFamily: GetSet<string, this>;
|
|
fontSize: GetSet<number, this>;
|
|
fontStyle: GetSet<string, this>;
|
|
fontVariant: GetSet<string, this>;
|
|
align: GetSet<string, this>;
|
|
letterSpacing: GetSet<number, this>;
|
|
text: GetSet<string, this>;
|
|
data: GetSet<string, this>;
|
|
|
|
kerningFunc: GetSet<(leftChar: string, rightChar: string) => number, this>;
|
|
textBaseline: GetSet<string, this>;
|
|
textDecoration: GetSet<string, this>;
|
|
}
|
|
|
|
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);
|
|
|
|
/**
|
|
* get/set SVG path data string. This method
|
|
* also automatically parses the data string
|
|
* into a data array. Currently supported SVG data:
|
|
* M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z
|
|
* @name Konva.TextPath#data
|
|
* @method
|
|
* @param {String} data svg path string
|
|
* @returns {String}
|
|
* @example
|
|
* // get data
|
|
* var data = shape.data();
|
|
*
|
|
* // set data
|
|
* shape.data('M200,100h100v50z');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'data');
|
|
|
|
/**
|
|
* get/set font family
|
|
* @name Konva.TextPath#fontFamily
|
|
* @method
|
|
* @param {String} fontFamily
|
|
* @returns {String}
|
|
* @example
|
|
* // get font family
|
|
* var fontFamily = shape.fontFamily();
|
|
*
|
|
* // set font family
|
|
* shape.fontFamily('Arial');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'fontFamily', 'Arial');
|
|
|
|
/**
|
|
* get/set font size in pixels
|
|
* @name Konva.TextPath#fontSize
|
|
* @method
|
|
* @param {Number} fontSize
|
|
* @returns {Number}
|
|
* @example
|
|
* // get font size
|
|
* var fontSize = shape.fontSize();
|
|
*
|
|
* // set font size to 22px
|
|
* shape.fontSize(22);
|
|
*/
|
|
|
|
Factory.addGetterSetter(TextPath, 'fontSize', 12, getNumberValidator());
|
|
|
|
/**
|
|
* get/set font style. Can be 'normal', 'italic', or 'bold'. 'normal' is the default.
|
|
* @name Konva.TextPath#fontStyle
|
|
* @method
|
|
* @param {String} fontStyle
|
|
* @returns {String}
|
|
* @example
|
|
* // get font style
|
|
* var fontStyle = shape.fontStyle();
|
|
*
|
|
* // set font style
|
|
* shape.fontStyle('bold');
|
|
*/
|
|
|
|
Factory.addGetterSetter(TextPath, 'fontStyle', NORMAL);
|
|
|
|
/**
|
|
* get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify'
|
|
* @name Konva.TextPath#align
|
|
* @method
|
|
* @param {String} align
|
|
* @returns {String}
|
|
* @example
|
|
* // get text align
|
|
* var align = text.align();
|
|
*
|
|
* // center text
|
|
* text.align('center');
|
|
*
|
|
* // align text to right
|
|
* text.align('right');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'align', 'left');
|
|
|
|
/**
|
|
* get/set letter spacing. The default is 0.
|
|
* @name Konva.TextPath#letterSpacing
|
|
* @method
|
|
* @param {Number} letterSpacing
|
|
* @returns {Number}
|
|
* @example
|
|
* // get letter spacing value
|
|
* var letterSpacing = shape.letterSpacing();
|
|
*
|
|
* // set the letter spacing value
|
|
* shape.letterSpacing(2);
|
|
*/
|
|
|
|
Factory.addGetterSetter(TextPath, 'letterSpacing', 0, getNumberValidator());
|
|
|
|
/**
|
|
* get/set text baseline. The default is 'middle'. Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'
|
|
* @name Konva.TextPath#textBaseline
|
|
* @method
|
|
* @param {String} textBaseline
|
|
* @returns {String}
|
|
* @example
|
|
* // get current text baseline
|
|
* var textBaseline = shape.textBaseline();
|
|
*
|
|
* // set new text baseline
|
|
* shape.textBaseline('top');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'textBaseline', 'middle');
|
|
|
|
/**
|
|
* get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default.
|
|
* @name Konva.TextPath#fontVariant
|
|
* @method
|
|
* @param {String} fontVariant
|
|
* @returns {String}
|
|
* @example
|
|
* // get font variant
|
|
* var fontVariant = shape.fontVariant();
|
|
*
|
|
* // set font variant
|
|
* shape.fontVariant('small-caps');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'fontVariant', NORMAL);
|
|
|
|
/**
|
|
* get/set text
|
|
* @name Konva.TextPath#getText
|
|
* @method
|
|
* @param {String} text
|
|
* @returns {String}
|
|
* @example
|
|
* // get text
|
|
* var text = text.text();
|
|
*
|
|
* // set text
|
|
* text.text('Hello world!');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING);
|
|
|
|
/**
|
|
* get/set text decoration of a text. Can be '' or 'underline'.
|
|
* @name Konva.TextPath#textDecoration
|
|
* @method
|
|
* @param {String} textDecoration
|
|
* @returns {String}
|
|
* @example
|
|
* // get text decoration
|
|
* var textDecoration = shape.textDecoration();
|
|
*
|
|
* // underline text
|
|
* shape.textDecoration('underline');
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'textDecoration', null);
|
|
|
|
/**
|
|
* get/set kerning function.
|
|
* @name Konva.TextPath#kerningFunc
|
|
* @method
|
|
* @param {String} kerningFunc
|
|
* @returns {String}
|
|
* @example
|
|
* // get text decoration
|
|
* var kerningFunc = text.kerningFunc();
|
|
*
|
|
* // center text
|
|
* text.kerningFunc(function(leftChar, rightChar) {
|
|
* return 1;
|
|
* });
|
|
*/
|
|
Factory.addGetterSetter(TextPath, 'kerningFunc', null);
|