konva
Version:
HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.
942 lines (941 loc) • 38.2 kB
JavaScript
import { Util } from "../Util.js";
import { Factory } from "../Factory.js";
import { Shape } from "../Shape.js";
import { Konva } from "../Global.js";
import { getNumberValidator, getStringValidator, getNumberOrAutoValidator, getBooleanValidator, } from "../Validators.js";
import { _registerNode } from "../Global.js";
// grapheme segmentation: one entry per user-perceived character, so flags,
// ZWJ emoji sequences and combining marks stay together
const segmenter = typeof Intl !== 'undefined' && Intl.Segmenter
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
: null;
// canvas has no API for the underline thickness of a font. This is close to
// what browsers draw for the common fonts
export function getDecorationLineWidth(fontSize) {
return fontSize / 15;
}
export function stringToArray(string) {
if (!segmenter) {
// no Intl.Segmenter (Firefox < 125): flags, then a code point with its
// combining marks, skin tone modifiers, variation selectors and ZWJ joins
return (string.match(/\p{RI}\p{RI}|\P{M}(?:\p{M}|\p{Emoji_Modifier}|\uFE0F|\u200D\P{M})*|\p{M}+/gu) || []);
}
return Array.from(segmenter.segment(string), (s) => s.segment);
}
// constants
const AUTO = 'auto',
//CANVAS = 'canvas',
CENTER = 'center', INHERIT = 'inherit', JUSTIFY = 'justify', CHANGE_KONVA = 'Change.konva', CONTEXT_2D = '2d', DASH = '-', LEFT = 'left', TEXT = 'text', TEXT_UPPER = 'Text', TOP = 'top', BOTTOM = 'bottom', MIDDLE = 'middle', NORMAL = 'normal', PX_SPACE = 'px ', SPACE = ' ', RIGHT = 'right', RTL = 'rtl', WORD = 'word', CHAR = 'char', NONE = 'none', ELLIPSIS = '…', ATTR_CHANGE_LIST = [
'direction',
'fontFamily',
'fontSize',
'fontStyle',
'fontVariant',
'padding',
'align',
'verticalAlign',
'lineHeight',
'text',
'width',
'height',
'wrap',
'ellipsis',
'letterSpacing',
];
// Safari on macOS renders text shadow even when globalAlpha is 0.
// This feature check detects that bug so we can work around it with a buffer canvas.
// Not covered by automated tests (our CI doesn't run Safari).
// Once Safari fixes the issue this check can be removed.
let _shadowOpacityBuggy = null;
function hasShadowOpacityBug() {
if (_shadowOpacityBuggy !== null) {
return _shadowOpacityBuggy;
}
_shadowOpacityBuggy = false;
let c;
try {
c = Util.createCanvasElement();
c.width = 10;
c.height = 10;
const ctx = c.getContext(CONTEXT_2D);
if (ctx) {
ctx.globalAlpha = 0;
ctx.shadowColor = 'black';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = 'black';
ctx.font = '10px Arial';
ctx.fillText('X', 0, 10);
const data = ctx.getImageData(0, 0, 10, 10).data;
for (let i = 3; i < data.length; i += 4) {
if (data[i] > 0) {
_shadowOpacityBuggy = true;
break;
}
}
}
}
catch (e) {
// no-op (SSR or restricted canvas)
}
finally {
if (c) {
Util.releaseCanvas(c);
}
}
return _shadowOpacityBuggy;
}
function normalizeFontFamily(fontFamily) {
return fontFamily
.split(',')
.map((family) => {
family = family.trim();
const hasSpace = family.indexOf(' ') >= 0;
const hasQuotes = family.indexOf('"') >= 0 || family.indexOf("'") >= 0;
if (hasSpace && !hasQuotes) {
family = `"${family}"`;
}
return family;
})
.join(', ');
}
let dummyContext;
export function getDummyContext() {
if (dummyContext) {
return dummyContext;
}
dummyContext = Util.createCanvasElement().getContext(CONTEXT_2D);
return dummyContext;
}
function _fillFunc(context) {
// re-apply a per-character style set by charRenderFunc (see _sceneFunc)
if (this._partialFillStyle) {
context.setAttr('fillStyle', this._partialFillStyle);
}
context.fillText(this._partialText, this._partialTextX, this._partialTextY);
}
function _strokeFunc(context) {
context.setAttr('miterLimit', 2);
if (this._partialStrokeStyle) {
context.setAttr('strokeStyle', this._partialStrokeStyle);
}
context.strokeText(this._partialText, this._partialTextX, this._partialTextY);
}
function checkDefaultFill(config) {
config = config || {};
// set default color to black, without mutating the caller's config
const hasFill = config.fill ||
config.fillLinearGradientColorStops ||
config.fillRadialGradientColorStops ||
config.fillPatternImage;
return hasFill ? config : { ...config, fill: 'black' };
}
/**
* Text constructor
* @constructor
* @memberof Konva
* @augments Konva.Shape
* @param {Object} config
* @param {String} [config.direction] default is inherit
* @param {String} [config.fontFamily] default is Arial
* @param {Number} [config.fontSize] in pixels. 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.textDecoration] can be line-through, underline or empty string. Default is empty string.
* @param {Number} [config.underlineOffset] offset for underline line. Default is calculated based on font size.
* @param {String} config.text
* @param {String} [config.align] can be left, center, right or justify
* @param {String} [config.verticalAlign] can be top, middle or bottom
* @param {Number} [config.padding]
* @param {Number} [config.lineHeight] default is 1
* @param {String} [config.wrap] can be "word", "char", or "none". Default is word
* @param {Boolean} [config.ellipsis] can be true or false. Default is false. If true, text that does not fit is cut and ends with "…". That needs a fixed height or wrap="none"
* @@shapeParams
* @@nodeParams
* @example
* var text = new Konva.Text({
* x: 10,
* y: 15,
* text: 'Simple Text',
* fontSize: 30,
* fontFamily: 'Calibri',
* fill: 'green'
* });
*/
export class Text extends Shape {
constructor(config) {
super(checkDefaultFill(config));
this._partialTextX = 0;
this._partialTextY = 0;
// distance from the line box center to the baseline, see _setTextData
this._baselineShift = 0;
this._setTextData();
}
_sceneFunc(context) {
var _a, _b;
const textArr = this.textArr, textArrLen = textArr.length;
if (!this.text()) {
return;
}
let padding = this.padding(), fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, verticalAlign = this.verticalAlign(), direction = this.direction(), alignY = 0, align = this.align(), totalWidth = this.getWidth(), letterSpacing = this.letterSpacing(), charRenderFunc = this.charRenderFunc(), textDecoration = this.textDecoration(), shouldUnderline = textDecoration.indexOf('underline') !== -1, shouldLineThrough = textDecoration.indexOf('line-through') !== -1, n;
let translateY = lineHeightPx / 2;
let baseline = MIDDLE;
if (!Konva.legacyTextRendering) {
const metrics = this.measureSize('M'); // Use a sample character to get the ascent
baseline = 'alphabetic';
const ascent = (_a = metrics.fontBoundingBoxAscent) !== null && _a !== void 0 ? _a : metrics.actualBoundingBoxAscent;
const descent = (_b = metrics.fontBoundingBoxDescent) !== null && _b !== void 0 ? _b : metrics.actualBoundingBoxDescent;
translateY = (ascent - descent) / 2 + lineHeightPx / 2;
}
if (direction !== INHERIT) {
context.setAttr('direction', direction);
}
else {
// 'inherit' resolves to the direction the canvas already has, so that
// an inherited rtl still takes the single native run below
direction = context.direction;
}
context.setAttr('font', this._getContextFont());
context.setAttr('textBaseline', baseline);
context.setAttr('textAlign', LEFT);
// handle vertical alignment
if (verticalAlign === MIDDLE) {
alignY = (this.getHeight() - textArrLen * lineHeightPx - padding * 2) / 2;
}
else if (verticalAlign === BOTTOM) {
alignY = this.getHeight() - textArrLen * lineHeightPx - padding * 2;
}
context.translate(padding, alignY + padding);
// Start the callback with the same fill that normal drawing would use.
// Assigning that color again then needs no override, including black.
if (charRenderFunc) {
const style = context._getFillStyle(this);
if (style !== undefined)
context.fillStyle = style;
if (this.hasStroke())
context._applyStrokeStyle(this);
}
// context styles entering the loop, used to detect styles set by
// charRenderFunc; every character is drawn inside save()/restore(),
// so the entry state is the same for each one
const fillStyleBefore = charRenderFunc ? context.fillStyle : undefined;
const strokeStyleBefore = charRenderFunc ? context.strokeStyle : undefined;
// global grapheme index passed to charRenderFunc, runs across lines
let charIndex = 0;
// draw text lines
for (n = 0; n < textArrLen; n++) {
let lineTranslateX = 0;
let lineTranslateY = 0;
const obj = textArr[n], text = obj.text, width = obj.width, lastLine = obj.lastInParagraph;
// horizontal alignment
context.save();
if (align === RIGHT) {
lineTranslateX += totalWidth - width - padding * 2;
}
else if (align === CENTER) {
lineTranslateX += (totalWidth - width - padding * 2) / 2;
}
if (shouldUnderline) {
context.save();
context.beginPath();
const yOffset = this._getUnderlineOffset();
const x = lineTranslateX;
const y = translateY + lineTranslateY + yOffset;
context.moveTo(x, y);
const lineWidth = align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width;
context.lineTo(x + Math.round(lineWidth), y);
context.lineWidth = getDecorationLineWidth(fontSize);
context.strokeStyle = context._getFillStyle(this);
context.stroke();
context.restore();
}
// store the starting x position for line-through which is drawn after text
const lineThroughStartX = lineTranslateX;
// Render graphemes separately for spacing, justification and callbacks.
// Keep RTL text in one native run to preserve shaping.
if (direction !== RTL &&
(letterSpacing !== 0 || align === JUSTIFY || charRenderFunc)) {
const spacesNumber = text.split(' ').length - 1;
const array = stringToArray(text);
for (let li = 0; li < array.length; li++) {
const letter = array[li];
// skip justify for the last line
if (letter === ' ' && !lastLine && align === JUSTIFY) {
lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber;
}
this._partialTextX = lineTranslateX;
this._partialTextY = translateY + lineTranslateY;
this._partialText = letter;
// the font of the shape is already set on the context
const letterWidth = context.measureText(letter).width;
if (charRenderFunc) {
context.save();
charRenderFunc({
char: letter,
index: charIndex,
x: lineTranslateX,
y: translateY + lineTranslateY,
lineIndex: n,
column: li,
isLastInLine: li === array.length - 1,
width: letterWidth,
context,
});
// a style the callback set on the context wins for this character;
// fillStrokeShape() overwrites the context styles from the shape
// attributes, so stash them for _fillFunc/_strokeFunc to re-apply
const fillStyleAfter = context.fillStyle;
if (fillStyleAfter !== fillStyleBefore) {
this._partialFillStyle = fillStyleAfter;
}
const strokeStyleAfter = context.strokeStyle;
if (strokeStyleAfter !== strokeStyleBefore) {
this._partialStrokeStyle = strokeStyleAfter;
}
}
context.fillStrokeShape(this);
if (charRenderFunc) {
this._partialFillStyle = undefined;
this._partialStrokeStyle = undefined;
context.restore();
}
lineTranslateX += letterWidth + letterSpacing;
charIndex++;
}
}
else {
if (letterSpacing !== 0) {
context.setAttr('letterSpacing', `${letterSpacing}px`);
}
this._partialTextX = lineTranslateX;
this._partialTextY = translateY + lineTranslateY;
this._partialText = text;
context.fillStrokeShape(this);
}
// draw line-through above the text content
if (shouldLineThrough) {
context.save();
context.beginPath();
const yOffset = !Konva.legacyTextRendering
? -Math.round(fontSize / 4)
: 0;
const x = lineThroughStartX;
context.moveTo(x, translateY + lineTranslateY + yOffset);
const lineWidth = align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width;
context.lineTo(x + Math.round(lineWidth), translateY + lineTranslateY + yOffset);
context.lineWidth = getDecorationLineWidth(fontSize);
context.strokeStyle = context._getFillStyle(this);
context.stroke();
context.restore();
}
context.restore();
if (textArrLen > 1) {
translateY += lineHeightPx;
}
}
}
_getUnderlineOffset() {
var _a;
return ((_a = this.underlineOffset()) !== null && _a !== void 0 ? _a : Math.round(this.fontSize() / (!Konva.legacyTextRendering ? 4 : 2)));
}
getSelfRect() {
const rect = super.getSelfRect();
const lines = this.textArr.length;
if (!lines || this.textDecoration().indexOf('underline') === -1) {
return rect;
}
// the underline of the last line is drawn below the text block
const fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, padding = this.padding(), verticalAlign = this.verticalAlign(), blockHeight = lines * lineHeightPx + padding * 2;
let bottom = lines * lineHeightPx - lineHeightPx / 2 + padding;
if (!Konva.legacyTextRendering) {
bottom += this._baselineShift;
}
if (verticalAlign === MIDDLE) {
bottom += (rect.height - blockHeight) / 2;
}
else if (verticalAlign === BOTTOM) {
bottom += rect.height - blockHeight;
}
bottom += this._getUnderlineOffset() + getDecorationLineWidth(fontSize) / 2;
rect.height = Math.max(rect.height, bottom);
return rect;
}
_hitFunc(context) {
const width = this.getWidth(), height = this.getHeight();
context.beginPath();
context.rect(0, 0, width, height);
context.closePath();
context.fillStrokeShape(this);
}
setText(text) {
const str = Util._isString(text)
? text
: text === null || text === undefined
? ''
: text + '';
this._setAttr(TEXT, str);
return this;
}
getWidth() {
const isAuto = this.attrs.width === AUTO || this.attrs.width === undefined;
return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width;
}
getHeight() {
const isAuto = this.attrs.height === AUTO || this.attrs.height === undefined;
return isAuto
? this.fontSize() * this.textArr.length * this.lineHeight() +
this.padding() * 2
: this.attrs.height;
}
/**
* get pure text width without padding
* @method
* @name Konva.Text#getTextWidth
* @returns {Number}
*/
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;
}
/**
* measure string with the font of current text shape.
* That method can't handle multiline text.
* @method
* @name Konva.Text#measureSize
* @param {String} text text to measure
* @returns {Object} { width , height } of measured text
*/
measureSize(text) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
let _context = getDummyContext(), fontSize = this.fontSize(), metrics;
_context.save();
_context.font = this._getContextFont();
metrics = _context.measureText(text);
_context.restore();
// Scale the fallback values based on the provided fontSize compared to the sample size (100 in your new case)
const scaleFactor = fontSize / 100;
// Note, fallback values are from chrome browser with 100px font size and font-family "Arial"
return {
actualBoundingBoxAscent: (_a = metrics.actualBoundingBoxAscent) !== null && _a !== void 0 ? _a : 71.58203125 * scaleFactor,
actualBoundingBoxDescent: (_b = metrics.actualBoundingBoxDescent) !== null && _b !== void 0 ? _b : 0, // Remains zero as there is no descent in the provided metrics
actualBoundingBoxLeft: (_c = metrics.actualBoundingBoxLeft) !== null && _c !== void 0 ? _c : -7.421875 * scaleFactor,
actualBoundingBoxRight: (_d = metrics.actualBoundingBoxRight) !== null && _d !== void 0 ? _d : 75.732421875 * scaleFactor,
alphabeticBaseline: (_e = metrics.alphabeticBaseline) !== null && _e !== void 0 ? _e : 0, // Remains zero as it's typically relative to the baseline itself
emHeightAscent: (_f = metrics.emHeightAscent) !== null && _f !== void 0 ? _f : 100 * scaleFactor,
emHeightDescent: (_g = metrics.emHeightDescent) !== null && _g !== void 0 ? _g : -20 * scaleFactor,
fontBoundingBoxAscent: (_h = metrics.fontBoundingBoxAscent) !== null && _h !== void 0 ? _h : 91 * scaleFactor,
fontBoundingBoxDescent: (_j = metrics.fontBoundingBoxDescent) !== null && _j !== void 0 ? _j : 21 * scaleFactor,
hangingBaseline: (_k = metrics.hangingBaseline) !== null && _k !== void 0 ? _k : 72.80000305175781 * scaleFactor,
ideographicBaseline: (_l = metrics.ideographicBaseline) !== null && _l !== void 0 ? _l : -21 * scaleFactor,
width: metrics.width,
height: fontSize, // Typically set to the font size
};
}
_getContextFont() {
return (this.fontStyle() +
SPACE +
this.fontVariant() +
SPACE +
(this.fontSize() + PX_SPACE) +
// wrap font family into " so font families with spaces works ok
normalizeFontFamily(this.fontFamily()));
}
_addTextLine(line) {
const align = this.align();
if (align === JUSTIFY) {
line = line.trim();
}
const width = this._getTextWidth(line);
return this.textArr.push({
text: line,
width: width,
lastInParagraph: false,
});
}
_getTextWidth(text, graphemes) {
const letterSpacing = this.letterSpacing();
// one spacing per grapheme, as the per-character rendering draws them
const spacing = letterSpacing
? letterSpacing * (graphemes !== null && graphemes !== void 0 ? graphemes : stringToArray(text).length)
: 0;
return getDummyContext().measureText(text).width + spacing;
}
_setTextData() {
let lines = this.text().split('\n'), fontSize = +this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, width = this.attrs.width, height = this.attrs.height, fixedWidth = width !== AUTO && width !== undefined, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxWidth = width - padding * 2, maxHeightPx = height - padding * 2, currentHeightPx = 0, wrap = this.wrap(),
// align = this.align(),
shouldWrap = wrap !== NONE, wrapAtWord = wrap !== CHAR && shouldWrap, shouldAddEllipsis = this.ellipsis();
// measured here so getSelfRect, which runs per drag frame, needs no
// measureText of its own
const sample = this.measureSize('M');
this._baselineShift =
(sample.fontBoundingBoxAscent - sample.fontBoundingBoxDescent) / 2;
this.textArr = [];
const dummyContext = getDummyContext();
dummyContext.font = this._getContextFont();
// the per-character draw path (same condition as in _sceneFunc) advances
// by unkerned glyph widths, so measure the line the same way
dummyContext.fontKerning =
this.direction() !== RTL &&
(this.letterSpacing() !== 0 ||
this.align() === JUSTIFY ||
!!this.charRenderFunc())
? 'none'
: 'auto';
const additionalWidth = shouldAddEllipsis
? this._getTextWidth(ELLIPSIS)
: 0;
for (let i = 0, max = lines.length; i < max; ++i) {
let line = lines[i];
let lineWidth = this._getTextWidth(line);
if (fixedWidth && lineWidth > maxWidth) {
/*
* if width is fixed and line does not fit entirely
* break the line into multiple fitting lines
*/
const graphemes = stringToArray(line);
const length = graphemes.length;
let start = 0;
const text = (end) => graphemes.slice(start, end).join('');
const isBreak = (char) => char === SPACE || char === DASH;
// graphemes per line, estimated from the average grapheme width
const perLine = Math.max(1, Math.ceil((length * maxWidth) / lineWidth));
while (start < length) {
// only reserve the ellipsis width on a line that may be the last
// visible one
const extraWidth = shouldAddEllipsis &&
fixedHeight &&
currentHeightPx + lineHeightPx > maxHeightPx
? additionalWidth
: 0;
const fits = (end) => {
const width = this._getTextWidth(text(end), end - start);
if (width + extraWidth > maxWidth) {
return false;
}
return true;
};
/*
* find the longest prefix that fits in the specified width:
* grow a window from the estimate until it stops fitting, then
* binary search inside it. Every probe measures about one line,
* not the whole remaining text, so wrapping stays linear
*/
let low = start, high = Math.min(length, start + perLine);
while (fits(high)) {
low = high;
if (high === length) {
break;
}
high = Math.min(length, high + (high - start));
}
while (high - low > 1) {
const mid = (low + high) >>> 1;
if (fits(mid)) {
low = mid;
}
else {
high = mid;
}
}
if (low === start) {
// not even one character could fit in the element, abort
break;
}
if (low === length) {
// the rest of the paragraph fits on one line: kept untrimmed and
// without the ellipsis check, like a paragraph that never wrapped
this._addTextLine(text(length));
currentHeightPx += lineHeightPx;
if (fixedHeight &&
currentHeightPx + lineHeightPx > maxHeightPx &&
i < max - 1) {
this._tryToAddEllipsisToLastLine();
}
break;
}
if (wrapAtWord && !isBreak(graphemes[low])) {
// wrap at the last space or dash of the line instead
let wrapIndex = low - 1;
while (wrapIndex >= start && !isBreak(graphemes[wrapIndex])) {
wrapIndex--;
}
if (wrapIndex >= start) {
low = wrapIndex + 1;
}
}
this._addTextLine(text(low).trimRight());
currentHeightPx += lineHeightPx;
if (this._shouldHandleEllipsis(currentHeightPx)) {
this._tryToAddEllipsisToLastLine();
/*
* stop wrapping if wrapping is disabled or if adding
* one more line would overflow the fixed height
*/
break;
}
// the next line starts after the whitespace of the break
start = low;
while (start < length && !graphemes[start].trim()) {
start++;
}
}
}
else {
// element width is automatically adjusted to max line width
this._addTextLine(line);
currentHeightPx += lineHeightPx;
if (fixedHeight &&
currentHeightPx + lineHeightPx > maxHeightPx &&
i < max - 1) {
this._tryToAddEllipsisToLastLine();
}
}
// if element height is fixed, abort if adding one more line would overflow
if (this.textArr[this.textArr.length - 1]) {
this.textArr[this.textArr.length - 1].lastInParagraph = true;
}
if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) {
break;
}
}
this.textHeight = fontSize;
this.textWidth = this.textArr.reduce((width, line) => Math.max(width, line.width), 0);
}
/**
* whether to handle ellipsis, there are two cases:
* 1. the current line is the last line
* 2. wrap is NONE
* @param {Number} currentHeightPx
* @returns {Boolean}
*/
_shouldHandleEllipsis(currentHeightPx) {
const fontSize = +this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, height = this.attrs.height, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxHeightPx = height - padding * 2, wrap = this.wrap(), shouldWrap = wrap !== NONE;
return (!shouldWrap ||
(fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx));
}
_tryToAddEllipsisToLastLine() {
const width = this.attrs.width, fixedWidth = width !== AUTO && width !== undefined, padding = this.padding(), maxWidth = width - padding * 2, shouldAddEllipsis = this.ellipsis();
const lastLine = this.textArr[this.textArr.length - 1];
if (!lastLine || !shouldAddEllipsis) {
return;
}
let text = lastLine.text + ELLIPSIS;
if (fixedWidth) {
const graphemes = stringToArray(lastLine.text);
while (graphemes.length && this._getTextWidth(text) > maxWidth) {
graphemes.pop();
text = graphemes.join('') + ELLIPSIS;
}
if (this._getTextWidth(text) > maxWidth)
text = '';
}
lastLine.text = text;
lastLine.width = this._getTextWidth(text);
}
// for text we can't disable stroke scaling
// if we do, the result will be unexpected
getStrokeScaleEnabled() {
return true;
}
_useBufferCanvas() {
if (this.attrs.perfectDrawEnabled === false)
return false;
const hasLine = this.textDecoration().indexOf('underline') !== -1 ||
this.textDecoration().indexOf('line-through') !== -1;
const hasShadow = this.hasShadow();
if (hasLine && hasShadow) {
return true;
}
if (hasShadow && this.getAbsoluteOpacity() !== 1 && hasShadowOpacityBug()) {
return true;
}
return super._useBufferCanvas();
}
}
Text.prototype._fillFunc = _fillFunc;
Text.prototype._strokeFunc = _strokeFunc;
Text.prototype.className = TEXT_UPPER;
// the decoration attrs are not in ATTR_CHANGE_LIST: they change the bounds
// through getSelfRect, but need no relayout
Text.prototype._attrsAffectingSize = ATTR_CHANGE_LIST.filter((attr) => attr !== 'width' && attr !== 'height').concat(['textDecoration', 'underlineOffset']);
_registerNode(Text);
// update text data for certain attr changes
Text.prototype.on(ATTR_CHANGE_LIST.map((attr) => attr + CHANGE_KONVA).join(' '), function () {
this._setTextData();
});
/**
* get/set width of text area, which includes padding.
* @name Konva.Text#width
* @method
* @param {Number} width
* @returns {Number}
* @example
* // get width
* var width = text.width();
*
* // set width
* text.width(20);
*
* // set to auto
* text.width('auto');
* text.width() // will return calculated width, and not "auto"
*/
Factory.overWriteSetter(Text, 'width', getNumberOrAutoValidator());
/**
* get/set the height of the text area, which takes into account multi-line text, line heights, and padding.
* @name Konva.Text#height
* @method
* @param {Number} height
* @returns {Number}
* @example
* // get height
* var height = text.height();
*
* // set height
* text.height(20);
*
* // set to auto
* text.height('auto');
* text.height() // will return calculated height, and not "auto"
*/
Factory.overWriteSetter(Text, 'height', getNumberOrAutoValidator());
/**
* get/set direction
* @name Konva.Text#direction
* @method
* @param {String} direction
* @returns {String}
* @example
* // get direction
* var direction = text.direction();
*
* // set direction
* text.direction('rtl');
*/
Factory.addGetterSetter(Text, 'direction', INHERIT);
/**
* get/set font family
* @name Konva.Text#fontFamily
* @method
* @param {String} fontFamily
* @returns {String}
* @example
* // get font family
* var fontFamily = text.fontFamily();
*
* // set font family
* text.fontFamily('Arial');
*/
Factory.addGetterSetter(Text, 'fontFamily', 'Arial');
/**
* get/set font size in pixels
* @name Konva.Text#fontSize
* @method
* @param {Number} fontSize
* @returns {Number}
* @example
* // get font size
* var fontSize = text.fontSize();
*
* // set font size to 22px
* text.fontSize(22);
*/
Factory.addGetterSetter(Text, 'fontSize', 12, getNumberValidator());
/**
* get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default.
* @name Konva.Text#fontStyle
* @method
* @param {String} fontStyle
* @returns {String}
* @example
* // get font style
* var fontStyle = text.fontStyle();
*
* // set font style
* text.fontStyle('bold');
*/
Factory.addGetterSetter(Text, 'fontStyle', NORMAL);
/**
* get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default.
* @name Konva.Text#fontVariant
* @method
* @param {String} fontVariant
* @returns {String}
* @example
* // get font variant
* var fontVariant = text.fontVariant();
*
* // set font variant
* text.fontVariant('small-caps');
*/
Factory.addGetterSetter(Text, 'fontVariant', NORMAL);
/**
* get/set padding
* @name Konva.Text#padding
* @method
* @param {Number} padding
* @returns {Number}
* @example
* // get padding
* var padding = text.padding();
*
* // set padding to 10 pixels
* text.padding(10);
*/
Factory.addGetterSetter(Text, 'padding', 0, getNumberValidator());
/**
* get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify'
* @name Konva.Text#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');
*
* // justify text
*/
Factory.addGetterSetter(Text, 'align', LEFT);
/**
* get/set vertical align of text. Can be 'top', 'middle', 'bottom'.
* @name Konva.Text#verticalAlign
* @method
* @param {String} verticalAlign
* @returns {String}
* @example
* // get text vertical align
* var verticalAlign = text.verticalAlign();
*
* // center text
* text.verticalAlign('middle');
*/
Factory.addGetterSetter(Text, 'verticalAlign', TOP);
/**
* get/set line height. The default is 1.
* @name Konva.Text#lineHeight
* @method
* @param {Number} lineHeight
* @returns {Number}
* @example
* // get line height
* var lineHeight = text.lineHeight();
*
* // set the line height
* text.lineHeight(2);
*/
Factory.addGetterSetter(Text, 'lineHeight', 1, getNumberValidator());
/**
* get/set wrap. Can be "word", "char", or "none". Default is "word".
* In "word" wrapping any word still can be wrapped if it can't be placed in the required width
* without breaks.
* @name Konva.Text#wrap
* @method
* @param {String} wrap
* @returns {String}
* @example
* // get wrap
* var wrap = text.wrap();
*
* // set wrap
* text.wrap('word');
*/
Factory.addGetterSetter(Text, 'wrap', WORD);
/**
* get/set ellipsis. Can be true or false. Default is false. If ellipsis is true,
* Konva cuts the text that does not fit and ends it with "…" (one character).
* That needs a fixed height or wrap set to "none"
* @name Konva.Text#ellipsis
* @method
* @param {Boolean} ellipsis
* @returns {Boolean}
* @example
* // get ellipsis param, returns true or false
* var ellipsis = text.ellipsis();
*
* // set ellipsis
* text.ellipsis(true);
*/
Factory.addGetterSetter(Text, 'ellipsis', false, getBooleanValidator());
/**
* set letter spacing property. Default value is 0.
* @name Konva.Text#letterSpacing
* @method
* @param {Number} letterSpacing
*/
Factory.addGetterSetter(Text, 'letterSpacing', 0, getNumberValidator());
/**
* get/set text
* @name Konva.Text#text
* @method
* @param {String} text
* @returns {String}
* @example
* // get text
* var text = text.text();
*
* // set text
* text.text('Hello world!');
*/
Factory.addGetterSetter(Text, 'text', '', getStringValidator());
/**
* get/set text decoration of a text. Possible values are 'underline', 'line-through' or combination of these values separated by space
* @name Konva.Text#textDecoration
* @method
* @param {String} textDecoration
* @returns {String}
* @example
* // get text decoration
* var textDecoration = text.textDecoration();
*
* // underline text
* text.textDecoration('underline');
*
* // strike text
* text.textDecoration('line-through');
*
* // underline and strike text
* text.textDecoration('underline line-through');
*/
Factory.addGetterSetter(Text, 'textDecoration', '');
/**
* get/set text underline decoration offset. Offset for underline line. Default is calculated based on font size.
* @name Konva.Text#underlineOffset
* @method
* @param {Number} underlineOffset
* @returns {Number}
* @example
* // get underline offset
* var underlineOffset = text.underlineOffset();
*
* // set underline offset
* text.underlineOffset(5);
*/
Factory.addGetterSetter(Text, 'underlineOffset', undefined, getNumberValidator());
/**
* get/set per-character render hook. The callback is invoked for each grapheme before drawing, except for RTL text.
* It can mutate the provided context (e.g. translate, rotate, change styles) and should return void.
* Note: per-character rendering may disable native kerning/ligatures.
* @name Konva.Text#charRenderFunc
* @method
* @param {Function} charRenderFunc
* @returns {Function}
* @example
* // apply small x-translation and a custom color to every second character
* text.charRenderFunc(function(props) {
* if (props.index % 2 === 1) {
* props.context.translate(2, 0);
* props.context.fillStyle = 'red';
* }
* });
*/
Factory.addGetterSetter(Text, 'charRenderFunc', undefined);