From 0423adf55ef6d95a16aff0c36f8fc70f7649c4ab Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 14 Apr 2019 22:24:44 +1200 Subject: [PATCH] Add touchscreen drag/zoom support (#93) * Add touchscreen pan/zoom support * Add polyfill for Pointer Events API Hopefully this commit can be reverted at some point in the future * Use minified pointer events polyfill * Code style fixes for touchscreen support * Add two-finger tap gesture to reset zoom * Touch support: more style fixes --- InteractiveHtmlBom/core/ibom.py | 1 + InteractiveHtmlBom/web/ibom.css | 4 + InteractiveHtmlBom/web/ibom.html | 8 +- InteractiveHtmlBom/web/pep.js | 43 +++++++ InteractiveHtmlBom/web/render.js | 185 ++++++++++++++++++++++--------- 5 files changed, 189 insertions(+), 52 deletions(-) create mode 100644 InteractiveHtmlBom/web/pep.js diff --git a/InteractiveHtmlBom/core/ibom.py b/InteractiveHtmlBom/core/ibom.py index c8c7127f..5dec2d90 100644 --- a/InteractiveHtmlBom/core/ibom.py +++ b/InteractiveHtmlBom/core/ibom.py @@ -545,6 +545,7 @@ def get_file_content(file_name): html = get_file_content("ibom.html") html = html.replace('///CSS///', get_file_content('ibom.css')) html = html.replace('///SPLITJS///', get_file_content('split.js')) + html = html.replace('///POINTER_EVENTS_POLYFILL///', get_file_content('pep.js')) html = html.replace('///CONFIG///', config_js) html = html.replace('///PCBDATA///', pcbdata_js) html = html.replace('///UTILJS///', get_file_content('util.js')) diff --git a/InteractiveHtmlBom/web/ibom.css b/InteractiveHtmlBom/web/ibom.css index 2e4b0a31..922248d6 100644 --- a/InteractiveHtmlBom/web/ibom.css +++ b/InteractiveHtmlBom/web/ibom.css @@ -611,3 +611,7 @@ a { .dark a { color: #00b9fd; } + +#frontcanvas, #backcanvas { + touch-action: none; +} diff --git a/InteractiveHtmlBom/web/ibom.html b/InteractiveHtmlBom/web/ibom.html index e7ae1363..4f4c383c 100644 --- a/InteractiveHtmlBom/web/ibom.html +++ b/InteractiveHtmlBom/web/ibom.html @@ -13,6 +13,10 @@ ///SPLITJS/// /////////////////////////////////////////////// +/////////////////////////////////////////////// +///POINTER_EVENTS_POLYFILL/// +/////////////////////////////////////////////// + /////////////////////////////////////////////// ///CONFIG/// /////////////////////////////////////////////// @@ -151,7 +155,7 @@
-
+
@@ -159,7 +163,7 @@
-
+
diff --git a/InteractiveHtmlBom/web/pep.js b/InteractiveHtmlBom/web/pep.js new file mode 100644 index 00000000..744f0f4d --- /dev/null +++ b/InteractiveHtmlBom/web/pep.js @@ -0,0 +1,43 @@ +/*! + * PEP v0.4.3 | https://github.com/jquery/PEP + * Copyright jQuery Foundation and other contributors | http://jquery.org/license + */ +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.PointerEventsPolyfill=b()}(this,function(){"use strict";function a(a,b){b=b||Object.create(null);var c=document.createEvent("Event");c.initEvent(a,b.bubbles||!1,b.cancelable||!1); +for(var d,e=2;e=h}return this.firstXY=null,b}},findTouch:function(a,b){for(var c,d=0,e=a.length;d=b.length){var c=[];R.forEach(function(a,d){ +if(1!==d&&!this.findTouch(b,d-2)){var e=a.out;c.push(e)}},this),c.forEach(this.cancelOut,this)}},touchstart:function(a){this.vacuumTouches(a),this.setPrimaryTouch(a.changedTouches[0]),this.dedupSynthMouse(a),this.scrolling||(this.clickCount++,this.processTouches(a,this.overDown))},overDown:function(a){R.set(a.pointerId,{target:a.target,out:a,outTarget:a.target}),u.enterOver(a),u.down(a)},touchmove:function(a){this.scrolling||(this.shouldScroll(a)?(this.scrolling=!0,this.touchcancel(a)):(a.preventDefault(),this.processTouches(a,this.moveOverOut)))},moveOverOut:function(a){var b=a,c=R.get(b.pointerId); +if(c){var d=c.out,e=c.outTarget;u.move(b),d&&e!==b.target&&(d.relatedTarget=b.target,b.relatedTarget=e, +d.target=e,b.target?(u.leaveOut(d),u.enterOver(b)):( +b.target=e,b.relatedTarget=null,this.cancelOut(b))),c.out=b,c.outTarget=b.target}},touchend:function(a){this.dedupSynthMouse(a),this.processTouches(a,this.upOut)},upOut:function(a){this.scrolling||(u.up(a),u.leaveOut(a)),this.cleanUpPointer(a)},touchcancel:function(a){this.processTouches(a,this.cancelOut)},cancelOut:function(a){u.cancel(a),u.leaveOut(a),this.cleanUpPointer(a)},cleanUpPointer:function(a){R["delete"](a.pointerId),this.removePrimaryPointer(a)}, +dedupSynthMouse:function(a){var b=N.lastTouches,c=a.changedTouches[0]; +if(this.isPrimaryTouch(c)){ +var d={x:c.clientX,y:c.clientY};b.push(d);var e=function(a,b){var c=a.indexOf(b);c>-1&&a.splice(c,1)}.bind(null,b,d);setTimeout(e,S)}}};M=new c(V.elementAdded,V.elementRemoved,V.elementChanged,V);var W,X,Y,Z=u.pointermap,$=window.MSPointerEvent&&"number"==typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE,_={events:["MSPointerDown","MSPointerMove","MSPointerUp","MSPointerOut","MSPointerOver","MSPointerCancel","MSGotPointerCapture","MSLostPointerCapture"],register:function(a){u.listen(a,this.events)},unregister:function(a){u.unlisten(a,this.events)},POINTER_TYPES:["","unavailable","touch","pen","mouse"],prepareEvent:function(a){var b=a;return $&&(b=u.cloneEvent(a),b.pointerType=this.POINTER_TYPES[a.pointerType]),b},cleanup:function(a){Z["delete"](a)},MSPointerDown:function(a){Z.set(a.pointerId,a);var b=this.prepareEvent(a);u.down(b)},MSPointerMove:function(a){var b=this.prepareEvent(a);u.move(b)},MSPointerUp:function(a){var b=this.prepareEvent(a);u.up(b),this.cleanup(a.pointerId)},MSPointerOut:function(a){var b=this.prepareEvent(a);u.leaveOut(b)},MSPointerOver:function(a){var b=this.prepareEvent(a);u.enterOver(b)},MSPointerCancel:function(a){var b=this.prepareEvent(a);u.cancel(b),this.cleanup(a.pointerId)},MSLostPointerCapture:function(a){var b=u.makeEvent("lostpointercapture",a);u.dispatchEvent(b)},MSGotPointerCapture:function(a){var b=u.makeEvent("gotpointercapture",a);u.dispatchEvent(b)}},aa=window.navigator;aa.msPointerEnabled?(W=function(a){i(a),j(this),k(a)&&(u.setCapture(a,this,!0),this.msSetPointerCapture(a))},X=function(a){i(a),u.releaseCapture(a,!0),this.msReleasePointerCapture(a)}):(W=function(a){i(a),j(this),k(a)&&u.setCapture(a,this)},X=function(a){i(a),u.releaseCapture(a)}),Y=function(a){return!!u.captureInfo[a]},g(),h(),l();var ba={dispatcher:u,Installer:c,PointerEvent:a,PointerMap:p,targetFinding:v};return ba}); diff --git a/InteractiveHtmlBom/web/render.js b/InteractiveHtmlBom/web/render.js index e4ba7dd2..5b9eae3a 100644 --- a/InteractiveHtmlBom/web/render.js +++ b/InteractiveHtmlBom/web/render.js @@ -429,20 +429,34 @@ function bboxScan(layer, x, y) { return result; } -function handleMouseDown(e, layerdict) { - if (e.which != 1) { +function handlePointerDown(e, layerdict) { + if (e.button != 0) { return; } e.preventDefault(); e.stopPropagation(); - layerdict.transform.mousestartx = e.offsetX; - layerdict.transform.mousestarty = e.offsetY; - layerdict.transform.mousedownx = e.offsetX; - layerdict.transform.mousedowny = e.offsetY; - layerdict.transform.mousedown = true; + + if (!e.hasOwnProperty("offsetX")) { + // The polyfill doesn't set this properly + e.offsetX = e.pageX - e.currentTarget.offsetLeft; + e.offsetY = e.pageY - e.currentTarget.offsetTop; + } + + layerdict.pointerStates[e.pointerId] = { + distanceTravelled: 0, + lastX: e.offsetX, + lastY: e.offsetY, + downTime: Date.now(), + }; } function handleMouseClick(e, layerdict) { + if (!e.hasOwnProperty("offsetX")) { + // The polyfill doesn't set this properly + e.offsetX = e.pageX - e.currentTarget.offsetLeft; + e.offsetY = e.pageY - e.currentTarget.offsetTop; + } + var x = e.offsetX; var y = e.offsetY; var t = layerdict.transform; @@ -459,42 +473,111 @@ function handleMouseClick(e, layerdict) { } } -function handleMouseUp(e, layerdict) { +function handlePointerLeave(e, layerdict) { e.preventDefault(); e.stopPropagation(); - if (e.which == 1 && - layerdict.transform.mousedown && - layerdict.transform.mousedownx == e.offsetX && - layerdict.transform.mousedowny == e.offsetY) { - // This is just a click - handleMouseClick(e, layerdict); - layerdict.transform.mousedown = false; - return; + + if (!redrawOnDrag) { + redrawCanvas(layerdict); + } + + delete layerdict.pointerStates[e.pointerId]; +} + +function resetTransform(layerdict) { + layerdict.transform.panx = 0; + layerdict.transform.pany = 0; + layerdict.transform.zoom = 1; + redrawCanvas(layerdict); +} + +function handlePointerUp(e, layerdict) { + if (!e.hasOwnProperty("offsetX")) { + // The polyfill doesn't set this properly + e.offsetX = e.pageX - e.currentTarget.offsetLeft; + e.offsetY = e.pageY - e.currentTarget.offsetTop; } - if (e.which == 3) { + + e.preventDefault(); + e.stopPropagation(); + + // We haven't necessarily had a pointermove event since the interaction started, so make sure we update this now + var ptr = layerdict.pointerStates[e.pointerId]; + ptr.distanceTravelled += Math.abs(e.offsetX - ptr.lastX) + Math.abs(e.offsetY - ptr.lastY); + + if (e.button == 0 && ptr.distanceTravelled < 10 && Date.now() - ptr.downTime <= 500) { + if (Object.keys(layerdict.pointerStates).length == 1) { + if (layerdict.anotherPointerTapped) { + // This is the second pointer coming off of a two-finger tap + resetTransform(layerdict); + } else { + // This is just a regular tap + handleMouseClick(e, layerdict); + } + layerdict.anotherPointerTapped = false; + } else { + // This is the first finger coming off of what could become a two-finger tap + layerdict.anotherPointerTapped = true; + } + } else if (e.button == 2) { // Reset pan and zoom on right click. - layerdict.transform.panx = 0; - layerdict.transform.pany = 0; - layerdict.transform.zoom = 1; - redrawCanvas(layerdict); - } else if (!redrawOnDrag) { - redrawCanvas(layerdict); + resetTransform(layerdict); + layerdict.anotherPointerTapped = false; + } else { + if (!redrawOnDrag) { + redrawCanvas(layerdict); + } + layerdict.anotherPointerTapped = false; } - layerdict.transform.mousedown = false; + + delete layerdict.pointerStates[e.pointerId]; } -function handleMouseMove(e, layerdict) { - if (!layerdict.transform.mousedown) { +function handlePointerMove(e, layerdict) { + if (!layerdict.pointerStates.hasOwnProperty(e.pointerId)) { return; } e.preventDefault(); e.stopPropagation(); - var dx = e.offsetX - layerdict.transform.mousestartx; - var dy = e.offsetY - layerdict.transform.mousestarty; - layerdict.transform.panx += devicePixelRatio * dx / layerdict.transform.zoom; - layerdict.transform.pany += devicePixelRatio * dy / layerdict.transform.zoom; - layerdict.transform.mousestartx = e.offsetX; - layerdict.transform.mousestarty = e.offsetY; + + if (!e.hasOwnProperty("offsetX")) { + // The polyfill doesn't set this properly + e.offsetX = e.pageX - e.currentTarget.offsetLeft; + e.offsetY = e.pageY - e.currentTarget.offsetTop; + } + + var thisPtr = layerdict.pointerStates[e.pointerId]; + + var dx = e.offsetX - thisPtr.lastX; + var dy = e.offsetY - thisPtr.lastY; + + // If this number is low on pointer up, we count the action as a click + thisPtr.distanceTravelled += Math.abs(dx) + Math.abs(dy); + + if (Object.keys(layerdict.pointerStates).length == 1) { + // This is a simple drag + layerdict.transform.panx += devicePixelRatio * dx / layerdict.transform.zoom; + layerdict.transform.pany += devicePixelRatio * dy / layerdict.transform.zoom; + } else if (Object.keys(layerdict.pointerStates).length == 2) { + var otherPtr = Object.values(layerdict.pointerStates).filter((ptr) => ptr != thisPtr)[0]; + + var oldDist = Math.sqrt(Math.pow(thisPtr.lastX - otherPtr.lastX, 2) + Math.pow(thisPtr.lastY - otherPtr.lastY, 2)); + var newDist = Math.sqrt(Math.pow(e.offsetX - otherPtr.lastX, 2) + Math.pow(e.offsetY - otherPtr.lastY, 2)); + + var scaleFactor = newDist/oldDist; + + if (scaleFactor != NaN) { + layerdict.transform.zoom *= scaleFactor; + + var zoomd = (1 - scaleFactor) / layerdict.transform.zoom; + layerdict.transform.panx += devicePixelRatio * otherPtr.lastX * zoomd; + layerdict.transform.pany += devicePixelRatio * otherPtr.lastY * zoomd; + } + } + + thisPtr.lastX = e.offsetX; + thisPtr.lastY = e.offsetY; + if (redrawOnDrag) { redrawCanvas(layerdict); } @@ -526,18 +609,22 @@ function handleMouseWheel(e, layerdict) { } function addMouseHandlers(div, layerdict) { - div.onmousedown = function(e) { - handleMouseDown(e, layerdict); - }; - div.onmousemove = function(e) { - handleMouseMove(e, layerdict); - }; - div.onmouseup = function(e) { - handleMouseUp(e, layerdict); - }; - div.onmouseout = function(e) { - handleMouseUp(e, layerdict); - } + div.addEventListener("pointerdown", function(e) { + handlePointerDown(e, layerdict); + }); + div.addEventListener("pointermove", function(e) { + handlePointerMove(e, layerdict); + }); + div.addEventListener("pointerup", function(e) { + handlePointerUp(e, layerdict); + }); + var pointerleave = function(e) { + handlePointerLeave(e, layerdict); + } + div.addEventListener("pointercancel", pointerleave); + div.addEventListener("pointerleave", pointerleave); + div.addEventListener("pointerout", pointerleave); + div.onwheel = function(e) { handleMouseWheel(e, layerdict); } @@ -570,10 +657,9 @@ function initRender() { panx: 0, pany: 0, zoom: 1, - mousestartx: 0, - mousestarty: 0, - mousedown: false, }, + pointerStates: {}, + anotherPointerTapped: false, bg: document.getElementById("F_bg"), fab: document.getElementById("F_fab"), silk: document.getElementById("F_slk"), @@ -588,10 +674,9 @@ function initRender() { panx: 0, pany: 0, zoom: 1, - mousestartx: 0, - mousestarty: 0, - mousedown: false, }, + pointerStates: {}, + anotherPointerTapped: false, bg: document.getElementById("B_bg"), fab: document.getElementById("B_fab"), silk: document.getElementById("B_slk"),