From 291a40d94b226deaa8aa75580f2f9911637c5ed7 Mon Sep 17 00:00:00 2001 From: xkxx Date: Sat, 9 Sep 2023 05:13:25 +0000 Subject: [PATCH 1/6] Add basic support for RTL text direction. --- src/Context.ts | 1 + src/shapes/Text.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Context.ts b/src/Context.ts index ab4d14a27..d2596b37a 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -82,6 +82,7 @@ var CONTEXT_PROPERTIES = [ 'lineJoin', 'lineWidth', 'miterLimit', + 'direction', 'font', 'textAlign', 'textBaseline', diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index a7820c421..8c47d9127 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -22,6 +22,7 @@ export function stringToArray(string: string) { } export interface TextConfig extends ShapeConfig { + direction?: string; text?: string; fontFamily?: string; fontSize?: number; @@ -46,6 +47,7 @@ var AUTO = 'auto', CONTEXT_2D = '2d', DASH = '-', LEFT = 'left', + LTR = 'ltr', TEXT = 'text', TEXT_UPPER = 'Text', TOP = 'top', @@ -60,6 +62,7 @@ var AUTO = 'auto', NONE = 'none', ELLIPSIS = '…', ATTR_CHANGE_LIST = [ + 'direction', 'fontFamily', 'fontSize', 'fontStyle', @@ -132,6 +135,7 @@ function checkDefaultFill(config?: TextConfig) { * @memberof Konva * @augments Konva.Shape * @param {Object} config + * @param {String} [config.direction] default is ltr * @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. @@ -201,6 +205,8 @@ export class Text extends Shape { var lineTranslateX = 0; var lineTranslateY = 0; + context.setAttr('direction', this.direction()); + context.setAttr('font', this._getContextFont()); context.setAttr('textBaseline', MIDDLE); @@ -615,6 +621,7 @@ export class Text extends Shape { return super._useBufferCanvas(); } + direction: GetSet; fontFamily: GetSet; fontSize: GetSet; fontStyle: GetSet; @@ -682,6 +689,22 @@ Factory.overWriteSetter(Text, 'width', getNumberOrAutoValidator()); 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', 'ltr'); + /** * get/set font family * @name Konva.Text#fontFamily From d7b86c5404d512227b6dad19e8680c9ad2d3a3c2 Mon Sep 17 00:00:00 2001 From: xkxx Date: Tue, 12 Sep 2023 00:07:25 +0000 Subject: [PATCH 2/6] feat: Support letterSpacing for RTL. --- src/Context.ts | 21 +++++++++++++++++++++ src/shapes/Text.ts | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Context.ts b/src/Context.ts index d2596b37a..b0c59e4dc 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -68,6 +68,7 @@ var COMMA = ',', 'strokeText', 'transform', 'translate', + 'trySetLetterSpacing', ]; var CONTEXT_PROPERTIES = [ @@ -92,6 +93,11 @@ var CONTEXT_PROPERTIES = [ ] as const; const traceArrMax = 100; + +interface CanvasRenderingContext2DFeatureDetection extends CanvasRenderingContext2D { + letterSpacing: string | undefined; +} + /** * Konva wrapper around native 2d canvas context. It has almost the same API of 2d context with some additional functions. * With core Konva shapes you don't need to use this object. But you will use it if you want to create @@ -706,6 +712,21 @@ export class Context { translate(x: number, y: number) { this._context.translate(x, y); } + /** + * Set letterSpacing if supported by browser. + * @method + * @name Konva.Context#trySetLetterSpacing + * @returns true if successful, false if not supported. + */ + trySetLetterSpacing(letterSpacing: number): boolean { + var context = this._context as CanvasRenderingContext2DFeatureDetection; + var letterSpacingSupported = 'letterSpacing' in context; + + if (letterSpacingSupported) { + context.letterSpacing = letterSpacing + "px"; + } + return letterSpacingSupported; + } _enableTrace() { var that = this, len = CONTEXT_METHODS.length, diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index 8c47d9127..03434dc08 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -57,6 +57,7 @@ var AUTO = 'auto', PX_SPACE = 'px ', SPACE = ' ', RIGHT = 'right', + RTL = 'rtl', WORD = 'word', CHAR = 'char', NONE = 'none', @@ -213,6 +214,8 @@ export class Text extends Shape { context.setAttr('textAlign', LEFT); + var letterSpacingSupported = context.trySetLetterSpacing(letterSpacing); + // handle vertical alignment if (verticalAlign === MIDDLE) { alignY = (this.getHeight() - textArrLen * lineHeightPx - padding * 2) / 2; @@ -288,7 +291,7 @@ export class Text extends Shape { context.stroke(); context.restore(); } - if (letterSpacing !== 0 || align === JUSTIFY) { + if ((letterSpacing !== 0 && !letterSpacingSupported) || align === JUSTIFY) { // var words = text.split(' '); spacesNumber = text.split(' ').length - 1; var array = stringToArray(text); From 5570b4d62bbe258d641941864c5ac99cc207331c Mon Sep 17 00:00:00 2001 From: xkxx Date: Mon, 18 Sep 2023 22:54:22 +0000 Subject: [PATCH 3/6] Use native `letterSpacing` for RTL and use polyfill for all other cases. --- src/shapes/Text.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index 03434dc08..f8c7af06a 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -190,6 +190,7 @@ export class Text extends Shape { fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, verticalAlign = this.verticalAlign(), + direction = this.direction(), alignY = 0, align = this.align(), totalWidth = this.getWidth(), @@ -206,7 +207,7 @@ export class Text extends Shape { var lineTranslateX = 0; var lineTranslateY = 0; - context.setAttr('direction', this.direction()); + context.setAttr('direction', direction); context.setAttr('font', this._getContextFont()); @@ -214,7 +215,6 @@ export class Text extends Shape { context.setAttr('textAlign', LEFT); - var letterSpacingSupported = context.trySetLetterSpacing(letterSpacing); // handle vertical alignment if (verticalAlign === MIDDLE) { @@ -291,7 +291,10 @@ export class Text extends Shape { context.stroke(); context.restore(); } - if ((letterSpacing !== 0 && !letterSpacingSupported) || align === JUSTIFY) { + // As `letterSpacing` isn't supported on Safari, we use this polyfill. + // The exception is for RTL text, which we rely on native as it cannot + // be supported otherwise. + if (direction !== RTL && (letterSpacing !== 0 || align === JUSTIFY)) { // var words = text.split(' '); spacesNumber = text.split(' ').length - 1; var array = stringToArray(text); @@ -312,11 +315,13 @@ export class Text extends Shape { lineTranslateX += this.measureSize(letter).width + letterSpacing; } } else { + context.trySetLetterSpacing(letterSpacing); this._partialTextX = lineTranslateX; this._partialTextY = translateY + lineTranslateY; this._partialText = text; context.fillStrokeShape(this); + context.trySetLetterSpacing(0); } context.restore(); if (textArrLen > 1) { From 2ef0a38ef84c5d24922da34131529358a861f4d7 Mon Sep 17 00:00:00 2001 From: xkxx Date: Tue, 19 Sep 2023 01:08:16 +0000 Subject: [PATCH 4/6] Address PR comments. --- src/Context.ts | 22 ++++------------------ src/shapes/Text.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Context.ts b/src/Context.ts index b0c59e4dc..ba6eeaaaa 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -78,6 +78,7 @@ var CONTEXT_PROPERTIES = [ 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY', + 'letterSpacing', 'lineCap', 'lineDashOffset', 'lineJoin', @@ -94,8 +95,8 @@ var CONTEXT_PROPERTIES = [ const traceArrMax = 100; -interface CanvasRenderingContext2DFeatureDetection extends CanvasRenderingContext2D { - letterSpacing: string | undefined; +interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D { + letterSpacing: string; } /** @@ -712,21 +713,6 @@ export class Context { translate(x: number, y: number) { this._context.translate(x, y); } - /** - * Set letterSpacing if supported by browser. - * @method - * @name Konva.Context#trySetLetterSpacing - * @returns true if successful, false if not supported. - */ - trySetLetterSpacing(letterSpacing: number): boolean { - var context = this._context as CanvasRenderingContext2DFeatureDetection; - var letterSpacingSupported = 'letterSpacing' in context; - - if (letterSpacingSupported) { - context.letterSpacing = letterSpacing + "px"; - } - return letterSpacingSupported; - } _enableTrace() { var that = this, len = CONTEXT_METHODS.length, @@ -785,7 +771,7 @@ export class Context { // supported context properties type CanvasContextProps = Pick< - CanvasRenderingContext2D, + ExtendedCanvasRenderingContext2D, (typeof CONTEXT_PROPERTIES)[number] >; diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index f8c7af06a..7f7ec14be 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -207,7 +207,9 @@ export class Text extends Shape { var lineTranslateX = 0; var lineTranslateY = 0; - context.setAttr('direction', direction); + if (direction === RTL) { + context.setAttr('direction', direction); + } context.setAttr('font', this._getContextFont()); @@ -315,13 +317,14 @@ export class Text extends Shape { lineTranslateX += this.measureSize(letter).width + letterSpacing; } } else { - context.trySetLetterSpacing(letterSpacing); + if (letterSpacing !== 0) { + context.setAttr('letterSpacing', `${letterSpacing}px`); + } this._partialTextX = lineTranslateX; this._partialTextY = translateY + lineTranslateY; this._partialText = text; context.fillStrokeShape(this); - context.trySetLetterSpacing(0); } context.restore(); if (textArrLen > 1) { From ddba4d6db2531fbdb023acb9f801ffb67283e55c Mon Sep 17 00:00:00 2001 From: xkxx Date: Tue, 19 Sep 2023 22:09:10 +0000 Subject: [PATCH 5/6] Add tests for rtl text direction. --- test/unit/Text-test.ts | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/unit/Text-test.ts b/test/unit/Text-test.ts index 838b670de..f8b127aa9 100644 --- a/test/unit/Text-test.ts +++ b/test/unit/Text-test.ts @@ -1654,4 +1654,63 @@ describe('Text', function () { assert.equal(layer.getContext().getTrace(), trace); }); + + it('sets ltr text direction', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + stage.add(layer); + var text = new Konva.Text({ + text: 'ltr text', + direction: 'ltr', + }); + + layer.add(text); + layer.draw(); + + var trace = + 'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);font=normal normal 12px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();fillStyle=black;fillText(ltr text,0,6);restore();restore();'; + + assert.equal(layer.getContext().getTrace(), trace); + }); + + + it('sets rtl text direction', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + stage.add(layer); + var text = new Konva.Text({ + text: 'rtl text', + direction: 'rtl', + }); + + layer.add(text); + layer.draw(); + + var trace = + 'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);direction=rtl;font=normal normal 12px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();fillStyle=black;fillText(rtl text,0,6);restore();restore();'; + + assert.equal(layer.getContext().getTrace(), trace); + }); + + it('sets rtl text direction with letterSpacing', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + stage.add(layer); + var text = new Konva.Text({ + text: 'rtl text', + direction: 'rtl', + letterSpacing: 2, + }); + + layer.add(text); + layer.draw(); + + var trace = + 'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);direction=rtl;font=normal normal 12px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();letterSpacing=2px;fillStyle=black;fillText(rtl text,0,6);restore();restore();'; + + assert.equal(layer.getContext().getTrace(), trace); + }); }); From 6a5fc5211b168ad2a980c9a6d841b12c1d45ccde Mon Sep 17 00:00:00 2001 From: xkxx Date: Wed, 20 Sep 2023 17:16:46 +0000 Subject: [PATCH 6/6] Address PR comments --- src/Context.ts | 1 - src/shapes/Text.ts | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Context.ts b/src/Context.ts index ba6eeaaaa..40e21bce9 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -68,7 +68,6 @@ var COMMA = ',', 'strokeText', 'transform', 'translate', - 'trySetLetterSpacing', ]; var CONTEXT_PROPERTIES = [ diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index 7f7ec14be..3c034de27 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -42,6 +42,7 @@ export interface TextConfig extends ShapeConfig { var AUTO = 'auto', //CANVAS = 'canvas', CENTER = 'center', + INHERIT = 'inherit', JUSTIFY = 'justify', CHANGE_KONVA = 'Change.konva', CONTEXT_2D = '2d', @@ -136,7 +137,7 @@ function checkDefaultFill(config?: TextConfig) { * @memberof Konva * @augments Konva.Shape * @param {Object} config - * @param {String} [config.direction] default is ltr + * @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. @@ -200,6 +201,8 @@ export class Text extends Shape { shouldUnderline = textDecoration.indexOf('underline') !== -1, shouldLineThrough = textDecoration.indexOf('line-through') !== -1, n; + + direction = direction === INHERIT ? context.direction : direction; var translateY = 0; var translateY = lineHeightPx / 2; @@ -714,7 +717,7 @@ Factory.overWriteSetter(Text, 'height', getNumberOrAutoValidator()); * // set direction * text.direction('rtl'); */ -Factory.addGetterSetter(Text, 'direction', 'ltr'); +Factory.addGetterSetter(Text, 'direction', INHERIT); /** * get/set font family