Skip to content

Instantly share code, notes, and snippets.

@elidupuis
Last active January 8, 2023 15:36
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save elidupuis/11325438 to your computer and use it in GitHub Desktop.
Save elidupuis/11325438 to your computer and use it in GitHub Desktop.
Path from mouse or touch input

Simple Drawing Example

Click and drag your mouse (or finger on touch device) to draw a line. A new path is created for each touch or click.

Paths are stored in a nested array; you can inspect the variable in console: session (assuming the example is in it's own window).

<!doctype html>
<html lang="en">
<head lang=en>
<meta charset="utf-8">
<title>Tracing a line with d3.js</title>
<style>
svg {
background: #ddd;
font: 10px sans-serif;
cursor: crosshair;
}
.line {
cursor: crosshair;
fill: none;
stroke: #000;
stroke-width: 2px;
stroke-linejoin: round;
}
#output {
position: relative;
top: -2em;
left: 0.67em;
font: 12px/1.4 monospace;
}
</style>
</head>
<body>
<div id="sketch"></div>
<div id="output"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="simplify.js"></script>
<script>
// based on http://bl.ocks.org/cloudshapes/5661984 by cloudshapes
var margin = {top: 0, right: 0, bottom: 0, left: 0},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// var npoints = 100;
var ptdata = [];
var session = [];
var path;
var drawing = false;
var output = d3.select('#output');
var line = d3.svg.line()
.interpolate("bundle") // basis, see http://bl.ocks.org/mbostock/4342190
.tension(1)
.x(function(d, i) { return d.x; })
.y(function(d, i) { return d.y; });
var svg = d3.select("#sketch").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg
.on("mousedown", listen)
.on("touchstart", listen)
.on("touchend", ignore)
.on("touchleave", ignore)
.on("mouseup", ignore)
.on("mouseleave", ignore);
// ignore default touch behavior
var touchEvents = ['touchstart', 'touchmove', 'touchend'];
touchEvents.forEach(function (eventName) {
document.body.addEventListener(eventName, function(e){
e.preventDefault();
});
});
function listen () {
drawing = true;
output.text('event: ' + d3.event.type);
ptdata = []; // reset point data
path = svg.append("path") // start a new line
.data([ptdata])
.attr("class", "line")
.attr("d", line);
if (d3.event.type === 'mousedown') {
svg.on("mousemove", onmove);
} else {
svg.on("touchmove", onmove);
}
}
function ignore () {
var before, after;
output.text('event: ' + d3.event.type);
svg.on("mousemove", null);
svg.on("touchmove", null);
// skip out if we're not drawing
if (!drawing) return;
drawing = false;
before = ptdata.length;
console.group('Line Simplification');
console.log("Before simplification:", before)
// simplify
ptdata = simplify(ptdata);
after = ptdata.length;
console.log("After simplification:", ptdata.length)
console.groupEnd();
var percentage = parseInt(100 - (after/before)*100, 10);
output.html('Points: ' + before + ' => ' + after + '. <b>' + percentage + '% simplification.</b>');
// add newly created line to the drawing session
session.push(ptdata);
// redraw the line after simplification
tick();
}
function onmove (e) {
var type = d3.event.type;
var point;
if (type === 'mousemove') {
point = d3.mouse(this);
output.text('event: ' + type + ': ' + d3.mouse(this));
} else {
// only deal with a single touch input
point = d3.touches(this)[0];
output.text('event: ' + type + ': ' + d3.touches(this)[0]);
}
// push a new data point onto the back
ptdata.push({ x: point[0], y: point[1] });
tick();
}
function tick() {
path.attr("d", function(d) { return line(d); }) // Redraw the path:
}
</script>
</body>
</html>
/*
(c) 2013, Vladimir Agafonkin
Simplify.js, a high-performance JS polyline simplification library
mourner.github.io/simplify-js
*/
(function () { 'use strict';
// to suit your point format, run search/replace for '.x' and '.y';
// for 3D version, see 3d branch (configurability would draw significant performance overhead)
// square distance between 2 points
function getSqDist(p1, p2) {
var dx = p1.x - p2.x,
dy = p1.y - p2.y;
return dx * dx + dy * dy;
}
// square distance from a point to a segment
function getSqSegDist(p, p1, p2) {
var x = p1.x,
y = p1.y,
dx = p2.x - x,
dy = p2.y - y;
if (dx !== 0 || dy !== 0) {
var t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = p2.x;
y = p2.y;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = p.x - x;
dy = p.y - y;
return dx * dx + dy * dy;
}
// rest of the code doesn't care about point format
// basic distance-based simplification
function simplifyRadialDist(points, sqTolerance) {
var prevPoint = points[0],
newPoints = [prevPoint],
point;
for (var i = 1, len = points.length; i < len; i++) {
point = points[i];
if (getSqDist(point, prevPoint) > sqTolerance) {
newPoints.push(point);
prevPoint = point;
}
}
if (prevPoint !== point) newPoints.push(point);
return newPoints;
}
// simplification using optimized Douglas-Peucker algorithm with recursion elimination
function simplifyDouglasPeucker(points, sqTolerance) {
var len = points.length,
MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array,
markers = new MarkerArray(len),
first = 0,
last = len - 1,
stack = [],
newPoints = [],
i, maxSqDist, sqDist, index;
markers[first] = markers[last] = 1;
while (last) {
maxSqDist = 0;
for (i = first + 1; i < last; i++) {
sqDist = getSqSegDist(points[i], points[first], points[last]);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
markers[index] = 1;
stack.push(first, index, index, last);
}
last = stack.pop();
first = stack.pop();
}
for (i = 0; i < len; i++) {
if (markers[i]) newPoints.push(points[i]);
}
return newPoints;
}
// both algorithms combined for awesome performance
function simplify(points, tolerance, highestQuality) {
if (points.length <= 1) return points;
var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
points = simplifyDouglasPeucker(points, sqTolerance);
return points;
}
// export as AMD module / Node module / browser or worker variable
if (typeof define === 'function' && define.amd) define(function() { return simplify; });
else if (typeof module !== 'undefined') module.exports = simplify;
else if (typeof self !== 'undefined') self.simplify = simplify;
else window.simplify = simplify;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment