UNPKG

konva

Version:

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

569 lines (568 loc) 18.2 kB
import { Factory } from "./Factory.js"; import { Node } from "./Node.js"; import { getNumberValidator } from "./Validators.js"; /** * Container constructor.&nbsp; Containers are used to contain nodes or other containers * @constructor * @memberof Konva * @augments Konva.Node * @abstract * @param {Object} config * @@nodeParams * @@containerParams */ export class Container extends Node { constructor() { super(...arguments); this.children = []; } /** * returns an array of direct descendant nodes * @method * @name Konva.Container#getChildren * @param {Function} [filterFunc] filter function * @returns {Array} * @example * // get all children * var children = layer.getChildren(); * * // get only circles * var circles = layer.getChildren(function(node){ * return node.getClassName() === 'Circle'; * }); */ getChildren(filterFunc) { // a copy, so that removing children while iterating over it is safe const children = this.children || []; return filterFunc ? children.filter(filterFunc) : children.slice(); } /** * determine if node has children * @method * @name Konva.Container#hasChildren * @returns {Boolean} */ hasChildren() { return this.children.length > 0; } /** * remove all children. Children will be still in memory. * If you want to completely destroy all children please use "destroyChildren" method instead * @method * @name Konva.Container#removeChildren */ removeChildren() { this.children.forEach((child) => { // reset parent to prevent many _setChildrenIndices calls child.parent = null; child.index = 0; child.remove(); }); this.children = []; // because all children were detached from parent, request draw via container this._requestDraw(); return this; } /** * destroy all children nodes. * @method * @name Konva.Container#destroyChildren */ destroyChildren() { this.children.forEach((child) => { // reset parent to prevent many _setChildrenIndices calls child.parent = null; child.index = 0; child.destroy(); }); this.children = []; // because all children were detached from parent, request draw via container this._requestDraw(); return this; } /** * add a child and children into container * @name Konva.Container#add * @method * @param {...Konva.Node} children * @returns {Container} * @example * layer.add(rect); * layer.add(shape1, shape2, shape3); * // empty arrays are accepted, though each individual child must be defined * layer.add(...shapes); */ add(...children) { if (children.length === 0) { return this; } if (children.length > 1) { for (let i = 0; i < children.length; i++) { this.add(children[i]); } return this; } const child = children[0]; if (child.getParent()) { child.moveTo(this); return this; } this._validateAdd(child); child.index = this.children.length; child.parent = this; child._clearCaches(); this.children.push(child); this._fire('add', { child: child, }); this._requestDraw(); // chainable return this; } destroy() { if (this.hasChildren()) { this.destroyChildren(); } super.destroy(); return this; } /** * return an array of nodes that match the selector. * You can provide a string with '#' for id selections and '.' for name selections. * Or a function that will return true/false when a node is passed through. See example below. * With strings you can also select by type or class name. Pass multiple selectors * separated by a comma. * @method * @name Konva.Container#find * @param {String | Function} selector * @returns {Array} * @example * * Passing a string as a selector * // select node with id foo * var node = stage.find('#foo'); * * // select nodes with name bar inside layer * var nodes = layer.find('.bar'); * * // select all groups inside layer * var nodes = layer.find('Group'); * * // select all rectangles inside layer * var nodes = layer.find('Rect'); * * // select node with an id of foo or a name of bar inside layer * var nodes = layer.find('#foo, .bar'); * * Passing a function as a selector * * // get all groups with a function * var groups = stage.find(node => { * return node.getType() === 'Group'; * }); * * // get only Nodes with partial opacity * var alphaNodes = layer.find(node => { * return node.getType() === 'Node' && node.getAbsoluteOpacity() < 1; * }); */ find(selector) { // protecting _generalFind to prevent user from accidentally adding // second argument and getting unexpected `findOne` result return this._generalFind(selector, false); } /** * return a first node from `find` method * @method * @name Konva.Container#findOne * @param {String | Function} selector * @returns {Konva.Node | Undefined} * @example * // select node with id foo * var node = stage.findOne('#foo'); * * // select node with name bar inside layer * var nodes = layer.findOne('.bar'); * * // select the first node to return true in a function * var node = stage.findOne(node => { * return node.getType() === 'Shape' * }) */ findOne(selector) { const result = this._generalFind(selector, true); return result.length > 0 ? result[0] : undefined; } _generalFind(selector, findOne) { const retArr = []; this._descendants((node) => { const valid = node._isMatch(selector); if (valid) { retArr.push(node); } if (valid && findOne) { return true; } return false; }); return retArr; } _descendants(fn) { let shouldStop = false; for (const child of this.children) { shouldStop = fn(child); if (shouldStop) { return true; } if (!child.hasChildren()) { continue; } shouldStop = child._descendants(fn); if (shouldStop) { return true; } } return false; } // extenders toObject() { const obj = Node.prototype.toObject.call(this); obj.children = []; this.children.forEach((child) => { obj.children.push(child.toObject()); }); return obj; } /** * determine if node is an ancestor * of descendant * @method * @name Konva.Container#isAncestorOf * @param {Konva.Node} node */ isAncestorOf(node) { let parent = node.getParent(); while (parent) { if (parent._id === this._id) { return true; } parent = parent.getParent(); } return false; } clone(obj) { // call super method const node = Node.prototype.clone.call(this, obj); this.children.forEach(function (no) { node.add(no.clone()); }); return node; } /** * get all shapes that intersect a point. Note: because this method must clear a temporary * canvas and redraw every shape inside the container, it should only be used for special situations * because it performs very poorly. Please use the {@link Konva.Stage#getIntersection} method if at all possible * because it performs much better. * Nodes with listening set to false or invisible nodes are not detected * @method * @name Konva.Container#getAllIntersections * @param {Object} pos * @param {Number} pos.x * @param {Number} pos.y * @returns {Array} array of shapes */ getAllIntersections(pos) { const arr = []; this.find('Shape').forEach((shape) => { if (shape.isVisible() && shape.intersects(pos)) { arr.push(shape); } }); return arr; } _clearSelfAndDescendantCache(attr) { var _a; // Perf: bracket absoluteTransform cascades with a depth counter so // Node._runAfterAbsTransformCascade can defer work until the outermost // walk returns. try/finally so depth/flush survive listener exceptions. const isAbsTransform = attr === undefined || attr === 'absoluteTransform'; if (isAbsTransform) Node._absTransformCascadeDepth++; try { super._clearSelfAndDescendantCache(attr); // a cached container is drawn from its bitmap, so its descendants keep // their transform caches while it moves (perf); Node.getAbsoluteTransform // skips those caches under a cached ancestor if (attr === 'absoluteTransform' && this.isCached()) return; (_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (node) { node._clearSelfAndDescendantCache(attr); }); } finally { if (isAbsTransform && --Node._absTransformCascadeDepth === 0) { const callbacks = Node._pendingAfterCascade; if (callbacks.length) { Node._pendingAfterCascade = []; for (let i = 0; i < callbacks.length; i++) callbacks[i](); } } } } _setChildrenIndices() { var _a; (_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (child, n) { child.index = n; }); this._requestDraw(); } drawScene(can, top, bufferCanvas) { const layer = this.getLayer(), canvas = can || (layer && layer.getCanvas()), context = canvas && canvas.getContext(), cachedCanvas = this._getCanvasCache(), cachedSceneCanvas = cachedCanvas && cachedCanvas.scene; const caching = top === this; if (!(top ? this._isVisible(top) : this.isVisible()) && !caching) { return this; } if (cachedSceneCanvas) { context.save(); const m = this.getAbsoluteTransform(top).getMatrix(); context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); this._drawCachedSceneCanvas(context); context.restore(); } else { this._drawChildren('drawScene', canvas, top, bufferCanvas); } return this; } drawHit(can, top) { if (!this.shouldDrawHit(top)) { return this; } const layer = this.getLayer(), canvas = can || (layer && layer.hitCanvas), context = canvas && canvas.getContext(), cachedHitCanvas = this._getCachedHitCanvas(top); if (cachedHitCanvas) { context.save(); const m = this.getAbsoluteTransform(top).getMatrix(); context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); this._drawCachedHitCanvas(context, cachedHitCanvas); context.restore(); } else { this._drawChildren('drawHit', canvas, top); } return this; } _drawChildren(drawMethod, canvas, top, bufferCanvas) { var _a; const context = canvas && canvas.getContext(), clipWidth = this.clipWidth(), clipHeight = this.clipHeight(), clipFunc = this.clipFunc(), hasClip = (typeof clipWidth === 'number' && typeof clipHeight === 'number') || clipFunc; const selfCache = top === this; if (hasClip) { context.save(); const transform = this.getAbsoluteTransform(top); let m = transform.getMatrix(); context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); context.beginPath(); let clipArgs; if (clipFunc) { clipArgs = clipFunc.call(this, context, this); } else { const clipX = this.clipX(); const clipY = this.clipY(); context.rect(clipX || 0, clipY || 0, clipWidth, clipHeight); } context.clip.apply(context, clipArgs); m = transform.copy().invert().getMatrix(); context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } const hasComposition = !selfCache && this.globalCompositeOperation() !== 'source-over' && drawMethod === 'drawScene'; if (hasComposition) { context.save(); context._applyGlobalCompositeOperation(this); } (_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (child) { child[drawMethod](canvas, top, bufferCanvas); }); if (hasComposition) { context.restore(); } if (hasClip) { context.restore(); } } getClientRect(config = {}) { var _a; const skipTransform = config.skipTransform; const relativeTo = config.relativeTo; const [a, b, c, d] = this.getAbsoluteTransform().getMatrix(); // A zero scale cannot project local padding for a non-scaling stroke. const measureTransformed = !skipTransform && a * d - b * c === 0; let minX, minY, maxX, maxY; let selfRect = { x: Infinity, y: Infinity, width: 0, height: 0, }; const that = this; (_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (child) { // skip invisible children if (!child.visible()) { return; } const rect = child.getClientRect({ relativeTo: measureTransformed ? relativeTo : that, skipShadow: config.skipShadow, skipStroke: config.skipStroke, }); // skip invisible children (like empty groups) and a child whose rect // would poison the whole box if ((rect.width === 0 && rect.height === 0) || !isFinite(rect.x + rect.y + rect.width + rect.height)) { return; } if (minX === undefined) { // initial value for first child minX = rect.x; minY = rect.y; maxX = rect.x + rect.width; maxY = rect.y + rect.height; } else { minX = Math.min(minX, rect.x); minY = Math.min(minY, rect.y); maxX = Math.max(maxX, rect.x + rect.width); maxY = Math.max(maxY, rect.y + rect.height); } }); if (minX !== undefined) { selfRect = { x: minX, y: minY, width: maxX - minX, height: maxY - minY, }; } else { selfRect = { x: 0, y: 0, width: 0, height: 0, }; } if (!skipTransform && !measureTransformed) { return this._transformedRect(selfRect, relativeTo); } return selfRect; } } // add getters setters Factory.addComponentsGetterSetter(Container, 'clip', [ 'x', 'y', 'width', 'height', ]); /** * get/set clip * @method * @name Konva.Container#clip * @param {Object} clip * @param {Number} clip.x * @param {Number} clip.y * @param {Number} clip.width * @param {Number} clip.height * @returns {Object} * @example * // get clip * var clip = container.clip(); * * // set clip * container.clip({ * x: 20, * y: 20, * width: 20, * height: 20 * }); */ Factory.addGetterSetter(Container, 'clipX', undefined, getNumberValidator()); /** * get/set clip x * @name Konva.Container#clipX * @method * @param {Number} x * @returns {Number} * @example * // get clip x * var clipX = container.clipX(); * * // set clip x * container.clipX(10); */ Factory.addGetterSetter(Container, 'clipY', undefined, getNumberValidator()); /** * get/set clip y * @name Konva.Container#clipY * @method * @param {Number} y * @returns {Number} * @example * // get clip y * var clipY = container.clipY(); * * // set clip y * container.clipY(10); */ Factory.addGetterSetter(Container, 'clipWidth', undefined, getNumberValidator()); /** * get/set clip width * @name Konva.Container#clipWidth * @method * @param {Number} width * @returns {Number} * @example * // get clip width * var clipWidth = container.clipWidth(); * * // set clip width * container.clipWidth(100); */ Factory.addGetterSetter(Container, 'clipHeight', undefined, getNumberValidator()); /** * get/set clip height * @name Konva.Container#clipHeight * @method * @param {Number} height * @returns {Number} * @example * // get clip height * var clipHeight = container.clipHeight(); * * // set clip height * container.clipHeight(100); */ Factory.addGetterSetter(Container, 'clipFunc'); /** * get/set clip function * @name Konva.Container#clipFunc * @method * @param {Function} function * @returns {Function} * @example * // get clip function * var clipFunction = container.clipFunc(); * * // set clip function * container.clipFunc(function(ctx) { * ctx.rect(0, 0, 100, 100); * }); * * container.clipFunc(function(ctx) { * // optionally return a clip Path2D and clip-rule or just the clip-rule * return [new Path2D('M0 0v50h50Z'), 'evenodd'] * }); */