Jonas Mohammed
Jonas Mohammed

Reputation: 417

Why does touchmove get interupted when the element updates position?

I have svg elements that updated based on mouse position in a mousemove event. I tried doing the same by binding touchmove to the elements as well, but when the element is moved, the touchmove stops working and the element simply stutters slightly before stopping the event alltogether. Is there a way to prevent touchmove from canceling when the element updates position?

$('svgCanvas').mousedown(function(e){
    // here I get element mouse is over
}

$('svgCanvas').mousemove(function(e){
    // here I update the position of the element based on e.pageX, e.pageY
    // the elements are saved in an array which updates as it moves
    Update();
}

//The function I'm trying to make work:
$('svgCanvas').bind('touchmove', function(e){
    e.preventDefault();
    var x = e.originalEvent.touches[0].pageX;
    var y = e.originalEvent.touches[0].pageY;
    if(elem){
       // elem is the element I get from mousedown (which also work on touch)
       elem.position[x,y];
    }
Update();
}

Now what Update() does is that it redraws all the svg elements on the screen from an array of elements I have. I'm pretty sure this is where the problem is, as the touch event stops working as soon as this runs once. The mousemove event however works just fine with it.

Edit: Added a snippet of the issue.

$(document).ready(function() {
  // Initialize variables
  var draw = SVG.get('svgCanvas'),
    group = draw.group().id('svgGroup'),
    clicking = false,
    data = [],
    newSvgElement = {},
    color = "#6658A4",
    elem;

  // Create a new svg element on click
  $('#image').click(function(e){
    var x = e.pageX;
    var y = e.pageY;
    var rand = Math.floor(Math.random() * 100000000);

    if(document.getElementById('createElement').checked){
      //Add the svg to an array as an Object
      newSvgElement.size = 30;
      newSvgElement.id = 'svgElement' + rand;
      newSvgElement.position = [x,y];
      data.push( Object.assign({}, newSvgElement) );
    }
    // Redraws all svg objects in the data[] array
    Update();
  });

  // Stop dragging the svg
  $("#svgCanvas").mouseup(function(){
    clicking = false;
  });

  // Drag the svg on mouse
  $("#svgCanvas").mousemove(function(e){
    if(clicking && document.getElementById("moveElement").checked){
      var x = e.pageX, y = e.pageY;

      if(elem){
        elem.position = [x,y];
        Update();
      }
    }
  });
  
  // Drag svg on touch, this is the one I have issues with
  $('#svgCanvas').bind('touchmove', function(e){
    if(document.getElementById("moveElement").checked) {
      var x = e.originalEvent.touches[0].pageX;
      var y = e.originalEvent.touches[0].pageY;
      
      if(elem){
        elem.position = [x,y];
        Update();
      }
    }
  });

  // Get the svg clicked, if its the background we dont want to move anything
  $("#svgCanvas").mousedown(function(e){
    if(document.getElementById("moveElement").checked) {
      var x = e.clientX,
        y = e.clientY,
        elementMouseIsOver = document.elementFromPoint(x, y).id;
      clicking = true;
      elem = dataInPosition(elementMouseIsOver);
    }
  });

  // Redraws every svg in the array
  function Update(){
    $("#svgGroup").empty();

    for (var i = 0; i < data.length; i++) {
      DrawPoint(data[i]);
    }
  }
  
  // Get the id of the clicked svg
  function dataInPosition(id) {
    if(data.length){
      var pos = data.map(function (element) {
        return element.id
      }).indexOf(id);
      return data[pos];
    }
  }

  // Creates the svg element using SVG.js
  function DrawPoint(svgObject){
    var size    = svgObject.size;
    var x       = svgObject.position[0];
    var y       = svgObject.position[1];

    var drawObject = group.circle(size).id(svgObject.id);
    drawObject.attr({
      fill: color,
      'fill-opacity': "0.0",
      stroke: color,
      'stroke-width': size/10,
      cx: x,
      cy: y
    });
  }
});
#form{
  position: absolute;
  left: 30px;
  top: 30px;
}
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
<svg id="svgCanvas" height="900px" width="450px">
  <image id='image' xlink:href="http://routegadget.jukola.com/kartat/41.jpg" x="0" y="0" height="900px" width="450px"/>
</svg>
<div id="form">
  <form>
    <input type="radio" name="state" id="createElement" class="state" checked>
    <label for="createElement">Create Element <br/></label>
    <input type="radio" name="state" id="moveElement" class="state">
    <label for="moveElement">Move Element <br/></label>
  </form>
</div>

Upvotes: 0

Views: 470

Answers (1)

Sergey Rudenko
Sergey Rudenko

Reputation: 9235

So your widget is "crazy":) I mean it does a lot of things inefficiently imho. I rewrote it to something how I would write it.

Please check it out. Let me know if you need clarifications

As for what i think is happening:

  • the issue is in the Update function
  • if you comment out first action there group.clear() which removes all drawn circles, the touchmove will start spawning circles all over the place...but it will kinda work
  • i suspect svg.js does something that silences touchmove listener hence i would not rely on this library
  • but in general current approach is extremely inefficient: redraw whole scene on each move event. There is a lot of dom manipulations here. Once you have a lot of circles - even though you only need to move one - you would re-render everything.

My code is not perfect yet as well and u can still optimize it further but its current performance should be good.

// Initialize variables

// mode: on/off
var createMode = true;
// caching DOM stuff:
var svgCanvas = document.getElementById('svgCanvas');
var svgGroupContainer = document.getElementById('svgGroupContainer');
// this will hold our target element:
var elem;

// init method:
function onSVGinit() {
  // this will hold SVG space coordinates:
  var inputCoordinates = svgCanvas.createSVGPoint();
  // adding initial event listeners:
  svgCanvas.addEventListener('mousedown', mouseDown, { passive: true })
  svgCanvas.addEventListener('touchstart', touchDown, { passive: true })
  // on input methods:
  function mouseDown(e) {
    elem = e.target;
    // need to do coordinates conversion to match SVG space:
    inputCoordinates.x = event.clientX;
    inputCoordinates.y = event.clientY;
    inputCoordinates = inputCoordinates.matrixTransform(svgCanvas.getScreenCTM().inverse());
    svgCanvas.addEventListener('mousemove', move, { passive: true })
    svgCanvas.addEventListener('mouseup', end, {passive:true});
  }
  function touchDown(e) {
    elem = e.target
    // need to do coordinates conversion to match SVG space:
    inputCoordinates.x = event.touches[0].clientX;
    inputCoordinates.y = event.touches[0].clientY;
    inputCoordinates = inputCoordinates.matrixTransform(svgCanvas.getScreenCTM().inverse());
    svgCanvas.addEventListener('touchmove', move, { passive: true })
    svgCanvas.addEventListener('touchup', end, {passive:true});
  }
  function move(e) {
    if (!createMode) {
    // need to do coordinates conversion to match SVG space:
    inputCoordinates.x = event.clientX? event.clientX : event.touches[0].clientX;
    inputCoordinates.y = event.clientY? event.clientY : event.touches[0].clientY;
    inputCoordinates = inputCoordinates.matrixTransform(svgCanvas.getScreenCTM().inverse());
    }
    elem.setAttribute("cx", inputCoordinates.x);
    elem.setAttribute("cy", inputCoordinates.y);
  }
  function end(e) {
    // dynamically remove listeners:
    svgCanvas.removeEventListener('touchmove', move, { passive: true });
    svgCanvas.removeEventListener('mousemove', move, { passive: true });
    svgCanvas.removeEventListener('touchup', move, { passive: true });
    svgCanvas.removeEventListener('mouseup', move, { passive: true });
    // if createMode is on - we spawn a circle:
    if (createMode) {
       createCircle()
    }
  }
  // function to create circle element:
  function createCircle() {
    var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle")
    circle.setAttribute("cx", inputCoordinates.x);
    circle.setAttribute("cy", inputCoordinates.y);
    circle.setAttribute("r", 30);
    circle.setAttribute("fill", "transparent");
    circle.setAttribute("stroke","#6658A4")
    svgGroupContainer.appendChild(circle);
  } 
};
function switchMode(e) {
    createMode = !createMode;
}
#form{
  position: absolute;
  left: 30px;
  top: 30px;
}
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
<svg id="svgCanvas" height="900px" width="450px" onload="onSVGinit()">
  <image id='image' xlink:href="http://routegadget.jukola.com/kartat/41.jpg" x="0" y="0" height="900px" width="450px"/>
  <g id="svgGroupContainer"></g>
</svg>
<div id="form">
  <form>
    <input type="radio" name="state" id="createElement" class="state" onclick="switchMode()" checked>
    <label for="createElement">Create Element <br/></label>
    <input type="radio" name="state" id="moveElement" class="state" onclick="switchMode()">
    <label for="moveElement">Move Element <br/></label>
  </form>
</div>

Upvotes: 1

Related Questions