first commit
This commit is contained in:
+573
@@ -0,0 +1,573 @@
|
||||
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']
|
||||
* });
|
||||
*/
|
||||
Reference in New Issue
Block a user