diff --git a/src/Node.ts b/src/Node.ts index 98a2f1ee..70963258 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -3,7 +3,18 @@ import { Factory } from './Factory'; import { SceneCanvas, HitCanvas, Canvas } from './Canvas'; import { Konva } from './Global'; import { Container } from './Container'; -import { GetSet, Vector2d, IRect } from './types'; +import { + GetSet, + Vector2d, + IRect, + ToCanvasConfig, + ToDataURLConfig, + ToImageConfig, + ToBlobConfig, + Size, + CacheConfig, + GetClientRectConfig, +} from './types'; import { DD } from './DragAndDrop'; import { getNumberValidator, @@ -172,11 +183,11 @@ export abstract class Node { // all change event listeners are attached to the prototype } - hasChildren() { + hasChildren(): boolean { return false; } - _clearCache(attr?: string) { + _clearCache(attr?: string): void { // if we want to clear transform cache // we don't really need to remove it from the cache // but instead mark as "dirty" @@ -300,17 +311,7 @@ export abstract class Node { * drawBorder: true * }); */ - cache(config?: { - x?: number; - y?: number; - width?: number; - height?: number; - drawBorder?: boolean; - offset?: number; - pixelRatio?: number; - imageSmoothingEnabled?: boolean; - hitCanvasPixelRatio?: number; - }) { + cache(config?: CacheConfig): typeof this | undefined { var conf = config || {}; var rect = {} as IRect; @@ -445,7 +446,7 @@ export abstract class Node { * @name Konva.Node#isCached * @returns {Boolean} */ - isCached() { + isCached(): boolean { return this._cache.has(CANVAS); } @@ -487,17 +488,12 @@ export abstract class Node { * rect.getClientRect(); * // returns Object {x: -2, y: 46, width: 104, height: 208} */ - getClientRect(config?: { - skipTransform?: boolean; - skipShadow?: boolean; - skipStroke?: boolean; - relativeTo?: Container; - }): { x: number; y: number; width: number; height: number } { + getClientRect(config?: GetClientRectConfig): IRect { // abstract method // redefine in Container and Shape throw new Error('abstract "getClientRect" method call'); } - _transformedRect(rect: IRect, top?: Node | null) { + _transformedRect(rect: IRect, top?: Node | null): IRect { var points = [ { x: rect.x, y: rect.y }, { x: rect.x + rect.width, y: rect.y }, @@ -527,7 +523,7 @@ export abstract class Node { height: maxY - minY, }; } - _drawCachedSceneCanvas(context: Context) { + _drawCachedSceneCanvas(context: Context): void { context.save(); context._applyOpacity(this); context._applyGlobalCompositeOperation(this); @@ -547,7 +543,7 @@ export abstract class Node { ); context.restore(); } - _drawCachedHitCanvas(context: Context) { + _drawCachedHitCanvas(context: Context): void { var canvasCache = this._getCanvasCache(), hitCanvas = canvasCache.hit; context.save(); @@ -750,7 +746,7 @@ export abstract class Node { * // remove listener by name * node.off('click.foo'); */ - off(evtStr?: string, callback?: Function) { + off(evtStr?: string, callback?: Function): typeof this { var events = (evtStr || '').split(SPACE), len = events.length, n, @@ -787,7 +783,7 @@ export abstract class Node { return this; } // some event aliases for third party integration like HammerJS - dispatchEvent(evt: any) { + dispatchEvent(evt: any): typeof this { var e = { target: this, type: evt.type, @@ -796,19 +792,23 @@ export abstract class Node { this.fire(evt.type, e); return this; } - addEventListener(type: string, handler: (e: Event) => void) { + addEventListener(type: string, handler: (e: Event) => void): typeof this { // we have to pass native event to handler this.on(type, function (evt) { handler.call(this, evt.evt); }); return this; } - removeEventListener(type: string) { + removeEventListener(type: string): typeof this { this.off(type); return this; } // like node.on - _delegate(event: string, selector: string, handler: (e: Event) => void) { + _delegate( + event: string, + selector: string, + handler: (e: Event) => void + ): void { var stopNode = this; this.on(event, function (evt) { var targets = evt.target.findAncestors(selector, true, stopNode); @@ -827,7 +827,7 @@ export abstract class Node { * @example * node.remove(); */ - remove() { + remove(): typeof this { if (this.isDragging()) { this.stopDrag(); } @@ -837,7 +837,7 @@ export abstract class Node { this._remove(); return this; } - _clearCaches() { + _clearCaches(): void { this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); @@ -845,7 +845,7 @@ export abstract class Node { this._clearSelfAndDescendantCache(VISIBLE); this._clearSelfAndDescendantCache(LISTENING); } - _remove() { + _remove(): void { // every cached attr that is calculated via node tree // traversal must be cleared when removing a node this._clearCaches(); @@ -898,9 +898,9 @@ export abstract class Node { * console.log(node.getId()); * }) */ - getAncestors() { + getAncestors(): Container[] { var parent = this.getParent(), - ancestors: Array = []; + ancestors: Array = []; while (parent) { ancestors.push(parent); @@ -915,8 +915,8 @@ export abstract class Node { * @name Konva.Node#getAttrs * @returns {Object} */ - getAttrs() { - return (this.attrs || {}) as Config & Record; + getAttrs(): Partial { + return this.attrs || {}; } /** * set multiple attrs at once using an object literal @@ -930,7 +930,7 @@ export abstract class Node { * fill: 'red' * }); */ - setAttrs(config: any) { + setAttrs(config?: Partial) { this._batchTransformChanges(() => { var key, method; if (!config) { @@ -1125,7 +1125,7 @@ export abstract class Node { // like node.position(pos) // for performance reasons, lets batch transform reset // so it work faster - _batchTransformChanges(func) { + _batchTransformChanges(func: () => void): void { this._batchingTransformChange = true; func(); this._batchingTransformChange = false; @@ -1136,14 +1136,14 @@ export abstract class Node { this._needClearTransformCache = false; } - setPosition(pos: Vector2d) { + setPosition(pos: Vector2d): typeof this { this._batchTransformChanges(() => { this.x(pos.x); this.y(pos.y); }); return this; } - getPosition() { + getPosition(): Vector2d { return { x: this.x(), y: this.y(), @@ -1161,7 +1161,7 @@ export abstract class Node { * // if you want to know position of the click, related to the rectangle you can use * rect.getRelativePointerPosition(); */ - getRelativePointerPosition() { + getRelativePointerPosition(): Vector2d | null { const stage = this.getStage(); if (!stage) { return null; @@ -1192,7 +1192,7 @@ export abstract class Node { * // so stage transforms are ignored * node.getAbsolutePosition(stage) */ - getAbsolutePosition(top?: Node) { + getAbsolutePosition(top?: Node): Vector2d { let haveCachedParent = false; let parent = this.parent; while (parent) { @@ -1217,7 +1217,7 @@ export abstract class Node { return absoluteTransform.getTranslation(); } - setAbsolutePosition(pos: Vector2d) { + setAbsolutePosition(pos: Vector2d): typeof this { const { x, y, ...origTrans } = this._clearTransform(); // don't clear translation @@ -1241,7 +1241,7 @@ export abstract class Node { return this; } - _setTransform(trans) { + _setTransform(trans): void { var key; for (key in trans) { @@ -1291,7 +1291,7 @@ export abstract class Node { * y: 2 * }); */ - move(change: Vector2d) { + move(change: Vector2d): typeof this { var changeX = change.x, changeY = change.y, x = this.x(), @@ -1308,7 +1308,7 @@ export abstract class Node { this.setPosition({ x: x, y: y }); return this; } - _eachAncestorReverse(func, top) { + _eachAncestorReverse(func: (node: Node) => void, top?: Node): void { var family: Array = [], parent = this.getParent(), len, @@ -1341,7 +1341,7 @@ export abstract class Node { * @param {Number} theta * @returns {Konva.Node} */ - rotate(theta: number) { + rotate(theta: number): typeof this { this.rotation(this.rotation() + theta); return this; } @@ -1413,7 +1413,7 @@ export abstract class Node { * @name Konva.Node#moveToBottom * @returns {Boolean} */ - moveToBottom() { + moveToBottom(): boolean { if (!this.parent) { Util.warn('Node has no parent. moveToBottom function is ignored.'); return false; @@ -1427,7 +1427,7 @@ export abstract class Node { } return false; } - setZIndex(zIndex) { + setZIndex(zIndex: number): typeof this { if (!this.parent) { Util.warn('Node has no parent. zIndex parameter is ignored.'); return this; @@ -1453,10 +1453,10 @@ export abstract class Node { * @name Konva.Node#getAbsoluteOpacity * @returns {Number} */ - getAbsoluteOpacity() { + getAbsoluteOpacity(): number { return this._getCache(ABSOLUTE_OPACITY, this._getAbsoluteOpacity); } - _getAbsoluteOpacity() { + _getAbsoluteOpacity(): number { var absOpacity = this.opacity(); var parent = this.getParent(); if (parent && !parent._isUnderCache) { @@ -1474,7 +1474,7 @@ export abstract class Node { * // move node from current layer into layer2 * node.moveTo(layer2); */ - moveTo(newContainer: any) { + moveTo(newContainer: Container): typeof this { // do nothing if new container is already parent if (this.getParent() !== newContainer) { this._remove(); @@ -1533,7 +1533,7 @@ export abstract class Node { * @name Konva.Node#toJSON * @returns {String} */ - toJSON() { + toJSON(): string { return JSON.stringify(this.toObject()); } /** @@ -1542,7 +1542,7 @@ export abstract class Node { * @name Konva.Node#getParent * @returns {Konva.Node} */ - getParent() { + getParent(): Container | null { return this.parent; } /** @@ -1561,7 +1561,7 @@ export abstract class Node { selector: string | Function, includeSelf?: boolean, stopNode?: Node - ) { + ): Node[] { var res: Array = []; if (includeSelf && this._isMatch(selector)) { @@ -1598,11 +1598,11 @@ export abstract class Node { selector: string | Function, includeSelf?: boolean, stopNode?: Container - ) { + ): Node | undefined { return this.findAncestors(selector, includeSelf, stopNode)[0]; } // is current node match passed selector? - _isMatch(selector: string | Function) { + _isMatch(selector: string | Function): boolean { if (!selector) { return false; } @@ -1663,7 +1663,7 @@ export abstract class Node { return this._getCache(STAGE, this._getStage); } - _getStage() { + _getStage(): Stage | null { var parent = this.getParent(); if (parent) { return parent.getStage(); @@ -1695,7 +1695,7 @@ export abstract class Node { * // fire click event that bubbles * node.fire('click', null, true); */ - fire(eventType: string, evt: any = {}, bubble?: boolean) { + fire(eventType: string, evt: any = {}, bubble?: boolean): typeof this { evt.target = evt.target || this; // bubble if (bubble) { @@ -1713,19 +1713,16 @@ export abstract class Node { * @name Konva.Node#getAbsoluteTransform * @returns {Konva.Transform} */ - getAbsoluteTransform(top?: Node | null) { + getAbsoluteTransform(top?: Node | null): Transform { // if using an argument, we can't cache the result. if (top) { return this._getAbsoluteTransform(top); } else { // if no argument, we can cache the result - return this._getCache( - ABSOLUTE_TRANSFORM, - this._getAbsoluteTransform - ) as Transform; + return this._getCache(ABSOLUTE_TRANSFORM, this._getAbsoluteTransform); } } - _getAbsoluteTransform(top?: Node) { + _getAbsoluteTransform(top?: Node): Transform { var at: Transform; // we we need position relative to an ancestor, we will iterate for all if (top) { @@ -1776,7 +1773,7 @@ export abstract class Node { * // get absolute scale x * var scaleX = node.getAbsoluteScale().x; */ - getAbsoluteScale(top?: Node) { + getAbsoluteScale(top?: Node): Vector2d { // do not cache this calculations, // because it use cache transform // this is special logic for caching with some shapes with shadow @@ -1806,7 +1803,7 @@ export abstract class Node { * // get absolute rotation * var rotation = node.getAbsoluteRotation(); */ - getAbsoluteRotation() { + getAbsoluteRotation(): number { // var parent: Node = this; // var rotation = 0; @@ -1823,8 +1820,8 @@ export abstract class Node { * @name Konva.Node#getTransform * @returns {Konva.Transform} */ - getTransform() { - return this._getCache(TRANSFORM, this._getTransform) as Transform; + getTransform(): Transform { + return this._getCache(TRANSFORM, this._getTransform); } _getTransform(): Transform { var m: Transform = this._cache.get(TRANSFORM) || new Transform(); @@ -1915,7 +1912,7 @@ export abstract class Node { } return node; } - _toKonvaCanvas(config) { + _toKonvaCanvas(config?: ToCanvasConfig): SceneCanvas { config = config || {}; var box = this.getClientRect(); @@ -1976,7 +1973,7 @@ export abstract class Node { * @example * var canvas = node.toCanvas(); */ - toCanvas(config?) { + toCanvas(config?: ToCanvasConfig): HTMLCanvasElement { return this._toKonvaCanvas(config)._canvas; } /** @@ -2002,16 +1999,7 @@ export abstract class Node { * @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing * @returns {String} */ - toDataURL(config?: { - x?: number; - y?: number; - width?: number; - height?: number; - pixelRatio?: number; - mimeType?: string; - quality?: number; - callback?: (str: string) => void; - }) { + toDataURL(config?: ToDataURLConfig): string { config = config || {}; var mimeType = config.mimeType || null, quality = config.quality || null; @@ -2052,16 +2040,7 @@ export abstract class Node { * } * }); */ - toImage(config?: { - x?: number; - y?: number; - width?: number; - height?: number; - pixelRatio?: number; - mimeType?: string; - quality?: number; - callback?: (img: HTMLImageElement) => void; - }) { + toImage(config?: ToImageConfig): Promise { return new Promise((resolve, reject) => { try { const callback = config?.callback; @@ -2096,16 +2075,7 @@ export abstract class Node { * var blob = await node.toBlob({}); * @returns {Promise} */ - toBlob(config?: { - x?: number; - y?: number; - width?: number; - height?: number; - pixelRatio?: number; - mimeType?: string; - quality?: number; - callback?: (blob: Blob | null) => void; - }) { + toBlob(config?: ToBlobConfig): Promise { return new Promise((resolve, reject) => { try { const callback = config?.callback; @@ -2123,12 +2093,12 @@ export abstract class Node { } }); } - setSize(size) { + setSize(size: IRect): typeof this { this.width(size.width); this.height(size.height); return this; } - getSize() { + getSize(): Size { return { width: this.width(), height: this.height(), @@ -2140,7 +2110,7 @@ export abstract class Node { * @name Konva.Node#getClassName * @returns {String} */ - getClassName() { + getClassName(): string { return this.className || this.nodeType; } /** @@ -2149,7 +2119,7 @@ export abstract class Node { * @name Konva.Node#getType * @returns {String} */ - getType() { + getType(): string { return this.nodeType; } getDragDistance(): number { @@ -2162,7 +2132,7 @@ export abstract class Node { return Konva.dragDistance; } } - _off(type, name?, callback?) { + _off(type, name?, callback?): void { var evtListeners = this.eventListeners[type], i, evtName, @@ -2190,7 +2160,7 @@ export abstract class Node { } } } - _fireChangeEvent(attr, oldVal, newVal) { + _fireChangeEvent(attr, oldVal, newVal): void { this._fire(attr + CHANGE, { oldVal: oldVal, newVal: newVal, @@ -2207,7 +2177,7 @@ export abstract class Node { * node.addName('selected'); * node.name(); // return 'red selected' */ - addName(name) { + addName(name: string): typeof this { if (!this.hasName(name)) { var oldName = this.name(); var newName = oldName ? oldName + ' ' + name : name; @@ -2227,7 +2197,7 @@ export abstract class Node { * node.hasName('selected'); // return false * node.hasName(''); // return false */ - hasName(name) { + hasName(name: string): boolean { if (!name) { return false; } @@ -2251,7 +2221,7 @@ export abstract class Node { * node.hasName('selected'); // return false * node.name(); // return 'red' */ - removeName(name) { + removeName(name: string): typeof this { var names = (this.name() || '').split(/\s/g); var index = names.indexOf(name); if (index !== -1) { @@ -2270,7 +2240,7 @@ export abstract class Node { * @example * node.setAttr('x', 5); */ - setAttr(attr, val) { + setAttr(attr: string, val: any): typeof this { var func = this[SET + Util._capitalize(attr)]; if (Util._isFunction(func)) { @@ -2281,13 +2251,13 @@ export abstract class Node { } return this; } - _requestDraw() { + _requestDraw(): void { if (Konva.autoDrawEnabled) { const drawNode = this.getLayer() || this.getStage(); drawNode?.batchDraw(); } } - _setAttr(key, val) { + _setAttr(key: string, val: any): void { var oldVal = this.attrs[key]; if (oldVal === val && !Util.isObject(val)) { return; @@ -2302,7 +2272,7 @@ export abstract class Node { } this._requestDraw(); } - _setComponentAttr(key, component, val) { + _setComponentAttr(key: string, component: string, val: any): void { var oldVal; if (val !== undefined) { oldVal = this.attrs[key]; @@ -2316,7 +2286,7 @@ export abstract class Node { this._fireChangeEvent(key, oldVal, val); } } - _fireAndBubble(eventType, evt, compareShape?) { + _fireAndBubble(eventType, evt, compareShape?): void { if (evt && this.nodeType === SHAPE) { evt.target = this; } @@ -2372,7 +2342,7 @@ export abstract class Node { return events; } - _fire(eventType, evt) { + _fire(eventType, evt): void { evt = evt || {}; evt.currentTarget = this; evt.type = eventType; @@ -2399,14 +2369,14 @@ export abstract class Node { * @name Konva.Node#draw * @returns {Konva.Node} */ - draw() { + draw(): typeof this { this.drawScene(); this.drawHit(); return this; } // drag & drop - _createDragElement(evt) { + _createDragElement(evt): void { var pointerId = evt ? evt.pointerId : undefined; var stage = this.getStage(); var ap = this.getAbsolutePosition(); @@ -2434,7 +2404,7 @@ export abstract class Node { * @method * @name Konva.Node#startDrag */ - startDrag(evt?: any, bubbleEvent = true) { + startDrag(evt?: any, bubbleEvent = true): void { if (!DD._dragElements.has(this._id)) { this._createDragElement(evt); } @@ -2452,7 +2422,7 @@ export abstract class Node { ); } - _setDragPosition(evt, elem) { + _setDragPosition(evt, elem): void { // const pointers = this.getStage().getPointersPositions(); // const pos = pointers.find(p => p.id === this._dragEventId); const pos = this.getStage()!._getPointerById(elem.pointerId); @@ -2494,7 +2464,7 @@ export abstract class Node { * @method * @name Konva.Node#stopDrag */ - stopDrag(evt?) { + stopDrag(evt?): void { const elem = DD._dragElements.get(this._id); if (elem) { elem.dragStatus = 'stopped'; @@ -2503,7 +2473,7 @@ export abstract class Node { DD._endDragAfter(evt); } - setDraggable(draggable) { + setDraggable(draggable): void { this._setAttr('draggable', draggable); this._dragChange(); } @@ -2513,12 +2483,12 @@ export abstract class Node { * @method * @name Konva.Node#isDragging */ - isDragging() { + isDragging(): boolean { const elem = DD._dragElements.get(this._id); return elem ? elem.dragStatus === 'dragging' : false; } - _listenDrag() { + _listenDrag(): void { this._dragCleanup(); this.on('mousedown.konva touchstart.konva', function (evt) { @@ -2546,7 +2516,7 @@ export abstract class Node { }); } - _dragChange() { + _dragChange(): void { if (this.attrs.draggable) { this._listenDrag(); } else { @@ -2574,7 +2544,7 @@ export abstract class Node { } } - _dragCleanup() { + _dragCleanup(): void { this.off('mousedown.konva'); this.off('touchstart.konva'); } diff --git a/src/types.ts b/src/types.ts index 6ce992fe..e89c0ef6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { Container } from './Container'; + export interface GetSet { (): Type; (v: Type): This; @@ -82,3 +84,148 @@ export interface RGB { export interface RGBA extends RGB { a: number; } + +export interface Size { + width: number; + height: number; +} + +export interface ToCanvasConfig { + /** + * The x coordinate of the canvas section to be exported. + * If omitted, the x coordinate of the node's rect will be used. + */ + x?: number; + /** + * The y coordinate of the canvas section to be exported. + * If omitted, the y coordinate of the node's rect will be used. + */ + y?: number; + /** + * The width of the canvas section to be exported. + * If omitted, the width of the node's rect will be used. + */ + width?: number; + /** + * The height of the canvas section to be exported. + * If omitted, the height of the node's rect will be used. + */ + height?: number; + /** + * The pixel ratio of the of output image. + * + * Use this property to increase resolution of the output image. For example, you may wish to increase the pixel ratio + * to support high resolution (retina) displays. + * + * `pixelRatio` will be used to multiply the size of exported image. For example, if you export a 500x500 section of the canvas + * with `pixelRatio: 2, the exported image will be 1000x1000. + * @default 1 + */ + pixelRatio?: number; + /** + * Whether to enable image smoothing. + * @default true + */ + imageSmoothingEnabled?: boolean; +} + +export type MIMEType = 'image/jpeg' | 'image/png' | 'image/webp'; + +interface MIMETypeConfig { + /** + * The MIME type of the exported image. Default is `image/png`. + * + * Browsers may support different MIME types. For example, Firefox and Chromium-based browsers support `image/webp` + * and `image/jpeg` in addition to `image/png`, while Safari supports only `image/png` and `image/jpeg`. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL#browser_compatibility. + * @default 'image/png' + */ + mimeType?: MIMEType; +} + +interface QualityConfig { + /** + * The quality of the exported image. Values from 0 to 1 are supported, where 0 is poorest quality and 1 is best quality. + * + * This only applies to `image/jpeg` and `image/webp` MIME types, which are lossy formats and support quality settings. + * @default 1 + */ + quality?: number; +} + +export interface ToDataURLConfig + extends ToCanvasConfig, + MIMETypeConfig, + QualityConfig { + /** + * A callback function that will be called with the data URL of the exported image. + * @param dataURL The data URL of the exported image. + * @returns void + */ + callback?: (dataURL: string) => void; +} + +export interface ToImageConfig + extends ToCanvasConfig, + MIMETypeConfig, + QualityConfig { + /** + * A callback function that will be called with the exported image element. + * @param image The exported image element. + * @returns void + */ + callback?: (image: HTMLImageElement) => void; +} + +export interface ToBlobConfig + extends ToCanvasConfig, + MIMETypeConfig, + QualityConfig { + /** + * A callback function that will be called with the exported blob. + * @param blob The exported blob, or null if the browser was unable to create the blob for any reason. + * @returns void + */ + callback?: (blob: Blob | null) => void; +} + +export interface CacheConfig extends ToCanvasConfig { + /** + * When set to `true`, a red border will be drawn around the cached region. Used for debugging. + * @default false + */ + drawBorder?: boolean; + /** + * Increases the size of the cached region by the specified amount of pixels in each direction. + * @default 0 + */ + offset?: number; + /** + * The pixel ratio of the cached hit canvas. Lower pixel ratios can result in better performance, but less accurate hit detection. + * @default 1 + */ + hitCanvasPixelRatio?: number; +} + +export interface GetClientRectConfig { + /** + * Whether to apply the current node's transforms when calculating the client rect. + * @default false + */ + skipTransform?: boolean; + /** + * Whether to apply shadow to the node when calculating the client rect. + * @default false + */ + skipShadow?: boolean; + /** + * Whether to apply stroke to the node when calculating the client rect. + * @default false + */ + skipStroke?: boolean; + /** + * If provided, the client rect will be calculated relative to the specified container. Must be a parent of the node. + */ + relativeTo?: Container; +}