UNPKG

konva

Version:

HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.

2,963 lines 102 kB
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, 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;
// "perfect drawing" buffer for cache() and toCanvas(): it mirrors the target
// canvas and is drawn back at (x, y) in the target's coordinate space.
// It starts empty and Shape.drawScene sizes it on first use, so nodes
// without buffered shapes never allocate it.
function createBufferCanvas(target, x, y) {
    const bufferCanvas = new SceneCanvas({
        width: 0,
        height: 0,
        pixelRatio: target.pixelRatio,
    });
    bufferCanvas.x = x;
    bufferCanvas.y = y;
    return bufferCanvas;
}
/**
 * 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 _runAfterAbsTransformCascade(cb) {
        if (Node._absTransformCascadeDepth > 0) {
            Node._pendingAfterCascade.push(cb);
        }
        else {
            cb();
        }
    }
    /**
     * 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();
        const bufferCanvas = createBufferCanvas(cachedSceneCanvas, x, y);
        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, bufferCanvas);
        }
        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();
            // the buffer is only needed while drawing
            Util.releaseCanvas(bufferCanvas._canvas);
        }
        // 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');
    }
    _transformedRect(rect, top) {
        const points = [
            { x: rect.x, y: rect.y },
            { x: rect.x + rect.width, y: rect.y },
            { x: rect.x + rect.width, y: rect.y + rect.height },
            { x: rect.x, y: rect.y + rect.height },
        ];
        let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
        const trans = this.getAbsoluteTransform(top);
        points.forEach(function (point) {
            const transformed = trans.point(point);
            minX = Math.min(minX, transformed.x);
            minY = Math.min(minY, transformed.y);
            maxX = Math.max(maxX, transformed.x);
            maxY = Math.max(maxY, transformed.y);
        });
        return {
            x: minX,
            y: minY,
            width: maxX - minX,
            height: maxY - minY,
        };
    }
    _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;
        }
        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];
            // 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();
        const bufferCanvas = createBufferCanvas(canvas, x, y);
        if (config.imageSmoothingEnabled === false) {
            context._context.imageSmoothingEnabled = false;
        }
        context.save();
        if (x || y) {
            context.translate(-1 * x, -1 * y);
        }
        this.drawScene(canvas, undefined, bufferCanvas);
        context.restore();
        // the buffer is only needed while drawing
        Util.releaseCanvas(bufferCanvas._canvas);
        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<Image>}
     * @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<Blob>} 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;
    }
    _requestDraw() {
        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;
    }
}
// Perf: lets a listener inside an absoluteTransform cascade (e.g. Konva
// .Transformer reacting to a stage drag fan-out across N attached nodes)
// defer work until the cascade ends — collapses O(N) listener-driven calls
// into one. Counter is mutated by Container's _clearSelfAndDescendantCache.
Node._absTransformCascadeDepth = 0;
Node._pendingAfterCascade = [];
// 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',
});