From 1d932bf76cc2234540e8206ec6de352ed4fea697 Mon Sep 17 00:00:00 2001 From: Anton Lavrenov Date: Sun, 4 Aug 2019 09:41:57 +0700 Subject: [PATCH] better mulitouch --- konva.js | 211 +++++++++++++++++------- konva.min.js | 4 +- src/Container.ts | 5 +- src/DragAndDrop.ts | 3 + src/Global.ts | 24 +++ src/Node.ts | 16 +- src/PointerEvents.ts | 2 +- src/Stage.ts | 176 ++++++++++++++------ test/functional/TouchEvents-test.js | 243 +++++++++++++++++++++++++++- test/runner.js | 79 +++++++-- test/unit/DragAndDrop-test.js | 168 +++++++++++++++++++ 11 files changed, 802 insertions(+), 129 deletions(-) diff --git a/konva.js b/konva.js index ccea110f..e7375f06 100644 --- a/konva.js +++ b/konva.js @@ -85,6 +85,30 @@ }, enableTrace: false, _pointerEventsEnabled: false, + /** + * 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 captureTouchEventsEnabled + * @default false + * @name captureTouchEventsEnabled + * @memberof Konva + * @example + * Konva.captureTouchEventsEnabled = true; + */ + captureTouchEventsEnabled: false, // TODO: move that to stage? listenClickTap: false, inDblClickWindow: false, @@ -2319,6 +2343,8 @@ y: 0 }, node: null, + _nodes: [], + _offsets: [], // methods _drag: function (evt) { var node = DD.node; @@ -2483,6 +2509,7 @@ this._filterUpToDate = false; this._isUnderCache = false; this.children = emptyChildren; + this._dragEventId = null; this.setAttrs(config); // event bindings for cache handling this.on(TRANSFORM_CHANGE_STR, function () { @@ -3860,8 +3887,8 @@ config = config || {}; var box = this.getClientRect(); var stage = this.getStage(), x = config.x !== undefined ? config.x : box.x, y = config.y !== undefined ? config.y : box.y, pixelRatio = config.pixelRatio || 1, canvas = new SceneCanvas({ - width: config.width || box.width || (stage ? stage.getWidth() : 0), - height: config.height || box.height || (stage ? stage.getHeight() : 0), + width: config.width || box.width || (stage ? stage.width() : 0), + height: config.height || box.height || (stage ? stage.height() : 0), pixelRatio: pixelRatio }), context = canvas.getContext(); context.save(); @@ -4241,7 +4268,10 @@ } }; Node.prototype._setDragPosition = function (evt) { - var pos = this.getStage().getPointerPosition(), dbf = this.dragBoundFunc(); + // const pointers = this.getStage().getPointersPositions(); + // const pos = pointers.find(p => p.id === this._dragEventId); + var pos = this.getStage().getPointerPosition(); + var dbf = this.dragBoundFunc(); if (!pos) { return; } @@ -5274,7 +5304,9 @@ }; Container.prototype.shouldDrawHit = function (canvas) { var layer = this.getLayer(); - var layerUnderDrag = DD.isDragging && DD.anim.getLayers().indexOf(layer) !== -1; + var layerUnderDrag = DD.isDragging && + !Konva.hitOnDragEnabled && + DD.anim.getLayers().indexOf(layer) !== -1; return ((canvas && canvas.isCache) || (layer && layer.hitGraphEnabled() && this.isVisible() && !layerUnderDrag)); }; @@ -5480,9 +5512,7 @@ if (!shape) return; var stage = shape.getStage(); - if (stage && stage.content) { - stage.content.releasePointerCapture(pointerId); - } + if (stage && stage.content) ; Captures.delete(pointerId); shape._fire('lostpointercapture', createEvent(new PointerEvent('lostpointercapture'))); } @@ -5642,10 +5672,14 @@ * @returns {Object} */ Stage.prototype.getPointerPosition = function () { - if (!this.pointerPos) { + var pos = this._pointerPositions[0]; + if (!pos) { Util.warn(NO_POINTERS_MESSAGE); } - return this.pointerPos; + return pos; + }; + Stage.prototype.getPointersPositions = function () { + return this._pointerPositions; }; Stage.prototype.getStage = function () { return this; @@ -5797,6 +5831,7 @@ }); } this.pointerPos = undefined; + this._pointerPositions = []; this._fire(CONTENT_MOUSEOUT, { evt: evt }); }; Stage.prototype._mousemove = function (evt) { @@ -5961,18 +5996,28 @@ this._fire(CONTENT_CONTEXTMENU, { evt: evt }); }; Stage.prototype._touchstart = function (evt) { + var _this = this; this.setPointersPositions(evt); - var shape = this.getIntersection(this.getPointerPosition()); - Konva.listenClickTap = true; - if (shape && shape.isListening()) { - this.tapStartShape = shape; - shape._fireAndBubble(TOUCHSTART, { evt: evt }); + var triggeredOnShape = false; + this._changedPointerPositions.forEach(function (pos) { + var shape = _this.getIntersection(pos); + Konva.listenClickTap = true; + var hasShape = shape && shape.isListening(); + if (!hasShape) { + return; + } + if (Konva.captureTouchEventsEnabled) { + shape.setPointerCapture(pos.id); + } + _this.tapStartShape = shape; + shape._fireAndBubble(TOUCHSTART, { evt: evt }, _this); + triggeredOnShape = true; // only call preventDefault if the shape is listening for events if (shape.isListening() && shape.preventDefault() && evt.cancelable) { evt.preventDefault(); } - } - else { + }); + if (!triggeredOnShape) { this._fire(TOUCHSTART, { evt: evt, target: this, @@ -5982,9 +6027,46 @@ // content event this._fire(CONTENT_TOUCHSTART, { evt: evt }); }; - Stage.prototype._touchend = function (evt) { + Stage.prototype._touchmove = function (evt) { + var _this = this; this.setPointersPositions(evt); - var shape = this.getIntersection(this.getPointerPosition()), clickEndShape = this.clickEndShape, fireDblClick = false; + if (!DD.isDragging) { + var triggeredOnShape = false; + var processedShapesIds = {}; + this._changedPointerPositions.forEach(function (pos) { + var shape = getCapturedShape(pos.id) || _this.getIntersection(pos); + var hasShape = shape && shape.isListening(); + if (!hasShape) { + return; + } + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + shape._fireAndBubble(TOUCHMOVE, { evt: evt }); + triggeredOnShape = true; + // only call preventDefault if the shape is listening for events + if (shape.isListening() && shape.preventDefault() && evt.cancelable) { + evt.preventDefault(); + } + }); + if (!triggeredOnShape) { + this._fire(TOUCHMOVE, { + evt: evt, + target: this, + currentTarget: this + }); + } + this._fire(CONTENT_TOUCHMOVE, { evt: evt }); + } + if (DD.isDragging && DD.node.preventDefault() && evt.cancelable) { + evt.preventDefault(); + } + }; + Stage.prototype._touchend = function (evt) { + var _this = this; + this.setPointersPositions(evt); + var clickEndShape = this.clickEndShape, fireDblClick = false; if (Konva.inDblClickWindow) { fireDblClick = true; clearTimeout(this.dblTimeout); @@ -5997,13 +6079,29 @@ this.dblTimeout = setTimeout(function () { Konva.inDblClickWindow = false; }, Konva.dblClickWindow); - if (shape && shape.isListening()) { - this.clickEndShape = shape; + var triggeredOnShape = false; + var processedShapesIds = {}; + this._changedPointerPositions.forEach(function (pos) { + var shape = getCapturedShape(pos.id) || + _this.getIntersection(pos); + if (shape) { + shape.releaseCapture(pos.id); + } + var hasShape = shape && shape.isListening(); + if (!hasShape) { + return; + } + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + _this.clickEndShape = shape; shape._fireAndBubble(TOUCHEND, { evt: evt }); + triggeredOnShape = true; // detect if tap or double tap occurred if (Konva.listenClickTap && - this.tapStartShape && - shape._id === this.tapStartShape._id) { + _this.tapStartShape && + shape._id === _this.tapStartShape._id) { shape._fireAndBubble(TAP, { evt: evt }); if (fireDblClick && clickEndShape && clickEndShape === shape) { shape._fireAndBubble(DBL_TAP, { evt: evt }); @@ -6013,19 +6111,19 @@ if (shape.isListening() && shape.preventDefault() && evt.cancelable) { evt.preventDefault(); } - } - else { + }); + if (!triggeredOnShape) { this._fire(TOUCHEND, { evt: evt, target: this, currentTarget: this }); - if (Konva.listenClickTap) { - this._fire(TAP, { evt: evt, target: this, currentTarget: this }); - } - if (fireDblClick) { - this._fire(DBL_TAP, { - evt: evt, - target: this, - currentTarget: this - }); - } + } + if (Konva.listenClickTap) { + this._fire(TAP, { evt: evt, target: this, currentTarget: this }); + } + if (fireDblClick) { + this._fire(DBL_TAP, { + evt: evt, + target: this, + currentTarget: this + }); } // content events this._fire(CONTENT_TOUCHEND, { evt: evt }); @@ -6037,31 +6135,6 @@ } Konva.listenClickTap = false; }; - Stage.prototype._touchmove = function (evt) { - this.setPointersPositions(evt); - var shape; - if (!DD.isDragging) { - shape = this.getIntersection(this.getPointerPosition()); - if (shape && shape.isListening()) { - shape._fireAndBubble(TOUCHMOVE, { evt: evt }); - // only call preventDefault if the shape is listening for events - if (shape.isListening() && shape.preventDefault() && evt.cancelable) { - evt.preventDefault(); - } - } - else { - this._fire(TOUCHMOVE, { - evt: evt, - target: this, - currentTarget: this - }); - } - this._fire(CONTENT_TOUCHMOVE, { evt: evt }); - } - if (DD.isDragging && DD.node.preventDefault() && evt.cancelable) { - evt.preventDefault(); - } - }; Stage.prototype._wheel = function (evt) { this.setPointersPositions(evt); var shape = this.getIntersection(this.getPointerPosition()); @@ -6141,10 +6214,29 @@ * }); */ Stage.prototype.setPointersPositions = function (evt) { + var _this = this; var contentPosition = this._getContentPosition(), 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 = []; + Collection.prototype.each.call(evt.touches, function (touch) { + _this._pointerPositions.push({ + id: touch.identifier, + x: touch.clientX - contentPosition.left, + y: touch.clientY - contentPosition.top + }); + }); + Collection.prototype.each.call(evt.changedTouches || evt.touches, function (touch) { + _this._changedPointerPositions.push({ + id: touch.identifier, + x: touch.clientX - contentPosition.left, + y: touch.clientY - contentPosition.top + }); + }); // currently, only handle one finger if (evt.touches.length > 0) { var touch = evt.touches[0]; @@ -6157,6 +6249,7 @@ // mouse events x = evt.clientX - contentPosition.left; y = evt.clientY - contentPosition.top; + this._pointerPositions = [{ x: x, y: y }]; } if (x !== null && y !== null) { this.pointerPos = { diff --git a/konva.min.js b/konva.min.js index ac6fe0ca..75d4d309 100644 --- a/konva.min.js +++ b/konva.min.js @@ -3,10 +3,10 @@ * Konva JavaScript Framework v3.4.1 * http://konvajs.org/ * Licensed under the MIT - * Date: Thu Jul 18 2019 + * Date: Thu Aug 01 2019 * * Original work Copyright (C) 2011 - 2013 by Eric Rowell (KineticJS) * Modified work Copyright (C) 2014 - present by Anton Lavrenov (Konva) * * @license - */var e=Math.PI/180;var t=function(t){var e=t.toLowerCase(),i=/(chrome)[ /]([\w.]+)/.exec(e)||/(webkit)[ /]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ /]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[],n=!!t.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i),r=!!t.match(/IEMobile/i);return{browser:i[1]||"",version:i[2]||"0",isIE:function(t){var e=t.indexOf("msie ");if(0>16&255,g:e>>8&255,b:255&e}},getRandomColor:function(){for(var t=(16777215*Math.random()<<0).toString(16);t.length<6;)t="0"+t;return"#"+t},get:function(t,e){return void 0===t?e:t},getRGB:function(t){var e;return t in l?{r:(e=l[t])[0],g:e[1],b:e[2]}:"#"===t[0]?this._hexToRgb(t.substring(1)):"rgb("===t.substr(0,4)?(e=d.exec(t.replace(/ /g,"")),{r:parseInt(e[1],10),g:parseInt(e[2],10),b:parseInt(e[3],10)}):{r:0,g:0,b:0}},colorToRGBA:function(t){return t=t||"black",D._namedColorToRBA(t)||D._hex3ColorToRGBA(t)||D._hex6ColorToRGBA(t)||D._rgbColorToRGBA(t)||D._rgbaColorToRGBA(t)},_namedColorToRBA:function(t){var e=l[t.toLowerCase()];return e?{r:e[0],g:e[1],b:e[2],a:1}:null},_rgbColorToRGBA:function(t){if(0===t.indexOf("rgb(")){var e=(t=t.match(/rgb\(([^)]+)\)/)[1]).split(/ *, */).map(Number);return{r:e[0],g:e[1],b:e[2],a:1}}},_rgbaColorToRGBA:function(t){if(0===t.indexOf("rgba(")){var e=(t=t.match(/rgba\(([^)]+)\)/)[1]).split(/ *, */).map(Number);return{r:e[0],g:e[1],b:e[2],a:e[3]}}},_hex6ColorToRGBA:function(t){if("#"===t[0]&&7===t.length)return{r:parseInt(t.slice(1,3),16),g:parseInt(t.slice(3,5),16),b:parseInt(t.slice(5,7),16),a:1}},_hex3ColorToRGBA:function(t){if("#"===t[0]&&4===t.length)return{r:parseInt(t[1]+t[1],16),g:parseInt(t[2]+t[2],16),b:parseInt(t[3]+t[3],16),a:1}},haveIntersection:function(t,e){return!(e.x>t.x+t.width||e.x+e.widtht.y+t.height||e.y+e.heighte.length){var a=e;e=t,t=a}for(n=0;n=this.parent.children.length)&&D.warn("Unexpected value "+t+" 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)+".");var e=this.index;return this.parent.children.splice(e,1),this.parent.children.splice(t,0,this),this.parent._setChildrenIndices(),this},s.prototype.getAbsoluteOpacity=function(){return this._getCache(N,this._getAbsoluteOpacity)},s.prototype._getAbsoluteOpacity=function(){var t=this.opacity(),e=this.getParent();return e&&!e._isUnderCache&&(t*=e.getAbsoluteOpacity()),t},s.prototype.moveTo=function(t){return this.getParent()!==t&&(this._remove(),t.add(this)),this},s.prototype.toObject=function(){var t,e,i,n={},r=this.getAttrs();for(t in n.attrs={},r)e=r[t],D.isObject(e)&&!D._isPlainObject(e)&&!D._isArray(e)||(i="function"==typeof this[t]&&this[t],delete r[t],(i?i.call(this):null)!==(r[t]=e)&&(n.attrs[t]=e));return n.className=this.getClassName(),D._prepareToStringify(n)},s.prototype.toJSON=function(){return JSON.stringify(this.toObject())},s.prototype.getParent=function(){return this.parent},s.prototype.findAncestors=function(t,e,i){var n=[];e&&this._isMatch(t)&&n.push(this);for(var r=this.parent;r;){if(r===i)return n;r._isMatch(t)&&n.push(r),r=r.parent}return n},s.prototype.isAncestorOf=function(t){return!1},s.prototype.findAncestor=function(t,e,i){return this.findAncestors(t,e,i)[0]},s.prototype._isMatch=function(t){if(!t)return!1;if("function"==typeof t)return t(this);var e,i,n=t.replace(/ /g,"").split(","),r=n.length;for(e=0;ethis.duration?this.yoyo?(this._time=this.duration,this.reverse()):this.finish():t<0?this.yoyo?(this._time=0,this.play()):this.reset():(this._time=t,this.update())},t.prototype.getTime=function(){return this._time},t.prototype.setPosition=function(t){this.prevPos=this._pos,this.propFunc(t),this._pos=t},t.prototype.getPosition=function(t){return void 0===t&&(t=this._time),this.func(t,this.begin,this._change,this.duration)},t.prototype.play=function(){this.state=2,this._startTime=this.getTimer()-this._time,this.onEnterFrame(),this.fire("onPlay")},t.prototype.reverse=function(){this.state=3,this._time=this.duration-this._time,this._startTime=this.getTimer()-this._time,this.onEnterFrame(),this.fire("onReverse")},t.prototype.seek=function(t){this.pause(),this._time=t,this.update(),this.fire("onSeek")},t.prototype.reset=function(){this.pause(),this._time=0,this.update(),this.fire("onReset")},t.prototype.finish=function(){this.pause(),this._time=this.duration,this.update(),this.fire("onFinish")},t.prototype.update=function(){this.setPosition(this.getPosition(this._time))},t.prototype.onEnterFrame=function(){var t=this.getTimer()-this._startTime;2===this.state?this.setTime(t):3===this.state&&this.setTime(this.duration-t)},t.prototype.pause=function(){this.state=1,this.fire("onPause")},t.prototype.getTimer=function(){return(new Date).getTime()},t}(),ie=function(){function u(t){var e,i,n=this,r=t.node,o=r._id,a=t.easing||ne.Linear,s=!!t.yoyo;e=void 0===t.duration?.3:0===t.duration?.001:t.duration,this.node=r,this._id=$t++;var h=r.getLayer()||(r instanceof O.Stage?r.getLayers():null);for(i in h||D.error("Tween constructor have `node` that is not in a layer. Please add node into layer first."),this.anim=new I(function(){n.tween.onEnterFrame()},h),this.tween=new ee(i,function(t){n._tweenFunc(t)},a,0,1,1e3*e,s),this._addListeners(),u.attrs[o]||(u.attrs[o]={}),u.attrs[o][this._id]||(u.attrs[o][this._id]={}),u.tweens[o]||(u.tweens[o]={}),t)void 0===Zt[i]&&this._addAttr(i,t[i]);this.reset(),this.onFinish=t.onFinish,this.onReset=t.onReset}return u.prototype._addAttr=function(t,e){var i,n,r,o,a,s,h,l,c=this.node,d=c._id;if((r=u.tweens[d][t])&&delete u.attrs[d][r][t],i=c.getAttr(t),D._isArray(e))if(n=[],a=Math.max(e.length,i.length),"points"===t&&e.length!==i.length&&(e.length>i.length?(h=i,i=D._prepareArrayForTween(i,e,c.closed())):(s=e,e=D._prepareArrayForTween(e,i,c.closed()))),0===t.indexOf("fill"))for(o=0;othis.dataArray[i].pathLength;)t-=this.dataArray[i].pathLength,++i;if(i===n)return{x:(e=this.dataArray[i-1].points.slice(-2))[0],y:e[1]};if(t<.01)return{x:(e=this.dataArray[i].points.slice(0,2))[0],y:e[1]};var r=this.dataArray[i],o=r.points;switch(r.command){case"L":return u.getPointOnLine(t,r.start.x,r.start.y,o[0],o[1]);case"C":return u.getPointOnCubicBezier(t/r.pathLength,r.start.x,r.start.y,o[0],o[1],o[2],o[3],o[4],o[5]);case"Q":return u.getPointOnQuadraticBezier(t/r.pathLength,r.start.x,r.start.y,o[0],o[1],o[2],o[3]);case"A":var a=o[0],s=o[1],h=o[2],l=o[3],c=o[4],d=o[5],p=o[6];return c+=d*t/r.pathLength,u.getPointOnEllipticalArc(a,s,h,l,c,p)}return null},u.getLineLength=function(t,e,i,n){return Math.sqrt((i-t)*(i-t)+(n-e)*(n-e))},u.getPointOnLine=function(t,e,i,n,r,o,a){void 0===o&&(o=e),void 0===a&&(a=i);var s=(r-i)/(n-e+1e-8),h=Math.sqrt(t*t/(1+s*s));n>>1,P=_.slice(0,1+k),T=this._getTextWidth(P)+v;T<=l?(b=1+k,w=P+(g?"…":""),C=T):x=k}if(!w)break;if(f){var M,A=_[w.length];0<(M=(" "===A||"-"===A)&&C<=l?w.length:Math.max(w.lastIndexOf(" "),w.lastIndexOf("-"))+1)&&(b=M,w=w.slice(0,b),C=this._getTextWidth(w))}if(w=w.trimRight(),this._addTextLine(w),i=Math.max(i,C),d+=n,!u||s&&ce?g=me.getPointOnLine(e,f.x,f.y,v.points[0],v.points[1],f.x,f.y):v=void 0;break;case"A":var a=v.points[4],s=v.points[5],h=v.points[4]+s;0===m?m=a+1e-8:iv.pathLength?1e-8:e/v.pathLength:ithis.findOne(".bottom-right").x()?-1:1;e=n*this.cos*c,i=n*this.sin*c,this.findOne(".top-left").x(this.findOne(".bottom-right").x()-e),this.findOne(".top-left").y(this.findOne(".bottom-right").y()-i)}}else if("top-center"===this.movingResizer)this.findOne(".top-left").y(r.y());else if("top-right"===this.movingResizer){if(l){n=Math.sqrt(Math.pow(this.findOne(".bottom-left").x()-r.x(),2)+Math.pow(this.findOne(".bottom-left").y()-r.y(),2));c=this.findOne(".top-right").x()this.findOne(".bottom-right").x()?-1:1;e=n*this.cos*c,i=n*this.sin*c,this.findOne(".bottom-right").x(e),this.findOne(".bottom-right").y(i)}}else if("rotater"===this.movingResizer){var p=this.padding(),u=this._getNodeRect();e=r.x()-u.width/2,i=-r.y()+u.height/2;var f=Math.atan2(-i,e)+Math.PI/2;u.height<0&&(f-=Math.PI);for(var g=O.getAngle(this.rotation()),v=D._radToDeg(g)+D._radToDeg(f),y=O.getAngle(this.getNode().rotation()),m=D._degToRad(v),_=this.rotationSnaps(),S=0;S<_.length;S++){var b=O.getAngle(_[S]);Math.abs(b-D._degToRad(v))%(2*Math.PI)<.1&&(v=D._radToDeg(b),m=D._degToRad(v))}var x=p,w=p;this._fitNodeInto({rotation:O.angleDeg?v:D._degToRad(v),x:u.x+(u.width/2+p)*(Math.cos(y)-Math.cos(m))+(u.height/2+p)*(Math.sin(-y)-Math.sin(-m))-(x*Math.cos(g)+w*Math.sin(-g)),y:u.y+(u.height/2+p)*(Math.cos(y)-Math.cos(m))+(u.width/2+p)*(Math.sin(y)-Math.sin(m))-(w*Math.cos(g)+x*Math.sin(g)),width:u.width+2*p,height:u.height+2*p},t)}else console.error(new Error("Wrong position argument of selection resizer: "+this.movingResizer));if("rotater"!==this.movingResizer){var C=this.findOne(".top-left").getAbsolutePosition(this.getParent());if(this.centeredScaling()||t.altKey){var k=this.findOne(".top-left"),P=this.findOne(".bottom-right"),T=k.x(),M=k.y(),A=this.getWidth()-P.x(),G=this.getHeight()-P.y();P.move({x:-T,y:-M}),k.move({x:A,y:G}),C=k.getAbsolutePosition(this.getParent())}e=C.x,i=C.y;var R=this.findOne(".bottom-right").x()-this.findOne(".top-left").x(),L=this.findOne(".bottom-right").y()-this.findOne(".top-left").y();this._fitNodeInto({x:e+this.offsetX(),y:i+this.offsetY(),width:R,height:L},t)}},t.prototype._handleMouseUp=function(t){this._removeEvents(t)},t.prototype._removeEvents=function(t){if(this._transforming){this._transforming=!1,window.removeEventListener("mousemove",this._handleMouseMove),window.removeEventListener("touchmove",this._handleMouseMove),window.removeEventListener("mouseup",this._handleMouseUp,!0),window.removeEventListener("touchend",this._handleMouseUp,!0),this._fire("transformend",{evt:t});var e=this.getNode();e&&e.fire("transformend",{evt:t})}},t.prototype._fitNodeInto=function(t,e){var i=this.boundBoxFunc();if(i){var n=this._getNodeRect();t=i.call(this,n,t)}var r=this.getNode();void 0!==t.rotation&&this.getNode().rotation(t.rotation);var o=r.getClientRect({skipTransform:!0,skipShadow:!0,skipStroke:this.ignoreStroke()}),a=this.padding(),s=(t.width-2*a)/o.width,h=(t.height-2*a)/o.height,l=O.getAngle(r.rotation()),c=o.x*s-a-r.offsetX()*s,d=o.y*h-a-r.offsetY()*h;this.getNode().setAttrs({scaleX:s,scaleY:h,x:t.x-(c*Math.cos(l)+d*Math.sin(-l)),y:t.y-(d*Math.cos(l)+c*Math.sin(l))}),this._fire("transform",{evt:e}),this.getNode()._fire("transform",{evt:e}),this.update(),this.getLayer().batchDraw()},t.prototype.forceUpdate=function(){this._resetTransformCache(),this.update()},t.prototype.update=function(){var e=this,t=this._getNodeRect(),i=this.getNode(),n={x:1,y:1};i&&i.getParent()&&(n=i.getParent().getAbsoluteScale());var r={x:1/n.x,y:1/n.y},o=t.width,a=t.height,s=this.enabledAnchors(),h=this.resizeEnabled(),l=this.padding(),c=this.anchorSize();this.find("._anchor").each(function(t){return t.setAttrs({width:c,height:c,offsetX:c/2,offsetY:c/2,stroke:e.anchorStroke(),strokeWidth:e.anchorStrokeWidth(),fill:e.anchorFill(),cornerRadius:e.anchorCornerRadius()})}),this.findOne(".top-left").setAttrs({x:-l,y:-l,scale:r,visible:h&&0<=s.indexOf("top-left")}),this.findOne(".top-center").setAttrs({x:o/2,y:-l,scale:r,visible:h&&0<=s.indexOf("top-center")}),this.findOne(".top-right").setAttrs({x:o+l,y:-l,scale:r,visible:h&&0<=s.indexOf("top-right")}),this.findOne(".middle-left").setAttrs({x:-l,y:a/2,scale:r,visible:h&&0<=s.indexOf("middle-left")}),this.findOne(".middle-right").setAttrs({x:o+l,y:a/2,scale:r,visible:h&&0<=s.indexOf("middle-right")}),this.findOne(".bottom-left").setAttrs({x:-l,y:a+l,scale:r,visible:h&&0<=s.indexOf("bottom-left")}),this.findOne(".bottom-center").setAttrs({x:o/2,y:a+l,scale:r,visible:h&&0<=s.indexOf("bottom-center")}),this.findOne(".bottom-right").setAttrs({x:o+l,y:a+l,scale:r,visible:h&&0<=s.indexOf("bottom-right")});var d=-this.rotateAnchorOffset()*Math.abs(r.y);this.findOne(".rotater").setAttrs({x:o/2,y:d*D._sign(a),scale:r,visible:this.rotateEnabled()}),this.findOne(".back").setAttrs({width:o*n.x,height:a*n.y,scale:r,visible:this.borderEnabled(),stroke:this.borderStroke(),strokeWidth:this.borderStrokeWidth(),dash:this.borderDash()})},t.prototype.isTransforming=function(){return this._transforming},t.prototype.stopTransform=function(){if(this._transforming){this._removeEvents();var t=this.findOne("."+this.movingResizer);t&&t.stopDrag()}},t.prototype.destroy=function(){return this.getStage()&&this._cursorChange&&(this.getStage().content.style.cursor=""),Jt.prototype.destroy.call(this),this.detach(),this._removeEvents(),this},t.prototype.toObject=function(){return et.prototype.toObject.call(this)},t}(Jt);We.prototype.className="Transformer",i(We),b.addGetterSetter(We,"enabledAnchors",Be,function(t){return t instanceof Array||D.warn("enabledAnchors value should be an array"),t instanceof Array&&t.forEach(function(t){-1===Be.indexOf(t)&&D.warn("Unknown anchor name: "+t+". Available names are: "+Be.join(", "))}),t||[]}),b.addGetterSetter(We,"resizeEnabled",!0),b.addGetterSetter(We,"anchorSize",10,g()),b.addGetterSetter(We,"rotateEnabled",!0),b.addGetterSetter(We,"rotationSnaps",[]),b.addGetterSetter(We,"rotateAnchorOffset",50,g()),b.addGetterSetter(We,"borderEnabled",!0),b.addGetterSetter(We,"anchorStroke","rgb(0, 161, 255)"),b.addGetterSetter(We,"anchorStrokeWidth",1,g()),b.addGetterSetter(We,"anchorFill","white"),b.addGetterSetter(We,"anchorCornerRadius",0,g()),b.addGetterSetter(We,"borderStroke","rgb(0, 161, 255)"),b.addGetterSetter(We,"borderStrokeWidth",1,g()),b.addGetterSetter(We,"borderDash"),b.addGetterSetter(We,"keepRatio",!0),b.addGetterSetter(We,"centeredScaling",!1),b.addGetterSetter(We,"ignoreStroke",!1),b.addGetterSetter(We,"padding",0,g()),b.addGetterSetter(We,"node"),b.addGetterSetter(We,"boundBoxFunc"),b.backCompat(We,{lineEnabled:"borderEnabled",rotateHandlerOffset:"rotateAnchorOffset",enabledHandlers:"enabledAnchors"}),o.mapMethods(We);var Ne=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return w(e,t),e.prototype._sceneFunc=function(t){t.beginPath(),t.arc(0,0,this.radius(),0,O.getAngle(this.angle()),this.clockwise()),t.lineTo(0,0),t.closePath(),t.fillStrokeShape(this)},e.prototype.getWidth=function(){return 2*this.radius()},e.prototype.getHeight=function(){return 2*this.radius()},e.prototype.setWidth=function(t){this.radius(t/2)},e.prototype.setHeight=function(t){this.radius(t/2)},e}(Ut);function He(){this.r=0,this.g=0,this.b=0,this.a=0,this.next=null}Ne.prototype.className="Wedge",Ne.prototype._centroid=!0,Ne.prototype._attrsAffectingSize=["radius"],i(Ne),b.addGetterSetter(Ne,"radius",0,g()),b.addGetterSetter(Ne,"angle",0,g()),b.addGetterSetter(Ne,"clockwise",!1),b.backCompat(Ne,{angleDeg:"angle",getAngleDeg:"getAngle",setAngleDeg:"setAngle"}),o.mapMethods(Ne);var Ye=[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],Xe=[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];b.addGetterSetter(et,"blurRadius",0,g(),b.afterSetFilter);b.addGetterSetter(et,"brightness",0,g(),b.afterSetFilter);b.addGetterSetter(et,"contrast",0,g(),b.afterSetFilter);function je(t,e,i,n,r){var o=i-e,a=r-n;return 0==o?n+a/2:0==a?n:a*((t-e)/o)+n}b.addGetterSetter(et,"embossStrength",.5,g(),b.afterSetFilter),b.addGetterSetter(et,"embossWhiteLevel",.5,g(),b.afterSetFilter),b.addGetterSetter(et,"embossDirection","top-left",null,b.afterSetFilter),b.addGetterSetter(et,"embossBlend",!1,null,b.afterSetFilter);b.addGetterSetter(et,"enhance",0,g(),b.afterSetFilter);b.addGetterSetter(et,"hue",0,g(),b.afterSetFilter),b.addGetterSetter(et,"saturation",0,g(),b.afterSetFilter),b.addGetterSetter(et,"luminance",0,g(),b.afterSetFilter);b.addGetterSetter(et,"hue",0,g(),b.afterSetFilter),b.addGetterSetter(et,"saturation",0,g(),b.afterSetFilter),b.addGetterSetter(et,"value",0,g(),b.afterSetFilter);function Ue(t,e,i){var n=4*(i*t.width+e),r=[];return r.push(t.data[n++],t.data[n++],t.data[n++],t.data[n++]),r}function qe(t,e){return Math.sqrt(Math.pow(t[0]-e[0],2)+Math.pow(t[1]-e[1],2)+Math.pow(t[2]-e[2],2))}b.addGetterSetter(et,"kaleidoscopePower",2,g(),b.afterSetFilter),b.addGetterSetter(et,"kaleidoscopeAngle",0,g(),b.afterSetFilter);b.addGetterSetter(et,"threshold",0,g(),b.afterSetFilter);b.addGetterSetter(et,"noise",.2,g(),b.afterSetFilter);b.addGetterSetter(et,"pixelSize",8,g(),b.afterSetFilter);b.addGetterSetter(et,"levels",.5,g(),b.afterSetFilter);b.addGetterSetter(et,"red",0,function(t){return this._filterUpToDate=!1,255>W,0!==C?(C=255/C,P[s]=(l*B>>W)*C,P[s+1]=(c*B>>W)*C,P[s+2]=(d*B>>W)*C):P[s]=P[s+1]=P[s+2]=0,l-=u,c-=f,d-=g,p-=v,u-=F.r,f-=F.g,g-=F.b,v-=F.a,o=h+((o=i+e+1)>W,0>W)*C,P[o+1]=(c*B>>W)*C,P[o+2]=(d*B>>W)*C):P[o]=P[o+1]=P[o+2]=0,l-=u,c-=f,d-=g,p-=v,u-=F.r,f-=F.g,g-=F.b,v-=F.a,o=i+((o=n+L)>16&255,g:e>>8&255,b:255&e}},getRandomColor:function(){for(var t=(16777215*Math.random()<<0).toString(16);t.length<6;)t="0"+t;return"#"+t},get:function(t,e){return void 0===t?e:t},getRGB:function(t){var e;return t in l?{r:(e=l[t])[0],g:e[1],b:e[2]}:"#"===t[0]?this._hexToRgb(t.substring(1)):"rgb("===t.substr(0,4)?(e=d.exec(t.replace(/ /g,"")),{r:parseInt(e[1],10),g:parseInt(e[2],10),b:parseInt(e[3],10)}):{r:0,g:0,b:0}},colorToRGBA:function(t){return t=t||"black",D._namedColorToRBA(t)||D._hex3ColorToRGBA(t)||D._hex6ColorToRGBA(t)||D._rgbColorToRGBA(t)||D._rgbaColorToRGBA(t)},_namedColorToRBA:function(t){var e=l[t.toLowerCase()];return e?{r:e[0],g:e[1],b:e[2],a:1}:null},_rgbColorToRGBA:function(t){if(0===t.indexOf("rgb(")){var e=(t=t.match(/rgb\(([^)]+)\)/)[1]).split(/ *, */).map(Number);return{r:e[0],g:e[1],b:e[2],a:1}}},_rgbaColorToRGBA:function(t){if(0===t.indexOf("rgba(")){var e=(t=t.match(/rgba\(([^)]+)\)/)[1]).split(/ *, */).map(Number);return{r:e[0],g:e[1],b:e[2],a:e[3]}}},_hex6ColorToRGBA:function(t){if("#"===t[0]&&7===t.length)return{r:parseInt(t.slice(1,3),16),g:parseInt(t.slice(3,5),16),b:parseInt(t.slice(5,7),16),a:1}},_hex3ColorToRGBA:function(t){if("#"===t[0]&&4===t.length)return{r:parseInt(t[1]+t[1],16),g:parseInt(t[2]+t[2],16),b:parseInt(t[3]+t[3],16),a:1}},haveIntersection:function(t,e){return!(e.x>t.x+t.width||e.x+e.widtht.y+t.height||e.y+e.heighte.length){var a=e;e=t,t=a}for(n=0;n=this.parent.children.length)&&D.warn("Unexpected value "+t+" 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)+".");var e=this.index;return this.parent.children.splice(e,1),this.parent.children.splice(t,0,this),this.parent._setChildrenIndices(),this},s.prototype.getAbsoluteOpacity=function(){return this._getCache(H,this._getAbsoluteOpacity)},s.prototype._getAbsoluteOpacity=function(){var t=this.opacity(),e=this.getParent();return e&&!e._isUnderCache&&(t*=e.getAbsoluteOpacity()),t},s.prototype.moveTo=function(t){return this.getParent()!==t&&(this._remove(),t.add(this)),this},s.prototype.toObject=function(){var t,e,i,n={},r=this.getAttrs();for(t in n.attrs={},r)e=r[t],D.isObject(e)&&!D._isPlainObject(e)&&!D._isArray(e)||(i="function"==typeof this[t]&&this[t],delete r[t],(i?i.call(this):null)!==(r[t]=e)&&(n.attrs[t]=e));return n.className=this.getClassName(),D._prepareToStringify(n)},s.prototype.toJSON=function(){return JSON.stringify(this.toObject())},s.prototype.getParent=function(){return this.parent},s.prototype.findAncestors=function(t,e,i){var n=[];e&&this._isMatch(t)&&n.push(this);for(var r=this.parent;r;){if(r===i)return n;r._isMatch(t)&&n.push(r),r=r.parent}return n},s.prototype.isAncestorOf=function(t){return!1},s.prototype.findAncestor=function(t,e,i){return this.findAncestors(t,e,i)[0]},s.prototype._isMatch=function(t){if(!t)return!1;if("function"==typeof t)return t(this);var e,i,n=t.replace(/ /g,"").split(","),r=n.length;for(e=0;ethis.duration?this.yoyo?(this._time=this.duration,this.reverse()):this.finish():t<0?this.yoyo?(this._time=0,this.play()):this.reset():(this._time=t,this.update())},t.prototype.getTime=function(){return this._time},t.prototype.setPosition=function(t){this.prevPos=this._pos,this.propFunc(t),this._pos=t},t.prototype.getPosition=function(t){return void 0===t&&(t=this._time),this.func(t,this.begin,this._change,this.duration)},t.prototype.play=function(){this.state=2,this._startTime=this.getTimer()-this._time,this.onEnterFrame(),this.fire("onPlay")},t.prototype.reverse=function(){this.state=3,this._time=this.duration-this._time,this._startTime=this.getTimer()-this._time,this.onEnterFrame(),this.fire("onReverse")},t.prototype.seek=function(t){this.pause(),this._time=t,this.update(),this.fire("onSeek")},t.prototype.reset=function(){this.pause(),this._time=0,this.update(),this.fire("onReset")},t.prototype.finish=function(){this.pause(),this._time=this.duration,this.update(),this.fire("onFinish")},t.prototype.update=function(){this.setPosition(this.getPosition(this._time))},t.prototype.onEnterFrame=function(){var t=this.getTimer()-this._startTime;2===this.state?this.setTime(t):3===this.state&&this.setTime(this.duration-t)},t.prototype.pause=function(){this.state=1,this.fire("onPause")},t.prototype.getTimer=function(){return(new Date).getTime()},t}(),ie=function(){function u(t){var e,i,n=this,r=t.node,o=r._id,a=t.easing||ne.Linear,s=!!t.yoyo;e=void 0===t.duration?.3:0===t.duration?.001:t.duration,this.node=r,this._id=$t++;var h=r.getLayer()||(r instanceof O.Stage?r.getLayers():null);for(i in h||D.error("Tween constructor have `node` that is not in a layer. Please add node into layer first."),this.anim=new E(function(){n.tween.onEnterFrame()},h),this.tween=new ee(i,function(t){n._tweenFunc(t)},a,0,1,1e3*e,s),this._addListeners(),u.attrs[o]||(u.attrs[o]={}),u.attrs[o][this._id]||(u.attrs[o][this._id]={}),u.tweens[o]||(u.tweens[o]={}),t)void 0===Zt[i]&&this._addAttr(i,t[i]);this.reset(),this.onFinish=t.onFinish,this.onReset=t.onReset}return u.prototype._addAttr=function(t,e){var i,n,r,o,a,s,h,l,c=this.node,d=c._id;if((r=u.tweens[d][t])&&delete u.attrs[d][r][t],i=c.getAttr(t),D._isArray(e))if(n=[],a=Math.max(e.length,i.length),"points"===t&&e.length!==i.length&&(e.length>i.length?(h=i,i=D._prepareArrayForTween(i,e,c.closed())):(s=e,e=D._prepareArrayForTween(e,i,c.closed()))),0===t.indexOf("fill"))for(o=0;othis.dataArray[i].pathLength;)t-=this.dataArray[i].pathLength,++i;if(i===n)return{x:(e=this.dataArray[i-1].points.slice(-2))[0],y:e[1]};if(t<.01)return{x:(e=this.dataArray[i].points.slice(0,2))[0],y:e[1]};var r=this.dataArray[i],o=r.points;switch(r.command){case"L":return u.getPointOnLine(t,r.start.x,r.start.y,o[0],o[1]);case"C":return u.getPointOnCubicBezier(t/r.pathLength,r.start.x,r.start.y,o[0],o[1],o[2],o[3],o[4],o[5]);case"Q":return u.getPointOnQuadraticBezier(t/r.pathLength,r.start.x,r.start.y,o[0],o[1],o[2],o[3]);case"A":var a=o[0],s=o[1],h=o[2],l=o[3],c=o[4],d=o[5],p=o[6];return c+=d*t/r.pathLength,u.getPointOnEllipticalArc(a,s,h,l,c,p)}return null},u.getLineLength=function(t,e,i,n){return Math.sqrt((i-t)*(i-t)+(n-e)*(n-e))},u.getPointOnLine=function(t,e,i,n,r,o,a){void 0===o&&(o=e),void 0===a&&(a=i);var s=(r-i)/(n-e+1e-8),h=Math.sqrt(t*t/(1+s*s));n>>1,k=_.slice(0,1+P),T=this._getTextWidth(k)+v;T<=l?(b=1+P,w=k+(g?"…":""),C=T):x=P}if(!w)break;if(f){var M,A=_[w.length];0<(M=(" "===A||"-"===A)&&C<=l?w.length:Math.max(w.lastIndexOf(" "),w.lastIndexOf("-"))+1)&&(b=M,w=w.slice(0,b),C=this._getTextWidth(w))}if(w=w.trimRight(),this._addTextLine(w),i=Math.max(i,C),d+=n,!u||s&&ce?g=me.getPointOnLine(e,f.x,f.y,v.points[0],v.points[1],f.x,f.y):v=void 0;break;case"A":var a=v.points[4],s=v.points[5],h=v.points[4]+s;0===m?m=a+1e-8:iv.pathLength?1e-8:e/v.pathLength:ithis.findOne(".bottom-right").x()?-1:1;e=n*this.cos*c,i=n*this.sin*c,this.findOne(".top-left").x(this.findOne(".bottom-right").x()-e),this.findOne(".top-left").y(this.findOne(".bottom-right").y()-i)}}else if("top-center"===this.movingResizer)this.findOne(".top-left").y(r.y());else if("top-right"===this.movingResizer){if(l){n=Math.sqrt(Math.pow(this.findOne(".bottom-left").x()-r.x(),2)+Math.pow(this.findOne(".bottom-left").y()-r.y(),2));c=this.findOne(".top-right").x()this.findOne(".bottom-right").x()?-1:1;e=n*this.cos*c,i=n*this.sin*c,this.findOne(".bottom-right").x(e),this.findOne(".bottom-right").y(i)}}else if("rotater"===this.movingResizer){var p=this.padding(),u=this._getNodeRect();e=r.x()-u.width/2,i=-r.y()+u.height/2;var f=Math.atan2(-i,e)+Math.PI/2;u.height<0&&(f-=Math.PI);for(var g=O.getAngle(this.rotation()),v=D._radToDeg(g)+D._radToDeg(f),y=O.getAngle(this.getNode().rotation()),m=D._degToRad(v),_=this.rotationSnaps(),S=0;S<_.length;S++){var b=O.getAngle(_[S]);Math.abs(b-D._degToRad(v))%(2*Math.PI)<.1&&(v=D._radToDeg(b),m=D._degToRad(v))}var x=p,w=p;this._fitNodeInto({rotation:O.angleDeg?v:D._degToRad(v),x:u.x+(u.width/2+p)*(Math.cos(y)-Math.cos(m))+(u.height/2+p)*(Math.sin(-y)-Math.sin(-m))-(x*Math.cos(g)+w*Math.sin(-g)),y:u.y+(u.height/2+p)*(Math.cos(y)-Math.cos(m))+(u.width/2+p)*(Math.sin(y)-Math.sin(m))-(w*Math.cos(g)+x*Math.sin(g)),width:u.width+2*p,height:u.height+2*p},t)}else console.error(new Error("Wrong position argument of selection resizer: "+this.movingResizer));if("rotater"!==this.movingResizer){var C=this.findOne(".top-left").getAbsolutePosition(this.getParent());if(this.centeredScaling()||t.altKey){var P=this.findOne(".top-left"),k=this.findOne(".bottom-right"),T=P.x(),M=P.y(),A=this.getWidth()-k.x(),G=this.getHeight()-k.y();k.move({x:-T,y:-M}),P.move({x:A,y:G}),C=P.getAbsolutePosition(this.getParent())}e=C.x,i=C.y;var R=this.findOne(".bottom-right").x()-this.findOne(".top-left").x(),L=this.findOne(".bottom-right").y()-this.findOne(".top-left").y();this._fitNodeInto({x:e+this.offsetX(),y:i+this.offsetY(),width:R,height:L},t)}},t.prototype._handleMouseUp=function(t){this._removeEvents(t)},t.prototype._removeEvents=function(t){if(this._transforming){this._transforming=!1,window.removeEventListener("mousemove",this._handleMouseMove),window.removeEventListener("touchmove",this._handleMouseMove),window.removeEventListener("mouseup",this._handleMouseUp,!0),window.removeEventListener("touchend",this._handleMouseUp,!0),this._fire("transformend",{evt:t});var e=this.getNode();e&&e.fire("transformend",{evt:t})}},t.prototype._fitNodeInto=function(t,e){var i=this.boundBoxFunc();if(i){var n=this._getNodeRect();t=i.call(this,n,t)}var r=this.getNode();void 0!==t.rotation&&this.getNode().rotation(t.rotation);var o=r.getClientRect({skipTransform:!0,skipShadow:!0,skipStroke:this.ignoreStroke()}),a=this.padding(),s=(t.width-2*a)/o.width,h=(t.height-2*a)/o.height,l=O.getAngle(r.rotation()),c=o.x*s-a-r.offsetX()*s,d=o.y*h-a-r.offsetY()*h;this.getNode().setAttrs({scaleX:s,scaleY:h,x:t.x-(c*Math.cos(l)+d*Math.sin(-l)),y:t.y-(d*Math.cos(l)+c*Math.sin(l))}),this._fire("transform",{evt:e}),this.getNode()._fire("transform",{evt:e}),this.update(),this.getLayer().batchDraw()},t.prototype.forceUpdate=function(){this._resetTransformCache(),this.update()},t.prototype.update=function(){var e=this,t=this._getNodeRect(),i=this.getNode(),n={x:1,y:1};i&&i.getParent()&&(n=i.getParent().getAbsoluteScale());var r={x:1/n.x,y:1/n.y},o=t.width,a=t.height,s=this.enabledAnchors(),h=this.resizeEnabled(),l=this.padding(),c=this.anchorSize();this.find("._anchor").each(function(t){return t.setAttrs({width:c,height:c,offsetX:c/2,offsetY:c/2,stroke:e.anchorStroke(),strokeWidth:e.anchorStrokeWidth(),fill:e.anchorFill(),cornerRadius:e.anchorCornerRadius()})}),this.findOne(".top-left").setAttrs({x:-l,y:-l,scale:r,visible:h&&0<=s.indexOf("top-left")}),this.findOne(".top-center").setAttrs({x:o/2,y:-l,scale:r,visible:h&&0<=s.indexOf("top-center")}),this.findOne(".top-right").setAttrs({x:o+l,y:-l,scale:r,visible:h&&0<=s.indexOf("top-right")}),this.findOne(".middle-left").setAttrs({x:-l,y:a/2,scale:r,visible:h&&0<=s.indexOf("middle-left")}),this.findOne(".middle-right").setAttrs({x:o+l,y:a/2,scale:r,visible:h&&0<=s.indexOf("middle-right")}),this.findOne(".bottom-left").setAttrs({x:-l,y:a+l,scale:r,visible:h&&0<=s.indexOf("bottom-left")}),this.findOne(".bottom-center").setAttrs({x:o/2,y:a+l,scale:r,visible:h&&0<=s.indexOf("bottom-center")}),this.findOne(".bottom-right").setAttrs({x:o+l,y:a+l,scale:r,visible:h&&0<=s.indexOf("bottom-right")});var d=-this.rotateAnchorOffset()*Math.abs(r.y);this.findOne(".rotater").setAttrs({x:o/2,y:d*D._sign(a),scale:r,visible:this.rotateEnabled()}),this.findOne(".back").setAttrs({width:o*n.x,height:a*n.y,scale:r,visible:this.borderEnabled(),stroke:this.borderStroke(),strokeWidth:this.borderStrokeWidth(),dash:this.borderDash()})},t.prototype.isTransforming=function(){return this._transforming},t.prototype.stopTransform=function(){if(this._transforming){this._removeEvents();var t=this.findOne("."+this.movingResizer);t&&t.stopDrag()}},t.prototype.destroy=function(){return this.getStage()&&this._cursorChange&&(this.getStage().content.style.cursor=""),Jt.prototype.destroy.call(this),this.detach(),this._removeEvents(),this},t.prototype.toObject=function(){return et.prototype.toObject.call(this)},t}(Jt);We.prototype.className="Transformer",i(We),b.addGetterSetter(We,"enabledAnchors",Be,function(t){return t instanceof Array||D.warn("enabledAnchors value should be an array"),t instanceof Array&&t.forEach(function(t){-1===Be.indexOf(t)&&D.warn("Unknown anchor name: "+t+". Available names are: "+Be.join(", "))}),t||[]}),b.addGetterSetter(We,"resizeEnabled",!0),b.addGetterSetter(We,"anchorSize",10,g()),b.addGetterSetter(We,"rotateEnabled",!0),b.addGetterSetter(We,"rotationSnaps",[]),b.addGetterSetter(We,"rotateAnchorOffset",50,g()),b.addGetterSetter(We,"borderEnabled",!0),b.addGetterSetter(We,"anchorStroke","rgb(0, 161, 255)"),b.addGetterSetter(We,"anchorStrokeWidth",1,g()),b.addGetterSetter(We,"anchorFill","white"),b.addGetterSetter(We,"anchorCornerRadius",0,g()),b.addGetterSetter(We,"borderStroke","rgb(0, 161, 255)"),b.addGetterSetter(We,"borderStrokeWidth",1,g()),b.addGetterSetter(We,"borderDash"),b.addGetterSetter(We,"keepRatio",!0),b.addGetterSetter(We,"centeredScaling",!1),b.addGetterSetter(We,"ignoreStroke",!1),b.addGetterSetter(We,"padding",0,g()),b.addGetterSetter(We,"node"),b.addGetterSetter(We,"boundBoxFunc"),b.backCompat(We,{lineEnabled:"borderEnabled",rotateHandlerOffset:"rotateAnchorOffset",enabledHandlers:"enabledAnchors"}),a.mapMethods(We);var He=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return w(e,t),e.prototype._sceneFunc=function(t){t.beginPath(),t.arc(0,0,this.radius(),0,O.getAngle(this.angle()),this.clockwise()),t.lineTo(0,0),t.closePath(),t.fillStrokeShape(this)},e.prototype.getWidth=function(){return 2*this.radius()},e.prototype.getHeight=function(){return 2*this.radius()},e.prototype.setWidth=function(t){this.radius(t/2)},e.prototype.setHeight=function(t){this.radius(t/2)},e}(Ut);function Ne(){this.r=0,this.g=0,this.b=0,this.a=0,this.next=null}He.prototype.className="Wedge",He.prototype._centroid=!0,He.prototype._attrsAffectingSize=["radius"],i(He),b.addGetterSetter(He,"radius",0,g()),b.addGetterSetter(He,"angle",0,g()),b.addGetterSetter(He,"clockwise",!1),b.backCompat(He,{angleDeg:"angle",getAngleDeg:"getAngle",setAngleDeg:"setAngle"}),a.mapMethods(He);var Ye=[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],Xe=[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];b.addGetterSetter(et,"blurRadius",0,g(),b.afterSetFilter);b.addGetterSetter(et,"brightness",0,g(),b.afterSetFilter);b.addGetterSetter(et,"contrast",0,g(),b.afterSetFilter);function je(t,e,i,n,r){var o=i-e,a=r-n;return 0==o?n+a/2:0==a?n:a*((t-e)/o)+n}b.addGetterSetter(et,"embossStrength",.5,g(),b.afterSetFilter),b.addGetterSetter(et,"embossWhiteLevel",.5,g(),b.afterSetFilter),b.addGetterSetter(et,"embossDirection","top-left",null,b.afterSetFilter),b.addGetterSetter(et,"embossBlend",!1,null,b.afterSetFilter);b.addGetterSetter(et,"enhance",0,g(),b.afterSetFilter);b.addGetterSetter(et,"hue",0,g(),b.afterSetFilter),b.addGetterSetter(et,"saturation",0,g(),b.afterSetFilter),b.addGetterSetter(et,"luminance",0,g(),b.afterSetFilter);b.addGetterSetter(et,"hue",0,g(),b.afterSetFilter),b.addGetterSetter(et,"saturation",0,g(),b.afterSetFilter),b.addGetterSetter(et,"value",0,g(),b.afterSetFilter);function Ue(t,e,i){var n=4*(i*t.width+e),r=[];return r.push(t.data[n++],t.data[n++],t.data[n++],t.data[n++]),r}function qe(t,e){return Math.sqrt(Math.pow(t[0]-e[0],2)+Math.pow(t[1]-e[1],2)+Math.pow(t[2]-e[2],2))}b.addGetterSetter(et,"kaleidoscopePower",2,g(),b.afterSetFilter),b.addGetterSetter(et,"kaleidoscopeAngle",0,g(),b.afterSetFilter);b.addGetterSetter(et,"threshold",0,g(),b.afterSetFilter);b.addGetterSetter(et,"noise",.2,g(),b.afterSetFilter);b.addGetterSetter(et,"pixelSize",8,g(),b.afterSetFilter);b.addGetterSetter(et,"levels",.5,g(),b.afterSetFilter);b.addGetterSetter(et,"red",0,function(t){return this._filterUpToDate=!1,255>W,0!==C?(C=255/C,k[s]=(l*B>>W)*C,k[s+1]=(c*B>>W)*C,k[s+2]=(d*B>>W)*C):k[s]=k[s+1]=k[s+2]=0,l-=u,c-=f,d-=g,p-=v,u-=F.r,f-=F.g,g-=F.b,v-=F.a,o=h+((o=i+e+1)>W,0>W)*C,k[o+1]=(c*B>>W)*C,k[o+2]=(d*B>>W)*C):k[o]=k[o+1]=k[o+2]=0,l-=u,c-=f,d-=g,p-=v,u-=F.r,f-=F.g,g-=F.b,v-=F.a,o=i+((o=n+L) extends Node< shouldDrawHit(canvas?) { var layer = this.getLayer(); var layerUnderDrag = - DD.isDragging && DD.anim.getLayers().indexOf(layer) !== -1; + DD.isDragging && + !Konva.hitOnDragEnabled && + DD.anim.getLayers().indexOf(layer) !== -1; return ( (canvas && canvas.isCache) || (layer && layer.hitGraphEnabled() && this.isVisible() && !layerUnderDrag) diff --git a/src/DragAndDrop.ts b/src/DragAndDrop.ts index fac2d337..2a9db6ef 100644 --- a/src/DragAndDrop.ts +++ b/src/DragAndDrop.ts @@ -1,5 +1,6 @@ import { Animation } from './Animation'; import { Konva } from './Global'; +import { Node } from './Node'; // TODO: make better module, // make sure other modules import it without global @@ -21,6 +22,8 @@ export const DD = { y: 0 }, node: null, + _nodes: [], + _offsets: [], // methods _drag(evt) { diff --git a/src/Global.ts b/src/Global.ts index 21bff6cf..a98b0e95 100644 --- a/src/Global.ts +++ b/src/Global.ts @@ -97,6 +97,30 @@ export const Konva = { }, enableTrace: false, _pointerEventsEnabled: false, + /** + * 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 captureTouchEventsEnabled + * @default false + * @name captureTouchEventsEnabled + * @memberof Konva + * @example + * Konva.captureTouchEventsEnabled = true; + */ + captureTouchEventsEnabled: false, // TODO: move that to stage? listenClickTap: false, diff --git a/src/Node.ts b/src/Node.ts index bbe877f7..0423237e 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -205,6 +205,7 @@ export abstract class Node { children = emptyChildren; nodeType!: string; className!: string; + _dragEventId: number | null = null; constructor(config?: Config) { this.setAttrs(config); @@ -1575,7 +1576,7 @@ export abstract class Node { * @name Konva.Node#getStage * @returns {Konva.Stage} */ - getStage(): any { + getStage(): Stage | null { return this._getCache(STAGE, this._getStage); } @@ -1636,7 +1637,7 @@ export abstract class Node { return this._getAbsoluteTransform(top); } else { // if no argument, we can cache the result - return this._getCache( + return this._getCache( ABSOLUTE_TRANSFORM, this._getAbsoluteTransform ) as Transform; @@ -1805,8 +1806,8 @@ export abstract class Node { y = config.y !== undefined ? config.y : box.y, pixelRatio = config.pixelRatio || 1, canvas = new SceneCanvas({ - width: config.width || box.width || (stage ? stage.getWidth() : 0), - height: config.height || box.height || (stage ? stage.getHeight() : 0), + width: config.width || box.width || (stage ? stage.width() : 0), + height: config.height || box.height || (stage ? stage.height() : 0), pixelRatio: pixelRatio }), context = canvas.getContext(); @@ -2241,8 +2242,11 @@ export abstract class Node { } _setDragPosition(evt?) { - var pos = this.getStage().getPointerPosition(), - dbf = this.dragBoundFunc(); + // const pointers = this.getStage().getPointersPositions(); + // const pos = pointers.find(p => p.id === this._dragEventId); + const pos = this.getStage().getPointerPosition(); + + var dbf = this.dragBoundFunc(); if (!pos) { return; } diff --git a/src/PointerEvents.ts b/src/PointerEvents.ts index e3b3e2fd..92948315 100644 --- a/src/PointerEvents.ts +++ b/src/PointerEvents.ts @@ -46,7 +46,7 @@ export function releaseCapture(pointerId: number, target?: Shape | Stage) { const stage = shape.getStage(); if (stage && stage.content) { - stage.content.releasePointerCapture(pointerId); + // stage.content.releasePointerCapture(pointerId); } Captures.delete(pointerId); diff --git a/src/Stage.ts b/src/Stage.ts index 8758374a..87c97763 100644 --- a/src/Stage.ts +++ b/src/Stage.ts @@ -127,6 +127,9 @@ function checkNoClip(attrs: any = {}) { export class Stage extends Container { content: HTMLDivElement; pointerPos: Vector2d | null; + _pointerPositions: (Vector2d & { id?: number })[]; + _changedPointerPositions: (Vector2d & { id?: number })[]; + bufferCanvas: SceneCanvas; bufferHitCanvas: HitCanvas; targetShape: Shape; @@ -244,10 +247,14 @@ export class Stage extends Container { * @returns {Object} */ getPointerPosition() { - if (!this.pointerPos) { + const pos = this._pointerPositions[0]; + if (!pos) { Util.warn(NO_POINTERS_MESSAGE); } - return this.pointerPos; + return pos; + } + getPointersPositions() { + return this._pointerPositions; } getStage() { return this; @@ -437,6 +444,7 @@ export class Stage extends Container { }); } this.pointerPos = undefined; + this._pointerPositions = []; this._fire(CONTENT_MOUSEOUT, { evt: evt }); } @@ -617,32 +625,83 @@ export class Stage extends Container { } _touchstart(evt) { this.setPointersPositions(evt); - var shape = this.getIntersection(this.getPointerPosition()); + var triggeredOnShape = false; + this._changedPointerPositions.forEach(pos => { + var shape = this.getIntersection(pos); + Konva.listenClickTap = true; + const hasShape = shape && shape.isListening(); - Konva.listenClickTap = true; + if (!hasShape) { + return; + } + + if (Konva.captureTouchEventsEnabled) { + shape.setPointerCapture(pos.id); + } - if (shape && shape.isListening()) { this.tapStartShape = shape; - shape._fireAndBubble(TOUCHSTART, { evt: evt }); - + shape._fireAndBubble(TOUCHSTART, { evt: evt }, this); + triggeredOnShape = true; // only call preventDefault if the shape is listening for events if (shape.isListening() && shape.preventDefault() && evt.cancelable) { evt.preventDefault(); } - } else { + }); + + if (!triggeredOnShape) { this._fire(TOUCHSTART, { evt: evt, target: this, currentTarget: this }); } + // content event this._fire(CONTENT_TOUCHSTART, { evt: evt }); } + _touchmove(evt) { + this.setPointersPositions(evt); + if (!DD.isDragging) { + var triggeredOnShape = false; + var processedShapesIds = {}; + this._changedPointerPositions.forEach(pos => { + const shape = + PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos); + + const hasShape = shape && shape.isListening(); + if (!hasShape) { + return; + } + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + shape._fireAndBubble(TOUCHMOVE, { evt: evt }); + triggeredOnShape = true; + // only call preventDefault if the shape is listening for events + if (shape.isListening() && shape.preventDefault() && evt.cancelable) { + evt.preventDefault(); + } + }); + + if (!triggeredOnShape) { + this._fire(TOUCHMOVE, { + evt: evt, + target: this, + currentTarget: this + }); + } + + this._fire(CONTENT_TOUCHMOVE, { evt: evt }); + } + if (DD.isDragging && DD.node.preventDefault() && evt.cancelable) { + evt.preventDefault(); + } + } _touchend(evt) { this.setPointersPositions(evt); - var shape = this.getIntersection(this.getPointerPosition()), - clickEndShape = this.clickEndShape, + + var clickEndShape = this.clickEndShape, fireDblClick = false; if (Konva.inDblClickWindow) { @@ -658,9 +717,29 @@ export class Stage extends Container { Konva.inDblClickWindow = false; }, Konva.dblClickWindow); - if (shape && shape.isListening()) { + var triggeredOnShape = false; + var processedShapesIds = {}; + this._changedPointerPositions.forEach(pos => { + var shape = + (PointerEvents.getCapturedShape(pos.id) as Shape) || + this.getIntersection(pos); + + if (shape) { + shape.releaseCapture(pos.id); + } + + const hasShape = shape && shape.isListening(); + if (!hasShape) { + return; + } + if (processedShapesIds[shape._id]) { + return; + } + processedShapesIds[shape._id] = true; + this.clickEndShape = shape; shape._fireAndBubble(TOUCHEND, { evt: evt }); + triggeredOnShape = true; // detect if tap or double tap occurred if ( @@ -674,22 +753,26 @@ export class Stage extends Container { shape._fireAndBubble(DBL_TAP, { evt: evt }); } } + // only call preventDefault if the shape is listening for events if (shape.isListening() && shape.preventDefault() && evt.cancelable) { evt.preventDefault(); } - } else { + }); + + if (!triggeredOnShape) { this._fire(TOUCHEND, { evt: evt, target: this, currentTarget: this }); - if (Konva.listenClickTap) { - this._fire(TAP, { evt: evt, target: this, currentTarget: this }); - } - if (fireDblClick) { - this._fire(DBL_TAP, { - evt: evt, - target: this, - currentTarget: this - }); - } + } + + if (Konva.listenClickTap) { + this._fire(TAP, { evt: evt, target: this, currentTarget: this }); + } + if (fireDblClick) { + this._fire(DBL_TAP, { + evt: evt, + target: this, + currentTarget: this + }); } // content events this._fire(CONTENT_TOUCHEND, { evt: evt }); @@ -702,30 +785,7 @@ export class Stage extends Container { Konva.listenClickTap = false; } - _touchmove(evt) { - this.setPointersPositions(evt); - var shape; - if (!DD.isDragging) { - shape = this.getIntersection(this.getPointerPosition()); - if (shape && shape.isListening()) { - shape._fireAndBubble(TOUCHMOVE, { evt: evt }); - // only call preventDefault if the shape is listening for events - if (shape.isListening() && shape.preventDefault() && evt.cancelable) { - evt.preventDefault(); - } - } else { - this._fire(TOUCHMOVE, { - evt: evt, - target: this, - currentTarget: this - }); - } - this._fire(CONTENT_TOUCHMOVE, { evt: evt }); - } - if (DD.isDragging && DD.node.preventDefault() && evt.cancelable) { - evt.preventDefault(); - } - } + _wheel(evt) { this.setPointersPositions(evt); var shape = this.getIntersection(this.getPointerPosition()); @@ -830,6 +890,29 @@ export class Stage extends Container { // touch events if (evt.touches !== undefined) { + // touchlist has not support for map method + // so we have to iterate + this._pointerPositions = []; + this._changedPointerPositions = []; + Collection.prototype.each.call(evt.touches, (touch: any) => { + this._pointerPositions.push({ + id: touch.identifier, + x: touch.clientX - contentPosition.left, + y: touch.clientY - contentPosition.top + }); + }); + + Collection.prototype.each.call( + evt.changedTouches || evt.touches, + (touch: any) => { + this._changedPointerPositions.push({ + id: touch.identifier, + x: touch.clientX - contentPosition.left, + y: touch.clientY - contentPosition.top + }); + } + ); + // currently, only handle one finger if (evt.touches.length > 0) { var touch = evt.touches[0]; @@ -841,6 +924,7 @@ export class Stage extends Container { // mouse events x = evt.clientX - contentPosition.left; y = evt.clientY - contentPosition.top; + this._pointerPositions = [{ x, y }]; } if (x !== null && y !== null) { this.pointerPos = { diff --git a/test/functional/TouchEvents-test.js b/test/functional/TouchEvents-test.js index 38de63b5..b86e8a51 100644 --- a/test/functional/TouchEvents-test.js +++ b/test/functional/TouchEvents-test.js @@ -58,7 +58,13 @@ suite('TouchEvents', function() { }); stage._touchend({ - touches: [] + touches: [], + changedTouches: [ + { + clientX: 100, + clientY: 100 + top + } + ] }); assert.equal(circleTouchstart, 1, 1); @@ -162,6 +168,12 @@ suite('TouchEvents', function() { // touchend circle stage._touchend({ touches: [], + changedTouches: [ + { + clientX: 289, + clientY: 100 + top + } + ], preventDefault: function() {} }); // end drag is tied to document mouseup and touchend event @@ -194,6 +206,12 @@ suite('TouchEvents', function() { // touchend circle to triger dbltap stage._touchend({ touches: [], + changedTouches: [ + { + clientX: 289, + clientY: 100 + top + } + ], preventDefault: function() {} }); // end drag is tied to document mouseup and touchend event @@ -351,4 +369,227 @@ suite('TouchEvents', function() { 'should NOT trigger dbltap on second circle' ); }); + + test('multitouch - register all touches', function() { + var stage = addStage(); + var layer = new Konva.Layer(); + stage.add(layer); + + var circle1 = new Konva.Circle({ + x: 100, + y: 100, + radius: 70, + fill: 'green', + stroke: 'black', + strokeWidth: 4, + name: 'myCircle1', + draggable: true + }); + layer.add(circle1); + + var circle2 = new Konva.Circle({ + x: 100, + y: 200, + radius: 80, + fill: 'red', + stroke: 'black', + strokeWidth: 4, + name: 'myCircle2', + draggable: true + }); + + layer.add(circle2); + layer.draw(); + + var touchStart = 0; + var touchMove = 0; + var touchEnd = 0; + var touchEnd2 = 0; + + circle1.on('touchstart', function() { + touchStart++; + }); + circle1.on('touchmove', function() { + touchMove++; + }); + circle1.on('touchend', function() { + touchEnd++; + }); + + circle2.on('touchend', function() { + touchEnd2++; + }); + + var stageTouchStart = 0; + var stageTouchMove = 0; + var stageTouchEnd = 0; + stage.on('touchstart', function() { + stageTouchStart++; + }); + stage.on('touchmove', function() { + stageTouchMove++; + }); + stage.on('touchend', function() { + stageTouchEnd++; + }); + + // start with one touch + stage.simulateTouchStart( + [{ x: 100, y: 100, id: 0 }], + [{ x: 100, y: 100, id: 0 }] + ); + + assert.equal(stageTouchStart, 1, 'trigger first touch start on stage'); + assert.equal(touchStart, 1, 'trigger first touch start on circle'); + + // make second touch + stage.simulateTouchStart( + [{ x: 100, y: 100, id: 0 }, { x: 210, y: 100, id: 1 }], + [{ x: 210, y: 100, id: 1 }] + ); + + assert.equal( + stageTouchStart, + 2, + 'should trigger the second touch on stage' + ); + assert.equal( + touchStart, + 1, + 'should not trigger the second touch start (it is outside)' + ); + + // now try to make two touches at the same time + // TODO: should we trigger touch end first? + stage.simulateTouchStart( + [{ x: 100, y: 100, id: 0 }, { x: 210, y: 100, id: 1 }], + [{ x: 100, y: 100, id: 0 }, { x: 210, y: 100, id: 1 }] + ); + + assert.equal(stageTouchStart, 3, 'should trigger one more touch'); + assert.equal( + touchStart, + 2, + 'should trigger the second touch start on the circle' + ); + + // check variables + assert.deepEqual(stage.getPointerPosition(), { x: 100, y: 100, id: 0 }); + assert.deepEqual(stage.getPointersPositions(), [ + { x: 100, y: 100, id: 0 }, + { x: 210, y: 100, id: 1 } + ]); + + // move one finger + stage.simulateTouchMove( + [{ x: 100, y: 100, id: 0 }, { x: 220, y: 100, id: 1 }], + [{ x: 220, y: 100, id: 1 }] + ); + assert.equal(touchMove, 0, 'should not trigger touch move on circle'); + assert.equal(stageTouchMove, 1, 'should trigger touch move on stage'); + + // move two fingers + stage.simulateTouchMove( + [{ x: 100, y: 100, id: 0 }, { x: 220, y: 100, id: 1 }], + [{ x: 100, y: 100, id: 0 }, { x: 220, y: 100, id: 1 }] + ); + assert.equal(touchMove, 1, 'should trigger touch move on circle'); + assert.equal( + stageTouchMove, + 2, + 'should trigger two more touchmoves on stage' + ); + + stage.simulateTouchEnd( + [], + [{ x: 100, y: 100, id: 0 }, { x: 220, y: 100, id: 1 }] + ); + assert.equal(touchEnd, 1); + assert.equal(stageTouchEnd, 1); + + // try two touch ends on both shapes + stage.simulateTouchEnd( + [], + [{ x: 100, y: 100, id: 0 }, { x: 100, y: 170, id: 1 }] + ); + + assert.equal(touchEnd, 2); + assert.equal(touchEnd2, 1); + // TODO: it should be 2, not 3 + assert.equal(stageTouchEnd, 3); + }); + + test('can capture touch events', function() { + Konva.captureTouchEventsEnabled = true; + var stage = addStage(); + var layer = new Konva.Layer(); + stage.add(layer); + + var circle1 = new Konva.Circle({ + x: 100, + y: 100, + radius: 70, + fill: 'green', + stroke: 'black', + strokeWidth: 4, + name: 'myCircle1' + }); + layer.add(circle1); + + layer.draw(); + + var touchStart = 0; + var touchMove = 0; + var touchEnd = 0; + + circle1.on('touchstart', function(e) { + touchStart++; + }); + circle1.on('touchmove', function() { + touchMove++; + }); + circle1.on('touchend', function() { + touchEnd++; + }); + + stage.simulateTouchStart( + [{ x: 100, y: 100, id: 0 }], + [{ x: 100, y: 100, id: 0 }] + ); + + // go out of circle + stage.simulateTouchMove( + [{ x: 180, y: 100, id: 0 }], + [{ x: 180, y: 100, id: 0 }] + ); + assert.equal(touchMove, 1, 'first touchmove'); + + // add another finger + stage.simulateTouchStart( + [{ x: 180, y: 100, id: 0 }, { x: 100, y: 100, id: 1 }], + [{ x: 100, y: 100, id: 1 }] + ); + + // move all out + stage.simulateTouchMove( + [{ x: 185, y: 100, id: 0 }, { x: 190, y: 100, id: 1 }], + [{ x: 185, y: 100, id: 0 }, { x: 190, y: 100, id: 1 }] + ); + // should trigger just one more touchmove + assert.equal(touchMove, 2, 'second touchmove'); + + // remove fingers + stage.simulateTouchEnd( + [], + [{ x: 185, y: 100, id: 0 }, { x: 190, y: 100, id: 1 }] + ); + + assert.equal(touchEnd, 1, 'first touchend'); + + // should release captures on touchend + assert.equal(circle1.hasPointerCapture(0), false); + assert.equal(circle1.hasPointerCapture(1), false); + + Konva.captureTouchEventsEnabled = false; + }); }); diff --git a/test/runner.js b/test/runner.js index 7f35f9bd..d9728726 100644 --- a/test/runner.js +++ b/test/runner.js @@ -283,45 +283,98 @@ Konva.Stage.prototype.simulateMouseUp = function(pos) { Konva.DD._endDragAfter(evt); }; -Konva.Stage.prototype.simulateTouchStart = function(pos) { +Konva.Stage.prototype.simulateTouchStart = function(pos, changed) { var top = this.content.getBoundingClientRect().top; - this._touchstart({ - touches: [ + var touches; + var changedTouches; + if (Array.isArray(pos)) { + touches = pos.map(touch => ({ + identifier: touch.id, + clientX: touch.x, + clientY: touch.y + top + })); + changedTouches = (changed || pos).map(touch => ({ + identifier: touch.id, + clientX: touch.x, + clientY: touch.y + top + })); + } else { + touches = [ { clientX: pos.x, clientY: pos.y + top } - ] - }); + ]; + } + var evt = { + touches: touches, + changedTouches: changedTouches + }; + + this._touchstart(evt); }; -Konva.Stage.prototype.simulateTouchMove = function(pos) { +Konva.Stage.prototype.simulateTouchMove = function(pos, changed) { var top = this.content.getBoundingClientRect().top; - var evt = { - touches: [ + var touches; + var changedTouches; + if (Array.isArray(pos)) { + touches = pos.map(touch => ({ + identifier: touch.id, + clientX: touch.x, + clientY: touch.y + top + })); + changedTouches = (changed || pos).map(touch => ({ + identifier: touch.id, + clientX: touch.x, + clientY: touch.y + top + })); + } else { + touches = [ { clientX: pos.x, clientY: pos.y + top } - ] + ]; + } + var evt = { + touches: touches, + changedTouches: changedTouches }; this._touchmove(evt); Konva.DD._drag(evt); }; -Konva.Stage.prototype.simulateTouchEnd = function(pos) { +Konva.Stage.prototype.simulateTouchEnd = function(pos, changed) { var top = this.content.getBoundingClientRect().top; - var evt = { - touches: [ + var touches; + var changedTouches; + if (Array.isArray(pos)) { + touches = pos.map(touch => ({ + identifier: touch.id, + clientX: touch.x, + clientY: touch.y + top + })); + changedTouches = (changed || pos).map(touch => ({ + identifier: touch.id, + clientX: touch.x, + clientY: touch.y + top + })); + } else { + touches = [ { clientX: pos.x, clientY: pos.y + top } - ] + ]; + } + var evt = { + touches: touches, + changedTouches: changedTouches }; Konva.DD._endDragBefore(evt); diff --git a/test/unit/DragAndDrop-test.js b/test/unit/DragAndDrop-test.js index 920c80d2..cb62a70b 100644 --- a/test/unit/DragAndDrop-test.js +++ b/test/unit/DragAndDrop-test.js @@ -535,6 +535,174 @@ suite('DragAndDrop', function() { assert.equal(circle.y(), 100); }); + test('drag with multi-touch (second finger on empty space)', function() { + var stage = addStage(); + var layer = new Konva.Layer(); + + var circle = new Konva.Circle({ + x: 70, + y: 70, + radius: 70, + fill: 'green', + stroke: 'black', + strokeWidth: 4, + name: 'myCircle', + draggable: true + }); + + layer.add(circle); + stage.add(layer); + + circle.on('dragstart', function() { + assert.equal(circle.x(), 70); + assert.equal(circle.y(), 70); + }); + + stage.simulateTouchStart([ + { + x: 70, + y: 70, + id: 0 + }, + { + x: 270, + y: 270, + id: 1 + } + ]); + + stage.simulateTouchMove([ + { + x: 100, + y: 100, + id: 0 + }, + { + x: 270, + y: 270, + id: 1 + } + ]); + + stage.simulateTouchEnd([ + { + x: 100, + y: 100, + id: 0 + }, + { + x: 270, + y: 270, + id: 1 + } + ]); + assert.equal(circle.x(), 100); + assert.equal(circle.y(), 100); + }); + + test.only('drag with multi-touch (two shapes)', function() { + var stage = addStage(); + var layer = new Konva.Layer(); + stage.add(layer); + + var circle1 = new Konva.Circle({ + x: 70, + y: 70, + radius: 70, + fill: 'green', + stroke: 'black', + strokeWidth: 4, + name: 'myCircle', + draggable: true + }); + layer.add(circle1); + + var circle2 = new Konva.Circle({ + x: 270, + y: 70, + radius: 70, + fill: 'green', + stroke: 'black', + strokeWidth: 4, + name: 'myCircle', + draggable: true + }); + layer.add(circle2); + layer.draw(); + + var dragstart1 = 0; + var dragmove1 = 0; + circle1.on('dragstart', function() { + dragstart1 += 1; + }); + circle1.on('dragmove', function() { + dragmove1 += 1; + }); + + var dragstart2 = 0; + var dragmove2 = 0; + circle2.on('dragstart', function() { + dragstart2 += 1; + }); + + circle2.on('dragmove', function() { + dragmove2 += 1; + }); + + stage.simulateTouchStart([ + { + x: 70, + y: 70, + id: 0 + }, + { + x: 270, + y: 70, + id: 1 + } + ]); + + // move one finger + stage.simulateTouchMove([ + { + x: 100, + y: 100, + id: 0 + }, + { + x: 270, + y: 270, + id: 1 + } + ]); + + assert.equal(dragstart1, 1); + assert.equal(circle1.isDragging(), true); + assert.equal(dragmove1, 1); + assert.equal(circle1.x(), 100); + assert.equal(circle1.y(), 100); + + // move second finger + stage.simulateTouchEnd([ + { + x: 100, + y: 100, + id: 0 + }, + { + x: 290, + y: 270, + id: 1 + } + ]); + + assert.equal(dragstart2, 1); + assert.equal(circle2.isDragging(), true); + assert.equal(dragmove2, 1); + assert.equal(circle2.x(), 290); + assert.equal(circle2.y(), 270); + }); + test('can stop drag on dragstart without changing position later', function() { var stage = addStage(); var layer = new Konva.Layer();