Skip to content

Commit a587ed6

Browse files
authored
Backport(): Support for text decoration thickness (#10647)
1 parent 977f797 commit a587ed6

24 files changed

+376
-228
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [6.7.0]
4+
5+
- feat(Text): Add support for text decoration tickness [#10643](https://github.com/fabricjs/fabric.js/pull/10643)
6+
37
## [6.6.7]
48

59
- fix(): Fix regex to parse the viewbox attribute to be more strict [#10636](https://github.com/fabricjs/fabric.js/pull/10636)

dist/index.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.min.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

dist/index.min.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.mjs

Lines changed: 96 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ class Cache {
405405
}
406406
const cache = new Cache();
407407

408-
var version = "6.6.7";
408+
var version = "6.7.0";
409409

410410
// use this syntax so babel plugin see this import here
411411
const VERSION = version;
@@ -4831,6 +4831,68 @@ function getSvgRegex(arr) {
48314831
return new RegExp('^(' + arr.join('|') + ')\\b', 'i');
48324832
}
48334833

4834+
const TEXT_DECORATION_THICKNESS = 'textDecorationThickness';
4835+
const fontProperties = ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle'];
4836+
const textDecorationProperties = ['underline', 'overline', 'linethrough'];
4837+
const textLayoutProperties = [...fontProperties, 'lineHeight', 'text', 'charSpacing', 'textAlign', 'styles', 'path', 'pathStartOffset', 'pathSide', 'pathAlign'];
4838+
const additionalProps = [...textLayoutProperties, ...textDecorationProperties, 'textBackgroundColor', 'direction', TEXT_DECORATION_THICKNESS];
4839+
const styleProperties = [...fontProperties, ...textDecorationProperties, STROKE, 'strokeWidth', FILL, 'deltaY', 'textBackgroundColor', TEXT_DECORATION_THICKNESS];
4840+
4841+
// @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
4842+
// regexes, list of properties that are not suppose to change by instances, magic consts.
4843+
// this will be a separated effort
4844+
const textDefaultValues = {
4845+
_reNewline: reNewline,
4846+
_reSpacesAndTabs: /[ \t\r]/g,
4847+
_reSpaceAndTab: /[ \t\r]/,
4848+
_reWords: /\S+/g,
4849+
fontSize: 40,
4850+
fontWeight: 'normal',
4851+
fontFamily: 'Times New Roman',
4852+
underline: false,
4853+
overline: false,
4854+
linethrough: false,
4855+
textAlign: LEFT,
4856+
fontStyle: 'normal',
4857+
lineHeight: 1.16,
4858+
textBackgroundColor: '',
4859+
stroke: null,
4860+
shadow: null,
4861+
path: undefined,
4862+
pathStartOffset: 0,
4863+
pathSide: LEFT,
4864+
pathAlign: 'baseline',
4865+
charSpacing: 0,
4866+
deltaY: 0,
4867+
direction: 'ltr',
4868+
CACHE_FONT_SIZE: 400,
4869+
MIN_TEXT_WIDTH: 2,
4870+
// Text magic numbers
4871+
superscript: {
4872+
size: 0.6,
4873+
// fontSize factor
4874+
baseline: -0.35 // baseline-shift factor (upwards)
4875+
},
4876+
subscript: {
4877+
size: 0.6,
4878+
// fontSize factor
4879+
baseline: 0.11 // baseline-shift factor (downwards)
4880+
},
4881+
_fontSizeFraction: 0.222,
4882+
offsets: {
4883+
underline: 0.1,
4884+
linethrough: -0.28167,
4885+
// added 1/30 to original number
4886+
overline: -0.81333 // added 1/15 to original number
4887+
},
4888+
_fontSizeMult: 1.13,
4889+
[TEXT_DECORATION_THICKNESS]: 66.667 // before implementation was 1/15
4890+
};
4891+
const JUSTIFY = 'justify';
4892+
const JUSTIFY_LEFT = 'justify-left';
4893+
const JUSTIFY_RIGHT = 'justify-right';
4894+
const JUSTIFY_CENTER = 'justify-center';
4895+
48344896
var _templateObject$1, _templateObject2$1, _templateObject3$1;
48354897

48364898
// matches, e.g.: +14.56e-12, etc.
@@ -4872,7 +4934,8 @@ const svgValidTagNames = ['path', 'circle', 'polygon', 'polyline', 'ellipse', 'r
48724934
'clip-path': 'clipPath',
48734935
'clip-rule': 'clipRule',
48744936
'vector-effect': 'strokeUniform',
4875-
'image-rendering': 'imageSmoothing'
4937+
'image-rendering': 'imageSmoothing',
4938+
'text-decoration-thickness': TEXT_DECORATION_THICKNESS
48764939
},
48774940
fSize = 'font-size',
48784941
cPath = 'clip-path';
@@ -10362,7 +10425,7 @@ var lang_string = /*#__PURE__*/Object.freeze({
1036210425
*/
1036310426
const hasStyleChanged = function (prevStyle, thisStyle) {
1036410427
let forTextSpans = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
10365-
return prevStyle.fill !== thisStyle.fill || prevStyle.stroke !== thisStyle.stroke || prevStyle.strokeWidth !== thisStyle.strokeWidth || prevStyle.fontSize !== thisStyle.fontSize || prevStyle.fontFamily !== thisStyle.fontFamily || prevStyle.fontWeight !== thisStyle.fontWeight || prevStyle.fontStyle !== thisStyle.fontStyle || prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || prevStyle.deltaY !== thisStyle.deltaY || forTextSpans && (prevStyle.overline !== thisStyle.overline || prevStyle.underline !== thisStyle.underline || prevStyle.linethrough !== thisStyle.linethrough);
10428+
return prevStyle.fill !== thisStyle.fill || prevStyle.stroke !== thisStyle.stroke || prevStyle.strokeWidth !== thisStyle.strokeWidth || prevStyle.fontSize !== thisStyle.fontSize || prevStyle.fontFamily !== thisStyle.fontFamily || prevStyle.fontWeight !== thisStyle.fontWeight || prevStyle.fontStyle !== thisStyle.fontStyle || prevStyle.textDecorationThickness !== thisStyle.textDecorationThickness || prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || prevStyle.deltaY !== thisStyle.deltaY || forTextSpans && (prevStyle.overline !== thisStyle.overline || prevStyle.underline !== thisStyle.underline || prevStyle.linethrough !== thisStyle.linethrough);
1036610429
};
1036710430

1036810431
/**
@@ -10650,7 +10713,7 @@ function normalizeValue(attr, value, parentAttributes, fontSize) {
1065010713
}
1065110714
} else if (attr === 'textAnchor' /* text-anchor */) {
1065210715
ouputValue = value === 'start' ? LEFT : value === 'end' ? RIGHT : CENTER;
10653-
} else if (attr === 'charSpacing') {
10716+
} else if (attr === 'charSpacing' || attr === TEXT_DECORATION_THICKNESS) {
1065410717
// parseUnit returns px and we convert it to em
1065510718
parsed = parseUnit(value, fontSize) / fontSize * 1000;
1065610719
} else if (attr === 'paintFirst') {
@@ -18404,64 +18467,6 @@ _defineProperty(Polygon, "type", 'Polygon');
1840418467
classRegistry.setClass(Polygon);
1840518468
classRegistry.setSVGClass(Polygon);
1840618469

18407-
const fontProperties = ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle'];
18408-
const textDecorationProperties = ['underline', 'overline', 'linethrough'];
18409-
const textLayoutProperties = [...fontProperties, 'lineHeight', 'text', 'charSpacing', 'textAlign', 'styles', 'path', 'pathStartOffset', 'pathSide', 'pathAlign'];
18410-
const additionalProps = [...textLayoutProperties, ...textDecorationProperties, 'textBackgroundColor', 'direction'];
18411-
const styleProperties = [...fontProperties, ...textDecorationProperties, STROKE, 'strokeWidth', FILL, 'deltaY', 'textBackgroundColor'];
18412-
18413-
// @TODO: Many things here are configuration related and shouldn't be on the class nor prototype
18414-
// regexes, list of properties that are not suppose to change by instances, magic consts.
18415-
// this will be a separated effort
18416-
const textDefaultValues = {
18417-
_reNewline: reNewline,
18418-
_reSpacesAndTabs: /[ \t\r]/g,
18419-
_reSpaceAndTab: /[ \t\r]/,
18420-
_reWords: /\S+/g,
18421-
fontSize: 40,
18422-
fontWeight: 'normal',
18423-
fontFamily: 'Times New Roman',
18424-
underline: false,
18425-
overline: false,
18426-
linethrough: false,
18427-
textAlign: LEFT,
18428-
fontStyle: 'normal',
18429-
lineHeight: 1.16,
18430-
superscript: {
18431-
size: 0.6,
18432-
// fontSize factor
18433-
baseline: -0.35 // baseline-shift factor (upwards)
18434-
},
18435-
subscript: {
18436-
size: 0.6,
18437-
// fontSize factor
18438-
baseline: 0.11 // baseline-shift factor (downwards)
18439-
},
18440-
textBackgroundColor: '',
18441-
stroke: null,
18442-
shadow: null,
18443-
path: undefined,
18444-
pathStartOffset: 0,
18445-
pathSide: LEFT,
18446-
pathAlign: 'baseline',
18447-
_fontSizeFraction: 0.222,
18448-
offsets: {
18449-
underline: 0.1,
18450-
linethrough: -0.315,
18451-
overline: -0.88
18452-
},
18453-
_fontSizeMult: 1.13,
18454-
charSpacing: 0,
18455-
deltaY: 0,
18456-
direction: 'ltr',
18457-
CACHE_FONT_SIZE: 400,
18458-
MIN_TEXT_WIDTH: 2
18459-
};
18460-
const JUSTIFY = 'justify';
18461-
const JUSTIFY_LEFT = 'justify-left';
18462-
const JUSTIFY_RIGHT = 'justify-right';
18463-
const JUSTIFY_CENTER = 'justify-center';
18464-
1846518470
class StyledText extends FabricObject {
1846618471
/**
1846718472
* Returns true if object has no styling or no styling in a line
@@ -18776,7 +18781,7 @@ class TextSVGExportMixin extends FabricObjectSVGExportMixin {
1877618781
} = _ref;
1877718782
const noShadow = true,
1877818783
textDecoration = this.getSvgTextDecoration(this);
18779-
return [textBgRects.join(''), '\t\t<text xml:space="preserve" ', this.fontFamily ? "font-family=\"".concat(this.fontFamily.replace(dblQuoteRegex, "'"), "\" ") : '', this.fontSize ? "font-size=\"".concat(this.fontSize, "\" ") : '', this.fontStyle ? "font-style=\"".concat(this.fontStyle, "\" ") : '', this.fontWeight ? "font-weight=\"".concat(this.fontWeight, "\" ") : '', textDecoration ? "text-decoration=\"".concat(textDecoration, "\" ") : '', this.direction === 'rtl' ? "direction=\"".concat(this.direction, "\" ") : '', 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textSpans.join(''), '</text>\n'];
18784+
return [textBgRects.join(''), '\t\t<text xml:space="preserve" ', "font-family=\"".concat(this.fontFamily.replace(dblQuoteRegex, "'"), "\" "), "font-size=\"".concat(this.fontSize, "\" "), this.fontStyle ? "font-style=\"".concat(this.fontStyle, "\" ") : '', this.fontWeight ? "font-weight=\"".concat(this.fontWeight, "\" ") : '', textDecoration ? "text-decoration=\"".concat(textDecoration, "\" ") : '', this.direction === 'rtl' ? "direction=\"".concat(this.direction, "\" ") : '', 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textSpans.join(''), '</text>\n'];
1878018785
}
1878118786

1878218787
/**
@@ -18933,7 +18938,7 @@ class TextSVGExportMixin extends FabricObjectSVGExportMixin {
1893318938
* @return {String}
1893418939
*/
1893518940
getSvgStyles(skipShadow) {
18936-
return "".concat(super.getSvgStyles(skipShadow), " white-space: pre;");
18941+
return "".concat(super.getSvgStyles(skipShadow), " text-decoration-thickness: ").concat(toFixed(this.textDecorationThickness * this.getObjectScaling().y / 10, config.NUM_FRACTION_DIGITS), "%; white-space: pre;");
1893718942
}
1893818943

1893918944
/**
@@ -18951,10 +18956,19 @@ class TextSVGExportMixin extends FabricObjectSVGExportMixin {
1895118956
fontSize,
1895218957
fontStyle,
1895318958
fontWeight,
18954-
deltaY
18959+
deltaY,
18960+
textDecorationThickness,
18961+
linethrough,
18962+
overline,
18963+
underline
1895518964
} = style;
18956-
const textDecoration = this.getSvgTextDecoration(style);
18957-
return [stroke ? colorPropToSVG(STROKE, stroke) : '', strokeWidth ? "stroke-width: ".concat(strokeWidth, "; ") : '', fontFamily ? "font-family: ".concat(!fontFamily.includes("'") && !fontFamily.includes('"') ? "'".concat(fontFamily, "'") : fontFamily, "; ") : '', fontSize ? "font-size: ".concat(fontSize, "px; ") : '', fontStyle ? "font-style: ".concat(fontStyle, "; ") : '', fontWeight ? "font-weight: ".concat(fontWeight, "; ") : '', textDecoration ? "text-decoration: ".concat(textDecoration, "; ") : textDecoration, fill ? colorPropToSVG(FILL, fill) : '', deltaY ? "baseline-shift: ".concat(-deltaY, "; ") : '', useWhiteSpace ? 'white-space: pre; ' : ''].join('');
18965+
const textDecoration = this.getSvgTextDecoration({
18966+
underline: underline !== null && underline !== void 0 ? underline : this.underline,
18967+
overline: overline !== null && overline !== void 0 ? overline : this.overline,
18968+
linethrough: linethrough !== null && linethrough !== void 0 ? linethrough : this.linethrough
18969+
});
18970+
const thickness = textDecorationThickness || this.textDecorationThickness;
18971+
return [stroke ? colorPropToSVG(STROKE, stroke) : '', strokeWidth ? "stroke-width: ".concat(strokeWidth, "; ") : '', fontFamily ? "font-family: ".concat(!fontFamily.includes("'") && !fontFamily.includes('"') ? "'".concat(fontFamily, "'") : fontFamily, "; ") : '', fontSize ? "font-size: ".concat(fontSize, "px; ") : '', fontStyle ? "font-style: ".concat(fontStyle, "; ") : '', fontWeight ? "font-weight: ".concat(fontWeight, "; ") : '', textDecoration ? "text-decoration: ".concat(textDecoration, "; text-decoration-thickness: ").concat(toFixed(thickness * this.getObjectScaling().y / 10, config.NUM_FRACTION_DIGITS), "%; ") : '', fill ? colorPropToSVG(FILL, fill) : '', deltaY ? "baseline-shift: ".concat(-deltaY, "; ") : '', useWhiteSpace ? 'white-space: pre; ' : ''].join('');
1895818972
}
1895918973

1896018974
/**
@@ -19971,6 +19985,7 @@ class FabricText extends StyledText {
1997119985
const leftOffset = this._getLeftOffset(),
1997219986
path = this.path,
1997319987
charSpacing = this._getWidthOfCharSpacing(),
19988+
offsetAligner = type === 'linethrough' ? 0.5 : type === 'overline' ? 1 : 0,
1997419989
offsetY = this.offsets[type];
1997519990
for (let i = 0, len = this._textLines.length; i < len; i++) {
1997619991
const heightOfLine = this.getHeightOfLine(i);
@@ -19985,38 +20000,44 @@ class FabricText extends StyledText {
1998520000
let boxWidth = 0;
1998620001
let lastDecoration = this.getValueOfPropertyAt(i, 0, type);
1998720002
let lastFill = this.getValueOfPropertyAt(i, 0, FILL);
19988-
let currentDecoration;
19989-
let currentFill;
20003+
let lastTickness = this.getValueOfPropertyAt(i, 0, TEXT_DECORATION_THICKNESS);
20004+
let currentDecoration = lastDecoration;
20005+
let currentFill = lastFill;
20006+
let currentTickness = lastTickness;
1999020007
const top = topOffset + maxHeight * (1 - this._fontSizeFraction);
1999120008
let size = this.getHeightOfChar(i, 0);
1999220009
let dy = this.getValueOfPropertyAt(i, 0, 'deltaY');
1999320010
for (let j = 0, jlen = line.length; j < jlen; j++) {
1999420011
const charBox = this.__charBounds[i][j];
1999520012
currentDecoration = this.getValueOfPropertyAt(i, j, type);
1999620013
currentFill = this.getValueOfPropertyAt(i, j, FILL);
20014+
currentTickness = this.getValueOfPropertyAt(i, j, TEXT_DECORATION_THICKNESS);
1999720015
const currentSize = this.getHeightOfChar(i, j);
1999820016
const currentDy = this.getValueOfPropertyAt(i, j, 'deltaY');
1999920017
if (path && currentDecoration && currentFill) {
20018+
const finalTickness = this.fontSize * currentTickness / 1000;
2000020019
ctx.save();
2000120020
// bug? verify lastFill is a valid fill here.
2000220021
ctx.fillStyle = lastFill;
2000320022
ctx.translate(charBox.renderLeft, charBox.renderTop);
2000420023
ctx.rotate(charBox.angle);
20005-
ctx.fillRect(-charBox.kernedWidth / 2, offsetY * currentSize + currentDy, charBox.kernedWidth, this.fontSize / 15);
20024+
ctx.fillRect(-charBox.kernedWidth / 2, offsetY * currentSize + currentDy - offsetAligner * finalTickness, charBox.kernedWidth, finalTickness);
2000620025
ctx.restore();
20007-
} else if ((currentDecoration !== lastDecoration || currentFill !== lastFill || currentSize !== size || currentDy !== dy) && boxWidth > 0) {
20026+
} else if ((currentDecoration !== lastDecoration || currentFill !== lastFill || currentSize !== size || currentTickness !== lastTickness || currentDy !== dy) && boxWidth > 0) {
20027+
const finalTickness = this.fontSize * lastTickness / 1000;
2000820028
let drawStart = leftOffset + lineLeftOffset + boxStart;
2000920029
if (this.direction === 'rtl') {
2001020030
drawStart = this.width - drawStart - boxWidth;
2001120031
}
20012-
if (lastDecoration && lastFill) {
20032+
if (lastDecoration && lastFill && lastTickness) {
2001320033
// bug? verify lastFill is a valid fill here.
2001420034
ctx.fillStyle = lastFill;
20015-
ctx.fillRect(drawStart, top + offsetY * size + dy, boxWidth, this.fontSize / 15);
20035+
ctx.fillRect(drawStart, top + offsetY * size + dy - offsetAligner * finalTickness, boxWidth, finalTickness);
2001620036
}
2001720037
boxStart = charBox.left;
2001820038
boxWidth = charBox.width;
2001920039
lastDecoration = currentDecoration;
20040+
lastTickness = currentTickness;
2002020041
lastFill = currentFill;
2002120042
size = currentSize;
2002220043
dy = currentDy;
@@ -20029,7 +20050,8 @@ class FabricText extends StyledText {
2002920050
drawStart = this.width - drawStart - boxWidth;
2003020051
}
2003120052
ctx.fillStyle = currentFill;
20032-
currentDecoration && currentFill && ctx.fillRect(drawStart, top + offsetY * size + dy, boxWidth - charSpacing, this.fontSize / 15);
20053+
const finalTickness = this.fontSize * currentTickness / 1000;
20054+
currentDecoration && currentFill && currentTickness && ctx.fillRect(drawStart, top + offsetY * size + dy - offsetAligner * finalTickness, boxWidth - charSpacing, finalTickness);
2003320055
topOffset += heightOfLine;
2003420056
}
2003520057
// if there is text background color no

dist/index.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)