Better unicode support in Konva.Text and Konva.TextPath. Emoji should work better now 👍. fix #690

This commit is contained in:
Anton Lavrenov
2020-09-14 09:46:26 -05:00
parent 800df5b110
commit 4b69631782
11 changed files with 170 additions and 65 deletions

View File

@@ -3,6 +3,8 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
* Better unicode support in `Konva.Text` and `Konva.TextPath`. Emoji should work better now 👍
## 7.1.0 ## 7.1.0
* Multi row support for `ellipsis` config for `Konva.Text` * Multi row support for `ellipsis` config for `Konva.Text`

112
konva.js
View File

@@ -8,7 +8,7 @@
* Konva JavaScript Framework v7.1.0 * Konva JavaScript Framework v7.1.0
* http://konvajs.org/ * http://konvajs.org/
* Licensed under the MIT * Licensed under the MIT
* Date: Mon Sep 07 2020 * Date: Mon Sep 14 2020
* *
* Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS) * Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS)
* Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva) * Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva)
@@ -1260,6 +1260,21 @@
}; };
} }
} }
function getNumberOrArrayOfNumbersValidator(noOfElements) {
if (Konva.isUnminified) {
return function (val, attr) {
var isNumber = Util._isNumber(val);
var isValidArray = Util._isArray(val) && val.length == noOfElements;
if (!isNumber && !isValidArray) {
Util.warn(_formatValue(val) +
' is a not valid value for "' +
attr +
'" attribute. The value should be a number or Array<number>(' + noOfElements + ')');
}
return val;
};
}
}
function getNumberOrAutoValidator() { function getNumberOrAutoValidator() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val, attr) { return function (val, attr) {
@@ -4833,7 +4848,7 @@
*/ */
addGetterSetter(Node, 'globalCompositeOperation', 'source-over', getStringValidator()); addGetterSetter(Node, 'globalCompositeOperation', 'source-over', getStringValidator());
/** /**
* get/set globalCompositeOperation of a shape * get/set globalCompositeOperation of a node. globalCompositeOperation DOESN'T affect hit graph of nodes. So they are still trigger to events as they have default "source-over" globalCompositeOperation.
* @name Konva.Node#globalCompositeOperation * @name Konva.Node#globalCompositeOperation
* @method * @method
* @param {String} type * @param {String} type
@@ -11378,59 +11393,50 @@
return _super !== null && _super.apply(this, arguments) || this; return _super !== null && _super.apply(this, arguments) || this;
} }
Tag.prototype._sceneFunc = function (context) { Tag.prototype._sceneFunc = function (context) {
var width = this.width(), height = this.height(), pointerDirection = this.pointerDirection(), pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), cornerRadius = Math.min(this.cornerRadius(), width / 2, height / 2); var width = this.width(), height = this.height(), pointerDirection = this.pointerDirection(), pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), cornerRadius = this.cornerRadius();
context.beginPath(); var topLeft = 0;
if (!cornerRadius) { var topRight = 0;
context.moveTo(0, 0); var bottomLeft = 0;
var bottomRight = 0;
if (typeof cornerRadius === 'number') {
topLeft = topRight = bottomLeft = bottomRight = Math.min(cornerRadius, width / 2, height / 2);
} }
else { else {
context.moveTo(cornerRadius, 0); topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2);
topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2);
bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2);
bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2);
} }
context.beginPath();
context.moveTo(topLeft, 0);
if (pointerDirection === UP) { if (pointerDirection === UP) {
context.lineTo((width - pointerWidth) / 2, 0); context.lineTo((width - pointerWidth) / 2, 0);
context.lineTo(width / 2, -1 * pointerHeight); context.lineTo(width / 2, -1 * pointerHeight);
context.lineTo((width + pointerWidth) / 2, 0); context.lineTo((width + pointerWidth) / 2, 0);
} }
if (!cornerRadius) { context.lineTo(width - topRight, 0);
context.lineTo(width, 0); context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false);
}
else {
context.lineTo(width - cornerRadius, 0);
context.arc(width - cornerRadius, cornerRadius, cornerRadius, (Math.PI * 3) / 2, 0, false);
}
if (pointerDirection === RIGHT) { if (pointerDirection === RIGHT) {
context.lineTo(width, (height - pointerHeight) / 2); context.lineTo(width, (height - pointerHeight) / 2);
context.lineTo(width + pointerWidth, height / 2); context.lineTo(width + pointerWidth, height / 2);
context.lineTo(width, (height + pointerHeight) / 2); context.lineTo(width, (height + pointerHeight) / 2);
} }
if (!cornerRadius) { context.lineTo(width, height - bottomRight);
context.lineTo(width, height); context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false);
}
else {
context.lineTo(width, height - cornerRadius);
context.arc(width - cornerRadius, height - cornerRadius, cornerRadius, 0, Math.PI / 2, false);
}
if (pointerDirection === DOWN) { if (pointerDirection === DOWN) {
context.lineTo((width + pointerWidth) / 2, height); context.lineTo((width + pointerWidth) / 2, height);
context.lineTo(width / 2, height + pointerHeight); context.lineTo(width / 2, height + pointerHeight);
context.lineTo((width - pointerWidth) / 2, height); context.lineTo((width - pointerWidth) / 2, height);
} }
if (!cornerRadius) { context.lineTo(bottomLeft, height);
context.lineTo(0, height); context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false);
}
else {
context.lineTo(cornerRadius, height);
context.arc(cornerRadius, height - cornerRadius, cornerRadius, Math.PI / 2, Math.PI, false);
}
if (pointerDirection === LEFT) { if (pointerDirection === LEFT) {
context.lineTo(0, (height + pointerHeight) / 2); context.lineTo(0, (height + pointerHeight) / 2);
context.lineTo(-1 * pointerWidth, height / 2); context.lineTo(-1 * pointerWidth, height / 2);
context.lineTo(0, (height - pointerHeight) / 2); context.lineTo(0, (height - pointerHeight) / 2);
} }
if (cornerRadius) { context.lineTo(0, topLeft);
context.lineTo(0, cornerRadius); context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false);
context.arc(cornerRadius, cornerRadius, cornerRadius, Math.PI, (Math.PI * 3) / 2, false);
}
context.closePath(); context.closePath();
context.fillStrokeShape(this); context.fillStrokeShape(this);
}; };
@@ -11500,8 +11506,12 @@
* @returns {Number} * @returns {Number}
* @example * @example
* tag.cornerRadius(20); * tag.cornerRadius(20);
*
* // set different corner radius values
* // top-left, top-right, bottom-right, bottom-left
* tag.cornerRadius([0, 10, 20, 30]);
*/ */
Factory.addGetterSetter(Tag, 'cornerRadius', 0, getNumberValidator()); Factory.addGetterSetter(Tag, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4));
Collection.mapMethods(Tag); Collection.mapMethods(Tag);
/** /**
@@ -12433,10 +12443,10 @@
topLeft = topRight = bottomLeft = bottomRight = Math.min(cornerRadius, width / 2, height / 2); topLeft = topRight = bottomLeft = bottomRight = Math.min(cornerRadius, width / 2, height / 2);
} }
else { else {
topLeft = Math.min(cornerRadius[0], width / 2, height / 2); topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2);
topRight = Math.min(cornerRadius[1], width / 2, height / 2); topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2);
bottomRight = Math.min(cornerRadius[2], width / 2, height / 2); bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2);
bottomLeft = Math.min(cornerRadius[3], width / 2, height / 2); bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2);
} }
context.moveTo(topLeft, 0); context.moveTo(topLeft, 0);
context.lineTo(width - topRight, 0); context.lineTo(width - topRight, 0);
@@ -12472,7 +12482,7 @@
* // top-left, top-right, bottom-right, bottom-left * // top-left, top-right, bottom-right, bottom-left
* rect.cornerRadius([0, 10, 20, 30]); * rect.cornerRadius([0, 10, 20, 30]);
*/ */
Factory.addGetterSetter(Rect, 'cornerRadius', 0); Factory.addGetterSetter(Rect, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4));
Collection.mapMethods(Rect); Collection.mapMethods(Rect);
/** /**
@@ -13329,6 +13339,13 @@
Factory.addGetterSetter(Star, 'outerRadius', 0, getNumberValidator()); Factory.addGetterSetter(Star, 'outerRadius', 0, getNumberValidator());
Collection.mapMethods(Star); Collection.mapMethods(Star);
function stringToArray(string) {
// we need to use `Array.from` because it can split unicode string correctly
// we also can use some regexp magic from lodash:
// https://github.com/lodash/lodash/blob/fb1f99d9d90ad177560d771bc5953a435b2dc119/lodash.toarray/index.js#L256
// but I decided it is too much code for that small fix
return Array.from(string);
}
// constants // constants
var AUTO = 'auto', var AUTO = 'auto',
//CANVAS = 'canvas', //CANVAS = 'canvas',
@@ -13569,8 +13586,9 @@
if (letterSpacing !== 0 || align === JUSTIFY) { if (letterSpacing !== 0 || align === JUSTIFY) {
// var words = text.split(' '); // var words = text.split(' ');
spacesNumber = text.split(' ').length - 1; spacesNumber = text.split(' ').length - 1;
for (var li = 0; li < text.length; li++) { var array = stringToArray(text);
var letter = text[li]; for (var li = 0; li < array.length; li++) {
var letter = array[li];
// skip justify for the last line // skip justify for the last line
if (letter === ' ' && n !== textArrLen - 1 && align === JUSTIFY) { if (letter === ' ' && n !== textArrLen - 1 && align === JUSTIFY) {
lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber; lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber;
@@ -14283,7 +14301,7 @@
_context.restore(); _context.restore();
return { return {
width: metrics.width, width: metrics.width,
height: parseInt(this.attrs.fontSize, 10) height: parseInt(this.attrs.fontSize, 10),
}; };
}; };
TextPath.prototype._setTextData = function () { TextPath.prototype._setTextData = function () {
@@ -14309,7 +14327,7 @@
if (align === 'right') { if (align === 'right') {
offset = Math.max(0, fullPathWidth - textFullWidth); offset = Math.max(0, fullPathWidth - textFullWidth);
} }
var charArr = this.text().split(''); var charArr = stringToArray(this.text());
var spacesNumber = this.text().split(' ').length - 1; var spacesNumber = this.text().split(' ').length - 1;
var p0, p1, pathCmd; var p0, p1, pathCmd;
var pIndex = -1; var pIndex = -1;
@@ -14333,7 +14351,7 @@
else if (pathData[j].command === 'M') { else if (pathData[j].command === 'M') {
p0 = { p0 = {
x: pathData[j].points[0], x: pathData[j].points[0],
y: pathData[j].points[1] y: pathData[j].points[1],
}; };
} }
} }
@@ -14485,7 +14503,7 @@
text: charArr[i], text: charArr[i],
rotation: rotation, rotation: rotation,
p0: p0, p0: p0,
p1: p1 p1: p1,
}); });
p0 = p1; p0 = p1;
} }
@@ -14496,7 +14514,7 @@
x: 0, x: 0,
y: 0, y: 0,
width: 0, width: 0,
height: 0 height: 0,
}; };
} }
var points = []; var points = [];
@@ -14524,7 +14542,7 @@
x: minX - fontSize / 2, x: minX - fontSize / 2,
y: minY - fontSize / 2, y: minY - fontSize / 2,
width: maxX - minX + fontSize, width: maxX - minX + fontSize,
height: maxY - minY + fontSize height: maxY - minY + fontSize,
}; };
}; };
return TextPath; return TextPath;
@@ -14989,7 +15007,7 @@
}); });
}; };
Transformer.prototype.getNodes = function () { Transformer.prototype.getNodes = function () {
return this._nodes; return this._nodes || [];
}; };
/** /**
* return the name of current active anchor * return the name of current active anchor

4
konva.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -2745,7 +2745,7 @@ addGetterSetter(
); );
/** /**
* get/set globalCompositeOperation of a shape * get/set globalCompositeOperation of a node. globalCompositeOperation DOESN'T affect hit graph of nodes. So they are still trigger to events as they have default "source-over" globalCompositeOperation.
* @name Konva.Node#globalCompositeOperation * @name Konva.Node#globalCompositeOperation
* @method * @method
* @param {String} type * @param {String} type

View File

@@ -12,6 +12,14 @@ import { _registerNode } from '../Global';
import { GetSet } from '../types'; import { GetSet } from '../types';
export function stringToArray(string: string) {
// we need to use `Array.from` because it can split unicode string correctly
// we also can use some regexp magic from lodash:
// https://github.com/lodash/lodash/blob/fb1f99d9d90ad177560d771bc5953a435b2dc119/lodash.toarray/index.js#L256
// but I decided it is too much code for that small fix
return Array.from(string);
}
export interface TextConfig extends ShapeConfig { export interface TextConfig extends ShapeConfig {
text?: string; text?: string;
fontFamily?: string; fontFamily?: string;
@@ -264,8 +272,9 @@ export class Text extends Shape<TextConfig> {
if (letterSpacing !== 0 || align === JUSTIFY) { if (letterSpacing !== 0 || align === JUSTIFY) {
// var words = text.split(' '); // var words = text.split(' ');
spacesNumber = text.split(' ').length - 1; spacesNumber = text.split(' ').length - 1;
for (var li = 0; li < text.length; li++) { var array = stringToArray(text);
var letter = text[li]; for (var li = 0; li < array.length; li++) {
var letter = array[li];
// skip justify for the last line // skip justify for the last line
if (letter === ' ' && n !== textArrLen - 1 && align === JUSTIFY) { if (letter === ' ' && n !== textArrLen - 1 && align === JUSTIFY) {
lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber; lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber;

View File

@@ -2,7 +2,7 @@ import { Util, Collection } from '../Util';
import { Factory } from '../Factory'; import { Factory } from '../Factory';
import { Shape, ShapeConfig } from '../Shape'; import { Shape, ShapeConfig } from '../Shape';
import { Path } from './Path'; import { Path } from './Path';
import { Text } from './Text'; import { Text, stringToArray } from './Text';
import { getNumberValidator } from '../Validators'; import { getNumberValidator } from '../Validators';
import { _registerNode } from '../Global'; import { _registerNode } from '../Global';
@@ -213,7 +213,7 @@ export class TextPath extends Shape<TextPathConfig> {
return { return {
width: metrics.width, width: metrics.width,
height: parseInt(this.attrs.fontSize, 10) height: parseInt(this.attrs.fontSize, 10),
}; };
} }
_setTextData() { _setTextData() {
@@ -248,7 +248,7 @@ export class TextPath extends Shape<TextPathConfig> {
offset = Math.max(0, fullPathWidth - textFullWidth); offset = Math.max(0, fullPathWidth - textFullWidth);
} }
var charArr = this.text().split(''); var charArr = stringToArray(this.text());
var spacesNumber = this.text().split(' ').length - 1; var spacesNumber = this.text().split(' ').length - 1;
var p0, p1, pathCmd; var p0, p1, pathCmd;
@@ -276,7 +276,7 @@ export class TextPath extends Shape<TextPathConfig> {
} else if (pathData[j].command === 'M') { } else if (pathData[j].command === 'M') {
p0 = { p0 = {
x: pathData[j].points[0], x: pathData[j].points[0],
y: pathData[j].points[1] y: pathData[j].points[1],
}; };
} }
} }
@@ -494,7 +494,7 @@ export class TextPath extends Shape<TextPathConfig> {
text: charArr[i], text: charArr[i],
rotation: rotation, rotation: rotation,
p0: p0, p0: p0,
p1: p1 p1: p1,
}); });
p0 = p1; p0 = p1;
} }
@@ -505,7 +505,7 @@ export class TextPath extends Shape<TextPathConfig> {
x: 0, x: 0,
y: 0, y: 0,
width: 0, width: 0,
height: 0 height: 0,
}; };
} }
var points = []; var points = [];
@@ -534,7 +534,7 @@ export class TextPath extends Shape<TextPathConfig> {
x: minX - fontSize / 2, x: minX - fontSize / 2,
y: minY - fontSize / 2, y: minY - fontSize / 2,
width: maxX - minX + fontSize, width: maxX - minX + fontSize,
height: maxY - minY + fontSize height: maxY - minY + fontSize,
}; };
} }

View File

@@ -234,7 +234,7 @@ beforeEach(function () {
this.currentTest.body.toLowerCase().indexOf('compare') !== -1 this.currentTest.body.toLowerCase().indexOf('compare') !== -1
) )
) { ) {
debugger; console.error(this.currentTest.title);
} }
}); });
@@ -250,7 +250,7 @@ afterEach(function () {
if (!isFailed && !isManual) { if (!isFailed && !isManual) {
Konva.stages.forEach(function (stage) { Konva.stages.forEach(function (stage) {
stage.destroy(); // stage.destroy();
}); });
if (Konva.DD._dragElements.size) { if (Konva.DD._dragElements.size) {
throw 'Why drag elements are not cleaned?'; throw 'Why drag elements are not cleaned?';

View File

@@ -21,6 +21,12 @@ suite('Container', function () {
layer.add(group); layer.add(group);
group.add(circle); group.add(circle);
layer.draw(); layer.draw();
var trace = layer.getContext().getTrace();
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);beginPath();rect(0,0,289,100);clip();transform(1,0,0,1,0,0);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);beginPath();rect(0,0,289,100);clip();transform(1,0,0,1,0,0);save();transform(1,0,0,1,289,100);beginPath();arc(0,0,70,0,6.283,false);closePath();fillStyle=green;fill();lineWidth=4;strokeStyle=black;stroke();restore();restore();'
);
}); });
// ====================================================== // ======================================================
@@ -203,6 +209,12 @@ suite('Container', function () {
layer.add(group); layer.add(group);
group.add(circle); group.add(circle);
layer.draw(); layer.draw();
var trace = layer.getContext().getTrace();
assert.equal(
trace,
'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,289,100);beginPath();arc(0,0,70,0,6.283,false);closePath();fillStyle=green;fill();lineWidth=4;strokeStyle=black;stroke();restore();'
);
}); });
// ====================================================== // ======================================================
@@ -223,6 +235,14 @@ suite('Container', function () {
group.add(circle); group.add(circle);
stage.add(layer); stage.add(layer);
layer.add(group); layer.add(group);
layer.draw();
var trace = layer.getContext().getTrace();
assert.equal(
trace,
'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,289,100);beginPath();arc(0,0,70,0,6.283,false);closePath();fillStyle=green;fill();lineWidth=4;strokeStyle=black;stroke();restore();'
);
}); });
// ====================================================== // ======================================================

View File

@@ -1813,6 +1813,12 @@ suite('Node', function () {
layer.add(circle); layer.add(circle);
stage.add(layer); stage.add(layer);
var trace = layer.getContext().getTrace();
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1.879,0.684,-0.342,0.94,14.581,42.306);beginPath();rect(0,0,100,50);closePath();fillStyle=green;fill();lineWidth=4;strokeStyle=black;stroke();restore();'
);
}); });
// ====================================================== // ======================================================

View File

@@ -137,6 +137,31 @@ suite('Text', function () {
compareLayerAndCanvas(layer, canvas, 254); compareLayerAndCanvas(layer, canvas, 254);
}); });
test('check emoji with letterSpacing', function () {
var stage = addStage();
var layer = new Konva.Layer();
var text = new Konva.Text({
x: 10,
y: 10,
text: '😬',
fontSize: 50,
letterSpacing: 1,
});
layer.add(text);
stage.add(layer);
var canvas = createCanvas();
var context = canvas.getContext('2d');
context.textBaseline = 'middle';
context.font = 'normal normal 50px Arial';
context.fillStyle = 'darkgrey';
context.fillText('😬', 10, 10 + 25);
compareLayerAndCanvas(layer, canvas, 254);
});
test('text cache with fill and shadow', function () { test('text cache with fill and shadow', function () {
var stage = addStage(); var stage = addStage();
var layer1 = new Konva.Layer(); var layer1 = new Konva.Layer();

View File

@@ -315,6 +315,31 @@ suite('TextPath', function () {
assert.equal(layer.getContext().getTrace(true), trace); assert.equal(layer.getContext().getTrace(true), trace);
}); });
test('Text path with emoji', function () {
var stage = addStage();
var layer = new Konva.Layer();
var c = 'M10,10 300, 10';
var textpath = new Konva.TextPath({
fill: 'black',
fontSize: 10,
fontFamily: 'Arial',
letterSpacing: 5,
text: '😬',
align: 'center',
data: c,
});
layer.add(textpath);
stage.add(layer);
var trace =
'clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);font=normal normal 10px Arial;textBaseline=middle;textAlign=left;save();save();translate(144.438,10);rotate(0);fillStyle=black;fillText(😬,0,0);restore();restore();restore();';
assert.equal(layer.getContext().getTrace(), trace);
});
test.skip('Text path with center align - arc', function () { test.skip('Text path with center align - arc', function () {
var stage = addStage(); var stage = addStage();
var layer = new Konva.Layer(); var layer = new Konva.Layer();