3005 lines
101 KiB
JavaScript
3005 lines
101 KiB
JavaScript
import { HitCanvas, SceneCanvas } from "./Canvas.js";
|
|
import { isCSSFiltersSupported } from "./Context.js";
|
|
import { DD } from "./DragAndDrop.js";
|
|
import * as PointerEvents from "./PointerEvents.js";
|
|
import { Factory } from "./Factory.js";
|
|
import { Konva } from "./Global.js";
|
|
import { Transform, TypedArray, Util } from "./Util.js";
|
|
import { getBooleanValidator, getNumberValidator, getStringValidator, } from "./Validators.js";
|
|
// CSS filter parser for fallback to function filters.
|
|
function parseCSSFilters(cssFilter) {
|
|
const steps = [];
|
|
const filterRegex = /(\w+)\(([^)]*)\)/g;
|
|
let match;
|
|
while ((match = filterRegex.exec(cssFilter)) !== null) {
|
|
const [, name, argument] = match;
|
|
if (![
|
|
'blur',
|
|
'brightness',
|
|
'contrast',
|
|
'grayscale',
|
|
'sepia',
|
|
'invert',
|
|
].includes(name)) {
|
|
Util.warn(`CSS filter "${name}" is not supported in fallback mode. Consider using function filters for better compatibility.`);
|
|
continue;
|
|
}
|
|
const value = argument.trim() === ''
|
|
? name === 'blur'
|
|
? 0
|
|
: 1
|
|
: parseFloat(argument) / (argument.includes('%') ? 100 : 1);
|
|
steps.push({ name, value });
|
|
}
|
|
return function (imageData, pixelRatio = 1) {
|
|
var _a;
|
|
for (const { name, value } of steps) {
|
|
if (['grayscale', 'sepia', 'invert'].includes(name)) {
|
|
const amount = Math.min(1, Math.max(0, value));
|
|
const data = imageData.data;
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const r = data[i], g = data[i + 1], b = data[i + 2];
|
|
let red, green, blue;
|
|
if (name === 'grayscale') {
|
|
red = green = blue = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
}
|
|
else if (name === 'sepia') {
|
|
red = 0.393 * r + 0.769 * g + 0.189 * b;
|
|
green = 0.349 * r + 0.686 * g + 0.168 * b;
|
|
blue = 0.272 * r + 0.534 * g + 0.131 * b;
|
|
}
|
|
else {
|
|
red = 255 - r;
|
|
green = 255 - g;
|
|
blue = 255 - b;
|
|
}
|
|
// CSS interpolates the color matrix before clamping the result.
|
|
data[i] = r + (red - r) * amount;
|
|
data[i + 1] = g + (green - g) * amount;
|
|
data[i + 2] = b + (blue - b) * amount;
|
|
}
|
|
continue;
|
|
}
|
|
const filter = (_a = Konva.Filters) === null || _a === void 0 ? void 0 : _a[Util._capitalize(name)];
|
|
if (!filter)
|
|
continue;
|
|
// Supply filter parameters without setters or changes to the node.
|
|
const context = Object.create(this);
|
|
context.attrs = { ...this.attrs };
|
|
if (name === 'blur')
|
|
context.attrs.blurRadius = value * 0.5;
|
|
if (name === 'brightness')
|
|
context.attrs.brightness = value;
|
|
if (name === 'contrast')
|
|
context.attrs.contrast = 100 * (Math.sqrt(value) - 1);
|
|
filter.call(context, imageData, pixelRatio);
|
|
}
|
|
};
|
|
}
|
|
// CONSTANTS
|
|
const ABSOLUTE_OPACITY = 'absoluteOpacity', ABSOLUTE_TRANSFORM = 'absoluteTransform', CHANGE = 'Change', CHILDREN = 'children', KONVA = 'konva', LISTENING = 'listening', MOUSEENTER = 'mouseenter', MOUSELEAVE = 'mouseleave', POINTERENTER = 'pointerenter', POINTERLEAVE = 'pointerleave', TOUCHENTER = 'touchenter', TOUCHLEAVE = 'touchleave', NON_BUBBLING_EVENTS = [
|
|
MOUSEENTER,
|
|
MOUSELEAVE,
|
|
POINTERENTER,
|
|
POINTERLEAVE,
|
|
TOUCHENTER,
|
|
TOUCHLEAVE,
|
|
], SET = 'set', SHAPE = 'Shape', SPACE = ' ', STAGE = 'stage', TRANSFORM = 'transform', UPPER_STAGE = 'Stage', VISIBLE = 'visible', TRANSFORM_CHANGE_STR = [
|
|
'xChange.konva',
|
|
'yChange.konva',
|
|
'scaleXChange.konva',
|
|
'scaleYChange.konva',
|
|
'skewXChange.konva',
|
|
'skewYChange.konva',
|
|
'rotationChange.konva',
|
|
'offsetXChange.konva',
|
|
'offsetYChange.konva',
|
|
'transformsEnabledChange.konva',
|
|
].join(SPACE);
|
|
let idCounter = 1;
|
|
/**
|
|
* Node constructor. Nodes are entities that can be transformed, layered,
|
|
* and have bound events. The stage, layers, groups, and shapes all extend Node.
|
|
* @constructor
|
|
* @memberof Konva
|
|
* @param {Object} config
|
|
* @@nodeParams
|
|
*/
|
|
export class Node {
|
|
constructor(config) {
|
|
this._id = idCounter++;
|
|
this.eventListeners = {};
|
|
this.attrs = {};
|
|
this.index = 0;
|
|
this.parent = null;
|
|
// derived values (transforms, visibility, opacity...), see _getCache()
|
|
this._cache = {};
|
|
// canvases of cache(), see _getCanvasCache()
|
|
this._canvasCache = null;
|
|
this._batchingTransformChange = false;
|
|
this._needClearTransformCache = false;
|
|
this._filterUpToDate = false;
|
|
this._isUnderCache = false;
|
|
this._dragEventId = null;
|
|
this._shouldFireChangeEvents = false;
|
|
// on initial set attrs wi don't need to fire change events
|
|
// because nobody is listening to them yet
|
|
this.setAttrs(config);
|
|
this._shouldFireChangeEvents = true;
|
|
// all change event listeners are attached to the prototype
|
|
}
|
|
hasChildren() {
|
|
return false;
|
|
}
|
|
_clearCache(attr) {
|
|
// if we want to clear transform cache
|
|
// we don't really need to remove it from the cache
|
|
// but instead mark as "dirty"
|
|
// so we don't need to create a new instance next time
|
|
if ((attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM) &&
|
|
this._cache[attr]) {
|
|
this._cache[attr].dirty = true;
|
|
}
|
|
else if (attr) {
|
|
this._cache[attr] = undefined;
|
|
}
|
|
else {
|
|
this._cache = {};
|
|
// clearCache() also reaches descendants whose ancestor transform changes
|
|
// were suppressed while cached. Invalidate their saved Transformer bounds.
|
|
if (this._attrsVersion !== undefined)
|
|
this._attrsVersion++;
|
|
}
|
|
}
|
|
// drop the cached canvases and give their memory back
|
|
_releaseCanvasCache() {
|
|
const cache = this._canvasCache;
|
|
if (cache) {
|
|
Util.releaseCanvas(cache.scene._canvas, cache.filter._canvas, ...(cache.hit ? [cache.hit._canvas] : []));
|
|
this._canvasCache = null;
|
|
}
|
|
}
|
|
_getCache(attr, privateGetter) {
|
|
let cache = this._cache[attr];
|
|
// for transform the cache can be NOT empty
|
|
// but we still need to recalculate it if it is dirty
|
|
const isTransform = attr === TRANSFORM || attr === ABSOLUTE_TRANSFORM;
|
|
const invalid = cache === undefined || (isTransform && cache.dirty === true);
|
|
// if not cached, we need to set it using the private getter method.
|
|
if (invalid) {
|
|
cache = privateGetter.call(this);
|
|
this._cache[attr] = cache;
|
|
}
|
|
return cache;
|
|
}
|
|
_getCanvasCache() {
|
|
return this._canvasCache;
|
|
}
|
|
/*
|
|
* when the logic for a cached result depends on ancestor propagation, use this
|
|
* method to clear self and children cache
|
|
*/
|
|
_clearSelfAndDescendantCache(attr) {
|
|
this._clearCache(attr);
|
|
// trigger clear cache, so transformer can use it
|
|
if (attr === undefined || attr === ABSOLUTE_TRANSFORM) {
|
|
this.fire('absoluteTransformChange');
|
|
}
|
|
}
|
|
static _runAfterUpdateBatch(cb) {
|
|
if (Node._updateBatchDepth > 0) {
|
|
Node._pendingUpdates.add(cb);
|
|
}
|
|
else {
|
|
cb();
|
|
}
|
|
}
|
|
static _flushUpdateBatch() {
|
|
if (Node._updateBatchDepth > 0)
|
|
return;
|
|
const callbacks = Node._pendingUpdates;
|
|
if (!callbacks.size)
|
|
return;
|
|
Node._pendingUpdates = new Set();
|
|
callbacks.forEach((callback) => callback());
|
|
}
|
|
static _updateBatch(fn) {
|
|
Node._updateBatchDepth++;
|
|
try {
|
|
return fn();
|
|
}
|
|
finally {
|
|
Node._updateBatchDepth--;
|
|
Node._flushUpdateBatch();
|
|
}
|
|
}
|
|
static _beginUpdateBatch() {
|
|
Node._updateBatchDepth++;
|
|
let finished = false;
|
|
return () => {
|
|
if (finished)
|
|
return;
|
|
finished = true;
|
|
Node._updateBatchDepth--;
|
|
Node._flushUpdateBatch();
|
|
};
|
|
}
|
|
/**
|
|
* clear cached canvas
|
|
* @method
|
|
* @name Konva.Node#clearCache
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* node.clearCache();
|
|
*/
|
|
clearCache() {
|
|
this._releaseCanvasCache();
|
|
this._clearSelfAndDescendantCache();
|
|
this._requestDraw();
|
|
return this;
|
|
}
|
|
/**
|
|
* cache node to improve drawing performance, apply filters, or create more accurate
|
|
* hit regions. For all basic shapes size of cache canvas will be automatically detected.
|
|
* If you need to cache your custom `Konva.Shape` instance you have to pass shape's bounding box
|
|
* properties. Look at [https://konvajs.org/docs/performance/Shape_Caching.html](https://konvajs.org/docs/performance/Shape_Caching.html) for more information.
|
|
* @method
|
|
* @name Konva.Node#cache
|
|
* @param {Object} [config]
|
|
* @param {Number} [config.x]
|
|
* @param {Number} [config.y]
|
|
* @param {Number} [config.width]
|
|
* @param {Number} [config.height]
|
|
* @param {Number} [config.offset] increase canvas size by `offset` pixel in all directions.
|
|
* @param {Boolean} [config.drawBorder] when set to true, a red border will be drawn around the cached
|
|
* region for debugging purposes
|
|
* @param {Number} [config.pixelRatio] pixel ratio of the cache canvas, the canvas pixels per CSS pixel of the node. Default is `Konva.pixelRatio`, the device pixel ratio. A higher value keeps the cache sharp when the node is scaled up.
|
|
* @param {Boolean} [config.imageSmoothingEnabled] control imageSmoothingEnabled property of created canvas for cache
|
|
* @param {Number} [config.hitCanvasPixelRatio] change quality (or pixel ratio) of cached hit canvas.
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // cache a shape with the x,y position of the bounding box at the center and
|
|
* // the width and height of the bounding box equal to the width and height of
|
|
* // the shape obtained from shape.width() and shape.height()
|
|
* image.cache();
|
|
*
|
|
* // cache a node and define the bounding box position and size
|
|
* node.cache({
|
|
* x: -30,
|
|
* y: -30,
|
|
* width: 100,
|
|
* height: 200
|
|
* });
|
|
*
|
|
* // cache a node and draw a red border around the bounding box
|
|
* // for debugging purposes
|
|
* node.cache({
|
|
* x: -30,
|
|
* y: -30,
|
|
* width: 100,
|
|
* height: 200,
|
|
* offset : 10,
|
|
* drawBorder: true
|
|
* });
|
|
*/
|
|
cache(config) {
|
|
const conf = config || {};
|
|
let rect = {};
|
|
// don't call getClientRect if we have all attributes
|
|
// it means call it only if have one undefined
|
|
if (conf.x === undefined ||
|
|
conf.y === undefined ||
|
|
conf.width === undefined ||
|
|
conf.height === undefined) {
|
|
const wasUnderCache = this._isUnderCache;
|
|
this._isUnderCache = true;
|
|
rect = this.getClientRect({
|
|
skipTransform: true,
|
|
relativeTo: this.getParent() || undefined,
|
|
});
|
|
this._isUnderCache = wasUnderCache;
|
|
}
|
|
let x = conf.x === undefined ? Math.floor(rect.x) : conf.x, y = conf.y === undefined ? Math.floor(rect.y) : conf.y,
|
|
// round the far edge, not the size, so the last row and column fit in
|
|
width = Math.ceil(conf.width || rect.x + rect.width - x), height = Math.ceil(conf.height || rect.y + rect.height - y), pixelRatio = conf.pixelRatio, offset = conf.offset || 0, drawBorder = conf.drawBorder || false, hitCanvasPixelRatio = conf.hitCanvasPixelRatio || 1;
|
|
if (width <= 0 || height <= 0 || !isFinite(x + y + width + height)) {
|
|
Util.error(`Can not cache the node. Its size is 0 or its bounds are not finite numbers (${x}, ${y}, ${width}x${height}). Caching is skipped.`);
|
|
return this;
|
|
}
|
|
width += offset * 2;
|
|
height += offset * 2;
|
|
x -= offset;
|
|
y -= offset;
|
|
const cachedSceneCanvas = new SceneCanvas({
|
|
pixelRatio: pixelRatio,
|
|
width: width,
|
|
height: height,
|
|
}), cachedFilterCanvas = new SceneCanvas({
|
|
pixelRatio: pixelRatio,
|
|
width: 0,
|
|
height: 0,
|
|
willReadFrequently: true,
|
|
}), sceneContext = cachedSceneCanvas.getContext();
|
|
cachedSceneCanvas.isCache = true;
|
|
this._releaseCanvasCache();
|
|
this._filterUpToDate = false;
|
|
if (conf.imageSmoothingEnabled === false) {
|
|
cachedSceneCanvas.getContext()._context.imageSmoothingEnabled = false;
|
|
cachedFilterCanvas.getContext()._context.imageSmoothingEnabled = false;
|
|
}
|
|
sceneContext.save();
|
|
sceneContext.translate(-x, -y);
|
|
// extra flag to skip on getAbsolute opacity calc
|
|
this._isUnderCache = true;
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
|
|
try {
|
|
this.drawScene(cachedSceneCanvas, this);
|
|
}
|
|
catch (e) {
|
|
Util.releaseCanvas(cachedSceneCanvas._canvas);
|
|
throw e;
|
|
}
|
|
finally {
|
|
this._isUnderCache = false;
|
|
// descendants cached their opacity relative to this node while drawing
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
|
|
sceneContext.restore();
|
|
cachedSceneCanvas._releaseIsolationCanvas();
|
|
}
|
|
// this will draw a red border around the cached box for
|
|
// debugging purposes
|
|
if (drawBorder) {
|
|
sceneContext.save();
|
|
sceneContext.beginPath();
|
|
sceneContext.rect(0, 0, width, height);
|
|
sceneContext.closePath();
|
|
sceneContext.setAttr('strokeStyle', 'red');
|
|
sceneContext.setAttr('lineWidth', 5);
|
|
sceneContext.stroke();
|
|
sceneContext.restore();
|
|
}
|
|
this._canvasCache = {
|
|
scene: cachedSceneCanvas,
|
|
filter: cachedFilterCanvas,
|
|
// the hit canvas is built on demand (see _getCachedHitCanvas)
|
|
hit: null,
|
|
hitConfig: {
|
|
pixelRatio: hitCanvasPixelRatio,
|
|
width: width,
|
|
height: height,
|
|
},
|
|
x: x,
|
|
y: y,
|
|
};
|
|
// Entering cache space changes non-scaling stroke bounds for descendants.
|
|
this._clearSelfAndDescendantCache();
|
|
this._requestDraw();
|
|
return this;
|
|
}
|
|
// build the hit canvas of a cached node lazily - only when the hit graph
|
|
// is actually needed, so a non-listening node never allocates it
|
|
// https://github.com/konvajs/konva/issues/2009
|
|
_getCachedHitCanvas(top) {
|
|
// top === this means the node is drawing into its own cache right now:
|
|
// the build below re-enters drawHit for this very node, and consulting
|
|
// the not-yet-stored canvas again would recurse forever
|
|
if (top === this) {
|
|
return null;
|
|
}
|
|
const cache = this._getCanvasCache();
|
|
if (!cache) {
|
|
return null;
|
|
}
|
|
if (cache.hit) {
|
|
return cache.hit;
|
|
}
|
|
const hitCanvas = new HitCanvas(cache.hitConfig);
|
|
hitCanvas.isCache = true;
|
|
const hitContext = hitCanvas.getContext();
|
|
hitContext.save();
|
|
hitContext.translate(-cache.x, -cache.y);
|
|
this.drawHit(hitCanvas, this);
|
|
hitContext.restore();
|
|
cache.hit = hitCanvas;
|
|
return hitCanvas;
|
|
}
|
|
/**
|
|
* determine if node is currently cached
|
|
* @method
|
|
* @name Konva.Node#isCached
|
|
* @returns {Boolean}
|
|
*/
|
|
isCached() {
|
|
return !!this._canvasCache;
|
|
}
|
|
/**
|
|
* Return client rectangle {x, y, width, height} of node. This rectangle also include all styling (strokes, shadows, etc).
|
|
* The purpose of the method is similar to getBoundingClientRect API of the DOM.
|
|
* Non-scaling stroke padding is converted from its drawing canvas to the requested coordinates.
|
|
* Cached strokes use the cache's coordinates; their pixels scale with the cached bitmap.
|
|
* @method
|
|
* @name Konva.Node#getClientRect
|
|
* @param {Object} config
|
|
* @param {Boolean} [config.skipTransform] should we apply transform to node for calculating rect?
|
|
* @param {Boolean} [config.skipShadow] should we apply shadow to the node for calculating bound box?
|
|
* @param {Boolean} [config.skipStroke] should we apply stroke to the node for calculating bound box?
|
|
* @param {Object} [config.relativeTo] calculate client rect relative to one of the parents
|
|
* @returns {Object} rect with {x, y, width, height} properties
|
|
* @example
|
|
* var rect = new Konva.Rect({
|
|
* width : 100,
|
|
* height : 100,
|
|
* x : 50,
|
|
* y : 50,
|
|
* strokeWidth : 4,
|
|
* stroke : 'black',
|
|
* offsetX : 50,
|
|
* scaleY : 2
|
|
* });
|
|
*
|
|
* // get client rect without think off transformations (position, rotation, scale, offset, etc)
|
|
* rect.getClientRect({ skipTransform: true});
|
|
* // returns {
|
|
* // x : -2, // two pixels for stroke / 2
|
|
* // y : -2,
|
|
* // width : 104, // increased by 4 for stroke
|
|
* // height : 104
|
|
* //}
|
|
*
|
|
* // get client rect with transformation applied
|
|
* rect.getClientRect();
|
|
* // returns Object {x: -2, y: 46, width: 104, height: 208}
|
|
*/
|
|
getClientRect(config) {
|
|
// abstract method
|
|
// redefine in Container and Shape
|
|
throw new Error('abstract "getClientRect" method call');
|
|
}
|
|
_getCachedSceneRect(config) {
|
|
const cache = config._forDrawing && !this._isUnderCache && this._getCanvasCache();
|
|
if (!cache)
|
|
return;
|
|
const rect = {
|
|
x: cache.x,
|
|
y: cache.y,
|
|
width: cache.scene.width / cache.scene.pixelRatio,
|
|
height: cache.scene.height / cache.scene.pixelRatio,
|
|
};
|
|
return config.skipTransform
|
|
? rect
|
|
: this._transformedRect(rect, config.relativeTo);
|
|
}
|
|
_transformedRect(rect, top) {
|
|
return this.getAbsoluteTransform(top)._getTransformedRect(rect);
|
|
}
|
|
_drawCachedSceneCanvas(context) {
|
|
context.save();
|
|
context._applyOpacity(this);
|
|
context._applyGlobalCompositeOperation(this);
|
|
const canvasCache = this._getCanvasCache();
|
|
context.translate(canvasCache.x, canvasCache.y);
|
|
const cacheCanvas = this._getCachedSceneCanvas();
|
|
const ratio = cacheCanvas.pixelRatio;
|
|
context.drawImage(cacheCanvas._canvas, 0, 0, cacheCanvas.width / ratio, cacheCanvas.height / ratio);
|
|
context.restore();
|
|
}
|
|
_drawCachedHitCanvas(context, hitCanvas) {
|
|
const canvasCache = this._getCanvasCache();
|
|
context.save();
|
|
context.translate(canvasCache.x, canvasCache.y);
|
|
context.drawImage(hitCanvas._canvas, 0, 0, hitCanvas.width / hitCanvas.pixelRatio, hitCanvas.height / hitCanvas.pixelRatio);
|
|
context.restore();
|
|
}
|
|
_getCachedSceneCanvas() {
|
|
let filters = this.filters(), cachedCanvas = this._getCanvasCache(), sceneCanvas = cachedCanvas.scene, filterCanvas = cachedCanvas.filter, filterContext = filterCanvas.getContext(), len, imageData, n, filter;
|
|
if (!filters || filters.length === 0) {
|
|
return sceneCanvas;
|
|
}
|
|
if (this._filterUpToDate) {
|
|
return filterCanvas;
|
|
}
|
|
let useNativeOnly = true;
|
|
for (let i = 0; i < filters.length; i++) {
|
|
if (typeof filters[i] !== 'string' || !isCSSFiltersSupported()) {
|
|
useNativeOnly = false;
|
|
break;
|
|
}
|
|
}
|
|
const ratio = sceneCanvas.pixelRatio;
|
|
filterCanvas.setSizeIfChanged(sceneCanvas._logicalWidth, sceneCanvas._logicalHeight);
|
|
if (useNativeOnly) {
|
|
// Canvas filters use backing-pixel coordinates, independent of the CTM.
|
|
// Scale length tokens, leaving colors, percentages and URL references alone.
|
|
const finalFilter = filters
|
|
.join(' ')
|
|
.replace(/url\((?:[^()"']|"[^"]*"|'[^']*')*\)|"[^"]*"|'[^']*'|([-+]?(?:\d*\.)?\d+(?:e[-+]?\d+)?)(px|em|rem|ex|ch|cap|ic|lh|rlh|vw|vh|vmin|vmax|cm|mm|q|in|pt|pc)\b/gi, (token, value, unit) => value === undefined ? token : `${Number(value) * ratio}${unit}`);
|
|
filterContext.clear();
|
|
filterContext.save();
|
|
filterContext.setAttr('filter', finalFilter);
|
|
filterContext.drawImage(sceneCanvas._canvas, 0, 0, sceneCanvas.getWidth() / ratio, sceneCanvas.getHeight() / ratio);
|
|
filterContext.restore();
|
|
this._filterUpToDate = true;
|
|
return filterCanvas;
|
|
}
|
|
try {
|
|
len = filters.length;
|
|
filterContext.clear();
|
|
// copy cached canvas onto filter context
|
|
filterContext.drawImage(sceneCanvas._canvas, 0, 0, sceneCanvas.getWidth() / ratio, sceneCanvas.getHeight() / ratio);
|
|
imageData = filterContext.getImageData(0, 0, filterCanvas.getWidth(), filterCanvas.getHeight());
|
|
// apply filters to filter context
|
|
for (n = 0; n < len; n++) {
|
|
filter = filters[n];
|
|
if (typeof filter === 'string') {
|
|
filter = parseCSSFilters(filter);
|
|
}
|
|
filter.call(this, imageData, ratio);
|
|
}
|
|
filterContext.putImageData(imageData, 0, 0);
|
|
}
|
|
catch (e) {
|
|
Util.error('Unable to apply filter. ' +
|
|
e.message +
|
|
' This post my help you https://konvajs.org/docs/posts/Tainted_Canvas.html.');
|
|
}
|
|
this._filterUpToDate = true;
|
|
return filterCanvas;
|
|
}
|
|
/**
|
|
* bind events to the node. KonvaJS supports mouseover, mousemove,
|
|
* mouseout, mouseenter, mouseleave, mousedown, mouseup, wheel, contextmenu, click, dblclick, touchstart, touchmove,
|
|
* touchend, tap, dbltap, dragstart, dragmove, dragend and destroy events.
|
|
* Pass in a string of events delimited by a space to bind multiple events at once
|
|
* such as 'mousedown mouseup mousemove'. Include a namespace to bind an
|
|
* event by name such as 'click.foobar'.
|
|
* @method
|
|
* @name Konva.Node#on
|
|
* @param {String} evtStr e.g. 'click', 'mousedown touchstart', 'mousedown.foo touchstart.foo'
|
|
* @param {Function} handler The handler function. The first argument of that function is event object. Event object has `target` as main target of the event, `currentTarget` as current node listener and `evt` as native browser event.
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // add click listener
|
|
* node.on('click', function() {
|
|
* console.log('you clicked me!');
|
|
* });
|
|
*
|
|
* // get the target node
|
|
* node.on('click', function(evt) {
|
|
* console.log(evt.target);
|
|
* });
|
|
*
|
|
* // stop event propagation
|
|
* node.on('click', function(evt) {
|
|
* evt.cancelBubble = true;
|
|
* });
|
|
*
|
|
* // bind multiple listeners
|
|
* node.on('click touchstart', function() {
|
|
* console.log('you clicked/touched me!');
|
|
* });
|
|
*
|
|
* // namespace listener
|
|
* node.on('click.foo', function() {
|
|
* console.log('you clicked/touched me!');
|
|
* });
|
|
*
|
|
* // get the event type
|
|
* node.on('click tap', function(evt) {
|
|
* var eventType = evt.type;
|
|
* });
|
|
*
|
|
* // get native event object
|
|
* node.on('click tap', function(evt) {
|
|
* var nativeEvent = evt.evt;
|
|
* });
|
|
*
|
|
* // for change events, get the old and new val
|
|
* node.on('xChange', function(evt) {
|
|
* var oldVal = evt.oldVal;
|
|
* var newVal = evt.newVal;
|
|
* });
|
|
*
|
|
* // get event targets
|
|
* // with event delegations
|
|
* layer.on('click', 'Group', function(evt) {
|
|
* var shape = evt.target;
|
|
* var group = evt.currentTarget;
|
|
* });
|
|
*/
|
|
on(...args) {
|
|
this._prepareListeners();
|
|
const evtStr = args[0];
|
|
const selectorOrHandler = args[1];
|
|
const handler = args[2];
|
|
if (args.length === 3) {
|
|
return this._delegate.apply(this, args);
|
|
}
|
|
const events = evtStr.split(SPACE);
|
|
/*
|
|
* loop through types and attach event listeners to
|
|
* each one. eg. 'click mouseover.namespace mouseout'
|
|
* will create three event bindings
|
|
*/
|
|
for (let n = 0; n < events.length; n++) {
|
|
const event = events[n];
|
|
const parts = event.split('.');
|
|
const baseEvent = parts[0];
|
|
const name = parts[1] || '';
|
|
// create events array if it doesn't exist
|
|
if (!this.eventListeners[baseEvent]) {
|
|
this.eventListeners[baseEvent] = [];
|
|
}
|
|
this.eventListeners[baseEvent].push({ name, handler: selectorOrHandler });
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* remove event bindings from the node. Pass in a string of
|
|
* event types delimmited by a space to remove multiple event
|
|
* bindings at once such as 'mousedown mouseup mousemove'.
|
|
* include a namespace to remove an event binding by name
|
|
* such as 'click.foobar'. If you only give a name like '.foobar',
|
|
* all events in that namespace will be removed.
|
|
* @method
|
|
* @name Konva.Node#off
|
|
* @param {String} evtStr e.g. 'click', 'mousedown touchstart', '.foobar'
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // remove listener
|
|
* node.off('click');
|
|
*
|
|
* // remove multiple listeners
|
|
* node.off('click touchstart');
|
|
*
|
|
* // remove listener by name
|
|
* node.off('click.foo');
|
|
*/
|
|
// listeners added to a prototype go into its own map, not the parent's,
|
|
// and the lists flattened from the prototype chain so far are stale
|
|
_prepareListeners() {
|
|
if (this === this.constructor.prototype) {
|
|
if (!this.hasOwnProperty('eventListeners')) {
|
|
this.eventListeners = {};
|
|
}
|
|
Node.protoListenerMap = new WeakMap();
|
|
}
|
|
}
|
|
off(evtStr, callback) {
|
|
this._prepareListeners();
|
|
let events = (evtStr || '').split(SPACE), len = events.length, n, t, event, parts, baseEvent, name;
|
|
if (!evtStr) {
|
|
// remove all events
|
|
for (t in this.eventListeners) {
|
|
this._off(t);
|
|
}
|
|
}
|
|
for (n = 0; n < len; n++) {
|
|
event = events[n];
|
|
parts = event.split('.');
|
|
baseEvent = parts[0];
|
|
name = parts[1];
|
|
if (baseEvent) {
|
|
if (this.eventListeners[baseEvent]) {
|
|
this._off(baseEvent, name, callback);
|
|
}
|
|
}
|
|
else {
|
|
for (t in this.eventListeners) {
|
|
this._off(t, name, callback);
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
// some event aliases for third party integration like HammerJS
|
|
dispatchEvent(evt) {
|
|
const e = {
|
|
target: this,
|
|
type: evt.type,
|
|
evt: evt,
|
|
};
|
|
this.fire(evt.type, e);
|
|
return this;
|
|
}
|
|
addEventListener(type, handler) {
|
|
// we have to pass native event to handler
|
|
this.on(type, function (evt) {
|
|
handler.call(this, evt.evt);
|
|
});
|
|
return this;
|
|
}
|
|
removeEventListener(type) {
|
|
this.off(type);
|
|
return this;
|
|
}
|
|
// like node.on
|
|
_delegate(event, selector, handler) {
|
|
const stopNode = this;
|
|
this.on(event, function (evt) {
|
|
const targets = evt.target.findAncestors(selector, true, stopNode);
|
|
for (let i = 0; i < targets.length; i++) {
|
|
evt = Util.cloneObject(evt);
|
|
evt.currentTarget = targets[i];
|
|
handler.call(targets[i], evt);
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* remove a node from parent, but don't destroy. You can reuse the node later.
|
|
* @method
|
|
* @name Konva.Node#remove
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* node.remove();
|
|
*/
|
|
remove() {
|
|
if (this.isDragging()) {
|
|
this.stopDrag();
|
|
}
|
|
// we can have drag element but that is not dragged yet
|
|
// so just clear it
|
|
DD._dragElements.delete(this._id);
|
|
// also remove any dragging descendants, otherwise they'll
|
|
// have a broken parent chain and crash on getStage()
|
|
DD._dragElements.forEach((elem, key) => {
|
|
if (this.isAncestorOf(elem.node)) {
|
|
DD._dragElements.delete(key);
|
|
}
|
|
});
|
|
this._remove();
|
|
return this;
|
|
}
|
|
_clearCaches() {
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
|
|
this._clearSelfAndDescendantCache(STAGE);
|
|
this._clearSelfAndDescendantCache(VISIBLE);
|
|
this._clearSelfAndDescendantCache(LISTENING);
|
|
}
|
|
_remove() {
|
|
const parent = this.getParent();
|
|
if (parent && parent.children) {
|
|
parent.children.splice(this.index, 1);
|
|
parent._setChildrenIndices();
|
|
this.parent = null;
|
|
}
|
|
// every cached attr that is calculated via node tree
|
|
// traversal must be cleared when removing a node
|
|
this._clearCaches();
|
|
}
|
|
/**
|
|
* remove and destroy a node. Kill it and delete forever! You should not reuse node after destroy().
|
|
* If the node is a container (Group, Stage or Layer) it will destroy all children too.
|
|
* The node fires a `destroy` event first, so anything holding a reference to it can let go.
|
|
* When an ancestor is being destroyed the node is already detached from its parent at that point.
|
|
* @method
|
|
* @name Konva.Node#destroy
|
|
* @example
|
|
* node.on('destroy', () => console.log('gone'));
|
|
* node.destroy();
|
|
*/
|
|
destroy() {
|
|
this._fire('destroy', { target: this });
|
|
PointerEvents.releaseCapturesOf(this);
|
|
this.remove();
|
|
this.clearCache();
|
|
return this;
|
|
}
|
|
/**
|
|
* get attr
|
|
* @method
|
|
* @name Konva.Node#getAttr
|
|
* @param {String} attr
|
|
* @returns {Integer|String|Object|Array}
|
|
* @example
|
|
* var x = node.getAttr('x');
|
|
*/
|
|
getAttr(attr) {
|
|
const method = 'get' + Util._capitalize(attr);
|
|
if (Util._isFunction(this[method])) {
|
|
return this[method]();
|
|
}
|
|
// otherwise get directly
|
|
return this.attrs[attr];
|
|
}
|
|
/**
|
|
* get ancestors
|
|
* @method
|
|
* @name Konva.Node#getAncestors
|
|
* @returns {Array}
|
|
* @example
|
|
* shape.getAncestors().forEach(function(node) {
|
|
* console.log(node.id());
|
|
* })
|
|
*/
|
|
getAncestors() {
|
|
let parent = this.getParent(), ancestors = [];
|
|
while (parent) {
|
|
ancestors.push(parent);
|
|
parent = parent.getParent();
|
|
}
|
|
return ancestors;
|
|
}
|
|
/**
|
|
* get attrs object literal
|
|
* @method
|
|
* @name Konva.Node#getAttrs
|
|
* @returns {Object}
|
|
*/
|
|
getAttrs() {
|
|
return this.attrs || {};
|
|
}
|
|
/**
|
|
* set multiple attrs at once using an object literal
|
|
* @method
|
|
* @name Konva.Node#setAttrs
|
|
* @param {Object} config object containing key value pairs
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* node.setAttrs({
|
|
* x: 5,
|
|
* fill: 'red'
|
|
* });
|
|
*/
|
|
setAttrs(config) {
|
|
this._batchTransformChanges(() => {
|
|
let key, method;
|
|
if (!config) {
|
|
return this;
|
|
}
|
|
for (key in config) {
|
|
if (key === CHILDREN) {
|
|
continue;
|
|
}
|
|
method = SET + Util._capitalize(key);
|
|
// use setter if available
|
|
if (Util._isFunction(this[method])) {
|
|
this[method](config[key]);
|
|
}
|
|
else {
|
|
// otherwise set directly
|
|
this._setAttr(key, config[key]);
|
|
}
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* determine if node is listening for events by taking into account ancestors.
|
|
*
|
|
* Parent | Self | isListening
|
|
* listening | listening |
|
|
* ----------+-----------+------------
|
|
* T | T | T
|
|
* T | F | F
|
|
* F | T | F
|
|
* F | F | F
|
|
*
|
|
* @method
|
|
* @name Konva.Node#isListening
|
|
* @returns {Boolean}
|
|
*/
|
|
isListening() {
|
|
return this._getCache(LISTENING, this._isListening);
|
|
}
|
|
_isListening(relativeTo) {
|
|
const listening = this.listening();
|
|
if (!listening) {
|
|
return false;
|
|
}
|
|
const parent = this.getParent();
|
|
if (parent && parent !== relativeTo && this !== relativeTo) {
|
|
return parent._isListening(relativeTo);
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* determine if node is visible by taking into account ancestors.
|
|
*
|
|
* Parent | Self | isVisible
|
|
* visible | visible |
|
|
* ----------+-----------+------------
|
|
* T | T | T
|
|
* T | F | F
|
|
* F | T | F
|
|
* F | F | F
|
|
* @method
|
|
* @name Konva.Node#isVisible
|
|
* @returns {Boolean}
|
|
*/
|
|
isVisible() {
|
|
return this._getCache(VISIBLE, this._isVisible);
|
|
}
|
|
_isVisible(relativeTo) {
|
|
const visible = this.visible();
|
|
if (!visible) {
|
|
return false;
|
|
}
|
|
const parent = this.getParent();
|
|
if (parent && parent !== relativeTo && this !== relativeTo) {
|
|
return parent._isVisible(relativeTo);
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
shouldDrawHit(top) {
|
|
if (top) {
|
|
return this._isVisible(top) && this._isListening(top);
|
|
}
|
|
return this.isListening() && this.isVisible();
|
|
}
|
|
/**
|
|
* show node. set visible = true
|
|
* @method
|
|
* @name Konva.Node#show
|
|
* @returns {Konva.Node}
|
|
*/
|
|
show() {
|
|
this.visible(true);
|
|
return this;
|
|
}
|
|
/**
|
|
* hide node. Hidden nodes are no longer detectable
|
|
* @method
|
|
* @name Konva.Node#hide
|
|
* @returns {Konva.Node}
|
|
*/
|
|
hide() {
|
|
this.visible(false);
|
|
return this;
|
|
}
|
|
getZIndex() {
|
|
return this.index || 0;
|
|
}
|
|
/**
|
|
* get absolute z-index which takes into account sibling
|
|
* and ancestor indices
|
|
* @method
|
|
* @name Konva.Node#getAbsoluteZIndex
|
|
* @returns {Integer}
|
|
*/
|
|
getAbsoluteZIndex() {
|
|
let depth = this.getDepth(), that = this, index = 0, nodes, len, n, child;
|
|
function addChildren(children) {
|
|
nodes = [];
|
|
len = children.length;
|
|
for (n = 0; n < len; n++) {
|
|
child = children[n];
|
|
index++;
|
|
if (child.nodeType !== SHAPE) {
|
|
nodes = nodes.concat(child.children);
|
|
}
|
|
if (child._id === that._id) {
|
|
n = len;
|
|
}
|
|
}
|
|
if (nodes.length > 0 && nodes[0].getDepth() <= depth) {
|
|
addChildren(nodes);
|
|
}
|
|
}
|
|
const stage = this.getStage();
|
|
if (that.nodeType !== UPPER_STAGE && stage) {
|
|
addChildren(stage.children);
|
|
}
|
|
return index;
|
|
}
|
|
/**
|
|
* get node depth in node tree. Returns an integer.
|
|
* e.g. Stage depth will always be 0. Layers will always be 1. Groups and Shapes will always
|
|
* be >= 2
|
|
* @method
|
|
* @name Konva.Node#getDepth
|
|
* @returns {Integer}
|
|
* @example
|
|
* var depth = shape.getDepth(); // 2 for a shape right inside of a layer
|
|
*/
|
|
getDepth() {
|
|
let depth = 0, parent = this.parent;
|
|
while (parent) {
|
|
depth++;
|
|
parent = parent.parent;
|
|
}
|
|
return depth;
|
|
}
|
|
// sometimes we do several attributes changes
|
|
// like node.position(pos)
|
|
// for performance reasons, lets batch transform reset
|
|
// so it work faster
|
|
_batchTransformChanges(func) {
|
|
if (this._batchingTransformChange) {
|
|
func();
|
|
return;
|
|
}
|
|
Node._updateBatch(() => {
|
|
this._batchingTransformChange = true;
|
|
try {
|
|
func();
|
|
}
|
|
finally {
|
|
this._batchingTransformChange = false;
|
|
if (this._needClearTransformCache) {
|
|
this._needClearTransformCache = false;
|
|
this._clearCache(TRANSFORM);
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
setPosition(pos) {
|
|
this._batchTransformChanges(() => {
|
|
this.x(pos.x);
|
|
this.y(pos.y);
|
|
});
|
|
return this;
|
|
}
|
|
getPosition() {
|
|
return {
|
|
x: this.x(),
|
|
y: this.y(),
|
|
};
|
|
}
|
|
/**
|
|
* get position of first pointer (like mouse or first touch) relative to local coordinates of current node
|
|
* @method
|
|
* @name Konva.Node#getRelativePointerPosition
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
*
|
|
* // let's think we have a rectangle at position x = 10, y = 10
|
|
* // now we clicked at x = 15, y = 15 of the stage
|
|
* // if you want to know position of the click, related to the rectangle you can use
|
|
* rect.getRelativePointerPosition();
|
|
*/
|
|
getRelativePointerPosition() {
|
|
const stage = this.getStage();
|
|
if (!stage) {
|
|
return null;
|
|
}
|
|
// get pointer (say mouse or touch) position
|
|
const pos = stage.getPointerPosition();
|
|
if (!pos) {
|
|
return null;
|
|
}
|
|
const transform = this.getAbsoluteTransform().copy();
|
|
// to detect relative position we need to invert transform
|
|
transform.invert();
|
|
// now we can find relative point
|
|
return transform.point(pos);
|
|
}
|
|
/**
|
|
* get absolute position of a node. That function can be used to calculate absolute position, but relative to any ancestor
|
|
* @method
|
|
* @name Konva.Node#getAbsolutePosition
|
|
* @param {Object} [top] optional ancestor node
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
*
|
|
* // returns absolute position relative to top-left corner of canvas
|
|
* node.getAbsolutePosition();
|
|
*
|
|
* // calculate absolute position of node, inside stage
|
|
* // so stage transforms are ignored
|
|
* node.getAbsolutePosition(stage)
|
|
*/
|
|
getAbsolutePosition(top) {
|
|
const absoluteMatrix = this.getAbsoluteTransform(top).getMatrix(), absoluteTransform = new Transform(), offset = this.offset();
|
|
// clone the matrix array
|
|
absoluteTransform.m = absoluteMatrix.slice();
|
|
absoluteTransform.translate(offset.x, offset.y);
|
|
return absoluteTransform.getTranslation();
|
|
}
|
|
setAbsolutePosition(pos) {
|
|
const { x, y, ...origTrans } = this._clearTransform();
|
|
// don't clear translation
|
|
this.attrs.x = x;
|
|
this.attrs.y = y;
|
|
// important, use non cached value
|
|
this._clearCache(TRANSFORM);
|
|
const it = this._getAbsoluteTransform().copy();
|
|
const invertible = it.isInvertible();
|
|
it.invert();
|
|
it.translate(pos.x, pos.y);
|
|
const { x: dx, y: dy } = it.getTranslation();
|
|
this._setTransform(origTrans);
|
|
if (invertible) {
|
|
this.setPosition({ x: this.attrs.x + dx, y: this.attrs.y + dy });
|
|
}
|
|
else {
|
|
Util.warn('Cannot set the absolute position: the absolute transform is not invertible (an ancestor has a zero scale).');
|
|
}
|
|
this._clearCache(TRANSFORM);
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
|
|
return this;
|
|
}
|
|
_setTransform(trans) {
|
|
let key;
|
|
for (key in trans) {
|
|
this.attrs[key] = trans[key];
|
|
}
|
|
}
|
|
_clearTransform() {
|
|
const trans = {
|
|
x: this.x(),
|
|
y: this.y(),
|
|
rotation: this.rotation(),
|
|
scaleX: this.scaleX(),
|
|
scaleY: this.scaleY(),
|
|
offsetX: this.offsetX(),
|
|
offsetY: this.offsetY(),
|
|
skewX: this.skewX(),
|
|
skewY: this.skewY(),
|
|
};
|
|
this.attrs.x = 0;
|
|
this.attrs.y = 0;
|
|
this.attrs.rotation = 0;
|
|
this.attrs.scaleX = 1;
|
|
this.attrs.scaleY = 1;
|
|
this.attrs.offsetX = 0;
|
|
this.attrs.offsetY = 0;
|
|
this.attrs.skewX = 0;
|
|
this.attrs.skewY = 0;
|
|
// return original transform
|
|
return trans;
|
|
}
|
|
/**
|
|
* move node by an amount relative to its current position
|
|
* @method
|
|
* @name Konva.Node#move
|
|
* @param {Object} change
|
|
* @param {Number} change.x
|
|
* @param {Number} change.y
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // move node in x direction by 1px and y direction by 2px
|
|
* node.move({
|
|
* x: 1,
|
|
* y: 2
|
|
* });
|
|
*/
|
|
move(change) {
|
|
let changeX = change.x, changeY = change.y, x = this.x(), y = this.y();
|
|
if (changeX !== undefined) {
|
|
x += changeX;
|
|
}
|
|
if (changeY !== undefined) {
|
|
y += changeY;
|
|
}
|
|
this.setPosition({ x: x, y: y });
|
|
return this;
|
|
}
|
|
/**
|
|
* rotate node by an amount in degrees relative to its current rotation
|
|
* @method
|
|
* @name Konva.Node#rotate
|
|
* @param {Number} theta
|
|
* @returns {Konva.Node}
|
|
*/
|
|
rotate(theta) {
|
|
this.rotation(this.rotation() + theta);
|
|
return this;
|
|
}
|
|
/**
|
|
* move node to the top of its siblings
|
|
* @method
|
|
* @name Konva.Node#moveToTop
|
|
* @returns {Boolean}
|
|
*/
|
|
moveToTop() {
|
|
if (!this.parent) {
|
|
Util.warn('Node has no parent. moveToTop function is ignored.');
|
|
return false;
|
|
}
|
|
const index = this.index, len = this.parent.children.length;
|
|
if (index < len - 1) {
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.push(this);
|
|
this.parent._setChildrenIndices();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* move node up
|
|
* @method
|
|
* @name Konva.Node#moveUp
|
|
* @returns {Boolean} flag is moved or not
|
|
*/
|
|
moveUp() {
|
|
if (!this.parent) {
|
|
Util.warn('Node has no parent. moveUp function is ignored.');
|
|
return false;
|
|
}
|
|
const index = this.index, len = this.parent.children.length;
|
|
if (index < len - 1) {
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.splice(index + 1, 0, this);
|
|
this.parent._setChildrenIndices();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* move node down
|
|
* @method
|
|
* @name Konva.Node#moveDown
|
|
* @returns {Boolean}
|
|
*/
|
|
moveDown() {
|
|
if (!this.parent) {
|
|
Util.warn('Node has no parent. moveDown function is ignored.');
|
|
return false;
|
|
}
|
|
const index = this.index;
|
|
if (index > 0) {
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.splice(index - 1, 0, this);
|
|
this.parent._setChildrenIndices();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* move node to the bottom of its siblings
|
|
* @method
|
|
* @name Konva.Node#moveToBottom
|
|
* @returns {Boolean}
|
|
*/
|
|
moveToBottom() {
|
|
if (!this.parent) {
|
|
Util.warn('Node has no parent. moveToBottom function is ignored.');
|
|
return false;
|
|
}
|
|
const index = this.index;
|
|
if (index > 0) {
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.unshift(this);
|
|
this.parent._setChildrenIndices();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
setZIndex(zIndex) {
|
|
if (!this.parent) {
|
|
Util.warn('Node has no parent. zIndex parameter is ignored.');
|
|
return this;
|
|
}
|
|
if (zIndex < 0 || zIndex >= this.parent.children.length) {
|
|
Util.warn('Unexpected value ' +
|
|
zIndex +
|
|
' for zIndex property. zIndex is just index of a node in children of its parent. Expected value is from 0 to ' +
|
|
(this.parent.children.length - 1) +
|
|
'.');
|
|
}
|
|
const index = this.index;
|
|
this.parent.children.splice(index, 1);
|
|
this.parent.children.splice(zIndex, 0, this);
|
|
this.parent._setChildrenIndices();
|
|
return this;
|
|
}
|
|
/**
|
|
* get absolute opacity
|
|
* @method
|
|
* @name Konva.Node#getAbsoluteOpacity
|
|
* @returns {Number}
|
|
*/
|
|
getAbsoluteOpacity() {
|
|
return this._getCache(ABSOLUTE_OPACITY, this._getAbsoluteOpacity);
|
|
}
|
|
_getAbsoluteOpacity() {
|
|
let absOpacity = this.opacity();
|
|
const parent = this.getParent();
|
|
if (parent && !parent._isUnderCache) {
|
|
absOpacity *= parent.getAbsoluteOpacity();
|
|
}
|
|
return absOpacity;
|
|
}
|
|
/**
|
|
* move node to another container
|
|
* @method
|
|
* @name Konva.Node#moveTo
|
|
* @param {Container} newContainer
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // move node from current layer into layer2
|
|
* node.moveTo(layer2);
|
|
*/
|
|
moveTo(newContainer) {
|
|
// do nothing if new container is already parent
|
|
if (this.getParent() !== newContainer) {
|
|
this._remove();
|
|
newContainer.add(this);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* convert Node into an object for serialization. Returns an object.
|
|
* @method
|
|
* @name Konva.Node#toObject
|
|
* @returns {Object}
|
|
*/
|
|
toObject() {
|
|
var _a;
|
|
const attrs = this.getAttrs();
|
|
let key, val, nonPlainObject;
|
|
const obj = {
|
|
attrs: {},
|
|
className: this.getClassName(),
|
|
};
|
|
for (key in attrs) {
|
|
val = attrs[key];
|
|
// JSON has no typed arrays, so typed line points are written as an array
|
|
if (val instanceof TypedArray) {
|
|
val = Array.from(val);
|
|
}
|
|
// if value is object and object is not plain
|
|
// like class instance, we should skip it and to not include
|
|
nonPlainObject =
|
|
Util.isObject(val) && !Util._isPlainObject(val) && !Util._isArray(val);
|
|
if (nonPlainObject) {
|
|
continue;
|
|
}
|
|
// only the getters generated by Factory carry a default, so an explicit
|
|
// value equal to a computed one (Text width, dragDistance) is kept
|
|
// Shared filter parameters can distinguish an explicit value from unset.
|
|
if (((_a = this['get' + Util._capitalize(key)]) === null || _a === void 0 ? void 0 : _a._def) !== val ||
|
|
key === 'brightness' ||
|
|
key === 'threshold') {
|
|
obj.attrs[key] =
|
|
key === 'filters' && Util._isArray(val)
|
|
? val.filter((filter) => typeof filter === 'string')
|
|
: Util.isObject(val)
|
|
? Util._prepareToStringify(val)
|
|
: val;
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
/**
|
|
* convert Node into a JSON string. Returns a JSON string.
|
|
* @method
|
|
* @name Konva.Node#toJSON
|
|
* @returns {String}
|
|
*/
|
|
toJSON() {
|
|
return JSON.stringify(this.toObject());
|
|
}
|
|
/**
|
|
* get parent container
|
|
* @method
|
|
* @name Konva.Node#getParent
|
|
* @returns {Konva.Node}
|
|
*/
|
|
getParent() {
|
|
return this.parent;
|
|
}
|
|
/**
|
|
* get all ancestors (parent then parent of the parent, etc) of the node
|
|
* @method
|
|
* @name Konva.Node#findAncestors
|
|
* @param {String} selector selector for search
|
|
* @param {Boolean} [includeSelf] show we think that node is ancestro itself?
|
|
* @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors)
|
|
* @returns {Array} [ancestors]
|
|
* @example
|
|
* // get one of the parent group
|
|
* var parentGroups = node.findAncestors('Group');
|
|
*/
|
|
findAncestors(selector, includeSelf, stopNode) {
|
|
const res = [];
|
|
if (includeSelf && this._isMatch(selector)) {
|
|
res.push(this);
|
|
}
|
|
let ancestor = this.parent;
|
|
while (ancestor) {
|
|
if (ancestor === stopNode) {
|
|
return res;
|
|
}
|
|
if (ancestor._isMatch(selector)) {
|
|
res.push(ancestor);
|
|
}
|
|
ancestor = ancestor.parent;
|
|
}
|
|
return res;
|
|
}
|
|
isAncestorOf(node) {
|
|
return false;
|
|
}
|
|
/**
|
|
* get ancestor (parent or parent of the parent, etc) of the node that match passed selector
|
|
* @method
|
|
* @name Konva.Node#findAncestor
|
|
* @param {String} selector selector for search
|
|
* @param {Boolean} [includeSelf] show we think that node is ancestro itself?
|
|
* @param {Konva.Node} [stopNode] optional node where we need to stop searching (one of ancestors)
|
|
* @returns {Konva.Node} ancestor
|
|
* @example
|
|
* // get one of the parent group
|
|
* var group = node.findAncestors('.mygroup');
|
|
*/
|
|
findAncestor(selector, includeSelf, stopNode) {
|
|
return this.findAncestors(selector, includeSelf, stopNode)[0];
|
|
}
|
|
// is current node match passed selector?
|
|
_isMatch(selector) {
|
|
if (!selector) {
|
|
return false;
|
|
}
|
|
if (typeof selector === 'function') {
|
|
return selector(this);
|
|
}
|
|
let selectorArr = selector.replace(/ /g, '').split(','), len = selectorArr.length, n, sel;
|
|
for (n = 0; n < len; n++) {
|
|
sel = selectorArr[n];
|
|
if (!Util.isValidSelector(sel)) {
|
|
Util.warn('Selector "' +
|
|
sel +
|
|
'" is invalid. Allowed selectors examples are "#foo", ".bar" or "Group".');
|
|
Util.warn('If you have a custom shape with such className, please change it to start with upper letter like "Triangle".');
|
|
Util.warn('Konva is awesome, right?');
|
|
}
|
|
// id selector
|
|
if (sel.charAt(0) === '#') {
|
|
if (this.id() === sel.slice(1)) {
|
|
return true;
|
|
}
|
|
}
|
|
else if (sel.charAt(0) === '.') {
|
|
// name selector
|
|
if (this.hasName(sel.slice(1))) {
|
|
return true;
|
|
}
|
|
}
|
|
else if (this.className === sel || this.nodeType === sel) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* get layer ancestor
|
|
* @method
|
|
* @name Konva.Node#getLayer
|
|
* @returns {Konva.Layer}
|
|
*/
|
|
getLayer() {
|
|
const parent = this.getParent();
|
|
return parent ? parent.getLayer() : null;
|
|
}
|
|
/**
|
|
* get stage ancestor
|
|
* @method
|
|
* @name Konva.Node#getStage
|
|
* @returns {Konva.Stage}
|
|
*/
|
|
getStage() {
|
|
return this._getCache(STAGE, this._getStage);
|
|
}
|
|
_getStage() {
|
|
const parent = this.getParent();
|
|
if (parent) {
|
|
return parent.getStage();
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* fire event
|
|
* @method
|
|
* @name Konva.Node#fire
|
|
* @param {String} eventType event type. can be a regular event, like click, mouseover, or mouseout, or it can be a custom event, like myCustomEvent
|
|
* @param {Event} [evt] event object
|
|
* @param {Boolean} [bubble] setting the value to false, or leaving it undefined, will result in the event
|
|
* not bubbling. Setting the value to true will result in the event bubbling.
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // manually fire click event
|
|
* node.fire('click');
|
|
*
|
|
* // fire custom event
|
|
* node.fire('foo');
|
|
*
|
|
* // fire custom event with custom event object
|
|
* node.fire('foo', {
|
|
* bar: 10
|
|
* });
|
|
*
|
|
* // fire click event that bubbles
|
|
* node.fire('click', null, true);
|
|
*/
|
|
fire(eventType, evt, bubble) {
|
|
evt = evt || {};
|
|
evt.target = evt.target || this;
|
|
// bubble
|
|
if (bubble) {
|
|
this._fireAndBubble(eventType, evt);
|
|
}
|
|
else {
|
|
// no bubble
|
|
this._fire(eventType, evt);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* get absolute transform of the node which takes into
|
|
* account its ancestor transforms
|
|
* @method
|
|
* @name Konva.Node#getAbsoluteTransform
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
getAbsoluteTransform(top) {
|
|
// relative to an ancestor, or under a cached one, the result can't come
|
|
// from the cache: a cached container keeps the transform caches of its
|
|
// descendants while it moves (see Container._clearSelfAndDescendantCache)
|
|
if (top || this._hasCachedAncestor()) {
|
|
return this._getAbsoluteTransform(top);
|
|
}
|
|
return this._getCache(ABSOLUTE_TRANSFORM, this._getCachedAbsoluteTransform);
|
|
}
|
|
_hasCachedAncestor() {
|
|
let parent = this.parent;
|
|
while (parent) {
|
|
if (parent.isCached()) {
|
|
return true;
|
|
}
|
|
parent = parent.parent;
|
|
}
|
|
return false;
|
|
}
|
|
// relative to `top`: the transforms of `top` and of its ancestors are
|
|
// left out. Goes through the parent, so an ancestor that redefines its
|
|
// absolute transform (Konva.Transformer) is honoured
|
|
_getAbsoluteTransform(top) {
|
|
const at = new Transform();
|
|
if (this === top) {
|
|
return at;
|
|
}
|
|
if (this.parent && this.parent !== top) {
|
|
this.parent.getAbsoluteTransform(top).copyInto(at);
|
|
}
|
|
return this._multiplyOwnTransform(at);
|
|
}
|
|
// the cached transform of the parent, then the own one
|
|
_getCachedAbsoluteTransform() {
|
|
const at = this._cache[ABSOLUTE_TRANSFORM] || new Transform();
|
|
if (this.parent) {
|
|
this.parent.getAbsoluteTransform().copyInto(at);
|
|
}
|
|
else {
|
|
at.reset();
|
|
}
|
|
this._multiplyOwnTransform(at);
|
|
at.dirty = false;
|
|
return at;
|
|
}
|
|
_multiplyOwnTransform(at) {
|
|
const transformsEnabled = this.transformsEnabled();
|
|
if (transformsEnabled === 'all') {
|
|
at.multiply(this.getTransform());
|
|
}
|
|
else if (transformsEnabled === 'position') {
|
|
// use "attrs" directly, because it is a bit faster
|
|
const x = this.attrs.x || 0;
|
|
const y = this.attrs.y || 0;
|
|
const offsetX = this.attrs.offsetX || 0;
|
|
const offsetY = this.attrs.offsetY || 0;
|
|
at.translate(x - offsetX, y - offsetY);
|
|
}
|
|
return at;
|
|
}
|
|
/**
|
|
* get absolute scale of the node which takes into
|
|
* account its ancestor scales
|
|
* @method
|
|
* @name Konva.Node#getAbsoluteScale
|
|
* @returns {Object}
|
|
* @example
|
|
* // get absolute scale x
|
|
* var scaleX = node.getAbsoluteScale().x;
|
|
*/
|
|
getAbsoluteScale(top) {
|
|
// do not cache this calculations,
|
|
// because it use cache transform
|
|
// this is special logic for caching with some shapes with shadow
|
|
let parent = this;
|
|
while (parent) {
|
|
if (parent._isUnderCache) {
|
|
top = parent;
|
|
}
|
|
parent = parent.getParent();
|
|
}
|
|
const transform = this.getAbsoluteTransform(top);
|
|
const attrs = transform.decompose();
|
|
return {
|
|
x: attrs.scaleX,
|
|
y: attrs.scaleY,
|
|
};
|
|
}
|
|
/**
|
|
* get absolute rotation of the node which takes into
|
|
* account its ancestor rotations
|
|
* @method
|
|
* @name Konva.Node#getAbsoluteRotation
|
|
* @returns {Number}
|
|
* @example
|
|
* // get absolute rotation
|
|
* var rotation = node.getAbsoluteRotation();
|
|
*/
|
|
getAbsoluteRotation() {
|
|
return this.getAbsoluteTransform().decompose().rotation;
|
|
}
|
|
/**
|
|
* get transform of the node
|
|
* @method
|
|
* @name Konva.Node#getTransform
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
getTransform() {
|
|
return this._getCache(TRANSFORM, this._getTransform);
|
|
}
|
|
_getTransform() {
|
|
var _a, _b;
|
|
const m = this._cache[TRANSFORM] || new Transform();
|
|
m.reset();
|
|
// I was trying to use attributes directly here
|
|
// but it doesn't work for Transformer well
|
|
// because it overwrite x,y getters
|
|
const x = this.x(), y = this.y(), rotation = Konva.getAngle(this.rotation()), scaleX = (_a = this.attrs.scaleX) !== null && _a !== void 0 ? _a : 1, scaleY = (_b = this.attrs.scaleY) !== null && _b !== void 0 ? _b : 1, skewX = this.attrs.skewX || 0, skewY = this.attrs.skewY || 0, offsetX = this.attrs.offsetX || 0, offsetY = this.attrs.offsetY || 0;
|
|
if (x !== 0 || y !== 0) {
|
|
m.translate(x, y);
|
|
}
|
|
if (rotation !== 0) {
|
|
m.rotate(rotation);
|
|
}
|
|
if (skewX !== 0 || skewY !== 0) {
|
|
m.skew(skewX, skewY);
|
|
}
|
|
if (scaleX !== 1 || scaleY !== 1) {
|
|
m.scale(scaleX, scaleY);
|
|
}
|
|
if (offsetX !== 0 || offsetY !== 0) {
|
|
m.translate(-1 * offsetX, -1 * offsetY);
|
|
}
|
|
m.dirty = false;
|
|
return m;
|
|
}
|
|
/**
|
|
* clone node. Returns a new Node instance with identical attributes. You can also override
|
|
* the node properties with an object literal, enabling you to use an existing node as a template
|
|
* for another node
|
|
* @method
|
|
* @name Konva.Node#clone
|
|
* @param {Object} obj override attrs
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* // simple clone
|
|
* var clone = node.clone();
|
|
*
|
|
* // clone a node and override the x position
|
|
* var clone = rect.clone({
|
|
* x: 5
|
|
* });
|
|
*/
|
|
clone(obj) {
|
|
// instantiate new node
|
|
let attrs = Util.cloneObject(this.attrs), key, allListeners, len, n, listener;
|
|
// apply attr overrides
|
|
for (key in obj) {
|
|
attrs[key] = obj[key];
|
|
}
|
|
const node = new this.constructor(attrs);
|
|
// copy over listeners
|
|
for (key in this.eventListeners) {
|
|
allListeners = this.eventListeners[key];
|
|
len = allListeners.length;
|
|
for (n = 0; n < len; n++) {
|
|
listener = allListeners[n];
|
|
/*
|
|
* don't include konva namespaced listeners because
|
|
* these are generated by the constructors
|
|
*/
|
|
if (listener.name.indexOf(KONVA) < 0) {
|
|
// if listeners array doesn't exist, then create it
|
|
if (!node.eventListeners[key]) {
|
|
node.eventListeners[key] = [];
|
|
}
|
|
node.eventListeners[key].push(listener);
|
|
}
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
_toKonvaCanvas(config) {
|
|
config = config || {};
|
|
// the client rect is only needed for the parts of the box not configured
|
|
const needsBox = config.x === undefined ||
|
|
config.y === undefined ||
|
|
!config.width ||
|
|
!config.height;
|
|
const box = needsBox
|
|
? this.getClientRect()
|
|
: { x: 0, y: 0, width: 0, height: 0 };
|
|
if (!isFinite(box.x + box.y + box.width + box.height)) {
|
|
Util.error(`Cannot find the bounds of the node to export, its client rect is ${box.x}, ${box.y}, ${box.width}x${box.height}. Check its position, size and points.`);
|
|
}
|
|
const stage = this.getStage(), x = config.x !== undefined ? config.x : Math.floor(box.x), y = config.y !== undefined ? config.y : Math.floor(box.y), pixelRatio = config.pixelRatio || 1, canvas = new SceneCanvas({
|
|
width: config.width ||
|
|
Math.max(0, Math.ceil(box.x + box.width - x)) ||
|
|
(stage ? stage.width() : 0),
|
|
height: config.height ||
|
|
Math.max(0, Math.ceil(box.y + box.height - y)) ||
|
|
(stage ? stage.height() : 0),
|
|
pixelRatio: pixelRatio,
|
|
}), context = canvas.getContext();
|
|
if (config.imageSmoothingEnabled === false) {
|
|
context._context.imageSmoothingEnabled = false;
|
|
}
|
|
context.save();
|
|
if (x || y) {
|
|
context.translate(-1 * x, -1 * y);
|
|
}
|
|
this.drawScene(canvas);
|
|
context.restore();
|
|
canvas._releaseIsolationCanvas();
|
|
return canvas;
|
|
}
|
|
/**
|
|
* converts node into an canvas element.
|
|
* @method
|
|
* @name Konva.Node#toCanvas
|
|
* @param {Object} config
|
|
* @param {Function} config.callback function executed when the composite has completed
|
|
* @param {Number} [config.x] x position of canvas section
|
|
* @param {Number} [config.y] y position of canvas section
|
|
* @param {Number} [config.width] width of canvas section
|
|
* @param {Number} [config.height] height of canvas section
|
|
* @param {Number} [config.pixelRatio] pixelRatio of output canvas. Default is 1.
|
|
* Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution.
|
|
* Rebuild caches at the export pixel ratio to preserve detail.
|
|
* The pixel ratio multiplies the dimensions of the exported image.
|
|
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
|
|
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
|
|
* @example
|
|
* var canvas = node.toCanvas();
|
|
*/
|
|
toCanvas(config) {
|
|
return this._toKonvaCanvas(config)._canvas;
|
|
}
|
|
/**
|
|
* Creates a composite data URL (base64 string). If MIME type is not
|
|
* specified, then "image/png" will result. For "image/jpeg", specify a quality
|
|
* level as quality (range 0.0 - 1.0)
|
|
* @method
|
|
* @name Konva.Node#toDataURL
|
|
* @param {Object} config
|
|
* @param {String} [config.mimeType] can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.x] x position of canvas section
|
|
* @param {Number} [config.y] y position of canvas section
|
|
* @param {Number} [config.width] width of canvas section
|
|
* @param {Number} [config.height] height of canvas section
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
* @param {Number} [config.pixelRatio] pixelRatio of output image url. Default is 1.
|
|
* Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution.
|
|
* Rebuild caches at the export pixel ratio to preserve detail.
|
|
* The pixel ratio multiplies the dimensions of the exported image.
|
|
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
|
|
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
|
|
* @returns {String}
|
|
*/
|
|
toDataURL(config) {
|
|
var _a;
|
|
config = config || {};
|
|
const mimeType = config.mimeType || null, quality = (_a = config.quality) !== null && _a !== void 0 ? _a : null;
|
|
const url = this._toKonvaCanvas(config).toDataURL(mimeType, quality);
|
|
if (config.callback) {
|
|
config.callback(url);
|
|
}
|
|
return url;
|
|
}
|
|
/**
|
|
* converts node into an image. Since the toImage
|
|
* method is asynchronous, the resulting image can only be retrieved from the config callback
|
|
* or the returned Promise. toImage is most commonly used
|
|
* to cache complex drawings as an image so that they don't have to constantly be redrawn
|
|
* @method
|
|
* @name Konva.Node#toImage
|
|
* @param {Object} config
|
|
* @param {Function} [config.callback] function executed when the composite has completed
|
|
* @param {String} [config.mimeType] can be "image/png" or "image/jpeg".
|
|
* "image/png" is the default
|
|
* @param {Number} [config.x] x position of canvas section
|
|
* @param {Number} [config.y] y position of canvas section
|
|
* @param {Number} [config.width] width of canvas section
|
|
* @param {Number} [config.height] height of canvas section
|
|
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
|
|
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
|
|
* is very high quality
|
|
* @param {Number} [config.pixelRatio] pixelRatio of output image. Default is 1.
|
|
* Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution.
|
|
* Rebuild caches at the export pixel ratio to preserve detail.
|
|
* The pixel ratio multiplies the dimensions of the exported image.
|
|
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
|
|
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
|
|
* @return {Promise<Image>}
|
|
* @example
|
|
* var image = node.toImage({
|
|
* callback(img) {
|
|
* // do stuff with img
|
|
* }
|
|
* });
|
|
*/
|
|
toImage(config) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
const { callback, ...rest } = config || {};
|
|
Util._urlToImage(this.toDataURL(rest), function (img) {
|
|
resolve(img);
|
|
callback === null || callback === void 0 ? void 0 : callback(img);
|
|
}, reject);
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Converts node into a blob. Since the toBlob method is asynchronous,
|
|
* the resulting blob can only be retrieved from the config callback
|
|
* or the returned Promise.
|
|
* @method
|
|
* @name Konva.Node#toBlob
|
|
* @param {Object} config
|
|
* @param {Function} [config.callback] function executed when the composite has completed
|
|
* @param {Number} [config.x] x position of canvas section
|
|
* @param {Number} [config.y] y position of canvas section
|
|
* @param {Number} [config.width] width of canvas section
|
|
* @param {Number} [config.height] height of canvas section
|
|
* @param {Number} [config.pixelRatio] pixelRatio of output canvas. Default is 1.
|
|
* Higher pixel ratios increase export resolution. Cached nodes keep their cache resolution.
|
|
* Rebuild caches at the export pixel ratio to preserve detail.
|
|
* The pixel ratio multiplies the dimensions of the exported image.
|
|
* If you export to 500x500 size with pixelRatio = 2, then produced image will have size 1000x1000.
|
|
* @param {Boolean} [config.imageSmoothingEnabled] set this to false if you want to disable imageSmoothing
|
|
* @example
|
|
* var blob = await node.toBlob({});
|
|
* @returns {Promise<Blob>} rejects if the canvas can not be encoded
|
|
*/
|
|
toBlob(config) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
this.toCanvas(config).toBlob((blob) => {
|
|
var _a;
|
|
// the canvas reports a failed encode by passing null
|
|
if (!blob) {
|
|
reject(new Error('Konva: toBlob() failed, the canvas was not encoded'));
|
|
return;
|
|
}
|
|
resolve(blob);
|
|
(_a = config === null || config === void 0 ? void 0 : config.callback) === null || _a === void 0 ? void 0 : _a.call(config, blob);
|
|
}, config === null || config === void 0 ? void 0 : config.mimeType, config === null || config === void 0 ? void 0 : config.quality);
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
setSize(size) {
|
|
this.width(size.width);
|
|
this.height(size.height);
|
|
return this;
|
|
}
|
|
getSize() {
|
|
return {
|
|
width: this.width(),
|
|
height: this.height(),
|
|
};
|
|
}
|
|
/**
|
|
* get class name, which may return Stage, Layer, Group, or shape class names like Rect, Circle, Text, etc.
|
|
* @method
|
|
* @name Konva.Node#getClassName
|
|
* @returns {String}
|
|
*/
|
|
getClassName() {
|
|
return this.className || this.nodeType;
|
|
}
|
|
/**
|
|
* get the node type, which may return Stage, Layer, Group, or Shape
|
|
* @method
|
|
* @name Konva.Node#getType
|
|
* @returns {String}
|
|
*/
|
|
getType() {
|
|
return this.nodeType;
|
|
}
|
|
getDragDistance() {
|
|
// compare with undefined because we need to track 0 value
|
|
if (this.attrs.dragDistance !== undefined) {
|
|
return this.attrs.dragDistance;
|
|
}
|
|
else if (this.parent) {
|
|
return this.parent.getDragDistance();
|
|
}
|
|
else {
|
|
return Konva.dragDistance;
|
|
}
|
|
}
|
|
_off(type, name, callback) {
|
|
let evtListeners = this.eventListeners[type], i, evtName, handler;
|
|
for (i = 0; i < evtListeners.length; i++) {
|
|
evtName = evtListeners[i].name;
|
|
handler = evtListeners[i].handler;
|
|
// the following two conditions must be true in order to remove a handler:
|
|
// 1) the current event name cannot be konva unless the event name is konva
|
|
// this enables developers to force remove a konva specific listener for whatever reason
|
|
// 2) an event name is not specified, or if one is specified, it matches the current event name
|
|
if ((evtName !== 'konva' || name === 'konva') &&
|
|
(!name || evtName === name) &&
|
|
(!callback || callback === handler)) {
|
|
evtListeners.splice(i, 1);
|
|
if (evtListeners.length === 0) {
|
|
delete this.eventListeners[type];
|
|
break;
|
|
}
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
_fireChangeEvent(attr, oldVal, newVal) {
|
|
this._fire(attr + CHANGE, {
|
|
oldVal: oldVal,
|
|
newVal: newVal,
|
|
});
|
|
}
|
|
/**
|
|
* add name to node
|
|
* @method
|
|
* @name Konva.Node#addName
|
|
* @param {String} name
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* node.name('red');
|
|
* node.addName('selected');
|
|
* node.name(); // return 'red selected'
|
|
*/
|
|
addName(name) {
|
|
if (!this.hasName(name)) {
|
|
const oldName = this.name();
|
|
const newName = oldName ? oldName + ' ' + name : name;
|
|
this.name(newName);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* check is node has name
|
|
* @method
|
|
* @name Konva.Node#hasName
|
|
* @param {String} name
|
|
* @returns {Boolean}
|
|
* @example
|
|
* node.name('red');
|
|
* node.hasName('red'); // return true
|
|
* node.hasName('selected'); // return false
|
|
* node.hasName(''); // return false
|
|
*/
|
|
hasName(name) {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
const fullName = this.name();
|
|
if (!fullName) {
|
|
return false;
|
|
}
|
|
// if name is '' the "names" will be [''], so I added extra check above
|
|
const names = (fullName || '').split(/\s/g);
|
|
return names.indexOf(name) !== -1;
|
|
}
|
|
/**
|
|
* remove name from node
|
|
* @method
|
|
* @name Konva.Node#removeName
|
|
* @param {String} name
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* node.name('red selected');
|
|
* node.removeName('selected');
|
|
* node.hasName('selected'); // return false
|
|
* node.name(); // return 'red'
|
|
*/
|
|
removeName(name) {
|
|
const names = (this.name() || '').split(/\s/g);
|
|
const index = names.indexOf(name);
|
|
if (index !== -1) {
|
|
names.splice(index, 1);
|
|
this.name(names.join(' '));
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* set attr
|
|
* @method
|
|
* @name Konva.Node#setAttr
|
|
* @param {String} attr
|
|
* @param {*} val
|
|
* @returns {Konva.Node}
|
|
* @example
|
|
* node.setAttr('x', 5);
|
|
*/
|
|
setAttr(attr, val) {
|
|
const func = this[SET + Util._capitalize(attr)];
|
|
if (Util._isFunction(func)) {
|
|
func.call(this, val);
|
|
}
|
|
else {
|
|
// otherwise set directly
|
|
this._setAttr(attr, val);
|
|
}
|
|
return this;
|
|
}
|
|
_addSubtreeObserver(observer) {
|
|
var _a;
|
|
((_a = this._subtreeObservers) !== null && _a !== void 0 ? _a : (this._subtreeObservers = new Set())).add(observer);
|
|
Node._subtreeObserverCount++;
|
|
}
|
|
_removeSubtreeObserver(observer) {
|
|
var _a;
|
|
if ((_a = this._subtreeObservers) === null || _a === void 0 ? void 0 : _a.delete(observer)) {
|
|
Node._subtreeObserverCount--;
|
|
}
|
|
}
|
|
// Tells the observers of this node and of its ancestors that something below
|
|
// them changed. A node's own changes reach its observers through its change
|
|
// events, so a draw request starts at the parent. A container calls this
|
|
// itself when its child list changes.
|
|
_notifySubtreeChange() {
|
|
if (!Node._subtreeObserverCount)
|
|
return;
|
|
for (let node = this; node; node = node.parent) {
|
|
const observers = node._subtreeObservers;
|
|
if (observers)
|
|
for (const o of observers)
|
|
o._onSubtreeChange(node);
|
|
}
|
|
}
|
|
_requestDraw() {
|
|
var _a;
|
|
(_a = this.parent) === null || _a === void 0 ? void 0 : _a._notifySubtreeChange();
|
|
if (Konva.autoDrawEnabled) {
|
|
const drawNode = this.getLayer() || this.getStage();
|
|
drawNode === null || drawNode === void 0 ? void 0 : drawNode.batchDraw();
|
|
}
|
|
}
|
|
_setAttr(key, val) {
|
|
const oldVal = this.attrs[key];
|
|
if (oldVal === val && !Util.isObject(val)) {
|
|
return;
|
|
}
|
|
if (val === undefined || val === null) {
|
|
delete this.attrs[key];
|
|
}
|
|
else {
|
|
this.attrs[key] = val;
|
|
}
|
|
if (this._attrsVersion !== undefined)
|
|
this._attrsVersion++;
|
|
if (this._shouldFireChangeEvents) {
|
|
this._fireChangeEvent(key, oldVal, val);
|
|
}
|
|
this._requestDraw();
|
|
}
|
|
_fireAndBubble(eventType, evt, compareShape) {
|
|
if (evt && this.nodeType === SHAPE) {
|
|
evt.target = this;
|
|
}
|
|
const nonBubbling = NON_BUBBLING_EVENTS.indexOf(eventType) !== -1;
|
|
const shouldStop = nonBubbling &&
|
|
((compareShape &&
|
|
(this === compareShape ||
|
|
(this.isAncestorOf && this.isAncestorOf(compareShape)))) ||
|
|
(this.nodeType === 'Stage' && !compareShape));
|
|
if (!shouldStop) {
|
|
this._fire(eventType, evt);
|
|
// simulate event bubbling
|
|
const stopBubble = nonBubbling &&
|
|
compareShape &&
|
|
compareShape.isAncestorOf &&
|
|
compareShape.isAncestorOf(this) &&
|
|
!compareShape.isAncestorOf(this.parent);
|
|
if (((evt && !evt.cancelBubble) || !evt) &&
|
|
this.parent &&
|
|
this.parent.isListening() &&
|
|
!stopBubble) {
|
|
if (compareShape && compareShape.parent) {
|
|
this._fireAndBubble.call(this.parent, eventType, evt, compareShape);
|
|
}
|
|
else {
|
|
this._fireAndBubble.call(this.parent, eventType, evt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_getProtoListeners(eventType) {
|
|
var _a, _b;
|
|
const proto = Object.getPrototypeOf(this);
|
|
let allListeners = Node.protoListenerMap.get(proto);
|
|
if (!allListeners) {
|
|
allListeners = {};
|
|
Node.protoListenerMap.set(proto, allListeners);
|
|
}
|
|
let events = allListeners[eventType];
|
|
if (events === undefined) {
|
|
//recalculate cache
|
|
events = [];
|
|
// Dedupe across the prototype chain: subclasses without their own
|
|
// `eventListeners` inherit the parent's, so a single handler would
|
|
// otherwise be pushed once per chain level (Stage→Container→Node = 3x).
|
|
const seen = new Set();
|
|
let obj = Object.getPrototypeOf(this);
|
|
while (obj) {
|
|
const hierarchyEvents = (_b = (_a = obj.eventListeners) === null || _a === void 0 ? void 0 : _a[eventType]) !== null && _b !== void 0 ? _b : [];
|
|
for (let i = 0; i < hierarchyEvents.length; i++) {
|
|
const entry = hierarchyEvents[i];
|
|
if (!seen.has(entry)) {
|
|
seen.add(entry);
|
|
events.push(entry);
|
|
}
|
|
}
|
|
obj = Object.getPrototypeOf(obj);
|
|
}
|
|
// update cache
|
|
allListeners[eventType] = events;
|
|
}
|
|
return events;
|
|
}
|
|
_fire(eventType, evt) {
|
|
evt = evt || {};
|
|
evt.currentTarget = this;
|
|
evt.type = eventType;
|
|
const topListeners = this._getProtoListeners(eventType);
|
|
if (topListeners) {
|
|
// The list is rebuilt when a prototype gains a listener, so no
|
|
// defensive .slice() needed (hot path: every *Change).
|
|
for (let i = 0; i < topListeners.length; i++) {
|
|
topListeners[i].handler.call(this, evt);
|
|
}
|
|
}
|
|
// snapshot prevents skipping listeners when a handler mutates the array
|
|
// (e.g. off + on for the same event), but we also need to fire listeners
|
|
// added during dispatch (e.g. draggable(true) inside mousedown)
|
|
const selfListeners = this.eventListeners[eventType];
|
|
if (selfListeners) {
|
|
const list = selfListeners.slice();
|
|
const origLen = list.length;
|
|
for (let i = 0; i < list.length; i++) {
|
|
list[i].handler.call(this, evt);
|
|
}
|
|
// fire any listeners appended during dispatch
|
|
const liveListeners = this.eventListeners[eventType];
|
|
if (liveListeners) {
|
|
for (let i = origLen; i < liveListeners.length; i++) {
|
|
liveListeners[i].handler.call(this, evt);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* draw both scene and hit graphs. If the node being drawn is the stage, all of the layers will be cleared and redrawn
|
|
* @method
|
|
* @name Konva.Node#draw
|
|
* @returns {Konva.Node}
|
|
*/
|
|
draw() {
|
|
this.drawScene();
|
|
this.drawHit();
|
|
return this;
|
|
}
|
|
// drag & drop
|
|
_createDragElement(evt) {
|
|
var _a;
|
|
const pointerId = evt ? evt.pointerId : undefined;
|
|
const stage = this.getStage();
|
|
const ap = this.getAbsolutePosition();
|
|
if (!stage) {
|
|
return;
|
|
}
|
|
const pos = stage._getPointerById(pointerId) ||
|
|
stage._changedPointerPositions[0] ||
|
|
ap;
|
|
const type = ((_a = evt === null || evt === void 0 ? void 0 : evt.evt) === null || _a === void 0 ? void 0 : _a.type)
|
|
? Util._getEventType(evt.evt.type)
|
|
: stage._pointerEventType;
|
|
// drags follow mouse and touch window events only, so a drag started from
|
|
// a pointer event stays unbound until such an event attaches its pointer
|
|
const bound = type !== 'pointer';
|
|
DD._dragElements.set(this._id, {
|
|
node: this,
|
|
startPointerPos: pos,
|
|
offset: {
|
|
x: pos.x - ap.x,
|
|
y: pos.y - ap.y,
|
|
},
|
|
dragStatus: 'ready',
|
|
pointerId: bound
|
|
? (pointerId !== null && pointerId !== void 0 ? pointerId : ('id' in pos ? pos.id : undefined))
|
|
: undefined,
|
|
pointerEventType: bound ? type : undefined,
|
|
startEvent: evt,
|
|
});
|
|
}
|
|
/**
|
|
* initiate drag and drop.
|
|
* @method
|
|
* @name Konva.Node#startDrag
|
|
*/
|
|
startDrag(evt, bubbleEvent = true) {
|
|
var _a;
|
|
if (!DD._dragElements.has(this._id)) {
|
|
this._createDragElement(evt);
|
|
}
|
|
const elem = DD._dragElements.get(this._id);
|
|
// a node outside of a stage has no pointer to follow
|
|
if (!elem) {
|
|
return;
|
|
}
|
|
elem.dragStatus = 'dragging';
|
|
(_a = this.getStage()) === null || _a === void 0 ? void 0 : _a._cancelClick(elem.pointerId, elem.pointerEventType);
|
|
this.fire('dragstart', {
|
|
type: 'dragstart',
|
|
target: this,
|
|
// Use the stored start event if available (from mousedown/touchstart),
|
|
// otherwise fall back to the provided event (when startDrag is called programmatically)
|
|
evt: (elem.startEvent && elem.startEvent.evt) || (evt && evt.evt),
|
|
}, bubbleEvent);
|
|
}
|
|
_setDragPosition(evt, elem) {
|
|
const pos = this.getStage()._getPointerById(elem.pointerId);
|
|
if (!pos) {
|
|
return;
|
|
}
|
|
let newNodePos = {
|
|
x: pos.x - elem.offset.x,
|
|
y: pos.y - elem.offset.y,
|
|
};
|
|
const dbf = this.dragBoundFunc();
|
|
if (dbf !== undefined) {
|
|
const bounded = dbf.call(this, newNodePos, evt);
|
|
if (!bounded) {
|
|
Util.warn('dragBoundFunc did not return any value. That is unexpected behavior. You must return new absolute position from dragBoundFunc.');
|
|
}
|
|
else if (!Util._isNumber(bounded.x) || !Util._isNumber(bounded.y)) {
|
|
Util.warn(`dragBoundFunc returned a position with a non-finite x or y (${bounded.x}, ${bounded.y}). The node was not moved.`);
|
|
return;
|
|
}
|
|
else {
|
|
newNodePos = bounded;
|
|
}
|
|
}
|
|
const lastPos = elem.lastPos;
|
|
if (!lastPos || lastPos.x !== newNodePos.x || lastPos.y !== newNodePos.y) {
|
|
this.setAbsolutePosition(newNodePos);
|
|
this._requestDraw();
|
|
}
|
|
elem.lastPos = newNodePos;
|
|
}
|
|
/**
|
|
* stop drag and drop
|
|
* @method
|
|
* @name Konva.Node#stopDrag
|
|
*/
|
|
stopDrag(evt) {
|
|
const elem = DD._dragElements.get(this._id);
|
|
if (!elem) {
|
|
return;
|
|
}
|
|
elem.dragStatus = 'stopped';
|
|
DD._endDragBefore(evt, undefined, elem);
|
|
DD._endDragAfter(evt, elem);
|
|
}
|
|
setDraggable(draggable) {
|
|
this._setAttr('draggable', draggable);
|
|
this._dragChange();
|
|
}
|
|
/**
|
|
* determine if node is currently in drag and drop mode
|
|
* @method
|
|
* @name Konva.Node#isDragging
|
|
*/
|
|
isDragging() {
|
|
const elem = DD._dragElements.get(this._id);
|
|
return elem ? elem.dragStatus === 'dragging' : false;
|
|
}
|
|
_listenDrag() {
|
|
this._dragCleanup();
|
|
this.on('mousedown.konva touchstart.konva', function (evt) {
|
|
const shouldCheckButton = evt.evt['button'] !== undefined;
|
|
const canDrag = !shouldCheckButton || Konva.dragButtons.indexOf(evt.evt['button']) >= 0;
|
|
if (!canDrag) {
|
|
return;
|
|
}
|
|
if (DD._dragElements.has(this._id)) {
|
|
return;
|
|
}
|
|
let hasDraggingChild = false;
|
|
DD._dragElements.forEach((elem) => {
|
|
if (this.isAncestorOf(elem.node)) {
|
|
hasDraggingChild = true;
|
|
}
|
|
});
|
|
// nested drag can be started
|
|
// in that case we don't need to start new drag
|
|
if (!hasDraggingChild) {
|
|
this._createDragElement(evt);
|
|
}
|
|
});
|
|
}
|
|
_dragChange() {
|
|
if (this.attrs.draggable) {
|
|
this._listenDrag();
|
|
}
|
|
else {
|
|
// remove event listeners
|
|
this._dragCleanup();
|
|
/*
|
|
* force drag and drop to end
|
|
* if this node is currently in
|
|
* drag and drop mode
|
|
*/
|
|
const stage = this.getStage();
|
|
if (!stage) {
|
|
return;
|
|
}
|
|
const dragElement = DD._dragElements.get(this._id);
|
|
const isDragging = dragElement && dragElement.dragStatus === 'dragging';
|
|
const isReady = dragElement && dragElement.dragStatus === 'ready';
|
|
if (isDragging) {
|
|
this.stopDrag();
|
|
}
|
|
else if (isReady) {
|
|
DD._dragElements.delete(this._id);
|
|
}
|
|
}
|
|
}
|
|
_dragCleanup() {
|
|
this.off('mousedown.konva');
|
|
this.off('touchstart.konva');
|
|
}
|
|
/**
|
|
* determine if node (at least partially) is currently in user-visible area
|
|
* @method
|
|
* @param {(Number | Object)} margin optional margin in pixels
|
|
* @param {Number} margin.x
|
|
* @param {Number} margin.y
|
|
* @returns {Boolean}
|
|
* @name Konva.Node#isClientRectOnScreen
|
|
* @example
|
|
* // get index
|
|
* // default calculations
|
|
* var isOnScreen = node.isClientRectOnScreen()
|
|
* // increase object size (or screen size) for cases when objects close to the screen still need to be marked as "visible"
|
|
* var isOnScreen = node.isClientRectOnScreen({ x: stage.width(), y: stage.height() })
|
|
*/
|
|
isClientRectOnScreen(margin = { x: 0, y: 0 }) {
|
|
const stage = this.getStage();
|
|
if (!stage) {
|
|
return false;
|
|
}
|
|
const screenRect = {
|
|
x: -margin.x,
|
|
y: -margin.y,
|
|
width: stage.width() + 2 * margin.x,
|
|
height: stage.height() + 2 * margin.y,
|
|
};
|
|
return Util.haveIntersection(screenRect, this.getClientRect());
|
|
}
|
|
/**
|
|
* create node with JSON string or an Object. Deserialization restores attributes only,
|
|
* not functions, images or event handlers (that would make the serialized object huge):
|
|
* `sceneFunc`/`hitFunc` of custom shapes, images and `fillPatternImage`, `clipFunc`,
|
|
* `dragBoundFunc`, filter functions, the `nodes`, `boundBoxFunc` and `anchorStyleFunc`
|
|
* of a Transformer, typed arrays and the `cache()` state. If your app uses them (it probably
|
|
* does), select the nodes after loading the stage and set these properties again with
|
|
* `on()`, `sceneFunc()`, `image()` and so on
|
|
* @method
|
|
* @memberof Konva.Node
|
|
* @param {String|Object} data string or object
|
|
* @param {Element} [container] optional container dom element used only if you're
|
|
* creating a stage node
|
|
*/
|
|
static create(data, container) {
|
|
if (Util._isString(data)) {
|
|
data = JSON.parse(data);
|
|
}
|
|
return this._createNode(data, container);
|
|
}
|
|
static _createNode(obj, container) {
|
|
let className = Node.prototype.getClassName.call(obj), children = obj.children, no, len, n;
|
|
// if container was passed in, add it to attrs
|
|
if (container) {
|
|
obj.attrs.container = container;
|
|
}
|
|
if (!Konva[className]) {
|
|
const fallback = children ? 'Group' : 'Shape';
|
|
Util.warn('Can not find a node with class name "' +
|
|
className +
|
|
`". Fallback to "${fallback}".`);
|
|
className = fallback;
|
|
}
|
|
const Class = Konva[className];
|
|
no = new Class(obj.attrs);
|
|
if (children) {
|
|
len = children.length;
|
|
for (n = 0; n < len; n++) {
|
|
no.add(Node._createNode(children[n]));
|
|
}
|
|
}
|
|
return no;
|
|
}
|
|
}
|
|
// Number of registered observers. The ancestor walk is skipped while it is 0.
|
|
Node._subtreeObserverCount = 0;
|
|
// Perf: while a batch is open, work that depends on many nodes (a Konva
|
|
// .Transformer refresh) is queued and runs once when the outermost scope
|
|
// ends, instead of once per node change. Scopes are the callers of
|
|
// _updateBatch() and _beginUpdateBatch().
|
|
Node._updateBatchDepth = 0;
|
|
// a Set, so a callback queued once per change runs once per batch
|
|
Node._pendingUpdates = new Set();
|
|
// keyed by prototype: subclasses that share a nodeType (every shape class)
|
|
// have their own listeners
|
|
Node.protoListenerMap = new WeakMap();
|
|
Node.prototype.nodeType = 'Node';
|
|
Node.prototype._attrsAffectingSize = [];
|
|
// attache events listeners once into prototype
|
|
// that way we don't spend too much time on making an new instance
|
|
Node.prototype.on(TRANSFORM_CHANGE_STR, function () {
|
|
if (this._batchingTransformChange) {
|
|
this._needClearTransformCache = true;
|
|
return;
|
|
}
|
|
this._clearCache(TRANSFORM);
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM);
|
|
});
|
|
Node.prototype.on('visibleChange.konva', function () {
|
|
this._clearSelfAndDescendantCache(VISIBLE);
|
|
});
|
|
Node.prototype.on('listeningChange.konva', function () {
|
|
this._clearSelfAndDescendantCache(LISTENING);
|
|
for (let parent = this.getParent(); parent; parent = parent.getParent()) {
|
|
const cache = parent._getCanvasCache();
|
|
if (cache === null || cache === void 0 ? void 0 : cache.hit) {
|
|
Util.releaseCanvas(cache.hit._canvas);
|
|
cache.hit = null;
|
|
}
|
|
}
|
|
});
|
|
Node.prototype.on('opacityChange.konva', function () {
|
|
this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY);
|
|
});
|
|
const addGetterSetter = Factory.addGetterSetter;
|
|
/**
|
|
* get/set zIndex relative to the node's siblings who share the same parent.
|
|
* Please remember that zIndex is not absolute (like in CSS). It is relative to parent element only.
|
|
* @name Konva.Node#zIndex
|
|
* @method
|
|
* @param {Number} index
|
|
* @returns {Number}
|
|
* @example
|
|
* // get index
|
|
* var index = node.zIndex();
|
|
*
|
|
* // set index
|
|
* node.zIndex(2);
|
|
*/
|
|
addGetterSetter(Node, 'zIndex');
|
|
/**
|
|
* get/set node absolute position
|
|
* @name Konva.Node#absolutePosition
|
|
* @method
|
|
* @param {Object} pos
|
|
* @param {Number} pos.x
|
|
* @param {Number} pos.y
|
|
* @returns {Object}
|
|
* @example
|
|
* // get position
|
|
* var position = node.absolutePosition();
|
|
*
|
|
* // set position
|
|
* node.absolutePosition({
|
|
* x: 5,
|
|
* y: 10
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'absolutePosition');
|
|
addGetterSetter(Node, 'position');
|
|
/**
|
|
* get/set node position relative to parent
|
|
* @name Konva.Node#position
|
|
* @method
|
|
* @param {Object} pos
|
|
* @param {Number} pos.x
|
|
* @param {Number} pos.y
|
|
* @returns {Object}
|
|
* @example
|
|
* // get position
|
|
* var position = node.position();
|
|
*
|
|
* // set position
|
|
* node.position({
|
|
* x: 5,
|
|
* y: 10
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'x', 0, getNumberValidator());
|
|
/**
|
|
* get/set x position
|
|
* @name Konva.Node#x
|
|
* @method
|
|
* @param {Number} x
|
|
* @returns {Object}
|
|
* @example
|
|
* // get x
|
|
* var x = node.x();
|
|
*
|
|
* // set x
|
|
* node.x(5);
|
|
*/
|
|
addGetterSetter(Node, 'y', 0, getNumberValidator());
|
|
/**
|
|
* get/set y position
|
|
* @name Konva.Node#y
|
|
* @method
|
|
* @param {Number} y
|
|
* @returns {Integer}
|
|
* @example
|
|
* // get y
|
|
* var y = node.y();
|
|
*
|
|
* // set y
|
|
* node.y(5);
|
|
*/
|
|
addGetterSetter(Node, 'globalCompositeOperation', 'source-over', getStringValidator());
|
|
/**
|
|
* get/set globalCompositeOperation of a node. globalCompositeOperation DOESN'T affect hit graph of nodes. So they are still trigger to events as they have default "source-over" globalCompositeOperation.
|
|
* @name Konva.Node#globalCompositeOperation
|
|
* @method
|
|
* @param {String} type
|
|
* @returns {String}
|
|
* @example
|
|
* // get globalCompositeOperation
|
|
* var globalCompositeOperation = shape.globalCompositeOperation();
|
|
*
|
|
* // set globalCompositeOperation
|
|
* shape.globalCompositeOperation('source-in');
|
|
*/
|
|
addGetterSetter(Node, 'opacity', 1, getNumberValidator());
|
|
/**
|
|
* get/set opacity. Opacity values range from 0 to 1.
|
|
* A node with an opacity of 0 is fully transparent, and a node
|
|
* with an opacity of 1 is fully opaque
|
|
* @name Konva.Node#opacity
|
|
* @method
|
|
* @param {Object} opacity
|
|
* @returns {Number}
|
|
* @example
|
|
* // get opacity
|
|
* var opacity = node.opacity();
|
|
*
|
|
* // set opacity
|
|
* node.opacity(0.5);
|
|
*/
|
|
addGetterSetter(Node, 'name', '', getStringValidator());
|
|
/**
|
|
* get/set name.
|
|
* @name Konva.Node#name
|
|
* @method
|
|
* @param {String} name
|
|
* @returns {String}
|
|
* @example
|
|
* // get name
|
|
* var name = node.name();
|
|
*
|
|
* // set name
|
|
* node.name('foo');
|
|
*
|
|
* // also node may have multiple names (as css classes)
|
|
* node.name('foo bar');
|
|
*/
|
|
addGetterSetter(Node, 'id', '', getStringValidator());
|
|
/**
|
|
* get/set id. Id is global for whole page.
|
|
* @name Konva.Node#id
|
|
* @method
|
|
* @param {String} id
|
|
* @returns {String}
|
|
* @example
|
|
* // get id
|
|
* var name = node.id();
|
|
*
|
|
* // set id
|
|
* node.id('foo');
|
|
*/
|
|
addGetterSetter(Node, 'rotation', 0, getNumberValidator());
|
|
/**
|
|
* get/set rotation in degrees
|
|
* @name Konva.Node#rotation
|
|
* @method
|
|
* @param {Number} rotation
|
|
* @returns {Number}
|
|
* @example
|
|
* // get rotation in degrees
|
|
* var rotation = node.rotation();
|
|
*
|
|
* // set rotation in degrees
|
|
* node.rotation(45);
|
|
*/
|
|
Factory.addComponentsGetterSetter(Node, 'scale', ['x', 'y']);
|
|
/**
|
|
* get/set scale
|
|
* @name Konva.Node#scale
|
|
* @param {Object} scale
|
|
* @param {Number} scale.x
|
|
* @param {Number} scale.y
|
|
* @method
|
|
* @returns {Object}
|
|
* @example
|
|
* // get scale
|
|
* var scale = node.scale();
|
|
*
|
|
* // set scale
|
|
* shape.scale({
|
|
* x: 2,
|
|
* y: 3
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'scaleX', 1, getNumberValidator());
|
|
/**
|
|
* get/set scale x
|
|
* @name Konva.Node#scaleX
|
|
* @param {Number} x
|
|
* @method
|
|
* @returns {Number}
|
|
* @example
|
|
* // get scale x
|
|
* var scaleX = node.scaleX();
|
|
*
|
|
* // set scale x
|
|
* node.scaleX(2);
|
|
*/
|
|
addGetterSetter(Node, 'scaleY', 1, getNumberValidator());
|
|
/**
|
|
* get/set scale y
|
|
* @name Konva.Node#scaleY
|
|
* @param {Number} y
|
|
* @method
|
|
* @returns {Number}
|
|
* @example
|
|
* // get scale y
|
|
* var scaleY = node.scaleY();
|
|
*
|
|
* // set scale y
|
|
* node.scaleY(2);
|
|
*/
|
|
Factory.addComponentsGetterSetter(Node, 'skew', ['x', 'y']);
|
|
/**
|
|
* get/set skew
|
|
* @name Konva.Node#skew
|
|
* @param {Object} skew
|
|
* @param {Number} skew.x
|
|
* @param {Number} skew.y
|
|
* @method
|
|
* @returns {Object}
|
|
* @example
|
|
* // get skew
|
|
* var skew = node.skew();
|
|
*
|
|
* // set skew
|
|
* node.skew({
|
|
* x: 20,
|
|
* y: 10
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'skewX', 0, getNumberValidator());
|
|
/**
|
|
* get/set skew x
|
|
* @name Konva.Node#skewX
|
|
* @param {Number} x
|
|
* @method
|
|
* @returns {Number}
|
|
* @example
|
|
* // get skew x
|
|
* var skewX = node.skewX();
|
|
*
|
|
* // set skew x
|
|
* node.skewX(3);
|
|
*/
|
|
addGetterSetter(Node, 'skewY', 0, getNumberValidator());
|
|
/**
|
|
* get/set skew y
|
|
* @name Konva.Node#skewY
|
|
* @param {Number} y
|
|
* @method
|
|
* @returns {Number}
|
|
* @example
|
|
* // get skew y
|
|
* var skewY = node.skewY();
|
|
*
|
|
* // set skew y
|
|
* node.skewY(3);
|
|
*/
|
|
Factory.addComponentsGetterSetter(Node, 'offset', ['x', 'y']);
|
|
/**
|
|
* get/set offset. Offsets the default position and rotation point
|
|
* @method
|
|
* @param {Object} offset
|
|
* @param {Number} offset.x
|
|
* @param {Number} offset.y
|
|
* @returns {Object}
|
|
* @example
|
|
* // get offset
|
|
* var offset = node.offset();
|
|
*
|
|
* // set offset
|
|
* node.offset({
|
|
* x: 20,
|
|
* y: 10
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'offsetX', 0, getNumberValidator());
|
|
/**
|
|
* get/set offset x
|
|
* @name Konva.Node#offsetX
|
|
* @method
|
|
* @param {Number} x
|
|
* @returns {Number}
|
|
* @example
|
|
* // get offset x
|
|
* var offsetX = node.offsetX();
|
|
*
|
|
* // set offset x
|
|
* node.offsetX(3);
|
|
*/
|
|
addGetterSetter(Node, 'offsetY', 0, getNumberValidator());
|
|
/**
|
|
* get/set offset y
|
|
* @name Konva.Node#offsetY
|
|
* @method
|
|
* @param {Number} y
|
|
* @returns {Number}
|
|
* @example
|
|
* // get offset y
|
|
* var offsetY = node.offsetY();
|
|
*
|
|
* // set offset y
|
|
* node.offsetY(3);
|
|
*/
|
|
addGetterSetter(Node, 'dragDistance', undefined, getNumberValidator());
|
|
/**
|
|
* get/set drag distance. A drag starts once the pointer moved this far from where it
|
|
* went down, on either axis. Default is `Konva.dragDistance`
|
|
* @name Konva.Node#dragDistance
|
|
* @method
|
|
* @param {Number} distance
|
|
* @returns {Number}
|
|
* @example
|
|
* // get drag distance
|
|
* var dragDistance = node.dragDistance();
|
|
*
|
|
* // set distance
|
|
* // node starts dragging only if pointer moved more then 3 pixels
|
|
* node.dragDistance(3);
|
|
* // or set globally
|
|
* Konva.dragDistance = 3;
|
|
*/
|
|
addGetterSetter(Node, 'width', 0, getNumberValidator());
|
|
/**
|
|
* get/set width
|
|
* @name Konva.Node#width
|
|
* @method
|
|
* @param {Number} width
|
|
* @returns {Number}
|
|
* @example
|
|
* // get width
|
|
* var width = node.width();
|
|
*
|
|
* // set width
|
|
* node.width(100);
|
|
*/
|
|
addGetterSetter(Node, 'height', 0, getNumberValidator());
|
|
/**
|
|
* get/set height
|
|
* @name Konva.Node#height
|
|
* @method
|
|
* @param {Number} height
|
|
* @returns {Number}
|
|
* @example
|
|
* // get height
|
|
* var height = node.height();
|
|
*
|
|
* // set height
|
|
* node.height(100);
|
|
*/
|
|
addGetterSetter(Node, 'listening', true, getBooleanValidator());
|
|
/**
|
|
* get/set listening attr. If you need to determine if a node is listening or not
|
|
* by taking into account its parents, use the isListening() method
|
|
* nodes with listening set to false will not be detected in hit graph
|
|
* so they will be ignored in container.getIntersection() method
|
|
* a non-listening layer (or a layer of a non-listening stage) also
|
|
* releases its hit canvas, so it uses less memory
|
|
* @name Konva.Node#listening
|
|
* @method
|
|
* @param {Boolean} listening Can be true, or false. The default is true.
|
|
* @returns {Boolean}
|
|
* @example
|
|
* // get listening attr
|
|
* var listening = node.listening();
|
|
*
|
|
* // stop listening for events, remove node and all its children from hit graph
|
|
* node.listening(false);
|
|
*
|
|
* // listen to events according to the parent
|
|
* node.listening(true);
|
|
*/
|
|
/**
|
|
* get/set preventDefault
|
|
* By default all shapes will prevent default behavior
|
|
* of a browser on a pointer move or tap.
|
|
* that will prevent native scrolling when you are trying to drag&drop a node
|
|
* but sometimes you may need to enable default actions
|
|
* in that case you can set the property to false
|
|
* @name Konva.Node#preventDefault
|
|
* @method
|
|
* @param {Boolean} preventDefault
|
|
* @returns {Boolean}
|
|
* @example
|
|
* // get preventDefault
|
|
* var shouldPrevent = shape.preventDefault();
|
|
*
|
|
* // set preventDefault
|
|
* shape.preventDefault(false);
|
|
*/
|
|
addGetterSetter(Node, 'preventDefault', true, getBooleanValidator());
|
|
addGetterSetter(Node, 'filters', undefined, function (val) {
|
|
this._filterUpToDate = false;
|
|
return val;
|
|
});
|
|
/**
|
|
* get/set filters. Supports function filters, CSS filter strings, or mixed arrays.
|
|
* CSS filters are applied using native browser performance when possible, function filters use ImageData manipulation.
|
|
* CSS filters automatically fall back to function filters in unsupported browsers.
|
|
* Blur radii, pixelation sizes and CSS lengths use node coordinates, independent of cache pixelRatio.
|
|
* Custom filters receive (imageData, pixelRatio). ImageData contains full-resolution backing pixels.
|
|
* @name Konva.Node#filters
|
|
* @method
|
|
* @param {Array} filters array of filter functions and/or CSS filter strings
|
|
* @returns {Array}
|
|
* @example
|
|
* // get filters
|
|
* var filters = node.filters();
|
|
*
|
|
* // set CSS filters only (caching required, uses native performance)
|
|
* node.cache();
|
|
* node.filters(['blur(5px)', 'brightness(1.2)', 'contrast(1.5)']);
|
|
*
|
|
* // set function filters only (caching required)
|
|
* node.cache();
|
|
* node.filters([Konva.Filters.Blur, Konva.Filters.Sepia, Konva.Filters.Invert]);
|
|
*
|
|
* // mix CSS and function filters (caching required, uses fallback strategy)
|
|
* node.cache();
|
|
* node.filters([
|
|
* 'blur(3px)', // CSS filter
|
|
* Konva.Filters.Invert, // function filter
|
|
* 'brightness(1.5)' // CSS filter
|
|
* ]);
|
|
*/
|
|
addGetterSetter(Node, 'visible', true, getBooleanValidator());
|
|
/**
|
|
* get/set visible attr. Can be true, or false. The default is true.
|
|
* If you need to determine if a node is visible or not
|
|
* by taking into account its parents, use the isVisible() method
|
|
* @name Konva.Node#visible
|
|
* @method
|
|
* @param {Boolean} visible
|
|
* @returns {Boolean}
|
|
* @example
|
|
* // get visible attr
|
|
* var visible = node.visible();
|
|
*
|
|
* // make invisible
|
|
* node.visible(false);
|
|
*
|
|
* // make visible (according to the parent)
|
|
* node.visible(true);
|
|
*
|
|
*/
|
|
addGetterSetter(Node, 'transformsEnabled', 'all', getStringValidator());
|
|
/**
|
|
* get/set transforms that are enabled. Can be "all", "none", or "position". The default
|
|
* is "all"
|
|
* @name Konva.Node#transformsEnabled
|
|
* @method
|
|
* @param {String} enabled
|
|
* @returns {String}
|
|
* @example
|
|
* // enable position transform only to improve draw performance
|
|
* node.transformsEnabled('position');
|
|
*
|
|
* // enable all transforms
|
|
* node.transformsEnabled('all');
|
|
*/
|
|
/**
|
|
* get/set node size
|
|
* @name Konva.Node#size
|
|
* @method
|
|
* @param {Object} size
|
|
* @param {Number} size.width
|
|
* @param {Number} size.height
|
|
* @returns {Object}
|
|
* @example
|
|
* // get node size
|
|
* var size = node.size();
|
|
* var width = size.width;
|
|
* var height = size.height;
|
|
*
|
|
* // set size
|
|
* node.size({
|
|
* width: 100,
|
|
* height: 200
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'size');
|
|
/**
|
|
* get/set drag bound function. This is used to override the default
|
|
* drag and drop position. The function is called with the absolute position
|
|
* the node is about to get and the native event, and must return the
|
|
* absolute position to use
|
|
* @name Konva.Node#dragBoundFunc
|
|
* @method
|
|
* @param {Function} dragBoundFunc function(pos, evt) returning `{ x, y }`
|
|
* @returns {Function}
|
|
* @example
|
|
* // get drag bound function
|
|
* var dragBoundFunc = node.dragBoundFunc();
|
|
*
|
|
* // create vertical drag and drop
|
|
* node.dragBoundFunc(function(pos){
|
|
* // important pos - is absolute position of the node
|
|
* // you should return absolute position too
|
|
* return {
|
|
* x: this.absolutePosition().x,
|
|
* y: pos.y
|
|
* };
|
|
* });
|
|
*/
|
|
addGetterSetter(Node, 'dragBoundFunc');
|
|
/**
|
|
* get/set draggable flag
|
|
* @name Konva.Node#draggable
|
|
* @method
|
|
* @param {Boolean} draggable
|
|
* @returns {Boolean}
|
|
* @example
|
|
* // get draggable flag
|
|
* var draggable = node.draggable();
|
|
*
|
|
* // enable drag and drop
|
|
* node.draggable(true);
|
|
*
|
|
* // disable drag and drop
|
|
* node.draggable(false);
|
|
*/
|
|
addGetterSetter(Node, 'draggable', false, getBooleanValidator());
|
|
Factory.backCompat(Node, {
|
|
rotateDeg: 'rotate',
|
|
setRotationDeg: 'setRotation',
|
|
getRotationDeg: 'getRotation',
|
|
});
|