Skip to content

Commit 3a80bb8

Browse files
rcoenenclaude
andcommitted
Add textDecorationColor, textDecorationWidth, and textDecorationStyle for Text
Three new properties for independent control of text decoration styling, mirroring CSS text-decoration sub-properties: - textDecorationColor: decoration color independent of text fill - textDecorationWidth: override default line thickness (fontSize / 15) - textDecorationStyle: 'solid' (default), 'dashed', 'dotted', 'double', 'wavy' All apply to both underline and line-through decorations. Falls back to existing behavior when unset (fully backward compatible). Addresses #2037 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 811d62a commit 3a80bb8

File tree

1 file changed

+136
-19
lines changed

1 file changed

+136
-19
lines changed

src/shapes/Text.ts

Lines changed: 136 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export interface TextConfig extends ShapeConfig {
7171
fontVariant?: string;
7272
textDecoration?: string;
7373
underlineOffset?: number;
74+
textDecorationColor?: string;
75+
textDecorationWidth?: number;
76+
textDecorationStyle?: string;
7477
align?: string;
7578
verticalAlign?: string;
7679
padding?: number;
@@ -242,6 +245,9 @@ export class Text extends Shape<TextConfig> {
242245
fill = this.fill(),
243246
textDecoration = this.textDecoration(),
244247
underlineOffset = this.underlineOffset(),
248+
textDecorationColor = this.textDecorationColor(),
249+
textDecorationWidth = this.textDecorationWidth(),
250+
textDecorationStyle = this.textDecorationStyle(),
245251
shouldUnderline = textDecoration.indexOf('underline') !== -1,
246252
shouldLineThrough = textDecoration.indexOf('line-through') !== -1,
247253
n;
@@ -309,19 +315,56 @@ export class Text extends Shape<TextConfig> {
309315
: Math.round(fontSize / 2));
310316
const x = lineTranslateX;
311317
const y = translateY + lineTranslateY + yOffset;
312-
context.moveTo(x, y);
313-
const lineWidth =
318+
const decoLineWidth =
314319
align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width;
315-
context.lineTo(x + Math.round(lineWidth), y);
320+
const lw = textDecorationWidth || fontSize / 15;
321+
context.lineWidth = lw;
322+
323+
if (textDecorationColor) {
324+
context.strokeStyle = textDecorationColor;
325+
} else {
326+
const gradient = this._getLinearGradient();
327+
context.strokeStyle = gradient || fill;
328+
}
316329

317-
// I have no idea what is real ratio
318-
// just /15 looks good enough
319-
context.lineWidth = fontSize / 15;
330+
if (textDecorationStyle === 'dashed') {
331+
context.setLineDash([lw * 3, lw * 2]);
332+
} else if (textDecorationStyle === 'dotted') {
333+
context.setLineDash([lw, lw * 1.5]);
334+
context.lineCap = 'round';
335+
} else if (textDecorationStyle === 'wavy') {
336+
// Draw a sine wave instead of a straight line
337+
const step = Math.max(lw * 2, 4);
338+
const amp = Math.max(lw, 1.5);
339+
context.moveTo(x, y);
340+
for (let wx = x; wx <= x + Math.round(decoLineWidth); wx += step) {
341+
const mid = wx + step / 2;
342+
const end = Math.min(wx + step, x + Math.round(decoLineWidth));
343+
context.quadraticCurveTo(mid, y - amp, end, y);
344+
wx += step;
345+
if (wx > x + Math.round(decoLineWidth)) break;
346+
const mid2 = wx + step / 2;
347+
const end2 = Math.min(wx + step, x + Math.round(decoLineWidth));
348+
context.quadraticCurveTo(mid2, y + amp, end2, y);
349+
}
350+
context.stroke();
351+
context.restore();
352+
} else if (textDecorationStyle === 'double') {
353+
const gap = lw * 2;
354+
context.moveTo(x, y);
355+
context.lineTo(x + Math.round(decoLineWidth), y);
356+
context.moveTo(x, y + gap);
357+
context.lineTo(x + Math.round(decoLineWidth), y + gap);
358+
context.stroke();
359+
context.restore();
360+
}
320361

321-
const gradient = this._getLinearGradient();
322-
context.strokeStyle = gradient || fill;
323-
context.stroke();
324-
context.restore();
362+
if (textDecorationStyle !== 'wavy' && textDecorationStyle !== 'double') {
363+
context.moveTo(x, y);
364+
context.lineTo(x + Math.round(decoLineWidth), y);
365+
context.stroke();
366+
context.restore();
367+
}
325368
}
326369

327370
// store the starting x position for line-through which is drawn after text
@@ -396,17 +439,35 @@ export class Text extends Shape<TextConfig> {
396439
? -Math.round(fontSize / 4)
397440
: 0;
398441
const x = lineThroughStartX;
399-
context.moveTo(x, translateY + lineTranslateY + yOffset);
400-
const lineWidth =
442+
const ltY = translateY + lineTranslateY + yOffset;
443+
const ltLineWidth =
401444
align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width;
445+
const ltLw = textDecorationWidth || fontSize / 15;
446+
context.lineWidth = ltLw;
447+
448+
if (textDecorationColor) {
449+
context.strokeStyle = textDecorationColor;
450+
} else {
451+
const gradient = this._getLinearGradient();
452+
context.strokeStyle = gradient || fill;
453+
}
454+
455+
if (textDecorationStyle === 'dashed') {
456+
context.setLineDash([ltLw * 3, ltLw * 2]);
457+
} else if (textDecorationStyle === 'dotted') {
458+
context.setLineDash([ltLw, ltLw * 1.5]);
459+
context.lineCap = 'round';
460+
}
461+
462+
context.moveTo(x, ltY);
463+
context.lineTo(x + Math.round(ltLineWidth), ltY);
464+
465+
if (textDecorationStyle === 'double') {
466+
const gap = ltLw * 2;
467+
context.moveTo(x, ltY + gap);
468+
context.lineTo(x + Math.round(ltLineWidth), ltY + gap);
469+
}
402470

403-
context.lineTo(
404-
x + Math.round(lineWidth),
405-
translateY + lineTranslateY + yOffset
406-
);
407-
context.lineWidth = fontSize / 15;
408-
const gradient = this._getLinearGradient();
409-
context.strokeStyle = gradient || fill;
410471
context.stroke();
411472
context.restore();
412473
}
@@ -771,6 +832,9 @@ export class Text extends Shape<TextConfig> {
771832
lineHeight: GetSet<number, this>;
772833
textDecoration: GetSet<string, this>;
773834
underlineOffset: GetSet<number, this>;
835+
textDecorationColor: GetSet<string, this>;
836+
textDecorationWidth: GetSet<number, this>;
837+
textDecorationStyle: GetSet<string, this>;
774838
text: GetSet<string, this>;
775839
wrap: GetSet<string, this>;
776840
ellipsis: GetSet<boolean, this>;
@@ -1078,6 +1142,59 @@ Factory.addGetterSetter(
10781142
getNumberValidator()
10791143
);
10801144

1145+
/**
1146+
* get/set text decoration color. When set, underline and line-through decorations
1147+
* use this color instead of the text fill color.
1148+
* @name Konva.Text#textDecorationColor
1149+
* @method
1150+
* @param {String} textDecorationColor
1151+
* @returns {String}
1152+
* @example
1153+
* // get text decoration color
1154+
* var color = text.textDecorationColor();
1155+
*
1156+
* // set text decoration color
1157+
* text.textDecorationColor('#18A0FB');
1158+
*/
1159+
Factory.addGetterSetter(Text, 'textDecorationColor', '');
1160+
1161+
/**
1162+
* get/set text decoration line width. When set, underline and line-through use
1163+
* this width instead of the default (fontSize / 15).
1164+
* @name Konva.Text#textDecorationWidth
1165+
* @method
1166+
* @param {Number} textDecorationWidth
1167+
* @returns {Number}
1168+
* @example
1169+
* // get text decoration width
1170+
* var width = text.textDecorationWidth();
1171+
*
1172+
* // set text decoration width
1173+
* text.textDecorationWidth(2);
1174+
*/
1175+
Factory.addGetterSetter(
1176+
Text,
1177+
'textDecorationWidth',
1178+
undefined,
1179+
getNumberValidator()
1180+
);
1181+
1182+
/**
1183+
* get/set text decoration style. Can be 'solid' (default), 'dashed', 'dotted',
1184+
* 'double', or 'wavy'. Applies to both underline and line-through decorations.
1185+
* @name Konva.Text#textDecorationStyle
1186+
* @method
1187+
* @param {String} textDecorationStyle
1188+
* @returns {String}
1189+
* @example
1190+
* // get text decoration style
1191+
* var style = text.textDecorationStyle();
1192+
*
1193+
* // set wavy underline (e.g. for spell-check)
1194+
* text.textDecorationStyle('wavy');
1195+
*/
1196+
Factory.addGetterSetter(Text, 'textDecorationStyle', '');
1197+
10811198
/**
10821199
* get/set per-character render hook. The callback is invoked for each grapheme before drawing.
10831200
* It can mutate the provided context (e.g. translate, rotate, change styles) and should return void.

0 commit comments

Comments
 (0)