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: 10px; | |
| stroke-linejoin: round; | |
| stroke-linecap: 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="https://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 - 200, | |
| 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; | |
| })(); |