import { HitCanvas, SceneCanvas } from "./Canvas.js"; import { isCSSFiltersSupported } from "./Context.js"; import { DD } from "./DragAndDrop.js"; import * as PointerEvents from "./PointerEvents.js"; import { Factory } from "./Factory.js"; import { Konva } from "./Global.js"; import { Transform, TypedArray, Util } from "./Util.js"; import { getBooleanValidator, getNumberValidator, getStringValidator, } from "./Validators.js"; // CSS filter parser for fallback to function filters. function parseCSSFilters(cssFilter) { const steps = []; const filterRegex = /(\w+)\(([^)]*)\)/g; let match; while ((match = filterRegex.exec(cssFilter)) !== null) { const [, name, argument] = match; if (![ 'blur', 'brightness', 'contrast', 'grayscale', 'sepia', 'invert', ].includes(name)) { Util.warn(`CSS filter "${name}" is not supported in fallback mode. Consider using function filters for better compatibility.`); continue; } const value = argument.trim() === '' ? name === 'blur' ? 0 : 1 : parseFloat(argument) / (argument.includes('%') ? 100 : 1); steps.push({ name, value }); } return function (imageData, pixelRatio = 1) { var _a; for (const { name, value } of steps) { if (['grayscale', 'sepia', 'invert'].includes(name)) { const amount = Math.min(1, Math.max(0, value)); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i], g = data[i + 1], b = data[i + 2]; let red, green, blue; if (name === 'grayscale') { red = green = blue = 0.2126 * r + 0.7152 * g + 0.0722 * b; } else if (name === 'sepia') { red = 0.393 * r + 0.769 * g + 0.189 * b; green = 0.349 * r + 0.686 * g + 0.168 * b; blue = 0.272 * r + 0.534 * g + 0.131 * b; } else { red = 255 - r; green = 255 - g; blue = 255 - b; } // CSS interpolates the color matrix before clamping the result. data[i] = r + (red - r) * amount; data[i + 1] = g + (green - g) * amount; data[i + 2] = b + (blue - b) * amount; } continue; } const filter = (_a = Konva.Filters) === null || _a === void 0 ? void 0 : _a[Util._capitalize(name)]; if (!filter) continue; // Supply filter parameters without setters or changes to the node. const context = Object.create(this); context.attrs = { ...this.attrs }; if (name === 'blur') context.attrs.blurRadius = value * 0.5; if (name === 'brightness') context.attrs.brightness = value; if (name === 'contrast') context.attrs.contrast = 100 * (Math.sqrt(value) - 1); filter.call(context, imageData, pixelRatio); } }; } // CONSTANTS const ABSOLUTE_OPACITY = 'absoluteOpacity', ABSOLUTE_TRANSFORM = 'absoluteTransform', CHANGE = 'Change', CHILDREN = 'children', KONVA = 'konva', LISTENING = 'listening', MOUSEENTER = 'mouseenter', MOUSELEAVE = 'mouseleave', POINTERENTER = 'pointerenter', POINTERLEAVE = 'pointerleave', TOUCHENTER = 'touchenter', TOUCHLEAVE = 'touchleave', NON_BUBBLING_EVENTS = [ MOUSEENTER, MOUSELEAVE, POINTERENTER, POINTERLEAVE, TOUCHENTER, TOUCHLEAVE, ], SET = 'set', SHAPE = 'Shape', SPACE = ' ', STAGE = 'stage', TRANSFORM = 'transform', UPPER_STAGE = 'Stage', VISIBLE = 'visible', TRANSFORM_CHANGE_STR = [ 'xChange.konva', 'yChange.konva', 'scaleXChange.konva', 'scaleYChange.konva', 'skewXChange.konva', 'skewYChange.konva', 'rotationChange.konva', 'offsetXChange.konva', 'offsetYChange.konva', 'transformsEnabledChange.konva', ].join(SPACE); let idCounter = 1; /** * Node constructor. Nodes are entities that can be transformed, layered, * and have bound events. The stage, layers, groups, and shapes all extend Node. * @constructor * @memberof Konva * @param {Object} config * @@nodeParams */ export class Node { constructor(config) { this._id = idCounter++; this.eventListeners = {}; this.attrs = {}; this.index = 0; this.parent = null; // derived values (transforms, visibility, opacity...), see _getCache() this._cache = {}; // canvases of cache(), see _getCanvasCache() this._canvasCache = null; this._batchingTransformChange = false; this._needClearTransformCache = false; this._filterUpToDate = false; this._isUnderCache = false; this._dragEventId = null; this._shouldFireChangeEvents = false; // on initial set attrs wi don't need to fire change events // because nobody is listening to them yet this.setAttrs(config); this._shouldFireChangeEvents = true; // all change event listeners are attached to the prototype } hasChildren() { return false; } _clearCache(attr) { // if we want to clear transform cache // we don't really need to remove it from the cache // but instead mark as "dirty" // so we don't need to create a new instance next time if ((attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM) && this._cache[attr]) { this._cache[attr].dirty = true; } else if (attr) { this._cache[attr] = undefined; } else { this._cache = {}; // clearCache() also reaches descendants whose ancestor transform changes // were suppressed while cached. Invalidate their saved Transformer bounds. if (this._attrsVersion !== undefined) this._attrsVersion++; } } // drop the cached canvases and give their memory back _releaseCanvasCache() { const cache = this._canvasCache; if (cache) { Util.releaseCanvas(cache.scene._canvas, cache.filter._canvas, ...(cache.hit ? [cache.hit._canvas] : [])); this._canvasCache = null; } } _getCache(attr, privateGetter) { let cache = this._cache[attr]; // 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); // if not cached, we need to set it using the private getter method. if (invalid) { cache = privateGetter.call(this); this._cache[attr] = cache; } return cache; } _getCanvasCache() { return this._canvasCache; } /* * when the logic for a cached result depends on ancestor propagation, use this * method to clear self and children cache */ _clearSelfAndDescendantCache(attr) { this._clearCache(attr); // trigger clear cache, so transformer can use it if (attr === undefined || attr === ABSOLUTE_TRANSFORM) { this.fire('absoluteTransformChange'); } } static _runAfterUpdateBatch(cb) { if (Node._updateBatchDepth > 0) { Node._pendingUpdates.add(cb); } else { cb(); } } static _flushUpdateBatch() { if (Node._updateBatchDepth > 0) return; const callbacks = Node._pendingUpdates; if (!callbacks.size) return; Node._pendingUpdates = new Set(); callbacks.forEach((callback) => callback()); } static _updateBatch(fn) { Node._updateBatchDepth++; try { return fn(); } finally { Node._updateBatchDepth--; Node._flushUpdateBatch(); } } static _beginUpdateBatch() { Node._updateBatchDepth++; let finished = false; return () => { if (finished) return; finished = true; Node._updateBatchDepth--; Node._flushUpdateBatch(); }; } /** * clear cached canvas * @method * @name Konva.Node#clearCache * @returns {Konva.Node} * @example * node.clearCache(); */ clearCache() { this._releaseCanvasCache(); this._clearSelfAndDescendantCache(); this._requestDraw(); return this; } /** * cache node to improve drawing performance, apply filters, or create more accurate * hit regions. For all basic shapes size of cache canvas will be automatically detected. * If you need to cache your custom `Konva.Shape` instance you have to pass shape's bounding box * properties. Look at [https://konvajs.org/docs/performance/Shape_Caching.html](https://konvajs.org/docs/performance/Shape_Caching.html) for more information. * @method * @name Konva.Node#cache * @param {Object} [config] * @param {Number} [config.x] * @param {Number} [config.y] * @param {Number} [config.width] * @param {Number} [config.height] * @param {Number} [config.offset] increase canvas size by `offset` pixel in all directions. * @param {Boolean} [config.drawBorder] when set to true, a red border will be drawn around the cached * region for debugging purposes * @param {Number} [config.pixelRatio] pixel ratio of the cache canvas, the canvas pixels per CSS pixel of the node. Default is `Konva.pixelRatio`, the device pixel ratio. A higher value keeps the cache sharp when the node is scaled up. * @param {Boolean} [config.imageSmoothingEnabled] control imageSmoothingEnabled property of created canvas for cache * @param {Number} [config.hitCanvasPixelRatio] change quality (or pixel ratio) of cached hit canvas. * @returns {Konva.Node} * @example * // cache a shape with the x,y position of the bounding box at the center and * // the width and height of the bounding box equal to the width and height of * // the shape obtained from shape.width() and shape.height() * image.cache(); * * // cache a node and define the bounding box position and size * node.cache({ * x: -30, * y: -30, * width: 100, * height: 200 * }); * * // cache a node and draw a red border around the bounding box * // for debugging purposes * node.cache({ * x: -30, * y: -30, * width: 100, * height: 200, * offset : 10, * drawBorder: true * }); */ cache(config) { const conf = config || {}; let rect = {}; // don't call getClientRect if we have all attributes // it means call it only if have one undefined if (conf.x === undefined || conf.y === undefined || conf.width === undefined || conf.height === undefined) { const wasUnderCache = this._isUnderCache; this._isUnderCache = true; rect = this.getClientRect({ skipTransform: true, relativeTo: this.getParent() || undefined, }); this._isUnderCache = wasUnderCache; } let x = conf.x === undefined ? Math.floor(rect.x) : conf.x, y = conf.y === undefined ? Math.floor(rect.y) : conf.y, // round the far edge, not the size, so the last row and column fit in width = Math.ceil(conf.width || rect.x + rect.width - x), height = Math.ceil(conf.height || rect.y + rect.height - y), pixelRatio = conf.pixelRatio, offset = conf.offset || 0, drawBorder = conf.drawBorder || false, hitCanvasPixelRatio = conf.hitCanvasPixelRatio || 1; if (width <= 0 || height <= 0 || !isFinite(x + y + width + height)) { Util.error(`Can not cache the node. Its size is 0 or its bounds are not finite numbers (${x}, ${y}, ${width}x${height}). Caching is skipped.`); return this; } width += offset * 2; height += offset * 2; x -= offset; y -= offset; const cachedSceneCanvas = new SceneCanvas({ pixelRatio: pixelRatio, width: width, height: height, }), cachedFilterCanvas = new SceneCanvas({ pixelRatio: pixelRatio, width: 0, height: 0, willReadFrequently: true, }), sceneContext = cachedSceneCanvas.getContext(); cachedSceneCanvas.isCache = true; this._releaseCanvasCache(); this._filterUpToDate = false; if (conf.imageSmoothingEnabled === false) { cachedSceneCanvas.getContext()._context.imageSmoothingEnabled = false; cachedFilterCanvas.getContext()._context.imageSmoothingEnabled = false; } sceneContext.save(); sceneContext.translate(-x, -y); // extra flag to skip on getAbsolute opacity calc this._isUnderCache = true; this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); try { this.drawScene(cachedSceneCanvas, this); } catch (e) { Util.releaseCanvas(cachedSceneCanvas._canvas); throw e; } finally { this._isUnderCache = false; // descendants cached their opacity relative to this node while drawing this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); sceneContext.restore(); cachedSceneCanvas._releaseIsolationCanvas(); } // this will draw a red border around the cached box for // debugging purposes if (drawBorder) { sceneContext.save(); sceneContext.beginPath(); sceneContext.rect(0, 0, width, height); sceneContext.closePath(); sceneContext.setAttr('strokeStyle', 'red'); sceneContext.setAttr('lineWidth', 5); sceneContext.stroke(); sceneContext.restore(); } this._canvasCache = { scene: cachedSceneCanvas, filter: cachedFilterCanvas, // the hit canvas is built on demand (see _getCachedHitCanvas) hit: null, hitConfig: { pixelRatio: hitCanvasPixelRatio, width: width, height: height, }, x: x, y: y, }; // Entering cache space changes non-scaling stroke bounds for descendants. this._clearSelfAndDescendantCache(); this._requestDraw(); return this; } // build the hit canvas of a cached node lazily - only when the hit graph // is actually needed, so a non-listening node never allocates it // https://github.com/konvajs/konva/issues/2009 _getCachedHitCanvas(top) { // top === this means the node is drawing into its own cache right now: // the build below re-enters drawHit for this very node, and consulting // the not-yet-stored canvas again would recurse forever if (top === this) { return null; } const cache = this._getCanvasCache(); if (!cache) { return null; } if (cache.hit) { return cache.hit; } const hitCanvas = new HitCanvas(cache.hitConfig); hitCanvas.isCache = true; const hitContext = hitCanvas.getContext(); hitContext.save(); hitContext.translate(-cache.x, -cache.y); this.drawHit(hitCanvas, this); hitContext.restore(); cache.hit = hitCanvas; return hitCanvas; } /** * determine if node is currently cached * @method * @name Konva.Node#isCached * @returns {Boolean} */ isCached() { return !!this._canvasCache; } /** * Return client rectangle {x, y, width, height} of node. This rectangle also include all styling (strokes, shadows, etc). * The purpose of the method is similar to getBoundingClientRect API of the DOM. * Non-scaling stroke padding is converted from its drawing canvas to the requested coordinates. * Cached strokes use the cache's coordinates; their pixels scale with the cached bitmap. * @method * @name Konva.Node#getClientRect * @param {Object} config * @param {Boolean} [config.skipTransform] should we apply transform to node for calculating rect? * @param {Boolean} [config.skipShadow] should we apply shadow to the node for calculating bound box? * @param {Boolean} [config.skipStroke] should we apply stroke to the node for calculating bound box? * @param {Object} [config.relativeTo] calculate client rect relative to one of the parents * @returns {Object} rect with {x, y, width, height} properties * @example * var rect = new Konva.Rect({ * width : 100, * height : 100, * x : 50, * y : 50, * strokeWidth : 4, * stroke : 'black', * offsetX : 50, * scaleY : 2 * }); * * // get client rect without think off transformations (position, rotation, scale, offset, etc) * rect.getClientRect({ skipTransform: true}); * // returns { * // x : -2, // two pixels for stroke / 2 * // y : -2, * // width : 104, // increased by 4 for stroke * // height : 104 * //} * * // get client rect with transformation applied * rect.getClientRect(); * // returns Object {x: -2, y: 46, width: 104, height: 208} */ getClientRect(config) { // abstract method // redefine in Container and Shape throw new Error('abstract "getClientRect" method call'); } _getCachedSceneRect(config) { const cache = config._forDrawing && !this._isUnderCache && this._getCanvasCache(); if (!cache) return; const rect = { x: cache.x, y: cache.y, width: cache.scene.width / cache.scene.pixelRatio, height: cache.scene.height / cache.scene.pixelRatio, }; return config.skipTransform ? rect : this._transformedRect(rect, config.relativeTo); } _transformedRect(rect, top) { return this.getAbsoluteTransform(top)._getTransformedRect(rect); } _drawCachedSceneCanvas(context) { context.save(); context._applyOpacity(this); context._applyGlobalCompositeOperation(this); const canvasCache = this._getCanvasCache(); context.translate(canvasCache.x, canvasCache.y); const cacheCanvas = this._getCachedSceneCanvas(); const ratio = cacheCanvas.pixelRatio; context.drawImage(cacheCanvas._canvas, 0, 0, cacheCanvas.width / ratio, cacheCanvas.height / ratio); context.restore(); } _drawCachedHitCanvas(context, hitCanvas) { const canvasCache = this._getCanvasCache(); context.save(); context.translate(canvasCache.x, canvasCache.y); context.drawImage(hitCanvas._canvas, 0, 0, hitCanvas.width / hitCanvas.pixelRatio, hitCanvas.height / hitCanvas.pixelRatio); context.restore(); } _getCachedSceneCanvas() { let filters = this.filters(), cachedCanvas = this._getCanvasCache(), sceneCanvas = cachedCanvas.scene, filterCanvas = cachedCanvas.filter, filterContext = filterCanvas.getContext(), len, imageData, n, filter; if (!filters || filters.length === 0) { return sceneCanvas; } if (this._filterUpToDate) { return filterCanvas; } let useNativeOnly = true; for (let i = 0; i < filters.length; i++) { if (typeof filters[i] !== 'string' || !isCSSFiltersSupported()) { useNativeOnly = false; break; } } const ratio = sceneCanvas.pixelRatio; filterCanvas.setSizeIfChanged(sceneCanvas._logicalWidth, sceneCanvas._logicalHeight); if (useNativeOnly) { // Canvas filters use backing-pixel coordinates, independent of the CTM. // Scale length tokens, leaving colors, percentages and URL references alone. const finalFilter = filters .join(' ') .replace(/url\((?:[^()"']|"[^"]*"|'[^']*')*\)|"[^"]*"|'[^']*'|([-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?)(px|em|rem|ex|ch|cap|ic|lh|rlh|vw|vh|vmin|vmax|cm|mm|q|in|pt|pc)\b/gi, (token, value, unit) => value === undefined ? token : `${Number(value) * ratio}${unit}`); filterContext.clear(); filterContext.save(); filterContext.setAttr('filter', finalFilter); filterContext.drawImage(sceneCanvas._canvas, 0, 0, sceneCanvas.getWidth() / ratio, sceneCanvas.getHeight() / ratio); filterContext.restore(); this._filterUpToDate = true; return filterCanvas; } try { len = filters.length; filterContext.clear(); // copy cached canvas onto filter context filterContext.drawImage(sceneCanvas._canvas, 0, 0, sceneCanvas.getWidth() / ratio, sceneCanvas.getHeight() / ratio); imageData = filterContext.getImageData(0, 0, filterCanvas.getWidth(), filterCanvas.getHeight()); // apply filters to filter context for (n = 0; n < len; n++) { filter = filters[n]; if (typeof filter === 'string') { filter = parseCSSFilters(filter); } filter.call(this, imageData, ratio); } filterContext.putImageData(imageData, 0, 0); } catch (e) { Util.error('Unable to apply filter. ' + e.message + ' This post my help you https://konvajs.org/docs/posts/Tainted_Canvas.html.'); } this._filterUpToDate = true; return filterCanvas; } /** * bind events to the node. KonvaJS supports mouseover, mousemove, * mouseout, mouseenter, mouseleave, mousedown, mouseup, wheel, contextmenu, click, dblclick, touchstart, touchmove, * touchend, tap, dbltap, dragstart, dragmove, dragend and destroy events. * Pass in a string of events delimited by a space to bind multiple events at once * such as 'mousedown mouseup mousemove'. Include a namespace to bind an * event by name such as 'click.foobar'. * @method * @name Konva.Node#on * @param {String} evtStr e.g. 'click', 'mousedown touchstart', 'mousedown.foo touchstart.foo' * @param {Function} handler The handler function. The first argument of that function is event object. Event object has `target` as main target of the event, `currentTarget` as current node listener and `evt` as native browser event. * @returns {Konva.Node} * @example * // add click listener * node.on('click', function() { * console.log('you clicked me!'); * }); * * // get the target node * node.on('click', function(evt) { * console.log(evt.target); * }); * * // stop event propagation * node.on('click', function(evt) { * evt.cancelBubble = true; * }); * * // bind multiple listeners * node.on('click touchstart', function() { * console.log('you clicked/touched me!'); * }); * * // namespace listener * node.on('click.foo', function() { * console.log('you clicked/touched me!'); * }); * * // get the event type * node.on('click tap', function(evt) { * var eventType = evt.type; * }); * * // get native event object * node.on('click tap', function(evt) { * var nativeEvent = evt.evt; * }); * * // for change events, get the old and new val * node.on('xChange', function(evt) { * var oldVal = evt.oldVal; * var newVal = evt.newVal; * }); * * // get event targets * // with event delegations * layer.on('click', 'Group', function(evt) { * var shape = evt.target; * var group = evt.currentTarget; * }); */ on(...args) { this._prepareListeners(); const evtStr = args[0]; const selectorOrHandler = args[1]; const handler = args[2]; if (args.length === 3) { return this._delegate.apply(this, args); } const events = evtStr.split(SPACE); /* * loop through types and attach event listeners to * each one. eg. 'click mouseover.namespace mouseout' * will create three event bindings */ for (let n = 0; n < events.length; n++) { const event = events[n]; const parts = event.split('.'); const baseEvent = parts[0]; const name = parts[1] || ''; // create events array if it doesn't exist if (!this.eventListeners[baseEvent]) { this.eventListeners[baseEvent] = []; } this.eventListeners[baseEvent].push({ name, handler: selectorOrHandler }); } return this; } /** * remove event bindings from the node. Pass in a string of * event types delimmited by a space to remove multiple event * bindings at once such as 'mousedown mouseup mousemove'. * include a namespace to remove an event binding by name * such as 'click.foobar'. If you only give a name like '.foobar', * all events in that namespace will be removed. * @method * @name Konva.Node#off * @param {String} evtStr e.g. 'click', 'mousedown touchstart', '.foobar' * @returns {Konva.Node} * @example * // remove listener * node.off('click'); * * // remove multiple listeners * node.off('click touchstart'); * * // remove listener by name * node.off('click.foo'); */ // listeners added to a prototype go into its own map, not the parent's, // and the lists flattened from the prototype chain so far are stale _prepareListeners() { if (this === this.constructor.prototype) { if (!this.hasOwnProperty('eventListeners')) { this.eventListeners = {}; } Node.protoListenerMap = new WeakMap(); } } off(evtStr, callback) { this._prepareListeners(); let events = (evtStr || '').split(SPACE), len = events.length, n, t, event, parts, baseEvent, name; if (!evtStr) { // remove all events for (t in this.eventListeners) { this._off(t); } } for (n = 0; n < len; n++) { event = events[n]; parts = event.split('.'); baseEvent = parts[0]; name = parts[1]; if (baseEvent) { if (this.eventListeners[baseEvent]) { this._off(baseEvent, name, callback); } } else { for (t in this.eventListeners) { this._off(t, name, callback); } } } return this; } // some event aliases for third party integration like HammerJS dispatchEvent(evt) { const e = { target: this, type: evt.type, evt: evt, }; this.fire(evt.type, e); return this; } addEventListener(type, handler) { // we have to pass native event to handler this.on(type, function (evt) { handler.call(this, evt.evt); }); return this; } removeEventListener(type) { this.off(type); return this; } // like node.on _delegate(event, selector, handler) { const stopNode = this; this.on(event, function (evt) { const targets = evt.target.findAncestors(selector, true, stopNode); for (let i = 0; i < targets.length; i++) { evt = Util.cloneObject(evt); evt.currentTarget = targets[i]; handler.call(targets[i], evt); } }); return this; } /** * remove a node from parent, but don't destroy. You can reuse the node later. * @method * @name Konva.Node#remove * @returns {Konva.Node} * @example * node.remove(); */ remove() { if (this.isDragging()) { this.stopDrag(); } // we can have drag element but that is not dragged yet // so just clear it DD._dragElements.delete(this._id); // also remove any dragging descendants, otherwise they'll // have a broken parent chain and crash on getStage() DD._dragElements.forEach((elem, key) => { if (this.isAncestorOf(elem.node)) { DD._dragElements.delete(key); } }); this._remove(); return this; } _clearCaches() { this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); this._clearSelfAndDescendantCache(STAGE); this._clearSelfAndDescendantCache(VISIBLE); this._clearSelfAndDescendantCache(LISTENING); } _remove() { const parent = this.getParent(); if (parent && parent.children) { parent.children.splice(this.index, 1); parent._setChildrenIndices(); this.parent = null; } // every cached attr that is calculated via node tree // traversal must be cleared when removing a node this._clearCaches(); } /** * remove and destroy a node. Kill it and delete forever! You should not reuse node after destroy(). * If the node is a container (Group, Stage or Layer) it will destroy all children too. * The node fires a `destroy` event first, so anything holding a reference to it can let go. * When an ancestor is being destroyed the node is already detached from its parent at that point. * @method * @name Konva.Node#destroy * @example * node.on('destroy', () => console.log('gone')); * node.destroy(); */ destroy() { this._fire('destroy', { target: this }); PointerEvents.releaseCapturesOf(this); this.remove(); this.clearCache(); return this; } /** * get attr * @method * @name Konva.Node#getAttr * @param {String} attr * @returns {Integer|String|Object|Array} * @example * var x = node.getAttr('x'); */ getAttr(attr) { const method = 'get' + Util._capitalize(attr); if (Util._isFunction(this[method])) { return this[method](); } // otherwise get directly return this.attrs[attr]; } /** * get ancestors * @method * @name Konva.Node#getAncestors * @returns {Array} * @example * shape.getAncestors().forEach(function(node) { * console.log(node.id()); * }) */ getAncestors() { let parent = this.getParent(), ancestors = []; while (parent) { ancestors.push(parent); parent = parent.getParent(); } return ancestors; } /** * get attrs object literal * @method * @name Konva.Node#getAttrs * @returns {Object} */ getAttrs() { return this.attrs || {}; } /** * set multiple attrs at once using an object literal * @method * @name Konva.Node#setAttrs * @param {Object} config object containing key value pairs * @returns {Konva.Node} * @example * node.setAttrs({ * x: 5, * fill: 'red' * }); */ setAttrs(config) { this._batchTransformChanges(() => { let key, method; if (!config) { return this; } for (key in config) { if (key === CHILDREN) { continue; } method = SET + Util._capitalize(key); // use setter if available if (Util._isFunction(this[method])) { this[method](config[key]); } else { // otherwise set directly this._setAttr(key, config[key]); } } }); return this; } /** * determine if node is listening for events by taking into account ancestors. * * Parent | Self | isListening * listening | listening | * ----------+-----------+------------ * T | T | T * T | F | F * F | T | F * F | F | F * * @method * @name Konva.Node#isListening * @returns {Boolean} */ isListening() { return this._getCache(LISTENING, this._isListening); } _isListening(relativeTo) { const listening = this.listening(); if (!listening) { return false; } const parent = this.getParent(); if (parent && parent !== relativeTo && this !== relativeTo) { return parent._isListening(relativeTo); } else { return true; } } /** * determine if node is visible by taking into account ancestors. * * Parent | Self | isVisible * visible | visible | * ----------+-----------+------------ * T | T | T * T | F | F * F | T | F * F | F | F * @method * @name Konva.Node#isVisible * @returns {Boolean} */ isVisible() { return this._getCache(VISIBLE, this._isVisible); } _isVisible(relativeTo) { const visible = this.visible(); if (!visible) { return false; } const parent = this.getParent(); if (parent && parent !== relativeTo && this !== relativeTo) { return parent._isVisible(relativeTo); } else { return true; } } shouldDrawHit(top) { if (top) { return this._isVisible(top) && this._isListening(top); } return this.isListening() && this.isVisible(); } /** * show node. set visible = true * @method * @name Konva.Node#show * @returns {Konva.Node} */ show() { this.visible(true); return this; } /** * hide node. Hidden nodes are no longer detectable * @method * @name Konva.Node#hide * @returns {Konva.Node} */ hide() { this.visible(false); return this; } getZIndex() { return this.index || 0; } /** * get absolute z-index which takes into account sibling * and ancestor indices * @method * @name Konva.Node#getAbsoluteZIndex * @returns {Integer} */ getAbsoluteZIndex() { let depth = this.getDepth(), that = this, index = 0, nodes, len, n, child; function addChildren(children) { nodes = []; len = children.length; for (n = 0; n < len; n++) { child = children[n]; index++; if (child.nodeType !== SHAPE) { nodes = nodes.concat(child.children); } if (child._id === that._id) { n = len; } } if (nodes.length > 0 && nodes[0].getDepth() <= depth) { addChildren(nodes); } } const stage = this.getStage(); if (that.nodeType !== UPPER_STAGE && stage) { addChildren(stage.children); } return index; } /** * get node depth in node tree. Returns an integer. * e.g. Stage depth will always be 0. Layers will always be 1. Groups and Shapes will always * be >= 2 * @method * @name Konva.Node#getDepth * @returns {Integer} * @example * var depth = shape.getDepth(); // 2 for a shape right inside of a layer */ getDepth() { let depth = 0, parent = this.parent; while (parent) { depth++; parent = parent.parent; } return depth; } // sometimes we do several attributes changes // like node.position(pos) // for performance reasons, lets batch transform reset // so it work faster _batchTransformChanges(func) { if (this._batchingTransformChange) { func(); return; } Node._updateBatch(() => { this._batchingTransformChange = true; try { func(); } finally { this._batchingTransformChange = false; if (this._needClearTransformCache) { this._needClearTransformCache = false; this._clearCache(TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); } } }); } setPosition(pos) { this._batchTransformChanges(() => { this.x(pos.x); this.y(pos.y); }); return this; } getPosition() { return { x: this.x(), y: this.y(), }; } /** * get position of first pointer (like mouse or first touch) relative to local coordinates of current node * @method * @name Konva.Node#getRelativePointerPosition * @returns {Konva.Node} * @example * * // let's think we have a rectangle at position x = 10, y = 10 * // now we clicked at x = 15, y = 15 of the stage * // if you want to know position of the click, related to the rectangle you can use * rect.getRelativePointerPosition(); */ getRelativePointerPosition() { const stage = this.getStage(); if (!stage) { return null; } // get pointer (say mouse or touch) position const pos = stage.getPointerPosition(); if (!pos) { return null; } const transform = this.getAbsoluteTransform().copy(); // to detect relative position we need to invert transform transform.invert(); // now we can find relative point return transform.point(pos); } /** * get absolute position of a node. That function can be used to calculate absolute position, but relative to any ancestor * @method * @name Konva.Node#getAbsolutePosition * @param {Object} [top] optional ancestor node * @returns {Konva.Node} * @example * * // returns absolute position relative to top-left corner of canvas * node.getAbsolutePosition(); * * // calculate absolute position of node, inside stage * // so stage transforms are ignored * node.getAbsolutePosition(stage) */ getAbsolutePosition(top) { const absoluteMatrix = this.getAbsoluteTransform(top).getMatrix(), absoluteTransform = new Transform(), offset = this.offset(); // clone the matrix array absoluteTransform.m = absoluteMatrix.slice(); absoluteTransform.translate(offset.x, offset.y); return absoluteTransform.getTranslation(); } setAbsolutePosition(pos) { const { x, y, ...origTrans } = this._clearTransform(); // don't clear translation this.attrs.x = x; this.attrs.y = y; // important, use non cached value this._clearCache(TRANSFORM); const it = this._getAbsoluteTransform().copy(); const invertible = it.isInvertible(); it.invert(); it.translate(pos.x, pos.y); const { x: dx, y: dy } = it.getTranslation(); this._setTransform(origTrans); if (invertible) { this.setPosition({ x: this.attrs.x + dx, y: this.attrs.y + dy }); } else { Util.warn('Cannot set the absolute position: the absolute transform is not invertible (an ancestor has a zero scale).'); } this._clearCache(TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); return this; } _setTransform(trans) { let key; for (key in trans) { this.attrs[key] = trans[key]; } } _clearTransform() { const trans = { x: this.x(), y: this.y(), rotation: this.rotation(), scaleX: this.scaleX(), scaleY: this.scaleY(), offsetX: this.offsetX(), offsetY: this.offsetY(), skewX: this.skewX(), skewY: this.skewY(), }; this.attrs.x = 0; this.attrs.y = 0; this.attrs.rotation = 0; this.attrs.scaleX = 1; this.attrs.scaleY = 1; this.attrs.offsetX = 0; this.attrs.offsetY = 0; this.attrs.skewX = 0; this.attrs.skewY = 0; // return original transform return trans; } /** * move node by an amount relative to its current position * @method * @name Konva.Node#move * @param {Object} change * @param {Number} change.x * @param {Number} change.y * @returns {Konva.Node} * @example * // move node in x direction by 1px and y direction by 2px * node.move({ * x: 1, * y: 2 * }); */ move(change) { let changeX = change.x, changeY = change.y, x = this.x(), y = this.y(); if (changeX !== undefined) { x += changeX; } if (changeY !== undefined) { y += changeY; } this.setPosition({ x: x, y: y }); return this; } /** * rotate node by an amount in degrees relative to its current rotation * @method * @name Konva.Node#rotate * @param {Number} theta * @returns {Konva.Node} */ rotate(theta) { this.rotation(this.rotation() + theta); return this; } /** * move node to the top of its siblings * @method * @name Konva.Node#moveToTop * @returns {Boolean} */ moveToTop() { if (!this.parent) { Util.warn('Node has no parent. moveToTop function is ignored.'); return false; } const index = this.index, len = this.parent.children.length; if (index < len - 1) { this.parent.children.splice(index, 1); this.parent.children.push(this); this.parent._setChildrenIndices(); return true; } return false; } /** * move node up * @method * @name Konva.Node#moveUp * @returns {Boolean} flag is moved or not */ moveUp() { if (!this.parent) { Util.warn('Node has no parent. moveUp function is ignored.'); return false; } const index = this.index, len = this.parent.children.length; if (index < len - 1) { this.parent.children.splice(index, 1); this.parent.children.splice(index + 1, 0, this); this.parent._setChildrenIndices(); return true; } return false; } /** * move node down * @method * @name Konva.Node#moveDown * @returns {Boolean} */ moveDown() { if (!this.parent) { Util.warn('Node has no parent. moveDown function is ignored.'); return false; } const index = this.index; if (index > 0) { this.parent.children.splice(index, 1); this.parent.children.splice(index - 1, 0, this); this.parent._setChildrenIndices(); return true; } return false; } /** * move node to the bottom of its siblings * @method * @name Konva.Node#moveToBottom * @returns {Boolean} */ moveToBottom() { if (!this.parent) { Util.warn('Node has no parent. moveToBottom function is ignored.'); return false; } const index = this.index; if (index > 0) { this.parent.children.splice(index, 1); this.parent.children.unshift(this); this.parent._setChildrenIndices(); return true; } return false; } setZIndex(zIndex) { if (!this.parent) { Util.warn('Node has no parent. zIndex parameter is ignored.'); return this; } if (zIndex < 0 || zIndex >= this.parent.children.length) { Util.warn('Unexpected value ' + zIndex + ' for zIndex property. zIndex is just index of a node in children of its parent. Expected value is from 0 to ' + (this.parent.children.length - 1) + '.'); } const index = this.index; this.parent.children.splice(index, 1); this.parent.children.splice(zIndex, 0, this); this.parent._setChildrenIndices(); return this; } /** * get absolute opacity * @method * @name Konva.Node#getAbsoluteOpacity * @returns {Number} */ getAbsoluteOpacity() { return this._getCache(ABSOLUTE_OPACITY, this._getAbsoluteOpacity); } _getAbsoluteOpacity() { let absOpacity = this.opacity(); const parent = this.getParent(); if (parent && !parent._isUnderCache) { absOpacity *= parent.getAbsoluteOpacity(); } return absOpacity; } /** * move node to another container * @method * @name Konva.Node#moveTo * @param {Container} newContainer * @returns {Konva.Node} * @example * // move node from current layer into layer2 * node.moveTo(layer2); */ moveTo(newContainer) { // do nothing if new container is already parent if (this.getParent() !== newContainer) { this._remove(); newContainer.add(this); } return this; } /** * convert Node into an object for serialization. Returns an object. * @method * @name Konva.Node#toObject * @returns {Object} */ toObject() { var _a; const attrs = this.getAttrs(); let key, val, nonPlainObject; const obj = { attrs: {}, className: this.getClassName(), }; for (key in attrs) { val = attrs[key]; // JSON has no typed arrays, so typed line points are written as an array if (val instanceof TypedArray) { val = Array.from(val); } // if value is object and object is not plain // like class instance, we should skip it and to not include nonPlainObject = Util.isObject(val) && !Util._isPlainObject(val) && !Util._isArray(val); if (nonPlainObject) { continue; } // only the getters generated by Factory carry a default, so an explicit // value equal to a computed one (Text width, dragDistance) is kept // Shared filter parameters can distinguish an explicit value from unset. if (((_a = this['get' + Util._capitalize(key)]) === null || _a === void 0 ? void 0 : _a._def) !== val || key === 'brightness' || key === 'threshold') { obj.attrs[key] = key === 'filters' && Util._isArray(val) ? val.filter((filter) => typeof filter === 'string') : Util.isObject(val) ? Util._prepareToStringify(val) : val; } } return obj; } /** * convert Node into a JSON string. Returns a JSON string. * @method * @name Konva.Node#toJSON * @returns {String} */ toJSON() { return JSON.stringify(this.toObject()); } /** * get parent container * @method * @name Konva.Node#getParent * @returns {Konva.Node} */ getParent() { return this.parent; } /** * get all ancestors (parent then parent of the parent, etc) of the node * @method * @name Konva.Node#findAncestors * @param {String} selector selector for search * @param {Boolean} [includeSelf] show we think that node is ancestro itself? * @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors) * @returns {Array} [ancestors] * @example * // get one of the parent group * var parentGroups = node.findAncestors('Group'); */ findAncestors(selector, includeSelf, stopNode) { const res = []; if (includeSelf && this._isMatch(selector)) { res.push(this); } let ancestor = this.parent; while (ancestor) { if (ancestor === stopNode) { return res; } if (ancestor._isMatch(selector)) { res.push(ancestor); } ancestor = ancestor.parent; } return res; } isAncestorOf(node) { return false; } /** * get ancestor (parent or parent of the parent, etc) of the node that match passed selector * @method * @name Konva.Node#findAncestor * @param {String} selector selector for search * @param {Boolean} [includeSelf] show we think that node is ancestro itself? * @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors) * @returns {Konva.Node} ancestor * @example * // get one of the parent group * var group = node.findAncestors('.mygroup'); */ findAncestor(selector, includeSelf, stopNode) { return this.findAncestors(selector, includeSelf, stopNode)[0]; } // is current node match passed selector? _isMatch(selector) { if (!selector) { return false; } if (typeof selector === 'function') { return selector(this); } let selectorArr = selector.replace(/ /g, '').split(','), len = selectorArr.length, n, sel; for (n = 0; n < len; n++) { sel = selectorArr[n]; if (!Util.isValidSelector(sel)) { Util.warn('Selector "' + sel + '" is invalid. Allowed selectors examples are "#foo", ".bar" or "Group".'); Util.warn('If you have a custom shape with such className, please change it to start with upper letter like "Triangle".'); Util.warn('Konva is awesome, right?'); } // id selector if (sel.charAt(0) === '#') { if (this.id() === sel.slice(1)) { return true; } } else if (sel.charAt(0) === '.') { // name selector if (this.hasName(sel.slice(1))) { return true; } } else if (this.className === sel || this.nodeType === sel) { return true; } } return false; } /** * get layer ancestor * @method * @name Konva.Node#getLayer * @returns {Konva.Layer} */ getLayer() { const parent = this.getParent(); return parent ? parent.getLayer() : null; } /** * get stage ancestor * @method * @name Konva.Node#getStage * @returns {Konva.Stage} */ getStage() { return this._getCache(STAGE, this._getStage); } _getStage() { const parent = this.getParent(); if (parent) { return parent.getStage(); } else { return null; } } /** * fire event * @method * @name Konva.Node#fire * @param {String} eventType event type. can be a regular event, like click, mouseover, or mouseout, or it can be a custom event, like myCustomEvent * @param {Event} [evt] event object * @param {Boolean} [bubble] setting the value to false, or leaving it undefined, will result in the event * not bubbling. Setting the value to true will result in the event bubbling. * @returns {Konva.Node} * @example * // manually fire click event * node.fire('click'); * * // fire custom event * node.fire('foo'); * * // fire custom event with custom event object * node.fire('foo', { * bar: 10 * }); * * // fire click event that bubbles * node.fire('click', null, true); */ fire(eventType, evt, bubble) { evt = evt || {}; evt.target = evt.target || this; // bubble if (bubble) { this._fireAndBubble(eventType, evt); } else { // no bubble this._fire(eventType, evt); } return this; } /** * get absolute transform of the node which takes into * account its ancestor transforms * @method * @name Konva.Node#getAbsoluteTransform * @returns {Konva.Transform} */ getAbsoluteTransform(top) { // relative to an ancestor, or under a cached one, the result can't come // from the cache: a cached container keeps the transform caches of its // descendants while it moves (see Container._clearSelfAndDescendantCache) if (top || this._hasCachedAncestor()) { return this._getAbsoluteTransform(top); } return this._getCache(ABSOLUTE_TRANSFORM, this._getCachedAbsoluteTransform); } _hasCachedAncestor() { let parent = this.parent; while (parent) { if (parent.isCached()) { return true; } parent = parent.parent; } return false; } // relative to `top`: the transforms of `top` and of its ancestors are // left out. Goes through the parent, so an ancestor that redefines its // absolute transform (Konva.Transformer) is honoured _getAbsoluteTransform(top) { const at = new Transform(); if (this === top) { return at; } if (this.parent && this.parent !== top) { this.parent.getAbsoluteTransform(top).copyInto(at); } return this._multiplyOwnTransform(at); } // the cached transform of the parent, then the own one _getCachedAbsoluteTransform() { const at = this._cache[ABSOLUTE_TRANSFORM] || new Transform(); if (this.parent) { this.parent.getAbsoluteTransform().copyInto(at); } else { at.reset(); } this._multiplyOwnTransform(at); at.dirty = false; return at; } _multiplyOwnTransform(at) { const transformsEnabled = this.transformsEnabled(); if (transformsEnabled === 'all') { at.multiply(this.getTransform()); } else if (transformsEnabled === 'position') { // use "attrs" directly, because it is a bit faster const x = this.attrs.x || 0; const y = this.attrs.y || 0; const offsetX = this.attrs.offsetX || 0; const offsetY = this.attrs.offsetY || 0; at.translate(x - offsetX, y - offsetY); } return at; } /** * get absolute scale of the node which takes into * account its ancestor scales * @method * @name Konva.Node#getAbsoluteScale * @returns {Object} * @example * // get absolute scale x * var scaleX = node.getAbsoluteScale().x; */ getAbsoluteScale(top) { // do not cache this calculations, // because it use cache transform // this is special logic for caching with some shapes with shadow let parent = this; while (parent) { if (parent._isUnderCache) { top = parent; } parent = parent.getParent(); } const transform = this.getAbsoluteTransform(top); const attrs = transform.decompose(); return { x: attrs.scaleX, y: attrs.scaleY, }; } /** * get absolute rotation of the node which takes into * account its ancestor rotations * @method * @name Konva.Node#getAbsoluteRotation * @returns {Number} * @example * // get absolute rotation * var rotation = node.getAbsoluteRotation(); */ getAbsoluteRotation() { return this.getAbsoluteTransform().decompose().rotation; } /** * get transform of the node * @method * @name Konva.Node#getTransform * @returns {Konva.Transform} */ getTransform() { return this._getCache(TRANSFORM, this._getTransform); } _getTransform() { var _a, _b; const m = this._cache[TRANSFORM] || new Transform(); m.reset(); // I was trying to use attributes directly here // but it doesn't work for Transformer well // because it overwrite x,y getters const x = this.x(), y = this.y(), rotation = Konva.getAngle(this.rotation()), scaleX = (_a = this.attrs.scaleX) !== null && _a !== void 0 ? _a : 1, scaleY = (_b = this.attrs.scaleY) !== null && _b !== void 0 ? _b : 1, skewX = this.attrs.skewX || 0, skewY = this.attrs.skewY || 0, offsetX = this.attrs.offsetX || 0, offsetY = this.attrs.offsetY || 0; if (x !== 0 || y !== 0) { m.translate(x, y); } if (rotation !== 0) { m.rotate(rotation); } if (skewX !== 0 || skewY !== 0) { m.skew(skewX, skewY); } if (scaleX !== 1 || scaleY !== 1) { m.scale(scaleX, scaleY); } if (offsetX !== 0 || offsetY !== 0) { m.translate(-1 * offsetX, -1 * offsetY); } m.dirty = false; return m; } /** * clone node. Returns a new Node instance with identical attributes. You can also override * the node properties with an object literal, enabling you to use an existing node as a template * for another node * @method * @name Konva.Node#clone * @param {Object} obj override attrs * @returns {Konva.Node} * @example * // simple clone * var clone = node.clone(); * * // clone a node and override the x position * var clone = rect.clone({ * x: 5 * }); */ clone(obj) { // instantiate new node let attrs = Util.cloneObject(this.attrs), key, allListeners, len, n, listener; // apply attr overrides for (key in obj) { attrs[key] = obj[key]; } const node = new this.constructor(attrs); // copy over listeners for (key in this.eventListeners) { allListeners = this.eventListeners[key]; len = allListeners.length; for (n = 0; n < len; n++) { listener = allListeners[n]; /* * don't include konva namespaced listeners because * these are generated by the constructors */ if (listener.name.indexOf(KONVA) < 0) { // if listeners array doesn't exist, then create it if (!node.eventListeners[key]) { node.eventListeners[key] = []; } node.eventListeners[key].push(listener); } } } return node; } _toKonvaCanvas(config) { config = config || {}; // the client rect is only needed for the parts of the box not configured const needsBox = config.x === undefined || config.y === undefined || !config.width || !config.height; const box = needsBox ? this.getClientRect() : { x: 0, y: 0, width: 0, height: 0 }; if (!isFinite(box.x + box.y + box.width + box.height)) { Util.error(`Cannot find the bounds of the node to export, its client rect is ${box.x}, ${box.y}, ${box.width}x${box.height}. Check its position, size and points.`); } const stage = this.getStage(), x = config.x !== undefined ? config.x : Math.floor(box.x), y = config.y !== undefined ? config.y : Math.floor(box.y), pixelRatio = config.pixelRatio || 1, canvas = new SceneCanvas({ width: config.width || Math.max(0, Math.ceil(box.x + box.width - x)) || (stage ? stage.width() : 0), height: config.height || Math.max(0, Math.ceil(box.y + box.height - y)) || (stage ? stage.height() : 0), pixelRatio: pixelRatio, }), context = canvas.getContext(); if (config.imageSmoothingEnabled === false) { context._context.imageSmoothingEnabled = false; } context.save(); if (x || y) { context.translate(-1 * x, -1 * y); } this.drawScene(canvas); context.restore(); canvas._releaseIsolationCanvas(); return canvas; } /** * converts node into an canvas element. * @method * @name Konva.Node#toCanvas * @param {Object} config * @param {Function} config.callback function executed when the composite has completed * @param {Number} [config.x] x position of canvas section * @param {Number} [config.y] y position of canvas section * @param {Number} [config.width] width of canvas section * @param {Number} [config.height] height of canvas section * @param {Number} [config.pixelRatio] pixelRatio of output canvas. Default is 1. * Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution. * Rebuild caches at the export pixel ratio to preserve detail. * The pixel ratio multiplies the dimensions of the exported image. * If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000. * @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing * @example * var canvas = node.toCanvas(); */ toCanvas(config) { return this._toKonvaCanvas(config)._canvas; } /** * Creates a composite data URL (base64 string). If MIME type is not * specified, then "image/png" will result. For "image/jpeg", specify a quality * level as quality (range 0.0 - 1.0) * @method * @name Konva.Node#toDataURL * @param {Object} config * @param {String} [config.mimeType] can be "image/png" or "image/jpeg". * "image/png" is the default * @param {Number} [config.x] x position of canvas section * @param {Number} [config.y] y position of canvas section * @param {Number} [config.width] width of canvas section * @param {Number} [config.height] height of canvas section * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 * is very high quality * @param {Number} [config.pixelRatio] pixelRatio of output image url. Default is 1. * Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution. * Rebuild caches at the export pixel ratio to preserve detail. * The pixel ratio multiplies the dimensions of the exported image. * If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000. * @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing * @returns {String} */ toDataURL(config) { var _a; config = config || {}; const mimeType = config.mimeType || null, quality = (_a = config.quality) !== null && _a !== void 0 ? _a : null; const url = this._toKonvaCanvas(config).toDataURL(mimeType, quality); if (config.callback) { config.callback(url); } return url; } /** * converts node into an image. Since the toImage * method is asynchronous, the resulting image can only be retrieved from the config callback * or the returned Promise. toImage is most commonly used * to cache complex drawings as an image so that they don't have to constantly be redrawn * @method * @name Konva.Node#toImage * @param {Object} config * @param {Function} [config.callback] function executed when the composite has completed * @param {String} [config.mimeType] can be "image/png" or "image/jpeg". * "image/png" is the default * @param {Number} [config.x] x position of canvas section * @param {Number} [config.y] y position of canvas section * @param {Number} [config.width] width of canvas section * @param {Number} [config.height] height of canvas section * @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType, * you can specify the quality from 0 to 1, where 0 is very poor quality and 1 * is very high quality * @param {Number} [config.pixelRatio] pixelRatio of output image. Default is 1. * Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution. * Rebuild caches at the export pixel ratio to preserve detail. * The pixel ratio multiplies the dimensions of the exported image. * If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000. * @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing * @return {Promise} * @example * var image = node.toImage({ * callback(img) { * // do stuff with img * } * }); */ toImage(config) { return new Promise((resolve, reject) => { try { const { callback, ...rest } = config || {}; Util._urlToImage(this.toDataURL(rest), function (img) { resolve(img); callback === null || callback === void 0 ? void 0 : callback(img); }, reject); } catch (err) { reject(err); } }); } /** * Converts node into a blob. Since the toBlob method is asynchronous, * the resulting blob can only be retrieved from the config callback * or the returned Promise. * @method * @name Konva.Node#toBlob * @param {Object} config * @param {Function} [config.callback] function executed when the composite has completed * @param {Number} [config.x] x position of canvas section * @param {Number} [config.y] y position of canvas section * @param {Number} [config.width] width of canvas section * @param {Number} [config.height] height of canvas section * @param {Number} [config.pixelRatio] pixelRatio of output canvas. Default is 1. * Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution. * Rebuild caches at the export pixel ratio to preserve detail. * The pixel ratio multiplies the dimensions of the exported image. * If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000. * @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing * @example * var blob = await node.toBlob({}); * @returns {Promise} rejects if the canvas can not be encoded */ toBlob(config) { return new Promise((resolve, reject) => { try { this.toCanvas(config).toBlob((blob) => { var _a; // the canvas reports a failed encode by passing null if (!blob) { reject(new Error('Konva: toBlob() failed, the canvas was not encoded')); return; } resolve(blob); (_a = config === null || config === void 0 ? void 0 : config.callback) === null || _a === void 0 ? void 0 : _a.call(config, blob); }, config === null || config === void 0 ? void 0 : config.mimeType, config === null || config === void 0 ? void 0 : config.quality); } catch (err) { reject(err); } }); } setSize(size) { this.width(size.width); this.height(size.height); return this; } getSize() { return { width: this.width(), height: this.height(), }; } /** * get class name, which may return Stage, Layer, Group, or shape class names like Rect, Circle, Text, etc. * @method * @name Konva.Node#getClassName * @returns {String} */ getClassName() { return this.className || this.nodeType; } /** * get the node type, which may return Stage, Layer, Group, or Shape * @method * @name Konva.Node#getType * @returns {String} */ getType() { return this.nodeType; } getDragDistance() { // compare with undefined because we need to track 0 value if (this.attrs.dragDistance !== undefined) { return this.attrs.dragDistance; } else if (this.parent) { return this.parent.getDragDistance(); } else { return Konva.dragDistance; } } _off(type, name, callback) { let evtListeners = this.eventListeners[type], i, evtName, handler; for (i = 0; i < evtListeners.length; i++) { evtName = evtListeners[i].name; handler = evtListeners[i].handler; // the following two conditions must be true in order to remove a handler: // 1) the current event name cannot be konva unless the event name is konva // this enables developers to force remove a konva specific listener for whatever reason // 2) an event name is not specified, or if one is specified, it matches the current event name if ((evtName !== 'konva' || name === 'konva') && (!name || evtName === name) && (!callback || callback === handler)) { evtListeners.splice(i, 1); if (evtListeners.length === 0) { delete this.eventListeners[type]; break; } i--; } } } _fireChangeEvent(attr, oldVal, newVal) { this._fire(attr + CHANGE, { oldVal: oldVal, newVal: newVal, }); } /** * add name to node * @method * @name Konva.Node#addName * @param {String} name * @returns {Konva.Node} * @example * node.name('red'); * node.addName('selected'); * node.name(); // return 'red selected' */ addName(name) { if (!this.hasName(name)) { const oldName = this.name(); const newName = oldName ? oldName + ' ' + name : name; this.name(newName); } return this; } /** * check is node has name * @method * @name Konva.Node#hasName * @param {String} name * @returns {Boolean} * @example * node.name('red'); * node.hasName('red'); // return true * node.hasName('selected'); // return false * node.hasName(''); // return false */ hasName(name) { if (!name) { return false; } const fullName = this.name(); if (!fullName) { return false; } // if name is '' the "names" will be [''], so I added extra check above const names = (fullName || '').split(/\s/g); return names.indexOf(name) !== -1; } /** * remove name from node * @method * @name Konva.Node#removeName * @param {String} name * @returns {Konva.Node} * @example * node.name('red selected'); * node.removeName('selected'); * node.hasName('selected'); // return false * node.name(); // return 'red' */ removeName(name) { const names = (this.name() || '').split(/\s/g); const index = names.indexOf(name); if (index !== -1) { names.splice(index, 1); this.name(names.join(' ')); } return this; } /** * set attr * @method * @name Konva.Node#setAttr * @param {String} attr * @param {*} val * @returns {Konva.Node} * @example * node.setAttr('x', 5); */ setAttr(attr, val) { const func = this[SET + Util._capitalize(attr)]; if (Util._isFunction(func)) { func.call(this, val); } else { // otherwise set directly this._setAttr(attr, val); } return this; } _addSubtreeObserver(observer) { var _a; ((_a = this._subtreeObservers) !== null && _a !== void 0 ? _a : (this._subtreeObservers = new Set())).add(observer); Node._subtreeObserverCount++; } _removeSubtreeObserver(observer) { var _a; if ((_a = this._subtreeObservers) === null || _a === void 0 ? void 0 : _a.delete(observer)) { Node._subtreeObserverCount--; } } // Tells the observers of this node and of its ancestors that something below // them changed. A node's own changes reach its observers through its change // events, so a draw request starts at the parent. A container calls this // itself when its child list changes. _notifySubtreeChange() { if (!Node._subtreeObserverCount) return; for (let node = this; node; node = node.parent) { const observers = node._subtreeObservers; if (observers) for (const o of observers) o._onSubtreeChange(node); } } _requestDraw() { var _a; (_a = this.parent) === null || _a === void 0 ? void 0 : _a._notifySubtreeChange(); if (Konva.autoDrawEnabled) { const drawNode = this.getLayer() || this.getStage(); drawNode === null || drawNode === void 0 ? void 0 : drawNode.batchDraw(); } } _setAttr(key, val) { const oldVal = this.attrs[key]; if (oldVal === val && !Util.isObject(val)) { return; } if (val === undefined || val === null) { delete this.attrs[key]; } else { this.attrs[key] = val; } if (this._attrsVersion !== undefined) this._attrsVersion++; if (this._shouldFireChangeEvents) { this._fireChangeEvent(key, oldVal, val); } this._requestDraw(); } _fireAndBubble(eventType, evt, compareShape) { if (evt && this.nodeType === SHAPE) { evt.target = this; } const nonBubbling = NON_BUBBLING_EVENTS.indexOf(eventType) !== -1; const shouldStop = nonBubbling && ((compareShape && (this === compareShape || (this.isAncestorOf && this.isAncestorOf(compareShape)))) || (this.nodeType === 'Stage' && !compareShape)); if (!shouldStop) { this._fire(eventType, evt); // simulate event bubbling const stopBubble = nonBubbling && compareShape && compareShape.isAncestorOf && compareShape.isAncestorOf(this) && !compareShape.isAncestorOf(this.parent); if (((evt && !evt.cancelBubble) || !evt) && this.parent && this.parent.isListening() && !stopBubble) { if (compareShape && compareShape.parent) { this._fireAndBubble.call(this.parent, eventType, evt, compareShape); } else { this._fireAndBubble.call(this.parent, eventType, evt); } } } } _getProtoListeners(eventType) { var _a, _b; const proto = Object.getPrototypeOf(this); let allListeners = Node.protoListenerMap.get(proto); if (!allListeners) { allListeners = {}; Node.protoListenerMap.set(proto, allListeners); } let events = allListeners[eventType]; if (events === undefined) { //recalculate cache events = []; // Dedupe across the prototype chain: subclasses without their own // `eventListeners` inherit the parent's, so a single handler would // otherwise be pushed once per chain level (Stage→Container→Node = 3x). const seen = new Set(); let obj = Object.getPrototypeOf(this); while (obj) { const hierarchyEvents = (_b = (_a = obj.eventListeners) === null || _a === void 0 ? void 0 : _a[eventType]) !== null && _b !== void 0 ? _b : []; for (let i = 0; i < hierarchyEvents.length; i++) { const entry = hierarchyEvents[i]; if (!seen.has(entry)) { seen.add(entry); events.push(entry); } } obj = Object.getPrototypeOf(obj); } // update cache allListeners[eventType] = events; } return events; } _fire(eventType, evt) { evt = evt || {}; evt.currentTarget = this; evt.type = eventType; const topListeners = this._getProtoListeners(eventType); if (topListeners) { // The list is rebuilt when a prototype gains a listener, so no // defensive .slice() needed (hot path: every *Change). for (let i = 0; i < topListeners.length; i++) { topListeners[i].handler.call(this, evt); } } // snapshot prevents skipping listeners when a handler mutates the array // (e.g. off + on for the same event), but we also need to fire listeners // added during dispatch (e.g. draggable(true) inside mousedown) const selfListeners = this.eventListeners[eventType]; if (selfListeners) { const list = selfListeners.slice(); const origLen = list.length; for (let i = 0; i < list.length; i++) { list[i].handler.call(this, evt); } // fire any listeners appended during dispatch const liveListeners = this.eventListeners[eventType]; if (liveListeners) { for (let i = origLen; i < liveListeners.length; i++) { liveListeners[i].handler.call(this, evt); } } } } /** * draw both scene and hit graphs. If the node being drawn is the stage, all of the layers will be cleared and redrawn * @method * @name Konva.Node#draw * @returns {Konva.Node} */ draw() { this.drawScene(); this.drawHit(); return this; } // drag & drop _createDragElement(evt) { var _a; const pointerId = evt ? evt.pointerId : undefined; const stage = this.getStage(); const ap = this.getAbsolutePosition(); if (!stage) { return; } const pos = stage._getPointerById(pointerId) || stage._changedPointerPositions[0] || ap; const type = ((_a = evt === null || evt === void 0 ? void 0 : evt.evt) === null || _a === void 0 ? void 0 : _a.type) ? Util._getEventType(evt.evt.type) : stage._pointerEventType; // drags follow mouse and touch window events only, so a drag started from // a pointer event stays unbound until such an event attaches its pointer const bound = type !== 'pointer'; DD._dragElements.set(this._id, { node: this, startPointerPos: pos, offset: { x: pos.x - ap.x, y: pos.y - ap.y, }, dragStatus: 'ready', pointerId: bound ? (pointerId !== null && pointerId !== void 0 ? pointerId : ('id' in pos ? pos.id : undefined)) : undefined, pointerEventType: bound ? type : undefined, startEvent: evt, }); } /** * initiate drag and drop. * @method * @name Konva.Node#startDrag */ startDrag(evt, bubbleEvent = true) { var _a; if (!DD._dragElements.has(this._id)) { this._createDragElement(evt); } const elem = DD._dragElements.get(this._id); // a node outside of a stage has no pointer to follow if (!elem) { return; } elem.dragStatus = 'dragging'; (_a = this.getStage()) === null || _a === void 0 ? void 0 : _a._cancelClick(elem.pointerId, elem.pointerEventType); this.fire('dragstart', { type: 'dragstart', target: this, // Use the stored start event if available (from mousedown/touchstart), // otherwise fall back to the provided event (when startDrag is called programmatically) evt: (elem.startEvent && elem.startEvent.evt) || (evt && evt.evt), }, bubbleEvent); } _setDragPosition(evt, elem) { const pos = this.getStage()._getPointerById(elem.pointerId); if (!pos) { return; } let newNodePos = { x: pos.x - elem.offset.x, y: pos.y - elem.offset.y, }; const dbf = this.dragBoundFunc(); if (dbf !== undefined) { const bounded = dbf.call(this, newNodePos, evt); if (!bounded) { Util.warn('dragBoundFunc did not return any value. That is unexpected behavior. You must return new absolute position from dragBoundFunc.'); } else if (!Util._isNumber(bounded.x) || !Util._isNumber(bounded.y)) { Util.warn(`dragBoundFunc returned a position with a non-finite x or y (${bounded.x}, ${bounded.y}). The node was not moved.`); return; } else { newNodePos = bounded; } } const lastPos = elem.lastPos; if (!lastPos || lastPos.x !== newNodePos.x || lastPos.y !== newNodePos.y) { this.setAbsolutePosition(newNodePos); this._requestDraw(); } elem.lastPos = newNodePos; } /** * stop drag and drop * @method * @name Konva.Node#stopDrag */ stopDrag(evt) { const elem = DD._dragElements.get(this._id); if (!elem) { return; } elem.dragStatus = 'stopped'; DD._endDragBefore(evt, undefined, elem); DD._endDragAfter(evt, elem); } setDraggable(draggable) { this._setAttr('draggable', draggable); this._dragChange(); } /** * determine if node is currently in drag and drop mode * @method * @name Konva.Node#isDragging */ isDragging() { const elem = DD._dragElements.get(this._id); return elem ? elem.dragStatus === 'dragging' : false; } _listenDrag() { this._dragCleanup(); this.on('mousedown.konva touchstart.konva', function (evt) { const shouldCheckButton = evt.evt['button'] !== undefined; const canDrag = !shouldCheckButton || Konva.dragButtons.indexOf(evt.evt['button']) >= 0; if (!canDrag) { return; } if (DD._dragElements.has(this._id)) { return; } let hasDraggingChild = false; DD._dragElements.forEach((elem) => { if (this.isAncestorOf(elem.node)) { hasDraggingChild = true; } }); // nested drag can be started // in that case we don't need to start new drag if (!hasDraggingChild) { this._createDragElement(evt); } }); } _dragChange() { if (this.attrs.draggable) { this._listenDrag(); } else { // remove event listeners this._dragCleanup(); /* * force drag and drop to end * if this node is currently in * drag and drop mode */ const stage = this.getStage(); if (!stage) { return; } const dragElement = DD._dragElements.get(this._id); const isDragging = dragElement && dragElement.dragStatus === 'dragging'; const isReady = dragElement && dragElement.dragStatus === 'ready'; if (isDragging) { this.stopDrag(); } else if (isReady) { DD._dragElements.delete(this._id); } } } _dragCleanup() { this.off('mousedown.konva'); this.off('touchstart.konva'); } /** * determine if node (at least partially) is currently in user-visible area * @method * @param {(Number | Object)} margin optional margin in pixels * @param {Number} margin.x * @param {Number} margin.y * @returns {Boolean} * @name Konva.Node#isClientRectOnScreen * @example * // get index * // default calculations * var isOnScreen = node.isClientRectOnScreen() * // increase object size (or screen size) for cases when objects close to the screen still need to be marked as "visible" * var isOnScreen = node.isClientRectOnScreen({ x: stage.width(), y: stage.height() }) */ isClientRectOnScreen(margin = { x: 0, y: 0 }) { const stage = this.getStage(); if (!stage) { return false; } const screenRect = { x: -margin.x, y: -margin.y, width: stage.width() + 2 * margin.x, height: stage.height() + 2 * margin.y, }; return Util.haveIntersection(screenRect, this.getClientRect()); } /** * create node with JSON string or an Object. Deserialization restores attributes only, * not functions, images or event handlers (that would make the serialized object huge): * `sceneFunc`/`hitFunc` of custom shapes, images and `fillPatternImage`, `clipFunc`, * `dragBoundFunc`, filter functions, the `nodes`, `boundBoxFunc` and `anchorStyleFunc` * of a Transformer, typed arrays and the `cache()` state. If your app uses them (it probably * does), select the nodes after loading the stage and set these properties again with * `on()`, `sceneFunc()`, `image()` and so on * @method * @memberof Konva.Node * @param {String|Object} data string or object * @param {Element} [container] optional container dom element used only if you're * creating a stage node */ static create(data, container) { if (Util._isString(data)) { data = JSON.parse(data); } return this._createNode(data, container); } static _createNode(obj, container) { let className = Node.prototype.getClassName.call(obj), children = obj.children, no, len, n; // if container was passed in, add it to attrs if (container) { obj.attrs.container = container; } if (!Konva[className]) { const fallback = children ? 'Group' : 'Shape'; Util.warn('Can not find a node with class name "' + className + `". Fallback to "${fallback}".`); className = fallback; } const Class = Konva[className]; no = new Class(obj.attrs); if (children) { len = children.length; for (n = 0; n < len; n++) { no.add(Node._createNode(children[n])); } } return no; } } // Number of registered observers. The ancestor walk is skipped while it is 0. Node._subtreeObserverCount = 0; // Perf: while a batch is open, work that depends on many nodes (a Konva // .Transformer refresh) is queued and runs once when the outermost scope // ends, instead of once per node change. Scopes are the callers of // _updateBatch() and _beginUpdateBatch(). Node._updateBatchDepth = 0; // a Set, so a callback queued once per change runs once per batch Node._pendingUpdates = new Set(); // keyed by prototype: subclasses that share a nodeType (every shape class) // have their own listeners Node.protoListenerMap = new WeakMap(); Node.prototype.nodeType = 'Node'; Node.prototype._attrsAffectingSize = []; // attache events listeners once into prototype // that way we don't spend too much time on making an new instance Node.prototype.on(TRANSFORM_CHANGE_STR, function () { if (this._batchingTransformChange) { this._needClearTransformCache = true; return; } this._clearCache(TRANSFORM); this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); }); Node.prototype.on('visibleChange.konva', function () { this._clearSelfAndDescendantCache(VISIBLE); }); Node.prototype.on('listeningChange.konva', function () { this._clearSelfAndDescendantCache(LISTENING); for (let parent = this.getParent(); parent; parent = parent.getParent()) { const cache = parent._getCanvasCache(); if (cache === null || cache === void 0 ? void 0 : cache.hit) { Util.releaseCanvas(cache.hit._canvas); cache.hit = null; } } }); Node.prototype.on('opacityChange.konva', function () { this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); }); const addGetterSetter = Factory.addGetterSetter; /** * get/set zIndex relative to the node's siblings who share the same parent. * Please remember that zIndex is not absolute (like in CSS). It is relative to parent element only. * @name Konva.Node#zIndex * @method * @param {Number} index * @returns {Number} * @example * // get index * var index = node.zIndex(); * * // set index * node.zIndex(2); */ addGetterSetter(Node, 'zIndex'); /** * get/set node absolute position * @name Konva.Node#absolutePosition * @method * @param {Object} pos * @param {Number} pos.x * @param {Number} pos.y * @returns {Object} * @example * // get position * var position = node.absolutePosition(); * * // set position * node.absolutePosition({ * x: 5, * y: 10 * }); */ addGetterSetter(Node, 'absolutePosition'); addGetterSetter(Node, 'position'); /** * get/set node position relative to parent * @name Konva.Node#position * @method * @param {Object} pos * @param {Number} pos.x * @param {Number} pos.y * @returns {Object} * @example * // get position * var position = node.position(); * * // set position * node.position({ * x: 5, * y: 10 * }); */ addGetterSetter(Node, 'x', 0, getNumberValidator()); /** * get/set x position * @name Konva.Node#x * @method * @param {Number} x * @returns {Object} * @example * // get x * var x = node.x(); * * // set x * node.x(5); */ addGetterSetter(Node, 'y', 0, getNumberValidator()); /** * get/set y position * @name Konva.Node#y * @method * @param {Number} y * @returns {Integer} * @example * // get y * var y = node.y(); * * // set y * node.y(5); */ addGetterSetter(Node, 'globalCompositeOperation', 'source-over', getStringValidator()); /** * get/set globalCompositeOperation of a node. globalCompositeOperation DOESN'T affect hit graph of nodes. So they are still trigger to events as they have default "source-over" globalCompositeOperation. * @name Konva.Node#globalCompositeOperation * @method * @param {String} type * @returns {String} * @example * // get globalCompositeOperation * var globalCompositeOperation = shape.globalCompositeOperation(); * * // set globalCompositeOperation * shape.globalCompositeOperation('source-in'); */ addGetterSetter(Node, 'opacity', 1, getNumberValidator()); /** * get/set opacity. Opacity values range from 0 to 1. * A node with an opacity of 0 is fully transparent, and a node * with an opacity of 1 is fully opaque * @name Konva.Node#opacity * @method * @param {Object} opacity * @returns {Number} * @example * // get opacity * var opacity = node.opacity(); * * // set opacity * node.opacity(0.5); */ addGetterSetter(Node, 'name', '', getStringValidator()); /** * get/set name. * @name Konva.Node#name * @method * @param {String} name * @returns {String} * @example * // get name * var name = node.name(); * * // set name * node.name('foo'); * * // also node may have multiple names (as css classes) * node.name('foo bar'); */ addGetterSetter(Node, 'id', '', getStringValidator()); /** * get/set id. Id is global for whole page. * @name Konva.Node#id * @method * @param {String} id * @returns {String} * @example * // get id * var name = node.id(); * * // set id * node.id('foo'); */ addGetterSetter(Node, 'rotation', 0, getNumberValidator()); /** * get/set rotation in degrees * @name Konva.Node#rotation * @method * @param {Number} rotation * @returns {Number} * @example * // get rotation in degrees * var rotation = node.rotation(); * * // set rotation in degrees * node.rotation(45); */ Factory.addComponentsGetterSetter(Node, 'scale', ['x', 'y']); /** * get/set scale * @name Konva.Node#scale * @param {Object} scale * @param {Number} scale.x * @param {Number} scale.y * @method * @returns {Object} * @example * // get scale * var scale = node.scale(); * * // set scale * shape.scale({ * x: 2, * y: 3 * }); */ addGetterSetter(Node, 'scaleX', 1, getNumberValidator()); /** * get/set scale x * @name Konva.Node#scaleX * @param {Number} x * @method * @returns {Number} * @example * // get scale x * var scaleX = node.scaleX(); * * // set scale x * node.scaleX(2); */ addGetterSetter(Node, 'scaleY', 1, getNumberValidator()); /** * get/set scale y * @name Konva.Node#scaleY * @param {Number} y * @method * @returns {Number} * @example * // get scale y * var scaleY = node.scaleY(); * * // set scale y * node.scaleY(2); */ Factory.addComponentsGetterSetter(Node, 'skew', ['x', 'y']); /** * get/set skew * @name Konva.Node#skew * @param {Object} skew * @param {Number} skew.x * @param {Number} skew.y * @method * @returns {Object} * @example * // get skew * var skew = node.skew(); * * // set skew * node.skew({ * x: 20, * y: 10 * }); */ addGetterSetter(Node, 'skewX', 0, getNumberValidator()); /** * get/set skew x * @name Konva.Node#skewX * @param {Number} x * @method * @returns {Number} * @example * // get skew x * var skewX = node.skewX(); * * // set skew x * node.skewX(3); */ addGetterSetter(Node, 'skewY', 0, getNumberValidator()); /** * get/set skew y * @name Konva.Node#skewY * @param {Number} y * @method * @returns {Number} * @example * // get skew y * var skewY = node.skewY(); * * // set skew y * node.skewY(3); */ Factory.addComponentsGetterSetter(Node, 'offset', ['x', 'y']); /** * get/set offset. Offsets the default position and rotation point * @method * @param {Object} offset * @param {Number} offset.x * @param {Number} offset.y * @returns {Object} * @example * // get offset * var offset = node.offset(); * * // set offset * node.offset({ * x: 20, * y: 10 * }); */ addGetterSetter(Node, 'offsetX', 0, getNumberValidator()); /** * get/set offset x * @name Konva.Node#offsetX * @method * @param {Number} x * @returns {Number} * @example * // get offset x * var offsetX = node.offsetX(); * * // set offset x * node.offsetX(3); */ addGetterSetter(Node, 'offsetY', 0, getNumberValidator()); /** * get/set offset y * @name Konva.Node#offsetY * @method * @param {Number} y * @returns {Number} * @example * // get offset y * var offsetY = node.offsetY(); * * // set offset y * node.offsetY(3); */ addGetterSetter(Node, 'dragDistance', undefined, getNumberValidator()); /** * get/set drag distance. A drag starts once the pointer moved this far from where it * went down, on either axis. Default is `Konva.dragDistance` * @name Konva.Node#dragDistance * @method * @param {Number} distance * @returns {Number} * @example * // get drag distance * var dragDistance = node.dragDistance(); * * // set distance * // node starts dragging only if pointer moved more then 3 pixels * node.dragDistance(3); * // or set globally * Konva.dragDistance = 3; */ addGetterSetter(Node, 'width', 0, getNumberValidator()); /** * get/set width * @name Konva.Node#width * @method * @param {Number} width * @returns {Number} * @example * // get width * var width = node.width(); * * // set width * node.width(100); */ addGetterSetter(Node, 'height', 0, getNumberValidator()); /** * get/set height * @name Konva.Node#height * @method * @param {Number} height * @returns {Number} * @example * // get height * var height = node.height(); * * // set height * node.height(100); */ addGetterSetter(Node, 'listening', true, getBooleanValidator()); /** * get/set listening attr. If you need to determine if a node is listening or not * by taking into account its parents, use the isListening() method * nodes with listening set to false will not be detected in hit graph * so they will be ignored in container.getIntersection() method * a non-listening layer (or a layer of a non-listening stage) also * releases its hit canvas, so it uses less memory * @name Konva.Node#listening * @method * @param {Boolean} listening Can be true, or false. The default is true. * @returns {Boolean} * @example * // get listening attr * var listening = node.listening(); * * // stop listening for events, remove node and all its children from hit graph * node.listening(false); * * // listen to events according to the parent * node.listening(true); */ /** * get/set preventDefault * By default all shapes will prevent default behavior * of a browser on a pointer move or tap. * that will prevent native scrolling when you are trying to drag&drop a node * but sometimes you may need to enable default actions * in that case you can set the property to false * @name Konva.Node#preventDefault * @method * @param {Boolean} preventDefault * @returns {Boolean} * @example * // get preventDefault * var shouldPrevent = shape.preventDefault(); * * // set preventDefault * shape.preventDefault(false); */ addGetterSetter(Node, 'preventDefault', true, getBooleanValidator()); addGetterSetter(Node, 'filters', undefined, function (val) { this._filterUpToDate = false; return val; }); /** * get/set filters. Supports function filters, CSS filter strings, or mixed arrays. * CSS filters are applied using native browser performance when possible, function filters use ImageData manipulation. * CSS filters automatically fall back to function filters in unsupported browsers. * Blur radii, pixelation sizes and CSS lengths use node coordinates, independent of cache pixelRatio. * Custom filters receive (imageData, pixelRatio). ImageData contains full-resolution backing pixels. * @name Konva.Node#filters * @method * @param {Array} filters array of filter functions and/or CSS filter strings * @returns {Array} * @example * // get filters * var filters = node.filters(); * * // set CSS filters only (caching required, uses native performance) * node.cache(); * node.filters(['blur(5px)', 'brightness(1.2)', 'contrast(1.5)']); * * // set function filters only (caching required) * node.cache(); * node.filters([Konva.Filters.Blur, Konva.Filters.Sepia, Konva.Filters.Invert]); * * // mix CSS and function filters (caching required, uses fallback strategy) * node.cache(); * node.filters([ * 'blur(3px)', // CSS filter * Konva.Filters.Invert, // function filter * 'brightness(1.5)' // CSS filter * ]); */ addGetterSetter(Node, 'visible', true, getBooleanValidator()); /** * get/set visible attr. Can be true, or false. The default is true. * If you need to determine if a node is visible or not * by taking into account its parents, use the isVisible() method * @name Konva.Node#visible * @method * @param {Boolean} visible * @returns {Boolean} * @example * // get visible attr * var visible = node.visible(); * * // make invisible * node.visible(false); * * // make visible (according to the parent) * node.visible(true); * */ addGetterSetter(Node, 'transformsEnabled', 'all', getStringValidator()); /** * get/set transforms that are enabled. Can be "all", "none", or "position". The default * is "all" * @name Konva.Node#transformsEnabled * @method * @param {String} enabled * @returns {String} * @example * // enable position transform only to improve draw performance * node.transformsEnabled('position'); * * // enable all transforms * node.transformsEnabled('all'); */ /** * get/set node size * @name Konva.Node#size * @method * @param {Object} size * @param {Number} size.width * @param {Number} size.height * @returns {Object} * @example * // get node size * var size = node.size(); * var width = size.width; * var height = size.height; * * // set size * node.size({ * width: 100, * height: 200 * }); */ addGetterSetter(Node, 'size'); /** * get/set drag bound function. This is used to override the default * drag and drop position. The function is called with the absolute position * the node is about to get and the native event, and must return the * absolute position to use * @name Konva.Node#dragBoundFunc * @method * @param {Function} dragBoundFunc function(pos, evt) returning `{ x, y }` * @returns {Function} * @example * // get drag bound function * var dragBoundFunc = node.dragBoundFunc(); * * // create vertical drag and drop * node.dragBoundFunc(function(pos){ * // important pos - is absolute position of the node * // you should return absolute position too * return { * x: this.absolutePosition().x, * y: pos.y * }; * }); */ addGetterSetter(Node, 'dragBoundFunc'); /** * get/set draggable flag * @name Konva.Node#draggable * @method * @param {Boolean} draggable * @returns {Boolean} * @example * // get draggable flag * var draggable = node.draggable(); * * // enable drag and drop * node.draggable(true); * * // disable drag and drop * node.draggable(false); */ addGetterSetter(Node, 'draggable', false, getBooleanValidator()); Factory.backCompat(Node, { rotateDeg: 'rotate', setRotationDeg: 'setRotation', getRotationDeg: 'getRotation', });