Hassan Thamery
Hassan Thamery

Reputation: 87

Move to Mouse Click in SVG path (d3.js)

I need to create a circle and move it to the closest point in an SVG path on mousedown event

here is the jsfiddle

The Code:

        var points = [[180,300],[234,335],[288,310],[350,290],[405,300],[430,305],[475,310],[513,300],[550,280]];

        var width = 1000, height = 600;
        var line = d3.svg.line().interpolate("cardinal");
        var svg = d3.select("#Con").append("svg").attr("width", width).attr("height", height);
        var path = svg.append("path").datum(points).attr("d", line);
        var line = svg.append("line");
        var circle = svg.append("   circle").attr("cx", -10).attr("cy", -10).attr("r", 3.5);
        svg.append("rect").attr("width", width).attr("height", height).on("mousedown", mouseclick);
        function mouseclick() {
            var m = d3.mouse(this),p = closestPoint(path.node(), m);
            circle.attr("cx", p[0]).attr("cy", p[1]);
        }
        function closestPoint(pathNode, point) {
            var pathLength = pathNode.getTotalLength(),precision = 8,best,bestLength,bestDistance = Infinity;
            for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) {
                if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) {
                    best = scan, bestLength = scanLength, bestDistance = scanDistance;
                }
            }
            precision /= 2;
            while (precision > 0.5) {
                var before,after,beforeLength,afterLength,beforeDistance,afterDistance;
                if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) {
                    best = before, bestLength = beforeLength, bestDistance = beforeDistance;
                } else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) {
                    best = after, bestLength = afterLength, bestDistance = afterDistance;
                } else {
                    precision /= 2;
                }
            }
            best = [best.x, best.y];
            best.distance = Math.sqrt(bestDistance);
            return best;
            function distance2(p) {
                var dx = p.x - point[0],dy = p.y - point[1];
                return dx * dx + dy * dy;
            }
        }

I need to move the circle to the closest point in the path when I click in the SVG space

In my code the circle moves without animation and I need to animate it so that it moves on the path from point to point

I need it to always move with one speed whether it moves a large or small distance

like this:

https://a.top4top.net/p_11885szd41.gif

Upvotes: 0

Views: 1008

Answers (2)

Charlie Wallace
Charlie Wallace

Reputation: 1830

This answer modifies your code to move a circle from the start of the line or from the last mouse click to the current mouse click. It animates the movement of the circle by calling an animate method with setTimeout until the circle has moved from the beginning of the line to the point where the mouse was clicked.

The interesting code is here:

        // getAnimate returns a function that is within a closure
       function getAnimate(pLength, path, currentIndex, finishPos, forward){
              let animate = function (){
                let scan = path.node().getPointAtLength(currentIndex);
                if (scan.x < finishPos || !forward && scan.x > finishPos){
                  circle.attr("cx", scan.x).attr("cy", scan.y);
                }
              if (forward){
                currentIndex += 1;
                lastIndex = currentIndex;
                if (scan.x < finishPos){
                  setTimeout(animate, 50);
                  }
                } else {
                    currentIndex -= 1;
                    lastIndex = currentIndex;
                if (scan.x > finishPos){
                  setTimeout(animate, 50);
                }
              }
            }
             return animate;
           }

    var points = [[80,100],[134,115],[188,130],[250,120],[305,120],[330,101],[375,103],[413,100],[550,90]];
                var width = 500, height = 200;
                var line = d3.svg.line().interpolate("cardinal");
                var svg = d3.select("#Con").append("svg").attr("width", width).attr("height", height);
                var path = svg.append("path").datum(points).attr("d", line);
                var line = svg.append("line");
                var circle = svg.append("circle").attr("cx", -10).attr("cy", -10).attr("r", 3.5);
                svg.append("rect").attr("width", width).attr("height", height).on("mousedown", mouseclick);
                var lastIndex = 0;
                function mouseclick() {
                  let m = d3.mouse(this);
                  let p = closestPoint(path.node(), m);
                  let forward = true;
                  let currentPoint = path.node().getPointAtLength(lastIndex);                  
                   if (p[0] < currentPoint.x){
                     forward = false;
                   }
                  let pathLength = path.node().getTotalLength();
                  getAnimate(pathLength, path, lastIndex, p[0], forward)();
                }

                function getAnimate(pLength, path, currentIndex, finishPos, forward){
                  let animate = function (){
                    let scan = path.node().getPointAtLength(currentIndex);
                    if (scan.x < finishPos || !forward && scan.x > finishPos){
                      circle.attr("cx", scan.x).attr("cy", scan.y);
                    }
                  if (forward){
                  currentIndex += 1;
                  lastIndex = currentIndex;
                  if (scan.x < finishPos){
                    setTimeout(animate, 50);
                    }
                    } else {
                          currentIndex -= 1;
                          lastIndex = currentIndex;
                  if (scan.x > finishPos){
                    setTimeout(animate, 50);
                    }
                    }
                }
                 return animate;
               }

                function closestPoint(pathNode, point) {
                    var pathLength = pathNode.getTotalLength(),precision = 8,best,bestLength,bestDistance = Infinity;
                    for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) {
                        if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) {
                            best = scan, bestLength = scanLength, bestDistance = scanDistance;
                        }
                    }
                    precision /= 2;
                    while (precision > 0.5) {
                        var before,after,beforeLength,afterLength,beforeDistance,afterDistance;
                        if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) {
                            best = before, bestLength = beforeLength, bestDistance = beforeDistance;
                        } else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) {
                            best = after, bestLength = afterLength, bestDistance = afterDistance;
                        } else {
                            precision /= 2;
                        }
                    }
                    best = [best.x, best.y];
                    best.distance = Math.sqrt(bestDistance);
                    return best;
                    function distance2(p) {
                        var dx = p.x - point[0],dy = p.y - point[1];
                        return dx * dx + dy * dy;
                    }
                }
    * {
        margin: 0;
        padding: 0;
    }
    #Con {
        border: 1px solid black;
        margin: auto;
        width: 1000px;
        height: 600px;
    }
    #Map{
        z-index: -1000;
        position: absolute;
    }
    #Cha {
        position: absolute;
    }
    path {
        z-index: 1000;
        fill: none;
        stroke: #000;
        stroke-width: 1.5px;
    }
    line {
        fill: none;
        stroke: red;
        stroke-width: 1.5px;
    }
    circle {
      fill: red;
    }
    rect {
        fill: none;
        pointer-events: all;
    }
    <script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?2.5.0"></script>

     <div id="Con"></div>

Upvotes: 0

enxaneta
enxaneta

Reputation: 33044

I'm not using your code but I hope you'll get the idea.

Instead of using a circle I'm using the track:

<use id="theUse" xlink:href="#track"

This track has a stroke-dasharray of ".1 398.80" This means a dash of .1 (very very small) and a gap of 398.80 as long as the track. The stroke-width is 7 with stroke-linecap= "round"and this is transforming the dash into a circle. I'm changing the position of the dash (the circle) using stroke-dashoffset and in order to animate the change I'm using transition: stroke-dashoffset 1s;in the css.

I hope it helps.

let m;
let L = track.getTotalLength();
let _start = {x:180,y:30}
let _end = {x:550,y:280}
let l = dist(_start, _end);
theUse.setAttributeNS(null,"stroke-dashoffset",L);

svg.addEventListener("click",(e)=>{
  m = oMousePosSVG(e)
  
  let pos = m.x - _start.x;
  let theDistance = map(pos,_start.x,_end.x,0,L)
  let s_dof = constrain(L-theDistance, .5, L-.5)
  
  theUse.setAttributeNS(null,"stroke-dashoffset",s_dof)  
})

function oMousePosSVG(e) {
      var p = svg.createSVGPoint();
      p.x = e.clientX;
      p.y = e.clientY;
      var ctm = svg.getScreenCTM().inverse();
      var p =  p.matrixTransform(ctm);
      return p;
}

function dist(p1, p2) {
  let dx = p2.x - p1.x;
  let dy = p2.y - p1.y;
  return Math.sqrt(dx * dx + dy * dy);
}

function map(n, a, b, _a, _b) {
  let d = b - a;
  let _d = _b - _a;
  let u = _d / d;
  return _a + n * u;
}

function constrain(n, low, high) {
  return Math.max(Math.min(n, high), low);
};
svg {
  border: 1px solid;
}
path {
  fill: none;
}

#theUse {
 
  transition: stroke-dashoffset 1s;
  
}
<svg id="svg" viewBox="150 250 450 100">
  <defs>
<path id="track" d="M180,300Q223.2,334,234,335C250.2,336.5,270.6,316.75,288,310S332.45,291.5,350,290S393,297.75,405,300S419.5,303.5,430,305S462.55,310.75,475,310S501.75,304.5,513,300Q520.5,297,550,280"></path>
  </defs> 
  <use  xlink:href="#track" stroke="black" />
  <use id="theUse" xlink:href="#track" stroke-width="7" stroke-dasharray = ".1 398.80" stroke="red" stroke-linecap= "round" />
  
 
</svg>

Upvotes: 3

Related Questions