Merge pull request #2012 from HusnainTaj/feat/text-underline-offset

Add option to adjust underline text decoration offset for Text Shape
This commit is contained in:
Anton Lavrenov
2025-12-25 15:23:56 -05:00
committed by GitHub
4 changed files with 68 additions and 5 deletions

View File

@@ -151,7 +151,7 @@ class TweenEngine {
}
export interface TweenConfig extends NodeConfig {
easing?: typeof Easings[keyof typeof Easings];
easing?: (typeof Easings)[keyof typeof Easings];
yoyo?: boolean;
onReset?: Function;
onFinish?: Function;

View File

@@ -452,7 +452,7 @@ let _isCanvasFarblingActive: boolean | null = null;
const req =
(typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) ||
function (f) {
setTimeout(f, 16); // 60fps ≈ 16.67ms per frame
setTimeout(f, 16); // 60fps ≈ 16.67ms per frame
};
/**
* @namespace Util

View File

@@ -70,6 +70,7 @@ export interface TextConfig extends ShapeConfig {
fontStyle?: string;
fontVariant?: string;
textDecoration?: string;
underlineOffset?: number;
align?: string;
verticalAlign?: string;
padding?: number;
@@ -183,6 +184,7 @@ function checkDefaultFill(config?: TextConfig) {
* @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 {String} [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
@@ -239,6 +241,7 @@ export class Text extends Shape<TextConfig> {
charRenderFunc = this.charRenderFunc(),
fill = this.fill(),
textDecoration = this.textDecoration(),
underlineOffset = this.underlineOffset(),
shouldUnderline = textDecoration.indexOf('underline') !== -1,
shouldLineThrough = textDecoration.indexOf('line-through') !== -1,
n;
@@ -299,9 +302,11 @@ export class Text extends Shape<TextConfig> {
context.save();
context.beginPath();
const yOffset = !Konva.legacyTextRendering
? Math.round(fontSize / 4)
: Math.round(fontSize / 2);
const yOffset =
underlineOffset ??
(!Konva.legacyTextRendering
? Math.round(fontSize / 4)
: Math.round(fontSize / 2));
const x = lineTranslateX;
const y = translateY + lineTranslateY + yOffset;
context.moveTo(x, y);
@@ -765,6 +770,7 @@ export class Text extends Shape<TextConfig> {
padding: GetSet<number, this>;
lineHeight: GetSet<number, this>;
textDecoration: GetSet<string, this>;
underlineOffset: GetSet<number, this>;
text: GetSet<string, this>;
wrap: GetSet<string, this>;
ellipsis: GetSet<boolean, this>;
@@ -1052,6 +1058,26 @@ Factory.addGetterSetter(Text, 'text', '', getStringValidator());
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.
* It can mutate the provided context (e.g. translate, rotate, change styles) and should return void.

View File

@@ -2003,4 +2003,41 @@ describe('Text', function () {
);
}
});
it('text with underline offset', function () {
var stage = addStage();
var layer = new Konva.Layer();
var text = new Konva.Text({
x: 10,
y: 10,
text: 'hello\nworld',
fontSize: 80,
fill: 'red',
textDecoration: 'underline',
underlineOffset: 16,
});
layer.add(text);
stage.add(layer);
const trace = layer.getContext().getTrace();
if (Konva._renderBackend === 'web') {
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,10,10);font=normal normal 80px Arial;textBaseline=alphabetic;textAlign=left;translate(0,0);save();save();beginPath();moveTo(0,83.5);lineTo(169,83.5);stroke();restore();fillStyle=red;fillText(hello,0,67.5);restore();save();save();beginPath();moveTo(0,163.5);lineTo(191,163.5);stroke();restore();fillStyle=red;fillText(world,0,147.5);restore();restore();'
);
} else if (Konva._renderBackend === 'node-canvas') {
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,10,10);font=normal normal 80px Arial;textBaseline=alphabetic;textAlign=left;translate(0,0);save();save();beginPath();moveTo(0,84);lineTo(169,84);stroke();restore();fillStyle=red;fillText(hello,0,68);restore();save();save();beginPath();moveTo(0,164);lineTo(191,164);stroke();restore();fillStyle=red;fillText(world,0,148);restore();restore();'
);
} else {
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,10,10);font=normal normal 80px Arial;textBaseline=alphabetic;textAlign=left;translate(0,0);save();save();beginPath();moveTo(0,83.734);lineTo(169,83.734);stroke();restore();fillStyle=red;fillText(hello,0,67.734);restore();save();save();beginPath();moveTo(0,163.734);lineTo(191,163.734);stroke();restore();fillStyle=red;fillText(world,0,147.734);restore();restore();'
);
}
});
});