Merge branch 'konvajs:master' into master

This commit is contained in:
Peak 2025-03-11 14:47:15 +08:00 committed by GitHub
commit 7cbebb4844
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 486 additions and 338 deletions

View File

@ -12,7 +12,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [20.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -12,7 +12,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [23.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -3,9 +3,16 @@
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/).
## 9.3.17 (2024-12-02) (unreleased) ## 9.3.18 (2024-12-23)
- Fix `Arrow.getClientRect()` - Fixed emoji split in multiple lines
## 9.3.17 (2024-12-23)
- Fixed `Arrow.getClientRect()`
- Fixed emoji rendering with letterSpacing
- Fixed line-through for justify text
- Changes in letter spacing width calculations to match DOM rendering
## 9.3.16 (2024-10-21) ## 9.3.16 (2024-10-21)

3
eslint.config.mjs Normal file
View File

@ -0,0 +1,3 @@
import tseslint from 'typescript-eslint';
export default tseslint.config(...tseslint.configs.recommended);

View File

@ -1,6 +1,7 @@
{ {
"name": "konva", "name": "konva",
"version": "9.3.16", "version": "9.3.18",
"description": "HTML5 2d canvas library.",
"author": "Anton Lavrenov", "author": "Anton Lavrenov",
"files": [ "files": [
"README.md", "README.md",
@ -18,8 +19,8 @@
"build": "npm run compile && cp ./src/index-types.d.ts ./lib && gulp build && node ./rename-imports.mjs", "build": "npm run compile && cp ./src/index-types.d.ts ./lib && gulp build && node ./rename-imports.mjs",
"test:import": "npm run build && node ./test/import-test.cjs && node ./test/import-test.mjs", "test:import": "npm run build && node ./test/import-test.cjs && node ./test/import-test.mjs",
"test": "npm run test:browser && npm run test:node", "test": "npm run test:browser && npm run test:node",
"test:build": "parcel build ./test/unit-tests.html --dist-dir ./test-build --target none --public-url ./ --no-source-maps", "test:build": "PARCEL_WORKER_BACKEND=process parcel build ./test/unit-tests.html --dist-dir ./test-build --target none --public-url ./ --no-source-maps",
"test:browser": "npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security", "test:browser": "npm run test:build && mocha-headless-chrome -f ./test-build/unit-tests.html -a disable-web-security -a no-sandbox -a disable-setuid-sandbox",
"test:watch": "rm -rf ./.parcel-cache && PARCEL_WORKERS=0 parcel serve ./test/unit-tests.html ./test/manual-tests.html ./test/sandbox.html ./test/text-paths.html ./test/bunnies.html", "test:watch": "rm -rf ./.parcel-cache && PARCEL_WORKERS=0 parcel serve ./test/unit-tests.html ./test/manual-tests.html ./test/sandbox.html ./test/text-paths.html ./test/bunnies.html",
"test:node": "ts-mocha -r ./test/node-global-setup.mjs -p ./test/tsconfig.json test/unit/**/*.ts --exit && npm run test:import", "test:node": "ts-mocha -r ./test/node-global-setup.mjs -p ./test/tsconfig.json test/unit/**/*.ts --exit && npm run test:import",
"tsc": "tsc --removeComments", "tsc": "tsc --removeComments",
@ -59,13 +60,13 @@
} }
], ],
"devDependencies": { "devDependencies": {
"@parcel/transformer-image": "2.10.1", "@parcel/transformer-image": "2.13.2",
"@size-limit/preset-big-lib": "^11.0.1", "@size-limit/preset-big-lib": "^11.1.6",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.10",
"canvas": "^2.11.2", "canvas": "^3.1.0",
"chai": "4.3.10", "chai": "5.1.2",
"filehound": "^1.17.6", "filehound": "^1.17.6",
"gulp": "^4.0.2", "gulp": "^5.0.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-connect": "^5.7.0", "gulp-connect": "^5.7.0",
"gulp-exec": "^5.0.0", "gulp-exec": "^5.0.0",
@ -78,14 +79,14 @@
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"mocha": "10.2.0", "mocha": "10.2.0",
"mocha-headless-chrome": "^4.0.0", "mocha-headless-chrome": "^4.0.0",
"parcel": "2.10.1", "parcel": "2.13.3",
"process": "^0.11.10", "process": "^0.11.10",
"rollup": "^4.9.1", "rollup": "^4.31.0",
"rollup-plugin-typescript2": "^0.36.0", "rollup-plugin-typescript2": "^0.36.0",
"size-limit": "^11.0.1", "size-limit": "^11.1.6",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.3.3" "typescript": "^5.7.3"
}, },
"keywords": [ "keywords": [
"canvas", "canvas",

View File

@ -82,9 +82,32 @@ export class Canvas {
getContext() { getContext() {
return this.context; return this.context;
} }
/**
* get pixel ratio
* @method
* @name Konva.Canvas#getPixelRatio
* @returns {Number} pixel ratio
* @example
* var pixelRatio = layer.getCanvas.getPixelRatio();
*/
getPixelRatio() { getPixelRatio() {
return this.pixelRatio; return this.pixelRatio;
} }
/**
* set pixel ratio
* KonvaJS automatically handles pixel ratio adustments in order to render crisp drawings
* on all devices. Most desktops, low end tablets, and low end phones, have device pixel ratios
* of 1. Some high end tablets and phones, like iPhones and iPads have a device pixel ratio
* of 2. Some Macbook Pros, and iMacs also have a device pixel ratio of 2. Some high end Android devices have pixel
* ratios of 2 or 3. Some browsers like Firefox allow you to configure the pixel ratio of the viewport. Unless otherwise
* specificed, the pixel ratio will be defaulted to the actual device pixel ratio. You can override the device pixel
* ratio for special situations, or, if you don't want the pixel ratio to be taken into account, you can set it to 1.
* @method
* @name Konva.Canvas#setPixelRatio
* @param {Number} pixelRatio
* @example
* layer.getCanvas().setPixelRatio(3);
*/
setPixelRatio(pixelRatio) { setPixelRatio(pixelRatio) {
const previousRatio = this.pixelRatio; const previousRatio = this.pixelRatio;
this.pixelRatio = pixelRatio; this.pixelRatio = pixelRatio;
@ -148,28 +171,6 @@ export class Canvas {
} }
} }
/**
* get/set pixel ratio.
* KonvaJS automatically handles pixel ratio adustments in order to render crisp drawings
* on all devices. Most desktops, low end tablets, and low end phones, have device pixel ratios
* of 1. Some high end tablets and phones, like iPhones and iPads have a device pixel ratio
* of 2. Some Macbook Pros, and iMacs also have a device pixel ratio of 2. Some high end Android devices have pixel
* ratios of 2 or 3. Some browsers like Firefox allow you to configure the pixel ratio of the viewport. Unless otherwise
* specificed, the pixel ratio will be defaulted to the actual device pixel ratio. You can override the device pixel
* ratio for special situations, or, if you don't want the pixel ratio to be taken into account, you can set it to 1.
* @name Konva.Canvas#pixelRatio
* @method
* @param {Number} pixelRatio
* @returns {Number}
* @example
* // get
* var pixelRatio = layer.getCanvas.pixelRatio();
*
* // set
* layer.getCanvas().pixelRatio(3);
*/
Factory.addGetterSetter(Canvas, 'pixelRatio', undefined, getNumberValidator());
export class SceneCanvas extends Canvas { export class SceneCanvas extends Canvas {
constructor( constructor(
config: ICanvasConfig = { width: 0, height: 0, willReadFrequently: false } config: ICanvasConfig = { width: 0, height: 0, willReadFrequently: false }

View File

@ -1,18 +1,76 @@
import { Node } from './Node'; import { Node } from './Node';
import { GetSet } from './types';
import { Util } from './Util'; import { Util } from './Util';
import { getComponentValidator } from './Validators'; import { getComponentValidator } from './Validators';
const GET = 'get', const GET = 'get';
SET = 'set'; const SET = 'set';
/**
* Enforces that a type is a string.
*/
type EnforceString<T> = T extends string ? T : never;
/**
* Represents a class.
*/
type Constructor = abstract new (...args: any) => any;
/**
* An attribute of an instance of the provided class. Attributes names be strings.
*/
type Attr<T extends Constructor> = EnforceString<keyof InstanceType<T>>;
/**
* A function that is called after a setter is called.
*/
type AfterFunc<T extends Constructor> = (this: InstanceType<T>) => void;
/**
* Extracts the type of a GetSet.
*/
type ExtractGetSet<T> = T extends GetSet<infer U, any> ? U : never;
/**
* Extracts the type of a GetSet class attribute.
*/
type Value<T extends Constructor, U extends Attr<T>> = ExtractGetSet<
InstanceType<T>[U]
>;
/**
* A function that validates a value.
*/
type ValidatorFunc<T> = (val: ExtractGetSet<T>, attr: string) => T;
/**
* Extracts the "components" (keys) of a GetSet value. The value must be an object.
*/
type ExtractComponents<T extends Constructor, U extends Attr<T>> = Value<
T,
U
> extends Record<string, any>
? EnforceString<keyof Value<T, U>>[]
: never;
export const Factory = { export const Factory = {
addGetterSetter(constructor, attr, def?, validator?, after?) { addGetterSetter<T extends Constructor, U extends Attr<T>>(
constructor: T,
attr: U,
def?: Value<T, U>,
validator?: ValidatorFunc<Value<T, U>>,
after?: AfterFunc<T>
): void {
Factory.addGetter(constructor, attr, def); Factory.addGetter(constructor, attr, def);
Factory.addSetter(constructor, attr, validator, after); Factory.addSetter(constructor, attr, validator, after);
Factory.addOverloadedGetterSetter(constructor, attr); Factory.addOverloadedGetterSetter(constructor, attr);
}, },
addGetter(constructor, attr, def?) { addGetter<T extends Constructor, U extends Attr<T>>(
const method = GET + Util._capitalize(attr); constructor: T,
attr: U,
def?: Value<T, U>
) {
var method = GET + Util._capitalize(attr);
constructor.prototype[method] = constructor.prototype[method] =
constructor.prototype[method] || constructor.prototype[method] ||
@ -22,15 +80,26 @@ export const Factory = {
}; };
}, },
addSetter(constructor, attr, validator?, after?) { addSetter<T extends Constructor, U extends Attr<T>>(
const method = SET + Util._capitalize(attr); constructor: T,
attr: U,
validator?: ValidatorFunc<Value<T, U>>,
after?: AfterFunc<T>
) {
var method = SET + Util._capitalize(attr);
if (!constructor.prototype[method]) { if (!constructor.prototype[method]) {
Factory.overWriteSetter(constructor, attr, validator, after); Factory.overWriteSetter(constructor, attr, validator, after);
} }
}, },
overWriteSetter(constructor, attr, validator?, after?) {
const method = SET + Util._capitalize(attr); overWriteSetter<T extends Constructor, U extends Attr<T>>(
constructor: T,
attr: U,
validator?: ValidatorFunc<Value<T, U>>,
after?: AfterFunc<T>
) {
var method = SET + Util._capitalize(attr);
constructor.prototype[method] = function (val) { constructor.prototype[method] = function (val) {
if (validator && val !== undefined && val !== null) { if (validator && val !== undefined && val !== null) {
val = validator.call(this, val, attr); val = validator.call(this, val, attr);
@ -45,12 +114,13 @@ export const Factory = {
return this; return this;
}; };
}, },
addComponentsGetterSetter(
constructor, addComponentsGetterSetter<T extends Constructor, U extends Attr<T>>(
attr: string, constructor: T,
components: Array<string>, attr: U,
validator?: Function, components: ExtractComponents<T, U>,
after?: Function validator?: ValidatorFunc<Value<T, U>>,
after?: AfterFunc<T>
) { ) {
const len = components.length, const len = components.length,
capitalize = Util._capitalize, capitalize = Util._capitalize,
@ -59,7 +129,7 @@ export const Factory = {
// getter // getter
constructor.prototype[getter] = function () { constructor.prototype[getter] = function () {
const ret = {}; const ret: Record<string, any> = {};
for (let n = 0; n < len; n++) { for (let n = 0; n < len; n++) {
const component = components[n]; const component = components[n];
@ -76,7 +146,7 @@ export const Factory = {
const oldVal = this.attrs[attr]; const oldVal = this.attrs[attr];
if (validator) { if (validator) {
val = validator.call(this, val); val = validator.call(this, val, attr);
} }
if (basicValidator) { if (basicValidator) {
@ -106,8 +176,11 @@ export const Factory = {
Factory.addOverloadedGetterSetter(constructor, attr); Factory.addOverloadedGetterSetter(constructor, attr);
}, },
addOverloadedGetterSetter(constructor, attr) { addOverloadedGetterSetter<T extends Constructor, U extends Attr<T>>(
const capitalizedAttr = Util._capitalize(attr), constructor: T,
attr: U
) {
var capitalizedAttr = Util._capitalize(attr),
setter = SET + capitalizedAttr, setter = SET + capitalizedAttr,
getter = GET + capitalizedAttr; getter = GET + capitalizedAttr;
@ -121,7 +194,12 @@ export const Factory = {
return this[getter](); return this[getter]();
}; };
}, },
addDeprecatedGetterSetter(constructor, attr, def, validator) { addDeprecatedGetterSetter<T extends Constructor, U extends Attr<T>>(
constructor: T,
attr: U,
def: Value<T, U>,
validator: ValidatorFunc<Value<T, U>>
) {
Util.error('Adding deprecated ' + attr); Util.error('Adding deprecated ' + attr);
const method = GET + Util._capitalize(attr); const method = GET + Util._capitalize(attr);
@ -139,7 +217,10 @@ export const Factory = {
}); });
Factory.addOverloadedGetterSetter(constructor, attr); Factory.addOverloadedGetterSetter(constructor, attr);
}, },
backCompat(constructor, methods) { backCompat<T extends Constructor>(
constructor: T,
methods: Record<string, string>
) {
Util.each(methods, function (oldMethodName, newMethodName) { Util.each(methods, function (oldMethodName, newMethodName) {
const method = constructor.prototype[newMethodName]; const method = constructor.prototype[newMethodName];
const oldGetter = GET + Util._capitalize(oldMethodName); const oldGetter = GET + Util._capitalize(oldMethodName);
@ -161,7 +242,7 @@ export const Factory = {
constructor.prototype[oldSetter] = deprecated; constructor.prototype[oldSetter] = deprecated;
}); });
}, },
afterSetFilter(this: Node) { afterSetFilter(this: Node): void {
this._filterUpToDate = false; this._filterUpToDate = false;
}, },
}; };

View File

@ -198,7 +198,8 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
// for transform the cache can be NOT empty // for transform the cache can be NOT empty
// but we still need to recalculate it if it is dirty // but we still need to recalculate it if it is dirty
const isTransform = attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM; const isTransform = attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM;
const invalid = cache === undefined || (isTransform && cache.dirty === true); const invalid =
cache === undefined || (isTransform && cache.dirty === true);
// if not cached, we need to set it using the private getter method. // if not cached, we need to set it using the private getter method.
if (invalid) { if (invalid) {
@ -2660,7 +2661,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
rotation: GetSet<number, this>; rotation: GetSet<number, this>;
zIndex: GetSet<number, this>; zIndex: GetSet<number, this>;
scale: GetSet<Vector2d | undefined, this>; scale: GetSet<Vector2d, this>;
scaleX: GetSet<number, this>; scaleX: GetSet<number, this>;
scaleY: GetSet<number, this>; scaleY: GetSet<number, this>;
skew: GetSet<Vector2d, this>; skew: GetSet<Vector2d, this>;
@ -3102,7 +3103,7 @@ addGetterSetter(Node, 'offsetY', 0, getNumberValidator());
* node.offsetY(3); * node.offsetY(3);
*/ */
addGetterSetter(Node, 'dragDistance', null, getNumberValidator()); addGetterSetter(Node, 'dragDistance', undefined, getNumberValidator());
/** /**
* get/set drag distance * get/set drag distance
@ -3193,7 +3194,7 @@ addGetterSetter(Node, 'listening', true, getBooleanValidator());
addGetterSetter(Node, 'preventDefault', true, getBooleanValidator()); addGetterSetter(Node, 'preventDefault', true, getBooleanValidator());
addGetterSetter(Node, 'filters', null, function (this: Node, val) { addGetterSetter(Node, 'filters', undefined, function (this: Node, val) {
this._filterUpToDate = false; this._filterUpToDate = false;
return val; return val;
}); });

View File

@ -837,6 +837,11 @@ export class Shape<
strokeLinearGradientStartPoint: GetSet<Vector2d, this>; strokeLinearGradientStartPoint: GetSet<Vector2d, this>;
strokeLinearGradientEndPoint: GetSet<Vector2d, this>; strokeLinearGradientEndPoint: GetSet<Vector2d, this>;
strokeLinearGradientColorStops: GetSet<Array<number | string>, this>; strokeLinearGradientColorStops: GetSet<Array<number | string>, this>;
strokeLinearGradientStartPointX: GetSet<number, this>;
strokeLinearGradientStartPointY: GetSet<number, this>;
strokeLinearGradientEndPointX: GetSet<number, this>;
strokeLinearGradientEndPointY: GetSet<number, this>;
fillRule: GetSet<CanvasFillRule, this>;
} }
Shape.prototype._fillFunc = _fillFunc; Shape.prototype._fillFunc = _fillFunc;

View File

@ -249,11 +249,10 @@ export class Stage extends Container<Layer> {
* @name Konva.Stage#clear * @name Konva.Stage#clear
*/ */
clear() { clear() {
let layers = this.children, const layers = this.children,
len = layers.length, len = layers.length;
n;
for (n = 0; n < len; n++) { for (let n = 0; n < len; n++) {
layers[n].clear(); layers[n].clear();
} }
return this; return this;
@ -367,12 +366,11 @@ export class Stage extends Container<Layer> {
if (!pos) { if (!pos) {
return null; return null;
} }
let layers = this.children, const layers = this.children,
len = layers.length, len = layers.length,
end = len - 1, end = len - 1;
n;
for (n = end; n >= 0; n--) { for (let n = end; n >= 0; n--) {
const shape = layers[n].getIntersection(pos); const shape = layers[n].getIntersection(pos);
if (shape) { if (shape) {
return shape; return shape;
@ -820,8 +818,8 @@ export class Stage extends Container<Layer> {
* }); * });
*/ */
setPointersPositions(evt) { setPointersPositions(evt) {
let contentPosition = this._getContentPosition(), const contentPosition = this._getContentPosition();
x: number | null = null, let x: number | null = null,
y: number | null = null; y: number | null = null;
evt = evt ? evt : window.event; evt = evt ? evt : window.event;

View File

@ -4,7 +4,7 @@ import { Node, NodeConfig } from './Node';
import { Konva } from './Global'; import { Konva } from './Global';
import { Line } from './shapes/Line'; import { Line } from './shapes/Line';
let blacklist = { const blacklist = {
node: 1, node: 1,
duration: 1, duration: 1,
easing: 1, easing: 1,
@ -14,8 +14,8 @@ let blacklist = {
PAUSED = 1, PAUSED = 1,
PLAYING = 2, PLAYING = 2,
REVERSING = 3, REVERSING = 3,
idCounter = 0,
colorAttrs = ['fill', 'stroke', 'shadowColor']; colorAttrs = ['fill', 'stroke', 'shadowColor'];
let idCounter = 0;
class TweenEngine { class TweenEngine {
prop: string; prop: string;
@ -195,12 +195,12 @@ export class Tween {
onUpdate: Function | undefined; onUpdate: Function | undefined;
constructor(config: TweenConfig) { constructor(config: TweenConfig) {
let that = this, const that = this,
node = config.node as any, node = config.node as any,
nodeId = node._id, nodeId = node._id,
duration,
easing = config.easing || Easings.Linear, easing = config.easing || Easings.Linear,
yoyo = !!config.yoyo, yoyo = !!config.yoyo;
let duration,
key; key;
if (typeof config.duration === 'undefined') { if (typeof config.duration === 'undefined') {
@ -266,26 +266,23 @@ export class Tween {
this.onUpdate = config.onUpdate; this.onUpdate = config.onUpdate;
} }
_addAttr(key, end) { _addAttr(key, end) {
let node = this.node, const node = this.node,
nodeId = node._id, nodeId = node._id;
start, let diff,
diff,
tweenId,
n,
len, len,
trueEnd, trueEnd,
trueStart, trueStart,
endRGBA; endRGBA;
// remove conflict from tween map if it exists // remove conflict from tween map if it exists
tweenId = Tween.tweens[nodeId][key]; const tweenId = Tween.tweens[nodeId][key];
if (tweenId) { if (tweenId) {
delete Tween.attrs[nodeId][tweenId][key]; delete Tween.attrs[nodeId][tweenId][key];
} }
// add to tween map // add to tween map
start = node.getAttr(key); let start = node.getAttr(key);
if (Util._isArray(end)) { if (Util._isArray(end)) {
diff = []; diff = [];
@ -310,7 +307,7 @@ export class Tween {
} }
if (key.indexOf('fill') === 0) { if (key.indexOf('fill') === 0) {
for (n = 0; n < len; n++) { for (let n = 0; n < len; n++) {
if (n % 2 === 0) { if (n % 2 === 0) {
diff.push(end[n] - start[n]); diff.push(end[n] - start[n]);
} else { } else {
@ -326,7 +323,7 @@ export class Tween {
} }
} }
} else { } else {
for (n = 0; n < len; n++) { for (let n = 0; n < len; n++) {
diff.push(end[n] - start[n]); diff.push(end[n] - start[n]);
} }
} }
@ -353,9 +350,9 @@ export class Tween {
Tween.tweens[nodeId][key] = this._id; Tween.tweens[nodeId][key] = this._id;
} }
_tweenFunc(i) { _tweenFunc(i) {
let node = this.node, const node = this.node,
attrs = Tween.attrs[node._id][this._id], attrs = Tween.attrs[node._id][this._id];
key, let key,
attr, attr,
start, start,
diff, diff,
@ -525,14 +522,13 @@ export class Tween {
* @name Konva.Tween#destroy * @name Konva.Tween#destroy
*/ */
destroy() { destroy() {
let nodeId = this.node._id, const nodeId = this.node._id,
thisId = this._id, thisId = this._id,
attrs = Tween.tweens[nodeId], attrs = Tween.tweens[nodeId];
key;
this.pause(); this.pause();
for (key in attrs) { for (const key in attrs) {
delete Tween.tweens[nodeId][key]; delete Tween.tweens[nodeId][key];
} }

View File

@ -260,7 +260,7 @@ export class Transform {
} }
// CONSTANTS // CONSTANTS
let OBJECT_ARRAY = '[object Array]', const OBJECT_ARRAY = '[object Array]',
OBJECT_NUMBER = '[object Number]', OBJECT_NUMBER = '[object Number]',
OBJECT_STRING = '[object String]', OBJECT_STRING = '[object String]',
OBJECT_BOOLEAN = '[object Boolean]', OBJECT_BOOLEAN = '[object Boolean]',
@ -423,8 +423,8 @@ let OBJECT_ARRAY = '[object Array]',
yellow: [255, 255, 0], yellow: [255, 255, 0],
yellowgreen: [154, 205, 5], yellowgreen: [154, 205, 5],
}, },
RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/, RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/;
animQueue: Array<Function> = []; let animQueue: Array<Function> = [];
const req = const req =
(typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) || (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) ||
@ -904,21 +904,20 @@ export const Util = {
return pc; return pc;
}, },
_prepareArrayForTween(startArray, endArray, isClosed) { _prepareArrayForTween(startArray, endArray, isClosed) {
let n, const start: Vector2d[] = [],
start: Vector2d[] = [],
end: Vector2d[] = []; end: Vector2d[] = [];
if (startArray.length > endArray.length) { if (startArray.length > endArray.length) {
const temp = endArray; const temp = endArray;
endArray = startArray; endArray = startArray;
startArray = temp; startArray = temp;
} }
for (n = 0; n < startArray.length; n += 2) { for (let n = 0; n < startArray.length; n += 2) {
start.push({ start.push({
x: startArray[n], x: startArray[n],
y: startArray[n + 1], y: startArray[n + 1],
}); });
} }
for (n = 0; n < endArray.length; n += 2) { for (let n = 0; n < endArray.length; n += 2) {
end.push({ end.push({
x: endArray[n], x: endArray[n],
y: endArray[n + 1], y: endArray[n + 1],

View File

@ -33,9 +33,9 @@ export function alphaComponent(val: number) {
return val; return val;
} }
export function getNumberValidator() { export function getNumberValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function <T>(val: T, attr: string): T | void { return function (val: T, attr: string): T {
if (!Util._isNumber(val)) { if (!Util._isNumber(val)) {
Util.warn( Util.warn(
_formatValue(val) + _formatValue(val) +
@ -49,11 +49,11 @@ export function getNumberValidator() {
} }
} }
export function getNumberOrArrayOfNumbersValidator(noOfElements: number) { export function getNumberOrArrayOfNumbersValidator<T>(noOfElements: number) {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function <T>(val: T, attr: string): T | void { return function (val: T, attr: string): T {
const isNumber = Util._isNumber(val); let isNumber = Util._isNumber(val);
const isValidArray = Util._isArray(val) && val.length == noOfElements; let isValidArray = Util._isArray(val) && val.length == noOfElements;
if (!isNumber && !isValidArray) { if (!isNumber && !isValidArray) {
Util.warn( Util.warn(
_formatValue(val) + _formatValue(val) +
@ -69,11 +69,11 @@ export function getNumberOrArrayOfNumbersValidator(noOfElements: number) {
} }
} }
export function getNumberOrAutoValidator() { export function getNumberOrAutoValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function <T extends string>(val: T, attr: string): T | void { return function (val: T, attr: string): T {
const isNumber = Util._isNumber(val); var isNumber = Util._isNumber(val);
const isAuto = val === 'auto'; var isAuto = val === 'auto';
if (!(isNumber || isAuto)) { if (!(isNumber || isAuto)) {
Util.warn( Util.warn(
@ -88,9 +88,9 @@ export function getNumberOrAutoValidator() {
} }
} }
export function getStringValidator() { export function getStringValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val: any, attr: string) { return function (val: T, attr: string): T {
if (!Util._isString(val)) { if (!Util._isString(val)) {
Util.warn( Util.warn(
_formatValue(val) + _formatValue(val) +
@ -104,13 +104,13 @@ export function getStringValidator() {
} }
} }
export function getStringOrGradientValidator() { export function getStringOrGradientValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val: any, attr: string) { return function (val: T, attr: string): T {
const isString = Util._isString(val); const isString = Util._isString(val);
const isGradient = const isGradient =
Object.prototype.toString.call(val) === '[object CanvasGradient]' || Object.prototype.toString.call(val) === '[object CanvasGradient]' ||
(val && val.addColorStop); (val && val['addColorStop']);
if (!(isString || isGradient)) { if (!(isString || isGradient)) {
Util.warn( Util.warn(
_formatValue(val) + _formatValue(val) +
@ -124,9 +124,9 @@ export function getStringOrGradientValidator() {
} }
} }
export function getFunctionValidator() { export function getFunctionValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val: any, attr: string) { return function (val: T, attr: string): T {
if (!Util._isFunction(val)) { if (!Util._isFunction(val)) {
Util.warn( Util.warn(
_formatValue(val) + _formatValue(val) +
@ -139,9 +139,9 @@ export function getFunctionValidator() {
}; };
} }
} }
export function getNumberArrayValidator() { export function getNumberArrayValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val: any, attr: string) { return function (val: T, attr: string): T {
// Retrieve TypedArray constructor as found in MDN (if TypedArray is available) // Retrieve TypedArray constructor as found in MDN (if TypedArray is available)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
const TypedArray = Int8Array ? Object.getPrototypeOf(Int8Array) : null; const TypedArray = Int8Array ? Object.getPrototypeOf(Int8Array) : null;
@ -172,10 +172,10 @@ export function getNumberArrayValidator() {
}; };
} }
} }
export function getBooleanValidator() { export function getBooleanValidator<T>() {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val: any, attr: string) { return function (val: T, attr: string): T {
const isBool = val === true || val === false; var isBool = val === true || val === false;
if (!isBool) { if (!isBool) {
Util.warn( Util.warn(
_formatValue(val) + _formatValue(val) +
@ -188,9 +188,9 @@ export function getBooleanValidator() {
}; };
} }
} }
export function getComponentValidator(components: any) { export function getComponentValidator<T>(components: string[]) {
if (Konva.isUnminified) { if (Konva.isUnminified) {
return function (val: any, attr: string) { return function (val: T, attr: string): T {
// ignore validation on undefined value, because it will reset to defalt // ignore validation on undefined value, because it will reset to defalt
if (val === undefined || val === null) { if (val === undefined || val === null) {
return val; return val;

View File

@ -174,7 +174,7 @@ Factory.addGetterSetter(
Node, Node,
'embossDirection', 'embossDirection',
'top-left', 'top-left',
null, undefined,
Factory.afterSetFilter Factory.afterSetFilter
); );
/** /**
@ -190,7 +190,7 @@ Factory.addGetterSetter(
Node, Node,
'embossBlend', 'embossBlend',
false, false,
null, undefined,
Factory.afterSetFilter Factory.afterSetFilter
); );
/** /**

View File

@ -59,13 +59,12 @@ Factory.addGetterSetter(
*/ */
export const HSL: Filter = function (imageData) { export const HSL: Filter = function (imageData) {
let data = imageData.data, const data = imageData.data,
nPixels = data.length, nPixels = data.length,
v = 1, v = 1,
s = Math.pow(2, this.saturation()), s = Math.pow(2, this.saturation()),
h = Math.abs(this.hue() + 360) % 360, h = Math.abs(this.hue() + 360) % 360,
l = this.luminance() * 127, l = this.luminance() * 127;
i;
// Basis for the technique used: // Basis for the technique used:
// http://beesbuzz.biz/code/hsv_color_transforms.php // http://beesbuzz.biz/code/hsv_color_transforms.php
@ -94,7 +93,7 @@ export const HSL: Filter = function (imageData) {
let r: number, g: number, b: number, a: number; let r: number, g: number, b: number, a: number;
for (i = 0; i < nPixels; i += 4) { for (let i = 0; i < nPixels; i += 4) {
r = data[i + 0]; r = data[i + 0];
g = data[i + 1]; g = data[i + 1];
b = data[i + 2]; b = data[i + 2];

View File

@ -9,11 +9,10 @@ import { Filter } from '../Node';
* node.filters([Konva.Filters.Invert]); * node.filters([Konva.Filters.Invert]);
*/ */
export const Invert: Filter = function (imageData) { export const Invert: Filter = function (imageData) {
let data = imageData.data, const data = imageData.data,
len = data.length, len = data.length;
i;
for (i = 0; i < len; i += 4) { for (let i = 0; i < len; i += 4) {
// red // red
data[i] = 255 - data[i]; data[i] = 255 - data[i];
// green // green

View File

@ -1,5 +1,5 @@
import { Factory } from '../Factory'; import { Factory } from '../Factory';
import { Node, Filter } from '../Node'; import { Filter, Node } from '../Node';
import { Util } from '../Util'; import { Util } from '../Util';
import { getNumberValidator } from '../Validators'; import { getNumberValidator } from '../Validators';
@ -20,53 +20,41 @@ import { getNumberValidator } from '../Validators';
*/ */
const ToPolar = function (src, dst, opt) { const ToPolar = function (src, dst, opt) {
let srcPixels = src.data, const srcPixels = src.data,
dstPixels = dst.data, dstPixels = dst.data,
xSize = src.width, xSize = src.width,
ySize = src.height, ySize = src.height,
xMid = opt.polarCenterX || xSize / 2, xMid = opt.polarCenterX || xSize / 2,
yMid = opt.polarCenterY || ySize / 2, yMid = opt.polarCenterY || ySize / 2;
i,
x,
y,
r = 0,
g = 0,
b = 0,
a = 0;
// Find the largest radius // Find the largest radius
let rad, let rMax = Math.sqrt(xMid * xMid + yMid * yMid);
rMax = Math.sqrt(xMid * xMid + yMid * yMid); let x = xSize - xMid;
x = xSize - xMid; let y = ySize - yMid;
y = ySize - yMid; const rad = Math.sqrt(x * x + y * y);
rad = Math.sqrt(x * x + y * y);
rMax = rad > rMax ? rad : rMax; rMax = rad > rMax ? rad : rMax;
// We'll be uisng y as the radius, and x as the angle (theta=t) // We'll be uisng y as the radius, and x as the angle (theta=t)
let rSize = ySize, const rSize = ySize,
tSize = xSize, tSize = xSize;
radius,
theta;
// We want to cover all angles (0-360) and we need to convert to // We want to cover all angles (0-360) and we need to convert to
// radians (*PI/180) // radians (*PI/180)
let conversion = ((360 / tSize) * Math.PI) / 180, const conversion = ((360 / tSize) * Math.PI) / 180;
sin,
cos;
// var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale; // var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale;
for (theta = 0; theta < tSize; theta += 1) { for (let theta = 0; theta < tSize; theta += 1) {
sin = Math.sin(theta * conversion); const sin = Math.sin(theta * conversion);
cos = Math.cos(theta * conversion); const cos = Math.cos(theta * conversion);
for (radius = 0; radius < rSize; radius += 1) { for (let radius = 0; radius < rSize; radius += 1) {
x = Math.floor(xMid + ((rMax * radius) / rSize) * cos); x = Math.floor(xMid + ((rMax * radius) / rSize) * cos);
y = Math.floor(yMid + ((rMax * radius) / rSize) * sin); y = Math.floor(yMid + ((rMax * radius) / rSize) * sin);
i = (y * xSize + x) * 4; let i = (y * xSize + x) * 4;
r = srcPixels[i + 0]; const r = srcPixels[i + 0];
g = srcPixels[i + 1]; const g = srcPixels[i + 1];
b = srcPixels[i + 2]; const b = srcPixels[i + 2];
a = srcPixels[i + 3]; const a = srcPixels[i + 3];
// Store it // Store it
//i = (theta * xSize + radius) * 4; //i = (theta * xSize + radius) * 4;
@ -97,35 +85,23 @@ const ToPolar = function (src, dst, opt) {
*/ */
const FromPolar = function (src, dst, opt) { const FromPolar = function (src, dst, opt) {
let srcPixels = src.data, const srcPixels = src.data,
dstPixels = dst.data, dstPixels = dst.data,
xSize = src.width, xSize = src.width,
ySize = src.height, ySize = src.height,
xMid = opt.polarCenterX || xSize / 2, xMid = opt.polarCenterX || xSize / 2,
yMid = opt.polarCenterY || ySize / 2, yMid = opt.polarCenterY || ySize / 2;
i,
x,
y,
dx,
dy,
r = 0,
g = 0,
b = 0,
a = 0;
// Find the largest radius // Find the largest radius
let rad, let rMax = Math.sqrt(xMid * xMid + yMid * yMid);
rMax = Math.sqrt(xMid * xMid + yMid * yMid); let x = xSize - xMid;
x = xSize - xMid; let y = ySize - yMid;
y = ySize - yMid; const rad = Math.sqrt(x * x + y * y);
rad = Math.sqrt(x * x + y * y);
rMax = rad > rMax ? rad : rMax; rMax = rad > rMax ? rad : rMax;
// We'll be uisng x as the radius, and y as the angle (theta=t) // We'll be uisng x as the radius, and y as the angle (theta=t)
let rSize = ySize, const rSize = ySize,
tSize = xSize, tSize = xSize,
radius,
theta,
phaseShift = opt.polarRotation || 0; phaseShift = opt.polarRotation || 0;
// We need to convert to degrees and we need to make sure // We need to convert to degrees and we need to make sure
@ -137,18 +113,18 @@ const FromPolar = function (src, dst, opt) {
for (x = 0; x < xSize; x += 1) { for (x = 0; x < xSize; x += 1) {
for (y = 0; y < ySize; y += 1) { for (y = 0; y < ySize; y += 1) {
dx = x - xMid; const dx = x - xMid;
dy = y - yMid; const dy = y - yMid;
radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax; const radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax;
theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360; let theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360;
theta = (theta * tSize) / 360; theta = (theta * tSize) / 360;
x1 = Math.floor(theta); x1 = Math.floor(theta);
y1 = Math.floor(radius); y1 = Math.floor(radius);
i = (y1 * xSize + x1) * 4; let i = (y1 * xSize + x1) * 4;
r = srcPixels[i + 0]; const r = srcPixels[i + 0];
g = srcPixels[i + 1]; const g = srcPixels[i + 1];
b = srcPixels[i + 2]; const b = srcPixels[i + 2];
a = srcPixels[i + 3]; const a = srcPixels[i + 3];
// Store it // Store it
i = (y * xSize + x) * 4; i = (y * xSize + x) * 4;

View File

@ -1,5 +1,5 @@
import { Factory } from '../Factory'; import { Factory } from '../Factory';
import { Node, Filter } from '../Node'; import { Filter, Node } from '../Node';
import { getNumberValidator } from '../Validators'; import { getNumberValidator } from '../Validators';
function pixelAt(idata, x, y) { function pixelAt(idata, x, y) {
@ -181,8 +181,8 @@ function smoothEdgeMask(mask, sw, sh) {
*/ */
export const Mask: Filter = function (imageData) { export const Mask: Filter = function (imageData) {
// Detect pixels close to the background color // Detect pixels close to the background color
let threshold = this.threshold(), const threshold = this.threshold();
mask = backgroundMask(imageData, threshold); let mask = backgroundMask(imageData, threshold);
if (mask) { if (mask) {
// Erode // Erode
mask = erodeMask(mask, imageData.width, imageData.height); mask = erodeMask(mask, imageData.width, imageData.height);

View File

@ -1,5 +1,5 @@
import { Factory } from '../Factory'; import { Factory } from '../Factory';
import { Node, Filter } from '../Node'; import { Filter, Node } from '../Node';
import { getNumberValidator } from '../Validators'; import { getNumberValidator } from '../Validators';
/** /**

View File

@ -1,6 +1,7 @@
import { Factory } from '../Factory'; import { Factory } from '../Factory';
import { Node, Filter } from '../Node'; import { Filter, Node } from '../Node';
import { getNumberValidator } from '../Validators'; import { getNumberValidator } from '../Validators';
/** /**
* Posterize Filter. Adjusts the channels so that there are no more * Posterize Filter. Adjusts the channels so that there are no more
* than n different values for that channel. This is also applied * than n different values for that channel. This is also applied
@ -15,16 +16,14 @@ import { getNumberValidator } from '../Validators';
* node.filters([Konva.Filters.Posterize]); * node.filters([Konva.Filters.Posterize]);
* node.levels(0.8); // between 0 and 1 * node.levels(0.8); // between 0 and 1
*/ */
export const Posterize: Filter = function (imageData) { export const Posterize: Filter = function (imageData) {
// level must be between 1 and 255 // level must be between 1 and 255
let levels = Math.round(this.levels() * 254) + 1, const levels = Math.round(this.levels() * 254) + 1,
data = imageData.data, data = imageData.data,
len = data.length, len = data.length,
scale = 255 / levels, scale = 255 / levels;
i;
for (i = 0; i < len; i += 1) { for (let i = 0; i < len; i += 1) {
data[i] = Math.floor(data[i] / scale) * scale; data[i] = Math.floor(data[i] / scale) * scale;
} }
}; };

View File

@ -15,18 +15,15 @@ import { RGBComponent } from '../Validators';
* node.blue(120); * node.blue(120);
* node.green(200); * node.green(200);
*/ */
export const RGB: Filter = function (imageData) { export const RGB: Filter = function (imageData) {
let data = imageData.data, const data = imageData.data,
nPixels = data.length, nPixels = data.length,
red = this.red(), red = this.red(),
green = this.green(), green = this.green(),
blue = this.blue(), blue = this.blue();
i,
brightness;
for (i = 0; i < nPixels; i += 4) { for (let i = 0; i < nPixels; i += 4) {
brightness = const brightness =
(0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]) / 255; (0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]) / 255;
data[i] = brightness * red; // r data[i] = brightness * red; // r
data[i + 1] = brightness * green; // g data[i + 1] = brightness * green; // g

View File

@ -12,17 +12,13 @@ import { Filter } from '../Node';
* node.filters([Konva.Filters.Sepia]); * node.filters([Konva.Filters.Sepia]);
*/ */
export const Sepia: Filter = function (imageData) { export const Sepia: Filter = function (imageData) {
let data = imageData.data, const data = imageData.data,
nPixels = data.length, nPixels = data.length;
i,
r,
g,
b;
for (i = 0; i < nPixels; i += 4) { for (let i = 0; i < nPixels; i += 4) {
r = data[i + 0]; const r = data[i + 0];
g = data[i + 1]; const g = data[i + 1];
b = data[i + 2]; const b = data[i + 2];
data[i + 0] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); data[i + 0] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);

View File

@ -98,7 +98,12 @@ export class Arc extends Shape<ArcConfig> {
Arc.prototype._centroid = true; Arc.prototype._centroid = true;
Arc.prototype.className = 'Arc'; Arc.prototype.className = 'Arc';
Arc.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius']; Arc.prototype._attrsAffectingSize = [
'innerRadius',
'outerRadius',
'angle',
'clockwise',
];
_registerNode(Arc); _registerNode(Arc);
// add getters setters // add getters setters

View File

@ -317,7 +317,10 @@ export class Tag extends Shape<TagConfig> {
}; };
} }
pointerDirection: GetSet<'left' | 'top' | 'right' | 'bottom' | 'up' | 'down', this>; pointerDirection: GetSet<
'left' | 'up' | 'right' | 'down' | typeof NONE,
this
>;
pointerWidth: GetSet<number, this>; pointerWidth: GetSet<number, this>;
pointerHeight: GetSet<number, this>; pointerHeight: GetSet<number, this>;
cornerRadius: GetSet<number, this>; cornerRadius: GetSet<number, this>;

View File

@ -16,14 +16,20 @@ import { GetSet } from '../types';
export function stringToArray(string: string): string[] { export function stringToArray(string: string): string[] {
// Use Unicode-aware splitting // Use Unicode-aware splitting
return [...string].reduce((acc, char, index, array) => { return [...string].reduce((acc, char, index, array) => {
// Handle emoji sequences (including ZWJ sequences) // Handle emoji with skin tone modifiers and ZWJ sequences
if ( if (/\p{Emoji}/u.test(char)) {
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?(?:\u200D\p{Emoji_Presentation})+/u.test( // Check if next character is a modifier or ZWJ sequence
char 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); acc.push(char);
} }
}
// Handle regional indicator symbols (flags) // Handle regional indicator symbols (flags)
else if ( else if (
/\p{Regional_Indicator}{2}/u.test(char + (array[index + 1] || '')) /\p{Regional_Indicator}{2}/u.test(char + (array[index + 1] || ''))
@ -35,7 +41,8 @@ export function stringToArray(string: string): string[] {
acc[acc.length - 1] += char; acc[acc.length - 1] += char;
} }
// Handle other characters // Handle other characters
else { else if (char) {
// Only push if not an empty string (skipped modifier)
acc.push(char); acc.push(char);
} }
return acc; return acc;
@ -383,7 +390,8 @@ export class Text extends Shape<TextConfig> {
return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width; return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width;
} }
getHeight() { getHeight() {
const isAuto = this.attrs.height === AUTO || this.attrs.height === undefined; const isAuto =
this.attrs.height === AUTO || this.attrs.height === undefined;
return isAuto return isAuto
? this.fontSize() * this.textArr.length * this.lineHeight() + ? this.fontSize() * this.textArr.length * this.lineHeight() +
this.padding() * 2 this.padding() * 2
@ -475,10 +483,9 @@ export class Text extends Shape<TextConfig> {
_getTextWidth(text: string) { _getTextWidth(text: string) {
const letterSpacing = this.letterSpacing(); const letterSpacing = this.letterSpacing();
const length = text.length; const length = text.length;
return ( // letterSpacing * length is the total letter spacing for the text
getDummyContext().measureText(text).width + // previously we used letterSpacing * (length - 1) but it doesn't match DOM behavior
(length ? letterSpacing * (length - 1) : 0) return getDummyContext().measureText(text).width + letterSpacing * length;
);
} }
_setTextData() { _setTextData() {
let lines = this.text().split('\n'), let lines = this.text().split('\n'),
@ -501,7 +508,9 @@ export class Text extends Shape<TextConfig> {
this.textArr = []; this.textArr = [];
getDummyContext().font = this._getContextFont(); getDummyContext().font = this._getContextFont();
const additionalWidth = shouldAddEllipsis ? this._getTextWidth(ELLIPSIS) : 0; const additionalWidth = shouldAddEllipsis
? this._getTextWidth(ELLIPSIS)
: 0;
for (let i = 0, max = lines.length; i < max; ++i) { for (let i = 0, max = lines.length; i < max; ++i) {
let line = lines[i]; let line = lines[i];
@ -517,13 +526,16 @@ export class Text extends Shape<TextConfig> {
* that would fit in the specified width * that would fit in the specified width
*/ */
let low = 0, let low = 0,
high = line.length, high = stringToArray(line).length, // Convert to array for proper emoji handling
match = '', match = '',
matchWidth = 0; matchWidth = 0;
while (low < high) { while (low < high) {
const mid = (low + high) >>> 1, 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; substrWidth = this._getTextWidth(substr) + additionalWidth;
if (substrWidth <= maxWidth) { if (substrWidth <= maxWidth) {
low = mid + 1; low = mid + 1;
match = substr; match = substr;
@ -541,20 +553,24 @@ export class Text extends Shape<TextConfig> {
// a fitting substring was found // a fitting substring was found
if (wrapAtWord) { if (wrapAtWord) {
// try to find a space or dash where wrapping could be done // try to find a space or dash where wrapping could be done
var wrapIndex; const lineArray = stringToArray(line);
const nextChar = line[match.length]; const matchArray = stringToArray(match);
const nextChar = lineArray[matchArray.length];
const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH; const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH;
let wrapIndex;
if (nextIsSpaceOrDash && matchWidth <= maxWidth) { if (nextIsSpaceOrDash && matchWidth <= maxWidth) {
wrapIndex = match.length; wrapIndex = matchArray.length;
} else { } else {
wrapIndex = // Find last space or dash in the array
Math.max(match.lastIndexOf(SPACE), match.lastIndexOf(DASH)) + const lastSpaceIndex = matchArray.lastIndexOf(SPACE);
1; const lastDashIndex = matchArray.lastIndexOf(DASH);
wrapIndex = Math.max(lastSpaceIndex, lastDashIndex) + 1;
} }
if (wrapIndex > 0) { if (wrapIndex > 0) {
// re-cut the substring found at the space/dash position
low = wrapIndex; low = wrapIndex;
match = match.slice(0, low); match = lineArray.slice(0, low).join('');
matchWidth = this._getTextWidth(match); matchWidth = this._getTextWidth(match);
} }
} }
@ -575,13 +591,14 @@ export class Text extends Shape<TextConfig> {
*/ */
break; 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) { if (line.length > 0) {
// Check if the remaining text would fit on one line
lineWidth = this._getTextWidth(line); lineWidth = this._getTextWidth(line);
if (lineWidth <= maxWidth) { if (lineWidth <= maxWidth) {
// if it does, add the line and break out of the loop
this._addTextLine(line); this._addTextLine(line);
currentHeightPx += lineHeightPx; currentHeightPx += lineHeightPx;
textWidth = Math.max(textWidth, lineWidth); textWidth = Math.max(textWidth, lineWidth);
@ -623,7 +640,7 @@ export class Text extends Shape<TextConfig> {
* 1. the current line is the last line * 1. the current line is the last line
* 2. wrap is NONE * 2. wrap is NONE
* @param {Number} currentHeightPx * @param {Number} currentHeightPx
* @returns * @returns {Boolean}
*/ */
_shouldHandleEllipsis(currentHeightPx: number): boolean { _shouldHandleEllipsis(currentHeightPx: number): boolean {
const fontSize = +this.fontSize(), const fontSize = +this.fontSize(),

View File

@ -551,7 +551,7 @@ Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING);
* // underline text * // underline text
* shape.textDecoration('underline'); * shape.textDecoration('underline');
*/ */
Factory.addGetterSetter(TextPath, 'textDecoration', null); Factory.addGetterSetter(TextPath, 'textDecoration', '');
/** /**
* get/set kerning function. * get/set kerning function.
@ -568,4 +568,4 @@ Factory.addGetterSetter(TextPath, 'textDecoration', null);
* return 1; * return 1;
* }); * });
*/ */
Factory.addGetterSetter(TextPath, 'kerningFunc', null); Factory.addGetterSetter(TextPath, 'kerningFunc', undefined);

View File

@ -335,10 +335,12 @@ export class Transformer extends Group {
this.update(); this.update();
} }
}; };
if (node._attrsAffectingSize.length) {
const additionalEvents = node._attrsAffectingSize const additionalEvents = node._attrsAffectingSize
.map((prop) => prop + 'Change.' + this._getEventNamespace()) .map((prop) => prop + 'Change.' + this._getEventNamespace())
.join(' '); .join(' ');
node.on(additionalEvents, onChange); node.on(additionalEvents, onChange);
}
node.on( node.on(
TRANSFORM_CHANGE_STR.map( TRANSFORM_CHANGE_STR.map(
(e) => e + `.${this._getEventNamespace()}` (e) => e + `.${this._getEventNamespace()}`
@ -1745,8 +1747,6 @@ Factory.addGetterSetter(Transformer, 'ignoreStroke', false);
*/ */
Factory.addGetterSetter(Transformer, 'padding', 0, getNumberValidator()); Factory.addGetterSetter(Transformer, 'padding', 0, getNumberValidator());
Factory.addGetterSetter(Transformer, 'node');
/** /**
* get/set attached nodes of the Transformer. Transformer will adapt to their size and listen to their events * get/set attached nodes of the Transformer. Transformer will adapt to their size and listen to their events
* @method * @method
@ -1767,6 +1767,9 @@ Factory.addGetterSetter(Transformer, 'node');
*/ */
Factory.addGetterSetter(Transformer, 'nodes'); Factory.addGetterSetter(Transformer, 'nodes');
// @ts-ignore
// deprecated
Factory.addGetterSetter(Transformer, 'node');
/** /**
* get/set bounding box function. **IMPORTANT!** boundBondFunc operates in absolute coordinates. * get/set bounding box function. **IMPORTANT!** boundBondFunc operates in absolute coordinates.

View File

@ -1,6 +1,6 @@
export interface GetSet<Type, This> { export interface GetSet<Type, This> {
(): Type; (): Type;
(v: Type): This; (v: Type | null | undefined): This;
} }
export interface Vector2d { export interface Vector2d {

View File

@ -13,6 +13,27 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
body {
padding: 0;
margin: 0;
}
.test {
position: absolute;
color: red;
font-size: 20px;
font-family: Arial;
border: 0;
background-color: transparent;
outline: none;
resize: none;
overflow: hidden;
line-height: 1;
padding: 0px;
letter-spacing: 20px;
width: 500px;
text-align: center;
}
</style> </style>
<!-- <script src="https://cdn.rawgit.com/hammerjs/touchemulator/master/touch-emulator.js"></script> --> <!-- <script src="https://cdn.rawgit.com/hammerjs/touchemulator/master/touch-emulator.js"></script> -->
<script> <script>
@ -26,6 +47,7 @@
<body> <body>
<div id="container"></div> <div id="container"></div>
<textarea class="test" id="text">Hello</textarea>
<script type="module"> <script type="module">
import Konva from '../src/index.ts'; import Konva from '../src/index.ts';
@ -41,31 +63,25 @@
height: stageHeight, height: stageHeight,
}); });
var layer = new Konva.Layer(); Konva._fixTextRendering = true;
const layer = new Konva.Layer();
stage.add(layer); stage.add(layer);
var rect = new Konva.Rect({ const shape = new Konva.Text({
x: 10, x: 50,
y: 10, y: 50,
width: 100, text: 'Hello',
height: 100, fontSize: 20,
fill: 'green', fontFamily: 'Arial',
draggable: true, letterSpacing: 20,
align: 'center',
width: 500,
}); });
layer.add(rect); layer.add(shape);
window.addEventListener('touchend', () => { text.style.top = shape.y() + 'px';
console.log('touchend'); text.style.left = shape.x() + 'px';
});
window.addEventListener('touchcancel', () => {
console.log('touchcancel');
});
window.addEventListener('lostpointercapture', () => {
console.log('lostpointercapture');
});
window.addEventListener('focusout', () => {
console.log('focusout');
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -107,17 +107,10 @@ describe('Image', function () {
var trace = layer.getContext().getTrace(); var trace = layer.getContext().getTrace();
if (isBrowser) {
assert.equal( assert.equal(
trace, trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object HTMLImageElement],135,7,167,134,0,0,100,100);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object HTMLImageElement],135,7,167,134,0,0,200,100);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object HTMLImageElement],135,7,167,134,0,0,100,100);restore();' 'clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object HTMLImageElement],135,7,167,134,0,0,100,100);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object HTMLImageElement],135,7,167,134,0,0,200,100);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object HTMLImageElement],135,7,167,134,0,0,100,100);restore();'
); );
} else {
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object Object],135,7,167,134,0,0,100,100);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object Object],135,7,167,134,0,0,200,100);restore();clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);drawImage([object Object],135,7,167,134,0,0,100,100);restore();'
);
}
done(); done();
}); });
@ -241,17 +234,10 @@ describe('Image', function () {
var trace = layer.getContext().getTrace(); var trace = layer.getContext().getTrace();
if (isBrowser) {
assert.equal( assert.equal(
trace, trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);globalAlpha=0.5;shadowColor=rgba(0,0,0,0.1);shadowBlur=10;shadowOffsetX=20;shadowOffsetY=20;drawImage([object HTMLImageElement],0,0,100,100);restore();' 'clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);globalAlpha=0.5;shadowColor=rgba(0,0,0,0.1);shadowBlur=10;shadowOffsetX=20;shadowOffsetY=20;drawImage([object HTMLImageElement],0,0,100,100);restore();'
); );
} else {
assert.equal(
trace,
'clearRect(0,0,578,200);save();transform(1,0,0,1,150,30);globalAlpha=0.5;shadowColor=rgba(0,0,0,0.1);shadowBlur=10;shadowOffsetX=20;shadowOffsetY=20;drawImage([object Object],0,0,100,100);restore();'
);
}
done(); done();
}); });
@ -285,17 +271,10 @@ describe('Image', function () {
var trace = layer.getContext().getTrace(); var trace = layer.getContext().getTrace();
if (isBrowser) {
assert.equal( assert.equal(
trace, trace,
'clearRect(0,0,578,200);save();shadowColor=rgba(0,0,0,0.5);shadowBlur=10;shadowOffsetX=20;shadowOffsetY=20;globalAlpha=0.5;drawImage([object HTMLCanvasElement],0,0,578,200);restore();' 'clearRect(0,0,578,200);save();shadowColor=rgba(0,0,0,0.5);shadowBlur=10;shadowOffsetX=20;shadowOffsetY=20;globalAlpha=0.5;drawImage([object HTMLCanvasElement],0,0,578,200);restore();'
); );
} else {
assert.equal(
trace,
'clearRect(0,0,578,200);save();shadowColor=rgba(0,0,0,0.5);shadowBlur=10;shadowOffsetX=20;shadowOffsetY=20;globalAlpha=0.5;drawImage([object Object],0,0,578,200);restore();'
);
}
done(); done();
}); });

View File

@ -2296,17 +2296,10 @@ describe('Node', function () {
var bufferTrace = stage.bufferCanvas.getContext().getTrace(); var bufferTrace = stage.bufferCanvas.getContext().getTrace();
if (isBrowser) {
assert.equal( assert.equal(
sceneTrace, sceneTrace,
'clearRect(0,0,578,200);save();globalAlpha=0.5;drawImage([object HTMLCanvasElement],0,0,578,200);restore();' 'clearRect(0,0,578,200);save();globalAlpha=0.5;drawImage([object HTMLCanvasElement],0,0,578,200);restore();'
); );
} else {
assert.equal(
sceneTrace,
'clearRect(0,0,578,200);save();globalAlpha=0.5;drawImage([object Object],0,0,578,200);restore();'
);
}
assert.equal( assert.equal(
bufferTrace, bufferTrace,

View File

@ -146,7 +146,7 @@ describe('Text', function () {
var text = new Konva.Text({ var text = new Konva.Text({
x: 10, x: 10,
y: 10, y: 10,
text: '😬', text: '😬👧🏿',
fontSize: 50, fontSize: 50,
letterSpacing: 1, letterSpacing: 1,
}); });
@ -159,7 +159,36 @@ describe('Text', function () {
context.textBaseline = 'middle'; context.textBaseline = 'middle';
context.font = 'normal normal 50px Arial'; context.font = 'normal normal 50px Arial';
context.fillStyle = 'darkgrey'; context.fillStyle = 'darkgrey';
context.fillText('😬', 10, 10 + 25); 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); compareLayerAndCanvas(layer, canvas, 254);
}); });
@ -282,7 +311,7 @@ describe('Text', function () {
var oldWidth = text.width(); var oldWidth = text.width();
text.letterSpacing(10); text.letterSpacing(10);
assert.equal(text.width(), oldWidth + 40); assert.equal(text.width(), oldWidth + 50);
layer.draw(); layer.draw();
}); });
// ====================================================== // ======================================================
@ -780,7 +809,7 @@ describe('Text', function () {
} }
var trace = var trace =
'fillText(;,106.482,77);fillStyle=#555;fillText( ,117.549,77);fillStyle=#555;fillText(A,126.438,77);fillStyle=#555;fillText(n,140.776,77);fillStyle=#555;fillText(d,153.563,77);fillStyle=#555;fillText( ,168.525,77);fillStyle=#555;fillText(o,177.415,77);fillStyle=#555;fillText(n,190.201,77);fillStyle=#555;fillText(e,202.987,77);fillStyle=#555;fillText( ,217.95,77);fillStyle=#555;fillText(m,226.84,77);fillStyle=#555;fillText(a,243.502,77);fillStyle=#555;fillText(n,256.288,77);fillStyle=#555;fillText( ,271.251,77);fillStyle=#555;fillText(i,280.141,77);fillStyle=#555;fillText(n,288.251,77);fillStyle=#555;fillText( ,303.214,77);fillStyle=#555;fillText(h,312.104,77);fillStyle=#555;fillText(i,324.89,77);fillStyle=#555;fillText(s,333,77);restore();save();save();beginPath();moveTo(0,98);lineTo(245,98);stroke();restore();save();beginPath();moveTo(0,91);lineTo(245,91);stroke();restore();fillStyle=#555;fillText(t,0,91);fillStyle=#555;fillText(i,8.89,91);fillStyle=#555;fillText(m,17,91);fillStyle=#555;fillText(e,33.662,91);fillStyle=#555;fillText( ,46.448,91);fillStyle=#555;fillText(p,55.338,91);fillStyle=#555;fillText(l,68.124,91);fillStyle=#555;fillText(a,76.234,91);fillStyle=#555;fillText(y,89.021,91);fillStyle=#555;fillText(s,101.021,91);fillStyle=#555;fillText( ,113.021,91);fillStyle=#555;fillText(m,121.91,91);fillStyle=#555;fillText(a,138.572,91);fillStyle=#555;fillText(n,151.358,91);fillStyle=#555;fillText(y,164.145,91);fillStyle=#555;fillText( ,176.145,91);fillStyle=#555;fillText(p,185.034,91);fillStyle=#555;fillText(a,197.82,91);fillStyle=#555;fillText(r,210.606,91);fillStyle=#555;fillText(t,220.269,91);fillStyle=#555;fillText(s,229.158,91);fillStyle=#555;fillText(.,241.158,91);restore();restore();'; 'fillText(;,106.482,77);fillStyle=#555;fillText( ,116.549,77);fillStyle=#555;fillText(A,125.438,77);fillStyle=#555;fillText(n,139.776,77);fillStyle=#555;fillText(d,152.563,77);fillStyle=#555;fillText( ,166.525,77);fillStyle=#555;fillText(o,175.415,77);fillStyle=#555;fillText(n,188.201,77);fillStyle=#555;fillText(e,200.987,77);fillStyle=#555;fillText( ,214.95,77);fillStyle=#555;fillText(m,223.84,77);fillStyle=#555;fillText(a,240.502,77);fillStyle=#555;fillText(n,253.288,77);fillStyle=#555;fillText( ,267.251,77);fillStyle=#555;fillText(i,276.141,77);fillStyle=#555;fillText(n,284.251,77);fillStyle=#555;fillText( ,298.214,77);fillStyle=#555;fillText(h,307.104,77);fillStyle=#555;fillText(i,319.89,77);fillStyle=#555;fillText(s,328,77);restore();save();save();beginPath();moveTo(0,98);lineTo(250,98);stroke();restore();save();beginPath();moveTo(0,91);lineTo(250,91);stroke();restore();fillStyle=#555;fillText(t,0,91);fillStyle=#555;fillText(i,8.89,91);fillStyle=#555;fillText(m,17,91);fillStyle=#555;fillText(e,33.662,91);fillStyle=#555;fillText( ,46.448,91);fillStyle=#555;fillText(p,55.338,91);fillStyle=#555;fillText(l,68.124,91);fillStyle=#555;fillText(a,76.234,91);fillStyle=#555;fillText(y,89.021,91);fillStyle=#555;fillText(s,101.021,91);fillStyle=#555;fillText( ,113.021,91);fillStyle=#555;fillText(m,121.91,91);fillStyle=#555;fillText(a,138.572,91);fillStyle=#555;fillText(n,151.358,91);fillStyle=#555;fillText(y,164.145,91);fillStyle=#555;fillText( ,176.145,91);fillStyle=#555;fillText(p,185.034,91);fillStyle=#555;fillText(a,197.82,91);fillStyle=#555;fillText(r,210.606,91);fillStyle=#555;fillText(t,220.269,91);fillStyle=#555;fillText(s,229.158,91);fillStyle=#555;fillText(.,241.158,91);restore();restore();';
assert.equal(layer.getContext().getTrace(), trace); assert.equal(layer.getContext().getTrace(), trace);
}); });
@ -1244,9 +1273,9 @@ describe('Text', function () {
// so we need to adjust offset // so we need to adjust offset
const diff = isBrowser ? 4 : 50; const diff = isBrowser ? 4 : 50;
assert.equal(Math.abs(Math.round(text1.width()) - 1725) < diff, true); assert.equal(Math.abs(Math.round(text1.width()) - 1725) < diff, true);
assert.equal(Math.abs(Math.round(text2.width()) - 2613) < diff, true); assert.equal(Math.abs(Math.round(text2.width()) - 2616) < diff, true);
assert.equal(Math.abs(Math.round(text3.width()) - 2005) < diff, true); assert.equal(Math.abs(Math.round(text3.width()) - 2009) < diff, true);
assert.equal(Math.abs(Math.round(text4.width()) - 1932) < diff, true); assert.equal(Math.abs(Math.round(text4.width()) - 1936) < diff, true);
}); });
it('default text color should be black', function () { it('default text color should be black', function () {

View File

@ -5088,4 +5088,49 @@ describe('Transformer', function () {
}, 100); }, 100);
}, 100); }, 100);
}); });
it('should properly clean up subscriptions on detach/destroy', function () {
var stage = addStage();
var layer = new Konva.Layer();
stage.add(layer);
var rect = new Konva.Rect({
x: 100,
y: 60,
width: 100,
height: 100,
fill: 'yellow',
});
layer.add(rect);
// draw to attach all listeners
layer.draw();
// Count initial number of event listeners
var initialListeners = Object.keys(rect.eventListeners).length;
// Create and attach first transformer
var tr1 = new Konva.Transformer({
nodes: [rect],
});
layer.add(tr1);
// Destroy first transformer
tr1.destroy();
// Create and attach second transformer
var tr2 = new Konva.Transformer({
nodes: [rect],
});
layer.add(tr2);
// Destroy second transformer
tr2.destroy();
// Check that we have same number of listeners as initially
assert.equal(
Object.keys(rect.eventListeners).length,
initialListeners,
'Event listeners should be cleaned up properly'
);
});
}); });