From 2d3840b312eecd924f768be0d3bff8cd89d17f25 Mon Sep 17 00:00:00 2001 From: Jidong Xiao Date: Wed, 23 Apr 2025 21:39:16 -0400 Subject: [PATCH] adding pq animations --- .../priority_queue/max_heap/animation.js | 188 + animations/priority_queue/max_heap/index.html | 32 + animations/priority_queue/max_heap/konva.js | 18926 ++++++++++++++++ .../priority_queue/min_heap/animation.js | 189 + animations/priority_queue/min_heap/index.html | 32 + animations/priority_queue/min_heap/konva.js | 18926 ++++++++++++++++ 6 files changed, 38293 insertions(+) create mode 100644 animations/priority_queue/max_heap/animation.js create mode 100644 animations/priority_queue/max_heap/index.html create mode 100644 animations/priority_queue/max_heap/konva.js create mode 100644 animations/priority_queue/min_heap/animation.js create mode 100644 animations/priority_queue/min_heap/index.html create mode 100644 animations/priority_queue/min_heap/konva.js diff --git a/animations/priority_queue/max_heap/animation.js b/animations/priority_queue/max_heap/animation.js new file mode 100644 index 0000000..3932818 --- /dev/null +++ b/animations/priority_queue/max_heap/animation.js @@ -0,0 +1,188 @@ +var stage = new Konva.Stage({ + container: 'container', + width: 2000, + height: 1000 +}); + +var layer = new Konva.Layer(); +stage.add(layer); + + +var heap = []; +var nodeRadius = 25; +var levelHeight = 80; +var startX = 500; +var startY = 50; +var horizontalSpacing = 150; + +var animationNodes = []; +var animationLines = []; +var currentStep = 0; + +var operations = [ + { type: 'push', value: 3 }, + { type: 'push', value: 4 }, + { type: 'push', value: 3 }, + { type: 'push', value: 1 }, + { type: 'push', value: 5 }, + { type: 'pop' }, + { type: 'pop' }, + { type: 'pop' }, + { type: 'pop' }, + { type: 'pop' } +]; + +function drawNode(x, y, value, color = '#ffffff') { + var group = new Konva.Group({ x: x, y: y }); + + var circle = new Konva.Circle({ + radius: nodeRadius, + fill: color, + stroke: '#000000', + strokeWidth: 2 + }); + + var text = new Konva.Text({ + text: value.toString(), + fill: 'black', + align: 'center', + width: nodeRadius * 2, + x: -nodeRadius, + y: -10 + }); + + group.add(circle); + group.add(text); + layer.add(group); + animationNodes.push(group); + + return group; +} + +function makeLine(x1, y1, x2, y2) { + var line = new Konva.Line({ + points: [x1, y1, x2, y2], + stroke: 'black', + strokeWidth: 2, + lineCap: 'round', + lineJoin: 'round' + }); + layer.add(line); + animationLines.push(line); + return line; +} + +function makeTree() { + animationNodes.forEach(n => n.destroy()); + animationLines.forEach(l => l.destroy()); + animationNodes = []; + animationLines = []; + + if (heap.length === 0) return; + + var positions = []; + + for (var i = 0; i < heap.length; i++) { + var level = Math.floor(Math.log2(i + 1)); + var indexInLevel = i - (2 ** level - 1); + var nodesInLevel = 2 ** level; + + var x = startX + (indexInLevel - (nodesInLevel - 1) / 2) * horizontalSpacing * (1.5 - level / 5); + var y = startY + level * levelHeight; + + positions.push({ x: x, y: y }); + drawNode(x, y, heap[i]); + + if (i > 0) { + var parentPos = positions[Math.floor((i - 1) / 2)]; + makeLine(parentPos.x, parentPos.y + nodeRadius, x, y - nodeRadius); + } + } + + layer.draw(); +} + +function push(val) { + heap.push(val); + var idx = heap.length - 1; + while (idx > 0) { + var parentIdx = Math.floor((idx - 1) / 2); + if (heap[parentIdx] >= heap[idx]) break; + [heap[parentIdx], heap[idx]] = [heap[idx], heap[parentIdx]]; + idx = parentIdx; + } +} + +function pop() { + if (heap.length === 0) return null; + + var max = heap[0]; + heap[0] = heap[heap.length - 1]; + heap.pop(); + + var idx = 0; + while (true) { + var left = 2 * idx + 1; + var right = 2 * idx + 2; + var largest = idx; + + if (left < heap.length && heap[left] > heap[largest]) largest = left; + if (right < heap.length && heap[right] > heap[largest]) largest = right; + + if (largest === idx) break; + + [heap[idx], heap[largest]] = [heap[largest], heap[idx]]; + idx = largest; + } + + return max; +} + +var cppCode = [ + "#include ", + "#include ", + "", + "int main() {", + " std::priority_queue maxHeap;", + "", + " maxHeap.push(3);", + " maxHeap.push(4);", + " maxHeap.push(3);", + " maxHeap.push(1);", + " maxHeap.push(5);", + "", + " while (!maxHeap.empty()) {", + " std::cout << maxHeap.top() << \" \";", + " maxHeap.pop();", + " }", + " std::cout << std::endl;", + "", + " return 0;", + "}" +]; + +function renderCode(step) { + var codeBlock = document.getElementById('codeBlock'); + codeBlock.innerHTML = cppCode.map((line, i) => { + var bold = false; + if (step >= 0 && step <= 4) bold = i === 6 + step; + else if (step >= 5 && step <= 10) bold = i === 13 || i === 14; + return bold ? `${line}` : line; + }).join('\n'); +} +function next() { + var op = operations[currentStep]; + if (op.type === 'push') { + push(op.value); + } else if (op.type === 'pop') { + pop(); + } + + renderCode(currentStep); + makeTree(); + currentStep++; +} + +document.getElementById('next').addEventListener('click', next); + +layer.draw(); diff --git a/animations/priority_queue/max_heap/index.html b/animations/priority_queue/max_heap/index.html new file mode 100644 index 0000000..dfb2686 --- /dev/null +++ b/animations/priority_queue/max_heap/index.html @@ -0,0 +1,32 @@ + + + + + + Max heap animation + + + + + +

Max heap animation

+ +
+
+

+    
+ + diff --git a/animations/priority_queue/max_heap/konva.js b/animations/priority_queue/max_heap/konva.js new file mode 100644 index 0000000..6b8cf52 --- /dev/null +++ b/animations/priority_queue/max_heap/konva.js @@ -0,0 +1,18926 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Konva = factory()); +})(this, (function () { 'use strict'; + + /* + * Konva JavaScript Framework v9.3.20 + * http://konvajs.org/ + * Licensed under the MIT + * Date: Thu Mar 20 2025 + * + * Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS) + * Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva) + * + * @license + */ + const PI_OVER_180 = Math.PI / 180; + /** + * @namespace Konva + */ + function detectBrowser() { + return (typeof window !== 'undefined' && + // browser case + ({}.toString.call(window) === '[object Window]' || + // electron case + {}.toString.call(window) === '[object global]')); + } + const glob = typeof global !== 'undefined' + ? global + : typeof window !== 'undefined' + ? window + : typeof WorkerGlobalScope !== 'undefined' + ? self + : {}; + const Konva$2 = { + _global: glob, + version: '9.3.20', + isBrowser: detectBrowser(), + isUnminified: /param/.test(function (param) { }.toString()), + dblClickWindow: 400, + getAngle(angle) { + return Konva$2.angleDeg ? angle * PI_OVER_180 : angle; + }, + enableTrace: false, + pointerEventsEnabled: true, + /** + * Should Konva automatically update canvas on any changes. Default is true. + * @property autoDrawEnabled + * @default true + * @name autoDrawEnabled + * @memberof Konva + * @example + * Konva.autoDrawEnabled = true; + */ + autoDrawEnabled: true, + /** + * Should we enable hit detection while dragging? For performance reasons, by default it is false. + * But on some rare cases you want to see hit graph and check intersections. Just set it to true. + * @property hitOnDragEnabled + * @default false + * @name hitOnDragEnabled + * @memberof Konva + * @example + * Konva.hitOnDragEnabled = true; + */ + hitOnDragEnabled: false, + /** + * Should we capture touch events and bind them to the touchstart target? That is how it works on DOM elements. + * The case: we touchstart on div1, then touchmove out of that element into another element div2. + * DOM will continue trigger touchmove events on div1 (not div2). Because events are "captured" into initial target. + * By default Konva do not do that and will trigger touchmove on another element, while pointer is moving. + * @property capturePointerEventsEnabled + * @default false + * @name capturePointerEventsEnabled + * @memberof Konva + * @example + * Konva.capturePointerEventsEnabled = true; + */ + capturePointerEventsEnabled: false, + _mouseListenClick: false, + _touchListenClick: false, + _pointerListenClick: false, + _mouseInDblClickWindow: false, + _touchInDblClickWindow: false, + _pointerInDblClickWindow: false, + _mouseDblClickPointerId: null, + _touchDblClickPointerId: null, + _pointerDblClickPointerId: null, + _fixTextRendering: false, + /** + * Global pixel ratio configuration. KonvaJS automatically detect pixel ratio of current device. + * But you may override such property, if you want to use your value. Set this value before any components initializations. + * @property pixelRatio + * @default undefined + * @name pixelRatio + * @memberof Konva + * @example + * // before any Konva code: + * Konva.pixelRatio = 1; + */ + pixelRatio: (typeof window !== 'undefined' && window.devicePixelRatio) || 1, + /** + * Drag distance property. If you start to drag a node you may want to wait until pointer is moved to some distance from start point, + * only then start dragging. Default is 3px. + * @property dragDistance + * @default 0 + * @memberof Konva + * @example + * Konva.dragDistance = 10; + */ + dragDistance: 3, + /** + * Use degree values for angle properties. You may set this property to false if you want to use radian values. + * @property angleDeg + * @default true + * @memberof Konva + * @example + * node.rotation(45); // 45 degrees + * Konva.angleDeg = false; + * node.rotation(Math.PI / 2); // PI/2 radian + */ + angleDeg: true, + /** + * Show different warnings about errors or wrong API usage + * @property showWarnings + * @default true + * @memberof Konva + * @example + * Konva.showWarnings = false; + */ + showWarnings: true, + /** + * Configure what mouse buttons can be used for drag and drop. + * Default value is [0] - only left mouse button. + * @property dragButtons + * @default true + * @memberof Konva + * @example + * // enable left and right mouse buttons + * Konva.dragButtons = [0, 2]; + */ + dragButtons: [0, 1], + /** + * returns whether or not drag and drop is currently active + * @method + * @memberof Konva + */ + isDragging() { + return Konva$2['DD'].isDragging; + }, + isTransforming() { + var _a; + return (_a = Konva$2['Transformer']) === null || _a === undefined ? undefined : _a.isTransforming(); + }, + /** + * returns whether or not a drag and drop operation is ready, but may + * not necessarily have started + * @method + * @memberof Konva + */ + isDragReady() { + return !!Konva$2['DD'].node; + }, + /** + * Should Konva release canvas elements on destroy. Default is true. + * Useful to avoid memory leak issues in Safari on macOS/iOS. + * @property releaseCanvasOnDestroy + * @default true + * @name releaseCanvasOnDestroy + * @memberof Konva + * @example + * Konva.releaseCanvasOnDestroy = true; + */ + releaseCanvasOnDestroy: true, + // user agent + document: glob.document, + // insert Konva into global namespace (window) + // it is required for npm packages + _injectGlobal(Konva) { + glob.Konva = Konva; + }, + }; + const _registerNode = (NodeClass) => { + Konva$2[NodeClass.prototype.getClassName()] = NodeClass; + }; + Konva$2._injectGlobal(Konva$2); + + /* + * Last updated November 2011 + * By Simon Sarris + * www.simonsarris.com + * sarris@acm.org + * + * Free to use and distribute at will + * So long as you are nice to people, etc + */ + /* + * The usage of this class was inspired by some of the work done by a forked + * project, KineticJS-Ext by Wappworks, which is based on Simon's Transform + * class. Modified by Eric Rowell + */ + /** + * Transform constructor. + * In most of the cases you don't need to use it in your app. Because it is for internal usage in Konva core. + * But there is a documentation for that class in case you still want + * to make some manual calculations. + * @constructor + * @param {Array} [m] Optional six-element matrix + * @memberof Konva + */ + class Transform { + constructor(m = [1, 0, 0, 1, 0, 0]) { + this.dirty = false; + this.m = (m && m.slice()) || [1, 0, 0, 1, 0, 0]; + } + reset() { + this.m[0] = 1; + this.m[1] = 0; + this.m[2] = 0; + this.m[3] = 1; + this.m[4] = 0; + this.m[5] = 0; + } + /** + * Copy Konva.Transform object + * @method + * @name Konva.Transform#copy + * @returns {Konva.Transform} + * @example + * const tr = shape.getTransform().copy() + */ + copy() { + return new Transform(this.m); + } + copyInto(tr) { + tr.m[0] = this.m[0]; + tr.m[1] = this.m[1]; + tr.m[2] = this.m[2]; + tr.m[3] = this.m[3]; + tr.m[4] = this.m[4]; + tr.m[5] = this.m[5]; + } + /** + * Transform point + * @method + * @name Konva.Transform#point + * @param {Object} point 2D point(x, y) + * @returns {Object} 2D point(x, y) + */ + point(point) { + const m = this.m; + return { + x: m[0] * point.x + m[2] * point.y + m[4], + y: m[1] * point.x + m[3] * point.y + m[5], + }; + } + /** + * Apply translation + * @method + * @name Konva.Transform#translate + * @param {Number} x + * @param {Number} y + * @returns {Konva.Transform} + */ + translate(x, y) { + this.m[4] += this.m[0] * x + this.m[2] * y; + this.m[5] += this.m[1] * x + this.m[3] * y; + return this; + } + /** + * Apply scale + * @method + * @name Konva.Transform#scale + * @param {Number} sx + * @param {Number} sy + * @returns {Konva.Transform} + */ + scale(sx, sy) { + this.m[0] *= sx; + this.m[1] *= sx; + this.m[2] *= sy; + this.m[3] *= sy; + return this; + } + /** + * Apply rotation + * @method + * @name Konva.Transform#rotate + * @param {Number} rad Angle in radians + * @returns {Konva.Transform} + */ + rotate(rad) { + const c = Math.cos(rad); + const s = Math.sin(rad); + const m11 = this.m[0] * c + this.m[2] * s; + const m12 = this.m[1] * c + this.m[3] * s; + const m21 = this.m[0] * -s + this.m[2] * c; + const m22 = this.m[1] * -s + this.m[3] * c; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + return this; + } + /** + * Returns the translation + * @method + * @name Konva.Transform#getTranslation + * @returns {Object} 2D point(x, y) + */ + getTranslation() { + return { + x: this.m[4], + y: this.m[5], + }; + } + /** + * Apply skew + * @method + * @name Konva.Transform#skew + * @param {Number} sx + * @param {Number} sy + * @returns {Konva.Transform} + */ + skew(sx, sy) { + const m11 = this.m[0] + this.m[2] * sy; + const m12 = this.m[1] + this.m[3] * sy; + const m21 = this.m[2] + this.m[0] * sx; + const m22 = this.m[3] + this.m[1] * sx; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + return this; + } + /** + * Transform multiplication + * @method + * @name Konva.Transform#multiply + * @param {Konva.Transform} matrix + * @returns {Konva.Transform} + */ + multiply(matrix) { + const m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1]; + const m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1]; + const m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3]; + const m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3]; + const dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4]; + const dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5]; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + this.m[4] = dx; + this.m[5] = dy; + return this; + } + /** + * Invert the matrix + * @method + * @name Konva.Transform#invert + * @returns {Konva.Transform} + */ + invert() { + const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); + const m0 = this.m[3] * d; + const m1 = -this.m[1] * d; + const m2 = -this.m[2] * d; + const m3 = this.m[0] * d; + const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); + const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); + this.m[0] = m0; + this.m[1] = m1; + this.m[2] = m2; + this.m[3] = m3; + this.m[4] = m4; + this.m[5] = m5; + return this; + } + /** + * return matrix + * @method + * @name Konva.Transform#getMatrix + */ + getMatrix() { + return this.m; + } + /** + * convert transformation matrix back into node's attributes + * @method + * @name Konva.Transform#decompose + * @returns {Konva.Transform} + */ + decompose() { + const a = this.m[0]; + const b = this.m[1]; + const c = this.m[2]; + const d = this.m[3]; + const e = this.m[4]; + const f = this.m[5]; + const delta = a * d - b * c; + const result = { + x: e, + y: f, + rotation: 0, + scaleX: 0, + scaleY: 0, + skewX: 0, + skewY: 0, + }; + // Apply the QR-like decomposition. + if (a != 0 || b != 0) { + const r = Math.sqrt(a * a + b * b); + result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r); + result.scaleX = r; + result.scaleY = delta / r; + result.skewX = (a * c + b * d) / delta; + result.skewY = 0; + } + else if (c != 0 || d != 0) { + const s = Math.sqrt(c * c + d * d); + result.rotation = + Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s)); + result.scaleX = delta / s; + result.scaleY = s; + result.skewX = 0; + result.skewY = (a * c + b * d) / delta; + } + else ; + result.rotation = Util._getRotation(result.rotation); + return result; + } + } + // CONSTANTS + const OBJECT_ARRAY = '[object Array]', OBJECT_NUMBER = '[object Number]', OBJECT_STRING = '[object String]', OBJECT_BOOLEAN = '[object Boolean]', PI_OVER_DEG180 = Math.PI / 180, DEG180_OVER_PI = 180 / Math.PI, HASH$1 = '#', EMPTY_STRING$1 = '', ZERO = '0', KONVA_WARNING = 'Konva warning: ', KONVA_ERROR = 'Konva error: ', RGB_PAREN = 'rgb(', COLORS = { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 132, 11], + darkgray: [169, 169, 169], + darkgreen: [0, 100, 0], + darkgrey: [169, 169, 169], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 255, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + grey: [128, 128, 128], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 203], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [119, 128, 144], + slategrey: [119, 128, 144], + snow: [255, 255, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + transparent: [255, 255, 255, 0], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 5], + }, RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/; + let animQueue = []; + const req = (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) || + function (f) { + setTimeout(f, 60); + }; + /** + * @namespace Util + * @memberof Konva + */ + const Util = { + /* + * cherry-picked utilities from underscore.js + */ + _isElement(obj) { + return !!(obj && obj.nodeType == 1); + }, + _isFunction(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }, + _isPlainObject(obj) { + return !!obj && obj.constructor === Object; + }, + _isArray(obj) { + return Object.prototype.toString.call(obj) === OBJECT_ARRAY; + }, + _isNumber(obj) { + return (Object.prototype.toString.call(obj) === OBJECT_NUMBER && + !isNaN(obj) && + isFinite(obj)); + }, + _isString(obj) { + return Object.prototype.toString.call(obj) === OBJECT_STRING; + }, + _isBoolean(obj) { + return Object.prototype.toString.call(obj) === OBJECT_BOOLEAN; + }, + // arrays are objects too + isObject(val) { + return val instanceof Object; + }, + isValidSelector(selector) { + if (typeof selector !== 'string') { + return false; + } + const firstChar = selector[0]; + return (firstChar === '#' || + firstChar === '.' || + firstChar === firstChar.toUpperCase()); + }, + _sign(number) { + if (number === 0) { + // that is not what sign usually returns + // but that is what we need + return 1; + } + if (number > 0) { + return 1; + } + else { + return -1; + } + }, + requestAnimFrame(callback) { + animQueue.push(callback); + if (animQueue.length === 1) { + req(function () { + const queue = animQueue; + animQueue = []; + queue.forEach(function (cb) { + cb(); + }); + }); + } + }, + createCanvasElement() { + const canvas = document.createElement('canvas'); + // on some environments canvas.style is readonly + try { + canvas.style = canvas.style || {}; + } + catch (e) { } + return canvas; + }, + createImageElement() { + return document.createElement('img'); + }, + _isInDocument(el) { + while ((el = el.parentNode)) { + if (el == document) { + return true; + } + } + return false; + }, + /* + * arg can be an image object or image data + */ + _urlToImage(url, callback) { + // if arg is a string, then it's a data url + const imageObj = Util.createImageElement(); + imageObj.onload = function () { + callback(imageObj); + }; + imageObj.src = url; + }, + _rgbToHex(r, g, b) { + return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + }, + _hexToRgb(hex) { + hex = hex.replace(HASH$1, EMPTY_STRING$1); + const bigint = parseInt(hex, 16); + return { + r: (bigint >> 16) & 255, + g: (bigint >> 8) & 255, + b: bigint & 255, + }; + }, + /** + * return random hex color + * @method + * @memberof Konva.Util + * @example + * shape.fill(Konva.Util.getRandomColor()); + */ + getRandomColor() { + let randColor = ((Math.random() * 0xffffff) << 0).toString(16); + while (randColor.length < 6) { + randColor = ZERO + randColor; + } + return HASH$1 + randColor; + }, + /** + * get RGB components of a color + * @method + * @memberof Konva.Util + * @param {String} color + * @example + * // each of the following examples return {r:0, g:0, b:255} + * var rgb = Konva.Util.getRGB('blue'); + * var rgb = Konva.Util.getRGB('#0000ff'); + * var rgb = Konva.Util.getRGB('rgb(0,0,255)'); + */ + getRGB(color) { + let rgb; + // color string + if (color in COLORS) { + rgb = COLORS[color]; + return { + r: rgb[0], + g: rgb[1], + b: rgb[2], + }; + } + else if (color[0] === HASH$1) { + // hex + return this._hexToRgb(color.substring(1)); + } + else if (color.substr(0, 4) === RGB_PAREN) { + // rgb string + rgb = RGB_REGEX.exec(color.replace(/ /g, '')); + return { + r: parseInt(rgb[1], 10), + g: parseInt(rgb[2], 10), + b: parseInt(rgb[3], 10), + }; + } + else { + // default + return { + r: 0, + g: 0, + b: 0, + }; + } + }, + // convert any color string to RGBA object + // from https://github.com/component/color-parser + colorToRGBA(str) { + str = str || 'black'; + return (Util._namedColorToRBA(str) || + Util._hex3ColorToRGBA(str) || + Util._hex4ColorToRGBA(str) || + Util._hex6ColorToRGBA(str) || + Util._hex8ColorToRGBA(str) || + Util._rgbColorToRGBA(str) || + Util._rgbaColorToRGBA(str) || + Util._hslColorToRGBA(str)); + }, + // Parse named css color. Like "green" + _namedColorToRBA(str) { + const c = COLORS[str.toLowerCase()]; + if (!c) { + return null; + } + return { + r: c[0], + g: c[1], + b: c[2], + a: 1, + }; + }, + // Parse rgb(n, n, n) + _rgbColorToRGBA(str) { + if (str.indexOf('rgb(') === 0) { + str = str.match(/rgb\(([^)]+)\)/)[1]; + const parts = str.split(/ *, */).map(Number); + return { + r: parts[0], + g: parts[1], + b: parts[2], + a: 1, + }; + } + }, + // Parse rgba(n, n, n, n) + _rgbaColorToRGBA(str) { + if (str.indexOf('rgba(') === 0) { + str = str.match(/rgba\(([^)]+)\)/)[1]; + const parts = str.split(/ *, */).map((n, index) => { + if (n.slice(-1) === '%') { + return index === 3 ? parseInt(n) / 100 : (parseInt(n) / 100) * 255; + } + return Number(n); + }); + return { + r: parts[0], + g: parts[1], + b: parts[2], + a: parts[3], + }; + } + }, + // Parse #nnnnnnnn + _hex8ColorToRGBA(str) { + if (str[0] === '#' && str.length === 9) { + return { + r: parseInt(str.slice(1, 3), 16), + g: parseInt(str.slice(3, 5), 16), + b: parseInt(str.slice(5, 7), 16), + a: parseInt(str.slice(7, 9), 16) / 0xff, + }; + } + }, + // Parse #nnnnnn + _hex6ColorToRGBA(str) { + if (str[0] === '#' && str.length === 7) { + return { + r: parseInt(str.slice(1, 3), 16), + g: parseInt(str.slice(3, 5), 16), + b: parseInt(str.slice(5, 7), 16), + a: 1, + }; + } + }, + // Parse #nnnn + _hex4ColorToRGBA(str) { + if (str[0] === '#' && str.length === 5) { + return { + r: parseInt(str[1] + str[1], 16), + g: parseInt(str[2] + str[2], 16), + b: parseInt(str[3] + str[3], 16), + a: parseInt(str[4] + str[4], 16) / 0xff, + }; + } + }, + // Parse #nnn + _hex3ColorToRGBA(str) { + if (str[0] === '#' && str.length === 4) { + return { + r: parseInt(str[1] + str[1], 16), + g: parseInt(str[2] + str[2], 16), + b: parseInt(str[3] + str[3], 16), + a: 1, + }; + } + }, + // Code adapted from https://github.com/Qix-/color-convert/blob/master/conversions.js#L244 + _hslColorToRGBA(str) { + // Check hsl() format + if (/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.test(str)) { + // Extract h, s, l + const [_, ...hsl] = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(str); + const h = Number(hsl[0]) / 360; + const s = Number(hsl[1]) / 100; + const l = Number(hsl[2]) / 100; + let t2; + let t3; + let val; + if (s === 0) { + val = l * 255; + return { + r: Math.round(val), + g: Math.round(val), + b: Math.round(val), + a: 1, + }; + } + if (l < 0.5) { + t2 = l * (1 + s); + } + else { + t2 = l + s - l * s; + } + const t1 = 2 * l - t2; + const rgb = [0, 0, 0]; + for (let i = 0; i < 3; i++) { + t3 = h + (1 / 3) * -(i - 1); + if (t3 < 0) { + t3++; + } + if (t3 > 1) { + t3--; + } + if (6 * t3 < 1) { + val = t1 + (t2 - t1) * 6 * t3; + } + else if (2 * t3 < 1) { + val = t2; + } + else if (3 * t3 < 2) { + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + } + else { + val = t1; + } + rgb[i] = val * 255; + } + return { + r: Math.round(rgb[0]), + g: Math.round(rgb[1]), + b: Math.round(rgb[2]), + a: 1, + }; + } + }, + /** + * check intersection of two client rectangles + * @method + * @memberof Konva.Util + * @param {Object} r1 - { x, y, width, height } client rectangle + * @param {Object} r2 - { x, y, width, height } client rectangle + * @example + * const overlapping = Konva.Util.haveIntersection(shape1.getClientRect(), shape2.getClientRect()); + */ + haveIntersection(r1, r2) { + return !(r2.x > r1.x + r1.width || + r2.x + r2.width < r1.x || + r2.y > r1.y + r1.height || + r2.y + r2.height < r1.y); + }, + cloneObject(obj) { + const retObj = {}; + for (const key in obj) { + if (this._isPlainObject(obj[key])) { + retObj[key] = this.cloneObject(obj[key]); + } + else if (this._isArray(obj[key])) { + retObj[key] = this.cloneArray(obj[key]); + } + else { + retObj[key] = obj[key]; + } + } + return retObj; + }, + cloneArray(arr) { + return arr.slice(0); + }, + degToRad(deg) { + return deg * PI_OVER_DEG180; + }, + radToDeg(rad) { + return rad * DEG180_OVER_PI; + }, + _degToRad(deg) { + Util.warn('Util._degToRad is removed. Please use public Util.degToRad instead.'); + return Util.degToRad(deg); + }, + _radToDeg(rad) { + Util.warn('Util._radToDeg is removed. Please use public Util.radToDeg instead.'); + return Util.radToDeg(rad); + }, + _getRotation(radians) { + return Konva$2.angleDeg ? Util.radToDeg(radians) : radians; + }, + _capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + }, + throw(str) { + throw new Error(KONVA_ERROR + str); + }, + error(str) { + console.error(KONVA_ERROR + str); + }, + warn(str) { + if (!Konva$2.showWarnings) { + return; + } + console.warn(KONVA_WARNING + str); + }, + each(obj, func) { + for (const key in obj) { + func(key, obj[key]); + } + }, + _inRange(val, left, right) { + return left <= val && val < right; + }, + _getProjectionToSegment(x1, y1, x2, y2, x3, y3) { + let x, y, dist; + const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); + if (pd2 == 0) { + x = x1; + y = y1; + dist = (x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2); + } + else { + const u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / pd2; + if (u < 0) { + x = x1; + y = y1; + dist = (x1 - x3) * (x1 - x3) + (y1 - y3) * (y1 - y3); + } + else if (u > 1.0) { + x = x2; + y = y2; + dist = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3); + } + else { + x = x1 + u * (x2 - x1); + y = y1 + u * (y2 - y1); + dist = (x - x3) * (x - x3) + (y - y3) * (y - y3); + } + } + return [x, y, dist]; + }, + // line as array of points. + // line might be closed + _getProjectionToLine(pt, line, isClosed) { + const pc = Util.cloneObject(pt); + let dist = Number.MAX_VALUE; + line.forEach(function (p1, i) { + if (!isClosed && i === line.length - 1) { + return; + } + const p2 = line[(i + 1) % line.length]; + const proj = Util._getProjectionToSegment(p1.x, p1.y, p2.x, p2.y, pt.x, pt.y); + const px = proj[0], py = proj[1], pdist = proj[2]; + if (pdist < dist) { + pc.x = px; + pc.y = py; + dist = pdist; + } + }); + return pc; + }, + _prepareArrayForTween(startArray, endArray, isClosed) { + const start = [], end = []; + if (startArray.length > endArray.length) { + const temp = endArray; + endArray = startArray; + startArray = temp; + } + for (let n = 0; n < startArray.length; n += 2) { + start.push({ + x: startArray[n], + y: startArray[n + 1], + }); + } + for (let n = 0; n < endArray.length; n += 2) { + end.push({ + x: endArray[n], + y: endArray[n + 1], + }); + } + const newStart = []; + end.forEach(function (point) { + const pr = Util._getProjectionToLine(point, start, isClosed); + newStart.push(pr.x); + newStart.push(pr.y); + }); + return newStart; + }, + _prepareToStringify(obj) { + let desc; + obj.visitedByCircularReferenceRemoval = true; + for (const key in obj) { + if (!(obj.hasOwnProperty(key) && obj[key] && typeof obj[key] == 'object')) { + continue; + } + desc = Object.getOwnPropertyDescriptor(obj, key); + if (obj[key].visitedByCircularReferenceRemoval || + Util._isElement(obj[key])) { + if (desc.configurable) { + delete obj[key]; + } + else { + return null; + } + } + else if (Util._prepareToStringify(obj[key]) === null) { + if (desc.configurable) { + delete obj[key]; + } + else { + return null; + } + } + } + delete obj.visitedByCircularReferenceRemoval; + return obj; + }, + // very simplified version of Object.assign + _assign(target, source) { + for (const key in source) { + target[key] = source[key]; + } + return target; + }, + _getFirstPointerId(evt) { + if (!evt.touches) { + // try to use pointer id or fake id + return evt.pointerId || 999; + } + else { + return evt.changedTouches[0].identifier; + } + }, + releaseCanvas(...canvases) { + if (!Konva$2.releaseCanvasOnDestroy) + return; + canvases.forEach((c) => { + c.width = 0; + c.height = 0; + }); + }, + drawRoundedRectPath(context, width, height, cornerRadius) { + let topLeft = 0; + let topRight = 0; + let bottomLeft = 0; + let bottomRight = 0; + if (typeof cornerRadius === 'number') { + topLeft = + topRight = + bottomLeft = + bottomRight = + Math.min(cornerRadius, width / 2, height / 2); + } + else { + topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2); + topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2); + bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2); + bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2); + } + context.moveTo(topLeft, 0); + context.lineTo(width - topRight, 0); + context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false); + context.lineTo(width, height - bottomRight); + context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false); + context.lineTo(bottomLeft, height); + context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false); + context.lineTo(0, topLeft); + context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); + }, + }; + + function _formatValue(val) { + if (Util._isString(val)) { + return '"' + val + '"'; + } + if (Object.prototype.toString.call(val) === '[object Number]') { + return val; + } + if (Util._isBoolean(val)) { + return val; + } + return Object.prototype.toString.call(val); + } + function RGBComponent(val) { + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + return Math.round(val); + } + function getNumberValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + if (!Util._isNumber(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a number.'); + } + return val; + }; + } + } + function getNumberOrArrayOfNumbersValidator(noOfElements) { + if (Konva$2.isUnminified) { + return function (val, attr) { + let isNumber = Util._isNumber(val); + let isValidArray = Util._isArray(val) && val.length == noOfElements; + if (!isNumber && !isValidArray) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a number or Array(' + + noOfElements + + ')'); + } + return val; + }; + } + } + function getNumberOrAutoValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + var isNumber = Util._isNumber(val); + var isAuto = val === 'auto'; + if (!(isNumber || isAuto)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a number or "auto".'); + } + return val; + }; + } + } + function getStringValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + if (!Util._isString(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a string.'); + } + return val; + }; + } + } + function getStringOrGradientValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + const isString = Util._isString(val); + const isGradient = Object.prototype.toString.call(val) === '[object CanvasGradient]' || + (val && val['addColorStop']); + if (!(isString || isGradient)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a string or a native gradient.'); + } + return val; + }; + } + } + function getNumberArrayValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + // Retrieve TypedArray constructor as found in MDN (if TypedArray is available) + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description + const TypedArray = Int8Array ? Object.getPrototypeOf(Int8Array) : null; + if (TypedArray && val instanceof TypedArray) { + return val; + } + if (!Util._isArray(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a array of numbers.'); + } + else { + val.forEach(function (item) { + if (!Util._isNumber(item)) { + Util.warn('"' + + attr + + '" attribute has non numeric element ' + + item + + '. Make sure that all elements are numbers.'); + } + }); + } + return val; + }; + } + } + function getBooleanValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + var isBool = val === true || val === false; + if (!isBool) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a boolean.'); + } + return val; + }; + } + } + function getComponentValidator(components) { + if (Konva$2.isUnminified) { + return function (val, attr) { + // ignore validation on undefined value, because it will reset to defalt + if (val === undefined || val === null) { + return val; + } + if (!Util.isObject(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be an object with properties ' + + components); + } + return val; + }; + } + } + + const GET = 'get'; + const SET$1 = 'set'; + const Factory = { + addGetterSetter(constructor, attr, def, validator, after) { + Factory.addGetter(constructor, attr, def); + Factory.addSetter(constructor, attr, validator, after); + Factory.addOverloadedGetterSetter(constructor, attr); + }, + addGetter(constructor, attr, def) { + var method = GET + Util._capitalize(attr); + constructor.prototype[method] = + constructor.prototype[method] || + function () { + const val = this.attrs[attr]; + return val === undefined ? def : val; + }; + }, + addSetter(constructor, attr, validator, after) { + var method = SET$1 + Util._capitalize(attr); + if (!constructor.prototype[method]) { + Factory.overWriteSetter(constructor, attr, validator, after); + } + }, + overWriteSetter(constructor, attr, validator, after) { + var method = SET$1 + Util._capitalize(attr); + constructor.prototype[method] = function (val) { + if (validator && val !== undefined && val !== null) { + val = validator.call(this, val, attr); + } + this._setAttr(attr, val); + if (after) { + after.call(this); + } + return this; + }; + }, + addComponentsGetterSetter(constructor, attr, components, validator, after) { + const len = components.length, capitalize = Util._capitalize, getter = GET + capitalize(attr), setter = SET$1 + capitalize(attr); + // getter + constructor.prototype[getter] = function () { + const ret = {}; + for (let n = 0; n < len; n++) { + const component = components[n]; + ret[component] = this.getAttr(attr + capitalize(component)); + } + return ret; + }; + const basicValidator = getComponentValidator(components); + // setter + constructor.prototype[setter] = function (val) { + const oldVal = this.attrs[attr]; + if (validator) { + val = validator.call(this, val, attr); + } + if (basicValidator) { + basicValidator.call(this, val, attr); + } + for (const key in val) { + if (!val.hasOwnProperty(key)) { + continue; + } + this._setAttr(attr + capitalize(key), val[key]); + } + if (!val) { + components.forEach((component) => { + this._setAttr(attr + capitalize(component), undefined); + }); + } + this._fireChangeEvent(attr, oldVal, val); + if (after) { + after.call(this); + } + return this; + }; + Factory.addOverloadedGetterSetter(constructor, attr); + }, + addOverloadedGetterSetter(constructor, attr) { + var capitalizedAttr = Util._capitalize(attr), setter = SET$1 + capitalizedAttr, getter = GET + capitalizedAttr; + constructor.prototype[attr] = function () { + // setting + if (arguments.length) { + this[setter](arguments[0]); + return this; + } + // getting + return this[getter](); + }; + }, + addDeprecatedGetterSetter(constructor, attr, def, validator) { + Util.error('Adding deprecated ' + attr); + const method = GET + Util._capitalize(attr); + const message = attr + + ' property is deprecated and will be removed soon. Look at Konva change log for more information.'; + constructor.prototype[method] = function () { + Util.error(message); + const val = this.attrs[attr]; + return val === undefined ? def : val; + }; + Factory.addSetter(constructor, attr, validator, function () { + Util.error(message); + }); + Factory.addOverloadedGetterSetter(constructor, attr); + }, + backCompat(constructor, methods) { + Util.each(methods, function (oldMethodName, newMethodName) { + const method = constructor.prototype[newMethodName]; + const oldGetter = GET + Util._capitalize(oldMethodName); + const oldSetter = SET$1 + Util._capitalize(oldMethodName); + function deprecated() { + method.apply(this, arguments); + Util.error('"' + + oldMethodName + + '" method is deprecated and will be removed soon. Use ""' + + newMethodName + + '" instead.'); + } + constructor.prototype[oldMethodName] = deprecated; + constructor.prototype[oldGetter] = deprecated; + constructor.prototype[oldSetter] = deprecated; + }); + }, + afterSetFilter() { + this._filterUpToDate = false; + }, + }; + + function simplifyArray(arr) { + const retArr = [], len = arr.length, util = Util; + for (let n = 0; n < len; n++) { + let val = arr[n]; + if (util._isNumber(val)) { + val = Math.round(val * 1000) / 1000; + } + else if (!util._isString(val)) { + val = val + ''; + } + retArr.push(val); + } + return retArr; + } + const COMMA = ',', OPEN_PAREN = '(', CLOSE_PAREN = ')', OPEN_PAREN_BRACKET = '([', CLOSE_BRACKET_PAREN = '])', SEMICOLON = ';', DOUBLE_PAREN = '()', + // EMPTY_STRING = '', + EQUALS = '=', + // SET = 'set', + CONTEXT_METHODS = [ + 'arc', + 'arcTo', + 'beginPath', + 'bezierCurveTo', + 'clearRect', + 'clip', + 'closePath', + 'createLinearGradient', + 'createPattern', + 'createRadialGradient', + 'drawImage', + 'ellipse', + 'fill', + 'fillText', + 'getImageData', + 'createImageData', + 'lineTo', + 'moveTo', + 'putImageData', + 'quadraticCurveTo', + 'rect', + 'roundRect', + 'restore', + 'rotate', + 'save', + 'scale', + 'setLineDash', + 'setTransform', + 'stroke', + 'strokeText', + 'transform', + 'translate', + ]; + const CONTEXT_PROPERTIES = [ + 'fillStyle', + 'strokeStyle', + 'shadowColor', + 'shadowBlur', + 'shadowOffsetX', + 'shadowOffsetY', + 'letterSpacing', + 'lineCap', + 'lineDashOffset', + 'lineJoin', + 'lineWidth', + 'miterLimit', + 'direction', + 'font', + 'textAlign', + 'textBaseline', + 'globalAlpha', + 'globalCompositeOperation', + 'imageSmoothingEnabled', + ]; + const traceArrMax = 100; + /** + * Konva wrapper around native 2d canvas context. It has almost the same API of 2d context with some additional functions. + * With core Konva shapes you don't need to use this object. But you will use it if you want to create + * a [custom shape](/docs/react/Custom_Shape.html) or a [custom hit regions](/docs/events/Custom_Hit_Region.html). + * For full information about each 2d context API use [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) + * @constructor + * @memberof Konva + * @example + * const rect = new Konva.Shape({ + * fill: 'red', + * width: 100, + * height: 100, + * sceneFunc: (ctx, shape) => { + * // ctx - is context wrapper + * // shape - is instance of Konva.Shape, so it equals to "rect" variable + * ctx.rect(0, 0, shape.getAttr('width'), shape.getAttr('height')); + * + * // automatically fill shape from props and draw hit region + * ctx.fillStrokeShape(shape); + * } + * }) + */ + class Context { + constructor(canvas) { + this.canvas = canvas; + if (Konva$2.enableTrace) { + this.traceArr = []; + this._enableTrace(); + } + } + /** + * fill shape + * @method + * @name Konva.Context#fillShape + * @param {Konva.Shape} shape + */ + fillShape(shape) { + if (shape.fillEnabled()) { + this._fill(shape); + } + } + _fill(shape) { + // abstract + } + /** + * stroke shape + * @method + * @name Konva.Context#strokeShape + * @param {Konva.Shape} shape + */ + strokeShape(shape) { + if (shape.hasStroke()) { + this._stroke(shape); + } + } + _stroke(shape) { + // abstract + } + /** + * fill then stroke + * @method + * @name Konva.Context#fillStrokeShape + * @param {Konva.Shape} shape + */ + fillStrokeShape(shape) { + if (shape.attrs.fillAfterStrokeEnabled) { + this.strokeShape(shape); + this.fillShape(shape); + } + else { + this.fillShape(shape); + this.strokeShape(shape); + } + } + getTrace(relaxed, rounded) { + let traceArr = this.traceArr, len = traceArr.length, str = '', n, trace, method, args; + for (n = 0; n < len; n++) { + trace = traceArr[n]; + method = trace.method; + // methods + if (method) { + args = trace.args; + str += method; + if (relaxed) { + str += DOUBLE_PAREN; + } + else { + if (Util._isArray(args[0])) { + str += OPEN_PAREN_BRACKET + args.join(COMMA) + CLOSE_BRACKET_PAREN; + } + else { + if (rounded) { + args = args.map((a) => typeof a === 'number' ? Math.floor(a) : a); + } + str += OPEN_PAREN + args.join(COMMA) + CLOSE_PAREN; + } + } + } + else { + // properties + str += trace.property; + if (!relaxed) { + str += EQUALS + trace.val; + } + } + str += SEMICOLON; + } + return str; + } + clearTrace() { + this.traceArr = []; + } + _trace(str) { + let traceArr = this.traceArr, len; + traceArr.push(str); + len = traceArr.length; + if (len >= traceArrMax) { + traceArr.shift(); + } + } + /** + * reset canvas context transform + * @method + * @name Konva.Context#reset + */ + reset() { + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(1 * pixelRatio, 0, 0, 1 * pixelRatio, 0, 0); + } + /** + * get canvas wrapper + * @method + * @name Konva.Context#getCanvas + * @returns {Konva.Canvas} + */ + getCanvas() { + return this.canvas; + } + /** + * clear canvas + * @method + * @name Konva.Context#clear + * @param {Object} [bounds] + * @param {Number} [bounds.x] + * @param {Number} [bounds.y] + * @param {Number} [bounds.width] + * @param {Number} [bounds.height] + */ + clear(bounds) { + const canvas = this.getCanvas(); + if (bounds) { + this.clearRect(bounds.x || 0, bounds.y || 0, bounds.width || 0, bounds.height || 0); + } + else { + this.clearRect(0, 0, canvas.getWidth() / canvas.pixelRatio, canvas.getHeight() / canvas.pixelRatio); + } + } + _applyLineCap(shape) { + const lineCap = shape.attrs.lineCap; + if (lineCap) { + this.setAttr('lineCap', lineCap); + } + } + _applyOpacity(shape) { + const absOpacity = shape.getAbsoluteOpacity(); + if (absOpacity !== 1) { + this.setAttr('globalAlpha', absOpacity); + } + } + _applyLineJoin(shape) { + const lineJoin = shape.attrs.lineJoin; + if (lineJoin) { + this.setAttr('lineJoin', lineJoin); + } + } + setAttr(attr, val) { + this._context[attr] = val; + } + /** + * arc function. + * @method + * @name Konva.Context#arc + */ + arc(x, y, radius, startAngle, endAngle, counterClockwise) { + this._context.arc(x, y, radius, startAngle, endAngle, counterClockwise); + } + /** + * arcTo function. + * @method + * @name Konva.Context#arcTo + * + */ + arcTo(x1, y1, x2, y2, radius) { + this._context.arcTo(x1, y1, x2, y2, radius); + } + /** + * beginPath function. + * @method + * @name Konva.Context#beginPath + */ + beginPath() { + this._context.beginPath(); + } + /** + * bezierCurveTo function. + * @method + * @name Konva.Context#bezierCurveTo + */ + bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { + this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); + } + /** + * clearRect function. + * @method + * @name Konva.Context#clearRect + */ + clearRect(x, y, width, height) { + this._context.clearRect(x, y, width, height); + } + clip(...args) { + this._context.clip.apply(this._context, args); + } + /** + * closePath function. + * @method + * @name Konva.Context#closePath + */ + closePath() { + this._context.closePath(); + } + /** + * createImageData function. + * @method + * @name Konva.Context#createImageData + */ + createImageData(width, height) { + const a = arguments; + if (a.length === 2) { + return this._context.createImageData(width, height); + } + else if (a.length === 1) { + return this._context.createImageData(width); + } + } + /** + * createLinearGradient function. + * @method + * @name Konva.Context#createLinearGradient + */ + createLinearGradient(x0, y0, x1, y1) { + return this._context.createLinearGradient(x0, y0, x1, y1); + } + /** + * createPattern function. + * @method + * @name Konva.Context#createPattern + */ + createPattern(image, repetition) { + return this._context.createPattern(image, repetition); + } + /** + * createRadialGradient function. + * @method + * @name Konva.Context#createRadialGradient + */ + createRadialGradient(x0, y0, r0, x1, y1, r1) { + return this._context.createRadialGradient(x0, y0, r0, x1, y1, r1); + } + /** + * drawImage function. + * @method + * @name Konva.Context#drawImage + */ + drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) { + // this._context.drawImage(...arguments); + const a = arguments, _context = this._context; + if (a.length === 3) { + _context.drawImage(image, sx, sy); + } + else if (a.length === 5) { + _context.drawImage(image, sx, sy, sWidth, sHeight); + } + else if (a.length === 9) { + _context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); + } + } + /** + * ellipse function. + * @method + * @name Konva.Context#ellipse + */ + ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) { + this._context.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise); + } + /** + * isPointInPath function. + * @method + * @name Konva.Context#isPointInPath + */ + isPointInPath(x, y, path, fillRule) { + if (path) { + return this._context.isPointInPath(path, x, y, fillRule); + } + return this._context.isPointInPath(x, y, fillRule); + } + fill(...args) { + // this._context.fill(); + this._context.fill.apply(this._context, args); + } + /** + * fillRect function. + * @method + * @name Konva.Context#fillRect + */ + fillRect(x, y, width, height) { + this._context.fillRect(x, y, width, height); + } + /** + * strokeRect function. + * @method + * @name Konva.Context#strokeRect + */ + strokeRect(x, y, width, height) { + this._context.strokeRect(x, y, width, height); + } + /** + * fillText function. + * @method + * @name Konva.Context#fillText + */ + fillText(text, x, y, maxWidth) { + if (maxWidth) { + this._context.fillText(text, x, y, maxWidth); + } + else { + this._context.fillText(text, x, y); + } + } + /** + * measureText function. + * @method + * @name Konva.Context#measureText + */ + measureText(text) { + return this._context.measureText(text); + } + /** + * getImageData function. + * @method + * @name Konva.Context#getImageData + */ + getImageData(sx, sy, sw, sh) { + return this._context.getImageData(sx, sy, sw, sh); + } + /** + * lineTo function. + * @method + * @name Konva.Context#lineTo + */ + lineTo(x, y) { + this._context.lineTo(x, y); + } + /** + * moveTo function. + * @method + * @name Konva.Context#moveTo + */ + moveTo(x, y) { + this._context.moveTo(x, y); + } + /** + * rect function. + * @method + * @name Konva.Context#rect + */ + rect(x, y, width, height) { + this._context.rect(x, y, width, height); + } + /** + * roundRect function. + * @method + * @name Konva.Context#roundRect + */ + roundRect(x, y, width, height, radii) { + this._context.roundRect(x, y, width, height, radii); + } + /** + * putImageData function. + * @method + * @name Konva.Context#putImageData + */ + putImageData(imageData, dx, dy) { + this._context.putImageData(imageData, dx, dy); + } + /** + * quadraticCurveTo function. + * @method + * @name Konva.Context#quadraticCurveTo + */ + quadraticCurveTo(cpx, cpy, x, y) { + this._context.quadraticCurveTo(cpx, cpy, x, y); + } + /** + * restore function. + * @method + * @name Konva.Context#restore + */ + restore() { + this._context.restore(); + } + /** + * rotate function. + * @method + * @name Konva.Context#rotate + */ + rotate(angle) { + this._context.rotate(angle); + } + /** + * save function. + * @method + * @name Konva.Context#save + */ + save() { + this._context.save(); + } + /** + * scale function. + * @method + * @name Konva.Context#scale + */ + scale(x, y) { + this._context.scale(x, y); + } + /** + * setLineDash function. + * @method + * @name Konva.Context#setLineDash + */ + setLineDash(segments) { + // works for Chrome and IE11 + if (this._context.setLineDash) { + this._context.setLineDash(segments); + } + else if ('mozDash' in this._context) { + // verified that this works in firefox + this._context['mozDash'] = segments; + } + else if ('webkitLineDash' in this._context) { + // does not currently work for Safari + this._context['webkitLineDash'] = segments; + } + // no support for IE9 and IE10 + } + /** + * getLineDash function. + * @method + * @name Konva.Context#getLineDash + */ + getLineDash() { + return this._context.getLineDash(); + } + /** + * setTransform function. + * @method + * @name Konva.Context#setTransform + */ + setTransform(a, b, c, d, e, f) { + this._context.setTransform(a, b, c, d, e, f); + } + /** + * stroke function. + * @method + * @name Konva.Context#stroke + */ + stroke(path2d) { + if (path2d) { + this._context.stroke(path2d); + } + else { + this._context.stroke(); + } + } + /** + * strokeText function. + * @method + * @name Konva.Context#strokeText + */ + strokeText(text, x, y, maxWidth) { + this._context.strokeText(text, x, y, maxWidth); + } + /** + * transform function. + * @method + * @name Konva.Context#transform + */ + transform(a, b, c, d, e, f) { + this._context.transform(a, b, c, d, e, f); + } + /** + * translate function. + * @method + * @name Konva.Context#translate + */ + translate(x, y) { + this._context.translate(x, y); + } + _enableTrace() { + let that = this, len = CONTEXT_METHODS.length, origSetter = this.setAttr, n, args; + // to prevent creating scope function at each loop + const func = function (methodName) { + let origMethod = that[methodName], ret; + that[methodName] = function () { + args = simplifyArray(Array.prototype.slice.call(arguments, 0)); + ret = origMethod.apply(that, arguments); + that._trace({ + method: methodName, + args: args, + }); + return ret; + }; + }; + // methods + for (n = 0; n < len; n++) { + func(CONTEXT_METHODS[n]); + } + // attrs + that.setAttr = function () { + origSetter.apply(that, arguments); + const prop = arguments[0]; + let val = arguments[1]; + if (prop === 'shadowOffsetX' || + prop === 'shadowOffsetY' || + prop === 'shadowBlur') { + val = val / this.canvas.getPixelRatio(); + } + that._trace({ + property: prop, + val: val, + }); + }; + } + _applyGlobalCompositeOperation(node) { + const op = node.attrs.globalCompositeOperation; + const def = !op || op === 'source-over'; + if (!def) { + this.setAttr('globalCompositeOperation', op); + } + } + } + CONTEXT_PROPERTIES.forEach(function (prop) { + Object.defineProperty(Context.prototype, prop, { + get() { + return this._context[prop]; + }, + set(val) { + this._context[prop] = val; + }, + }); + }); + class SceneContext extends Context { + constructor(canvas, { willReadFrequently = false } = {}) { + super(canvas); + this._context = canvas._canvas.getContext('2d', { + willReadFrequently, + }); + } + _fillColor(shape) { + const fill = shape.fill(); + this.setAttr('fillStyle', fill); + shape._fillFunc(this); + } + _fillPattern(shape) { + this.setAttr('fillStyle', shape._getFillPattern()); + shape._fillFunc(this); + } + _fillLinearGradient(shape) { + const grd = shape._getLinearGradient(); + if (grd) { + this.setAttr('fillStyle', grd); + shape._fillFunc(this); + } + } + _fillRadialGradient(shape) { + const grd = shape._getRadialGradient(); + if (grd) { + this.setAttr('fillStyle', grd); + shape._fillFunc(this); + } + } + _fill(shape) { + const hasColor = shape.fill(), fillPriority = shape.getFillPriority(); + // priority fills + if (hasColor && fillPriority === 'color') { + this._fillColor(shape); + return; + } + const hasPattern = shape.getFillPatternImage(); + if (hasPattern && fillPriority === 'pattern') { + this._fillPattern(shape); + return; + } + const hasLinearGradient = shape.getFillLinearGradientColorStops(); + if (hasLinearGradient && fillPriority === 'linear-gradient') { + this._fillLinearGradient(shape); + return; + } + const hasRadialGradient = shape.getFillRadialGradientColorStops(); + if (hasRadialGradient && fillPriority === 'radial-gradient') { + this._fillRadialGradient(shape); + return; + } + // now just try and fill with whatever is available + if (hasColor) { + this._fillColor(shape); + } + else if (hasPattern) { + this._fillPattern(shape); + } + else if (hasLinearGradient) { + this._fillLinearGradient(shape); + } + else if (hasRadialGradient) { + this._fillRadialGradient(shape); + } + } + _strokeLinearGradient(shape) { + const start = shape.getStrokeLinearGradientStartPoint(), end = shape.getStrokeLinearGradientEndPoint(), colorStops = shape.getStrokeLinearGradientColorStops(), grd = this.createLinearGradient(start.x, start.y, end.x, end.y); + if (colorStops) { + // build color stops + for (let n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + this.setAttr('strokeStyle', grd); + } + } + _stroke(shape) { + const dash = shape.dash(), + // ignore strokeScaleEnabled for Text + strokeScaleEnabled = shape.getStrokeScaleEnabled(); + if (shape.hasStroke()) { + if (!strokeScaleEnabled) { + this.save(); + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + } + this._applyLineCap(shape); + if (dash && shape.dashEnabled()) { + this.setLineDash(dash); + this.setAttr('lineDashOffset', shape.dashOffset()); + } + this.setAttr('lineWidth', shape.strokeWidth()); + if (!shape.getShadowForStrokeEnabled()) { + this.setAttr('shadowColor', 'rgba(0,0,0,0)'); + } + const hasLinearGradient = shape.getStrokeLinearGradientColorStops(); + if (hasLinearGradient) { + this._strokeLinearGradient(shape); + } + else { + this.setAttr('strokeStyle', shape.stroke()); + } + shape._strokeFunc(this); + if (!strokeScaleEnabled) { + this.restore(); + } + } + } + _applyShadow(shape) { + var _a, _b, _c; + const color = (_a = shape.getShadowRGBA()) !== null && _a !== undefined ? _a : 'black', blur = (_b = shape.getShadowBlur()) !== null && _b !== undefined ? _b : 5, offset = (_c = shape.getShadowOffset()) !== null && _c !== undefined ? _c : { + x: 0, + y: 0, + }, scale = shape.getAbsoluteScale(), ratio = this.canvas.getPixelRatio(), scaleX = scale.x * ratio, scaleY = scale.y * ratio; + this.setAttr('shadowColor', color); + this.setAttr('shadowBlur', blur * Math.min(Math.abs(scaleX), Math.abs(scaleY))); + this.setAttr('shadowOffsetX', offset.x * scaleX); + this.setAttr('shadowOffsetY', offset.y * scaleY); + } + } + class HitContext extends Context { + constructor(canvas) { + super(canvas); + this._context = canvas._canvas.getContext('2d', { + willReadFrequently: true, + }); + } + _fill(shape) { + this.save(); + this.setAttr('fillStyle', shape.colorKey); + shape._fillFuncHit(this); + this.restore(); + } + strokeShape(shape) { + if (shape.hasHitStroke()) { + this._stroke(shape); + } + } + _stroke(shape) { + if (shape.hasHitStroke()) { + // ignore strokeScaleEnabled for Text + const strokeScaleEnabled = shape.getStrokeScaleEnabled(); + if (!strokeScaleEnabled) { + this.save(); + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + } + this._applyLineCap(shape); + const hitStrokeWidth = shape.hitStrokeWidth(); + const strokeWidth = hitStrokeWidth === 'auto' ? shape.strokeWidth() : hitStrokeWidth; + this.setAttr('lineWidth', strokeWidth); + this.setAttr('strokeStyle', shape.colorKey); + shape._strokeFuncHit(this); + if (!strokeScaleEnabled) { + this.restore(); + } + } + } + } + + // calculate pixel ratio + let _pixelRatio; + function getDevicePixelRatio() { + if (_pixelRatio) { + return _pixelRatio; + } + const canvas = Util.createCanvasElement(); + const context = canvas.getContext('2d'); + _pixelRatio = (function () { + const devicePixelRatio = Konva$2._global.devicePixelRatio || 1, backingStoreRatio = context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || + 1; + return devicePixelRatio / backingStoreRatio; + })(); + Util.releaseCanvas(canvas); + return _pixelRatio; + } + /** + * Canvas Renderer constructor. It is a wrapper around native canvas element. + * Usually you don't need to use it manually. + * @constructor + * @abstract + * @memberof Konva + * @param {Object} config + * @param {Number} config.width + * @param {Number} config.height + * @param {Number} config.pixelRatio + */ + class Canvas { + constructor(config) { + this.pixelRatio = 1; + this.width = 0; + this.height = 0; + this.isCache = false; + const conf = config || {}; + const pixelRatio = conf.pixelRatio || Konva$2.pixelRatio || getDevicePixelRatio(); + this.pixelRatio = pixelRatio; + this._canvas = Util.createCanvasElement(); + // set inline styles + this._canvas.style.padding = '0'; + this._canvas.style.margin = '0'; + this._canvas.style.border = '0'; + this._canvas.style.background = 'transparent'; + this._canvas.style.position = 'absolute'; + this._canvas.style.top = '0'; + this._canvas.style.left = '0'; + } + /** + * get canvas context + * @method + * @name Konva.Canvas#getContext + * @returns {CanvasContext} context + */ + getContext() { + return this.context; + } + /** + * get pixel ratio + * @method + * @name Konva.Canvas#getPixelRatio + * @returns {Number} pixel ratio + * @example + * var pixelRatio = layer.getCanvas.getPixelRatio(); + */ + getPixelRatio() { + return this.pixelRatio; + } + /** + * set pixel ratio + * KonvaJS automatically handles pixel ratio adustments in order to render crisp drawings + * on all devices. Most desktops, low end tablets, and low end phones, have device pixel ratios + * of 1. Some high end tablets and phones, like iPhones and iPads have a device pixel ratio + * of 2. Some Macbook Pros, and iMacs also have a device pixel ratio of 2. Some high end Android devices have pixel + * ratios of 2 or 3. Some browsers like Firefox allow you to configure the pixel ratio of the viewport. Unless otherwise + * specificed, the pixel ratio will be defaulted to the actual device pixel ratio. You can override the device pixel + * ratio for special situations, or, if you don't want the pixel ratio to be taken into account, you can set it to 1. + * @method + * @name Konva.Canvas#setPixelRatio + * @param {Number} pixelRatio + * @example + * layer.getCanvas().setPixelRatio(3); + */ + setPixelRatio(pixelRatio) { + const previousRatio = this.pixelRatio; + this.pixelRatio = pixelRatio; + this.setSize(this.getWidth() / previousRatio, this.getHeight() / previousRatio); + } + setWidth(width) { + // take into account pixel ratio + this.width = this._canvas.width = width * this.pixelRatio; + this._canvas.style.width = width + 'px'; + const pixelRatio = this.pixelRatio, _context = this.getContext()._context; + _context.scale(pixelRatio, pixelRatio); + } + setHeight(height) { + // take into account pixel ratio + this.height = this._canvas.height = height * this.pixelRatio; + this._canvas.style.height = height + 'px'; + const pixelRatio = this.pixelRatio, _context = this.getContext()._context; + _context.scale(pixelRatio, pixelRatio); + } + getWidth() { + return this.width; + } + getHeight() { + return this.height; + } + setSize(width, height) { + this.setWidth(width || 0); + this.setHeight(height || 0); + } + /** + * to data url + * @method + * @name Konva.Canvas#toDataURL + * @param {String} mimeType + * @param {Number} quality between 0 and 1 for jpg mime types + * @returns {String} data url string + */ + toDataURL(mimeType, quality) { + try { + // If this call fails (due to browser bug, like in Firefox 3.6), + // then revert to previous no-parameter image/png behavior + return this._canvas.toDataURL(mimeType, quality); + } + catch (e) { + try { + return this._canvas.toDataURL(); + } + catch (err) { + Util.error('Unable to get data URL. ' + + err.message + + ' For more info read https://konvajs.org/docs/posts/Tainted_Canvas.html.'); + return ''; + } + } + } + } + class SceneCanvas extends Canvas { + constructor(config = { width: 0, height: 0, willReadFrequently: false }) { + super(config); + this.context = new SceneContext(this, { + willReadFrequently: config.willReadFrequently, + }); + this.setSize(config.width, config.height); + } + } + class HitCanvas extends Canvas { + constructor(config = { width: 0, height: 0 }) { + super(config); + this.hitCanvas = true; + this.context = new HitContext(this); + this.setSize(config.width, config.height); + } + } + + const DD = { + get isDragging() { + let flag = false; + DD._dragElements.forEach((elem) => { + if (elem.dragStatus === 'dragging') { + flag = true; + } + }); + return flag; + }, + justDragged: false, + get node() { + // return first dragging node + let node; + DD._dragElements.forEach((elem) => { + node = elem.node; + }); + return node; + }, + _dragElements: new Map(), + // methods + _drag(evt) { + const nodesToFireEvents = []; + DD._dragElements.forEach((elem, key) => { + const { node } = elem; + // we need to find pointer relative to that node + const stage = node.getStage(); + stage.setPointersPositions(evt); + // it is possible that user call startDrag without any event + // it that case we need to detect first movable pointer and attach it into the node + if (elem.pointerId === undefined) { + elem.pointerId = Util._getFirstPointerId(evt); + } + const pos = stage._changedPointerPositions.find((pos) => pos.id === elem.pointerId); + // not related pointer + if (!pos) { + return; + } + if (elem.dragStatus !== 'dragging') { + const dragDistance = node.dragDistance(); + const distance = Math.max(Math.abs(pos.x - elem.startPointerPos.x), Math.abs(pos.y - elem.startPointerPos.y)); + if (distance < dragDistance) { + return; + } + node.startDrag({ evt }); + // a user can stop dragging inside `dragstart` + if (!node.isDragging()) { + return; + } + } + node._setDragPosition(evt, elem); + nodesToFireEvents.push(node); + }); + // call dragmove only after ALL positions are changed + nodesToFireEvents.forEach((node) => { + node.fire('dragmove', { + type: 'dragmove', + target: node, + evt: evt, + }, true); + }); + }, + // dragBefore and dragAfter allows us to set correct order of events + // setup all in dragbefore, and stop dragging only after pointerup triggered. + _endDragBefore(evt) { + const drawNodes = []; + DD._dragElements.forEach((elem) => { + const { node } = elem; + // we need to find pointer relative to that node + const stage = node.getStage(); + if (evt) { + stage.setPointersPositions(evt); + } + const pos = stage._changedPointerPositions.find((pos) => pos.id === elem.pointerId); + // that pointer is not related + if (!pos) { + return; + } + if (elem.dragStatus === 'dragging' || elem.dragStatus === 'stopped') { + // if a node is stopped manually we still need to reset events: + DD.justDragged = true; + Konva$2._mouseListenClick = false; + Konva$2._touchListenClick = false; + Konva$2._pointerListenClick = false; + elem.dragStatus = 'stopped'; + } + const drawNode = elem.node.getLayer() || + (elem.node instanceof Konva$2['Stage'] && elem.node); + if (drawNode && drawNodes.indexOf(drawNode) === -1) { + drawNodes.push(drawNode); + } + }); + // draw in a sync way + // because mousemove event may trigger BEFORE batch draw is called + // but as we have not hit canvas updated yet, it will trigger incorrect mouseover/mouseout events + drawNodes.forEach((drawNode) => { + drawNode.draw(); + }); + }, + _endDragAfter(evt) { + DD._dragElements.forEach((elem, key) => { + if (elem.dragStatus === 'stopped') { + elem.node.fire('dragend', { + type: 'dragend', + target: elem.node, + evt: evt, + }, true); + } + if (elem.dragStatus !== 'dragging') { + DD._dragElements.delete(key); + } + }); + }, + }; + if (Konva$2.isBrowser) { + window.addEventListener('mouseup', DD._endDragBefore, true); + window.addEventListener('touchend', DD._endDragBefore, true); + // add touchcancel to fix this: https://github.com/konvajs/konva/issues/1843 + window.addEventListener('touchcancel', DD._endDragBefore, true); + window.addEventListener('mousemove', DD._drag); + window.addEventListener('touchmove', DD._drag); + window.addEventListener('mouseup', DD._endDragAfter, false); + window.addEventListener('touchend', DD._endDragAfter, false); + window.addEventListener('touchcancel', DD._endDragAfter, false); + } + + // CONSTANTS + const ABSOLUTE_OPACITY = 'absoluteOpacity', ALL_LISTENERS = 'allEventListeners', ABSOLUTE_TRANSFORM = 'absoluteTransform', ABSOLUTE_SCALE = 'absoluteScale', CANVAS = 'canvas', CHANGE = 'Change', CHILDREN = 'children', KONVA = 'konva', LISTENING = 'listening', MOUSEENTER$1 = 'mouseenter', MOUSELEAVE$1 = 'mouseleave', SET = 'set', SHAPE = 'Shape', SPACE$1 = ' ', STAGE$1 = 'stage', TRANSFORM = 'transform', UPPER_STAGE = 'Stage', VISIBLE = 'visible', TRANSFORM_CHANGE_STR$1 = [ + 'xChange.konva', + 'yChange.konva', + 'scaleXChange.konva', + 'scaleYChange.konva', + 'skewXChange.konva', + 'skewYChange.konva', + 'rotationChange.konva', + 'offsetXChange.konva', + 'offsetYChange.konva', + 'transformsEnabledChange.konva', + ].join(SPACE$1); + let idCounter$1 = 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 + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + */ + class Node { + constructor(config) { + this._id = idCounter$1++; + this.eventListeners = {}; + this.attrs = {}; + this.index = 0; + this._allEventListeners = null; + this.parent = null; + this._cache = new Map(); + this._attachedDepsListeners = new Map(); + this._lastPos = 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.get(attr)) { + this._cache.get(attr).dirty = true; + } + else if (attr) { + this._cache.delete(attr); + } + else { + this._cache.clear(); + } + } + _getCache(attr, privateGetter) { + let cache = this._cache.get(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.set(attr, cache); + } + return cache; + } + _calculate(name, deps, getter) { + // if we are trying to calculate function for the first time + // we need to attach listeners for change events + if (!this._attachedDepsListeners.get(name)) { + const depsString = deps.map((dep) => dep + 'Change.konva').join(SPACE$1); + this.on(depsString, () => { + this._clearCache(name); + }); + this._attachedDepsListeners.set(name, true); + } + // just use cache function + return this._getCache(name, getter); + } + _getCanvasCache() { + return this._cache.get(CANVAS); + } + /* + * 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 === ABSOLUTE_TRANSFORM) { + this.fire('absoluteTransformChange'); + } + } + /** + * clear cached canvas + * @method + * @name Konva.Node#clearCache + * @returns {Konva.Node} + * @example + * node.clearCache(); + */ + clearCache() { + if (this._cache.has(CANVAS)) { + const { scene, filter, hit } = this._cache.get(CANVAS); + Util.releaseCanvas(scene, filter, hit); + this._cache.delete(CANVAS); + } + 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] change quality (or pixel ratio) of cached image. pixelRatio = 2 will produce 2x sized cache. + * @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) { + rect = this.getClientRect({ + skipTransform: true, + relativeTo: this.getParent() || undefined, + }); + } + let width = Math.ceil(conf.width || rect.width), height = Math.ceil(conf.height || rect.height), pixelRatio = conf.pixelRatio, x = conf.x === undefined ? Math.floor(rect.x) : conf.x, y = conf.y === undefined ? Math.floor(rect.y) : conf.y, offset = conf.offset || 0, drawBorder = conf.drawBorder || false, hitCanvasPixelRatio = conf.hitCanvasPixelRatio || 1; + if (!width || !height) { + Util.error('Can not cache the node. Width or height of the node equals 0. Caching is skipped.'); + return; + } + // because using Math.floor on x, y position may shift drawing + // to avoid shift we need to increase size + // but we better to avoid it, for better filters flows + const extraPaddingX = Math.abs(Math.round(rect.x) - x) > 0.5 ? 1 : 0; + const extraPaddingY = Math.abs(Math.round(rect.y) - y) > 0.5 ? 1 : 0; + width += offset * 2 + extraPaddingX; + height += offset * 2 + extraPaddingY; + x -= offset; + y -= offset; + // if (Math.floor(x) < x) { + // x = Math.floor(x); + // // width += 1; + // } + // if (Math.floor(y) < y) { + // y = Math.floor(y); + // // height += 1; + // } + // console.log({ x, y, width, height }, rect); + const cachedSceneCanvas = new SceneCanvas({ + pixelRatio: pixelRatio, + width: width, + height: height, + }), cachedFilterCanvas = new SceneCanvas({ + pixelRatio: pixelRatio, + width: 0, + height: 0, + willReadFrequently: true, + }), cachedHitCanvas = new HitCanvas({ + pixelRatio: hitCanvasPixelRatio, + width: width, + height: height, + }), sceneContext = cachedSceneCanvas.getContext(), hitContext = cachedHitCanvas.getContext(); + cachedHitCanvas.isCache = true; + cachedSceneCanvas.isCache = true; + this._cache.delete(CANVAS); + this._filterUpToDate = false; + if (conf.imageSmoothingEnabled === false) { + cachedSceneCanvas.getContext()._context.imageSmoothingEnabled = false; + cachedFilterCanvas.getContext()._context.imageSmoothingEnabled = false; + } + sceneContext.save(); + hitContext.save(); + sceneContext.translate(-x, -y); + hitContext.translate(-x, -y); + // extra flag to skip on getAbsolute opacity calc + this._isUnderCache = true; + this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); + this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); + this.drawScene(cachedSceneCanvas, this); + this.drawHit(cachedHitCanvas, this); + this._isUnderCache = false; + sceneContext.restore(); + hitContext.restore(); + // 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._cache.set(CANVAS, { + scene: cachedSceneCanvas, + filter: cachedFilterCanvas, + hit: cachedHitCanvas, + x: x, + y: y, + }); + this._requestDraw(); + return this; + } + /** + * determine if node is currently cached + * @method + * @name Konva.Node#isCached + * @returns {Boolean} + */ + isCached() { + return this._cache.has(CANVAS); + } + /** + * 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. + * @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'); + } + _transformedRect(rect, top) { + const points = [ + { x: rect.x, y: rect.y }, + { x: rect.x + rect.width, y: rect.y }, + { x: rect.x + rect.width, y: rect.y + rect.height }, + { x: rect.x, y: rect.y + rect.height }, + ]; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + const trans = this.getAbsoluteTransform(top); + points.forEach(function (point) { + const transformed = trans.point(point); + if (minX === undefined) { + minX = maxX = transformed.x; + minY = maxY = transformed.y; + } + minX = Math.min(minX, transformed.x); + minY = Math.min(minY, transformed.y); + maxX = Math.max(maxX, transformed.x); + maxY = Math.max(maxY, transformed.y); + }); + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + _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) { + const canvasCache = this._getCanvasCache(), hitCanvas = canvasCache.hit; + 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) { + if (!this._filterUpToDate) { + const ratio = sceneCanvas.pixelRatio; + filterCanvas.setSize(sceneCanvas.width / sceneCanvas.pixelRatio, sceneCanvas.height / sceneCanvas.pixelRatio); + 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 !== 'function') { + Util.error('Filter should be type of function, but got ' + + typeof filter + + ' instead. Please check correct filters'); + continue; + } + filter.call(this, imageData); + 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; + } + return sceneCanvas; + } + /** + * 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, and dragend 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(evtStr, handler) { + this._cache && this._cache.delete(ALL_LISTENERS); + if (arguments.length === 3) { + return this._delegate.apply(this, arguments); + } + let events = evtStr.split(SPACE$1), len = events.length, n, event, parts, baseEvent, name; + /* + * loop through types and attach event listeners to + * each one. eg. 'click mouseover.namespace mouseout' + * will create three event bindings + */ + for (n = 0; n < len; n++) { + event = events[n]; + parts = event.split('.'); + baseEvent = parts[0]; + name = parts[1] || ''; + // create events array if it doesn't exist + if (!this.eventListeners[baseEvent]) { + this.eventListeners[baseEvent] = []; + } + this.eventListeners[baseEvent].push({ + name: name, + handler: handler, + }); + } + 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'); + */ + off(evtStr, callback) { + let events = (evtStr || '').split(SPACE$1), len = events.length, n, t, event, parts, baseEvent, name; + this._cache && this._cache.delete(ALL_LISTENERS); + 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); + } + }); + } + /** + * 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); + this._remove(); + return this; + } + _clearCaches() { + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); + this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); + this._clearSelfAndDescendantCache(STAGE$1); + this._clearSelfAndDescendantCache(VISIBLE); + this._clearSelfAndDescendantCache(LISTENING); + } + _remove() { + // every cached attr that is calculated via node tree + // traversal must be cleared when removing a node + this._clearCaches(); + const parent = this.getParent(); + if (parent && parent.children) { + parent.children.splice(this.index, 1); + parent._setChildrenIndices(); + this.parent = null; + } + } + /** + * 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. + * @method + * @name Konva.Node#destroy + * @example + * node.destroy(); + */ + destroy() { + 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.getId()); + * }) + */ + 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, skipDragCheck = false) { + if (top) { + return this._isVisible(top) && this._isListening(top); + } + const layer = this.getLayer(); + let layerUnderDrag = false; + DD._dragElements.forEach((elem) => { + if (elem.dragStatus !== 'dragging') { + return; + } + else if (elem.node.nodeType === 'Stage') { + layerUnderDrag = true; + } + else if (elem.node.getLayer() === layer) { + layerUnderDrag = true; + } + }); + const dragSkip = !skipDragCheck && + !Konva$2.hitOnDragEnabled && + (layerUnderDrag || Konva$2.isTransforming()); + return this.isListening() && this.isVisible() && !dragSkip; + } + /** + * 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.getChildren().slice()); + } + 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.getChildren()); + } + 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} + */ + 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) { + this._batchingTransformChange = true; + func(); + this._batchingTransformChange = false; + if (this._needClearTransformCache) { + this._clearCache(TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + } + this._needClearTransformCache = false; + } + 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} Ancestor 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) { + let haveCachedParent = false; + let parent = this.parent; + while (parent) { + if (parent.isCached()) { + haveCachedParent = true; + break; + } + parent = parent.parent; + } + if (haveCachedParent && !top) { + // make fake top element + // "true" is not a node, but it will just allow skip all caching + top = true; + } + 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(); + it.invert(); + it.translate(pos.x, pos.y); + pos = { + x: this.attrs.x + it.getTranslation().x, + y: this.attrs.y + it.getTranslation().y, + }; + this._setTransform(origTrans); + this.setPosition({ x: pos.x, y: pos.y }); + this._clearCache(TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + return this; + } + _setTransform(trans) { + let key; + for (key in trans) { + this.attrs[key] = trans[key]; + } + // this._clearCache(TRANSFORM); + // this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + } + _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; + } + _eachAncestorReverse(func, top) { + let family = [], parent = this.getParent(), len, n; + // if top node is defined, and this node is top node, + // there's no need to build a family tree. just execute + // func with this because it will be the only node + if (top && top._id === this._id) { + // func(this); + return; + } + family.unshift(this); + while (parent && (!top || parent._id !== top._id)) { + family.unshift(parent); + parent = parent.parent; + } + len = family.length; + for (n = 0; n < len; n++) { + func(family[n]); + } + } + /** + * 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.getChildren().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.getChildren().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() { + let attrs = this.getAttrs(), key, val, getter, defaultValue, nonPlainObject; + const obj = { + attrs: {}, + className: this.getClassName(), + }; + for (key in attrs) { + val = attrs[key]; + // 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; + } + getter = typeof this[key] === 'function' && this[key]; + // remove attr value so that we can extract the default value from the getter + delete attrs[key]; + defaultValue = getter ? getter.call(this) : null; + // restore attr value + attrs[key] = val; + if (defaultValue !== val) { + obj.attrs[key] = val; + } + } + return Util._prepareToStringify(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$1, 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.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) { + // if using an argument, we can't cache the result. + if (top) { + return this._getAbsoluteTransform(top); + } + else { + // if no argument, we can cache the result + return this._getCache(ABSOLUTE_TRANSFORM, this._getAbsoluteTransform); + } + } + _getAbsoluteTransform(top) { + let at; + // we we need position relative to an ancestor, we will iterate for all + if (top) { + at = new Transform(); + // start with stage and traverse downwards to self + this._eachAncestorReverse(function (node) { + const transformsEnabled = node.transformsEnabled(); + if (transformsEnabled === 'all') { + at.multiply(node.getTransform()); + } + else if (transformsEnabled === 'position') { + at.translate(node.x() - node.offsetX(), node.y() - node.offsetY()); + } + }, top); + return at; + } + else { + // try to use a cached value + at = this._cache.get(ABSOLUTE_TRANSFORM) || new Transform(); + if (this.parent) { + // transform will be cached + this.parent.getAbsoluteTransform().copyInto(at); + } + else { + at.reset(); + } + 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); + } + at.dirty = false; + 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() { + // var parent: Node = this; + // var rotation = 0; + // while (parent) { + // rotation += parent.rotation(); + // parent = parent.getParent(); + // } + // return rotation; + 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.get(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$2.getAngle(this.rotation()), scaleX = (_a = this.attrs.scaleX) !== null && _a !== undefined ? _a : 1, scaleY = (_b = this.attrs.scaleY) !== null && _b !== undefined ? _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 || {}; + const box = this.getClientRect(); + 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.ceil(box.width) || (stage ? stage.width() : 0), + height: config.height || + Math.ceil(box.height) || + (stage ? stage.height() : 0), + pixelRatio: pixelRatio, + }), context = canvas.getContext(); + const bufferCanvas = new SceneCanvas({ + // width and height already multiplied by pixelRatio + // so we need to revert that + // also increase size by x nd y offset to make sure content fits canvas + width: canvas.width / canvas.pixelRatio + Math.abs(x), + height: canvas.height / canvas.pixelRatio + Math.abs(y), + pixelRatio: canvas.pixelRatio, + }); + if (config.imageSmoothingEnabled === false) { + context._context.imageSmoothingEnabled = false; + } + context.save(); + if (x || y) { + context.translate(-1 * x, -1 * y); + } + this.drawScene(canvas, undefined, bufferCanvas); + context.restore(); + 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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) { + config = config || {}; + const mimeType = config.mimeType || null, quality = config.quality || 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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} + * @example + * var image = node.toImage({ + * callback(img) { + * // do stuff with img + * } + * }); + */ + toImage(config) { + return new Promise((resolve, reject) => { + try { + const callback = config === null || config === void 0 ? void 0 : config.callback; + if (callback) + delete config.callback; + Util._urlToImage(this.toDataURL(config), function (img) { + resolve(img); + callback === null || callback === void 0 ? void 0 : callback(img); + }); + } + 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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} + */ + toBlob(config) { + return new Promise((resolve, reject) => { + try { + const callback = config === null || config === void 0 ? void 0 : config.callback; + if (callback) + delete config.callback; + this.toCanvas(config).toBlob((blob) => { + resolve(blob); + callback === null || callback === void 0 ? void 0 : callback(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$2.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; + } + _requestDraw() { + if (Konva$2.autoDrawEnabled) { + const drawNode = this.getLayer() || this.getStage(); + drawNode === null || drawNode === undefined ? undefined : 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._shouldFireChangeEvents) { + this._fireChangeEvent(key, oldVal, val); + } + this._requestDraw(); + } + _setComponentAttr(key, component, val) { + let oldVal; + if (val !== undefined) { + oldVal = this.attrs[key]; + if (!oldVal) { + // set value to default value using getAttr + this.attrs[key] = this.getAttr(key); + } + this.attrs[key][component] = val; + this._fireChangeEvent(key, oldVal, val); + } + } + _fireAndBubble(eventType, evt, compareShape) { + if (evt && this.nodeType === SHAPE) { + evt.target = this; + } + const shouldStop = (eventType === MOUSEENTER$1 || eventType === MOUSELEAVE$1) && + ((compareShape && + (this === compareShape || + (this.isAncestorOf && this.isAncestorOf(compareShape)))) || + (this.nodeType === 'Stage' && !compareShape)); + if (!shouldStop) { + this._fire(eventType, evt); + // simulate event bubbling + const stopBubble = (eventType === MOUSEENTER$1 || eventType === MOUSELEAVE$1) && + 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, _c; + const allListeners = (_a = this._cache.get(ALL_LISTENERS)) !== null && _a !== undefined ? _a : {}; + let events = allListeners === null || allListeners === undefined ? undefined : allListeners[eventType]; + if (events === undefined) { + //recalculate cache + events = []; + let obj = Object.getPrototypeOf(this); + while (obj) { + const hierarchyEvents = (_c = (_b = obj.eventListeners) === null || _b === undefined ? undefined : _b[eventType]) !== null && _c !== undefined ? _c : []; + events.push(...hierarchyEvents); + obj = Object.getPrototypeOf(obj); + } + // update cache + allListeners[eventType] = events; + this._cache.set(ALL_LISTENERS, allListeners); + } + return events; + } + _fire(eventType, evt) { + evt = evt || {}; + evt.currentTarget = this; + evt.type = eventType; + const topListeners = this._getProtoListeners(eventType); + if (topListeners) { + for (var i = 0; i < topListeners.length; i++) { + topListeners[i].handler.call(this, evt); + } + } + // it is important to iterate over self listeners without cache + // because events can be added/removed while firing + const selfListeners = this.eventListeners[eventType]; + if (selfListeners) { + for (var i = 0; i < selfListeners.length; i++) { + selfListeners[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) { + 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; + DD._dragElements.set(this._id, { + node: this, + startPointerPos: pos, + offset: { + x: pos.x - ap.x, + y: pos.y - ap.y, + }, + dragStatus: 'ready', + pointerId, + }); + } + /** + * initiate drag and drop. + * @method + * @name Konva.Node#startDrag + */ + startDrag(evt, bubbleEvent = true) { + if (!DD._dragElements.has(this._id)) { + this._createDragElement(evt); + } + const elem = DD._dragElements.get(this._id); + elem.dragStatus = 'dragging'; + this.fire('dragstart', { + type: 'dragstart', + target: this, + evt: evt && evt.evt, + }, bubbleEvent); + } + _setDragPosition(evt, elem) { + // const pointers = this.getStage().getPointersPositions(); + // const pos = pointers.find(p => p.id === this._dragEventId); + 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 { + newNodePos = bounded; + } + } + if (!this._lastPos || + this._lastPos.x !== newNodePos.x || + this._lastPos.y !== newNodePos.y) { + this.setAbsolutePosition(newNodePos); + this._requestDraw(); + } + this._lastPos = newNodePos; + } + /** + * stop drag and drop + * @method + * @name Konva.Node#stopDrag + */ + stopDrag(evt) { + const elem = DD._dragElements.get(this._id); + if (elem) { + elem.dragStatus = 'stopped'; + } + DD._endDragBefore(evt); + DD._endDragAfter(evt); + } + 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$2.dragButtons.indexOf(evt.evt['button']) >= 0; + if (!canDrag) { + return; + } + if (this.isDragging()) { + 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. De-serializtion does not generate custom + * shape drawing functions, images, or event handlers (this would make the + * serialized object huge). If your app uses custom shapes, images, and + * event handlers (it probably does), then you need to select the appropriate + * shapes after loading the stage and set these properties via on(), setSceneFunc(), + * and setImage() methods + * @method + * @memberof Konva.Node + * @param {String|Object} json 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$2[className]) { + Util.warn('Can not find a node with class name "' + + className + + '". Fallback to "Shape".'); + className = 'Shape'; + } + const Class = Konva$2[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; + } + } + 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.eventListeners = {}; + Node.prototype.on.call(Node.prototype, TRANSFORM_CHANGE_STR$1, function () { + if (this._batchingTransformChange) { + this._needClearTransformCache = true; + return; + } + this._clearCache(TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + }); + Node.prototype.on.call(Node.prototype, 'visibleChange.konva', function () { + this._clearSelfAndDescendantCache(VISIBLE); + }); + Node.prototype.on.call(Node.prototype, 'listeningChange.konva', function () { + this._clearSelfAndDescendantCache(LISTENING); + }); + Node.prototype.on.call(Node.prototype, '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 + * @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 + * @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. Filters are applied to cached canvases + * @name Konva.Node#filters + * @method + * @param {Array} filters array of filters + * @returns {Array} + * @example + * // get filters + * var filters = node.filters(); + * + * // set a single filter + * node.cache(); + * node.filters([Konva.Filters.Blur]); + * + * // set multiple filters + * node.cache(); + * node.filters([ + * Konva.Filters.Blur, + * Konva.Filters.Sepia, + * Konva.Filters.Invert + * ]); + */ + 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. + * @name Konva.Node#dragBoundFunc + * @method + * @param {Function} dragBoundFunc + * @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', + }); + + /** + * Container constructor.  Containers are used to contain nodes or other containers + * @constructor + * @memberof Konva + * @augments Konva.Node + * @abstract + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + */ + class Container extends Node { + constructor() { + super(...arguments); + this.children = []; + } + /** + * returns an array of direct descendant nodes + * @method + * @name Konva.Container#getChildren + * @param {Function} [filterFunc] filter function + * @returns {Array} + * @example + * // get all children + * var children = layer.getChildren(); + * + * // get only circles + * var circles = layer.getChildren(function(node){ + * return node.getClassName() === 'Circle'; + * }); + */ + getChildren(filterFunc) { + if (!filterFunc) { + return this.children || []; + } + const children = this.children || []; + const results = []; + children.forEach(function (child) { + if (filterFunc(child)) { + results.push(child); + } + }); + return results; + } + /** + * determine if node has children + * @method + * @name Konva.Container#hasChildren + * @returns {Boolean} + */ + hasChildren() { + return this.getChildren().length > 0; + } + /** + * remove all children. Children will be still in memory. + * If you want to completely destroy all children please use "destroyChildren" method instead + * @method + * @name Konva.Container#removeChildren + */ + removeChildren() { + this.getChildren().forEach((child) => { + // reset parent to prevent many _setChildrenIndices calls + child.parent = null; + child.index = 0; + child.remove(); + }); + this.children = []; + // because all children were detached from parent, request draw via container + this._requestDraw(); + return this; + } + /** + * destroy all children nodes. + * @method + * @name Konva.Container#destroyChildren + */ + destroyChildren() { + this.getChildren().forEach((child) => { + // reset parent to prevent many _setChildrenIndices calls + child.parent = null; + child.index = 0; + child.destroy(); + }); + this.children = []; + // because all children were detached from parent, request draw via container + this._requestDraw(); + return this; + } + /** + * add a child and children into container + * @name Konva.Container#add + * @method + * @param {...Konva.Node} children + * @returns {Container} + * @example + * layer.add(rect); + * layer.add(shape1, shape2, shape3); + * // empty arrays are accepted, though each individual child must be defined + * layer.add(...shapes); + * // remember to redraw layer if you changed something + * layer.draw(); + */ + add(...children) { + if (children.length === 0) { + return this; + } + if (children.length > 1) { + for (let i = 0; i < children.length; i++) { + this.add(children[i]); + } + return this; + } + const child = children[0]; + if (child.getParent()) { + child.moveTo(this); + return this; + } + this._validateAdd(child); + child.index = this.getChildren().length; + child.parent = this; + child._clearCaches(); + this.getChildren().push(child); + this._fire('add', { + child: child, + }); + this._requestDraw(); + // chainable + return this; + } + destroy() { + if (this.hasChildren()) { + this.destroyChildren(); + } + super.destroy(); + return this; + } + /** + * return an array of nodes that match the selector. + * You can provide a string with '#' for id selections and '.' for name selections. + * Or a function that will return true/false when a node is passed through. See example below. + * With strings you can also select by type or class name. Pass multiple selectors + * separated by a comma. + * @method + * @name Konva.Container#find + * @param {String | Function} selector + * @returns {Array} + * @example + * + * Passing a string as a selector + * // select node with id foo + * var node = stage.find('#foo'); + * + * // select nodes with name bar inside layer + * var nodes = layer.find('.bar'); + * + * // select all groups inside layer + * var nodes = layer.find('Group'); + * + * // select all rectangles inside layer + * var nodes = layer.find('Rect'); + * + * // select node with an id of foo or a name of bar inside layer + * var nodes = layer.find('#foo, .bar'); + * + * Passing a function as a selector + * + * // get all groups with a function + * var groups = stage.find(node => { + * return node.getType() === 'Group'; + * }); + * + * // get only Nodes with partial opacity + * var alphaNodes = layer.find(node => { + * return node.getType() === 'Node' && node.getAbsoluteOpacity() < 1; + * }); + */ + find(selector) { + // protecting _generalFind to prevent user from accidentally adding + // second argument and getting unexpected `findOne` result + return this._generalFind(selector, false); + } + /** + * return a first node from `find` method + * @method + * @name Konva.Container#findOne + * @param {String | Function} selector + * @returns {Konva.Node | Undefined} + * @example + * // select node with id foo + * var node = stage.findOne('#foo'); + * + * // select node with name bar inside layer + * var nodes = layer.findOne('.bar'); + * + * // select the first node to return true in a function + * var node = stage.findOne(node => { + * return node.getType() === 'Shape' + * }) + */ + findOne(selector) { + const result = this._generalFind(selector, true); + return result.length > 0 ? result[0] : undefined; + } + _generalFind(selector, findOne) { + const retArr = []; + this._descendants((node) => { + const valid = node._isMatch(selector); + if (valid) { + retArr.push(node); + } + if (valid && findOne) { + return true; + } + return false; + }); + return retArr; + } + _descendants(fn) { + let shouldStop = false; + const children = this.getChildren(); + for (const child of children) { + shouldStop = fn(child); + if (shouldStop) { + return true; + } + if (!child.hasChildren()) { + continue; + } + shouldStop = child._descendants(fn); + if (shouldStop) { + return true; + } + } + return false; + } + // extenders + toObject() { + const obj = Node.prototype.toObject.call(this); + obj.children = []; + this.getChildren().forEach((child) => { + obj.children.push(child.toObject()); + }); + return obj; + } + /** + * determine if node is an ancestor + * of descendant + * @method + * @name Konva.Container#isAncestorOf + * @param {Konva.Node} node + */ + isAncestorOf(node) { + let parent = node.getParent(); + while (parent) { + if (parent._id === this._id) { + return true; + } + parent = parent.getParent(); + } + return false; + } + clone(obj) { + // call super method + const node = Node.prototype.clone.call(this, obj); + this.getChildren().forEach(function (no) { + node.add(no.clone()); + }); + return node; + } + /** + * get all shapes that intersect a point. Note: because this method must clear a temporary + * canvas and redraw every shape inside the container, it should only be used for special situations + * because it performs very poorly. Please use the {@link Konva.Stage#getIntersection} method if at all possible + * because it performs much better + * nodes with listening set to false will not be detected + * @method + * @name Konva.Container#getAllIntersections + * @param {Object} pos + * @param {Number} pos.x + * @param {Number} pos.y + * @returns {Array} array of shapes + */ + getAllIntersections(pos) { + const arr = []; + this.find('Shape').forEach((shape) => { + if (shape.isVisible() && shape.intersects(pos)) { + arr.push(shape); + } + }); + return arr; + } + _clearSelfAndDescendantCache(attr) { + var _a; + super._clearSelfAndDescendantCache(attr); + // skip clearing if node is cached with canvas + // for performance reasons !!! + if (this.isCached()) { + return; + } + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (node) { + node._clearSelfAndDescendantCache(attr); + }); + } + _setChildrenIndices() { + var _a; + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (child, n) { + child.index = n; + }); + this._requestDraw(); + } + drawScene(can, top, bufferCanvas) { + const layer = this.getLayer(), canvas = can || (layer && layer.getCanvas()), context = canvas && canvas.getContext(), cachedCanvas = this._getCanvasCache(), cachedSceneCanvas = cachedCanvas && cachedCanvas.scene; + const caching = canvas && canvas.isCache; + if (!this.isVisible() && !caching) { + return this; + } + if (cachedSceneCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedSceneCanvas(context); + context.restore(); + } + else { + this._drawChildren('drawScene', canvas, top, bufferCanvas); + } + return this; + } + drawHit(can, top) { + if (!this.shouldDrawHit(top)) { + return this; + } + const layer = this.getLayer(), canvas = can || (layer && layer.hitCanvas), context = canvas && canvas.getContext(), cachedCanvas = this._getCanvasCache(), cachedHitCanvas = cachedCanvas && cachedCanvas.hit; + if (cachedHitCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedHitCanvas(context); + context.restore(); + } + else { + this._drawChildren('drawHit', canvas, top); + } + return this; + } + _drawChildren(drawMethod, canvas, top, bufferCanvas) { + var _a; + const context = canvas && canvas.getContext(), clipWidth = this.clipWidth(), clipHeight = this.clipHeight(), clipFunc = this.clipFunc(), hasClip = (typeof clipWidth === 'number' && typeof clipHeight === 'number') || + clipFunc; + const selfCache = top === this; + if (hasClip) { + context.save(); + const transform = this.getAbsoluteTransform(top); + let m = transform.getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + context.beginPath(); + let clipArgs; + if (clipFunc) { + clipArgs = clipFunc.call(this, context, this); + } + else { + const clipX = this.clipX(); + const clipY = this.clipY(); + context.rect(clipX || 0, clipY || 0, clipWidth, clipHeight); + } + context.clip.apply(context, clipArgs); + m = transform.copy().invert().getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + const hasComposition = !selfCache && + this.globalCompositeOperation() !== 'source-over' && + drawMethod === 'drawScene'; + if (hasComposition) { + context.save(); + context._applyGlobalCompositeOperation(this); + } + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (child) { + child[drawMethod](canvas, top, bufferCanvas); + }); + if (hasComposition) { + context.restore(); + } + if (hasClip) { + context.restore(); + } + } + getClientRect(config = {}) { + var _a; + const skipTransform = config.skipTransform; + const relativeTo = config.relativeTo; + let minX, minY, maxX, maxY; + let selfRect = { + x: Infinity, + y: Infinity, + width: 0, + height: 0, + }; + const that = this; + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (child) { + // skip invisible children + if (!child.visible()) { + return; + } + const rect = child.getClientRect({ + relativeTo: that, + skipShadow: config.skipShadow, + skipStroke: config.skipStroke, + }); + // skip invisible children (like empty groups) + if (rect.width === 0 && rect.height === 0) { + return; + } + if (minX === undefined) { + // initial value for first child + minX = rect.x; + minY = rect.y; + maxX = rect.x + rect.width; + maxY = rect.y + rect.height; + } + else { + minX = Math.min(minX, rect.x); + minY = Math.min(minY, rect.y); + maxX = Math.max(maxX, rect.x + rect.width); + maxY = Math.max(maxY, rect.y + rect.height); + } + }); + // if child is group we need to make sure it has visible shapes inside + const shapes = this.find('Shape'); + let hasVisible = false; + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + if (shape._isVisible(this)) { + hasVisible = true; + break; + } + } + if (hasVisible && minX !== undefined) { + selfRect = { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + else { + selfRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + if (!skipTransform) { + return this._transformedRect(selfRect, relativeTo); + } + return selfRect; + } + } + // add getters setters + Factory.addComponentsGetterSetter(Container, 'clip', [ + 'x', + 'y', + 'width', + 'height', + ]); + /** + * get/set clip + * @method + * @name Konva.Container#clip + * @param {Object} clip + * @param {Number} clip.x + * @param {Number} clip.y + * @param {Number} clip.width + * @param {Number} clip.height + * @returns {Object} + * @example + * // get clip + * var clip = container.clip(); + * + * // set clip + * container.clip({ + * x: 20, + * y: 20, + * width: 20, + * height: 20 + * }); + */ + Factory.addGetterSetter(Container, 'clipX', undefined, getNumberValidator()); + /** + * get/set clip x + * @name Konva.Container#clipX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get clip x + * var clipX = container.clipX(); + * + * // set clip x + * container.clipX(10); + */ + Factory.addGetterSetter(Container, 'clipY', undefined, getNumberValidator()); + /** + * get/set clip y + * @name Konva.Container#clipY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get clip y + * var clipY = container.clipY(); + * + * // set clip y + * container.clipY(10); + */ + Factory.addGetterSetter(Container, 'clipWidth', undefined, getNumberValidator()); + /** + * get/set clip width + * @name Konva.Container#clipWidth + * @method + * @param {Number} width + * @returns {Number} + * @example + * // get clip width + * var clipWidth = container.clipWidth(); + * + * // set clip width + * container.clipWidth(100); + */ + Factory.addGetterSetter(Container, 'clipHeight', undefined, getNumberValidator()); + /** + * get/set clip height + * @name Konva.Container#clipHeight + * @method + * @param {Number} height + * @returns {Number} + * @example + * // get clip height + * var clipHeight = container.clipHeight(); + * + * // set clip height + * container.clipHeight(100); + */ + Factory.addGetterSetter(Container, 'clipFunc'); + /** + * get/set clip function + * @name Konva.Container#clipFunc + * @method + * @param {Function} function + * @returns {Function} + * @example + * // get clip function + * var clipFunction = container.clipFunc(); + * + * // set clip function + * container.clipFunc(function(ctx) { + * ctx.rect(0, 0, 100, 100); + * }); + * + * container.clipFunc(function(ctx) { + * // optionally return a clip Path2D and clip-rule or just the clip-rule + * return [new Path2D('M0 0v50h50Z'), 'evenodd'] + * }); + */ + + const Captures = new Map(); + // we may use this module for capturing touch events too + // so make sure we don't do something super specific to pointer + const SUPPORT_POINTER_EVENTS = Konva$2._global['PointerEvent'] !== undefined; + function getCapturedShape(pointerId) { + return Captures.get(pointerId); + } + function createEvent(evt) { + return { + evt, + pointerId: evt.pointerId, + }; + } + function hasPointerCapture(pointerId, shape) { + return Captures.get(pointerId) === shape; + } + function setPointerCapture(pointerId, shape) { + releaseCapture(pointerId); + const stage = shape.getStage(); + if (!stage) + return; + Captures.set(pointerId, shape); + if (SUPPORT_POINTER_EVENTS) { + shape._fire('gotpointercapture', createEvent(new PointerEvent('gotpointercapture'))); + } + } + function releaseCapture(pointerId, target) { + const shape = Captures.get(pointerId); + if (!shape) + return; + const stage = shape.getStage(); + if (stage && stage.content) ; + Captures.delete(pointerId); + if (SUPPORT_POINTER_EVENTS) { + shape._fire('lostpointercapture', createEvent(new PointerEvent('lostpointercapture'))); + } + } + + // CONSTANTS + const STAGE = 'Stage', STRING = 'string', PX = 'px', MOUSEOUT = 'mouseout', MOUSELEAVE = 'mouseleave', MOUSEOVER = 'mouseover', MOUSEENTER = 'mouseenter', MOUSEMOVE = 'mousemove', MOUSEDOWN = 'mousedown', MOUSEUP = 'mouseup', POINTERMOVE = 'pointermove', POINTERDOWN = 'pointerdown', POINTERUP = 'pointerup', POINTERCANCEL = 'pointercancel', LOSTPOINTERCAPTURE = 'lostpointercapture', POINTEROUT = 'pointerout', POINTERLEAVE = 'pointerleave', POINTEROVER = 'pointerover', POINTERENTER = 'pointerenter', CONTEXTMENU = 'contextmenu', TOUCHSTART = 'touchstart', TOUCHEND = 'touchend', TOUCHMOVE = 'touchmove', TOUCHCANCEL = 'touchcancel', WHEEL = 'wheel', MAX_LAYERS_NUMBER = 5, EVENTS = [ + [MOUSEENTER, '_pointerenter'], + [MOUSEDOWN, '_pointerdown'], + [MOUSEMOVE, '_pointermove'], + [MOUSEUP, '_pointerup'], + [MOUSELEAVE, '_pointerleave'], + [TOUCHSTART, '_pointerdown'], + [TOUCHMOVE, '_pointermove'], + [TOUCHEND, '_pointerup'], + [TOUCHCANCEL, '_pointercancel'], + [MOUSEOVER, '_pointerover'], + [WHEEL, '_wheel'], + [CONTEXTMENU, '_contextmenu'], + [POINTERDOWN, '_pointerdown'], + [POINTERMOVE, '_pointermove'], + [POINTERUP, '_pointerup'], + [POINTERCANCEL, '_pointercancel'], + [LOSTPOINTERCAPTURE, '_lostpointercapture'], + ]; + const EVENTS_MAP = { + mouse: { + [POINTEROUT]: MOUSEOUT, + [POINTERLEAVE]: MOUSELEAVE, + [POINTEROVER]: MOUSEOVER, + [POINTERENTER]: MOUSEENTER, + [POINTERMOVE]: MOUSEMOVE, + [POINTERDOWN]: MOUSEDOWN, + [POINTERUP]: MOUSEUP, + [POINTERCANCEL]: 'mousecancel', + pointerclick: 'click', + pointerdblclick: 'dblclick', + }, + touch: { + [POINTEROUT]: 'touchout', + [POINTERLEAVE]: 'touchleave', + [POINTEROVER]: 'touchover', + [POINTERENTER]: 'touchenter', + [POINTERMOVE]: TOUCHMOVE, + [POINTERDOWN]: TOUCHSTART, + [POINTERUP]: TOUCHEND, + [POINTERCANCEL]: TOUCHCANCEL, + pointerclick: 'tap', + pointerdblclick: 'dbltap', + }, + pointer: { + [POINTEROUT]: POINTEROUT, + [POINTERLEAVE]: POINTERLEAVE, + [POINTEROVER]: POINTEROVER, + [POINTERENTER]: POINTERENTER, + [POINTERMOVE]: POINTERMOVE, + [POINTERDOWN]: POINTERDOWN, + [POINTERUP]: POINTERUP, + [POINTERCANCEL]: POINTERCANCEL, + pointerclick: 'pointerclick', + pointerdblclick: 'pointerdblclick', + }, + }; + const getEventType = (type) => { + if (type.indexOf('pointer') >= 0) { + return 'pointer'; + } + if (type.indexOf('touch') >= 0) { + return 'touch'; + } + return 'mouse'; + }; + const getEventsMap = (eventType) => { + const type = getEventType(eventType); + if (type === 'pointer') { + return Konva$2.pointerEventsEnabled && EVENTS_MAP.pointer; + } + if (type === 'touch') { + return EVENTS_MAP.touch; + } + if (type === 'mouse') { + return EVENTS_MAP.mouse; + } + }; + function checkNoClip(attrs = {}) { + if (attrs.clipFunc || attrs.clipWidth || attrs.clipHeight) { + Util.warn('Stage does not support clipping. Please use clip for Layers or Groups.'); + } + return attrs; + } + const NO_POINTERS_MESSAGE = `Pointer position is missing and not registered by the stage. Looks like it is outside of the stage container. You can set it manually from event: stage.setPointersPositions(event);`; + const stages = []; + /** + * Stage constructor. A stage is used to contain multiple layers + * @constructor + * @memberof Konva + * @augments Konva.Container + * @param {Object} config + * @param {String|Element} config.container Container selector or DOM element + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var stage = new Konva.Stage({ + * width: 500, + * height: 800, + * container: 'containerId' // or "#containerId" or ".containerClass" + * }); + */ + class Stage extends Container { + constructor(config) { + super(checkNoClip(config)); + this._pointerPositions = []; + this._changedPointerPositions = []; + this._buildDOM(); + this._bindContentEvents(); + stages.push(this); + this.on('widthChange.konva heightChange.konva', this._resizeDOM); + this.on('visibleChange.konva', this._checkVisibility); + this.on('clipWidthChange.konva clipHeightChange.konva clipFuncChange.konva', () => { + checkNoClip(this.attrs); + }); + this._checkVisibility(); + } + _validateAdd(child) { + const isLayer = child.getType() === 'Layer'; + const isFastLayer = child.getType() === 'FastLayer'; + const valid = isLayer || isFastLayer; + if (!valid) { + Util.throw('You may only add layers to the stage.'); + } + } + _checkVisibility() { + if (!this.content) { + return; + } + const style = this.visible() ? '' : 'none'; + this.content.style.display = style; + } + /** + * set container dom element which contains the stage wrapper div element + * @method + * @name Konva.Stage#setContainer + * @param {DomElement} container can pass in a dom element or id string + */ + setContainer(container) { + if (typeof container === STRING) { + if (container.charAt(0) === '.') { + const className = container.slice(1); + container = document.getElementsByClassName(className)[0]; + } + else { + var id; + if (container.charAt(0) !== '#') { + id = container; + } + else { + id = container.slice(1); + } + container = document.getElementById(id); + } + if (!container) { + throw 'Can not find container in document with id ' + id; + } + } + this._setAttr('container', container); + if (this.content) { + if (this.content.parentElement) { + this.content.parentElement.removeChild(this.content); + } + container.appendChild(this.content); + } + return this; + } + shouldDrawHit() { + return true; + } + /** + * clear all layers + * @method + * @name Konva.Stage#clear + */ + clear() { + const layers = this.children, len = layers.length; + for (let n = 0; n < len; n++) { + layers[n].clear(); + } + return this; + } + clone(obj) { + if (!obj) { + obj = {}; + } + obj.container = + typeof document !== 'undefined' && document.createElement('div'); + return Container.prototype.clone.call(this, obj); + } + destroy() { + super.destroy(); + const content = this.content; + if (content && Util._isInDocument(content)) { + this.container().removeChild(content); + } + const index = stages.indexOf(this); + if (index > -1) { + stages.splice(index, 1); + } + Util.releaseCanvas(this.bufferCanvas._canvas, this.bufferHitCanvas._canvas); + return this; + } + /** + * returns ABSOLUTE pointer position which can be a touch position or mouse position + * pointer position doesn't include any transforms (such as scale) of the stage + * it is just a plain position of pointer relative to top-left corner of the canvas + * @method + * @name Konva.Stage#getPointerPosition + * @returns {Vector2d|null} + */ + getPointerPosition() { + const pos = this._pointerPositions[0] || this._changedPointerPositions[0]; + if (!pos) { + Util.warn(NO_POINTERS_MESSAGE); + return null; + } + return { + x: pos.x, + y: pos.y, + }; + } + _getPointerById(id) { + return this._pointerPositions.find((p) => p.id === id); + } + getPointersPositions() { + return this._pointerPositions; + } + getStage() { + return this; + } + getContent() { + return this.content; + } + _toKonvaCanvas(config) { + config = config || {}; + config.x = config.x || 0; + config.y = config.y || 0; + config.width = config.width || this.width(); + config.height = config.height || this.height(); + const canvas = new SceneCanvas({ + width: config.width, + height: config.height, + pixelRatio: config.pixelRatio || 1, + }); + const _context = canvas.getContext()._context; + const layers = this.children; + if (config.x || config.y) { + _context.translate(-1 * config.x, -1 * config.y); + } + layers.forEach(function (layer) { + if (!layer.isVisible()) { + return; + } + const layerCanvas = layer._toKonvaCanvas(config); + _context.drawImage(layerCanvas._canvas, config.x, config.y, layerCanvas.getWidth() / layerCanvas.getPixelRatio(), layerCanvas.getHeight() / layerCanvas.getPixelRatio()); + }); + return canvas; + } + /** + * get visible intersection shape. This is the preferred + * method for determining if a point intersects a shape or not + * nodes with listening set to false will not be detected + * @method + * @name Konva.Stage#getIntersection + * @param {Object} pos + * @param {Number} pos.x + * @param {Number} pos.y + * @returns {Konva.Node} + * @example + * var shape = stage.getIntersection({x: 50, y: 50}); + */ + getIntersection(pos) { + if (!pos) { + return null; + } + const layers = this.children, len = layers.length, end = len - 1; + for (let n = end; n >= 0; n--) { + const shape = layers[n].getIntersection(pos); + if (shape) { + return shape; + } + } + return null; + } + _resizeDOM() { + const width = this.width(); + const height = this.height(); + if (this.content) { + // set content dimensions + this.content.style.width = width + PX; + this.content.style.height = height + PX; + } + this.bufferCanvas.setSize(width, height); + this.bufferHitCanvas.setSize(width, height); + // set layer dimensions + this.children.forEach((layer) => { + layer.setSize({ width, height }); + layer.draw(); + }); + } + add(layer, ...rest) { + if (arguments.length > 1) { + for (let i = 0; i < arguments.length; i++) { + this.add(arguments[i]); + } + return this; + } + super.add(layer); + const length = this.children.length; + if (length > MAX_LAYERS_NUMBER) { + Util.warn('The stage has ' + + length + + ' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'); + } + layer.setSize({ width: this.width(), height: this.height() }); + // draw layer and append canvas to container + layer.draw(); + if (Konva$2.isBrowser) { + this.content.appendChild(layer.canvas._canvas); + } + // chainable + return this; + } + getParent() { + return null; + } + getLayer() { + return null; + } + hasPointerCapture(pointerId) { + return hasPointerCapture(pointerId, this); + } + setPointerCapture(pointerId) { + setPointerCapture(pointerId, this); + } + releaseCapture(pointerId) { + releaseCapture(pointerId); + } + /** + * returns an array of layers + * @method + * @name Konva.Stage#getLayers + */ + getLayers() { + return this.children; + } + _bindContentEvents() { + if (!Konva$2.isBrowser) { + return; + } + EVENTS.forEach(([event, methodName]) => { + this.content.addEventListener(event, (evt) => { + this[methodName](evt); + }, { passive: false }); + }); + } + _pointerenter(evt) { + this.setPointersPositions(evt); + const events = getEventsMap(evt.type); + if (events) { + this._fire(events.pointerenter, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _pointerover(evt) { + this.setPointersPositions(evt); + const events = getEventsMap(evt.type); + if (events) { + this._fire(events.pointerover, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _getTargetShape(evenType) { + let shape = this[evenType + 'targetShape']; + if (shape && !shape.getStage()) { + shape = null; + } + return shape; + } + _pointerleave(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + this.setPointersPositions(evt); + const targetShape = this._getTargetShape(eventType); + const eventsEnabled = !(Konva$2.isDragging() || Konva$2.isTransforming()) || Konva$2.hitOnDragEnabled; + if (targetShape && eventsEnabled) { + targetShape._fireAndBubble(events.pointerout, { evt: evt }); + targetShape._fireAndBubble(events.pointerleave, { evt: evt }); + this._fire(events.pointerleave, { + evt: evt, + target: this, + currentTarget: this, + }); + this[eventType + 'targetShape'] = null; + } + else if (eventsEnabled) { + this._fire(events.pointerleave, { + evt: evt, + target: this, + currentTarget: this, + }); + this._fire(events.pointerout, { + evt: evt, + target: this, + currentTarget: this, + }); + } + this.pointerPos = null; + this._pointerPositions = []; + } + _pointerdown(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + this.setPointersPositions(evt); + let triggeredOnShape = false; + this._changedPointerPositions.forEach((pos) => { + const shape = this.getIntersection(pos); + DD.justDragged = false; + // probably we are staring a click + Konva$2['_' + eventType + 'ListenClick'] = true; + // no shape detected? do nothing + if (!shape || !shape.isListening()) { + this[eventType + 'ClickStartShape'] = undefined; + return; + } + if (Konva$2.capturePointerEventsEnabled) { + shape.setPointerCapture(pos.id); + } + // save where we started the click + this[eventType + 'ClickStartShape'] = shape; + shape._fireAndBubble(events.pointerdown, { + evt: evt, + pointerId: pos.id, + }); + triggeredOnShape = true; + // TODO: test in iframe + // only call preventDefault if the shape is listening for events + const isTouch = evt.type.indexOf('touch') >= 0; + if (shape.preventDefault() && evt.cancelable && isTouch) { + evt.preventDefault(); + } + }); + // trigger down on stage if not already + if (!triggeredOnShape) { + this._fire(events.pointerdown, { + evt: evt, + target: this, + currentTarget: this, + pointerId: this._pointerPositions[0].id, + }); + } + } + _pointermove(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + if (Konva$2.isDragging() && DD.node.preventDefault() && evt.cancelable) { + evt.preventDefault(); + } + this.setPointersPositions(evt); + const eventsEnabled = !(Konva$2.isDragging() || Konva$2.isTransforming()) || Konva$2.hitOnDragEnabled; + if (!eventsEnabled) { + return; + } + const processedShapesIds = {}; + let triggeredOnShape = false; + const targetShape = this._getTargetShape(eventType); + this._changedPointerPositions.forEach((pos) => { + const shape = (getCapturedShape(pos.id) || + this.getIntersection(pos)); + const pointerId = pos.id; + const event = { evt: evt, pointerId }; + const differentTarget = targetShape !== shape; + if (differentTarget && targetShape) { + targetShape._fireAndBubble(events.pointerout, { ...event }, shape); + targetShape._fireAndBubble(events.pointerleave, { ...event }, shape); + } + if (shape) { + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + } + if (shape && shape.isListening()) { + triggeredOnShape = true; + if (differentTarget) { + shape._fireAndBubble(events.pointerover, { ...event }, targetShape); + shape._fireAndBubble(events.pointerenter, { ...event }, targetShape); + this[eventType + 'targetShape'] = shape; + } + shape._fireAndBubble(events.pointermove, { ...event }); + } + else { + if (targetShape) { + this._fire(events.pointerover, { + evt: evt, + target: this, + currentTarget: this, + pointerId, + }); + this[eventType + 'targetShape'] = null; + } + } + }); + if (!triggeredOnShape) { + this._fire(events.pointermove, { + evt: evt, + target: this, + currentTarget: this, + pointerId: this._changedPointerPositions[0].id, + }); + } + } + _pointerup(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + this.setPointersPositions(evt); + const clickStartShape = this[eventType + 'ClickStartShape']; + const clickEndShape = this[eventType + 'ClickEndShape']; + const processedShapesIds = {}; + let triggeredOnShape = false; + this._changedPointerPositions.forEach((pos) => { + const shape = (getCapturedShape(pos.id) || + this.getIntersection(pos)); + if (shape) { + shape.releaseCapture(pos.id); + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + } + const pointerId = pos.id; + const event = { evt: evt, pointerId }; + let fireDblClick = false; + if (Konva$2['_' + eventType + 'InDblClickWindow']) { + fireDblClick = true; + clearTimeout(this[eventType + 'DblTimeout']); + } + else if (!DD.justDragged) { + // don't set inDblClickWindow after dragging + Konva$2['_' + eventType + 'InDblClickWindow'] = true; + clearTimeout(this[eventType + 'DblTimeout']); + } + this[eventType + 'DblTimeout'] = setTimeout(function () { + Konva$2['_' + eventType + 'InDblClickWindow'] = false; + }, Konva$2.dblClickWindow); + if (shape && shape.isListening()) { + triggeredOnShape = true; + this[eventType + 'ClickEndShape'] = shape; + shape._fireAndBubble(events.pointerup, { ...event }); + // detect if click or double click occurred + if (Konva$2['_' + eventType + 'ListenClick'] && + clickStartShape && + clickStartShape === shape) { + shape._fireAndBubble(events.pointerclick, { ...event }); + if (fireDblClick && clickEndShape && clickEndShape === shape) { + shape._fireAndBubble(events.pointerdblclick, { ...event }); + } + } + } + else { + this[eventType + 'ClickEndShape'] = null; + if (Konva$2['_' + eventType + 'ListenClick']) { + this._fire(events.pointerclick, { + evt: evt, + target: this, + currentTarget: this, + pointerId, + }); + } + if (fireDblClick) { + this._fire(events.pointerdblclick, { + evt: evt, + target: this, + currentTarget: this, + pointerId, + }); + } + } + }); + if (!triggeredOnShape) { + this._fire(events.pointerup, { + evt: evt, + target: this, + currentTarget: this, + pointerId: this._changedPointerPositions[0].id, + }); + } + Konva$2['_' + eventType + 'ListenClick'] = false; + // always call preventDefault for desktop events because some browsers + // try to drag and drop the canvas element + // TODO: are we sure we need to prevent default at all? + // do not call this function on mobile because it prevent "click" event on all parent containers + // but apps may listen to it. + if (evt.cancelable && eventType !== 'touch' && eventType !== 'pointer') { + evt.preventDefault(); + } + } + _contextmenu(evt) { + this.setPointersPositions(evt); + const shape = this.getIntersection(this.getPointerPosition()); + if (shape && shape.isListening()) { + shape._fireAndBubble(CONTEXTMENU, { evt: evt }); + } + else { + this._fire(CONTEXTMENU, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _wheel(evt) { + this.setPointersPositions(evt); + const shape = this.getIntersection(this.getPointerPosition()); + if (shape && shape.isListening()) { + shape._fireAndBubble(WHEEL, { evt: evt }); + } + else { + this._fire(WHEEL, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _pointercancel(evt) { + this.setPointersPositions(evt); + const shape = getCapturedShape(evt.pointerId) || + this.getIntersection(this.getPointerPosition()); + if (shape) { + shape._fireAndBubble(POINTERUP, createEvent(evt)); + } + releaseCapture(evt.pointerId); + } + _lostpointercapture(evt) { + releaseCapture(evt.pointerId); + } + /** + * manually register pointers positions (mouse/touch) in the stage. + * So you can use stage.getPointerPosition(). Usually you don't need to use that method + * because all internal events are automatically registered. It may be useful if event + * is triggered outside of the stage, but you still want to use Konva methods to get pointers position. + * @method + * @name Konva.Stage#setPointersPositions + * @param {Object} event Event object + * @example + * + * window.addEventListener('mousemove', (e) => { + * stage.setPointersPositions(e); + * }); + */ + setPointersPositions(evt) { + const contentPosition = this._getContentPosition(); + let x = null, y = null; + evt = evt ? evt : window.event; + // touch events + if (evt.touches !== undefined) { + // touchlist has not support for map method + // so we have to iterate + this._pointerPositions = []; + this._changedPointerPositions = []; + Array.prototype.forEach.call(evt.touches, (touch) => { + this._pointerPositions.push({ + id: touch.identifier, + x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, + y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, + }); + }); + Array.prototype.forEach.call(evt.changedTouches || evt.touches, (touch) => { + this._changedPointerPositions.push({ + id: touch.identifier, + x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, + y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, + }); + }); + } + else { + // mouse events + x = (evt.clientX - contentPosition.left) / contentPosition.scaleX; + y = (evt.clientY - contentPosition.top) / contentPosition.scaleY; + this.pointerPos = { + x: x, + y: y, + }; + this._pointerPositions = [{ x, y, id: Util._getFirstPointerId(evt) }]; + this._changedPointerPositions = [ + { x, y, id: Util._getFirstPointerId(evt) }, + ]; + } + } + _setPointerPosition(evt) { + Util.warn('Method _setPointerPosition is deprecated. Use "stage.setPointersPositions(event)" instead.'); + this.setPointersPositions(evt); + } + _getContentPosition() { + if (!this.content || !this.content.getBoundingClientRect) { + return { + top: 0, + left: 0, + scaleX: 1, + scaleY: 1, + }; + } + const rect = this.content.getBoundingClientRect(); + return { + top: rect.top, + left: rect.left, + // sometimes clientWidth can be equals to 0 + // i saw it in react-konva test, looks like it is because of hidden testing element + scaleX: rect.width / this.content.clientWidth || 1, + scaleY: rect.height / this.content.clientHeight || 1, + }; + } + _buildDOM() { + this.bufferCanvas = new SceneCanvas({ + width: this.width(), + height: this.height(), + }); + this.bufferHitCanvas = new HitCanvas({ + pixelRatio: 1, + width: this.width(), + height: this.height(), + }); + if (!Konva$2.isBrowser) { + return; + } + const container = this.container(); + if (!container) { + throw 'Stage has no container. A container is required.'; + } + // clear content inside container + container.innerHTML = ''; + // content + this.content = document.createElement('div'); + this.content.style.position = 'relative'; + this.content.style.userSelect = 'none'; + this.content.className = 'konvajs-content'; + this.content.setAttribute('role', 'presentation'); + container.appendChild(this.content); + this._resizeDOM(); + } + // currently cache function is now working for stage, because stage has no its own canvas element + cache() { + Util.warn('Cache function is not allowed for stage. You may use cache only for layers, groups and shapes.'); + return this; + } + clearCache() { + return this; + } + /** + * batch draw + * @method + * @name Konva.Stage#batchDraw + * @return {Konva.Stage} this + */ + batchDraw() { + this.getChildren().forEach(function (layer) { + layer.batchDraw(); + }); + return this; + } + } + Stage.prototype.nodeType = STAGE; + _registerNode(Stage); + /** + * get/set container DOM element + * @method + * @name Konva.Stage#container + * @returns {DomElement} container + * @example + * // get container + * var container = stage.container(); + * // set container + * var container = document.createElement('div'); + * body.appendChild(container); + * stage.container(container); + */ + Factory.addGetterSetter(Stage, 'container'); + // chrome is clearing canvas in inactive browser window, causing layer content to be erased + // so let's redraw layers as soon as window becomes active + // TODO: any other way to solve this issue? + // TODO: should we remove it if chrome fixes the issue? + if (Konva$2.isBrowser) { + document.addEventListener('visibilitychange', () => { + stages.forEach((stage) => { + stage.batchDraw(); + }); + }); + } + + const HAS_SHADOW = 'hasShadow'; + const SHADOW_RGBA = 'shadowRGBA'; + const patternImage = 'patternImage'; + const linearGradient = 'linearGradient'; + const radialGradient = 'radialGradient'; + let dummyContext$1; + function getDummyContext$1() { + if (dummyContext$1) { + return dummyContext$1; + } + dummyContext$1 = Util.createCanvasElement().getContext('2d'); + return dummyContext$1; + } + const shapes = {}; + // TODO: idea - use only "remove" (or destroy method) + // how? on add, check that every inner shape has reference in konva store with color + // on remove - clear that reference + // the approach is good. But what if we want to cache the shape before we add it into the stage + // what color to use for hit test? + function _fillFunc$2(context) { + const fillRule = this.attrs.fillRule; + if (fillRule) { + context.fill(fillRule); + } + else { + context.fill(); + } + } + function _strokeFunc$2(context) { + context.stroke(); + } + function _fillFuncHit(context) { + const fillRule = this.attrs.fillRule; + if (fillRule) { + context.fill(fillRule); + } + else { + context.fill(); + } + } + function _strokeFuncHit(context) { + context.stroke(); + } + function _clearHasShadowCache() { + this._clearCache(HAS_SHADOW); + } + function _clearGetShadowRGBACache() { + this._clearCache(SHADOW_RGBA); + } + function _clearFillPatternCache() { + this._clearCache(patternImage); + } + function _clearLinearGradientCache() { + this._clearCache(linearGradient); + } + function _clearRadialGradientCache() { + this._clearCache(radialGradient); + } + /** + * Shape constructor. Shapes are primitive objects such as rectangles, + * circles, text, lines, etc. + * @constructor + * @memberof Konva + * @augments Konva.Node + * @param {Object} config + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var customShape = new Konva.Shape({ + * x: 5, + * y: 10, + * fill: 'red', + * // a Konva.Canvas renderer is passed into the sceneFunc function + * sceneFunc (context, shape) { + * context.beginPath(); + * context.moveTo(200, 50); + * context.lineTo(420, 80); + * context.quadraticCurveTo(300, 100, 260, 170); + * context.closePath(); + * // Konva specific method + * context.fillStrokeShape(shape); + * } + *}); + */ + class Shape extends Node { + constructor(config) { + super(config); + // set colorKey + let key; + while (true) { + key = Util.getRandomColor(); + if (key && !(key in shapes)) { + break; + } + } + this.colorKey = key; + shapes[key] = this; + } + getContext() { + Util.warn('shape.getContext() method is deprecated. Please do not use it.'); + return this.getLayer().getContext(); + } + getCanvas() { + Util.warn('shape.getCanvas() method is deprecated. Please do not use it.'); + return this.getLayer().getCanvas(); + } + getSceneFunc() { + return this.attrs.sceneFunc || this['_sceneFunc']; + } + getHitFunc() { + return this.attrs.hitFunc || this['_hitFunc']; + } + /** + * returns whether or not a shadow will be rendered + * @method + * @name Konva.Shape#hasShadow + * @returns {Boolean} + */ + hasShadow() { + return this._getCache(HAS_SHADOW, this._hasShadow); + } + _hasShadow() { + return (this.shadowEnabled() && + this.shadowOpacity() !== 0 && + !!(this.shadowColor() || + this.shadowBlur() || + this.shadowOffsetX() || + this.shadowOffsetY())); + } + _getFillPattern() { + return this._getCache(patternImage, this.__getFillPattern); + } + __getFillPattern() { + if (this.fillPatternImage()) { + const ctx = getDummyContext$1(); + const pattern = ctx.createPattern(this.fillPatternImage(), this.fillPatternRepeat() || 'repeat'); + if (pattern && pattern.setTransform) { + const tr = new Transform(); + tr.translate(this.fillPatternX(), this.fillPatternY()); + tr.rotate(Konva$2.getAngle(this.fillPatternRotation())); + tr.scale(this.fillPatternScaleX(), this.fillPatternScaleY()); + tr.translate(-1 * this.fillPatternOffsetX(), -1 * this.fillPatternOffsetY()); + const m = tr.getMatrix(); + const matrix = typeof DOMMatrix === 'undefined' + ? { + a: m[0], // Horizontal scaling. A value of 1 results in no scaling. + b: m[1], // Vertical skewing. + c: m[2], // Horizontal skewing. + d: m[3], + e: m[4], // Horizontal translation (moving). + f: m[5], // Vertical translation (moving). + } + : new DOMMatrix(m); + pattern.setTransform(matrix); + } + return pattern; + } + } + _getLinearGradient() { + return this._getCache(linearGradient, this.__getLinearGradient); + } + __getLinearGradient() { + const colorStops = this.fillLinearGradientColorStops(); + if (colorStops) { + const ctx = getDummyContext$1(); + const start = this.fillLinearGradientStartPoint(); + const end = this.fillLinearGradientEndPoint(); + const grd = ctx.createLinearGradient(start.x, start.y, end.x, end.y); + // build color stops + for (let n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + return grd; + } + } + _getRadialGradient() { + return this._getCache(radialGradient, this.__getRadialGradient); + } + __getRadialGradient() { + const colorStops = this.fillRadialGradientColorStops(); + if (colorStops) { + const ctx = getDummyContext$1(); + const start = this.fillRadialGradientStartPoint(); + const end = this.fillRadialGradientEndPoint(); + const grd = ctx.createRadialGradient(start.x, start.y, this.fillRadialGradientStartRadius(), end.x, end.y, this.fillRadialGradientEndRadius()); + // build color stops + for (let n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + return grd; + } + } + getShadowRGBA() { + return this._getCache(SHADOW_RGBA, this._getShadowRGBA); + } + _getShadowRGBA() { + if (!this.hasShadow()) { + return; + } + const rgba = Util.colorToRGBA(this.shadowColor()); + if (rgba) { + return ('rgba(' + + rgba.r + + ',' + + rgba.g + + ',' + + rgba.b + + ',' + + rgba.a * (this.shadowOpacity() || 1) + + ')'); + } + } + /** + * returns whether or not the shape will be filled + * @method + * @name Konva.Shape#hasFill + * @returns {Boolean} + */ + hasFill() { + return this._calculate('hasFill', [ + 'fillEnabled', + 'fill', + 'fillPatternImage', + 'fillLinearGradientColorStops', + 'fillRadialGradientColorStops', + ], () => { + return (this.fillEnabled() && + !!(this.fill() || + this.fillPatternImage() || + this.fillLinearGradientColorStops() || + this.fillRadialGradientColorStops())); + }); + } + /** + * returns whether or not the shape will be stroked + * @method + * @name Konva.Shape#hasStroke + * @returns {Boolean} + */ + hasStroke() { + return this._calculate('hasStroke', [ + 'strokeEnabled', + 'strokeWidth', + 'stroke', + 'strokeLinearGradientColorStops', + ], () => { + return (this.strokeEnabled() && + this.strokeWidth() && + !!(this.stroke() || this.strokeLinearGradientColorStops()) + // this.getStrokeRadialGradientColorStops() + ); + }); + // return ( + // this.strokeEnabled() && + // this.strokeWidth() && + // !!(this.stroke() || this.strokeLinearGradientColorStops()) + // // this.getStrokeRadialGradientColorStops() + // ); + } + hasHitStroke() { + const width = this.hitStrokeWidth(); + // on auto just check by stroke + if (width === 'auto') { + return this.hasStroke(); + } + // we should enable hit stroke if stroke is enabled + // and we have some value from width + return this.strokeEnabled() && !!width; + } + /** + * determines if point is in the shape, regardless if other shapes are on top of it. Note: because + * this method clears a temporary canvas and then redraws the shape, it performs very poorly if executed many times + * consecutively. Please use the {@link Konva.Stage#getIntersection} method if at all possible + * because it performs much better + * @method + * @name Konva.Shape#intersects + * @param {Object} point + * @param {Number} point.x + * @param {Number} point.y + * @returns {Boolean} + */ + intersects(point) { + const stage = this.getStage(); + if (!stage) { + return false; + } + const bufferHitCanvas = stage.bufferHitCanvas; + bufferHitCanvas.getContext().clear(); + this.drawHit(bufferHitCanvas, undefined, true); + const p = bufferHitCanvas.context.getImageData(Math.round(point.x), Math.round(point.y), 1, 1).data; + return p[3] > 0; + } + destroy() { + Node.prototype.destroy.call(this); + delete shapes[this.colorKey]; + delete this.colorKey; + return this; + } + // why do we need buffer canvas? + // it give better result when a shape has + // stroke with fill and with some opacity + _useBufferCanvas(forceFill) { + // image and sprite still has "fill" as image + // so they use that method with forced fill + // it probably will be simpler, then copy/paste the code + var _a; + // force skip buffer canvas + const perfectDrawEnabled = (_a = this.attrs.perfectDrawEnabled) !== null && _a !== undefined ? _a : true; + if (!perfectDrawEnabled) { + return false; + } + const hasFill = forceFill || this.hasFill(); + const hasStroke = this.hasStroke(); + const isTransparent = this.getAbsoluteOpacity() !== 1; + if (hasFill && hasStroke && isTransparent) { + return true; + } + const hasShadow = this.hasShadow(); + const strokeForShadow = this.shadowForStrokeEnabled(); + if (hasFill && hasStroke && hasShadow && strokeForShadow) { + return true; + } + return false; + } + setStrokeHitEnabled(val) { + Util.warn('strokeHitEnabled property is deprecated. Please use hitStrokeWidth instead.'); + if (val) { + this.hitStrokeWidth('auto'); + } + else { + this.hitStrokeWidth(0); + } + } + getStrokeHitEnabled() { + if (this.hitStrokeWidth() === 0) { + return false; + } + else { + return true; + } + } + /** + * return self rectangle (x, y, width, height) of shape. + * This method are not taken into account transformation and styles. + * @method + * @name Konva.Shape#getSelfRect + * @returns {Object} rect with {x, y, width, height} properties + * @example + * + * rect.getSelfRect(); // return {x:0, y:0, width:rect.width(), height:rect.height()} + * circle.getSelfRect(); // return {x: - circle.width() / 2, y: - circle.height() / 2, width:circle.width(), height:circle.height()} + * + */ + getSelfRect() { + const size = this.size(); + return { + x: this._centroid ? -size.width / 2 : 0, + y: this._centroid ? -size.height / 2 : 0, + width: size.width, + height: size.height, + }; + } + getClientRect(config = {}) { + // if we have a cached parent, it will use cached transform matrix + // but we don't want to that + let hasCachedParent = false; + let parent = this.getParent(); + while (parent) { + if (parent.isCached()) { + hasCachedParent = true; + break; + } + parent = parent.getParent(); + } + const skipTransform = config.skipTransform; + // force relative to stage if we have a cached parent + const relativeTo = config.relativeTo || (hasCachedParent && this.getStage()) || undefined; + const fillRect = this.getSelfRect(); + const applyStroke = !config.skipStroke && this.hasStroke(); + const strokeWidth = (applyStroke && this.strokeWidth()) || 0; + const fillAndStrokeWidth = fillRect.width + strokeWidth; + const fillAndStrokeHeight = fillRect.height + strokeWidth; + const applyShadow = !config.skipShadow && this.hasShadow(); + const shadowOffsetX = applyShadow ? this.shadowOffsetX() : 0; + const shadowOffsetY = applyShadow ? this.shadowOffsetY() : 0; + const preWidth = fillAndStrokeWidth + Math.abs(shadowOffsetX); + const preHeight = fillAndStrokeHeight + Math.abs(shadowOffsetY); + const blurRadius = (applyShadow && this.shadowBlur()) || 0; + const width = preWidth + blurRadius * 2; + const height = preHeight + blurRadius * 2; + const rect = { + width: width, + height: height, + x: -(strokeWidth / 2 + blurRadius) + + Math.min(shadowOffsetX, 0) + + fillRect.x, + y: -(strokeWidth / 2 + blurRadius) + + Math.min(shadowOffsetY, 0) + + fillRect.y, + }; + if (!skipTransform) { + return this._transformedRect(rect, relativeTo); + } + return rect; + } + drawScene(can, top, bufferCanvas) { + // basically there are 3 drawing modes + // 1 - simple drawing when nothing is cached. + // 2 - when we are caching current + // 3 - when node is cached and we need to draw it into layer + const layer = this.getLayer(); + let canvas = can || layer.getCanvas(), context = canvas.getContext(), cachedCanvas = this._getCanvasCache(), drawFunc = this.getSceneFunc(), hasShadow = this.hasShadow(), stage, bufferContext; + const skipBuffer = canvas.isCache; + const cachingSelf = top === this; + if (!this.isVisible() && !cachingSelf) { + return this; + } + // if node is cached we just need to draw from cache + if (cachedCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedSceneCanvas(context); + context.restore(); + return this; + } + if (!drawFunc) { + return this; + } + context.save(); + // if buffer canvas is needed + if (this._useBufferCanvas() && !skipBuffer) { + stage = this.getStage(); + const bc = bufferCanvas || stage.bufferCanvas; + bufferContext = bc.getContext(); + bufferContext.clear(); + bufferContext.save(); + bufferContext._applyLineJoin(this); + // layer might be undefined if we are using cache before adding to layer + var o = this.getAbsoluteTransform(top).getMatrix(); + bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]); + drawFunc.call(this, bufferContext, this); + bufferContext.restore(); + const ratio = bc.pixelRatio; + if (hasShadow) { + context._applyShadow(this); + } + context._applyOpacity(this); + context._applyGlobalCompositeOperation(this); + context.drawImage(bc._canvas, 0, 0, bc.width / ratio, bc.height / ratio); + } + else { + context._applyLineJoin(this); + if (!cachingSelf) { + var o = this.getAbsoluteTransform(top).getMatrix(); + context.transform(o[0], o[1], o[2], o[3], o[4], o[5]); + context._applyOpacity(this); + context._applyGlobalCompositeOperation(this); + } + if (hasShadow) { + context._applyShadow(this); + } + drawFunc.call(this, context, this); + } + context.restore(); + return this; + } + drawHit(can, top, skipDragCheck = false) { + if (!this.shouldDrawHit(top, skipDragCheck)) { + return this; + } + const layer = this.getLayer(), canvas = can || layer.hitCanvas, context = canvas && canvas.getContext(), drawFunc = this.hitFunc() || this.sceneFunc(), cachedCanvas = this._getCanvasCache(), cachedHitCanvas = cachedCanvas && cachedCanvas.hit; + if (!this.colorKey) { + Util.warn('Looks like your canvas has a destroyed shape in it. Do not reuse shape after you destroyed it. If you want to reuse shape you should call remove() instead of destroy()'); + } + if (cachedHitCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedHitCanvas(context); + context.restore(); + return this; + } + if (!drawFunc) { + return this; + } + context.save(); + context._applyLineJoin(this); + const selfCache = this === top; + if (!selfCache) { + const o = this.getAbsoluteTransform(top).getMatrix(); + context.transform(o[0], o[1], o[2], o[3], o[4], o[5]); + } + drawFunc.call(this, context, this); + context.restore(); + return this; + } + /** + * draw hit graph using the cached scene canvas + * @method + * @name Konva.Shape#drawHitFromCache + * @param {Integer} alphaThreshold alpha channel threshold that determines whether or not + * a pixel should be drawn onto the hit graph. Must be a value between 0 and 255. + * The default is 0 + * @returns {Konva.Shape} + * @example + * shape.cache(); + * shape.drawHitFromCache(); + */ + drawHitFromCache(alphaThreshold = 0) { + const cachedCanvas = this._getCanvasCache(), sceneCanvas = this._getCachedSceneCanvas(), hitCanvas = cachedCanvas.hit, hitContext = hitCanvas.getContext(), hitWidth = hitCanvas.getWidth(), hitHeight = hitCanvas.getHeight(); + hitContext.clear(); + hitContext.drawImage(sceneCanvas._canvas, 0, 0, hitWidth, hitHeight); + try { + const hitImageData = hitContext.getImageData(0, 0, hitWidth, hitHeight); + const hitData = hitImageData.data; + const len = hitData.length; + const rgbColorKey = Util._hexToRgb(this.colorKey); + // replace non transparent pixels with color key + for (let i = 0; i < len; i += 4) { + const alpha = hitData[i + 3]; + if (alpha > alphaThreshold) { + hitData[i] = rgbColorKey.r; + hitData[i + 1] = rgbColorKey.g; + hitData[i + 2] = rgbColorKey.b; + hitData[i + 3] = 255; + } + else { + hitData[i + 3] = 0; + } + } + hitContext.putImageData(hitImageData, 0, 0); + } + catch (e) { + Util.error('Unable to draw hit graph from cached scene canvas. ' + e.message); + } + return this; + } + hasPointerCapture(pointerId) { + return hasPointerCapture(pointerId, this); + } + setPointerCapture(pointerId) { + setPointerCapture(pointerId, this); + } + releaseCapture(pointerId) { + releaseCapture(pointerId); + } + } + Shape.prototype._fillFunc = _fillFunc$2; + Shape.prototype._strokeFunc = _strokeFunc$2; + Shape.prototype._fillFuncHit = _fillFuncHit; + Shape.prototype._strokeFuncHit = _strokeFuncHit; + Shape.prototype._centroid = false; + Shape.prototype.nodeType = 'Shape'; + _registerNode(Shape); + Shape.prototype.eventListeners = {}; + Shape.prototype.on.call(Shape.prototype, 'shadowColorChange.konva shadowBlurChange.konva shadowOffsetChange.konva shadowOpacityChange.konva shadowEnabledChange.konva', _clearHasShadowCache); + Shape.prototype.on.call(Shape.prototype, 'shadowColorChange.konva shadowOpacityChange.konva shadowEnabledChange.konva', _clearGetShadowRGBACache); + Shape.prototype.on.call(Shape.prototype, 'fillPriorityChange.konva fillPatternImageChange.konva fillPatternRepeatChange.konva fillPatternScaleXChange.konva fillPatternScaleYChange.konva fillPatternOffsetXChange.konva fillPatternOffsetYChange.konva fillPatternXChange.konva fillPatternYChange.konva fillPatternRotationChange.konva', _clearFillPatternCache); + Shape.prototype.on.call(Shape.prototype, 'fillPriorityChange.konva fillLinearGradientColorStopsChange.konva fillLinearGradientStartPointXChange.konva fillLinearGradientStartPointYChange.konva fillLinearGradientEndPointXChange.konva fillLinearGradientEndPointYChange.konva', _clearLinearGradientCache); + Shape.prototype.on.call(Shape.prototype, 'fillPriorityChange.konva fillRadialGradientColorStopsChange.konva fillRadialGradientStartPointXChange.konva fillRadialGradientStartPointYChange.konva fillRadialGradientEndPointXChange.konva fillRadialGradientEndPointYChange.konva fillRadialGradientStartRadiusChange.konva fillRadialGradientEndRadiusChange.konva', _clearRadialGradientCache); + // add getters and setters + Factory.addGetterSetter(Shape, 'stroke', undefined, getStringOrGradientValidator()); + /** + * get/set stroke color + * @name Konva.Shape#stroke + * @method + * @param {String} color + * @returns {String} + * @example + * // get stroke color + * var stroke = shape.stroke(); + * + * // set stroke color with color string + * shape.stroke('green'); + * + * // set stroke color with hex + * shape.stroke('#00ff00'); + * + * // set stroke color with rgb + * shape.stroke('rgb(0,255,0)'); + * + * // set stroke color with rgba and make it 50% opaque + * shape.stroke('rgba(0,255,0,0.5'); + */ + Factory.addGetterSetter(Shape, 'strokeWidth', 2, getNumberValidator()); + /** + * get/set stroke width + * @name Konva.Shape#strokeWidth + * @method + * @param {Number} strokeWidth + * @returns {Number} + * @example + * // get stroke width + * var strokeWidth = shape.strokeWidth(); + * + * // set stroke width + * shape.strokeWidth(10); + */ + Factory.addGetterSetter(Shape, 'fillAfterStrokeEnabled', false); + /** + * get/set fillAfterStrokeEnabled property. By default Konva is drawing filling first, then stroke on top of the fill. + * In rare situations you may want a different behavior. When you have a stroke first then fill on top of it. + * Especially useful for Text objects. + * Default is false. + * @name Konva.Shape#fillAfterStrokeEnabled + * @method + * @param {Boolean} fillAfterStrokeEnabled + * @returns {Boolean} + * @example + * // get stroke width + * var fillAfterStrokeEnabled = shape.fillAfterStrokeEnabled(); + * + * // set stroke width + * shape.fillAfterStrokeEnabled(true); + */ + Factory.addGetterSetter(Shape, 'hitStrokeWidth', 'auto', getNumberOrAutoValidator()); + /** + * get/set stroke width for hit detection. Default value is "auto", it means it will be equals to strokeWidth + * @name Konva.Shape#hitStrokeWidth + * @method + * @param {Number} hitStrokeWidth + * @returns {Number} + * @example + * // get stroke width + * var hitStrokeWidth = shape.hitStrokeWidth(); + * + * // set hit stroke width + * shape.hitStrokeWidth(20); + * // set hit stroke width always equals to scene stroke width + * shape.hitStrokeWidth('auto'); + */ + Factory.addGetterSetter(Shape, 'strokeHitEnabled', true, getBooleanValidator()); + /** + * **deprecated, use hitStrokeWidth instead!** get/set strokeHitEnabled property. Useful for performance optimization. + * You may set `shape.strokeHitEnabled(false)`. In this case stroke will be no draw on hit canvas, so hit area + * of shape will be decreased (by lineWidth / 2). Remember that non closed line with `strokeHitEnabled = false` + * will be not drawn on hit canvas, that is mean line will no trigger pointer events (like mouseover) + * Default value is true. + * @name Konva.Shape#strokeHitEnabled + * @method + * @param {Boolean} strokeHitEnabled + * @returns {Boolean} + * @example + * // get strokeHitEnabled + * var strokeHitEnabled = shape.strokeHitEnabled(); + * + * // set strokeHitEnabled + * shape.strokeHitEnabled(); + */ + Factory.addGetterSetter(Shape, 'perfectDrawEnabled', true, getBooleanValidator()); + /** + * get/set perfectDrawEnabled. If a shape has fill, stroke and opacity you may set `perfectDrawEnabled` to false to improve performance. + * See http://konvajs.org/docs/performance/Disable_Perfect_Draw.html for more information. + * Default value is true + * @name Konva.Shape#perfectDrawEnabled + * @method + * @param {Boolean} perfectDrawEnabled + * @returns {Boolean} + * @example + * // get perfectDrawEnabled + * var perfectDrawEnabled = shape.perfectDrawEnabled(); + * + * // set perfectDrawEnabled + * shape.perfectDrawEnabled(); + */ + Factory.addGetterSetter(Shape, 'shadowForStrokeEnabled', true, getBooleanValidator()); + /** + * get/set shadowForStrokeEnabled. Useful for performance optimization. + * You may set `shape.shadowForStrokeEnabled(false)`. In this case stroke will no effect shadow. + * Remember if you set `shadowForStrokeEnabled = false` for non closed line - that line will have no shadow!. + * Default value is true + * @name Konva.Shape#shadowForStrokeEnabled + * @method + * @param {Boolean} shadowForStrokeEnabled + * @returns {Boolean} + * @example + * // get shadowForStrokeEnabled + * var shadowForStrokeEnabled = shape.shadowForStrokeEnabled(); + * + * // set shadowForStrokeEnabled + * shape.shadowForStrokeEnabled(); + */ + Factory.addGetterSetter(Shape, 'lineJoin'); + /** + * get/set line join. Can be miter, round, or bevel. The + * default is miter + * @name Konva.Shape#lineJoin + * @method + * @param {String} lineJoin + * @returns {String} + * @example + * // get line join + * var lineJoin = shape.lineJoin(); + * + * // set line join + * shape.lineJoin('round'); + */ + Factory.addGetterSetter(Shape, 'lineCap'); + /** + * get/set line cap. Can be butt, round, or square + * @name Konva.Shape#lineCap + * @method + * @param {String} lineCap + * @returns {String} + * @example + * // get line cap + * var lineCap = shape.lineCap(); + * + * // set line cap + * shape.lineCap('round'); + */ + Factory.addGetterSetter(Shape, 'sceneFunc'); + /** + * get/set scene draw function. That function is used to draw the shape on a canvas. + * Also that function will be used to draw hit area of the shape, in case if hitFunc is not defined. + * @name Konva.Shape#sceneFunc + * @method + * @param {Function} drawFunc drawing function + * @returns {Function} + * @example + * // get scene draw function + * var sceneFunc = shape.sceneFunc(); + * + * // set scene draw function + * shape.sceneFunc(function(context, shape) { + * context.beginPath(); + * context.rect(0, 0, shape.width(), shape.height()); + * context.closePath(); + * // important Konva method that fill and stroke shape from its properties + * // like stroke and fill + * context.fillStrokeShape(shape); + * }); + */ + Factory.addGetterSetter(Shape, 'hitFunc'); + /** + * get/set hit draw function. That function is used to draw custom hit area of a shape. + * @name Konva.Shape#hitFunc + * @method + * @param {Function} drawFunc drawing function + * @returns {Function} + * @example + * // get hit draw function + * var hitFunc = shape.hitFunc(); + * + * // set hit draw function + * shape.hitFunc(function(context) { + * context.beginPath(); + * context.rect(0, 0, shape.width(), shape.height()); + * context.closePath(); + * // important Konva method that fill and stroke shape from its properties + * context.fillStrokeShape(shape); + * }); + */ + Factory.addGetterSetter(Shape, 'dash'); + /** + * get/set dash array for stroke. + * @name Konva.Shape#dash + * @method + * @param {Array} dash + * @returns {Array} + * @example + * // apply dashed stroke that is 10px long and 5 pixels apart + * line.dash([10, 5]); + * // apply dashed stroke that is made up of alternating dashed + * // lines that are 10px long and 20px apart, and dots that have + * // a radius of 5px and are 20px apart + * line.dash([10, 20, 0.001, 20]); + */ + Factory.addGetterSetter(Shape, 'dashOffset', 0, getNumberValidator()); + /** + * get/set dash offset for stroke. + * @name Konva.Shape#dash + * @method + * @param {Number} dash offset + * @returns {Number} + * @example + * // apply dashed stroke that is 10px long and 5 pixels apart with an offset of 5px + * line.dash([10, 5]); + * line.dashOffset(5); + */ + Factory.addGetterSetter(Shape, 'shadowColor', undefined, getStringValidator()); + /** + * get/set shadow color + * @name Konva.Shape#shadowColor + * @method + * @param {String} color + * @returns {String} + * @example + * // get shadow color + * var shadow = shape.shadowColor(); + * + * // set shadow color with color string + * shape.shadowColor('green'); + * + * // set shadow color with hex + * shape.shadowColor('#00ff00'); + * + * // set shadow color with rgb + * shape.shadowColor('rgb(0,255,0)'); + * + * // set shadow color with rgba and make it 50% opaque + * shape.shadowColor('rgba(0,255,0,0.5'); + */ + Factory.addGetterSetter(Shape, 'shadowBlur', 0, getNumberValidator()); + /** + * get/set shadow blur + * @name Konva.Shape#shadowBlur + * @method + * @param {Number} blur + * @returns {Number} + * @example + * // get shadow blur + * var shadowBlur = shape.shadowBlur(); + * + * // set shadow blur + * shape.shadowBlur(10); + */ + Factory.addGetterSetter(Shape, 'shadowOpacity', 1, getNumberValidator()); + /** + * get/set shadow opacity. must be a value between 0 and 1 + * @name Konva.Shape#shadowOpacity + * @method + * @param {Number} opacity + * @returns {Number} + * @example + * // get shadow opacity + * var shadowOpacity = shape.shadowOpacity(); + * + * // set shadow opacity + * shape.shadowOpacity(0.5); + */ + Factory.addComponentsGetterSetter(Shape, 'shadowOffset', ['x', 'y']); + /** + * get/set shadow offset + * @name Konva.Shape#shadowOffset + * @method + * @param {Object} offset + * @param {Number} offset.x + * @param {Number} offset.y + * @returns {Object} + * @example + * // get shadow offset + * var shadowOffset = shape.shadowOffset(); + * + * // set shadow offset + * shape.shadowOffset({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'shadowOffsetX', 0, getNumberValidator()); + /** + * get/set shadow offset x + * @name Konva.Shape#shadowOffsetX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get shadow offset x + * var shadowOffsetX = shape.shadowOffsetX(); + * + * // set shadow offset x + * shape.shadowOffsetX(5); + */ + Factory.addGetterSetter(Shape, 'shadowOffsetY', 0, getNumberValidator()); + /** + * get/set shadow offset y + * @name Konva.Shape#shadowOffsetY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get shadow offset y + * var shadowOffsetY = shape.shadowOffsetY(); + * + * // set shadow offset y + * shape.shadowOffsetY(5); + */ + Factory.addGetterSetter(Shape, 'fillPatternImage'); + /** + * get/set fill pattern image + * @name Konva.Shape#fillPatternImage + * @method + * @param {Image} image object + * @returns {Image} + * @example + * // get fill pattern image + * var fillPatternImage = shape.fillPatternImage(); + * + * // set fill pattern image + * var imageObj = new Image(); + * imageObj.onload = function() { + * shape.fillPatternImage(imageObj); + * }; + * imageObj.src = 'path/to/image/jpg'; + */ + Factory.addGetterSetter(Shape, 'fill', undefined, getStringOrGradientValidator()); + /** + * get/set fill color + * @name Konva.Shape#fill + * @method + * @param {String} color + * @returns {String} + * @example + * // get fill color + * var fill = shape.fill(); + * + * // set fill color with color string + * shape.fill('green'); + * + * // set fill color with hex + * shape.fill('#00ff00'); + * + * // set fill color with rgb + * shape.fill('rgb(0,255,0)'); + * + * // set fill color with rgba and make it 50% opaque + * shape.fill('rgba(0,255,0,0.5'); + * + * // shape without fill + * shape.fill(null); + */ + Factory.addGetterSetter(Shape, 'fillPatternX', 0, getNumberValidator()); + /** + * get/set fill pattern x + * @name Konva.Shape#fillPatternX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill pattern x + * var fillPatternX = shape.fillPatternX(); + * // set fill pattern x + * shape.fillPatternX(20); + */ + Factory.addGetterSetter(Shape, 'fillPatternY', 0, getNumberValidator()); + /** + * get/set fill pattern y + * @name Konva.Shape#fillPatternY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill pattern y + * var fillPatternY = shape.fillPatternY(); + * // set fill pattern y + * shape.fillPatternY(20); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientColorStops'); + /** + * get/set fill linear gradient color stops + * @name Konva.Shape#fillLinearGradientColorStops + * @method + * @param {Array} colorStops + * @returns {Array} colorStops + * @example + * // get fill linear gradient color stops + * var colorStops = shape.fillLinearGradientColorStops(); + * + * // create a linear gradient that starts with red, changes to blue + * // halfway through, and then changes to green + * shape.fillLinearGradientColorStops(0, 'red', 0.5, 'blue', 1, 'green'); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientColorStops'); + /** + * get/set stroke linear gradient color stops + * @name Konva.Shape#strokeLinearGradientColorStops + * @method + * @param {Array} colorStops + * @returns {Array} colorStops + * @example + * // get stroke linear gradient color stops + * var colorStops = shape.strokeLinearGradientColorStops(); + * + * // create a linear gradient that starts with red, changes to blue + * // halfway through, and then changes to green + * shape.strokeLinearGradientColorStops([0, 'red', 0.5, 'blue', 1, 'green']); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientStartRadius', 0); + /** + * get/set fill radial gradient start radius + * @name Konva.Shape#fillRadialGradientStartRadius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radial gradient start radius + * var startRadius = shape.fillRadialGradientStartRadius(); + * + * // set radial gradient start radius + * shape.fillRadialGradientStartRadius(0); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientEndRadius', 0); + /** + * get/set fill radial gradient end radius + * @name Konva.Shape#fillRadialGradientEndRadius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radial gradient end radius + * var endRadius = shape.fillRadialGradientEndRadius(); + * + * // set radial gradient end radius + * shape.fillRadialGradientEndRadius(100); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientColorStops'); + /** + * get/set fill radial gradient color stops + * @name Konva.Shape#fillRadialGradientColorStops + * @method + * @param {Number} colorStops + * @returns {Array} + * @example + * // get fill radial gradient color stops + * var colorStops = shape.fillRadialGradientColorStops(); + * + * // create a radial gradient that starts with red, changes to blue + * // halfway through, and then changes to green + * shape.fillRadialGradientColorStops(0, 'red', 0.5, 'blue', 1, 'green'); + */ + Factory.addGetterSetter(Shape, 'fillPatternRepeat', 'repeat'); + /** + * get/set fill pattern repeat. Can be 'repeat', 'repeat-x', 'repeat-y', or 'no-repeat'. The default is 'repeat' + * @name Konva.Shape#fillPatternRepeat + * @method + * @param {String} repeat + * @returns {String} + * @example + * // get fill pattern repeat + * var repeat = shape.fillPatternRepeat(); + * + * // repeat pattern in x direction only + * shape.fillPatternRepeat('repeat-x'); + * + * // do not repeat the pattern + * shape.fillPatternRepeat('no-repeat'); + */ + Factory.addGetterSetter(Shape, 'fillEnabled', true); + /** + * get/set fill enabled flag + * @name Konva.Shape#fillEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get fill enabled flag + * var fillEnabled = shape.fillEnabled(); + * + * // disable fill + * shape.fillEnabled(false); + * + * // enable fill + * shape.fillEnabled(true); + */ + Factory.addGetterSetter(Shape, 'strokeEnabled', true); + /** + * get/set stroke enabled flag + * @name Konva.Shape#strokeEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get stroke enabled flag + * var strokeEnabled = shape.strokeEnabled(); + * + * // disable stroke + * shape.strokeEnabled(false); + * + * // enable stroke + * shape.strokeEnabled(true); + */ + Factory.addGetterSetter(Shape, 'shadowEnabled', true); + /** + * get/set shadow enabled flag + * @name Konva.Shape#shadowEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get shadow enabled flag + * var shadowEnabled = shape.shadowEnabled(); + * + * // disable shadow + * shape.shadowEnabled(false); + * + * // enable shadow + * shape.shadowEnabled(true); + */ + Factory.addGetterSetter(Shape, 'dashEnabled', true); + /** + * get/set dash enabled flag + * @name Konva.Shape#dashEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get dash enabled flag + * var dashEnabled = shape.dashEnabled(); + * + * // disable dash + * shape.dashEnabled(false); + * + * // enable dash + * shape.dashEnabled(true); + */ + Factory.addGetterSetter(Shape, 'strokeScaleEnabled', true); + /** + * get/set strokeScale enabled flag + * @name Konva.Shape#strokeScaleEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get stroke scale enabled flag + * var strokeScaleEnabled = shape.strokeScaleEnabled(); + * + * // disable stroke scale + * shape.strokeScaleEnabled(false); + * + * // enable stroke scale + * shape.strokeScaleEnabled(true); + */ + Factory.addGetterSetter(Shape, 'fillPriority', 'color'); + /** + * get/set fill priority. can be color, pattern, linear-gradient, or radial-gradient. The default is color. + * This is handy if you want to toggle between different fill types. + * @name Konva.Shape#fillPriority + * @method + * @param {String} priority + * @returns {String} + * @example + * // get fill priority + * var fillPriority = shape.fillPriority(); + * + * // set fill priority + * shape.fillPriority('linear-gradient'); + */ + Factory.addComponentsGetterSetter(Shape, 'fillPatternOffset', ['x', 'y']); + /** + * get/set fill pattern offset + * @name Konva.Shape#fillPatternOffset + * @method + * @param {Object} offset + * @param {Number} offset.x + * @param {Number} offset.y + * @returns {Object} + * @example + * // get fill pattern offset + * var patternOffset = shape.fillPatternOffset(); + * + * // set fill pattern offset + * shape.fillPatternOffset({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillPatternOffsetX', 0, getNumberValidator()); + /** + * get/set fill pattern offset x + * @name Konva.Shape#fillPatternOffsetX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill pattern offset x + * var patternOffsetX = shape.fillPatternOffsetX(); + * + * // set fill pattern offset x + * shape.fillPatternOffsetX(20); + */ + Factory.addGetterSetter(Shape, 'fillPatternOffsetY', 0, getNumberValidator()); + /** + * get/set fill pattern offset y + * @name Konva.Shape#fillPatternOffsetY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill pattern offset y + * var patternOffsetY = shape.fillPatternOffsetY(); + * + * // set fill pattern offset y + * shape.fillPatternOffsetY(10); + */ + Factory.addComponentsGetterSetter(Shape, 'fillPatternScale', ['x', 'y']); + /** + * get/set fill pattern scale + * @name Konva.Shape#fillPatternScale + * @method + * @param {Object} scale + * @param {Number} scale.x + * @param {Number} scale.y + * @returns {Object} + * @example + * // get fill pattern scale + * var patternScale = shape.fillPatternScale(); + * + * // set fill pattern scale + * shape.fillPatternScale({ + * x: 2, + * y: 2 + * }); + */ + Factory.addGetterSetter(Shape, 'fillPatternScaleX', 1, getNumberValidator()); + /** + * get/set fill pattern scale x + * @name Konva.Shape#fillPatternScaleX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill pattern scale x + * var patternScaleX = shape.fillPatternScaleX(); + * + * // set fill pattern scale x + * shape.fillPatternScaleX(2); + */ + Factory.addGetterSetter(Shape, 'fillPatternScaleY', 1, getNumberValidator()); + /** + * get/set fill pattern scale y + * @name Konva.Shape#fillPatternScaleY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill pattern scale y + * var patternScaleY = shape.fillPatternScaleY(); + * + * // set fill pattern scale y + * shape.fillPatternScaleY(2); + */ + Factory.addComponentsGetterSetter(Shape, 'fillLinearGradientStartPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill linear gradient start point + * @name Konva.Shape#fillLinearGradientStartPoint + * @method + * @param {Object} startPoint + * @param {Number} startPoint.x + * @param {Number} startPoint.y + * @returns {Object} + * @example + * // get fill linear gradient start point + * var startPoint = shape.fillLinearGradientStartPoint(); + * + * // set fill linear gradient start point + * shape.fillLinearGradientStartPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addComponentsGetterSetter(Shape, 'strokeLinearGradientStartPoint', [ + 'x', + 'y', + ]); + /** + * get/set stroke linear gradient start point + * @name Konva.Shape#strokeLinearGradientStartPoint + * @method + * @param {Object} startPoint + * @param {Number} startPoint.x + * @param {Number} startPoint.y + * @returns {Object} + * @example + * // get stroke linear gradient start point + * var startPoint = shape.strokeLinearGradientStartPoint(); + * + * // set stroke linear gradient start point + * shape.strokeLinearGradientStartPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientStartPointX', 0); + /** + * get/set fill linear gradient start point x + * @name Konva.Shape#fillLinearGradientStartPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill linear gradient start point x + * var startPointX = shape.fillLinearGradientStartPointX(); + * + * // set fill linear gradient start point x + * shape.fillLinearGradientStartPointX(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientStartPointX', 0); + /** + * get/set stroke linear gradient start point x + * @name Konva.Shape#linearLinearGradientStartPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get stroke linear gradient start point x + * var startPointX = shape.strokeLinearGradientStartPointX(); + * + * // set stroke linear gradient start point x + * shape.strokeLinearGradientStartPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientStartPointY', 0); + /** + * get/set fill linear gradient start point y + * @name Konva.Shape#fillLinearGradientStartPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill linear gradient start point y + * var startPointY = shape.fillLinearGradientStartPointY(); + * + * // set fill linear gradient start point y + * shape.fillLinearGradientStartPointY(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientStartPointY', 0); + /** + * get/set stroke linear gradient start point y + * @name Konva.Shape#strokeLinearGradientStartPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get stroke linear gradient start point y + * var startPointY = shape.strokeLinearGradientStartPointY(); + * + * // set stroke linear gradient start point y + * shape.strokeLinearGradientStartPointY(20); + */ + Factory.addComponentsGetterSetter(Shape, 'fillLinearGradientEndPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill linear gradient end point + * @name Konva.Shape#fillLinearGradientEndPoint + * @method + * @param {Object} endPoint + * @param {Number} endPoint.x + * @param {Number} endPoint.y + * @returns {Object} + * @example + * // get fill linear gradient end point + * var endPoint = shape.fillLinearGradientEndPoint(); + * + * // set fill linear gradient end point + * shape.fillLinearGradientEndPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addComponentsGetterSetter(Shape, 'strokeLinearGradientEndPoint', [ + 'x', + 'y', + ]); + /** + * get/set stroke linear gradient end point + * @name Konva.Shape#strokeLinearGradientEndPoint + * @method + * @param {Object} endPoint + * @param {Number} endPoint.x + * @param {Number} endPoint.y + * @returns {Object} + * @example + * // get stroke linear gradient end point + * var endPoint = shape.strokeLinearGradientEndPoint(); + * + * // set stroke linear gradient end point + * shape.strokeLinearGradientEndPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientEndPointX', 0); + /** + * get/set fill linear gradient end point x + * @name Konva.Shape#fillLinearGradientEndPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill linear gradient end point x + * var endPointX = shape.fillLinearGradientEndPointX(); + * + * // set fill linear gradient end point x + * shape.fillLinearGradientEndPointX(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientEndPointX', 0); + /** + * get/set fill linear gradient end point x + * @name Konva.Shape#strokeLinearGradientEndPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get stroke linear gradient end point x + * var endPointX = shape.strokeLinearGradientEndPointX(); + * + * // set stroke linear gradient end point x + * shape.strokeLinearGradientEndPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientEndPointY', 0); + /** + * get/set fill linear gradient end point y + * @name Konva.Shape#fillLinearGradientEndPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill linear gradient end point y + * var endPointY = shape.fillLinearGradientEndPointY(); + * + * // set fill linear gradient end point y + * shape.fillLinearGradientEndPointY(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientEndPointY', 0); + /** + * get/set stroke linear gradient end point y + * @name Konva.Shape#strokeLinearGradientEndPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get stroke linear gradient end point y + * var endPointY = shape.strokeLinearGradientEndPointY(); + * + * // set stroke linear gradient end point y + * shape.strokeLinearGradientEndPointY(20); + */ + Factory.addComponentsGetterSetter(Shape, 'fillRadialGradientStartPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill radial gradient start point + * @name Konva.Shape#fillRadialGradientStartPoint + * @method + * @param {Object} startPoint + * @param {Number} startPoint.x + * @param {Number} startPoint.y + * @returns {Object} + * @example + * // get fill radial gradient start point + * var startPoint = shape.fillRadialGradientStartPoint(); + * + * // set fill radial gradient start point + * shape.fillRadialGradientStartPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientStartPointX', 0); + /** + * get/set fill radial gradient start point x + * @name Konva.Shape#fillRadialGradientStartPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill radial gradient start point x + * var startPointX = shape.fillRadialGradientStartPointX(); + * + * // set fill radial gradient start point x + * shape.fillRadialGradientStartPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientStartPointY', 0); + /** + * get/set fill radial gradient start point y + * @name Konva.Shape#fillRadialGradientStartPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill radial gradient start point y + * var startPointY = shape.fillRadialGradientStartPointY(); + * + * // set fill radial gradient start point y + * shape.fillRadialGradientStartPointY(20); + */ + Factory.addComponentsGetterSetter(Shape, 'fillRadialGradientEndPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill radial gradient end point + * @name Konva.Shape#fillRadialGradientEndPoint + * @method + * @param {Object} endPoint + * @param {Number} endPoint.x + * @param {Number} endPoint.y + * @returns {Object} + * @example + * // get fill radial gradient end point + * var endPoint = shape.fillRadialGradientEndPoint(); + * + * // set fill radial gradient end point + * shape.fillRadialGradientEndPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientEndPointX', 0); + /** + * get/set fill radial gradient end point x + * @name Konva.Shape#fillRadialGradientEndPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill radial gradient end point x + * var endPointX = shape.fillRadialGradientEndPointX(); + * + * // set fill radial gradient end point x + * shape.fillRadialGradientEndPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientEndPointY', 0); + /** + * get/set fill radial gradient end point y + * @name Konva.Shape#fillRadialGradientEndPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill radial gradient end point y + * var endPointY = shape.fillRadialGradientEndPointY(); + * + * // set fill radial gradient end point y + * shape.fillRadialGradientEndPointY(20); + */ + Factory.addGetterSetter(Shape, 'fillPatternRotation', 0); + /** + * get/set fill pattern rotation in degrees + * @name Konva.Shape#fillPatternRotation + * @method + * @param {Number} rotation + * @returns {Konva.Shape} + * @example + * // get fill pattern rotation + * var patternRotation = shape.fillPatternRotation(); + * + * // set fill pattern rotation + * shape.fillPatternRotation(20); + */ + Factory.addGetterSetter(Shape, 'fillRule', undefined, getStringValidator()); + /** + * get/set fill rule + * @name Konva.Shape#fillRule + * @method + * @param {CanvasFillRule} rotation + * @returns {Konva.Shape} + * @example + * // get fill rule + * var fillRule = shape.fillRule(); + * + * // set fill rule + * shape.fillRule('evenodd'); + */ + Factory.backCompat(Shape, { + dashArray: 'dash', + getDashArray: 'getDash', + setDashArray: 'getDash', + drawFunc: 'sceneFunc', + getDrawFunc: 'getSceneFunc', + setDrawFunc: 'setSceneFunc', + drawHitFunc: 'hitFunc', + getDrawHitFunc: 'getHitFunc', + setDrawHitFunc: 'setHitFunc', + }); + + // constants + const HASH = '#', BEFORE_DRAW = 'beforeDraw', DRAW = 'draw', + /* + * 2 - 3 - 4 + * | | + * 1 - 0 5 + * | + * 8 - 7 - 6 + */ + INTERSECTION_OFFSETS = [ + { x: 0, y: 0 }, // 0 + { x: -1, y: -1 }, // 2 + { x: 1, y: -1 }, // 4 + { x: 1, y: 1 }, // 6 + { x: -1, y: 1 }, // 8 + ], INTERSECTION_OFFSETS_LEN = INTERSECTION_OFFSETS.length; + /** + * Layer constructor. Layers are tied to their own canvas element and are used + * to contain groups or shapes. + * @constructor + * @memberof Konva + * @augments Konva.Container + * @param {Object} config + * @param {Boolean} [config.clearBeforeDraw] set this property to false if you don't want + * to clear the canvas before each layer draw. The default value is true. + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + * @example + * var layer = new Konva.Layer(); + * stage.add(layer); + * // now you can add shapes, groups into the layer + */ + class Layer extends Container { + constructor(config) { + super(config); + this.canvas = new SceneCanvas(); + this.hitCanvas = new HitCanvas({ + pixelRatio: 1, + }); + this._waitingForDraw = false; + this.on('visibleChange.konva', this._checkVisibility); + this._checkVisibility(); + this.on('imageSmoothingEnabledChange.konva', this._setSmoothEnabled); + this._setSmoothEnabled(); + } + // for nodejs? + createPNGStream() { + const c = this.canvas._canvas; + return c.createPNGStream(); + } + /** + * get layer canvas wrapper + * @method + * @name Konva.Layer#getCanvas + */ + getCanvas() { + return this.canvas; + } + /** + * get native canvas element + * @method + * @name Konva.Layer#getNativeCanvasElement + */ + getNativeCanvasElement() { + return this.canvas._canvas; + } + /** + * get layer hit canvas + * @method + * @name Konva.Layer#getHitCanvas + */ + getHitCanvas() { + return this.hitCanvas; + } + /** + * get layer canvas context + * @method + * @name Konva.Layer#getContext + */ + getContext() { + return this.getCanvas().getContext(); + } + // TODO: deprecate this method + clear(bounds) { + this.getContext().clear(bounds); + this.getHitCanvas().getContext().clear(bounds); + return this; + } + // extend Node.prototype.setZIndex + setZIndex(index) { + super.setZIndex(index); + const stage = this.getStage(); + if (stage && stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + if (index < stage.children.length - 1) { + stage.content.insertBefore(this.getNativeCanvasElement(), stage.children[index + 1].getCanvas()._canvas); + } + else { + stage.content.appendChild(this.getNativeCanvasElement()); + } + } + return this; + } + moveToTop() { + Node.prototype.moveToTop.call(this); + const stage = this.getStage(); + if (stage && stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + stage.content.appendChild(this.getNativeCanvasElement()); + } + return true; + } + moveUp() { + const moved = Node.prototype.moveUp.call(this); + if (!moved) { + return false; + } + const stage = this.getStage(); + if (!stage || !stage.content) { + return false; + } + stage.content.removeChild(this.getNativeCanvasElement()); + if (this.index < stage.children.length - 1) { + stage.content.insertBefore(this.getNativeCanvasElement(), stage.children[this.index + 1].getCanvas()._canvas); + } + else { + stage.content.appendChild(this.getNativeCanvasElement()); + } + return true; + } + // extend Node.prototype.moveDown + moveDown() { + if (Node.prototype.moveDown.call(this)) { + const stage = this.getStage(); + if (stage) { + const children = stage.children; + if (stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + stage.content.insertBefore(this.getNativeCanvasElement(), children[this.index + 1].getCanvas()._canvas); + } + } + return true; + } + return false; + } + // extend Node.prototype.moveToBottom + moveToBottom() { + if (Node.prototype.moveToBottom.call(this)) { + const stage = this.getStage(); + if (stage) { + const children = stage.children; + if (stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + stage.content.insertBefore(this.getNativeCanvasElement(), children[1].getCanvas()._canvas); + } + } + return true; + } + return false; + } + getLayer() { + return this; + } + remove() { + const _canvas = this.getNativeCanvasElement(); + Node.prototype.remove.call(this); + if (_canvas && _canvas.parentNode && Util._isInDocument(_canvas)) { + _canvas.parentNode.removeChild(_canvas); + } + return this; + } + getStage() { + return this.parent; + } + setSize({ width, height }) { + this.canvas.setSize(width, height); + this.hitCanvas.setSize(width, height); + this._setSmoothEnabled(); + return this; + } + _validateAdd(child) { + const type = child.getType(); + if (type !== 'Group' && type !== 'Shape') { + Util.throw('You may only add groups and shapes to a layer.'); + } + } + _toKonvaCanvas(config) { + config = config || {}; + config.width = config.width || this.getWidth(); + config.height = config.height || this.getHeight(); + config.x = config.x !== undefined ? config.x : this.x(); + config.y = config.y !== undefined ? config.y : this.y(); + return Node.prototype._toKonvaCanvas.call(this, config); + } + _checkVisibility() { + const visible = this.visible(); + if (visible) { + this.canvas._canvas.style.display = 'block'; + } + else { + this.canvas._canvas.style.display = 'none'; + } + } + _setSmoothEnabled() { + this.getContext()._context.imageSmoothingEnabled = + this.imageSmoothingEnabled(); + } + /** + * get/set width of layer. getter return width of stage. setter doing nothing. + * if you want change width use `stage.width(value);` + * @name Konva.Layer#width + * @method + * @returns {Number} + * @example + * var width = layer.width(); + */ + getWidth() { + if (this.parent) { + return this.parent.width(); + } + } + setWidth() { + Util.warn('Can not change width of layer. Use "stage.width(value)" function instead.'); + } + /** + * get/set height of layer.getter return height of stage. setter doing nothing. + * if you want change height use `stage.height(value);` + * @name Konva.Layer#height + * @method + * @returns {Number} + * @example + * var height = layer.height(); + */ + getHeight() { + if (this.parent) { + return this.parent.height(); + } + } + setHeight() { + Util.warn('Can not change height of layer. Use "stage.height(value)" function instead.'); + } + /** + * batch draw. this function will not do immediate draw + * but it will schedule drawing to next tick (requestAnimFrame) + * @method + * @name Konva.Layer#batchDraw + * @return {Konva.Layer} this + */ + batchDraw() { + if (!this._waitingForDraw) { + this._waitingForDraw = true; + Util.requestAnimFrame(() => { + this.draw(); + this._waitingForDraw = false; + }); + } + return this; + } + /** + * get visible intersection shape. This is the preferred + * method for determining if a point intersects a shape or not + * also you may pass optional selector parameter to return ancestor of intersected shape + * nodes with listening set to false will not be detected + * @method + * @name Konva.Layer#getIntersection + * @param {Object} pos + * @param {Number} pos.x + * @param {Number} pos.y + * @returns {Konva.Node} + * @example + * var shape = layer.getIntersection({x: 50, y: 50}); + */ + getIntersection(pos) { + if (!this.isListening() || !this.isVisible()) { + return null; + } + // in some cases antialiased area may be bigger than 1px + // it is possible if we will cache node, then scale it a lot + let spiralSearchDistance = 1; + let continueSearch = false; + while (true) { + for (let i = 0; i < INTERSECTION_OFFSETS_LEN; i++) { + const intersectionOffset = INTERSECTION_OFFSETS[i]; + const obj = this._getIntersection({ + x: pos.x + intersectionOffset.x * spiralSearchDistance, + y: pos.y + intersectionOffset.y * spiralSearchDistance, + }); + const shape = obj.shape; + if (shape) { + return shape; + } + // we should continue search if we found antialiased pixel + // that means our node somewhere very close + continueSearch = !!obj.antialiased; + // stop search if found empty pixel + if (!obj.antialiased) { + break; + } + } + // if no shape, and no antialiased pixel, we should end searching + if (continueSearch) { + spiralSearchDistance += 1; + } + else { + return null; + } + } + } + _getIntersection(pos) { + const ratio = this.hitCanvas.pixelRatio; + const p = this.hitCanvas.context.getImageData(Math.round(pos.x * ratio), Math.round(pos.y * ratio), 1, 1).data; + const p3 = p[3]; + // fully opaque pixel + if (p3 === 255) { + const colorKey = Util._rgbToHex(p[0], p[1], p[2]); + const shape = shapes[HASH + colorKey]; + if (shape) { + return { + shape: shape, + }; + } + return { + antialiased: true, + }; + } + else if (p3 > 0) { + // antialiased pixel + return { + antialiased: true, + }; + } + // empty pixel + return {}; + } + drawScene(can, top) { + const layer = this.getLayer(), canvas = can || (layer && layer.getCanvas()); + this._fire(BEFORE_DRAW, { + node: this, + }); + if (this.clearBeforeDraw()) { + canvas.getContext().clear(); + } + Container.prototype.drawScene.call(this, canvas, top); + this._fire(DRAW, { + node: this, + }); + return this; + } + drawHit(can, top) { + const layer = this.getLayer(), canvas = can || (layer && layer.hitCanvas); + if (layer && layer.clearBeforeDraw()) { + layer.getHitCanvas().getContext().clear(); + } + Container.prototype.drawHit.call(this, canvas, top); + return this; + } + /** + * enable hit graph. **DEPRECATED!** Use `layer.listening(true)` instead. + * @name Konva.Layer#enableHitGraph + * @method + * @returns {Layer} + */ + enableHitGraph() { + this.hitGraphEnabled(true); + return this; + } + /** + * disable hit graph. **DEPRECATED!** Use `layer.listening(false)` instead. + * @name Konva.Layer#disableHitGraph + * @method + * @returns {Layer} + */ + disableHitGraph() { + this.hitGraphEnabled(false); + return this; + } + setHitGraphEnabled(val) { + Util.warn('hitGraphEnabled method is deprecated. Please use layer.listening() instead.'); + this.listening(val); + } + getHitGraphEnabled(val) { + Util.warn('hitGraphEnabled method is deprecated. Please use layer.listening() instead.'); + return this.listening(); + } + /** + * Show or hide hit canvas over the stage. May be useful for debugging custom hitFunc + * @name Konva.Layer#toggleHitCanvas + * @method + */ + toggleHitCanvas() { + if (!this.parent || !this.parent['content']) { + return; + } + const parent = this.parent; + const added = !!this.hitCanvas._canvas.parentNode; + if (added) { + parent.content.removeChild(this.hitCanvas._canvas); + } + else { + parent.content.appendChild(this.hitCanvas._canvas); + } + } + destroy() { + Util.releaseCanvas(this.getNativeCanvasElement(), this.getHitCanvas()._canvas); + return super.destroy(); + } + } + Layer.prototype.nodeType = 'Layer'; + _registerNode(Layer); + /** + * get/set imageSmoothingEnabled flag + * For more info see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled + * @name Konva.Layer#imageSmoothingEnabled + * @method + * @param {Boolean} imageSmoothingEnabled + * @returns {Boolean} + * @example + * // get imageSmoothingEnabled flag + * var imageSmoothingEnabled = layer.imageSmoothingEnabled(); + * + * layer.imageSmoothingEnabled(false); + * + * layer.imageSmoothingEnabled(true); + */ + Factory.addGetterSetter(Layer, 'imageSmoothingEnabled', true); + /** + * get/set clearBeforeDraw flag which determines if the layer is cleared or not + * before drawing + * @name Konva.Layer#clearBeforeDraw + * @method + * @param {Boolean} clearBeforeDraw + * @returns {Boolean} + * @example + * // get clearBeforeDraw flag + * var clearBeforeDraw = layer.clearBeforeDraw(); + * + * // disable clear before draw + * layer.clearBeforeDraw(false); + * + * // enable clear before draw + * layer.clearBeforeDraw(true); + */ + Factory.addGetterSetter(Layer, 'clearBeforeDraw', true); + Factory.addGetterSetter(Layer, 'hitGraphEnabled', true, getBooleanValidator()); + /** + * get/set hitGraphEnabled flag. **DEPRECATED!** Use `layer.listening(false)` instead. + * Disabling the hit graph will greatly increase + * draw performance because the hit graph will not be redrawn each time the layer is + * drawn. This, however, also disables mouse/touch event detection + * @name Konva.Layer#hitGraphEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get hitGraphEnabled flag + * var hitGraphEnabled = layer.hitGraphEnabled(); + * + * // disable hit graph + * layer.hitGraphEnabled(false); + * + * // enable hit graph + * layer.hitGraphEnabled(true); + */ + + /** + * FastLayer constructor. **DEPRECATED!** Please use `Konva.Layer({ listening: false})` instead. Layers are tied to their own canvas element and are used + * to contain shapes only. If you don't need node nesting, mouse and touch interactions, + * or event pub/sub, you should use FastLayer instead of Layer to create your layers. + * It renders about 2x faster than normal layers. + * + * @constructor + * @memberof Konva + * @augments Konva.Layer + * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + * @example + * var layer = new Konva.FastLayer(); + */ + class FastLayer extends Layer { + constructor(attrs) { + super(attrs); + this.listening(false); + Util.warn('Konva.Fast layer is deprecated. Please use "new Konva.Layer({ listening: false })" instead.'); + } + } + FastLayer.prototype.nodeType = 'FastLayer'; + _registerNode(FastLayer); + + /** + * Group constructor. Groups are used to contain shapes or other groups. + * @constructor + * @memberof Konva + * @augments Konva.Container + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + * @example + * var group = new Konva.Group(); + */ + class Group extends Container { + _validateAdd(child) { + const type = child.getType(); + if (type !== 'Group' && type !== 'Shape') { + Util.throw('You may only add groups and shapes to groups.'); + } + } + } + Group.prototype.nodeType = 'Group'; + _registerNode(Group); + + const now = (function () { + if (glob.performance && glob.performance.now) { + return function () { + return glob.performance.now(); + }; + } + return function () { + return new Date().getTime(); + }; + })(); + /** + * Animation constructor. + * @constructor + * @memberof Konva + * @param {AnimationFn} func function executed on each animation frame. The function is passed a frame object, which contains + * timeDiff, lastTime, time, and frameRate properties. The timeDiff property is the number of milliseconds that have passed + * since the last animation frame. The time property is the time in milliseconds that elapsed from the moment the animation started + * to the current animation frame. The lastTime property is a `time` value from the previous frame. The frameRate property is the current frame rate in frames / second. + * Return false from function, if you don't need to redraw layer/layers on some frames. + * @param {Konva.Layer|Array} [layers] layer(s) to be redrawn on each animation frame. Can be a layer, an array of layers, or null. + * Not specifying a node will result in no redraw. + * @example + * // move a node to the right at 50 pixels / second + * var velocity = 50; + * + * var anim = new Konva.Animation(function(frame) { + * var dist = velocity * (frame.timeDiff / 1000); + * node.move({x: dist, y: 0}); + * }, layer); + * + * anim.start(); + */ + class Animation { + constructor(func, layers) { + this.id = Animation.animIdCounter++; + this.frame = { + time: 0, + timeDiff: 0, + lastTime: now(), + frameRate: 0, + }; + this.func = func; + this.setLayers(layers); + } + /** + * set layers to be redrawn on each animation frame + * @method + * @name Konva.Animation#setLayers + * @param {Konva.Layer|Array} [layers] layer(s) to be redrawn. Can be a layer, an array of layers, or null. Not specifying a node will result in no redraw. + * @return {Konva.Animation} this + */ + setLayers(layers) { + let lays = []; + // if passing in no layers + if (layers) { + lays = Array.isArray(layers) ? layers : [layers]; + } + this.layers = lays; + return this; + } + /** + * get layers + * @method + * @name Konva.Animation#getLayers + * @return {Array} Array of Konva.Layer + */ + getLayers() { + return this.layers; + } + /** + * add layer. Returns true if the layer was added, and false if it was not + * @method + * @name Konva.Animation#addLayer + * @param {Konva.Layer} layer to add + * @return {Bool} true if layer is added to animation, otherwise false + */ + addLayer(layer) { + const layers = this.layers; + const len = layers.length; + // don't add the layer if it already exists + for (let n = 0; n < len; n++) { + if (layers[n]._id === layer._id) { + return false; + } + } + this.layers.push(layer); + return true; + } + /** + * determine if animation is running or not. returns true or false + * @method + * @name Konva.Animation#isRunning + * @return {Bool} is animation running? + */ + isRunning() { + const a = Animation; + const animations = a.animations; + const len = animations.length; + for (let n = 0; n < len; n++) { + if (animations[n].id === this.id) { + return true; + } + } + return false; + } + /** + * start animation + * @method + * @name Konva.Animation#start + * @return {Konva.Animation} this + */ + start() { + this.stop(); + this.frame.timeDiff = 0; + this.frame.lastTime = now(); + Animation._addAnimation(this); + return this; + } + /** + * stop animation + * @method + * @name Konva.Animation#stop + * @return {Konva.Animation} this + */ + stop() { + Animation._removeAnimation(this); + return this; + } + _updateFrameObject(time) { + this.frame.timeDiff = time - this.frame.lastTime; + this.frame.lastTime = time; + this.frame.time += this.frame.timeDiff; + this.frame.frameRate = 1000 / this.frame.timeDiff; + } + static _addAnimation(anim) { + this.animations.push(anim); + this._handleAnimation(); + } + static _removeAnimation(anim) { + const id = anim.id; + const animations = this.animations; + const len = animations.length; + for (let n = 0; n < len; n++) { + if (animations[n].id === id) { + this.animations.splice(n, 1); + break; + } + } + } + static _runFrames() { + const layerHash = {}; + const animations = this.animations; + /* + * loop through all animations and execute animation + * function. if the animation object has specified node, + * we can add the node to the nodes hash to eliminate + * drawing the same node multiple times. The node property + * can be the stage itself or a layer + */ + /* + * WARNING: don't cache animations.length because it could change while + * the for loop is running, causing a JS error + */ + for (let n = 0; n < animations.length; n++) { + const anim = animations[n]; + const layers = anim.layers; + const func = anim.func; + anim._updateFrameObject(now()); + const layersLen = layers.length; + // if animation object has a function, execute it + let needRedraw; + if (func) { + // allow anim bypassing drawing + needRedraw = func.call(anim, anim.frame) !== false; + } + else { + needRedraw = true; + } + if (!needRedraw) { + continue; + } + for (let i = 0; i < layersLen; i++) { + const layer = layers[i]; + if (layer._id !== undefined) { + layerHash[layer._id] = layer; + } + } + } + for (const key in layerHash) { + if (!layerHash.hasOwnProperty(key)) { + continue; + } + layerHash[key].batchDraw(); + } + } + static _animationLoop() { + const Anim = Animation; + if (Anim.animations.length) { + Anim._runFrames(); + Util.requestAnimFrame(Anim._animationLoop); + } + else { + Anim.animRunning = false; + } + } + static _handleAnimation() { + if (!this.animRunning) { + this.animRunning = true; + Util.requestAnimFrame(this._animationLoop); + } + } + } + Animation.animations = []; + Animation.animIdCounter = 0; + Animation.animRunning = false; + + const blacklist = { + node: 1, + duration: 1, + easing: 1, + onFinish: 1, + yoyo: 1, + }, PAUSED = 1, PLAYING = 2, REVERSING = 3, colorAttrs = ['fill', 'stroke', 'shadowColor']; + let idCounter = 0; + class TweenEngine { + constructor(prop, propFunc, func, begin, finish, duration, yoyo) { + this.prop = prop; + this.propFunc = propFunc; + this.begin = begin; + this._pos = begin; + this.duration = duration; + this._change = 0; + this.prevPos = 0; + this.yoyo = yoyo; + this._time = 0; + this._position = 0; + this._startTime = 0; + this._finish = 0; + this.func = func; + this._change = finish - this.begin; + this.pause(); + } + fire(str) { + const handler = this[str]; + if (handler) { + handler(); + } + } + setTime(t) { + if (t > this.duration) { + if (this.yoyo) { + this._time = this.duration; + this.reverse(); + } + else { + this.finish(); + } + } + else if (t < 0) { + if (this.yoyo) { + this._time = 0; + this.play(); + } + else { + this.reset(); + } + } + else { + this._time = t; + this.update(); + } + } + getTime() { + return this._time; + } + setPosition(p) { + this.prevPos = this._pos; + this.propFunc(p); + this._pos = p; + } + getPosition(t) { + if (t === undefined) { + t = this._time; + } + return this.func(t, this.begin, this._change, this.duration); + } + play() { + this.state = PLAYING; + this._startTime = this.getTimer() - this._time; + this.onEnterFrame(); + this.fire('onPlay'); + } + reverse() { + this.state = REVERSING; + this._time = this.duration - this._time; + this._startTime = this.getTimer() - this._time; + this.onEnterFrame(); + this.fire('onReverse'); + } + seek(t) { + this.pause(); + this._time = t; + this.update(); + this.fire('onSeek'); + } + reset() { + this.pause(); + this._time = 0; + this.update(); + this.fire('onReset'); + } + finish() { + this.pause(); + this._time = this.duration; + this.update(); + this.fire('onFinish'); + } + update() { + this.setPosition(this.getPosition(this._time)); + this.fire('onUpdate'); + } + onEnterFrame() { + const t = this.getTimer() - this._startTime; + if (this.state === PLAYING) { + this.setTime(t); + } + else if (this.state === REVERSING) { + this.setTime(this.duration - t); + } + } + pause() { + this.state = PAUSED; + this.fire('onPause'); + } + getTimer() { + return new Date().getTime(); + } + } + /** + * Tween constructor. Tweens enable you to animate a node between the current state and a new state. + * You can play, pause, reverse, seek, reset, and finish tweens. By default, tweens are animated using + * a linear easing. For more tweening options, check out {@link Konva.Easings} + * @constructor + * @memberof Konva + * @example + * // instantiate new tween which fully rotates a node in 1 second + * var tween = new Konva.Tween({ + * // list of tween specific properties + * node: node, + * duration: 1, + * easing: Konva.Easings.EaseInOut, + * onUpdate: () => console.log('node attrs updated') + * onFinish: () => console.log('finished'), + * // set new values for any attributes of a passed node + * rotation: 360, + * fill: 'red' + * }); + * + * // play tween + * tween.play(); + * + * // pause tween + * tween.pause(); + */ + class Tween { + constructor(config) { + const that = this, node = config.node, nodeId = node._id, easing = config.easing || Easings.Linear, yoyo = !!config.yoyo; + let duration, key; + if (typeof config.duration === 'undefined') { + duration = 0.3; + } + else if (config.duration === 0) { + // zero is bad value for duration + duration = 0.001; + } + else { + duration = config.duration; + } + this.node = node; + this._id = idCounter++; + const layers = node.getLayer() || + (node instanceof Konva$2['Stage'] ? node.getLayers() : null); + if (!layers) { + Util.error('Tween constructor have `node` that is not in a layer. Please add node into layer first.'); + } + this.anim = new Animation(function () { + that.tween.onEnterFrame(); + }, layers); + this.tween = new TweenEngine(key, function (i) { + that._tweenFunc(i); + }, easing, 0, 1, duration * 1000, yoyo); + this._addListeners(); + // init attrs map + if (!Tween.attrs[nodeId]) { + Tween.attrs[nodeId] = {}; + } + if (!Tween.attrs[nodeId][this._id]) { + Tween.attrs[nodeId][this._id] = {}; + } + // init tweens map + if (!Tween.tweens[nodeId]) { + Tween.tweens[nodeId] = {}; + } + for (key in config) { + if (blacklist[key] === undefined) { + this._addAttr(key, config[key]); + } + } + this.reset(); + // callbacks + this.onFinish = config.onFinish; + this.onReset = config.onReset; + this.onUpdate = config.onUpdate; + } + _addAttr(key, end) { + const node = this.node, nodeId = node._id; + let diff, len, trueEnd, trueStart, endRGBA; + // remove conflict from tween map if it exists + const tweenId = Tween.tweens[nodeId][key]; + if (tweenId) { + delete Tween.attrs[nodeId][tweenId][key]; + } + // add to tween map + let start = node.getAttr(key); + if (Util._isArray(end)) { + diff = []; + len = Math.max(end.length, start.length); + if (key === 'points' && end.length !== start.length) { + // before tweening points we need to make sure that start.length === end.length + // Util._prepareArrayForTween thinking that end.length > start.length + if (end.length > start.length) { + // so in this case we will increase number of starting points + trueStart = start; + start = Util._prepareArrayForTween(start, end, node.closed()); + } + else { + // in this case we will increase number of eding points + trueEnd = end; + end = Util._prepareArrayForTween(end, start, node.closed()); + } + } + if (key.indexOf('fill') === 0) { + for (let n = 0; n < len; n++) { + if (n % 2 === 0) { + diff.push(end[n] - start[n]); + } + else { + const startRGBA = Util.colorToRGBA(start[n]); + endRGBA = Util.colorToRGBA(end[n]); + start[n] = startRGBA; + diff.push({ + r: endRGBA.r - startRGBA.r, + g: endRGBA.g - startRGBA.g, + b: endRGBA.b - startRGBA.b, + a: endRGBA.a - startRGBA.a, + }); + } + } + } + else { + for (let n = 0; n < len; n++) { + diff.push(end[n] - start[n]); + } + } + } + else if (colorAttrs.indexOf(key) !== -1) { + start = Util.colorToRGBA(start); + endRGBA = Util.colorToRGBA(end); + diff = { + r: endRGBA.r - start.r, + g: endRGBA.g - start.g, + b: endRGBA.b - start.b, + a: endRGBA.a - start.a, + }; + } + else { + diff = end - start; + } + Tween.attrs[nodeId][this._id][key] = { + start: start, + diff: diff, + end: end, + trueEnd: trueEnd, + trueStart: trueStart, + }; + Tween.tweens[nodeId][key] = this._id; + } + _tweenFunc(i) { + const node = this.node, attrs = Tween.attrs[node._id][this._id]; + let key, attr, start, diff, newVal, n, len, end; + for (key in attrs) { + attr = attrs[key]; + start = attr.start; + diff = attr.diff; + end = attr.end; + if (Util._isArray(start)) { + newVal = []; + len = Math.max(start.length, end.length); + if (key.indexOf('fill') === 0) { + for (n = 0; n < len; n++) { + if (n % 2 === 0) { + newVal.push((start[n] || 0) + diff[n] * i); + } + else { + newVal.push('rgba(' + + Math.round(start[n].r + diff[n].r * i) + + ',' + + Math.round(start[n].g + diff[n].g * i) + + ',' + + Math.round(start[n].b + diff[n].b * i) + + ',' + + (start[n].a + diff[n].a * i) + + ')'); + } + } + } + else { + for (n = 0; n < len; n++) { + newVal.push((start[n] || 0) + diff[n] * i); + } + } + } + else if (colorAttrs.indexOf(key) !== -1) { + newVal = + 'rgba(' + + Math.round(start.r + diff.r * i) + + ',' + + Math.round(start.g + diff.g * i) + + ',' + + Math.round(start.b + diff.b * i) + + ',' + + (start.a + diff.a * i) + + ')'; + } + else { + newVal = start + diff * i; + } + node.setAttr(key, newVal); + } + } + _addListeners() { + // start listeners + this.tween.onPlay = () => { + this.anim.start(); + }; + this.tween.onReverse = () => { + this.anim.start(); + }; + // stop listeners + this.tween.onPause = () => { + this.anim.stop(); + }; + this.tween.onFinish = () => { + const node = this.node; + // after tweening points of line we need to set original end + const attrs = Tween.attrs[node._id][this._id]; + if (attrs.points && attrs.points.trueEnd) { + node.setAttr('points', attrs.points.trueEnd); + } + if (this.onFinish) { + this.onFinish.call(this); + } + }; + this.tween.onReset = () => { + const node = this.node; + // after tweening points of line we need to set original start + const attrs = Tween.attrs[node._id][this._id]; + if (attrs.points && attrs.points.trueStart) { + node.points(attrs.points.trueStart); + } + if (this.onReset) { + this.onReset(); + } + }; + this.tween.onUpdate = () => { + if (this.onUpdate) { + this.onUpdate.call(this); + } + }; + } + /** + * play + * @method + * @name Konva.Tween#play + * @returns {Tween} + */ + play() { + this.tween.play(); + return this; + } + /** + * reverse + * @method + * @name Konva.Tween#reverse + * @returns {Tween} + */ + reverse() { + this.tween.reverse(); + return this; + } + /** + * reset + * @method + * @name Konva.Tween#reset + * @returns {Tween} + */ + reset() { + this.tween.reset(); + return this; + } + /** + * seek + * @method + * @name Konva.Tween#seek( + * @param {Integer} t time in seconds between 0 and the duration + * @returns {Tween} + */ + seek(t) { + this.tween.seek(t * 1000); + return this; + } + /** + * pause + * @method + * @name Konva.Tween#pause + * @returns {Tween} + */ + pause() { + this.tween.pause(); + return this; + } + /** + * finish + * @method + * @name Konva.Tween#finish + * @returns {Tween} + */ + finish() { + this.tween.finish(); + return this; + } + /** + * destroy + * @method + * @name Konva.Tween#destroy + */ + destroy() { + const nodeId = this.node._id, thisId = this._id, attrs = Tween.tweens[nodeId]; + this.pause(); + for (const key in attrs) { + delete Tween.tweens[nodeId][key]; + } + delete Tween.attrs[nodeId][thisId]; + } + } + Tween.attrs = {}; + Tween.tweens = {}; + /** + * Tween node properties. Shorter usage of {@link Konva.Tween} object. + * + * @method Konva.Node#to + * @param {Object} [params] tween params + * @example + * + * circle.to({ + * x : 50, + * duration : 0.5, + * onUpdate: () => console.log('props updated'), + * onFinish: () => console.log('finished'), + * }); + */ + Node.prototype.to = function (params) { + const onFinish = params.onFinish; + params.node = this; + params.onFinish = function () { + this.destroy(); + if (onFinish) { + onFinish(); + } + }; + const tween = new Tween(params); + tween.play(); + }; + /* + * These eases were ported from an Adobe Flash tweening library to JavaScript + * by Xaric + */ + /** + * @namespace Easings + * @memberof Konva + */ + const Easings = { + /** + * back ease in + * @function + * @memberof Konva.Easings + */ + BackEaseIn(t, b, c, d) { + const s = 1.70158; + return c * (t /= d) * t * ((s + 1) * t - s) + b; + }, + /** + * back ease out + * @function + * @memberof Konva.Easings + */ + BackEaseOut(t, b, c, d) { + const s = 1.70158; + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + }, + /** + * back ease in out + * @function + * @memberof Konva.Easings + */ + BackEaseInOut(t, b, c, d) { + let s = 1.70158; + if ((t /= d / 2) < 1) { + return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b; + } + return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b; + }, + /** + * elastic ease in + * @function + * @memberof Konva.Easings + */ + ElasticEaseIn(t, b, c, d, a, p) { + // added s = 0 + let s = 0; + if (t === 0) { + return b; + } + if ((t /= d) === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + if (!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = (p / (2 * Math.PI)) * Math.asin(c / a); + } + return (-(a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b); + }, + /** + * elastic ease out + * @function + * @memberof Konva.Easings + */ + ElasticEaseOut(t, b, c, d, a, p) { + // added s = 0 + let s = 0; + if (t === 0) { + return b; + } + if ((t /= d) === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + if (!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = (p / (2 * Math.PI)) * Math.asin(c / a); + } + return (a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) + + c + + b); + }, + /** + * elastic ease in out + * @function + * @memberof Konva.Easings + */ + ElasticEaseInOut(t, b, c, d, a, p) { + // added s = 0 + let s = 0; + if (t === 0) { + return b; + } + if ((t /= d / 2) === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + if (!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = (p / (2 * Math.PI)) * Math.asin(c / a); + } + if (t < 1) { + return (-0.5 * + (a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + + b); + } + return (a * + Math.pow(2, -10 * (t -= 1)) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p) * + 0.5 + + c + + b); + }, + /** + * bounce ease out + * @function + * @memberof Konva.Easings + */ + BounceEaseOut(t, b, c, d) { + if ((t /= d) < 1 / 2.75) { + return c * (7.5625 * t * t) + b; + } + else if (t < 2 / 2.75) { + return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b; + } + else if (t < 2.5 / 2.75) { + return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b; + } + }, + /** + * bounce ease in + * @function + * @memberof Konva.Easings + */ + BounceEaseIn(t, b, c, d) { + return c - Easings.BounceEaseOut(d - t, 0, c, d) + b; + }, + /** + * bounce ease in out + * @function + * @memberof Konva.Easings + */ + BounceEaseInOut(t, b, c, d) { + if (t < d / 2) { + return Easings.BounceEaseIn(t * 2, 0, c, d) * 0.5 + b; + } + else { + return Easings.BounceEaseOut(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + }, + /** + * ease in + * @function + * @memberof Konva.Easings + */ + EaseIn(t, b, c, d) { + return c * (t /= d) * t + b; + }, + /** + * ease out + * @function + * @memberof Konva.Easings + */ + EaseOut(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + /** + * ease in out + * @function + * @memberof Konva.Easings + */ + EaseInOut(t, b, c, d) { + if ((t /= d / 2) < 1) { + return (c / 2) * t * t + b; + } + return (-c / 2) * (--t * (t - 2) - 1) + b; + }, + /** + * strong ease in + * @function + * @memberof Konva.Easings + */ + StrongEaseIn(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + }, + /** + * strong ease out + * @function + * @memberof Konva.Easings + */ + StrongEaseOut(t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + }, + /** + * strong ease in out + * @function + * @memberof Konva.Easings + */ + StrongEaseInOut(t, b, c, d) { + if ((t /= d / 2) < 1) { + return (c / 2) * t * t * t * t * t + b; + } + return (c / 2) * ((t -= 2) * t * t * t * t + 2) + b; + }, + /** + * linear + * @function + * @memberof Konva.Easings + */ + Linear(t, b, c, d) { + return (c * t) / d + b; + }, + }; + + // what is core parts of Konva? + const Konva$1 = Util._assign(Konva$2, { + Util, + Transform, + Node, + Container, + Stage, + stages, + Layer, + FastLayer, + Group, + DD, + Shape, + shapes, + Animation, + Tween, + Easings, + Context, + Canvas, + }); + + /** + * Arc constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.angle in degrees + * @param {Number} config.innerRadius + * @param {Number} config.outerRadius + * @param {Boolean} [config.clockwise] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // draw a Arc that's pointing downwards + * var arc = new Konva.Arc({ + * innerRadius: 40, + * outerRadius: 80, + * fill: 'red', + * stroke: 'black' + * strokeWidth: 5, + * angle: 60, + * rotationDeg: -120 + * }); + */ + class Arc extends Shape { + _sceneFunc(context) { + const angle = Konva$2.getAngle(this.angle()), clockwise = this.clockwise(); + context.beginPath(); + context.arc(0, 0, this.outerRadius(), 0, angle, clockwise); + context.arc(0, 0, this.innerRadius(), angle, 0, !clockwise); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.outerRadius() * 2; + } + getHeight() { + return this.outerRadius() * 2; + } + setWidth(width) { + this.outerRadius(width / 2); + } + setHeight(height) { + this.outerRadius(height / 2); + } + getSelfRect() { + const innerRadius = this.innerRadius(); + const outerRadius = this.outerRadius(); + const clockwise = this.clockwise(); + const angle = Konva$2.getAngle(clockwise ? 360 - this.angle() : this.angle()); + const boundLeftRatio = Math.cos(Math.min(angle, Math.PI)); + const boundRightRatio = 1; + const boundTopRatio = Math.sin(Math.min(Math.max(Math.PI, angle), (3 * Math.PI) / 2)); + const boundBottomRatio = Math.sin(Math.min(angle, Math.PI / 2)); + const boundLeft = boundLeftRatio * (boundLeftRatio > 0 ? innerRadius : outerRadius); + const boundRight = boundRightRatio * (outerRadius ); + const boundTop = boundTopRatio * (boundTopRatio > 0 ? innerRadius : outerRadius); + const boundBottom = boundBottomRatio * (boundBottomRatio > 0 ? outerRadius : innerRadius); + return { + x: boundLeft, + y: clockwise ? -1 * boundBottom : boundTop, + width: boundRight - boundLeft, + height: boundBottom - boundTop, + }; + } + } + Arc.prototype._centroid = true; + Arc.prototype.className = 'Arc'; + Arc.prototype._attrsAffectingSize = [ + 'innerRadius', + 'outerRadius', + 'angle', + 'clockwise', + ]; + _registerNode(Arc); + // add getters setters + Factory.addGetterSetter(Arc, 'innerRadius', 0, getNumberValidator()); + /** + * get/set innerRadius + * @name Konva.Arc#innerRadius + * @method + * @param {Number} innerRadius + * @returns {Number} + * @example + * // get inner radius + * var innerRadius = arc.innerRadius(); + * + * // set inner radius + * arc.innerRadius(20); + */ + Factory.addGetterSetter(Arc, 'outerRadius', 0, getNumberValidator()); + /** + * get/set outerRadius + * @name Konva.Arc#outerRadius + * @method + * @param {Number} outerRadius + * @returns {Number} + * @example + * // get outer radius + * var outerRadius = arc.outerRadius(); + * + * // set outer radius + * arc.outerRadius(20); + */ + Factory.addGetterSetter(Arc, 'angle', 0, getNumberValidator()); + /** + * get/set angle in degrees + * @name Konva.Arc#angle + * @method + * @param {Number} angle + * @returns {Number} + * @example + * // get angle + * var angle = arc.angle(); + * + * // set angle + * arc.angle(20); + */ + Factory.addGetterSetter(Arc, 'clockwise', false, getBooleanValidator()); + /** + * get/set clockwise flag + * @name Konva.Arc#clockwise + * @method + * @param {Boolean} clockwise + * @returns {Boolean} + * @example + * // get clockwise flag + * var clockwise = arc.clockwise(); + * + * // draw arc counter-clockwise + * arc.clockwise(false); + * + * // draw arc clockwise + * arc.clockwise(true); + */ + + function getControlPoints(x0, y0, x1, y1, x2, y2, t) { + const d01 = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)), d12 = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)), fa = (t * d01) / (d01 + d12), fb = (t * d12) / (d01 + d12), p1x = x1 - fa * (x2 - x0), p1y = y1 - fa * (y2 - y0), p2x = x1 + fb * (x2 - x0), p2y = y1 + fb * (y2 - y0); + return [p1x, p1y, p2x, p2y]; + } + function expandPoints(p, tension) { + const len = p.length, allPoints = []; + for (let n = 2; n < len - 2; n += 2) { + const cp = getControlPoints(p[n - 2], p[n - 1], p[n], p[n + 1], p[n + 2], p[n + 3], tension); + if (isNaN(cp[0])) { + continue; + } + allPoints.push(cp[0]); + allPoints.push(cp[1]); + allPoints.push(p[n]); + allPoints.push(p[n + 1]); + allPoints.push(cp[2]); + allPoints.push(cp[3]); + } + return allPoints; + } + /** + * Line constructor.  Lines are defined by an array of points and + * a tension + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3]. + * @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation. + * The default is 0 + * @param {Boolean} [config.closed] defines whether or not the line shape is closed, creating a polygon or blob + * @param {Boolean} [config.bezier] if no tension is provided but bezier=true, we draw the line as a bezier using the passed points + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var line = new Konva.Line({ + * x: 100, + * y: 50, + * points: [73, 70, 340, 23, 450, 60, 500, 20], + * stroke: 'red', + * tension: 1 + * }); + */ + class Line extends Shape { + constructor(config) { + super(config); + this.on('pointsChange.konva tensionChange.konva closedChange.konva bezierChange.konva', function () { + this._clearCache('tensionPoints'); + }); + } + _sceneFunc(context) { + let points = this.points(), length = points.length, tension = this.tension(), closed = this.closed(), bezier = this.bezier(), tp, len, n; + if (!length) { + return; + } + context.beginPath(); + context.moveTo(points[0], points[1]); + // tension + if (tension !== 0 && length > 4) { + tp = this.getTensionPoints(); + len = tp.length; + n = closed ? 0 : 4; + if (!closed) { + context.quadraticCurveTo(tp[0], tp[1], tp[2], tp[3]); + } + while (n < len - 2) { + context.bezierCurveTo(tp[n++], tp[n++], tp[n++], tp[n++], tp[n++], tp[n++]); + } + if (!closed) { + context.quadraticCurveTo(tp[len - 2], tp[len - 1], points[length - 2], points[length - 1]); + } + } + else if (bezier) { + // no tension but bezier + n = 2; + while (n < length) { + context.bezierCurveTo(points[n++], points[n++], points[n++], points[n++], points[n++], points[n++]); + } + } + else { + // no tension + for (n = 2; n < length; n += 2) { + context.lineTo(points[n], points[n + 1]); + } + } + // closed e.g. polygons and blobs + if (closed) { + context.closePath(); + context.fillStrokeShape(this); + } + else { + // open e.g. lines and splines + context.strokeShape(this); + } + } + getTensionPoints() { + return this._getCache('tensionPoints', this._getTensionPoints); + } + _getTensionPoints() { + if (this.closed()) { + return this._getTensionPointsClosed(); + } + else { + return expandPoints(this.points(), this.tension()); + } + } + _getTensionPointsClosed() { + const p = this.points(), len = p.length, tension = this.tension(), firstControlPoints = getControlPoints(p[len - 2], p[len - 1], p[0], p[1], p[2], p[3], tension), lastControlPoints = getControlPoints(p[len - 4], p[len - 3], p[len - 2], p[len - 1], p[0], p[1], tension), middle = expandPoints(p, tension), tp = [firstControlPoints[2], firstControlPoints[3]] + .concat(middle) + .concat([ + lastControlPoints[0], + lastControlPoints[1], + p[len - 2], + p[len - 1], + lastControlPoints[2], + lastControlPoints[3], + firstControlPoints[0], + firstControlPoints[1], + p[0], + p[1], + ]); + return tp; + } + getWidth() { + return this.getSelfRect().width; + } + getHeight() { + return this.getSelfRect().height; + } + // overload size detection + getSelfRect() { + let points = this.points(); + if (points.length < 4) { + return { + x: points[0] || 0, + y: points[1] || 0, + width: 0, + height: 0, + }; + } + if (this.tension() !== 0) { + points = [ + points[0], + points[1], + ...this._getTensionPoints(), + points[points.length - 2], + points[points.length - 1], + ]; + } + else { + points = this.points(); + } + let minX = points[0]; + let maxX = points[0]; + let minY = points[1]; + let maxY = points[1]; + let x, y; + for (let i = 0; i < points.length / 2; i++) { + x = points[i * 2]; + y = points[i * 2 + 1]; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + } + Line.prototype.className = 'Line'; + Line.prototype._attrsAffectingSize = ['points', 'bezier', 'tension']; + _registerNode(Line); + // add getters setters + Factory.addGetterSetter(Line, 'closed', false); + /** + * get/set closed flag. The default is false + * @name Konva.Line#closed + * @method + * @param {Boolean} closed + * @returns {Boolean} + * @example + * // get closed flag + * var closed = line.closed(); + * + * // close the shape + * line.closed(true); + * + * // open the shape + * line.closed(false); + */ + Factory.addGetterSetter(Line, 'bezier', false); + /** + * get/set bezier flag. The default is false + * @name Konva.Line#bezier + * @method + * @param {Boolean} bezier + * @returns {Boolean} + * @example + * // get whether the line is a bezier + * var isBezier = line.bezier(); + * + * // set whether the line is a bezier + * line.bezier(true); + */ + Factory.addGetterSetter(Line, 'tension', 0, getNumberValidator()); + /** + * get/set tension + * @name Konva.Line#tension + * @method + * @param {Number} tension Higher values will result in a more curvy line. A value of 0 will result in no interpolation. The default is 0 + * @returns {Number} + * @example + * // get tension + * var tension = line.tension(); + * + * // set tension + * line.tension(3); + */ + Factory.addGetterSetter(Line, 'points', [], getNumberArrayValidator()); + /** + * get/set points array. Points is a flat array [x1, y1, x2, y2]. It is flat for performance reasons. + * @name Konva.Line#points + * @method + * @param {Array} points + * @returns {Array} + * @example + * // get points + * var points = line.points(); + * + * // set points + * line.points([10, 20, 30, 40, 50, 60]); + * + * // push a new point + * line.points(line.points().concat([70, 80])); + */ + + // Credits: rveciana/svg-path-properties + // Legendre-Gauss abscissae (xi values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + const tValues = [ + [], + [], + [ + -0.5773502691896257, + 0.5773502691896257645091487805019574556476, + ], + [ + 0, -0.7745966692414834, + 0.7745966692414833770358530799564799221665, + ], + [ + -0.33998104358485626, + 0.3399810435848562648026657591032446872005, + -0.8611363115940526, + 0.8611363115940525752239464888928095050957, + ], + [ + 0, -0.5384693101056831, + 0.5384693101056830910363144207002088049672, + -0.906179845938664, + 0.9061798459386639927976268782993929651256, + ], + [ + 0.6612093864662645136613995950199053470064, + -0.6612093864662645, + -0.2386191860831969, + 0.2386191860831969086305017216807119354186, + -0.932469514203152, + 0.9324695142031520278123015544939946091347, + ], + [ + 0, 0.4058451513773971669066064120769614633473, + -0.4058451513773972, + -0.7415311855993945, + 0.7415311855993944398638647732807884070741, + -0.9491079123427585, + 0.9491079123427585245261896840478512624007, + ], + [ + -0.1834346424956498, + 0.1834346424956498049394761423601839806667, + -0.525532409916329, + 0.5255324099163289858177390491892463490419, + -0.7966664774136267, + 0.7966664774136267395915539364758304368371, + -0.9602898564975363, + 0.9602898564975362316835608685694729904282, + ], + [ + 0, -0.8360311073266358, + 0.8360311073266357942994297880697348765441, + -0.9681602395076261, + 0.9681602395076260898355762029036728700494, + -0.3242534234038089, + 0.3242534234038089290385380146433366085719, + -0.6133714327005904, + 0.6133714327005903973087020393414741847857, + ], + [ + -0.14887433898163122, + 0.1488743389816312108848260011297199846175, + -0.4333953941292472, + 0.4333953941292471907992659431657841622, + -0.6794095682990244, + 0.6794095682990244062343273651148735757692, + -0.8650633666889845, + 0.8650633666889845107320966884234930485275, + -0.9739065285171717, + 0.9739065285171717200779640120844520534282, + ], + [ + 0, -0.26954315595234496, + 0.2695431559523449723315319854008615246796, + -0.5190961292068118, + 0.5190961292068118159257256694586095544802, + -0.7301520055740494, + 0.7301520055740493240934162520311534580496, + -0.8870625997680953, + 0.8870625997680952990751577693039272666316, + -0.978228658146057, + 0.9782286581460569928039380011228573907714, + ], + [ + -0.1252334085114689, + 0.1252334085114689154724413694638531299833, + -0.3678314989981802, + 0.3678314989981801937526915366437175612563, + -0.5873179542866175, + 0.587317954286617447296702418940534280369, + -0.7699026741943047, + 0.7699026741943046870368938332128180759849, + -0.9041172563704749, + 0.9041172563704748566784658661190961925375, + -0.9815606342467192, + 0.9815606342467192506905490901492808229601, + ], + [ + 0, -0.2304583159551348, + 0.2304583159551347940655281210979888352115, + -0.44849275103644687, + 0.4484927510364468528779128521276398678019, + -0.6423493394403402, + 0.6423493394403402206439846069955156500716, + -0.8015780907333099, + 0.8015780907333099127942064895828598903056, + -0.9175983992229779, + 0.9175983992229779652065478365007195123904, + -0.9841830547185881, + 0.9841830547185881494728294488071096110649, + ], + [ + -0.10805494870734367, + 0.1080549487073436620662446502198347476119, + -0.31911236892788974, + 0.3191123689278897604356718241684754668342, + -0.5152486363581541, + 0.5152486363581540919652907185511886623088, + -0.6872929048116855, + 0.6872929048116854701480198030193341375384, + -0.827201315069765, + 0.8272013150697649931897947426503949610397, + -0.9284348836635735, + 0.928434883663573517336391139377874264477, + -0.9862838086968123, + 0.986283808696812338841597266704052801676, + ], + [ + 0, -0.20119409399743451, + 0.2011940939974345223006283033945962078128, + -0.3941513470775634, + 0.3941513470775633698972073709810454683627, + -0.5709721726085388, + 0.5709721726085388475372267372539106412383, + -0.7244177313601701, + 0.7244177313601700474161860546139380096308, + -0.8482065834104272, + 0.8482065834104272162006483207742168513662, + -0.937273392400706, + 0.9372733924007059043077589477102094712439, + -0.9879925180204854, + 0.9879925180204854284895657185866125811469, + ], + [ + -0.09501250983763744, + 0.0950125098376374401853193354249580631303, + -0.2816035507792589, + 0.281603550779258913230460501460496106486, + -0.45801677765722737, + 0.45801677765722738634241944298357757354, + -0.6178762444026438, + 0.6178762444026437484466717640487910189918, + -0.755404408355003, + 0.7554044083550030338951011948474422683538, + -0.8656312023878318, + 0.8656312023878317438804678977123931323873, + -0.9445750230732326, + 0.9445750230732325760779884155346083450911, + -0.9894009349916499, + 0.9894009349916499325961541734503326274262, + ], + [ + 0, -0.17848418149584785, + 0.1784841814958478558506774936540655574754, + -0.3512317634538763, + 0.3512317634538763152971855170953460050405, + -0.5126905370864769, + 0.5126905370864769678862465686295518745829, + -0.6576711592166907, + 0.6576711592166907658503022166430023351478, + -0.7815140038968014, + 0.7815140038968014069252300555204760502239, + -0.8802391537269859, + 0.8802391537269859021229556944881556926234, + -0.9506755217687678, + 0.9506755217687677612227169578958030214433, + -0.9905754753144174, + 0.9905754753144173356754340199406652765077, + ], + [ + -0.0847750130417353, + 0.0847750130417353012422618529357838117333, + -0.2518862256915055, + 0.2518862256915055095889728548779112301628, + -0.41175116146284263, + 0.4117511614628426460359317938330516370789, + -0.5597708310739475, + 0.5597708310739475346078715485253291369276, + -0.6916870430603532, + 0.6916870430603532078748910812888483894522, + -0.8037049589725231, + 0.8037049589725231156824174550145907971032, + -0.8926024664975557, + 0.8926024664975557392060605911271455154078, + -0.9558239495713977, + 0.9558239495713977551811958929297763099728, + -0.9915651684209309, + 0.9915651684209309467300160047061507702525, + ], + [ + 0, -0.16035864564022537, + 0.1603586456402253758680961157407435495048, + -0.31656409996362983, + 0.3165640999636298319901173288498449178922, + -0.46457074137596094, + 0.4645707413759609457172671481041023679762, + -0.600545304661681, + 0.6005453046616810234696381649462392798683, + -0.7209661773352294, + 0.7209661773352293786170958608237816296571, + -0.8227146565371428, + 0.8227146565371428249789224867127139017745, + -0.9031559036148179, + 0.9031559036148179016426609285323124878093, + -0.96020815213483, + 0.960208152134830030852778840687651526615, + -0.9924068438435844, + 0.9924068438435844031890176702532604935893, + ], + [ + -0.07652652113349734, + 0.0765265211334973337546404093988382110047, + -0.22778585114164507, + 0.227785851141645078080496195368574624743, + -0.37370608871541955, + 0.3737060887154195606725481770249272373957, + -0.5108670019508271, + 0.5108670019508270980043640509552509984254, + -0.636053680726515, + 0.6360536807265150254528366962262859367433, + -0.7463319064601508, + 0.7463319064601507926143050703556415903107, + -0.8391169718222188, + 0.8391169718222188233945290617015206853296, + -0.912234428251326, + 0.9122344282513259058677524412032981130491, + -0.9639719272779138, + 0.963971927277913791267666131197277221912, + -0.9931285991850949, + 0.9931285991850949247861223884713202782226, + ], + [ + 0, -0.1455618541608951, + 0.1455618541608950909370309823386863301163, + -0.2880213168024011, + 0.288021316802401096600792516064600319909, + -0.4243421202074388, + 0.4243421202074387835736688885437880520964, + -0.5516188358872198, + 0.551618835887219807059018796724313286622, + -0.6671388041974123, + 0.667138804197412319305966669990339162597, + -0.7684399634756779, + 0.7684399634756779086158778513062280348209, + -0.8533633645833173, + 0.8533633645833172836472506385875676702761, + -0.9200993341504008, + 0.9200993341504008287901871337149688941591, + -0.9672268385663063, + 0.9672268385663062943166222149076951614246, + -0.9937521706203895, + 0.9937521706203895002602420359379409291933, + ], + [ + -0.06973927331972223, + 0.0697392733197222212138417961186280818222, + -0.20786042668822127, + 0.2078604266882212854788465339195457342156, + -0.34193582089208424, + 0.3419358208920842251581474204273796195591, + -0.469355837986757, + 0.4693558379867570264063307109664063460953, + -0.5876404035069116, + 0.5876404035069115929588769276386473488776, + -0.6944872631866827, + 0.6944872631866827800506898357622567712673, + -0.7878168059792081, + 0.7878168059792081620042779554083515213881, + -0.8658125777203002, + 0.8658125777203001365364256370193787290847, + -0.926956772187174, + 0.9269567721871740005206929392590531966353, + -0.9700604978354287, + 0.9700604978354287271239509867652687108059, + -0.9942945854823992, + 0.994294585482399292073031421161298980393, + ], + [ + 0, -0.1332568242984661, + 0.1332568242984661109317426822417661370104, + -0.26413568097034495, + 0.264135680970344930533869538283309602979, + -0.3903010380302908, + 0.390301038030290831421488872880605458578, + -0.5095014778460075, + 0.5095014778460075496897930478668464305448, + -0.6196098757636461, + 0.6196098757636461563850973116495956533871, + -0.7186613631319502, + 0.7186613631319501944616244837486188483299, + -0.8048884016188399, + 0.8048884016188398921511184069967785579414, + -0.8767523582704416, + 0.8767523582704416673781568859341456716389, + -0.9329710868260161, + 0.9329710868260161023491969890384229782357, + -0.9725424712181152, + 0.9725424712181152319560240768207773751816, + -0.9947693349975522, + 0.9947693349975521235239257154455743605736, + ], + [ + -0.06405689286260563, + 0.0640568928626056260850430826247450385909, + -0.1911188674736163, + 0.1911188674736163091586398207570696318404, + -0.3150426796961634, + 0.3150426796961633743867932913198102407864, + -0.4337935076260451, + 0.4337935076260451384870842319133497124524, + -0.5454214713888396, + 0.5454214713888395356583756172183723700107, + -0.6480936519369755, + 0.6480936519369755692524957869107476266696, + -0.7401241915785544, + 0.7401241915785543642438281030999784255232, + -0.820001985973903, + 0.8200019859739029219539498726697452080761, + -0.8864155270044011, + 0.8864155270044010342131543419821967550873, + -0.9382745520027328, + 0.9382745520027327585236490017087214496548, + -0.9747285559713095, + 0.9747285559713094981983919930081690617411, + -0.9951872199970213, + 0.9951872199970213601799974097007368118745, + ], + ]; + // Legendre-Gauss weights (wi values, defined by a function linked to in the Bezier primer article) + const cValues = [ + [], + [], + [1.0, 1.0], + [ + 0.8888888888888888888888888888888888888888, + 0.5555555555555555555555555555555555555555, + 0.5555555555555555555555555555555555555555, + ], + [ + 0.6521451548625461426269360507780005927646, + 0.6521451548625461426269360507780005927646, + 0.3478548451374538573730639492219994072353, + 0.3478548451374538573730639492219994072353, + ], + [ + 0.5688888888888888888888888888888888888888, + 0.4786286704993664680412915148356381929122, + 0.4786286704993664680412915148356381929122, + 0.2369268850561890875142640407199173626432, + 0.2369268850561890875142640407199173626432, + ], + [ + 0.3607615730481386075698335138377161116615, + 0.3607615730481386075698335138377161116615, + 0.4679139345726910473898703439895509948116, + 0.4679139345726910473898703439895509948116, + 0.1713244923791703450402961421727328935268, + 0.1713244923791703450402961421727328935268, + ], + [ + 0.4179591836734693877551020408163265306122, + 0.3818300505051189449503697754889751338783, + 0.3818300505051189449503697754889751338783, + 0.2797053914892766679014677714237795824869, + 0.2797053914892766679014677714237795824869, + 0.1294849661688696932706114326790820183285, + 0.1294849661688696932706114326790820183285, + ], + [ + 0.3626837833783619829651504492771956121941, + 0.3626837833783619829651504492771956121941, + 0.3137066458778872873379622019866013132603, + 0.3137066458778872873379622019866013132603, + 0.2223810344533744705443559944262408844301, + 0.2223810344533744705443559944262408844301, + 0.1012285362903762591525313543099621901153, + 0.1012285362903762591525313543099621901153, + ], + [ + 0.3302393550012597631645250692869740488788, + 0.1806481606948574040584720312429128095143, + 0.1806481606948574040584720312429128095143, + 0.0812743883615744119718921581105236506756, + 0.0812743883615744119718921581105236506756, + 0.3123470770400028400686304065844436655987, + 0.3123470770400028400686304065844436655987, + 0.2606106964029354623187428694186328497718, + 0.2606106964029354623187428694186328497718, + ], + [ + 0.295524224714752870173892994651338329421, + 0.295524224714752870173892994651338329421, + 0.2692667193099963550912269215694693528597, + 0.2692667193099963550912269215694693528597, + 0.2190863625159820439955349342281631924587, + 0.2190863625159820439955349342281631924587, + 0.1494513491505805931457763396576973324025, + 0.1494513491505805931457763396576973324025, + 0.0666713443086881375935688098933317928578, + 0.0666713443086881375935688098933317928578, + ], + [ + 0.272925086777900630714483528336342189156, + 0.2628045445102466621806888698905091953727, + 0.2628045445102466621806888698905091953727, + 0.2331937645919904799185237048431751394317, + 0.2331937645919904799185237048431751394317, + 0.1862902109277342514260976414316558916912, + 0.1862902109277342514260976414316558916912, + 0.1255803694649046246346942992239401001976, + 0.1255803694649046246346942992239401001976, + 0.0556685671161736664827537204425485787285, + 0.0556685671161736664827537204425485787285, + ], + [ + 0.2491470458134027850005624360429512108304, + 0.2491470458134027850005624360429512108304, + 0.2334925365383548087608498989248780562594, + 0.2334925365383548087608498989248780562594, + 0.2031674267230659217490644558097983765065, + 0.2031674267230659217490644558097983765065, + 0.160078328543346226334652529543359071872, + 0.160078328543346226334652529543359071872, + 0.1069393259953184309602547181939962242145, + 0.1069393259953184309602547181939962242145, + 0.047175336386511827194615961485017060317, + 0.047175336386511827194615961485017060317, + ], + [ + 0.2325515532308739101945895152688359481566, + 0.2262831802628972384120901860397766184347, + 0.2262831802628972384120901860397766184347, + 0.2078160475368885023125232193060527633865, + 0.2078160475368885023125232193060527633865, + 0.1781459807619457382800466919960979955128, + 0.1781459807619457382800466919960979955128, + 0.1388735102197872384636017768688714676218, + 0.1388735102197872384636017768688714676218, + 0.0921214998377284479144217759537971209236, + 0.0921214998377284479144217759537971209236, + 0.0404840047653158795200215922009860600419, + 0.0404840047653158795200215922009860600419, + ], + [ + 0.2152638534631577901958764433162600352749, + 0.2152638534631577901958764433162600352749, + 0.2051984637212956039659240656612180557103, + 0.2051984637212956039659240656612180557103, + 0.1855383974779378137417165901251570362489, + 0.1855383974779378137417165901251570362489, + 0.1572031671581935345696019386238421566056, + 0.1572031671581935345696019386238421566056, + 0.1215185706879031846894148090724766259566, + 0.1215185706879031846894148090724766259566, + 0.0801580871597602098056332770628543095836, + 0.0801580871597602098056332770628543095836, + 0.0351194603317518630318328761381917806197, + 0.0351194603317518630318328761381917806197, + ], + [ + 0.2025782419255612728806201999675193148386, + 0.1984314853271115764561183264438393248186, + 0.1984314853271115764561183264438393248186, + 0.1861610000155622110268005618664228245062, + 0.1861610000155622110268005618664228245062, + 0.1662692058169939335532008604812088111309, + 0.1662692058169939335532008604812088111309, + 0.1395706779261543144478047945110283225208, + 0.1395706779261543144478047945110283225208, + 0.1071592204671719350118695466858693034155, + 0.1071592204671719350118695466858693034155, + 0.0703660474881081247092674164506673384667, + 0.0703660474881081247092674164506673384667, + 0.0307532419961172683546283935772044177217, + 0.0307532419961172683546283935772044177217, + ], + [ + 0.1894506104550684962853967232082831051469, + 0.1894506104550684962853967232082831051469, + 0.1826034150449235888667636679692199393835, + 0.1826034150449235888667636679692199393835, + 0.1691565193950025381893120790303599622116, + 0.1691565193950025381893120790303599622116, + 0.1495959888165767320815017305474785489704, + 0.1495959888165767320815017305474785489704, + 0.1246289712555338720524762821920164201448, + 0.1246289712555338720524762821920164201448, + 0.0951585116824927848099251076022462263552, + 0.0951585116824927848099251076022462263552, + 0.0622535239386478928628438369943776942749, + 0.0622535239386478928628438369943776942749, + 0.0271524594117540948517805724560181035122, + 0.0271524594117540948517805724560181035122, + ], + [ + 0.1794464703562065254582656442618856214487, + 0.1765627053669926463252709901131972391509, + 0.1765627053669926463252709901131972391509, + 0.1680041021564500445099706637883231550211, + 0.1680041021564500445099706637883231550211, + 0.1540457610768102880814315948019586119404, + 0.1540457610768102880814315948019586119404, + 0.1351363684685254732863199817023501973721, + 0.1351363684685254732863199817023501973721, + 0.1118838471934039710947883856263559267358, + 0.1118838471934039710947883856263559267358, + 0.0850361483171791808835353701910620738504, + 0.0850361483171791808835353701910620738504, + 0.0554595293739872011294401653582446605128, + 0.0554595293739872011294401653582446605128, + 0.0241483028685479319601100262875653246916, + 0.0241483028685479319601100262875653246916, + ], + [ + 0.1691423829631435918406564701349866103341, + 0.1691423829631435918406564701349866103341, + 0.1642764837458327229860537764659275904123, + 0.1642764837458327229860537764659275904123, + 0.1546846751262652449254180038363747721932, + 0.1546846751262652449254180038363747721932, + 0.1406429146706506512047313037519472280955, + 0.1406429146706506512047313037519472280955, + 0.1225552067114784601845191268002015552281, + 0.1225552067114784601845191268002015552281, + 0.1009420441062871655628139849248346070628, + 0.1009420441062871655628139849248346070628, + 0.0764257302548890565291296776166365256053, + 0.0764257302548890565291296776166365256053, + 0.0497145488949697964533349462026386416808, + 0.0497145488949697964533349462026386416808, + 0.0216160135264833103133427102664524693876, + 0.0216160135264833103133427102664524693876, + ], + [ + 0.1610544498487836959791636253209167350399, + 0.1589688433939543476499564394650472016787, + 0.1589688433939543476499564394650472016787, + 0.152766042065859666778855400897662998461, + 0.152766042065859666778855400897662998461, + 0.1426067021736066117757461094419029724756, + 0.1426067021736066117757461094419029724756, + 0.1287539625393362276755157848568771170558, + 0.1287539625393362276755157848568771170558, + 0.1115666455473339947160239016817659974813, + 0.1115666455473339947160239016817659974813, + 0.0914900216224499994644620941238396526609, + 0.0914900216224499994644620941238396526609, + 0.0690445427376412265807082580060130449618, + 0.0690445427376412265807082580060130449618, + 0.0448142267656996003328381574019942119517, + 0.0448142267656996003328381574019942119517, + 0.0194617882297264770363120414644384357529, + 0.0194617882297264770363120414644384357529, + ], + [ + 0.1527533871307258506980843319550975934919, + 0.1527533871307258506980843319550975934919, + 0.1491729864726037467878287370019694366926, + 0.1491729864726037467878287370019694366926, + 0.1420961093183820513292983250671649330345, + 0.1420961093183820513292983250671649330345, + 0.1316886384491766268984944997481631349161, + 0.1316886384491766268984944997481631349161, + 0.118194531961518417312377377711382287005, + 0.118194531961518417312377377711382287005, + 0.1019301198172404350367501354803498761666, + 0.1019301198172404350367501354803498761666, + 0.0832767415767047487247581432220462061001, + 0.0832767415767047487247581432220462061001, + 0.0626720483341090635695065351870416063516, + 0.0626720483341090635695065351870416063516, + 0.040601429800386941331039952274932109879, + 0.040601429800386941331039952274932109879, + 0.0176140071391521183118619623518528163621, + 0.0176140071391521183118619623518528163621, + ], + [ + 0.1460811336496904271919851476833711882448, + 0.1445244039899700590638271665537525436099, + 0.1445244039899700590638271665537525436099, + 0.1398873947910731547221334238675831108927, + 0.1398873947910731547221334238675831108927, + 0.132268938633337461781052574496775604329, + 0.132268938633337461781052574496775604329, + 0.1218314160537285341953671771257335983563, + 0.1218314160537285341953671771257335983563, + 0.1087972991671483776634745780701056420336, + 0.1087972991671483776634745780701056420336, + 0.0934444234560338615532897411139320884835, + 0.0934444234560338615532897411139320884835, + 0.0761001136283793020170516533001831792261, + 0.0761001136283793020170516533001831792261, + 0.0571344254268572082836358264724479574912, + 0.0571344254268572082836358264724479574912, + 0.0369537897708524937999506682993296661889, + 0.0369537897708524937999506682993296661889, + 0.0160172282577743333242246168584710152658, + 0.0160172282577743333242246168584710152658, + ], + [ + 0.1392518728556319933754102483418099578739, + 0.1392518728556319933754102483418099578739, + 0.1365414983460151713525738312315173965863, + 0.1365414983460151713525738312315173965863, + 0.1311735047870623707329649925303074458757, + 0.1311735047870623707329649925303074458757, + 0.1232523768105124242855609861548144719594, + 0.1232523768105124242855609861548144719594, + 0.1129322960805392183934006074217843191142, + 0.1129322960805392183934006074217843191142, + 0.1004141444428809649320788378305362823508, + 0.1004141444428809649320788378305362823508, + 0.0859416062170677274144436813727028661891, + 0.0859416062170677274144436813727028661891, + 0.0697964684245204880949614189302176573987, + 0.0697964684245204880949614189302176573987, + 0.0522933351526832859403120512732112561121, + 0.0522933351526832859403120512732112561121, + 0.0337749015848141547933022468659129013491, + 0.0337749015848141547933022468659129013491, + 0.0146279952982722006849910980471854451902, + 0.0146279952982722006849910980471854451902, + ], + [ + 0.1336545721861061753514571105458443385831, + 0.132462039404696617371642464703316925805, + 0.132462039404696617371642464703316925805, + 0.1289057221880821499785953393997936532597, + 0.1289057221880821499785953393997936532597, + 0.1230490843067295304675784006720096548158, + 0.1230490843067295304675784006720096548158, + 0.1149966402224113649416435129339613014914, + 0.1149966402224113649416435129339613014914, + 0.1048920914645414100740861850147438548584, + 0.1048920914645414100740861850147438548584, + 0.0929157660600351474770186173697646486034, + 0.0929157660600351474770186173697646486034, + 0.0792814117767189549228925247420432269137, + 0.0792814117767189549228925247420432269137, + 0.0642324214085258521271696151589109980391, + 0.0642324214085258521271696151589109980391, + 0.0480376717310846685716410716320339965612, + 0.0480376717310846685716410716320339965612, + 0.0309880058569794443106942196418845053837, + 0.0309880058569794443106942196418845053837, + 0.0134118594871417720813094934586150649766, + 0.0134118594871417720813094934586150649766, + ], + [ + 0.1279381953467521569740561652246953718517, + 0.1279381953467521569740561652246953718517, + 0.1258374563468282961213753825111836887264, + 0.1258374563468282961213753825111836887264, + 0.121670472927803391204463153476262425607, + 0.121670472927803391204463153476262425607, + 0.1155056680537256013533444839067835598622, + 0.1155056680537256013533444839067835598622, + 0.1074442701159656347825773424466062227946, + 0.1074442701159656347825773424466062227946, + 0.0976186521041138882698806644642471544279, + 0.0976186521041138882698806644642471544279, + 0.086190161531953275917185202983742667185, + 0.086190161531953275917185202983742667185, + 0.0733464814110803057340336152531165181193, + 0.0733464814110803057340336152531165181193, + 0.0592985849154367807463677585001085845412, + 0.0592985849154367807463677585001085845412, + 0.0442774388174198061686027482113382288593, + 0.0442774388174198061686027482113382288593, + 0.0285313886289336631813078159518782864491, + 0.0285313886289336631813078159518782864491, + 0.0123412297999871995468056670700372915759, + 0.0123412297999871995468056670700372915759, + ], + ]; + // LUT for binomial coefficient arrays per curve order 'n' + const binomialCoefficients = [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]]; + const getCubicArcLength = (xs, ys, t) => { + let sum; + let correctedT; + /*if (xs.length >= tValues.length) { + throw new Error('too high n bezier'); + }*/ + const n = 20; + const z = t / 2; + sum = 0; + for (let i = 0; i < n; i++) { + correctedT = z * tValues[n][i] + z; + sum += cValues[n][i] * BFunc(xs, ys, correctedT); + } + return z * sum; + }; + const getQuadraticArcLength = (xs, ys, t) => { + if (t === undefined) { + t = 1; + } + const ax = xs[0] - 2 * xs[1] + xs[2]; + const ay = ys[0] - 2 * ys[1] + ys[2]; + const bx = 2 * xs[1] - 2 * xs[0]; + const by = 2 * ys[1] - 2 * ys[0]; + const A = 4 * (ax * ax + ay * ay); + const B = 4 * (ax * bx + ay * by); + const C = bx * bx + by * by; + if (A === 0) { + return (t * Math.sqrt(Math.pow(xs[2] - xs[0], 2) + Math.pow(ys[2] - ys[0], 2))); + } + const b = B / (2 * A); + const c = C / A; + const u = t + b; + const k = c - b * b; + const uuk = u * u + k > 0 ? Math.sqrt(u * u + k) : 0; + const bbk = b * b + k > 0 ? Math.sqrt(b * b + k) : 0; + const term = b + Math.sqrt(b * b + k) !== 0 + ? k * Math.log(Math.abs((u + uuk) / (b + bbk))) + : 0; + return (Math.sqrt(A) / 2) * (u * uuk - b * bbk + term); + }; + function BFunc(xs, ys, t) { + const xbase = getDerivative(1, t, xs); + const ybase = getDerivative(1, t, ys); + const combined = xbase * xbase + ybase * ybase; + return Math.sqrt(combined); + } + /** + * Compute the curve derivative (hodograph) at t. + */ + const getDerivative = (derivative, t, vs) => { + // the derivative of any 't'-less function is zero. + const n = vs.length - 1; + let _vs; + let value; + if (n === 0) { + return 0; + } + // direct values? compute! + if (derivative === 0) { + value = 0; + for (let k = 0; k <= n; k++) { + value += + binomialCoefficients[n][k] * + Math.pow(1 - t, n - k) * + Math.pow(t, k) * + vs[k]; + } + return value; + } + else { + // Still some derivative? go down one order, then try + // for the lower order curve's. + _vs = new Array(n); + for (let k = 0; k < n; k++) { + _vs[k] = n * (vs[k + 1] - vs[k]); + } + return getDerivative(derivative - 1, t, _vs); + } + }; + const t2length = (length, totalLength, func) => { + let error = 1; + let t = length / totalLength; + let step = (length - func(t)) / totalLength; + let numIterations = 0; + while (error > 0.001) { + const increasedTLength = func(t + step); + const increasedTError = Math.abs(length - increasedTLength) / totalLength; + if (increasedTError < error) { + error = increasedTError; + t += step; + } + else { + const decreasedTLength = func(t - step); + const decreasedTError = Math.abs(length - decreasedTLength) / totalLength; + if (decreasedTError < error) { + error = decreasedTError; + t -= step; + } + else { + step /= 2; + } + } + numIterations++; + if (numIterations > 500) { + break; + } + } + return t; + }; + + /** + * Path constructor. + * @author Jason Follas + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} config.data SVG data string + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var path = new Konva.Path({ + * x: 240, + * y: 40, + * data: 'M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z', + * fill: 'green', + * scaleX: 2, + * scaleY: 2 + * }); + */ + class Path extends Shape { + constructor(config) { + super(config); + this.dataArray = []; + this.pathLength = 0; + this._readDataAttribute(); + this.on('dataChange.konva', function () { + this._readDataAttribute(); + }); + } + _readDataAttribute() { + this.dataArray = Path.parsePathData(this.data()); + this.pathLength = Path.getPathLength(this.dataArray); + } + _sceneFunc(context) { + const ca = this.dataArray; + // context position + context.beginPath(); + let isClosed = false; + for (let n = 0; n < ca.length; n++) { + const c = ca[n].command; + const p = ca[n].points; + switch (c) { + case 'L': + context.lineTo(p[0], p[1]); + break; + case 'M': + context.moveTo(p[0], p[1]); + break; + case 'C': + context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]); + break; + case 'Q': + context.quadraticCurveTo(p[0], p[1], p[2], p[3]); + break; + case 'A': + var cx = p[0], cy = p[1], rx = p[2], ry = p[3], theta = p[4], dTheta = p[5], psi = p[6], fs = p[7]; + var r = rx > ry ? rx : ry; + var scaleX = rx > ry ? 1 : rx / ry; + var scaleY = rx > ry ? ry / rx : 1; + context.translate(cx, cy); + context.rotate(psi); + context.scale(scaleX, scaleY); + context.arc(0, 0, r, theta, theta + dTheta, 1 - fs); + context.scale(1 / scaleX, 1 / scaleY); + context.rotate(-psi); + context.translate(-cx, -cy); + break; + case 'z': + isClosed = true; + context.closePath(); + break; + } + } + if (!isClosed && !this.hasFill()) { + context.strokeShape(this); + } + else { + context.fillStrokeShape(this); + } + } + getSelfRect() { + let points = []; + this.dataArray.forEach(function (data) { + if (data.command === 'A') { + // Approximates by breaking curve into line segments + const start = data.points[4]; + // 4 = theta + const dTheta = data.points[5]; + // 5 = dTheta + const end = data.points[4] + dTheta; + let inc = Math.PI / 180.0; + // 1 degree resolution + if (Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + if (dTheta < 0) { + // clockwise + for (let t = start - inc; t > end; t -= inc) { + const point = Path.getPointOnEllipticalArc(data.points[0], data.points[1], data.points[2], data.points[3], t, 0); + points.push(point.x, point.y); + } + } + else { + // counter-clockwise + for (let t = start + inc; t < end; t += inc) { + const point = Path.getPointOnEllipticalArc(data.points[0], data.points[1], data.points[2], data.points[3], t, 0); + points.push(point.x, point.y); + } + } + } + else if (data.command === 'C') { + // Approximates by breaking curve into 100 line segments + for (let t = 0.0; t <= 1; t += 0.01) { + const point = Path.getPointOnCubicBezier(t, data.start.x, data.start.y, data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]); + points.push(point.x, point.y); + } + } + else { + // TODO: how can we calculate bezier curves better? + points = points.concat(data.points); + } + }); + let minX = points[0]; + let maxX = points[0]; + let minY = points[1]; + let maxY = points[1]; + let x, y; + for (let i = 0; i < points.length / 2; i++) { + x = points[i * 2]; + y = points[i * 2 + 1]; + // skip bad values + if (!isNaN(x)) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + } + if (!isNaN(y)) { + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + /** + * Return length of the path. + * @method + * @name Konva.Path#getLength + * @returns {Number} length + * @example + * var length = path.getLength(); + */ + getLength() { + return this.pathLength; + } + /** + * Get point on path at specific length of the path + * @method + * @name Konva.Path#getPointAtLength + * @param {Number} length length + * @returns {Object} point {x,y} point + * @example + * var point = path.getPointAtLength(10); + */ + getPointAtLength(length) { + return Path.getPointAtLengthOfDataArray(length, this.dataArray); + } + static getLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + static getPathLength(dataArray) { + let pathLength = 0; + for (let i = 0; i < dataArray.length; ++i) { + pathLength += dataArray[i].pathLength; + } + return pathLength; + } + static getPointAtLengthOfDataArray(length, dataArray) { + let points, i = 0, ii = dataArray.length; + if (!ii) { + return null; + } + while (i < ii && length > dataArray[i].pathLength) { + length -= dataArray[i].pathLength; + ++i; + } + if (i === ii) { + points = dataArray[i - 1].points.slice(-2); + return { + x: points[0], + y: points[1], + }; + } + if (length < 0.01) { + points = dataArray[i].points.slice(0, 2); + return { + x: points[0], + y: points[1], + }; + } + const cp = dataArray[i]; + const p = cp.points; + switch (cp.command) { + case 'L': + return Path.getPointOnLine(length, cp.start.x, cp.start.y, p[0], p[1]); + case 'C': + return Path.getPointOnCubicBezier(t2length(length, Path.getPathLength(dataArray), (i) => { + return getCubicArcLength([cp.start.x, p[0], p[2], p[4]], [cp.start.y, p[1], p[3], p[5]], i); + }), cp.start.x, cp.start.y, p[0], p[1], p[2], p[3], p[4], p[5]); + case 'Q': + return Path.getPointOnQuadraticBezier(t2length(length, Path.getPathLength(dataArray), (i) => { + return getQuadraticArcLength([cp.start.x, p[0], p[2]], [cp.start.y, p[1], p[3]], i); + }), cp.start.x, cp.start.y, p[0], p[1], p[2], p[3]); + case 'A': + var cx = p[0], cy = p[1], rx = p[2], ry = p[3], theta = p[4], dTheta = p[5], psi = p[6]; + theta += (dTheta * length) / cp.pathLength; + return Path.getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi); + } + return null; + } + static getPointOnLine(dist, P1x, P1y, P2x, P2y, fromX, fromY) { + fromX = fromX !== null && fromX !== undefined ? fromX : P1x; + fromY = fromY !== null && fromY !== undefined ? fromY : P1y; + const len = this.getLineLength(P1x, P1y, P2x, P2y); + if (len < 1e-10) { + return { x: P1x, y: P1y }; + } + if (P2x === P1x) { + // Vertical line + return { x: fromX, y: fromY + (P2y > P1y ? dist : -dist) }; + } + const m = (P2y - P1y) / (P2x - P1x); + const run = Math.sqrt((dist * dist) / (1 + m * m)) * (P2x < P1x ? -1 : 1); + const rise = m * run; + if (Math.abs(fromY - P1y - m * (fromX - P1x)) < 1e-10) { + return { x: fromX + run, y: fromY + rise }; + } + const u = ((fromX - P1x) * (P2x - P1x) + (fromY - P1y) * (P2y - P1y)) / (len * len); + const ix = P1x + u * (P2x - P1x); + const iy = P1y + u * (P2y - P1y); + const pRise = this.getLineLength(fromX, fromY, ix, iy); + const pRun = Math.sqrt(dist * dist - pRise * pRise); + const adjustedRun = Math.sqrt((pRun * pRun) / (1 + m * m)) * (P2x < P1x ? -1 : 1); + const adjustedRise = m * adjustedRun; + return { x: ix + adjustedRun, y: iy + adjustedRise }; + } + static getPointOnCubicBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) { + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + const x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct); + const y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct); + return { + x: x, + y: y, + }; + } + static getPointOnQuadraticBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y) { + function QB1(t) { + return t * t; + } + function QB2(t) { + return 2 * t * (1 - t); + } + function QB3(t) { + return (1 - t) * (1 - t); + } + const x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct); + const y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct); + return { + x: x, + y: y, + }; + } + static getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) { + const cosPsi = Math.cos(psi), sinPsi = Math.sin(psi); + const pt = { + x: rx * Math.cos(theta), + y: ry * Math.sin(theta), + }; + return { + x: cx + (pt.x * cosPsi - pt.y * sinPsi), + y: cy + (pt.x * sinPsi + pt.y * cosPsi), + }; + } + /* + * get parsed data array from the data + * string. V, v, H, h, and l data are converted to + * L data for the purpose of high performance Path + * rendering + */ + static parsePathData(data) { + // Path Data Segment must begin with a moveTo + //m (x y)+ Relative moveTo (subsequent points are treated as lineTo) + //M (x y)+ Absolute moveTo (subsequent points are treated as lineTo) + //l (x y)+ Relative lineTo + //L (x y)+ Absolute LineTo + //h (x)+ Relative horizontal lineTo + //H (x)+ Absolute horizontal lineTo + //v (y)+ Relative vertical lineTo + //V (y)+ Absolute vertical lineTo + //z (closepath) + //Z (closepath) + //c (x1 y1 x2 y2 x y)+ Relative Bezier curve + //C (x1 y1 x2 y2 x y)+ Absolute Bezier curve + //q (x1 y1 x y)+ Relative Quadratic Bezier + //Q (x1 y1 x y)+ Absolute Quadratic Bezier + //t (x y)+ Shorthand/Smooth Relative Quadratic Bezier + //T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier + //s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve + //S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve + //a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc + //A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc + // return early if data is not defined + if (!data) { + return []; + } + // command string + let cs = data; + // command chars + const cc = [ + 'm', + 'M', + 'l', + 'L', + 'v', + 'V', + 'h', + 'H', + 'z', + 'Z', + 'c', + 'C', + 'q', + 'Q', + 't', + 'T', + 's', + 'S', + 'a', + 'A', + ]; + // convert white spaces to commas + cs = cs.replace(new RegExp(' ', 'g'), ','); + // create pipes so that we can split the data + for (var n = 0; n < cc.length; n++) { + cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]); + } + // create array + const arr = cs.split('|'); + const ca = []; + const coords = []; + // init context point + let cpx = 0; + let cpy = 0; + const re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/gi; + let match; + for (n = 1; n < arr.length; n++) { + let str = arr[n]; + let c = str.charAt(0); + str = str.slice(1); + coords.length = 0; + while ((match = re.exec(str))) { + coords.push(match[0]); + } + // while ((match = re.exec(str))) { + // coords.push(match[0]); + // } + const p = []; + for (let j = 0, jlen = coords.length; j < jlen; j++) { + // extra case for merged flags + if (coords[j] === '00') { + p.push(0, 0); + continue; + } + const parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + p.push(parsed); + } + else { + p.push(0); + } + } + while (p.length > 0) { + if (isNaN(p[0])) { + // case for a trailing comma before next command + break; + } + let cmd = ''; + let points = []; + const startX = cpx, startY = cpy; + // Move var from within the switch to up here (jshint) + var prevCmd, ctlPtx, ctlPty; // Ss, Tt + var rx, ry, psi, fa, fs, x1, y1; // Aa + // convert l, H, h, V, and v to L + switch (c) { + // Note: Keep the lineTo's above the moveTo's in this switch + case 'l': + cpx += p.shift(); + cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'L': + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + // Note: lineTo handlers need to be above this point + case 'm': + var dx = p.shift(); + var dy = p.shift(); + cpx += dx; + cpy += dy; + cmd = 'M'; + // After closing the path move the current position + // to the the first point of the path (if any). + if (ca.length > 2 && ca[ca.length - 1].command === 'z') { + for (let idx = ca.length - 2; idx >= 0; idx--) { + if (ca[idx].command === 'M') { + cpx = ca[idx].points[0] + dx; + cpy = ca[idx].points[1] + dy; + break; + } + } + } + points.push(cpx, cpy); + c = 'l'; + // subsequent points are treated as relative lineTo + break; + case 'M': + cpx = p.shift(); + cpy = p.shift(); + cmd = 'M'; + points.push(cpx, cpy); + c = 'L'; + // subsequent points are treated as absolute lineTo + break; + case 'h': + cpx += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'H': + cpx = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'v': + cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'V': + cpy = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'C': + points.push(p.shift(), p.shift(), p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + case 'c': + points.push(cpx + p.shift(), cpy + p.shift(), cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 'S': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'C') { + ctlPtx = cpx + (cpx - prevCmd.points[2]); + ctlPty = cpy + (cpy - prevCmd.points[3]); + } + points.push(ctlPtx, ctlPty, p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 's': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'C') { + ctlPtx = cpx + (cpx - prevCmd.points[2]); + ctlPty = cpy + (cpy - prevCmd.points[3]); + } + points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 'Q': + points.push(p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + case 'q': + points.push(cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'Q'; + points.push(cpx, cpy); + break; + case 'T': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'Q') { + ctlPtx = cpx + (cpx - prevCmd.points[0]); + ctlPty = cpy + (cpy - prevCmd.points[1]); + } + cpx = p.shift(); + cpy = p.shift(); + cmd = 'Q'; + points.push(ctlPtx, ctlPty, cpx, cpy); + break; + case 't': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'Q') { + ctlPtx = cpx + (cpx - prevCmd.points[0]); + ctlPty = cpy + (cpy - prevCmd.points[1]); + } + cpx += p.shift(); + cpy += p.shift(); + cmd = 'Q'; + points.push(ctlPtx, ctlPty, cpx, cpy); + break; + case 'A': + rx = p.shift(); + ry = p.shift(); + psi = p.shift(); + fa = p.shift(); + fs = p.shift(); + x1 = cpx; + y1 = cpy; + cpx = p.shift(); + cpy = p.shift(); + cmd = 'A'; + points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi); + break; + case 'a': + rx = p.shift(); + ry = p.shift(); + psi = p.shift(); + fa = p.shift(); + fs = p.shift(); + x1 = cpx; + y1 = cpy; + cpx += p.shift(); + cpy += p.shift(); + cmd = 'A'; + points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi); + break; + } + ca.push({ + command: cmd || c, + points: points, + start: { + x: startX, + y: startY, + }, + pathLength: this.calcLength(startX, startY, cmd || c, points), + }); + } + if (c === 'z' || c === 'Z') { + ca.push({ + command: 'z', + points: [], + start: undefined, + pathLength: 0, + }); + } + } + return ca; + } + static calcLength(x, y, cmd, points) { + let len, p1, p2, t; + const path = Path; + switch (cmd) { + case 'L': + return path.getLineLength(x, y, points[0], points[1]); + case 'C': + return getCubicArcLength([x, points[0], points[2], points[4]], [y, points[1], points[3], points[5]], 1); + case 'Q': + return getQuadraticArcLength([x, points[0], points[2]], [y, points[1], points[3]], 1); + case 'A': + // Approximates by breaking curve into line segments + len = 0.0; + var start = points[4]; + // 4 = theta + var dTheta = points[5]; + // 5 = dTheta + var end = points[4] + dTheta; + var inc = Math.PI / 180.0; + // 1 degree resolution + if (Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi + p1 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0); + if (dTheta < 0) { + // clockwise + for (t = start - inc; t > end; t -= inc) { + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + else { + // counter-clockwise + for (t = start + inc; t < end; t += inc) { + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + return len; + } + return 0; + } + static convertEndpointToCenterParameterization(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) { + // Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + const psi = psiDeg * (Math.PI / 180.0); + const xp = (Math.cos(psi) * (x1 - x2)) / 2.0 + (Math.sin(psi) * (y1 - y2)) / 2.0; + const yp = (-1 * Math.sin(psi) * (x1 - x2)) / 2.0 + + (Math.cos(psi) * (y1 - y2)) / 2.0; + const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry); + if (lambda > 1) { + rx *= Math.sqrt(lambda); + ry *= Math.sqrt(lambda); + } + let f = Math.sqrt((rx * rx * (ry * ry) - rx * rx * (yp * yp) - ry * ry * (xp * xp)) / + (rx * rx * (yp * yp) + ry * ry * (xp * xp))); + if (fa === fs) { + f *= -1; + } + if (isNaN(f)) { + f = 0; + } + const cxp = (f * rx * yp) / ry; + const cyp = (f * -ry * xp) / rx; + const cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp; + const cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp; + const vMag = function (v) { + return Math.sqrt(v[0] * v[0] + v[1] * v[1]); + }; + const vRatio = function (u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); + }; + const vAngle = function (u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); + }; + const theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]); + const u = [(xp - cxp) / rx, (yp - cyp) / ry]; + const v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry]; + let dTheta = vAngle(u, v); + if (vRatio(u, v) <= -1) { + dTheta = Math.PI; + } + if (vRatio(u, v) >= 1) { + dTheta = 0; + } + if (fs === 0 && dTheta > 0) { + dTheta = dTheta - 2 * Math.PI; + } + if (fs === 1 && dTheta < 0) { + dTheta = dTheta + 2 * Math.PI; + } + return [cx, cy, rx, ry, theta, dTheta, psi, fs]; + } + } + Path.prototype.className = 'Path'; + Path.prototype._attrsAffectingSize = ['data']; + _registerNode(Path); + /** + * get/set SVG path data string. This method + * also automatically parses the data string + * into a data array. Currently supported SVG data: + * M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z + * @name Konva.Path#data + * @method + * @param {String} data svg path string + * @returns {String} + * @example + * // get data + * var data = path.data(); + * + * // set data + * path.data('M200,100h100v50z'); + */ + Factory.addGetterSetter(Path, 'data'); + + /** + * Arrow constructor + * @constructor + * @memberof Konva + * @augments Konva.Line + * @param {Object} config + * @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3]. + * @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation. + * The default is 0 + * @param {Number} config.pointerLength Arrow pointer length. Default value is 10. + * @param {Number} config.pointerWidth Arrow pointer width. Default value is 10. + * @param {Boolean} config.pointerAtBeginning Do we need to draw pointer on beginning position?. Default false. + * @param {Boolean} config.pointerAtEnding Do we need to draw pointer on ending position?. Default true. + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var line = new Konva.Line({ + * points: [73, 70, 340, 23, 450, 60, 500, 20], + * stroke: 'red', + * tension: 1, + * pointerLength : 10, + * pointerWidth : 12 + * }); + */ + class Arrow extends Line { + _sceneFunc(ctx) { + super._sceneFunc(ctx); + const PI2 = Math.PI * 2; + const points = this.points(); + let tp = points; + const fromTension = this.tension() !== 0 && points.length > 4; + if (fromTension) { + tp = this.getTensionPoints(); + } + const length = this.pointerLength(); + const n = points.length; + let dx, dy; + if (fromTension) { + const lp = [ + tp[tp.length - 4], + tp[tp.length - 3], + tp[tp.length - 2], + tp[tp.length - 1], + points[n - 2], + points[n - 1], + ]; + const lastLength = Path.calcLength(tp[tp.length - 4], tp[tp.length - 3], 'C', lp); + const previous = Path.getPointOnQuadraticBezier(Math.min(1, 1 - length / lastLength), lp[0], lp[1], lp[2], lp[3], lp[4], lp[5]); + dx = points[n - 2] - previous.x; + dy = points[n - 1] - previous.y; + } + else { + dx = points[n - 2] - points[n - 4]; + dy = points[n - 1] - points[n - 3]; + } + const radians = (Math.atan2(dy, dx) + PI2) % PI2; + const width = this.pointerWidth(); + if (this.pointerAtEnding()) { + ctx.save(); + ctx.beginPath(); + ctx.translate(points[n - 2], points[n - 1]); + ctx.rotate(radians); + ctx.moveTo(0, 0); + ctx.lineTo(-length, width / 2); + ctx.lineTo(-length, -width / 2); + ctx.closePath(); + ctx.restore(); + this.__fillStroke(ctx); + } + if (this.pointerAtBeginning()) { + ctx.save(); + ctx.beginPath(); + ctx.translate(points[0], points[1]); + if (fromTension) { + dx = (tp[0] + tp[2]) / 2 - points[0]; + dy = (tp[1] + tp[3]) / 2 - points[1]; + } + else { + dx = points[2] - points[0]; + dy = points[3] - points[1]; + } + ctx.rotate((Math.atan2(-dy, -dx) + PI2) % PI2); + ctx.moveTo(0, 0); + ctx.lineTo(-length, width / 2); + ctx.lineTo(-length, -width / 2); + ctx.closePath(); + ctx.restore(); + this.__fillStroke(ctx); + } + } + __fillStroke(ctx) { + // here is a tricky part + // we need to disable dash for arrow pointers + const isDashEnabled = this.dashEnabled(); + if (isDashEnabled) { + // manually disable dash for head + // it is better not to use setter here, + // because it will trigger attr change event + this.attrs.dashEnabled = false; + ctx.setLineDash([]); + } + ctx.fillStrokeShape(this); + // restore old value + if (isDashEnabled) { + this.attrs.dashEnabled = true; + } + } + getSelfRect() { + const lineRect = super.getSelfRect(); + const offset = this.pointerWidth() / 2; + return { + x: lineRect.x, + y: lineRect.y - offset, + width: lineRect.width, + height: lineRect.height + offset * 2, + }; + } + } + Arrow.prototype.className = 'Arrow'; + _registerNode(Arrow); + /** + * get/set pointerLength + * @name Konva.Arrow#pointerLength + * @method + * @param {Number} Length of pointer of arrow. The default is 10. + * @returns {Number} + * @example + * // get length + * var pointerLength = line.pointerLength(); + * + * // set length + * line.pointerLength(15); + */ + Factory.addGetterSetter(Arrow, 'pointerLength', 10, getNumberValidator()); + /** + * get/set pointerWidth + * @name Konva.Arrow#pointerWidth + * @method + * @param {Number} Width of pointer of arrow. + * The default is 10. + * @returns {Number} + * @example + * // get width + * var pointerWidth = line.pointerWidth(); + * + * // set width + * line.pointerWidth(15); + */ + Factory.addGetterSetter(Arrow, 'pointerWidth', 10, getNumberValidator()); + /** + * get/set pointerAtBeginning + * @name Konva.Arrow#pointerAtBeginning + * @method + * @param {Number} Should pointer displayed at beginning of arrow. The default is false. + * @returns {Boolean} + * @example + * // get value + * var pointerAtBeginning = line.pointerAtBeginning(); + * + * // set value + * line.pointerAtBeginning(true); + */ + Factory.addGetterSetter(Arrow, 'pointerAtBeginning', false); + /** + * get/set pointerAtEnding + * @name Konva.Arrow#pointerAtEnding + * @method + * @param {Number} Should pointer displayed at ending of arrow. The default is true. + * @returns {Boolean} + * @example + * // get value + * var pointerAtEnding = line.pointerAtEnding(); + * + * // set value + * line.pointerAtEnding(false); + */ + Factory.addGetterSetter(Arrow, 'pointerAtEnding', true); + + /** + * Circle constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.radius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // create circle + * var circle = new Konva.Circle({ + * radius: 40, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 5 + * }); + */ + class Circle extends Shape { + _sceneFunc(context) { + context.beginPath(); + context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.radius() * 2; + } + getHeight() { + return this.radius() * 2; + } + setWidth(width) { + if (this.radius() !== width / 2) { + this.radius(width / 2); + } + } + setHeight(height) { + if (this.radius() !== height / 2) { + this.radius(height / 2); + } + } + } + Circle.prototype._centroid = true; + Circle.prototype.className = 'Circle'; + Circle.prototype._attrsAffectingSize = ['radius']; + _registerNode(Circle); + /** + * get/set radius + * @name Konva.Circle#radius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radius + * var radius = circle.radius(); + * + * // set radius + * circle.radius(10); + */ + Factory.addGetterSetter(Circle, 'radius', 0, getNumberValidator()); + + /** + * Ellipse constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Object} config.radius defines x and y radius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var ellipse = new Konva.Ellipse({ + * radius : { + * x : 50, + * y : 50 + * }, + * fill: 'red' + * }); + */ + class Ellipse extends Shape { + _sceneFunc(context) { + const rx = this.radiusX(), ry = this.radiusY(); + context.beginPath(); + context.save(); + if (rx !== ry) { + context.scale(1, ry / rx); + } + context.arc(0, 0, rx, 0, Math.PI * 2, false); + context.restore(); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.radiusX() * 2; + } + getHeight() { + return this.radiusY() * 2; + } + setWidth(width) { + this.radiusX(width / 2); + } + setHeight(height) { + this.radiusY(height / 2); + } + } + Ellipse.prototype.className = 'Ellipse'; + Ellipse.prototype._centroid = true; + Ellipse.prototype._attrsAffectingSize = ['radiusX', 'radiusY']; + _registerNode(Ellipse); + // add getters setters + Factory.addComponentsGetterSetter(Ellipse, 'radius', ['x', 'y']); + /** + * get/set radius + * @name Konva.Ellipse#radius + * @method + * @param {Object} radius + * @param {Number} radius.x + * @param {Number} radius.y + * @returns {Object} + * @example + * // get radius + * var radius = ellipse.radius(); + * + * // set radius + * ellipse.radius({ + * x: 200, + * y: 100 + * }); + */ + Factory.addGetterSetter(Ellipse, 'radiusX', 0, getNumberValidator()); + /** + * get/set radius x + * @name Konva.Ellipse#radiusX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get radius x + * var radiusX = ellipse.radiusX(); + * + * // set radius x + * ellipse.radiusX(200); + */ + Factory.addGetterSetter(Ellipse, 'radiusY', 0, getNumberValidator()); + /** + * get/set radius y + * @name Konva.Ellipse#radiusY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get radius y + * var radiusY = ellipse.radiusY(); + * + * // set radius y + * ellipse.radiusY(200); + */ + + /** + * Image constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Image} config.image + * @param {Object} [config.crop] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var imageObj = new Image(); + * imageObj.onload = function() { + * var image = new Konva.Image({ + * x: 200, + * y: 50, + * image: imageObj, + * width: 100, + * height: 100 + * }); + * }; + * imageObj.src = '/path/to/image.jpg' + */ + class Image extends Shape { + constructor(attrs) { + super(attrs); + this._loadListener = () => { + this._requestDraw(); + }; + this.on('imageChange.konva', (props) => { + this._removeImageLoad(props.oldVal); + this._setImageLoad(); + }); + this._setImageLoad(); + } + _setImageLoad() { + const image = this.image(); + // check is image is already loaded + if (image && image.complete) { + return; + } + // check is video is already loaded + if (image && image.readyState === 4) { + return; + } + if (image && image['addEventListener']) { + image['addEventListener']('load', this._loadListener); + } + } + _removeImageLoad(image) { + if (image && image['removeEventListener']) { + image['removeEventListener']('load', this._loadListener); + } + } + destroy() { + this._removeImageLoad(this.image()); + super.destroy(); + return this; + } + _useBufferCanvas() { + const hasCornerRadius = !!this.cornerRadius(); + const hasShadow = this.hasShadow(); + if (hasCornerRadius && hasShadow) { + return true; + } + return super._useBufferCanvas(true); + } + _sceneFunc(context) { + const width = this.getWidth(); + const height = this.getHeight(); + const cornerRadius = this.cornerRadius(); + const image = this.attrs.image; + let params; + if (image) { + const cropWidth = this.attrs.cropWidth; + const cropHeight = this.attrs.cropHeight; + if (cropWidth && cropHeight) { + params = [ + image, + this.cropX(), + this.cropY(), + cropWidth, + cropHeight, + 0, + 0, + width, + height, + ]; + } + else { + params = [image, 0, 0, width, height]; + } + } + if (this.hasFill() || this.hasStroke() || cornerRadius) { + context.beginPath(); + cornerRadius + ? Util.drawRoundedRectPath(context, width, height, cornerRadius) + : context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); + } + if (image) { + if (cornerRadius) { + context.clip(); + } + context.drawImage.apply(context, params); + } + // If you need to draw later, you need to execute save/restore + } + _hitFunc(context) { + const width = this.width(), height = this.height(), cornerRadius = this.cornerRadius(); + context.beginPath(); + if (!cornerRadius) { + context.rect(0, 0, width, height); + } + else { + Util.drawRoundedRectPath(context, width, height, cornerRadius); + } + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + var _a, _b; + return (_a = this.attrs.width) !== null && _a !== undefined ? _a : (_b = this.image()) === null || _b === undefined ? undefined : _b.width; + } + getHeight() { + var _a, _b; + return (_a = this.attrs.height) !== null && _a !== undefined ? _a : (_b = this.image()) === null || _b === undefined ? undefined : _b.height; + } + /** + * load image from given url and create `Konva.Image` instance + * @method + * @memberof Konva.Image + * @param {String} url image source + * @param {Function} callback with Konva.Image instance as first argument + * @param {Function} onError optional error handler + * @example + * Konva.Image.fromURL(imageURL, function(image){ + * // image is Konva.Image instance + * layer.add(image); + * layer.draw(); + * }); + */ + static fromURL(url, callback, onError = null) { + const img = Util.createImageElement(); + img.onload = function () { + const image = new Image({ + image: img, + }); + callback(image); + }; + img.onerror = onError; + img.crossOrigin = 'Anonymous'; + img.src = url; + } + } + Image.prototype.className = 'Image'; + _registerNode(Image); + /** + * get/set corner radius + * @method + * @name Konva.Image#cornerRadius + * @param {Number} cornerRadius + * @returns {Number} + * @example + * // get corner radius + * var cornerRadius = image.cornerRadius(); + * + * // set corner radius + * image.cornerRadius(10); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * image.cornerRadius([0, 10, 20, 30]); + */ + Factory.addGetterSetter(Image, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4)); + /** + * get/set image source. It can be image, canvas or video element + * @name Konva.Image#image + * @method + * @param {Object} image source + * @returns {Object} + * @example + * // get value + * var image = shape.image(); + * + * // set value + * shape.image(img); + */ + Factory.addGetterSetter(Image, 'image'); + Factory.addComponentsGetterSetter(Image, 'crop', ['x', 'y', 'width', 'height']); + /** + * get/set crop + * @method + * @name Konva.Image#crop + * @param {Object} crop + * @param {Number} crop.x + * @param {Number} crop.y + * @param {Number} crop.width + * @param {Number} crop.height + * @returns {Object} + * @example + * // get crop + * var crop = image.crop(); + * + * // set crop + * image.crop({ + * x: 20, + * y: 20, + * width: 20, + * height: 20 + * }); + */ + Factory.addGetterSetter(Image, 'cropX', 0, getNumberValidator()); + /** + * get/set crop x + * @method + * @name Konva.Image#cropX + * @param {Number} x + * @returns {Number} + * @example + * // get crop x + * var cropX = image.cropX(); + * + * // set crop x + * image.cropX(20); + */ + Factory.addGetterSetter(Image, 'cropY', 0, getNumberValidator()); + /** + * get/set crop y + * @name Konva.Image#cropY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get crop y + * var cropY = image.cropY(); + * + * // set crop y + * image.cropY(20); + */ + Factory.addGetterSetter(Image, 'cropWidth', 0, getNumberValidator()); + /** + * get/set crop width + * @name Konva.Image#cropWidth + * @method + * @param {Number} width + * @returns {Number} + * @example + * // get crop width + * var cropWidth = image.cropWidth(); + * + * // set crop width + * image.cropWidth(20); + */ + Factory.addGetterSetter(Image, 'cropHeight', 0, getNumberValidator()); + /** + * get/set crop height + * @name Konva.Image#cropHeight + * @method + * @param {Number} height + * @returns {Number} + * @example + * // get crop height + * var cropHeight = image.cropHeight(); + * + * // set crop height + * image.cropHeight(20); + */ + + // constants + const ATTR_CHANGE_LIST$2 = [ + 'fontFamily', + 'fontSize', + 'fontStyle', + 'padding', + 'lineHeight', + 'text', + 'width', + 'height', + 'pointerDirection', + 'pointerWidth', + 'pointerHeight', + ], CHANGE_KONVA$1 = 'Change.konva', NONE$1 = 'none', UP = 'up', RIGHT$1 = 'right', DOWN = 'down', LEFT$1 = 'left', + // cached variables + attrChangeListLen$1 = ATTR_CHANGE_LIST$2.length; + /** + * Label constructor.  Labels are groups that contain a Text and Tag shape + * @constructor + * @memberof Konva + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // create label + * var label = new Konva.Label({ + * x: 100, + * y: 100, + * draggable: true + * }); + * + * // add a tag to the label + * label.add(new Konva.Tag({ + * fill: '#bbb', + * stroke: '#333', + * shadowColor: 'black', + * shadowBlur: 10, + * shadowOffset: [10, 10], + * shadowOpacity: 0.2, + * lineJoin: 'round', + * pointerDirection: 'up', + * pointerWidth: 20, + * pointerHeight: 20, + * cornerRadius: 5 + * })); + * + * // add text to the label + * label.add(new Konva.Text({ + * text: 'Hello World!', + * fontSize: 50, + * lineHeight: 1.2, + * padding: 10, + * fill: 'green' + * })); + */ + class Label extends Group { + constructor(config) { + super(config); + this.on('add.konva', function (evt) { + this._addListeners(evt.child); + this._sync(); + }); + } + /** + * get Text shape for the label. You need to access the Text shape in order to update + * the text properties + * @name Konva.Label#getText + * @method + * @example + * label.getText().fill('red') + */ + getText() { + return this.find('Text')[0]; + } + /** + * get Tag shape for the label. You need to access the Tag shape in order to update + * the pointer properties and the corner radius + * @name Konva.Label#getTag + * @method + */ + getTag() { + return this.find('Tag')[0]; + } + _addListeners(text) { + let that = this, n; + const func = function () { + that._sync(); + }; + // update text data for certain attr changes + for (n = 0; n < attrChangeListLen$1; n++) { + text.on(ATTR_CHANGE_LIST$2[n] + CHANGE_KONVA$1, func); + } + } + getWidth() { + return this.getText().width(); + } + getHeight() { + return this.getText().height(); + } + _sync() { + let text = this.getText(), tag = this.getTag(), width, height, pointerDirection, pointerWidth, x, y, pointerHeight; + if (text && tag) { + width = text.width(); + height = text.height(); + pointerDirection = tag.pointerDirection(); + pointerWidth = tag.pointerWidth(); + pointerHeight = tag.pointerHeight(); + x = 0; + y = 0; + switch (pointerDirection) { + case UP: + x = width / 2; + y = -1 * pointerHeight; + break; + case RIGHT$1: + x = width + pointerWidth; + y = height / 2; + break; + case DOWN: + x = width / 2; + y = height + pointerHeight; + break; + case LEFT$1: + x = -1 * pointerWidth; + y = height / 2; + break; + } + tag.setAttrs({ + x: -1 * x, + y: -1 * y, + width: width, + height: height, + }); + text.setAttrs({ + x: -1 * x, + y: -1 * y, + }); + } + } + } + Label.prototype.className = 'Label'; + _registerNode(Label); + /** + * Tag constructor.  A Tag can be configured + * to have a pointer element that points up, right, down, or left + * @constructor + * @memberof Konva + * @param {Object} config + * @param {String} [config.pointerDirection] can be up, right, down, left, or none; the default + * is none. When a pointer is present, the positioning of the label is relative to the tip of the pointer. + * @param {Number} [config.pointerWidth] + * @param {Number} [config.pointerHeight] + * @param {Number} [config.cornerRadius] + */ + class Tag extends Shape { + _sceneFunc(context) { + const width = this.width(), height = this.height(), pointerDirection = this.pointerDirection(), pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), cornerRadius = this.cornerRadius(); + let topLeft = 0; + let topRight = 0; + let bottomLeft = 0; + let bottomRight = 0; + if (typeof cornerRadius === 'number') { + topLeft = + topRight = + bottomLeft = + bottomRight = + Math.min(cornerRadius, width / 2, height / 2); + } + else { + topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2); + topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2); + bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2); + bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2); + } + context.beginPath(); + context.moveTo(topLeft, 0); + if (pointerDirection === UP) { + context.lineTo((width - pointerWidth) / 2, 0); + context.lineTo(width / 2, -1 * pointerHeight); + context.lineTo((width + pointerWidth) / 2, 0); + } + context.lineTo(width - topRight, 0); + context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false); + if (pointerDirection === RIGHT$1) { + context.lineTo(width, (height - pointerHeight) / 2); + context.lineTo(width + pointerWidth, height / 2); + context.lineTo(width, (height + pointerHeight) / 2); + } + context.lineTo(width, height - bottomRight); + context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false); + if (pointerDirection === DOWN) { + context.lineTo((width + pointerWidth) / 2, height); + context.lineTo(width / 2, height + pointerHeight); + context.lineTo((width - pointerWidth) / 2, height); + } + context.lineTo(bottomLeft, height); + context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false); + if (pointerDirection === LEFT$1) { + context.lineTo(0, (height + pointerHeight) / 2); + context.lineTo(-1 * pointerWidth, height / 2); + context.lineTo(0, (height - pointerHeight) / 2); + } + context.lineTo(0, topLeft); + context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); + context.closePath(); + context.fillStrokeShape(this); + } + getSelfRect() { + let x = 0, y = 0, pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), direction = this.pointerDirection(), width = this.width(), height = this.height(); + if (direction === UP) { + y -= pointerHeight; + height += pointerHeight; + } + else if (direction === DOWN) { + height += pointerHeight; + } + else if (direction === LEFT$1) { + // ARGH!!! I have no idea why should I used magic 1.5!!!!!!!!! + x -= pointerWidth * 1.5; + width += pointerWidth; + } + else if (direction === RIGHT$1) { + width += pointerWidth * 1.5; + } + return { + x: x, + y: y, + width: width, + height: height, + }; + } + } + Tag.prototype.className = 'Tag'; + _registerNode(Tag); + /** + * get/set pointer direction + * @name Konva.Tag#pointerDirection + * @method + * @param {String} pointerDirection can be up, right, down, left, or none. The default is none. + * @returns {String} + * @example + * tag.pointerDirection('right'); + */ + Factory.addGetterSetter(Tag, 'pointerDirection', NONE$1); + /** + * get/set pointer width + * @name Konva.Tag#pointerWidth + * @method + * @param {Number} pointerWidth + * @returns {Number} + * @example + * tag.pointerWidth(20); + */ + Factory.addGetterSetter(Tag, 'pointerWidth', 0, getNumberValidator()); + /** + * get/set pointer height + * @method + * @name Konva.Tag#pointerHeight + * @param {Number} pointerHeight + * @returns {Number} + * @example + * tag.pointerHeight(20); + */ + Factory.addGetterSetter(Tag, 'pointerHeight', 0, getNumberValidator()); + /** + * get/set cornerRadius + * @name Konva.Tag#cornerRadius + * @method + * @param {Number} cornerRadius + * @returns {Number} + * @example + * tag.cornerRadius(20); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * tag.cornerRadius([0, 10, 20, 30]); + */ + Factory.addGetterSetter(Tag, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4)); + + /** + * Rect constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} [config.cornerRadius] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var rect = new Konva.Rect({ + * width: 100, + * height: 50, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 5 + * }); + */ + class Rect extends Shape { + _sceneFunc(context) { + const cornerRadius = this.cornerRadius(), width = this.width(), height = this.height(); + context.beginPath(); + if (!cornerRadius) { + // simple rect - don't bother doing all that complicated maths stuff. + context.rect(0, 0, width, height); + } + else { + Util.drawRoundedRectPath(context, width, height, cornerRadius); + } + context.closePath(); + context.fillStrokeShape(this); + } + } + Rect.prototype.className = 'Rect'; + _registerNode(Rect); + /** + * get/set corner radius + * @method + * @name Konva.Rect#cornerRadius + * @param {Number} cornerRadius + * @returns {Number} + * @example + * // get corner radius + * var cornerRadius = rect.cornerRadius(); + * + * // set corner radius + * rect.cornerRadius(10); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * rect.cornerRadius([0, 10, 20, 30]); + */ + Factory.addGetterSetter(Rect, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4)); + + /** + * RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc. + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.sides + * @param {Number} config.radius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var hexagon = new Konva.RegularPolygon({ + * x: 100, + * y: 200, + * sides: 6, + * radius: 70, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 4 + * }); + */ + class RegularPolygon extends Shape { + _sceneFunc(context) { + const points = this._getPoints(); + context.beginPath(); + context.moveTo(points[0].x, points[0].y); + for (let n = 1; n < points.length; n++) { + context.lineTo(points[n].x, points[n].y); + } + context.closePath(); + context.fillStrokeShape(this); + } + _getPoints() { + const sides = this.attrs.sides; + const radius = this.attrs.radius || 0; + const points = []; + for (let n = 0; n < sides; n++) { + points.push({ + x: radius * Math.sin((n * 2 * Math.PI) / sides), + y: -1 * radius * Math.cos((n * 2 * Math.PI) / sides), + }); + } + return points; + } + getSelfRect() { + const points = this._getPoints(); + let minX = points[0].x; + let maxX = points[0].y; + let minY = points[0].x; + let maxY = points[0].y; + points.forEach((point) => { + minX = Math.min(minX, point.x); + maxX = Math.max(maxX, point.x); + minY = Math.min(minY, point.y); + maxY = Math.max(maxY, point.y); + }); + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + getWidth() { + return this.radius() * 2; + } + getHeight() { + return this.radius() * 2; + } + setWidth(width) { + this.radius(width / 2); + } + setHeight(height) { + this.radius(height / 2); + } + } + RegularPolygon.prototype.className = 'RegularPolygon'; + RegularPolygon.prototype._centroid = true; + RegularPolygon.prototype._attrsAffectingSize = ['radius']; + _registerNode(RegularPolygon); + /** + * get/set radius + * @method + * @name Konva.RegularPolygon#radius + * @param {Number} radius + * @returns {Number} + * @example + * // get radius + * var radius = shape.radius(); + * + * // set radius + * shape.radius(10); + */ + Factory.addGetterSetter(RegularPolygon, 'radius', 0, getNumberValidator()); + /** + * get/set sides + * @method + * @name Konva.RegularPolygon#sides + * @param {Number} sides + * @returns {Number} + * @example + * // get sides + * var sides = shape.sides(); + * + * // set sides + * shape.sides(10); + */ + Factory.addGetterSetter(RegularPolygon, 'sides', 0, getNumberValidator()); + + const PIx2 = Math.PI * 2; + /** + * Ring constructor + * @constructor + * @augments Konva.Shape + * @memberof Konva + * @param {Object} config + * @param {Number} config.innerRadius + * @param {Number} config.outerRadius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var ring = new Konva.Ring({ + * innerRadius: 40, + * outerRadius: 80, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 5 + * }); + */ + class Ring extends Shape { + _sceneFunc(context) { + context.beginPath(); + context.arc(0, 0, this.innerRadius(), 0, PIx2, false); + context.moveTo(this.outerRadius(), 0); + context.arc(0, 0, this.outerRadius(), PIx2, 0, true); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.outerRadius() * 2; + } + getHeight() { + return this.outerRadius() * 2; + } + setWidth(width) { + this.outerRadius(width / 2); + } + setHeight(height) { + this.outerRadius(height / 2); + } + } + Ring.prototype.className = 'Ring'; + Ring.prototype._centroid = true; + Ring.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius']; + _registerNode(Ring); + /** + * get/set innerRadius + * @method + * @name Konva.Ring#innerRadius + * @param {Number} innerRadius + * @returns {Number} + * @example + * // get inner radius + * var innerRadius = ring.innerRadius(); + * + * // set inner radius + * ring.innerRadius(20); + */ + Factory.addGetterSetter(Ring, 'innerRadius', 0, getNumberValidator()); + /** + * get/set outerRadius + * @name Konva.Ring#outerRadius + * @method + * @param {Number} outerRadius + * @returns {Number} + * @example + * // get outer radius + * var outerRadius = ring.outerRadius(); + * + * // set outer radius + * ring.outerRadius(20); + */ + Factory.addGetterSetter(Ring, 'outerRadius', 0, getNumberValidator()); + + /** + * Sprite constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} config.animation animation key + * @param {Object} config.animations animation map + * @param {Integer} [config.frameIndex] animation frame index + * @param {Image} config.image image object + * @param {Integer} [config.frameRate] animation frame rate + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var imageObj = new Image(); + * imageObj.onload = function() { + * var sprite = new Konva.Sprite({ + * x: 200, + * y: 100, + * image: imageObj, + * animation: 'standing', + * animations: { + * standing: [ + * // x, y, width, height (6 frames) + * 0, 0, 49, 109, + * 52, 0, 49, 109, + * 105, 0, 49, 109, + * 158, 0, 49, 109, + * 210, 0, 49, 109, + * 262, 0, 49, 109 + * ], + * kicking: [ + * // x, y, width, height (6 frames) + * 0, 109, 45, 98, + * 45, 109, 45, 98, + * 95, 109, 63, 98, + * 156, 109, 70, 98, + * 229, 109, 60, 98, + * 287, 109, 41, 98 + * ] + * }, + * frameRate: 7, + * frameIndex: 0 + * }); + * }; + * imageObj.src = '/path/to/image.jpg' + */ + class Sprite extends Shape { + constructor(config) { + super(config); + this._updated = true; + this.anim = new Animation(() => { + // if we don't need to redraw layer we should return false + const updated = this._updated; + this._updated = false; + return updated; + }); + this.on('animationChange.konva', function () { + // reset index when animation changes + this.frameIndex(0); + }); + this.on('frameIndexChange.konva', function () { + this._updated = true; + }); + // smooth change for frameRate + this.on('frameRateChange.konva', function () { + if (!this.anim.isRunning()) { + return; + } + clearInterval(this.interval); + this._setInterval(); + }); + } + _sceneFunc(context) { + const anim = this.animation(), index = this.frameIndex(), ix4 = index * 4, set = this.animations()[anim], offsets = this.frameOffsets(), x = set[ix4 + 0], y = set[ix4 + 1], width = set[ix4 + 2], height = set[ix4 + 3], image = this.image(); + if (this.hasFill() || this.hasStroke()) { + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); + } + if (image) { + if (offsets) { + const offset = offsets[anim], ix2 = index * 2; + context.drawImage(image, x, y, width, height, offset[ix2 + 0], offset[ix2 + 1], width, height); + } + else { + context.drawImage(image, x, y, width, height, 0, 0, width, height); + } + } + } + _hitFunc(context) { + const anim = this.animation(), index = this.frameIndex(), ix4 = index * 4, set = this.animations()[anim], offsets = this.frameOffsets(), width = set[ix4 + 2], height = set[ix4 + 3]; + context.beginPath(); + if (offsets) { + const offset = offsets[anim]; + const ix2 = index * 2; + context.rect(offset[ix2 + 0], offset[ix2 + 1], width, height); + } + else { + context.rect(0, 0, width, height); + } + context.closePath(); + context.fillShape(this); + } + _useBufferCanvas() { + return super._useBufferCanvas(true); + } + _setInterval() { + const that = this; + this.interval = setInterval(function () { + that._updateIndex(); + }, 1000 / this.frameRate()); + } + /** + * start sprite animation + * @method + * @name Konva.Sprite#start + */ + start() { + if (this.isRunning()) { + return; + } + const layer = this.getLayer(); + /* + * animation object has no executable function because + * the updates are done with a fixed FPS with the setInterval + * below. The anim object only needs the layer reference for + * redraw + */ + this.anim.setLayers(layer); + this._setInterval(); + this.anim.start(); + } + /** + * stop sprite animation + * @method + * @name Konva.Sprite#stop + */ + stop() { + this.anim.stop(); + clearInterval(this.interval); + } + /** + * determine if animation of sprite is running or not. returns true or false + * @method + * @name Konva.Sprite#isRunning + * @returns {Boolean} + */ + isRunning() { + return this.anim.isRunning(); + } + _updateIndex() { + const index = this.frameIndex(), animation = this.animation(), animations = this.animations(), anim = animations[animation], len = anim.length / 4; + if (index < len - 1) { + this.frameIndex(index + 1); + } + else { + this.frameIndex(0); + } + } + } + Sprite.prototype.className = 'Sprite'; + _registerNode(Sprite); + // add getters setters + Factory.addGetterSetter(Sprite, 'animation'); + /** + * get/set animation key + * @name Konva.Sprite#animation + * @method + * @param {String} anim animation key + * @returns {String} + * @example + * // get animation key + * var animation = sprite.animation(); + * + * // set animation key + * sprite.animation('kicking'); + */ + Factory.addGetterSetter(Sprite, 'animations'); + /** + * get/set animations map + * @name Konva.Sprite#animations + * @method + * @param {Object} animations + * @returns {Object} + * @example + * // get animations map + * var animations = sprite.animations(); + * + * // set animations map + * sprite.animations({ + * standing: [ + * // x, y, width, height (6 frames) + * 0, 0, 49, 109, + * 52, 0, 49, 109, + * 105, 0, 49, 109, + * 158, 0, 49, 109, + * 210, 0, 49, 109, + * 262, 0, 49, 109 + * ], + * kicking: [ + * // x, y, width, height (6 frames) + * 0, 109, 45, 98, + * 45, 109, 45, 98, + * 95, 109, 63, 98, + * 156, 109, 70, 98, + * 229, 109, 60, 98, + * 287, 109, 41, 98 + * ] + * }); + */ + Factory.addGetterSetter(Sprite, 'frameOffsets'); + /** + * get/set offsets map + * @name Konva.Sprite#offsets + * @method + * @param {Object} offsets + * @returns {Object} + * @example + * // get offsets map + * var offsets = sprite.offsets(); + * + * // set offsets map + * sprite.offsets({ + * standing: [ + * // x, y (6 frames) + * 0, 0, + * 0, 0, + * 5, 0, + * 0, 0, + * 0, 3, + * 2, 0 + * ], + * kicking: [ + * // x, y (6 frames) + * 0, 5, + * 5, 0, + * 10, 0, + * 0, 0, + * 2, 1, + * 0, 0 + * ] + * }); + */ + Factory.addGetterSetter(Sprite, 'image'); + /** + * get/set image + * @name Konva.Sprite#image + * @method + * @param {Image} image + * @returns {Image} + * @example + * // get image + * var image = sprite.image(); + * + * // set image + * sprite.image(imageObj); + */ + Factory.addGetterSetter(Sprite, 'frameIndex', 0, getNumberValidator()); + /** + * set/set animation frame index + * @name Konva.Sprite#frameIndex + * @method + * @param {Integer} frameIndex + * @returns {Integer} + * @example + * // get animation frame index + * var frameIndex = sprite.frameIndex(); + * + * // set animation frame index + * sprite.frameIndex(3); + */ + Factory.addGetterSetter(Sprite, 'frameRate', 17, getNumberValidator()); + /** + * get/set frame rate in frames per second. Increase this number to make the sprite + * animation run faster, and decrease the number to make the sprite animation run slower + * The default is 17 frames per second + * @name Konva.Sprite#frameRate + * @method + * @param {Integer} frameRate + * @returns {Integer} + * @example + * // get frame rate + * var frameRate = sprite.frameRate(); + * + * // set frame rate to 2 frames per second + * sprite.frameRate(2); + */ + Factory.backCompat(Sprite, { + index: 'frameIndex', + getIndex: 'getFrameIndex', + setIndex: 'setFrameIndex', + }); + + /** + * Star constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Integer} config.numPoints + * @param {Number} config.innerRadius + * @param {Number} config.outerRadius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var star = new Konva.Star({ + * x: 100, + * y: 200, + * numPoints: 5, + * innerRadius: 70, + * outerRadius: 70, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 4 + * }); + */ + class Star extends Shape { + _sceneFunc(context) { + const innerRadius = this.innerRadius(), outerRadius = this.outerRadius(), numPoints = this.numPoints(); + context.beginPath(); + context.moveTo(0, 0 - outerRadius); + for (let n = 1; n < numPoints * 2; n++) { + const radius = n % 2 === 0 ? outerRadius : innerRadius; + const x = radius * Math.sin((n * Math.PI) / numPoints); + const y = -1 * radius * Math.cos((n * Math.PI) / numPoints); + context.lineTo(x, y); + } + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.outerRadius() * 2; + } + getHeight() { + return this.outerRadius() * 2; + } + setWidth(width) { + this.outerRadius(width / 2); + } + setHeight(height) { + this.outerRadius(height / 2); + } + } + Star.prototype.className = 'Star'; + Star.prototype._centroid = true; + Star.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius']; + _registerNode(Star); + /** + * get/set number of points + * @name Konva.Star#numPoints + * @method + * @param {Number} numPoints + * @returns {Number} + * @example + * // get inner radius + * var numPoints = star.numPoints(); + * + * // set inner radius + * star.numPoints(20); + */ + Factory.addGetterSetter(Star, 'numPoints', 5, getNumberValidator()); + /** + * get/set innerRadius + * @name Konva.Star#innerRadius + * @method + * @param {Number} innerRadius + * @returns {Number} + * @example + * // get inner radius + * var innerRadius = star.innerRadius(); + * + * // set inner radius + * star.innerRadius(20); + */ + Factory.addGetterSetter(Star, 'innerRadius', 0, getNumberValidator()); + /** + * get/set outerRadius + * @name Konva.Star#outerRadius + * @method + * @param {Number} outerRadius + * @returns {Number} + * @example + * // get inner radius + * var outerRadius = star.outerRadius(); + * + * // set inner radius + * star.outerRadius(20); + */ + Factory.addGetterSetter(Star, 'outerRadius', 0, getNumberValidator()); + + function stringToArray(string) { + // Use Unicode-aware splitting + return [...string].reduce((acc, char, index, array) => { + // Handle emoji with skin tone modifiers and ZWJ sequences + if (/\p{Emoji}/u.test(char)) { + // Check if next character is a modifier or ZWJ sequence + const nextChar = array[index + 1]; + if (nextChar && /\p{Emoji_Modifier}|\u200D/u.test(nextChar)) { + // If we have a modifier, combine with current emoji + acc.push(char + nextChar); + // Skip the next character since we've used it + array[index + 1] = ''; + } + else { + // No modifier - treat as separate emoji + acc.push(char); + } + } + // Handle regional indicator symbols (flags) + else if (/\p{Regional_Indicator}{2}/u.test(char + (array[index + 1] || ''))) { + acc.push(char + array[index + 1]); + } + // Handle Indic scripts and other combining characters + else if (index > 0 && /\p{Mn}|\p{Me}|\p{Mc}/u.test(char)) { + acc[acc.length - 1] += char; + } + // Handle other characters + else if (char) { + // Only push if not an empty string (skipped modifier) + acc.push(char); + } + return acc; + }, []); + } + // constants + const AUTO = 'auto', + //CANVAS = 'canvas', + CENTER = 'center', INHERIT = 'inherit', JUSTIFY = 'justify', CHANGE_KONVA = 'Change.konva', CONTEXT_2D = '2d', DASH = '-', LEFT = 'left', TEXT = 'text', TEXT_UPPER = 'Text', TOP = 'top', BOTTOM = 'bottom', MIDDLE = 'middle', NORMAL$1 = 'normal', PX_SPACE = 'px ', SPACE = ' ', RIGHT = 'right', RTL = 'rtl', WORD = 'word', CHAR = 'char', NONE = 'none', ELLIPSIS = '…', ATTR_CHANGE_LIST$1 = [ + 'direction', + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontVariant', + 'padding', + 'align', + 'verticalAlign', + 'lineHeight', + 'text', + 'width', + 'height', + 'wrap', + 'ellipsis', + 'letterSpacing', + ], + // cached variables + attrChangeListLen = ATTR_CHANGE_LIST$1.length; + function normalizeFontFamily(fontFamily) { + return fontFamily + .split(',') + .map((family) => { + family = family.trim(); + const hasSpace = family.indexOf(' ') >= 0; + const hasQuotes = family.indexOf('"') >= 0 || family.indexOf("'") >= 0; + if (hasSpace && !hasQuotes) { + family = `"${family}"`; + } + return family; + }) + .join(', '); + } + let dummyContext; + function getDummyContext() { + if (dummyContext) { + return dummyContext; + } + dummyContext = Util.createCanvasElement().getContext(CONTEXT_2D); + return dummyContext; + } + function _fillFunc$1(context) { + context.fillText(this._partialText, this._partialTextX, this._partialTextY); + } + function _strokeFunc$1(context) { + context.setAttr('miterLimit', 2); + context.strokeText(this._partialText, this._partialTextX, this._partialTextY); + } + function checkDefaultFill(config) { + config = config || {}; + // set default color to black + if (!config.fillLinearGradientColorStops && + !config.fillRadialGradientColorStops && + !config.fillPatternImage) { + config.fill = config.fill || 'black'; + } + return config; + } + /** + * Text constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} [config.direction] default is inherit + * @param {String} [config.fontFamily] default is Arial + * @param {Number} [config.fontSize] in pixels. Default is 12 + * @param {String} [config.fontStyle] can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @param {String} [config.fontVariant] can be normal or small-caps. Default is normal + * @param {String} [config.textDecoration] can be line-through, underline or empty string. Default is empty string. + * @param {String} config.text + * @param {String} [config.align] can be left, center, or right + * @param {String} [config.verticalAlign] can be top, middle or bottom + * @param {Number} [config.padding] + * @param {Number} [config.lineHeight] default is 1 + * @param {String} [config.wrap] can be "word", "char", or "none". Default is word + * @param {Boolean} [config.ellipsis] can be true or false. Default is false. if Konva.Text config is set to wrap="none" and ellipsis=true, then it will add "..." to the end + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var text = new Konva.Text({ + * x: 10, + * y: 15, + * text: 'Simple Text', + * fontSize: 30, + * fontFamily: 'Calibri', + * fill: 'green' + * }); + */ + class Text extends Shape { + constructor(config) { + super(checkDefaultFill(config)); + this._partialTextX = 0; + this._partialTextY = 0; + // update text data for certain attr changes + for (let n = 0; n < attrChangeListLen; n++) { + this.on(ATTR_CHANGE_LIST$1[n] + CHANGE_KONVA, this._setTextData); + } + this._setTextData(); + } + _sceneFunc(context) { + const textArr = this.textArr, textArrLen = textArr.length; + if (!this.text()) { + return; + } + let padding = this.padding(), fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, verticalAlign = this.verticalAlign(), direction = this.direction(), alignY = 0, align = this.align(), totalWidth = this.getWidth(), letterSpacing = this.letterSpacing(), fill = this.fill(), textDecoration = this.textDecoration(), shouldUnderline = textDecoration.indexOf('underline') !== -1, shouldLineThrough = textDecoration.indexOf('line-through') !== -1, n; + direction = direction === INHERIT ? context.direction : direction; + let translateY = lineHeightPx / 2; + let baseline = MIDDLE; + if (Konva$2._fixTextRendering) { + const metrics = this.measureSize('M'); // Use a sample character to get the ascent + baseline = 'alphabetic'; + translateY = + (metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2 + + lineHeightPx / 2; + } + var lineTranslateX = 0; + var lineTranslateY = 0; + if (direction === RTL) { + context.setAttr('direction', direction); + } + context.setAttr('font', this._getContextFont()); + context.setAttr('textBaseline', baseline); + context.setAttr('textAlign', LEFT); + // handle vertical alignment + if (verticalAlign === MIDDLE) { + alignY = (this.getHeight() - textArrLen * lineHeightPx - padding * 2) / 2; + } + else if (verticalAlign === BOTTOM) { + alignY = this.getHeight() - textArrLen * lineHeightPx - padding * 2; + } + context.translate(padding, alignY + padding); + // draw text lines + for (n = 0; n < textArrLen; n++) { + var lineTranslateX = 0; + var lineTranslateY = 0; + var obj = textArr[n], text = obj.text, width = obj.width, lastLine = obj.lastInParagraph, spacesNumber, lineWidth; + // horizontal alignment + context.save(); + if (align === RIGHT) { + lineTranslateX += totalWidth - width - padding * 2; + } + else if (align === CENTER) { + lineTranslateX += (totalWidth - width - padding * 2) / 2; + } + if (shouldUnderline) { + context.save(); + context.beginPath(); + const yOffset = Konva$2._fixTextRendering + ? Math.round(fontSize / 4) + : Math.round(fontSize / 2); + const x = lineTranslateX; + const y = translateY + lineTranslateY + yOffset; + context.moveTo(x, y); + spacesNumber = text.split(' ').length - 1; + lineWidth = + align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width; + context.lineTo(x + Math.round(lineWidth), y); + // I have no idea what is real ratio + // just /15 looks good enough + context.lineWidth = fontSize / 15; + const gradient = this._getLinearGradient(); + context.strokeStyle = gradient || fill; + context.stroke(); + context.restore(); + } + if (shouldLineThrough) { + context.save(); + context.beginPath(); + const yOffset = Konva$2._fixTextRendering ? -Math.round(fontSize / 4) : 0; + context.moveTo(lineTranslateX, translateY + lineTranslateY + yOffset); + spacesNumber = text.split(' ').length - 1; + lineWidth = + align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width; + context.lineTo(lineTranslateX + Math.round(lineWidth), translateY + lineTranslateY + yOffset); + context.lineWidth = fontSize / 15; + const gradient = this._getLinearGradient(); + context.strokeStyle = gradient || fill; + context.stroke(); + context.restore(); + } + // As `letterSpacing` isn't supported on Safari, we use this polyfill. + // The exception is for RTL text, which we rely on native as it cannot + // be supported otherwise. + if (direction !== RTL && (letterSpacing !== 0 || align === JUSTIFY)) { + // var words = text.split(' '); + spacesNumber = text.split(' ').length - 1; + const array = stringToArray(text); + for (let li = 0; li < array.length; li++) { + const letter = array[li]; + // skip justify for the last line + if (letter === ' ' && !lastLine && align === JUSTIFY) { + lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber; + // context.translate( + // Math.floor((totalWidth - padding * 2 - width) / spacesNumber), + // 0 + // ); + } + this._partialTextX = lineTranslateX; + this._partialTextY = translateY + lineTranslateY; + this._partialText = letter; + context.fillStrokeShape(this); + lineTranslateX += this.measureSize(letter).width + letterSpacing; + } + } + else { + if (letterSpacing !== 0) { + context.setAttr('letterSpacing', `${letterSpacing}px`); + } + this._partialTextX = lineTranslateX; + this._partialTextY = translateY + lineTranslateY; + this._partialText = text; + context.fillStrokeShape(this); + } + context.restore(); + if (textArrLen > 1) { + translateY += lineHeightPx; + } + } + } + _hitFunc(context) { + const width = this.getWidth(), height = this.getHeight(); + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); + } + setText(text) { + const str = Util._isString(text) + ? text + : text === null || text === undefined + ? '' + : text + ''; + this._setAttr(TEXT, str); + return this; + } + getWidth() { + const isAuto = this.attrs.width === AUTO || this.attrs.width === undefined; + return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width; + } + getHeight() { + const isAuto = this.attrs.height === AUTO || this.attrs.height === undefined; + return isAuto + ? this.fontSize() * this.textArr.length * this.lineHeight() + + this.padding() * 2 + : this.attrs.height; + } + /** + * get pure text width without padding + * @method + * @name Konva.Text#getTextWidth + * @returns {Number} + */ + getTextWidth() { + return this.textWidth; + } + getTextHeight() { + Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.'); + return this.textHeight; + } + /** + * measure string with the font of current text shape. + * That method can't handle multiline text. + * @method + * @name Konva.Text#measureSize + * @param {String} text text to measure + * @returns {Object} { width , height } of measured text + */ + measureSize(text) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + let _context = getDummyContext(), fontSize = this.fontSize(), metrics; + _context.save(); + _context.font = this._getContextFont(); + metrics = _context.measureText(text); + _context.restore(); + // Scale the fallback values based on the provided fontSize compared to the sample size (100 in your new case) + const scaleFactor = fontSize / 100; + // Note, fallback values are from chrome browser with 100px font size and font-family "Arial" + return { + actualBoundingBoxAscent: (_a = metrics.actualBoundingBoxAscent) !== null && _a !== undefined ? _a : 71.58203125 * scaleFactor, + actualBoundingBoxDescent: (_b = metrics.actualBoundingBoxDescent) !== null && _b !== undefined ? _b : 0, // Remains zero as there is no descent in the provided metrics + actualBoundingBoxLeft: (_c = metrics.actualBoundingBoxLeft) !== null && _c !== undefined ? _c : -7.421875 * scaleFactor, + actualBoundingBoxRight: (_d = metrics.actualBoundingBoxRight) !== null && _d !== undefined ? _d : 75.732421875 * scaleFactor, + alphabeticBaseline: (_e = metrics.alphabeticBaseline) !== null && _e !== undefined ? _e : 0, // Remains zero as it's typically relative to the baseline itself + emHeightAscent: (_f = metrics.emHeightAscent) !== null && _f !== undefined ? _f : 100 * scaleFactor, + emHeightDescent: (_g = metrics.emHeightDescent) !== null && _g !== undefined ? _g : -20 * scaleFactor, + fontBoundingBoxAscent: (_h = metrics.fontBoundingBoxAscent) !== null && _h !== undefined ? _h : 91 * scaleFactor, + fontBoundingBoxDescent: (_j = metrics.fontBoundingBoxDescent) !== null && _j !== undefined ? _j : 21 * scaleFactor, + hangingBaseline: (_k = metrics.hangingBaseline) !== null && _k !== undefined ? _k : 72.80000305175781 * scaleFactor, + ideographicBaseline: (_l = metrics.ideographicBaseline) !== null && _l !== undefined ? _l : -21 * scaleFactor, + width: metrics.width, + height: fontSize, // Typically set to the font size + }; + } + _getContextFont() { + return (this.fontStyle() + + SPACE + + this.fontVariant() + + SPACE + + (this.fontSize() + PX_SPACE) + + // wrap font family into " so font families with spaces works ok + normalizeFontFamily(this.fontFamily())); + } + _addTextLine(line) { + const align = this.align(); + if (align === JUSTIFY) { + line = line.trim(); + } + const width = this._getTextWidth(line); + return this.textArr.push({ + text: line, + width: width, + lastInParagraph: false, + }); + } + _getTextWidth(text) { + const letterSpacing = this.letterSpacing(); + const length = text.length; + // letterSpacing * length is the total letter spacing for the text + // previously we used letterSpacing * (length - 1) but it doesn't match DOM behavior + return getDummyContext().measureText(text).width + letterSpacing * length; + } + _setTextData() { + let lines = this.text().split('\n'), fontSize = +this.fontSize(), textWidth = 0, lineHeightPx = this.lineHeight() * fontSize, width = this.attrs.width, height = this.attrs.height, fixedWidth = width !== AUTO && width !== undefined, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxWidth = width - padding * 2, maxHeightPx = height - padding * 2, currentHeightPx = 0, wrap = this.wrap(), + // align = this.align(), + shouldWrap = wrap !== NONE, wrapAtWord = wrap !== CHAR && shouldWrap, shouldAddEllipsis = this.ellipsis(); + this.textArr = []; + getDummyContext().font = this._getContextFont(); + const additionalWidth = shouldAddEllipsis + ? this._getTextWidth(ELLIPSIS) + : 0; + for (let i = 0, max = lines.length; i < max; ++i) { + let line = lines[i]; + let lineWidth = this._getTextWidth(line); + if (fixedWidth && lineWidth > maxWidth) { + /* + * if width is fixed and line does not fit entirely + * break the line into multiple fitting lines + */ + while (line.length > 0) { + /* + * use binary search to find the longest substring that + * that would fit in the specified width + */ + let low = 0, high = stringToArray(line).length, // Convert to array for proper emoji handling + match = '', matchWidth = 0; + while (low < high) { + const mid = (low + high) >>> 1, + // Convert array indices to string + lineArray = stringToArray(line), substr = lineArray.slice(0, mid + 1).join(''), substrWidth = this._getTextWidth(substr); + // Only add ellipsis width when we need to consider truncation + // for the current line (when it might be the last visible line) + const shouldConsiderEllipsis = shouldAddEllipsis && + fixedHeight && + currentHeightPx + lineHeightPx > maxHeightPx; + const effectiveWidth = shouldConsiderEllipsis + ? substrWidth + additionalWidth + : substrWidth; + if (effectiveWidth <= maxWidth) { + low = mid + 1; + match = substr; + matchWidth = substrWidth; // Store actual text width without ellipsis + } + else { + high = mid; + } + } + /* + * 'low' is now the index of the substring end + * 'match' is the substring + * 'matchWidth' is the substring width in px + */ + if (match) { + // a fitting substring was found + if (wrapAtWord) { + // try to find a space or dash where wrapping could be done + const lineArray = stringToArray(line); + const matchArray = stringToArray(match); + const nextChar = lineArray[matchArray.length]; + const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH; + let wrapIndex; + if (nextIsSpaceOrDash && matchWidth <= maxWidth) { + wrapIndex = matchArray.length; + } + else { + // Find last space or dash in the array + const lastSpaceIndex = matchArray.lastIndexOf(SPACE); + const lastDashIndex = matchArray.lastIndexOf(DASH); + wrapIndex = Math.max(lastSpaceIndex, lastDashIndex) + 1; + } + if (wrapIndex > 0) { + low = wrapIndex; + match = lineArray.slice(0, low).join(''); + matchWidth = this._getTextWidth(match); + } + } + // if (align === 'right') { + match = match.trimRight(); + // } + this._addTextLine(match); + textWidth = Math.max(textWidth, matchWidth); + currentHeightPx += lineHeightPx; + const shouldHandleEllipsis = this._shouldHandleEllipsis(currentHeightPx); + if (shouldHandleEllipsis) { + this._tryToAddEllipsisToLastLine(); + /* + * stop wrapping if wrapping is disabled or if adding + * one more line would overflow the fixed height + */ + break; + } + // Convert remaining text using array operations + const lineArray = stringToArray(line); + line = lineArray.slice(low).join('').trimLeft(); + if (line.length > 0) { + lineWidth = this._getTextWidth(line); + if (lineWidth <= maxWidth) { + this._addTextLine(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + break; + } + } + } + else { + // not even one character could fit in the element, abort + break; + } + } + } + else { + // element width is automatically adjusted to max line width + this._addTextLine(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + if (this._shouldHandleEllipsis(currentHeightPx) && i < max - 1) { + this._tryToAddEllipsisToLastLine(); + } + } + // if element height is fixed, abort if adding one more line would overflow + if (this.textArr[this.textArr.length - 1]) { + this.textArr[this.textArr.length - 1].lastInParagraph = true; + } + if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) { + break; + } + } + this.textHeight = fontSize; + // var maxTextWidth = 0; + // for(var j = 0; j < this.textArr.length; j++) { + // maxTextWidth = Math.max(maxTextWidth, this.textArr[j].width); + // } + this.textWidth = textWidth; + } + /** + * whether to handle ellipsis, there are two cases: + * 1. the current line is the last line + * 2. wrap is NONE + * @param {Number} currentHeightPx + * @returns {Boolean} + */ + _shouldHandleEllipsis(currentHeightPx) { + const fontSize = +this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, height = this.attrs.height, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxHeightPx = height - padding * 2, wrap = this.wrap(), shouldWrap = wrap !== NONE; + return (!shouldWrap || + (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx)); + } + _tryToAddEllipsisToLastLine() { + const width = this.attrs.width, fixedWidth = width !== AUTO && width !== undefined, padding = this.padding(), maxWidth = width - padding * 2, shouldAddEllipsis = this.ellipsis(); + const lastLine = this.textArr[this.textArr.length - 1]; + if (!lastLine || !shouldAddEllipsis) { + return; + } + if (fixedWidth) { + const haveSpace = this._getTextWidth(lastLine.text + ELLIPSIS) < maxWidth; + if (!haveSpace) { + lastLine.text = lastLine.text.slice(0, lastLine.text.length - 3); + } + } + this.textArr.splice(this.textArr.length - 1, 1); + this._addTextLine(lastLine.text + ELLIPSIS); + } + // for text we can't disable stroke scaling + // if we do, the result will be unexpected + getStrokeScaleEnabled() { + return true; + } + _useBufferCanvas() { + const hasLine = this.textDecoration().indexOf('underline') !== -1 || + this.textDecoration().indexOf('line-through') !== -1; + const hasShadow = this.hasShadow(); + if (hasLine && hasShadow) { + return true; + } + return super._useBufferCanvas(); + } + } + Text.prototype._fillFunc = _fillFunc$1; + Text.prototype._strokeFunc = _strokeFunc$1; + Text.prototype.className = TEXT_UPPER; + Text.prototype._attrsAffectingSize = [ + 'text', + 'fontSize', + 'padding', + 'wrap', + 'lineHeight', + 'letterSpacing', + ]; + _registerNode(Text); + /** + * get/set width of text area, which includes padding. + * @name Konva.Text#width + * @method + * @param {Number} width + * @returns {Number} + * @example + * // get width + * var width = text.width(); + * + * // set width + * text.width(20); + * + * // set to auto + * text.width('auto'); + * text.width() // will return calculated width, and not "auto" + */ + Factory.overWriteSetter(Text, 'width', getNumberOrAutoValidator()); + /** + * get/set the height of the text area, which takes into account multi-line text, line heights, and padding. + * @name Konva.Text#height + * @method + * @param {Number} height + * @returns {Number} + * @example + * // get height + * var height = text.height(); + * + * // set height + * text.height(20); + * + * // set to auto + * text.height('auto'); + * text.height() // will return calculated height, and not "auto" + */ + Factory.overWriteSetter(Text, 'height', getNumberOrAutoValidator()); + /** + * get/set direction + * @name Konva.Text#direction + * @method + * @param {String} direction + * @returns {String} + * @example + * // get direction + * var direction = text.direction(); + * + * // set direction + * text.direction('rtl'); + */ + Factory.addGetterSetter(Text, 'direction', INHERIT); + /** + * get/set font family + * @name Konva.Text#fontFamily + * @method + * @param {String} fontFamily + * @returns {String} + * @example + * // get font family + * var fontFamily = text.fontFamily(); + * + * // set font family + * text.fontFamily('Arial'); + */ + Factory.addGetterSetter(Text, 'fontFamily', 'Arial'); + /** + * get/set font size in pixels + * @name Konva.Text#fontSize + * @method + * @param {Number} fontSize + * @returns {Number} + * @example + * // get font size + * var fontSize = text.fontSize(); + * + * // set font size to 22px + * text.fontSize(22); + */ + Factory.addGetterSetter(Text, 'fontSize', 12, getNumberValidator()); + /** + * get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @name Konva.Text#fontStyle + * @method + * @param {String} fontStyle + * @returns {String} + * @example + * // get font style + * var fontStyle = text.fontStyle(); + * + * // set font style + * text.fontStyle('bold'); + */ + Factory.addGetterSetter(Text, 'fontStyle', NORMAL$1); + /** + * get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default. + * @name Konva.Text#fontVariant + * @method + * @param {String} fontVariant + * @returns {String} + * @example + * // get font variant + * var fontVariant = text.fontVariant(); + * + * // set font variant + * text.fontVariant('small-caps'); + */ + Factory.addGetterSetter(Text, 'fontVariant', NORMAL$1); + /** + * get/set padding + * @name Konva.Text#padding + * @method + * @param {Number} padding + * @returns {Number} + * @example + * // get padding + * var padding = text.padding(); + * + * // set padding to 10 pixels + * text.padding(10); + */ + Factory.addGetterSetter(Text, 'padding', 0, getNumberValidator()); + /** + * get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify' + * @name Konva.Text#align + * @method + * @param {String} align + * @returns {String} + * @example + * // get text align + * var align = text.align(); + * + * // center text + * text.align('center'); + * + * // align text to right + * text.align('right'); + */ + Factory.addGetterSetter(Text, 'align', LEFT); + /** + * get/set vertical align of text. Can be 'top', 'middle', 'bottom'. + * @name Konva.Text#verticalAlign + * @method + * @param {String} verticalAlign + * @returns {String} + * @example + * // get text vertical align + * var verticalAlign = text.verticalAlign(); + * + * // center text + * text.verticalAlign('middle'); + */ + Factory.addGetterSetter(Text, 'verticalAlign', TOP); + /** + * get/set line height. The default is 1. + * @name Konva.Text#lineHeight + * @method + * @param {Number} lineHeight + * @returns {Number} + * @example + * // get line height + * var lineHeight = text.lineHeight(); + * + * // set the line height + * text.lineHeight(2); + */ + Factory.addGetterSetter(Text, 'lineHeight', 1, getNumberValidator()); + /** + * get/set wrap. Can be "word", "char", or "none". Default is "word". + * In "word" wrapping any word still can be wrapped if it can't be placed in the required width + * without breaks. + * @name Konva.Text#wrap + * @method + * @param {String} wrap + * @returns {String} + * @example + * // get wrap + * var wrap = text.wrap(); + * + * // set wrap + * text.wrap('word'); + */ + Factory.addGetterSetter(Text, 'wrap', WORD); + /** + * get/set ellipsis. Can be true or false. Default is false. If ellipses is true, + * Konva will add "..." at the end of the text if it doesn't have enough space to write characters. + * That is possible only when you limit both width and height of the text + * @name Konva.Text#ellipsis + * @method + * @param {Boolean} ellipsis + * @returns {Boolean} + * @example + * // get ellipsis param, returns true or false + * var ellipsis = text.ellipsis(); + * + * // set ellipsis + * text.ellipsis(true); + */ + Factory.addGetterSetter(Text, 'ellipsis', false, getBooleanValidator()); + /** + * set letter spacing property. Default value is 0. + * @name Konva.Text#letterSpacing + * @method + * @param {Number} letterSpacing + */ + Factory.addGetterSetter(Text, 'letterSpacing', 0, getNumberValidator()); + /** + * get/set text + * @name Konva.Text#text + * @method + * @param {String} text + * @returns {String} + * @example + * // get text + * var text = text.text(); + * + * // set text + * text.text('Hello world!'); + */ + Factory.addGetterSetter(Text, 'text', '', getStringValidator()); + /** + * get/set text decoration of a text. Possible values are 'underline', 'line-through' or combination of these values separated by space + * @name Konva.Text#textDecoration + * @method + * @param {String} textDecoration + * @returns {String} + * @example + * // get text decoration + * var textDecoration = text.textDecoration(); + * + * // underline text + * text.textDecoration('underline'); + * + * // strike text + * text.textDecoration('line-through'); + * + * // underline and strike text + * text.textDecoration('underline line-through'); + */ + Factory.addGetterSetter(Text, 'textDecoration', ''); + + const EMPTY_STRING = '', NORMAL = 'normal'; + function _fillFunc(context) { + context.fillText(this.partialText, 0, 0); + } + function _strokeFunc(context) { + context.strokeText(this.partialText, 0, 0); + } + /** + * Path constructor. + * @author Jason Follas + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} [config.fontFamily] default is Arial + * @param {Number} [config.fontSize] default is 12 + * @param {String} [config.fontStyle] Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @param {String} [config.fontVariant] can be normal or small-caps. Default is normal + * @param {String} [config.textBaseline] Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'. Default is middle + * @param {String} config.text + * @param {String} config.data SVG data string + * @param {Function} config.kerningFunc a getter for kerning values for the specified characters + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var kerningPairs = { + * 'A': { + * ' ': -0.05517578125, + * 'T': -0.07421875, + * 'V': -0.07421875 + * } + * 'V': { + * ',': -0.091796875, + * ":": -0.037109375, + * ";": -0.037109375, + * "A": -0.07421875 + * } + * } + * var textpath = new Konva.TextPath({ + * x: 100, + * y: 50, + * fill: '#333', + * fontSize: '24', + * fontFamily: 'Arial', + * text: 'All the world\'s a stage, and all the men and women merely players.', + * data: 'M10,10 C0,0 10,150 100,100 S300,150 400,50', + * kerningFunc(leftChar, rightChar) { + * return kerningPairs.hasOwnProperty(leftChar) ? pairs[leftChar][rightChar] || 0 : 0 + * } + * }); + */ + class TextPath extends Shape { + constructor(config) { + // call super constructor + super(config); + this.dummyCanvas = Util.createCanvasElement(); + this.dataArray = []; + this._readDataAttribute(); + this.on('dataChange.konva', function () { + this._readDataAttribute(); + this._setTextData(); + }); + // update text data for certain attr changes + this.on('textChange.konva alignChange.konva letterSpacingChange.konva kerningFuncChange.konva fontSizeChange.konva fontFamilyChange.konva', this._setTextData); + this._setTextData(); + } + _getTextPathLength() { + return Path.getPathLength(this.dataArray); + } + _getPointAtLength(length) { + // if path is not defined yet, do nothing + if (!this.attrs.data) { + return null; + } + const totalLength = this.pathLength; + // -1px for rounding of the last symbol + if (length - 1 > totalLength) { + return null; + } + return Path.getPointAtLengthOfDataArray(length, this.dataArray); + } + _readDataAttribute() { + this.dataArray = Path.parsePathData(this.attrs.data); + this.pathLength = this._getTextPathLength(); + } + _sceneFunc(context) { + context.setAttr('font', this._getContextFont()); + context.setAttr('textBaseline', this.textBaseline()); + context.setAttr('textAlign', 'left'); + context.save(); + const textDecoration = this.textDecoration(); + const fill = this.fill(); + const fontSize = this.fontSize(); + const glyphInfo = this.glyphInfo; + if (textDecoration === 'underline') { + context.beginPath(); + } + for (let i = 0; i < glyphInfo.length; i++) { + context.save(); + const p0 = glyphInfo[i].p0; + context.translate(p0.x, p0.y); + context.rotate(glyphInfo[i].rotation); + this.partialText = glyphInfo[i].text; + context.fillStrokeShape(this); + if (textDecoration === 'underline') { + if (i === 0) { + context.moveTo(0, fontSize / 2 + 1); + } + context.lineTo(fontSize, fontSize / 2 + 1); + } + context.restore(); + //// To assist with debugging visually, uncomment following + // + // if (i % 2) context.strokeStyle = 'cyan'; + // else context.strokeStyle = 'green'; + // var p1 = glyphInfo[i].p1; + // context.moveTo(p0.x, p0.y); + // context.lineTo(p1.x, p1.y); + // context.stroke(); + } + if (textDecoration === 'underline') { + context.strokeStyle = fill; + context.lineWidth = fontSize / 20; + context.stroke(); + } + context.restore(); + } + _hitFunc(context) { + context.beginPath(); + const glyphInfo = this.glyphInfo; + if (glyphInfo.length >= 1) { + const p0 = glyphInfo[0].p0; + context.moveTo(p0.x, p0.y); + } + for (let i = 0; i < glyphInfo.length; i++) { + const p1 = glyphInfo[i].p1; + context.lineTo(p1.x, p1.y); + } + context.setAttr('lineWidth', this.fontSize()); + context.setAttr('strokeStyle', this.colorKey); + context.stroke(); + } + /** + * get text width in pixels + * @method + * @name Konva.TextPath#getTextWidth + */ + getTextWidth() { + return this.textWidth; + } + getTextHeight() { + Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.'); + return this.textHeight; + } + setText(text) { + return Text.prototype.setText.call(this, text); + } + _getContextFont() { + return Text.prototype._getContextFont.call(this); + } + _getTextSize(text) { + const dummyCanvas = this.dummyCanvas; + const _context = dummyCanvas.getContext('2d'); + _context.save(); + _context.font = this._getContextFont(); + const metrics = _context.measureText(text); + _context.restore(); + return { + width: metrics.width, + height: parseInt(`${this.fontSize()}`, 10), + }; + } + _setTextData() { + const { width, height } = this._getTextSize(this.attrs.text); + this.textWidth = width; + this.textHeight = height; + this.glyphInfo = []; + if (!this.attrs.data) { + return null; + } + const letterSpacing = this.letterSpacing(); + const align = this.align(); + const kerningFunc = this.kerningFunc(); + // defines the width of the text on a straight line + const textWidth = Math.max(this.textWidth + ((this.attrs.text || '').length - 1) * letterSpacing, 0); + let offset = 0; + if (align === 'center') { + offset = Math.max(0, this.pathLength / 2 - textWidth / 2); + } + if (align === 'right') { + offset = Math.max(0, this.pathLength - textWidth); + } + const charArr = stringToArray(this.text()); + // Algorithm for calculating glyph positions: + // 1. Get the begging point of the glyph on the path using the offsetToGlyph, + // 2. Get the ending point of the glyph on the path using the offsetToGlyph plus glyph width, + // 3. Calculate the rotation, width, and midpoint of the glyph using the start and end points, + // 4. Add glyph width to the offsetToGlyph and repeat + let offsetToGlyph = offset; + for (let i = 0; i < charArr.length; i++) { + const charStartPoint = this._getPointAtLength(offsetToGlyph); + if (!charStartPoint) + return; + let glyphWidth = this._getTextSize(charArr[i]).width + letterSpacing; + if (charArr[i] === ' ' && align === 'justify') { + const numberOfSpaces = this.text().split(' ').length - 1; + glyphWidth += (this.pathLength - textWidth) / numberOfSpaces; + } + const charEndPoint = this._getPointAtLength(offsetToGlyph + glyphWidth); + if (!charEndPoint) + return; + const width = Path.getLineLength(charStartPoint.x, charStartPoint.y, charEndPoint.x, charEndPoint.y); + let kern = 0; + if (kerningFunc) { + try { + // getKerning is a user provided getter. Make sure it never breaks our logic + kern = kerningFunc(charArr[i - 1], charArr[i]) * this.fontSize(); + } + catch (e) { + kern = 0; + } + } + charStartPoint.x += kern; + charEndPoint.x += kern; + this.textWidth += kern; + const midpoint = Path.getPointOnLine(kern + width / 2.0, charStartPoint.x, charStartPoint.y, charEndPoint.x, charEndPoint.y); + const rotation = Math.atan2(charEndPoint.y - charStartPoint.y, charEndPoint.x - charStartPoint.x); + this.glyphInfo.push({ + transposeX: midpoint.x, + transposeY: midpoint.y, + text: charArr[i], + rotation: rotation, + p0: charStartPoint, + p1: charEndPoint, + }); + offsetToGlyph += glyphWidth; + } + } + getSelfRect() { + if (!this.glyphInfo.length) { + return { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + const points = []; + this.glyphInfo.forEach(function (info) { + points.push(info.p0.x); + points.push(info.p0.y); + points.push(info.p1.x); + points.push(info.p1.y); + }); + let minX = points[0] || 0; + let maxX = points[0] || 0; + let minY = points[1] || 0; + let maxY = points[1] || 0; + let x, y; + for (let i = 0; i < points.length / 2; i++) { + x = points[i * 2]; + y = points[i * 2 + 1]; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + const fontSize = this.fontSize(); + return { + x: minX - fontSize / 2, + y: minY - fontSize / 2, + width: maxX - minX + fontSize, + height: maxY - minY + fontSize, + }; + } + destroy() { + Util.releaseCanvas(this.dummyCanvas); + return super.destroy(); + } + } + TextPath.prototype._fillFunc = _fillFunc; + TextPath.prototype._strokeFunc = _strokeFunc; + TextPath.prototype._fillFuncHit = _fillFunc; + TextPath.prototype._strokeFuncHit = _strokeFunc; + TextPath.prototype.className = 'TextPath'; + TextPath.prototype._attrsAffectingSize = ['text', 'fontSize', 'data']; + _registerNode(TextPath); + /** + * get/set SVG path data string. This method + * also automatically parses the data string + * into a data array. Currently supported SVG data: + * M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z + * @name Konva.TextPath#data + * @method + * @param {String} data svg path string + * @returns {String} + * @example + * // get data + * var data = shape.data(); + * + * // set data + * shape.data('M200,100h100v50z'); + */ + Factory.addGetterSetter(TextPath, 'data'); + /** + * get/set font family + * @name Konva.TextPath#fontFamily + * @method + * @param {String} fontFamily + * @returns {String} + * @example + * // get font family + * var fontFamily = shape.fontFamily(); + * + * // set font family + * shape.fontFamily('Arial'); + */ + Factory.addGetterSetter(TextPath, 'fontFamily', 'Arial'); + /** + * get/set font size in pixels + * @name Konva.TextPath#fontSize + * @method + * @param {Number} fontSize + * @returns {Number} + * @example + * // get font size + * var fontSize = shape.fontSize(); + * + * // set font size to 22px + * shape.fontSize(22); + */ + Factory.addGetterSetter(TextPath, 'fontSize', 12, getNumberValidator()); + /** + * get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @name Konva.TextPath#fontStyle + * @method + * @param {String} fontStyle + * @returns {String} + * @example + * // get font style + * var fontStyle = shape.fontStyle(); + * + * // set font style + * shape.fontStyle('bold'); + */ + Factory.addGetterSetter(TextPath, 'fontStyle', NORMAL); + /** + * get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify' + * @name Konva.TextPath#align + * @method + * @param {String} align + * @returns {String} + * @example + * // get text align + * var align = text.align(); + * + * // center text + * text.align('center'); + * + * // align text to right + * text.align('right'); + */ + Factory.addGetterSetter(TextPath, 'align', 'left'); + /** + * get/set letter spacing. The default is 0. + * @name Konva.TextPath#letterSpacing + * @method + * @param {Number} letterSpacing + * @returns {Number} + * @example + * // get letter spacing value + * var letterSpacing = shape.letterSpacing(); + * + * // set the letter spacing value + * shape.letterSpacing(2); + */ + Factory.addGetterSetter(TextPath, 'letterSpacing', 0, getNumberValidator()); + /** + * get/set text baseline. The default is 'middle'. Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging' + * @name Konva.TextPath#textBaseline + * @method + * @param {String} textBaseline + * @returns {String} + * @example + * // get current text baseline + * var textBaseline = shape.textBaseline(); + * + * // set new text baseline + * shape.textBaseline('top'); + */ + Factory.addGetterSetter(TextPath, 'textBaseline', 'middle'); + /** + * get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default. + * @name Konva.TextPath#fontVariant + * @method + * @param {String} fontVariant + * @returns {String} + * @example + * // get font variant + * var fontVariant = shape.fontVariant(); + * + * // set font variant + * shape.fontVariant('small-caps'); + */ + Factory.addGetterSetter(TextPath, 'fontVariant', NORMAL); + /** + * get/set text + * @name Konva.TextPath#getText + * @method + * @param {String} text + * @returns {String} + * @example + * // get text + * var text = text.text(); + * + * // set text + * text.text('Hello world!'); + */ + Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING); + /** + * get/set text decoration of a text. Can be '' or 'underline'. + * @name Konva.TextPath#textDecoration + * @method + * @param {String} textDecoration + * @returns {String} + * @example + * // get text decoration + * var textDecoration = shape.textDecoration(); + * + * // underline text + * shape.textDecoration('underline'); + */ + Factory.addGetterSetter(TextPath, 'textDecoration', ''); + /** + * get/set kerning function. + * @name Konva.TextPath#kerningFunc + * @method + * @param {String} kerningFunc + * @returns {String} + * @example + * // get text decoration + * var kerningFunc = text.kerningFunc(); + * + * // center text + * text.kerningFunc(function(leftChar, rightChar) { + * return 1; + * }); + */ + Factory.addGetterSetter(TextPath, 'kerningFunc', undefined); + + const EVENTS_NAME = 'tr-konva'; + const ATTR_CHANGE_LIST = [ + 'resizeEnabledChange', + 'rotateAnchorOffsetChange', + 'rotateEnabledChange', + 'enabledAnchorsChange', + 'anchorSizeChange', + 'borderEnabledChange', + 'borderStrokeChange', + 'borderStrokeWidthChange', + 'borderDashChange', + 'anchorStrokeChange', + 'anchorStrokeWidthChange', + 'anchorFillChange', + 'anchorCornerRadiusChange', + 'ignoreStrokeChange', + 'anchorStyleFuncChange', + ] + .map((e) => e + `.${EVENTS_NAME}`) + .join(' '); + const NODES_RECT = 'nodesRect'; + const TRANSFORM_CHANGE_STR = [ + 'widthChange', + 'heightChange', + 'scaleXChange', + 'scaleYChange', + 'skewXChange', + 'skewYChange', + 'rotationChange', + 'offsetXChange', + 'offsetYChange', + 'transformsEnabledChange', + 'strokeWidthChange', + ]; + const ANGLES = { + 'top-left': -45, + 'top-center': 0, + 'top-right': 45, + 'middle-right': -90, + 'middle-left': 90, + 'bottom-left': -135, + 'bottom-center': 180, + 'bottom-right': 135, + }; + const TOUCH_DEVICE = 'ontouchstart' in Konva$2._global; + function getCursor(anchorName, rad, rotateCursor) { + if (anchorName === 'rotater') { + return rotateCursor; + } + rad += Util.degToRad(ANGLES[anchorName] || 0); + const angle = ((Util.radToDeg(rad) % 360) + 360) % 360; + if (Util._inRange(angle, 315 + 22.5, 360) || Util._inRange(angle, 0, 22.5)) { + // TOP + return 'ns-resize'; + } + else if (Util._inRange(angle, 45 - 22.5, 45 + 22.5)) { + // TOP - RIGHT + return 'nesw-resize'; + } + else if (Util._inRange(angle, 90 - 22.5, 90 + 22.5)) { + // RIGHT + return 'ew-resize'; + } + else if (Util._inRange(angle, 135 - 22.5, 135 + 22.5)) { + // BOTTOM - RIGHT + return 'nwse-resize'; + } + else if (Util._inRange(angle, 180 - 22.5, 180 + 22.5)) { + // BOTTOM + return 'ns-resize'; + } + else if (Util._inRange(angle, 225 - 22.5, 225 + 22.5)) { + // BOTTOM - LEFT + return 'nesw-resize'; + } + else if (Util._inRange(angle, 270 - 22.5, 270 + 22.5)) { + // RIGHT + return 'ew-resize'; + } + else if (Util._inRange(angle, 315 - 22.5, 315 + 22.5)) { + // BOTTOM - RIGHT + return 'nwse-resize'; + } + else { + // how can we can there? + Util.error('Transformer has unknown angle for cursor detection: ' + angle); + return 'pointer'; + } + } + const ANCHORS_NAMES = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', + ]; + function getCenter(shape) { + return { + x: shape.x + + (shape.width / 2) * Math.cos(shape.rotation) + + (shape.height / 2) * Math.sin(-shape.rotation), + y: shape.y + + (shape.height / 2) * Math.cos(shape.rotation) + + (shape.width / 2) * Math.sin(shape.rotation), + }; + } + function rotateAroundPoint(shape, angleRad, point) { + const x = point.x + + (shape.x - point.x) * Math.cos(angleRad) - + (shape.y - point.y) * Math.sin(angleRad); + const y = point.y + + (shape.x - point.x) * Math.sin(angleRad) + + (shape.y - point.y) * Math.cos(angleRad); + return { + ...shape, + rotation: shape.rotation + angleRad, + x, + y, + }; + } + function rotateAroundCenter(shape, deltaRad) { + const center = getCenter(shape); + return rotateAroundPoint(shape, deltaRad, center); + } + function getSnap(snaps, newRotationRad, tol) { + let snapped = newRotationRad; + for (let i = 0; i < snaps.length; i++) { + const angle = Konva$2.getAngle(snaps[i]); + const absDiff = Math.abs(angle - newRotationRad) % (Math.PI * 2); + const dif = Math.min(absDiff, Math.PI * 2 - absDiff); + if (dif < tol) { + snapped = angle; + } + } + return snapped; + } + let activeTransformersCount = 0; + /** + * Transformer constructor. Transformer is a special type of group that allow you transform Konva + * primitives and shapes. Transforming tool is not changing `width` and `height` properties of nodes + * when you resize them. Instead it changes `scaleX` and `scaleY` properties. + * @constructor + * @memberof Konva + * @param {Object} config + * @param {Boolean} [config.resizeEnabled] Default is true + * @param {Boolean} [config.rotateEnabled] Default is true + * @param {Boolean} [config.rotateLineVisible] Default is true + * @param {Array} [config.rotationSnaps] Array of angles for rotation snaps. Default is [] + * @param {Number} [config.rotationSnapTolerance] Snapping tolerance. If closer than this it will snap. Default is 5 + * @param {Number} [config.rotateAnchorOffset] Default is 50 + * @param {String} [config.rotateAnchorCursor] Default is crosshair + * @param {Number} [config.padding] Default is 0 + * @param {Boolean} [config.borderEnabled] Should we draw border? Default is true + * @param {String} [config.borderStroke] Border stroke color + * @param {Number} [config.borderStrokeWidth] Border stroke size + * @param {Array} [config.borderDash] Array for border dash. + * @param {String} [config.anchorFill] Anchor fill color + * @param {String} [config.anchorStroke] Anchor stroke color + * @param {String} [config.anchorCornerRadius] Anchor corner radius + * @param {Number} [config.anchorStrokeWidth] Anchor stroke size + * @param {Number} [config.anchorSize] Default is 10 + * @param {Boolean} [config.keepRatio] Should we keep ratio when we are moving edges? Default is true + * @param {String} [config.shiftBehavior] How does transformer react on shift key press when we are moving edges? Default is 'default' + * @param {Boolean} [config.centeredScaling] Should we resize relative to node's center? Default is false + * @param {Array} [config.enabledAnchors] Array of names of enabled handles + * @param {Boolean} [config.flipEnabled] Can we flip/mirror shape on transform?. True by default + * @param {Function} [config.boundBoxFunc] Bounding box function + * @param {Function} [config.ignoreStroke] Should we ignore stroke size? Default is false + * @param {Boolean} [config.useSingleNodeRotation] When just one node attached, should we use its rotation for transformer? + * @param {Boolean} [config.shouldOverdrawWholeArea] Should we fill whole transformer area with fake transparent shape to enable dragging from empty spaces? + * @example + * var transformer = new Konva.Transformer({ + * nodes: [rectangle], + * rotateAnchorOffset: 60, + * enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'] + * }); + * layer.add(transformer); + */ + class Transformer extends Group { + constructor(config) { + // call super constructor + super(config); + this._movingAnchorName = null; + this._transforming = false; + this._createElements(); + // bindings + this._handleMouseMove = this._handleMouseMove.bind(this); + this._handleMouseUp = this._handleMouseUp.bind(this); + this.update = this.update.bind(this); + // update transformer data for certain attr changes + this.on(ATTR_CHANGE_LIST, this.update); + if (this.getNode()) { + this.update(); + } + } + /** + * alias to `tr.nodes([shape])`/ This method is deprecated and will be removed soon. + * @method + * @name Konva.Transformer#attachTo + * @returns {Konva.Transformer} + * @example + * transformer.attachTo(shape); + */ + attachTo(node) { + this.setNode(node); + return this; + } + setNode(node) { + Util.warn('tr.setNode(shape), tr.node(shape) and tr.attachTo(shape) methods are deprecated. Please use tr.nodes(nodesArray) instead.'); + return this.setNodes([node]); + } + getNode() { + return this._nodes && this._nodes[0]; + } + _getEventNamespace() { + return EVENTS_NAME + this._id; + } + setNodes(nodes = []) { + if (this._nodes && this._nodes.length) { + this.detach(); + } + const filteredNodes = nodes.filter((node) => { + // check if ancestor of the transformer + if (node.isAncestorOf(this)) { + Util.error('Konva.Transformer cannot be an a child of the node you are trying to attach'); + return false; + } + return true; + }); + this._nodes = nodes = filteredNodes; + if (nodes.length === 1 && this.useSingleNodeRotation()) { + this.rotation(nodes[0].getAbsoluteRotation()); + } + else { + this.rotation(0); + } + this._nodes.forEach((node) => { + const onChange = () => { + if (this.nodes().length === 1 && this.useSingleNodeRotation()) { + this.rotation(this.nodes()[0].getAbsoluteRotation()); + } + this._resetTransformCache(); + if (!this._transforming && !this.isDragging()) { + this.update(); + } + }; + if (node._attrsAffectingSize.length) { + const additionalEvents = node._attrsAffectingSize + .map((prop) => prop + 'Change.' + this._getEventNamespace()) + .join(' '); + node.on(additionalEvents, onChange); + } + node.on(TRANSFORM_CHANGE_STR.map((e) => e + `.${this._getEventNamespace()}`).join(' '), onChange); + node.on(`absoluteTransformChange.${this._getEventNamespace()}`, onChange); + this._proxyDrag(node); + }); + this._resetTransformCache(); + // we may need it if we set node in initial props + // so elements are not defined yet + const elementsCreated = !!this.findOne('.top-left'); + if (elementsCreated) { + this.update(); + } + return this; + } + _proxyDrag(node) { + let lastPos; + node.on(`dragstart.${this._getEventNamespace()}`, (e) => { + lastPos = node.getAbsolutePosition(); + // actual dragging of Transformer doesn't make sense + // but we need to make sure it also has all drag events + if (!this.isDragging() && node !== this.findOne('.back')) { + this.startDrag(e, false); + } + }); + node.on(`dragmove.${this._getEventNamespace()}`, (e) => { + if (!lastPos) { + return; + } + const abs = node.getAbsolutePosition(); + const dx = abs.x - lastPos.x; + const dy = abs.y - lastPos.y; + this.nodes().forEach((otherNode) => { + if (otherNode === node) { + return; + } + if (otherNode.isDragging()) { + return; + } + const otherAbs = otherNode.getAbsolutePosition(); + otherNode.setAbsolutePosition({ + x: otherAbs.x + dx, + y: otherAbs.y + dy, + }); + otherNode.startDrag(e); + }); + lastPos = null; + }); + } + getNodes() { + return this._nodes || []; + } + /** + * return the name of current active anchor + * @method + * @name Konva.Transformer#getActiveAnchor + * @returns {String | Null} + * @example + * transformer.getActiveAnchor(); + */ + getActiveAnchor() { + return this._movingAnchorName; + } + /** + * detach transformer from an attached node + * @method + * @name Konva.Transformer#detach + * @returns {Konva.Transformer} + * @example + * transformer.detach(); + */ + detach() { + // remove events + if (this._nodes) { + this._nodes.forEach((node) => { + node.off('.' + this._getEventNamespace()); + }); + } + this._nodes = []; + this._resetTransformCache(); + } + /** + * bind events to the Transformer. You can use events: `transform`, `transformstart`, `transformend`, `dragstart`, `dragmove`, `dragend` + * @method + * @name Konva.Transformer#on + * @param {String} evtStr e.g. 'transform' + * @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.Transformer} + * @example + * // add click listener + * tr.on('transformstart', function() { + * console.log('transform started'); + * }); + */ + _resetTransformCache() { + this._clearCache(NODES_RECT); + this._clearCache('transform'); + this._clearSelfAndDescendantCache('absoluteTransform'); + } + _getNodeRect() { + return this._getCache(NODES_RECT, this.__getNodeRect); + } + // return absolute rotated bounding rectangle + __getNodeShape(node, rot = this.rotation(), relative) { + const rect = node.getClientRect({ + skipTransform: true, + skipShadow: true, + skipStroke: this.ignoreStroke(), + }); + const absScale = node.getAbsoluteScale(relative); + const absPos = node.getAbsolutePosition(relative); + const dx = rect.x * absScale.x - node.offsetX() * absScale.x; + const dy = rect.y * absScale.y - node.offsetY() * absScale.y; + const rotation = (Konva$2.getAngle(node.getAbsoluteRotation()) + Math.PI * 2) % + (Math.PI * 2); + const box = { + x: absPos.x + dx * Math.cos(rotation) + dy * Math.sin(-rotation), + y: absPos.y + dy * Math.cos(rotation) + dx * Math.sin(rotation), + width: rect.width * absScale.x, + height: rect.height * absScale.y, + rotation: rotation, + }; + return rotateAroundPoint(box, -Konva$2.getAngle(rot), { + x: 0, + y: 0, + }); + } + // returns box + rotation of all shapes + __getNodeRect() { + const node = this.getNode(); + if (!node) { + return { + x: -1e8, + y: -1e8, + width: 0, + height: 0, + rotation: 0, + }; + } + const totalPoints = []; + this.nodes().map((node) => { + const box = node.getClientRect({ + skipTransform: true, + skipShadow: true, + skipStroke: this.ignoreStroke(), + }); + const points = [ + { x: box.x, y: box.y }, + { x: box.x + box.width, y: box.y }, + { x: box.x + box.width, y: box.y + box.height }, + { x: box.x, y: box.y + box.height }, + ]; + const trans = node.getAbsoluteTransform(); + points.forEach(function (point) { + const transformed = trans.point(point); + totalPoints.push(transformed); + }); + }); + const tr = new Transform(); + tr.rotate(-Konva$2.getAngle(this.rotation())); + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + totalPoints.forEach(function (point) { + const transformed = tr.point(point); + if (minX === undefined) { + minX = maxX = transformed.x; + minY = maxY = transformed.y; + } + minX = Math.min(minX, transformed.x); + minY = Math.min(minY, transformed.y); + maxX = Math.max(maxX, transformed.x); + maxY = Math.max(maxY, transformed.y); + }); + tr.invert(); + const p = tr.point({ x: minX, y: minY }); + return { + x: p.x, + y: p.y, + width: maxX - minX, + height: maxY - minY, + rotation: Konva$2.getAngle(this.rotation()), + }; + // const shapes = this.nodes().map(node => { + // return this.__getNodeShape(node); + // }); + // const box = getShapesRect(shapes); + // return rotateAroundPoint(box, Konva.getAngle(this.rotation()), { + // x: 0, + // y: 0 + // }); + } + getX() { + return this._getNodeRect().x; + } + getY() { + return this._getNodeRect().y; + } + getWidth() { + return this._getNodeRect().width; + } + getHeight() { + return this._getNodeRect().height; + } + _createElements() { + this._createBack(); + ANCHORS_NAMES.forEach((name) => { + this._createAnchor(name); + }); + this._createAnchor('rotater'); + } + _createAnchor(name) { + const anchor = new Rect({ + stroke: 'rgb(0, 161, 255)', + fill: 'white', + strokeWidth: 1, + name: name + ' _anchor', + dragDistance: 0, + // make it draggable, + // so activating the anchor will not start drag&drop of any parent + draggable: true, + hitStrokeWidth: TOUCH_DEVICE ? 10 : 'auto', + }); + const self = this; + anchor.on('mousedown touchstart', function (e) { + self._handleMouseDown(e); + }); + anchor.on('dragstart', (e) => { + anchor.stopDrag(); + e.cancelBubble = true; + }); + anchor.on('dragend', (e) => { + e.cancelBubble = true; + }); + // add hover styling + anchor.on('mouseenter', () => { + const rad = Konva$2.getAngle(this.rotation()); + const rotateCursor = this.rotateAnchorCursor(); + const cursor = getCursor(name, rad, rotateCursor); + anchor.getStage().content && + (anchor.getStage().content.style.cursor = cursor); + this._cursorChange = true; + }); + anchor.on('mouseout', () => { + anchor.getStage().content && + (anchor.getStage().content.style.cursor = ''); + this._cursorChange = false; + }); + this.add(anchor); + } + _createBack() { + const back = new Shape({ + name: 'back', + width: 0, + height: 0, + draggable: true, + sceneFunc(ctx, shape) { + const tr = shape.getParent(); + const padding = tr.padding(); + ctx.beginPath(); + ctx.rect(-padding, -padding, shape.width() + padding * 2, shape.height() + padding * 2); + ctx.moveTo(shape.width() / 2, -padding); + if (tr.rotateEnabled() && tr.rotateLineVisible()) { + ctx.lineTo(shape.width() / 2, -tr.rotateAnchorOffset() * Util._sign(shape.height()) - padding); + } + ctx.fillStrokeShape(shape); + }, + hitFunc: (ctx, shape) => { + if (!this.shouldOverdrawWholeArea()) { + return; + } + const padding = this.padding(); + ctx.beginPath(); + ctx.rect(-padding, -padding, shape.width() + padding * 2, shape.height() + padding * 2); + ctx.fillStrokeShape(shape); + }, + }); + this.add(back); + this._proxyDrag(back); + // do not bubble drag from the back shape + // because we already "drag" whole transformer + // so we don't want to trigger drag twice on transformer + back.on('dragstart', (e) => { + e.cancelBubble = true; + }); + back.on('dragmove', (e) => { + e.cancelBubble = true; + }); + back.on('dragend', (e) => { + e.cancelBubble = true; + }); + // force self update when we drag with shouldOverDrawWholeArea setting + this.on('dragmove', (e) => { + this.update(); + }); + } + _handleMouseDown(e) { + // do nothing if we already transforming + // that is possible to trigger with multitouch + if (this._transforming) { + return; + } + this._movingAnchorName = e.target.name().split(' ')[0]; + const attrs = this._getNodeRect(); + const width = attrs.width; + const height = attrs.height; + const hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); + this.sin = Math.abs(height / hypotenuse); + this.cos = Math.abs(width / hypotenuse); + if (typeof window !== 'undefined') { + window.addEventListener('mousemove', this._handleMouseMove); + window.addEventListener('touchmove', this._handleMouseMove); + window.addEventListener('mouseup', this._handleMouseUp, true); + window.addEventListener('touchend', this._handleMouseUp, true); + } + this._transforming = true; + const ap = e.target.getAbsolutePosition(); + const pos = e.target.getStage().getPointerPosition(); + this._anchorDragOffset = { + x: pos.x - ap.x, + y: pos.y - ap.y, + }; + activeTransformersCount++; + this._fire('transformstart', { evt: e.evt, target: this.getNode() }); + this._nodes.forEach((target) => { + target._fire('transformstart', { evt: e.evt, target }); + }); + } + _handleMouseMove(e) { + let x, y, newHypotenuse; + const anchorNode = this.findOne('.' + this._movingAnchorName); + const stage = anchorNode.getStage(); + stage.setPointersPositions(e); + const pp = stage.getPointerPosition(); + let newNodePos = { + x: pp.x - this._anchorDragOffset.x, + y: pp.y - this._anchorDragOffset.y, + }; + const oldAbs = anchorNode.getAbsolutePosition(); + if (this.anchorDragBoundFunc()) { + newNodePos = this.anchorDragBoundFunc()(oldAbs, newNodePos, e); + } + anchorNode.setAbsolutePosition(newNodePos); + const newAbs = anchorNode.getAbsolutePosition(); + // console.log(oldAbs, newNodePos, newAbs); + if (oldAbs.x === newAbs.x && oldAbs.y === newAbs.y) { + return; + } + // rotater is working very differently, so do it first + if (this._movingAnchorName === 'rotater') { + const attrs = this._getNodeRect(); + x = anchorNode.x() - attrs.width / 2; + y = -anchorNode.y() + attrs.height / 2; + // hor angle is changed? + let delta = Math.atan2(-y, x) + Math.PI / 2; + if (attrs.height < 0) { + delta -= Math.PI; + } + const oldRotation = Konva$2.getAngle(this.rotation()); + const newRotation = oldRotation + delta; + const tol = Konva$2.getAngle(this.rotationSnapTolerance()); + const snappedRot = getSnap(this.rotationSnaps(), newRotation, tol); + const diff = snappedRot - attrs.rotation; + const shape = rotateAroundCenter(attrs, diff); + this._fitNodesInto(shape, e); + return; + } + const shiftBehavior = this.shiftBehavior(); + let keepProportion; + if (shiftBehavior === 'inverted') { + keepProportion = this.keepRatio() && !e.shiftKey; + } + else if (shiftBehavior === 'none') { + keepProportion = this.keepRatio(); + } + else { + keepProportion = this.keepRatio() || e.shiftKey; + } + var centeredScaling = this.centeredScaling() || e.altKey; + if (this._movingAnchorName === 'top-left') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.bottom-right').x(), + y: this.findOne('.bottom-right').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(comparePoint.x - anchorNode.x(), 2) + + Math.pow(comparePoint.y - anchorNode.y(), 2)); + var reverseX = this.findOne('.top-left').x() > comparePoint.x ? -1 : 1; + var reverseY = this.findOne('.top-left').y() > comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + this.findOne('.top-left').x(comparePoint.x - x); + this.findOne('.top-left').y(comparePoint.y - y); + } + } + else if (this._movingAnchorName === 'top-center') { + this.findOne('.top-left').y(anchorNode.y()); + } + else if (this._movingAnchorName === 'top-right') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.bottom-left').x(), + y: this.findOne('.bottom-left').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(anchorNode.x() - comparePoint.x, 2) + + Math.pow(comparePoint.y - anchorNode.y(), 2)); + var reverseX = this.findOne('.top-right').x() < comparePoint.x ? -1 : 1; + var reverseY = this.findOne('.top-right').y() > comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + this.findOne('.top-right').x(comparePoint.x + x); + this.findOne('.top-right').y(comparePoint.y - y); + } + var pos = anchorNode.position(); + this.findOne('.top-left').y(pos.y); + this.findOne('.bottom-right').x(pos.x); + } + else if (this._movingAnchorName === 'middle-left') { + this.findOne('.top-left').x(anchorNode.x()); + } + else if (this._movingAnchorName === 'middle-right') { + this.findOne('.bottom-right').x(anchorNode.x()); + } + else if (this._movingAnchorName === 'bottom-left') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.top-right').x(), + y: this.findOne('.top-right').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(comparePoint.x - anchorNode.x(), 2) + + Math.pow(anchorNode.y() - comparePoint.y, 2)); + var reverseX = comparePoint.x < anchorNode.x() ? -1 : 1; + var reverseY = anchorNode.y() < comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + anchorNode.x(comparePoint.x - x); + anchorNode.y(comparePoint.y + y); + } + pos = anchorNode.position(); + this.findOne('.top-left').x(pos.x); + this.findOne('.bottom-right').y(pos.y); + } + else if (this._movingAnchorName === 'bottom-center') { + this.findOne('.bottom-right').y(anchorNode.y()); + } + else if (this._movingAnchorName === 'bottom-right') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.top-left').x(), + y: this.findOne('.top-left').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(anchorNode.x() - comparePoint.x, 2) + + Math.pow(anchorNode.y() - comparePoint.y, 2)); + var reverseX = this.findOne('.bottom-right').x() < comparePoint.x ? -1 : 1; + var reverseY = this.findOne('.bottom-right').y() < comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + this.findOne('.bottom-right').x(comparePoint.x + x); + this.findOne('.bottom-right').y(comparePoint.y + y); + } + } + else { + console.error(new Error('Wrong position argument of selection resizer: ' + + this._movingAnchorName)); + } + var centeredScaling = this.centeredScaling() || e.altKey; + if (centeredScaling) { + const topLeft = this.findOne('.top-left'); + const bottomRight = this.findOne('.bottom-right'); + const topOffsetX = topLeft.x(); + const topOffsetY = topLeft.y(); + const bottomOffsetX = this.getWidth() - bottomRight.x(); + const bottomOffsetY = this.getHeight() - bottomRight.y(); + bottomRight.move({ + x: -topOffsetX, + y: -topOffsetY, + }); + topLeft.move({ + x: bottomOffsetX, + y: bottomOffsetY, + }); + } + const absPos = this.findOne('.top-left').getAbsolutePosition(); + x = absPos.x; + y = absPos.y; + const width = this.findOne('.bottom-right').x() - this.findOne('.top-left').x(); + const height = this.findOne('.bottom-right').y() - this.findOne('.top-left').y(); + this._fitNodesInto({ + x: x, + y: y, + width: width, + height: height, + rotation: Konva$2.getAngle(this.rotation()), + }, e); + } + _handleMouseUp(e) { + this._removeEvents(e); + } + getAbsoluteTransform() { + return this.getTransform(); + } + _removeEvents(e) { + var _a; + if (this._transforming) { + this._transforming = false; + if (typeof window !== 'undefined') { + window.removeEventListener('mousemove', this._handleMouseMove); + window.removeEventListener('touchmove', this._handleMouseMove); + window.removeEventListener('mouseup', this._handleMouseUp, true); + window.removeEventListener('touchend', this._handleMouseUp, true); + } + const node = this.getNode(); + activeTransformersCount--; + this._fire('transformend', { evt: e, target: node }); + // redraw layer to restore hit graph + (_a = this.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + if (node) { + this._nodes.forEach((target) => { + var _a; + target._fire('transformend', { evt: e, target }); + // redraw layer to restore hit graph + (_a = target.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + }); + } + this._movingAnchorName = null; + } + } + _fitNodesInto(newAttrs, evt) { + const oldAttrs = this._getNodeRect(); + const minSize = 1; + if (Util._inRange(newAttrs.width, -this.padding() * 2 - minSize, minSize)) { + this.update(); + return; + } + if (Util._inRange(newAttrs.height, -this.padding() * 2 - minSize, minSize)) { + this.update(); + return; + } + const t = new Transform(); + t.rotate(Konva$2.getAngle(this.rotation())); + if (this._movingAnchorName && + newAttrs.width < 0 && + this._movingAnchorName.indexOf('left') >= 0) { + const offset = t.point({ + x: -this.padding() * 2, + y: 0, + }); + newAttrs.x += offset.x; + newAttrs.y += offset.y; + newAttrs.width += this.padding() * 2; + this._movingAnchorName = this._movingAnchorName.replace('left', 'right'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + } + else if (this._movingAnchorName && + newAttrs.width < 0 && + this._movingAnchorName.indexOf('right') >= 0) { + const offset = t.point({ + x: this.padding() * 2, + y: 0, + }); + this._movingAnchorName = this._movingAnchorName.replace('right', 'left'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + newAttrs.width += this.padding() * 2; + } + if (this._movingAnchorName && + newAttrs.height < 0 && + this._movingAnchorName.indexOf('top') >= 0) { + const offset = t.point({ + x: 0, + y: -this.padding() * 2, + }); + newAttrs.x += offset.x; + newAttrs.y += offset.y; + this._movingAnchorName = this._movingAnchorName.replace('top', 'bottom'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + newAttrs.height += this.padding() * 2; + } + else if (this._movingAnchorName && + newAttrs.height < 0 && + this._movingAnchorName.indexOf('bottom') >= 0) { + const offset = t.point({ + x: 0, + y: this.padding() * 2, + }); + this._movingAnchorName = this._movingAnchorName.replace('bottom', 'top'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + newAttrs.height += this.padding() * 2; + } + if (this.boundBoxFunc()) { + const bounded = this.boundBoxFunc()(oldAttrs, newAttrs); + if (bounded) { + newAttrs = bounded; + } + else { + Util.warn('boundBoxFunc returned falsy. You should return new bound rect from it!'); + } + } + // base size value doesn't really matter + // we just need to think about bounding boxes as transforms + // but how? + // the idea is that we have a transformed rectangle with the size of "baseSize" + const baseSize = 10000000; + const oldTr = new Transform(); + oldTr.translate(oldAttrs.x, oldAttrs.y); + oldTr.rotate(oldAttrs.rotation); + oldTr.scale(oldAttrs.width / baseSize, oldAttrs.height / baseSize); + const newTr = new Transform(); + const newScaleX = newAttrs.width / baseSize; + const newScaleY = newAttrs.height / baseSize; + if (this.flipEnabled() === false) { + newTr.translate(newAttrs.x, newAttrs.y); + newTr.rotate(newAttrs.rotation); + newTr.translate(newAttrs.width < 0 ? newAttrs.width : 0, newAttrs.height < 0 ? newAttrs.height : 0); + newTr.scale(Math.abs(newScaleX), Math.abs(newScaleY)); + } + else { + newTr.translate(newAttrs.x, newAttrs.y); + newTr.rotate(newAttrs.rotation); + newTr.scale(newScaleX, newScaleY); + } + // now lets think we had [old transform] and n ow we have [new transform] + // Now, the questions is: how can we transform "parent" to go from [old transform] into [new transform] + // in equation it will be: + // [delta transform] * [old transform] = [new transform] + // that means that + // [delta transform] = [new transform] * [old transform inverted] + const delta = newTr.multiply(oldTr.invert()); + this._nodes.forEach((node) => { + var _a; + // for each node we have the same [delta transform] + // the equations is + // [delta transform] * [parent transform] * [old local transform] = [parent transform] * [new local transform] + // and we need to find [new local transform] + // [new local] = [parent inverted] * [delta] * [parent] * [old local] + const parentTransform = node.getParent().getAbsoluteTransform(); + const localTransform = node.getTransform().copy(); + // skip offset: + localTransform.translate(node.offsetX(), node.offsetY()); + const newLocalTransform = new Transform(); + newLocalTransform + .multiply(parentTransform.copy().invert()) + .multiply(delta) + .multiply(parentTransform) + .multiply(localTransform); + const attrs = newLocalTransform.decompose(); + node.setAttrs(attrs); + (_a = node.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + }); + this.rotation(Util._getRotation(newAttrs.rotation)); + // trigger transform event AFTER we update rotation + this._nodes.forEach((node) => { + this._fire('transform', { evt: evt, target: node }); + node._fire('transform', { evt: evt, target: node }); + }); + this._resetTransformCache(); + this.update(); + this.getLayer().batchDraw(); + } + /** + * force update of Konva.Transformer. + * Use it when you updated attached Konva.Group and now you need to reset transformer size + * @method + * @name Konva.Transformer#forceUpdate + */ + forceUpdate() { + this._resetTransformCache(); + this.update(); + } + _batchChangeChild(selector, attrs) { + const anchor = this.findOne(selector); + anchor.setAttrs(attrs); + } + update() { + var _a; + const attrs = this._getNodeRect(); + this.rotation(Util._getRotation(attrs.rotation)); + const width = attrs.width; + const height = attrs.height; + const enabledAnchors = this.enabledAnchors(); + const resizeEnabled = this.resizeEnabled(); + const padding = this.padding(); + const anchorSize = this.anchorSize(); + const anchors = this.find('._anchor'); + anchors.forEach((node) => { + node.setAttrs({ + width: anchorSize, + height: anchorSize, + offsetX: anchorSize / 2, + offsetY: anchorSize / 2, + stroke: this.anchorStroke(), + strokeWidth: this.anchorStrokeWidth(), + fill: this.anchorFill(), + cornerRadius: this.anchorCornerRadius(), + }); + }); + this._batchChangeChild('.top-left', { + x: 0, + y: 0, + offsetX: anchorSize / 2 + padding, + offsetY: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('top-left') >= 0, + }); + this._batchChangeChild('.top-center', { + x: width / 2, + y: 0, + offsetY: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('top-center') >= 0, + }); + this._batchChangeChild('.top-right', { + x: width, + y: 0, + offsetX: anchorSize / 2 - padding, + offsetY: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('top-right') >= 0, + }); + this._batchChangeChild('.middle-left', { + x: 0, + y: height / 2, + offsetX: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('middle-left') >= 0, + }); + this._batchChangeChild('.middle-right', { + x: width, + y: height / 2, + offsetX: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('middle-right') >= 0, + }); + this._batchChangeChild('.bottom-left', { + x: 0, + y: height, + offsetX: anchorSize / 2 + padding, + offsetY: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('bottom-left') >= 0, + }); + this._batchChangeChild('.bottom-center', { + x: width / 2, + y: height, + offsetY: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('bottom-center') >= 0, + }); + this._batchChangeChild('.bottom-right', { + x: width, + y: height, + offsetX: anchorSize / 2 - padding, + offsetY: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('bottom-right') >= 0, + }); + this._batchChangeChild('.rotater', { + x: width / 2, + y: -this.rotateAnchorOffset() * Util._sign(height) - padding, + visible: this.rotateEnabled(), + }); + this._batchChangeChild('.back', { + width: width, + height: height, + visible: this.borderEnabled(), + stroke: this.borderStroke(), + strokeWidth: this.borderStrokeWidth(), + dash: this.borderDash(), + x: 0, + y: 0, + }); + const styleFunc = this.anchorStyleFunc(); + if (styleFunc) { + anchors.forEach((node) => { + styleFunc(node); + }); + } + (_a = this.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + } + /** + * determine if transformer is in active transform + * @method + * @name Konva.Transformer#isTransforming + * @returns {Boolean} + */ + isTransforming() { + return this._transforming; + } + /** + * Stop active transform action + * @method + * @name Konva.Transformer#stopTransform + * @returns {Boolean} + */ + stopTransform() { + if (this._transforming) { + this._removeEvents(); + const anchorNode = this.findOne('.' + this._movingAnchorName); + if (anchorNode) { + anchorNode.stopDrag(); + } + } + } + destroy() { + if (this.getStage() && this._cursorChange) { + this.getStage().content && (this.getStage().content.style.cursor = ''); + } + Group.prototype.destroy.call(this); + this.detach(); + this._removeEvents(); + return this; + } + // do not work as a container + // we will recreate inner nodes manually + toObject() { + return Node.prototype.toObject.call(this); + } + // overwrite clone to NOT use method from Container + clone(obj) { + const node = Node.prototype.clone.call(this, obj); + return node; + } + getClientRect() { + if (this.nodes().length > 0) { + return super.getClientRect(); + } + else { + // if we are detached return zero size + // so it will be skipped in calculations + return { x: 0, y: 0, width: 0, height: 0 }; + } + } + } + Transformer.isTransforming = () => { + return activeTransformersCount > 0; + }; + function validateAnchors(val) { + if (!(val instanceof Array)) { + Util.warn('enabledAnchors value should be an array'); + } + if (val instanceof Array) { + val.forEach(function (name) { + if (ANCHORS_NAMES.indexOf(name) === -1) { + Util.warn('Unknown anchor name: ' + + name + + '. Available names are: ' + + ANCHORS_NAMES.join(', ')); + } + }); + } + return val || []; + } + Transformer.prototype.className = 'Transformer'; + _registerNode(Transformer); + /** + * get/set enabled handlers + * @name Konva.Transformer#enabledAnchors + * @method + * @param {Array} array + * @returns {Array} + * @example + * // get list of handlers + * var enabledAnchors = transformer.enabledAnchors(); + * + * // set handlers + * transformer.enabledAnchors(['top-left', 'top-center', 'top-right', 'middle-right', 'middle-left', 'bottom-left', 'bottom-center', 'bottom-right']); + */ + Factory.addGetterSetter(Transformer, 'enabledAnchors', ANCHORS_NAMES, validateAnchors); + /** + * get/set flip enabled + * @name Konva.Transformer#flipEnabled + * @method + * @param {Boolean} flag + * @returns {Boolean} + * @example + * // get flip enabled property + * var flipEnabled = transformer.flipEnabled(); + * + * // set flip enabled property + * transformer.flipEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'flipEnabled', true, getBooleanValidator()); + /** + * get/set resize ability. If false it will automatically hide resizing handlers + * @name Konva.Transformer#resizeEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var resizeEnabled = transformer.resizeEnabled(); + * + * // set + * transformer.resizeEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'resizeEnabled', true); + /** + * get/set anchor size. Default is 10 + * @name Konva.Transformer#anchorSize + * @method + * @param {Number} size + * @returns {Number} + * @example + * // get + * var anchorSize = transformer.anchorSize(); + * + * // set + * transformer.anchorSize(20) + */ + Factory.addGetterSetter(Transformer, 'anchorSize', 10, getNumberValidator()); + /** + * get/set ability to rotate. + * @name Konva.Transformer#rotateEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var rotateEnabled = transformer.rotateEnabled(); + * + * // set + * transformer.rotateEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'rotateEnabled', true); + /** + * get/set visibility of a little line that connects transformer and rotate anchor. + * @name Konva.Transformer#rotateLineVisible + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var rotateLineVisible = transformer.rotateLineVisible(); + * + * // set + * transformer.rotateLineVisible(false); + */ + Factory.addGetterSetter(Transformer, 'rotateLineVisible', true); + /** + * get/set rotation snaps angles. + * @name Konva.Transformer#rotationSnaps + * @method + * @param {Array} array + * @returns {Array} + * @example + * // get + * var rotationSnaps = transformer.rotationSnaps(); + * + * // set + * transformer.rotationSnaps([0, 90, 180, 270]); + */ + Factory.addGetterSetter(Transformer, 'rotationSnaps', []); + /** + * get/set distance for rotation handler + * @name Konva.Transformer#rotateAnchorOffset + * @method + * @param {Number} offset + * @returns {Number} + * @example + * // get + * var rotateAnchorOffset = transformer.rotateAnchorOffset(); + * + * // set + * transformer.rotateAnchorOffset(100); + */ + Factory.addGetterSetter(Transformer, 'rotateAnchorOffset', 50, getNumberValidator()); + /** + * get/set rotation anchor cursor + * @name Konva.Transformer#rotateAnchorCursor + * @method + * @param {String} cursorName + * @returns {String} + * @example + * // get + * var currentRotationAnchorCursor = transformer.rotateAnchorCursor(); + * + * // set + * transformer.rotateAnchorCursor('grab'); + */ + Factory.addGetterSetter(Transformer, 'rotateAnchorCursor', 'crosshair'); + /** + * get/set distance for rotation tolerance + * @name Konva.Transformer#rotationSnapTolerance + * @method + * @param {Number} tolerance + * @returns {Number} + * @example + * // get + * var rotationSnapTolerance = transformer.rotationSnapTolerance(); + * + * // set + * transformer.rotationSnapTolerance(100); + */ + Factory.addGetterSetter(Transformer, 'rotationSnapTolerance', 5, getNumberValidator()); + /** + * get/set visibility of border + * @name Konva.Transformer#borderEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var borderEnabled = transformer.borderEnabled(); + * + * // set + * transformer.borderEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'borderEnabled', true); + /** + * get/set anchor stroke color + * @name Konva.Transformer#anchorStroke + * @method + * @param {String} strokeColor + * @returns {String} + * @example + * // get + * var anchorStroke = transformer.anchorStroke(); + * + * // set + * transformer.anchorStroke('red'); + */ + Factory.addGetterSetter(Transformer, 'anchorStroke', 'rgb(0, 161, 255)'); + /** + * get/set anchor stroke width + * @name Konva.Transformer#anchorStrokeWidth + * @method + * @param {Number} anchorStrokeWidth + * @returns {Number} + * @example + * // get + * var anchorStrokeWidth = transformer.anchorStrokeWidth(); + * + * // set + * transformer.anchorStrokeWidth(3); + */ + Factory.addGetterSetter(Transformer, 'anchorStrokeWidth', 1, getNumberValidator()); + /** + * get/set anchor fill color + * @name Konva.Transformer#anchorFill + * @method + * @param {String} anchorFill + * @returns {String} + * @example + * // get + * var anchorFill = transformer.anchorFill(); + * + * // set + * transformer.anchorFill('red'); + */ + Factory.addGetterSetter(Transformer, 'anchorFill', 'white'); + /** + * get/set anchor corner radius + * @name Konva.Transformer#anchorCornerRadius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get + * var anchorCornerRadius = transformer.anchorCornerRadius(); + * + * // set + * transformer.anchorCornerRadius(3); + */ + Factory.addGetterSetter(Transformer, 'anchorCornerRadius', 0, getNumberValidator()); + /** + * get/set border stroke color + * @name Konva.Transformer#borderStroke + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var borderStroke = transformer.borderStroke(); + * + * // set + * transformer.borderStroke('red'); + */ + Factory.addGetterSetter(Transformer, 'borderStroke', 'rgb(0, 161, 255)'); + /** + * get/set border stroke width + * @name Konva.Transformer#borderStrokeWidth + * @method + * @param {Number} strokeWidth + * @returns {Number} + * @example + * // get + * var borderStrokeWidth = transformer.borderStrokeWidth(); + * + * // set + * transformer.borderStrokeWidth(3); + */ + Factory.addGetterSetter(Transformer, 'borderStrokeWidth', 1, getNumberValidator()); + /** + * get/set border dash array + * @name Konva.Transformer#borderDash + * @method + * @param {Array} dash array + * @returns {Array} + * @example + * // get + * var borderDash = transformer.borderDash(); + * + * // set + * transformer.borderDash([2, 2]); + */ + Factory.addGetterSetter(Transformer, 'borderDash'); + /** + * get/set should we keep ratio while resize anchors at corners + * @name Konva.Transformer#keepRatio + * @method + * @param {Boolean} keepRatio + * @returns {Boolean} + * @example + * // get + * var keepRatio = transformer.keepRatio(); + * + * // set + * transformer.keepRatio(false); + */ + Factory.addGetterSetter(Transformer, 'keepRatio', true); + /** + * get/set how to react on skift key while resizing anchors at corners + * @name Konva.Transformer#shiftBehavior + * @method + * @param {String} shiftBehavior + * @returns {String} + * @example + * // get + * var shiftBehavior = transformer.shiftBehavior(); + * + * // set + * transformer.shiftBehavior('none'); + */ + Factory.addGetterSetter(Transformer, 'shiftBehavior', 'default'); + /** + * get/set should we resize relative to node's center? + * @name Konva.Transformer#centeredScaling + * @method + * @param {Boolean} centeredScaling + * @returns {Boolean} + * @example + * // get + * var centeredScaling = transformer.centeredScaling(); + * + * // set + * transformer.centeredScaling(true); + */ + Factory.addGetterSetter(Transformer, 'centeredScaling', false); + /** + * get/set should we think about stroke while resize? Good to use when a shape has strokeScaleEnabled = false + * default is false + * @name Konva.Transformer#ignoreStroke + * @method + * @param {Boolean} ignoreStroke + * @returns {Boolean} + * @example + * // get + * var ignoreStroke = transformer.ignoreStroke(); + * + * // set + * transformer.ignoreStroke(true); + */ + Factory.addGetterSetter(Transformer, 'ignoreStroke', false); + /** + * get/set padding + * @name Konva.Transformer#padding + * @method + * @param {Number} padding + * @returns {Number} + * @example + * // get + * var padding = transformer.padding(); + * + * // set + * transformer.padding(10); + */ + Factory.addGetterSetter(Transformer, 'padding', 0, getNumberValidator()); + /** + * get/set attached nodes of the Transformer. Transformer will adapt to their size and listen to their events + * @method + * @name Konva.Transformer#nodes + * @returns {Konva.Node} + * @example + * // get + * const nodes = transformer.nodes(); + * + * // set + * transformer.nodes([rect, circle]); + * + * // push new item: + * const oldNodes = transformer.nodes(); + * const newNodes = oldNodes.concat([newShape]); + * // it is important to set new array instance (and concat method above will create it) + * transformer.nodes(newNodes); + */ + Factory.addGetterSetter(Transformer, 'nodes'); + // @ts-ignore + // deprecated + Factory.addGetterSetter(Transformer, 'node'); + /** + * get/set bounding box function. **IMPORTANT!** boundBondFunc operates in absolute coordinates. + * @name Konva.Transformer#boundBoxFunc + * @method + * @param {Function} func + * @returns {Function} + * @example + * // get + * var boundBoxFunc = transformer.boundBoxFunc(); + * + * // set + * transformer.boundBoxFunc(function(oldBox, newBox) { + * // width and height of the boxes are corresponding to total absolute width and height of all nodes combined + * // so it includes scale of the node. + * if (newBox.width > 200) { + * return oldBox; + * } + * return newBox; + * }); + */ + Factory.addGetterSetter(Transformer, 'boundBoxFunc'); + /** + * get/set dragging func for transformer anchors + * @name Konva.Transformer#anchorDragBoundFunc + * @method + * @param {Function} func + * @returns {Function} + * @example + * // get + * var anchorDragBoundFunc = transformer.anchorDragBoundFunc(); + * + * // set + * transformer.anchorDragBoundFunc(function(oldAbsPos, newAbsPos, event) { + * return { + * x: 0, + * y: newAbsolutePosition.y + * } + * }); + */ + Factory.addGetterSetter(Transformer, 'anchorDragBoundFunc'); + /** + * get/set styling function for transformer anchors to overwrite default styles + * @name Konva.Transformer#anchorStyleFunc + * @method + * @param {Function} func + * @returns {Function} + * @example + * // get + * var anchorStyleFunc = transformer.anchorStyleFunc(); + * + * // set + * transformer.anchorStyleFunc(function(anchor) { + * // anchor is a simple Konva.Rect instance + * // it will be executed AFTER all attributes are set, like 'anchorStrokeWidth' or 'anchorFill' + * if (anchor.hasName('.rotater')) { + * // make rotater anchor filled black and looks like a circle + * anchor.fill('black'); + * anchor.cornerRadius(anchor.width() / 2); + * } + * }); + */ + Factory.addGetterSetter(Transformer, 'anchorStyleFunc'); + /** + * using this setting you can drag transformer group by dragging empty space between attached nodes + * shouldOverdrawWholeArea = true may temporary disable all events on attached nodes + * @name Konva.Transformer#shouldOverdrawWholeArea + * @method + * @param {Boolean} shouldOverdrawWholeArea + * @returns {Boolean} + * @example + * // get + * var shouldOverdrawWholeArea = transformer.shouldOverdrawWholeArea(); + * + * // set + * transformer.shouldOverdrawWholeArea(true); + */ + Factory.addGetterSetter(Transformer, 'shouldOverdrawWholeArea', false); + /** + * If you have just one attached node to Transformer it will set its initial rotation to the rotation of that node. + * In some cases you may need to set a different rotation. + * @name Konva.Transformer#useSingleNodeRotation + * @method + * @param {Boolean} useSingleNodeRotation + * @returns {Boolean} + * @example + * // set flag to false + * transformer.useSingleNodeRotation(false); + * // attach a shape + * transformer.nodes([shape]); + * transformer.rotation(45); + * transformer.update(); + */ + Factory.addGetterSetter(Transformer, 'useSingleNodeRotation', true); + Factory.backCompat(Transformer, { + lineEnabled: 'borderEnabled', + rotateHandlerOffset: 'rotateAnchorOffset', + enabledHandlers: 'enabledAnchors', + }); + + /** + * Wedge constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.angle in degrees + * @param {Number} config.radius + * @param {Boolean} [config.clockwise] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // draw a wedge that's pointing downwards + * var wedge = new Konva.Wedge({ + * radius: 40, + * fill: 'red', + * stroke: 'black' + * strokeWidth: 5, + * angleDeg: 60, + * rotationDeg: -120 + * }); + */ + class Wedge extends Shape { + _sceneFunc(context) { + context.beginPath(); + context.arc(0, 0, this.radius(), 0, Konva$2.getAngle(this.angle()), this.clockwise()); + context.lineTo(0, 0); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.radius() * 2; + } + getHeight() { + return this.radius() * 2; + } + setWidth(width) { + this.radius(width / 2); + } + setHeight(height) { + this.radius(height / 2); + } + } + Wedge.prototype.className = 'Wedge'; + Wedge.prototype._centroid = true; + Wedge.prototype._attrsAffectingSize = ['radius']; + _registerNode(Wedge); + /** + * get/set radius + * @name Konva.Wedge#radius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radius + * var radius = wedge.radius(); + * + * // set radius + * wedge.radius(10); + */ + Factory.addGetterSetter(Wedge, 'radius', 0, getNumberValidator()); + /** + * get/set angle in degrees + * @name Konva.Wedge#angle + * @method + * @param {Number} angle + * @returns {Number} + * @example + * // get angle + * var angle = wedge.angle(); + * + * // set angle + * wedge.angle(20); + */ + Factory.addGetterSetter(Wedge, 'angle', 0, getNumberValidator()); + /** + * get/set clockwise flag + * @name Konva.Wedge#clockwise + * @method + * @param {Number} clockwise + * @returns {Number} + * @example + * // get clockwise flag + * var clockwise = wedge.clockwise(); + * + * // draw wedge counter-clockwise + * wedge.clockwise(false); + * + * // draw wedge clockwise + * wedge.clockwise(true); + */ + Factory.addGetterSetter(Wedge, 'clockwise', false); + Factory.backCompat(Wedge, { + angleDeg: 'angle', + getAngleDeg: 'getAngle', + setAngleDeg: 'setAngle', + }); + + /* + the Gauss filter + master repo: https://github.com/pavelpower/kineticjsGaussFilter + */ + /* + + StackBlur - a fast almost Gaussian Blur For Canvas + + Version: 0.5 + Author: Mario Klingemann + Contact: mario@quasimondo.com + Website: http://www.quasimondo.com/StackBlurForCanvas + Twitter: @quasimondo + + In case you find this class useful - especially in commercial projects - + I am not totally unhappy for a small donation to my PayPal account + mario@quasimondo.de + + Or support me on flattr: + https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript + + Copyright (c) 2010 Mario Klingemann + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + */ + function BlurStack() { + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; + } + const mul_table = [ + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, + 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, + 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, + 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, + 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, + 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, + 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, + 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, + 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, + 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, + 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, + 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, + 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, + 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, + 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, + 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259, + ]; + const shg_table = [ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, + 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, + ]; + function filterGaussBlurRGBA(imageData, radius) { + const pixels = imageData.data, width = imageData.width, height = imageData.height; + let x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, r_out_sum, g_out_sum, b_out_sum, a_out_sum, r_in_sum, g_in_sum, b_in_sum, a_in_sum, pr, pg, pb, pa, rbs; + const div = radius + radius + 1, widthMinus1 = width - 1, heightMinus1 = height - 1, radiusPlus1 = radius + 1, sumFactor = (radiusPlus1 * (radiusPlus1 + 1)) / 2, stackStart = new BlurStack(), mul_sum = mul_table[radius], shg_sum = shg_table[radius]; + let stackEnd = null, stack = stackStart, stackIn = null, stackOut = null; + for (i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + yw = yi = 0; + for (y = 0; y < height; y++) { + r_in_sum = + g_in_sum = + b_in_sum = + a_in_sum = + r_sum = + g_sum = + b_sum = + a_sum = + 0; + r_out_sum = radiusPlus1 * (pr = pixels[yi]); + g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); + b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); + a_out_sum = radiusPlus1 * (pa = pixels[yi + 3]); + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + a_sum += sumFactor * pa; + stack = stackStart; + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + for (i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + r_sum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); + g_sum += (stack.g = pg = pixels[p + 1]) * rbs; + b_sum += (stack.b = pb = pixels[p + 2]) * rbs; + a_sum += (stack.a = pa = pixels[p + 3]) * rbs; + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + a_in_sum += pa; + stack = stack.next; + } + stackIn = stackStart; + stackOut = stackEnd; + for (x = 0; x < width; x++) { + pixels[yi + 3] = pa = (a_sum * mul_sum) >> shg_sum; + if (pa !== 0) { + pa = 255 / pa; + pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa; + pixels[yi + 1] = ((g_sum * mul_sum) >> shg_sum) * pa; + pixels[yi + 2] = ((b_sum * mul_sum) >> shg_sum) * pa; + } + else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + a_sum -= a_out_sum; + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + a_out_sum -= stackIn.a; + p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2; + r_in_sum += stackIn.r = pixels[p]; + g_in_sum += stackIn.g = pixels[p + 1]; + b_in_sum += stackIn.b = pixels[p + 2]; + a_in_sum += stackIn.a = pixels[p + 3]; + r_sum += r_in_sum; + g_sum += g_in_sum; + b_sum += b_in_sum; + a_sum += a_in_sum; + stackIn = stackIn.next; + r_out_sum += pr = stackOut.r; + g_out_sum += pg = stackOut.g; + b_out_sum += pb = stackOut.b; + a_out_sum += pa = stackOut.a; + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + a_in_sum -= pa; + stackOut = stackOut.next; + yi += 4; + } + yw += width; + } + for (x = 0; x < width; x++) { + g_in_sum = + b_in_sum = + a_in_sum = + r_in_sum = + g_sum = + b_sum = + a_sum = + r_sum = + 0; + yi = x << 2; + r_out_sum = radiusPlus1 * (pr = pixels[yi]); + g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); + b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); + a_out_sum = radiusPlus1 * (pa = pixels[yi + 3]); + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + a_sum += sumFactor * pa; + stack = stackStart; + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + yp = width; + for (i = 1; i <= radius; i++) { + yi = (yp + x) << 2; + r_sum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); + g_sum += (stack.g = pg = pixels[yi + 1]) * rbs; + b_sum += (stack.b = pb = pixels[yi + 2]) * rbs; + a_sum += (stack.a = pa = pixels[yi + 3]) * rbs; + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + a_in_sum += pa; + stack = stack.next; + if (i < heightMinus1) { + yp += width; + } + } + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (y = 0; y < height; y++) { + p = yi << 2; + pixels[p + 3] = pa = (a_sum * mul_sum) >> shg_sum; + if (pa > 0) { + pa = 255 / pa; + pixels[p] = ((r_sum * mul_sum) >> shg_sum) * pa; + pixels[p + 1] = ((g_sum * mul_sum) >> shg_sum) * pa; + pixels[p + 2] = ((b_sum * mul_sum) >> shg_sum) * pa; + } + else { + pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; + } + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + a_sum -= a_out_sum; + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + a_out_sum -= stackIn.a; + p = + (x + + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width) << + 2; + r_sum += r_in_sum += stackIn.r = pixels[p]; + g_sum += g_in_sum += stackIn.g = pixels[p + 1]; + b_sum += b_in_sum += stackIn.b = pixels[p + 2]; + a_sum += a_in_sum += stackIn.a = pixels[p + 3]; + stackIn = stackIn.next; + r_out_sum += pr = stackOut.r; + g_out_sum += pg = stackOut.g; + b_out_sum += pb = stackOut.b; + a_out_sum += pa = stackOut.a; + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + a_in_sum -= pa; + stackOut = stackOut.next; + yi += width; + } + } + } + /** + * Blur Filter + * @function + * @name Blur + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Blur]); + * node.blurRadius(10); + */ + const Blur = function Blur(imageData) { + const radius = Math.round(this.blurRadius()); + if (radius > 0) { + filterGaussBlurRGBA(imageData, radius); + } + }; + Factory.addGetterSetter(Node, 'blurRadius', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set blur radius. Use with {@link Konva.Filters.Blur} filter + * @name Konva.Node#blurRadius + * @method + * @param {Integer} radius + * @returns {Integer} + */ + + /** + * Brighten Filter. + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Brighten]); + * node.brightness(0.8); + */ + const Brighten = function (imageData) { + const brightness = this.brightness() * 255, data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 4) { + // red + data[i] += brightness; + // green + data[i + 1] += brightness; + // blue + data[i + 2] += brightness; + } + }; + Factory.addGetterSetter(Node, 'brightness', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set filter brightness. The brightness is a number between -1 and 1.  Positive values + * brighten the pixels and negative values darken them. Use with {@link Konva.Filters.Brighten} filter. + * @name Konva.Node#brightness + * @method + + * @param {Number} brightness value between -1 and 1 + * @returns {Number} + */ + + /** + * Contrast Filter. + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Contrast]); + * node.contrast(10); + */ + const Contrast = function (imageData) { + const adjust = Math.pow((this.contrast() + 100) / 100, 2); + const data = imageData.data, nPixels = data.length; + let red = 150, green = 150, blue = 150; + for (let i = 0; i < nPixels; i += 4) { + red = data[i]; + green = data[i + 1]; + blue = data[i + 2]; + //Red channel + red /= 255; + red -= 0.5; + red *= adjust; + red += 0.5; + red *= 255; + //Green channel + green /= 255; + green -= 0.5; + green *= adjust; + green += 0.5; + green *= 255; + //Blue channel + blue /= 255; + blue -= 0.5; + blue *= adjust; + blue += 0.5; + blue *= 255; + red = red < 0 ? 0 : red > 255 ? 255 : red; + green = green < 0 ? 0 : green > 255 ? 255 : green; + blue = blue < 0 ? 0 : blue > 255 ? 255 : blue; + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + } + }; + /** + * get/set filter contrast. The contrast is a number between -100 and 100. + * Use with {@link Konva.Filters.Contrast} filter. + * @name Konva.Node#contrast + * @method + * @param {Number} contrast value between -100 and 100 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'contrast', 0, getNumberValidator(), Factory.afterSetFilter); + + /** + * Emboss Filter. + * Pixastic Lib - Emboss filter - v0.1.0 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * License: [http://www.pixastic.com/lib/license.txt] + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Emboss]); + * node.embossStrength(0.8); + * node.embossWhiteLevel(0.3); + * node.embossDirection('right'); + * node.embossBlend(true); + */ + const Emboss = function (imageData) { + // pixastic strength is between 0 and 10. I want it between 0 and 1 + // pixastic greyLevel is between 0 and 255. I want it between 0 and 1. Also, + // a max value of greyLevel yields a white emboss, and the min value yields a black + // emboss. Therefore, I changed greyLevel to whiteLevel + const strength = this.embossStrength() * 10, greyLevel = this.embossWhiteLevel() * 255, direction = this.embossDirection(), blend = this.embossBlend(), data = imageData.data, w = imageData.width, h = imageData.height, w4 = w * 4; + let dirY = 0, dirX = 0, y = h; + switch (direction) { + case 'top-left': + dirY = -1; + dirX = -1; + break; + case 'top': + dirY = -1; + dirX = 0; + break; + case 'top-right': + dirY = -1; + dirX = 1; + break; + case 'right': + dirY = 0; + dirX = 1; + break; + case 'bottom-right': + dirY = 1; + dirX = 1; + break; + case 'bottom': + dirY = 1; + dirX = 0; + break; + case 'bottom-left': + dirY = 1; + dirX = -1; + break; + case 'left': + dirY = 0; + dirX = -1; + break; + default: + Util.error('Unknown emboss direction: ' + direction); + } + do { + const offsetY = (y - 1) * w4; + let otherY = dirY; + if (y + otherY < 1) { + otherY = 0; + } + if (y + otherY > h) { + otherY = 0; + } + const offsetYOther = (y - 1 + otherY) * w * 4; + let x = w; + do { + const offset = offsetY + (x - 1) * 4; + let otherX = dirX; + if (x + otherX < 1) { + otherX = 0; + } + if (x + otherX > w) { + otherX = 0; + } + const offsetOther = offsetYOther + (x - 1 + otherX) * 4; + const dR = data[offset] - data[offsetOther]; + const dG = data[offset + 1] - data[offsetOther + 1]; + const dB = data[offset + 2] - data[offsetOther + 2]; + let dif = dR; + const absDif = dif > 0 ? dif : -dif; + const absG = dG > 0 ? dG : -dG; + const absB = dB > 0 ? dB : -dB; + if (absG > absDif) { + dif = dG; + } + if (absB > absDif) { + dif = dB; + } + dif *= strength; + if (blend) { + const r = data[offset] + dif; + const g = data[offset + 1] + dif; + const b = data[offset + 2] + dif; + data[offset] = r > 255 ? 255 : r < 0 ? 0 : r; + data[offset + 1] = g > 255 ? 255 : g < 0 ? 0 : g; + data[offset + 2] = b > 255 ? 255 : b < 0 ? 0 : b; + } + else { + let grey = greyLevel - dif; + if (grey < 0) { + grey = 0; + } + else if (grey > 255) { + grey = 255; + } + data[offset] = data[offset + 1] = data[offset + 2] = grey; + } + } while (--x); + } while (--y); + }; + Factory.addGetterSetter(Node, 'embossStrength', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set emboss strength. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossStrength + * @method + * @param {Number} level between 0 and 1. Default is 0.5 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'embossWhiteLevel', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set emboss white level. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossWhiteLevel + * @method + * @param {Number} embossWhiteLevel between 0 and 1. Default is 0.5 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'embossDirection', 'top-left', undefined, Factory.afterSetFilter); + /** + * get/set emboss direction. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossDirection + * @method + * @param {String} embossDirection can be top-left, top, top-right, right, bottom-right, bottom, bottom-left or left + * The default is top-left + * @returns {String} + */ + Factory.addGetterSetter(Node, 'embossBlend', false, undefined, Factory.afterSetFilter); + /** + * get/set emboss blend. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossBlend + * @method + * @param {Boolean} embossBlend + * @returns {Boolean} + */ + + function remap(fromValue, fromMin, fromMax, toMin, toMax) { + // Compute the range of the data + const fromRange = fromMax - fromMin, toRange = toMax - toMin; + // If either range is 0, then the value can only be mapped to 1 value + if (fromRange === 0) { + return toMin + toRange / 2; + } + if (toRange === 0) { + return toMin; + } + // (1) untranslate, (2) unscale, (3) rescale, (4) retranslate + let toValue = (fromValue - fromMin) / fromRange; + toValue = toRange * toValue + toMin; + return toValue; + } + /** + * Enhance Filter. Adjusts the colors so that they span the widest + * possible range (ie 0-255). Performs w*h pixel reads and w*h pixel + * writes. + * @function + * @name Enhance + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Enhance]); + * node.enhance(0.4); + */ + const Enhance = function (imageData) { + const data = imageData.data, nSubPixels = data.length; + let rMin = data[0], rMax = rMin, r, gMin = data[1], gMax = gMin, g, bMin = data[2], bMax = bMin, b; + // If we are not enhancing anything - don't do any computation + const enhanceAmount = this.enhance(); + if (enhanceAmount === 0) { + return; + } + // 1st Pass - find the min and max for each channel: + for (let i = 0; i < nSubPixels; i += 4) { + r = data[i + 0]; + if (r < rMin) { + rMin = r; + } + else if (r > rMax) { + rMax = r; + } + g = data[i + 1]; + if (g < gMin) { + gMin = g; + } + else if (g > gMax) { + gMax = g; + } + b = data[i + 2]; + if (b < bMin) { + bMin = b; + } + else if (b > bMax) { + bMax = b; + } + //a = data[i + 3]; + //if (a < aMin) { aMin = a; } else + //if (a > aMax) { aMax = a; } + } + // If there is only 1 level - don't remap + if (rMax === rMin) { + rMax = 255; + rMin = 0; + } + if (gMax === gMin) { + gMax = 255; + gMin = 0; + } + if (bMax === bMin) { + bMax = 255; + bMin = 0; + } + let rMid, rGoalMax, rGoalMin, gMid, gGoalMax, gGoalMin, bMid, bGoalMax, bGoalMin; + // If the enhancement is positive - stretch the histogram + if (enhanceAmount > 0) { + rGoalMax = rMax + enhanceAmount * (255 - rMax); + rGoalMin = rMin - enhanceAmount * (rMin - 0); + gGoalMax = gMax + enhanceAmount * (255 - gMax); + gGoalMin = gMin - enhanceAmount * (gMin - 0); + bGoalMax = bMax + enhanceAmount * (255 - bMax); + bGoalMin = bMin - enhanceAmount * (bMin - 0); + // If the enhancement is negative - compress the histogram + } + else { + rMid = (rMax + rMin) * 0.5; + rGoalMax = rMax + enhanceAmount * (rMax - rMid); + rGoalMin = rMin + enhanceAmount * (rMin - rMid); + gMid = (gMax + gMin) * 0.5; + gGoalMax = gMax + enhanceAmount * (gMax - gMid); + gGoalMin = gMin + enhanceAmount * (gMin - gMid); + bMid = (bMax + bMin) * 0.5; + bGoalMax = bMax + enhanceAmount * (bMax - bMid); + bGoalMin = bMin + enhanceAmount * (bMin - bMid); + } + // Pass 2 - remap everything, except the alpha + for (let i = 0; i < nSubPixels; i += 4) { + data[i + 0] = remap(data[i + 0], rMin, rMax, rGoalMin, rGoalMax); + data[i + 1] = remap(data[i + 1], gMin, gMax, gGoalMin, gGoalMax); + data[i + 2] = remap(data[i + 2], bMin, bMax, bGoalMin, bGoalMax); + //data[i + 3] = remap(data[i + 3], aMin, aMax, aGoalMin, aGoalMax); + } + }; + /** + * get/set enhance. Use with {@link Konva.Filters.Enhance} filter. -1 to 1 values + * @name Konva.Node#enhance + * @method + * @param {Float} amount + * @returns {Float} + */ + Factory.addGetterSetter(Node, 'enhance', 0, getNumberValidator(), Factory.afterSetFilter); + + /** + * Grayscale Filter + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Grayscale]); + */ + const Grayscale = function (imageData) { + const data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 4) { + const brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]; + // red + data[i] = brightness; + // green + data[i + 1] = brightness; + // blue + data[i + 2] = brightness; + } + }; + + Factory.addGetterSetter(Node, 'hue', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv hue in degrees. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#hue + * @method + * @param {Number} hue value between 0 and 359 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'saturation', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv saturation. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#saturation + * @method + * @param {Number} saturation 0 is no change, -1.0 halves the saturation, 1.0 doubles, etc.. + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'luminance', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsl luminance. Use with {@link Konva.Filters.HSL} filter. + * @name Konva.Node#luminance + * @method + * @param {Number} value from -1 to 1 + * @returns {Number} + */ + /** + * HSL Filter. Adjusts the hue, saturation and luminance (or lightness) + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * image.filters([Konva.Filters.HSL]); + * image.luminance(0.2); + */ + const HSL = function (imageData) { + const data = imageData.data, nPixels = data.length, v = 1, s = Math.pow(2, this.saturation()), h = Math.abs(this.hue() + 360) % 360, l = this.luminance() * 127; + // Basis for the technique used: + // http://beesbuzz.biz/code/hsv_color_transforms.php + // V is the value multiplier (1 for none, 2 for double, 0.5 for half) + // S is the saturation multiplier (1 for none, 2 for double, 0.5 for half) + // H is the hue shift in degrees (0 to 360) + // vsu = V*S*cos(H*PI/180); + // vsw = V*S*sin(H*PI/180); + //[ .299V+.701vsu+.168vsw .587V-.587vsu+.330vsw .114V-.114vsu-.497vsw ] [R] + //[ .299V-.299vsu-.328vsw .587V+.413vsu+.035vsw .114V-.114vsu+.292vsw ]*[G] + //[ .299V-.300vsu+1.25vsw .587V-.588vsu-1.05vsw .114V+.886vsu-.203vsw ] [B] + // Precompute the values in the matrix: + const vsu = v * s * Math.cos((h * Math.PI) / 180), vsw = v * s * Math.sin((h * Math.PI) / 180); + // (result spot)(source spot) + const rr = 0.299 * v + 0.701 * vsu + 0.167 * vsw, rg = 0.587 * v - 0.587 * vsu + 0.33 * vsw, rb = 0.114 * v - 0.114 * vsu - 0.497 * vsw; + const gr = 0.299 * v - 0.299 * vsu - 0.328 * vsw, gg = 0.587 * v + 0.413 * vsu + 0.035 * vsw, gb = 0.114 * v - 0.114 * vsu + 0.293 * vsw; + const br = 0.299 * v - 0.3 * vsu + 1.25 * vsw, bg = 0.587 * v - 0.586 * vsu - 1.05 * vsw, bb = 0.114 * v + 0.886 * vsu - 0.2 * vsw; + let r, g, b, a; + for (let i = 0; i < nPixels; i += 4) { + r = data[i + 0]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + data[i + 0] = rr * r + rg * g + rb * b + l; + data[i + 1] = gr * r + gg * g + gb * b + l; + data[i + 2] = br * r + bg * g + bb * b + l; + data[i + 3] = a; // alpha + } + }; + + /** + * HSV Filter. Adjusts the hue, saturation and value + * @function + * @name HSV + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * image.filters([Konva.Filters.HSV]); + * image.value(200); + */ + const HSV = function (imageData) { + const data = imageData.data, nPixels = data.length, v = Math.pow(2, this.value()), s = Math.pow(2, this.saturation()), h = Math.abs(this.hue() + 360) % 360; + // Basis for the technique used: + // http://beesbuzz.biz/code/hsv_color_transforms.php + // V is the value multiplier (1 for none, 2 for double, 0.5 for half) + // S is the saturation multiplier (1 for none, 2 for double, 0.5 for half) + // H is the hue shift in degrees (0 to 360) + // vsu = V*S*cos(H*PI/180); + // vsw = V*S*sin(H*PI/180); + //[ .299V+.701vsu+.168vsw .587V-.587vsu+.330vsw .114V-.114vsu-.497vsw ] [R] + //[ .299V-.299vsu-.328vsw .587V+.413vsu+.035vsw .114V-.114vsu+.292vsw ]*[G] + //[ .299V-.300vsu+1.25vsw .587V-.588vsu-1.05vsw .114V+.886vsu-.203vsw ] [B] + // Precompute the values in the matrix: + const vsu = v * s * Math.cos((h * Math.PI) / 180), vsw = v * s * Math.sin((h * Math.PI) / 180); + // (result spot)(source spot) + const rr = 0.299 * v + 0.701 * vsu + 0.167 * vsw, rg = 0.587 * v - 0.587 * vsu + 0.33 * vsw, rb = 0.114 * v - 0.114 * vsu - 0.497 * vsw; + const gr = 0.299 * v - 0.299 * vsu - 0.328 * vsw, gg = 0.587 * v + 0.413 * vsu + 0.035 * vsw, gb = 0.114 * v - 0.114 * vsu + 0.293 * vsw; + const br = 0.299 * v - 0.3 * vsu + 1.25 * vsw, bg = 0.587 * v - 0.586 * vsu - 1.05 * vsw, bb = 0.114 * v + 0.886 * vsu - 0.2 * vsw; + let r, g, b, a; + for (let i = 0; i < nPixels; i += 4) { + r = data[i + 0]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + data[i + 0] = rr * r + rg * g + rb * b; + data[i + 1] = gr * r + gg * g + gb * b; + data[i + 2] = br * r + bg * g + bb * b; + data[i + 3] = a; // alpha + } + }; + Factory.addGetterSetter(Node, 'hue', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv hue in degrees. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#hue + * @method + * @param {Number} hue value between 0 and 359 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'saturation', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv saturation. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#saturation + * @method + * @param {Number} saturation 0 is no change, -1.0 halves the saturation, 1.0 doubles, etc.. + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'value', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv value. Use with {@link Konva.Filters.HSV} filter. + * @name Konva.Node#value + * @method + * @param {Number} value 0 is no change, -1.0 halves the value, 1.0 doubles, etc.. + * @returns {Number} + */ + + /** + * Invert Filter + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Invert]); + */ + const Invert = function (imageData) { + const data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 4) { + // red + data[i] = 255 - data[i]; + // green + data[i + 1] = 255 - data[i + 1]; + // blue + data[i + 2] = 255 - data[i + 2]; + } + }; + + /* + * ToPolar Filter. Converts image data to polar coordinates. Performs + * w*h*4 pixel reads and w*h pixel writes. The r axis is placed along + * what would be the y axis and the theta axis along the x axis. + * @function + * @author ippo615 + * @memberof Konva.Filters + * @param {ImageData} src, the source image data (what will be transformed) + * @param {ImageData} dst, the destination image data (where it will be saved) + * @param {Object} opt + * @param {Number} [opt.polarCenterX] horizontal location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarCenterY] vertical location for the center of the circle, + * default is in the middle + */ + const ToPolar = function (src, dst, opt) { + const srcPixels = src.data, dstPixels = dst.data, xSize = src.width, ySize = src.height, xMid = opt.polarCenterX || xSize / 2, yMid = opt.polarCenterY || ySize / 2; + // Find the largest radius + let rMax = Math.sqrt(xMid * xMid + yMid * yMid); + let x = xSize - xMid; + let y = ySize - yMid; + const rad = Math.sqrt(x * x + y * y); + rMax = rad > rMax ? rad : rMax; + // We'll be uisng y as the radius, and x as the angle (theta=t) + const rSize = ySize, tSize = xSize; + // We want to cover all angles (0-360) and we need to convert to + // radians (*PI/180) + const conversion = ((360 / tSize) * Math.PI) / 180; + // var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale; + for (let theta = 0; theta < tSize; theta += 1) { + const sin = Math.sin(theta * conversion); + const cos = Math.cos(theta * conversion); + for (let radius = 0; radius < rSize; radius += 1) { + x = Math.floor(xMid + ((rMax * radius) / rSize) * cos); + y = Math.floor(yMid + ((rMax * radius) / rSize) * sin); + let i = (y * xSize + x) * 4; + const r = srcPixels[i + 0]; + const g = srcPixels[i + 1]; + const b = srcPixels[i + 2]; + const a = srcPixels[i + 3]; + // Store it + //i = (theta * xSize + radius) * 4; + i = (theta + radius * xSize) * 4; + dstPixels[i + 0] = r; + dstPixels[i + 1] = g; + dstPixels[i + 2] = b; + dstPixels[i + 3] = a; + } + } + }; + /* + * FromPolar Filter. Converts image data from polar coordinates back to rectangular. + * Performs w*h*4 pixel reads and w*h pixel writes. + * @function + * @author ippo615 + * @memberof Konva.Filters + * @param {ImageData} src, the source image data (what will be transformed) + * @param {ImageData} dst, the destination image data (where it will be saved) + * @param {Object} opt + * @param {Number} [opt.polarCenterX] horizontal location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarCenterY] vertical location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarRotation] amount to rotate the image counterclockwis, + * 0 is no rotation, 360 degrees is a full rotation + */ + const FromPolar = function (src, dst, opt) { + const srcPixels = src.data, dstPixels = dst.data, xSize = src.width, ySize = src.height, xMid = opt.polarCenterX || xSize / 2, yMid = opt.polarCenterY || ySize / 2; + // Find the largest radius + let rMax = Math.sqrt(xMid * xMid + yMid * yMid); + let x = xSize - xMid; + let y = ySize - yMid; + const rad = Math.sqrt(x * x + y * y); + rMax = rad > rMax ? rad : rMax; + // We'll be uisng x as the radius, and y as the angle (theta=t) + const rSize = ySize, tSize = xSize, phaseShift = 0; + // We need to convert to degrees and we need to make sure + // it's between (0-360) + // var conversion = tSize/360*180/Math.PI; + //var conversion = tSize/360*180/Math.PI; + let x1, y1; + for (x = 0; x < xSize; x += 1) { + for (y = 0; y < ySize; y += 1) { + const dx = x - xMid; + const dy = y - yMid; + const radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax; + let theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360; + theta = (theta * tSize) / 360; + x1 = Math.floor(theta); + y1 = Math.floor(radius); + let i = (y1 * xSize + x1) * 4; + const r = srcPixels[i + 0]; + const g = srcPixels[i + 1]; + const b = srcPixels[i + 2]; + const a = srcPixels[i + 3]; + // Store it + i = (y * xSize + x) * 4; + dstPixels[i + 0] = r; + dstPixels[i + 1] = g; + dstPixels[i + 2] = b; + dstPixels[i + 3] = a; + } + } + }; + //Konva.Filters.ToPolar = Util._FilterWrapDoubleBuffer(ToPolar); + //Konva.Filters.FromPolar = Util._FilterWrapDoubleBuffer(FromPolar); + // create a temporary canvas for working - shared between multiple calls + /* + * Kaleidoscope Filter. + * @function + * @name Kaleidoscope + * @author ippo615 + * @memberof Konva.Filters + * @example + * node.cache(); + * node.filters([Konva.Filters.Kaleidoscope]); + * node.kaleidoscopePower(3); + * node.kaleidoscopeAngle(45); + */ + const Kaleidoscope = function (imageData) { + const xSize = imageData.width, ySize = imageData.height; + let x, y, xoff, i, r, g, b, a, srcPos, dstPos; + let power = Math.round(this.kaleidoscopePower()); + const angle = Math.round(this.kaleidoscopeAngle()); + const offset = Math.floor((xSize * (angle % 360)) / 360); + if (power < 1) { + return; + } + // Work with our shared buffer canvas + const tempCanvas = Util.createCanvasElement(); + tempCanvas.width = xSize; + tempCanvas.height = ySize; + const scratchData = tempCanvas + .getContext('2d') + .getImageData(0, 0, xSize, ySize); + Util.releaseCanvas(tempCanvas); + // Convert thhe original to polar coordinates + ToPolar(imageData, scratchData, { + polarCenterX: xSize / 2, + polarCenterY: ySize / 2, + }); + // Determine how big each section will be, if it's too small + // make it bigger + let minSectionSize = xSize / Math.pow(2, power); + while (minSectionSize <= 8) { + minSectionSize = minSectionSize * 2; + power -= 1; + } + minSectionSize = Math.ceil(minSectionSize); + let sectionSize = minSectionSize; + // Copy the offset region to 0 + // Depending on the size of filter and location of the offset we may need + // to copy the section backwards to prevent it from rewriting itself + let xStart = 0, xEnd = sectionSize, xDelta = 1; + if (offset + minSectionSize > xSize) { + xStart = sectionSize; + xEnd = 0; + xDelta = -1; + } + for (y = 0; y < ySize; y += 1) { + for (x = xStart; x !== xEnd; x += xDelta) { + xoff = Math.round(x + offset) % xSize; + srcPos = (xSize * y + xoff) * 4; + r = scratchData.data[srcPos + 0]; + g = scratchData.data[srcPos + 1]; + b = scratchData.data[srcPos + 2]; + a = scratchData.data[srcPos + 3]; + dstPos = (xSize * y + x) * 4; + scratchData.data[dstPos + 0] = r; + scratchData.data[dstPos + 1] = g; + scratchData.data[dstPos + 2] = b; + scratchData.data[dstPos + 3] = a; + } + } + // Perform the actual effect + for (y = 0; y < ySize; y += 1) { + sectionSize = Math.floor(minSectionSize); + for (i = 0; i < power; i += 1) { + for (x = 0; x < sectionSize + 1; x += 1) { + srcPos = (xSize * y + x) * 4; + r = scratchData.data[srcPos + 0]; + g = scratchData.data[srcPos + 1]; + b = scratchData.data[srcPos + 2]; + a = scratchData.data[srcPos + 3]; + dstPos = (xSize * y + sectionSize * 2 - x - 1) * 4; + scratchData.data[dstPos + 0] = r; + scratchData.data[dstPos + 1] = g; + scratchData.data[dstPos + 2] = b; + scratchData.data[dstPos + 3] = a; + } + sectionSize *= 2; + } + } + // Convert back from polar coordinates + FromPolar(scratchData, imageData, { polarRotation: 0 }); + }; + /** + * get/set kaleidoscope power. Use with {@link Konva.Filters.Kaleidoscope} filter. + * @name Konva.Node#kaleidoscopePower + * @method + * @param {Integer} power of kaleidoscope + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'kaleidoscopePower', 2, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set kaleidoscope angle. Use with {@link Konva.Filters.Kaleidoscope} filter. + * @name Konva.Node#kaleidoscopeAngle + * @method + * @param {Integer} degrees + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'kaleidoscopeAngle', 0, getNumberValidator(), Factory.afterSetFilter); + + function pixelAt(idata, x, y) { + let idx = (y * idata.width + x) * 4; + const d = []; + d.push(idata.data[idx++], idata.data[idx++], idata.data[idx++], idata.data[idx++]); + return d; + } + function rgbDistance(p1, p2) { + return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + + Math.pow(p1[1] - p2[1], 2) + + Math.pow(p1[2] - p2[2], 2)); + } + function rgbMean(pTab) { + const m = [0, 0, 0]; + for (let i = 0; i < pTab.length; i++) { + m[0] += pTab[i][0]; + m[1] += pTab[i][1]; + m[2] += pTab[i][2]; + } + m[0] /= pTab.length; + m[1] /= pTab.length; + m[2] /= pTab.length; + return m; + } + function backgroundMask(idata, threshold) { + const rgbv_no = pixelAt(idata, 0, 0); + const rgbv_ne = pixelAt(idata, idata.width - 1, 0); + const rgbv_so = pixelAt(idata, 0, idata.height - 1); + const rgbv_se = pixelAt(idata, idata.width - 1, idata.height - 1); + const thres = threshold || 10; + if (rgbDistance(rgbv_no, rgbv_ne) < thres && + rgbDistance(rgbv_ne, rgbv_se) < thres && + rgbDistance(rgbv_se, rgbv_so) < thres && + rgbDistance(rgbv_so, rgbv_no) < thres) { + // Mean color + const mean = rgbMean([rgbv_ne, rgbv_no, rgbv_se, rgbv_so]); + // Mask based on color distance + const mask = []; + for (let i = 0; i < idata.width * idata.height; i++) { + const d = rgbDistance(mean, [ + idata.data[i * 4], + idata.data[i * 4 + 1], + idata.data[i * 4 + 2], + ]); + mask[i] = d < thres ? 0 : 255; + } + return mask; + } + } + function applyMask(idata, mask) { + for (let i = 0; i < idata.width * idata.height; i++) { + idata.data[4 * i + 3] = mask[i]; + } + } + function erodeMask(mask, sw, sh) { + const weights = [1, 1, 1, 1, 0, 1, 1, 1, 1]; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const maskResult = []; + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const so = y * sw + x; + let a = 0; + for (let cy = 0; cy < side; cy++) { + for (let cx = 0; cx < side; cx++) { + const scy = y + cy - halfSide; + const scx = x + cx - halfSide; + if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { + const srcOff = scy * sw + scx; + const wt = weights[cy * side + cx]; + a += mask[srcOff] * wt; + } + } + } + maskResult[so] = a === 255 * 8 ? 255 : 0; + } + } + return maskResult; + } + function dilateMask(mask, sw, sh) { + const weights = [1, 1, 1, 1, 1, 1, 1, 1, 1]; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const maskResult = []; + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const so = y * sw + x; + let a = 0; + for (let cy = 0; cy < side; cy++) { + for (let cx = 0; cx < side; cx++) { + const scy = y + cy - halfSide; + const scx = x + cx - halfSide; + if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { + const srcOff = scy * sw + scx; + const wt = weights[cy * side + cx]; + a += mask[srcOff] * wt; + } + } + } + maskResult[so] = a >= 255 * 4 ? 255 : 0; + } + } + return maskResult; + } + function smoothEdgeMask(mask, sw, sh) { + const weights = [1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9]; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const maskResult = []; + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const so = y * sw + x; + let a = 0; + for (let cy = 0; cy < side; cy++) { + for (let cx = 0; cx < side; cx++) { + const scy = y + cy - halfSide; + const scx = x + cx - halfSide; + if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { + const srcOff = scy * sw + scx; + const wt = weights[cy * side + cx]; + a += mask[srcOff] * wt; + } + } + } + maskResult[so] = a; + } + } + return maskResult; + } + /** + * Mask Filter + * @function + * @name Mask + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Mask]); + * node.threshold(200); + */ + const Mask = function (imageData) { + // Detect pixels close to the background color + const threshold = this.threshold(); + let mask = backgroundMask(imageData, threshold); + if (mask) { + // Erode + mask = erodeMask(mask, imageData.width, imageData.height); + // Dilate + mask = dilateMask(mask, imageData.width, imageData.height); + // Gradient + mask = smoothEdgeMask(mask, imageData.width, imageData.height); + // Apply mask + applyMask(imageData, mask); + } + return imageData; + }; + Factory.addGetterSetter(Node, 'threshold', 0, getNumberValidator(), Factory.afterSetFilter); + + /** + * Noise Filter. Randomly adds or substracts to the color channels + * @function + * @name Noise + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Noise]); + * node.noise(0.8); + */ + const Noise = function (imageData) { + const amount = this.noise() * 255, data = imageData.data, nPixels = data.length, half = amount / 2; + for (let i = 0; i < nPixels; i += 4) { + data[i + 0] += half - 2 * half * Math.random(); + data[i + 1] += half - 2 * half * Math.random(); + data[i + 2] += half - 2 * half * Math.random(); + } + }; + Factory.addGetterSetter(Node, 'noise', 0.2, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set noise amount. Must be a value between 0 and 1. Use with {@link Konva.Filters.Noise} filter. + * @name Konva.Node#noise + * @method + * @param {Number} noise + * @returns {Number} + */ + + /** + * Pixelate Filter. Averages groups of pixels and redraws + * them as larger pixels + * @function + * @name Pixelate + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Pixelate]); + * node.pixelSize(10); + */ + const Pixelate = function (imageData) { + let pixelSize = Math.ceil(this.pixelSize()), width = imageData.width, height = imageData.height, x, y, i, + //pixelsPerBin = pixelSize * pixelSize, + red, green, blue, alpha, nBinsX = Math.ceil(width / pixelSize), nBinsY = Math.ceil(height / pixelSize), xBinStart, xBinEnd, yBinStart, yBinEnd, xBin, yBin, pixelsInBin, data = imageData.data; + if (pixelSize <= 0) { + Util.error('pixelSize value can not be <= 0'); + return; + } + for (xBin = 0; xBin < nBinsX; xBin += 1) { + for (yBin = 0; yBin < nBinsY; yBin += 1) { + // Initialize the color accumlators to 0 + red = 0; + green = 0; + blue = 0; + alpha = 0; + // Determine which pixels are included in this bin + xBinStart = xBin * pixelSize; + xBinEnd = xBinStart + pixelSize; + yBinStart = yBin * pixelSize; + yBinEnd = yBinStart + pixelSize; + // Add all of the pixels to this bin! + pixelsInBin = 0; + for (x = xBinStart; x < xBinEnd; x += 1) { + if (x >= width) { + continue; + } + for (y = yBinStart; y < yBinEnd; y += 1) { + if (y >= height) { + continue; + } + i = (width * y + x) * 4; + red += data[i + 0]; + green += data[i + 1]; + blue += data[i + 2]; + alpha += data[i + 3]; + pixelsInBin += 1; + } + } + // Make sure the channels are between 0-255 + red = red / pixelsInBin; + green = green / pixelsInBin; + blue = blue / pixelsInBin; + alpha = alpha / pixelsInBin; + // Draw this bin + for (x = xBinStart; x < xBinEnd; x += 1) { + if (x >= width) { + continue; + } + for (y = yBinStart; y < yBinEnd; y += 1) { + if (y >= height) { + continue; + } + i = (width * y + x) * 4; + data[i + 0] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = alpha; + } + } + } + } + }; + Factory.addGetterSetter(Node, 'pixelSize', 8, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set pixel size. Use with {@link Konva.Filters.Pixelate} filter. + * @name Konva.Node#pixelSize + * @method + * @param {Integer} pixelSize + * @returns {Integer} + */ + + /** + * Posterize Filter. Adjusts the channels so that there are no more + * than n different values for that channel. This is also applied + * to the alpha channel. + * @function + * @name Posterize + * @author ippo615 + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Posterize]); + * node.levels(0.8); // between 0 and 1 + */ + const Posterize = function (imageData) { + // level must be between 1 and 255 + const levels = Math.round(this.levels() * 254) + 1, data = imageData.data, len = data.length, scale = 255 / levels; + for (let i = 0; i < len; i += 1) { + data[i] = Math.floor(data[i] / scale) * scale; + } + }; + Factory.addGetterSetter(Node, 'levels', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set levels. Must be a number between 0 and 1. Use with {@link Konva.Filters.Posterize} filter. + * @name Konva.Node#levels + * @method + * @param {Number} level between 0 and 1 + * @returns {Number} + */ + + /** + * RGB Filter + * @function + * @name RGB + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.RGB]); + * node.blue(120); + * node.green(200); + */ + const RGB = function (imageData) { + const data = imageData.data, nPixels = data.length, red = this.red(), green = this.green(), blue = this.blue(); + for (let i = 0; i < nPixels; i += 4) { + const brightness = (0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]) / 255; + data[i] = brightness * red; // r + data[i + 1] = brightness * green; // g + data[i + 2] = brightness * blue; // b + data[i + 3] = data[i + 3]; // alpha + } + }; + Factory.addGetterSetter(Node, 'red', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter red value. Use with {@link Konva.Filters.RGB} filter. + * @name red + * @method + * @memberof Konva.Node.prototype + * @param {Integer} red value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'green', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter green value. Use with {@link Konva.Filters.RGB} filter. + * @name green + * @method + * @memberof Konva.Node.prototype + * @param {Integer} green value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'blue', 0, RGBComponent, Factory.afterSetFilter); + /** + * get/set filter blue value. Use with {@link Konva.Filters.RGB} filter. + * @name blue + * @method + * @memberof Konva.Node.prototype + * @param {Integer} blue value between 0 and 255 + * @returns {Integer} + */ + + /** + * RGBA Filter + * @function + * @name RGBA + * @memberof Konva.Filters + * @param {Object} imageData + * @author codefo + * @example + * node.cache(); + * node.filters([Konva.Filters.RGBA]); + * node.blue(120); + * node.green(200); + * node.alpha(0.3); + */ + const RGBA = function (imageData) { + const data = imageData.data, nPixels = data.length, red = this.red(), green = this.green(), blue = this.blue(), alpha = this.alpha(); + for (let i = 0; i < nPixels; i += 4) { + const ia = 1 - alpha; + data[i] = red * alpha + data[i] * ia; // r + data[i + 1] = green * alpha + data[i + 1] * ia; // g + data[i + 2] = blue * alpha + data[i + 2] * ia; // b + } + }; + Factory.addGetterSetter(Node, 'red', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter red value. Use with {@link Konva.Filters.RGBA} filter. + * @name red + * @method + * @memberof Konva.Node.prototype + * @param {Integer} red value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'green', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter green value. Use with {@link Konva.Filters.RGBA} filter. + * @name green + * @method + * @memberof Konva.Node.prototype + * @param {Integer} green value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'blue', 0, RGBComponent, Factory.afterSetFilter); + /** + * get/set filter blue value. Use with {@link Konva.Filters.RGBA} filter. + * @name blue + * @method + * @memberof Konva.Node.prototype + * @param {Integer} blue value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'alpha', 1, function (val) { + this._filterUpToDate = false; + if (val > 1) { + return 1; + } + else if (val < 0) { + return 0; + } + else { + return val; + } + }); + /** + * get/set filter alpha value. Use with {@link Konva.Filters.RGBA} filter. + * @name alpha + * @method + * @memberof Konva.Node.prototype + * @param {Float} alpha value between 0 and 1 + * @returns {Float} + */ + + // based on https://stackoverflow.com/questions/1061093/how-is-a-sepia-tone-created + /** + * @function + * @name Sepia + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Sepia]); + */ + const Sepia = function (imageData) { + const data = imageData.data, nPixels = data.length; + for (let i = 0; i < nPixels; i += 4) { + const r = data[i + 0]; + const g = data[i + 1]; + const b = data[i + 2]; + data[i + 0] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); + data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); + data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); + } + }; + + /** + * Solarize Filter + * Pixastic Lib - Solarize filter - v0.1.0 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * License: [http://www.pixastic.com/lib/license.txt] + * @function + * @name Solarize + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Solarize]); + */ + const Solarize = function (imageData) { + const data = imageData.data, w = imageData.width, h = imageData.height, w4 = w * 4; + let y = h; + do { + const offsetY = (y - 1) * w4; + let x = w; + do { + const offset = offsetY + (x - 1) * 4; + let r = data[offset]; + let g = data[offset + 1]; + let b = data[offset + 2]; + if (r > 127) { + r = 255 - r; + } + if (g > 127) { + g = 255 - g; + } + if (b > 127) { + b = 255 - b; + } + data[offset] = r; + data[offset + 1] = g; + data[offset + 2] = b; + } while (--x); + } while (--y); + }; + + /** + * Threshold Filter. Pushes any value above the mid point to + * the max and any value below the mid point to the min. + * This affects the alpha channel. + * @function + * @name Threshold + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Threshold]); + * node.threshold(0.1); + */ + const Threshold = function (imageData) { + const level = this.threshold() * 255, data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 1) { + data[i] = data[i] < level ? 0 : 255; + } + }; + Factory.addGetterSetter(Node, 'threshold', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set threshold. Must be a value between 0 and 1. Use with {@link Konva.Filters.Threshold} or {@link Konva.Filters.Mask} filter. + * @name threshold + * @method + * @memberof Konva.Node.prototype + * @param {Number} threshold + * @returns {Number} + */ + + // we need to import core of the Konva and then extend it with all additional objects + const Konva = Konva$1.Util._assign(Konva$1, { + Arc, + Arrow, + Circle, + Ellipse, + Image, + Label, + Tag, + Line, + Path, + Rect, + RegularPolygon, + Ring, + Sprite, + Star, + Text, + TextPath, + Transformer, + Wedge, + /** + * @namespace Filters + * @memberof Konva + */ + Filters: { + Blur, + Brighten, + Contrast, + Emboss, + Enhance, + Grayscale, + HSL, + HSV, + Invert, + Kaleidoscope, + Mask, + Noise, + Pixelate, + Posterize, + RGB, + RGBA, + Sepia, + Solarize, + Threshold, + }, + }); + + return Konva; + +})); diff --git a/animations/priority_queue/min_heap/animation.js b/animations/priority_queue/min_heap/animation.js new file mode 100644 index 0000000..eb425b5 --- /dev/null +++ b/animations/priority_queue/min_heap/animation.js @@ -0,0 +1,189 @@ +var stage = new Konva.Stage({ + container: 'container', + width: 2000, + height: 1000 +}); + +var layer = new Konva.Layer(); +stage.add(layer); + + +var heap = []; +var nodeRadius = 25; +var levelHeight = 80; +var startX = 500; +var startY = 50; +var horizontalSpacing = 150; + +var animationNodes = []; +var animationLines = []; +var currentStep = 0; + +var operations = [ + { type: 'push', value: 3 }, + { type: 'push', value: 4 }, + { type: 'push', value: 3 }, + { type: 'push', value: 1 }, + { type: 'push', value: 5 }, + { type: 'pop' }, + { type: 'pop' }, + { type: 'pop' }, + { type: 'pop' }, + { type: 'pop' } +]; + +function drawNode(x, y, value, color = '#ffffff') { + var group = new Konva.Group({ x: x, y: y }); + + var circle = new Konva.Circle({ + radius: nodeRadius, + fill: color, + stroke: '#000000', + strokeWidth: 2 + }); + + var text = new Konva.Text({ + text: value.toString(), + fill: 'black', + align: 'center', + width: nodeRadius * 2, + x: -nodeRadius, + y: -10 + }); + + group.add(circle); + group.add(text); + layer.add(group); + animationNodes.push(group); + + return group; +} + +function makeLine(x1, y1, x2, y2) { + var line = new Konva.Line({ + points: [x1, y1, x2, y2], + stroke: 'black', + strokeWidth: 2, + lineCap: 'round', + lineJoin: 'round' + }); + layer.add(line); + animationLines.push(line); + return line; +} + +function makeTree() { + animationNodes.forEach(n => n.destroy()); + animationLines.forEach(l => l.destroy()); + animationNodes = []; + animationLines = []; + + if (heap.length === 0) return; + + var positions = []; + + for (var i = 0; i < heap.length; i++) { + var level = Math.floor(Math.log2(i + 1)); + var indexInLevel = i - (2 ** level - 1); + var nodesInLevel = 2 ** level; + + var x = startX + (indexInLevel - (nodesInLevel - 1) / 2) * horizontalSpacing * (1.5 - level / 5); + var y = startY + level * levelHeight; + + positions.push({ x: x, y: y }); + drawNode(x, y, heap[i]); + + if (i > 0) { + var parentPos = positions[Math.floor((i - 1) / 2)]; + makeLine(parentPos.x, parentPos.y + nodeRadius, x, y - nodeRadius); + } + } + + layer.draw(); +} + +function push(val) { + heap.push(val); + var idx = heap.length - 1; + while (idx > 0) { + var parentIdx = Math.floor((idx - 1) / 2); + if (heap[parentIdx] <= heap[idx]) break; + [heap[parentIdx], heap[idx]] = [heap[idx], heap[parentIdx]]; + idx = parentIdx; + } +} + +function pop() { + if (heap.length === 0) return null; + + var min = heap[0]; + heap[0] = heap[heap.length - 1]; + heap.pop(); + + var idx = 0; + while (true) { + var left = 2 * idx + 1; + var right = 2 * idx + 2; + var smallest = idx; + + if (left < heap.length && heap[left] < heap[smallest]) smallest = left; + if (right < heap.length && heap[right] < heap[smallest]) smallest = right; + + if (smallest === idx) break; + + [heap[idx], heap[smallest]] = [heap[smallest], heap[idx]]; + idx = smallest; + } + + return min; +} + +var cppCode = [ + "#include ", + "#include ", + "", + "int main() {", + " std::priority_queue, std::greater> minHeap;", + "", + " minHeap.push(3);", + " minHeap.push(4);", + " minHeap.push(3);", + " minHeap.push(1);", + " minHeap.push(5);", + "", + " while (!minHeap.empty()) {", + " std::cout << minHeap.top() << \" \";", + " minHeap.pop();", + " }", + " std::cout << std::endl;", + "", + " return 0;", + "}" +]; + +function renderCode(step) { + var codeBlock = document.getElementById('codeBlock'); + codeBlock.innerHTML = cppCode.map((line, i) => { + var bold = false; + if (step >= 0 && step <= 4) bold = i === 6 + step; + else if (step >= 5 && step <= 10) bold = i === 13 || i === 14; + return bold ? `${line}` : line; + }).join('\n'); +} + +function next() { + var op = operations[currentStep]; + if (op.type === 'push') { + push(op.value); + } else if (op.type === 'pop') { + pop(); + } + + renderCode(currentStep); + makeTree(); + currentStep++; +} + +document.getElementById('next').addEventListener('click', next); + +layer.draw(); diff --git a/animations/priority_queue/min_heap/index.html b/animations/priority_queue/min_heap/index.html new file mode 100644 index 0000000..23a2ca2 --- /dev/null +++ b/animations/priority_queue/min_heap/index.html @@ -0,0 +1,32 @@ + + + + + + Min heap animation + + + + + +

Min heap animation

+ +
+
+

+    
+ + diff --git a/animations/priority_queue/min_heap/konva.js b/animations/priority_queue/min_heap/konva.js new file mode 100644 index 0000000..6b8cf52 --- /dev/null +++ b/animations/priority_queue/min_heap/konva.js @@ -0,0 +1,18926 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Konva = factory()); +})(this, (function () { 'use strict'; + + /* + * Konva JavaScript Framework v9.3.20 + * http://konvajs.org/ + * Licensed under the MIT + * Date: Thu Mar 20 2025 + * + * Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS) + * Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva) + * + * @license + */ + const PI_OVER_180 = Math.PI / 180; + /** + * @namespace Konva + */ + function detectBrowser() { + return (typeof window !== 'undefined' && + // browser case + ({}.toString.call(window) === '[object Window]' || + // electron case + {}.toString.call(window) === '[object global]')); + } + const glob = typeof global !== 'undefined' + ? global + : typeof window !== 'undefined' + ? window + : typeof WorkerGlobalScope !== 'undefined' + ? self + : {}; + const Konva$2 = { + _global: glob, + version: '9.3.20', + isBrowser: detectBrowser(), + isUnminified: /param/.test(function (param) { }.toString()), + dblClickWindow: 400, + getAngle(angle) { + return Konva$2.angleDeg ? angle * PI_OVER_180 : angle; + }, + enableTrace: false, + pointerEventsEnabled: true, + /** + * Should Konva automatically update canvas on any changes. Default is true. + * @property autoDrawEnabled + * @default true + * @name autoDrawEnabled + * @memberof Konva + * @example + * Konva.autoDrawEnabled = true; + */ + autoDrawEnabled: true, + /** + * Should we enable hit detection while dragging? For performance reasons, by default it is false. + * But on some rare cases you want to see hit graph and check intersections. Just set it to true. + * @property hitOnDragEnabled + * @default false + * @name hitOnDragEnabled + * @memberof Konva + * @example + * Konva.hitOnDragEnabled = true; + */ + hitOnDragEnabled: false, + /** + * Should we capture touch events and bind them to the touchstart target? That is how it works on DOM elements. + * The case: we touchstart on div1, then touchmove out of that element into another element div2. + * DOM will continue trigger touchmove events on div1 (not div2). Because events are "captured" into initial target. + * By default Konva do not do that and will trigger touchmove on another element, while pointer is moving. + * @property capturePointerEventsEnabled + * @default false + * @name capturePointerEventsEnabled + * @memberof Konva + * @example + * Konva.capturePointerEventsEnabled = true; + */ + capturePointerEventsEnabled: false, + _mouseListenClick: false, + _touchListenClick: false, + _pointerListenClick: false, + _mouseInDblClickWindow: false, + _touchInDblClickWindow: false, + _pointerInDblClickWindow: false, + _mouseDblClickPointerId: null, + _touchDblClickPointerId: null, + _pointerDblClickPointerId: null, + _fixTextRendering: false, + /** + * Global pixel ratio configuration. KonvaJS automatically detect pixel ratio of current device. + * But you may override such property, if you want to use your value. Set this value before any components initializations. + * @property pixelRatio + * @default undefined + * @name pixelRatio + * @memberof Konva + * @example + * // before any Konva code: + * Konva.pixelRatio = 1; + */ + pixelRatio: (typeof window !== 'undefined' && window.devicePixelRatio) || 1, + /** + * Drag distance property. If you start to drag a node you may want to wait until pointer is moved to some distance from start point, + * only then start dragging. Default is 3px. + * @property dragDistance + * @default 0 + * @memberof Konva + * @example + * Konva.dragDistance = 10; + */ + dragDistance: 3, + /** + * Use degree values for angle properties. You may set this property to false if you want to use radian values. + * @property angleDeg + * @default true + * @memberof Konva + * @example + * node.rotation(45); // 45 degrees + * Konva.angleDeg = false; + * node.rotation(Math.PI / 2); // PI/2 radian + */ + angleDeg: true, + /** + * Show different warnings about errors or wrong API usage + * @property showWarnings + * @default true + * @memberof Konva + * @example + * Konva.showWarnings = false; + */ + showWarnings: true, + /** + * Configure what mouse buttons can be used for drag and drop. + * Default value is [0] - only left mouse button. + * @property dragButtons + * @default true + * @memberof Konva + * @example + * // enable left and right mouse buttons + * Konva.dragButtons = [0, 2]; + */ + dragButtons: [0, 1], + /** + * returns whether or not drag and drop is currently active + * @method + * @memberof Konva + */ + isDragging() { + return Konva$2['DD'].isDragging; + }, + isTransforming() { + var _a; + return (_a = Konva$2['Transformer']) === null || _a === undefined ? undefined : _a.isTransforming(); + }, + /** + * returns whether or not a drag and drop operation is ready, but may + * not necessarily have started + * @method + * @memberof Konva + */ + isDragReady() { + return !!Konva$2['DD'].node; + }, + /** + * Should Konva release canvas elements on destroy. Default is true. + * Useful to avoid memory leak issues in Safari on macOS/iOS. + * @property releaseCanvasOnDestroy + * @default true + * @name releaseCanvasOnDestroy + * @memberof Konva + * @example + * Konva.releaseCanvasOnDestroy = true; + */ + releaseCanvasOnDestroy: true, + // user agent + document: glob.document, + // insert Konva into global namespace (window) + // it is required for npm packages + _injectGlobal(Konva) { + glob.Konva = Konva; + }, + }; + const _registerNode = (NodeClass) => { + Konva$2[NodeClass.prototype.getClassName()] = NodeClass; + }; + Konva$2._injectGlobal(Konva$2); + + /* + * Last updated November 2011 + * By Simon Sarris + * www.simonsarris.com + * sarris@acm.org + * + * Free to use and distribute at will + * So long as you are nice to people, etc + */ + /* + * The usage of this class was inspired by some of the work done by a forked + * project, KineticJS-Ext by Wappworks, which is based on Simon's Transform + * class. Modified by Eric Rowell + */ + /** + * Transform constructor. + * In most of the cases you don't need to use it in your app. Because it is for internal usage in Konva core. + * But there is a documentation for that class in case you still want + * to make some manual calculations. + * @constructor + * @param {Array} [m] Optional six-element matrix + * @memberof Konva + */ + class Transform { + constructor(m = [1, 0, 0, 1, 0, 0]) { + this.dirty = false; + this.m = (m && m.slice()) || [1, 0, 0, 1, 0, 0]; + } + reset() { + this.m[0] = 1; + this.m[1] = 0; + this.m[2] = 0; + this.m[3] = 1; + this.m[4] = 0; + this.m[5] = 0; + } + /** + * Copy Konva.Transform object + * @method + * @name Konva.Transform#copy + * @returns {Konva.Transform} + * @example + * const tr = shape.getTransform().copy() + */ + copy() { + return new Transform(this.m); + } + copyInto(tr) { + tr.m[0] = this.m[0]; + tr.m[1] = this.m[1]; + tr.m[2] = this.m[2]; + tr.m[3] = this.m[3]; + tr.m[4] = this.m[4]; + tr.m[5] = this.m[5]; + } + /** + * Transform point + * @method + * @name Konva.Transform#point + * @param {Object} point 2D point(x, y) + * @returns {Object} 2D point(x, y) + */ + point(point) { + const m = this.m; + return { + x: m[0] * point.x + m[2] * point.y + m[4], + y: m[1] * point.x + m[3] * point.y + m[5], + }; + } + /** + * Apply translation + * @method + * @name Konva.Transform#translate + * @param {Number} x + * @param {Number} y + * @returns {Konva.Transform} + */ + translate(x, y) { + this.m[4] += this.m[0] * x + this.m[2] * y; + this.m[5] += this.m[1] * x + this.m[3] * y; + return this; + } + /** + * Apply scale + * @method + * @name Konva.Transform#scale + * @param {Number} sx + * @param {Number} sy + * @returns {Konva.Transform} + */ + scale(sx, sy) { + this.m[0] *= sx; + this.m[1] *= sx; + this.m[2] *= sy; + this.m[3] *= sy; + return this; + } + /** + * Apply rotation + * @method + * @name Konva.Transform#rotate + * @param {Number} rad Angle in radians + * @returns {Konva.Transform} + */ + rotate(rad) { + const c = Math.cos(rad); + const s = Math.sin(rad); + const m11 = this.m[0] * c + this.m[2] * s; + const m12 = this.m[1] * c + this.m[3] * s; + const m21 = this.m[0] * -s + this.m[2] * c; + const m22 = this.m[1] * -s + this.m[3] * c; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + return this; + } + /** + * Returns the translation + * @method + * @name Konva.Transform#getTranslation + * @returns {Object} 2D point(x, y) + */ + getTranslation() { + return { + x: this.m[4], + y: this.m[5], + }; + } + /** + * Apply skew + * @method + * @name Konva.Transform#skew + * @param {Number} sx + * @param {Number} sy + * @returns {Konva.Transform} + */ + skew(sx, sy) { + const m11 = this.m[0] + this.m[2] * sy; + const m12 = this.m[1] + this.m[3] * sy; + const m21 = this.m[2] + this.m[0] * sx; + const m22 = this.m[3] + this.m[1] * sx; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + return this; + } + /** + * Transform multiplication + * @method + * @name Konva.Transform#multiply + * @param {Konva.Transform} matrix + * @returns {Konva.Transform} + */ + multiply(matrix) { + const m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1]; + const m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1]; + const m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3]; + const m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3]; + const dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4]; + const dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5]; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + this.m[4] = dx; + this.m[5] = dy; + return this; + } + /** + * Invert the matrix + * @method + * @name Konva.Transform#invert + * @returns {Konva.Transform} + */ + invert() { + const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); + const m0 = this.m[3] * d; + const m1 = -this.m[1] * d; + const m2 = -this.m[2] * d; + const m3 = this.m[0] * d; + const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); + const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); + this.m[0] = m0; + this.m[1] = m1; + this.m[2] = m2; + this.m[3] = m3; + this.m[4] = m4; + this.m[5] = m5; + return this; + } + /** + * return matrix + * @method + * @name Konva.Transform#getMatrix + */ + getMatrix() { + return this.m; + } + /** + * convert transformation matrix back into node's attributes + * @method + * @name Konva.Transform#decompose + * @returns {Konva.Transform} + */ + decompose() { + const a = this.m[0]; + const b = this.m[1]; + const c = this.m[2]; + const d = this.m[3]; + const e = this.m[4]; + const f = this.m[5]; + const delta = a * d - b * c; + const result = { + x: e, + y: f, + rotation: 0, + scaleX: 0, + scaleY: 0, + skewX: 0, + skewY: 0, + }; + // Apply the QR-like decomposition. + if (a != 0 || b != 0) { + const r = Math.sqrt(a * a + b * b); + result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r); + result.scaleX = r; + result.scaleY = delta / r; + result.skewX = (a * c + b * d) / delta; + result.skewY = 0; + } + else if (c != 0 || d != 0) { + const s = Math.sqrt(c * c + d * d); + result.rotation = + Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s)); + result.scaleX = delta / s; + result.scaleY = s; + result.skewX = 0; + result.skewY = (a * c + b * d) / delta; + } + else ; + result.rotation = Util._getRotation(result.rotation); + return result; + } + } + // CONSTANTS + const OBJECT_ARRAY = '[object Array]', OBJECT_NUMBER = '[object Number]', OBJECT_STRING = '[object String]', OBJECT_BOOLEAN = '[object Boolean]', PI_OVER_DEG180 = Math.PI / 180, DEG180_OVER_PI = 180 / Math.PI, HASH$1 = '#', EMPTY_STRING$1 = '', ZERO = '0', KONVA_WARNING = 'Konva warning: ', KONVA_ERROR = 'Konva error: ', RGB_PAREN = 'rgb(', COLORS = { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 132, 11], + darkgray: [169, 169, 169], + darkgreen: [0, 100, 0], + darkgrey: [169, 169, 169], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 255, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + grey: [128, 128, 128], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 203], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [119, 128, 144], + slategrey: [119, 128, 144], + snow: [255, 255, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + transparent: [255, 255, 255, 0], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 5], + }, RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/; + let animQueue = []; + const req = (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame) || + function (f) { + setTimeout(f, 60); + }; + /** + * @namespace Util + * @memberof Konva + */ + const Util = { + /* + * cherry-picked utilities from underscore.js + */ + _isElement(obj) { + return !!(obj && obj.nodeType == 1); + }, + _isFunction(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }, + _isPlainObject(obj) { + return !!obj && obj.constructor === Object; + }, + _isArray(obj) { + return Object.prototype.toString.call(obj) === OBJECT_ARRAY; + }, + _isNumber(obj) { + return (Object.prototype.toString.call(obj) === OBJECT_NUMBER && + !isNaN(obj) && + isFinite(obj)); + }, + _isString(obj) { + return Object.prototype.toString.call(obj) === OBJECT_STRING; + }, + _isBoolean(obj) { + return Object.prototype.toString.call(obj) === OBJECT_BOOLEAN; + }, + // arrays are objects too + isObject(val) { + return val instanceof Object; + }, + isValidSelector(selector) { + if (typeof selector !== 'string') { + return false; + } + const firstChar = selector[0]; + return (firstChar === '#' || + firstChar === '.' || + firstChar === firstChar.toUpperCase()); + }, + _sign(number) { + if (number === 0) { + // that is not what sign usually returns + // but that is what we need + return 1; + } + if (number > 0) { + return 1; + } + else { + return -1; + } + }, + requestAnimFrame(callback) { + animQueue.push(callback); + if (animQueue.length === 1) { + req(function () { + const queue = animQueue; + animQueue = []; + queue.forEach(function (cb) { + cb(); + }); + }); + } + }, + createCanvasElement() { + const canvas = document.createElement('canvas'); + // on some environments canvas.style is readonly + try { + canvas.style = canvas.style || {}; + } + catch (e) { } + return canvas; + }, + createImageElement() { + return document.createElement('img'); + }, + _isInDocument(el) { + while ((el = el.parentNode)) { + if (el == document) { + return true; + } + } + return false; + }, + /* + * arg can be an image object or image data + */ + _urlToImage(url, callback) { + // if arg is a string, then it's a data url + const imageObj = Util.createImageElement(); + imageObj.onload = function () { + callback(imageObj); + }; + imageObj.src = url; + }, + _rgbToHex(r, g, b) { + return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + }, + _hexToRgb(hex) { + hex = hex.replace(HASH$1, EMPTY_STRING$1); + const bigint = parseInt(hex, 16); + return { + r: (bigint >> 16) & 255, + g: (bigint >> 8) & 255, + b: bigint & 255, + }; + }, + /** + * return random hex color + * @method + * @memberof Konva.Util + * @example + * shape.fill(Konva.Util.getRandomColor()); + */ + getRandomColor() { + let randColor = ((Math.random() * 0xffffff) << 0).toString(16); + while (randColor.length < 6) { + randColor = ZERO + randColor; + } + return HASH$1 + randColor; + }, + /** + * get RGB components of a color + * @method + * @memberof Konva.Util + * @param {String} color + * @example + * // each of the following examples return {r:0, g:0, b:255} + * var rgb = Konva.Util.getRGB('blue'); + * var rgb = Konva.Util.getRGB('#0000ff'); + * var rgb = Konva.Util.getRGB('rgb(0,0,255)'); + */ + getRGB(color) { + let rgb; + // color string + if (color in COLORS) { + rgb = COLORS[color]; + return { + r: rgb[0], + g: rgb[1], + b: rgb[2], + }; + } + else if (color[0] === HASH$1) { + // hex + return this._hexToRgb(color.substring(1)); + } + else if (color.substr(0, 4) === RGB_PAREN) { + // rgb string + rgb = RGB_REGEX.exec(color.replace(/ /g, '')); + return { + r: parseInt(rgb[1], 10), + g: parseInt(rgb[2], 10), + b: parseInt(rgb[3], 10), + }; + } + else { + // default + return { + r: 0, + g: 0, + b: 0, + }; + } + }, + // convert any color string to RGBA object + // from https://github.com/component/color-parser + colorToRGBA(str) { + str = str || 'black'; + return (Util._namedColorToRBA(str) || + Util._hex3ColorToRGBA(str) || + Util._hex4ColorToRGBA(str) || + Util._hex6ColorToRGBA(str) || + Util._hex8ColorToRGBA(str) || + Util._rgbColorToRGBA(str) || + Util._rgbaColorToRGBA(str) || + Util._hslColorToRGBA(str)); + }, + // Parse named css color. Like "green" + _namedColorToRBA(str) { + const c = COLORS[str.toLowerCase()]; + if (!c) { + return null; + } + return { + r: c[0], + g: c[1], + b: c[2], + a: 1, + }; + }, + // Parse rgb(n, n, n) + _rgbColorToRGBA(str) { + if (str.indexOf('rgb(') === 0) { + str = str.match(/rgb\(([^)]+)\)/)[1]; + const parts = str.split(/ *, */).map(Number); + return { + r: parts[0], + g: parts[1], + b: parts[2], + a: 1, + }; + } + }, + // Parse rgba(n, n, n, n) + _rgbaColorToRGBA(str) { + if (str.indexOf('rgba(') === 0) { + str = str.match(/rgba\(([^)]+)\)/)[1]; + const parts = str.split(/ *, */).map((n, index) => { + if (n.slice(-1) === '%') { + return index === 3 ? parseInt(n) / 100 : (parseInt(n) / 100) * 255; + } + return Number(n); + }); + return { + r: parts[0], + g: parts[1], + b: parts[2], + a: parts[3], + }; + } + }, + // Parse #nnnnnnnn + _hex8ColorToRGBA(str) { + if (str[0] === '#' && str.length === 9) { + return { + r: parseInt(str.slice(1, 3), 16), + g: parseInt(str.slice(3, 5), 16), + b: parseInt(str.slice(5, 7), 16), + a: parseInt(str.slice(7, 9), 16) / 0xff, + }; + } + }, + // Parse #nnnnnn + _hex6ColorToRGBA(str) { + if (str[0] === '#' && str.length === 7) { + return { + r: parseInt(str.slice(1, 3), 16), + g: parseInt(str.slice(3, 5), 16), + b: parseInt(str.slice(5, 7), 16), + a: 1, + }; + } + }, + // Parse #nnnn + _hex4ColorToRGBA(str) { + if (str[0] === '#' && str.length === 5) { + return { + r: parseInt(str[1] + str[1], 16), + g: parseInt(str[2] + str[2], 16), + b: parseInt(str[3] + str[3], 16), + a: parseInt(str[4] + str[4], 16) / 0xff, + }; + } + }, + // Parse #nnn + _hex3ColorToRGBA(str) { + if (str[0] === '#' && str.length === 4) { + return { + r: parseInt(str[1] + str[1], 16), + g: parseInt(str[2] + str[2], 16), + b: parseInt(str[3] + str[3], 16), + a: 1, + }; + } + }, + // Code adapted from https://github.com/Qix-/color-convert/blob/master/conversions.js#L244 + _hslColorToRGBA(str) { + // Check hsl() format + if (/hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.test(str)) { + // Extract h, s, l + const [_, ...hsl] = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(str); + const h = Number(hsl[0]) / 360; + const s = Number(hsl[1]) / 100; + const l = Number(hsl[2]) / 100; + let t2; + let t3; + let val; + if (s === 0) { + val = l * 255; + return { + r: Math.round(val), + g: Math.round(val), + b: Math.round(val), + a: 1, + }; + } + if (l < 0.5) { + t2 = l * (1 + s); + } + else { + t2 = l + s - l * s; + } + const t1 = 2 * l - t2; + const rgb = [0, 0, 0]; + for (let i = 0; i < 3; i++) { + t3 = h + (1 / 3) * -(i - 1); + if (t3 < 0) { + t3++; + } + if (t3 > 1) { + t3--; + } + if (6 * t3 < 1) { + val = t1 + (t2 - t1) * 6 * t3; + } + else if (2 * t3 < 1) { + val = t2; + } + else if (3 * t3 < 2) { + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + } + else { + val = t1; + } + rgb[i] = val * 255; + } + return { + r: Math.round(rgb[0]), + g: Math.round(rgb[1]), + b: Math.round(rgb[2]), + a: 1, + }; + } + }, + /** + * check intersection of two client rectangles + * @method + * @memberof Konva.Util + * @param {Object} r1 - { x, y, width, height } client rectangle + * @param {Object} r2 - { x, y, width, height } client rectangle + * @example + * const overlapping = Konva.Util.haveIntersection(shape1.getClientRect(), shape2.getClientRect()); + */ + haveIntersection(r1, r2) { + return !(r2.x > r1.x + r1.width || + r2.x + r2.width < r1.x || + r2.y > r1.y + r1.height || + r2.y + r2.height < r1.y); + }, + cloneObject(obj) { + const retObj = {}; + for (const key in obj) { + if (this._isPlainObject(obj[key])) { + retObj[key] = this.cloneObject(obj[key]); + } + else if (this._isArray(obj[key])) { + retObj[key] = this.cloneArray(obj[key]); + } + else { + retObj[key] = obj[key]; + } + } + return retObj; + }, + cloneArray(arr) { + return arr.slice(0); + }, + degToRad(deg) { + return deg * PI_OVER_DEG180; + }, + radToDeg(rad) { + return rad * DEG180_OVER_PI; + }, + _degToRad(deg) { + Util.warn('Util._degToRad is removed. Please use public Util.degToRad instead.'); + return Util.degToRad(deg); + }, + _radToDeg(rad) { + Util.warn('Util._radToDeg is removed. Please use public Util.radToDeg instead.'); + return Util.radToDeg(rad); + }, + _getRotation(radians) { + return Konva$2.angleDeg ? Util.radToDeg(radians) : radians; + }, + _capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + }, + throw(str) { + throw new Error(KONVA_ERROR + str); + }, + error(str) { + console.error(KONVA_ERROR + str); + }, + warn(str) { + if (!Konva$2.showWarnings) { + return; + } + console.warn(KONVA_WARNING + str); + }, + each(obj, func) { + for (const key in obj) { + func(key, obj[key]); + } + }, + _inRange(val, left, right) { + return left <= val && val < right; + }, + _getProjectionToSegment(x1, y1, x2, y2, x3, y3) { + let x, y, dist; + const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); + if (pd2 == 0) { + x = x1; + y = y1; + dist = (x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2); + } + else { + const u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / pd2; + if (u < 0) { + x = x1; + y = y1; + dist = (x1 - x3) * (x1 - x3) + (y1 - y3) * (y1 - y3); + } + else if (u > 1.0) { + x = x2; + y = y2; + dist = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3); + } + else { + x = x1 + u * (x2 - x1); + y = y1 + u * (y2 - y1); + dist = (x - x3) * (x - x3) + (y - y3) * (y - y3); + } + } + return [x, y, dist]; + }, + // line as array of points. + // line might be closed + _getProjectionToLine(pt, line, isClosed) { + const pc = Util.cloneObject(pt); + let dist = Number.MAX_VALUE; + line.forEach(function (p1, i) { + if (!isClosed && i === line.length - 1) { + return; + } + const p2 = line[(i + 1) % line.length]; + const proj = Util._getProjectionToSegment(p1.x, p1.y, p2.x, p2.y, pt.x, pt.y); + const px = proj[0], py = proj[1], pdist = proj[2]; + if (pdist < dist) { + pc.x = px; + pc.y = py; + dist = pdist; + } + }); + return pc; + }, + _prepareArrayForTween(startArray, endArray, isClosed) { + const start = [], end = []; + if (startArray.length > endArray.length) { + const temp = endArray; + endArray = startArray; + startArray = temp; + } + for (let n = 0; n < startArray.length; n += 2) { + start.push({ + x: startArray[n], + y: startArray[n + 1], + }); + } + for (let n = 0; n < endArray.length; n += 2) { + end.push({ + x: endArray[n], + y: endArray[n + 1], + }); + } + const newStart = []; + end.forEach(function (point) { + const pr = Util._getProjectionToLine(point, start, isClosed); + newStart.push(pr.x); + newStart.push(pr.y); + }); + return newStart; + }, + _prepareToStringify(obj) { + let desc; + obj.visitedByCircularReferenceRemoval = true; + for (const key in obj) { + if (!(obj.hasOwnProperty(key) && obj[key] && typeof obj[key] == 'object')) { + continue; + } + desc = Object.getOwnPropertyDescriptor(obj, key); + if (obj[key].visitedByCircularReferenceRemoval || + Util._isElement(obj[key])) { + if (desc.configurable) { + delete obj[key]; + } + else { + return null; + } + } + else if (Util._prepareToStringify(obj[key]) === null) { + if (desc.configurable) { + delete obj[key]; + } + else { + return null; + } + } + } + delete obj.visitedByCircularReferenceRemoval; + return obj; + }, + // very simplified version of Object.assign + _assign(target, source) { + for (const key in source) { + target[key] = source[key]; + } + return target; + }, + _getFirstPointerId(evt) { + if (!evt.touches) { + // try to use pointer id or fake id + return evt.pointerId || 999; + } + else { + return evt.changedTouches[0].identifier; + } + }, + releaseCanvas(...canvases) { + if (!Konva$2.releaseCanvasOnDestroy) + return; + canvases.forEach((c) => { + c.width = 0; + c.height = 0; + }); + }, + drawRoundedRectPath(context, width, height, cornerRadius) { + let topLeft = 0; + let topRight = 0; + let bottomLeft = 0; + let bottomRight = 0; + if (typeof cornerRadius === 'number') { + topLeft = + topRight = + bottomLeft = + bottomRight = + Math.min(cornerRadius, width / 2, height / 2); + } + else { + topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2); + topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2); + bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2); + bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2); + } + context.moveTo(topLeft, 0); + context.lineTo(width - topRight, 0); + context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false); + context.lineTo(width, height - bottomRight); + context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false); + context.lineTo(bottomLeft, height); + context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false); + context.lineTo(0, topLeft); + context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); + }, + }; + + function _formatValue(val) { + if (Util._isString(val)) { + return '"' + val + '"'; + } + if (Object.prototype.toString.call(val) === '[object Number]') { + return val; + } + if (Util._isBoolean(val)) { + return val; + } + return Object.prototype.toString.call(val); + } + function RGBComponent(val) { + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + return Math.round(val); + } + function getNumberValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + if (!Util._isNumber(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a number.'); + } + return val; + }; + } + } + function getNumberOrArrayOfNumbersValidator(noOfElements) { + if (Konva$2.isUnminified) { + return function (val, attr) { + let isNumber = Util._isNumber(val); + let isValidArray = Util._isArray(val) && val.length == noOfElements; + if (!isNumber && !isValidArray) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a number or Array(' + + noOfElements + + ')'); + } + return val; + }; + } + } + function getNumberOrAutoValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + var isNumber = Util._isNumber(val); + var isAuto = val === 'auto'; + if (!(isNumber || isAuto)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a number or "auto".'); + } + return val; + }; + } + } + function getStringValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + if (!Util._isString(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a string.'); + } + return val; + }; + } + } + function getStringOrGradientValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + const isString = Util._isString(val); + const isGradient = Object.prototype.toString.call(val) === '[object CanvasGradient]' || + (val && val['addColorStop']); + if (!(isString || isGradient)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a string or a native gradient.'); + } + return val; + }; + } + } + function getNumberArrayValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + // Retrieve TypedArray constructor as found in MDN (if TypedArray is available) + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description + const TypedArray = Int8Array ? Object.getPrototypeOf(Int8Array) : null; + if (TypedArray && val instanceof TypedArray) { + return val; + } + if (!Util._isArray(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a array of numbers.'); + } + else { + val.forEach(function (item) { + if (!Util._isNumber(item)) { + Util.warn('"' + + attr + + '" attribute has non numeric element ' + + item + + '. Make sure that all elements are numbers.'); + } + }); + } + return val; + }; + } + } + function getBooleanValidator() { + if (Konva$2.isUnminified) { + return function (val, attr) { + var isBool = val === true || val === false; + if (!isBool) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be a boolean.'); + } + return val; + }; + } + } + function getComponentValidator(components) { + if (Konva$2.isUnminified) { + return function (val, attr) { + // ignore validation on undefined value, because it will reset to defalt + if (val === undefined || val === null) { + return val; + } + if (!Util.isObject(val)) { + Util.warn(_formatValue(val) + + ' is a not valid value for "' + + attr + + '" attribute. The value should be an object with properties ' + + components); + } + return val; + }; + } + } + + const GET = 'get'; + const SET$1 = 'set'; + const Factory = { + addGetterSetter(constructor, attr, def, validator, after) { + Factory.addGetter(constructor, attr, def); + Factory.addSetter(constructor, attr, validator, after); + Factory.addOverloadedGetterSetter(constructor, attr); + }, + addGetter(constructor, attr, def) { + var method = GET + Util._capitalize(attr); + constructor.prototype[method] = + constructor.prototype[method] || + function () { + const val = this.attrs[attr]; + return val === undefined ? def : val; + }; + }, + addSetter(constructor, attr, validator, after) { + var method = SET$1 + Util._capitalize(attr); + if (!constructor.prototype[method]) { + Factory.overWriteSetter(constructor, attr, validator, after); + } + }, + overWriteSetter(constructor, attr, validator, after) { + var method = SET$1 + Util._capitalize(attr); + constructor.prototype[method] = function (val) { + if (validator && val !== undefined && val !== null) { + val = validator.call(this, val, attr); + } + this._setAttr(attr, val); + if (after) { + after.call(this); + } + return this; + }; + }, + addComponentsGetterSetter(constructor, attr, components, validator, after) { + const len = components.length, capitalize = Util._capitalize, getter = GET + capitalize(attr), setter = SET$1 + capitalize(attr); + // getter + constructor.prototype[getter] = function () { + const ret = {}; + for (let n = 0; n < len; n++) { + const component = components[n]; + ret[component] = this.getAttr(attr + capitalize(component)); + } + return ret; + }; + const basicValidator = getComponentValidator(components); + // setter + constructor.prototype[setter] = function (val) { + const oldVal = this.attrs[attr]; + if (validator) { + val = validator.call(this, val, attr); + } + if (basicValidator) { + basicValidator.call(this, val, attr); + } + for (const key in val) { + if (!val.hasOwnProperty(key)) { + continue; + } + this._setAttr(attr + capitalize(key), val[key]); + } + if (!val) { + components.forEach((component) => { + this._setAttr(attr + capitalize(component), undefined); + }); + } + this._fireChangeEvent(attr, oldVal, val); + if (after) { + after.call(this); + } + return this; + }; + Factory.addOverloadedGetterSetter(constructor, attr); + }, + addOverloadedGetterSetter(constructor, attr) { + var capitalizedAttr = Util._capitalize(attr), setter = SET$1 + capitalizedAttr, getter = GET + capitalizedAttr; + constructor.prototype[attr] = function () { + // setting + if (arguments.length) { + this[setter](arguments[0]); + return this; + } + // getting + return this[getter](); + }; + }, + addDeprecatedGetterSetter(constructor, attr, def, validator) { + Util.error('Adding deprecated ' + attr); + const method = GET + Util._capitalize(attr); + const message = attr + + ' property is deprecated and will be removed soon. Look at Konva change log for more information.'; + constructor.prototype[method] = function () { + Util.error(message); + const val = this.attrs[attr]; + return val === undefined ? def : val; + }; + Factory.addSetter(constructor, attr, validator, function () { + Util.error(message); + }); + Factory.addOverloadedGetterSetter(constructor, attr); + }, + backCompat(constructor, methods) { + Util.each(methods, function (oldMethodName, newMethodName) { + const method = constructor.prototype[newMethodName]; + const oldGetter = GET + Util._capitalize(oldMethodName); + const oldSetter = SET$1 + Util._capitalize(oldMethodName); + function deprecated() { + method.apply(this, arguments); + Util.error('"' + + oldMethodName + + '" method is deprecated and will be removed soon. Use ""' + + newMethodName + + '" instead.'); + } + constructor.prototype[oldMethodName] = deprecated; + constructor.prototype[oldGetter] = deprecated; + constructor.prototype[oldSetter] = deprecated; + }); + }, + afterSetFilter() { + this._filterUpToDate = false; + }, + }; + + function simplifyArray(arr) { + const retArr = [], len = arr.length, util = Util; + for (let n = 0; n < len; n++) { + let val = arr[n]; + if (util._isNumber(val)) { + val = Math.round(val * 1000) / 1000; + } + else if (!util._isString(val)) { + val = val + ''; + } + retArr.push(val); + } + return retArr; + } + const COMMA = ',', OPEN_PAREN = '(', CLOSE_PAREN = ')', OPEN_PAREN_BRACKET = '([', CLOSE_BRACKET_PAREN = '])', SEMICOLON = ';', DOUBLE_PAREN = '()', + // EMPTY_STRING = '', + EQUALS = '=', + // SET = 'set', + CONTEXT_METHODS = [ + 'arc', + 'arcTo', + 'beginPath', + 'bezierCurveTo', + 'clearRect', + 'clip', + 'closePath', + 'createLinearGradient', + 'createPattern', + 'createRadialGradient', + 'drawImage', + 'ellipse', + 'fill', + 'fillText', + 'getImageData', + 'createImageData', + 'lineTo', + 'moveTo', + 'putImageData', + 'quadraticCurveTo', + 'rect', + 'roundRect', + 'restore', + 'rotate', + 'save', + 'scale', + 'setLineDash', + 'setTransform', + 'stroke', + 'strokeText', + 'transform', + 'translate', + ]; + const CONTEXT_PROPERTIES = [ + 'fillStyle', + 'strokeStyle', + 'shadowColor', + 'shadowBlur', + 'shadowOffsetX', + 'shadowOffsetY', + 'letterSpacing', + 'lineCap', + 'lineDashOffset', + 'lineJoin', + 'lineWidth', + 'miterLimit', + 'direction', + 'font', + 'textAlign', + 'textBaseline', + 'globalAlpha', + 'globalCompositeOperation', + 'imageSmoothingEnabled', + ]; + const traceArrMax = 100; + /** + * Konva wrapper around native 2d canvas context. It has almost the same API of 2d context with some additional functions. + * With core Konva shapes you don't need to use this object. But you will use it if you want to create + * a [custom shape](/docs/react/Custom_Shape.html) or a [custom hit regions](/docs/events/Custom_Hit_Region.html). + * For full information about each 2d context API use [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) + * @constructor + * @memberof Konva + * @example + * const rect = new Konva.Shape({ + * fill: 'red', + * width: 100, + * height: 100, + * sceneFunc: (ctx, shape) => { + * // ctx - is context wrapper + * // shape - is instance of Konva.Shape, so it equals to "rect" variable + * ctx.rect(0, 0, shape.getAttr('width'), shape.getAttr('height')); + * + * // automatically fill shape from props and draw hit region + * ctx.fillStrokeShape(shape); + * } + * }) + */ + class Context { + constructor(canvas) { + this.canvas = canvas; + if (Konva$2.enableTrace) { + this.traceArr = []; + this._enableTrace(); + } + } + /** + * fill shape + * @method + * @name Konva.Context#fillShape + * @param {Konva.Shape} shape + */ + fillShape(shape) { + if (shape.fillEnabled()) { + this._fill(shape); + } + } + _fill(shape) { + // abstract + } + /** + * stroke shape + * @method + * @name Konva.Context#strokeShape + * @param {Konva.Shape} shape + */ + strokeShape(shape) { + if (shape.hasStroke()) { + this._stroke(shape); + } + } + _stroke(shape) { + // abstract + } + /** + * fill then stroke + * @method + * @name Konva.Context#fillStrokeShape + * @param {Konva.Shape} shape + */ + fillStrokeShape(shape) { + if (shape.attrs.fillAfterStrokeEnabled) { + this.strokeShape(shape); + this.fillShape(shape); + } + else { + this.fillShape(shape); + this.strokeShape(shape); + } + } + getTrace(relaxed, rounded) { + let traceArr = this.traceArr, len = traceArr.length, str = '', n, trace, method, args; + for (n = 0; n < len; n++) { + trace = traceArr[n]; + method = trace.method; + // methods + if (method) { + args = trace.args; + str += method; + if (relaxed) { + str += DOUBLE_PAREN; + } + else { + if (Util._isArray(args[0])) { + str += OPEN_PAREN_BRACKET + args.join(COMMA) + CLOSE_BRACKET_PAREN; + } + else { + if (rounded) { + args = args.map((a) => typeof a === 'number' ? Math.floor(a) : a); + } + str += OPEN_PAREN + args.join(COMMA) + CLOSE_PAREN; + } + } + } + else { + // properties + str += trace.property; + if (!relaxed) { + str += EQUALS + trace.val; + } + } + str += SEMICOLON; + } + return str; + } + clearTrace() { + this.traceArr = []; + } + _trace(str) { + let traceArr = this.traceArr, len; + traceArr.push(str); + len = traceArr.length; + if (len >= traceArrMax) { + traceArr.shift(); + } + } + /** + * reset canvas context transform + * @method + * @name Konva.Context#reset + */ + reset() { + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(1 * pixelRatio, 0, 0, 1 * pixelRatio, 0, 0); + } + /** + * get canvas wrapper + * @method + * @name Konva.Context#getCanvas + * @returns {Konva.Canvas} + */ + getCanvas() { + return this.canvas; + } + /** + * clear canvas + * @method + * @name Konva.Context#clear + * @param {Object} [bounds] + * @param {Number} [bounds.x] + * @param {Number} [bounds.y] + * @param {Number} [bounds.width] + * @param {Number} [bounds.height] + */ + clear(bounds) { + const canvas = this.getCanvas(); + if (bounds) { + this.clearRect(bounds.x || 0, bounds.y || 0, bounds.width || 0, bounds.height || 0); + } + else { + this.clearRect(0, 0, canvas.getWidth() / canvas.pixelRatio, canvas.getHeight() / canvas.pixelRatio); + } + } + _applyLineCap(shape) { + const lineCap = shape.attrs.lineCap; + if (lineCap) { + this.setAttr('lineCap', lineCap); + } + } + _applyOpacity(shape) { + const absOpacity = shape.getAbsoluteOpacity(); + if (absOpacity !== 1) { + this.setAttr('globalAlpha', absOpacity); + } + } + _applyLineJoin(shape) { + const lineJoin = shape.attrs.lineJoin; + if (lineJoin) { + this.setAttr('lineJoin', lineJoin); + } + } + setAttr(attr, val) { + this._context[attr] = val; + } + /** + * arc function. + * @method + * @name Konva.Context#arc + */ + arc(x, y, radius, startAngle, endAngle, counterClockwise) { + this._context.arc(x, y, radius, startAngle, endAngle, counterClockwise); + } + /** + * arcTo function. + * @method + * @name Konva.Context#arcTo + * + */ + arcTo(x1, y1, x2, y2, radius) { + this._context.arcTo(x1, y1, x2, y2, radius); + } + /** + * beginPath function. + * @method + * @name Konva.Context#beginPath + */ + beginPath() { + this._context.beginPath(); + } + /** + * bezierCurveTo function. + * @method + * @name Konva.Context#bezierCurveTo + */ + bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { + this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); + } + /** + * clearRect function. + * @method + * @name Konva.Context#clearRect + */ + clearRect(x, y, width, height) { + this._context.clearRect(x, y, width, height); + } + clip(...args) { + this._context.clip.apply(this._context, args); + } + /** + * closePath function. + * @method + * @name Konva.Context#closePath + */ + closePath() { + this._context.closePath(); + } + /** + * createImageData function. + * @method + * @name Konva.Context#createImageData + */ + createImageData(width, height) { + const a = arguments; + if (a.length === 2) { + return this._context.createImageData(width, height); + } + else if (a.length === 1) { + return this._context.createImageData(width); + } + } + /** + * createLinearGradient function. + * @method + * @name Konva.Context#createLinearGradient + */ + createLinearGradient(x0, y0, x1, y1) { + return this._context.createLinearGradient(x0, y0, x1, y1); + } + /** + * createPattern function. + * @method + * @name Konva.Context#createPattern + */ + createPattern(image, repetition) { + return this._context.createPattern(image, repetition); + } + /** + * createRadialGradient function. + * @method + * @name Konva.Context#createRadialGradient + */ + createRadialGradient(x0, y0, r0, x1, y1, r1) { + return this._context.createRadialGradient(x0, y0, r0, x1, y1, r1); + } + /** + * drawImage function. + * @method + * @name Konva.Context#drawImage + */ + drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) { + // this._context.drawImage(...arguments); + const a = arguments, _context = this._context; + if (a.length === 3) { + _context.drawImage(image, sx, sy); + } + else if (a.length === 5) { + _context.drawImage(image, sx, sy, sWidth, sHeight); + } + else if (a.length === 9) { + _context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); + } + } + /** + * ellipse function. + * @method + * @name Konva.Context#ellipse + */ + ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) { + this._context.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise); + } + /** + * isPointInPath function. + * @method + * @name Konva.Context#isPointInPath + */ + isPointInPath(x, y, path, fillRule) { + if (path) { + return this._context.isPointInPath(path, x, y, fillRule); + } + return this._context.isPointInPath(x, y, fillRule); + } + fill(...args) { + // this._context.fill(); + this._context.fill.apply(this._context, args); + } + /** + * fillRect function. + * @method + * @name Konva.Context#fillRect + */ + fillRect(x, y, width, height) { + this._context.fillRect(x, y, width, height); + } + /** + * strokeRect function. + * @method + * @name Konva.Context#strokeRect + */ + strokeRect(x, y, width, height) { + this._context.strokeRect(x, y, width, height); + } + /** + * fillText function. + * @method + * @name Konva.Context#fillText + */ + fillText(text, x, y, maxWidth) { + if (maxWidth) { + this._context.fillText(text, x, y, maxWidth); + } + else { + this._context.fillText(text, x, y); + } + } + /** + * measureText function. + * @method + * @name Konva.Context#measureText + */ + measureText(text) { + return this._context.measureText(text); + } + /** + * getImageData function. + * @method + * @name Konva.Context#getImageData + */ + getImageData(sx, sy, sw, sh) { + return this._context.getImageData(sx, sy, sw, sh); + } + /** + * lineTo function. + * @method + * @name Konva.Context#lineTo + */ + lineTo(x, y) { + this._context.lineTo(x, y); + } + /** + * moveTo function. + * @method + * @name Konva.Context#moveTo + */ + moveTo(x, y) { + this._context.moveTo(x, y); + } + /** + * rect function. + * @method + * @name Konva.Context#rect + */ + rect(x, y, width, height) { + this._context.rect(x, y, width, height); + } + /** + * roundRect function. + * @method + * @name Konva.Context#roundRect + */ + roundRect(x, y, width, height, radii) { + this._context.roundRect(x, y, width, height, radii); + } + /** + * putImageData function. + * @method + * @name Konva.Context#putImageData + */ + putImageData(imageData, dx, dy) { + this._context.putImageData(imageData, dx, dy); + } + /** + * quadraticCurveTo function. + * @method + * @name Konva.Context#quadraticCurveTo + */ + quadraticCurveTo(cpx, cpy, x, y) { + this._context.quadraticCurveTo(cpx, cpy, x, y); + } + /** + * restore function. + * @method + * @name Konva.Context#restore + */ + restore() { + this._context.restore(); + } + /** + * rotate function. + * @method + * @name Konva.Context#rotate + */ + rotate(angle) { + this._context.rotate(angle); + } + /** + * save function. + * @method + * @name Konva.Context#save + */ + save() { + this._context.save(); + } + /** + * scale function. + * @method + * @name Konva.Context#scale + */ + scale(x, y) { + this._context.scale(x, y); + } + /** + * setLineDash function. + * @method + * @name Konva.Context#setLineDash + */ + setLineDash(segments) { + // works for Chrome and IE11 + if (this._context.setLineDash) { + this._context.setLineDash(segments); + } + else if ('mozDash' in this._context) { + // verified that this works in firefox + this._context['mozDash'] = segments; + } + else if ('webkitLineDash' in this._context) { + // does not currently work for Safari + this._context['webkitLineDash'] = segments; + } + // no support for IE9 and IE10 + } + /** + * getLineDash function. + * @method + * @name Konva.Context#getLineDash + */ + getLineDash() { + return this._context.getLineDash(); + } + /** + * setTransform function. + * @method + * @name Konva.Context#setTransform + */ + setTransform(a, b, c, d, e, f) { + this._context.setTransform(a, b, c, d, e, f); + } + /** + * stroke function. + * @method + * @name Konva.Context#stroke + */ + stroke(path2d) { + if (path2d) { + this._context.stroke(path2d); + } + else { + this._context.stroke(); + } + } + /** + * strokeText function. + * @method + * @name Konva.Context#strokeText + */ + strokeText(text, x, y, maxWidth) { + this._context.strokeText(text, x, y, maxWidth); + } + /** + * transform function. + * @method + * @name Konva.Context#transform + */ + transform(a, b, c, d, e, f) { + this._context.transform(a, b, c, d, e, f); + } + /** + * translate function. + * @method + * @name Konva.Context#translate + */ + translate(x, y) { + this._context.translate(x, y); + } + _enableTrace() { + let that = this, len = CONTEXT_METHODS.length, origSetter = this.setAttr, n, args; + // to prevent creating scope function at each loop + const func = function (methodName) { + let origMethod = that[methodName], ret; + that[methodName] = function () { + args = simplifyArray(Array.prototype.slice.call(arguments, 0)); + ret = origMethod.apply(that, arguments); + that._trace({ + method: methodName, + args: args, + }); + return ret; + }; + }; + // methods + for (n = 0; n < len; n++) { + func(CONTEXT_METHODS[n]); + } + // attrs + that.setAttr = function () { + origSetter.apply(that, arguments); + const prop = arguments[0]; + let val = arguments[1]; + if (prop === 'shadowOffsetX' || + prop === 'shadowOffsetY' || + prop === 'shadowBlur') { + val = val / this.canvas.getPixelRatio(); + } + that._trace({ + property: prop, + val: val, + }); + }; + } + _applyGlobalCompositeOperation(node) { + const op = node.attrs.globalCompositeOperation; + const def = !op || op === 'source-over'; + if (!def) { + this.setAttr('globalCompositeOperation', op); + } + } + } + CONTEXT_PROPERTIES.forEach(function (prop) { + Object.defineProperty(Context.prototype, prop, { + get() { + return this._context[prop]; + }, + set(val) { + this._context[prop] = val; + }, + }); + }); + class SceneContext extends Context { + constructor(canvas, { willReadFrequently = false } = {}) { + super(canvas); + this._context = canvas._canvas.getContext('2d', { + willReadFrequently, + }); + } + _fillColor(shape) { + const fill = shape.fill(); + this.setAttr('fillStyle', fill); + shape._fillFunc(this); + } + _fillPattern(shape) { + this.setAttr('fillStyle', shape._getFillPattern()); + shape._fillFunc(this); + } + _fillLinearGradient(shape) { + const grd = shape._getLinearGradient(); + if (grd) { + this.setAttr('fillStyle', grd); + shape._fillFunc(this); + } + } + _fillRadialGradient(shape) { + const grd = shape._getRadialGradient(); + if (grd) { + this.setAttr('fillStyle', grd); + shape._fillFunc(this); + } + } + _fill(shape) { + const hasColor = shape.fill(), fillPriority = shape.getFillPriority(); + // priority fills + if (hasColor && fillPriority === 'color') { + this._fillColor(shape); + return; + } + const hasPattern = shape.getFillPatternImage(); + if (hasPattern && fillPriority === 'pattern') { + this._fillPattern(shape); + return; + } + const hasLinearGradient = shape.getFillLinearGradientColorStops(); + if (hasLinearGradient && fillPriority === 'linear-gradient') { + this._fillLinearGradient(shape); + return; + } + const hasRadialGradient = shape.getFillRadialGradientColorStops(); + if (hasRadialGradient && fillPriority === 'radial-gradient') { + this._fillRadialGradient(shape); + return; + } + // now just try and fill with whatever is available + if (hasColor) { + this._fillColor(shape); + } + else if (hasPattern) { + this._fillPattern(shape); + } + else if (hasLinearGradient) { + this._fillLinearGradient(shape); + } + else if (hasRadialGradient) { + this._fillRadialGradient(shape); + } + } + _strokeLinearGradient(shape) { + const start = shape.getStrokeLinearGradientStartPoint(), end = shape.getStrokeLinearGradientEndPoint(), colorStops = shape.getStrokeLinearGradientColorStops(), grd = this.createLinearGradient(start.x, start.y, end.x, end.y); + if (colorStops) { + // build color stops + for (let n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + this.setAttr('strokeStyle', grd); + } + } + _stroke(shape) { + const dash = shape.dash(), + // ignore strokeScaleEnabled for Text + strokeScaleEnabled = shape.getStrokeScaleEnabled(); + if (shape.hasStroke()) { + if (!strokeScaleEnabled) { + this.save(); + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + } + this._applyLineCap(shape); + if (dash && shape.dashEnabled()) { + this.setLineDash(dash); + this.setAttr('lineDashOffset', shape.dashOffset()); + } + this.setAttr('lineWidth', shape.strokeWidth()); + if (!shape.getShadowForStrokeEnabled()) { + this.setAttr('shadowColor', 'rgba(0,0,0,0)'); + } + const hasLinearGradient = shape.getStrokeLinearGradientColorStops(); + if (hasLinearGradient) { + this._strokeLinearGradient(shape); + } + else { + this.setAttr('strokeStyle', shape.stroke()); + } + shape._strokeFunc(this); + if (!strokeScaleEnabled) { + this.restore(); + } + } + } + _applyShadow(shape) { + var _a, _b, _c; + const color = (_a = shape.getShadowRGBA()) !== null && _a !== undefined ? _a : 'black', blur = (_b = shape.getShadowBlur()) !== null && _b !== undefined ? _b : 5, offset = (_c = shape.getShadowOffset()) !== null && _c !== undefined ? _c : { + x: 0, + y: 0, + }, scale = shape.getAbsoluteScale(), ratio = this.canvas.getPixelRatio(), scaleX = scale.x * ratio, scaleY = scale.y * ratio; + this.setAttr('shadowColor', color); + this.setAttr('shadowBlur', blur * Math.min(Math.abs(scaleX), Math.abs(scaleY))); + this.setAttr('shadowOffsetX', offset.x * scaleX); + this.setAttr('shadowOffsetY', offset.y * scaleY); + } + } + class HitContext extends Context { + constructor(canvas) { + super(canvas); + this._context = canvas._canvas.getContext('2d', { + willReadFrequently: true, + }); + } + _fill(shape) { + this.save(); + this.setAttr('fillStyle', shape.colorKey); + shape._fillFuncHit(this); + this.restore(); + } + strokeShape(shape) { + if (shape.hasHitStroke()) { + this._stroke(shape); + } + } + _stroke(shape) { + if (shape.hasHitStroke()) { + // ignore strokeScaleEnabled for Text + const strokeScaleEnabled = shape.getStrokeScaleEnabled(); + if (!strokeScaleEnabled) { + this.save(); + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + } + this._applyLineCap(shape); + const hitStrokeWidth = shape.hitStrokeWidth(); + const strokeWidth = hitStrokeWidth === 'auto' ? shape.strokeWidth() : hitStrokeWidth; + this.setAttr('lineWidth', strokeWidth); + this.setAttr('strokeStyle', shape.colorKey); + shape._strokeFuncHit(this); + if (!strokeScaleEnabled) { + this.restore(); + } + } + } + } + + // calculate pixel ratio + let _pixelRatio; + function getDevicePixelRatio() { + if (_pixelRatio) { + return _pixelRatio; + } + const canvas = Util.createCanvasElement(); + const context = canvas.getContext('2d'); + _pixelRatio = (function () { + const devicePixelRatio = Konva$2._global.devicePixelRatio || 1, backingStoreRatio = context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || + 1; + return devicePixelRatio / backingStoreRatio; + })(); + Util.releaseCanvas(canvas); + return _pixelRatio; + } + /** + * Canvas Renderer constructor. It is a wrapper around native canvas element. + * Usually you don't need to use it manually. + * @constructor + * @abstract + * @memberof Konva + * @param {Object} config + * @param {Number} config.width + * @param {Number} config.height + * @param {Number} config.pixelRatio + */ + class Canvas { + constructor(config) { + this.pixelRatio = 1; + this.width = 0; + this.height = 0; + this.isCache = false; + const conf = config || {}; + const pixelRatio = conf.pixelRatio || Konva$2.pixelRatio || getDevicePixelRatio(); + this.pixelRatio = pixelRatio; + this._canvas = Util.createCanvasElement(); + // set inline styles + this._canvas.style.padding = '0'; + this._canvas.style.margin = '0'; + this._canvas.style.border = '0'; + this._canvas.style.background = 'transparent'; + this._canvas.style.position = 'absolute'; + this._canvas.style.top = '0'; + this._canvas.style.left = '0'; + } + /** + * get canvas context + * @method + * @name Konva.Canvas#getContext + * @returns {CanvasContext} context + */ + getContext() { + return this.context; + } + /** + * get pixel ratio + * @method + * @name Konva.Canvas#getPixelRatio + * @returns {Number} pixel ratio + * @example + * var pixelRatio = layer.getCanvas.getPixelRatio(); + */ + getPixelRatio() { + return this.pixelRatio; + } + /** + * set pixel ratio + * KonvaJS automatically handles pixel ratio adustments in order to render crisp drawings + * on all devices. Most desktops, low end tablets, and low end phones, have device pixel ratios + * of 1. Some high end tablets and phones, like iPhones and iPads have a device pixel ratio + * of 2. Some Macbook Pros, and iMacs also have a device pixel ratio of 2. Some high end Android devices have pixel + * ratios of 2 or 3. Some browsers like Firefox allow you to configure the pixel ratio of the viewport. Unless otherwise + * specificed, the pixel ratio will be defaulted to the actual device pixel ratio. You can override the device pixel + * ratio for special situations, or, if you don't want the pixel ratio to be taken into account, you can set it to 1. + * @method + * @name Konva.Canvas#setPixelRatio + * @param {Number} pixelRatio + * @example + * layer.getCanvas().setPixelRatio(3); + */ + setPixelRatio(pixelRatio) { + const previousRatio = this.pixelRatio; + this.pixelRatio = pixelRatio; + this.setSize(this.getWidth() / previousRatio, this.getHeight() / previousRatio); + } + setWidth(width) { + // take into account pixel ratio + this.width = this._canvas.width = width * this.pixelRatio; + this._canvas.style.width = width + 'px'; + const pixelRatio = this.pixelRatio, _context = this.getContext()._context; + _context.scale(pixelRatio, pixelRatio); + } + setHeight(height) { + // take into account pixel ratio + this.height = this._canvas.height = height * this.pixelRatio; + this._canvas.style.height = height + 'px'; + const pixelRatio = this.pixelRatio, _context = this.getContext()._context; + _context.scale(pixelRatio, pixelRatio); + } + getWidth() { + return this.width; + } + getHeight() { + return this.height; + } + setSize(width, height) { + this.setWidth(width || 0); + this.setHeight(height || 0); + } + /** + * to data url + * @method + * @name Konva.Canvas#toDataURL + * @param {String} mimeType + * @param {Number} quality between 0 and 1 for jpg mime types + * @returns {String} data url string + */ + toDataURL(mimeType, quality) { + try { + // If this call fails (due to browser bug, like in Firefox 3.6), + // then revert to previous no-parameter image/png behavior + return this._canvas.toDataURL(mimeType, quality); + } + catch (e) { + try { + return this._canvas.toDataURL(); + } + catch (err) { + Util.error('Unable to get data URL. ' + + err.message + + ' For more info read https://konvajs.org/docs/posts/Tainted_Canvas.html.'); + return ''; + } + } + } + } + class SceneCanvas extends Canvas { + constructor(config = { width: 0, height: 0, willReadFrequently: false }) { + super(config); + this.context = new SceneContext(this, { + willReadFrequently: config.willReadFrequently, + }); + this.setSize(config.width, config.height); + } + } + class HitCanvas extends Canvas { + constructor(config = { width: 0, height: 0 }) { + super(config); + this.hitCanvas = true; + this.context = new HitContext(this); + this.setSize(config.width, config.height); + } + } + + const DD = { + get isDragging() { + let flag = false; + DD._dragElements.forEach((elem) => { + if (elem.dragStatus === 'dragging') { + flag = true; + } + }); + return flag; + }, + justDragged: false, + get node() { + // return first dragging node + let node; + DD._dragElements.forEach((elem) => { + node = elem.node; + }); + return node; + }, + _dragElements: new Map(), + // methods + _drag(evt) { + const nodesToFireEvents = []; + DD._dragElements.forEach((elem, key) => { + const { node } = elem; + // we need to find pointer relative to that node + const stage = node.getStage(); + stage.setPointersPositions(evt); + // it is possible that user call startDrag without any event + // it that case we need to detect first movable pointer and attach it into the node + if (elem.pointerId === undefined) { + elem.pointerId = Util._getFirstPointerId(evt); + } + const pos = stage._changedPointerPositions.find((pos) => pos.id === elem.pointerId); + // not related pointer + if (!pos) { + return; + } + if (elem.dragStatus !== 'dragging') { + const dragDistance = node.dragDistance(); + const distance = Math.max(Math.abs(pos.x - elem.startPointerPos.x), Math.abs(pos.y - elem.startPointerPos.y)); + if (distance < dragDistance) { + return; + } + node.startDrag({ evt }); + // a user can stop dragging inside `dragstart` + if (!node.isDragging()) { + return; + } + } + node._setDragPosition(evt, elem); + nodesToFireEvents.push(node); + }); + // call dragmove only after ALL positions are changed + nodesToFireEvents.forEach((node) => { + node.fire('dragmove', { + type: 'dragmove', + target: node, + evt: evt, + }, true); + }); + }, + // dragBefore and dragAfter allows us to set correct order of events + // setup all in dragbefore, and stop dragging only after pointerup triggered. + _endDragBefore(evt) { + const drawNodes = []; + DD._dragElements.forEach((elem) => { + const { node } = elem; + // we need to find pointer relative to that node + const stage = node.getStage(); + if (evt) { + stage.setPointersPositions(evt); + } + const pos = stage._changedPointerPositions.find((pos) => pos.id === elem.pointerId); + // that pointer is not related + if (!pos) { + return; + } + if (elem.dragStatus === 'dragging' || elem.dragStatus === 'stopped') { + // if a node is stopped manually we still need to reset events: + DD.justDragged = true; + Konva$2._mouseListenClick = false; + Konva$2._touchListenClick = false; + Konva$2._pointerListenClick = false; + elem.dragStatus = 'stopped'; + } + const drawNode = elem.node.getLayer() || + (elem.node instanceof Konva$2['Stage'] && elem.node); + if (drawNode && drawNodes.indexOf(drawNode) === -1) { + drawNodes.push(drawNode); + } + }); + // draw in a sync way + // because mousemove event may trigger BEFORE batch draw is called + // but as we have not hit canvas updated yet, it will trigger incorrect mouseover/mouseout events + drawNodes.forEach((drawNode) => { + drawNode.draw(); + }); + }, + _endDragAfter(evt) { + DD._dragElements.forEach((elem, key) => { + if (elem.dragStatus === 'stopped') { + elem.node.fire('dragend', { + type: 'dragend', + target: elem.node, + evt: evt, + }, true); + } + if (elem.dragStatus !== 'dragging') { + DD._dragElements.delete(key); + } + }); + }, + }; + if (Konva$2.isBrowser) { + window.addEventListener('mouseup', DD._endDragBefore, true); + window.addEventListener('touchend', DD._endDragBefore, true); + // add touchcancel to fix this: https://github.com/konvajs/konva/issues/1843 + window.addEventListener('touchcancel', DD._endDragBefore, true); + window.addEventListener('mousemove', DD._drag); + window.addEventListener('touchmove', DD._drag); + window.addEventListener('mouseup', DD._endDragAfter, false); + window.addEventListener('touchend', DD._endDragAfter, false); + window.addEventListener('touchcancel', DD._endDragAfter, false); + } + + // CONSTANTS + const ABSOLUTE_OPACITY = 'absoluteOpacity', ALL_LISTENERS = 'allEventListeners', ABSOLUTE_TRANSFORM = 'absoluteTransform', ABSOLUTE_SCALE = 'absoluteScale', CANVAS = 'canvas', CHANGE = 'Change', CHILDREN = 'children', KONVA = 'konva', LISTENING = 'listening', MOUSEENTER$1 = 'mouseenter', MOUSELEAVE$1 = 'mouseleave', SET = 'set', SHAPE = 'Shape', SPACE$1 = ' ', STAGE$1 = 'stage', TRANSFORM = 'transform', UPPER_STAGE = 'Stage', VISIBLE = 'visible', TRANSFORM_CHANGE_STR$1 = [ + 'xChange.konva', + 'yChange.konva', + 'scaleXChange.konva', + 'scaleYChange.konva', + 'skewXChange.konva', + 'skewYChange.konva', + 'rotationChange.konva', + 'offsetXChange.konva', + 'offsetYChange.konva', + 'transformsEnabledChange.konva', + ].join(SPACE$1); + let idCounter$1 = 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 + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + */ + class Node { + constructor(config) { + this._id = idCounter$1++; + this.eventListeners = {}; + this.attrs = {}; + this.index = 0; + this._allEventListeners = null; + this.parent = null; + this._cache = new Map(); + this._attachedDepsListeners = new Map(); + this._lastPos = 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.get(attr)) { + this._cache.get(attr).dirty = true; + } + else if (attr) { + this._cache.delete(attr); + } + else { + this._cache.clear(); + } + } + _getCache(attr, privateGetter) { + let cache = this._cache.get(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.set(attr, cache); + } + return cache; + } + _calculate(name, deps, getter) { + // if we are trying to calculate function for the first time + // we need to attach listeners for change events + if (!this._attachedDepsListeners.get(name)) { + const depsString = deps.map((dep) => dep + 'Change.konva').join(SPACE$1); + this.on(depsString, () => { + this._clearCache(name); + }); + this._attachedDepsListeners.set(name, true); + } + // just use cache function + return this._getCache(name, getter); + } + _getCanvasCache() { + return this._cache.get(CANVAS); + } + /* + * 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 === ABSOLUTE_TRANSFORM) { + this.fire('absoluteTransformChange'); + } + } + /** + * clear cached canvas + * @method + * @name Konva.Node#clearCache + * @returns {Konva.Node} + * @example + * node.clearCache(); + */ + clearCache() { + if (this._cache.has(CANVAS)) { + const { scene, filter, hit } = this._cache.get(CANVAS); + Util.releaseCanvas(scene, filter, hit); + this._cache.delete(CANVAS); + } + 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] change quality (or pixel ratio) of cached image. pixelRatio = 2 will produce 2x sized cache. + * @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) { + rect = this.getClientRect({ + skipTransform: true, + relativeTo: this.getParent() || undefined, + }); + } + let width = Math.ceil(conf.width || rect.width), height = Math.ceil(conf.height || rect.height), pixelRatio = conf.pixelRatio, x = conf.x === undefined ? Math.floor(rect.x) : conf.x, y = conf.y === undefined ? Math.floor(rect.y) : conf.y, offset = conf.offset || 0, drawBorder = conf.drawBorder || false, hitCanvasPixelRatio = conf.hitCanvasPixelRatio || 1; + if (!width || !height) { + Util.error('Can not cache the node. Width or height of the node equals 0. Caching is skipped.'); + return; + } + // because using Math.floor on x, y position may shift drawing + // to avoid shift we need to increase size + // but we better to avoid it, for better filters flows + const extraPaddingX = Math.abs(Math.round(rect.x) - x) > 0.5 ? 1 : 0; + const extraPaddingY = Math.abs(Math.round(rect.y) - y) > 0.5 ? 1 : 0; + width += offset * 2 + extraPaddingX; + height += offset * 2 + extraPaddingY; + x -= offset; + y -= offset; + // if (Math.floor(x) < x) { + // x = Math.floor(x); + // // width += 1; + // } + // if (Math.floor(y) < y) { + // y = Math.floor(y); + // // height += 1; + // } + // console.log({ x, y, width, height }, rect); + const cachedSceneCanvas = new SceneCanvas({ + pixelRatio: pixelRatio, + width: width, + height: height, + }), cachedFilterCanvas = new SceneCanvas({ + pixelRatio: pixelRatio, + width: 0, + height: 0, + willReadFrequently: true, + }), cachedHitCanvas = new HitCanvas({ + pixelRatio: hitCanvasPixelRatio, + width: width, + height: height, + }), sceneContext = cachedSceneCanvas.getContext(), hitContext = cachedHitCanvas.getContext(); + cachedHitCanvas.isCache = true; + cachedSceneCanvas.isCache = true; + this._cache.delete(CANVAS); + this._filterUpToDate = false; + if (conf.imageSmoothingEnabled === false) { + cachedSceneCanvas.getContext()._context.imageSmoothingEnabled = false; + cachedFilterCanvas.getContext()._context.imageSmoothingEnabled = false; + } + sceneContext.save(); + hitContext.save(); + sceneContext.translate(-x, -y); + hitContext.translate(-x, -y); + // extra flag to skip on getAbsolute opacity calc + this._isUnderCache = true; + this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); + this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); + this.drawScene(cachedSceneCanvas, this); + this.drawHit(cachedHitCanvas, this); + this._isUnderCache = false; + sceneContext.restore(); + hitContext.restore(); + // 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._cache.set(CANVAS, { + scene: cachedSceneCanvas, + filter: cachedFilterCanvas, + hit: cachedHitCanvas, + x: x, + y: y, + }); + this._requestDraw(); + return this; + } + /** + * determine if node is currently cached + * @method + * @name Konva.Node#isCached + * @returns {Boolean} + */ + isCached() { + return this._cache.has(CANVAS); + } + /** + * 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. + * @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'); + } + _transformedRect(rect, top) { + const points = [ + { x: rect.x, y: rect.y }, + { x: rect.x + rect.width, y: rect.y }, + { x: rect.x + rect.width, y: rect.y + rect.height }, + { x: rect.x, y: rect.y + rect.height }, + ]; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + const trans = this.getAbsoluteTransform(top); + points.forEach(function (point) { + const transformed = trans.point(point); + if (minX === undefined) { + minX = maxX = transformed.x; + minY = maxY = transformed.y; + } + minX = Math.min(minX, transformed.x); + minY = Math.min(minY, transformed.y); + maxX = Math.max(maxX, transformed.x); + maxY = Math.max(maxY, transformed.y); + }); + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + _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) { + const canvasCache = this._getCanvasCache(), hitCanvas = canvasCache.hit; + 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) { + if (!this._filterUpToDate) { + const ratio = sceneCanvas.pixelRatio; + filterCanvas.setSize(sceneCanvas.width / sceneCanvas.pixelRatio, sceneCanvas.height / sceneCanvas.pixelRatio); + 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 !== 'function') { + Util.error('Filter should be type of function, but got ' + + typeof filter + + ' instead. Please check correct filters'); + continue; + } + filter.call(this, imageData); + 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; + } + return sceneCanvas; + } + /** + * 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, and dragend 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(evtStr, handler) { + this._cache && this._cache.delete(ALL_LISTENERS); + if (arguments.length === 3) { + return this._delegate.apply(this, arguments); + } + let events = evtStr.split(SPACE$1), len = events.length, n, event, parts, baseEvent, name; + /* + * loop through types and attach event listeners to + * each one. eg. 'click mouseover.namespace mouseout' + * will create three event bindings + */ + for (n = 0; n < len; n++) { + event = events[n]; + parts = event.split('.'); + baseEvent = parts[0]; + name = parts[1] || ''; + // create events array if it doesn't exist + if (!this.eventListeners[baseEvent]) { + this.eventListeners[baseEvent] = []; + } + this.eventListeners[baseEvent].push({ + name: name, + handler: handler, + }); + } + 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'); + */ + off(evtStr, callback) { + let events = (evtStr || '').split(SPACE$1), len = events.length, n, t, event, parts, baseEvent, name; + this._cache && this._cache.delete(ALL_LISTENERS); + 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); + } + }); + } + /** + * 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); + this._remove(); + return this; + } + _clearCaches() { + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); + this._clearSelfAndDescendantCache(ABSOLUTE_SCALE); + this._clearSelfAndDescendantCache(STAGE$1); + this._clearSelfAndDescendantCache(VISIBLE); + this._clearSelfAndDescendantCache(LISTENING); + } + _remove() { + // every cached attr that is calculated via node tree + // traversal must be cleared when removing a node + this._clearCaches(); + const parent = this.getParent(); + if (parent && parent.children) { + parent.children.splice(this.index, 1); + parent._setChildrenIndices(); + this.parent = null; + } + } + /** + * 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. + * @method + * @name Konva.Node#destroy + * @example + * node.destroy(); + */ + destroy() { + 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.getId()); + * }) + */ + 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, skipDragCheck = false) { + if (top) { + return this._isVisible(top) && this._isListening(top); + } + const layer = this.getLayer(); + let layerUnderDrag = false; + DD._dragElements.forEach((elem) => { + if (elem.dragStatus !== 'dragging') { + return; + } + else if (elem.node.nodeType === 'Stage') { + layerUnderDrag = true; + } + else if (elem.node.getLayer() === layer) { + layerUnderDrag = true; + } + }); + const dragSkip = !skipDragCheck && + !Konva$2.hitOnDragEnabled && + (layerUnderDrag || Konva$2.isTransforming()); + return this.isListening() && this.isVisible() && !dragSkip; + } + /** + * 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.getChildren().slice()); + } + 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.getChildren()); + } + 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} + */ + 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) { + this._batchingTransformChange = true; + func(); + this._batchingTransformChange = false; + if (this._needClearTransformCache) { + this._clearCache(TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + } + this._needClearTransformCache = false; + } + 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} Ancestor 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) { + let haveCachedParent = false; + let parent = this.parent; + while (parent) { + if (parent.isCached()) { + haveCachedParent = true; + break; + } + parent = parent.parent; + } + if (haveCachedParent && !top) { + // make fake top element + // "true" is not a node, but it will just allow skip all caching + top = true; + } + 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(); + it.invert(); + it.translate(pos.x, pos.y); + pos = { + x: this.attrs.x + it.getTranslation().x, + y: this.attrs.y + it.getTranslation().y, + }; + this._setTransform(origTrans); + this.setPosition({ x: pos.x, y: pos.y }); + this._clearCache(TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + return this; + } + _setTransform(trans) { + let key; + for (key in trans) { + this.attrs[key] = trans[key]; + } + // this._clearCache(TRANSFORM); + // this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + } + _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; + } + _eachAncestorReverse(func, top) { + let family = [], parent = this.getParent(), len, n; + // if top node is defined, and this node is top node, + // there's no need to build a family tree. just execute + // func with this because it will be the only node + if (top && top._id === this._id) { + // func(this); + return; + } + family.unshift(this); + while (parent && (!top || parent._id !== top._id)) { + family.unshift(parent); + parent = parent.parent; + } + len = family.length; + for (n = 0; n < len; n++) { + func(family[n]); + } + } + /** + * 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.getChildren().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.getChildren().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() { + let attrs = this.getAttrs(), key, val, getter, defaultValue, nonPlainObject; + const obj = { + attrs: {}, + className: this.getClassName(), + }; + for (key in attrs) { + val = attrs[key]; + // 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; + } + getter = typeof this[key] === 'function' && this[key]; + // remove attr value so that we can extract the default value from the getter + delete attrs[key]; + defaultValue = getter ? getter.call(this) : null; + // restore attr value + attrs[key] = val; + if (defaultValue !== val) { + obj.attrs[key] = val; + } + } + return Util._prepareToStringify(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$1, 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.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) { + // if using an argument, we can't cache the result. + if (top) { + return this._getAbsoluteTransform(top); + } + else { + // if no argument, we can cache the result + return this._getCache(ABSOLUTE_TRANSFORM, this._getAbsoluteTransform); + } + } + _getAbsoluteTransform(top) { + let at; + // we we need position relative to an ancestor, we will iterate for all + if (top) { + at = new Transform(); + // start with stage and traverse downwards to self + this._eachAncestorReverse(function (node) { + const transformsEnabled = node.transformsEnabled(); + if (transformsEnabled === 'all') { + at.multiply(node.getTransform()); + } + else if (transformsEnabled === 'position') { + at.translate(node.x() - node.offsetX(), node.y() - node.offsetY()); + } + }, top); + return at; + } + else { + // try to use a cached value + at = this._cache.get(ABSOLUTE_TRANSFORM) || new Transform(); + if (this.parent) { + // transform will be cached + this.parent.getAbsoluteTransform().copyInto(at); + } + else { + at.reset(); + } + 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); + } + at.dirty = false; + 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() { + // var parent: Node = this; + // var rotation = 0; + // while (parent) { + // rotation += parent.rotation(); + // parent = parent.getParent(); + // } + // return rotation; + 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.get(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$2.getAngle(this.rotation()), scaleX = (_a = this.attrs.scaleX) !== null && _a !== undefined ? _a : 1, scaleY = (_b = this.attrs.scaleY) !== null && _b !== undefined ? _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 || {}; + const box = this.getClientRect(); + 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.ceil(box.width) || (stage ? stage.width() : 0), + height: config.height || + Math.ceil(box.height) || + (stage ? stage.height() : 0), + pixelRatio: pixelRatio, + }), context = canvas.getContext(); + const bufferCanvas = new SceneCanvas({ + // width and height already multiplied by pixelRatio + // so we need to revert that + // also increase size by x nd y offset to make sure content fits canvas + width: canvas.width / canvas.pixelRatio + Math.abs(x), + height: canvas.height / canvas.pixelRatio + Math.abs(y), + pixelRatio: canvas.pixelRatio, + }); + if (config.imageSmoothingEnabled === false) { + context._context.imageSmoothingEnabled = false; + } + context.save(); + if (x || y) { + context.translate(-1 * x, -1 * y); + } + this.drawScene(canvas, undefined, bufferCanvas); + context.restore(); + 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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) { + config = config || {}; + const mimeType = config.mimeType || null, quality = config.quality || 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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} + * @example + * var image = node.toImage({ + * callback(img) { + * // do stuff with img + * } + * }); + */ + toImage(config) { + return new Promise((resolve, reject) => { + try { + const callback = config === null || config === void 0 ? void 0 : config.callback; + if (callback) + delete config.callback; + Util._urlToImage(this.toDataURL(config), function (img) { + resolve(img); + callback === null || callback === void 0 ? void 0 : callback(img); + }); + } + 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. + * You can use that property to increase quality of the image, for example for super hight quality exports + * or usage on retina (or similar) displays. pixelRatio will be used to multiply the size of 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} + */ + toBlob(config) { + return new Promise((resolve, reject) => { + try { + const callback = config === null || config === void 0 ? void 0 : config.callback; + if (callback) + delete config.callback; + this.toCanvas(config).toBlob((blob) => { + resolve(blob); + callback === null || callback === void 0 ? void 0 : callback(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$2.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; + } + _requestDraw() { + if (Konva$2.autoDrawEnabled) { + const drawNode = this.getLayer() || this.getStage(); + drawNode === null || drawNode === undefined ? undefined : 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._shouldFireChangeEvents) { + this._fireChangeEvent(key, oldVal, val); + } + this._requestDraw(); + } + _setComponentAttr(key, component, val) { + let oldVal; + if (val !== undefined) { + oldVal = this.attrs[key]; + if (!oldVal) { + // set value to default value using getAttr + this.attrs[key] = this.getAttr(key); + } + this.attrs[key][component] = val; + this._fireChangeEvent(key, oldVal, val); + } + } + _fireAndBubble(eventType, evt, compareShape) { + if (evt && this.nodeType === SHAPE) { + evt.target = this; + } + const shouldStop = (eventType === MOUSEENTER$1 || eventType === MOUSELEAVE$1) && + ((compareShape && + (this === compareShape || + (this.isAncestorOf && this.isAncestorOf(compareShape)))) || + (this.nodeType === 'Stage' && !compareShape)); + if (!shouldStop) { + this._fire(eventType, evt); + // simulate event bubbling + const stopBubble = (eventType === MOUSEENTER$1 || eventType === MOUSELEAVE$1) && + 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, _c; + const allListeners = (_a = this._cache.get(ALL_LISTENERS)) !== null && _a !== undefined ? _a : {}; + let events = allListeners === null || allListeners === undefined ? undefined : allListeners[eventType]; + if (events === undefined) { + //recalculate cache + events = []; + let obj = Object.getPrototypeOf(this); + while (obj) { + const hierarchyEvents = (_c = (_b = obj.eventListeners) === null || _b === undefined ? undefined : _b[eventType]) !== null && _c !== undefined ? _c : []; + events.push(...hierarchyEvents); + obj = Object.getPrototypeOf(obj); + } + // update cache + allListeners[eventType] = events; + this._cache.set(ALL_LISTENERS, allListeners); + } + return events; + } + _fire(eventType, evt) { + evt = evt || {}; + evt.currentTarget = this; + evt.type = eventType; + const topListeners = this._getProtoListeners(eventType); + if (topListeners) { + for (var i = 0; i < topListeners.length; i++) { + topListeners[i].handler.call(this, evt); + } + } + // it is important to iterate over self listeners without cache + // because events can be added/removed while firing + const selfListeners = this.eventListeners[eventType]; + if (selfListeners) { + for (var i = 0; i < selfListeners.length; i++) { + selfListeners[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) { + 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; + DD._dragElements.set(this._id, { + node: this, + startPointerPos: pos, + offset: { + x: pos.x - ap.x, + y: pos.y - ap.y, + }, + dragStatus: 'ready', + pointerId, + }); + } + /** + * initiate drag and drop. + * @method + * @name Konva.Node#startDrag + */ + startDrag(evt, bubbleEvent = true) { + if (!DD._dragElements.has(this._id)) { + this._createDragElement(evt); + } + const elem = DD._dragElements.get(this._id); + elem.dragStatus = 'dragging'; + this.fire('dragstart', { + type: 'dragstart', + target: this, + evt: evt && evt.evt, + }, bubbleEvent); + } + _setDragPosition(evt, elem) { + // const pointers = this.getStage().getPointersPositions(); + // const pos = pointers.find(p => p.id === this._dragEventId); + 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 { + newNodePos = bounded; + } + } + if (!this._lastPos || + this._lastPos.x !== newNodePos.x || + this._lastPos.y !== newNodePos.y) { + this.setAbsolutePosition(newNodePos); + this._requestDraw(); + } + this._lastPos = newNodePos; + } + /** + * stop drag and drop + * @method + * @name Konva.Node#stopDrag + */ + stopDrag(evt) { + const elem = DD._dragElements.get(this._id); + if (elem) { + elem.dragStatus = 'stopped'; + } + DD._endDragBefore(evt); + DD._endDragAfter(evt); + } + 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$2.dragButtons.indexOf(evt.evt['button']) >= 0; + if (!canDrag) { + return; + } + if (this.isDragging()) { + 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. De-serializtion does not generate custom + * shape drawing functions, images, or event handlers (this would make the + * serialized object huge). If your app uses custom shapes, images, and + * event handlers (it probably does), then you need to select the appropriate + * shapes after loading the stage and set these properties via on(), setSceneFunc(), + * and setImage() methods + * @method + * @memberof Konva.Node + * @param {String|Object} json 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$2[className]) { + Util.warn('Can not find a node with class name "' + + className + + '". Fallback to "Shape".'); + className = 'Shape'; + } + const Class = Konva$2[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; + } + } + 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.eventListeners = {}; + Node.prototype.on.call(Node.prototype, TRANSFORM_CHANGE_STR$1, function () { + if (this._batchingTransformChange) { + this._needClearTransformCache = true; + return; + } + this._clearCache(TRANSFORM); + this._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + }); + Node.prototype.on.call(Node.prototype, 'visibleChange.konva', function () { + this._clearSelfAndDescendantCache(VISIBLE); + }); + Node.prototype.on.call(Node.prototype, 'listeningChange.konva', function () { + this._clearSelfAndDescendantCache(LISTENING); + }); + Node.prototype.on.call(Node.prototype, '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 + * @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 + * @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. Filters are applied to cached canvases + * @name Konva.Node#filters + * @method + * @param {Array} filters array of filters + * @returns {Array} + * @example + * // get filters + * var filters = node.filters(); + * + * // set a single filter + * node.cache(); + * node.filters([Konva.Filters.Blur]); + * + * // set multiple filters + * node.cache(); + * node.filters([ + * Konva.Filters.Blur, + * Konva.Filters.Sepia, + * Konva.Filters.Invert + * ]); + */ + 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. + * @name Konva.Node#dragBoundFunc + * @method + * @param {Function} dragBoundFunc + * @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', + }); + + /** + * Container constructor.  Containers are used to contain nodes or other containers + * @constructor + * @memberof Konva + * @augments Konva.Node + * @abstract + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + */ + class Container extends Node { + constructor() { + super(...arguments); + this.children = []; + } + /** + * returns an array of direct descendant nodes + * @method + * @name Konva.Container#getChildren + * @param {Function} [filterFunc] filter function + * @returns {Array} + * @example + * // get all children + * var children = layer.getChildren(); + * + * // get only circles + * var circles = layer.getChildren(function(node){ + * return node.getClassName() === 'Circle'; + * }); + */ + getChildren(filterFunc) { + if (!filterFunc) { + return this.children || []; + } + const children = this.children || []; + const results = []; + children.forEach(function (child) { + if (filterFunc(child)) { + results.push(child); + } + }); + return results; + } + /** + * determine if node has children + * @method + * @name Konva.Container#hasChildren + * @returns {Boolean} + */ + hasChildren() { + return this.getChildren().length > 0; + } + /** + * remove all children. Children will be still in memory. + * If you want to completely destroy all children please use "destroyChildren" method instead + * @method + * @name Konva.Container#removeChildren + */ + removeChildren() { + this.getChildren().forEach((child) => { + // reset parent to prevent many _setChildrenIndices calls + child.parent = null; + child.index = 0; + child.remove(); + }); + this.children = []; + // because all children were detached from parent, request draw via container + this._requestDraw(); + return this; + } + /** + * destroy all children nodes. + * @method + * @name Konva.Container#destroyChildren + */ + destroyChildren() { + this.getChildren().forEach((child) => { + // reset parent to prevent many _setChildrenIndices calls + child.parent = null; + child.index = 0; + child.destroy(); + }); + this.children = []; + // because all children were detached from parent, request draw via container + this._requestDraw(); + return this; + } + /** + * add a child and children into container + * @name Konva.Container#add + * @method + * @param {...Konva.Node} children + * @returns {Container} + * @example + * layer.add(rect); + * layer.add(shape1, shape2, shape3); + * // empty arrays are accepted, though each individual child must be defined + * layer.add(...shapes); + * // remember to redraw layer if you changed something + * layer.draw(); + */ + add(...children) { + if (children.length === 0) { + return this; + } + if (children.length > 1) { + for (let i = 0; i < children.length; i++) { + this.add(children[i]); + } + return this; + } + const child = children[0]; + if (child.getParent()) { + child.moveTo(this); + return this; + } + this._validateAdd(child); + child.index = this.getChildren().length; + child.parent = this; + child._clearCaches(); + this.getChildren().push(child); + this._fire('add', { + child: child, + }); + this._requestDraw(); + // chainable + return this; + } + destroy() { + if (this.hasChildren()) { + this.destroyChildren(); + } + super.destroy(); + return this; + } + /** + * return an array of nodes that match the selector. + * You can provide a string with '#' for id selections and '.' for name selections. + * Or a function that will return true/false when a node is passed through. See example below. + * With strings you can also select by type or class name. Pass multiple selectors + * separated by a comma. + * @method + * @name Konva.Container#find + * @param {String | Function} selector + * @returns {Array} + * @example + * + * Passing a string as a selector + * // select node with id foo + * var node = stage.find('#foo'); + * + * // select nodes with name bar inside layer + * var nodes = layer.find('.bar'); + * + * // select all groups inside layer + * var nodes = layer.find('Group'); + * + * // select all rectangles inside layer + * var nodes = layer.find('Rect'); + * + * // select node with an id of foo or a name of bar inside layer + * var nodes = layer.find('#foo, .bar'); + * + * Passing a function as a selector + * + * // get all groups with a function + * var groups = stage.find(node => { + * return node.getType() === 'Group'; + * }); + * + * // get only Nodes with partial opacity + * var alphaNodes = layer.find(node => { + * return node.getType() === 'Node' && node.getAbsoluteOpacity() < 1; + * }); + */ + find(selector) { + // protecting _generalFind to prevent user from accidentally adding + // second argument and getting unexpected `findOne` result + return this._generalFind(selector, false); + } + /** + * return a first node from `find` method + * @method + * @name Konva.Container#findOne + * @param {String | Function} selector + * @returns {Konva.Node | Undefined} + * @example + * // select node with id foo + * var node = stage.findOne('#foo'); + * + * // select node with name bar inside layer + * var nodes = layer.findOne('.bar'); + * + * // select the first node to return true in a function + * var node = stage.findOne(node => { + * return node.getType() === 'Shape' + * }) + */ + findOne(selector) { + const result = this._generalFind(selector, true); + return result.length > 0 ? result[0] : undefined; + } + _generalFind(selector, findOne) { + const retArr = []; + this._descendants((node) => { + const valid = node._isMatch(selector); + if (valid) { + retArr.push(node); + } + if (valid && findOne) { + return true; + } + return false; + }); + return retArr; + } + _descendants(fn) { + let shouldStop = false; + const children = this.getChildren(); + for (const child of children) { + shouldStop = fn(child); + if (shouldStop) { + return true; + } + if (!child.hasChildren()) { + continue; + } + shouldStop = child._descendants(fn); + if (shouldStop) { + return true; + } + } + return false; + } + // extenders + toObject() { + const obj = Node.prototype.toObject.call(this); + obj.children = []; + this.getChildren().forEach((child) => { + obj.children.push(child.toObject()); + }); + return obj; + } + /** + * determine if node is an ancestor + * of descendant + * @method + * @name Konva.Container#isAncestorOf + * @param {Konva.Node} node + */ + isAncestorOf(node) { + let parent = node.getParent(); + while (parent) { + if (parent._id === this._id) { + return true; + } + parent = parent.getParent(); + } + return false; + } + clone(obj) { + // call super method + const node = Node.prototype.clone.call(this, obj); + this.getChildren().forEach(function (no) { + node.add(no.clone()); + }); + return node; + } + /** + * get all shapes that intersect a point. Note: because this method must clear a temporary + * canvas and redraw every shape inside the container, it should only be used for special situations + * because it performs very poorly. Please use the {@link Konva.Stage#getIntersection} method if at all possible + * because it performs much better + * nodes with listening set to false will not be detected + * @method + * @name Konva.Container#getAllIntersections + * @param {Object} pos + * @param {Number} pos.x + * @param {Number} pos.y + * @returns {Array} array of shapes + */ + getAllIntersections(pos) { + const arr = []; + this.find('Shape').forEach((shape) => { + if (shape.isVisible() && shape.intersects(pos)) { + arr.push(shape); + } + }); + return arr; + } + _clearSelfAndDescendantCache(attr) { + var _a; + super._clearSelfAndDescendantCache(attr); + // skip clearing if node is cached with canvas + // for performance reasons !!! + if (this.isCached()) { + return; + } + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (node) { + node._clearSelfAndDescendantCache(attr); + }); + } + _setChildrenIndices() { + var _a; + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (child, n) { + child.index = n; + }); + this._requestDraw(); + } + drawScene(can, top, bufferCanvas) { + const layer = this.getLayer(), canvas = can || (layer && layer.getCanvas()), context = canvas && canvas.getContext(), cachedCanvas = this._getCanvasCache(), cachedSceneCanvas = cachedCanvas && cachedCanvas.scene; + const caching = canvas && canvas.isCache; + if (!this.isVisible() && !caching) { + return this; + } + if (cachedSceneCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedSceneCanvas(context); + context.restore(); + } + else { + this._drawChildren('drawScene', canvas, top, bufferCanvas); + } + return this; + } + drawHit(can, top) { + if (!this.shouldDrawHit(top)) { + return this; + } + const layer = this.getLayer(), canvas = can || (layer && layer.hitCanvas), context = canvas && canvas.getContext(), cachedCanvas = this._getCanvasCache(), cachedHitCanvas = cachedCanvas && cachedCanvas.hit; + if (cachedHitCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedHitCanvas(context); + context.restore(); + } + else { + this._drawChildren('drawHit', canvas, top); + } + return this; + } + _drawChildren(drawMethod, canvas, top, bufferCanvas) { + var _a; + const context = canvas && canvas.getContext(), clipWidth = this.clipWidth(), clipHeight = this.clipHeight(), clipFunc = this.clipFunc(), hasClip = (typeof clipWidth === 'number' && typeof clipHeight === 'number') || + clipFunc; + const selfCache = top === this; + if (hasClip) { + context.save(); + const transform = this.getAbsoluteTransform(top); + let m = transform.getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + context.beginPath(); + let clipArgs; + if (clipFunc) { + clipArgs = clipFunc.call(this, context, this); + } + else { + const clipX = this.clipX(); + const clipY = this.clipY(); + context.rect(clipX || 0, clipY || 0, clipWidth, clipHeight); + } + context.clip.apply(context, clipArgs); + m = transform.copy().invert().getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + const hasComposition = !selfCache && + this.globalCompositeOperation() !== 'source-over' && + drawMethod === 'drawScene'; + if (hasComposition) { + context.save(); + context._applyGlobalCompositeOperation(this); + } + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (child) { + child[drawMethod](canvas, top, bufferCanvas); + }); + if (hasComposition) { + context.restore(); + } + if (hasClip) { + context.restore(); + } + } + getClientRect(config = {}) { + var _a; + const skipTransform = config.skipTransform; + const relativeTo = config.relativeTo; + let minX, minY, maxX, maxY; + let selfRect = { + x: Infinity, + y: Infinity, + width: 0, + height: 0, + }; + const that = this; + (_a = this.children) === null || _a === undefined ? undefined : _a.forEach(function (child) { + // skip invisible children + if (!child.visible()) { + return; + } + const rect = child.getClientRect({ + relativeTo: that, + skipShadow: config.skipShadow, + skipStroke: config.skipStroke, + }); + // skip invisible children (like empty groups) + if (rect.width === 0 && rect.height === 0) { + return; + } + if (minX === undefined) { + // initial value for first child + minX = rect.x; + minY = rect.y; + maxX = rect.x + rect.width; + maxY = rect.y + rect.height; + } + else { + minX = Math.min(minX, rect.x); + minY = Math.min(minY, rect.y); + maxX = Math.max(maxX, rect.x + rect.width); + maxY = Math.max(maxY, rect.y + rect.height); + } + }); + // if child is group we need to make sure it has visible shapes inside + const shapes = this.find('Shape'); + let hasVisible = false; + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + if (shape._isVisible(this)) { + hasVisible = true; + break; + } + } + if (hasVisible && minX !== undefined) { + selfRect = { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + else { + selfRect = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + if (!skipTransform) { + return this._transformedRect(selfRect, relativeTo); + } + return selfRect; + } + } + // add getters setters + Factory.addComponentsGetterSetter(Container, 'clip', [ + 'x', + 'y', + 'width', + 'height', + ]); + /** + * get/set clip + * @method + * @name Konva.Container#clip + * @param {Object} clip + * @param {Number} clip.x + * @param {Number} clip.y + * @param {Number} clip.width + * @param {Number} clip.height + * @returns {Object} + * @example + * // get clip + * var clip = container.clip(); + * + * // set clip + * container.clip({ + * x: 20, + * y: 20, + * width: 20, + * height: 20 + * }); + */ + Factory.addGetterSetter(Container, 'clipX', undefined, getNumberValidator()); + /** + * get/set clip x + * @name Konva.Container#clipX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get clip x + * var clipX = container.clipX(); + * + * // set clip x + * container.clipX(10); + */ + Factory.addGetterSetter(Container, 'clipY', undefined, getNumberValidator()); + /** + * get/set clip y + * @name Konva.Container#clipY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get clip y + * var clipY = container.clipY(); + * + * // set clip y + * container.clipY(10); + */ + Factory.addGetterSetter(Container, 'clipWidth', undefined, getNumberValidator()); + /** + * get/set clip width + * @name Konva.Container#clipWidth + * @method + * @param {Number} width + * @returns {Number} + * @example + * // get clip width + * var clipWidth = container.clipWidth(); + * + * // set clip width + * container.clipWidth(100); + */ + Factory.addGetterSetter(Container, 'clipHeight', undefined, getNumberValidator()); + /** + * get/set clip height + * @name Konva.Container#clipHeight + * @method + * @param {Number} height + * @returns {Number} + * @example + * // get clip height + * var clipHeight = container.clipHeight(); + * + * // set clip height + * container.clipHeight(100); + */ + Factory.addGetterSetter(Container, 'clipFunc'); + /** + * get/set clip function + * @name Konva.Container#clipFunc + * @method + * @param {Function} function + * @returns {Function} + * @example + * // get clip function + * var clipFunction = container.clipFunc(); + * + * // set clip function + * container.clipFunc(function(ctx) { + * ctx.rect(0, 0, 100, 100); + * }); + * + * container.clipFunc(function(ctx) { + * // optionally return a clip Path2D and clip-rule or just the clip-rule + * return [new Path2D('M0 0v50h50Z'), 'evenodd'] + * }); + */ + + const Captures = new Map(); + // we may use this module for capturing touch events too + // so make sure we don't do something super specific to pointer + const SUPPORT_POINTER_EVENTS = Konva$2._global['PointerEvent'] !== undefined; + function getCapturedShape(pointerId) { + return Captures.get(pointerId); + } + function createEvent(evt) { + return { + evt, + pointerId: evt.pointerId, + }; + } + function hasPointerCapture(pointerId, shape) { + return Captures.get(pointerId) === shape; + } + function setPointerCapture(pointerId, shape) { + releaseCapture(pointerId); + const stage = shape.getStage(); + if (!stage) + return; + Captures.set(pointerId, shape); + if (SUPPORT_POINTER_EVENTS) { + shape._fire('gotpointercapture', createEvent(new PointerEvent('gotpointercapture'))); + } + } + function releaseCapture(pointerId, target) { + const shape = Captures.get(pointerId); + if (!shape) + return; + const stage = shape.getStage(); + if (stage && stage.content) ; + Captures.delete(pointerId); + if (SUPPORT_POINTER_EVENTS) { + shape._fire('lostpointercapture', createEvent(new PointerEvent('lostpointercapture'))); + } + } + + // CONSTANTS + const STAGE = 'Stage', STRING = 'string', PX = 'px', MOUSEOUT = 'mouseout', MOUSELEAVE = 'mouseleave', MOUSEOVER = 'mouseover', MOUSEENTER = 'mouseenter', MOUSEMOVE = 'mousemove', MOUSEDOWN = 'mousedown', MOUSEUP = 'mouseup', POINTERMOVE = 'pointermove', POINTERDOWN = 'pointerdown', POINTERUP = 'pointerup', POINTERCANCEL = 'pointercancel', LOSTPOINTERCAPTURE = 'lostpointercapture', POINTEROUT = 'pointerout', POINTERLEAVE = 'pointerleave', POINTEROVER = 'pointerover', POINTERENTER = 'pointerenter', CONTEXTMENU = 'contextmenu', TOUCHSTART = 'touchstart', TOUCHEND = 'touchend', TOUCHMOVE = 'touchmove', TOUCHCANCEL = 'touchcancel', WHEEL = 'wheel', MAX_LAYERS_NUMBER = 5, EVENTS = [ + [MOUSEENTER, '_pointerenter'], + [MOUSEDOWN, '_pointerdown'], + [MOUSEMOVE, '_pointermove'], + [MOUSEUP, '_pointerup'], + [MOUSELEAVE, '_pointerleave'], + [TOUCHSTART, '_pointerdown'], + [TOUCHMOVE, '_pointermove'], + [TOUCHEND, '_pointerup'], + [TOUCHCANCEL, '_pointercancel'], + [MOUSEOVER, '_pointerover'], + [WHEEL, '_wheel'], + [CONTEXTMENU, '_contextmenu'], + [POINTERDOWN, '_pointerdown'], + [POINTERMOVE, '_pointermove'], + [POINTERUP, '_pointerup'], + [POINTERCANCEL, '_pointercancel'], + [LOSTPOINTERCAPTURE, '_lostpointercapture'], + ]; + const EVENTS_MAP = { + mouse: { + [POINTEROUT]: MOUSEOUT, + [POINTERLEAVE]: MOUSELEAVE, + [POINTEROVER]: MOUSEOVER, + [POINTERENTER]: MOUSEENTER, + [POINTERMOVE]: MOUSEMOVE, + [POINTERDOWN]: MOUSEDOWN, + [POINTERUP]: MOUSEUP, + [POINTERCANCEL]: 'mousecancel', + pointerclick: 'click', + pointerdblclick: 'dblclick', + }, + touch: { + [POINTEROUT]: 'touchout', + [POINTERLEAVE]: 'touchleave', + [POINTEROVER]: 'touchover', + [POINTERENTER]: 'touchenter', + [POINTERMOVE]: TOUCHMOVE, + [POINTERDOWN]: TOUCHSTART, + [POINTERUP]: TOUCHEND, + [POINTERCANCEL]: TOUCHCANCEL, + pointerclick: 'tap', + pointerdblclick: 'dbltap', + }, + pointer: { + [POINTEROUT]: POINTEROUT, + [POINTERLEAVE]: POINTERLEAVE, + [POINTEROVER]: POINTEROVER, + [POINTERENTER]: POINTERENTER, + [POINTERMOVE]: POINTERMOVE, + [POINTERDOWN]: POINTERDOWN, + [POINTERUP]: POINTERUP, + [POINTERCANCEL]: POINTERCANCEL, + pointerclick: 'pointerclick', + pointerdblclick: 'pointerdblclick', + }, + }; + const getEventType = (type) => { + if (type.indexOf('pointer') >= 0) { + return 'pointer'; + } + if (type.indexOf('touch') >= 0) { + return 'touch'; + } + return 'mouse'; + }; + const getEventsMap = (eventType) => { + const type = getEventType(eventType); + if (type === 'pointer') { + return Konva$2.pointerEventsEnabled && EVENTS_MAP.pointer; + } + if (type === 'touch') { + return EVENTS_MAP.touch; + } + if (type === 'mouse') { + return EVENTS_MAP.mouse; + } + }; + function checkNoClip(attrs = {}) { + if (attrs.clipFunc || attrs.clipWidth || attrs.clipHeight) { + Util.warn('Stage does not support clipping. Please use clip for Layers or Groups.'); + } + return attrs; + } + const NO_POINTERS_MESSAGE = `Pointer position is missing and not registered by the stage. Looks like it is outside of the stage container. You can set it manually from event: stage.setPointersPositions(event);`; + const stages = []; + /** + * Stage constructor. A stage is used to contain multiple layers + * @constructor + * @memberof Konva + * @augments Konva.Container + * @param {Object} config + * @param {String|Element} config.container Container selector or DOM element + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var stage = new Konva.Stage({ + * width: 500, + * height: 800, + * container: 'containerId' // or "#containerId" or ".containerClass" + * }); + */ + class Stage extends Container { + constructor(config) { + super(checkNoClip(config)); + this._pointerPositions = []; + this._changedPointerPositions = []; + this._buildDOM(); + this._bindContentEvents(); + stages.push(this); + this.on('widthChange.konva heightChange.konva', this._resizeDOM); + this.on('visibleChange.konva', this._checkVisibility); + this.on('clipWidthChange.konva clipHeightChange.konva clipFuncChange.konva', () => { + checkNoClip(this.attrs); + }); + this._checkVisibility(); + } + _validateAdd(child) { + const isLayer = child.getType() === 'Layer'; + const isFastLayer = child.getType() === 'FastLayer'; + const valid = isLayer || isFastLayer; + if (!valid) { + Util.throw('You may only add layers to the stage.'); + } + } + _checkVisibility() { + if (!this.content) { + return; + } + const style = this.visible() ? '' : 'none'; + this.content.style.display = style; + } + /** + * set container dom element which contains the stage wrapper div element + * @method + * @name Konva.Stage#setContainer + * @param {DomElement} container can pass in a dom element or id string + */ + setContainer(container) { + if (typeof container === STRING) { + if (container.charAt(0) === '.') { + const className = container.slice(1); + container = document.getElementsByClassName(className)[0]; + } + else { + var id; + if (container.charAt(0) !== '#') { + id = container; + } + else { + id = container.slice(1); + } + container = document.getElementById(id); + } + if (!container) { + throw 'Can not find container in document with id ' + id; + } + } + this._setAttr('container', container); + if (this.content) { + if (this.content.parentElement) { + this.content.parentElement.removeChild(this.content); + } + container.appendChild(this.content); + } + return this; + } + shouldDrawHit() { + return true; + } + /** + * clear all layers + * @method + * @name Konva.Stage#clear + */ + clear() { + const layers = this.children, len = layers.length; + for (let n = 0; n < len; n++) { + layers[n].clear(); + } + return this; + } + clone(obj) { + if (!obj) { + obj = {}; + } + obj.container = + typeof document !== 'undefined' && document.createElement('div'); + return Container.prototype.clone.call(this, obj); + } + destroy() { + super.destroy(); + const content = this.content; + if (content && Util._isInDocument(content)) { + this.container().removeChild(content); + } + const index = stages.indexOf(this); + if (index > -1) { + stages.splice(index, 1); + } + Util.releaseCanvas(this.bufferCanvas._canvas, this.bufferHitCanvas._canvas); + return this; + } + /** + * returns ABSOLUTE pointer position which can be a touch position or mouse position + * pointer position doesn't include any transforms (such as scale) of the stage + * it is just a plain position of pointer relative to top-left corner of the canvas + * @method + * @name Konva.Stage#getPointerPosition + * @returns {Vector2d|null} + */ + getPointerPosition() { + const pos = this._pointerPositions[0] || this._changedPointerPositions[0]; + if (!pos) { + Util.warn(NO_POINTERS_MESSAGE); + return null; + } + return { + x: pos.x, + y: pos.y, + }; + } + _getPointerById(id) { + return this._pointerPositions.find((p) => p.id === id); + } + getPointersPositions() { + return this._pointerPositions; + } + getStage() { + return this; + } + getContent() { + return this.content; + } + _toKonvaCanvas(config) { + config = config || {}; + config.x = config.x || 0; + config.y = config.y || 0; + config.width = config.width || this.width(); + config.height = config.height || this.height(); + const canvas = new SceneCanvas({ + width: config.width, + height: config.height, + pixelRatio: config.pixelRatio || 1, + }); + const _context = canvas.getContext()._context; + const layers = this.children; + if (config.x || config.y) { + _context.translate(-1 * config.x, -1 * config.y); + } + layers.forEach(function (layer) { + if (!layer.isVisible()) { + return; + } + const layerCanvas = layer._toKonvaCanvas(config); + _context.drawImage(layerCanvas._canvas, config.x, config.y, layerCanvas.getWidth() / layerCanvas.getPixelRatio(), layerCanvas.getHeight() / layerCanvas.getPixelRatio()); + }); + return canvas; + } + /** + * get visible intersection shape. This is the preferred + * method for determining if a point intersects a shape or not + * nodes with listening set to false will not be detected + * @method + * @name Konva.Stage#getIntersection + * @param {Object} pos + * @param {Number} pos.x + * @param {Number} pos.y + * @returns {Konva.Node} + * @example + * var shape = stage.getIntersection({x: 50, y: 50}); + */ + getIntersection(pos) { + if (!pos) { + return null; + } + const layers = this.children, len = layers.length, end = len - 1; + for (let n = end; n >= 0; n--) { + const shape = layers[n].getIntersection(pos); + if (shape) { + return shape; + } + } + return null; + } + _resizeDOM() { + const width = this.width(); + const height = this.height(); + if (this.content) { + // set content dimensions + this.content.style.width = width + PX; + this.content.style.height = height + PX; + } + this.bufferCanvas.setSize(width, height); + this.bufferHitCanvas.setSize(width, height); + // set layer dimensions + this.children.forEach((layer) => { + layer.setSize({ width, height }); + layer.draw(); + }); + } + add(layer, ...rest) { + if (arguments.length > 1) { + for (let i = 0; i < arguments.length; i++) { + this.add(arguments[i]); + } + return this; + } + super.add(layer); + const length = this.children.length; + if (length > MAX_LAYERS_NUMBER) { + Util.warn('The stage has ' + + length + + ' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'); + } + layer.setSize({ width: this.width(), height: this.height() }); + // draw layer and append canvas to container + layer.draw(); + if (Konva$2.isBrowser) { + this.content.appendChild(layer.canvas._canvas); + } + // chainable + return this; + } + getParent() { + return null; + } + getLayer() { + return null; + } + hasPointerCapture(pointerId) { + return hasPointerCapture(pointerId, this); + } + setPointerCapture(pointerId) { + setPointerCapture(pointerId, this); + } + releaseCapture(pointerId) { + releaseCapture(pointerId); + } + /** + * returns an array of layers + * @method + * @name Konva.Stage#getLayers + */ + getLayers() { + return this.children; + } + _bindContentEvents() { + if (!Konva$2.isBrowser) { + return; + } + EVENTS.forEach(([event, methodName]) => { + this.content.addEventListener(event, (evt) => { + this[methodName](evt); + }, { passive: false }); + }); + } + _pointerenter(evt) { + this.setPointersPositions(evt); + const events = getEventsMap(evt.type); + if (events) { + this._fire(events.pointerenter, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _pointerover(evt) { + this.setPointersPositions(evt); + const events = getEventsMap(evt.type); + if (events) { + this._fire(events.pointerover, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _getTargetShape(evenType) { + let shape = this[evenType + 'targetShape']; + if (shape && !shape.getStage()) { + shape = null; + } + return shape; + } + _pointerleave(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + this.setPointersPositions(evt); + const targetShape = this._getTargetShape(eventType); + const eventsEnabled = !(Konva$2.isDragging() || Konva$2.isTransforming()) || Konva$2.hitOnDragEnabled; + if (targetShape && eventsEnabled) { + targetShape._fireAndBubble(events.pointerout, { evt: evt }); + targetShape._fireAndBubble(events.pointerleave, { evt: evt }); + this._fire(events.pointerleave, { + evt: evt, + target: this, + currentTarget: this, + }); + this[eventType + 'targetShape'] = null; + } + else if (eventsEnabled) { + this._fire(events.pointerleave, { + evt: evt, + target: this, + currentTarget: this, + }); + this._fire(events.pointerout, { + evt: evt, + target: this, + currentTarget: this, + }); + } + this.pointerPos = null; + this._pointerPositions = []; + } + _pointerdown(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + this.setPointersPositions(evt); + let triggeredOnShape = false; + this._changedPointerPositions.forEach((pos) => { + const shape = this.getIntersection(pos); + DD.justDragged = false; + // probably we are staring a click + Konva$2['_' + eventType + 'ListenClick'] = true; + // no shape detected? do nothing + if (!shape || !shape.isListening()) { + this[eventType + 'ClickStartShape'] = undefined; + return; + } + if (Konva$2.capturePointerEventsEnabled) { + shape.setPointerCapture(pos.id); + } + // save where we started the click + this[eventType + 'ClickStartShape'] = shape; + shape._fireAndBubble(events.pointerdown, { + evt: evt, + pointerId: pos.id, + }); + triggeredOnShape = true; + // TODO: test in iframe + // only call preventDefault if the shape is listening for events + const isTouch = evt.type.indexOf('touch') >= 0; + if (shape.preventDefault() && evt.cancelable && isTouch) { + evt.preventDefault(); + } + }); + // trigger down on stage if not already + if (!triggeredOnShape) { + this._fire(events.pointerdown, { + evt: evt, + target: this, + currentTarget: this, + pointerId: this._pointerPositions[0].id, + }); + } + } + _pointermove(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + if (Konva$2.isDragging() && DD.node.preventDefault() && evt.cancelable) { + evt.preventDefault(); + } + this.setPointersPositions(evt); + const eventsEnabled = !(Konva$2.isDragging() || Konva$2.isTransforming()) || Konva$2.hitOnDragEnabled; + if (!eventsEnabled) { + return; + } + const processedShapesIds = {}; + let triggeredOnShape = false; + const targetShape = this._getTargetShape(eventType); + this._changedPointerPositions.forEach((pos) => { + const shape = (getCapturedShape(pos.id) || + this.getIntersection(pos)); + const pointerId = pos.id; + const event = { evt: evt, pointerId }; + const differentTarget = targetShape !== shape; + if (differentTarget && targetShape) { + targetShape._fireAndBubble(events.pointerout, { ...event }, shape); + targetShape._fireAndBubble(events.pointerleave, { ...event }, shape); + } + if (shape) { + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + } + if (shape && shape.isListening()) { + triggeredOnShape = true; + if (differentTarget) { + shape._fireAndBubble(events.pointerover, { ...event }, targetShape); + shape._fireAndBubble(events.pointerenter, { ...event }, targetShape); + this[eventType + 'targetShape'] = shape; + } + shape._fireAndBubble(events.pointermove, { ...event }); + } + else { + if (targetShape) { + this._fire(events.pointerover, { + evt: evt, + target: this, + currentTarget: this, + pointerId, + }); + this[eventType + 'targetShape'] = null; + } + } + }); + if (!triggeredOnShape) { + this._fire(events.pointermove, { + evt: evt, + target: this, + currentTarget: this, + pointerId: this._changedPointerPositions[0].id, + }); + } + } + _pointerup(evt) { + const events = getEventsMap(evt.type); + const eventType = getEventType(evt.type); + if (!events) { + return; + } + this.setPointersPositions(evt); + const clickStartShape = this[eventType + 'ClickStartShape']; + const clickEndShape = this[eventType + 'ClickEndShape']; + const processedShapesIds = {}; + let triggeredOnShape = false; + this._changedPointerPositions.forEach((pos) => { + const shape = (getCapturedShape(pos.id) || + this.getIntersection(pos)); + if (shape) { + shape.releaseCapture(pos.id); + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + } + const pointerId = pos.id; + const event = { evt: evt, pointerId }; + let fireDblClick = false; + if (Konva$2['_' + eventType + 'InDblClickWindow']) { + fireDblClick = true; + clearTimeout(this[eventType + 'DblTimeout']); + } + else if (!DD.justDragged) { + // don't set inDblClickWindow after dragging + Konva$2['_' + eventType + 'InDblClickWindow'] = true; + clearTimeout(this[eventType + 'DblTimeout']); + } + this[eventType + 'DblTimeout'] = setTimeout(function () { + Konva$2['_' + eventType + 'InDblClickWindow'] = false; + }, Konva$2.dblClickWindow); + if (shape && shape.isListening()) { + triggeredOnShape = true; + this[eventType + 'ClickEndShape'] = shape; + shape._fireAndBubble(events.pointerup, { ...event }); + // detect if click or double click occurred + if (Konva$2['_' + eventType + 'ListenClick'] && + clickStartShape && + clickStartShape === shape) { + shape._fireAndBubble(events.pointerclick, { ...event }); + if (fireDblClick && clickEndShape && clickEndShape === shape) { + shape._fireAndBubble(events.pointerdblclick, { ...event }); + } + } + } + else { + this[eventType + 'ClickEndShape'] = null; + if (Konva$2['_' + eventType + 'ListenClick']) { + this._fire(events.pointerclick, { + evt: evt, + target: this, + currentTarget: this, + pointerId, + }); + } + if (fireDblClick) { + this._fire(events.pointerdblclick, { + evt: evt, + target: this, + currentTarget: this, + pointerId, + }); + } + } + }); + if (!triggeredOnShape) { + this._fire(events.pointerup, { + evt: evt, + target: this, + currentTarget: this, + pointerId: this._changedPointerPositions[0].id, + }); + } + Konva$2['_' + eventType + 'ListenClick'] = false; + // always call preventDefault for desktop events because some browsers + // try to drag and drop the canvas element + // TODO: are we sure we need to prevent default at all? + // do not call this function on mobile because it prevent "click" event on all parent containers + // but apps may listen to it. + if (evt.cancelable && eventType !== 'touch' && eventType !== 'pointer') { + evt.preventDefault(); + } + } + _contextmenu(evt) { + this.setPointersPositions(evt); + const shape = this.getIntersection(this.getPointerPosition()); + if (shape && shape.isListening()) { + shape._fireAndBubble(CONTEXTMENU, { evt: evt }); + } + else { + this._fire(CONTEXTMENU, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _wheel(evt) { + this.setPointersPositions(evt); + const shape = this.getIntersection(this.getPointerPosition()); + if (shape && shape.isListening()) { + shape._fireAndBubble(WHEEL, { evt: evt }); + } + else { + this._fire(WHEEL, { + evt: evt, + target: this, + currentTarget: this, + }); + } + } + _pointercancel(evt) { + this.setPointersPositions(evt); + const shape = getCapturedShape(evt.pointerId) || + this.getIntersection(this.getPointerPosition()); + if (shape) { + shape._fireAndBubble(POINTERUP, createEvent(evt)); + } + releaseCapture(evt.pointerId); + } + _lostpointercapture(evt) { + releaseCapture(evt.pointerId); + } + /** + * manually register pointers positions (mouse/touch) in the stage. + * So you can use stage.getPointerPosition(). Usually you don't need to use that method + * because all internal events are automatically registered. It may be useful if event + * is triggered outside of the stage, but you still want to use Konva methods to get pointers position. + * @method + * @name Konva.Stage#setPointersPositions + * @param {Object} event Event object + * @example + * + * window.addEventListener('mousemove', (e) => { + * stage.setPointersPositions(e); + * }); + */ + setPointersPositions(evt) { + const contentPosition = this._getContentPosition(); + let x = null, y = null; + evt = evt ? evt : window.event; + // touch events + if (evt.touches !== undefined) { + // touchlist has not support for map method + // so we have to iterate + this._pointerPositions = []; + this._changedPointerPositions = []; + Array.prototype.forEach.call(evt.touches, (touch) => { + this._pointerPositions.push({ + id: touch.identifier, + x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, + y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, + }); + }); + Array.prototype.forEach.call(evt.changedTouches || evt.touches, (touch) => { + this._changedPointerPositions.push({ + id: touch.identifier, + x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, + y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, + }); + }); + } + else { + // mouse events + x = (evt.clientX - contentPosition.left) / contentPosition.scaleX; + y = (evt.clientY - contentPosition.top) / contentPosition.scaleY; + this.pointerPos = { + x: x, + y: y, + }; + this._pointerPositions = [{ x, y, id: Util._getFirstPointerId(evt) }]; + this._changedPointerPositions = [ + { x, y, id: Util._getFirstPointerId(evt) }, + ]; + } + } + _setPointerPosition(evt) { + Util.warn('Method _setPointerPosition is deprecated. Use "stage.setPointersPositions(event)" instead.'); + this.setPointersPositions(evt); + } + _getContentPosition() { + if (!this.content || !this.content.getBoundingClientRect) { + return { + top: 0, + left: 0, + scaleX: 1, + scaleY: 1, + }; + } + const rect = this.content.getBoundingClientRect(); + return { + top: rect.top, + left: rect.left, + // sometimes clientWidth can be equals to 0 + // i saw it in react-konva test, looks like it is because of hidden testing element + scaleX: rect.width / this.content.clientWidth || 1, + scaleY: rect.height / this.content.clientHeight || 1, + }; + } + _buildDOM() { + this.bufferCanvas = new SceneCanvas({ + width: this.width(), + height: this.height(), + }); + this.bufferHitCanvas = new HitCanvas({ + pixelRatio: 1, + width: this.width(), + height: this.height(), + }); + if (!Konva$2.isBrowser) { + return; + } + const container = this.container(); + if (!container) { + throw 'Stage has no container. A container is required.'; + } + // clear content inside container + container.innerHTML = ''; + // content + this.content = document.createElement('div'); + this.content.style.position = 'relative'; + this.content.style.userSelect = 'none'; + this.content.className = 'konvajs-content'; + this.content.setAttribute('role', 'presentation'); + container.appendChild(this.content); + this._resizeDOM(); + } + // currently cache function is now working for stage, because stage has no its own canvas element + cache() { + Util.warn('Cache function is not allowed for stage. You may use cache only for layers, groups and shapes.'); + return this; + } + clearCache() { + return this; + } + /** + * batch draw + * @method + * @name Konva.Stage#batchDraw + * @return {Konva.Stage} this + */ + batchDraw() { + this.getChildren().forEach(function (layer) { + layer.batchDraw(); + }); + return this; + } + } + Stage.prototype.nodeType = STAGE; + _registerNode(Stage); + /** + * get/set container DOM element + * @method + * @name Konva.Stage#container + * @returns {DomElement} container + * @example + * // get container + * var container = stage.container(); + * // set container + * var container = document.createElement('div'); + * body.appendChild(container); + * stage.container(container); + */ + Factory.addGetterSetter(Stage, 'container'); + // chrome is clearing canvas in inactive browser window, causing layer content to be erased + // so let's redraw layers as soon as window becomes active + // TODO: any other way to solve this issue? + // TODO: should we remove it if chrome fixes the issue? + if (Konva$2.isBrowser) { + document.addEventListener('visibilitychange', () => { + stages.forEach((stage) => { + stage.batchDraw(); + }); + }); + } + + const HAS_SHADOW = 'hasShadow'; + const SHADOW_RGBA = 'shadowRGBA'; + const patternImage = 'patternImage'; + const linearGradient = 'linearGradient'; + const radialGradient = 'radialGradient'; + let dummyContext$1; + function getDummyContext$1() { + if (dummyContext$1) { + return dummyContext$1; + } + dummyContext$1 = Util.createCanvasElement().getContext('2d'); + return dummyContext$1; + } + const shapes = {}; + // TODO: idea - use only "remove" (or destroy method) + // how? on add, check that every inner shape has reference in konva store with color + // on remove - clear that reference + // the approach is good. But what if we want to cache the shape before we add it into the stage + // what color to use for hit test? + function _fillFunc$2(context) { + const fillRule = this.attrs.fillRule; + if (fillRule) { + context.fill(fillRule); + } + else { + context.fill(); + } + } + function _strokeFunc$2(context) { + context.stroke(); + } + function _fillFuncHit(context) { + const fillRule = this.attrs.fillRule; + if (fillRule) { + context.fill(fillRule); + } + else { + context.fill(); + } + } + function _strokeFuncHit(context) { + context.stroke(); + } + function _clearHasShadowCache() { + this._clearCache(HAS_SHADOW); + } + function _clearGetShadowRGBACache() { + this._clearCache(SHADOW_RGBA); + } + function _clearFillPatternCache() { + this._clearCache(patternImage); + } + function _clearLinearGradientCache() { + this._clearCache(linearGradient); + } + function _clearRadialGradientCache() { + this._clearCache(radialGradient); + } + /** + * Shape constructor. Shapes are primitive objects such as rectangles, + * circles, text, lines, etc. + * @constructor + * @memberof Konva + * @augments Konva.Node + * @param {Object} config + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var customShape = new Konva.Shape({ + * x: 5, + * y: 10, + * fill: 'red', + * // a Konva.Canvas renderer is passed into the sceneFunc function + * sceneFunc (context, shape) { + * context.beginPath(); + * context.moveTo(200, 50); + * context.lineTo(420, 80); + * context.quadraticCurveTo(300, 100, 260, 170); + * context.closePath(); + * // Konva specific method + * context.fillStrokeShape(shape); + * } + *}); + */ + class Shape extends Node { + constructor(config) { + super(config); + // set colorKey + let key; + while (true) { + key = Util.getRandomColor(); + if (key && !(key in shapes)) { + break; + } + } + this.colorKey = key; + shapes[key] = this; + } + getContext() { + Util.warn('shape.getContext() method is deprecated. Please do not use it.'); + return this.getLayer().getContext(); + } + getCanvas() { + Util.warn('shape.getCanvas() method is deprecated. Please do not use it.'); + return this.getLayer().getCanvas(); + } + getSceneFunc() { + return this.attrs.sceneFunc || this['_sceneFunc']; + } + getHitFunc() { + return this.attrs.hitFunc || this['_hitFunc']; + } + /** + * returns whether or not a shadow will be rendered + * @method + * @name Konva.Shape#hasShadow + * @returns {Boolean} + */ + hasShadow() { + return this._getCache(HAS_SHADOW, this._hasShadow); + } + _hasShadow() { + return (this.shadowEnabled() && + this.shadowOpacity() !== 0 && + !!(this.shadowColor() || + this.shadowBlur() || + this.shadowOffsetX() || + this.shadowOffsetY())); + } + _getFillPattern() { + return this._getCache(patternImage, this.__getFillPattern); + } + __getFillPattern() { + if (this.fillPatternImage()) { + const ctx = getDummyContext$1(); + const pattern = ctx.createPattern(this.fillPatternImage(), this.fillPatternRepeat() || 'repeat'); + if (pattern && pattern.setTransform) { + const tr = new Transform(); + tr.translate(this.fillPatternX(), this.fillPatternY()); + tr.rotate(Konva$2.getAngle(this.fillPatternRotation())); + tr.scale(this.fillPatternScaleX(), this.fillPatternScaleY()); + tr.translate(-1 * this.fillPatternOffsetX(), -1 * this.fillPatternOffsetY()); + const m = tr.getMatrix(); + const matrix = typeof DOMMatrix === 'undefined' + ? { + a: m[0], // Horizontal scaling. A value of 1 results in no scaling. + b: m[1], // Vertical skewing. + c: m[2], // Horizontal skewing. + d: m[3], + e: m[4], // Horizontal translation (moving). + f: m[5], // Vertical translation (moving). + } + : new DOMMatrix(m); + pattern.setTransform(matrix); + } + return pattern; + } + } + _getLinearGradient() { + return this._getCache(linearGradient, this.__getLinearGradient); + } + __getLinearGradient() { + const colorStops = this.fillLinearGradientColorStops(); + if (colorStops) { + const ctx = getDummyContext$1(); + const start = this.fillLinearGradientStartPoint(); + const end = this.fillLinearGradientEndPoint(); + const grd = ctx.createLinearGradient(start.x, start.y, end.x, end.y); + // build color stops + for (let n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + return grd; + } + } + _getRadialGradient() { + return this._getCache(radialGradient, this.__getRadialGradient); + } + __getRadialGradient() { + const colorStops = this.fillRadialGradientColorStops(); + if (colorStops) { + const ctx = getDummyContext$1(); + const start = this.fillRadialGradientStartPoint(); + const end = this.fillRadialGradientEndPoint(); + const grd = ctx.createRadialGradient(start.x, start.y, this.fillRadialGradientStartRadius(), end.x, end.y, this.fillRadialGradientEndRadius()); + // build color stops + for (let n = 0; n < colorStops.length; n += 2) { + grd.addColorStop(colorStops[n], colorStops[n + 1]); + } + return grd; + } + } + getShadowRGBA() { + return this._getCache(SHADOW_RGBA, this._getShadowRGBA); + } + _getShadowRGBA() { + if (!this.hasShadow()) { + return; + } + const rgba = Util.colorToRGBA(this.shadowColor()); + if (rgba) { + return ('rgba(' + + rgba.r + + ',' + + rgba.g + + ',' + + rgba.b + + ',' + + rgba.a * (this.shadowOpacity() || 1) + + ')'); + } + } + /** + * returns whether or not the shape will be filled + * @method + * @name Konva.Shape#hasFill + * @returns {Boolean} + */ + hasFill() { + return this._calculate('hasFill', [ + 'fillEnabled', + 'fill', + 'fillPatternImage', + 'fillLinearGradientColorStops', + 'fillRadialGradientColorStops', + ], () => { + return (this.fillEnabled() && + !!(this.fill() || + this.fillPatternImage() || + this.fillLinearGradientColorStops() || + this.fillRadialGradientColorStops())); + }); + } + /** + * returns whether or not the shape will be stroked + * @method + * @name Konva.Shape#hasStroke + * @returns {Boolean} + */ + hasStroke() { + return this._calculate('hasStroke', [ + 'strokeEnabled', + 'strokeWidth', + 'stroke', + 'strokeLinearGradientColorStops', + ], () => { + return (this.strokeEnabled() && + this.strokeWidth() && + !!(this.stroke() || this.strokeLinearGradientColorStops()) + // this.getStrokeRadialGradientColorStops() + ); + }); + // return ( + // this.strokeEnabled() && + // this.strokeWidth() && + // !!(this.stroke() || this.strokeLinearGradientColorStops()) + // // this.getStrokeRadialGradientColorStops() + // ); + } + hasHitStroke() { + const width = this.hitStrokeWidth(); + // on auto just check by stroke + if (width === 'auto') { + return this.hasStroke(); + } + // we should enable hit stroke if stroke is enabled + // and we have some value from width + return this.strokeEnabled() && !!width; + } + /** + * determines if point is in the shape, regardless if other shapes are on top of it. Note: because + * this method clears a temporary canvas and then redraws the shape, it performs very poorly if executed many times + * consecutively. Please use the {@link Konva.Stage#getIntersection} method if at all possible + * because it performs much better + * @method + * @name Konva.Shape#intersects + * @param {Object} point + * @param {Number} point.x + * @param {Number} point.y + * @returns {Boolean} + */ + intersects(point) { + const stage = this.getStage(); + if (!stage) { + return false; + } + const bufferHitCanvas = stage.bufferHitCanvas; + bufferHitCanvas.getContext().clear(); + this.drawHit(bufferHitCanvas, undefined, true); + const p = bufferHitCanvas.context.getImageData(Math.round(point.x), Math.round(point.y), 1, 1).data; + return p[3] > 0; + } + destroy() { + Node.prototype.destroy.call(this); + delete shapes[this.colorKey]; + delete this.colorKey; + return this; + } + // why do we need buffer canvas? + // it give better result when a shape has + // stroke with fill and with some opacity + _useBufferCanvas(forceFill) { + // image and sprite still has "fill" as image + // so they use that method with forced fill + // it probably will be simpler, then copy/paste the code + var _a; + // force skip buffer canvas + const perfectDrawEnabled = (_a = this.attrs.perfectDrawEnabled) !== null && _a !== undefined ? _a : true; + if (!perfectDrawEnabled) { + return false; + } + const hasFill = forceFill || this.hasFill(); + const hasStroke = this.hasStroke(); + const isTransparent = this.getAbsoluteOpacity() !== 1; + if (hasFill && hasStroke && isTransparent) { + return true; + } + const hasShadow = this.hasShadow(); + const strokeForShadow = this.shadowForStrokeEnabled(); + if (hasFill && hasStroke && hasShadow && strokeForShadow) { + return true; + } + return false; + } + setStrokeHitEnabled(val) { + Util.warn('strokeHitEnabled property is deprecated. Please use hitStrokeWidth instead.'); + if (val) { + this.hitStrokeWidth('auto'); + } + else { + this.hitStrokeWidth(0); + } + } + getStrokeHitEnabled() { + if (this.hitStrokeWidth() === 0) { + return false; + } + else { + return true; + } + } + /** + * return self rectangle (x, y, width, height) of shape. + * This method are not taken into account transformation and styles. + * @method + * @name Konva.Shape#getSelfRect + * @returns {Object} rect with {x, y, width, height} properties + * @example + * + * rect.getSelfRect(); // return {x:0, y:0, width:rect.width(), height:rect.height()} + * circle.getSelfRect(); // return {x: - circle.width() / 2, y: - circle.height() / 2, width:circle.width(), height:circle.height()} + * + */ + getSelfRect() { + const size = this.size(); + return { + x: this._centroid ? -size.width / 2 : 0, + y: this._centroid ? -size.height / 2 : 0, + width: size.width, + height: size.height, + }; + } + getClientRect(config = {}) { + // if we have a cached parent, it will use cached transform matrix + // but we don't want to that + let hasCachedParent = false; + let parent = this.getParent(); + while (parent) { + if (parent.isCached()) { + hasCachedParent = true; + break; + } + parent = parent.getParent(); + } + const skipTransform = config.skipTransform; + // force relative to stage if we have a cached parent + const relativeTo = config.relativeTo || (hasCachedParent && this.getStage()) || undefined; + const fillRect = this.getSelfRect(); + const applyStroke = !config.skipStroke && this.hasStroke(); + const strokeWidth = (applyStroke && this.strokeWidth()) || 0; + const fillAndStrokeWidth = fillRect.width + strokeWidth; + const fillAndStrokeHeight = fillRect.height + strokeWidth; + const applyShadow = !config.skipShadow && this.hasShadow(); + const shadowOffsetX = applyShadow ? this.shadowOffsetX() : 0; + const shadowOffsetY = applyShadow ? this.shadowOffsetY() : 0; + const preWidth = fillAndStrokeWidth + Math.abs(shadowOffsetX); + const preHeight = fillAndStrokeHeight + Math.abs(shadowOffsetY); + const blurRadius = (applyShadow && this.shadowBlur()) || 0; + const width = preWidth + blurRadius * 2; + const height = preHeight + blurRadius * 2; + const rect = { + width: width, + height: height, + x: -(strokeWidth / 2 + blurRadius) + + Math.min(shadowOffsetX, 0) + + fillRect.x, + y: -(strokeWidth / 2 + blurRadius) + + Math.min(shadowOffsetY, 0) + + fillRect.y, + }; + if (!skipTransform) { + return this._transformedRect(rect, relativeTo); + } + return rect; + } + drawScene(can, top, bufferCanvas) { + // basically there are 3 drawing modes + // 1 - simple drawing when nothing is cached. + // 2 - when we are caching current + // 3 - when node is cached and we need to draw it into layer + const layer = this.getLayer(); + let canvas = can || layer.getCanvas(), context = canvas.getContext(), cachedCanvas = this._getCanvasCache(), drawFunc = this.getSceneFunc(), hasShadow = this.hasShadow(), stage, bufferContext; + const skipBuffer = canvas.isCache; + const cachingSelf = top === this; + if (!this.isVisible() && !cachingSelf) { + return this; + } + // if node is cached we just need to draw from cache + if (cachedCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedSceneCanvas(context); + context.restore(); + return this; + } + if (!drawFunc) { + return this; + } + context.save(); + // if buffer canvas is needed + if (this._useBufferCanvas() && !skipBuffer) { + stage = this.getStage(); + const bc = bufferCanvas || stage.bufferCanvas; + bufferContext = bc.getContext(); + bufferContext.clear(); + bufferContext.save(); + bufferContext._applyLineJoin(this); + // layer might be undefined if we are using cache before adding to layer + var o = this.getAbsoluteTransform(top).getMatrix(); + bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]); + drawFunc.call(this, bufferContext, this); + bufferContext.restore(); + const ratio = bc.pixelRatio; + if (hasShadow) { + context._applyShadow(this); + } + context._applyOpacity(this); + context._applyGlobalCompositeOperation(this); + context.drawImage(bc._canvas, 0, 0, bc.width / ratio, bc.height / ratio); + } + else { + context._applyLineJoin(this); + if (!cachingSelf) { + var o = this.getAbsoluteTransform(top).getMatrix(); + context.transform(o[0], o[1], o[2], o[3], o[4], o[5]); + context._applyOpacity(this); + context._applyGlobalCompositeOperation(this); + } + if (hasShadow) { + context._applyShadow(this); + } + drawFunc.call(this, context, this); + } + context.restore(); + return this; + } + drawHit(can, top, skipDragCheck = false) { + if (!this.shouldDrawHit(top, skipDragCheck)) { + return this; + } + const layer = this.getLayer(), canvas = can || layer.hitCanvas, context = canvas && canvas.getContext(), drawFunc = this.hitFunc() || this.sceneFunc(), cachedCanvas = this._getCanvasCache(), cachedHitCanvas = cachedCanvas && cachedCanvas.hit; + if (!this.colorKey) { + Util.warn('Looks like your canvas has a destroyed shape in it. Do not reuse shape after you destroyed it. If you want to reuse shape you should call remove() instead of destroy()'); + } + if (cachedHitCanvas) { + context.save(); + const m = this.getAbsoluteTransform(top).getMatrix(); + context.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + this._drawCachedHitCanvas(context); + context.restore(); + return this; + } + if (!drawFunc) { + return this; + } + context.save(); + context._applyLineJoin(this); + const selfCache = this === top; + if (!selfCache) { + const o = this.getAbsoluteTransform(top).getMatrix(); + context.transform(o[0], o[1], o[2], o[3], o[4], o[5]); + } + drawFunc.call(this, context, this); + context.restore(); + return this; + } + /** + * draw hit graph using the cached scene canvas + * @method + * @name Konva.Shape#drawHitFromCache + * @param {Integer} alphaThreshold alpha channel threshold that determines whether or not + * a pixel should be drawn onto the hit graph. Must be a value between 0 and 255. + * The default is 0 + * @returns {Konva.Shape} + * @example + * shape.cache(); + * shape.drawHitFromCache(); + */ + drawHitFromCache(alphaThreshold = 0) { + const cachedCanvas = this._getCanvasCache(), sceneCanvas = this._getCachedSceneCanvas(), hitCanvas = cachedCanvas.hit, hitContext = hitCanvas.getContext(), hitWidth = hitCanvas.getWidth(), hitHeight = hitCanvas.getHeight(); + hitContext.clear(); + hitContext.drawImage(sceneCanvas._canvas, 0, 0, hitWidth, hitHeight); + try { + const hitImageData = hitContext.getImageData(0, 0, hitWidth, hitHeight); + const hitData = hitImageData.data; + const len = hitData.length; + const rgbColorKey = Util._hexToRgb(this.colorKey); + // replace non transparent pixels with color key + for (let i = 0; i < len; i += 4) { + const alpha = hitData[i + 3]; + if (alpha > alphaThreshold) { + hitData[i] = rgbColorKey.r; + hitData[i + 1] = rgbColorKey.g; + hitData[i + 2] = rgbColorKey.b; + hitData[i + 3] = 255; + } + else { + hitData[i + 3] = 0; + } + } + hitContext.putImageData(hitImageData, 0, 0); + } + catch (e) { + Util.error('Unable to draw hit graph from cached scene canvas. ' + e.message); + } + return this; + } + hasPointerCapture(pointerId) { + return hasPointerCapture(pointerId, this); + } + setPointerCapture(pointerId) { + setPointerCapture(pointerId, this); + } + releaseCapture(pointerId) { + releaseCapture(pointerId); + } + } + Shape.prototype._fillFunc = _fillFunc$2; + Shape.prototype._strokeFunc = _strokeFunc$2; + Shape.prototype._fillFuncHit = _fillFuncHit; + Shape.prototype._strokeFuncHit = _strokeFuncHit; + Shape.prototype._centroid = false; + Shape.prototype.nodeType = 'Shape'; + _registerNode(Shape); + Shape.prototype.eventListeners = {}; + Shape.prototype.on.call(Shape.prototype, 'shadowColorChange.konva shadowBlurChange.konva shadowOffsetChange.konva shadowOpacityChange.konva shadowEnabledChange.konva', _clearHasShadowCache); + Shape.prototype.on.call(Shape.prototype, 'shadowColorChange.konva shadowOpacityChange.konva shadowEnabledChange.konva', _clearGetShadowRGBACache); + Shape.prototype.on.call(Shape.prototype, 'fillPriorityChange.konva fillPatternImageChange.konva fillPatternRepeatChange.konva fillPatternScaleXChange.konva fillPatternScaleYChange.konva fillPatternOffsetXChange.konva fillPatternOffsetYChange.konva fillPatternXChange.konva fillPatternYChange.konva fillPatternRotationChange.konva', _clearFillPatternCache); + Shape.prototype.on.call(Shape.prototype, 'fillPriorityChange.konva fillLinearGradientColorStopsChange.konva fillLinearGradientStartPointXChange.konva fillLinearGradientStartPointYChange.konva fillLinearGradientEndPointXChange.konva fillLinearGradientEndPointYChange.konva', _clearLinearGradientCache); + Shape.prototype.on.call(Shape.prototype, 'fillPriorityChange.konva fillRadialGradientColorStopsChange.konva fillRadialGradientStartPointXChange.konva fillRadialGradientStartPointYChange.konva fillRadialGradientEndPointXChange.konva fillRadialGradientEndPointYChange.konva fillRadialGradientStartRadiusChange.konva fillRadialGradientEndRadiusChange.konva', _clearRadialGradientCache); + // add getters and setters + Factory.addGetterSetter(Shape, 'stroke', undefined, getStringOrGradientValidator()); + /** + * get/set stroke color + * @name Konva.Shape#stroke + * @method + * @param {String} color + * @returns {String} + * @example + * // get stroke color + * var stroke = shape.stroke(); + * + * // set stroke color with color string + * shape.stroke('green'); + * + * // set stroke color with hex + * shape.stroke('#00ff00'); + * + * // set stroke color with rgb + * shape.stroke('rgb(0,255,0)'); + * + * // set stroke color with rgba and make it 50% opaque + * shape.stroke('rgba(0,255,0,0.5'); + */ + Factory.addGetterSetter(Shape, 'strokeWidth', 2, getNumberValidator()); + /** + * get/set stroke width + * @name Konva.Shape#strokeWidth + * @method + * @param {Number} strokeWidth + * @returns {Number} + * @example + * // get stroke width + * var strokeWidth = shape.strokeWidth(); + * + * // set stroke width + * shape.strokeWidth(10); + */ + Factory.addGetterSetter(Shape, 'fillAfterStrokeEnabled', false); + /** + * get/set fillAfterStrokeEnabled property. By default Konva is drawing filling first, then stroke on top of the fill. + * In rare situations you may want a different behavior. When you have a stroke first then fill on top of it. + * Especially useful for Text objects. + * Default is false. + * @name Konva.Shape#fillAfterStrokeEnabled + * @method + * @param {Boolean} fillAfterStrokeEnabled + * @returns {Boolean} + * @example + * // get stroke width + * var fillAfterStrokeEnabled = shape.fillAfterStrokeEnabled(); + * + * // set stroke width + * shape.fillAfterStrokeEnabled(true); + */ + Factory.addGetterSetter(Shape, 'hitStrokeWidth', 'auto', getNumberOrAutoValidator()); + /** + * get/set stroke width for hit detection. Default value is "auto", it means it will be equals to strokeWidth + * @name Konva.Shape#hitStrokeWidth + * @method + * @param {Number} hitStrokeWidth + * @returns {Number} + * @example + * // get stroke width + * var hitStrokeWidth = shape.hitStrokeWidth(); + * + * // set hit stroke width + * shape.hitStrokeWidth(20); + * // set hit stroke width always equals to scene stroke width + * shape.hitStrokeWidth('auto'); + */ + Factory.addGetterSetter(Shape, 'strokeHitEnabled', true, getBooleanValidator()); + /** + * **deprecated, use hitStrokeWidth instead!** get/set strokeHitEnabled property. Useful for performance optimization. + * You may set `shape.strokeHitEnabled(false)`. In this case stroke will be no draw on hit canvas, so hit area + * of shape will be decreased (by lineWidth / 2). Remember that non closed line with `strokeHitEnabled = false` + * will be not drawn on hit canvas, that is mean line will no trigger pointer events (like mouseover) + * Default value is true. + * @name Konva.Shape#strokeHitEnabled + * @method + * @param {Boolean} strokeHitEnabled + * @returns {Boolean} + * @example + * // get strokeHitEnabled + * var strokeHitEnabled = shape.strokeHitEnabled(); + * + * // set strokeHitEnabled + * shape.strokeHitEnabled(); + */ + Factory.addGetterSetter(Shape, 'perfectDrawEnabled', true, getBooleanValidator()); + /** + * get/set perfectDrawEnabled. If a shape has fill, stroke and opacity you may set `perfectDrawEnabled` to false to improve performance. + * See http://konvajs.org/docs/performance/Disable_Perfect_Draw.html for more information. + * Default value is true + * @name Konva.Shape#perfectDrawEnabled + * @method + * @param {Boolean} perfectDrawEnabled + * @returns {Boolean} + * @example + * // get perfectDrawEnabled + * var perfectDrawEnabled = shape.perfectDrawEnabled(); + * + * // set perfectDrawEnabled + * shape.perfectDrawEnabled(); + */ + Factory.addGetterSetter(Shape, 'shadowForStrokeEnabled', true, getBooleanValidator()); + /** + * get/set shadowForStrokeEnabled. Useful for performance optimization. + * You may set `shape.shadowForStrokeEnabled(false)`. In this case stroke will no effect shadow. + * Remember if you set `shadowForStrokeEnabled = false` for non closed line - that line will have no shadow!. + * Default value is true + * @name Konva.Shape#shadowForStrokeEnabled + * @method + * @param {Boolean} shadowForStrokeEnabled + * @returns {Boolean} + * @example + * // get shadowForStrokeEnabled + * var shadowForStrokeEnabled = shape.shadowForStrokeEnabled(); + * + * // set shadowForStrokeEnabled + * shape.shadowForStrokeEnabled(); + */ + Factory.addGetterSetter(Shape, 'lineJoin'); + /** + * get/set line join. Can be miter, round, or bevel. The + * default is miter + * @name Konva.Shape#lineJoin + * @method + * @param {String} lineJoin + * @returns {String} + * @example + * // get line join + * var lineJoin = shape.lineJoin(); + * + * // set line join + * shape.lineJoin('round'); + */ + Factory.addGetterSetter(Shape, 'lineCap'); + /** + * get/set line cap. Can be butt, round, or square + * @name Konva.Shape#lineCap + * @method + * @param {String} lineCap + * @returns {String} + * @example + * // get line cap + * var lineCap = shape.lineCap(); + * + * // set line cap + * shape.lineCap('round'); + */ + Factory.addGetterSetter(Shape, 'sceneFunc'); + /** + * get/set scene draw function. That function is used to draw the shape on a canvas. + * Also that function will be used to draw hit area of the shape, in case if hitFunc is not defined. + * @name Konva.Shape#sceneFunc + * @method + * @param {Function} drawFunc drawing function + * @returns {Function} + * @example + * // get scene draw function + * var sceneFunc = shape.sceneFunc(); + * + * // set scene draw function + * shape.sceneFunc(function(context, shape) { + * context.beginPath(); + * context.rect(0, 0, shape.width(), shape.height()); + * context.closePath(); + * // important Konva method that fill and stroke shape from its properties + * // like stroke and fill + * context.fillStrokeShape(shape); + * }); + */ + Factory.addGetterSetter(Shape, 'hitFunc'); + /** + * get/set hit draw function. That function is used to draw custom hit area of a shape. + * @name Konva.Shape#hitFunc + * @method + * @param {Function} drawFunc drawing function + * @returns {Function} + * @example + * // get hit draw function + * var hitFunc = shape.hitFunc(); + * + * // set hit draw function + * shape.hitFunc(function(context) { + * context.beginPath(); + * context.rect(0, 0, shape.width(), shape.height()); + * context.closePath(); + * // important Konva method that fill and stroke shape from its properties + * context.fillStrokeShape(shape); + * }); + */ + Factory.addGetterSetter(Shape, 'dash'); + /** + * get/set dash array for stroke. + * @name Konva.Shape#dash + * @method + * @param {Array} dash + * @returns {Array} + * @example + * // apply dashed stroke that is 10px long and 5 pixels apart + * line.dash([10, 5]); + * // apply dashed stroke that is made up of alternating dashed + * // lines that are 10px long and 20px apart, and dots that have + * // a radius of 5px and are 20px apart + * line.dash([10, 20, 0.001, 20]); + */ + Factory.addGetterSetter(Shape, 'dashOffset', 0, getNumberValidator()); + /** + * get/set dash offset for stroke. + * @name Konva.Shape#dash + * @method + * @param {Number} dash offset + * @returns {Number} + * @example + * // apply dashed stroke that is 10px long and 5 pixels apart with an offset of 5px + * line.dash([10, 5]); + * line.dashOffset(5); + */ + Factory.addGetterSetter(Shape, 'shadowColor', undefined, getStringValidator()); + /** + * get/set shadow color + * @name Konva.Shape#shadowColor + * @method + * @param {String} color + * @returns {String} + * @example + * // get shadow color + * var shadow = shape.shadowColor(); + * + * // set shadow color with color string + * shape.shadowColor('green'); + * + * // set shadow color with hex + * shape.shadowColor('#00ff00'); + * + * // set shadow color with rgb + * shape.shadowColor('rgb(0,255,0)'); + * + * // set shadow color with rgba and make it 50% opaque + * shape.shadowColor('rgba(0,255,0,0.5'); + */ + Factory.addGetterSetter(Shape, 'shadowBlur', 0, getNumberValidator()); + /** + * get/set shadow blur + * @name Konva.Shape#shadowBlur + * @method + * @param {Number} blur + * @returns {Number} + * @example + * // get shadow blur + * var shadowBlur = shape.shadowBlur(); + * + * // set shadow blur + * shape.shadowBlur(10); + */ + Factory.addGetterSetter(Shape, 'shadowOpacity', 1, getNumberValidator()); + /** + * get/set shadow opacity. must be a value between 0 and 1 + * @name Konva.Shape#shadowOpacity + * @method + * @param {Number} opacity + * @returns {Number} + * @example + * // get shadow opacity + * var shadowOpacity = shape.shadowOpacity(); + * + * // set shadow opacity + * shape.shadowOpacity(0.5); + */ + Factory.addComponentsGetterSetter(Shape, 'shadowOffset', ['x', 'y']); + /** + * get/set shadow offset + * @name Konva.Shape#shadowOffset + * @method + * @param {Object} offset + * @param {Number} offset.x + * @param {Number} offset.y + * @returns {Object} + * @example + * // get shadow offset + * var shadowOffset = shape.shadowOffset(); + * + * // set shadow offset + * shape.shadowOffset({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'shadowOffsetX', 0, getNumberValidator()); + /** + * get/set shadow offset x + * @name Konva.Shape#shadowOffsetX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get shadow offset x + * var shadowOffsetX = shape.shadowOffsetX(); + * + * // set shadow offset x + * shape.shadowOffsetX(5); + */ + Factory.addGetterSetter(Shape, 'shadowOffsetY', 0, getNumberValidator()); + /** + * get/set shadow offset y + * @name Konva.Shape#shadowOffsetY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get shadow offset y + * var shadowOffsetY = shape.shadowOffsetY(); + * + * // set shadow offset y + * shape.shadowOffsetY(5); + */ + Factory.addGetterSetter(Shape, 'fillPatternImage'); + /** + * get/set fill pattern image + * @name Konva.Shape#fillPatternImage + * @method + * @param {Image} image object + * @returns {Image} + * @example + * // get fill pattern image + * var fillPatternImage = shape.fillPatternImage(); + * + * // set fill pattern image + * var imageObj = new Image(); + * imageObj.onload = function() { + * shape.fillPatternImage(imageObj); + * }; + * imageObj.src = 'path/to/image/jpg'; + */ + Factory.addGetterSetter(Shape, 'fill', undefined, getStringOrGradientValidator()); + /** + * get/set fill color + * @name Konva.Shape#fill + * @method + * @param {String} color + * @returns {String} + * @example + * // get fill color + * var fill = shape.fill(); + * + * // set fill color with color string + * shape.fill('green'); + * + * // set fill color with hex + * shape.fill('#00ff00'); + * + * // set fill color with rgb + * shape.fill('rgb(0,255,0)'); + * + * // set fill color with rgba and make it 50% opaque + * shape.fill('rgba(0,255,0,0.5'); + * + * // shape without fill + * shape.fill(null); + */ + Factory.addGetterSetter(Shape, 'fillPatternX', 0, getNumberValidator()); + /** + * get/set fill pattern x + * @name Konva.Shape#fillPatternX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill pattern x + * var fillPatternX = shape.fillPatternX(); + * // set fill pattern x + * shape.fillPatternX(20); + */ + Factory.addGetterSetter(Shape, 'fillPatternY', 0, getNumberValidator()); + /** + * get/set fill pattern y + * @name Konva.Shape#fillPatternY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill pattern y + * var fillPatternY = shape.fillPatternY(); + * // set fill pattern y + * shape.fillPatternY(20); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientColorStops'); + /** + * get/set fill linear gradient color stops + * @name Konva.Shape#fillLinearGradientColorStops + * @method + * @param {Array} colorStops + * @returns {Array} colorStops + * @example + * // get fill linear gradient color stops + * var colorStops = shape.fillLinearGradientColorStops(); + * + * // create a linear gradient that starts with red, changes to blue + * // halfway through, and then changes to green + * shape.fillLinearGradientColorStops(0, 'red', 0.5, 'blue', 1, 'green'); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientColorStops'); + /** + * get/set stroke linear gradient color stops + * @name Konva.Shape#strokeLinearGradientColorStops + * @method + * @param {Array} colorStops + * @returns {Array} colorStops + * @example + * // get stroke linear gradient color stops + * var colorStops = shape.strokeLinearGradientColorStops(); + * + * // create a linear gradient that starts with red, changes to blue + * // halfway through, and then changes to green + * shape.strokeLinearGradientColorStops([0, 'red', 0.5, 'blue', 1, 'green']); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientStartRadius', 0); + /** + * get/set fill radial gradient start radius + * @name Konva.Shape#fillRadialGradientStartRadius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radial gradient start radius + * var startRadius = shape.fillRadialGradientStartRadius(); + * + * // set radial gradient start radius + * shape.fillRadialGradientStartRadius(0); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientEndRadius', 0); + /** + * get/set fill radial gradient end radius + * @name Konva.Shape#fillRadialGradientEndRadius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radial gradient end radius + * var endRadius = shape.fillRadialGradientEndRadius(); + * + * // set radial gradient end radius + * shape.fillRadialGradientEndRadius(100); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientColorStops'); + /** + * get/set fill radial gradient color stops + * @name Konva.Shape#fillRadialGradientColorStops + * @method + * @param {Number} colorStops + * @returns {Array} + * @example + * // get fill radial gradient color stops + * var colorStops = shape.fillRadialGradientColorStops(); + * + * // create a radial gradient that starts with red, changes to blue + * // halfway through, and then changes to green + * shape.fillRadialGradientColorStops(0, 'red', 0.5, 'blue', 1, 'green'); + */ + Factory.addGetterSetter(Shape, 'fillPatternRepeat', 'repeat'); + /** + * get/set fill pattern repeat. Can be 'repeat', 'repeat-x', 'repeat-y', or 'no-repeat'. The default is 'repeat' + * @name Konva.Shape#fillPatternRepeat + * @method + * @param {String} repeat + * @returns {String} + * @example + * // get fill pattern repeat + * var repeat = shape.fillPatternRepeat(); + * + * // repeat pattern in x direction only + * shape.fillPatternRepeat('repeat-x'); + * + * // do not repeat the pattern + * shape.fillPatternRepeat('no-repeat'); + */ + Factory.addGetterSetter(Shape, 'fillEnabled', true); + /** + * get/set fill enabled flag + * @name Konva.Shape#fillEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get fill enabled flag + * var fillEnabled = shape.fillEnabled(); + * + * // disable fill + * shape.fillEnabled(false); + * + * // enable fill + * shape.fillEnabled(true); + */ + Factory.addGetterSetter(Shape, 'strokeEnabled', true); + /** + * get/set stroke enabled flag + * @name Konva.Shape#strokeEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get stroke enabled flag + * var strokeEnabled = shape.strokeEnabled(); + * + * // disable stroke + * shape.strokeEnabled(false); + * + * // enable stroke + * shape.strokeEnabled(true); + */ + Factory.addGetterSetter(Shape, 'shadowEnabled', true); + /** + * get/set shadow enabled flag + * @name Konva.Shape#shadowEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get shadow enabled flag + * var shadowEnabled = shape.shadowEnabled(); + * + * // disable shadow + * shape.shadowEnabled(false); + * + * // enable shadow + * shape.shadowEnabled(true); + */ + Factory.addGetterSetter(Shape, 'dashEnabled', true); + /** + * get/set dash enabled flag + * @name Konva.Shape#dashEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get dash enabled flag + * var dashEnabled = shape.dashEnabled(); + * + * // disable dash + * shape.dashEnabled(false); + * + * // enable dash + * shape.dashEnabled(true); + */ + Factory.addGetterSetter(Shape, 'strokeScaleEnabled', true); + /** + * get/set strokeScale enabled flag + * @name Konva.Shape#strokeScaleEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get stroke scale enabled flag + * var strokeScaleEnabled = shape.strokeScaleEnabled(); + * + * // disable stroke scale + * shape.strokeScaleEnabled(false); + * + * // enable stroke scale + * shape.strokeScaleEnabled(true); + */ + Factory.addGetterSetter(Shape, 'fillPriority', 'color'); + /** + * get/set fill priority. can be color, pattern, linear-gradient, or radial-gradient. The default is color. + * This is handy if you want to toggle between different fill types. + * @name Konva.Shape#fillPriority + * @method + * @param {String} priority + * @returns {String} + * @example + * // get fill priority + * var fillPriority = shape.fillPriority(); + * + * // set fill priority + * shape.fillPriority('linear-gradient'); + */ + Factory.addComponentsGetterSetter(Shape, 'fillPatternOffset', ['x', 'y']); + /** + * get/set fill pattern offset + * @name Konva.Shape#fillPatternOffset + * @method + * @param {Object} offset + * @param {Number} offset.x + * @param {Number} offset.y + * @returns {Object} + * @example + * // get fill pattern offset + * var patternOffset = shape.fillPatternOffset(); + * + * // set fill pattern offset + * shape.fillPatternOffset({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillPatternOffsetX', 0, getNumberValidator()); + /** + * get/set fill pattern offset x + * @name Konva.Shape#fillPatternOffsetX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill pattern offset x + * var patternOffsetX = shape.fillPatternOffsetX(); + * + * // set fill pattern offset x + * shape.fillPatternOffsetX(20); + */ + Factory.addGetterSetter(Shape, 'fillPatternOffsetY', 0, getNumberValidator()); + /** + * get/set fill pattern offset y + * @name Konva.Shape#fillPatternOffsetY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill pattern offset y + * var patternOffsetY = shape.fillPatternOffsetY(); + * + * // set fill pattern offset y + * shape.fillPatternOffsetY(10); + */ + Factory.addComponentsGetterSetter(Shape, 'fillPatternScale', ['x', 'y']); + /** + * get/set fill pattern scale + * @name Konva.Shape#fillPatternScale + * @method + * @param {Object} scale + * @param {Number} scale.x + * @param {Number} scale.y + * @returns {Object} + * @example + * // get fill pattern scale + * var patternScale = shape.fillPatternScale(); + * + * // set fill pattern scale + * shape.fillPatternScale({ + * x: 2, + * y: 2 + * }); + */ + Factory.addGetterSetter(Shape, 'fillPatternScaleX', 1, getNumberValidator()); + /** + * get/set fill pattern scale x + * @name Konva.Shape#fillPatternScaleX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill pattern scale x + * var patternScaleX = shape.fillPatternScaleX(); + * + * // set fill pattern scale x + * shape.fillPatternScaleX(2); + */ + Factory.addGetterSetter(Shape, 'fillPatternScaleY', 1, getNumberValidator()); + /** + * get/set fill pattern scale y + * @name Konva.Shape#fillPatternScaleY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill pattern scale y + * var patternScaleY = shape.fillPatternScaleY(); + * + * // set fill pattern scale y + * shape.fillPatternScaleY(2); + */ + Factory.addComponentsGetterSetter(Shape, 'fillLinearGradientStartPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill linear gradient start point + * @name Konva.Shape#fillLinearGradientStartPoint + * @method + * @param {Object} startPoint + * @param {Number} startPoint.x + * @param {Number} startPoint.y + * @returns {Object} + * @example + * // get fill linear gradient start point + * var startPoint = shape.fillLinearGradientStartPoint(); + * + * // set fill linear gradient start point + * shape.fillLinearGradientStartPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addComponentsGetterSetter(Shape, 'strokeLinearGradientStartPoint', [ + 'x', + 'y', + ]); + /** + * get/set stroke linear gradient start point + * @name Konva.Shape#strokeLinearGradientStartPoint + * @method + * @param {Object} startPoint + * @param {Number} startPoint.x + * @param {Number} startPoint.y + * @returns {Object} + * @example + * // get stroke linear gradient start point + * var startPoint = shape.strokeLinearGradientStartPoint(); + * + * // set stroke linear gradient start point + * shape.strokeLinearGradientStartPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientStartPointX', 0); + /** + * get/set fill linear gradient start point x + * @name Konva.Shape#fillLinearGradientStartPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill linear gradient start point x + * var startPointX = shape.fillLinearGradientStartPointX(); + * + * // set fill linear gradient start point x + * shape.fillLinearGradientStartPointX(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientStartPointX', 0); + /** + * get/set stroke linear gradient start point x + * @name Konva.Shape#linearLinearGradientStartPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get stroke linear gradient start point x + * var startPointX = shape.strokeLinearGradientStartPointX(); + * + * // set stroke linear gradient start point x + * shape.strokeLinearGradientStartPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientStartPointY', 0); + /** + * get/set fill linear gradient start point y + * @name Konva.Shape#fillLinearGradientStartPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill linear gradient start point y + * var startPointY = shape.fillLinearGradientStartPointY(); + * + * // set fill linear gradient start point y + * shape.fillLinearGradientStartPointY(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientStartPointY', 0); + /** + * get/set stroke linear gradient start point y + * @name Konva.Shape#strokeLinearGradientStartPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get stroke linear gradient start point y + * var startPointY = shape.strokeLinearGradientStartPointY(); + * + * // set stroke linear gradient start point y + * shape.strokeLinearGradientStartPointY(20); + */ + Factory.addComponentsGetterSetter(Shape, 'fillLinearGradientEndPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill linear gradient end point + * @name Konva.Shape#fillLinearGradientEndPoint + * @method + * @param {Object} endPoint + * @param {Number} endPoint.x + * @param {Number} endPoint.y + * @returns {Object} + * @example + * // get fill linear gradient end point + * var endPoint = shape.fillLinearGradientEndPoint(); + * + * // set fill linear gradient end point + * shape.fillLinearGradientEndPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addComponentsGetterSetter(Shape, 'strokeLinearGradientEndPoint', [ + 'x', + 'y', + ]); + /** + * get/set stroke linear gradient end point + * @name Konva.Shape#strokeLinearGradientEndPoint + * @method + * @param {Object} endPoint + * @param {Number} endPoint.x + * @param {Number} endPoint.y + * @returns {Object} + * @example + * // get stroke linear gradient end point + * var endPoint = shape.strokeLinearGradientEndPoint(); + * + * // set stroke linear gradient end point + * shape.strokeLinearGradientEndPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientEndPointX', 0); + /** + * get/set fill linear gradient end point x + * @name Konva.Shape#fillLinearGradientEndPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill linear gradient end point x + * var endPointX = shape.fillLinearGradientEndPointX(); + * + * // set fill linear gradient end point x + * shape.fillLinearGradientEndPointX(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientEndPointX', 0); + /** + * get/set fill linear gradient end point x + * @name Konva.Shape#strokeLinearGradientEndPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get stroke linear gradient end point x + * var endPointX = shape.strokeLinearGradientEndPointX(); + * + * // set stroke linear gradient end point x + * shape.strokeLinearGradientEndPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillLinearGradientEndPointY', 0); + /** + * get/set fill linear gradient end point y + * @name Konva.Shape#fillLinearGradientEndPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill linear gradient end point y + * var endPointY = shape.fillLinearGradientEndPointY(); + * + * // set fill linear gradient end point y + * shape.fillLinearGradientEndPointY(20); + */ + Factory.addGetterSetter(Shape, 'strokeLinearGradientEndPointY', 0); + /** + * get/set stroke linear gradient end point y + * @name Konva.Shape#strokeLinearGradientEndPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get stroke linear gradient end point y + * var endPointY = shape.strokeLinearGradientEndPointY(); + * + * // set stroke linear gradient end point y + * shape.strokeLinearGradientEndPointY(20); + */ + Factory.addComponentsGetterSetter(Shape, 'fillRadialGradientStartPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill radial gradient start point + * @name Konva.Shape#fillRadialGradientStartPoint + * @method + * @param {Object} startPoint + * @param {Number} startPoint.x + * @param {Number} startPoint.y + * @returns {Object} + * @example + * // get fill radial gradient start point + * var startPoint = shape.fillRadialGradientStartPoint(); + * + * // set fill radial gradient start point + * shape.fillRadialGradientStartPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientStartPointX', 0); + /** + * get/set fill radial gradient start point x + * @name Konva.Shape#fillRadialGradientStartPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill radial gradient start point x + * var startPointX = shape.fillRadialGradientStartPointX(); + * + * // set fill radial gradient start point x + * shape.fillRadialGradientStartPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientStartPointY', 0); + /** + * get/set fill radial gradient start point y + * @name Konva.Shape#fillRadialGradientStartPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill radial gradient start point y + * var startPointY = shape.fillRadialGradientStartPointY(); + * + * // set fill radial gradient start point y + * shape.fillRadialGradientStartPointY(20); + */ + Factory.addComponentsGetterSetter(Shape, 'fillRadialGradientEndPoint', [ + 'x', + 'y', + ]); + /** + * get/set fill radial gradient end point + * @name Konva.Shape#fillRadialGradientEndPoint + * @method + * @param {Object} endPoint + * @param {Number} endPoint.x + * @param {Number} endPoint.y + * @returns {Object} + * @example + * // get fill radial gradient end point + * var endPoint = shape.fillRadialGradientEndPoint(); + * + * // set fill radial gradient end point + * shape.fillRadialGradientEndPoint({ + * x: 20, + * y: 10 + * }); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientEndPointX', 0); + /** + * get/set fill radial gradient end point x + * @name Konva.Shape#fillRadialGradientEndPointX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get fill radial gradient end point x + * var endPointX = shape.fillRadialGradientEndPointX(); + * + * // set fill radial gradient end point x + * shape.fillRadialGradientEndPointX(20); + */ + Factory.addGetterSetter(Shape, 'fillRadialGradientEndPointY', 0); + /** + * get/set fill radial gradient end point y + * @name Konva.Shape#fillRadialGradientEndPointY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get fill radial gradient end point y + * var endPointY = shape.fillRadialGradientEndPointY(); + * + * // set fill radial gradient end point y + * shape.fillRadialGradientEndPointY(20); + */ + Factory.addGetterSetter(Shape, 'fillPatternRotation', 0); + /** + * get/set fill pattern rotation in degrees + * @name Konva.Shape#fillPatternRotation + * @method + * @param {Number} rotation + * @returns {Konva.Shape} + * @example + * // get fill pattern rotation + * var patternRotation = shape.fillPatternRotation(); + * + * // set fill pattern rotation + * shape.fillPatternRotation(20); + */ + Factory.addGetterSetter(Shape, 'fillRule', undefined, getStringValidator()); + /** + * get/set fill rule + * @name Konva.Shape#fillRule + * @method + * @param {CanvasFillRule} rotation + * @returns {Konva.Shape} + * @example + * // get fill rule + * var fillRule = shape.fillRule(); + * + * // set fill rule + * shape.fillRule('evenodd'); + */ + Factory.backCompat(Shape, { + dashArray: 'dash', + getDashArray: 'getDash', + setDashArray: 'getDash', + drawFunc: 'sceneFunc', + getDrawFunc: 'getSceneFunc', + setDrawFunc: 'setSceneFunc', + drawHitFunc: 'hitFunc', + getDrawHitFunc: 'getHitFunc', + setDrawHitFunc: 'setHitFunc', + }); + + // constants + const HASH = '#', BEFORE_DRAW = 'beforeDraw', DRAW = 'draw', + /* + * 2 - 3 - 4 + * | | + * 1 - 0 5 + * | + * 8 - 7 - 6 + */ + INTERSECTION_OFFSETS = [ + { x: 0, y: 0 }, // 0 + { x: -1, y: -1 }, // 2 + { x: 1, y: -1 }, // 4 + { x: 1, y: 1 }, // 6 + { x: -1, y: 1 }, // 8 + ], INTERSECTION_OFFSETS_LEN = INTERSECTION_OFFSETS.length; + /** + * Layer constructor. Layers are tied to their own canvas element and are used + * to contain groups or shapes. + * @constructor + * @memberof Konva + * @augments Konva.Container + * @param {Object} config + * @param {Boolean} [config.clearBeforeDraw] set this property to false if you don't want + * to clear the canvas before each layer draw. The default value is true. + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + * @example + * var layer = new Konva.Layer(); + * stage.add(layer); + * // now you can add shapes, groups into the layer + */ + class Layer extends Container { + constructor(config) { + super(config); + this.canvas = new SceneCanvas(); + this.hitCanvas = new HitCanvas({ + pixelRatio: 1, + }); + this._waitingForDraw = false; + this.on('visibleChange.konva', this._checkVisibility); + this._checkVisibility(); + this.on('imageSmoothingEnabledChange.konva', this._setSmoothEnabled); + this._setSmoothEnabled(); + } + // for nodejs? + createPNGStream() { + const c = this.canvas._canvas; + return c.createPNGStream(); + } + /** + * get layer canvas wrapper + * @method + * @name Konva.Layer#getCanvas + */ + getCanvas() { + return this.canvas; + } + /** + * get native canvas element + * @method + * @name Konva.Layer#getNativeCanvasElement + */ + getNativeCanvasElement() { + return this.canvas._canvas; + } + /** + * get layer hit canvas + * @method + * @name Konva.Layer#getHitCanvas + */ + getHitCanvas() { + return this.hitCanvas; + } + /** + * get layer canvas context + * @method + * @name Konva.Layer#getContext + */ + getContext() { + return this.getCanvas().getContext(); + } + // TODO: deprecate this method + clear(bounds) { + this.getContext().clear(bounds); + this.getHitCanvas().getContext().clear(bounds); + return this; + } + // extend Node.prototype.setZIndex + setZIndex(index) { + super.setZIndex(index); + const stage = this.getStage(); + if (stage && stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + if (index < stage.children.length - 1) { + stage.content.insertBefore(this.getNativeCanvasElement(), stage.children[index + 1].getCanvas()._canvas); + } + else { + stage.content.appendChild(this.getNativeCanvasElement()); + } + } + return this; + } + moveToTop() { + Node.prototype.moveToTop.call(this); + const stage = this.getStage(); + if (stage && stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + stage.content.appendChild(this.getNativeCanvasElement()); + } + return true; + } + moveUp() { + const moved = Node.prototype.moveUp.call(this); + if (!moved) { + return false; + } + const stage = this.getStage(); + if (!stage || !stage.content) { + return false; + } + stage.content.removeChild(this.getNativeCanvasElement()); + if (this.index < stage.children.length - 1) { + stage.content.insertBefore(this.getNativeCanvasElement(), stage.children[this.index + 1].getCanvas()._canvas); + } + else { + stage.content.appendChild(this.getNativeCanvasElement()); + } + return true; + } + // extend Node.prototype.moveDown + moveDown() { + if (Node.prototype.moveDown.call(this)) { + const stage = this.getStage(); + if (stage) { + const children = stage.children; + if (stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + stage.content.insertBefore(this.getNativeCanvasElement(), children[this.index + 1].getCanvas()._canvas); + } + } + return true; + } + return false; + } + // extend Node.prototype.moveToBottom + moveToBottom() { + if (Node.prototype.moveToBottom.call(this)) { + const stage = this.getStage(); + if (stage) { + const children = stage.children; + if (stage.content) { + stage.content.removeChild(this.getNativeCanvasElement()); + stage.content.insertBefore(this.getNativeCanvasElement(), children[1].getCanvas()._canvas); + } + } + return true; + } + return false; + } + getLayer() { + return this; + } + remove() { + const _canvas = this.getNativeCanvasElement(); + Node.prototype.remove.call(this); + if (_canvas && _canvas.parentNode && Util._isInDocument(_canvas)) { + _canvas.parentNode.removeChild(_canvas); + } + return this; + } + getStage() { + return this.parent; + } + setSize({ width, height }) { + this.canvas.setSize(width, height); + this.hitCanvas.setSize(width, height); + this._setSmoothEnabled(); + return this; + } + _validateAdd(child) { + const type = child.getType(); + if (type !== 'Group' && type !== 'Shape') { + Util.throw('You may only add groups and shapes to a layer.'); + } + } + _toKonvaCanvas(config) { + config = config || {}; + config.width = config.width || this.getWidth(); + config.height = config.height || this.getHeight(); + config.x = config.x !== undefined ? config.x : this.x(); + config.y = config.y !== undefined ? config.y : this.y(); + return Node.prototype._toKonvaCanvas.call(this, config); + } + _checkVisibility() { + const visible = this.visible(); + if (visible) { + this.canvas._canvas.style.display = 'block'; + } + else { + this.canvas._canvas.style.display = 'none'; + } + } + _setSmoothEnabled() { + this.getContext()._context.imageSmoothingEnabled = + this.imageSmoothingEnabled(); + } + /** + * get/set width of layer. getter return width of stage. setter doing nothing. + * if you want change width use `stage.width(value);` + * @name Konva.Layer#width + * @method + * @returns {Number} + * @example + * var width = layer.width(); + */ + getWidth() { + if (this.parent) { + return this.parent.width(); + } + } + setWidth() { + Util.warn('Can not change width of layer. Use "stage.width(value)" function instead.'); + } + /** + * get/set height of layer.getter return height of stage. setter doing nothing. + * if you want change height use `stage.height(value);` + * @name Konva.Layer#height + * @method + * @returns {Number} + * @example + * var height = layer.height(); + */ + getHeight() { + if (this.parent) { + return this.parent.height(); + } + } + setHeight() { + Util.warn('Can not change height of layer. Use "stage.height(value)" function instead.'); + } + /** + * batch draw. this function will not do immediate draw + * but it will schedule drawing to next tick (requestAnimFrame) + * @method + * @name Konva.Layer#batchDraw + * @return {Konva.Layer} this + */ + batchDraw() { + if (!this._waitingForDraw) { + this._waitingForDraw = true; + Util.requestAnimFrame(() => { + this.draw(); + this._waitingForDraw = false; + }); + } + return this; + } + /** + * get visible intersection shape. This is the preferred + * method for determining if a point intersects a shape or not + * also you may pass optional selector parameter to return ancestor of intersected shape + * nodes with listening set to false will not be detected + * @method + * @name Konva.Layer#getIntersection + * @param {Object} pos + * @param {Number} pos.x + * @param {Number} pos.y + * @returns {Konva.Node} + * @example + * var shape = layer.getIntersection({x: 50, y: 50}); + */ + getIntersection(pos) { + if (!this.isListening() || !this.isVisible()) { + return null; + } + // in some cases antialiased area may be bigger than 1px + // it is possible if we will cache node, then scale it a lot + let spiralSearchDistance = 1; + let continueSearch = false; + while (true) { + for (let i = 0; i < INTERSECTION_OFFSETS_LEN; i++) { + const intersectionOffset = INTERSECTION_OFFSETS[i]; + const obj = this._getIntersection({ + x: pos.x + intersectionOffset.x * spiralSearchDistance, + y: pos.y + intersectionOffset.y * spiralSearchDistance, + }); + const shape = obj.shape; + if (shape) { + return shape; + } + // we should continue search if we found antialiased pixel + // that means our node somewhere very close + continueSearch = !!obj.antialiased; + // stop search if found empty pixel + if (!obj.antialiased) { + break; + } + } + // if no shape, and no antialiased pixel, we should end searching + if (continueSearch) { + spiralSearchDistance += 1; + } + else { + return null; + } + } + } + _getIntersection(pos) { + const ratio = this.hitCanvas.pixelRatio; + const p = this.hitCanvas.context.getImageData(Math.round(pos.x * ratio), Math.round(pos.y * ratio), 1, 1).data; + const p3 = p[3]; + // fully opaque pixel + if (p3 === 255) { + const colorKey = Util._rgbToHex(p[0], p[1], p[2]); + const shape = shapes[HASH + colorKey]; + if (shape) { + return { + shape: shape, + }; + } + return { + antialiased: true, + }; + } + else if (p3 > 0) { + // antialiased pixel + return { + antialiased: true, + }; + } + // empty pixel + return {}; + } + drawScene(can, top) { + const layer = this.getLayer(), canvas = can || (layer && layer.getCanvas()); + this._fire(BEFORE_DRAW, { + node: this, + }); + if (this.clearBeforeDraw()) { + canvas.getContext().clear(); + } + Container.prototype.drawScene.call(this, canvas, top); + this._fire(DRAW, { + node: this, + }); + return this; + } + drawHit(can, top) { + const layer = this.getLayer(), canvas = can || (layer && layer.hitCanvas); + if (layer && layer.clearBeforeDraw()) { + layer.getHitCanvas().getContext().clear(); + } + Container.prototype.drawHit.call(this, canvas, top); + return this; + } + /** + * enable hit graph. **DEPRECATED!** Use `layer.listening(true)` instead. + * @name Konva.Layer#enableHitGraph + * @method + * @returns {Layer} + */ + enableHitGraph() { + this.hitGraphEnabled(true); + return this; + } + /** + * disable hit graph. **DEPRECATED!** Use `layer.listening(false)` instead. + * @name Konva.Layer#disableHitGraph + * @method + * @returns {Layer} + */ + disableHitGraph() { + this.hitGraphEnabled(false); + return this; + } + setHitGraphEnabled(val) { + Util.warn('hitGraphEnabled method is deprecated. Please use layer.listening() instead.'); + this.listening(val); + } + getHitGraphEnabled(val) { + Util.warn('hitGraphEnabled method is deprecated. Please use layer.listening() instead.'); + return this.listening(); + } + /** + * Show or hide hit canvas over the stage. May be useful for debugging custom hitFunc + * @name Konva.Layer#toggleHitCanvas + * @method + */ + toggleHitCanvas() { + if (!this.parent || !this.parent['content']) { + return; + } + const parent = this.parent; + const added = !!this.hitCanvas._canvas.parentNode; + if (added) { + parent.content.removeChild(this.hitCanvas._canvas); + } + else { + parent.content.appendChild(this.hitCanvas._canvas); + } + } + destroy() { + Util.releaseCanvas(this.getNativeCanvasElement(), this.getHitCanvas()._canvas); + return super.destroy(); + } + } + Layer.prototype.nodeType = 'Layer'; + _registerNode(Layer); + /** + * get/set imageSmoothingEnabled flag + * For more info see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled + * @name Konva.Layer#imageSmoothingEnabled + * @method + * @param {Boolean} imageSmoothingEnabled + * @returns {Boolean} + * @example + * // get imageSmoothingEnabled flag + * var imageSmoothingEnabled = layer.imageSmoothingEnabled(); + * + * layer.imageSmoothingEnabled(false); + * + * layer.imageSmoothingEnabled(true); + */ + Factory.addGetterSetter(Layer, 'imageSmoothingEnabled', true); + /** + * get/set clearBeforeDraw flag which determines if the layer is cleared or not + * before drawing + * @name Konva.Layer#clearBeforeDraw + * @method + * @param {Boolean} clearBeforeDraw + * @returns {Boolean} + * @example + * // get clearBeforeDraw flag + * var clearBeforeDraw = layer.clearBeforeDraw(); + * + * // disable clear before draw + * layer.clearBeforeDraw(false); + * + * // enable clear before draw + * layer.clearBeforeDraw(true); + */ + Factory.addGetterSetter(Layer, 'clearBeforeDraw', true); + Factory.addGetterSetter(Layer, 'hitGraphEnabled', true, getBooleanValidator()); + /** + * get/set hitGraphEnabled flag. **DEPRECATED!** Use `layer.listening(false)` instead. + * Disabling the hit graph will greatly increase + * draw performance because the hit graph will not be redrawn each time the layer is + * drawn. This, however, also disables mouse/touch event detection + * @name Konva.Layer#hitGraphEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get hitGraphEnabled flag + * var hitGraphEnabled = layer.hitGraphEnabled(); + * + * // disable hit graph + * layer.hitGraphEnabled(false); + * + * // enable hit graph + * layer.hitGraphEnabled(true); + */ + + /** + * FastLayer constructor. **DEPRECATED!** Please use `Konva.Layer({ listening: false})` instead. Layers are tied to their own canvas element and are used + * to contain shapes only. If you don't need node nesting, mouse and touch interactions, + * or event pub/sub, you should use FastLayer instead of Layer to create your layers. + * It renders about 2x faster than normal layers. + * + * @constructor + * @memberof Konva + * @augments Konva.Layer + * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + * @example + * var layer = new Konva.FastLayer(); + */ + class FastLayer extends Layer { + constructor(attrs) { + super(attrs); + this.listening(false); + Util.warn('Konva.Fast layer is deprecated. Please use "new Konva.Layer({ listening: false })" instead.'); + } + } + FastLayer.prototype.nodeType = 'FastLayer'; + _registerNode(FastLayer); + + /** + * Group constructor. Groups are used to contain shapes or other groups. + * @constructor + * @memberof Konva + * @augments Konva.Container + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + * @param {Function} [config.clipFunc] set clip func + + * @example + * var group = new Konva.Group(); + */ + class Group extends Container { + _validateAdd(child) { + const type = child.getType(); + if (type !== 'Group' && type !== 'Shape') { + Util.throw('You may only add groups and shapes to groups.'); + } + } + } + Group.prototype.nodeType = 'Group'; + _registerNode(Group); + + const now = (function () { + if (glob.performance && glob.performance.now) { + return function () { + return glob.performance.now(); + }; + } + return function () { + return new Date().getTime(); + }; + })(); + /** + * Animation constructor. + * @constructor + * @memberof Konva + * @param {AnimationFn} func function executed on each animation frame. The function is passed a frame object, which contains + * timeDiff, lastTime, time, and frameRate properties. The timeDiff property is the number of milliseconds that have passed + * since the last animation frame. The time property is the time in milliseconds that elapsed from the moment the animation started + * to the current animation frame. The lastTime property is a `time` value from the previous frame. The frameRate property is the current frame rate in frames / second. + * Return false from function, if you don't need to redraw layer/layers on some frames. + * @param {Konva.Layer|Array} [layers] layer(s) to be redrawn on each animation frame. Can be a layer, an array of layers, or null. + * Not specifying a node will result in no redraw. + * @example + * // move a node to the right at 50 pixels / second + * var velocity = 50; + * + * var anim = new Konva.Animation(function(frame) { + * var dist = velocity * (frame.timeDiff / 1000); + * node.move({x: dist, y: 0}); + * }, layer); + * + * anim.start(); + */ + class Animation { + constructor(func, layers) { + this.id = Animation.animIdCounter++; + this.frame = { + time: 0, + timeDiff: 0, + lastTime: now(), + frameRate: 0, + }; + this.func = func; + this.setLayers(layers); + } + /** + * set layers to be redrawn on each animation frame + * @method + * @name Konva.Animation#setLayers + * @param {Konva.Layer|Array} [layers] layer(s) to be redrawn. Can be a layer, an array of layers, or null. Not specifying a node will result in no redraw. + * @return {Konva.Animation} this + */ + setLayers(layers) { + let lays = []; + // if passing in no layers + if (layers) { + lays = Array.isArray(layers) ? layers : [layers]; + } + this.layers = lays; + return this; + } + /** + * get layers + * @method + * @name Konva.Animation#getLayers + * @return {Array} Array of Konva.Layer + */ + getLayers() { + return this.layers; + } + /** + * add layer. Returns true if the layer was added, and false if it was not + * @method + * @name Konva.Animation#addLayer + * @param {Konva.Layer} layer to add + * @return {Bool} true if layer is added to animation, otherwise false + */ + addLayer(layer) { + const layers = this.layers; + const len = layers.length; + // don't add the layer if it already exists + for (let n = 0; n < len; n++) { + if (layers[n]._id === layer._id) { + return false; + } + } + this.layers.push(layer); + return true; + } + /** + * determine if animation is running or not. returns true or false + * @method + * @name Konva.Animation#isRunning + * @return {Bool} is animation running? + */ + isRunning() { + const a = Animation; + const animations = a.animations; + const len = animations.length; + for (let n = 0; n < len; n++) { + if (animations[n].id === this.id) { + return true; + } + } + return false; + } + /** + * start animation + * @method + * @name Konva.Animation#start + * @return {Konva.Animation} this + */ + start() { + this.stop(); + this.frame.timeDiff = 0; + this.frame.lastTime = now(); + Animation._addAnimation(this); + return this; + } + /** + * stop animation + * @method + * @name Konva.Animation#stop + * @return {Konva.Animation} this + */ + stop() { + Animation._removeAnimation(this); + return this; + } + _updateFrameObject(time) { + this.frame.timeDiff = time - this.frame.lastTime; + this.frame.lastTime = time; + this.frame.time += this.frame.timeDiff; + this.frame.frameRate = 1000 / this.frame.timeDiff; + } + static _addAnimation(anim) { + this.animations.push(anim); + this._handleAnimation(); + } + static _removeAnimation(anim) { + const id = anim.id; + const animations = this.animations; + const len = animations.length; + for (let n = 0; n < len; n++) { + if (animations[n].id === id) { + this.animations.splice(n, 1); + break; + } + } + } + static _runFrames() { + const layerHash = {}; + const animations = this.animations; + /* + * loop through all animations and execute animation + * function. if the animation object has specified node, + * we can add the node to the nodes hash to eliminate + * drawing the same node multiple times. The node property + * can be the stage itself or a layer + */ + /* + * WARNING: don't cache animations.length because it could change while + * the for loop is running, causing a JS error + */ + for (let n = 0; n < animations.length; n++) { + const anim = animations[n]; + const layers = anim.layers; + const func = anim.func; + anim._updateFrameObject(now()); + const layersLen = layers.length; + // if animation object has a function, execute it + let needRedraw; + if (func) { + // allow anim bypassing drawing + needRedraw = func.call(anim, anim.frame) !== false; + } + else { + needRedraw = true; + } + if (!needRedraw) { + continue; + } + for (let i = 0; i < layersLen; i++) { + const layer = layers[i]; + if (layer._id !== undefined) { + layerHash[layer._id] = layer; + } + } + } + for (const key in layerHash) { + if (!layerHash.hasOwnProperty(key)) { + continue; + } + layerHash[key].batchDraw(); + } + } + static _animationLoop() { + const Anim = Animation; + if (Anim.animations.length) { + Anim._runFrames(); + Util.requestAnimFrame(Anim._animationLoop); + } + else { + Anim.animRunning = false; + } + } + static _handleAnimation() { + if (!this.animRunning) { + this.animRunning = true; + Util.requestAnimFrame(this._animationLoop); + } + } + } + Animation.animations = []; + Animation.animIdCounter = 0; + Animation.animRunning = false; + + const blacklist = { + node: 1, + duration: 1, + easing: 1, + onFinish: 1, + yoyo: 1, + }, PAUSED = 1, PLAYING = 2, REVERSING = 3, colorAttrs = ['fill', 'stroke', 'shadowColor']; + let idCounter = 0; + class TweenEngine { + constructor(prop, propFunc, func, begin, finish, duration, yoyo) { + this.prop = prop; + this.propFunc = propFunc; + this.begin = begin; + this._pos = begin; + this.duration = duration; + this._change = 0; + this.prevPos = 0; + this.yoyo = yoyo; + this._time = 0; + this._position = 0; + this._startTime = 0; + this._finish = 0; + this.func = func; + this._change = finish - this.begin; + this.pause(); + } + fire(str) { + const handler = this[str]; + if (handler) { + handler(); + } + } + setTime(t) { + if (t > this.duration) { + if (this.yoyo) { + this._time = this.duration; + this.reverse(); + } + else { + this.finish(); + } + } + else if (t < 0) { + if (this.yoyo) { + this._time = 0; + this.play(); + } + else { + this.reset(); + } + } + else { + this._time = t; + this.update(); + } + } + getTime() { + return this._time; + } + setPosition(p) { + this.prevPos = this._pos; + this.propFunc(p); + this._pos = p; + } + getPosition(t) { + if (t === undefined) { + t = this._time; + } + return this.func(t, this.begin, this._change, this.duration); + } + play() { + this.state = PLAYING; + this._startTime = this.getTimer() - this._time; + this.onEnterFrame(); + this.fire('onPlay'); + } + reverse() { + this.state = REVERSING; + this._time = this.duration - this._time; + this._startTime = this.getTimer() - this._time; + this.onEnterFrame(); + this.fire('onReverse'); + } + seek(t) { + this.pause(); + this._time = t; + this.update(); + this.fire('onSeek'); + } + reset() { + this.pause(); + this._time = 0; + this.update(); + this.fire('onReset'); + } + finish() { + this.pause(); + this._time = this.duration; + this.update(); + this.fire('onFinish'); + } + update() { + this.setPosition(this.getPosition(this._time)); + this.fire('onUpdate'); + } + onEnterFrame() { + const t = this.getTimer() - this._startTime; + if (this.state === PLAYING) { + this.setTime(t); + } + else if (this.state === REVERSING) { + this.setTime(this.duration - t); + } + } + pause() { + this.state = PAUSED; + this.fire('onPause'); + } + getTimer() { + return new Date().getTime(); + } + } + /** + * Tween constructor. Tweens enable you to animate a node between the current state and a new state. + * You can play, pause, reverse, seek, reset, and finish tweens. By default, tweens are animated using + * a linear easing. For more tweening options, check out {@link Konva.Easings} + * @constructor + * @memberof Konva + * @example + * // instantiate new tween which fully rotates a node in 1 second + * var tween = new Konva.Tween({ + * // list of tween specific properties + * node: node, + * duration: 1, + * easing: Konva.Easings.EaseInOut, + * onUpdate: () => console.log('node attrs updated') + * onFinish: () => console.log('finished'), + * // set new values for any attributes of a passed node + * rotation: 360, + * fill: 'red' + * }); + * + * // play tween + * tween.play(); + * + * // pause tween + * tween.pause(); + */ + class Tween { + constructor(config) { + const that = this, node = config.node, nodeId = node._id, easing = config.easing || Easings.Linear, yoyo = !!config.yoyo; + let duration, key; + if (typeof config.duration === 'undefined') { + duration = 0.3; + } + else if (config.duration === 0) { + // zero is bad value for duration + duration = 0.001; + } + else { + duration = config.duration; + } + this.node = node; + this._id = idCounter++; + const layers = node.getLayer() || + (node instanceof Konva$2['Stage'] ? node.getLayers() : null); + if (!layers) { + Util.error('Tween constructor have `node` that is not in a layer. Please add node into layer first.'); + } + this.anim = new Animation(function () { + that.tween.onEnterFrame(); + }, layers); + this.tween = new TweenEngine(key, function (i) { + that._tweenFunc(i); + }, easing, 0, 1, duration * 1000, yoyo); + this._addListeners(); + // init attrs map + if (!Tween.attrs[nodeId]) { + Tween.attrs[nodeId] = {}; + } + if (!Tween.attrs[nodeId][this._id]) { + Tween.attrs[nodeId][this._id] = {}; + } + // init tweens map + if (!Tween.tweens[nodeId]) { + Tween.tweens[nodeId] = {}; + } + for (key in config) { + if (blacklist[key] === undefined) { + this._addAttr(key, config[key]); + } + } + this.reset(); + // callbacks + this.onFinish = config.onFinish; + this.onReset = config.onReset; + this.onUpdate = config.onUpdate; + } + _addAttr(key, end) { + const node = this.node, nodeId = node._id; + let diff, len, trueEnd, trueStart, endRGBA; + // remove conflict from tween map if it exists + const tweenId = Tween.tweens[nodeId][key]; + if (tweenId) { + delete Tween.attrs[nodeId][tweenId][key]; + } + // add to tween map + let start = node.getAttr(key); + if (Util._isArray(end)) { + diff = []; + len = Math.max(end.length, start.length); + if (key === 'points' && end.length !== start.length) { + // before tweening points we need to make sure that start.length === end.length + // Util._prepareArrayForTween thinking that end.length > start.length + if (end.length > start.length) { + // so in this case we will increase number of starting points + trueStart = start; + start = Util._prepareArrayForTween(start, end, node.closed()); + } + else { + // in this case we will increase number of eding points + trueEnd = end; + end = Util._prepareArrayForTween(end, start, node.closed()); + } + } + if (key.indexOf('fill') === 0) { + for (let n = 0; n < len; n++) { + if (n % 2 === 0) { + diff.push(end[n] - start[n]); + } + else { + const startRGBA = Util.colorToRGBA(start[n]); + endRGBA = Util.colorToRGBA(end[n]); + start[n] = startRGBA; + diff.push({ + r: endRGBA.r - startRGBA.r, + g: endRGBA.g - startRGBA.g, + b: endRGBA.b - startRGBA.b, + a: endRGBA.a - startRGBA.a, + }); + } + } + } + else { + for (let n = 0; n < len; n++) { + diff.push(end[n] - start[n]); + } + } + } + else if (colorAttrs.indexOf(key) !== -1) { + start = Util.colorToRGBA(start); + endRGBA = Util.colorToRGBA(end); + diff = { + r: endRGBA.r - start.r, + g: endRGBA.g - start.g, + b: endRGBA.b - start.b, + a: endRGBA.a - start.a, + }; + } + else { + diff = end - start; + } + Tween.attrs[nodeId][this._id][key] = { + start: start, + diff: diff, + end: end, + trueEnd: trueEnd, + trueStart: trueStart, + }; + Tween.tweens[nodeId][key] = this._id; + } + _tweenFunc(i) { + const node = this.node, attrs = Tween.attrs[node._id][this._id]; + let key, attr, start, diff, newVal, n, len, end; + for (key in attrs) { + attr = attrs[key]; + start = attr.start; + diff = attr.diff; + end = attr.end; + if (Util._isArray(start)) { + newVal = []; + len = Math.max(start.length, end.length); + if (key.indexOf('fill') === 0) { + for (n = 0; n < len; n++) { + if (n % 2 === 0) { + newVal.push((start[n] || 0) + diff[n] * i); + } + else { + newVal.push('rgba(' + + Math.round(start[n].r + diff[n].r * i) + + ',' + + Math.round(start[n].g + diff[n].g * i) + + ',' + + Math.round(start[n].b + diff[n].b * i) + + ',' + + (start[n].a + diff[n].a * i) + + ')'); + } + } + } + else { + for (n = 0; n < len; n++) { + newVal.push((start[n] || 0) + diff[n] * i); + } + } + } + else if (colorAttrs.indexOf(key) !== -1) { + newVal = + 'rgba(' + + Math.round(start.r + diff.r * i) + + ',' + + Math.round(start.g + diff.g * i) + + ',' + + Math.round(start.b + diff.b * i) + + ',' + + (start.a + diff.a * i) + + ')'; + } + else { + newVal = start + diff * i; + } + node.setAttr(key, newVal); + } + } + _addListeners() { + // start listeners + this.tween.onPlay = () => { + this.anim.start(); + }; + this.tween.onReverse = () => { + this.anim.start(); + }; + // stop listeners + this.tween.onPause = () => { + this.anim.stop(); + }; + this.tween.onFinish = () => { + const node = this.node; + // after tweening points of line we need to set original end + const attrs = Tween.attrs[node._id][this._id]; + if (attrs.points && attrs.points.trueEnd) { + node.setAttr('points', attrs.points.trueEnd); + } + if (this.onFinish) { + this.onFinish.call(this); + } + }; + this.tween.onReset = () => { + const node = this.node; + // after tweening points of line we need to set original start + const attrs = Tween.attrs[node._id][this._id]; + if (attrs.points && attrs.points.trueStart) { + node.points(attrs.points.trueStart); + } + if (this.onReset) { + this.onReset(); + } + }; + this.tween.onUpdate = () => { + if (this.onUpdate) { + this.onUpdate.call(this); + } + }; + } + /** + * play + * @method + * @name Konva.Tween#play + * @returns {Tween} + */ + play() { + this.tween.play(); + return this; + } + /** + * reverse + * @method + * @name Konva.Tween#reverse + * @returns {Tween} + */ + reverse() { + this.tween.reverse(); + return this; + } + /** + * reset + * @method + * @name Konva.Tween#reset + * @returns {Tween} + */ + reset() { + this.tween.reset(); + return this; + } + /** + * seek + * @method + * @name Konva.Tween#seek( + * @param {Integer} t time in seconds between 0 and the duration + * @returns {Tween} + */ + seek(t) { + this.tween.seek(t * 1000); + return this; + } + /** + * pause + * @method + * @name Konva.Tween#pause + * @returns {Tween} + */ + pause() { + this.tween.pause(); + return this; + } + /** + * finish + * @method + * @name Konva.Tween#finish + * @returns {Tween} + */ + finish() { + this.tween.finish(); + return this; + } + /** + * destroy + * @method + * @name Konva.Tween#destroy + */ + destroy() { + const nodeId = this.node._id, thisId = this._id, attrs = Tween.tweens[nodeId]; + this.pause(); + for (const key in attrs) { + delete Tween.tweens[nodeId][key]; + } + delete Tween.attrs[nodeId][thisId]; + } + } + Tween.attrs = {}; + Tween.tweens = {}; + /** + * Tween node properties. Shorter usage of {@link Konva.Tween} object. + * + * @method Konva.Node#to + * @param {Object} [params] tween params + * @example + * + * circle.to({ + * x : 50, + * duration : 0.5, + * onUpdate: () => console.log('props updated'), + * onFinish: () => console.log('finished'), + * }); + */ + Node.prototype.to = function (params) { + const onFinish = params.onFinish; + params.node = this; + params.onFinish = function () { + this.destroy(); + if (onFinish) { + onFinish(); + } + }; + const tween = new Tween(params); + tween.play(); + }; + /* + * These eases were ported from an Adobe Flash tweening library to JavaScript + * by Xaric + */ + /** + * @namespace Easings + * @memberof Konva + */ + const Easings = { + /** + * back ease in + * @function + * @memberof Konva.Easings + */ + BackEaseIn(t, b, c, d) { + const s = 1.70158; + return c * (t /= d) * t * ((s + 1) * t - s) + b; + }, + /** + * back ease out + * @function + * @memberof Konva.Easings + */ + BackEaseOut(t, b, c, d) { + const s = 1.70158; + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + }, + /** + * back ease in out + * @function + * @memberof Konva.Easings + */ + BackEaseInOut(t, b, c, d) { + let s = 1.70158; + if ((t /= d / 2) < 1) { + return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b; + } + return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b; + }, + /** + * elastic ease in + * @function + * @memberof Konva.Easings + */ + ElasticEaseIn(t, b, c, d, a, p) { + // added s = 0 + let s = 0; + if (t === 0) { + return b; + } + if ((t /= d) === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + if (!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = (p / (2 * Math.PI)) * Math.asin(c / a); + } + return (-(a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b); + }, + /** + * elastic ease out + * @function + * @memberof Konva.Easings + */ + ElasticEaseOut(t, b, c, d, a, p) { + // added s = 0 + let s = 0; + if (t === 0) { + return b; + } + if ((t /= d) === 1) { + return b + c; + } + if (!p) { + p = d * 0.3; + } + if (!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = (p / (2 * Math.PI)) * Math.asin(c / a); + } + return (a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) + + c + + b); + }, + /** + * elastic ease in out + * @function + * @memberof Konva.Easings + */ + ElasticEaseInOut(t, b, c, d, a, p) { + // added s = 0 + let s = 0; + if (t === 0) { + return b; + } + if ((t /= d / 2) === 2) { + return b + c; + } + if (!p) { + p = d * (0.3 * 1.5); + } + if (!a || a < Math.abs(c)) { + a = c; + s = p / 4; + } + else { + s = (p / (2 * Math.PI)) * Math.asin(c / a); + } + if (t < 1) { + return (-0.5 * + (a * + Math.pow(2, 10 * (t -= 1)) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + + b); + } + return (a * + Math.pow(2, -10 * (t -= 1)) * + Math.sin(((t * d - s) * (2 * Math.PI)) / p) * + 0.5 + + c + + b); + }, + /** + * bounce ease out + * @function + * @memberof Konva.Easings + */ + BounceEaseOut(t, b, c, d) { + if ((t /= d) < 1 / 2.75) { + return c * (7.5625 * t * t) + b; + } + else if (t < 2 / 2.75) { + return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b; + } + else if (t < 2.5 / 2.75) { + return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b; + } + else { + return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b; + } + }, + /** + * bounce ease in + * @function + * @memberof Konva.Easings + */ + BounceEaseIn(t, b, c, d) { + return c - Easings.BounceEaseOut(d - t, 0, c, d) + b; + }, + /** + * bounce ease in out + * @function + * @memberof Konva.Easings + */ + BounceEaseInOut(t, b, c, d) { + if (t < d / 2) { + return Easings.BounceEaseIn(t * 2, 0, c, d) * 0.5 + b; + } + else { + return Easings.BounceEaseOut(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; + } + }, + /** + * ease in + * @function + * @memberof Konva.Easings + */ + EaseIn(t, b, c, d) { + return c * (t /= d) * t + b; + }, + /** + * ease out + * @function + * @memberof Konva.Easings + */ + EaseOut(t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + /** + * ease in out + * @function + * @memberof Konva.Easings + */ + EaseInOut(t, b, c, d) { + if ((t /= d / 2) < 1) { + return (c / 2) * t * t + b; + } + return (-c / 2) * (--t * (t - 2) - 1) + b; + }, + /** + * strong ease in + * @function + * @memberof Konva.Easings + */ + StrongEaseIn(t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + }, + /** + * strong ease out + * @function + * @memberof Konva.Easings + */ + StrongEaseOut(t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + }, + /** + * strong ease in out + * @function + * @memberof Konva.Easings + */ + StrongEaseInOut(t, b, c, d) { + if ((t /= d / 2) < 1) { + return (c / 2) * t * t * t * t * t + b; + } + return (c / 2) * ((t -= 2) * t * t * t * t + 2) + b; + }, + /** + * linear + * @function + * @memberof Konva.Easings + */ + Linear(t, b, c, d) { + return (c * t) / d + b; + }, + }; + + // what is core parts of Konva? + const Konva$1 = Util._assign(Konva$2, { + Util, + Transform, + Node, + Container, + Stage, + stages, + Layer, + FastLayer, + Group, + DD, + Shape, + shapes, + Animation, + Tween, + Easings, + Context, + Canvas, + }); + + /** + * Arc constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.angle in degrees + * @param {Number} config.innerRadius + * @param {Number} config.outerRadius + * @param {Boolean} [config.clockwise] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // draw a Arc that's pointing downwards + * var arc = new Konva.Arc({ + * innerRadius: 40, + * outerRadius: 80, + * fill: 'red', + * stroke: 'black' + * strokeWidth: 5, + * angle: 60, + * rotationDeg: -120 + * }); + */ + class Arc extends Shape { + _sceneFunc(context) { + const angle = Konva$2.getAngle(this.angle()), clockwise = this.clockwise(); + context.beginPath(); + context.arc(0, 0, this.outerRadius(), 0, angle, clockwise); + context.arc(0, 0, this.innerRadius(), angle, 0, !clockwise); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.outerRadius() * 2; + } + getHeight() { + return this.outerRadius() * 2; + } + setWidth(width) { + this.outerRadius(width / 2); + } + setHeight(height) { + this.outerRadius(height / 2); + } + getSelfRect() { + const innerRadius = this.innerRadius(); + const outerRadius = this.outerRadius(); + const clockwise = this.clockwise(); + const angle = Konva$2.getAngle(clockwise ? 360 - this.angle() : this.angle()); + const boundLeftRatio = Math.cos(Math.min(angle, Math.PI)); + const boundRightRatio = 1; + const boundTopRatio = Math.sin(Math.min(Math.max(Math.PI, angle), (3 * Math.PI) / 2)); + const boundBottomRatio = Math.sin(Math.min(angle, Math.PI / 2)); + const boundLeft = boundLeftRatio * (boundLeftRatio > 0 ? innerRadius : outerRadius); + const boundRight = boundRightRatio * (outerRadius ); + const boundTop = boundTopRatio * (boundTopRatio > 0 ? innerRadius : outerRadius); + const boundBottom = boundBottomRatio * (boundBottomRatio > 0 ? outerRadius : innerRadius); + return { + x: boundLeft, + y: clockwise ? -1 * boundBottom : boundTop, + width: boundRight - boundLeft, + height: boundBottom - boundTop, + }; + } + } + Arc.prototype._centroid = true; + Arc.prototype.className = 'Arc'; + Arc.prototype._attrsAffectingSize = [ + 'innerRadius', + 'outerRadius', + 'angle', + 'clockwise', + ]; + _registerNode(Arc); + // add getters setters + Factory.addGetterSetter(Arc, 'innerRadius', 0, getNumberValidator()); + /** + * get/set innerRadius + * @name Konva.Arc#innerRadius + * @method + * @param {Number} innerRadius + * @returns {Number} + * @example + * // get inner radius + * var innerRadius = arc.innerRadius(); + * + * // set inner radius + * arc.innerRadius(20); + */ + Factory.addGetterSetter(Arc, 'outerRadius', 0, getNumberValidator()); + /** + * get/set outerRadius + * @name Konva.Arc#outerRadius + * @method + * @param {Number} outerRadius + * @returns {Number} + * @example + * // get outer radius + * var outerRadius = arc.outerRadius(); + * + * // set outer radius + * arc.outerRadius(20); + */ + Factory.addGetterSetter(Arc, 'angle', 0, getNumberValidator()); + /** + * get/set angle in degrees + * @name Konva.Arc#angle + * @method + * @param {Number} angle + * @returns {Number} + * @example + * // get angle + * var angle = arc.angle(); + * + * // set angle + * arc.angle(20); + */ + Factory.addGetterSetter(Arc, 'clockwise', false, getBooleanValidator()); + /** + * get/set clockwise flag + * @name Konva.Arc#clockwise + * @method + * @param {Boolean} clockwise + * @returns {Boolean} + * @example + * // get clockwise flag + * var clockwise = arc.clockwise(); + * + * // draw arc counter-clockwise + * arc.clockwise(false); + * + * // draw arc clockwise + * arc.clockwise(true); + */ + + function getControlPoints(x0, y0, x1, y1, x2, y2, t) { + const d01 = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)), d12 = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)), fa = (t * d01) / (d01 + d12), fb = (t * d12) / (d01 + d12), p1x = x1 - fa * (x2 - x0), p1y = y1 - fa * (y2 - y0), p2x = x1 + fb * (x2 - x0), p2y = y1 + fb * (y2 - y0); + return [p1x, p1y, p2x, p2y]; + } + function expandPoints(p, tension) { + const len = p.length, allPoints = []; + for (let n = 2; n < len - 2; n += 2) { + const cp = getControlPoints(p[n - 2], p[n - 1], p[n], p[n + 1], p[n + 2], p[n + 3], tension); + if (isNaN(cp[0])) { + continue; + } + allPoints.push(cp[0]); + allPoints.push(cp[1]); + allPoints.push(p[n]); + allPoints.push(p[n + 1]); + allPoints.push(cp[2]); + allPoints.push(cp[3]); + } + return allPoints; + } + /** + * Line constructor.  Lines are defined by an array of points and + * a tension + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3]. + * @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation. + * The default is 0 + * @param {Boolean} [config.closed] defines whether or not the line shape is closed, creating a polygon or blob + * @param {Boolean} [config.bezier] if no tension is provided but bezier=true, we draw the line as a bezier using the passed points + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var line = new Konva.Line({ + * x: 100, + * y: 50, + * points: [73, 70, 340, 23, 450, 60, 500, 20], + * stroke: 'red', + * tension: 1 + * }); + */ + class Line extends Shape { + constructor(config) { + super(config); + this.on('pointsChange.konva tensionChange.konva closedChange.konva bezierChange.konva', function () { + this._clearCache('tensionPoints'); + }); + } + _sceneFunc(context) { + let points = this.points(), length = points.length, tension = this.tension(), closed = this.closed(), bezier = this.bezier(), tp, len, n; + if (!length) { + return; + } + context.beginPath(); + context.moveTo(points[0], points[1]); + // tension + if (tension !== 0 && length > 4) { + tp = this.getTensionPoints(); + len = tp.length; + n = closed ? 0 : 4; + if (!closed) { + context.quadraticCurveTo(tp[0], tp[1], tp[2], tp[3]); + } + while (n < len - 2) { + context.bezierCurveTo(tp[n++], tp[n++], tp[n++], tp[n++], tp[n++], tp[n++]); + } + if (!closed) { + context.quadraticCurveTo(tp[len - 2], tp[len - 1], points[length - 2], points[length - 1]); + } + } + else if (bezier) { + // no tension but bezier + n = 2; + while (n < length) { + context.bezierCurveTo(points[n++], points[n++], points[n++], points[n++], points[n++], points[n++]); + } + } + else { + // no tension + for (n = 2; n < length; n += 2) { + context.lineTo(points[n], points[n + 1]); + } + } + // closed e.g. polygons and blobs + if (closed) { + context.closePath(); + context.fillStrokeShape(this); + } + else { + // open e.g. lines and splines + context.strokeShape(this); + } + } + getTensionPoints() { + return this._getCache('tensionPoints', this._getTensionPoints); + } + _getTensionPoints() { + if (this.closed()) { + return this._getTensionPointsClosed(); + } + else { + return expandPoints(this.points(), this.tension()); + } + } + _getTensionPointsClosed() { + const p = this.points(), len = p.length, tension = this.tension(), firstControlPoints = getControlPoints(p[len - 2], p[len - 1], p[0], p[1], p[2], p[3], tension), lastControlPoints = getControlPoints(p[len - 4], p[len - 3], p[len - 2], p[len - 1], p[0], p[1], tension), middle = expandPoints(p, tension), tp = [firstControlPoints[2], firstControlPoints[3]] + .concat(middle) + .concat([ + lastControlPoints[0], + lastControlPoints[1], + p[len - 2], + p[len - 1], + lastControlPoints[2], + lastControlPoints[3], + firstControlPoints[0], + firstControlPoints[1], + p[0], + p[1], + ]); + return tp; + } + getWidth() { + return this.getSelfRect().width; + } + getHeight() { + return this.getSelfRect().height; + } + // overload size detection + getSelfRect() { + let points = this.points(); + if (points.length < 4) { + return { + x: points[0] || 0, + y: points[1] || 0, + width: 0, + height: 0, + }; + } + if (this.tension() !== 0) { + points = [ + points[0], + points[1], + ...this._getTensionPoints(), + points[points.length - 2], + points[points.length - 1], + ]; + } + else { + points = this.points(); + } + let minX = points[0]; + let maxX = points[0]; + let minY = points[1]; + let maxY = points[1]; + let x, y; + for (let i = 0; i < points.length / 2; i++) { + x = points[i * 2]; + y = points[i * 2 + 1]; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + } + Line.prototype.className = 'Line'; + Line.prototype._attrsAffectingSize = ['points', 'bezier', 'tension']; + _registerNode(Line); + // add getters setters + Factory.addGetterSetter(Line, 'closed', false); + /** + * get/set closed flag. The default is false + * @name Konva.Line#closed + * @method + * @param {Boolean} closed + * @returns {Boolean} + * @example + * // get closed flag + * var closed = line.closed(); + * + * // close the shape + * line.closed(true); + * + * // open the shape + * line.closed(false); + */ + Factory.addGetterSetter(Line, 'bezier', false); + /** + * get/set bezier flag. The default is false + * @name Konva.Line#bezier + * @method + * @param {Boolean} bezier + * @returns {Boolean} + * @example + * // get whether the line is a bezier + * var isBezier = line.bezier(); + * + * // set whether the line is a bezier + * line.bezier(true); + */ + Factory.addGetterSetter(Line, 'tension', 0, getNumberValidator()); + /** + * get/set tension + * @name Konva.Line#tension + * @method + * @param {Number} tension Higher values will result in a more curvy line. A value of 0 will result in no interpolation. The default is 0 + * @returns {Number} + * @example + * // get tension + * var tension = line.tension(); + * + * // set tension + * line.tension(3); + */ + Factory.addGetterSetter(Line, 'points', [], getNumberArrayValidator()); + /** + * get/set points array. Points is a flat array [x1, y1, x2, y2]. It is flat for performance reasons. + * @name Konva.Line#points + * @method + * @param {Array} points + * @returns {Array} + * @example + * // get points + * var points = line.points(); + * + * // set points + * line.points([10, 20, 30, 40, 50, 60]); + * + * // push a new point + * line.points(line.points().concat([70, 80])); + */ + + // Credits: rveciana/svg-path-properties + // Legendre-Gauss abscissae (xi values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + const tValues = [ + [], + [], + [ + -0.5773502691896257, + 0.5773502691896257645091487805019574556476, + ], + [ + 0, -0.7745966692414834, + 0.7745966692414833770358530799564799221665, + ], + [ + -0.33998104358485626, + 0.3399810435848562648026657591032446872005, + -0.8611363115940526, + 0.8611363115940525752239464888928095050957, + ], + [ + 0, -0.5384693101056831, + 0.5384693101056830910363144207002088049672, + -0.906179845938664, + 0.9061798459386639927976268782993929651256, + ], + [ + 0.6612093864662645136613995950199053470064, + -0.6612093864662645, + -0.2386191860831969, + 0.2386191860831969086305017216807119354186, + -0.932469514203152, + 0.9324695142031520278123015544939946091347, + ], + [ + 0, 0.4058451513773971669066064120769614633473, + -0.4058451513773972, + -0.7415311855993945, + 0.7415311855993944398638647732807884070741, + -0.9491079123427585, + 0.9491079123427585245261896840478512624007, + ], + [ + -0.1834346424956498, + 0.1834346424956498049394761423601839806667, + -0.525532409916329, + 0.5255324099163289858177390491892463490419, + -0.7966664774136267, + 0.7966664774136267395915539364758304368371, + -0.9602898564975363, + 0.9602898564975362316835608685694729904282, + ], + [ + 0, -0.8360311073266358, + 0.8360311073266357942994297880697348765441, + -0.9681602395076261, + 0.9681602395076260898355762029036728700494, + -0.3242534234038089, + 0.3242534234038089290385380146433366085719, + -0.6133714327005904, + 0.6133714327005903973087020393414741847857, + ], + [ + -0.14887433898163122, + 0.1488743389816312108848260011297199846175, + -0.4333953941292472, + 0.4333953941292471907992659431657841622, + -0.6794095682990244, + 0.6794095682990244062343273651148735757692, + -0.8650633666889845, + 0.8650633666889845107320966884234930485275, + -0.9739065285171717, + 0.9739065285171717200779640120844520534282, + ], + [ + 0, -0.26954315595234496, + 0.2695431559523449723315319854008615246796, + -0.5190961292068118, + 0.5190961292068118159257256694586095544802, + -0.7301520055740494, + 0.7301520055740493240934162520311534580496, + -0.8870625997680953, + 0.8870625997680952990751577693039272666316, + -0.978228658146057, + 0.9782286581460569928039380011228573907714, + ], + [ + -0.1252334085114689, + 0.1252334085114689154724413694638531299833, + -0.3678314989981802, + 0.3678314989981801937526915366437175612563, + -0.5873179542866175, + 0.587317954286617447296702418940534280369, + -0.7699026741943047, + 0.7699026741943046870368938332128180759849, + -0.9041172563704749, + 0.9041172563704748566784658661190961925375, + -0.9815606342467192, + 0.9815606342467192506905490901492808229601, + ], + [ + 0, -0.2304583159551348, + 0.2304583159551347940655281210979888352115, + -0.44849275103644687, + 0.4484927510364468528779128521276398678019, + -0.6423493394403402, + 0.6423493394403402206439846069955156500716, + -0.8015780907333099, + 0.8015780907333099127942064895828598903056, + -0.9175983992229779, + 0.9175983992229779652065478365007195123904, + -0.9841830547185881, + 0.9841830547185881494728294488071096110649, + ], + [ + -0.10805494870734367, + 0.1080549487073436620662446502198347476119, + -0.31911236892788974, + 0.3191123689278897604356718241684754668342, + -0.5152486363581541, + 0.5152486363581540919652907185511886623088, + -0.6872929048116855, + 0.6872929048116854701480198030193341375384, + -0.827201315069765, + 0.8272013150697649931897947426503949610397, + -0.9284348836635735, + 0.928434883663573517336391139377874264477, + -0.9862838086968123, + 0.986283808696812338841597266704052801676, + ], + [ + 0, -0.20119409399743451, + 0.2011940939974345223006283033945962078128, + -0.3941513470775634, + 0.3941513470775633698972073709810454683627, + -0.5709721726085388, + 0.5709721726085388475372267372539106412383, + -0.7244177313601701, + 0.7244177313601700474161860546139380096308, + -0.8482065834104272, + 0.8482065834104272162006483207742168513662, + -0.937273392400706, + 0.9372733924007059043077589477102094712439, + -0.9879925180204854, + 0.9879925180204854284895657185866125811469, + ], + [ + -0.09501250983763744, + 0.0950125098376374401853193354249580631303, + -0.2816035507792589, + 0.281603550779258913230460501460496106486, + -0.45801677765722737, + 0.45801677765722738634241944298357757354, + -0.6178762444026438, + 0.6178762444026437484466717640487910189918, + -0.755404408355003, + 0.7554044083550030338951011948474422683538, + -0.8656312023878318, + 0.8656312023878317438804678977123931323873, + -0.9445750230732326, + 0.9445750230732325760779884155346083450911, + -0.9894009349916499, + 0.9894009349916499325961541734503326274262, + ], + [ + 0, -0.17848418149584785, + 0.1784841814958478558506774936540655574754, + -0.3512317634538763, + 0.3512317634538763152971855170953460050405, + -0.5126905370864769, + 0.5126905370864769678862465686295518745829, + -0.6576711592166907, + 0.6576711592166907658503022166430023351478, + -0.7815140038968014, + 0.7815140038968014069252300555204760502239, + -0.8802391537269859, + 0.8802391537269859021229556944881556926234, + -0.9506755217687678, + 0.9506755217687677612227169578958030214433, + -0.9905754753144174, + 0.9905754753144173356754340199406652765077, + ], + [ + -0.0847750130417353, + 0.0847750130417353012422618529357838117333, + -0.2518862256915055, + 0.2518862256915055095889728548779112301628, + -0.41175116146284263, + 0.4117511614628426460359317938330516370789, + -0.5597708310739475, + 0.5597708310739475346078715485253291369276, + -0.6916870430603532, + 0.6916870430603532078748910812888483894522, + -0.8037049589725231, + 0.8037049589725231156824174550145907971032, + -0.8926024664975557, + 0.8926024664975557392060605911271455154078, + -0.9558239495713977, + 0.9558239495713977551811958929297763099728, + -0.9915651684209309, + 0.9915651684209309467300160047061507702525, + ], + [ + 0, -0.16035864564022537, + 0.1603586456402253758680961157407435495048, + -0.31656409996362983, + 0.3165640999636298319901173288498449178922, + -0.46457074137596094, + 0.4645707413759609457172671481041023679762, + -0.600545304661681, + 0.6005453046616810234696381649462392798683, + -0.7209661773352294, + 0.7209661773352293786170958608237816296571, + -0.8227146565371428, + 0.8227146565371428249789224867127139017745, + -0.9031559036148179, + 0.9031559036148179016426609285323124878093, + -0.96020815213483, + 0.960208152134830030852778840687651526615, + -0.9924068438435844, + 0.9924068438435844031890176702532604935893, + ], + [ + -0.07652652113349734, + 0.0765265211334973337546404093988382110047, + -0.22778585114164507, + 0.227785851141645078080496195368574624743, + -0.37370608871541955, + 0.3737060887154195606725481770249272373957, + -0.5108670019508271, + 0.5108670019508270980043640509552509984254, + -0.636053680726515, + 0.6360536807265150254528366962262859367433, + -0.7463319064601508, + 0.7463319064601507926143050703556415903107, + -0.8391169718222188, + 0.8391169718222188233945290617015206853296, + -0.912234428251326, + 0.9122344282513259058677524412032981130491, + -0.9639719272779138, + 0.963971927277913791267666131197277221912, + -0.9931285991850949, + 0.9931285991850949247861223884713202782226, + ], + [ + 0, -0.1455618541608951, + 0.1455618541608950909370309823386863301163, + -0.2880213168024011, + 0.288021316802401096600792516064600319909, + -0.4243421202074388, + 0.4243421202074387835736688885437880520964, + -0.5516188358872198, + 0.551618835887219807059018796724313286622, + -0.6671388041974123, + 0.667138804197412319305966669990339162597, + -0.7684399634756779, + 0.7684399634756779086158778513062280348209, + -0.8533633645833173, + 0.8533633645833172836472506385875676702761, + -0.9200993341504008, + 0.9200993341504008287901871337149688941591, + -0.9672268385663063, + 0.9672268385663062943166222149076951614246, + -0.9937521706203895, + 0.9937521706203895002602420359379409291933, + ], + [ + -0.06973927331972223, + 0.0697392733197222212138417961186280818222, + -0.20786042668822127, + 0.2078604266882212854788465339195457342156, + -0.34193582089208424, + 0.3419358208920842251581474204273796195591, + -0.469355837986757, + 0.4693558379867570264063307109664063460953, + -0.5876404035069116, + 0.5876404035069115929588769276386473488776, + -0.6944872631866827, + 0.6944872631866827800506898357622567712673, + -0.7878168059792081, + 0.7878168059792081620042779554083515213881, + -0.8658125777203002, + 0.8658125777203001365364256370193787290847, + -0.926956772187174, + 0.9269567721871740005206929392590531966353, + -0.9700604978354287, + 0.9700604978354287271239509867652687108059, + -0.9942945854823992, + 0.994294585482399292073031421161298980393, + ], + [ + 0, -0.1332568242984661, + 0.1332568242984661109317426822417661370104, + -0.26413568097034495, + 0.264135680970344930533869538283309602979, + -0.3903010380302908, + 0.390301038030290831421488872880605458578, + -0.5095014778460075, + 0.5095014778460075496897930478668464305448, + -0.6196098757636461, + 0.6196098757636461563850973116495956533871, + -0.7186613631319502, + 0.7186613631319501944616244837486188483299, + -0.8048884016188399, + 0.8048884016188398921511184069967785579414, + -0.8767523582704416, + 0.8767523582704416673781568859341456716389, + -0.9329710868260161, + 0.9329710868260161023491969890384229782357, + -0.9725424712181152, + 0.9725424712181152319560240768207773751816, + -0.9947693349975522, + 0.9947693349975521235239257154455743605736, + ], + [ + -0.06405689286260563, + 0.0640568928626056260850430826247450385909, + -0.1911188674736163, + 0.1911188674736163091586398207570696318404, + -0.3150426796961634, + 0.3150426796961633743867932913198102407864, + -0.4337935076260451, + 0.4337935076260451384870842319133497124524, + -0.5454214713888396, + 0.5454214713888395356583756172183723700107, + -0.6480936519369755, + 0.6480936519369755692524957869107476266696, + -0.7401241915785544, + 0.7401241915785543642438281030999784255232, + -0.820001985973903, + 0.8200019859739029219539498726697452080761, + -0.8864155270044011, + 0.8864155270044010342131543419821967550873, + -0.9382745520027328, + 0.9382745520027327585236490017087214496548, + -0.9747285559713095, + 0.9747285559713094981983919930081690617411, + -0.9951872199970213, + 0.9951872199970213601799974097007368118745, + ], + ]; + // Legendre-Gauss weights (wi values, defined by a function linked to in the Bezier primer article) + const cValues = [ + [], + [], + [1.0, 1.0], + [ + 0.8888888888888888888888888888888888888888, + 0.5555555555555555555555555555555555555555, + 0.5555555555555555555555555555555555555555, + ], + [ + 0.6521451548625461426269360507780005927646, + 0.6521451548625461426269360507780005927646, + 0.3478548451374538573730639492219994072353, + 0.3478548451374538573730639492219994072353, + ], + [ + 0.5688888888888888888888888888888888888888, + 0.4786286704993664680412915148356381929122, + 0.4786286704993664680412915148356381929122, + 0.2369268850561890875142640407199173626432, + 0.2369268850561890875142640407199173626432, + ], + [ + 0.3607615730481386075698335138377161116615, + 0.3607615730481386075698335138377161116615, + 0.4679139345726910473898703439895509948116, + 0.4679139345726910473898703439895509948116, + 0.1713244923791703450402961421727328935268, + 0.1713244923791703450402961421727328935268, + ], + [ + 0.4179591836734693877551020408163265306122, + 0.3818300505051189449503697754889751338783, + 0.3818300505051189449503697754889751338783, + 0.2797053914892766679014677714237795824869, + 0.2797053914892766679014677714237795824869, + 0.1294849661688696932706114326790820183285, + 0.1294849661688696932706114326790820183285, + ], + [ + 0.3626837833783619829651504492771956121941, + 0.3626837833783619829651504492771956121941, + 0.3137066458778872873379622019866013132603, + 0.3137066458778872873379622019866013132603, + 0.2223810344533744705443559944262408844301, + 0.2223810344533744705443559944262408844301, + 0.1012285362903762591525313543099621901153, + 0.1012285362903762591525313543099621901153, + ], + [ + 0.3302393550012597631645250692869740488788, + 0.1806481606948574040584720312429128095143, + 0.1806481606948574040584720312429128095143, + 0.0812743883615744119718921581105236506756, + 0.0812743883615744119718921581105236506756, + 0.3123470770400028400686304065844436655987, + 0.3123470770400028400686304065844436655987, + 0.2606106964029354623187428694186328497718, + 0.2606106964029354623187428694186328497718, + ], + [ + 0.295524224714752870173892994651338329421, + 0.295524224714752870173892994651338329421, + 0.2692667193099963550912269215694693528597, + 0.2692667193099963550912269215694693528597, + 0.2190863625159820439955349342281631924587, + 0.2190863625159820439955349342281631924587, + 0.1494513491505805931457763396576973324025, + 0.1494513491505805931457763396576973324025, + 0.0666713443086881375935688098933317928578, + 0.0666713443086881375935688098933317928578, + ], + [ + 0.272925086777900630714483528336342189156, + 0.2628045445102466621806888698905091953727, + 0.2628045445102466621806888698905091953727, + 0.2331937645919904799185237048431751394317, + 0.2331937645919904799185237048431751394317, + 0.1862902109277342514260976414316558916912, + 0.1862902109277342514260976414316558916912, + 0.1255803694649046246346942992239401001976, + 0.1255803694649046246346942992239401001976, + 0.0556685671161736664827537204425485787285, + 0.0556685671161736664827537204425485787285, + ], + [ + 0.2491470458134027850005624360429512108304, + 0.2491470458134027850005624360429512108304, + 0.2334925365383548087608498989248780562594, + 0.2334925365383548087608498989248780562594, + 0.2031674267230659217490644558097983765065, + 0.2031674267230659217490644558097983765065, + 0.160078328543346226334652529543359071872, + 0.160078328543346226334652529543359071872, + 0.1069393259953184309602547181939962242145, + 0.1069393259953184309602547181939962242145, + 0.047175336386511827194615961485017060317, + 0.047175336386511827194615961485017060317, + ], + [ + 0.2325515532308739101945895152688359481566, + 0.2262831802628972384120901860397766184347, + 0.2262831802628972384120901860397766184347, + 0.2078160475368885023125232193060527633865, + 0.2078160475368885023125232193060527633865, + 0.1781459807619457382800466919960979955128, + 0.1781459807619457382800466919960979955128, + 0.1388735102197872384636017768688714676218, + 0.1388735102197872384636017768688714676218, + 0.0921214998377284479144217759537971209236, + 0.0921214998377284479144217759537971209236, + 0.0404840047653158795200215922009860600419, + 0.0404840047653158795200215922009860600419, + ], + [ + 0.2152638534631577901958764433162600352749, + 0.2152638534631577901958764433162600352749, + 0.2051984637212956039659240656612180557103, + 0.2051984637212956039659240656612180557103, + 0.1855383974779378137417165901251570362489, + 0.1855383974779378137417165901251570362489, + 0.1572031671581935345696019386238421566056, + 0.1572031671581935345696019386238421566056, + 0.1215185706879031846894148090724766259566, + 0.1215185706879031846894148090724766259566, + 0.0801580871597602098056332770628543095836, + 0.0801580871597602098056332770628543095836, + 0.0351194603317518630318328761381917806197, + 0.0351194603317518630318328761381917806197, + ], + [ + 0.2025782419255612728806201999675193148386, + 0.1984314853271115764561183264438393248186, + 0.1984314853271115764561183264438393248186, + 0.1861610000155622110268005618664228245062, + 0.1861610000155622110268005618664228245062, + 0.1662692058169939335532008604812088111309, + 0.1662692058169939335532008604812088111309, + 0.1395706779261543144478047945110283225208, + 0.1395706779261543144478047945110283225208, + 0.1071592204671719350118695466858693034155, + 0.1071592204671719350118695466858693034155, + 0.0703660474881081247092674164506673384667, + 0.0703660474881081247092674164506673384667, + 0.0307532419961172683546283935772044177217, + 0.0307532419961172683546283935772044177217, + ], + [ + 0.1894506104550684962853967232082831051469, + 0.1894506104550684962853967232082831051469, + 0.1826034150449235888667636679692199393835, + 0.1826034150449235888667636679692199393835, + 0.1691565193950025381893120790303599622116, + 0.1691565193950025381893120790303599622116, + 0.1495959888165767320815017305474785489704, + 0.1495959888165767320815017305474785489704, + 0.1246289712555338720524762821920164201448, + 0.1246289712555338720524762821920164201448, + 0.0951585116824927848099251076022462263552, + 0.0951585116824927848099251076022462263552, + 0.0622535239386478928628438369943776942749, + 0.0622535239386478928628438369943776942749, + 0.0271524594117540948517805724560181035122, + 0.0271524594117540948517805724560181035122, + ], + [ + 0.1794464703562065254582656442618856214487, + 0.1765627053669926463252709901131972391509, + 0.1765627053669926463252709901131972391509, + 0.1680041021564500445099706637883231550211, + 0.1680041021564500445099706637883231550211, + 0.1540457610768102880814315948019586119404, + 0.1540457610768102880814315948019586119404, + 0.1351363684685254732863199817023501973721, + 0.1351363684685254732863199817023501973721, + 0.1118838471934039710947883856263559267358, + 0.1118838471934039710947883856263559267358, + 0.0850361483171791808835353701910620738504, + 0.0850361483171791808835353701910620738504, + 0.0554595293739872011294401653582446605128, + 0.0554595293739872011294401653582446605128, + 0.0241483028685479319601100262875653246916, + 0.0241483028685479319601100262875653246916, + ], + [ + 0.1691423829631435918406564701349866103341, + 0.1691423829631435918406564701349866103341, + 0.1642764837458327229860537764659275904123, + 0.1642764837458327229860537764659275904123, + 0.1546846751262652449254180038363747721932, + 0.1546846751262652449254180038363747721932, + 0.1406429146706506512047313037519472280955, + 0.1406429146706506512047313037519472280955, + 0.1225552067114784601845191268002015552281, + 0.1225552067114784601845191268002015552281, + 0.1009420441062871655628139849248346070628, + 0.1009420441062871655628139849248346070628, + 0.0764257302548890565291296776166365256053, + 0.0764257302548890565291296776166365256053, + 0.0497145488949697964533349462026386416808, + 0.0497145488949697964533349462026386416808, + 0.0216160135264833103133427102664524693876, + 0.0216160135264833103133427102664524693876, + ], + [ + 0.1610544498487836959791636253209167350399, + 0.1589688433939543476499564394650472016787, + 0.1589688433939543476499564394650472016787, + 0.152766042065859666778855400897662998461, + 0.152766042065859666778855400897662998461, + 0.1426067021736066117757461094419029724756, + 0.1426067021736066117757461094419029724756, + 0.1287539625393362276755157848568771170558, + 0.1287539625393362276755157848568771170558, + 0.1115666455473339947160239016817659974813, + 0.1115666455473339947160239016817659974813, + 0.0914900216224499994644620941238396526609, + 0.0914900216224499994644620941238396526609, + 0.0690445427376412265807082580060130449618, + 0.0690445427376412265807082580060130449618, + 0.0448142267656996003328381574019942119517, + 0.0448142267656996003328381574019942119517, + 0.0194617882297264770363120414644384357529, + 0.0194617882297264770363120414644384357529, + ], + [ + 0.1527533871307258506980843319550975934919, + 0.1527533871307258506980843319550975934919, + 0.1491729864726037467878287370019694366926, + 0.1491729864726037467878287370019694366926, + 0.1420961093183820513292983250671649330345, + 0.1420961093183820513292983250671649330345, + 0.1316886384491766268984944997481631349161, + 0.1316886384491766268984944997481631349161, + 0.118194531961518417312377377711382287005, + 0.118194531961518417312377377711382287005, + 0.1019301198172404350367501354803498761666, + 0.1019301198172404350367501354803498761666, + 0.0832767415767047487247581432220462061001, + 0.0832767415767047487247581432220462061001, + 0.0626720483341090635695065351870416063516, + 0.0626720483341090635695065351870416063516, + 0.040601429800386941331039952274932109879, + 0.040601429800386941331039952274932109879, + 0.0176140071391521183118619623518528163621, + 0.0176140071391521183118619623518528163621, + ], + [ + 0.1460811336496904271919851476833711882448, + 0.1445244039899700590638271665537525436099, + 0.1445244039899700590638271665537525436099, + 0.1398873947910731547221334238675831108927, + 0.1398873947910731547221334238675831108927, + 0.132268938633337461781052574496775604329, + 0.132268938633337461781052574496775604329, + 0.1218314160537285341953671771257335983563, + 0.1218314160537285341953671771257335983563, + 0.1087972991671483776634745780701056420336, + 0.1087972991671483776634745780701056420336, + 0.0934444234560338615532897411139320884835, + 0.0934444234560338615532897411139320884835, + 0.0761001136283793020170516533001831792261, + 0.0761001136283793020170516533001831792261, + 0.0571344254268572082836358264724479574912, + 0.0571344254268572082836358264724479574912, + 0.0369537897708524937999506682993296661889, + 0.0369537897708524937999506682993296661889, + 0.0160172282577743333242246168584710152658, + 0.0160172282577743333242246168584710152658, + ], + [ + 0.1392518728556319933754102483418099578739, + 0.1392518728556319933754102483418099578739, + 0.1365414983460151713525738312315173965863, + 0.1365414983460151713525738312315173965863, + 0.1311735047870623707329649925303074458757, + 0.1311735047870623707329649925303074458757, + 0.1232523768105124242855609861548144719594, + 0.1232523768105124242855609861548144719594, + 0.1129322960805392183934006074217843191142, + 0.1129322960805392183934006074217843191142, + 0.1004141444428809649320788378305362823508, + 0.1004141444428809649320788378305362823508, + 0.0859416062170677274144436813727028661891, + 0.0859416062170677274144436813727028661891, + 0.0697964684245204880949614189302176573987, + 0.0697964684245204880949614189302176573987, + 0.0522933351526832859403120512732112561121, + 0.0522933351526832859403120512732112561121, + 0.0337749015848141547933022468659129013491, + 0.0337749015848141547933022468659129013491, + 0.0146279952982722006849910980471854451902, + 0.0146279952982722006849910980471854451902, + ], + [ + 0.1336545721861061753514571105458443385831, + 0.132462039404696617371642464703316925805, + 0.132462039404696617371642464703316925805, + 0.1289057221880821499785953393997936532597, + 0.1289057221880821499785953393997936532597, + 0.1230490843067295304675784006720096548158, + 0.1230490843067295304675784006720096548158, + 0.1149966402224113649416435129339613014914, + 0.1149966402224113649416435129339613014914, + 0.1048920914645414100740861850147438548584, + 0.1048920914645414100740861850147438548584, + 0.0929157660600351474770186173697646486034, + 0.0929157660600351474770186173697646486034, + 0.0792814117767189549228925247420432269137, + 0.0792814117767189549228925247420432269137, + 0.0642324214085258521271696151589109980391, + 0.0642324214085258521271696151589109980391, + 0.0480376717310846685716410716320339965612, + 0.0480376717310846685716410716320339965612, + 0.0309880058569794443106942196418845053837, + 0.0309880058569794443106942196418845053837, + 0.0134118594871417720813094934586150649766, + 0.0134118594871417720813094934586150649766, + ], + [ + 0.1279381953467521569740561652246953718517, + 0.1279381953467521569740561652246953718517, + 0.1258374563468282961213753825111836887264, + 0.1258374563468282961213753825111836887264, + 0.121670472927803391204463153476262425607, + 0.121670472927803391204463153476262425607, + 0.1155056680537256013533444839067835598622, + 0.1155056680537256013533444839067835598622, + 0.1074442701159656347825773424466062227946, + 0.1074442701159656347825773424466062227946, + 0.0976186521041138882698806644642471544279, + 0.0976186521041138882698806644642471544279, + 0.086190161531953275917185202983742667185, + 0.086190161531953275917185202983742667185, + 0.0733464814110803057340336152531165181193, + 0.0733464814110803057340336152531165181193, + 0.0592985849154367807463677585001085845412, + 0.0592985849154367807463677585001085845412, + 0.0442774388174198061686027482113382288593, + 0.0442774388174198061686027482113382288593, + 0.0285313886289336631813078159518782864491, + 0.0285313886289336631813078159518782864491, + 0.0123412297999871995468056670700372915759, + 0.0123412297999871995468056670700372915759, + ], + ]; + // LUT for binomial coefficient arrays per curve order 'n' + const binomialCoefficients = [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]]; + const getCubicArcLength = (xs, ys, t) => { + let sum; + let correctedT; + /*if (xs.length >= tValues.length) { + throw new Error('too high n bezier'); + }*/ + const n = 20; + const z = t / 2; + sum = 0; + for (let i = 0; i < n; i++) { + correctedT = z * tValues[n][i] + z; + sum += cValues[n][i] * BFunc(xs, ys, correctedT); + } + return z * sum; + }; + const getQuadraticArcLength = (xs, ys, t) => { + if (t === undefined) { + t = 1; + } + const ax = xs[0] - 2 * xs[1] + xs[2]; + const ay = ys[0] - 2 * ys[1] + ys[2]; + const bx = 2 * xs[1] - 2 * xs[0]; + const by = 2 * ys[1] - 2 * ys[0]; + const A = 4 * (ax * ax + ay * ay); + const B = 4 * (ax * bx + ay * by); + const C = bx * bx + by * by; + if (A === 0) { + return (t * Math.sqrt(Math.pow(xs[2] - xs[0], 2) + Math.pow(ys[2] - ys[0], 2))); + } + const b = B / (2 * A); + const c = C / A; + const u = t + b; + const k = c - b * b; + const uuk = u * u + k > 0 ? Math.sqrt(u * u + k) : 0; + const bbk = b * b + k > 0 ? Math.sqrt(b * b + k) : 0; + const term = b + Math.sqrt(b * b + k) !== 0 + ? k * Math.log(Math.abs((u + uuk) / (b + bbk))) + : 0; + return (Math.sqrt(A) / 2) * (u * uuk - b * bbk + term); + }; + function BFunc(xs, ys, t) { + const xbase = getDerivative(1, t, xs); + const ybase = getDerivative(1, t, ys); + const combined = xbase * xbase + ybase * ybase; + return Math.sqrt(combined); + } + /** + * Compute the curve derivative (hodograph) at t. + */ + const getDerivative = (derivative, t, vs) => { + // the derivative of any 't'-less function is zero. + const n = vs.length - 1; + let _vs; + let value; + if (n === 0) { + return 0; + } + // direct values? compute! + if (derivative === 0) { + value = 0; + for (let k = 0; k <= n; k++) { + value += + binomialCoefficients[n][k] * + Math.pow(1 - t, n - k) * + Math.pow(t, k) * + vs[k]; + } + return value; + } + else { + // Still some derivative? go down one order, then try + // for the lower order curve's. + _vs = new Array(n); + for (let k = 0; k < n; k++) { + _vs[k] = n * (vs[k + 1] - vs[k]); + } + return getDerivative(derivative - 1, t, _vs); + } + }; + const t2length = (length, totalLength, func) => { + let error = 1; + let t = length / totalLength; + let step = (length - func(t)) / totalLength; + let numIterations = 0; + while (error > 0.001) { + const increasedTLength = func(t + step); + const increasedTError = Math.abs(length - increasedTLength) / totalLength; + if (increasedTError < error) { + error = increasedTError; + t += step; + } + else { + const decreasedTLength = func(t - step); + const decreasedTError = Math.abs(length - decreasedTLength) / totalLength; + if (decreasedTError < error) { + error = decreasedTError; + t -= step; + } + else { + step /= 2; + } + } + numIterations++; + if (numIterations > 500) { + break; + } + } + return t; + }; + + /** + * Path constructor. + * @author Jason Follas + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} config.data SVG data string + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var path = new Konva.Path({ + * x: 240, + * y: 40, + * data: 'M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z', + * fill: 'green', + * scaleX: 2, + * scaleY: 2 + * }); + */ + class Path extends Shape { + constructor(config) { + super(config); + this.dataArray = []; + this.pathLength = 0; + this._readDataAttribute(); + this.on('dataChange.konva', function () { + this._readDataAttribute(); + }); + } + _readDataAttribute() { + this.dataArray = Path.parsePathData(this.data()); + this.pathLength = Path.getPathLength(this.dataArray); + } + _sceneFunc(context) { + const ca = this.dataArray; + // context position + context.beginPath(); + let isClosed = false; + for (let n = 0; n < ca.length; n++) { + const c = ca[n].command; + const p = ca[n].points; + switch (c) { + case 'L': + context.lineTo(p[0], p[1]); + break; + case 'M': + context.moveTo(p[0], p[1]); + break; + case 'C': + context.bezierCurveTo(p[0], p[1], p[2], p[3], p[4], p[5]); + break; + case 'Q': + context.quadraticCurveTo(p[0], p[1], p[2], p[3]); + break; + case 'A': + var cx = p[0], cy = p[1], rx = p[2], ry = p[3], theta = p[4], dTheta = p[5], psi = p[6], fs = p[7]; + var r = rx > ry ? rx : ry; + var scaleX = rx > ry ? 1 : rx / ry; + var scaleY = rx > ry ? ry / rx : 1; + context.translate(cx, cy); + context.rotate(psi); + context.scale(scaleX, scaleY); + context.arc(0, 0, r, theta, theta + dTheta, 1 - fs); + context.scale(1 / scaleX, 1 / scaleY); + context.rotate(-psi); + context.translate(-cx, -cy); + break; + case 'z': + isClosed = true; + context.closePath(); + break; + } + } + if (!isClosed && !this.hasFill()) { + context.strokeShape(this); + } + else { + context.fillStrokeShape(this); + } + } + getSelfRect() { + let points = []; + this.dataArray.forEach(function (data) { + if (data.command === 'A') { + // Approximates by breaking curve into line segments + const start = data.points[4]; + // 4 = theta + const dTheta = data.points[5]; + // 5 = dTheta + const end = data.points[4] + dTheta; + let inc = Math.PI / 180.0; + // 1 degree resolution + if (Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + if (dTheta < 0) { + // clockwise + for (let t = start - inc; t > end; t -= inc) { + const point = Path.getPointOnEllipticalArc(data.points[0], data.points[1], data.points[2], data.points[3], t, 0); + points.push(point.x, point.y); + } + } + else { + // counter-clockwise + for (let t = start + inc; t < end; t += inc) { + const point = Path.getPointOnEllipticalArc(data.points[0], data.points[1], data.points[2], data.points[3], t, 0); + points.push(point.x, point.y); + } + } + } + else if (data.command === 'C') { + // Approximates by breaking curve into 100 line segments + for (let t = 0.0; t <= 1; t += 0.01) { + const point = Path.getPointOnCubicBezier(t, data.start.x, data.start.y, data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]); + points.push(point.x, point.y); + } + } + else { + // TODO: how can we calculate bezier curves better? + points = points.concat(data.points); + } + }); + let minX = points[0]; + let maxX = points[0]; + let minY = points[1]; + let maxY = points[1]; + let x, y; + for (let i = 0; i < points.length / 2; i++) { + x = points[i * 2]; + y = points[i * 2 + 1]; + // skip bad values + if (!isNaN(x)) { + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + } + if (!isNaN(y)) { + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + /** + * Return length of the path. + * @method + * @name Konva.Path#getLength + * @returns {Number} length + * @example + * var length = path.getLength(); + */ + getLength() { + return this.pathLength; + } + /** + * Get point on path at specific length of the path + * @method + * @name Konva.Path#getPointAtLength + * @param {Number} length length + * @returns {Object} point {x,y} point + * @example + * var point = path.getPointAtLength(10); + */ + getPointAtLength(length) { + return Path.getPointAtLengthOfDataArray(length, this.dataArray); + } + static getLineLength(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); + } + static getPathLength(dataArray) { + let pathLength = 0; + for (let i = 0; i < dataArray.length; ++i) { + pathLength += dataArray[i].pathLength; + } + return pathLength; + } + static getPointAtLengthOfDataArray(length, dataArray) { + let points, i = 0, ii = dataArray.length; + if (!ii) { + return null; + } + while (i < ii && length > dataArray[i].pathLength) { + length -= dataArray[i].pathLength; + ++i; + } + if (i === ii) { + points = dataArray[i - 1].points.slice(-2); + return { + x: points[0], + y: points[1], + }; + } + if (length < 0.01) { + points = dataArray[i].points.slice(0, 2); + return { + x: points[0], + y: points[1], + }; + } + const cp = dataArray[i]; + const p = cp.points; + switch (cp.command) { + case 'L': + return Path.getPointOnLine(length, cp.start.x, cp.start.y, p[0], p[1]); + case 'C': + return Path.getPointOnCubicBezier(t2length(length, Path.getPathLength(dataArray), (i) => { + return getCubicArcLength([cp.start.x, p[0], p[2], p[4]], [cp.start.y, p[1], p[3], p[5]], i); + }), cp.start.x, cp.start.y, p[0], p[1], p[2], p[3], p[4], p[5]); + case 'Q': + return Path.getPointOnQuadraticBezier(t2length(length, Path.getPathLength(dataArray), (i) => { + return getQuadraticArcLength([cp.start.x, p[0], p[2]], [cp.start.y, p[1], p[3]], i); + }), cp.start.x, cp.start.y, p[0], p[1], p[2], p[3]); + case 'A': + var cx = p[0], cy = p[1], rx = p[2], ry = p[3], theta = p[4], dTheta = p[5], psi = p[6]; + theta += (dTheta * length) / cp.pathLength; + return Path.getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi); + } + return null; + } + static getPointOnLine(dist, P1x, P1y, P2x, P2y, fromX, fromY) { + fromX = fromX !== null && fromX !== undefined ? fromX : P1x; + fromY = fromY !== null && fromY !== undefined ? fromY : P1y; + const len = this.getLineLength(P1x, P1y, P2x, P2y); + if (len < 1e-10) { + return { x: P1x, y: P1y }; + } + if (P2x === P1x) { + // Vertical line + return { x: fromX, y: fromY + (P2y > P1y ? dist : -dist) }; + } + const m = (P2y - P1y) / (P2x - P1x); + const run = Math.sqrt((dist * dist) / (1 + m * m)) * (P2x < P1x ? -1 : 1); + const rise = m * run; + if (Math.abs(fromY - P1y - m * (fromX - P1x)) < 1e-10) { + return { x: fromX + run, y: fromY + rise }; + } + const u = ((fromX - P1x) * (P2x - P1x) + (fromY - P1y) * (P2y - P1y)) / (len * len); + const ix = P1x + u * (P2x - P1x); + const iy = P1y + u * (P2y - P1y); + const pRise = this.getLineLength(fromX, fromY, ix, iy); + const pRun = Math.sqrt(dist * dist - pRise * pRise); + const adjustedRun = Math.sqrt((pRun * pRun) / (1 + m * m)) * (P2x < P1x ? -1 : 1); + const adjustedRise = m * adjustedRun; + return { x: ix + adjustedRun, y: iy + adjustedRise }; + } + static getPointOnCubicBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y, P4x, P4y) { + function CB1(t) { + return t * t * t; + } + function CB2(t) { + return 3 * t * t * (1 - t); + } + function CB3(t) { + return 3 * t * (1 - t) * (1 - t); + } + function CB4(t) { + return (1 - t) * (1 - t) * (1 - t); + } + const x = P4x * CB1(pct) + P3x * CB2(pct) + P2x * CB3(pct) + P1x * CB4(pct); + const y = P4y * CB1(pct) + P3y * CB2(pct) + P2y * CB3(pct) + P1y * CB4(pct); + return { + x: x, + y: y, + }; + } + static getPointOnQuadraticBezier(pct, P1x, P1y, P2x, P2y, P3x, P3y) { + function QB1(t) { + return t * t; + } + function QB2(t) { + return 2 * t * (1 - t); + } + function QB3(t) { + return (1 - t) * (1 - t); + } + const x = P3x * QB1(pct) + P2x * QB2(pct) + P1x * QB3(pct); + const y = P3y * QB1(pct) + P2y * QB2(pct) + P1y * QB3(pct); + return { + x: x, + y: y, + }; + } + static getPointOnEllipticalArc(cx, cy, rx, ry, theta, psi) { + const cosPsi = Math.cos(psi), sinPsi = Math.sin(psi); + const pt = { + x: rx * Math.cos(theta), + y: ry * Math.sin(theta), + }; + return { + x: cx + (pt.x * cosPsi - pt.y * sinPsi), + y: cy + (pt.x * sinPsi + pt.y * cosPsi), + }; + } + /* + * get parsed data array from the data + * string. V, v, H, h, and l data are converted to + * L data for the purpose of high performance Path + * rendering + */ + static parsePathData(data) { + // Path Data Segment must begin with a moveTo + //m (x y)+ Relative moveTo (subsequent points are treated as lineTo) + //M (x y)+ Absolute moveTo (subsequent points are treated as lineTo) + //l (x y)+ Relative lineTo + //L (x y)+ Absolute LineTo + //h (x)+ Relative horizontal lineTo + //H (x)+ Absolute horizontal lineTo + //v (y)+ Relative vertical lineTo + //V (y)+ Absolute vertical lineTo + //z (closepath) + //Z (closepath) + //c (x1 y1 x2 y2 x y)+ Relative Bezier curve + //C (x1 y1 x2 y2 x y)+ Absolute Bezier curve + //q (x1 y1 x y)+ Relative Quadratic Bezier + //Q (x1 y1 x y)+ Absolute Quadratic Bezier + //t (x y)+ Shorthand/Smooth Relative Quadratic Bezier + //T (x y)+ Shorthand/Smooth Absolute Quadratic Bezier + //s (x2 y2 x y)+ Shorthand/Smooth Relative Bezier curve + //S (x2 y2 x y)+ Shorthand/Smooth Absolute Bezier curve + //a (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Relative Elliptical Arc + //A (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+ Absolute Elliptical Arc + // return early if data is not defined + if (!data) { + return []; + } + // command string + let cs = data; + // command chars + const cc = [ + 'm', + 'M', + 'l', + 'L', + 'v', + 'V', + 'h', + 'H', + 'z', + 'Z', + 'c', + 'C', + 'q', + 'Q', + 't', + 'T', + 's', + 'S', + 'a', + 'A', + ]; + // convert white spaces to commas + cs = cs.replace(new RegExp(' ', 'g'), ','); + // create pipes so that we can split the data + for (var n = 0; n < cc.length; n++) { + cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]); + } + // create array + const arr = cs.split('|'); + const ca = []; + const coords = []; + // init context point + let cpx = 0; + let cpy = 0; + const re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/gi; + let match; + for (n = 1; n < arr.length; n++) { + let str = arr[n]; + let c = str.charAt(0); + str = str.slice(1); + coords.length = 0; + while ((match = re.exec(str))) { + coords.push(match[0]); + } + // while ((match = re.exec(str))) { + // coords.push(match[0]); + // } + const p = []; + for (let j = 0, jlen = coords.length; j < jlen; j++) { + // extra case for merged flags + if (coords[j] === '00') { + p.push(0, 0); + continue; + } + const parsed = parseFloat(coords[j]); + if (!isNaN(parsed)) { + p.push(parsed); + } + else { + p.push(0); + } + } + while (p.length > 0) { + if (isNaN(p[0])) { + // case for a trailing comma before next command + break; + } + let cmd = ''; + let points = []; + const startX = cpx, startY = cpy; + // Move var from within the switch to up here (jshint) + var prevCmd, ctlPtx, ctlPty; // Ss, Tt + var rx, ry, psi, fa, fs, x1, y1; // Aa + // convert l, H, h, V, and v to L + switch (c) { + // Note: Keep the lineTo's above the moveTo's in this switch + case 'l': + cpx += p.shift(); + cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'L': + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + // Note: lineTo handlers need to be above this point + case 'm': + var dx = p.shift(); + var dy = p.shift(); + cpx += dx; + cpy += dy; + cmd = 'M'; + // After closing the path move the current position + // to the the first point of the path (if any). + if (ca.length > 2 && ca[ca.length - 1].command === 'z') { + for (let idx = ca.length - 2; idx >= 0; idx--) { + if (ca[idx].command === 'M') { + cpx = ca[idx].points[0] + dx; + cpy = ca[idx].points[1] + dy; + break; + } + } + } + points.push(cpx, cpy); + c = 'l'; + // subsequent points are treated as relative lineTo + break; + case 'M': + cpx = p.shift(); + cpy = p.shift(); + cmd = 'M'; + points.push(cpx, cpy); + c = 'L'; + // subsequent points are treated as absolute lineTo + break; + case 'h': + cpx += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'H': + cpx = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'v': + cpy += p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'V': + cpy = p.shift(); + cmd = 'L'; + points.push(cpx, cpy); + break; + case 'C': + points.push(p.shift(), p.shift(), p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + case 'c': + points.push(cpx + p.shift(), cpy + p.shift(), cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 'S': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'C') { + ctlPtx = cpx + (cpx - prevCmd.points[2]); + ctlPty = cpy + (cpy - prevCmd.points[3]); + } + points.push(ctlPtx, ctlPty, p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 's': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'C') { + ctlPtx = cpx + (cpx - prevCmd.points[2]); + ctlPty = cpy + (cpy - prevCmd.points[3]); + } + points.push(ctlPtx, ctlPty, cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'C'; + points.push(cpx, cpy); + break; + case 'Q': + points.push(p.shift(), p.shift()); + cpx = p.shift(); + cpy = p.shift(); + points.push(cpx, cpy); + break; + case 'q': + points.push(cpx + p.shift(), cpy + p.shift()); + cpx += p.shift(); + cpy += p.shift(); + cmd = 'Q'; + points.push(cpx, cpy); + break; + case 'T': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'Q') { + ctlPtx = cpx + (cpx - prevCmd.points[0]); + ctlPty = cpy + (cpy - prevCmd.points[1]); + } + cpx = p.shift(); + cpy = p.shift(); + cmd = 'Q'; + points.push(ctlPtx, ctlPty, cpx, cpy); + break; + case 't': + ctlPtx = cpx; + ctlPty = cpy; + prevCmd = ca[ca.length - 1]; + if (prevCmd.command === 'Q') { + ctlPtx = cpx + (cpx - prevCmd.points[0]); + ctlPty = cpy + (cpy - prevCmd.points[1]); + } + cpx += p.shift(); + cpy += p.shift(); + cmd = 'Q'; + points.push(ctlPtx, ctlPty, cpx, cpy); + break; + case 'A': + rx = p.shift(); + ry = p.shift(); + psi = p.shift(); + fa = p.shift(); + fs = p.shift(); + x1 = cpx; + y1 = cpy; + cpx = p.shift(); + cpy = p.shift(); + cmd = 'A'; + points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi); + break; + case 'a': + rx = p.shift(); + ry = p.shift(); + psi = p.shift(); + fa = p.shift(); + fs = p.shift(); + x1 = cpx; + y1 = cpy; + cpx += p.shift(); + cpy += p.shift(); + cmd = 'A'; + points = this.convertEndpointToCenterParameterization(x1, y1, cpx, cpy, fa, fs, rx, ry, psi); + break; + } + ca.push({ + command: cmd || c, + points: points, + start: { + x: startX, + y: startY, + }, + pathLength: this.calcLength(startX, startY, cmd || c, points), + }); + } + if (c === 'z' || c === 'Z') { + ca.push({ + command: 'z', + points: [], + start: undefined, + pathLength: 0, + }); + } + } + return ca; + } + static calcLength(x, y, cmd, points) { + let len, p1, p2, t; + const path = Path; + switch (cmd) { + case 'L': + return path.getLineLength(x, y, points[0], points[1]); + case 'C': + return getCubicArcLength([x, points[0], points[2], points[4]], [y, points[1], points[3], points[5]], 1); + case 'Q': + return getQuadraticArcLength([x, points[0], points[2]], [y, points[1], points[3]], 1); + case 'A': + // Approximates by breaking curve into line segments + len = 0.0; + var start = points[4]; + // 4 = theta + var dTheta = points[5]; + // 5 = dTheta + var end = points[4] + dTheta; + var inc = Math.PI / 180.0; + // 1 degree resolution + if (Math.abs(start - end) < inc) { + inc = Math.abs(start - end); + } + // Note: for purpose of calculating arc length, not going to worry about rotating X-axis by angle psi + p1 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], start, 0); + if (dTheta < 0) { + // clockwise + for (t = start - inc; t > end; t -= inc) { + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + else { + // counter-clockwise + for (t = start + inc; t < end; t += inc) { + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], t, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + p1 = p2; + } + } + p2 = path.getPointOnEllipticalArc(points[0], points[1], points[2], points[3], end, 0); + len += path.getLineLength(p1.x, p1.y, p2.x, p2.y); + return len; + } + return 0; + } + static convertEndpointToCenterParameterization(x1, y1, x2, y2, fa, fs, rx, ry, psiDeg) { + // Derived from: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + const psi = psiDeg * (Math.PI / 180.0); + const xp = (Math.cos(psi) * (x1 - x2)) / 2.0 + (Math.sin(psi) * (y1 - y2)) / 2.0; + const yp = (-1 * Math.sin(psi) * (x1 - x2)) / 2.0 + + (Math.cos(psi) * (y1 - y2)) / 2.0; + const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry); + if (lambda > 1) { + rx *= Math.sqrt(lambda); + ry *= Math.sqrt(lambda); + } + let f = Math.sqrt((rx * rx * (ry * ry) - rx * rx * (yp * yp) - ry * ry * (xp * xp)) / + (rx * rx * (yp * yp) + ry * ry * (xp * xp))); + if (fa === fs) { + f *= -1; + } + if (isNaN(f)) { + f = 0; + } + const cxp = (f * rx * yp) / ry; + const cyp = (f * -ry * xp) / rx; + const cx = (x1 + x2) / 2.0 + Math.cos(psi) * cxp - Math.sin(psi) * cyp; + const cy = (y1 + y2) / 2.0 + Math.sin(psi) * cxp + Math.cos(psi) * cyp; + const vMag = function (v) { + return Math.sqrt(v[0] * v[0] + v[1] * v[1]); + }; + const vRatio = function (u, v) { + return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); + }; + const vAngle = function (u, v) { + return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); + }; + const theta = vAngle([1, 0], [(xp - cxp) / rx, (yp - cyp) / ry]); + const u = [(xp - cxp) / rx, (yp - cyp) / ry]; + const v = [(-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry]; + let dTheta = vAngle(u, v); + if (vRatio(u, v) <= -1) { + dTheta = Math.PI; + } + if (vRatio(u, v) >= 1) { + dTheta = 0; + } + if (fs === 0 && dTheta > 0) { + dTheta = dTheta - 2 * Math.PI; + } + if (fs === 1 && dTheta < 0) { + dTheta = dTheta + 2 * Math.PI; + } + return [cx, cy, rx, ry, theta, dTheta, psi, fs]; + } + } + Path.prototype.className = 'Path'; + Path.prototype._attrsAffectingSize = ['data']; + _registerNode(Path); + /** + * get/set SVG path data string. This method + * also automatically parses the data string + * into a data array. Currently supported SVG data: + * M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z + * @name Konva.Path#data + * @method + * @param {String} data svg path string + * @returns {String} + * @example + * // get data + * var data = path.data(); + * + * // set data + * path.data('M200,100h100v50z'); + */ + Factory.addGetterSetter(Path, 'data'); + + /** + * Arrow constructor + * @constructor + * @memberof Konva + * @augments Konva.Line + * @param {Object} config + * @param {Array} config.points Flat array of points coordinates. You should define them as [x1, y1, x2, y2, x3, y3]. + * @param {Number} [config.tension] Higher values will result in a more curvy line. A value of 0 will result in no interpolation. + * The default is 0 + * @param {Number} config.pointerLength Arrow pointer length. Default value is 10. + * @param {Number} config.pointerWidth Arrow pointer width. Default value is 10. + * @param {Boolean} config.pointerAtBeginning Do we need to draw pointer on beginning position?. Default false. + * @param {Boolean} config.pointerAtEnding Do we need to draw pointer on ending position?. Default true. + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var line = new Konva.Line({ + * points: [73, 70, 340, 23, 450, 60, 500, 20], + * stroke: 'red', + * tension: 1, + * pointerLength : 10, + * pointerWidth : 12 + * }); + */ + class Arrow extends Line { + _sceneFunc(ctx) { + super._sceneFunc(ctx); + const PI2 = Math.PI * 2; + const points = this.points(); + let tp = points; + const fromTension = this.tension() !== 0 && points.length > 4; + if (fromTension) { + tp = this.getTensionPoints(); + } + const length = this.pointerLength(); + const n = points.length; + let dx, dy; + if (fromTension) { + const lp = [ + tp[tp.length - 4], + tp[tp.length - 3], + tp[tp.length - 2], + tp[tp.length - 1], + points[n - 2], + points[n - 1], + ]; + const lastLength = Path.calcLength(tp[tp.length - 4], tp[tp.length - 3], 'C', lp); + const previous = Path.getPointOnQuadraticBezier(Math.min(1, 1 - length / lastLength), lp[0], lp[1], lp[2], lp[3], lp[4], lp[5]); + dx = points[n - 2] - previous.x; + dy = points[n - 1] - previous.y; + } + else { + dx = points[n - 2] - points[n - 4]; + dy = points[n - 1] - points[n - 3]; + } + const radians = (Math.atan2(dy, dx) + PI2) % PI2; + const width = this.pointerWidth(); + if (this.pointerAtEnding()) { + ctx.save(); + ctx.beginPath(); + ctx.translate(points[n - 2], points[n - 1]); + ctx.rotate(radians); + ctx.moveTo(0, 0); + ctx.lineTo(-length, width / 2); + ctx.lineTo(-length, -width / 2); + ctx.closePath(); + ctx.restore(); + this.__fillStroke(ctx); + } + if (this.pointerAtBeginning()) { + ctx.save(); + ctx.beginPath(); + ctx.translate(points[0], points[1]); + if (fromTension) { + dx = (tp[0] + tp[2]) / 2 - points[0]; + dy = (tp[1] + tp[3]) / 2 - points[1]; + } + else { + dx = points[2] - points[0]; + dy = points[3] - points[1]; + } + ctx.rotate((Math.atan2(-dy, -dx) + PI2) % PI2); + ctx.moveTo(0, 0); + ctx.lineTo(-length, width / 2); + ctx.lineTo(-length, -width / 2); + ctx.closePath(); + ctx.restore(); + this.__fillStroke(ctx); + } + } + __fillStroke(ctx) { + // here is a tricky part + // we need to disable dash for arrow pointers + const isDashEnabled = this.dashEnabled(); + if (isDashEnabled) { + // manually disable dash for head + // it is better not to use setter here, + // because it will trigger attr change event + this.attrs.dashEnabled = false; + ctx.setLineDash([]); + } + ctx.fillStrokeShape(this); + // restore old value + if (isDashEnabled) { + this.attrs.dashEnabled = true; + } + } + getSelfRect() { + const lineRect = super.getSelfRect(); + const offset = this.pointerWidth() / 2; + return { + x: lineRect.x, + y: lineRect.y - offset, + width: lineRect.width, + height: lineRect.height + offset * 2, + }; + } + } + Arrow.prototype.className = 'Arrow'; + _registerNode(Arrow); + /** + * get/set pointerLength + * @name Konva.Arrow#pointerLength + * @method + * @param {Number} Length of pointer of arrow. The default is 10. + * @returns {Number} + * @example + * // get length + * var pointerLength = line.pointerLength(); + * + * // set length + * line.pointerLength(15); + */ + Factory.addGetterSetter(Arrow, 'pointerLength', 10, getNumberValidator()); + /** + * get/set pointerWidth + * @name Konva.Arrow#pointerWidth + * @method + * @param {Number} Width of pointer of arrow. + * The default is 10. + * @returns {Number} + * @example + * // get width + * var pointerWidth = line.pointerWidth(); + * + * // set width + * line.pointerWidth(15); + */ + Factory.addGetterSetter(Arrow, 'pointerWidth', 10, getNumberValidator()); + /** + * get/set pointerAtBeginning + * @name Konva.Arrow#pointerAtBeginning + * @method + * @param {Number} Should pointer displayed at beginning of arrow. The default is false. + * @returns {Boolean} + * @example + * // get value + * var pointerAtBeginning = line.pointerAtBeginning(); + * + * // set value + * line.pointerAtBeginning(true); + */ + Factory.addGetterSetter(Arrow, 'pointerAtBeginning', false); + /** + * get/set pointerAtEnding + * @name Konva.Arrow#pointerAtEnding + * @method + * @param {Number} Should pointer displayed at ending of arrow. The default is true. + * @returns {Boolean} + * @example + * // get value + * var pointerAtEnding = line.pointerAtEnding(); + * + * // set value + * line.pointerAtEnding(false); + */ + Factory.addGetterSetter(Arrow, 'pointerAtEnding', true); + + /** + * Circle constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.radius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // create circle + * var circle = new Konva.Circle({ + * radius: 40, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 5 + * }); + */ + class Circle extends Shape { + _sceneFunc(context) { + context.beginPath(); + context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.radius() * 2; + } + getHeight() { + return this.radius() * 2; + } + setWidth(width) { + if (this.radius() !== width / 2) { + this.radius(width / 2); + } + } + setHeight(height) { + if (this.radius() !== height / 2) { + this.radius(height / 2); + } + } + } + Circle.prototype._centroid = true; + Circle.prototype.className = 'Circle'; + Circle.prototype._attrsAffectingSize = ['radius']; + _registerNode(Circle); + /** + * get/set radius + * @name Konva.Circle#radius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radius + * var radius = circle.radius(); + * + * // set radius + * circle.radius(10); + */ + Factory.addGetterSetter(Circle, 'radius', 0, getNumberValidator()); + + /** + * Ellipse constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Object} config.radius defines x and y radius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var ellipse = new Konva.Ellipse({ + * radius : { + * x : 50, + * y : 50 + * }, + * fill: 'red' + * }); + */ + class Ellipse extends Shape { + _sceneFunc(context) { + const rx = this.radiusX(), ry = this.radiusY(); + context.beginPath(); + context.save(); + if (rx !== ry) { + context.scale(1, ry / rx); + } + context.arc(0, 0, rx, 0, Math.PI * 2, false); + context.restore(); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.radiusX() * 2; + } + getHeight() { + return this.radiusY() * 2; + } + setWidth(width) { + this.radiusX(width / 2); + } + setHeight(height) { + this.radiusY(height / 2); + } + } + Ellipse.prototype.className = 'Ellipse'; + Ellipse.prototype._centroid = true; + Ellipse.prototype._attrsAffectingSize = ['radiusX', 'radiusY']; + _registerNode(Ellipse); + // add getters setters + Factory.addComponentsGetterSetter(Ellipse, 'radius', ['x', 'y']); + /** + * get/set radius + * @name Konva.Ellipse#radius + * @method + * @param {Object} radius + * @param {Number} radius.x + * @param {Number} radius.y + * @returns {Object} + * @example + * // get radius + * var radius = ellipse.radius(); + * + * // set radius + * ellipse.radius({ + * x: 200, + * y: 100 + * }); + */ + Factory.addGetterSetter(Ellipse, 'radiusX', 0, getNumberValidator()); + /** + * get/set radius x + * @name Konva.Ellipse#radiusX + * @method + * @param {Number} x + * @returns {Number} + * @example + * // get radius x + * var radiusX = ellipse.radiusX(); + * + * // set radius x + * ellipse.radiusX(200); + */ + Factory.addGetterSetter(Ellipse, 'radiusY', 0, getNumberValidator()); + /** + * get/set radius y + * @name Konva.Ellipse#radiusY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get radius y + * var radiusY = ellipse.radiusY(); + * + * // set radius y + * ellipse.radiusY(200); + */ + + /** + * Image constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Image} config.image + * @param {Object} [config.crop] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var imageObj = new Image(); + * imageObj.onload = function() { + * var image = new Konva.Image({ + * x: 200, + * y: 50, + * image: imageObj, + * width: 100, + * height: 100 + * }); + * }; + * imageObj.src = '/path/to/image.jpg' + */ + class Image extends Shape { + constructor(attrs) { + super(attrs); + this._loadListener = () => { + this._requestDraw(); + }; + this.on('imageChange.konva', (props) => { + this._removeImageLoad(props.oldVal); + this._setImageLoad(); + }); + this._setImageLoad(); + } + _setImageLoad() { + const image = this.image(); + // check is image is already loaded + if (image && image.complete) { + return; + } + // check is video is already loaded + if (image && image.readyState === 4) { + return; + } + if (image && image['addEventListener']) { + image['addEventListener']('load', this._loadListener); + } + } + _removeImageLoad(image) { + if (image && image['removeEventListener']) { + image['removeEventListener']('load', this._loadListener); + } + } + destroy() { + this._removeImageLoad(this.image()); + super.destroy(); + return this; + } + _useBufferCanvas() { + const hasCornerRadius = !!this.cornerRadius(); + const hasShadow = this.hasShadow(); + if (hasCornerRadius && hasShadow) { + return true; + } + return super._useBufferCanvas(true); + } + _sceneFunc(context) { + const width = this.getWidth(); + const height = this.getHeight(); + const cornerRadius = this.cornerRadius(); + const image = this.attrs.image; + let params; + if (image) { + const cropWidth = this.attrs.cropWidth; + const cropHeight = this.attrs.cropHeight; + if (cropWidth && cropHeight) { + params = [ + image, + this.cropX(), + this.cropY(), + cropWidth, + cropHeight, + 0, + 0, + width, + height, + ]; + } + else { + params = [image, 0, 0, width, height]; + } + } + if (this.hasFill() || this.hasStroke() || cornerRadius) { + context.beginPath(); + cornerRadius + ? Util.drawRoundedRectPath(context, width, height, cornerRadius) + : context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); + } + if (image) { + if (cornerRadius) { + context.clip(); + } + context.drawImage.apply(context, params); + } + // If you need to draw later, you need to execute save/restore + } + _hitFunc(context) { + const width = this.width(), height = this.height(), cornerRadius = this.cornerRadius(); + context.beginPath(); + if (!cornerRadius) { + context.rect(0, 0, width, height); + } + else { + Util.drawRoundedRectPath(context, width, height, cornerRadius); + } + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + var _a, _b; + return (_a = this.attrs.width) !== null && _a !== undefined ? _a : (_b = this.image()) === null || _b === undefined ? undefined : _b.width; + } + getHeight() { + var _a, _b; + return (_a = this.attrs.height) !== null && _a !== undefined ? _a : (_b = this.image()) === null || _b === undefined ? undefined : _b.height; + } + /** + * load image from given url and create `Konva.Image` instance + * @method + * @memberof Konva.Image + * @param {String} url image source + * @param {Function} callback with Konva.Image instance as first argument + * @param {Function} onError optional error handler + * @example + * Konva.Image.fromURL(imageURL, function(image){ + * // image is Konva.Image instance + * layer.add(image); + * layer.draw(); + * }); + */ + static fromURL(url, callback, onError = null) { + const img = Util.createImageElement(); + img.onload = function () { + const image = new Image({ + image: img, + }); + callback(image); + }; + img.onerror = onError; + img.crossOrigin = 'Anonymous'; + img.src = url; + } + } + Image.prototype.className = 'Image'; + _registerNode(Image); + /** + * get/set corner radius + * @method + * @name Konva.Image#cornerRadius + * @param {Number} cornerRadius + * @returns {Number} + * @example + * // get corner radius + * var cornerRadius = image.cornerRadius(); + * + * // set corner radius + * image.cornerRadius(10); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * image.cornerRadius([0, 10, 20, 30]); + */ + Factory.addGetterSetter(Image, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4)); + /** + * get/set image source. It can be image, canvas or video element + * @name Konva.Image#image + * @method + * @param {Object} image source + * @returns {Object} + * @example + * // get value + * var image = shape.image(); + * + * // set value + * shape.image(img); + */ + Factory.addGetterSetter(Image, 'image'); + Factory.addComponentsGetterSetter(Image, 'crop', ['x', 'y', 'width', 'height']); + /** + * get/set crop + * @method + * @name Konva.Image#crop + * @param {Object} crop + * @param {Number} crop.x + * @param {Number} crop.y + * @param {Number} crop.width + * @param {Number} crop.height + * @returns {Object} + * @example + * // get crop + * var crop = image.crop(); + * + * // set crop + * image.crop({ + * x: 20, + * y: 20, + * width: 20, + * height: 20 + * }); + */ + Factory.addGetterSetter(Image, 'cropX', 0, getNumberValidator()); + /** + * get/set crop x + * @method + * @name Konva.Image#cropX + * @param {Number} x + * @returns {Number} + * @example + * // get crop x + * var cropX = image.cropX(); + * + * // set crop x + * image.cropX(20); + */ + Factory.addGetterSetter(Image, 'cropY', 0, getNumberValidator()); + /** + * get/set crop y + * @name Konva.Image#cropY + * @method + * @param {Number} y + * @returns {Number} + * @example + * // get crop y + * var cropY = image.cropY(); + * + * // set crop y + * image.cropY(20); + */ + Factory.addGetterSetter(Image, 'cropWidth', 0, getNumberValidator()); + /** + * get/set crop width + * @name Konva.Image#cropWidth + * @method + * @param {Number} width + * @returns {Number} + * @example + * // get crop width + * var cropWidth = image.cropWidth(); + * + * // set crop width + * image.cropWidth(20); + */ + Factory.addGetterSetter(Image, 'cropHeight', 0, getNumberValidator()); + /** + * get/set crop height + * @name Konva.Image#cropHeight + * @method + * @param {Number} height + * @returns {Number} + * @example + * // get crop height + * var cropHeight = image.cropHeight(); + * + * // set crop height + * image.cropHeight(20); + */ + + // constants + const ATTR_CHANGE_LIST$2 = [ + 'fontFamily', + 'fontSize', + 'fontStyle', + 'padding', + 'lineHeight', + 'text', + 'width', + 'height', + 'pointerDirection', + 'pointerWidth', + 'pointerHeight', + ], CHANGE_KONVA$1 = 'Change.konva', NONE$1 = 'none', UP = 'up', RIGHT$1 = 'right', DOWN = 'down', LEFT$1 = 'left', + // cached variables + attrChangeListLen$1 = ATTR_CHANGE_LIST$2.length; + /** + * Label constructor.  Labels are groups that contain a Text and Tag shape + * @constructor + * @memberof Konva + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // create label + * var label = new Konva.Label({ + * x: 100, + * y: 100, + * draggable: true + * }); + * + * // add a tag to the label + * label.add(new Konva.Tag({ + * fill: '#bbb', + * stroke: '#333', + * shadowColor: 'black', + * shadowBlur: 10, + * shadowOffset: [10, 10], + * shadowOpacity: 0.2, + * lineJoin: 'round', + * pointerDirection: 'up', + * pointerWidth: 20, + * pointerHeight: 20, + * cornerRadius: 5 + * })); + * + * // add text to the label + * label.add(new Konva.Text({ + * text: 'Hello World!', + * fontSize: 50, + * lineHeight: 1.2, + * padding: 10, + * fill: 'green' + * })); + */ + class Label extends Group { + constructor(config) { + super(config); + this.on('add.konva', function (evt) { + this._addListeners(evt.child); + this._sync(); + }); + } + /** + * get Text shape for the label. You need to access the Text shape in order to update + * the text properties + * @name Konva.Label#getText + * @method + * @example + * label.getText().fill('red') + */ + getText() { + return this.find('Text')[0]; + } + /** + * get Tag shape for the label. You need to access the Tag shape in order to update + * the pointer properties and the corner radius + * @name Konva.Label#getTag + * @method + */ + getTag() { + return this.find('Tag')[0]; + } + _addListeners(text) { + let that = this, n; + const func = function () { + that._sync(); + }; + // update text data for certain attr changes + for (n = 0; n < attrChangeListLen$1; n++) { + text.on(ATTR_CHANGE_LIST$2[n] + CHANGE_KONVA$1, func); + } + } + getWidth() { + return this.getText().width(); + } + getHeight() { + return this.getText().height(); + } + _sync() { + let text = this.getText(), tag = this.getTag(), width, height, pointerDirection, pointerWidth, x, y, pointerHeight; + if (text && tag) { + width = text.width(); + height = text.height(); + pointerDirection = tag.pointerDirection(); + pointerWidth = tag.pointerWidth(); + pointerHeight = tag.pointerHeight(); + x = 0; + y = 0; + switch (pointerDirection) { + case UP: + x = width / 2; + y = -1 * pointerHeight; + break; + case RIGHT$1: + x = width + pointerWidth; + y = height / 2; + break; + case DOWN: + x = width / 2; + y = height + pointerHeight; + break; + case LEFT$1: + x = -1 * pointerWidth; + y = height / 2; + break; + } + tag.setAttrs({ + x: -1 * x, + y: -1 * y, + width: width, + height: height, + }); + text.setAttrs({ + x: -1 * x, + y: -1 * y, + }); + } + } + } + Label.prototype.className = 'Label'; + _registerNode(Label); + /** + * Tag constructor.  A Tag can be configured + * to have a pointer element that points up, right, down, or left + * @constructor + * @memberof Konva + * @param {Object} config + * @param {String} [config.pointerDirection] can be up, right, down, left, or none; the default + * is none. When a pointer is present, the positioning of the label is relative to the tip of the pointer. + * @param {Number} [config.pointerWidth] + * @param {Number} [config.pointerHeight] + * @param {Number} [config.cornerRadius] + */ + class Tag extends Shape { + _sceneFunc(context) { + const width = this.width(), height = this.height(), pointerDirection = this.pointerDirection(), pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), cornerRadius = this.cornerRadius(); + let topLeft = 0; + let topRight = 0; + let bottomLeft = 0; + let bottomRight = 0; + if (typeof cornerRadius === 'number') { + topLeft = + topRight = + bottomLeft = + bottomRight = + Math.min(cornerRadius, width / 2, height / 2); + } + else { + topLeft = Math.min(cornerRadius[0] || 0, width / 2, height / 2); + topRight = Math.min(cornerRadius[1] || 0, width / 2, height / 2); + bottomRight = Math.min(cornerRadius[2] || 0, width / 2, height / 2); + bottomLeft = Math.min(cornerRadius[3] || 0, width / 2, height / 2); + } + context.beginPath(); + context.moveTo(topLeft, 0); + if (pointerDirection === UP) { + context.lineTo((width - pointerWidth) / 2, 0); + context.lineTo(width / 2, -1 * pointerHeight); + context.lineTo((width + pointerWidth) / 2, 0); + } + context.lineTo(width - topRight, 0); + context.arc(width - topRight, topRight, topRight, (Math.PI * 3) / 2, 0, false); + if (pointerDirection === RIGHT$1) { + context.lineTo(width, (height - pointerHeight) / 2); + context.lineTo(width + pointerWidth, height / 2); + context.lineTo(width, (height + pointerHeight) / 2); + } + context.lineTo(width, height - bottomRight); + context.arc(width - bottomRight, height - bottomRight, bottomRight, 0, Math.PI / 2, false); + if (pointerDirection === DOWN) { + context.lineTo((width + pointerWidth) / 2, height); + context.lineTo(width / 2, height + pointerHeight); + context.lineTo((width - pointerWidth) / 2, height); + } + context.lineTo(bottomLeft, height); + context.arc(bottomLeft, height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false); + if (pointerDirection === LEFT$1) { + context.lineTo(0, (height + pointerHeight) / 2); + context.lineTo(-1 * pointerWidth, height / 2); + context.lineTo(0, (height - pointerHeight) / 2); + } + context.lineTo(0, topLeft); + context.arc(topLeft, topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false); + context.closePath(); + context.fillStrokeShape(this); + } + getSelfRect() { + let x = 0, y = 0, pointerWidth = this.pointerWidth(), pointerHeight = this.pointerHeight(), direction = this.pointerDirection(), width = this.width(), height = this.height(); + if (direction === UP) { + y -= pointerHeight; + height += pointerHeight; + } + else if (direction === DOWN) { + height += pointerHeight; + } + else if (direction === LEFT$1) { + // ARGH!!! I have no idea why should I used magic 1.5!!!!!!!!! + x -= pointerWidth * 1.5; + width += pointerWidth; + } + else if (direction === RIGHT$1) { + width += pointerWidth * 1.5; + } + return { + x: x, + y: y, + width: width, + height: height, + }; + } + } + Tag.prototype.className = 'Tag'; + _registerNode(Tag); + /** + * get/set pointer direction + * @name Konva.Tag#pointerDirection + * @method + * @param {String} pointerDirection can be up, right, down, left, or none. The default is none. + * @returns {String} + * @example + * tag.pointerDirection('right'); + */ + Factory.addGetterSetter(Tag, 'pointerDirection', NONE$1); + /** + * get/set pointer width + * @name Konva.Tag#pointerWidth + * @method + * @param {Number} pointerWidth + * @returns {Number} + * @example + * tag.pointerWidth(20); + */ + Factory.addGetterSetter(Tag, 'pointerWidth', 0, getNumberValidator()); + /** + * get/set pointer height + * @method + * @name Konva.Tag#pointerHeight + * @param {Number} pointerHeight + * @returns {Number} + * @example + * tag.pointerHeight(20); + */ + Factory.addGetterSetter(Tag, 'pointerHeight', 0, getNumberValidator()); + /** + * get/set cornerRadius + * @name Konva.Tag#cornerRadius + * @method + * @param {Number} cornerRadius + * @returns {Number} + * @example + * tag.cornerRadius(20); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * tag.cornerRadius([0, 10, 20, 30]); + */ + Factory.addGetterSetter(Tag, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4)); + + /** + * Rect constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} [config.cornerRadius] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var rect = new Konva.Rect({ + * width: 100, + * height: 50, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 5 + * }); + */ + class Rect extends Shape { + _sceneFunc(context) { + const cornerRadius = this.cornerRadius(), width = this.width(), height = this.height(); + context.beginPath(); + if (!cornerRadius) { + // simple rect - don't bother doing all that complicated maths stuff. + context.rect(0, 0, width, height); + } + else { + Util.drawRoundedRectPath(context, width, height, cornerRadius); + } + context.closePath(); + context.fillStrokeShape(this); + } + } + Rect.prototype.className = 'Rect'; + _registerNode(Rect); + /** + * get/set corner radius + * @method + * @name Konva.Rect#cornerRadius + * @param {Number} cornerRadius + * @returns {Number} + * @example + * // get corner radius + * var cornerRadius = rect.cornerRadius(); + * + * // set corner radius + * rect.cornerRadius(10); + * + * // set different corner radius values + * // top-left, top-right, bottom-right, bottom-left + * rect.cornerRadius([0, 10, 20, 30]); + */ + Factory.addGetterSetter(Rect, 'cornerRadius', 0, getNumberOrArrayOfNumbersValidator(4)); + + /** + * RegularPolygon constructor. Examples include triangles, squares, pentagons, hexagons, etc. + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.sides + * @param {Number} config.radius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var hexagon = new Konva.RegularPolygon({ + * x: 100, + * y: 200, + * sides: 6, + * radius: 70, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 4 + * }); + */ + class RegularPolygon extends Shape { + _sceneFunc(context) { + const points = this._getPoints(); + context.beginPath(); + context.moveTo(points[0].x, points[0].y); + for (let n = 1; n < points.length; n++) { + context.lineTo(points[n].x, points[n].y); + } + context.closePath(); + context.fillStrokeShape(this); + } + _getPoints() { + const sides = this.attrs.sides; + const radius = this.attrs.radius || 0; + const points = []; + for (let n = 0; n < sides; n++) { + points.push({ + x: radius * Math.sin((n * 2 * Math.PI) / sides), + y: -1 * radius * Math.cos((n * 2 * Math.PI) / sides), + }); + } + return points; + } + getSelfRect() { + const points = this._getPoints(); + let minX = points[0].x; + let maxX = points[0].y; + let minY = points[0].x; + let maxY = points[0].y; + points.forEach((point) => { + minX = Math.min(minX, point.x); + maxX = Math.max(maxX, point.x); + minY = Math.min(minY, point.y); + maxY = Math.max(maxY, point.y); + }); + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } + getWidth() { + return this.radius() * 2; + } + getHeight() { + return this.radius() * 2; + } + setWidth(width) { + this.radius(width / 2); + } + setHeight(height) { + this.radius(height / 2); + } + } + RegularPolygon.prototype.className = 'RegularPolygon'; + RegularPolygon.prototype._centroid = true; + RegularPolygon.prototype._attrsAffectingSize = ['radius']; + _registerNode(RegularPolygon); + /** + * get/set radius + * @method + * @name Konva.RegularPolygon#radius + * @param {Number} radius + * @returns {Number} + * @example + * // get radius + * var radius = shape.radius(); + * + * // set radius + * shape.radius(10); + */ + Factory.addGetterSetter(RegularPolygon, 'radius', 0, getNumberValidator()); + /** + * get/set sides + * @method + * @name Konva.RegularPolygon#sides + * @param {Number} sides + * @returns {Number} + * @example + * // get sides + * var sides = shape.sides(); + * + * // set sides + * shape.sides(10); + */ + Factory.addGetterSetter(RegularPolygon, 'sides', 0, getNumberValidator()); + + const PIx2 = Math.PI * 2; + /** + * Ring constructor + * @constructor + * @augments Konva.Shape + * @memberof Konva + * @param {Object} config + * @param {Number} config.innerRadius + * @param {Number} config.outerRadius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var ring = new Konva.Ring({ + * innerRadius: 40, + * outerRadius: 80, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 5 + * }); + */ + class Ring extends Shape { + _sceneFunc(context) { + context.beginPath(); + context.arc(0, 0, this.innerRadius(), 0, PIx2, false); + context.moveTo(this.outerRadius(), 0); + context.arc(0, 0, this.outerRadius(), PIx2, 0, true); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.outerRadius() * 2; + } + getHeight() { + return this.outerRadius() * 2; + } + setWidth(width) { + this.outerRadius(width / 2); + } + setHeight(height) { + this.outerRadius(height / 2); + } + } + Ring.prototype.className = 'Ring'; + Ring.prototype._centroid = true; + Ring.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius']; + _registerNode(Ring); + /** + * get/set innerRadius + * @method + * @name Konva.Ring#innerRadius + * @param {Number} innerRadius + * @returns {Number} + * @example + * // get inner radius + * var innerRadius = ring.innerRadius(); + * + * // set inner radius + * ring.innerRadius(20); + */ + Factory.addGetterSetter(Ring, 'innerRadius', 0, getNumberValidator()); + /** + * get/set outerRadius + * @name Konva.Ring#outerRadius + * @method + * @param {Number} outerRadius + * @returns {Number} + * @example + * // get outer radius + * var outerRadius = ring.outerRadius(); + * + * // set outer radius + * ring.outerRadius(20); + */ + Factory.addGetterSetter(Ring, 'outerRadius', 0, getNumberValidator()); + + /** + * Sprite constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} config.animation animation key + * @param {Object} config.animations animation map + * @param {Integer} [config.frameIndex] animation frame index + * @param {Image} config.image image object + * @param {Integer} [config.frameRate] animation frame rate + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var imageObj = new Image(); + * imageObj.onload = function() { + * var sprite = new Konva.Sprite({ + * x: 200, + * y: 100, + * image: imageObj, + * animation: 'standing', + * animations: { + * standing: [ + * // x, y, width, height (6 frames) + * 0, 0, 49, 109, + * 52, 0, 49, 109, + * 105, 0, 49, 109, + * 158, 0, 49, 109, + * 210, 0, 49, 109, + * 262, 0, 49, 109 + * ], + * kicking: [ + * // x, y, width, height (6 frames) + * 0, 109, 45, 98, + * 45, 109, 45, 98, + * 95, 109, 63, 98, + * 156, 109, 70, 98, + * 229, 109, 60, 98, + * 287, 109, 41, 98 + * ] + * }, + * frameRate: 7, + * frameIndex: 0 + * }); + * }; + * imageObj.src = '/path/to/image.jpg' + */ + class Sprite extends Shape { + constructor(config) { + super(config); + this._updated = true; + this.anim = new Animation(() => { + // if we don't need to redraw layer we should return false + const updated = this._updated; + this._updated = false; + return updated; + }); + this.on('animationChange.konva', function () { + // reset index when animation changes + this.frameIndex(0); + }); + this.on('frameIndexChange.konva', function () { + this._updated = true; + }); + // smooth change for frameRate + this.on('frameRateChange.konva', function () { + if (!this.anim.isRunning()) { + return; + } + clearInterval(this.interval); + this._setInterval(); + }); + } + _sceneFunc(context) { + const anim = this.animation(), index = this.frameIndex(), ix4 = index * 4, set = this.animations()[anim], offsets = this.frameOffsets(), x = set[ix4 + 0], y = set[ix4 + 1], width = set[ix4 + 2], height = set[ix4 + 3], image = this.image(); + if (this.hasFill() || this.hasStroke()) { + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); + } + if (image) { + if (offsets) { + const offset = offsets[anim], ix2 = index * 2; + context.drawImage(image, x, y, width, height, offset[ix2 + 0], offset[ix2 + 1], width, height); + } + else { + context.drawImage(image, x, y, width, height, 0, 0, width, height); + } + } + } + _hitFunc(context) { + const anim = this.animation(), index = this.frameIndex(), ix4 = index * 4, set = this.animations()[anim], offsets = this.frameOffsets(), width = set[ix4 + 2], height = set[ix4 + 3]; + context.beginPath(); + if (offsets) { + const offset = offsets[anim]; + const ix2 = index * 2; + context.rect(offset[ix2 + 0], offset[ix2 + 1], width, height); + } + else { + context.rect(0, 0, width, height); + } + context.closePath(); + context.fillShape(this); + } + _useBufferCanvas() { + return super._useBufferCanvas(true); + } + _setInterval() { + const that = this; + this.interval = setInterval(function () { + that._updateIndex(); + }, 1000 / this.frameRate()); + } + /** + * start sprite animation + * @method + * @name Konva.Sprite#start + */ + start() { + if (this.isRunning()) { + return; + } + const layer = this.getLayer(); + /* + * animation object has no executable function because + * the updates are done with a fixed FPS with the setInterval + * below. The anim object only needs the layer reference for + * redraw + */ + this.anim.setLayers(layer); + this._setInterval(); + this.anim.start(); + } + /** + * stop sprite animation + * @method + * @name Konva.Sprite#stop + */ + stop() { + this.anim.stop(); + clearInterval(this.interval); + } + /** + * determine if animation of sprite is running or not. returns true or false + * @method + * @name Konva.Sprite#isRunning + * @returns {Boolean} + */ + isRunning() { + return this.anim.isRunning(); + } + _updateIndex() { + const index = this.frameIndex(), animation = this.animation(), animations = this.animations(), anim = animations[animation], len = anim.length / 4; + if (index < len - 1) { + this.frameIndex(index + 1); + } + else { + this.frameIndex(0); + } + } + } + Sprite.prototype.className = 'Sprite'; + _registerNode(Sprite); + // add getters setters + Factory.addGetterSetter(Sprite, 'animation'); + /** + * get/set animation key + * @name Konva.Sprite#animation + * @method + * @param {String} anim animation key + * @returns {String} + * @example + * // get animation key + * var animation = sprite.animation(); + * + * // set animation key + * sprite.animation('kicking'); + */ + Factory.addGetterSetter(Sprite, 'animations'); + /** + * get/set animations map + * @name Konva.Sprite#animations + * @method + * @param {Object} animations + * @returns {Object} + * @example + * // get animations map + * var animations = sprite.animations(); + * + * // set animations map + * sprite.animations({ + * standing: [ + * // x, y, width, height (6 frames) + * 0, 0, 49, 109, + * 52, 0, 49, 109, + * 105, 0, 49, 109, + * 158, 0, 49, 109, + * 210, 0, 49, 109, + * 262, 0, 49, 109 + * ], + * kicking: [ + * // x, y, width, height (6 frames) + * 0, 109, 45, 98, + * 45, 109, 45, 98, + * 95, 109, 63, 98, + * 156, 109, 70, 98, + * 229, 109, 60, 98, + * 287, 109, 41, 98 + * ] + * }); + */ + Factory.addGetterSetter(Sprite, 'frameOffsets'); + /** + * get/set offsets map + * @name Konva.Sprite#offsets + * @method + * @param {Object} offsets + * @returns {Object} + * @example + * // get offsets map + * var offsets = sprite.offsets(); + * + * // set offsets map + * sprite.offsets({ + * standing: [ + * // x, y (6 frames) + * 0, 0, + * 0, 0, + * 5, 0, + * 0, 0, + * 0, 3, + * 2, 0 + * ], + * kicking: [ + * // x, y (6 frames) + * 0, 5, + * 5, 0, + * 10, 0, + * 0, 0, + * 2, 1, + * 0, 0 + * ] + * }); + */ + Factory.addGetterSetter(Sprite, 'image'); + /** + * get/set image + * @name Konva.Sprite#image + * @method + * @param {Image} image + * @returns {Image} + * @example + * // get image + * var image = sprite.image(); + * + * // set image + * sprite.image(imageObj); + */ + Factory.addGetterSetter(Sprite, 'frameIndex', 0, getNumberValidator()); + /** + * set/set animation frame index + * @name Konva.Sprite#frameIndex + * @method + * @param {Integer} frameIndex + * @returns {Integer} + * @example + * // get animation frame index + * var frameIndex = sprite.frameIndex(); + * + * // set animation frame index + * sprite.frameIndex(3); + */ + Factory.addGetterSetter(Sprite, 'frameRate', 17, getNumberValidator()); + /** + * get/set frame rate in frames per second. Increase this number to make the sprite + * animation run faster, and decrease the number to make the sprite animation run slower + * The default is 17 frames per second + * @name Konva.Sprite#frameRate + * @method + * @param {Integer} frameRate + * @returns {Integer} + * @example + * // get frame rate + * var frameRate = sprite.frameRate(); + * + * // set frame rate to 2 frames per second + * sprite.frameRate(2); + */ + Factory.backCompat(Sprite, { + index: 'frameIndex', + getIndex: 'getFrameIndex', + setIndex: 'setFrameIndex', + }); + + /** + * Star constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Integer} config.numPoints + * @param {Number} config.innerRadius + * @param {Number} config.outerRadius + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var star = new Konva.Star({ + * x: 100, + * y: 200, + * numPoints: 5, + * innerRadius: 70, + * outerRadius: 70, + * fill: 'red', + * stroke: 'black', + * strokeWidth: 4 + * }); + */ + class Star extends Shape { + _sceneFunc(context) { + const innerRadius = this.innerRadius(), outerRadius = this.outerRadius(), numPoints = this.numPoints(); + context.beginPath(); + context.moveTo(0, 0 - outerRadius); + for (let n = 1; n < numPoints * 2; n++) { + const radius = n % 2 === 0 ? outerRadius : innerRadius; + const x = radius * Math.sin((n * Math.PI) / numPoints); + const y = -1 * radius * Math.cos((n * Math.PI) / numPoints); + context.lineTo(x, y); + } + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.outerRadius() * 2; + } + getHeight() { + return this.outerRadius() * 2; + } + setWidth(width) { + this.outerRadius(width / 2); + } + setHeight(height) { + this.outerRadius(height / 2); + } + } + Star.prototype.className = 'Star'; + Star.prototype._centroid = true; + Star.prototype._attrsAffectingSize = ['innerRadius', 'outerRadius']; + _registerNode(Star); + /** + * get/set number of points + * @name Konva.Star#numPoints + * @method + * @param {Number} numPoints + * @returns {Number} + * @example + * // get inner radius + * var numPoints = star.numPoints(); + * + * // set inner radius + * star.numPoints(20); + */ + Factory.addGetterSetter(Star, 'numPoints', 5, getNumberValidator()); + /** + * get/set innerRadius + * @name Konva.Star#innerRadius + * @method + * @param {Number} innerRadius + * @returns {Number} + * @example + * // get inner radius + * var innerRadius = star.innerRadius(); + * + * // set inner radius + * star.innerRadius(20); + */ + Factory.addGetterSetter(Star, 'innerRadius', 0, getNumberValidator()); + /** + * get/set outerRadius + * @name Konva.Star#outerRadius + * @method + * @param {Number} outerRadius + * @returns {Number} + * @example + * // get inner radius + * var outerRadius = star.outerRadius(); + * + * // set inner radius + * star.outerRadius(20); + */ + Factory.addGetterSetter(Star, 'outerRadius', 0, getNumberValidator()); + + function stringToArray(string) { + // Use Unicode-aware splitting + return [...string].reduce((acc, char, index, array) => { + // Handle emoji with skin tone modifiers and ZWJ sequences + if (/\p{Emoji}/u.test(char)) { + // Check if next character is a modifier or ZWJ sequence + const nextChar = array[index + 1]; + if (nextChar && /\p{Emoji_Modifier}|\u200D/u.test(nextChar)) { + // If we have a modifier, combine with current emoji + acc.push(char + nextChar); + // Skip the next character since we've used it + array[index + 1] = ''; + } + else { + // No modifier - treat as separate emoji + acc.push(char); + } + } + // Handle regional indicator symbols (flags) + else if (/\p{Regional_Indicator}{2}/u.test(char + (array[index + 1] || ''))) { + acc.push(char + array[index + 1]); + } + // Handle Indic scripts and other combining characters + else if (index > 0 && /\p{Mn}|\p{Me}|\p{Mc}/u.test(char)) { + acc[acc.length - 1] += char; + } + // Handle other characters + else if (char) { + // Only push if not an empty string (skipped modifier) + acc.push(char); + } + return acc; + }, []); + } + // constants + const AUTO = 'auto', + //CANVAS = 'canvas', + CENTER = 'center', INHERIT = 'inherit', JUSTIFY = 'justify', CHANGE_KONVA = 'Change.konva', CONTEXT_2D = '2d', DASH = '-', LEFT = 'left', TEXT = 'text', TEXT_UPPER = 'Text', TOP = 'top', BOTTOM = 'bottom', MIDDLE = 'middle', NORMAL$1 = 'normal', PX_SPACE = 'px ', SPACE = ' ', RIGHT = 'right', RTL = 'rtl', WORD = 'word', CHAR = 'char', NONE = 'none', ELLIPSIS = '…', ATTR_CHANGE_LIST$1 = [ + 'direction', + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontVariant', + 'padding', + 'align', + 'verticalAlign', + 'lineHeight', + 'text', + 'width', + 'height', + 'wrap', + 'ellipsis', + 'letterSpacing', + ], + // cached variables + attrChangeListLen = ATTR_CHANGE_LIST$1.length; + function normalizeFontFamily(fontFamily) { + return fontFamily + .split(',') + .map((family) => { + family = family.trim(); + const hasSpace = family.indexOf(' ') >= 0; + const hasQuotes = family.indexOf('"') >= 0 || family.indexOf("'") >= 0; + if (hasSpace && !hasQuotes) { + family = `"${family}"`; + } + return family; + }) + .join(', '); + } + let dummyContext; + function getDummyContext() { + if (dummyContext) { + return dummyContext; + } + dummyContext = Util.createCanvasElement().getContext(CONTEXT_2D); + return dummyContext; + } + function _fillFunc$1(context) { + context.fillText(this._partialText, this._partialTextX, this._partialTextY); + } + function _strokeFunc$1(context) { + context.setAttr('miterLimit', 2); + context.strokeText(this._partialText, this._partialTextX, this._partialTextY); + } + function checkDefaultFill(config) { + config = config || {}; + // set default color to black + if (!config.fillLinearGradientColorStops && + !config.fillRadialGradientColorStops && + !config.fillPatternImage) { + config.fill = config.fill || 'black'; + } + return config; + } + /** + * Text constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} [config.direction] default is inherit + * @param {String} [config.fontFamily] default is Arial + * @param {Number} [config.fontSize] in pixels. Default is 12 + * @param {String} [config.fontStyle] can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @param {String} [config.fontVariant] can be normal or small-caps. Default is normal + * @param {String} [config.textDecoration] can be line-through, underline or empty string. Default is empty string. + * @param {String} config.text + * @param {String} [config.align] can be left, center, or right + * @param {String} [config.verticalAlign] can be top, middle or bottom + * @param {Number} [config.padding] + * @param {Number} [config.lineHeight] default is 1 + * @param {String} [config.wrap] can be "word", "char", or "none". Default is word + * @param {Boolean} [config.ellipsis] can be true or false. Default is false. if Konva.Text config is set to wrap="none" and ellipsis=true, then it will add "..." to the end + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var text = new Konva.Text({ + * x: 10, + * y: 15, + * text: 'Simple Text', + * fontSize: 30, + * fontFamily: 'Calibri', + * fill: 'green' + * }); + */ + class Text extends Shape { + constructor(config) { + super(checkDefaultFill(config)); + this._partialTextX = 0; + this._partialTextY = 0; + // update text data for certain attr changes + for (let n = 0; n < attrChangeListLen; n++) { + this.on(ATTR_CHANGE_LIST$1[n] + CHANGE_KONVA, this._setTextData); + } + this._setTextData(); + } + _sceneFunc(context) { + const textArr = this.textArr, textArrLen = textArr.length; + if (!this.text()) { + return; + } + let padding = this.padding(), fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, verticalAlign = this.verticalAlign(), direction = this.direction(), alignY = 0, align = this.align(), totalWidth = this.getWidth(), letterSpacing = this.letterSpacing(), fill = this.fill(), textDecoration = this.textDecoration(), shouldUnderline = textDecoration.indexOf('underline') !== -1, shouldLineThrough = textDecoration.indexOf('line-through') !== -1, n; + direction = direction === INHERIT ? context.direction : direction; + let translateY = lineHeightPx / 2; + let baseline = MIDDLE; + if (Konva$2._fixTextRendering) { + const metrics = this.measureSize('M'); // Use a sample character to get the ascent + baseline = 'alphabetic'; + translateY = + (metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2 + + lineHeightPx / 2; + } + var lineTranslateX = 0; + var lineTranslateY = 0; + if (direction === RTL) { + context.setAttr('direction', direction); + } + context.setAttr('font', this._getContextFont()); + context.setAttr('textBaseline', baseline); + context.setAttr('textAlign', LEFT); + // handle vertical alignment + if (verticalAlign === MIDDLE) { + alignY = (this.getHeight() - textArrLen * lineHeightPx - padding * 2) / 2; + } + else if (verticalAlign === BOTTOM) { + alignY = this.getHeight() - textArrLen * lineHeightPx - padding * 2; + } + context.translate(padding, alignY + padding); + // draw text lines + for (n = 0; n < textArrLen; n++) { + var lineTranslateX = 0; + var lineTranslateY = 0; + var obj = textArr[n], text = obj.text, width = obj.width, lastLine = obj.lastInParagraph, spacesNumber, lineWidth; + // horizontal alignment + context.save(); + if (align === RIGHT) { + lineTranslateX += totalWidth - width - padding * 2; + } + else if (align === CENTER) { + lineTranslateX += (totalWidth - width - padding * 2) / 2; + } + if (shouldUnderline) { + context.save(); + context.beginPath(); + const yOffset = Konva$2._fixTextRendering + ? Math.round(fontSize / 4) + : Math.round(fontSize / 2); + const x = lineTranslateX; + const y = translateY + lineTranslateY + yOffset; + context.moveTo(x, y); + spacesNumber = text.split(' ').length - 1; + lineWidth = + align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width; + context.lineTo(x + Math.round(lineWidth), y); + // I have no idea what is real ratio + // just /15 looks good enough + context.lineWidth = fontSize / 15; + const gradient = this._getLinearGradient(); + context.strokeStyle = gradient || fill; + context.stroke(); + context.restore(); + } + if (shouldLineThrough) { + context.save(); + context.beginPath(); + const yOffset = Konva$2._fixTextRendering ? -Math.round(fontSize / 4) : 0; + context.moveTo(lineTranslateX, translateY + lineTranslateY + yOffset); + spacesNumber = text.split(' ').length - 1; + lineWidth = + align === JUSTIFY && !lastLine ? totalWidth - padding * 2 : width; + context.lineTo(lineTranslateX + Math.round(lineWidth), translateY + lineTranslateY + yOffset); + context.lineWidth = fontSize / 15; + const gradient = this._getLinearGradient(); + context.strokeStyle = gradient || fill; + context.stroke(); + context.restore(); + } + // As `letterSpacing` isn't supported on Safari, we use this polyfill. + // The exception is for RTL text, which we rely on native as it cannot + // be supported otherwise. + if (direction !== RTL && (letterSpacing !== 0 || align === JUSTIFY)) { + // var words = text.split(' '); + spacesNumber = text.split(' ').length - 1; + const array = stringToArray(text); + for (let li = 0; li < array.length; li++) { + const letter = array[li]; + // skip justify for the last line + if (letter === ' ' && !lastLine && align === JUSTIFY) { + lineTranslateX += (totalWidth - padding * 2 - width) / spacesNumber; + // context.translate( + // Math.floor((totalWidth - padding * 2 - width) / spacesNumber), + // 0 + // ); + } + this._partialTextX = lineTranslateX; + this._partialTextY = translateY + lineTranslateY; + this._partialText = letter; + context.fillStrokeShape(this); + lineTranslateX += this.measureSize(letter).width + letterSpacing; + } + } + else { + if (letterSpacing !== 0) { + context.setAttr('letterSpacing', `${letterSpacing}px`); + } + this._partialTextX = lineTranslateX; + this._partialTextY = translateY + lineTranslateY; + this._partialText = text; + context.fillStrokeShape(this); + } + context.restore(); + if (textArrLen > 1) { + translateY += lineHeightPx; + } + } + } + _hitFunc(context) { + const width = this.getWidth(), height = this.getHeight(); + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + context.fillStrokeShape(this); + } + setText(text) { + const str = Util._isString(text) + ? text + : text === null || text === undefined + ? '' + : text + ''; + this._setAttr(TEXT, str); + return this; + } + getWidth() { + const isAuto = this.attrs.width === AUTO || this.attrs.width === undefined; + return isAuto ? this.getTextWidth() + this.padding() * 2 : this.attrs.width; + } + getHeight() { + const isAuto = this.attrs.height === AUTO || this.attrs.height === undefined; + return isAuto + ? this.fontSize() * this.textArr.length * this.lineHeight() + + this.padding() * 2 + : this.attrs.height; + } + /** + * get pure text width without padding + * @method + * @name Konva.Text#getTextWidth + * @returns {Number} + */ + getTextWidth() { + return this.textWidth; + } + getTextHeight() { + Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.'); + return this.textHeight; + } + /** + * measure string with the font of current text shape. + * That method can't handle multiline text. + * @method + * @name Konva.Text#measureSize + * @param {String} text text to measure + * @returns {Object} { width , height } of measured text + */ + measureSize(text) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + let _context = getDummyContext(), fontSize = this.fontSize(), metrics; + _context.save(); + _context.font = this._getContextFont(); + metrics = _context.measureText(text); + _context.restore(); + // Scale the fallback values based on the provided fontSize compared to the sample size (100 in your new case) + const scaleFactor = fontSize / 100; + // Note, fallback values are from chrome browser with 100px font size and font-family "Arial" + return { + actualBoundingBoxAscent: (_a = metrics.actualBoundingBoxAscent) !== null && _a !== undefined ? _a : 71.58203125 * scaleFactor, + actualBoundingBoxDescent: (_b = metrics.actualBoundingBoxDescent) !== null && _b !== undefined ? _b : 0, // Remains zero as there is no descent in the provided metrics + actualBoundingBoxLeft: (_c = metrics.actualBoundingBoxLeft) !== null && _c !== undefined ? _c : -7.421875 * scaleFactor, + actualBoundingBoxRight: (_d = metrics.actualBoundingBoxRight) !== null && _d !== undefined ? _d : 75.732421875 * scaleFactor, + alphabeticBaseline: (_e = metrics.alphabeticBaseline) !== null && _e !== undefined ? _e : 0, // Remains zero as it's typically relative to the baseline itself + emHeightAscent: (_f = metrics.emHeightAscent) !== null && _f !== undefined ? _f : 100 * scaleFactor, + emHeightDescent: (_g = metrics.emHeightDescent) !== null && _g !== undefined ? _g : -20 * scaleFactor, + fontBoundingBoxAscent: (_h = metrics.fontBoundingBoxAscent) !== null && _h !== undefined ? _h : 91 * scaleFactor, + fontBoundingBoxDescent: (_j = metrics.fontBoundingBoxDescent) !== null && _j !== undefined ? _j : 21 * scaleFactor, + hangingBaseline: (_k = metrics.hangingBaseline) !== null && _k !== undefined ? _k : 72.80000305175781 * scaleFactor, + ideographicBaseline: (_l = metrics.ideographicBaseline) !== null && _l !== undefined ? _l : -21 * scaleFactor, + width: metrics.width, + height: fontSize, // Typically set to the font size + }; + } + _getContextFont() { + return (this.fontStyle() + + SPACE + + this.fontVariant() + + SPACE + + (this.fontSize() + PX_SPACE) + + // wrap font family into " so font families with spaces works ok + normalizeFontFamily(this.fontFamily())); + } + _addTextLine(line) { + const align = this.align(); + if (align === JUSTIFY) { + line = line.trim(); + } + const width = this._getTextWidth(line); + return this.textArr.push({ + text: line, + width: width, + lastInParagraph: false, + }); + } + _getTextWidth(text) { + const letterSpacing = this.letterSpacing(); + const length = text.length; + // letterSpacing * length is the total letter spacing for the text + // previously we used letterSpacing * (length - 1) but it doesn't match DOM behavior + return getDummyContext().measureText(text).width + letterSpacing * length; + } + _setTextData() { + let lines = this.text().split('\n'), fontSize = +this.fontSize(), textWidth = 0, lineHeightPx = this.lineHeight() * fontSize, width = this.attrs.width, height = this.attrs.height, fixedWidth = width !== AUTO && width !== undefined, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxWidth = width - padding * 2, maxHeightPx = height - padding * 2, currentHeightPx = 0, wrap = this.wrap(), + // align = this.align(), + shouldWrap = wrap !== NONE, wrapAtWord = wrap !== CHAR && shouldWrap, shouldAddEllipsis = this.ellipsis(); + this.textArr = []; + getDummyContext().font = this._getContextFont(); + const additionalWidth = shouldAddEllipsis + ? this._getTextWidth(ELLIPSIS) + : 0; + for (let i = 0, max = lines.length; i < max; ++i) { + let line = lines[i]; + let lineWidth = this._getTextWidth(line); + if (fixedWidth && lineWidth > maxWidth) { + /* + * if width is fixed and line does not fit entirely + * break the line into multiple fitting lines + */ + while (line.length > 0) { + /* + * use binary search to find the longest substring that + * that would fit in the specified width + */ + let low = 0, high = stringToArray(line).length, // Convert to array for proper emoji handling + match = '', matchWidth = 0; + while (low < high) { + const mid = (low + high) >>> 1, + // Convert array indices to string + lineArray = stringToArray(line), substr = lineArray.slice(0, mid + 1).join(''), substrWidth = this._getTextWidth(substr); + // Only add ellipsis width when we need to consider truncation + // for the current line (when it might be the last visible line) + const shouldConsiderEllipsis = shouldAddEllipsis && + fixedHeight && + currentHeightPx + lineHeightPx > maxHeightPx; + const effectiveWidth = shouldConsiderEllipsis + ? substrWidth + additionalWidth + : substrWidth; + if (effectiveWidth <= maxWidth) { + low = mid + 1; + match = substr; + matchWidth = substrWidth; // Store actual text width without ellipsis + } + else { + high = mid; + } + } + /* + * 'low' is now the index of the substring end + * 'match' is the substring + * 'matchWidth' is the substring width in px + */ + if (match) { + // a fitting substring was found + if (wrapAtWord) { + // try to find a space or dash where wrapping could be done + const lineArray = stringToArray(line); + const matchArray = stringToArray(match); + const nextChar = lineArray[matchArray.length]; + const nextIsSpaceOrDash = nextChar === SPACE || nextChar === DASH; + let wrapIndex; + if (nextIsSpaceOrDash && matchWidth <= maxWidth) { + wrapIndex = matchArray.length; + } + else { + // Find last space or dash in the array + const lastSpaceIndex = matchArray.lastIndexOf(SPACE); + const lastDashIndex = matchArray.lastIndexOf(DASH); + wrapIndex = Math.max(lastSpaceIndex, lastDashIndex) + 1; + } + if (wrapIndex > 0) { + low = wrapIndex; + match = lineArray.slice(0, low).join(''); + matchWidth = this._getTextWidth(match); + } + } + // if (align === 'right') { + match = match.trimRight(); + // } + this._addTextLine(match); + textWidth = Math.max(textWidth, matchWidth); + currentHeightPx += lineHeightPx; + const shouldHandleEllipsis = this._shouldHandleEllipsis(currentHeightPx); + if (shouldHandleEllipsis) { + this._tryToAddEllipsisToLastLine(); + /* + * stop wrapping if wrapping is disabled or if adding + * one more line would overflow the fixed height + */ + break; + } + // Convert remaining text using array operations + const lineArray = stringToArray(line); + line = lineArray.slice(low).join('').trimLeft(); + if (line.length > 0) { + lineWidth = this._getTextWidth(line); + if (lineWidth <= maxWidth) { + this._addTextLine(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + break; + } + } + } + else { + // not even one character could fit in the element, abort + break; + } + } + } + else { + // element width is automatically adjusted to max line width + this._addTextLine(line); + currentHeightPx += lineHeightPx; + textWidth = Math.max(textWidth, lineWidth); + if (this._shouldHandleEllipsis(currentHeightPx) && i < max - 1) { + this._tryToAddEllipsisToLastLine(); + } + } + // if element height is fixed, abort if adding one more line would overflow + if (this.textArr[this.textArr.length - 1]) { + this.textArr[this.textArr.length - 1].lastInParagraph = true; + } + if (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx) { + break; + } + } + this.textHeight = fontSize; + // var maxTextWidth = 0; + // for(var j = 0; j < this.textArr.length; j++) { + // maxTextWidth = Math.max(maxTextWidth, this.textArr[j].width); + // } + this.textWidth = textWidth; + } + /** + * whether to handle ellipsis, there are two cases: + * 1. the current line is the last line + * 2. wrap is NONE + * @param {Number} currentHeightPx + * @returns {Boolean} + */ + _shouldHandleEllipsis(currentHeightPx) { + const fontSize = +this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, height = this.attrs.height, fixedHeight = height !== AUTO && height !== undefined, padding = this.padding(), maxHeightPx = height - padding * 2, wrap = this.wrap(), shouldWrap = wrap !== NONE; + return (!shouldWrap || + (fixedHeight && currentHeightPx + lineHeightPx > maxHeightPx)); + } + _tryToAddEllipsisToLastLine() { + const width = this.attrs.width, fixedWidth = width !== AUTO && width !== undefined, padding = this.padding(), maxWidth = width - padding * 2, shouldAddEllipsis = this.ellipsis(); + const lastLine = this.textArr[this.textArr.length - 1]; + if (!lastLine || !shouldAddEllipsis) { + return; + } + if (fixedWidth) { + const haveSpace = this._getTextWidth(lastLine.text + ELLIPSIS) < maxWidth; + if (!haveSpace) { + lastLine.text = lastLine.text.slice(0, lastLine.text.length - 3); + } + } + this.textArr.splice(this.textArr.length - 1, 1); + this._addTextLine(lastLine.text + ELLIPSIS); + } + // for text we can't disable stroke scaling + // if we do, the result will be unexpected + getStrokeScaleEnabled() { + return true; + } + _useBufferCanvas() { + const hasLine = this.textDecoration().indexOf('underline') !== -1 || + this.textDecoration().indexOf('line-through') !== -1; + const hasShadow = this.hasShadow(); + if (hasLine && hasShadow) { + return true; + } + return super._useBufferCanvas(); + } + } + Text.prototype._fillFunc = _fillFunc$1; + Text.prototype._strokeFunc = _strokeFunc$1; + Text.prototype.className = TEXT_UPPER; + Text.prototype._attrsAffectingSize = [ + 'text', + 'fontSize', + 'padding', + 'wrap', + 'lineHeight', + 'letterSpacing', + ]; + _registerNode(Text); + /** + * get/set width of text area, which includes padding. + * @name Konva.Text#width + * @method + * @param {Number} width + * @returns {Number} + * @example + * // get width + * var width = text.width(); + * + * // set width + * text.width(20); + * + * // set to auto + * text.width('auto'); + * text.width() // will return calculated width, and not "auto" + */ + Factory.overWriteSetter(Text, 'width', getNumberOrAutoValidator()); + /** + * get/set the height of the text area, which takes into account multi-line text, line heights, and padding. + * @name Konva.Text#height + * @method + * @param {Number} height + * @returns {Number} + * @example + * // get height + * var height = text.height(); + * + * // set height + * text.height(20); + * + * // set to auto + * text.height('auto'); + * text.height() // will return calculated height, and not "auto" + */ + Factory.overWriteSetter(Text, 'height', getNumberOrAutoValidator()); + /** + * get/set direction + * @name Konva.Text#direction + * @method + * @param {String} direction + * @returns {String} + * @example + * // get direction + * var direction = text.direction(); + * + * // set direction + * text.direction('rtl'); + */ + Factory.addGetterSetter(Text, 'direction', INHERIT); + /** + * get/set font family + * @name Konva.Text#fontFamily + * @method + * @param {String} fontFamily + * @returns {String} + * @example + * // get font family + * var fontFamily = text.fontFamily(); + * + * // set font family + * text.fontFamily('Arial'); + */ + Factory.addGetterSetter(Text, 'fontFamily', 'Arial'); + /** + * get/set font size in pixels + * @name Konva.Text#fontSize + * @method + * @param {Number} fontSize + * @returns {Number} + * @example + * // get font size + * var fontSize = text.fontSize(); + * + * // set font size to 22px + * text.fontSize(22); + */ + Factory.addGetterSetter(Text, 'fontSize', 12, getNumberValidator()); + /** + * get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @name Konva.Text#fontStyle + * @method + * @param {String} fontStyle + * @returns {String} + * @example + * // get font style + * var fontStyle = text.fontStyle(); + * + * // set font style + * text.fontStyle('bold'); + */ + Factory.addGetterSetter(Text, 'fontStyle', NORMAL$1); + /** + * get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default. + * @name Konva.Text#fontVariant + * @method + * @param {String} fontVariant + * @returns {String} + * @example + * // get font variant + * var fontVariant = text.fontVariant(); + * + * // set font variant + * text.fontVariant('small-caps'); + */ + Factory.addGetterSetter(Text, 'fontVariant', NORMAL$1); + /** + * get/set padding + * @name Konva.Text#padding + * @method + * @param {Number} padding + * @returns {Number} + * @example + * // get padding + * var padding = text.padding(); + * + * // set padding to 10 pixels + * text.padding(10); + */ + Factory.addGetterSetter(Text, 'padding', 0, getNumberValidator()); + /** + * get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify' + * @name Konva.Text#align + * @method + * @param {String} align + * @returns {String} + * @example + * // get text align + * var align = text.align(); + * + * // center text + * text.align('center'); + * + * // align text to right + * text.align('right'); + */ + Factory.addGetterSetter(Text, 'align', LEFT); + /** + * get/set vertical align of text. Can be 'top', 'middle', 'bottom'. + * @name Konva.Text#verticalAlign + * @method + * @param {String} verticalAlign + * @returns {String} + * @example + * // get text vertical align + * var verticalAlign = text.verticalAlign(); + * + * // center text + * text.verticalAlign('middle'); + */ + Factory.addGetterSetter(Text, 'verticalAlign', TOP); + /** + * get/set line height. The default is 1. + * @name Konva.Text#lineHeight + * @method + * @param {Number} lineHeight + * @returns {Number} + * @example + * // get line height + * var lineHeight = text.lineHeight(); + * + * // set the line height + * text.lineHeight(2); + */ + Factory.addGetterSetter(Text, 'lineHeight', 1, getNumberValidator()); + /** + * get/set wrap. Can be "word", "char", or "none". Default is "word". + * In "word" wrapping any word still can be wrapped if it can't be placed in the required width + * without breaks. + * @name Konva.Text#wrap + * @method + * @param {String} wrap + * @returns {String} + * @example + * // get wrap + * var wrap = text.wrap(); + * + * // set wrap + * text.wrap('word'); + */ + Factory.addGetterSetter(Text, 'wrap', WORD); + /** + * get/set ellipsis. Can be true or false. Default is false. If ellipses is true, + * Konva will add "..." at the end of the text if it doesn't have enough space to write characters. + * That is possible only when you limit both width and height of the text + * @name Konva.Text#ellipsis + * @method + * @param {Boolean} ellipsis + * @returns {Boolean} + * @example + * // get ellipsis param, returns true or false + * var ellipsis = text.ellipsis(); + * + * // set ellipsis + * text.ellipsis(true); + */ + Factory.addGetterSetter(Text, 'ellipsis', false, getBooleanValidator()); + /** + * set letter spacing property. Default value is 0. + * @name Konva.Text#letterSpacing + * @method + * @param {Number} letterSpacing + */ + Factory.addGetterSetter(Text, 'letterSpacing', 0, getNumberValidator()); + /** + * get/set text + * @name Konva.Text#text + * @method + * @param {String} text + * @returns {String} + * @example + * // get text + * var text = text.text(); + * + * // set text + * text.text('Hello world!'); + */ + Factory.addGetterSetter(Text, 'text', '', getStringValidator()); + /** + * get/set text decoration of a text. Possible values are 'underline', 'line-through' or combination of these values separated by space + * @name Konva.Text#textDecoration + * @method + * @param {String} textDecoration + * @returns {String} + * @example + * // get text decoration + * var textDecoration = text.textDecoration(); + * + * // underline text + * text.textDecoration('underline'); + * + * // strike text + * text.textDecoration('line-through'); + * + * // underline and strike text + * text.textDecoration('underline line-through'); + */ + Factory.addGetterSetter(Text, 'textDecoration', ''); + + const EMPTY_STRING = '', NORMAL = 'normal'; + function _fillFunc(context) { + context.fillText(this.partialText, 0, 0); + } + function _strokeFunc(context) { + context.strokeText(this.partialText, 0, 0); + } + /** + * Path constructor. + * @author Jason Follas + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {String} [config.fontFamily] default is Arial + * @param {Number} [config.fontSize] default is 12 + * @param {String} [config.fontStyle] Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @param {String} [config.fontVariant] can be normal or small-caps. Default is normal + * @param {String} [config.textBaseline] Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging'. Default is middle + * @param {String} config.text + * @param {String} config.data SVG data string + * @param {Function} config.kerningFunc a getter for kerning values for the specified characters + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var kerningPairs = { + * 'A': { + * ' ': -0.05517578125, + * 'T': -0.07421875, + * 'V': -0.07421875 + * } + * 'V': { + * ',': -0.091796875, + * ":": -0.037109375, + * ";": -0.037109375, + * "A": -0.07421875 + * } + * } + * var textpath = new Konva.TextPath({ + * x: 100, + * y: 50, + * fill: '#333', + * fontSize: '24', + * fontFamily: 'Arial', + * text: 'All the world\'s a stage, and all the men and women merely players.', + * data: 'M10,10 C0,0 10,150 100,100 S300,150 400,50', + * kerningFunc(leftChar, rightChar) { + * return kerningPairs.hasOwnProperty(leftChar) ? pairs[leftChar][rightChar] || 0 : 0 + * } + * }); + */ + class TextPath extends Shape { + constructor(config) { + // call super constructor + super(config); + this.dummyCanvas = Util.createCanvasElement(); + this.dataArray = []; + this._readDataAttribute(); + this.on('dataChange.konva', function () { + this._readDataAttribute(); + this._setTextData(); + }); + // update text data for certain attr changes + this.on('textChange.konva alignChange.konva letterSpacingChange.konva kerningFuncChange.konva fontSizeChange.konva fontFamilyChange.konva', this._setTextData); + this._setTextData(); + } + _getTextPathLength() { + return Path.getPathLength(this.dataArray); + } + _getPointAtLength(length) { + // if path is not defined yet, do nothing + if (!this.attrs.data) { + return null; + } + const totalLength = this.pathLength; + // -1px for rounding of the last symbol + if (length - 1 > totalLength) { + return null; + } + return Path.getPointAtLengthOfDataArray(length, this.dataArray); + } + _readDataAttribute() { + this.dataArray = Path.parsePathData(this.attrs.data); + this.pathLength = this._getTextPathLength(); + } + _sceneFunc(context) { + context.setAttr('font', this._getContextFont()); + context.setAttr('textBaseline', this.textBaseline()); + context.setAttr('textAlign', 'left'); + context.save(); + const textDecoration = this.textDecoration(); + const fill = this.fill(); + const fontSize = this.fontSize(); + const glyphInfo = this.glyphInfo; + if (textDecoration === 'underline') { + context.beginPath(); + } + for (let i = 0; i < glyphInfo.length; i++) { + context.save(); + const p0 = glyphInfo[i].p0; + context.translate(p0.x, p0.y); + context.rotate(glyphInfo[i].rotation); + this.partialText = glyphInfo[i].text; + context.fillStrokeShape(this); + if (textDecoration === 'underline') { + if (i === 0) { + context.moveTo(0, fontSize / 2 + 1); + } + context.lineTo(fontSize, fontSize / 2 + 1); + } + context.restore(); + //// To assist with debugging visually, uncomment following + // + // if (i % 2) context.strokeStyle = 'cyan'; + // else context.strokeStyle = 'green'; + // var p1 = glyphInfo[i].p1; + // context.moveTo(p0.x, p0.y); + // context.lineTo(p1.x, p1.y); + // context.stroke(); + } + if (textDecoration === 'underline') { + context.strokeStyle = fill; + context.lineWidth = fontSize / 20; + context.stroke(); + } + context.restore(); + } + _hitFunc(context) { + context.beginPath(); + const glyphInfo = this.glyphInfo; + if (glyphInfo.length >= 1) { + const p0 = glyphInfo[0].p0; + context.moveTo(p0.x, p0.y); + } + for (let i = 0; i < glyphInfo.length; i++) { + const p1 = glyphInfo[i].p1; + context.lineTo(p1.x, p1.y); + } + context.setAttr('lineWidth', this.fontSize()); + context.setAttr('strokeStyle', this.colorKey); + context.stroke(); + } + /** + * get text width in pixels + * @method + * @name Konva.TextPath#getTextWidth + */ + getTextWidth() { + return this.textWidth; + } + getTextHeight() { + Util.warn('text.getTextHeight() method is deprecated. Use text.height() - for full height and text.fontSize() - for one line height.'); + return this.textHeight; + } + setText(text) { + return Text.prototype.setText.call(this, text); + } + _getContextFont() { + return Text.prototype._getContextFont.call(this); + } + _getTextSize(text) { + const dummyCanvas = this.dummyCanvas; + const _context = dummyCanvas.getContext('2d'); + _context.save(); + _context.font = this._getContextFont(); + const metrics = _context.measureText(text); + _context.restore(); + return { + width: metrics.width, + height: parseInt(`${this.fontSize()}`, 10), + }; + } + _setTextData() { + const { width, height } = this._getTextSize(this.attrs.text); + this.textWidth = width; + this.textHeight = height; + this.glyphInfo = []; + if (!this.attrs.data) { + return null; + } + const letterSpacing = this.letterSpacing(); + const align = this.align(); + const kerningFunc = this.kerningFunc(); + // defines the width of the text on a straight line + const textWidth = Math.max(this.textWidth + ((this.attrs.text || '').length - 1) * letterSpacing, 0); + let offset = 0; + if (align === 'center') { + offset = Math.max(0, this.pathLength / 2 - textWidth / 2); + } + if (align === 'right') { + offset = Math.max(0, this.pathLength - textWidth); + } + const charArr = stringToArray(this.text()); + // Algorithm for calculating glyph positions: + // 1. Get the begging point of the glyph on the path using the offsetToGlyph, + // 2. Get the ending point of the glyph on the path using the offsetToGlyph plus glyph width, + // 3. Calculate the rotation, width, and midpoint of the glyph using the start and end points, + // 4. Add glyph width to the offsetToGlyph and repeat + let offsetToGlyph = offset; + for (let i = 0; i < charArr.length; i++) { + const charStartPoint = this._getPointAtLength(offsetToGlyph); + if (!charStartPoint) + return; + let glyphWidth = this._getTextSize(charArr[i]).width + letterSpacing; + if (charArr[i] === ' ' && align === 'justify') { + const numberOfSpaces = this.text().split(' ').length - 1; + glyphWidth += (this.pathLength - textWidth) / numberOfSpaces; + } + const charEndPoint = this._getPointAtLength(offsetToGlyph + glyphWidth); + if (!charEndPoint) + return; + const width = Path.getLineLength(charStartPoint.x, charStartPoint.y, charEndPoint.x, charEndPoint.y); + let kern = 0; + if (kerningFunc) { + try { + // getKerning is a user provided getter. Make sure it never breaks our logic + kern = kerningFunc(charArr[i - 1], charArr[i]) * this.fontSize(); + } + catch (e) { + kern = 0; + } + } + charStartPoint.x += kern; + charEndPoint.x += kern; + this.textWidth += kern; + const midpoint = Path.getPointOnLine(kern + width / 2.0, charStartPoint.x, charStartPoint.y, charEndPoint.x, charEndPoint.y); + const rotation = Math.atan2(charEndPoint.y - charStartPoint.y, charEndPoint.x - charStartPoint.x); + this.glyphInfo.push({ + transposeX: midpoint.x, + transposeY: midpoint.y, + text: charArr[i], + rotation: rotation, + p0: charStartPoint, + p1: charEndPoint, + }); + offsetToGlyph += glyphWidth; + } + } + getSelfRect() { + if (!this.glyphInfo.length) { + return { + x: 0, + y: 0, + width: 0, + height: 0, + }; + } + const points = []; + this.glyphInfo.forEach(function (info) { + points.push(info.p0.x); + points.push(info.p0.y); + points.push(info.p1.x); + points.push(info.p1.y); + }); + let minX = points[0] || 0; + let maxX = points[0] || 0; + let minY = points[1] || 0; + let maxY = points[1] || 0; + let x, y; + for (let i = 0; i < points.length / 2; i++) { + x = points[i * 2]; + y = points[i * 2 + 1]; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + const fontSize = this.fontSize(); + return { + x: minX - fontSize / 2, + y: minY - fontSize / 2, + width: maxX - minX + fontSize, + height: maxY - minY + fontSize, + }; + } + destroy() { + Util.releaseCanvas(this.dummyCanvas); + return super.destroy(); + } + } + TextPath.prototype._fillFunc = _fillFunc; + TextPath.prototype._strokeFunc = _strokeFunc; + TextPath.prototype._fillFuncHit = _fillFunc; + TextPath.prototype._strokeFuncHit = _strokeFunc; + TextPath.prototype.className = 'TextPath'; + TextPath.prototype._attrsAffectingSize = ['text', 'fontSize', 'data']; + _registerNode(TextPath); + /** + * get/set SVG path data string. This method + * also automatically parses the data string + * into a data array. Currently supported SVG data: + * M, m, L, l, H, h, V, v, Q, q, T, t, C, c, S, s, A, a, Z, z + * @name Konva.TextPath#data + * @method + * @param {String} data svg path string + * @returns {String} + * @example + * // get data + * var data = shape.data(); + * + * // set data + * shape.data('M200,100h100v50z'); + */ + Factory.addGetterSetter(TextPath, 'data'); + /** + * get/set font family + * @name Konva.TextPath#fontFamily + * @method + * @param {String} fontFamily + * @returns {String} + * @example + * // get font family + * var fontFamily = shape.fontFamily(); + * + * // set font family + * shape.fontFamily('Arial'); + */ + Factory.addGetterSetter(TextPath, 'fontFamily', 'Arial'); + /** + * get/set font size in pixels + * @name Konva.TextPath#fontSize + * @method + * @param {Number} fontSize + * @returns {Number} + * @example + * // get font size + * var fontSize = shape.fontSize(); + * + * // set font size to 22px + * shape.fontSize(22); + */ + Factory.addGetterSetter(TextPath, 'fontSize', 12, getNumberValidator()); + /** + * get/set font style. Can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. + * @name Konva.TextPath#fontStyle + * @method + * @param {String} fontStyle + * @returns {String} + * @example + * // get font style + * var fontStyle = shape.fontStyle(); + * + * // set font style + * shape.fontStyle('bold'); + */ + Factory.addGetterSetter(TextPath, 'fontStyle', NORMAL); + /** + * get/set horizontal align of text. Can be 'left', 'center', 'right' or 'justify' + * @name Konva.TextPath#align + * @method + * @param {String} align + * @returns {String} + * @example + * // get text align + * var align = text.align(); + * + * // center text + * text.align('center'); + * + * // align text to right + * text.align('right'); + */ + Factory.addGetterSetter(TextPath, 'align', 'left'); + /** + * get/set letter spacing. The default is 0. + * @name Konva.TextPath#letterSpacing + * @method + * @param {Number} letterSpacing + * @returns {Number} + * @example + * // get letter spacing value + * var letterSpacing = shape.letterSpacing(); + * + * // set the letter spacing value + * shape.letterSpacing(2); + */ + Factory.addGetterSetter(TextPath, 'letterSpacing', 0, getNumberValidator()); + /** + * get/set text baseline. The default is 'middle'. Can be 'top', 'bottom', 'middle', 'alphabetic', 'hanging' + * @name Konva.TextPath#textBaseline + * @method + * @param {String} textBaseline + * @returns {String} + * @example + * // get current text baseline + * var textBaseline = shape.textBaseline(); + * + * // set new text baseline + * shape.textBaseline('top'); + */ + Factory.addGetterSetter(TextPath, 'textBaseline', 'middle'); + /** + * get/set font variant. Can be 'normal' or 'small-caps'. 'normal' is the default. + * @name Konva.TextPath#fontVariant + * @method + * @param {String} fontVariant + * @returns {String} + * @example + * // get font variant + * var fontVariant = shape.fontVariant(); + * + * // set font variant + * shape.fontVariant('small-caps'); + */ + Factory.addGetterSetter(TextPath, 'fontVariant', NORMAL); + /** + * get/set text + * @name Konva.TextPath#getText + * @method + * @param {String} text + * @returns {String} + * @example + * // get text + * var text = text.text(); + * + * // set text + * text.text('Hello world!'); + */ + Factory.addGetterSetter(TextPath, 'text', EMPTY_STRING); + /** + * get/set text decoration of a text. Can be '' or 'underline'. + * @name Konva.TextPath#textDecoration + * @method + * @param {String} textDecoration + * @returns {String} + * @example + * // get text decoration + * var textDecoration = shape.textDecoration(); + * + * // underline text + * shape.textDecoration('underline'); + */ + Factory.addGetterSetter(TextPath, 'textDecoration', ''); + /** + * get/set kerning function. + * @name Konva.TextPath#kerningFunc + * @method + * @param {String} kerningFunc + * @returns {String} + * @example + * // get text decoration + * var kerningFunc = text.kerningFunc(); + * + * // center text + * text.kerningFunc(function(leftChar, rightChar) { + * return 1; + * }); + */ + Factory.addGetterSetter(TextPath, 'kerningFunc', undefined); + + const EVENTS_NAME = 'tr-konva'; + const ATTR_CHANGE_LIST = [ + 'resizeEnabledChange', + 'rotateAnchorOffsetChange', + 'rotateEnabledChange', + 'enabledAnchorsChange', + 'anchorSizeChange', + 'borderEnabledChange', + 'borderStrokeChange', + 'borderStrokeWidthChange', + 'borderDashChange', + 'anchorStrokeChange', + 'anchorStrokeWidthChange', + 'anchorFillChange', + 'anchorCornerRadiusChange', + 'ignoreStrokeChange', + 'anchorStyleFuncChange', + ] + .map((e) => e + `.${EVENTS_NAME}`) + .join(' '); + const NODES_RECT = 'nodesRect'; + const TRANSFORM_CHANGE_STR = [ + 'widthChange', + 'heightChange', + 'scaleXChange', + 'scaleYChange', + 'skewXChange', + 'skewYChange', + 'rotationChange', + 'offsetXChange', + 'offsetYChange', + 'transformsEnabledChange', + 'strokeWidthChange', + ]; + const ANGLES = { + 'top-left': -45, + 'top-center': 0, + 'top-right': 45, + 'middle-right': -90, + 'middle-left': 90, + 'bottom-left': -135, + 'bottom-center': 180, + 'bottom-right': 135, + }; + const TOUCH_DEVICE = 'ontouchstart' in Konva$2._global; + function getCursor(anchorName, rad, rotateCursor) { + if (anchorName === 'rotater') { + return rotateCursor; + } + rad += Util.degToRad(ANGLES[anchorName] || 0); + const angle = ((Util.radToDeg(rad) % 360) + 360) % 360; + if (Util._inRange(angle, 315 + 22.5, 360) || Util._inRange(angle, 0, 22.5)) { + // TOP + return 'ns-resize'; + } + else if (Util._inRange(angle, 45 - 22.5, 45 + 22.5)) { + // TOP - RIGHT + return 'nesw-resize'; + } + else if (Util._inRange(angle, 90 - 22.5, 90 + 22.5)) { + // RIGHT + return 'ew-resize'; + } + else if (Util._inRange(angle, 135 - 22.5, 135 + 22.5)) { + // BOTTOM - RIGHT + return 'nwse-resize'; + } + else if (Util._inRange(angle, 180 - 22.5, 180 + 22.5)) { + // BOTTOM + return 'ns-resize'; + } + else if (Util._inRange(angle, 225 - 22.5, 225 + 22.5)) { + // BOTTOM - LEFT + return 'nesw-resize'; + } + else if (Util._inRange(angle, 270 - 22.5, 270 + 22.5)) { + // RIGHT + return 'ew-resize'; + } + else if (Util._inRange(angle, 315 - 22.5, 315 + 22.5)) { + // BOTTOM - RIGHT + return 'nwse-resize'; + } + else { + // how can we can there? + Util.error('Transformer has unknown angle for cursor detection: ' + angle); + return 'pointer'; + } + } + const ANCHORS_NAMES = [ + 'top-left', + 'top-center', + 'top-right', + 'middle-right', + 'middle-left', + 'bottom-left', + 'bottom-center', + 'bottom-right', + ]; + function getCenter(shape) { + return { + x: shape.x + + (shape.width / 2) * Math.cos(shape.rotation) + + (shape.height / 2) * Math.sin(-shape.rotation), + y: shape.y + + (shape.height / 2) * Math.cos(shape.rotation) + + (shape.width / 2) * Math.sin(shape.rotation), + }; + } + function rotateAroundPoint(shape, angleRad, point) { + const x = point.x + + (shape.x - point.x) * Math.cos(angleRad) - + (shape.y - point.y) * Math.sin(angleRad); + const y = point.y + + (shape.x - point.x) * Math.sin(angleRad) + + (shape.y - point.y) * Math.cos(angleRad); + return { + ...shape, + rotation: shape.rotation + angleRad, + x, + y, + }; + } + function rotateAroundCenter(shape, deltaRad) { + const center = getCenter(shape); + return rotateAroundPoint(shape, deltaRad, center); + } + function getSnap(snaps, newRotationRad, tol) { + let snapped = newRotationRad; + for (let i = 0; i < snaps.length; i++) { + const angle = Konva$2.getAngle(snaps[i]); + const absDiff = Math.abs(angle - newRotationRad) % (Math.PI * 2); + const dif = Math.min(absDiff, Math.PI * 2 - absDiff); + if (dif < tol) { + snapped = angle; + } + } + return snapped; + } + let activeTransformersCount = 0; + /** + * Transformer constructor. Transformer is a special type of group that allow you transform Konva + * primitives and shapes. Transforming tool is not changing `width` and `height` properties of nodes + * when you resize them. Instead it changes `scaleX` and `scaleY` properties. + * @constructor + * @memberof Konva + * @param {Object} config + * @param {Boolean} [config.resizeEnabled] Default is true + * @param {Boolean} [config.rotateEnabled] Default is true + * @param {Boolean} [config.rotateLineVisible] Default is true + * @param {Array} [config.rotationSnaps] Array of angles for rotation snaps. Default is [] + * @param {Number} [config.rotationSnapTolerance] Snapping tolerance. If closer than this it will snap. Default is 5 + * @param {Number} [config.rotateAnchorOffset] Default is 50 + * @param {String} [config.rotateAnchorCursor] Default is crosshair + * @param {Number} [config.padding] Default is 0 + * @param {Boolean} [config.borderEnabled] Should we draw border? Default is true + * @param {String} [config.borderStroke] Border stroke color + * @param {Number} [config.borderStrokeWidth] Border stroke size + * @param {Array} [config.borderDash] Array for border dash. + * @param {String} [config.anchorFill] Anchor fill color + * @param {String} [config.anchorStroke] Anchor stroke color + * @param {String} [config.anchorCornerRadius] Anchor corner radius + * @param {Number} [config.anchorStrokeWidth] Anchor stroke size + * @param {Number} [config.anchorSize] Default is 10 + * @param {Boolean} [config.keepRatio] Should we keep ratio when we are moving edges? Default is true + * @param {String} [config.shiftBehavior] How does transformer react on shift key press when we are moving edges? Default is 'default' + * @param {Boolean} [config.centeredScaling] Should we resize relative to node's center? Default is false + * @param {Array} [config.enabledAnchors] Array of names of enabled handles + * @param {Boolean} [config.flipEnabled] Can we flip/mirror shape on transform?. True by default + * @param {Function} [config.boundBoxFunc] Bounding box function + * @param {Function} [config.ignoreStroke] Should we ignore stroke size? Default is false + * @param {Boolean} [config.useSingleNodeRotation] When just one node attached, should we use its rotation for transformer? + * @param {Boolean} [config.shouldOverdrawWholeArea] Should we fill whole transformer area with fake transparent shape to enable dragging from empty spaces? + * @example + * var transformer = new Konva.Transformer({ + * nodes: [rectangle], + * rotateAnchorOffset: 60, + * enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'] + * }); + * layer.add(transformer); + */ + class Transformer extends Group { + constructor(config) { + // call super constructor + super(config); + this._movingAnchorName = null; + this._transforming = false; + this._createElements(); + // bindings + this._handleMouseMove = this._handleMouseMove.bind(this); + this._handleMouseUp = this._handleMouseUp.bind(this); + this.update = this.update.bind(this); + // update transformer data for certain attr changes + this.on(ATTR_CHANGE_LIST, this.update); + if (this.getNode()) { + this.update(); + } + } + /** + * alias to `tr.nodes([shape])`/ This method is deprecated and will be removed soon. + * @method + * @name Konva.Transformer#attachTo + * @returns {Konva.Transformer} + * @example + * transformer.attachTo(shape); + */ + attachTo(node) { + this.setNode(node); + return this; + } + setNode(node) { + Util.warn('tr.setNode(shape), tr.node(shape) and tr.attachTo(shape) methods are deprecated. Please use tr.nodes(nodesArray) instead.'); + return this.setNodes([node]); + } + getNode() { + return this._nodes && this._nodes[0]; + } + _getEventNamespace() { + return EVENTS_NAME + this._id; + } + setNodes(nodes = []) { + if (this._nodes && this._nodes.length) { + this.detach(); + } + const filteredNodes = nodes.filter((node) => { + // check if ancestor of the transformer + if (node.isAncestorOf(this)) { + Util.error('Konva.Transformer cannot be an a child of the node you are trying to attach'); + return false; + } + return true; + }); + this._nodes = nodes = filteredNodes; + if (nodes.length === 1 && this.useSingleNodeRotation()) { + this.rotation(nodes[0].getAbsoluteRotation()); + } + else { + this.rotation(0); + } + this._nodes.forEach((node) => { + const onChange = () => { + if (this.nodes().length === 1 && this.useSingleNodeRotation()) { + this.rotation(this.nodes()[0].getAbsoluteRotation()); + } + this._resetTransformCache(); + if (!this._transforming && !this.isDragging()) { + this.update(); + } + }; + if (node._attrsAffectingSize.length) { + const additionalEvents = node._attrsAffectingSize + .map((prop) => prop + 'Change.' + this._getEventNamespace()) + .join(' '); + node.on(additionalEvents, onChange); + } + node.on(TRANSFORM_CHANGE_STR.map((e) => e + `.${this._getEventNamespace()}`).join(' '), onChange); + node.on(`absoluteTransformChange.${this._getEventNamespace()}`, onChange); + this._proxyDrag(node); + }); + this._resetTransformCache(); + // we may need it if we set node in initial props + // so elements are not defined yet + const elementsCreated = !!this.findOne('.top-left'); + if (elementsCreated) { + this.update(); + } + return this; + } + _proxyDrag(node) { + let lastPos; + node.on(`dragstart.${this._getEventNamespace()}`, (e) => { + lastPos = node.getAbsolutePosition(); + // actual dragging of Transformer doesn't make sense + // but we need to make sure it also has all drag events + if (!this.isDragging() && node !== this.findOne('.back')) { + this.startDrag(e, false); + } + }); + node.on(`dragmove.${this._getEventNamespace()}`, (e) => { + if (!lastPos) { + return; + } + const abs = node.getAbsolutePosition(); + const dx = abs.x - lastPos.x; + const dy = abs.y - lastPos.y; + this.nodes().forEach((otherNode) => { + if (otherNode === node) { + return; + } + if (otherNode.isDragging()) { + return; + } + const otherAbs = otherNode.getAbsolutePosition(); + otherNode.setAbsolutePosition({ + x: otherAbs.x + dx, + y: otherAbs.y + dy, + }); + otherNode.startDrag(e); + }); + lastPos = null; + }); + } + getNodes() { + return this._nodes || []; + } + /** + * return the name of current active anchor + * @method + * @name Konva.Transformer#getActiveAnchor + * @returns {String | Null} + * @example + * transformer.getActiveAnchor(); + */ + getActiveAnchor() { + return this._movingAnchorName; + } + /** + * detach transformer from an attached node + * @method + * @name Konva.Transformer#detach + * @returns {Konva.Transformer} + * @example + * transformer.detach(); + */ + detach() { + // remove events + if (this._nodes) { + this._nodes.forEach((node) => { + node.off('.' + this._getEventNamespace()); + }); + } + this._nodes = []; + this._resetTransformCache(); + } + /** + * bind events to the Transformer. You can use events: `transform`, `transformstart`, `transformend`, `dragstart`, `dragmove`, `dragend` + * @method + * @name Konva.Transformer#on + * @param {String} evtStr e.g. 'transform' + * @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.Transformer} + * @example + * // add click listener + * tr.on('transformstart', function() { + * console.log('transform started'); + * }); + */ + _resetTransformCache() { + this._clearCache(NODES_RECT); + this._clearCache('transform'); + this._clearSelfAndDescendantCache('absoluteTransform'); + } + _getNodeRect() { + return this._getCache(NODES_RECT, this.__getNodeRect); + } + // return absolute rotated bounding rectangle + __getNodeShape(node, rot = this.rotation(), relative) { + const rect = node.getClientRect({ + skipTransform: true, + skipShadow: true, + skipStroke: this.ignoreStroke(), + }); + const absScale = node.getAbsoluteScale(relative); + const absPos = node.getAbsolutePosition(relative); + const dx = rect.x * absScale.x - node.offsetX() * absScale.x; + const dy = rect.y * absScale.y - node.offsetY() * absScale.y; + const rotation = (Konva$2.getAngle(node.getAbsoluteRotation()) + Math.PI * 2) % + (Math.PI * 2); + const box = { + x: absPos.x + dx * Math.cos(rotation) + dy * Math.sin(-rotation), + y: absPos.y + dy * Math.cos(rotation) + dx * Math.sin(rotation), + width: rect.width * absScale.x, + height: rect.height * absScale.y, + rotation: rotation, + }; + return rotateAroundPoint(box, -Konva$2.getAngle(rot), { + x: 0, + y: 0, + }); + } + // returns box + rotation of all shapes + __getNodeRect() { + const node = this.getNode(); + if (!node) { + return { + x: -1e8, + y: -1e8, + width: 0, + height: 0, + rotation: 0, + }; + } + const totalPoints = []; + this.nodes().map((node) => { + const box = node.getClientRect({ + skipTransform: true, + skipShadow: true, + skipStroke: this.ignoreStroke(), + }); + const points = [ + { x: box.x, y: box.y }, + { x: box.x + box.width, y: box.y }, + { x: box.x + box.width, y: box.y + box.height }, + { x: box.x, y: box.y + box.height }, + ]; + const trans = node.getAbsoluteTransform(); + points.forEach(function (point) { + const transformed = trans.point(point); + totalPoints.push(transformed); + }); + }); + const tr = new Transform(); + tr.rotate(-Konva$2.getAngle(this.rotation())); + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + totalPoints.forEach(function (point) { + const transformed = tr.point(point); + if (minX === undefined) { + minX = maxX = transformed.x; + minY = maxY = transformed.y; + } + minX = Math.min(minX, transformed.x); + minY = Math.min(minY, transformed.y); + maxX = Math.max(maxX, transformed.x); + maxY = Math.max(maxY, transformed.y); + }); + tr.invert(); + const p = tr.point({ x: minX, y: minY }); + return { + x: p.x, + y: p.y, + width: maxX - minX, + height: maxY - minY, + rotation: Konva$2.getAngle(this.rotation()), + }; + // const shapes = this.nodes().map(node => { + // return this.__getNodeShape(node); + // }); + // const box = getShapesRect(shapes); + // return rotateAroundPoint(box, Konva.getAngle(this.rotation()), { + // x: 0, + // y: 0 + // }); + } + getX() { + return this._getNodeRect().x; + } + getY() { + return this._getNodeRect().y; + } + getWidth() { + return this._getNodeRect().width; + } + getHeight() { + return this._getNodeRect().height; + } + _createElements() { + this._createBack(); + ANCHORS_NAMES.forEach((name) => { + this._createAnchor(name); + }); + this._createAnchor('rotater'); + } + _createAnchor(name) { + const anchor = new Rect({ + stroke: 'rgb(0, 161, 255)', + fill: 'white', + strokeWidth: 1, + name: name + ' _anchor', + dragDistance: 0, + // make it draggable, + // so activating the anchor will not start drag&drop of any parent + draggable: true, + hitStrokeWidth: TOUCH_DEVICE ? 10 : 'auto', + }); + const self = this; + anchor.on('mousedown touchstart', function (e) { + self._handleMouseDown(e); + }); + anchor.on('dragstart', (e) => { + anchor.stopDrag(); + e.cancelBubble = true; + }); + anchor.on('dragend', (e) => { + e.cancelBubble = true; + }); + // add hover styling + anchor.on('mouseenter', () => { + const rad = Konva$2.getAngle(this.rotation()); + const rotateCursor = this.rotateAnchorCursor(); + const cursor = getCursor(name, rad, rotateCursor); + anchor.getStage().content && + (anchor.getStage().content.style.cursor = cursor); + this._cursorChange = true; + }); + anchor.on('mouseout', () => { + anchor.getStage().content && + (anchor.getStage().content.style.cursor = ''); + this._cursorChange = false; + }); + this.add(anchor); + } + _createBack() { + const back = new Shape({ + name: 'back', + width: 0, + height: 0, + draggable: true, + sceneFunc(ctx, shape) { + const tr = shape.getParent(); + const padding = tr.padding(); + ctx.beginPath(); + ctx.rect(-padding, -padding, shape.width() + padding * 2, shape.height() + padding * 2); + ctx.moveTo(shape.width() / 2, -padding); + if (tr.rotateEnabled() && tr.rotateLineVisible()) { + ctx.lineTo(shape.width() / 2, -tr.rotateAnchorOffset() * Util._sign(shape.height()) - padding); + } + ctx.fillStrokeShape(shape); + }, + hitFunc: (ctx, shape) => { + if (!this.shouldOverdrawWholeArea()) { + return; + } + const padding = this.padding(); + ctx.beginPath(); + ctx.rect(-padding, -padding, shape.width() + padding * 2, shape.height() + padding * 2); + ctx.fillStrokeShape(shape); + }, + }); + this.add(back); + this._proxyDrag(back); + // do not bubble drag from the back shape + // because we already "drag" whole transformer + // so we don't want to trigger drag twice on transformer + back.on('dragstart', (e) => { + e.cancelBubble = true; + }); + back.on('dragmove', (e) => { + e.cancelBubble = true; + }); + back.on('dragend', (e) => { + e.cancelBubble = true; + }); + // force self update when we drag with shouldOverDrawWholeArea setting + this.on('dragmove', (e) => { + this.update(); + }); + } + _handleMouseDown(e) { + // do nothing if we already transforming + // that is possible to trigger with multitouch + if (this._transforming) { + return; + } + this._movingAnchorName = e.target.name().split(' ')[0]; + const attrs = this._getNodeRect(); + const width = attrs.width; + const height = attrs.height; + const hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); + this.sin = Math.abs(height / hypotenuse); + this.cos = Math.abs(width / hypotenuse); + if (typeof window !== 'undefined') { + window.addEventListener('mousemove', this._handleMouseMove); + window.addEventListener('touchmove', this._handleMouseMove); + window.addEventListener('mouseup', this._handleMouseUp, true); + window.addEventListener('touchend', this._handleMouseUp, true); + } + this._transforming = true; + const ap = e.target.getAbsolutePosition(); + const pos = e.target.getStage().getPointerPosition(); + this._anchorDragOffset = { + x: pos.x - ap.x, + y: pos.y - ap.y, + }; + activeTransformersCount++; + this._fire('transformstart', { evt: e.evt, target: this.getNode() }); + this._nodes.forEach((target) => { + target._fire('transformstart', { evt: e.evt, target }); + }); + } + _handleMouseMove(e) { + let x, y, newHypotenuse; + const anchorNode = this.findOne('.' + this._movingAnchorName); + const stage = anchorNode.getStage(); + stage.setPointersPositions(e); + const pp = stage.getPointerPosition(); + let newNodePos = { + x: pp.x - this._anchorDragOffset.x, + y: pp.y - this._anchorDragOffset.y, + }; + const oldAbs = anchorNode.getAbsolutePosition(); + if (this.anchorDragBoundFunc()) { + newNodePos = this.anchorDragBoundFunc()(oldAbs, newNodePos, e); + } + anchorNode.setAbsolutePosition(newNodePos); + const newAbs = anchorNode.getAbsolutePosition(); + // console.log(oldAbs, newNodePos, newAbs); + if (oldAbs.x === newAbs.x && oldAbs.y === newAbs.y) { + return; + } + // rotater is working very differently, so do it first + if (this._movingAnchorName === 'rotater') { + const attrs = this._getNodeRect(); + x = anchorNode.x() - attrs.width / 2; + y = -anchorNode.y() + attrs.height / 2; + // hor angle is changed? + let delta = Math.atan2(-y, x) + Math.PI / 2; + if (attrs.height < 0) { + delta -= Math.PI; + } + const oldRotation = Konva$2.getAngle(this.rotation()); + const newRotation = oldRotation + delta; + const tol = Konva$2.getAngle(this.rotationSnapTolerance()); + const snappedRot = getSnap(this.rotationSnaps(), newRotation, tol); + const diff = snappedRot - attrs.rotation; + const shape = rotateAroundCenter(attrs, diff); + this._fitNodesInto(shape, e); + return; + } + const shiftBehavior = this.shiftBehavior(); + let keepProportion; + if (shiftBehavior === 'inverted') { + keepProportion = this.keepRatio() && !e.shiftKey; + } + else if (shiftBehavior === 'none') { + keepProportion = this.keepRatio(); + } + else { + keepProportion = this.keepRatio() || e.shiftKey; + } + var centeredScaling = this.centeredScaling() || e.altKey; + if (this._movingAnchorName === 'top-left') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.bottom-right').x(), + y: this.findOne('.bottom-right').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(comparePoint.x - anchorNode.x(), 2) + + Math.pow(comparePoint.y - anchorNode.y(), 2)); + var reverseX = this.findOne('.top-left').x() > comparePoint.x ? -1 : 1; + var reverseY = this.findOne('.top-left').y() > comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + this.findOne('.top-left').x(comparePoint.x - x); + this.findOne('.top-left').y(comparePoint.y - y); + } + } + else if (this._movingAnchorName === 'top-center') { + this.findOne('.top-left').y(anchorNode.y()); + } + else if (this._movingAnchorName === 'top-right') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.bottom-left').x(), + y: this.findOne('.bottom-left').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(anchorNode.x() - comparePoint.x, 2) + + Math.pow(comparePoint.y - anchorNode.y(), 2)); + var reverseX = this.findOne('.top-right').x() < comparePoint.x ? -1 : 1; + var reverseY = this.findOne('.top-right').y() > comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + this.findOne('.top-right').x(comparePoint.x + x); + this.findOne('.top-right').y(comparePoint.y - y); + } + var pos = anchorNode.position(); + this.findOne('.top-left').y(pos.y); + this.findOne('.bottom-right').x(pos.x); + } + else if (this._movingAnchorName === 'middle-left') { + this.findOne('.top-left').x(anchorNode.x()); + } + else if (this._movingAnchorName === 'middle-right') { + this.findOne('.bottom-right').x(anchorNode.x()); + } + else if (this._movingAnchorName === 'bottom-left') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.top-right').x(), + y: this.findOne('.top-right').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(comparePoint.x - anchorNode.x(), 2) + + Math.pow(anchorNode.y() - comparePoint.y, 2)); + var reverseX = comparePoint.x < anchorNode.x() ? -1 : 1; + var reverseY = anchorNode.y() < comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + anchorNode.x(comparePoint.x - x); + anchorNode.y(comparePoint.y + y); + } + pos = anchorNode.position(); + this.findOne('.top-left').x(pos.x); + this.findOne('.bottom-right').y(pos.y); + } + else if (this._movingAnchorName === 'bottom-center') { + this.findOne('.bottom-right').y(anchorNode.y()); + } + else if (this._movingAnchorName === 'bottom-right') { + if (keepProportion) { + var comparePoint = centeredScaling + ? { + x: this.width() / 2, + y: this.height() / 2, + } + : { + x: this.findOne('.top-left').x(), + y: this.findOne('.top-left').y(), + }; + newHypotenuse = Math.sqrt(Math.pow(anchorNode.x() - comparePoint.x, 2) + + Math.pow(anchorNode.y() - comparePoint.y, 2)); + var reverseX = this.findOne('.bottom-right').x() < comparePoint.x ? -1 : 1; + var reverseY = this.findOne('.bottom-right').y() < comparePoint.y ? -1 : 1; + x = newHypotenuse * this.cos * reverseX; + y = newHypotenuse * this.sin * reverseY; + this.findOne('.bottom-right').x(comparePoint.x + x); + this.findOne('.bottom-right').y(comparePoint.y + y); + } + } + else { + console.error(new Error('Wrong position argument of selection resizer: ' + + this._movingAnchorName)); + } + var centeredScaling = this.centeredScaling() || e.altKey; + if (centeredScaling) { + const topLeft = this.findOne('.top-left'); + const bottomRight = this.findOne('.bottom-right'); + const topOffsetX = topLeft.x(); + const topOffsetY = topLeft.y(); + const bottomOffsetX = this.getWidth() - bottomRight.x(); + const bottomOffsetY = this.getHeight() - bottomRight.y(); + bottomRight.move({ + x: -topOffsetX, + y: -topOffsetY, + }); + topLeft.move({ + x: bottomOffsetX, + y: bottomOffsetY, + }); + } + const absPos = this.findOne('.top-left').getAbsolutePosition(); + x = absPos.x; + y = absPos.y; + const width = this.findOne('.bottom-right').x() - this.findOne('.top-left').x(); + const height = this.findOne('.bottom-right').y() - this.findOne('.top-left').y(); + this._fitNodesInto({ + x: x, + y: y, + width: width, + height: height, + rotation: Konva$2.getAngle(this.rotation()), + }, e); + } + _handleMouseUp(e) { + this._removeEvents(e); + } + getAbsoluteTransform() { + return this.getTransform(); + } + _removeEvents(e) { + var _a; + if (this._transforming) { + this._transforming = false; + if (typeof window !== 'undefined') { + window.removeEventListener('mousemove', this._handleMouseMove); + window.removeEventListener('touchmove', this._handleMouseMove); + window.removeEventListener('mouseup', this._handleMouseUp, true); + window.removeEventListener('touchend', this._handleMouseUp, true); + } + const node = this.getNode(); + activeTransformersCount--; + this._fire('transformend', { evt: e, target: node }); + // redraw layer to restore hit graph + (_a = this.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + if (node) { + this._nodes.forEach((target) => { + var _a; + target._fire('transformend', { evt: e, target }); + // redraw layer to restore hit graph + (_a = target.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + }); + } + this._movingAnchorName = null; + } + } + _fitNodesInto(newAttrs, evt) { + const oldAttrs = this._getNodeRect(); + const minSize = 1; + if (Util._inRange(newAttrs.width, -this.padding() * 2 - minSize, minSize)) { + this.update(); + return; + } + if (Util._inRange(newAttrs.height, -this.padding() * 2 - minSize, minSize)) { + this.update(); + return; + } + const t = new Transform(); + t.rotate(Konva$2.getAngle(this.rotation())); + if (this._movingAnchorName && + newAttrs.width < 0 && + this._movingAnchorName.indexOf('left') >= 0) { + const offset = t.point({ + x: -this.padding() * 2, + y: 0, + }); + newAttrs.x += offset.x; + newAttrs.y += offset.y; + newAttrs.width += this.padding() * 2; + this._movingAnchorName = this._movingAnchorName.replace('left', 'right'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + } + else if (this._movingAnchorName && + newAttrs.width < 0 && + this._movingAnchorName.indexOf('right') >= 0) { + const offset = t.point({ + x: this.padding() * 2, + y: 0, + }); + this._movingAnchorName = this._movingAnchorName.replace('right', 'left'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + newAttrs.width += this.padding() * 2; + } + if (this._movingAnchorName && + newAttrs.height < 0 && + this._movingAnchorName.indexOf('top') >= 0) { + const offset = t.point({ + x: 0, + y: -this.padding() * 2, + }); + newAttrs.x += offset.x; + newAttrs.y += offset.y; + this._movingAnchorName = this._movingAnchorName.replace('top', 'bottom'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + newAttrs.height += this.padding() * 2; + } + else if (this._movingAnchorName && + newAttrs.height < 0 && + this._movingAnchorName.indexOf('bottom') >= 0) { + const offset = t.point({ + x: 0, + y: this.padding() * 2, + }); + this._movingAnchorName = this._movingAnchorName.replace('bottom', 'top'); + this._anchorDragOffset.x -= offset.x; + this._anchorDragOffset.y -= offset.y; + newAttrs.height += this.padding() * 2; + } + if (this.boundBoxFunc()) { + const bounded = this.boundBoxFunc()(oldAttrs, newAttrs); + if (bounded) { + newAttrs = bounded; + } + else { + Util.warn('boundBoxFunc returned falsy. You should return new bound rect from it!'); + } + } + // base size value doesn't really matter + // we just need to think about bounding boxes as transforms + // but how? + // the idea is that we have a transformed rectangle with the size of "baseSize" + const baseSize = 10000000; + const oldTr = new Transform(); + oldTr.translate(oldAttrs.x, oldAttrs.y); + oldTr.rotate(oldAttrs.rotation); + oldTr.scale(oldAttrs.width / baseSize, oldAttrs.height / baseSize); + const newTr = new Transform(); + const newScaleX = newAttrs.width / baseSize; + const newScaleY = newAttrs.height / baseSize; + if (this.flipEnabled() === false) { + newTr.translate(newAttrs.x, newAttrs.y); + newTr.rotate(newAttrs.rotation); + newTr.translate(newAttrs.width < 0 ? newAttrs.width : 0, newAttrs.height < 0 ? newAttrs.height : 0); + newTr.scale(Math.abs(newScaleX), Math.abs(newScaleY)); + } + else { + newTr.translate(newAttrs.x, newAttrs.y); + newTr.rotate(newAttrs.rotation); + newTr.scale(newScaleX, newScaleY); + } + // now lets think we had [old transform] and n ow we have [new transform] + // Now, the questions is: how can we transform "parent" to go from [old transform] into [new transform] + // in equation it will be: + // [delta transform] * [old transform] = [new transform] + // that means that + // [delta transform] = [new transform] * [old transform inverted] + const delta = newTr.multiply(oldTr.invert()); + this._nodes.forEach((node) => { + var _a; + // for each node we have the same [delta transform] + // the equations is + // [delta transform] * [parent transform] * [old local transform] = [parent transform] * [new local transform] + // and we need to find [new local transform] + // [new local] = [parent inverted] * [delta] * [parent] * [old local] + const parentTransform = node.getParent().getAbsoluteTransform(); + const localTransform = node.getTransform().copy(); + // skip offset: + localTransform.translate(node.offsetX(), node.offsetY()); + const newLocalTransform = new Transform(); + newLocalTransform + .multiply(parentTransform.copy().invert()) + .multiply(delta) + .multiply(parentTransform) + .multiply(localTransform); + const attrs = newLocalTransform.decompose(); + node.setAttrs(attrs); + (_a = node.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + }); + this.rotation(Util._getRotation(newAttrs.rotation)); + // trigger transform event AFTER we update rotation + this._nodes.forEach((node) => { + this._fire('transform', { evt: evt, target: node }); + node._fire('transform', { evt: evt, target: node }); + }); + this._resetTransformCache(); + this.update(); + this.getLayer().batchDraw(); + } + /** + * force update of Konva.Transformer. + * Use it when you updated attached Konva.Group and now you need to reset transformer size + * @method + * @name Konva.Transformer#forceUpdate + */ + forceUpdate() { + this._resetTransformCache(); + this.update(); + } + _batchChangeChild(selector, attrs) { + const anchor = this.findOne(selector); + anchor.setAttrs(attrs); + } + update() { + var _a; + const attrs = this._getNodeRect(); + this.rotation(Util._getRotation(attrs.rotation)); + const width = attrs.width; + const height = attrs.height; + const enabledAnchors = this.enabledAnchors(); + const resizeEnabled = this.resizeEnabled(); + const padding = this.padding(); + const anchorSize = this.anchorSize(); + const anchors = this.find('._anchor'); + anchors.forEach((node) => { + node.setAttrs({ + width: anchorSize, + height: anchorSize, + offsetX: anchorSize / 2, + offsetY: anchorSize / 2, + stroke: this.anchorStroke(), + strokeWidth: this.anchorStrokeWidth(), + fill: this.anchorFill(), + cornerRadius: this.anchorCornerRadius(), + }); + }); + this._batchChangeChild('.top-left', { + x: 0, + y: 0, + offsetX: anchorSize / 2 + padding, + offsetY: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('top-left') >= 0, + }); + this._batchChangeChild('.top-center', { + x: width / 2, + y: 0, + offsetY: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('top-center') >= 0, + }); + this._batchChangeChild('.top-right', { + x: width, + y: 0, + offsetX: anchorSize / 2 - padding, + offsetY: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('top-right') >= 0, + }); + this._batchChangeChild('.middle-left', { + x: 0, + y: height / 2, + offsetX: anchorSize / 2 + padding, + visible: resizeEnabled && enabledAnchors.indexOf('middle-left') >= 0, + }); + this._batchChangeChild('.middle-right', { + x: width, + y: height / 2, + offsetX: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('middle-right') >= 0, + }); + this._batchChangeChild('.bottom-left', { + x: 0, + y: height, + offsetX: anchorSize / 2 + padding, + offsetY: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('bottom-left') >= 0, + }); + this._batchChangeChild('.bottom-center', { + x: width / 2, + y: height, + offsetY: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('bottom-center') >= 0, + }); + this._batchChangeChild('.bottom-right', { + x: width, + y: height, + offsetX: anchorSize / 2 - padding, + offsetY: anchorSize / 2 - padding, + visible: resizeEnabled && enabledAnchors.indexOf('bottom-right') >= 0, + }); + this._batchChangeChild('.rotater', { + x: width / 2, + y: -this.rotateAnchorOffset() * Util._sign(height) - padding, + visible: this.rotateEnabled(), + }); + this._batchChangeChild('.back', { + width: width, + height: height, + visible: this.borderEnabled(), + stroke: this.borderStroke(), + strokeWidth: this.borderStrokeWidth(), + dash: this.borderDash(), + x: 0, + y: 0, + }); + const styleFunc = this.anchorStyleFunc(); + if (styleFunc) { + anchors.forEach((node) => { + styleFunc(node); + }); + } + (_a = this.getLayer()) === null || _a === undefined ? undefined : _a.batchDraw(); + } + /** + * determine if transformer is in active transform + * @method + * @name Konva.Transformer#isTransforming + * @returns {Boolean} + */ + isTransforming() { + return this._transforming; + } + /** + * Stop active transform action + * @method + * @name Konva.Transformer#stopTransform + * @returns {Boolean} + */ + stopTransform() { + if (this._transforming) { + this._removeEvents(); + const anchorNode = this.findOne('.' + this._movingAnchorName); + if (anchorNode) { + anchorNode.stopDrag(); + } + } + } + destroy() { + if (this.getStage() && this._cursorChange) { + this.getStage().content && (this.getStage().content.style.cursor = ''); + } + Group.prototype.destroy.call(this); + this.detach(); + this._removeEvents(); + return this; + } + // do not work as a container + // we will recreate inner nodes manually + toObject() { + return Node.prototype.toObject.call(this); + } + // overwrite clone to NOT use method from Container + clone(obj) { + const node = Node.prototype.clone.call(this, obj); + return node; + } + getClientRect() { + if (this.nodes().length > 0) { + return super.getClientRect(); + } + else { + // if we are detached return zero size + // so it will be skipped in calculations + return { x: 0, y: 0, width: 0, height: 0 }; + } + } + } + Transformer.isTransforming = () => { + return activeTransformersCount > 0; + }; + function validateAnchors(val) { + if (!(val instanceof Array)) { + Util.warn('enabledAnchors value should be an array'); + } + if (val instanceof Array) { + val.forEach(function (name) { + if (ANCHORS_NAMES.indexOf(name) === -1) { + Util.warn('Unknown anchor name: ' + + name + + '. Available names are: ' + + ANCHORS_NAMES.join(', ')); + } + }); + } + return val || []; + } + Transformer.prototype.className = 'Transformer'; + _registerNode(Transformer); + /** + * get/set enabled handlers + * @name Konva.Transformer#enabledAnchors + * @method + * @param {Array} array + * @returns {Array} + * @example + * // get list of handlers + * var enabledAnchors = transformer.enabledAnchors(); + * + * // set handlers + * transformer.enabledAnchors(['top-left', 'top-center', 'top-right', 'middle-right', 'middle-left', 'bottom-left', 'bottom-center', 'bottom-right']); + */ + Factory.addGetterSetter(Transformer, 'enabledAnchors', ANCHORS_NAMES, validateAnchors); + /** + * get/set flip enabled + * @name Konva.Transformer#flipEnabled + * @method + * @param {Boolean} flag + * @returns {Boolean} + * @example + * // get flip enabled property + * var flipEnabled = transformer.flipEnabled(); + * + * // set flip enabled property + * transformer.flipEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'flipEnabled', true, getBooleanValidator()); + /** + * get/set resize ability. If false it will automatically hide resizing handlers + * @name Konva.Transformer#resizeEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var resizeEnabled = transformer.resizeEnabled(); + * + * // set + * transformer.resizeEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'resizeEnabled', true); + /** + * get/set anchor size. Default is 10 + * @name Konva.Transformer#anchorSize + * @method + * @param {Number} size + * @returns {Number} + * @example + * // get + * var anchorSize = transformer.anchorSize(); + * + * // set + * transformer.anchorSize(20) + */ + Factory.addGetterSetter(Transformer, 'anchorSize', 10, getNumberValidator()); + /** + * get/set ability to rotate. + * @name Konva.Transformer#rotateEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var rotateEnabled = transformer.rotateEnabled(); + * + * // set + * transformer.rotateEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'rotateEnabled', true); + /** + * get/set visibility of a little line that connects transformer and rotate anchor. + * @name Konva.Transformer#rotateLineVisible + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var rotateLineVisible = transformer.rotateLineVisible(); + * + * // set + * transformer.rotateLineVisible(false); + */ + Factory.addGetterSetter(Transformer, 'rotateLineVisible', true); + /** + * get/set rotation snaps angles. + * @name Konva.Transformer#rotationSnaps + * @method + * @param {Array} array + * @returns {Array} + * @example + * // get + * var rotationSnaps = transformer.rotationSnaps(); + * + * // set + * transformer.rotationSnaps([0, 90, 180, 270]); + */ + Factory.addGetterSetter(Transformer, 'rotationSnaps', []); + /** + * get/set distance for rotation handler + * @name Konva.Transformer#rotateAnchorOffset + * @method + * @param {Number} offset + * @returns {Number} + * @example + * // get + * var rotateAnchorOffset = transformer.rotateAnchorOffset(); + * + * // set + * transformer.rotateAnchorOffset(100); + */ + Factory.addGetterSetter(Transformer, 'rotateAnchorOffset', 50, getNumberValidator()); + /** + * get/set rotation anchor cursor + * @name Konva.Transformer#rotateAnchorCursor + * @method + * @param {String} cursorName + * @returns {String} + * @example + * // get + * var currentRotationAnchorCursor = transformer.rotateAnchorCursor(); + * + * // set + * transformer.rotateAnchorCursor('grab'); + */ + Factory.addGetterSetter(Transformer, 'rotateAnchorCursor', 'crosshair'); + /** + * get/set distance for rotation tolerance + * @name Konva.Transformer#rotationSnapTolerance + * @method + * @param {Number} tolerance + * @returns {Number} + * @example + * // get + * var rotationSnapTolerance = transformer.rotationSnapTolerance(); + * + * // set + * transformer.rotationSnapTolerance(100); + */ + Factory.addGetterSetter(Transformer, 'rotationSnapTolerance', 5, getNumberValidator()); + /** + * get/set visibility of border + * @name Konva.Transformer#borderEnabled + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var borderEnabled = transformer.borderEnabled(); + * + * // set + * transformer.borderEnabled(false); + */ + Factory.addGetterSetter(Transformer, 'borderEnabled', true); + /** + * get/set anchor stroke color + * @name Konva.Transformer#anchorStroke + * @method + * @param {String} strokeColor + * @returns {String} + * @example + * // get + * var anchorStroke = transformer.anchorStroke(); + * + * // set + * transformer.anchorStroke('red'); + */ + Factory.addGetterSetter(Transformer, 'anchorStroke', 'rgb(0, 161, 255)'); + /** + * get/set anchor stroke width + * @name Konva.Transformer#anchorStrokeWidth + * @method + * @param {Number} anchorStrokeWidth + * @returns {Number} + * @example + * // get + * var anchorStrokeWidth = transformer.anchorStrokeWidth(); + * + * // set + * transformer.anchorStrokeWidth(3); + */ + Factory.addGetterSetter(Transformer, 'anchorStrokeWidth', 1, getNumberValidator()); + /** + * get/set anchor fill color + * @name Konva.Transformer#anchorFill + * @method + * @param {String} anchorFill + * @returns {String} + * @example + * // get + * var anchorFill = transformer.anchorFill(); + * + * // set + * transformer.anchorFill('red'); + */ + Factory.addGetterSetter(Transformer, 'anchorFill', 'white'); + /** + * get/set anchor corner radius + * @name Konva.Transformer#anchorCornerRadius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get + * var anchorCornerRadius = transformer.anchorCornerRadius(); + * + * // set + * transformer.anchorCornerRadius(3); + */ + Factory.addGetterSetter(Transformer, 'anchorCornerRadius', 0, getNumberValidator()); + /** + * get/set border stroke color + * @name Konva.Transformer#borderStroke + * @method + * @param {Boolean} enabled + * @returns {Boolean} + * @example + * // get + * var borderStroke = transformer.borderStroke(); + * + * // set + * transformer.borderStroke('red'); + */ + Factory.addGetterSetter(Transformer, 'borderStroke', 'rgb(0, 161, 255)'); + /** + * get/set border stroke width + * @name Konva.Transformer#borderStrokeWidth + * @method + * @param {Number} strokeWidth + * @returns {Number} + * @example + * // get + * var borderStrokeWidth = transformer.borderStrokeWidth(); + * + * // set + * transformer.borderStrokeWidth(3); + */ + Factory.addGetterSetter(Transformer, 'borderStrokeWidth', 1, getNumberValidator()); + /** + * get/set border dash array + * @name Konva.Transformer#borderDash + * @method + * @param {Array} dash array + * @returns {Array} + * @example + * // get + * var borderDash = transformer.borderDash(); + * + * // set + * transformer.borderDash([2, 2]); + */ + Factory.addGetterSetter(Transformer, 'borderDash'); + /** + * get/set should we keep ratio while resize anchors at corners + * @name Konva.Transformer#keepRatio + * @method + * @param {Boolean} keepRatio + * @returns {Boolean} + * @example + * // get + * var keepRatio = transformer.keepRatio(); + * + * // set + * transformer.keepRatio(false); + */ + Factory.addGetterSetter(Transformer, 'keepRatio', true); + /** + * get/set how to react on skift key while resizing anchors at corners + * @name Konva.Transformer#shiftBehavior + * @method + * @param {String} shiftBehavior + * @returns {String} + * @example + * // get + * var shiftBehavior = transformer.shiftBehavior(); + * + * // set + * transformer.shiftBehavior('none'); + */ + Factory.addGetterSetter(Transformer, 'shiftBehavior', 'default'); + /** + * get/set should we resize relative to node's center? + * @name Konva.Transformer#centeredScaling + * @method + * @param {Boolean} centeredScaling + * @returns {Boolean} + * @example + * // get + * var centeredScaling = transformer.centeredScaling(); + * + * // set + * transformer.centeredScaling(true); + */ + Factory.addGetterSetter(Transformer, 'centeredScaling', false); + /** + * get/set should we think about stroke while resize? Good to use when a shape has strokeScaleEnabled = false + * default is false + * @name Konva.Transformer#ignoreStroke + * @method + * @param {Boolean} ignoreStroke + * @returns {Boolean} + * @example + * // get + * var ignoreStroke = transformer.ignoreStroke(); + * + * // set + * transformer.ignoreStroke(true); + */ + Factory.addGetterSetter(Transformer, 'ignoreStroke', false); + /** + * get/set padding + * @name Konva.Transformer#padding + * @method + * @param {Number} padding + * @returns {Number} + * @example + * // get + * var padding = transformer.padding(); + * + * // set + * transformer.padding(10); + */ + Factory.addGetterSetter(Transformer, 'padding', 0, getNumberValidator()); + /** + * get/set attached nodes of the Transformer. Transformer will adapt to their size and listen to their events + * @method + * @name Konva.Transformer#nodes + * @returns {Konva.Node} + * @example + * // get + * const nodes = transformer.nodes(); + * + * // set + * transformer.nodes([rect, circle]); + * + * // push new item: + * const oldNodes = transformer.nodes(); + * const newNodes = oldNodes.concat([newShape]); + * // it is important to set new array instance (and concat method above will create it) + * transformer.nodes(newNodes); + */ + Factory.addGetterSetter(Transformer, 'nodes'); + // @ts-ignore + // deprecated + Factory.addGetterSetter(Transformer, 'node'); + /** + * get/set bounding box function. **IMPORTANT!** boundBondFunc operates in absolute coordinates. + * @name Konva.Transformer#boundBoxFunc + * @method + * @param {Function} func + * @returns {Function} + * @example + * // get + * var boundBoxFunc = transformer.boundBoxFunc(); + * + * // set + * transformer.boundBoxFunc(function(oldBox, newBox) { + * // width and height of the boxes are corresponding to total absolute width and height of all nodes combined + * // so it includes scale of the node. + * if (newBox.width > 200) { + * return oldBox; + * } + * return newBox; + * }); + */ + Factory.addGetterSetter(Transformer, 'boundBoxFunc'); + /** + * get/set dragging func for transformer anchors + * @name Konva.Transformer#anchorDragBoundFunc + * @method + * @param {Function} func + * @returns {Function} + * @example + * // get + * var anchorDragBoundFunc = transformer.anchorDragBoundFunc(); + * + * // set + * transformer.anchorDragBoundFunc(function(oldAbsPos, newAbsPos, event) { + * return { + * x: 0, + * y: newAbsolutePosition.y + * } + * }); + */ + Factory.addGetterSetter(Transformer, 'anchorDragBoundFunc'); + /** + * get/set styling function for transformer anchors to overwrite default styles + * @name Konva.Transformer#anchorStyleFunc + * @method + * @param {Function} func + * @returns {Function} + * @example + * // get + * var anchorStyleFunc = transformer.anchorStyleFunc(); + * + * // set + * transformer.anchorStyleFunc(function(anchor) { + * // anchor is a simple Konva.Rect instance + * // it will be executed AFTER all attributes are set, like 'anchorStrokeWidth' or 'anchorFill' + * if (anchor.hasName('.rotater')) { + * // make rotater anchor filled black and looks like a circle + * anchor.fill('black'); + * anchor.cornerRadius(anchor.width() / 2); + * } + * }); + */ + Factory.addGetterSetter(Transformer, 'anchorStyleFunc'); + /** + * using this setting you can drag transformer group by dragging empty space between attached nodes + * shouldOverdrawWholeArea = true may temporary disable all events on attached nodes + * @name Konva.Transformer#shouldOverdrawWholeArea + * @method + * @param {Boolean} shouldOverdrawWholeArea + * @returns {Boolean} + * @example + * // get + * var shouldOverdrawWholeArea = transformer.shouldOverdrawWholeArea(); + * + * // set + * transformer.shouldOverdrawWholeArea(true); + */ + Factory.addGetterSetter(Transformer, 'shouldOverdrawWholeArea', false); + /** + * If you have just one attached node to Transformer it will set its initial rotation to the rotation of that node. + * In some cases you may need to set a different rotation. + * @name Konva.Transformer#useSingleNodeRotation + * @method + * @param {Boolean} useSingleNodeRotation + * @returns {Boolean} + * @example + * // set flag to false + * transformer.useSingleNodeRotation(false); + * // attach a shape + * transformer.nodes([shape]); + * transformer.rotation(45); + * transformer.update(); + */ + Factory.addGetterSetter(Transformer, 'useSingleNodeRotation', true); + Factory.backCompat(Transformer, { + lineEnabled: 'borderEnabled', + rotateHandlerOffset: 'rotateAnchorOffset', + enabledHandlers: 'enabledAnchors', + }); + + /** + * Wedge constructor + * @constructor + * @memberof Konva + * @augments Konva.Shape + * @param {Object} config + * @param {Number} config.angle in degrees + * @param {Number} config.radius + * @param {Boolean} [config.clockwise] + * @param {String} [config.fill] fill color + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.fillAfterStrokeEnabled]. Should we draw fill AFTER stroke? Default is false. + * @param {Number} [config.hitStrokeWidth] size of the stroke on hit canvas. The default is "auto" - equals to strokeWidth + * @param {Boolean} [config.strokeHitEnabled] flag which enables or disables stroke hit region. The default is true + * @param {Boolean} [config.perfectDrawEnabled] flag which enables or disables using buffer canvas. The default is true + * @param {Boolean} [config.shadowForStrokeEnabled] flag which enables or disables shadow for stroke. The default is true + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or square. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * // draw a wedge that's pointing downwards + * var wedge = new Konva.Wedge({ + * radius: 40, + * fill: 'red', + * stroke: 'black' + * strokeWidth: 5, + * angleDeg: 60, + * rotationDeg: -120 + * }); + */ + class Wedge extends Shape { + _sceneFunc(context) { + context.beginPath(); + context.arc(0, 0, this.radius(), 0, Konva$2.getAngle(this.angle()), this.clockwise()); + context.lineTo(0, 0); + context.closePath(); + context.fillStrokeShape(this); + } + getWidth() { + return this.radius() * 2; + } + getHeight() { + return this.radius() * 2; + } + setWidth(width) { + this.radius(width / 2); + } + setHeight(height) { + this.radius(height / 2); + } + } + Wedge.prototype.className = 'Wedge'; + Wedge.prototype._centroid = true; + Wedge.prototype._attrsAffectingSize = ['radius']; + _registerNode(Wedge); + /** + * get/set radius + * @name Konva.Wedge#radius + * @method + * @param {Number} radius + * @returns {Number} + * @example + * // get radius + * var radius = wedge.radius(); + * + * // set radius + * wedge.radius(10); + */ + Factory.addGetterSetter(Wedge, 'radius', 0, getNumberValidator()); + /** + * get/set angle in degrees + * @name Konva.Wedge#angle + * @method + * @param {Number} angle + * @returns {Number} + * @example + * // get angle + * var angle = wedge.angle(); + * + * // set angle + * wedge.angle(20); + */ + Factory.addGetterSetter(Wedge, 'angle', 0, getNumberValidator()); + /** + * get/set clockwise flag + * @name Konva.Wedge#clockwise + * @method + * @param {Number} clockwise + * @returns {Number} + * @example + * // get clockwise flag + * var clockwise = wedge.clockwise(); + * + * // draw wedge counter-clockwise + * wedge.clockwise(false); + * + * // draw wedge clockwise + * wedge.clockwise(true); + */ + Factory.addGetterSetter(Wedge, 'clockwise', false); + Factory.backCompat(Wedge, { + angleDeg: 'angle', + getAngleDeg: 'getAngle', + setAngleDeg: 'setAngle', + }); + + /* + the Gauss filter + master repo: https://github.com/pavelpower/kineticjsGaussFilter + */ + /* + + StackBlur - a fast almost Gaussian Blur For Canvas + + Version: 0.5 + Author: Mario Klingemann + Contact: mario@quasimondo.com + Website: http://www.quasimondo.com/StackBlurForCanvas + Twitter: @quasimondo + + In case you find this class useful - especially in commercial projects - + I am not totally unhappy for a small donation to my PayPal account + mario@quasimondo.de + + Or support me on flattr: + https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript + + Copyright (c) 2010 Mario Klingemann + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + */ + function BlurStack() { + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; + } + const mul_table = [ + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, + 512, 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, + 273, 512, 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, + 496, 475, 456, 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, + 282, 273, 265, 512, 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, + 364, 354, 345, 337, 328, 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, + 507, 496, 485, 475, 465, 456, 446, 437, 428, 420, 412, 404, 396, 388, 381, + 374, 367, 360, 354, 347, 341, 335, 329, 323, 318, 312, 307, 302, 297, 292, + 287, 282, 278, 273, 269, 265, 261, 512, 505, 497, 489, 482, 475, 468, 461, + 454, 447, 441, 435, 428, 422, 417, 411, 405, 399, 394, 389, 383, 378, 373, + 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, 324, 320, 316, 312, 309, + 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, 268, 265, 262, 259, + 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, 451, 446, 442, + 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, 385, 381, + 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, 332, + 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259, + ]; + const shg_table = [ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 17, 17, + 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, + ]; + function filterGaussBlurRGBA(imageData, radius) { + const pixels = imageData.data, width = imageData.width, height = imageData.height; + let x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, r_out_sum, g_out_sum, b_out_sum, a_out_sum, r_in_sum, g_in_sum, b_in_sum, a_in_sum, pr, pg, pb, pa, rbs; + const div = radius + radius + 1, widthMinus1 = width - 1, heightMinus1 = height - 1, radiusPlus1 = radius + 1, sumFactor = (radiusPlus1 * (radiusPlus1 + 1)) / 2, stackStart = new BlurStack(), mul_sum = mul_table[radius], shg_sum = shg_table[radius]; + let stackEnd = null, stack = stackStart, stackIn = null, stackOut = null; + for (i = 1; i < div; i++) { + stack = stack.next = new BlurStack(); + if (i === radiusPlus1) { + stackEnd = stack; + } + } + stack.next = stackStart; + yw = yi = 0; + for (y = 0; y < height; y++) { + r_in_sum = + g_in_sum = + b_in_sum = + a_in_sum = + r_sum = + g_sum = + b_sum = + a_sum = + 0; + r_out_sum = radiusPlus1 * (pr = pixels[yi]); + g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); + b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); + a_out_sum = radiusPlus1 * (pa = pixels[yi + 3]); + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + a_sum += sumFactor * pa; + stack = stackStart; + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + for (i = 1; i < radiusPlus1; i++) { + p = yi + ((widthMinus1 < i ? widthMinus1 : i) << 2); + r_sum += (stack.r = pr = pixels[p]) * (rbs = radiusPlus1 - i); + g_sum += (stack.g = pg = pixels[p + 1]) * rbs; + b_sum += (stack.b = pb = pixels[p + 2]) * rbs; + a_sum += (stack.a = pa = pixels[p + 3]) * rbs; + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + a_in_sum += pa; + stack = stack.next; + } + stackIn = stackStart; + stackOut = stackEnd; + for (x = 0; x < width; x++) { + pixels[yi + 3] = pa = (a_sum * mul_sum) >> shg_sum; + if (pa !== 0) { + pa = 255 / pa; + pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa; + pixels[yi + 1] = ((g_sum * mul_sum) >> shg_sum) * pa; + pixels[yi + 2] = ((b_sum * mul_sum) >> shg_sum) * pa; + } + else { + pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0; + } + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + a_sum -= a_out_sum; + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + a_out_sum -= stackIn.a; + p = (yw + ((p = x + radius + 1) < widthMinus1 ? p : widthMinus1)) << 2; + r_in_sum += stackIn.r = pixels[p]; + g_in_sum += stackIn.g = pixels[p + 1]; + b_in_sum += stackIn.b = pixels[p + 2]; + a_in_sum += stackIn.a = pixels[p + 3]; + r_sum += r_in_sum; + g_sum += g_in_sum; + b_sum += b_in_sum; + a_sum += a_in_sum; + stackIn = stackIn.next; + r_out_sum += pr = stackOut.r; + g_out_sum += pg = stackOut.g; + b_out_sum += pb = stackOut.b; + a_out_sum += pa = stackOut.a; + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + a_in_sum -= pa; + stackOut = stackOut.next; + yi += 4; + } + yw += width; + } + for (x = 0; x < width; x++) { + g_in_sum = + b_in_sum = + a_in_sum = + r_in_sum = + g_sum = + b_sum = + a_sum = + r_sum = + 0; + yi = x << 2; + r_out_sum = radiusPlus1 * (pr = pixels[yi]); + g_out_sum = radiusPlus1 * (pg = pixels[yi + 1]); + b_out_sum = radiusPlus1 * (pb = pixels[yi + 2]); + a_out_sum = radiusPlus1 * (pa = pixels[yi + 3]); + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + a_sum += sumFactor * pa; + stack = stackStart; + for (i = 0; i < radiusPlus1; i++) { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + yp = width; + for (i = 1; i <= radius; i++) { + yi = (yp + x) << 2; + r_sum += (stack.r = pr = pixels[yi]) * (rbs = radiusPlus1 - i); + g_sum += (stack.g = pg = pixels[yi + 1]) * rbs; + b_sum += (stack.b = pb = pixels[yi + 2]) * rbs; + a_sum += (stack.a = pa = pixels[yi + 3]) * rbs; + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + a_in_sum += pa; + stack = stack.next; + if (i < heightMinus1) { + yp += width; + } + } + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for (y = 0; y < height; y++) { + p = yi << 2; + pixels[p + 3] = pa = (a_sum * mul_sum) >> shg_sum; + if (pa > 0) { + pa = 255 / pa; + pixels[p] = ((r_sum * mul_sum) >> shg_sum) * pa; + pixels[p + 1] = ((g_sum * mul_sum) >> shg_sum) * pa; + pixels[p + 2] = ((b_sum * mul_sum) >> shg_sum) * pa; + } + else { + pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; + } + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + a_sum -= a_out_sum; + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + a_out_sum -= stackIn.a; + p = + (x + + ((p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1) * width) << + 2; + r_sum += r_in_sum += stackIn.r = pixels[p]; + g_sum += g_in_sum += stackIn.g = pixels[p + 1]; + b_sum += b_in_sum += stackIn.b = pixels[p + 2]; + a_sum += a_in_sum += stackIn.a = pixels[p + 3]; + stackIn = stackIn.next; + r_out_sum += pr = stackOut.r; + g_out_sum += pg = stackOut.g; + b_out_sum += pb = stackOut.b; + a_out_sum += pa = stackOut.a; + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + a_in_sum -= pa; + stackOut = stackOut.next; + yi += width; + } + } + } + /** + * Blur Filter + * @function + * @name Blur + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Blur]); + * node.blurRadius(10); + */ + const Blur = function Blur(imageData) { + const radius = Math.round(this.blurRadius()); + if (radius > 0) { + filterGaussBlurRGBA(imageData, radius); + } + }; + Factory.addGetterSetter(Node, 'blurRadius', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set blur radius. Use with {@link Konva.Filters.Blur} filter + * @name Konva.Node#blurRadius + * @method + * @param {Integer} radius + * @returns {Integer} + */ + + /** + * Brighten Filter. + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Brighten]); + * node.brightness(0.8); + */ + const Brighten = function (imageData) { + const brightness = this.brightness() * 255, data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 4) { + // red + data[i] += brightness; + // green + data[i + 1] += brightness; + // blue + data[i + 2] += brightness; + } + }; + Factory.addGetterSetter(Node, 'brightness', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set filter brightness. The brightness is a number between -1 and 1.  Positive values + * brighten the pixels and negative values darken them. Use with {@link Konva.Filters.Brighten} filter. + * @name Konva.Node#brightness + * @method + + * @param {Number} brightness value between -1 and 1 + * @returns {Number} + */ + + /** + * Contrast Filter. + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Contrast]); + * node.contrast(10); + */ + const Contrast = function (imageData) { + const adjust = Math.pow((this.contrast() + 100) / 100, 2); + const data = imageData.data, nPixels = data.length; + let red = 150, green = 150, blue = 150; + for (let i = 0; i < nPixels; i += 4) { + red = data[i]; + green = data[i + 1]; + blue = data[i + 2]; + //Red channel + red /= 255; + red -= 0.5; + red *= adjust; + red += 0.5; + red *= 255; + //Green channel + green /= 255; + green -= 0.5; + green *= adjust; + green += 0.5; + green *= 255; + //Blue channel + blue /= 255; + blue -= 0.5; + blue *= adjust; + blue += 0.5; + blue *= 255; + red = red < 0 ? 0 : red > 255 ? 255 : red; + green = green < 0 ? 0 : green > 255 ? 255 : green; + blue = blue < 0 ? 0 : blue > 255 ? 255 : blue; + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + } + }; + /** + * get/set filter contrast. The contrast is a number between -100 and 100. + * Use with {@link Konva.Filters.Contrast} filter. + * @name Konva.Node#contrast + * @method + * @param {Number} contrast value between -100 and 100 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'contrast', 0, getNumberValidator(), Factory.afterSetFilter); + + /** + * Emboss Filter. + * Pixastic Lib - Emboss filter - v0.1.0 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * License: [http://www.pixastic.com/lib/license.txt] + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Emboss]); + * node.embossStrength(0.8); + * node.embossWhiteLevel(0.3); + * node.embossDirection('right'); + * node.embossBlend(true); + */ + const Emboss = function (imageData) { + // pixastic strength is between 0 and 10. I want it between 0 and 1 + // pixastic greyLevel is between 0 and 255. I want it between 0 and 1. Also, + // a max value of greyLevel yields a white emboss, and the min value yields a black + // emboss. Therefore, I changed greyLevel to whiteLevel + const strength = this.embossStrength() * 10, greyLevel = this.embossWhiteLevel() * 255, direction = this.embossDirection(), blend = this.embossBlend(), data = imageData.data, w = imageData.width, h = imageData.height, w4 = w * 4; + let dirY = 0, dirX = 0, y = h; + switch (direction) { + case 'top-left': + dirY = -1; + dirX = -1; + break; + case 'top': + dirY = -1; + dirX = 0; + break; + case 'top-right': + dirY = -1; + dirX = 1; + break; + case 'right': + dirY = 0; + dirX = 1; + break; + case 'bottom-right': + dirY = 1; + dirX = 1; + break; + case 'bottom': + dirY = 1; + dirX = 0; + break; + case 'bottom-left': + dirY = 1; + dirX = -1; + break; + case 'left': + dirY = 0; + dirX = -1; + break; + default: + Util.error('Unknown emboss direction: ' + direction); + } + do { + const offsetY = (y - 1) * w4; + let otherY = dirY; + if (y + otherY < 1) { + otherY = 0; + } + if (y + otherY > h) { + otherY = 0; + } + const offsetYOther = (y - 1 + otherY) * w * 4; + let x = w; + do { + const offset = offsetY + (x - 1) * 4; + let otherX = dirX; + if (x + otherX < 1) { + otherX = 0; + } + if (x + otherX > w) { + otherX = 0; + } + const offsetOther = offsetYOther + (x - 1 + otherX) * 4; + const dR = data[offset] - data[offsetOther]; + const dG = data[offset + 1] - data[offsetOther + 1]; + const dB = data[offset + 2] - data[offsetOther + 2]; + let dif = dR; + const absDif = dif > 0 ? dif : -dif; + const absG = dG > 0 ? dG : -dG; + const absB = dB > 0 ? dB : -dB; + if (absG > absDif) { + dif = dG; + } + if (absB > absDif) { + dif = dB; + } + dif *= strength; + if (blend) { + const r = data[offset] + dif; + const g = data[offset + 1] + dif; + const b = data[offset + 2] + dif; + data[offset] = r > 255 ? 255 : r < 0 ? 0 : r; + data[offset + 1] = g > 255 ? 255 : g < 0 ? 0 : g; + data[offset + 2] = b > 255 ? 255 : b < 0 ? 0 : b; + } + else { + let grey = greyLevel - dif; + if (grey < 0) { + grey = 0; + } + else if (grey > 255) { + grey = 255; + } + data[offset] = data[offset + 1] = data[offset + 2] = grey; + } + } while (--x); + } while (--y); + }; + Factory.addGetterSetter(Node, 'embossStrength', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set emboss strength. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossStrength + * @method + * @param {Number} level between 0 and 1. Default is 0.5 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'embossWhiteLevel', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set emboss white level. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossWhiteLevel + * @method + * @param {Number} embossWhiteLevel between 0 and 1. Default is 0.5 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'embossDirection', 'top-left', undefined, Factory.afterSetFilter); + /** + * get/set emboss direction. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossDirection + * @method + * @param {String} embossDirection can be top-left, top, top-right, right, bottom-right, bottom, bottom-left or left + * The default is top-left + * @returns {String} + */ + Factory.addGetterSetter(Node, 'embossBlend', false, undefined, Factory.afterSetFilter); + /** + * get/set emboss blend. Use with {@link Konva.Filters.Emboss} filter. + * @name Konva.Node#embossBlend + * @method + * @param {Boolean} embossBlend + * @returns {Boolean} + */ + + function remap(fromValue, fromMin, fromMax, toMin, toMax) { + // Compute the range of the data + const fromRange = fromMax - fromMin, toRange = toMax - toMin; + // If either range is 0, then the value can only be mapped to 1 value + if (fromRange === 0) { + return toMin + toRange / 2; + } + if (toRange === 0) { + return toMin; + } + // (1) untranslate, (2) unscale, (3) rescale, (4) retranslate + let toValue = (fromValue - fromMin) / fromRange; + toValue = toRange * toValue + toMin; + return toValue; + } + /** + * Enhance Filter. Adjusts the colors so that they span the widest + * possible range (ie 0-255). Performs w*h pixel reads and w*h pixel + * writes. + * @function + * @name Enhance + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Enhance]); + * node.enhance(0.4); + */ + const Enhance = function (imageData) { + const data = imageData.data, nSubPixels = data.length; + let rMin = data[0], rMax = rMin, r, gMin = data[1], gMax = gMin, g, bMin = data[2], bMax = bMin, b; + // If we are not enhancing anything - don't do any computation + const enhanceAmount = this.enhance(); + if (enhanceAmount === 0) { + return; + } + // 1st Pass - find the min and max for each channel: + for (let i = 0; i < nSubPixels; i += 4) { + r = data[i + 0]; + if (r < rMin) { + rMin = r; + } + else if (r > rMax) { + rMax = r; + } + g = data[i + 1]; + if (g < gMin) { + gMin = g; + } + else if (g > gMax) { + gMax = g; + } + b = data[i + 2]; + if (b < bMin) { + bMin = b; + } + else if (b > bMax) { + bMax = b; + } + //a = data[i + 3]; + //if (a < aMin) { aMin = a; } else + //if (a > aMax) { aMax = a; } + } + // If there is only 1 level - don't remap + if (rMax === rMin) { + rMax = 255; + rMin = 0; + } + if (gMax === gMin) { + gMax = 255; + gMin = 0; + } + if (bMax === bMin) { + bMax = 255; + bMin = 0; + } + let rMid, rGoalMax, rGoalMin, gMid, gGoalMax, gGoalMin, bMid, bGoalMax, bGoalMin; + // If the enhancement is positive - stretch the histogram + if (enhanceAmount > 0) { + rGoalMax = rMax + enhanceAmount * (255 - rMax); + rGoalMin = rMin - enhanceAmount * (rMin - 0); + gGoalMax = gMax + enhanceAmount * (255 - gMax); + gGoalMin = gMin - enhanceAmount * (gMin - 0); + bGoalMax = bMax + enhanceAmount * (255 - bMax); + bGoalMin = bMin - enhanceAmount * (bMin - 0); + // If the enhancement is negative - compress the histogram + } + else { + rMid = (rMax + rMin) * 0.5; + rGoalMax = rMax + enhanceAmount * (rMax - rMid); + rGoalMin = rMin + enhanceAmount * (rMin - rMid); + gMid = (gMax + gMin) * 0.5; + gGoalMax = gMax + enhanceAmount * (gMax - gMid); + gGoalMin = gMin + enhanceAmount * (gMin - gMid); + bMid = (bMax + bMin) * 0.5; + bGoalMax = bMax + enhanceAmount * (bMax - bMid); + bGoalMin = bMin + enhanceAmount * (bMin - bMid); + } + // Pass 2 - remap everything, except the alpha + for (let i = 0; i < nSubPixels; i += 4) { + data[i + 0] = remap(data[i + 0], rMin, rMax, rGoalMin, rGoalMax); + data[i + 1] = remap(data[i + 1], gMin, gMax, gGoalMin, gGoalMax); + data[i + 2] = remap(data[i + 2], bMin, bMax, bGoalMin, bGoalMax); + //data[i + 3] = remap(data[i + 3], aMin, aMax, aGoalMin, aGoalMax); + } + }; + /** + * get/set enhance. Use with {@link Konva.Filters.Enhance} filter. -1 to 1 values + * @name Konva.Node#enhance + * @method + * @param {Float} amount + * @returns {Float} + */ + Factory.addGetterSetter(Node, 'enhance', 0, getNumberValidator(), Factory.afterSetFilter); + + /** + * Grayscale Filter + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Grayscale]); + */ + const Grayscale = function (imageData) { + const data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 4) { + const brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]; + // red + data[i] = brightness; + // green + data[i + 1] = brightness; + // blue + data[i + 2] = brightness; + } + }; + + Factory.addGetterSetter(Node, 'hue', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv hue in degrees. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#hue + * @method + * @param {Number} hue value between 0 and 359 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'saturation', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv saturation. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#saturation + * @method + * @param {Number} saturation 0 is no change, -1.0 halves the saturation, 1.0 doubles, etc.. + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'luminance', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsl luminance. Use with {@link Konva.Filters.HSL} filter. + * @name Konva.Node#luminance + * @method + * @param {Number} value from -1 to 1 + * @returns {Number} + */ + /** + * HSL Filter. Adjusts the hue, saturation and luminance (or lightness) + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * image.filters([Konva.Filters.HSL]); + * image.luminance(0.2); + */ + const HSL = function (imageData) { + const data = imageData.data, nPixels = data.length, v = 1, s = Math.pow(2, this.saturation()), h = Math.abs(this.hue() + 360) % 360, l = this.luminance() * 127; + // Basis for the technique used: + // http://beesbuzz.biz/code/hsv_color_transforms.php + // V is the value multiplier (1 for none, 2 for double, 0.5 for half) + // S is the saturation multiplier (1 for none, 2 for double, 0.5 for half) + // H is the hue shift in degrees (0 to 360) + // vsu = V*S*cos(H*PI/180); + // vsw = V*S*sin(H*PI/180); + //[ .299V+.701vsu+.168vsw .587V-.587vsu+.330vsw .114V-.114vsu-.497vsw ] [R] + //[ .299V-.299vsu-.328vsw .587V+.413vsu+.035vsw .114V-.114vsu+.292vsw ]*[G] + //[ .299V-.300vsu+1.25vsw .587V-.588vsu-1.05vsw .114V+.886vsu-.203vsw ] [B] + // Precompute the values in the matrix: + const vsu = v * s * Math.cos((h * Math.PI) / 180), vsw = v * s * Math.sin((h * Math.PI) / 180); + // (result spot)(source spot) + const rr = 0.299 * v + 0.701 * vsu + 0.167 * vsw, rg = 0.587 * v - 0.587 * vsu + 0.33 * vsw, rb = 0.114 * v - 0.114 * vsu - 0.497 * vsw; + const gr = 0.299 * v - 0.299 * vsu - 0.328 * vsw, gg = 0.587 * v + 0.413 * vsu + 0.035 * vsw, gb = 0.114 * v - 0.114 * vsu + 0.293 * vsw; + const br = 0.299 * v - 0.3 * vsu + 1.25 * vsw, bg = 0.587 * v - 0.586 * vsu - 1.05 * vsw, bb = 0.114 * v + 0.886 * vsu - 0.2 * vsw; + let r, g, b, a; + for (let i = 0; i < nPixels; i += 4) { + r = data[i + 0]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + data[i + 0] = rr * r + rg * g + rb * b + l; + data[i + 1] = gr * r + gg * g + gb * b + l; + data[i + 2] = br * r + bg * g + bb * b + l; + data[i + 3] = a; // alpha + } + }; + + /** + * HSV Filter. Adjusts the hue, saturation and value + * @function + * @name HSV + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * image.filters([Konva.Filters.HSV]); + * image.value(200); + */ + const HSV = function (imageData) { + const data = imageData.data, nPixels = data.length, v = Math.pow(2, this.value()), s = Math.pow(2, this.saturation()), h = Math.abs(this.hue() + 360) % 360; + // Basis for the technique used: + // http://beesbuzz.biz/code/hsv_color_transforms.php + // V is the value multiplier (1 for none, 2 for double, 0.5 for half) + // S is the saturation multiplier (1 for none, 2 for double, 0.5 for half) + // H is the hue shift in degrees (0 to 360) + // vsu = V*S*cos(H*PI/180); + // vsw = V*S*sin(H*PI/180); + //[ .299V+.701vsu+.168vsw .587V-.587vsu+.330vsw .114V-.114vsu-.497vsw ] [R] + //[ .299V-.299vsu-.328vsw .587V+.413vsu+.035vsw .114V-.114vsu+.292vsw ]*[G] + //[ .299V-.300vsu+1.25vsw .587V-.588vsu-1.05vsw .114V+.886vsu-.203vsw ] [B] + // Precompute the values in the matrix: + const vsu = v * s * Math.cos((h * Math.PI) / 180), vsw = v * s * Math.sin((h * Math.PI) / 180); + // (result spot)(source spot) + const rr = 0.299 * v + 0.701 * vsu + 0.167 * vsw, rg = 0.587 * v - 0.587 * vsu + 0.33 * vsw, rb = 0.114 * v - 0.114 * vsu - 0.497 * vsw; + const gr = 0.299 * v - 0.299 * vsu - 0.328 * vsw, gg = 0.587 * v + 0.413 * vsu + 0.035 * vsw, gb = 0.114 * v - 0.114 * vsu + 0.293 * vsw; + const br = 0.299 * v - 0.3 * vsu + 1.25 * vsw, bg = 0.587 * v - 0.586 * vsu - 1.05 * vsw, bb = 0.114 * v + 0.886 * vsu - 0.2 * vsw; + let r, g, b, a; + for (let i = 0; i < nPixels; i += 4) { + r = data[i + 0]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + data[i + 0] = rr * r + rg * g + rb * b; + data[i + 1] = gr * r + gg * g + gb * b; + data[i + 2] = br * r + bg * g + bb * b; + data[i + 3] = a; // alpha + } + }; + Factory.addGetterSetter(Node, 'hue', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv hue in degrees. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#hue + * @method + * @param {Number} hue value between 0 and 359 + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'saturation', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv saturation. Use with {@link Konva.Filters.HSV} or {@link Konva.Filters.HSL} filter. + * @name Konva.Node#saturation + * @method + * @param {Number} saturation 0 is no change, -1.0 halves the saturation, 1.0 doubles, etc.. + * @returns {Number} + */ + Factory.addGetterSetter(Node, 'value', 0, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set hsv value. Use with {@link Konva.Filters.HSV} filter. + * @name Konva.Node#value + * @method + * @param {Number} value 0 is no change, -1.0 halves the value, 1.0 doubles, etc.. + * @returns {Number} + */ + + /** + * Invert Filter + * @function + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Invert]); + */ + const Invert = function (imageData) { + const data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 4) { + // red + data[i] = 255 - data[i]; + // green + data[i + 1] = 255 - data[i + 1]; + // blue + data[i + 2] = 255 - data[i + 2]; + } + }; + + /* + * ToPolar Filter. Converts image data to polar coordinates. Performs + * w*h*4 pixel reads and w*h pixel writes. The r axis is placed along + * what would be the y axis and the theta axis along the x axis. + * @function + * @author ippo615 + * @memberof Konva.Filters + * @param {ImageData} src, the source image data (what will be transformed) + * @param {ImageData} dst, the destination image data (where it will be saved) + * @param {Object} opt + * @param {Number} [opt.polarCenterX] horizontal location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarCenterY] vertical location for the center of the circle, + * default is in the middle + */ + const ToPolar = function (src, dst, opt) { + const srcPixels = src.data, dstPixels = dst.data, xSize = src.width, ySize = src.height, xMid = opt.polarCenterX || xSize / 2, yMid = opt.polarCenterY || ySize / 2; + // Find the largest radius + let rMax = Math.sqrt(xMid * xMid + yMid * yMid); + let x = xSize - xMid; + let y = ySize - yMid; + const rad = Math.sqrt(x * x + y * y); + rMax = rad > rMax ? rad : rMax; + // We'll be uisng y as the radius, and x as the angle (theta=t) + const rSize = ySize, tSize = xSize; + // We want to cover all angles (0-360) and we need to convert to + // radians (*PI/180) + const conversion = ((360 / tSize) * Math.PI) / 180; + // var x1, x2, x1i, x2i, y1, y2, y1i, y2i, scale; + for (let theta = 0; theta < tSize; theta += 1) { + const sin = Math.sin(theta * conversion); + const cos = Math.cos(theta * conversion); + for (let radius = 0; radius < rSize; radius += 1) { + x = Math.floor(xMid + ((rMax * radius) / rSize) * cos); + y = Math.floor(yMid + ((rMax * radius) / rSize) * sin); + let i = (y * xSize + x) * 4; + const r = srcPixels[i + 0]; + const g = srcPixels[i + 1]; + const b = srcPixels[i + 2]; + const a = srcPixels[i + 3]; + // Store it + //i = (theta * xSize + radius) * 4; + i = (theta + radius * xSize) * 4; + dstPixels[i + 0] = r; + dstPixels[i + 1] = g; + dstPixels[i + 2] = b; + dstPixels[i + 3] = a; + } + } + }; + /* + * FromPolar Filter. Converts image data from polar coordinates back to rectangular. + * Performs w*h*4 pixel reads and w*h pixel writes. + * @function + * @author ippo615 + * @memberof Konva.Filters + * @param {ImageData} src, the source image data (what will be transformed) + * @param {ImageData} dst, the destination image data (where it will be saved) + * @param {Object} opt + * @param {Number} [opt.polarCenterX] horizontal location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarCenterY] vertical location for the center of the circle, + * default is in the middle + * @param {Number} [opt.polarRotation] amount to rotate the image counterclockwis, + * 0 is no rotation, 360 degrees is a full rotation + */ + const FromPolar = function (src, dst, opt) { + const srcPixels = src.data, dstPixels = dst.data, xSize = src.width, ySize = src.height, xMid = opt.polarCenterX || xSize / 2, yMid = opt.polarCenterY || ySize / 2; + // Find the largest radius + let rMax = Math.sqrt(xMid * xMid + yMid * yMid); + let x = xSize - xMid; + let y = ySize - yMid; + const rad = Math.sqrt(x * x + y * y); + rMax = rad > rMax ? rad : rMax; + // We'll be uisng x as the radius, and y as the angle (theta=t) + const rSize = ySize, tSize = xSize, phaseShift = 0; + // We need to convert to degrees and we need to make sure + // it's between (0-360) + // var conversion = tSize/360*180/Math.PI; + //var conversion = tSize/360*180/Math.PI; + let x1, y1; + for (x = 0; x < xSize; x += 1) { + for (y = 0; y < ySize; y += 1) { + const dx = x - xMid; + const dy = y - yMid; + const radius = (Math.sqrt(dx * dx + dy * dy) * rSize) / rMax; + let theta = ((Math.atan2(dy, dx) * 180) / Math.PI + 360 + phaseShift) % 360; + theta = (theta * tSize) / 360; + x1 = Math.floor(theta); + y1 = Math.floor(radius); + let i = (y1 * xSize + x1) * 4; + const r = srcPixels[i + 0]; + const g = srcPixels[i + 1]; + const b = srcPixels[i + 2]; + const a = srcPixels[i + 3]; + // Store it + i = (y * xSize + x) * 4; + dstPixels[i + 0] = r; + dstPixels[i + 1] = g; + dstPixels[i + 2] = b; + dstPixels[i + 3] = a; + } + } + }; + //Konva.Filters.ToPolar = Util._FilterWrapDoubleBuffer(ToPolar); + //Konva.Filters.FromPolar = Util._FilterWrapDoubleBuffer(FromPolar); + // create a temporary canvas for working - shared between multiple calls + /* + * Kaleidoscope Filter. + * @function + * @name Kaleidoscope + * @author ippo615 + * @memberof Konva.Filters + * @example + * node.cache(); + * node.filters([Konva.Filters.Kaleidoscope]); + * node.kaleidoscopePower(3); + * node.kaleidoscopeAngle(45); + */ + const Kaleidoscope = function (imageData) { + const xSize = imageData.width, ySize = imageData.height; + let x, y, xoff, i, r, g, b, a, srcPos, dstPos; + let power = Math.round(this.kaleidoscopePower()); + const angle = Math.round(this.kaleidoscopeAngle()); + const offset = Math.floor((xSize * (angle % 360)) / 360); + if (power < 1) { + return; + } + // Work with our shared buffer canvas + const tempCanvas = Util.createCanvasElement(); + tempCanvas.width = xSize; + tempCanvas.height = ySize; + const scratchData = tempCanvas + .getContext('2d') + .getImageData(0, 0, xSize, ySize); + Util.releaseCanvas(tempCanvas); + // Convert thhe original to polar coordinates + ToPolar(imageData, scratchData, { + polarCenterX: xSize / 2, + polarCenterY: ySize / 2, + }); + // Determine how big each section will be, if it's too small + // make it bigger + let minSectionSize = xSize / Math.pow(2, power); + while (minSectionSize <= 8) { + minSectionSize = minSectionSize * 2; + power -= 1; + } + minSectionSize = Math.ceil(minSectionSize); + let sectionSize = minSectionSize; + // Copy the offset region to 0 + // Depending on the size of filter and location of the offset we may need + // to copy the section backwards to prevent it from rewriting itself + let xStart = 0, xEnd = sectionSize, xDelta = 1; + if (offset + minSectionSize > xSize) { + xStart = sectionSize; + xEnd = 0; + xDelta = -1; + } + for (y = 0; y < ySize; y += 1) { + for (x = xStart; x !== xEnd; x += xDelta) { + xoff = Math.round(x + offset) % xSize; + srcPos = (xSize * y + xoff) * 4; + r = scratchData.data[srcPos + 0]; + g = scratchData.data[srcPos + 1]; + b = scratchData.data[srcPos + 2]; + a = scratchData.data[srcPos + 3]; + dstPos = (xSize * y + x) * 4; + scratchData.data[dstPos + 0] = r; + scratchData.data[dstPos + 1] = g; + scratchData.data[dstPos + 2] = b; + scratchData.data[dstPos + 3] = a; + } + } + // Perform the actual effect + for (y = 0; y < ySize; y += 1) { + sectionSize = Math.floor(minSectionSize); + for (i = 0; i < power; i += 1) { + for (x = 0; x < sectionSize + 1; x += 1) { + srcPos = (xSize * y + x) * 4; + r = scratchData.data[srcPos + 0]; + g = scratchData.data[srcPos + 1]; + b = scratchData.data[srcPos + 2]; + a = scratchData.data[srcPos + 3]; + dstPos = (xSize * y + sectionSize * 2 - x - 1) * 4; + scratchData.data[dstPos + 0] = r; + scratchData.data[dstPos + 1] = g; + scratchData.data[dstPos + 2] = b; + scratchData.data[dstPos + 3] = a; + } + sectionSize *= 2; + } + } + // Convert back from polar coordinates + FromPolar(scratchData, imageData, { polarRotation: 0 }); + }; + /** + * get/set kaleidoscope power. Use with {@link Konva.Filters.Kaleidoscope} filter. + * @name Konva.Node#kaleidoscopePower + * @method + * @param {Integer} power of kaleidoscope + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'kaleidoscopePower', 2, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set kaleidoscope angle. Use with {@link Konva.Filters.Kaleidoscope} filter. + * @name Konva.Node#kaleidoscopeAngle + * @method + * @param {Integer} degrees + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'kaleidoscopeAngle', 0, getNumberValidator(), Factory.afterSetFilter); + + function pixelAt(idata, x, y) { + let idx = (y * idata.width + x) * 4; + const d = []; + d.push(idata.data[idx++], idata.data[idx++], idata.data[idx++], idata.data[idx++]); + return d; + } + function rgbDistance(p1, p2) { + return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + + Math.pow(p1[1] - p2[1], 2) + + Math.pow(p1[2] - p2[2], 2)); + } + function rgbMean(pTab) { + const m = [0, 0, 0]; + for (let i = 0; i < pTab.length; i++) { + m[0] += pTab[i][0]; + m[1] += pTab[i][1]; + m[2] += pTab[i][2]; + } + m[0] /= pTab.length; + m[1] /= pTab.length; + m[2] /= pTab.length; + return m; + } + function backgroundMask(idata, threshold) { + const rgbv_no = pixelAt(idata, 0, 0); + const rgbv_ne = pixelAt(idata, idata.width - 1, 0); + const rgbv_so = pixelAt(idata, 0, idata.height - 1); + const rgbv_se = pixelAt(idata, idata.width - 1, idata.height - 1); + const thres = threshold || 10; + if (rgbDistance(rgbv_no, rgbv_ne) < thres && + rgbDistance(rgbv_ne, rgbv_se) < thres && + rgbDistance(rgbv_se, rgbv_so) < thres && + rgbDistance(rgbv_so, rgbv_no) < thres) { + // Mean color + const mean = rgbMean([rgbv_ne, rgbv_no, rgbv_se, rgbv_so]); + // Mask based on color distance + const mask = []; + for (let i = 0; i < idata.width * idata.height; i++) { + const d = rgbDistance(mean, [ + idata.data[i * 4], + idata.data[i * 4 + 1], + idata.data[i * 4 + 2], + ]); + mask[i] = d < thres ? 0 : 255; + } + return mask; + } + } + function applyMask(idata, mask) { + for (let i = 0; i < idata.width * idata.height; i++) { + idata.data[4 * i + 3] = mask[i]; + } + } + function erodeMask(mask, sw, sh) { + const weights = [1, 1, 1, 1, 0, 1, 1, 1, 1]; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const maskResult = []; + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const so = y * sw + x; + let a = 0; + for (let cy = 0; cy < side; cy++) { + for (let cx = 0; cx < side; cx++) { + const scy = y + cy - halfSide; + const scx = x + cx - halfSide; + if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { + const srcOff = scy * sw + scx; + const wt = weights[cy * side + cx]; + a += mask[srcOff] * wt; + } + } + } + maskResult[so] = a === 255 * 8 ? 255 : 0; + } + } + return maskResult; + } + function dilateMask(mask, sw, sh) { + const weights = [1, 1, 1, 1, 1, 1, 1, 1, 1]; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const maskResult = []; + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const so = y * sw + x; + let a = 0; + for (let cy = 0; cy < side; cy++) { + for (let cx = 0; cx < side; cx++) { + const scy = y + cy - halfSide; + const scx = x + cx - halfSide; + if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { + const srcOff = scy * sw + scx; + const wt = weights[cy * side + cx]; + a += mask[srcOff] * wt; + } + } + } + maskResult[so] = a >= 255 * 4 ? 255 : 0; + } + } + return maskResult; + } + function smoothEdgeMask(mask, sw, sh) { + const weights = [1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9, 1 / 9]; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const maskResult = []; + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const so = y * sw + x; + let a = 0; + for (let cy = 0; cy < side; cy++) { + for (let cx = 0; cx < side; cx++) { + const scy = y + cy - halfSide; + const scx = x + cx - halfSide; + if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) { + const srcOff = scy * sw + scx; + const wt = weights[cy * side + cx]; + a += mask[srcOff] * wt; + } + } + } + maskResult[so] = a; + } + } + return maskResult; + } + /** + * Mask Filter + * @function + * @name Mask + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Mask]); + * node.threshold(200); + */ + const Mask = function (imageData) { + // Detect pixels close to the background color + const threshold = this.threshold(); + let mask = backgroundMask(imageData, threshold); + if (mask) { + // Erode + mask = erodeMask(mask, imageData.width, imageData.height); + // Dilate + mask = dilateMask(mask, imageData.width, imageData.height); + // Gradient + mask = smoothEdgeMask(mask, imageData.width, imageData.height); + // Apply mask + applyMask(imageData, mask); + } + return imageData; + }; + Factory.addGetterSetter(Node, 'threshold', 0, getNumberValidator(), Factory.afterSetFilter); + + /** + * Noise Filter. Randomly adds or substracts to the color channels + * @function + * @name Noise + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Noise]); + * node.noise(0.8); + */ + const Noise = function (imageData) { + const amount = this.noise() * 255, data = imageData.data, nPixels = data.length, half = amount / 2; + for (let i = 0; i < nPixels; i += 4) { + data[i + 0] += half - 2 * half * Math.random(); + data[i + 1] += half - 2 * half * Math.random(); + data[i + 2] += half - 2 * half * Math.random(); + } + }; + Factory.addGetterSetter(Node, 'noise', 0.2, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set noise amount. Must be a value between 0 and 1. Use with {@link Konva.Filters.Noise} filter. + * @name Konva.Node#noise + * @method + * @param {Number} noise + * @returns {Number} + */ + + /** + * Pixelate Filter. Averages groups of pixels and redraws + * them as larger pixels + * @function + * @name Pixelate + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Pixelate]); + * node.pixelSize(10); + */ + const Pixelate = function (imageData) { + let pixelSize = Math.ceil(this.pixelSize()), width = imageData.width, height = imageData.height, x, y, i, + //pixelsPerBin = pixelSize * pixelSize, + red, green, blue, alpha, nBinsX = Math.ceil(width / pixelSize), nBinsY = Math.ceil(height / pixelSize), xBinStart, xBinEnd, yBinStart, yBinEnd, xBin, yBin, pixelsInBin, data = imageData.data; + if (pixelSize <= 0) { + Util.error('pixelSize value can not be <= 0'); + return; + } + for (xBin = 0; xBin < nBinsX; xBin += 1) { + for (yBin = 0; yBin < nBinsY; yBin += 1) { + // Initialize the color accumlators to 0 + red = 0; + green = 0; + blue = 0; + alpha = 0; + // Determine which pixels are included in this bin + xBinStart = xBin * pixelSize; + xBinEnd = xBinStart + pixelSize; + yBinStart = yBin * pixelSize; + yBinEnd = yBinStart + pixelSize; + // Add all of the pixels to this bin! + pixelsInBin = 0; + for (x = xBinStart; x < xBinEnd; x += 1) { + if (x >= width) { + continue; + } + for (y = yBinStart; y < yBinEnd; y += 1) { + if (y >= height) { + continue; + } + i = (width * y + x) * 4; + red += data[i + 0]; + green += data[i + 1]; + blue += data[i + 2]; + alpha += data[i + 3]; + pixelsInBin += 1; + } + } + // Make sure the channels are between 0-255 + red = red / pixelsInBin; + green = green / pixelsInBin; + blue = blue / pixelsInBin; + alpha = alpha / pixelsInBin; + // Draw this bin + for (x = xBinStart; x < xBinEnd; x += 1) { + if (x >= width) { + continue; + } + for (y = yBinStart; y < yBinEnd; y += 1) { + if (y >= height) { + continue; + } + i = (width * y + x) * 4; + data[i + 0] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = alpha; + } + } + } + } + }; + Factory.addGetterSetter(Node, 'pixelSize', 8, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set pixel size. Use with {@link Konva.Filters.Pixelate} filter. + * @name Konva.Node#pixelSize + * @method + * @param {Integer} pixelSize + * @returns {Integer} + */ + + /** + * Posterize Filter. Adjusts the channels so that there are no more + * than n different values for that channel. This is also applied + * to the alpha channel. + * @function + * @name Posterize + * @author ippo615 + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Posterize]); + * node.levels(0.8); // between 0 and 1 + */ + const Posterize = function (imageData) { + // level must be between 1 and 255 + const levels = Math.round(this.levels() * 254) + 1, data = imageData.data, len = data.length, scale = 255 / levels; + for (let i = 0; i < len; i += 1) { + data[i] = Math.floor(data[i] / scale) * scale; + } + }; + Factory.addGetterSetter(Node, 'levels', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set levels. Must be a number between 0 and 1. Use with {@link Konva.Filters.Posterize} filter. + * @name Konva.Node#levels + * @method + * @param {Number} level between 0 and 1 + * @returns {Number} + */ + + /** + * RGB Filter + * @function + * @name RGB + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.RGB]); + * node.blue(120); + * node.green(200); + */ + const RGB = function (imageData) { + const data = imageData.data, nPixels = data.length, red = this.red(), green = this.green(), blue = this.blue(); + for (let i = 0; i < nPixels; i += 4) { + const brightness = (0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]) / 255; + data[i] = brightness * red; // r + data[i + 1] = brightness * green; // g + data[i + 2] = brightness * blue; // b + data[i + 3] = data[i + 3]; // alpha + } + }; + Factory.addGetterSetter(Node, 'red', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter red value. Use with {@link Konva.Filters.RGB} filter. + * @name red + * @method + * @memberof Konva.Node.prototype + * @param {Integer} red value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'green', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter green value. Use with {@link Konva.Filters.RGB} filter. + * @name green + * @method + * @memberof Konva.Node.prototype + * @param {Integer} green value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'blue', 0, RGBComponent, Factory.afterSetFilter); + /** + * get/set filter blue value. Use with {@link Konva.Filters.RGB} filter. + * @name blue + * @method + * @memberof Konva.Node.prototype + * @param {Integer} blue value between 0 and 255 + * @returns {Integer} + */ + + /** + * RGBA Filter + * @function + * @name RGBA + * @memberof Konva.Filters + * @param {Object} imageData + * @author codefo + * @example + * node.cache(); + * node.filters([Konva.Filters.RGBA]); + * node.blue(120); + * node.green(200); + * node.alpha(0.3); + */ + const RGBA = function (imageData) { + const data = imageData.data, nPixels = data.length, red = this.red(), green = this.green(), blue = this.blue(), alpha = this.alpha(); + for (let i = 0; i < nPixels; i += 4) { + const ia = 1 - alpha; + data[i] = red * alpha + data[i] * ia; // r + data[i + 1] = green * alpha + data[i + 1] * ia; // g + data[i + 2] = blue * alpha + data[i + 2] * ia; // b + } + }; + Factory.addGetterSetter(Node, 'red', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter red value. Use with {@link Konva.Filters.RGBA} filter. + * @name red + * @method + * @memberof Konva.Node.prototype + * @param {Integer} red value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'green', 0, function (val) { + this._filterUpToDate = false; + if (val > 255) { + return 255; + } + else if (val < 0) { + return 0; + } + else { + return Math.round(val); + } + }); + /** + * get/set filter green value. Use with {@link Konva.Filters.RGBA} filter. + * @name green + * @method + * @memberof Konva.Node.prototype + * @param {Integer} green value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'blue', 0, RGBComponent, Factory.afterSetFilter); + /** + * get/set filter blue value. Use with {@link Konva.Filters.RGBA} filter. + * @name blue + * @method + * @memberof Konva.Node.prototype + * @param {Integer} blue value between 0 and 255 + * @returns {Integer} + */ + Factory.addGetterSetter(Node, 'alpha', 1, function (val) { + this._filterUpToDate = false; + if (val > 1) { + return 1; + } + else if (val < 0) { + return 0; + } + else { + return val; + } + }); + /** + * get/set filter alpha value. Use with {@link Konva.Filters.RGBA} filter. + * @name alpha + * @method + * @memberof Konva.Node.prototype + * @param {Float} alpha value between 0 and 1 + * @returns {Float} + */ + + // based on https://stackoverflow.com/questions/1061093/how-is-a-sepia-tone-created + /** + * @function + * @name Sepia + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Sepia]); + */ + const Sepia = function (imageData) { + const data = imageData.data, nPixels = data.length; + for (let i = 0; i < nPixels; i += 4) { + const r = data[i + 0]; + const g = data[i + 1]; + const b = data[i + 2]; + data[i + 0] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); + data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); + data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); + } + }; + + /** + * Solarize Filter + * Pixastic Lib - Solarize filter - v0.1.0 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * License: [http://www.pixastic.com/lib/license.txt] + * @function + * @name Solarize + * @memberof Konva.Filters + * @param {Object} imageData + * @example + * node.cache(); + * node.filters([Konva.Filters.Solarize]); + */ + const Solarize = function (imageData) { + const data = imageData.data, w = imageData.width, h = imageData.height, w4 = w * 4; + let y = h; + do { + const offsetY = (y - 1) * w4; + let x = w; + do { + const offset = offsetY + (x - 1) * 4; + let r = data[offset]; + let g = data[offset + 1]; + let b = data[offset + 2]; + if (r > 127) { + r = 255 - r; + } + if (g > 127) { + g = 255 - g; + } + if (b > 127) { + b = 255 - b; + } + data[offset] = r; + data[offset + 1] = g; + data[offset + 2] = b; + } while (--x); + } while (--y); + }; + + /** + * Threshold Filter. Pushes any value above the mid point to + * the max and any value below the mid point to the min. + * This affects the alpha channel. + * @function + * @name Threshold + * @memberof Konva.Filters + * @param {Object} imageData + * @author ippo615 + * @example + * node.cache(); + * node.filters([Konva.Filters.Threshold]); + * node.threshold(0.1); + */ + const Threshold = function (imageData) { + const level = this.threshold() * 255, data = imageData.data, len = data.length; + for (let i = 0; i < len; i += 1) { + data[i] = data[i] < level ? 0 : 255; + } + }; + Factory.addGetterSetter(Node, 'threshold', 0.5, getNumberValidator(), Factory.afterSetFilter); + /** + * get/set threshold. Must be a value between 0 and 1. Use with {@link Konva.Filters.Threshold} or {@link Konva.Filters.Mask} filter. + * @name threshold + * @method + * @memberof Konva.Node.prototype + * @param {Number} threshold + * @returns {Number} + */ + + // we need to import core of the Konva and then extend it with all additional objects + const Konva = Konva$1.Util._assign(Konva$1, { + Arc, + Arrow, + Circle, + Ellipse, + Image, + Label, + Tag, + Line, + Path, + Rect, + RegularPolygon, + Ring, + Sprite, + Star, + Text, + TextPath, + Transformer, + Wedge, + /** + * @namespace Filters + * @memberof Konva + */ + Filters: { + Blur, + Brighten, + Contrast, + Emboss, + Enhance, + Grayscale, + HSL, + HSV, + Invert, + Kaleidoscope, + Mask, + Noise, + Pixelate, + Posterize, + RGB, + RGBA, + Sepia, + Solarize, + Threshold, + }, + }); + + return Konva; + +}));