konva/src/Container.ts

656 lines
16 KiB
TypeScript
Raw Normal View History

2021-05-05 22:54:03 +08:00
import { Factory } from './Factory';
import { Node, NodeConfig } from './Node';
import { getNumberValidator } from './Validators';
2019-01-02 04:59:27 +08:00
2019-01-25 11:52:16 +08:00
import { GetSet, IRect } from './types';
2021-05-05 22:54:03 +08:00
import { Shape } from './Shape';
import { HitCanvas, SceneCanvas } from './Canvas';
2021-05-06 20:46:36 +08:00
import { SceneContext } from './Context';
2019-01-02 04:59:27 +08:00
2023-06-05 23:28:44 +08:00
export type ClipFuncOutput =
| void
| [Path2D | CanvasFillRule]
| [Path2D, CanvasFillRule];
export interface ContainerConfig extends NodeConfig {
clearBeforeDraw?: boolean;
clipFunc?: (ctx: SceneContext) => ClipFuncOutput;
clipX?: number;
clipY?: number;
clipWidth?: number;
clipHeight?: number;
}
2019-01-02 04:59:27 +08:00
/**
* Container constructor.  Containers are used to contain nodes or other containers
* @constructor
* @memberof Konva
* @augments Konva.Node
* @abstract
* @param {Object} config
* @@nodeParams
* @@containerParams
*/
export abstract class Container<
2021-05-05 22:19:24 +08:00
ChildType extends Node = Node
> extends Node<ContainerConfig> {
2021-04-30 22:24:27 +08:00
children: Array<ChildType> | undefined = [];
2019-01-02 04:59:27 +08:00
/**
2021-04-30 22:24:27 +08:00
* returns an array of direct descendant nodes
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#getChildren
2019-01-02 04:59:27 +08:00
* @param {Function} [filterFunc] filter function
2021-04-30 22:24:27 +08:00
* @returns {Array}
2019-01-02 04:59:27 +08:00
* @example
* // get all children
* var children = layer.getChildren();
*
* // get only circles
* var circles = layer.getChildren(function(node){
* return node.getClassName() === 'Circle';
* });
*/
getChildren(filterFunc?: (item: Node) => boolean) {
if (!filterFunc) {
2021-04-30 22:24:27 +08:00
return this.children || [];
2019-01-02 04:59:27 +08:00
}
2021-04-30 22:24:27 +08:00
const children = this.children || [];
var results: Array<ChildType> = [];
children.forEach(function (child) {
2019-01-02 04:59:27 +08:00
if (filterFunc(child)) {
results.push(child);
}
});
return results;
}
/**
* determine if node has children
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#hasChildren
2019-01-02 04:59:27 +08:00
* @returns {Boolean}
*/
hasChildren() {
return this.getChildren().length > 0;
}
/**
* remove all children. Children will be still in memory.
* If you want to completely destroy all children please use "destroyChildren" method instead
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#removeChildren
2019-01-02 04:59:27 +08:00
*/
removeChildren() {
2021-04-30 22:24:27 +08:00
this.getChildren().forEach((child) => {
2019-01-02 04:59:27 +08:00
// reset parent to prevent many _setChildrenIndices calls
child.parent = null;
2019-01-02 04:59:27 +08:00
child.index = 0;
child.remove();
2021-04-30 22:24:27 +08:00
});
this.children = [];
// because all children were detached from parent, request draw via container
this._requestDraw();
2019-01-02 04:59:27 +08:00
return this;
}
/**
* destroy all children nodes.
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#destroyChildren
2019-01-02 04:59:27 +08:00
*/
destroyChildren() {
2021-04-30 22:24:27 +08:00
this.getChildren().forEach((child) => {
2019-01-02 04:59:27 +08:00
// reset parent to prevent many _setChildrenIndices calls
child.parent = null;
2019-01-02 04:59:27 +08:00
child.index = 0;
child.destroy();
2021-04-30 22:24:27 +08:00
});
this.children = [];
// because all children were detached from parent, request draw via container
this._requestDraw();
2019-01-02 04:59:27 +08:00
return this;
}
abstract _validateAdd(node: Node): void;
/**
2019-01-06 16:01:20 +08:00
* add a child and children into container
* @name Konva.Container#add
2019-01-02 04:59:27 +08:00
* @method
* @param {...Konva.Node} children
2019-01-02 04:59:27 +08:00
* @returns {Container}
* @example
2019-01-06 16:01:20 +08:00
* layer.add(rect);
2019-01-02 04:59:27 +08:00
* layer.add(shape1, shape2, shape3);
* // empty arrays are accepted, though each individual child must be defined
* layer.add(...shapes);
2019-01-06 16:01:20 +08:00
* // remember to redraw layer if you changed something
* layer.draw();
2019-01-02 04:59:27 +08:00
*/
add(...children: ChildType[]) {
if (children.length === 0) {
return this;
}
if (children.length > 1) {
for (var i = 0; i < children.length; i++) {
this.add(children[i]);
2019-01-02 04:59:27 +08:00
}
return this;
}
const child = children[0];
2019-01-02 04:59:27 +08:00
if (child.getParent()) {
child.moveTo(this);
return this;
}
this._validateAdd(child);
2021-04-30 22:24:27 +08:00
child.index = this.getChildren().length;
2019-01-02 04:59:27 +08:00
child.parent = this;
2021-05-28 04:02:59 +08:00
child._clearCaches();
2021-04-30 22:24:27 +08:00
this.getChildren().push(child);
2019-01-02 04:59:27 +08:00
this._fire('add', {
child: child,
2019-01-02 04:59:27 +08:00
});
2021-05-04 21:57:03 +08:00
this._requestDraw();
2019-01-02 04:59:27 +08:00
// chainable
return this;
}
destroy() {
if (this.hasChildren()) {
this.destroyChildren();
}
2019-01-06 16:01:20 +08:00
super.destroy();
2019-01-02 04:59:27 +08:00
return this;
}
/**
2021-04-30 22:24:27 +08:00
* return an array of nodes that match the selector.
2019-01-02 04:59:27 +08:00
* 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.
2019-01-02 04:59:27 +08:00
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#find
2019-01-02 04:59:27 +08:00
* @param {String | Function} selector
2021-04-30 22:24:27 +08:00
* @returns {Array}
2019-01-02 04:59:27 +08:00
* @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
*
2019-01-06 16:01:20 +08:00
* // get all groups with a function
2019-01-02 04:59:27 +08:00
* 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;
* });
*/
2021-04-30 22:24:27 +08:00
find<ChildNode extends Node = Node>(selector): Array<ChildNode> {
2019-01-02 04:59:27 +08:00
// protecting _generalFind to prevent user from accidentally adding
// second argument and getting unexpected `findOne` result
2019-04-22 21:17:25 +08:00
return this._generalFind<ChildNode>(selector, false);
2019-01-02 04:59:27 +08:00
}
/**
* return a first node from `find` method
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#findOne
2019-01-02 04:59:27 +08:00
* @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'
* })
*/
2023-07-12 16:04:47 +08:00
findOne<ChildNode extends Node = Node>(selector: string | Function): ChildNode | undefined {
2019-04-22 21:17:25 +08:00
var result = this._generalFind<ChildNode>(selector, true);
2019-01-02 04:59:27 +08:00
return result.length > 0 ? result[0] : undefined;
}
2020-03-29 20:45:24 +08:00
_generalFind<ChildNode extends Node = Node>(
selector: string | Function,
findOne: boolean
) {
2020-07-06 23:20:47 +08:00
var retArr: Array<ChildNode> = [];
2019-01-02 04:59:27 +08:00
2020-07-06 23:20:47 +08:00
this._descendants((node: ChildNode) => {
2019-03-19 03:18:03 +08:00
const valid = node._isMatch(selector);
if (valid) {
retArr.push(node);
2019-01-02 04:59:27 +08:00
}
2019-03-19 03:18:03 +08:00
if (valid && findOne) {
return true;
2019-01-02 04:59:27 +08:00
}
2019-03-19 03:18:03 +08:00
return false;
});
2019-01-02 04:59:27 +08:00
2021-04-30 22:24:27 +08:00
return retArr;
2019-01-02 04:59:27 +08:00
}
2020-03-29 20:45:24 +08:00
private _descendants(fn: (n: Node) => boolean) {
2019-03-19 03:18:03 +08:00
let shouldStop = false;
2021-04-30 22:24:27 +08:00
const children = this.getChildren();
for (const child of children) {
2019-03-19 03:18:03 +08:00
shouldStop = fn(child);
if (shouldStop) {
return true;
2019-01-02 04:59:27 +08:00
}
2019-03-19 03:18:03 +08:00
if (!child.hasChildren()) {
continue;
2019-01-02 04:59:27 +08:00
}
2019-04-17 23:45:47 +08:00
shouldStop = (child as any)._descendants(fn);
2019-03-19 03:18:03 +08:00
if (shouldStop) {
return true;
2019-01-02 04:59:27 +08:00
}
}
2019-03-19 03:18:03 +08:00
return false;
2019-01-02 04:59:27 +08:00
}
// extenders
toObject() {
var obj = Node.prototype.toObject.call(this);
obj.children = [];
2021-04-30 22:24:27 +08:00
this.getChildren().forEach((child) => {
2019-01-02 04:59:27 +08:00
obj.children.push(child.toObject());
2021-04-30 22:24:27 +08:00
});
2019-01-02 04:59:27 +08:00
return obj;
}
/**
* determine if node is an ancestor
* of descendant
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#isAncestorOf
2019-01-02 04:59:27 +08:00
* @param {Konva.Node} node
*/
2020-03-29 20:45:24 +08:00
isAncestorOf(node: Node) {
2019-01-02 04:59:27 +08:00
var parent = node.getParent();
while (parent) {
if (parent._id === this._id) {
return true;
}
parent = parent.getParent();
}
return false;
}
2020-03-29 20:45:24 +08:00
clone(obj?: any) {
2019-01-02 04:59:27 +08:00
// call super method
var node = Node.prototype.clone.call(this, obj);
2021-04-30 22:24:27 +08:00
this.getChildren().forEach(function (no) {
2019-01-02 04:59:27 +08:00
node.add(no.clone());
});
2021-04-30 22:24:27 +08:00
return node as this;
2019-01-02 04:59:27 +08:00
}
/**
* get all shapes that intersect a point. Note: because this method must clear a temporary
2019-01-06 16:01:20 +08:00
* canvas and redraw every shape inside the container, it should only be used for special situations
2019-01-02 04:59:27 +08:00
* because it performs very poorly. Please use the {@link Konva.Stage#getIntersection} method if at all possible
* because it performs much better
* @method
* @name Konva.Container#getAllIntersections
2019-01-02 04:59:27 +08:00
* @param {Object} pos
* @param {Number} pos.x
* @param {Number} pos.y
* @returns {Array} array of shapes
*/
getAllIntersections(pos) {
var arr = [];
2021-04-30 22:24:27 +08:00
this.find('Shape').forEach(function (shape: Shape) {
2019-01-02 04:59:27 +08:00
if (shape.isVisible() && shape.intersects(pos)) {
arr.push(shape);
}
});
return arr;
}
2021-05-05 22:54:03 +08:00
_clearSelfAndDescendantCache(attr?: string) {
super._clearSelfAndDescendantCache(attr);
2021-04-30 22:24:27 +08:00
// skip clearing if node is cached with canvas
// for performance reasons !!!
if (this.isCached()) {
return;
}
this.children?.forEach(function (node) {
2021-05-05 22:54:03 +08:00
node._clearSelfAndDescendantCache(attr);
2021-04-30 22:24:27 +08:00
});
}
2019-01-02 04:59:27 +08:00
_setChildrenIndices() {
2021-04-30 22:24:27 +08:00
this.children?.forEach(function (child, n) {
2019-01-02 04:59:27 +08:00
child.index = n;
});
2021-05-04 21:57:03 +08:00
this._requestDraw();
2019-01-02 04:59:27 +08:00
}
drawScene(can?: SceneCanvas, top?: Node) {
2019-01-02 04:59:27 +08:00
var layer = this.getLayer(),
canvas = can || (layer && layer.getCanvas()),
context = canvas && canvas.getContext(),
cachedCanvas = this._getCanvasCache(),
2019-01-02 04:59:27 +08:00
cachedSceneCanvas = cachedCanvas && cachedCanvas.scene;
var caching = canvas && canvas.isCache;
if (!this.isVisible() && !caching) {
return this;
}
if (cachedSceneCanvas) {
context.save();
2020-06-11 00:57:48 +08:00
var 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);
2019-01-02 04:59:27 +08:00
}
return this;
}
drawHit(can?: HitCanvas, top?: Node) {
if (!this.shouldDrawHit(top)) {
return this;
}
2019-01-02 04:59:27 +08:00
var layer = this.getLayer(),
canvas = can || (layer && layer.hitCanvas),
context = canvas && canvas.getContext(),
cachedCanvas = this._getCanvasCache(),
2019-01-02 04:59:27 +08:00
cachedHitCanvas = cachedCanvas && cachedCanvas.hit;
if (cachedHitCanvas) {
context.save();
2020-06-11 00:57:48 +08:00
var m = this.getAbsoluteTransform(top).getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
this._drawCachedHitCanvas(context);
context.restore();
} else {
this._drawChildren('drawHit', canvas, top);
2019-01-02 04:59:27 +08:00
}
return this;
}
_drawChildren(drawMethod, canvas, top) {
2020-06-11 00:57:48 +08:00
var context = canvas && canvas.getContext(),
2019-01-02 04:59:27 +08:00
clipWidth = this.clipWidth(),
clipHeight = this.clipHeight(),
clipFunc = this.clipFunc(),
hasClip = (clipWidth && clipHeight) || clipFunc;
const selfCache = top === this;
2019-01-02 04:59:27 +08:00
if (hasClip) {
2019-01-02 04:59:27 +08:00
context.save();
var transform = this.getAbsoluteTransform(top);
var m = transform.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
context.beginPath();
let clipArgs;
2019-01-02 04:59:27 +08:00
if (clipFunc) {
clipArgs = clipFunc.call(this, context, this);
2019-01-02 04:59:27 +08:00
} else {
var clipX = this.clipX();
var clipY = this.clipY();
2019-01-02 04:59:27 +08:00
context.rect(clipX, clipY, clipWidth, clipHeight);
}
context.clip.apply(context, clipArgs);
m = transform.copy().invert().getMatrix();
2019-01-02 04:59:27 +08:00
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
2019-02-19 01:12:03 +08:00
var hasComposition =
!selfCache &&
this.globalCompositeOperation() !== 'source-over' &&
drawMethod === 'drawScene';
if (hasComposition) {
2019-02-19 01:12:03 +08:00
context.save();
context._applyGlobalCompositeOperation(this);
}
2021-04-30 22:24:27 +08:00
this.children?.forEach(function (child) {
child[drawMethod](canvas, top);
2019-01-02 04:59:27 +08:00
});
if (hasComposition) {
2019-02-19 01:12:03 +08:00
context.restore();
}
2019-01-02 04:59:27 +08:00
if (hasClip) {
2019-01-02 04:59:27 +08:00
context.restore();
}
}
2019-08-04 15:38:57 +08:00
2020-03-29 20:45:24 +08:00
getClientRect(config?: {
skipTransform?: boolean;
skipShadow?: boolean;
skipStroke?: boolean;
relativeTo?: Container<Node>;
}): IRect {
config = config || {};
var skipTransform = config.skipTransform;
var relativeTo = config.relativeTo;
2019-01-02 04:59:27 +08:00
var minX, minY, maxX, maxY;
var selfRect = {
x: Infinity,
y: Infinity,
width: 0,
height: 0,
2019-01-02 04:59:27 +08:00
};
var that = this;
2021-04-30 22:24:27 +08:00
this.children?.forEach(function (child) {
2019-01-02 04:59:27 +08:00
// skip invisible children
2019-04-09 01:17:26 +08:00
if (!child.visible()) {
2019-01-02 04:59:27 +08:00
return;
}
var rect = child.getClientRect({
relativeTo: that,
2020-03-29 20:45:24 +08:00
skipShadow: config.skipShadow,
skipStroke: config.skipStroke,
2019-01-02 04:59:27 +08:00
});
// skip invisible children (like empty groups)
if (rect.width === 0 && rect.height === 0) {
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 child is group we need to make sure it has visible shapes inside
var shapes = this.find('Shape');
var hasVisible = false;
for (var i = 0; i < shapes.length; i++) {
var shape = shapes[i];
if (shape._isVisible(this)) {
hasVisible = true;
break;
}
}
if (hasVisible && minX !== undefined) {
2019-01-02 04:59:27 +08:00
selfRect = {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
2019-01-02 04:59:27 +08:00
};
} else {
selfRect = {
x: 0,
y: 0,
width: 0,
height: 0,
2019-01-02 04:59:27 +08:00
};
}
if (!skipTransform) {
return this._transformedRect(selfRect, relativeTo);
}
return selfRect;
}
clip: GetSet<IRect, this>;
clipX: GetSet<number, this>;
clipY: GetSet<number, this>;
clipWidth: GetSet<number, this>;
clipHeight: GetSet<number, this>;
2019-08-07 19:45:55 +08:00
// there was "this" instead of "Container<ChildType>",
// but it breaks react-konva types: https://github.com/konvajs/react-konva/issues/390
clipFunc: GetSet<
2023-06-05 23:28:44 +08:00
(
ctx: CanvasRenderingContext2D,
shape: Container<ChildType>
) => ClipFuncOutput,
2019-08-07 19:45:55 +08:00
this
>;
2019-01-02 04:59:27 +08:00
}
// add getters setters
Factory.addComponentsGetterSetter(Container, 'clip', [
'x',
'y',
'width',
'height',
2019-01-02 04:59:27 +08:00
]);
/**
* get/set clip
* @method
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clip
2019-01-02 04:59:27 +08:00
* @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
2019-01-06 16:01:20 +08:00
* container.clip({
2019-01-02 04:59:27 +08:00
* x: 20,
* y: 20,
* width: 20,
* height: 20
* });
*/
2019-02-25 01:06:04 +08:00
Factory.addGetterSetter(Container, 'clipX', undefined, getNumberValidator());
2019-01-02 04:59:27 +08:00
/**
* get/set clip x
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipX
2019-01-02 04:59:27 +08:00
* @method
* @param {Number} x
* @returns {Number}
* @example
* // get clip x
* var clipX = container.clipX();
*
* // set clip x
* container.clipX(10);
*/
2019-02-25 01:06:04 +08:00
Factory.addGetterSetter(Container, 'clipY', undefined, getNumberValidator());
2019-01-02 04:59:27 +08:00
/**
* get/set clip y
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipY
2019-01-02 04:59:27 +08:00
* @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,
2019-02-25 01:06:04 +08:00
getNumberValidator()
2019-01-02 04:59:27 +08:00
);
/**
* get/set clip width
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipWidth
2019-01-02 04:59:27 +08:00
* @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,
2019-02-25 01:06:04 +08:00
getNumberValidator()
2019-01-02 04:59:27 +08:00
);
/**
* get/set clip height
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipHeight
2019-01-02 04:59:27 +08:00
* @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
2019-01-06 16:01:20 +08:00
* @name Konva.Container#clipFunc
2019-01-02 04:59:27 +08:00
* @method
* @param {Function} function
* @returns {Function}
* @example
* // get clip function
* var clipFunction = container.clipFunc();
*
2023-06-05 23:28:44 +08:00
* // set clip function
2019-01-02 04:59:27 +08:00
* container.clipFunc(function(ctx) {
* ctx.rect(0, 0, 100, 100);
2023-06-05 23:28:44 +08:00
* });
*
2023-06-05 23:28:44 +08:00
* container.clipFunc(function(ctx) {
* // optionally return a clip Path2D and clip-rule or just the clip-rule
* return [new Path2D('M0 0v50h50Z'), 'evenodd']
2019-01-02 04:59:27 +08:00
* });
*/