UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

1,040 lines (1,039 loc) 39 kB
import { _defineProperty } from "../../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs"; import { cache } from "../../cache.mjs"; import { BOTTOM, CENTER, FILL, LEFT, RIGHT } from "../../constants.mjs"; import { classRegistry } from "../../ClassRegistry.mjs"; import { createCanvasElementFor } from "../../util/misc/dom.mjs"; import { isFiller } from "../../util/typeAssertions.mjs"; import { graphemeSplit } from "../../util/lang_string.mjs"; import { normalizeWs } from "../../util/internals/normalizeWhiteSpace.mjs"; import { JUSTIFY, TEXT_DECORATION_THICKNESS, additionalProps, textDefaultValues, textLayoutProperties } from "./constants.mjs"; import { cacheProperties } from "../Object/defaultValues.mjs"; import { applyMixins } from "../../util/applyMixins.mjs"; import { hasStyleChanged, stylesFromArray, stylesToArray } from "../../util/misc/textStyles.mjs"; import { SHARED_ATTRIBUTES } from "../../parser/attributes.mjs"; import { parseAttributes } from "../../parser/parseAttributes.mjs"; import { getPathSegmentsInfo, getPointOnPath } from "../../util/path/index.mjs"; import { StyledText } from "./StyledText.mjs"; import { TextSVGExportMixin } from "./TextSVGExportMixin.mjs"; //#region src/shapes/Text/Text.ts let measuringContext; /** * Return a context for measurement of text string. * if created it gets stored for reuse */ function getMeasuringContext() { if (!measuringContext) measuringContext = createCanvasElementFor({ width: 0, height: 0 }).getContext("2d"); return measuringContext; } /** * Text class * @see {@link http://fabric5.fabricjs.com/fabric-intro-part-2#text} */ var FabricText = class FabricText extends StyledText { static getDefaults() { return { ...super.getDefaults(), ...FabricText.ownDefaults }; } constructor(text, options) { super(); _defineProperty( this, /** * contains characters bounding boxes * This variable is considered to be protected. * But for how mixins are implemented right now, we can't leave it private * @protected */ "__charBounds", [] ); Object.assign(this, FabricText.ownDefaults); this.setOptions(options); if (!this.styles) this.styles = {}; this.text = text; this.initialized = true; if (this.path) this.setPathInfo(); this.initDimensions(); this.setCoords(); } /** * If text has a path, it will add the extra information needed * for path and text calculations */ setPathInfo() { const path = this.path; if (path) path.segmentsInfo = getPathSegmentsInfo(path.path); } /** * @private * Divides text into lines of text and lines of graphemes. */ _splitText() { const newLines = this._splitTextIntoLines(this.text); this.textLines = newLines.lines; this._textLines = newLines.graphemeLines; this._unwrappedTextLines = newLines._unwrappedLines; this._text = newLines.graphemeText; return newLines; } /** * Initialize or update text dimensions. * Updates this.width and this.height with the proper values. * Does not return dimensions. */ initDimensions() { this._splitText(); this._clearCache(); this.dirty = true; if (this.path) { this.width = this.path.width; this.height = this.path.height; } else { this.width = this.calcTextWidth() || this.cursorWidth || this.MIN_TEXT_WIDTH; this.height = this.calcTextHeight(); } if (this.textAlign.includes("justify")) this.enlargeSpaces(); } /** * Enlarge space boxes and shift the others */ enlargeSpaces() { let diffSpace, currentLineWidth, numberOfSpaces, accumulatedSpace, line, charBound, spaces; for (let i = 0, len = this._textLines.length; i < len; i++) { if (this.textAlign !== "justify" && (i === len - 1 || this.isEndOfWrapping(i))) continue; accumulatedSpace = 0; line = this._textLines[i]; currentLineWidth = this.getLineWidth(i); if (currentLineWidth < this.width && (spaces = this.textLines[i].match(this._reSpacesAndTabs))) { numberOfSpaces = spaces.length; diffSpace = (this.width - currentLineWidth) / numberOfSpaces; for (let j = 0; j <= line.length; j++) { charBound = this.__charBounds[i][j]; if (this._reSpaceAndTab.test(line[j])) { charBound.width += diffSpace; charBound.kernedWidth += diffSpace; charBound.left += accumulatedSpace; accumulatedSpace += diffSpace; } else charBound.left += accumulatedSpace; } } } } /** * Detect if the text line is ended with an hard break * text and itext do not have wrapping, return false * @return {Boolean} */ isEndOfWrapping(lineIndex) { return lineIndex === this._textLines.length - 1; } missingNewlineOffset(_lineIndex) { return 1; } /** * Returns 2d representation (lineIndex and charIndex) of cursor * @param {Number} selectionStart * @param {Boolean} [skipWrapping] consider the location for unwrapped lines. useful to manage styles. */ get2DCursorLocation(selectionStart, skipWrapping) { const lines = skipWrapping ? this._unwrappedTextLines : this._textLines; let i; for (i = 0; i < lines.length; i++) { if (selectionStart <= lines[i].length) return { lineIndex: i, charIndex: selectionStart }; selectionStart -= lines[i].length + this.missingNewlineOffset(i, skipWrapping); } return { lineIndex: i - 1, charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart }; } /** * Returns string representation of an instance * @return {String} String representation of text object */ toString() { return `#<Text (${this.complexity()}): { "text": "${this.text}", "fontFamily": "${this.fontFamily}" }>`; } /** * Return the dimension and the zoom level needed to create a cache canvas * big enough to host the object to be cached. * @private * @param {Object} dim.x width of object to be cached * @param {Object} dim.y height of object to be cached * @return {Object}.width width of canvas * @return {Object}.height height of canvas * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache */ _getCacheCanvasDimensions() { const dims = super._getCacheCanvasDimensions(); const fontSize = this.fontSize; dims.width += fontSize * dims.zoomX; dims.height += fontSize * dims.zoomY; return dims; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render(ctx) { const path = this.path; path && !path.isNotVisible() && path._render(ctx); this._setTextStyles(ctx); this._renderTextLinesBackground(ctx); this._renderTextDecoration(ctx, "underline"); this._renderText(ctx); this._renderTextDecoration(ctx, "overline"); this._renderTextDecoration(ctx, "linethrough"); } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderText(ctx) { if (this.paintFirst === "stroke") { this._renderTextStroke(ctx); this._renderTextFill(ctx); } else { this._renderTextFill(ctx); this._renderTextStroke(ctx); } } /** * Set the font parameter of the context with the object properties or with charStyle * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Object} [charStyle] object with font style properties * @param {String} [charStyle.fontFamily] Font Family * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix ) * @param {String} [charStyle.fontWeight] Font weight * @param {String} [charStyle.fontStyle] Font style (italic|normal) */ _setTextStyles(ctx, charStyle, forMeasuring) { ctx.textBaseline = "alphabetic"; if (this.path) switch (this.pathAlign) { case CENTER: ctx.textBaseline = "middle"; break; case "ascender": ctx.textBaseline = "top"; break; case "descender": ctx.textBaseline = BOTTOM; break; } ctx.font = this._getFontDeclaration(charStyle, forMeasuring); } /** * calculate and return the text Width measuring each line. * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @return {Number} Maximum width of Text object */ calcTextWidth() { let maxWidth = this.getLineWidth(0); for (let i = 1, len = this._textLines.length; i < len; i++) { const currentLineWidth = this.getLineWidth(i); if (currentLineWidth > maxWidth) maxWidth = currentLineWidth; } return maxWidth; } /** * @private * @param {String} method Method name ("fillText" or "strokeText") * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line Text to render * @param {Number} left Left position of text * @param {Number} top Top position of text * @param {Number} lineIndex Index of a line in a text */ _renderTextLine(method, ctx, line, left, top, lineIndex) { this._renderChars(method, ctx, line, left, top, lineIndex); } /** * Renders the text background for lines, taking care of style * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextLinesBackground(ctx) { if (!this.textBackgroundColor && !this.styleHas("textBackgroundColor")) return; const originalFill = ctx.fillStyle, leftOffset = this._getLeftOffset(); let lineTopOffset = this._getTopOffset(); for (let i = 0, len = this._textLines.length; i < len; i++) { const heightOfLine = this.getHeightOfLine(i); if (!this.textBackgroundColor && !this.styleHas("textBackgroundColor", i)) { lineTopOffset += heightOfLine; continue; } const jlen = this._textLines[i].length; const lineLeftOffset = this._getLineLeftOffset(i); let boxWidth = 0; let boxStart = 0; let drawStart; let currentColor; let lastColor = this.getValueOfPropertyAt(i, 0, "textBackgroundColor"); const bgHeight = this.getHeightOfLineImpl(i); for (let j = 0; j < jlen; j++) { const charBox = this.__charBounds[i][j]; currentColor = this.getValueOfPropertyAt(i, j, "textBackgroundColor"); if (this.path) { ctx.save(); ctx.translate(charBox.renderLeft, charBox.renderTop); ctx.rotate(charBox.angle); ctx.fillStyle = currentColor; currentColor && ctx.fillRect(-charBox.width / 2, -bgHeight * (1 - this._fontSizeFraction), charBox.width, bgHeight); ctx.restore(); } else if (currentColor !== lastColor) { drawStart = leftOffset + lineLeftOffset + boxStart; if (this.direction === "rtl") drawStart = this.width - drawStart - boxWidth; ctx.fillStyle = lastColor; lastColor && ctx.fillRect(drawStart, lineTopOffset, boxWidth, bgHeight); boxStart = charBox.left; boxWidth = charBox.width; lastColor = currentColor; } else boxWidth += charBox.kernedWidth; } if (currentColor && !this.path) { drawStart = leftOffset + lineLeftOffset + boxStart; if (this.direction === "rtl") drawStart = this.width - drawStart - boxWidth; ctx.fillStyle = currentColor; ctx.fillRect(drawStart, lineTopOffset, boxWidth, bgHeight); } lineTopOffset += heightOfLine; } ctx.fillStyle = originalFill; this._removeShadow(ctx); } /** * measure and return the width of a single character. * possibly overridden to accommodate different measure logic or * to hook some external lib for character measurement * @private * @param {String} _char, char to be measured * @param {Object} charStyle style of char to be measured * @param {String} [previousChar] previous char * @param {Object} [prevCharStyle] style of previous char */ _measureChar(_char, charStyle, previousChar, prevCharStyle) { const fontCache = cache.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle), couple = previousChar ? previousChar + _char : _char, stylesAreEqual = previousChar && fontDeclaration === this._getFontDeclaration(prevCharStyle), fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE; let width, coupleWidth, previousWidth, kernedWidth; if (previousChar && fontCache.has(previousChar)) previousWidth = fontCache.get(previousChar); if (fontCache.has(_char)) kernedWidth = width = fontCache.get(_char); if (stylesAreEqual && fontCache.has(couple)) { coupleWidth = fontCache.get(couple); kernedWidth = coupleWidth - previousWidth; } if (width === void 0 || previousWidth === void 0 || coupleWidth === void 0) { const ctx = getMeasuringContext(); this._setTextStyles(ctx, charStyle, true); if (width === void 0) { kernedWidth = width = ctx.measureText(_char).width; fontCache.set(_char, width); } if (previousWidth === void 0 && stylesAreEqual && previousChar) { previousWidth = ctx.measureText(previousChar).width; fontCache.set(previousChar, previousWidth); } if (stylesAreEqual && coupleWidth === void 0) { coupleWidth = ctx.measureText(couple).width; fontCache.set(couple, coupleWidth); kernedWidth = coupleWidth - previousWidth; } } return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier }; } /** * Computes height of character at given position * @param {Number} line the line index number * @param {Number} _char the character index number * @return {Number} fontSize of the character */ getHeightOfChar(line, _char) { return this.getValueOfPropertyAt(line, _char, "fontSize"); } /** * measure a text line measuring all characters. * @param {Number} lineIndex line number */ measureLine(lineIndex) { const lineInfo = this._measureLine(lineIndex); if (this.charSpacing !== 0) lineInfo.width -= this._getWidthOfCharSpacing(); if (lineInfo.width < 0) lineInfo.width = 0; return lineInfo; } /** * measure every grapheme of a line, populating __charBounds * @param {Number} lineIndex * @return {Object} object.width total width of characters * @return {Object} object.numOfSpaces length of chars that match this._reSpacesAndTabs */ _measureLine(lineIndex) { let width = 0, prevGrapheme, graphemeInfo; const reverse = this.pathSide === RIGHT, path = this.path, line = this._textLines[lineIndex], llength = line.length, lineBounds = new Array(llength); this.__charBounds[lineIndex] = lineBounds; for (let i = 0; i < llength; i++) { const grapheme = line[i]; graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme); lineBounds[i] = graphemeInfo; width += graphemeInfo.kernedWidth; prevGrapheme = grapheme; } lineBounds[llength] = { left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0, width: 0, kernedWidth: 0, height: this.fontSize, deltaY: 0 }; if (path && path.segmentsInfo) { let positionInPath = 0; const totalPathLength = path.segmentsInfo[path.segmentsInfo.length - 1].length; switch (this.textAlign) { case LEFT: positionInPath = reverse ? totalPathLength - width : 0; break; case CENTER: positionInPath = (totalPathLength - width) / 2; break; case RIGHT: positionInPath = reverse ? 0 : totalPathLength - width; break; } positionInPath += this.pathStartOffset * (reverse ? -1 : 1); for (let i = reverse ? llength - 1 : 0; reverse ? i >= 0 : i < llength; reverse ? i-- : i++) { graphemeInfo = lineBounds[i]; if (positionInPath > totalPathLength) positionInPath %= totalPathLength; else if (positionInPath < 0) positionInPath += totalPathLength; this._setGraphemeOnPath(positionInPath, graphemeInfo); positionInPath += graphemeInfo.kernedWidth; } } return { width, numOfSpaces: 0 }; } /** * Calculate the angle and the left,top position of the char that follow a path. * It appends it to graphemeInfo to be reused later at rendering * @private * @param {Number} positionInPath to be measured * @param {GraphemeBBox} graphemeInfo current grapheme box information * @param {Object} startingPoint position of the point */ _setGraphemeOnPath(positionInPath, graphemeInfo) { const centerPosition = positionInPath + graphemeInfo.kernedWidth / 2, path = this.path; const info = getPointOnPath(path.path, centerPosition, path.segmentsInfo); graphemeInfo.renderLeft = info.x - path.pathOffset.x; graphemeInfo.renderTop = info.y - path.pathOffset.y; graphemeInfo.angle = info.angle + (this.pathSide === "right" ? Math.PI : 0); } /** * * @param {String} grapheme to be measured * @param {Number} lineIndex index of the line where the char is * @param {Number} charIndex position in the line * @param {String} [prevGrapheme] character preceding the one to be measured * @returns {GraphemeBBox} grapheme bbox */ _getGraphemeBox(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) { const style = this.getCompleteStyleDeclaration(lineIndex, charIndex), prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : {}, info = this._measureChar(grapheme, style, prevGrapheme, prevStyle); let kernedWidth = info.kernedWidth, width = info.width, charSpacing; if (this.charSpacing !== 0) { charSpacing = this._getWidthOfCharSpacing(); width += charSpacing; kernedWidth += charSpacing; } const box = { width, left: 0, height: style.fontSize, kernedWidth, deltaY: style.deltaY }; if (charIndex > 0 && !skipLeft) { const previousBox = this.__charBounds[lineIndex][charIndex - 1]; box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width; } return box; } /** * Calculate height of line at 'lineIndex', * without the lineHeigth multiplication factor * @private * @param {Number} lineIndex index of line to calculate * @return {Number} */ getHeightOfLineImpl(lineIndex) { const lh = this.__lineHeights; if (lh[lineIndex]) return lh[lineIndex]; let maxHeight = this.getHeightOfChar(lineIndex, 0); for (let i = 1, len = this._textLines[lineIndex].length; i < len; i++) maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight); return lh[lineIndex] = maxHeight * this._fontSizeMult; } /** * Calculate height of line at 'lineIndex' * @param {Number} lineIndex index of line to calculate * @return {Number} */ getHeightOfLine(lineIndex) { return this.getHeightOfLineImpl(lineIndex) * this.lineHeight; } /** * Calculate text box height */ calcTextHeight() { let height = 0; for (let i = 0, len = this._textLines.length; i < len; i++) height += i === len - 1 ? this.getHeightOfLineImpl(i) : this.getHeightOfLine(i); return height; } /** * @private * @return {Number} Left offset */ _getLeftOffset() { return this.direction === "ltr" ? -this.width / 2 : this.width / 2; } /** * @private * @return {Number} Top offset */ _getTopOffset() { return -this.height / 2; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} method Method name ("fillText" or "strokeText") */ _renderTextCommon(ctx, method) { ctx.save(); let lineHeights = 0; const left = this._getLeftOffset(), top = this._getTopOffset(); for (let i = 0, len = this._textLines.length; i < len; i++) { this._renderTextLine(method, ctx, this._textLines[i], left + this._getLineLeftOffset(i), top + lineHeights + this.getHeightOfLineImpl(i), i); lineHeights += this.getHeightOfLine(i); } ctx.restore(); } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextFill(ctx) { if (!this.fill && !this.styleHas("fill")) return; this._renderTextCommon(ctx, "fillText"); } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextStroke(ctx) { if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) return; if (this.shadow && !this.shadow.affectStroke) this._removeShadow(ctx); ctx.save(); this._setLineDash(ctx, this.strokeDashArray); ctx.beginPath(); this._renderTextCommon(ctx, "strokeText"); ctx.closePath(); ctx.restore(); } /** * @private * @param {String} method fillText or strokeText. * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} line Content of the line, splitted in an array by grapheme * @param {Number} left * @param {Number} top * @param {Number} lineIndex */ _renderChars(method, ctx, line, left, top, lineIndex) { const isJustify = this.textAlign.includes(JUSTIFY), path = this.path, shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex) && !path, isLtr = this.direction === "ltr", sign = this.direction === "ltr" ? 1 : -1, currentDirection = ctx.direction; let actualStyle, nextStyle, charsToRender = "", charBox, boxWidth = 0, timeToRender, drawingLeft; ctx.save(); if (currentDirection !== this.direction) { ctx.canvas.setAttribute("dir", isLtr ? "ltr" : "rtl"); ctx.direction = isLtr ? "ltr" : "rtl"; ctx.textAlign = isLtr ? LEFT : RIGHT; } top -= this.getHeightOfLineImpl(lineIndex) * this._fontSizeFraction; if (shortCut) { this._renderChar(method, ctx, lineIndex, 0, line.join(""), left, top); ctx.restore(); return; } for (let i = 0, len = line.length - 1; i <= len; i++) { timeToRender = i === len || this.charSpacing || path; charsToRender += line[i]; charBox = this.__charBounds[lineIndex][i]; if (boxWidth === 0) { left += sign * (charBox.kernedWidth - charBox.width); boxWidth += charBox.width; } else boxWidth += charBox.kernedWidth; if (isJustify && !timeToRender) { if (this._reSpaceAndTab.test(line[i])) timeToRender = true; } if (!timeToRender) { actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); timeToRender = hasStyleChanged(actualStyle, nextStyle, false); } if (timeToRender) { if (path) { ctx.save(); ctx.translate(charBox.renderLeft, charBox.renderTop); ctx.rotate(charBox.angle); this._renderChar(method, ctx, lineIndex, i, charsToRender, -boxWidth / 2, 0); ctx.restore(); } else { drawingLeft = left; this._renderChar(method, ctx, lineIndex, i, charsToRender, drawingLeft, top); } charsToRender = ""; actualStyle = nextStyle; left += sign * boxWidth; boxWidth = 0; } } ctx.restore(); } /** * This function try to patch the missing gradientTransform on canvas gradients. * transforming a context to transform the gradient, is going to transform the stroke too. * we want to transform the gradient but not the stroke operation, so we create * a transformed gradient on a pattern and then we use the pattern instead of the gradient. * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size * is limited. * @private * @param {TFiller} filler a fabric gradient instance * @return {CanvasPattern} a pattern to use as fill/stroke style */ _applyPatternGradientTransformText(filler) { const width = this.width + this.strokeWidth, height = this.height + this.strokeWidth, pCanvas = createCanvasElementFor({ width, height }), pCtx = pCanvas.getContext("2d"); pCanvas.width = width; pCanvas.height = height; pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); pCtx.lineTo(0, height); pCtx.closePath(); pCtx.translate(width / 2, height / 2); pCtx.fillStyle = filler.toLive(pCtx); this._applyPatternGradientTransform(pCtx, filler); pCtx.fill(); return pCtx.createPattern(pCanvas, "no-repeat"); } handleFiller(ctx, property, filler) { let offsetX, offsetY; if (isFiller(filler)) if (filler.gradientUnits === "percentage" || filler.gradientTransform || filler.patternTransform) { offsetX = -this.width / 2; offsetY = -this.height / 2; ctx.translate(offsetX, offsetY); ctx[property] = this._applyPatternGradientTransformText(filler); return { offsetX, offsetY }; } else { ctx[property] = filler.toLive(ctx); return this._applyPatternGradientTransform(ctx, filler); } else ctx[property] = filler; return { offsetX: 0, offsetY: 0 }; } /** * This function prepare the canvas for a stroke style, and stroke and strokeWidth * need to be sent in as defined * @param {CanvasRenderingContext2D} ctx * @param {CompleteTextStyleDeclaration} style with stroke and strokeWidth defined * @returns */ _setStrokeStyles(ctx, { stroke, strokeWidth }) { ctx.lineWidth = strokeWidth; ctx.lineCap = this.strokeLineCap; ctx.lineDashOffset = this.strokeDashOffset; ctx.lineJoin = this.strokeLineJoin; ctx.miterLimit = this.strokeMiterLimit; return this.handleFiller(ctx, "strokeStyle", stroke); } /** * This function prepare the canvas for a ill style, and fill * need to be sent in as defined * @param {CanvasRenderingContext2D} ctx * @param {CompleteTextStyleDeclaration} style with ill defined * @returns */ _setFillStyles(ctx, { fill }) { return this.handleFiller(ctx, "fillStyle", fill); } /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex * @param {Number} charIndex * @param {String} _char * @param {Number} left Left coordinate * @param {Number} top Top coordinate * @param {Number} lineHeight Height of the line */ _renderChar(method, ctx, lineIndex, charIndex, _char, left, top) { const decl = this._getStyleDeclaration(lineIndex, charIndex), fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex), shouldFill = method === "fillText" && fullDecl.fill, shouldStroke = method === "strokeText" && fullDecl.stroke && fullDecl.strokeWidth; if (!shouldStroke && !shouldFill) return; ctx.save(); ctx.font = this._getFontDeclaration(fullDecl); if (decl.textBackgroundColor) this._removeShadow(ctx); if (decl.deltaY) top += decl.deltaY; if (shouldFill) { const fillOffsets = this._setFillStyles(ctx, fullDecl); ctx.fillText(_char, left - fillOffsets.offsetX, top - fillOffsets.offsetY); } if (shouldStroke) { const strokeOffsets = this._setStrokeStyles(ctx, fullDecl); ctx.strokeText(_char, left - strokeOffsets.offsetX, top - strokeOffsets.offsetY); } ctx.restore(); } /** * Turns the character into a 'superior figure' (i.e. 'superscript') * @param {Number} start selection start * @param {Number} end selection end */ setSuperscript(start, end) { this._setScript(start, end, this.superscript); } /** * Turns the character into an 'inferior figure' (i.e. 'subscript') * @param {Number} start selection start * @param {Number} end selection end */ setSubscript(start, end) { this._setScript(start, end, this.subscript); } /** * Applies 'schema' at given position * @private * @param {Number} start selection start * @param {Number} end selection end * @param {Number} schema */ _setScript(start, end, schema) { const loc = this.get2DCursorLocation(start, true), fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, "fontSize"), dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, "deltaY"), style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline }; this.setSelectionStyles(style, start, end); } /** * @private * @param {Number} lineIndex index text line * @return {Number} Line left offset */ _getLineLeftOffset(lineIndex) { const lineWidth = this.getLineWidth(lineIndex), lineDiff = this.width - lineWidth, textAlign = this.textAlign, direction = this.direction, isEndOfWrapping = this.isEndOfWrapping(lineIndex); let leftOffset = 0; if (textAlign === "justify" || textAlign === "justify-center" && !isEndOfWrapping || textAlign === "justify-right" && !isEndOfWrapping || textAlign === "justify-left" && !isEndOfWrapping) return 0; if (textAlign === "center") leftOffset = lineDiff / 2; if (textAlign === "right") leftOffset = lineDiff; if (textAlign === "justify-center") leftOffset = lineDiff / 2; if (textAlign === "justify-right") leftOffset = lineDiff; if (direction === "rtl") { if (textAlign === "right" || textAlign === "justify-right") leftOffset = 0; else if (textAlign === "left" || textAlign === "justify-left") leftOffset = -lineDiff; else if (textAlign === "center" || textAlign === "justify-center") leftOffset = -lineDiff / 2; } return leftOffset; } /** * @private */ _clearCache() { this._forceClearCache = false; this.__lineWidths = []; this.__lineHeights = []; this.__charBounds = []; } /** * Measure a single line given its index. Used to calculate the initial * text bounding box. The values are calculated and stored in __lineWidths cache. * @private * @param {Number} lineIndex line number * @return {Number} Line width */ getLineWidth(lineIndex) { if (this.__lineWidths[lineIndex] !== void 0) return this.__lineWidths[lineIndex]; const { width } = this.measureLine(lineIndex); this.__lineWidths[lineIndex] = width; return width; } _getWidthOfCharSpacing() { if (this.charSpacing !== 0) return this.fontSize * this.charSpacing / 1e3; return 0; } /** * Retrieves the value of property at given character position * @param {Number} lineIndex the line number * @param {Number} charIndex the character number * @param {String} property the property name * @returns the value of 'property' */ getValueOfPropertyAt(lineIndex, charIndex, property) { var _charStyle$property; return (_charStyle$property = this._getStyleDeclaration(lineIndex, charIndex)[property]) !== null && _charStyle$property !== void 0 ? _charStyle$property : this[property]; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextDecoration(ctx, type) { if (!this[type] && !this.styleHas(type)) return; let topOffset = this._getTopOffset(); const leftOffset = this._getLeftOffset(), path = this.path, charSpacing = this._getWidthOfCharSpacing(), offsetAligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0, offsetY = this.offsets[type]; for (let i = 0, len = this._textLines.length; i < len; i++) { const heightOfLine = this.getHeightOfLine(i); if (!this[type] && !this.styleHas(type, i)) { topOffset += heightOfLine; continue; } const line = this._textLines[i]; const maxHeight = heightOfLine / this.lineHeight; const lineLeftOffset = this._getLineLeftOffset(i); let boxStart = 0; let boxWidth = 0; let lastDecoration = this.getValueOfPropertyAt(i, 0, type); let lastFill = this.getValueOfPropertyAt(i, 0, FILL); let lastDecorationColor = this.getValueOfPropertyAt(i, 0, "textDecorationColor") || lastFill; let lastTickness = this.getValueOfPropertyAt(i, 0, TEXT_DECORATION_THICKNESS); let currentDecoration = lastDecoration; let currentFill = lastFill; let currentDecorationColor = lastDecorationColor; let currentTickness = lastTickness; const top = topOffset + maxHeight * (1 - this._fontSizeFraction); let size = this.getHeightOfChar(i, 0); let dy = this.getValueOfPropertyAt(i, 0, "deltaY"); for (let j = 0, jlen = line.length; j < jlen; j++) { const charBox = this.__charBounds[i][j]; currentDecoration = this.getValueOfPropertyAt(i, j, type); currentFill = this.getValueOfPropertyAt(i, j, FILL); currentDecorationColor = this.getValueOfPropertyAt(i, j, "textDecorationColor") || currentFill; currentTickness = this.getValueOfPropertyAt(i, j, TEXT_DECORATION_THICKNESS); const currentSize = this.getHeightOfChar(i, j); const currentDy = this.getValueOfPropertyAt(i, j, "deltaY"); if (path && currentDecoration && currentFill) { const finalTickness = this.fontSize * currentTickness / 1e3; ctx.save(); ctx.fillStyle = currentDecorationColor; ctx.translate(charBox.renderLeft, charBox.renderTop); ctx.rotate(charBox.angle); ctx.fillRect(-charBox.kernedWidth / 2, offsetY * currentSize + currentDy - offsetAligner * finalTickness, charBox.kernedWidth, finalTickness); ctx.restore(); } else if ((currentDecoration !== lastDecoration || currentFill !== lastFill || currentDecorationColor !== lastDecorationColor || currentSize !== size || currentTickness !== lastTickness || currentDy !== dy) && boxWidth > 0) { const finalTickness = this.fontSize * lastTickness / 1e3; let drawStart = leftOffset + lineLeftOffset + boxStart; if (this.direction === "rtl") drawStart = this.width - drawStart - boxWidth; if (lastDecoration && lastDecorationColor && lastTickness) { ctx.fillStyle = lastDecorationColor; ctx.fillRect(drawStart, top + offsetY * size + dy - offsetAligner * finalTickness, boxWidth, finalTickness); } boxStart = charBox.left; boxWidth = charBox.width; lastDecoration = currentDecoration; lastDecorationColor = currentDecorationColor; lastTickness = currentTickness; lastFill = currentFill; size = currentSize; dy = currentDy; } else boxWidth += charBox.kernedWidth; } let drawStart = leftOffset + lineLeftOffset + boxStart; if (this.direction === "rtl") drawStart = this.width - drawStart - boxWidth; ctx.fillStyle = currentDecorationColor; const finalTickness = this.fontSize * currentTickness / 1e3; currentDecoration && currentDecorationColor && currentTickness && ctx.fillRect(drawStart, top + offsetY * size + dy - offsetAligner * finalTickness, boxWidth - charSpacing, finalTickness); topOffset += heightOfLine; } this._removeShadow(ctx); } /** * return font declaration string for canvas context * @param {Object} [styleObject] object * @returns {String} font declaration formatted for canvas context. */ _getFontDeclaration({ fontFamily = this.fontFamily, fontStyle = this.fontStyle, fontWeight = this.fontWeight, fontSize = this.fontSize } = {}, forMeasuring) { const parsedFontFamily = fontFamily.includes("'") || fontFamily.includes("\"") || fontFamily.includes(",") || FabricText.genericFonts.includes(fontFamily.toLowerCase()) ? fontFamily : `"${fontFamily}"`; return [ fontStyle, fontWeight, `${forMeasuring ? this.CACHE_FONT_SIZE : fontSize}px`, parsedFontFamily ].join(" "); } /** * Renders text instance on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on */ render(ctx) { if (!this.visible) return; if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) return; if (this._forceClearCache) this.initDimensions(); super.render(ctx); } /** * Override this method to customize grapheme splitting * @todo the util `graphemeSplit` needs to be injectable in some way. * is more comfortable to inject the correct util rather than having to override text * in the middle of the prototype chain * @param {string} value * @returns {string[]} array of graphemes */ graphemeSplit(value) { return graphemeSplit(value); } /** * Returns the text as an array of lines. * @param {String} text text to split * @returns Lines in the text */ _splitTextIntoLines(text) { const lines = text.split(this._reNewline), newLines = new Array(lines.length), newLine = ["\n"]; let newText = []; for (let i = 0; i < lines.length; i++) { newLines[i] = this.graphemeSplit(lines[i]); newText = newText.concat(newLines[i], newLine); } newText.pop(); return { _unwrappedLines: newLines, lines, graphemeText: newText, graphemeLines: newLines }; } /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject(propertiesToInclude = []) { return { ...super.toObject([...additionalProps, ...propertiesToInclude]), styles: stylesToArray(this.styles, this.text), ...this.path ? { path: this.path.toObject() } : {} }; } set(key, value) { const { textLayoutProperties } = this.constructor; super.set(key, value); let needsDims = false; let isAddingPath = false; if (typeof key === "object") for (const _key in key) { if (_key === "path") this.setPathInfo(); needsDims = needsDims || textLayoutProperties.includes(_key); isAddingPath = isAddingPath || _key === "path"; } else { needsDims = textLayoutProperties.includes(key); isAddingPath = key === "path"; } if (isAddingPath) this.setPathInfo(); if (needsDims && this.initialized) { this.initDimensions(); this.setCoords(); } return this; } /** * Returns complexity of an instance * @return {Number} complexity */ complexity() { return 1; } /** * Returns FabricText instance from an SVG element (<b>not yet implemented</b>) * @param {HTMLElement} element Element to parse * @param {Object} [options] Options object */ static async fromElement(element, options, cssRules) { const parsedAttributes = parseAttributes(element, FabricText.ATTRIBUTE_NAMES, cssRules); const { textAnchor = LEFT, textDecoration = "", dx = 0, dy = 0, top = 0, left = 0, fontSize = 16, strokeWidth = 1, ...restOfOptions } = { ...options, ...parsedAttributes }; const textContent = normalizeWs(element.textContent || "").trim(); const text = new this(textContent, { left: left + dx, top: top + dy, underline: textDecoration.includes("underline"), overline: textDecoration.includes("overline"), linethrough: textDecoration.includes("line-through"), strokeWidth: 0, fontSize, ...restOfOptions }), textHeightScaleFactor = text.getScaledHeight() / text.height, scaledDiff = ((text.height + text.strokeWidth) * text.lineHeight - text.height) * textHeightScaleFactor, textHeight = text.getScaledHeight() + scaledDiff; let offX = 0; if (textAnchor === "center") offX = text.getScaledWidth() / 2; if (textAnchor === "right") offX = text.getScaledWidth(); text.set({ left: text.left - offX, top: text.top - (textHeight - text.fontSize * (.07 + text._fontSizeFraction)) / text.lineHeight, strokeWidth }); return text; } /** * Returns FabricText instance from an object representation * @param {Object} object plain js Object to create an instance from * @returns {Promise<FabricText>} */ static fromObject(object) { return this._fromObject({ ...object, styles: stylesFromArray(object.styles || {}, object.text) }, { extraParam: "text" }); } }; _defineProperty(FabricText, "textLayoutProperties", textLayoutProperties); _defineProperty(FabricText, "cacheProperties", [...cacheProperties, ...additionalProps]); _defineProperty(FabricText, "ownDefaults", textDefaultValues); _defineProperty(FabricText, "type", "Text"); _defineProperty(FabricText, "genericFonts", [ "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong" ]); _defineProperty(FabricText, "ATTRIBUTE_NAMES", SHARED_ATTRIBUTES.concat("x", "y", "dx", "dy", "font-family", "font-style", "font-weight", "font-size", "letter-spacing", "text-decoration", "text-decoration-thickness", "text-decoration-color", "text-anchor")); applyMixins(FabricText, [TextSVGExportMixin]); classRegistry.setClass(FabricText); classRegistry.setSVGClass(FabricText); //#endregion export { FabricText }; //# sourceMappingURL=Text.mjs.map