Skip to content

Instantly share code, notes, and snippets.

# pbeshai/.block

Last active Oct 11, 2019
Jagged Lines
 license: mit height: 500 border: no
 { "extends": "eslint-config-airbnb", "rules": { "no-mixed-operators": 0, "no-param-reassign": 0, }, "globals": { "d3": true, "dat": true, }, "env": { "browser": true, }, }

### Jagged Lines

Given two points, draw a jagged line between them using D3 v4.

You can configure the height of the peaks via `maxPeakHeight` and the distance between peaks with `minPeakDistance`.

The logic for computing the jagged points is done in `createJaggedPoints()`. The basic process is that the two ends points are rotated so that they are in line with the x-axis. Then at random points in between the ends (based on `minPeakDistance`), the y value is modified (based on `maxPeakHeight`). Finally, the line is unrotated and you get the desired result.

An alternative approach that does not involve rotation would be computing the slope perpendicular to the line and using that to compute the offset points. It is slightly more challenging to give intuitive inputs like the pixels defined by `maxPeakHeight` and `minPeakDistance` if you take that approach, but still possible.

 path{fill:none;stroke-width:2px;stroke:#0bb}.baseline{stroke:#ddd;stroke-dasharray:4 4}circle{fill:none;stroke:#888}
 function rotate(t,a,n){var e=t[0],i=t[1],r=a[0],d=a[1],h=e+(r-e)*Math.cos(n)-(d-i)*Math.sin(n),o=i+(r-e)*Math.sin(n)+(d-i)*Math.cos(n);return[h,o]}function createJaggedPoints(t,a,n,e){var i=!1;if(t[0]>a[0]){var r=t;t=a,a=r,i=!0}var d=t[0],h=t[1],o=a[0],s=a[1],g=[t],u=s-h,c=o-d,p=-Math.atan(u/c),l=Math.sqrt(Math.pow(o-d,2)+Math.pow(s-h,2));e||(e=.05*l);for(var v=rotate(t,a,p),w=v[0],m=v[1],f=d;f
 Jagged Lines
 /** * Setup globals */ const width = 700; const height = 500; const padding = 50; const plotAreaWidth = width - (2 * padding); const plotAreaHeight = height - (2 * padding); const svg = d3.select('#main-svg') .attr('width', width) .attr('height', height) .append('g') .attr('transform', `translate(\${padding} \${padding})`); /** * Helper function to rotate a point around an origin by theta radians */ function rotate(origin, point, thetaRadians) { const [originX, originY] = origin; const [pointX, pointY] = point; const rotatedEndX = originX + (pointX - originX) * Math.cos(thetaRadians) - (pointY - originY) * Math.sin(thetaRadians); const rotatedEndY = originY + (pointX - originX) * Math.sin(thetaRadians) + (pointY - originY) * Math.cos(thetaRadians); return [rotatedEndX, rotatedEndY]; } /** * Creates a series of jagged points between start and end based on * maxPeakHeight for how far away from the midline they get to be and * minPeakDistance for how often they occur. If minPeakDistance is not * provided, it will add roughly 18 points to the line (every 5% of the * line length). */ function createJaggedPoints(start, end, maxPeakHeight, minPeakDistance) { // we want the one with farthest left X to be 'start' let reversed = false; if (start[0] > end[0]) { const swap = start; start = end; end = swap; reversed = true; } const [startX, startY] = start; const [endX, endY] = end; // keep the start point unmodified const points = [start]; // rotate it so end point is horizontal with start point const opposite = endY - startY; const adjacent = endX - startX; const thetaRadians = -Math.atan(opposite / adjacent); // compute the overall length of the line const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); if (!minPeakDistance) { minPeakDistance = length * 0.05; } // compute rotated end point const [rotatedEndX, rotatedEndY] = rotate(start, end, thetaRadians); // generate the intermediate peak points let lastX = startX; while (lastX < rotatedEndX - minPeakDistance) { // move minPeakDistance from previous X + some random amount, but stop at most at // minPeakDistance from the end const nextX = Math.min(lastX + minPeakDistance + (Math.random() * minPeakDistance), rotatedEndX - minPeakDistance); // add some randomness to the expected y position to get peaks // we can use startY as the expected y position since we rotated the line to be flat const nextY = (maxPeakHeight * (Math.random() - 0.5)) + startY; points.push([nextX, nextY]); lastX = nextX; } // add in the end point points.push([rotatedEndX, rotatedEndY]); // undo the rotation and return the points as the result const unrotated = points.map((point, i) => { if (i === 0) { return start; } else if (i === points.length - 1) { return end; } return rotate(start, point, -thetaRadians); }); // restore original directionality if we reversed it return reversed ? unrotated.reverse() : unrotated; } /* * Animate the line based on pathSpeed. Uses sroke-dasharray. */ function transitionLine(path, pathSpeed) { const pathLength = path.node().getTotalLength(); path .attr('stroke-dasharray', '0,100000') // fix safari flash .transition() .duration(pathLength / (pathSpeed / 1000)) .ease(d3.easeQuadOut) .attrTween('stroke-dasharray', function tweenDash() { // Dashed line interpolation trick from https://bl.ocks.org/mbostock/5649592 const length = this.getTotalLength(); return d3.interpolateString(`0,\${length}`, `\${length},\${length}`); }) // Remove stroke-dasharray property at the end .on('end', function endDashTransition() { d3.select(this).attr('stroke-dasharray', 'none'); }); } /** * Draw the jagged path with animation */ function drawJaggedPath(start, end, maxPeakHeight, minPeakDistance, pathSpeed, curved) { // generate the intermediate points to make the jagged line const points = createJaggedPoints(start, end, maxPeakHeight, minPeakDistance); // draw the line svg.append('path').datum(points) .attr('d', d3.line().curve(curved ? d3.curveBasis : d3.curveLinear)) .call(path => transitionLine(path, pathSpeed)); } /** * Draw the end points as circles so we can verify that the * path is still going through the expected points. */ function drawPoints(...points) { const circles = svg.selectAll('circle').data(points); circles.merge(circles.enter().append('circle') .attr('r', 3)) .attr('cx', d => d[0]) .attr('cy', d => d[1]); circles.exit().remove(); } /** * Draw the original line between the two points */ function drawBaseline(start, end) { svg.append('path').datum([start, end]) .classed('baseline', true) .attr('d', d3.line()); } /** * Helper function to generate a random point in the plot area */ function randomPoint() { return [ Math.round(Math.random() * plotAreaWidth), Math.round(Math.random() * plotAreaHeight), ]; } function update(maxPeakHeight, minPeakDistance, pathSpeed, curved, showEndPoints, showBaseline) { // remove existing path svg.selectAll('path').remove(); // generate random start and end points const start = randomPoint(); const end = randomPoint(); // draw circles for the endpoints if (showEndPoints) { drawPoints(start, end); } else { drawPoints(); } if (showBaseline) { drawBaseline(start, end); } // draw the jagged path drawJaggedPath(start, end, maxPeakHeight, minPeakDistance, pathSpeed, curved); } /** * Initialize the application with datGUI to control parameters */ function JaggedLines() { this.maxPeakHeight = 80; this.minPeakDistance = 15; this.pathSpeed = 400; // pixels per second this.curved = false; this.showEndPoints = true; this.showBaseline = true; this.makeNewLine = function makeNewLine() { update(Math.round(this.maxPeakHeight), Math.round(this.minPeakDistance), this.pathSpeed, this.curved, this.showEndPoints, this.showBaseline); }; } window.onload = function onLoad() { const jaggedLines = new JaggedLines(); const gui = new dat.GUI(); // callback so when the input is changed, we make a new line function newLineOnChange() { jaggedLines.makeNewLine(); } gui.add(jaggedLines, 'maxPeakHeight', 10, 100).onFinishChange(newLineOnChange); gui.add(jaggedLines, 'minPeakDistance', 0, 50).onFinishChange(newLineOnChange); gui.add(jaggedLines, 'pathSpeed', 100, 1000).onFinishChange(newLineOnChange); gui.add(jaggedLines, 'curved').onFinishChange(newLineOnChange); gui.add(jaggedLines, 'showEndPoints').onFinishChange(newLineOnChange); gui.add(jaggedLines, 'showBaseline').onFinishChange(newLineOnChange); gui.add(jaggedLines, 'makeNewLine'); // do first draw with defaults jaggedLines.makeNewLine(); };
 path { fill: none; stroke-width: 2px; stroke: #0bb; } .baseline { stroke: #ddd; stroke-dasharray: 4 4; } circle { fill: none; stroke: #888; }
to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.