Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented path recognizer for recognizing along an SVGpath #991

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/expose.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ assign(Hammer, {
Pinch: PinchRecognizer,
Rotate: RotateRecognizer,
Press: PressRecognizer,
Path: PathRecognizer,

on: addEventListeners,
off: removeEventListeners,
Expand Down
168 changes: 168 additions & 0 deletions src/recognizers/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Path
* Recognized when the pointer is down and moves following a path.
* @constructor
* @extends AttrRecognizer
*/
function PathRecognizer() {
AttrRecognizer.apply(this, arguments);

this.pathTotalLength = this.options.pathElement.getTotalLength();
this.segmentLength = (this.pathTotalLength / this.options.resolution);
this.currentSegmentIndex = 0; //which segment should we match against next
this.pX = null;
this.pY = null;

this.state = STATE_BEGAN;

}

inherit(PathRecognizer, AttrRecognizer, {
/**
* @namespace
* @memberof PathRecognizer
*/
defaults: {
event: 'path',
threshold: 5,
pointers: 1,
resolution: 10, //path will be quantizied to this amount of segments
maxDistanceFromSegment: 30
},

attrTest: function(input) {
return AttrRecognizer.prototype.attrTest.call(this, input);
},

emit: function(input) {
this.pX = input.deltaX;
this.pY = input.deltaY;

this._super.emit.call(this, input);
},

/**
* Process the input and return the state for the recognizer
* @memberof AttrRecognizer
* @param {Object} input
* @returns {*} State
*/
process: function(input) {
if (!this.attrTest(input)) {
return STATE_FAILED;
}

var svgCoords = getSvgLocalPoint(this.options.svgElement, input.center.x, input.center.y);

input.localX = svgCoords.x;
input.localY = svgCoords.y;

var closestPoint = findClosestPoint(this.options.pathElement, svgCoords);
this.pathPercent = input.pathPercent = closestPoint.pathPercent;
this.pathLength = input.pathLength = closestPoint.pathLength;

//veer too far from path - fail
if (closestPoint.distance > this.options.maxDistanceFromSegment) {
input.pathComplete = false;
return STATE_ENDED;
}
else if (input.distance === 0 && this.state != STATE_BEGAN) {
return STATE_BEGAN;
}
else if (this.currentSegmentIndex == this.options.resolution && (100 - this.pathPercent) < 0.9) {
input.pathComplete = true;
return STATE_RECOGNIZED;
}
else if (this.pathPercent / 100 > (this.currentSegmentIndex) * this.segmentLength / this.pathTotalLength &&
this.pathPercent / 100 < (this.currentSegmentIndex + 1) * this.segmentLength / this.pathTotalLength) {
this.currentSegmentIndex++;
}
//start drawing path from middle or jump ahead - fail
else if (this.pathPercent / 100 > (this.currentSegmentIndex + 1) * this.segmentLength / this.pathTotalLength) {
input.pathComplete = false;
return STATE_ENDED;
}
//stop input before reaching end - fail
else if (input.eventType == INPUT_END) {
input.pathComplete = false;
return STATE_ENDED;
}

return STATE_CHANGED;

}
});

// var distance = function(p1, p2) {
// return Math.sqrt((p2.x -= p1.x) * p2.x + (p2.y -= p1.y) * p2.y);
// };

//adapted from https://bl.ocks.org/mbostock/8027637
function findClosestPoint(pathNode, point) {
var pathLength = pathNode.getTotalLength(),
precision = 8,
best,
bestLength,
bestDistance = Infinity;

// linear scan for coarse approximation
for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) {
if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) {
best = scan, bestLength = scanLength, bestDistance = scanDistance;
}
}

// binary search for precise estimate
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 = {
x: best.x,
y: best.y,
distance: Math.sqrt(bestDistance),
pathLength: bestLength,
pathPercent: (bestLength / pathLength * 100 * 100) / 100
};

return best;

function distance2(p) {
var dx = p.x - point.x,
dy = p.y - point.y;
return dx * dx + dy * dy;
}
}

// Get point in global SVG space
var getSvgLocalPoint = (function() {
var pt = null;

return function(svg, x, y) {

if (pt == null) {
pt = svg.createSVGPoint();
}

pt.x = x;
pt.y = y;
return pt.matrixTransform(svg.getScreenCTM().inverse());

};

})();
120 changes: 120 additions & 0 deletions tests/manual/pathRecognizer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="assets/style.css">
<title>Hammer.js</title>

<style>
path.sourcePath{
transition: all 0.5s ease-out;
}
</style>
</head>
<body>
<div class="container">

<div id="hit" class="bg1" style="padding: 30px;">
<div id="target" class="bg5" style="display: block; text-align: center;">
<svg viewBox="-20 -20 140 140" style="width: 100%; height: 100%;">
<path class="sourcePath" d="" stroke-width="4" stroke="black" fill="none"/>
<path class="trackPath" d="" stroke-width="4.1" stroke="red" fill="none"/>
</svg>
</div>
</div>

<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>

<pre id="log" style="overflow:hidden;"></pre>

</div>
<script src="../../hammer.min.js"></script>

<script>

var svgEl = document.querySelector("svg");
var pathEl = document.querySelector("path.sourcePath");

var trackPathEl = document.querySelector("path.trackPath");

var currentPathIndex = 0;
var paths = ["M0 0 L50 25 L75 50 L100 100",
"m18.479046,18.981368l-0.144146,62.395447l60.279122,-60.530285l0,64.548894",
"m17.002326,40.230229c0,0 6.813966,-27.004652 32.813966,-25.004652c26,2 32.176729,16.860487 34.697664,28.813966c2.520935,11.953478 -0.82327,35.702316 -29.82327,40.702316c-29,5 -29.413937,-23.079085 -19.413937,-36.079085c10,-13 16.493022,-5.092993 25.493022,8.907007",
"m12.702296,59.293031c6.560497,-19.062802 20.84653,-13.060477 21.851182,0.753489c1.004652,13.813966 26.120953,19.590715 25.367464,-0.251163c-0.753489,-19.841878 27.376768,-19.841878 27.125605,0.502326"
];

var mc;

function changePath(index) {
pathEl.setAttribute("d", paths[index]);
trackPathEl.setAttribute("d", paths[index]);
var pathLength = trackPathEl.getTotalLength();
trackPathEl.style.strokeDasharray = [0, pathLength].join(' ');

trackPathEl.style.transition = "none";
trackPathEl.style.stroke = "red";

if (mc) {
mc.destroy();
}

var options = {
touchAction: "none"
};

mc = new Hammer(document.querySelector("svg"), options);
mc.add(new Hammer.Path({
svgElement: svgEl,
pathElement: pathEl
}));

mc.on("pathmove", function(ev){
console.log(Math.round(ev.pathPercent), "% completed");
trackPathEl.style.strokeDasharray = [ev.pathLength, pathLength].join(' ');
});

mc.on("pathstart", function(ev){
trackPathEl.style.transition = "none";
});

mc.on("pathend", function(ev){
if (ev.pathComplete == true) {
trackPathEl.style.transition = "stroke 0.3s ease-in, stroke-dasharray 0.4s ease-in";
trackPathEl.style.stroke = "rgba(66,214,84,1)";
trackPathEl.style.strokeDasharray = [pathLength, 0 ].join(' ');

currentPathIndex++;
currentPathIndex = currentPathIndex == paths.length ? 0 : currentPathIndex;

setTimeout(function(){
changePath(currentPathIndex);
}, 1000);

}
else{
trackPathEl.style.transition = "stroke 0.5s ease-out, stroke-dasharray 0.4s ease-out";
trackPathEl.style.strokeDasharray = [0, pathLength].join(' ');
}

});

// mc.on("path pathstart pathend pathmove pathcancel",
// logGesture);
//
//
// function logGesture(ev) {
// console.log(ev.timeStamp, ev.type, ev);
// }

}

changePath(0);




</script>
</body>
</html>