574 lines
18 KiB
JavaScript
574 lines
18 KiB
JavaScript
import { Factory } from "./Factory.js";
|
|
import { Node } from "./Node.js";
|
|
import { getNumberValidator } from "./Validators.js";
|
|
/**
|
|
* Container constructor. 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() {
|
|
return this._clearChildren('remove');
|
|
}
|
|
/**
|
|
* destroy all children nodes.
|
|
* @method
|
|
* @name Konva.Container#destroyChildren
|
|
*/
|
|
destroyChildren() {
|
|
return this._clearChildren('destroy');
|
|
}
|
|
_clearChildren(dispose) {
|
|
return Node._updateBatch(() => {
|
|
this.children.forEach((child) => {
|
|
// reset parent to prevent many _setChildrenIndices calls
|
|
child.parent = null;
|
|
child.index = 0;
|
|
child[dispose]();
|
|
});
|
|
this.children = [];
|
|
// because all children were detached from parent, request draw via container
|
|
this._notifySubtreeChange();
|
|
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._notifySubtreeChange();
|
|
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: an absoluteTransform cascade is an update batch, so a listener on
|
|
// one of N descendants defers its work until the outermost walk returns.
|
|
// try/finally so depth/flush survive listener exceptions.
|
|
const isAbsTransform = attr === undefined || attr === 'absoluteTransform';
|
|
if (isAbsTransform)
|
|
Node._updateBatchDepth++;
|
|
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._updateBatchDepth === 0) {
|
|
Node._flushUpdateBatch();
|
|
}
|
|
}
|
|
}
|
|
_setChildrenIndices() {
|
|
var _a;
|
|
(_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (child, n) {
|
|
child.index = n;
|
|
});
|
|
this._notifySubtreeChange();
|
|
this._requestDraw();
|
|
}
|
|
drawScene(can, top) {
|
|
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);
|
|
}
|
|
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) {
|
|
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);
|
|
}
|
|
try {
|
|
this._drawChildNodes(drawMethod, canvas, top);
|
|
}
|
|
finally {
|
|
if (hasComposition) {
|
|
context.restore();
|
|
}
|
|
if (hasClip) {
|
|
context.restore();
|
|
}
|
|
}
|
|
}
|
|
_drawChildNodes(drawMethod, canvas, top) {
|
|
var _a;
|
|
(_a = this.children) === null || _a === void 0 ? void 0 : _a.forEach(function (child) {
|
|
child[drawMethod](canvas, top);
|
|
});
|
|
}
|
|
getClientRect(config = {}) {
|
|
var _a;
|
|
const cachedRect = this._getCachedSceneRect(config);
|
|
if (cachedRect)
|
|
return cachedRect;
|
|
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 && (config._forDrawing || 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,
|
|
_forDrawing: config._forDrawing,
|
|
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']
|
|
* });
|
|
*/
|