konva
Version:
HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.
569 lines (568 loc) • 18.2 kB
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() {
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']
* });
*/