diff --git a/.github/workflows/test-browser.yml b/.github/workflows/test-browser.yml index bc2eda60..9683ae12 100644 --- a/.github/workflows/test-browser.yml +++ b/.github/workflows/test-browser.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test-node.yml b/.github/workflows/test-node.yml index 795887b4..b9e0c02b 100644 --- a/.github/workflows/test-node.yml +++ b/.github/workflows/test-node.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [23.x] steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa23cef..922a43a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,16 @@ All notable changes to this project will be documented in this file. 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) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..c6231ea1 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,3 @@ +import tseslint from 'typescript-eslint'; + +export default tseslint.config(...tseslint.configs.recommended); diff --git a/package.json b/package.json index d04c19d7..6b31ba08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "konva", - "version": "9.3.16", + "version": "9.3.18", + "description": "HTML5 2d canvas library.", "author": "Anton Lavrenov", "files": [ "README.md", @@ -16,10 +17,10 @@ "start": "npm run test:watch", "compile": "npm run clean && npm run tsc && cp ./src/index-types.d.ts ./lib/index-types.d.ts && npm run rollup", "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:build": "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: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 -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:node": "ts-mocha -r ./test/node-global-setup.mjs -p ./test/tsconfig.json test/unit/**/*.ts --exit && npm run test:import", "tsc": "tsc --removeComments", @@ -59,13 +60,13 @@ } ], "devDependencies": { - "@parcel/transformer-image": "2.10.1", - "@size-limit/preset-big-lib": "^11.0.1", - "@types/mocha": "^10.0.6", - "canvas": "^2.11.2", - "chai": "4.3.10", + "@parcel/transformer-image": "2.13.2", + "@size-limit/preset-big-lib": "^11.1.6", + "@types/mocha": "^10.0.10", + "canvas": "^3.1.0", + "chai": "5.1.2", "filehound": "^1.17.6", - "gulp": "^4.0.2", + "gulp": "^5.0.0", "gulp-concat": "^2.6.1", "gulp-connect": "^5.7.0", "gulp-exec": "^5.0.0", @@ -78,14 +79,14 @@ "gulp-util": "^3.0.8", "mocha": "10.2.0", "mocha-headless-chrome": "^4.0.0", - "parcel": "2.10.1", + "parcel": "2.13.3", "process": "^0.11.10", - "rollup": "^4.9.1", + "rollup": "^4.31.0", "rollup-plugin-typescript2": "^0.36.0", - "size-limit": "^11.0.1", + "size-limit": "^11.1.6", "ts-mocha": "^10.0.0", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.7.3" }, "keywords": [ "canvas", diff --git a/src/Canvas.ts b/src/Canvas.ts index eb122941..96002cca 100644 --- a/src/Canvas.ts +++ b/src/Canvas.ts @@ -82,9 +82,32 @@ export class Canvas { getContext() { return this.context; } + /** + * get pixel ratio + * @method + * @name Konva.Canvas#getPixelRatio + * @returns {Number} pixel ratio + * @example + * var pixelRatio = layer.getCanvas.getPixelRatio(); + */ getPixelRatio() { 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) { const previousRatio = this.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 { constructor( config: ICanvasConfig = { width: 0, height: 0, willReadFrequently: false } diff --git a/src/Factory.ts b/src/Factory.ts index e86117b3..b1af978d 100644 --- a/src/Factory.ts +++ b/src/Factory.ts @@ -1,18 +1,76 @@ import { Node } from './Node'; +import { GetSet } from './types'; import { Util } from './Util'; import { getComponentValidator } from './Validators'; -const GET = 'get', - SET = 'set'; +const GET = 'get'; +const SET = 'set'; + +/** + * Enforces that a type is a string. + */ +type EnforceString = 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 = EnforceString>; + +/** + * A function that is called after a setter is called. + */ +type AfterFunc = (this: InstanceType) => void; + +/** + * Extracts the type of a GetSet. + */ +type ExtractGetSet = T extends GetSet ? U : never; + +/** + * Extracts the type of a GetSet class attribute. + */ +type Value> = ExtractGetSet< + InstanceType[U] +>; + +/** + * A function that validates a value. + */ +type ValidatorFunc = (val: ExtractGetSet, attr: string) => T; + +/** + * Extracts the "components" (keys) of a GetSet value. The value must be an object. + */ +type ExtractComponents> = Value< + T, + U +> extends Record + ? EnforceString>[] + : never; export const Factory = { - addGetterSetter(constructor, attr, def?, validator?, after?) { + addGetterSetter>( + constructor: T, + attr: U, + def?: Value, + validator?: ValidatorFunc>, + after?: AfterFunc + ): void { Factory.addGetter(constructor, attr, def); Factory.addSetter(constructor, attr, validator, after); Factory.addOverloadedGetterSetter(constructor, attr); }, - addGetter(constructor, attr, def?) { - const method = GET + Util._capitalize(attr); + addGetter>( + constructor: T, + attr: U, + def?: Value + ) { + var method = GET + Util._capitalize(attr); constructor.prototype[method] = constructor.prototype[method] || @@ -22,15 +80,26 @@ export const Factory = { }; }, - addSetter(constructor, attr, validator?, after?) { - const method = SET + Util._capitalize(attr); + addSetter>( + constructor: T, + attr: U, + validator?: ValidatorFunc>, + after?: AfterFunc + ) { + var method = SET + Util._capitalize(attr); if (!constructor.prototype[method]) { Factory.overWriteSetter(constructor, attr, validator, after); } }, - overWriteSetter(constructor, attr, validator?, after?) { - const method = SET + Util._capitalize(attr); + + overWriteSetter>( + constructor: T, + attr: U, + validator?: ValidatorFunc>, + after?: AfterFunc + ) { + var method = SET + Util._capitalize(attr); constructor.prototype[method] = function (val) { if (validator && val !== undefined && val !== null) { val = validator.call(this, val, attr); @@ -45,12 +114,13 @@ export const Factory = { return this; }; }, - addComponentsGetterSetter( - constructor, - attr: string, - components: Array, - validator?: Function, - after?: Function + + addComponentsGetterSetter>( + constructor: T, + attr: U, + components: ExtractComponents, + validator?: ValidatorFunc>, + after?: AfterFunc ) { const len = components.length, capitalize = Util._capitalize, @@ -59,7 +129,7 @@ export const Factory = { // getter constructor.prototype[getter] = function () { - const ret = {}; + const ret: Record = {}; for (let n = 0; n < len; n++) { const component = components[n]; @@ -76,7 +146,7 @@ export const Factory = { const oldVal = this.attrs[attr]; if (validator) { - val = validator.call(this, val); + val = validator.call(this, val, attr); } if (basicValidator) { @@ -106,8 +176,11 @@ export const Factory = { Factory.addOverloadedGetterSetter(constructor, attr); }, - addOverloadedGetterSetter(constructor, attr) { - const capitalizedAttr = Util._capitalize(attr), + addOverloadedGetterSetter>( + constructor: T, + attr: U + ) { + var capitalizedAttr = Util._capitalize(attr), setter = SET + capitalizedAttr, getter = GET + capitalizedAttr; @@ -121,7 +194,12 @@ export const Factory = { return this[getter](); }; }, - addDeprecatedGetterSetter(constructor, attr, def, validator) { + addDeprecatedGetterSetter>( + constructor: T, + attr: U, + def: Value, + validator: ValidatorFunc> + ) { Util.error('Adding deprecated ' + attr); const method = GET + Util._capitalize(attr); @@ -139,7 +217,10 @@ export const Factory = { }); Factory.addOverloadedGetterSetter(constructor, attr); }, - backCompat(constructor, methods) { + backCompat( + constructor: T, + methods: Record + ) { Util.each(methods, function (oldMethodName, newMethodName) { const method = constructor.prototype[newMethodName]; const oldGetter = GET + Util._capitalize(oldMethodName); @@ -161,7 +242,7 @@ export const Factory = { constructor.prototype[oldSetter] = deprecated; }); }, - afterSetFilter(this: Node) { + afterSetFilter(this: Node): void { this._filterUpToDate = false; }, }; diff --git a/src/Node.ts b/src/Node.ts index a23ad397..86ee06f7 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -198,7 +198,8 @@ export abstract class Node { // for transform the cache can be NOT empty // but we still need to recalculate it if it is dirty 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 (invalid) { @@ -2660,7 +2661,7 @@ export abstract class Node { rotation: GetSet; zIndex: GetSet; - scale: GetSet; + scale: GetSet; scaleX: GetSet; scaleY: GetSet; skew: GetSet; @@ -3102,7 +3103,7 @@ addGetterSetter(Node, 'offsetY', 0, getNumberValidator()); * node.offsetY(3); */ -addGetterSetter(Node, 'dragDistance', null, getNumberValidator()); +addGetterSetter(Node, 'dragDistance', undefined, getNumberValidator()); /** * get/set drag distance @@ -3193,7 +3194,7 @@ addGetterSetter(Node, 'listening', 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; return val; }); diff --git a/src/Shape.ts b/src/Shape.ts index aec9d064..70305ec2 100644 --- a/src/Shape.ts +++ b/src/Shape.ts @@ -837,6 +837,11 @@ export class Shape< strokeLinearGradientStartPoint: GetSet; strokeLinearGradientEndPoint: GetSet; strokeLinearGradientColorStops: GetSet, this>; + strokeLinearGradientStartPointX: GetSet; + strokeLinearGradientStartPointY: GetSet; + strokeLinearGradientEndPointX: GetSet; + strokeLinearGradientEndPointY: GetSet; + fillRule: GetSet; } Shape.prototype._fillFunc = _fillFunc; diff --git a/src/Stage.ts b/src/Stage.ts index f2f56d1a..495f2b68 100644 --- a/src/Stage.ts +++ b/src/Stage.ts @@ -249,11 +249,10 @@ export class Stage extends Container { * @name Konva.Stage#clear */ clear() { - let layers = this.children, - len = layers.length, - n; + const layers = this.children, + len = layers.length; - for (n = 0; n < len; n++) { + for (let n = 0; n < len; n++) { layers[n].clear(); } return this; @@ -367,12 +366,11 @@ export class Stage extends Container { if (!pos) { return null; } - let layers = this.children, + const layers = this.children, len = layers.length, - end = len - 1, - n; + end = len - 1; - for (n = end; n >= 0; n--) { + for (let n = end; n >= 0; n--) { const shape = layers[n].getIntersection(pos); if (shape) { return shape; @@ -820,8 +818,8 @@ export class Stage extends Container { * }); */ setPointersPositions(evt) { - let contentPosition = this._getContentPosition(), - x: number | null = null, + const contentPosition = this._getContentPosition(); + let x: number | null = null, y: number | null = null; evt = evt ? evt : window.event; diff --git a/src/Tween.ts b/src/Tween.ts index d29d6f15..bb2d8278 100644 --- a/src/Tween.ts +++ b/src/Tween.ts @@ -4,7 +4,7 @@ import { Node, NodeConfig } from './Node'; import { Konva } from './Global'; import { Line } from './shapes/Line'; -let blacklist = { +const blacklist = { node: 1, duration: 1, easing: 1, @@ -14,8 +14,8 @@ let blacklist = { PAUSED = 1, PLAYING = 2, REVERSING = 3, - idCounter = 0, colorAttrs = ['fill', 'stroke', 'shadowColor']; +let idCounter = 0; class TweenEngine { prop: string; @@ -195,12 +195,12 @@ export class Tween { onUpdate: Function | undefined; constructor(config: TweenConfig) { - let that = this, + const that = this, node = config.node as any, nodeId = node._id, - duration, easing = config.easing || Easings.Linear, - yoyo = !!config.yoyo, + yoyo = !!config.yoyo; + let duration, key; if (typeof config.duration === 'undefined') { @@ -266,26 +266,23 @@ export class Tween { this.onUpdate = config.onUpdate; } _addAttr(key, end) { - let node = this.node, - nodeId = node._id, - start, - diff, - tweenId, - n, + const node = this.node, + nodeId = node._id; + let diff, len, trueEnd, trueStart, endRGBA; // remove conflict from tween map if it exists - tweenId = Tween.tweens[nodeId][key]; + const tweenId = Tween.tweens[nodeId][key]; if (tweenId) { delete Tween.attrs[nodeId][tweenId][key]; } // add to tween map - start = node.getAttr(key); + let start = node.getAttr(key); if (Util._isArray(end)) { diff = []; @@ -310,7 +307,7 @@ export class Tween { } if (key.indexOf('fill') === 0) { - for (n = 0; n < len; n++) { + for (let n = 0; n < len; n++) { if (n % 2 === 0) { diff.push(end[n] - start[n]); } else { @@ -326,7 +323,7 @@ export class Tween { } } } else { - for (n = 0; n < len; n++) { + for (let n = 0; n < len; n++) { diff.push(end[n] - start[n]); } } @@ -353,9 +350,9 @@ export class Tween { Tween.tweens[nodeId][key] = this._id; } _tweenFunc(i) { - let node = this.node, - attrs = Tween.attrs[node._id][this._id], - key, + const node = this.node, + attrs = Tween.attrs[node._id][this._id]; + let key, attr, start, diff, @@ -525,14 +522,13 @@ export class Tween { * @name Konva.Tween#destroy */ destroy() { - let nodeId = this.node._id, + const nodeId = this.node._id, thisId = this._id, - attrs = Tween.tweens[nodeId], - key; + attrs = Tween.tweens[nodeId]; this.pause(); - for (key in attrs) { + for (const key in attrs) { delete Tween.tweens[nodeId][key]; } diff --git a/src/Util.ts b/src/Util.ts index 8dff2d51..ae5075a5 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -260,7 +260,7 @@ export class Transform { } // CONSTANTS -let OBJECT_ARRAY = '[object Array]', +const OBJECT_ARRAY = '[object Array]', OBJECT_NUMBER = '[object Number]', OBJECT_STRING = '[object String]', OBJECT_BOOLEAN = '[object Boolean]', @@ -423,8 +423,8 @@ let OBJECT_ARRAY = '[object Array]', yellow: [255, 255, 0], yellowgreen: [154, 205, 5], }, - RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/, - animQueue: Array = []; + RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/; + let animQueue: Array = []; const req = (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) || @@ -904,21 +904,20 @@ export const Util = { return pc; }, _prepareArrayForTween(startArray, endArray, isClosed) { - let n, - start: Vector2d[] = [], + const start: Vector2d[] = [], end: Vector2d[] = []; if (startArray.length > endArray.length) { const temp = endArray; endArray = startArray; startArray = temp; } - for (n = 0; n < startArray.length; n += 2) { + for (let n = 0; n < startArray.length; n += 2) { start.push({ x: startArray[n], y: startArray[n + 1], }); } - for (n = 0; n < endArray.length; n += 2) { + for (let n = 0; n < endArray.length; n += 2) { end.push({ x: endArray[n], y: endArray[n + 1], diff --git a/src/Validators.ts b/src/Validators.ts index 023e0d90..a8534b3b 100644 --- a/src/Validators.ts +++ b/src/Validators.ts @@ -33,9 +33,9 @@ export function alphaComponent(val: number) { return val; } -export function getNumberValidator() { +export function getNumberValidator() { if (Konva.isUnminified) { - return function (val: T, attr: string): T | void { + return function (val: T, attr: string): T { if (!Util._isNumber(val)) { Util.warn( _formatValue(val) + @@ -49,11 +49,11 @@ export function getNumberValidator() { } } -export function getNumberOrArrayOfNumbersValidator(noOfElements: number) { +export function getNumberOrArrayOfNumbersValidator(noOfElements: number) { if (Konva.isUnminified) { - return function (val: T, attr: string): T | void { - const isNumber = Util._isNumber(val); - const isValidArray = Util._isArray(val) && val.length == noOfElements; + return function (val: T, attr: string): T { + let isNumber = Util._isNumber(val); + let isValidArray = Util._isArray(val) && val.length == noOfElements; if (!isNumber && !isValidArray) { Util.warn( _formatValue(val) + @@ -69,11 +69,11 @@ export function getNumberOrArrayOfNumbersValidator(noOfElements: number) { } } -export function getNumberOrAutoValidator() { +export function getNumberOrAutoValidator() { if (Konva.isUnminified) { - return function (val: T, attr: string): T | void { - const isNumber = Util._isNumber(val); - const isAuto = val === 'auto'; + return function (val: T, attr: string): T { + var isNumber = Util._isNumber(val); + var isAuto = val === 'auto'; if (!(isNumber || isAuto)) { Util.warn( @@ -88,9 +88,9 @@ export function getNumberOrAutoValidator() { } } -export function getStringValidator() { +export function getStringValidator() { if (Konva.isUnminified) { - return function (val: any, attr: string) { + return function (val: T, attr: string): T { if (!Util._isString(val)) { Util.warn( _formatValue(val) + @@ -104,13 +104,13 @@ export function getStringValidator() { } } -export function getStringOrGradientValidator() { +export function getStringOrGradientValidator() { if (Konva.isUnminified) { - return function (val: any, attr: string) { + return function (val: T, attr: string): T { const isString = Util._isString(val); const isGradient = Object.prototype.toString.call(val) === '[object CanvasGradient]' || - (val && val.addColorStop); + (val && val['addColorStop']); if (!(isString || isGradient)) { Util.warn( _formatValue(val) + @@ -124,9 +124,9 @@ export function getStringOrGradientValidator() { } } -export function getFunctionValidator() { +export function getFunctionValidator() { if (Konva.isUnminified) { - return function (val: any, attr: string) { + return function (val: T, attr: string): T { if (!Util._isFunction(val)) { Util.warn( _formatValue(val) + @@ -139,9 +139,9 @@ export function getFunctionValidator() { }; } } -export function getNumberArrayValidator() { +export function getNumberArrayValidator() { 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) // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description const TypedArray = Int8Array ? Object.getPrototypeOf(Int8Array) : null; @@ -172,10 +172,10 @@ export function getNumberArrayValidator() { }; } } -export function getBooleanValidator() { +export function getBooleanValidator() { if (Konva.isUnminified) { - return function (val: any, attr: string) { - const isBool = val === true || val === false; + return function (val: T, attr: string): T { + var isBool = val === true || val === false; if (!isBool) { Util.warn( _formatValue(val) + @@ -188,9 +188,9 @@ export function getBooleanValidator() { }; } } -export function getComponentValidator(components: any) { +export function getComponentValidator(components: string[]) { 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 if (val === undefined || val === null) { return val; diff --git a/src/filters/Emboss.ts b/src/filters/Emboss.ts index cbeacd3c..a040cc8f 100644 --- a/src/filters/Emboss.ts +++ b/src/filters/Emboss.ts @@ -174,7 +174,7 @@ Factory.addGetterSetter( Node, 'embossDirection', 'top-left', - null, + undefined, Factory.afterSetFilter ); /** @@ -190,7 +190,7 @@ Factory.addGetterSetter( Node, 'embossBlend', false, - null, + undefined, Factory.afterSetFilter ); /** diff --git a/src/filters/HSL.ts b/src/filters/HSL.ts index 1f1b4d9a..47763ad2 100644 --- a/src/filters/HSL.ts +++ b/src/filters/HSL.ts @@ -59,13 +59,12 @@ Factory.addGetterSetter( */ export const HSL: Filter = function (imageData) { - let data = imageData.data, + const data = imageData.data, nPixels = data.length, v = 1, s = Math.pow(2, this.saturation()), h = Math.abs(this.hue() + 360) % 360, - l = this.luminance() * 127, - i; + l = this.luminance() * 127; // Basis for the technique used: // 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; - for (i = 0; i < nPixels; i += 4) { + for (let i = 0; i < nPixels; i += 4) { r = data[i + 0]; g = data[i + 1]; b = data[i + 2]; diff --git a/src/filters/Invert.ts b/src/filters/Invert.ts index a9bb259b..766c9506 100644 --- a/src/filters/Invert.ts +++ b/src/filters/Invert.ts @@ -9,11 +9,10 @@ import { Filter } from '../Node'; * node.filters([Konva.Filters.Invert]); */ export const Invert: Filter = function (imageData) { - let data = imageData.data, - len = data.length, - i; + const data = imageData.data, + len = data.length; - for (i = 0; i < len; i += 4) { + for (let i = 0; i < len; i += 4) { // red data[i] = 255 - data[i]; // green diff --git a/src/filters/Kaleidoscope.ts b/src/filters/Kaleidoscope.ts index 70336fa8..bdca220c 100644 --- a/src/filters/Kaleidoscope.ts +++ b/src/filters/Kaleidoscope.ts @@ -1,5 +1,5 @@ import { Factory } from '../Factory'; -import { Node, Filter } from '../Node'; +import { Filter, Node } from '../Node'; import { Util } from '../Util'; import { getNumberValidator } from '../Validators'; @@ -20,53 +20,41 @@ import { getNumberValidator } from '../Validators'; */ const ToPolar = function (src, dst, opt) { - let srcPixels = src.data, + const srcPixels = src.data, dstPixels = dst.data, xSize = src.width, ySize = src.height, xMid = opt.polarCenterX || xSize / 2, - yMid = opt.polarCenterY || ySize / 2, - i, - x, - y, - r = 0, - g = 0, - b = 0, - a = 0; + yMid = opt.polarCenterY || ySize / 2; // Find the largest radius - let rad, - rMax = Math.sqrt(xMid * xMid + yMid * yMid); - x = xSize - xMid; - y = ySize - yMid; - rad = Math.sqrt(x * x + y * y); + let rMax = Math.sqrt(xMid * xMid + yMid * yMid); + let x = xSize - xMid; + let y = ySize - yMid; + const rad = Math.sqrt(x * x + y * y); rMax = rad > rMax ? rad : rMax; // We'll be uisng y as the radius, and x as the angle (theta=t) - let rSize = ySize, - tSize = xSize, - radius, - theta; + const rSize = ySize, + tSize = xSize; // We want to cover all angles (0-360) and we need to convert to // radians (*PI/180) - let conversion = ((360 / tSize) * Math.PI) / 180, - sin, - cos; + const conversion = ((360 / tSize) * Math.PI) / 180; // var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale; - for (theta = 0; theta < tSize; theta += 1) { - sin = Math.sin(theta * conversion); - cos = Math.cos(theta * conversion); - for (radius = 0; radius < rSize; radius += 1) { + for (let theta = 0; theta < tSize; theta += 1) { + const sin = Math.sin(theta * conversion); + const cos = Math.cos(theta * conversion); + for (let radius = 0; radius < rSize; radius += 1) { x = Math.floor(xMid + ((rMax * radius) / rSize) * cos); y = Math.floor(yMid + ((rMax * radius) / rSize) * sin); - i = (y * xSize + x) * 4; - r = srcPixels[i + 0]; - g = srcPixels[i + 1]; - b = srcPixels[i + 2]; - a = srcPixels[i + 3]; + let i = (y * xSize + x) * 4; + const r = srcPixels[i + 0]; + const g = srcPixels[i + 1]; + const b = srcPixels[i + 2]; + const a = srcPixels[i + 3]; // Store it //i = (theta * xSize + radius) * 4; @@ -97,35 +85,23 @@ const ToPolar = function (src, dst, opt) { */ const FromPolar = function (src, dst, opt) { - let srcPixels = src.data, + const srcPixels = src.data, dstPixels = dst.data, xSize = src.width, ySize = src.height, xMid = opt.polarCenterX || xSize / 2, - yMid = opt.polarCenterY || ySize / 2, - i, - x, - y, - dx, - dy, - r = 0, - g = 0, - b = 0, - a = 0; + yMid = opt.polarCenterY || ySize / 2; // Find the largest radius - let rad, - rMax = Math.sqrt(xMid * xMid + yMid * yMid); - x = xSize - xMid; - y = ySize - yMid; - rad = Math.sqrt(x * x + y * y); + let rMax = Math.sqrt(xMid * xMid + yMid * yMid); + let x = xSize - xMid; + let y = ySize - yMid; + const rad = Math.sqrt(x * x + y * y); rMax = rad > rMax ? rad : rMax; // We'll be uisng x as the radius, and y as the angle (theta=t) - let rSize = ySize, + const rSize = ySize, tSize = xSize, - radius, - theta, phaseShift = opt.polarRotation || 0; // 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 (y = 0; y < ySize; y += 1) { - dx = x - xMid; - dy = y - yMid; - radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax; - theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360; + const dx = x - xMid; + const dy = y - yMid; + const radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax; + let theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360; theta = (theta * tSize) / 360; x1 = Math.floor(theta); y1 = Math.floor(radius); - i = (y1 * xSize + x1) * 4; - r = srcPixels[i + 0]; - g = srcPixels[i + 1]; - b = srcPixels[i + 2]; - a = srcPixels[i + 3]; + let i = (y1 * xSize + x1) * 4; + const r = srcPixels[i + 0]; + const g = srcPixels[i + 1]; + const b = srcPixels[i + 2]; + const a = srcPixels[i + 3]; // Store it i = (y * xSize + x) * 4; diff --git a/src/filters/Mask.ts b/src/filters/Mask.ts index 81a0bc44..0668c0d5 100644 --- a/src/filters/Mask.ts +++ b/src/filters/Mask.ts @@ -1,5 +1,5 @@ import { Factory } from '../Factory'; -import { Node, Filter } from '../Node'; +import { Filter, Node } from '../Node'; import { getNumberValidator } from '../Validators'; function pixelAt(idata, x, y) { @@ -181,8 +181,8 @@ function smoothEdgeMask(mask, sw, sh) { */ export const Mask: Filter = function (imageData) { // Detect pixels close to the background color - let threshold = this.threshold(), - mask = backgroundMask(imageData, threshold); + const threshold = this.threshold(); + let mask = backgroundMask(imageData, threshold); if (mask) { // Erode mask = erodeMask(mask, imageData.width, imageData.height); diff --git a/src/filters/Noise.ts b/src/filters/Noise.ts index 39fd3ea1..77a9d710 100644 --- a/src/filters/Noise.ts +++ b/src/filters/Noise.ts @@ -1,5 +1,5 @@ import { Factory } from '../Factory'; -import { Node, Filter } from '../Node'; +import { Filter, Node } from '../Node'; import { getNumberValidator } from '../Validators'; /** diff --git a/src/filters/Posterize.ts b/src/filters/Posterize.ts index 8c768e19..6fb89b91 100644 --- a/src/filters/Posterize.ts +++ b/src/filters/Posterize.ts @@ -1,6 +1,7 @@ import { Factory } from '../Factory'; -import { Node, Filter } from '../Node'; +import { Filter, Node } from '../Node'; import { getNumberValidator } from '../Validators'; + /** * Posterize Filter. Adjusts the channels so that there are no more * 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.levels(0.8); // between 0 and 1 */ - export const Posterize: Filter = function (imageData) { // 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, len = data.length, - scale = 255 / levels, - i; + scale = 255 / levels; - for (i = 0; i < len; i += 1) { + for (let i = 0; i < len; i += 1) { data[i] = Math.floor(data[i] / scale) * scale; } }; diff --git a/src/filters/RGB.ts b/src/filters/RGB.ts index 8bdca660..e5ea879c 100644 --- a/src/filters/RGB.ts +++ b/src/filters/RGB.ts @@ -15,18 +15,15 @@ import { RGBComponent } from '../Validators'; * node.blue(120); * node.green(200); */ - export const RGB: Filter = function (imageData) { - let data = imageData.data, + const data = imageData.data, nPixels = data.length, red = this.red(), green = this.green(), - blue = this.blue(), - i, - brightness; + blue = this.blue(); - for (i = 0; i < nPixels; i += 4) { - brightness = + for (let i = 0; i < nPixels; i += 4) { + const brightness = (0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]) / 255; data[i] = brightness * red; // r data[i + 1] = brightness * green; // g diff --git a/src/filters/Sepia.ts b/src/filters/Sepia.ts index 2187bd0e..3719ffe3 100644 --- a/src/filters/Sepia.ts +++ b/src/filters/Sepia.ts @@ -12,17 +12,13 @@ import { Filter } from '../Node'; * node.filters([Konva.Filters.Sepia]); */ export const Sepia: Filter = function (imageData) { - let data = imageData.data, - nPixels = data.length, - i, - r, - g, - b; + const data = imageData.data, + nPixels = data.length; - for (i = 0; i < nPixels; i += 4) { - r = data[i + 0]; - g = data[i + 1]; - b = data[i + 2]; + for (let i = 0; i < nPixels; i += 4) { + const r = data[i + 0]; + const g = data[i + 1]; + const b = data[i + 2]; 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); diff --git a/src/shapes/Arc.ts b/src/shapes/Arc.ts index 63b97c5f..f92162d5 100644 --- a/src/shapes/Arc.ts +++ b/src/shapes/Arc.ts @@ -98,7 +98,12 @@ export class Arc extends Shape { Arc.prototype._centroid = true; Arc.prototype.className = 'Arc'; -Arc.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius']; +Arc.prototype._attrsAffectingSize = [ + 'innerRadius', + 'outerRadius', + 'angle', + 'clockwise', +]; _registerNode(Arc); // add getters setters diff --git a/src/shapes/Label.ts b/src/shapes/Label.ts index a1e9888a..be15df8c 100644 --- a/src/shapes/Label.ts +++ b/src/shapes/Label.ts @@ -317,7 +317,10 @@ export class Tag extends Shape { }; } - pointerDirection: GetSet<'left' | 'top' | 'right' | 'bottom' | 'up' | 'down', this>; + pointerDirection: GetSet< + 'left' | 'up' | 'right' | 'down' | typeof NONE, + this + >; pointerWidth: GetSet; pointerHeight: GetSet; cornerRadius: GetSet; diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index dbd3b948..984b553d 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -16,13 +16,19 @@ import { GetSet } from '../types'; export function stringToArray(string: string): string[] { // Use Unicode-aware splitting return [...string].reduce((acc, char, index, array) => { - // Handle emoji sequences (including ZWJ sequences) - if ( - /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?(?:\u200D\p{Emoji_Presentation})+/u.test( - char - ) - ) { - acc.push(char); + // Handle emoji with skin tone modifiers and ZWJ sequences + if (/\p{Emoji}/u.test(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); + } } // Handle regional indicator symbols (flags) else if ( @@ -35,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; @@ -310,7 +317,7 @@ export class Text extends Shape { spacesNumber = text.split(' ').length - 1; oneWord = spacesNumber === 0; lineWidth = - align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width; + align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width; context.lineTo( lineTranslateX + Math.round(lineWidth), translateY + lineTranslateY + yOffset @@ -383,7 +390,8 @@ export class Text extends Shape { return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width; } getHeight() { - const isAuto = this.attrs.height === AUTO || this.attrs.height === undefined; + const isAuto = + this.attrs.height === AUTO || this.attrs.height === undefined; return isAuto ? this.fontSize() * this.textArr.length * this.lineHeight() + this.padding() * 2 @@ -475,10 +483,9 @@ export class Text extends Shape { _getTextWidth(text: string) { const letterSpacing = this.letterSpacing(); const length = text.length; - return ( - getDummyContext().measureText(text).width + - (length ? letterSpacing * (length - 1) : 0) - ); + // letterSpacing * length is the total letter spacing for the text + // previously we used letterSpacing * (length - 1) but it doesn't match DOM behavior + return getDummyContext().measureText(text).width + letterSpacing * length; } _setTextData() { let lines = this.text().split('\n'), @@ -501,7 +508,9 @@ export class Text extends Shape { this.textArr = []; 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) { let line = lines[i]; @@ -517,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; @@ -541,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); } } @@ -575,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); @@ -623,7 +640,7 @@ export class Text extends Shape { * 1. the current line is the last line * 2. wrap is NONE * @param {Number} currentHeightPx - * @returns + * @returns {Boolean} */ _shouldHandleEllipsis(currentHeightPx: number): boolean { const fontSize = +this.fontSize(), diff --git a/src/shapes/TextPath.ts b/src/shapes/TextPath.ts index 6912fe55..1ae77462 100644 --- a/src/shapes/TextPath.ts +++ b/src/shapes/TextPath.ts @@ -551,7 +551,7 @@ Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING); * // underline text * shape.textDecoration('underline'); */ -Factory.addGetterSetter(TextPath, 'textDecoration', null); +Factory.addGetterSetter(TextPath, 'textDecoration', ''); /** * get/set kerning function. @@ -568,4 +568,4 @@ Factory.addGetterSetter(TextPath, 'textDecoration', null); * return 1; * }); */ -Factory.addGetterSetter(TextPath, 'kerningFunc', null); +Factory.addGetterSetter(TextPath, 'kerningFunc', undefined); diff --git a/src/shapes/Transformer.ts b/src/shapes/Transformer.ts index d5c6bd0e..6918da17 100644 --- a/src/shapes/Transformer.ts +++ b/src/shapes/Transformer.ts @@ -335,10 +335,12 @@ export class Transformer extends Group { this.update(); } }; - const additionalEvents = node._attrsAffectingSize - .map((prop) => prop + 'Change.' + this._getEventNamespace()) - .join(' '); - node.on(additionalEvents, onChange); + if (node._attrsAffectingSize.length) { + const additionalEvents = node._attrsAffectingSize + .map((prop) => prop + 'Change.' + this._getEventNamespace()) + .join(' '); + node.on(additionalEvents, onChange); + } node.on( TRANSFORM_CHANGE_STR.map( (e) => e + `.${this._getEventNamespace()}` @@ -1745,8 +1747,6 @@ Factory.addGetterSetter(Transformer, 'ignoreStroke', false); */ 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 * @method @@ -1767,6 +1767,9 @@ Factory.addGetterSetter(Transformer, 'node'); */ Factory.addGetterSetter(Transformer, 'nodes'); +// @ts-ignore +// deprecated +Factory.addGetterSetter(Transformer, 'node'); /** * get/set bounding box function. **IMPORTANT!** boundBondFunc operates in absolute coordinates. diff --git a/src/types.ts b/src/types.ts index 6ce992fe..f9d8af1f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ export interface GetSet { (): Type; - (v: Type): This; + (v: Type | null | undefined): This; } export interface Vector2d { diff --git a/test/sandbox.html b/test/sandbox.html index 2b17ce67..82bf29e9 100644 --- a/test/sandbox.html +++ b/test/sandbox.html @@ -13,6 +13,27 @@ width: 100vw; 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; + } diff --git a/test/unit/Image-test.ts b/test/unit/Image-test.ts index be9c9516..34ac6d87 100644 --- a/test/unit/Image-test.ts +++ b/test/unit/Image-test.ts @@ -107,17 +107,10 @@ describe('Image', function () { var trace = layer.getContext().getTrace(); - if (isBrowser) { - assert.equal( - 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();' - ); - } 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();' - ); - } + assert.equal( + 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();' + ); done(); }); @@ -241,17 +234,10 @@ describe('Image', function () { var trace = layer.getContext().getTrace(); - if (isBrowser) { - 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 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();' - ); - } + 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 HTMLImageElement],0,0,100,100);restore();' + ); done(); }); @@ -285,17 +271,10 @@ describe('Image', function () { var trace = layer.getContext().getTrace(); - if (isBrowser) { - 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 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();' - ); - } + 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 HTMLCanvasElement],0,0,578,200);restore();' + ); done(); }); diff --git a/test/unit/Node-test.ts b/test/unit/Node-test.ts index 599254b6..0fe1921c 100644 --- a/test/unit/Node-test.ts +++ b/test/unit/Node-test.ts @@ -2296,17 +2296,10 @@ describe('Node', function () { var bufferTrace = stage.bufferCanvas.getContext().getTrace(); - if (isBrowser) { - assert.equal( - sceneTrace, - '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( + sceneTrace, + 'clearRect(0,0,578,200);save();globalAlpha=0.5;drawImage([object HTMLCanvasElement],0,0,578,200);restore();' + ); assert.equal( bufferTrace, diff --git a/test/unit/Text-test.ts b/test/unit/Text-test.ts index 6f5ae001..b2d7ea52 100644 --- a/test/unit/Text-test.ts +++ b/test/unit/Text-test.ts @@ -146,7 +146,7 @@ describe('Text', function () { var text = new Konva.Text({ x: 10, y: 10, - text: '😬', + text: '😬👧🏿', fontSize: 50, letterSpacing: 1, }); @@ -159,7 +159,36 @@ describe('Text', function () { context.textBaseline = 'middle'; context.font = 'normal normal 50px Arial'; 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); }); @@ -282,7 +311,7 @@ describe('Text', function () { var oldWidth = text.width(); text.letterSpacing(10); - assert.equal(text.width(), oldWidth + 40); + assert.equal(text.width(), oldWidth + 50); layer.draw(); }); // ====================================================== @@ -780,7 +809,7 @@ describe('Text', function () { } 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); }); @@ -1244,9 +1273,9 @@ describe('Text', function () { // so we need to adjust offset const diff = isBrowser ? 4 : 50; 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(text3.width()) - 2005) < diff, true); - assert.equal(Math.abs(Math.round(text4.width()) - 1932) < diff, true); + assert.equal(Math.abs(Math.round(text2.width()) - 2616) < diff, true); + assert.equal(Math.abs(Math.round(text3.width()) - 2009) < diff, true); + assert.equal(Math.abs(Math.round(text4.width()) - 1936) < diff, true); }); it('default text color should be black', function () { diff --git a/test/unit/Transformer-test.ts b/test/unit/Transformer-test.ts index af6782df..1c930314 100644 --- a/test/unit/Transformer-test.ts +++ b/test/unit/Transformer-test.ts @@ -5088,4 +5088,49 @@ describe('Transformer', function () { }, 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' + ); + }); });