Reputation: 87
I need to create a circle and move it to the closest point in an SVG path on mousedown event
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
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
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