first commit
This commit is contained in:
+550
@@ -0,0 +1,550 @@
|
||||
import { Util } from "../Util.js";
|
||||
import { Factory } from "../Factory.js";
|
||||
import { Shape } from "../Shape.js";
|
||||
import { Path } from "./Path.js";
|
||||
import { Text, getDecorationLineWidth, getDummyContext, stringToArray, } from "./Text.js";
|
||||
import { getNumberValidator } from "../Validators.js";
|
||||
import { _registerNode } from "../Global.js";
|
||||
const 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', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
|
||||
* @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 {
|
||||
constructor(config) {
|
||||
// call super constructor
|
||||
super(config);
|
||||
this.dataArray = [];
|
||||
this._readDataAttribute();
|
||||
this._setTextData();
|
||||
}
|
||||
_getTextPathLength() {
|
||||
return Path.getPathLength(this.dataArray);
|
||||
}
|
||||
_getPointAtLength(length, cursor) {
|
||||
// if path is not defined yet, do nothing
|
||||
if (!this.attrs.data) {
|
||||
return null;
|
||||
}
|
||||
const totalLength = this.pathLength;
|
||||
if (length > totalLength) {
|
||||
return null;
|
||||
}
|
||||
return Path.getPointAtLengthOfDataArray(length, this.dataArray, cursor);
|
||||
}
|
||||
_readDataAttribute() {
|
||||
this.dataArray = Path.parsePathData(this.attrs.data);
|
||||
this.pathLength = this._getTextPathLength();
|
||||
}
|
||||
_sceneFunc(context) {
|
||||
context.setAttr('font', this._getContextFont());
|
||||
context.setAttr('textBaseline', this.textBaseline());
|
||||
context.setAttr('textAlign', 'left');
|
||||
context.save();
|
||||
const textDecoration = this.textDecoration();
|
||||
const fill = this.fill();
|
||||
const fontSize = this.fontSize();
|
||||
const glyphInfo = this.glyphInfo;
|
||||
const hasUnderline = textDecoration.indexOf('underline') !== -1;
|
||||
const hasLineThrough = textDecoration.indexOf('line-through') !== -1;
|
||||
if (hasUnderline) {
|
||||
context.beginPath();
|
||||
}
|
||||
for (let i = 0; i < glyphInfo.length; i++) {
|
||||
context.save();
|
||||
const p0 = glyphInfo[i].p0;
|
||||
context.translate(p0.x, p0.y);
|
||||
context.rotate(glyphInfo[i].rotation);
|
||||
this.partialText = glyphInfo[i].text;
|
||||
context.fillStrokeShape(this);
|
||||
if (hasUnderline) {
|
||||
if (i === 0) {
|
||||
context.moveTo(0, fontSize / 2 + 1);
|
||||
}
|
||||
context.lineTo(glyphInfo[i].width, 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 (hasUnderline) {
|
||||
context.strokeStyle = fill;
|
||||
context.lineWidth = getDecorationLineWidth(fontSize);
|
||||
context.stroke();
|
||||
}
|
||||
if (hasLineThrough) {
|
||||
context.beginPath();
|
||||
for (let i = 0; i < glyphInfo.length; i++) {
|
||||
context.save();
|
||||
const p0 = glyphInfo[i].p0;
|
||||
context.translate(p0.x, p0.y);
|
||||
context.rotate(glyphInfo[i].rotation);
|
||||
if (i === 0) {
|
||||
context.moveTo(0, 0);
|
||||
}
|
||||
context.lineTo(glyphInfo[i].width, 0);
|
||||
context.restore();
|
||||
}
|
||||
context.strokeStyle = fill;
|
||||
context.lineWidth = getDecorationLineWidth(fontSize);
|
||||
context.stroke();
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
_hitFunc(context) {
|
||||
context.beginPath();
|
||||
const glyphInfo = this.glyphInfo;
|
||||
if (glyphInfo.length >= 1) {
|
||||
const p0 = glyphInfo[0].p0;
|
||||
context.moveTo(p0.x, p0.y);
|
||||
}
|
||||
for (let i = 0; i < glyphInfo.length; i++) {
|
||||
const 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) {
|
||||
return Text.prototype.setText.call(this, text);
|
||||
}
|
||||
_getContextFont() {
|
||||
return Text.prototype._getContextFont.call(this);
|
||||
}
|
||||
_getTextSize(text) {
|
||||
const _context = getDummyContext();
|
||||
_context.save();
|
||||
_context.font = this._getContextFont();
|
||||
// Text leaves its own kerning mode on the shared dummy context
|
||||
_context.fontKerning = 'auto';
|
||||
const metrics = _context.measureText(text);
|
||||
_context.restore();
|
||||
return {
|
||||
width: metrics.width,
|
||||
height: parseInt(`${this.fontSize()}`, 10),
|
||||
};
|
||||
}
|
||||
_setTextData() {
|
||||
const charArr = stringToArray(this.text());
|
||||
// For RTL scripts (Hebrew, Arabic, …) the path is still walked left to
|
||||
// right by glyph index, so we lay glyphs out in reversed logical order.
|
||||
// A reader scanning the path right-to-left then sees the text in its
|
||||
// intended reading order. This does not perform full Unicode bidi or
|
||||
// Arabic cursive shaping — those need a real shaping engine — but it
|
||||
// makes pure RTL strings render in the right direction.
|
||||
if (this.direction() === 'rtl') {
|
||||
charArr.reverse();
|
||||
}
|
||||
const kerningFunc = this.kerningFunc();
|
||||
const chars = [];
|
||||
let width = 0;
|
||||
for (let i = 0; i < charArr.length; i++) {
|
||||
let kern = 0;
|
||||
if (kerningFunc && i > 0) {
|
||||
try {
|
||||
// kerningFunc is a user provided getter. Make sure it never breaks our logic
|
||||
kern = kerningFunc(charArr[i - 1], charArr[i]) * this.fontSize();
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
chars.push({
|
||||
char: charArr[i],
|
||||
width: this._getTextSize(charArr[i]).width,
|
||||
kern: kern,
|
||||
});
|
||||
width += chars[i].width + kern;
|
||||
}
|
||||
const { width: fullTextWidth, 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 numberOfSpaces = align === 'justify' ? this.text().split(' ').length - 1 : 0;
|
||||
// The sum of individual character widths can exceed the whole-string width
|
||||
// due to kerning (browsers place adjacent glyphs closer together than the
|
||||
// individual measurements suggest). This difference is the maximum amount
|
||||
// by which the accumulated glyph offset can legitimately overshoot the
|
||||
// path length when the path is sized to match the visually-measured text.
|
||||
const kerningAdjustment = Math.max(0, width - fullTextWidth);
|
||||
// defines the width of the text on a straight line
|
||||
const textWidth = Math.max(this.textWidth + (charArr.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);
|
||||
}
|
||||
// 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;
|
||||
// Keep the original boundary selection across gaps between subpaths.
|
||||
const cursor = this.dataArray.some((segment, index) => index > 0 && segment.command === 'M')
|
||||
? undefined
|
||||
: { index: 0, offset: 0 };
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
offsetToGlyph += chars[i].kern;
|
||||
const charStartPoint = this._getPointAtLength(offsetToGlyph, cursor);
|
||||
if (!charStartPoint)
|
||||
return;
|
||||
const char = chars[i].char;
|
||||
let glyphWidth = chars[i].width + (i < chars.length - 1 ? letterSpacing : 0);
|
||||
if (char === ' ' && align === 'justify') {
|
||||
glyphWidth += (this.pathLength - textWidth) / numberOfSpaces;
|
||||
}
|
||||
const charEndLength = offsetToGlyph + glyphWidth;
|
||||
// When the path length is sized to match the visually-measured (kerned)
|
||||
// text width, the accumulated per-glyph offset can exceed pathLength by
|
||||
// up to kerningAdjustment pixels. Clamp to pathLength in that case so
|
||||
// the last character is not silently dropped.
|
||||
const charEndPoint = this._getPointAtLength(charEndLength > this.pathLength &&
|
||||
charEndLength - this.pathLength <= kerningAdjustment
|
||||
? this.pathLength
|
||||
: charEndLength, cursor);
|
||||
if (!charEndPoint) {
|
||||
return;
|
||||
}
|
||||
const width = Path.getLineLength(charStartPoint.x, charStartPoint.y, charEndPoint.x, charEndPoint.y);
|
||||
const rotation = Math.atan2(charEndPoint.y - charStartPoint.y, charEndPoint.x - charStartPoint.x);
|
||||
this.glyphInfo.push({
|
||||
text: charArr[i],
|
||||
rotation: rotation,
|
||||
p0: charStartPoint,
|
||||
p1: charEndPoint,
|
||||
width: width,
|
||||
});
|
||||
offsetToGlyph += glyphWidth;
|
||||
}
|
||||
}
|
||||
getSelfRect() {
|
||||
if (!this.glyphInfo.length) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
const 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);
|
||||
});
|
||||
const rect = Util._getPointsRect(points);
|
||||
const fontSize = this.fontSize();
|
||||
return {
|
||||
x: rect.x - fontSize / 2,
|
||||
y: rect.y - fontSize / 2,
|
||||
width: rect.width + fontSize,
|
||||
height: rect.height + fontSize,
|
||||
};
|
||||
}
|
||||
// Like Text: a glyph stroke is drawn with the text, so it always scales.
|
||||
getStrokeScaleEnabled() {
|
||||
return true;
|
||||
}
|
||||
_getSelfRectForDrawing() {
|
||||
// getSelfRect() pads glyph anchors by half the font size; ascenders,
|
||||
// accents and other baselines reach further from the path.
|
||||
const rect = this.getSelfRect(), pad = this.fontSize();
|
||||
return {
|
||||
x: rect.x - pad,
|
||||
y: rect.y - pad,
|
||||
width: rect.width + pad * 2,
|
||||
height: rect.height + pad * 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
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',
|
||||
'align',
|
||||
'letterSpacing',
|
||||
'kerningFunc',
|
||||
'fontFamily',
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'direction',
|
||||
];
|
||||
_registerNode(TextPath, true);
|
||||
TextPath.prototype.on('dataChange.konva', function () {
|
||||
this._readDataAttribute();
|
||||
this._setTextData();
|
||||
});
|
||||
// update text data for certain attr changes
|
||||
TextPath.prototype.on(TextPath.prototype._attrsAffectingSize
|
||||
.filter((attr) => attr !== 'data')
|
||||
.map((attr) => attr + 'Change.konva')
|
||||
.join(' '), function () {
|
||||
this._setTextData();
|
||||
});
|
||||
/**
|
||||
* 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', '500' or even 'italic 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 '', 'underline', 'line-through', or a combination like 'underline line-through'.
|
||||
* @name Konva.TextPath#textDecoration
|
||||
* @method
|
||||
* @param {String} textDecoration
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get text decoration
|
||||
* var textDecoration = shape.textDecoration();
|
||||
*
|
||||
* // underline text
|
||||
* shape.textDecoration('underline');
|
||||
*
|
||||
* // line-through text
|
||||
* shape.textDecoration('line-through');
|
||||
*
|
||||
* // combine both
|
||||
* shape.textDecoration('underline line-through');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'textDecoration', '');
|
||||
/**
|
||||
* 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', undefined);
|
||||
/**
|
||||
* get/set text direction. Can be 'inherit', 'ltr' or 'rtl'. Default is 'inherit'.
|
||||
*
|
||||
* Setting `'rtl'` reverses the per-glyph layout order along the path, so
|
||||
* Hebrew/Arabic strings read correctly when scanned right-to-left. This is a
|
||||
* pragmatic fix limited by canvas TextPath rendering: each glyph is still
|
||||
* drawn independently, so Arabic cursive joining and full Unicode bidi
|
||||
* reordering of mixed-direction text are not supported. For best results,
|
||||
* pair `direction: 'rtl'` with `align: 'right'` so the text starts at the
|
||||
* end of the path.
|
||||
* @name Konva.TextPath#direction
|
||||
* @method
|
||||
* @param {String} direction
|
||||
* @returns {String}
|
||||
* @example
|
||||
* // get direction
|
||||
* var direction = textpath.direction();
|
||||
*
|
||||
* // set direction
|
||||
* textpath.direction('rtl');
|
||||
*/
|
||||
Factory.addGetterSetter(TextPath, 'direction', 'inherit');
|
||||
Reference in New Issue
Block a user