Merge branch 'master' of github.com:konvajs/konva

This commit is contained in:
Anton Lavrevov
2025-10-21 14:52:42 -05:00
13 changed files with 72 additions and 67 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ types
out.png out.png
cmj cmj
.test-temp .test-temp
.history
# Numerous always-ignore extensions # Numerous always-ignore extensions
*.diff *.diff

View File

@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="https://konvajs.org/android-chrome-192x192.png" alt="Konva logo" height="180" /> <img src="https://konvajs.org/img/icon.png" alt="Konva logo" height="60" />
</p> </p>
<h1 align="center">Konva</h1> <h1 align="center">Konva</h1>

View File

@@ -44,13 +44,13 @@
"test": "npm run test:browser && npm run test:node && npm run test:import", "test": "npm run test:browser && npm run test:node && npm run test:import",
"test:build": "PARCEL_WORKER_BACKEND=process 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 -a no-sandbox -a disable-setuid-sandbox", "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": "rimraf ./.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:canvas": "mocha -r tsx -r ./test/node-canvas-global-setup.mjs --extension ts --recursive './test/unit/' --exit", "test:node:canvas": "mocha -r tsx -r ./test/node-canvas-global-setup.mjs --extension ts --recursive './test/unit/' --exit",
"test:node:skia": "mocha -r tsx -r ./test/node-skia-global-setup.mjs --extension ts --recursive './test/unit/' --exit", "test:node:skia": "mocha -r tsx -r ./test/node-skia-global-setup.mjs --extension ts --recursive './test/unit/' --exit",
"test:node": "npm run test:node:canvas && npm run test:node:skia", "test:node": "npm run test:node:canvas && npm run test:node:skia",
"tsc": "tsc --removeComments", "tsc": "tsc --removeComments",
"rollup": "rollup -c", "rollup": "rollup -c",
"clean": "rm -rf ./lib && rm -rf ./types && rm -rf ./cmj && rm -rf ./test-build", "clean": "rimraf ./lib && rimraf ./types && rimraf ./cmj && rimraf ./test-build",
"watch": "rollup -c -w", "watch": "rollup -c -w",
"size": "size-limit" "size": "size-limit"
}, },
@@ -107,6 +107,7 @@
"parcel": "2.15.4", "parcel": "2.15.4",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"process": "^0.11.10", "process": "^0.11.10",
"rimraf": "^6.0.1",
"rollup": "^4.48.0", "rollup": "^4.48.0",
"rollup-plugin-typescript2": "^0.36.0", "rollup-plugin-typescript2": "^0.36.0",
"size-limit": "^11.2.0", "size-limit": "^11.2.0",

View File

@@ -32,7 +32,8 @@ export interface ContainerConfig extends NodeConfig {
*/ */
export abstract class Container< export abstract class Container<
ChildType extends Node = Node, ChildType extends Node = Node,
> extends Node<ContainerConfig> { Config extends ContainerConfig = ContainerConfig
> extends Node<Config> {
children: Array<ChildType> = []; children: Array<ChildType> = [];
/** /**

View File

@@ -196,7 +196,7 @@ export const Konva = {
_injectGlobal(Konva) { _injectGlobal(Konva) {
if (typeof glob.Konva !== 'undefined') { if (typeof glob.Konva !== 'undefined') {
console.error( console.error(
'Severa Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.' 'Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.'
); );
} }
glob.Konva = Konva; glob.Konva = Konva;

View File

@@ -10,7 +10,7 @@ import type { Layer } from './Layer.ts';
import type { Shape } from './Shape.ts'; import type { Shape } from './Shape.ts';
import type { Stage } from './Stage.ts'; import type { Stage } from './Stage.ts';
import type { GetSet, IRect, Vector2d } from './types.ts'; import type { GetSet, IRect, Vector2d } from './types.ts';
import { Transform, Util } from './Util.ts'; import { Transform, Util, type AnyString } from './Util.ts';
import { import {
getBooleanValidator, getBooleanValidator,
getNumberValidator, getNumberValidator,
@@ -134,9 +134,8 @@ type globalCompositeOperationType =
| 'color' | 'color'
| 'luminosity'; | 'luminosity';
export interface NodeConfig { // allow any custom attribute
// allow any custom attribute export type NodeConfig<Props extends Record<string, any> = {}> = Props & {
[index: string]: any;
x?: number; x?: number;
y?: number; y?: number;
width?: number; width?: number;
@@ -223,6 +222,20 @@ export type KonvaEventListener<This, EventType> = (
ev: KonvaEventObject<EventType, This> ev: KonvaEventObject<EventType, This>
) => void; ) => void;
export type CanvasConfig = {
x?: number;
y?: number;
width?: number;
height?: number;
pixelRatio?: number;
imageSmoothingEnabled?: boolean;
};
export type ImageConfig = CanvasConfig & {
mimeType?: string;
quality?: number;
};
/** /**
* Node constructor. Nodes are entities that can be transformed, layered, * Node constructor. Nodes are entities that can be transformed, layered,
* and have bound events. The stage, layers, groups, and shapes all extend Node. * and have bound events. The stage, layers, groups, and shapes all extend Node.
@@ -393,17 +406,13 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* drawBorder: true * drawBorder: true
* }); * });
*/ */
cache(config?: { cache(
x?: number; config?: CanvasConfig & {
y?: number; drawBorder?: boolean;
width?: number; offset?: number;
height?: number; hitCanvasPixelRatio?: number;
drawBorder?: boolean; }
offset?: number; ) {
pixelRatio?: number;
imageSmoothingEnabled?: boolean;
hitCanvasPixelRatio?: number;
}) {
const conf = config || {}; const conf = config || {};
let rect = {} as IRect; let rect = {} as IRect;
@@ -1015,13 +1024,13 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* @example * @example
* var x = node.getAttr('x'); * var x = node.getAttr('x');
*/ */
getAttr<T>(attr: string) { getAttr<AttrConfig extends Config, K extends AnyString<keyof Config>>(attr: K): K extends keyof AttrConfig ? AttrConfig[K] : any {
const method = 'get' + Util._capitalize(attr); const method = 'get' + Util._capitalize(attr as string);
if (Util._isFunction((this as any)[method])) { if (Util._isFunction((this as any)[method])) {
return (this as any)[method](); return (this as any)[method]();
} }
// otherwise get directly // otherwise get directly
return this.attrs[attr] as T | undefined; return this.attrs[attr];
} }
/** /**
* get ancestors * get ancestors
@@ -1050,8 +1059,8 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* @name Konva.Node#getAttrs * @name Konva.Node#getAttrs
* @returns {Object} * @returns {Object}
*/ */
getAttrs() { getAttrs(): Config {
return (this.attrs || {}) as Config & Record<string, any>; return (this.attrs || {});
} }
/** /**
* set multiple attrs at once using an object literal * set multiple attrs at once using an object literal
@@ -1065,7 +1074,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* fill: 'red' * fill: 'red'
* }); * });
*/ */
setAttrs(config: any) { setAttrs(config?: Config) {
this._batchTransformChanges(() => { this._batchTransformChanges(() => {
let key, method; let key, method;
if (!config) { if (!config) {
@@ -1624,7 +1633,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* @returns {Object} * @returns {Object}
*/ */
toObject() { toObject() {
let attrs = this.getAttrs() as any, let attrs = this.getAttrs(),
key, key,
val, val,
getter, getter,
@@ -2111,7 +2120,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* @example * @example
* var canvas = node.toCanvas(); * var canvas = node.toCanvas();
*/ */
toCanvas(config?) { toCanvas(config?: CanvasConfig) {
return this._toKonvaCanvas(config)._canvas; return this._toKonvaCanvas(config)._canvas;
} }
/** /**
@@ -2137,16 +2146,11 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing * @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
* @returns {String} * @returns {String}
*/ */
toDataURL(config?: { toDataURL(
x?: number; config?: ImageConfig & {
y?: number; callback?: (url: string) => void;
width?: number; }
height?: number; ) {
pixelRatio?: number;
mimeType?: string;
quality?: number;
callback?: (str: string) => void;
}) {
config = config || {}; config = config || {};
const mimeType = config.mimeType || null, const mimeType = config.mimeType || null,
quality = config.quality || null; quality = config.quality || null;
@@ -2187,16 +2191,11 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* } * }
* }); * });
*/ */
toImage(config?: { toImage(
x?: number; config?: ImageConfig & {
y?: number; callback?: (img: HTMLImageElement) => void;
width?: number; }
height?: number; ) {
pixelRatio?: number;
mimeType?: string;
quality?: number;
callback?: (img: HTMLImageElement) => void;
}) {
return new Promise<HTMLImageElement>((resolve, reject) => { return new Promise<HTMLImageElement>((resolve, reject) => {
try { try {
const callback = config?.callback; const callback = config?.callback;
@@ -2231,16 +2230,11 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* var blob = await node.toBlob({}); * var blob = await node.toBlob({});
* @returns {Promise<Blob>} * @returns {Promise<Blob>}
*/ */
toBlob(config?: { toBlob(
x?: number; config?: ImageConfig & {
y?: number; callback?: (blob: Blob | null) => void;
width?: number; }
height?: number; ) {
pixelRatio?: number;
mimeType?: string;
quality?: number;
callback?: (blob: Blob | null) => void;
}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const callback = config?.callback; const callback = config?.callback;
@@ -2405,8 +2399,8 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
* @example * @example
* node.setAttr('x', 5); * node.setAttr('x', 5);
*/ */
setAttr(attr: string, val) { setAttr<AttrConfig extends Config, K extends AnyString<keyof Config>>(attr: K, val: K extends keyof AttrConfig ? AttrConfig[K] : any) {
const func = this[SET + Util._capitalize(attr)]; const func = this[SET + Util._capitalize(attr as string)];
if (Util._isFunction(func)) { if (Util._isFunction(func)) {
func.call(this, val); func.call(this, val);
@@ -2422,7 +2416,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
drawNode?.batchDraw(); drawNode?.batchDraw();
} }
} }
_setAttr(key: string, val) { _setAttr<AttrConfig extends Config, K extends AnyString<keyof Config>>(key: K, val: K extends keyof AttrConfig ? AttrConfig[K] : any) {
const oldVal = this.attrs[key]; const oldVal = this.attrs[key];
if (oldVal === val && !Util.isObject(val)) { if (oldVal === val && !Util.isObject(val)) {
return; return;
@@ -2878,7 +2872,7 @@ export abstract class Node<Config extends NodeConfig = NodeConfig> {
} }
} }
interface AnimTo extends NodeConfig { interface AnimTo extends NodeConfig<Record<string, any>> {
onFinish?: Function; onFinish?: Function;
onUpdate?: Function; onUpdate?: Function;
duration?: number; duration?: number;

View File

@@ -26,7 +26,7 @@ export type ShapeConfigHandler<TTarget> = {
export type LineJoin = 'round' | 'bevel' | 'miter'; export type LineJoin = 'round' | 'bevel' | 'miter';
export type LineCap = 'butt' | 'round' | 'square'; export type LineCap = 'butt' | 'round' | 'square';
export interface ShapeConfig extends NodeConfig { export type ShapeConfig<Props extends Record<string, any> = {}> = NodeConfig<Props> & {
fill?: string | CanvasGradient; fill?: string | CanvasGradient;
fillPatternImage?: HTMLImageElement; fillPatternImage?: HTMLImageElement;
fillPatternX?: number; fillPatternX?: number;

View File

@@ -154,7 +154,7 @@ export const stages: Stage[] = [];
* }); * });
*/ */
export class Stage extends Container<Layer> { export class Stage extends Container<Layer, StageConfig> {
content: HTMLDivElement; content: HTMLDivElement;
pointerPos: Vector2d | null; pointerPos: Vector2d | null;
_pointerPositions: (Vector2d & { id?: number })[] = []; _pointerPositions: (Vector2d & { id?: number })[] = [];

View File

@@ -151,6 +151,9 @@ class TweenEngine {
} }
export interface TweenConfig extends NodeConfig { export interface TweenConfig extends NodeConfig {
easing?: typeof Easings[keyof typeof Easings];
yoyo?: boolean;
onReset?: Function;
onFinish?: Function; onFinish?: Function;
onUpdate?: Function; onUpdate?: Function;
duration?: number; duration?: number;
@@ -419,7 +422,7 @@ export class Tween {
// after tweening points of line we need to set original end // after tweening points of line we need to set original end
const attrs = Tween.attrs[node._id][this._id]; const attrs = Tween.attrs[node._id][this._id];
if (attrs.points && attrs.points.trueEnd) { if (attrs.points && attrs.points.trueEnd) {
node.setAttr('points', attrs.points.trueEnd); node.setAttr('points' as any, attrs.points.trueEnd);
} }
if (this.onFinish) { if (this.onFinish) {

View File

@@ -1121,3 +1121,5 @@ export const Util = {
} }
}, },
}; };
export type AnyString<T> = T | (string & {})

View File

@@ -7,6 +7,7 @@ const canvas = Canvas['default'] || Canvas;
// @ts-ignore // @ts-ignore
global.DOMMatrix = canvas.DOMMatrix; global.DOMMatrix = canvas.DOMMatrix;
// @ts-ignore
(global as any).Path2D ??= class Path2D { (global as any).Path2D ??= class Path2D {
constructor(path: any) { constructor(path: any) {
(this as any).path = path; (this as any).path = path;

View File

@@ -8,7 +8,7 @@ import type { GetSet } from '../types.ts';
import type { Context } from '../Context.ts'; import type { Context } from '../Context.ts';
import { getNumberOrArrayOfNumbersValidator } from '../Validators.ts'; import { getNumberOrArrayOfNumbersValidator } from '../Validators.ts';
export interface RectConfig extends ShapeConfig { export type RectConfig<Props extends Record<string, any> = {}> = ShapeConfig<Props> & {
cornerRadius?: number | number[]; cornerRadius?: number | number[];
} }
@@ -30,7 +30,7 @@ export interface RectConfig extends ShapeConfig {
* strokeWidth: 5 * strokeWidth: 5
* }); * });
*/ */
export class Rect extends Shape<RectConfig> { export class Rect<Props extends Record<string, any> = {}> extends Shape<RectConfig<Props>> {
_sceneFunc(context: Context) { _sceneFunc(context: Context) {
const cornerRadius = this.cornerRadius(), const cornerRadius = this.cornerRadius(),
width = this.width(), width = this.width(),

View File

@@ -2,8 +2,10 @@ import { Konva } from './_CoreInternals.ts';
// @ts-ignore // @ts-ignore
import { Canvas, DOMMatrix, Image, Path2D } from 'skia-canvas'; import { Canvas, DOMMatrix, Image, Path2D } from 'skia-canvas';
// @ts-ignore
global.DOMMatrix = DOMMatrix as any; global.DOMMatrix = DOMMatrix as any;
// @ts-ignore
global.Path2D = Path2D as any; global.Path2D = Path2D as any;
Path2D.prototype.toString = () => '[object Path2D]'; Path2D.prototype.toString = () => '[object Path2D]';