From adba0073e398593a912368aa0ec41715f821abf7 Mon Sep 17 00:00:00 2001 From: Anton Lavrevov Date: Mon, 23 Dec 2024 12:29:30 -0500 Subject: [PATCH] fixes in emoji rendering --- src/shapes/Text.ts | 50 +++++++++++++++++++++++++++--------------- test/unit/Text-test.ts | 29 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index 284590ae..a54b1d6a 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -18,10 +18,15 @@ export function stringToArray(string: string): string[] { return [...string].reduce((acc, char, index, array) => { // Handle emoji with skin tone modifiers and ZWJ sequences if (/\p{Emoji}/u.test(char)) { - if (acc.length > 0 && /\p{Emoji}/u.test(acc[acc.length - 1])) { - // Combine with previous emoji if it's part of a sequence - acc[acc.length - 1] += char; + // Check if next character is a modifier or ZWJ sequence + const nextChar = array[index + 1]; + if (nextChar && /\p{Emoji_Modifier}|\u200D/u.test(nextChar)) { + // If we have a modifier, combine with current emoji + acc.push(char + nextChar); + // Skip the next character since we've used it + array[index + 1] = ''; } else { + // No modifier - treat as separate emoji acc.push(char); } } @@ -36,7 +41,8 @@ export function stringToArray(string: string): string[] { acc[acc.length - 1] += char; } // Handle other characters - else { + else if (char) { + // Only push if not an empty string (skipped modifier) acc.push(char); } return acc; @@ -520,13 +526,16 @@ export class Text extends Shape { * that would fit in the specified width */ let low = 0, - high = line.length, + high = stringToArray(line).length, // Convert to array for proper emoji handling match = '', matchWidth = 0; while (low < high) { const mid = (low + high) >>> 1, - substr = line.slice(0, mid + 1), + // Convert array indices to string + lineArray = stringToArray(line), + substr = lineArray.slice(0, mid + 1).join(''), substrWidth = this._getTextWidth(substr) + additionalWidth; + if (substrWidth <= maxWidth) { low = mid + 1; match = substr; @@ -544,20 +553,24 @@ export class Text extends Shape { // a fitting substring was found if (wrapAtWord) { // try to find a space or dash where wrapping could be done - var wrapIndex; - const nextChar = line[match.length]; + const lineArray = stringToArray(line); + const matchArray = stringToArray(match); + const nextChar = lineArray[matchArray.length]; const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH; + + let wrapIndex; if (nextIsSpaceOrDash && matchWidth <= maxWidth) { - wrapIndex = match.length; + wrapIndex = matchArray.length; } else { - wrapIndex = - Math.max(match.lastIndexOf(SPACE), match.lastIndexOf(DASH)) + - 1; + // Find last space or dash in the array + const lastSpaceIndex = matchArray.lastIndexOf(SPACE); + const lastDashIndex = matchArray.lastIndexOf(DASH); + wrapIndex = Math.max(lastSpaceIndex, lastDashIndex) + 1; } + if (wrapIndex > 0) { - // re-cut the substring found at the space/dash position low = wrapIndex; - match = match.slice(0, low); + match = lineArray.slice(0, low).join(''); matchWidth = this._getTextWidth(match); } } @@ -578,13 +591,14 @@ export class Text extends Shape { */ break; } - line = line.slice(low); - line = line.trimLeft(); + + // Convert remaining text using array operations + const lineArray = stringToArray(line); + line = lineArray.slice(low).join('').trimLeft(); + if (line.length > 0) { - // Check if the remaining text would fit on one line lineWidth = this._getTextWidth(line); if (lineWidth <= maxWidth) { - // if it does, add the line and break out of the loop this._addTextLine(line); currentHeightPx += lineHeightPx; textWidth = Math.max(textWidth, lineWidth); diff --git a/test/unit/Text-test.ts b/test/unit/Text-test.ts index 206fe0f2..b2d7ea52 100644 --- a/test/unit/Text-test.ts +++ b/test/unit/Text-test.ts @@ -161,6 +161,35 @@ describe('Text', function () { context.fillStyle = 'darkgrey'; context.fillText('😬👧🏿', 10, 10 + 25); + compareLayerAndCanvas(layer, canvas, 254, 100); + }); + + it('check emoji rendering', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + var text = new Konva.Text({ + text: '😁😁😁', + x: 10, + y: 10, + fontSize: 20, + draggable: true, + width: 65, + fill: 'black', + scaleY: 0.9999999999999973, + }); + + layer.add(text); + stage.add(layer); + + var canvas = createCanvas(); + var context = canvas.getContext('2d'); + context.textBaseline = 'middle'; + context.font = 'normal normal 20px Arial'; + context.fillStyle = 'black'; + context.fillText('😁😁', 10, 10 + 10); + context.fillText('😁', 10, 10 + 30); + compareLayerAndCanvas(layer, canvas, 254); });