Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active May 25, 2022 17:34
Show Gist options
  • Save pbeshai/72c446033a98f99ce1e1371c6eee9644 to your computer and use it in GitHub Desktop.
Save pbeshai/72c446033a98f99ce1e1371c6eee9644 to your computer and use it in GitHub Desktop.
Compute points and angles along a bezier curve
license: mit
height: 320
border: no

Compute points and angles along a bezier curve

Given the end points and control points along a bezier curve, this code shows how to get any point and any angle along the curve.

function quadraticPath(t){return"M"+t.start[0]+","+t.start[1]+" Q"+t.control[0]+","+t.control[1]+" "+t.end[0]+","+t.end[1]}function interpolateQuadraticBezier(t,r,a){return function(n){return[Math.pow(1-n,2)*t[0]+2*(1-n)*n*r[0]+Math.pow(n,2)*a[0],Math.pow(1-n,2)*t[1]+2*(1-n)*n*r[1]+Math.pow(n,2)*a[1]]}}function interpolateQuadraticBezierAngle(t,r,a){return function(n){var e=2*(1-n)*(r[0]-t[0])+2*n*(a[0]-r[0]),o=2*(1-n)*(r[1]-t[1])+2*n*(a[1]-r[1]);return Math.atan2(o,e)*(180/Math.PI)}}function cubicPath(t){return"M"+t.start[0]+","+t.start[1]+" C"+t.control1[0]+","+t.control1[1]+" "+t.control2[0]+","+t.control2[1]+" "+t.end[0]+","+t.end[1]}function interpolateCubicBezier(t,r,a,n){return function(e){return[Math.pow(1-e,3)*t[0]+3*Math.pow(1-e,2)*e*r[0]+3*(1-e)*Math.pow(e,2)*a[0]+Math.pow(e,3)*n[0],Math.pow(1-e,3)*t[1]+3*Math.pow(1-e,2)*e*r[1]+3*(1-e)*Math.pow(e,2)*a[1]+Math.pow(e,3)*n[1]]}}function interpolateCubicBezierAngle(t,r,a,n){return function(e){var o=3*Math.pow(1-e,2)*(r[0]-t[0])+6*(1-e)*e*(a[0]-r[0])+3*Math.pow(e,2)*(n[0]-a[0]),c=3*Math.pow(1-e,2)*(r[1]-t[1])+6*(1-e)*e*(a[1]-r[1])+3*Math.pow(e,2)*(n[1]-a[1]);return Math.atan2(c,o)*(180/Math.PI)}}function drawQuadratic(t){var r=g.append("g").attr("class","quadratic");r.append("text").text("Quadratic Bezier").attr("dy","0.4em"),r.append("circle").attr("r",5).attr("class","start-point").attr("cx",t.start[0]).attr("cy",t.start[1]),r.append("circle").attr("r",5).attr("class","end-point").attr("cx",t.end[0]).attr("cy",t.end[1]),r.append("circle").attr("r",3).attr("class","control-point").attr("cx",t.control[0]).attr("cy",t.control[1]),r.append("path").attr("class","curve").attr("d",quadraticPath(t));var a=interpolateQuadraticBezier(t.start,t.control,t.end),n=d3.range(10).map(function(t,r,n){return a(t/(n.length-1))});r.selectAll(".interpolated-point").data(n).enter().append("circle").attr("class","interpolated-point").attr("r",3).attr("cx",function(t){return t[0]}).attr("cy",function(t){return t[1]});var e=interpolateQuadraticBezierAngle(t.start,t.control,t.end),o=d3.range(3).map(function(t,r,n){var o=t/(n.length-1);return{t:o,position:a(o),angle:e(o)}});return r.selectAll(".rotated-point").data(o).enter().append("path").attr("d","M12,0 L-5,-8 L0,0 L-5,8 Z").attr("class","rotated-point").attr("transform",function(t){return"translate("+t.position[0]+", "+t.position[1]+") rotate("+t.angle+")"}),console.log("QUADRATIC"),console.log(JSON.stringify(t)),console.log(JSON.stringify(o,null,2)),r}function drawCubic(t){var r=g.append("g").attr("class","cubic").attr("transform","translate(250, 0)");r.append("text").text("Cubic Bezier").attr("dy","0.4em"),r.append("circle").attr("r",5).attr("class","start-point").attr("cx",t.start[0]).attr("cy",t.start[1]),r.append("circle").attr("r",5).attr("class","end-point").attr("cx",t.end[0]).attr("cy",t.end[1]),r.append("circle").attr("r",3).attr("class","control-point").attr("cx",t.control1[0]).attr("cy",t.control1[1]),r.append("circle").attr("r",3).attr("class","control-point").attr("cx",t.control2[0]).attr("cy",t.control2[1]),r.append("path").attr("class","curve").attr("d",cubicPath(t));var a=interpolateCubicBezier(t.start,t.control1,t.control2,t.end),n=d3.range(10).map(function(t,r,n){return a(t/(n.length-1))});r.selectAll(".interpolated-point").data(n).enter().append("circle").attr("class","interpolated-point").attr("r",3).attr("cx",function(t){return t[0]}).attr("cy",function(t){return t[1]});var e=interpolateCubicBezierAngle(t.start,t.control1,t.control2,t.end),o=d3.range(3).map(function(t,r,n){var o=t/(n.length-1);return{t:o,position:a(o),angle:e(o)}});return r.selectAll(".rotated-point").data(o).enter().append("path").attr("d","M12,0 L-5,-8 L0,0 L-5,8 Z").attr("class","rotated-point").attr("transform",function(t){return"translate("+t.position[0]+", "+t.position[1]+") rotate("+t.angle+")"}),console.log("CUBIC"),console.log(JSON.stringify(t)),console.log(JSON.stringify(o,null,2)),r}var width=500,height=300,padding={top:10,right:10,bottom:10,left:10},svg=d3.select("body").append("svg").attr("width",width).attr("height",height),g=svg.append("g").attr("transform","translate("+padding.left+", "+padding.top+")");drawQuadratic({start:[0,70],end:[200,100],control:[160,20]}),drawCubic({start:[0,70],end:[200,100],control1:[40,180],control2:[160,20]});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJxdWFkcmF0aWNQYXRoIiwicSIsInN0YXJ0IiwiY29udHJvbCIsImVuZCIsImludGVycG9sYXRlUXVhZHJhdGljQmV6aWVyIiwidCIsIk1hdGgiLCJwb3ciLCJpbnRlcnBvbGF0ZVF1YWRyYXRpY0JlemllckFuZ2xlIiwiY29uc3QiLCJ0YW5nZW50WCIsInRhbmdlbnRZIiwiYXRhbjIiLCJQSSIsImN1YmljUGF0aCIsImMiLCJjb250cm9sMSIsImNvbnRyb2wyIiwiaW50ZXJwb2xhdGVDdWJpY0JlemllciIsImludGVycG9sYXRlQ3ViaWNCZXppZXJBbmdsZSIsImRyYXdRdWFkcmF0aWMiLCJxdWFkcmF0aWMiLCJnUXVhZHJhdGljIiwiZyIsImFwcGVuZCIsImF0dHIiLCJ0ZXh0IiwicXVhZHJhdGljSW50ZXJwb2xhdG9yIiwiaW50ZXJwb2xhdGVkUG9pbnRzIiwiZDMiLCJyYW5nZSIsIm1hcCIsImQiLCJpIiwiYSIsImxlbmd0aCIsInNlbGVjdEFsbCIsImRhdGEiLCJlbnRlciIsInF1YWRyYXRpY0FuZ2xlSW50ZXJwb2xhdG9yIiwicm90YXRlZFBvaW50cyIsInBvc2l0aW9uIiwiYW5nbGUiLCJjb25zb2xlIiwibG9nIiwiSlNPTiIsInN0cmluZ2lmeSIsImRyYXdDdWJpYyIsImN1YmljIiwiZ0N1YmljIiwiY3ViaWNJbnRlcnBvbGF0b3IiLCJjdWJpY0FuZ2xlSW50ZXJwb2xhdG9yIiwid2lkdGgiLCJoZWlnaHQiLCJwYWRkaW5nIiwidG9wIiwicmlnaHQiLCJib3R0b20iLCJsZWZ0Iiwic3ZnIiwic2VsZWN0Il0sIm1hcHBpbmdzIjoiQUFjQSxRQUFTQSxlQUFjQyxHQUN0QixNQUFPLElBQUVBLEVBQUFDLE1BQUksR0FBQSxJQUFRRCxFQUFBQyxNQUFFLEdBQUEsS0FBSUQsRUFBQUUsUUFBUSxHQUFBLElBQUdGLEVBQUVFLFFBQUUsR0FBUSxJQUFFRixFQUFBRyxJQUFFLEdBQUEsSUFBSUgsRUFBQUcsSUFBTyxH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TyxRQUFRcEMsT0FBTyxPQUNsQ0MsS0FBSyxRQUFTMkIsT0FDZDNCLEtBQUssU0FBVTRCLFFBRVg5QixFQUFHb0MsSUFBSW5DLE9BQU8sS0FDbEJDLEtBQUssWUFBYSxhQUFXNkIsUUFBVSxLQUFBLEtBQUlBLFFBQUssSUFBQSxJQStObkRsQyxnQkFEb0JuQixPQUFTLEVBQUksSUFBR0UsS0FBTSxJQUFLLEtBQU1ELFNBQVUsSUFBTyxNQUl0RTZDLFdBRGdCOUMsT0FBUyxFQUFJLElBQUdFLEtBQU0sSUFBSyxLQUFNYSxVQUFhLEdBQUUsS0FBTUMsVUFBVyxJQUFPIiwiZmlsZSI6InNjcmlwdC5qcyIsInNvdXJjZXNDb250ZW50IjpbIlxuY29uc3Qgd2lkdGggPSA1MDA7XG5jb25zdCBoZWlnaHQgPSAzMDA7XG5jb25zdCBwYWRkaW5nID0geyB0b3A6IDEwLCByaWdodDogMTAsIGJvdHRvbTogMTAsIGxlZnQ6IDEwIH07XG5cblxuY29uc3Qgc3ZnID0gZDMuc2VsZWN0KCdib2R5JykuYXBwZW5kKCdzdmcnKVxuXHRcdC5hdHRyKCd3aWR0aCcsIHdpZHRoKVxuXHRcdC5hdHRyKCdoZWlnaHQnLCBoZWlnaHQpO1xuXG5jb25zdCBnID0gc3ZnLmFwcGVuZCgnZycpXG5cdFx0LmF0dHIoJ3RyYW5zZm9ybScsIGB0cmFuc2xhdGUoJHtwYWRkaW5nLmxlZnR9LCAke3BhZGRpbmcudG9wfSlgKVxuXG5cbmZ1bmN0aW9uIHF1YWRyYXRpY1BhdGgocSkge1xuXHRyZXR1cm4gYE0ke3Euc3RhcnRbMF19LCR7cS5zdGFydFsxXX0gUSR7cS5jb250cm9sWzBdfSwke3EuY29udHJvbFsxXX0gJHtxLmVuZFswXX0sJHtxLmVuZFsxXX1gO1xufVxuXG4vLyBCKHQpID0gKDEgLSB0KV4yUDAgKyAyKDEgLSB0KXRQMSArIHReMlAyXG5mdW5jdGlvbiBpbnRlcnBvbGF0ZVF1YWRyYXRpY0JlemllcihzdGFydCwgY29udHJvbCwgZW5kKSB7XG5cdC8vIDAgPD0gdCA8PSAxXG5cdHJldHVybiBmdW5jdGlvbiBpbnRlcnBvbGF0b3IodCkge1xuXHRcdHJldHVybiBbXG5cdFx0XHQoTWF0aC5wb3coMSAtIHQsIDIpICogc3RhcnRbMF0pICtcbiAgICAgICgyICogKDEgLSB0KSAqIHQgKiBjb250cm9sWzBdKSArXG4gICAgICAoTWF0aC5wb3codCwgMikgKiBlbmRbMF0pLFxuICAgICAgKE1hdGgucG93KDEgLSB0LCAyKSAqIHN0YXJ0WzFdKSArXG4gICAgICAoMiAqICgxIC0gdCkgKiB0ICogY29udHJvbFsxXSkgK1xuICAgICAgKE1hdGgucG93KHQsIDIpICogZW5kWzFdKSxcblx0XHRdO1xuXHR9O1xufVxuXG4vLyBCJyh0KSA9IDIoMSAtIHQpKFAxIC0gUDApICsgMnQoUDIgLSBQMSlcbmZ1bmN0aW9uIGludGVycG9sYXRlUXVhZHJhdGljQmV6aWVyQW5nbGUoc3RhcnQsIGNvbnRyb2wsIGVuZCkge1xuXHQvLyAwIDw9IHQgPD0gMVxuXHRyZXR1cm4gZnVuY3Rpb24gaW50ZXJwb2xhdG9yKHQpIHtcblx0XHRjb25zdCB0YW5nZW50WCA9ICgyICogKDEgLSB0KSAqIChjb250cm9sWzBdIC0gc3RhcnRbMF0pKSArXG5cdFx0XHRcdFx0XHRcdFx0XHRcdCAoMiAqIHQgKiAoZW5kWzBdIC0gY29udHJvbFswXSkpO1xuXHRcdGNvbnN0IHRhbmdlbnRZID0gKDIgKiAoMSAtIHQpICogKGNvbnRyb2xbMV0gLSBzdGFydFsxXSkpICtcblx0XHRcdFx0XHRcdFx0XHRcdFx0ICgyICogdCAqIChlbmRbMV0gLSBjb250cm9sWzFdKSk7XG5cblx0XHRyZXR1cm4gTWF0aC5hdGFuMih0YW5nZW50WSwgdGFuZ2VudFgpICogKDE4MCAvIE1hdGguUEkpO1xuXHR9XG59XG5cbmZ1bmN0aW9uIGN1YmljUGF0aChjKSB7XG5cdHJldHVybiBgTSR7Yy5zdGFydFswXX0sJHtjLnN0YXJ0WzFdfSBDJHtjLmNvbnRyb2wxWzBdfSwke2MuY29udHJvbDFbMV19ICR7Yy5jb250cm9sMlswXX0sJHtjLmNvbnRyb2wyWzFdfSAke2MuZW5kWzBdfSwke2MuZW5kWzFdfWA7XG59XG5cbi8vIEIodCkgPSAoMSAtIHQpXjNQMCArIDMoMSAtIHQpXjJ0UDEgKyAzKDEgLSB0KXReMlAyICsgdF4zUDNcbmZ1bmN0aW9uIGludGVycG9sYXRlQ3ViaWNCZXppZXIoc3RhcnQsIGNvbnRyb2wxLCBjb250cm9sMiwgZW5kKSB7XG5cdC8vIDAgPD0gdCA8PSAxXG5cdHJldHVybiBmdW5jdGlvbiBpbnRlcnBvbGF0b3IodCkge1xuXHRcdHJldHVybiBbXG4gICAgICAoTWF0aC5wb3coMSAtIHQsIDMpICogc3RhcnRbMF0pICtcbiAgICAgICgzICogTWF0aC5wb3coMSAtIHQsIDIpICogdCAqIGNvbnRyb2wxWzBdKSArXG4gICAgICAoMyAqICgxIC0gdCkgKiBNYXRoLnBvdyh0LCAyKSAqIGNvbnRyb2wyWzBdKSArXG5cdFx0XHQoTWF0aC5wb3codCwgMykgKiBlbmRbMF0pLFxuXHRcdFx0KE1hdGgucG93KDEgLSB0LCAzKSAqIHN0YXJ0WzFdKSArXG4gICAgICAoMyAqIE1hdGgucG93KDEgLSB0LCAyKSAqIHQgKiBjb250cm9sMVsxXSkgK1xuICAgICAgKDMgKiAoMSAtIHQpICogTWF0aC5wb3codCwgMikgKiBjb250cm9sMlsxXSkgK1xuXHRcdFx0KE1hdGgucG93KHQsIDMpICogZW5kWzFdKSxcblx0XHRdO1xuXHR9O1xufVxuXG4vLyBCJyh0KSA9IDMoMS0gdCleMihQMSAtIFAwKSArIDYoMSAtIHQpdChQMiAtIFAxKSArIDN0XjIoUDMgLSBQMilcbmZ1bmN0aW9uIGludGVycG9sYXRlQ3ViaWNCZXppZXJBbmdsZShzdGFydCwgY29udHJvbDEsIGNvbnRyb2wyLCBlbmQpIHtcblx0Ly8gMCA8PSB0IDw9IDFcblx0cmV0dXJuIGZ1bmN0aW9uIGludGVycG9sYXRvcih0KSB7XG5cdFx0Y29uc3QgdGFuZ2VudFggPSAgKDMgKiBNYXRoLnBvdygxIC0gdCwgMikgKiAoY29udHJvbDFbMF0gLSBzdGFydFswXSkpICtcblx0XHRcdFx0XHRcdFx0XHRcdFx0XHQoNiAqICgxIC0gdCkgKiB0ICogKGNvbnRyb2wyWzBdIC0gY29udHJvbDFbMF0pKSArXG5cdFx0XHRcdFx0XHRcdFx0XHRcdFx0KDMgKiBNYXRoLnBvdyh0LCAyKSAqIChlbmRbMF0gLSBjb250cm9sMlswXSkpO1xuXHRcdGNvbnN0IHRhbmdlbnRZID0gICgzICogTWF0aC5wb3coMSAtIHQsIDIpICogKGNvbnRyb2wxWzFdIC0gc3RhcnRbMV0pKSArXG5cdFx0XHRcdFx0XHRcdFx0XHRcdFx0KDYgKiAoMSAtIHQpICogdCAqIChjb250cm9sMlsxXSAtIGNvbnRyb2wxWzFdKSkgK1xuXHRcdFx0XHRcdFx0XHRcdFx0XHRcdCgzICogTWF0aC5wb3codCwgMikgKiAoZW5kWzFdIC0gY29udHJvbDJbMV0pKTtcblxuXHRcdHJldHVybiBNYXRoLmF0YW4yKHRhbmdlbnRZLCB0YW5nZW50WCkgKiAoMTgwIC8gTWF0aC5QSSk7XG5cdH1cbn1cblxuXG4vLyBkcmF3IGEgcXVhZHJhdGljIGJlemllciBjdXJ2ZVxuZnVuY3Rpb24gZHJhd1F1YWRyYXRpYyhxdWFkcmF0aWMpIHtcblx0Y29uc3QgZ1F1YWRyYXRpYyA9IGcuYXBwZW5kKCdnJylcblx0XHQuYXR0cignY2xhc3MnLCAncXVhZHJhdGljJyk7XG5cblx0Z1F1YWRyYXRpYy5hcHBlbmQoJ3RleHQnKVxuXHRcdC50ZXh0KCdRdWFkcmF0aWMgQmV6aWVyJylcblx0XHQuYXR0cignZHknLCAnMC40ZW0nKVxuXG5cdC8vIGRyYXcgdGhlIHBvaW50c1xuXHRnUXVhZHJhdGljLmFwcGVuZCgnY2lyY2xlJylcblx0XHQuYXR0cigncicsIDUpXG5cdFx0LmF0dHIoJ2NsYXNzJywgJ3N0YXJ0LXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBxdWFkcmF0aWMuc3RhcnRbMF0pXG5cdFx0LmF0dHIoJ2N5JywgcXVhZHJhdGljLnN0YXJ0WzFdKVxuXG5cdGdRdWFkcmF0aWMuYXBwZW5kKCdjaXJjbGUnKVxuXHRcdC5hdHRyKCdyJywgNSlcblx0XHQuYXR0cignY2xhc3MnLCAnZW5kLXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBxdWFkcmF0aWMuZW5kWzBdKVxuXHRcdC5hdHRyKCdjeScsIHF1YWRyYXRpYy5lbmRbMV0pXG5cblx0Z1F1YWRyYXRpYy5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0LmF0dHIoJ3InLCAzKVxuXHRcdC5hdHRyKCdjbGFzcycsICdjb250cm9sLXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBxdWFkcmF0aWMuY29udHJvbFswXSlcblx0XHQuYXR0cignY3knLCBxdWFkcmF0aWMuY29udHJvbFsxXSlcblxuXHQvLyBkcmF3IHRoZSBwYXRoXG5cdGdRdWFkcmF0aWMuYXBwZW5kKCdwYXRoJylcblx0XHQuYXR0cignY2xhc3MnLCAnY3VydmUnKVxuXHRcdC5hdHRyKCdkJywgcXVhZHJhdGljUGF0aChxdWFkcmF0aWMpKTtcblxuXHRjb25zdCBxdWFkcmF0aWNJbnRlcnBvbGF0b3IgPSBpbnRlcnBvbGF0ZVF1YWRyYXRpY0JlemllcihxdWFkcmF0aWMuc3RhcnQsIHF1YWRyYXRpYy5jb250cm9sLCBxdWFkcmF0aWMuZW5kKTtcblx0Y29uc3QgaW50ZXJwb2xhdGVkUG9pbnRzID0gZDMucmFuZ2UoMTApLm1hcCgoZCwgaSwgYSkgPT4gcXVhZHJhdGljSW50ZXJwb2xhdG9yKGQgLyAoYS5sZW5ndGggLSAxKSkpO1xuXG5cdGdRdWFkcmF0aWMuc2VsZWN0QWxsKCcuaW50ZXJwb2xhdGVkLXBvaW50JykuZGF0YShpbnRlcnBvbGF0ZWRQb2ludHMpXG5cdFx0LmVudGVyKClcblx0XHRcdC5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0XHRcdC5hdHRyKCdjbGFzcycsICdpbnRlcnBvbGF0ZWQtcG9pbnQnKVxuXHRcdFx0XHQuYXR0cigncicsIDMpXG5cdFx0XHRcdC5hdHRyKCdjeCcsIGQgPT4gZFswXSlcblx0XHRcdFx0LmF0dHIoJ2N5JywgZCA9PiBkWzFdKTtcblxuXG5cdGNvbnN0IHF1YWRyYXRpY0FuZ2xlSW50ZXJwb2xhdG9yID0gaW50ZXJwb2xhdGVRdWFkcmF0aWNCZXppZXJBbmdsZShxdWFkcmF0aWMuc3RhcnQsIHF1YWRyYXRpYy5jb250cm9sLCBxdWFkcmF0aWMuZW5kKTtcblxuXHRjb25zdCByb3RhdGVkUG9pbnRzID0gZDMucmFuZ2UoMykubWFwKChkLCBpLCBhKSA9PiB7XG5cdFx0Y29uc3QgdCA9IGQgLyAoYS5sZW5ndGggLSAxKTtcblx0XHRyZXR1cm4ge1xuXHRcdFx0dDogdCxcblx0XHRcdHBvc2l0aW9uOiBxdWFkcmF0aWNJbnRlcnBvbGF0b3IodCksXG5cdFx0XHRhbmdsZTogcXVhZHJhdGljQW5nbGVJbnRlcnBvbGF0b3IodCksXG5cdFx0fTtcblx0fSk7XG5cblx0Z1F1YWRyYXRpYy5zZWxlY3RBbGwoJy5yb3RhdGVkLXBvaW50JykuZGF0YShyb3RhdGVkUG9pbnRzKVxuXHRcdC5lbnRlcigpXG5cdFx0XHQuYXBwZW5kKCdwYXRoJylcblx0XHRcdFx0LmF0dHIoJ2QnLCAnTTEyLDAgTC01LC04IEwwLDAgTC01LDggWicpXG5cdFx0XHRcdC5hdHRyKCdjbGFzcycsICdyb3RhdGVkLXBvaW50Jylcblx0XHRcdFx0LmF0dHIoJ3RyYW5zZm9ybScsIGQgPT4gYHRyYW5zbGF0ZSgke2QucG9zaXRpb25bMF19LCAke2QucG9zaXRpb25bMV19KSByb3RhdGUoJHtkLmFuZ2xlfSlgKVxuXG5cblx0Y29uc29sZS5sb2coJ1FVQURSQVRJQycpO1xuXHRjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShxdWFkcmF0aWMpKTtcblx0Y29uc29sZS5sb2coSlNPTi5zdHJpbmdpZnkocm90YXRlZFBvaW50cywgbnVsbCwgMikpXG5cblx0cmV0dXJuIGdRdWFkcmF0aWM7XG59XG5cblxuZnVuY3Rpb24gZHJhd0N1YmljKGN1YmljKSB7XG5cdGNvbnN0IGdDdWJpYyA9IGcuYXBwZW5kKCdnJylcblx0XHQuYXR0cignY2xhc3MnLCAnY3ViaWMnKVxuXHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCAndHJhbnNsYXRlKDI1MCwgMCknKTtcblxuXHRnQ3ViaWMuYXBwZW5kKCd0ZXh0Jylcblx0XHQudGV4dCgnQ3ViaWMgQmV6aWVyJylcblx0XHQuYXR0cignZHknLCAnMC40ZW0nKVxuXG5cdC8vIGRyYXcgdGhlIHBvaW50c1xuXHRnQ3ViaWMuYXBwZW5kKCdjaXJjbGUnKVxuXHRcdC5hdHRyKCdyJywgNSlcblx0XHQuYXR0cignY2xhc3MnLCAnc3RhcnQtcG9pbnQnKVxuXHRcdC5hdHRyKCdjeCcsIGN1YmljLnN0YXJ0WzBdKVxuXHRcdC5hdHRyKCdjeScsIGN1YmljLnN0YXJ0WzFdKVxuXG5cdGdDdWJpYy5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0LmF0dHIoJ3InLCA1KVxuXHRcdC5hdHRyKCdjbGFzcycsICdlbmQtcG9pbnQnKVxuXHRcdC5hdHRyKCdjeCcsIGN1YmljLmVuZFswXSlcblx0XHQuYXR0cignY3knLCBjdWJpYy5lbmRbMV0pXG5cblx0Z0N1YmljLmFwcGVuZCgnY2lyY2xlJylcblx0XHQuYXR0cigncicsIDMpXG5cdFx0LmF0dHIoJ2NsYXNzJywgJ2NvbnRyb2wtcG9pbnQnKVxuXHRcdC5hdHRyKCdjeCcsIGN1YmljLmNvbnRyb2wxWzBdKVxuXHRcdC5hdHRyKCdjeScsIGN1YmljLmNvbnRyb2wxWzFdKVxuXG5cdGdDdWJpYy5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0LmF0dHIoJ3InLCAzKVxuXHRcdC5hdHRyKCdjbGFzcycsICdjb250cm9sLXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBjdWJpYy5jb250cm9sMlswXSlcblx0XHQuYXR0cignY3knLCBjdWJpYy5jb250cm9sMlsxXSlcblxuXHQvLyBkcmF3IHRoZSBwYXRoXG5cdGdDdWJpYy5hcHBlbmQoJ3BhdGgnKVxuXHRcdC5hdHRyKCdjbGFzcycsICdjdXJ2ZScpXG5cdFx0LmF0dHIoJ2QnLCBjdWJpY1BhdGgoY3ViaWMpKTtcblxuXHRjb25zdCBjdWJpY0ludGVycG9sYXRvciA9IGludGVycG9sYXRlQ3ViaWNCZXppZXIoY3ViaWMuc3RhcnQsIGN1YmljLmNvbnRyb2wxLCBjdWJpYy5jb250cm9sMiwgY3ViaWMuZW5kKTtcblx0Y29uc3QgaW50ZXJwb2xhdGVkUG9pbnRzID0gZDMucmFuZ2UoMTApLm1hcCgoZCwgaSwgYSkgPT4gY3ViaWNJbnRlcnBvbGF0b3IoZCAvIChhLmxlbmd0aCAtIDEpKSk7XG5cblx0Z0N1YmljLnNlbGVjdEFsbCgnLmludGVycG9sYXRlZC1wb2ludCcpLmRhdGEoaW50ZXJwb2xhdGVkUG9pbnRzKVxuXHRcdC5lbnRlcigpXG5cdFx0XHQuYXBwZW5kKCdjaXJjbGUnKVxuXHRcdFx0XHQuYXR0cignY2xhc3MnLCAnaW50ZXJwb2xhdGVkLXBvaW50Jylcblx0XHRcdFx0LmF0dHIoJ3InLCAzKVxuXHRcdFx0XHQuYXR0cignY3gnLCBkID0+IGRbMF0pXG5cdFx0XHRcdC5hdHRyKCdjeScsIGQgPT4gZFsxXSk7XG5cblxuXHRjb25zdCBjdWJpY0FuZ2xlSW50ZXJwb2xhdG9yID0gaW50ZXJwb2xhdGVDdWJpY0JlemllckFuZ2xlKGN1YmljLnN0YXJ0LCBjdWJpYy5jb250cm9sMSwgY3ViaWMuY29udHJvbDIsIGN1YmljLmVuZCk7XG5cblx0Y29uc3Qgcm90YXRlZFBvaW50cyA9IGQzLnJhbmdlKDMpLm1hcCgoZCwgaSwgYSkgPT4ge1xuXHRcdGNvbnN0IHQgPSBkIC8gKGEubGVuZ3RoIC0gMSk7XG5cdFx0cmV0dXJuIHtcblx0XHRcdHQ6IHQsXG5cdFx0XHRwb3NpdGlvbjogY3ViaWNJbnRlcnBvbGF0b3IodCksXG5cdFx0XHRhbmdsZTogY3ViaWNBbmdsZUludGVycG9sYXRvcih0KSxcblx0XHR9O1xuXHR9KTtcblxuXHRnQ3ViaWMuc2VsZWN0QWxsKCcucm90YXRlZC1wb2ludCcpLmRhdGEocm90YXRlZFBvaW50cylcblx0XHQuZW50ZXIoKVxuXHRcdFx0LmFwcGVuZCgncGF0aCcpXG5cdFx0XHRcdC5hdHRyKCdkJywgJ00xMiwwIEwtNSwtOCBMMCwwIEwtNSw4IFonKVxuXHRcdFx0XHQuYXR0cignY2xhc3MnLCAncm90YXRlZC1wb2ludCcpXG5cdFx0XHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCBkID0+IGB0cmFuc2xhdGUoJHtkLnBvc2l0aW9uWzBdfSwgJHtkLnBvc2l0aW9uWzFdfSkgcm90YXRlKCR7ZC5hbmdsZX0pYClcblxuXG5cdGNvbnNvbGUubG9nKCdDVUJJQycpO1xuXHRjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShjdWJpYykpO1xuXHRjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShyb3RhdGVkUG9pbnRzLCBudWxsLCAyKSlcblxuXHRyZXR1cm4gZ0N1YmljO1xufVxuXG5cbmNvbnN0IHF1YWRyYXRpYyA9IHsgc3RhcnQ6IFswLCA3MF0sIGVuZDogWzIwMCwgMTAwXSwgY29udHJvbDogWzE2MCwgMjBdIH07XG5kcmF3UXVhZHJhdGljKHF1YWRyYXRpYyk7XG5cbmNvbnN0IGN1YmljID0geyBzdGFydDogWzAsIDcwXSwgZW5kOiBbMjAwLCAxMDBdLCBjb250cm9sMTogWzQwLCAxODBdLCBjb250cm9sMjogWzE2MCwgMjBdIH07XG5kcmF3Q3ViaWMoY3ViaWMpO1xuIl19
<!DOCTYPE html>
<title>Compute points and angles along a bezier curve</title>
<link href='style.css' rel='stylesheet' />
<body>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src='dist.js'></script>
</body>
const width = 500;
const height = 300;
const padding = { top: 10, right: 10, bottom: 10, left: 10 };
const svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
function quadraticPath(q) {
return `M${q.start[0]},${q.start[1]} Q${q.control[0]},${q.control[1]} ${q.end[0]},${q.end[1]}`;
}
// B(t) = (1 - t)^2P0 + 2(1 - t)tP1 + t^2P2
function interpolateQuadraticBezier(start, control, end) {
// 0 <= t <= 1
return function interpolator(t) {
return [
(Math.pow(1 - t, 2) * start[0]) +
(2 * (1 - t) * t * control[0]) +
(Math.pow(t, 2) * end[0]),
(Math.pow(1 - t, 2) * start[1]) +
(2 * (1 - t) * t * control[1]) +
(Math.pow(t, 2) * end[1]),
];
};
}
// B'(t) = 2(1 - t)(P1 - P0) + 2t(P2 - P1)
function interpolateQuadraticBezierAngle(start, control, end) {
// 0 <= t <= 1
return function interpolator(t) {
const tangentX = (2 * (1 - t) * (control[0] - start[0])) +
(2 * t * (end[0] - control[0]));
const tangentY = (2 * (1 - t) * (control[1] - start[1])) +
(2 * t * (end[1] - control[1]));
return Math.atan2(tangentY, tangentX) * (180 / Math.PI);
}
}
function cubicPath(c) {
return `M${c.start[0]},${c.start[1]} C${c.control1[0]},${c.control1[1]} ${c.control2[0]},${c.control2[1]} ${c.end[0]},${c.end[1]}`;
}
// B(t) = (1 - t)^3P0 + 3(1 - t)^2tP1 + 3(1 - t)t^2P2 + t^3P3
function interpolateCubicBezier(start, control1, control2, end) {
// 0 <= t <= 1
return function interpolator(t) {
return [
(Math.pow(1 - t, 3) * start[0]) +
(3 * Math.pow(1 - t, 2) * t * control1[0]) +
(3 * (1 - t) * Math.pow(t, 2) * control2[0]) +
(Math.pow(t, 3) * end[0]),
(Math.pow(1 - t, 3) * start[1]) +
(3 * Math.pow(1 - t, 2) * t * control1[1]) +
(3 * (1 - t) * Math.pow(t, 2) * control2[1]) +
(Math.pow(t, 3) * end[1]),
];
};
}
// B'(t) = 3(1- t)^2(P1 - P0) + 6(1 - t)t(P2 - P1) + 3t^2(P3 - P2)
function interpolateCubicBezierAngle(start, control1, control2, end) {
// 0 <= t <= 1
return function interpolator(t) {
const tangentX = (3 * Math.pow(1 - t, 2) * (control1[0] - start[0])) +
(6 * (1 - t) * t * (control2[0] - control1[0])) +
(3 * Math.pow(t, 2) * (end[0] - control2[0]));
const tangentY = (3 * Math.pow(1 - t, 2) * (control1[1] - start[1])) +
(6 * (1 - t) * t * (control2[1] - control1[1])) +
(3 * Math.pow(t, 2) * (end[1] - control2[1]));
return Math.atan2(tangentY, tangentX) * (180 / Math.PI);
}
}
// draw a quadratic bezier curve
function drawQuadratic(quadratic) {
const gQuadratic = g.append('g')
.attr('class', 'quadratic');
gQuadratic.append('text')
.text('Quadratic Bezier')
.attr('dy', '0.4em')
// draw the points
gQuadratic.append('circle')
.attr('r', 5)
.attr('class', 'start-point')
.attr('cx', quadratic.start[0])
.attr('cy', quadratic.start[1])
gQuadratic.append('circle')
.attr('r', 5)
.attr('class', 'end-point')
.attr('cx', quadratic.end[0])
.attr('cy', quadratic.end[1])
gQuadratic.append('circle')
.attr('r', 3)
.attr('class', 'control-point')
.attr('cx', quadratic.control[0])
.attr('cy', quadratic.control[1])
// draw the path
gQuadratic.append('path')
.attr('class', 'curve')
.attr('d', quadraticPath(quadratic));
const quadraticInterpolator = interpolateQuadraticBezier(quadratic.start, quadratic.control, quadratic.end);
const interpolatedPoints = d3.range(10).map((d, i, a) => quadraticInterpolator(d / (a.length - 1)));
gQuadratic.selectAll('.interpolated-point').data(interpolatedPoints)
.enter()
.append('circle')
.attr('class', 'interpolated-point')
.attr('r', 3)
.attr('cx', d => d[0])
.attr('cy', d => d[1]);
const quadraticAngleInterpolator = interpolateQuadraticBezierAngle(quadratic.start, quadratic.control, quadratic.end);
const rotatedPoints = d3.range(3).map((d, i, a) => {
const t = d / (a.length - 1);
return {
t: t,
position: quadraticInterpolator(t),
angle: quadraticAngleInterpolator(t),
};
});
gQuadratic.selectAll('.rotated-point').data(rotatedPoints)
.enter()
.append('path')
.attr('d', 'M12,0 L-5,-8 L0,0 L-5,8 Z')
.attr('class', 'rotated-point')
.attr('transform', d => `translate(${d.position[0]}, ${d.position[1]}) rotate(${d.angle})`)
console.log('QUADRATIC');
console.log(JSON.stringify(quadratic));
console.log(JSON.stringify(rotatedPoints, null, 2))
return gQuadratic;
}
function drawCubic(cubic) {
const gCubic = g.append('g')
.attr('class', 'cubic')
.attr('transform', 'translate(250, 0)');
gCubic.append('text')
.text('Cubic Bezier')
.attr('dy', '0.4em')
// draw the points
gCubic.append('circle')
.attr('r', 5)
.attr('class', 'start-point')
.attr('cx', cubic.start[0])
.attr('cy', cubic.start[1])
gCubic.append('circle')
.attr('r', 5)
.attr('class', 'end-point')
.attr('cx', cubic.end[0])
.attr('cy', cubic.end[1])
gCubic.append('circle')
.attr('r', 3)
.attr('class', 'control-point')
.attr('cx', cubic.control1[0])
.attr('cy', cubic.control1[1])
gCubic.append('circle')
.attr('r', 3)
.attr('class', 'control-point')
.attr('cx', cubic.control2[0])
.attr('cy', cubic.control2[1])
// draw the path
gCubic.append('path')
.attr('class', 'curve')
.attr('d', cubicPath(cubic));
const cubicInterpolator = interpolateCubicBezier(cubic.start, cubic.control1, cubic.control2, cubic.end);
const interpolatedPoints = d3.range(10).map((d, i, a) => cubicInterpolator(d / (a.length - 1)));
gCubic.selectAll('.interpolated-point').data(interpolatedPoints)
.enter()
.append('circle')
.attr('class', 'interpolated-point')
.attr('r', 3)
.attr('cx', d => d[0])
.attr('cy', d => d[1]);
const cubicAngleInterpolator = interpolateCubicBezierAngle(cubic.start, cubic.control1, cubic.control2, cubic.end);
const rotatedPoints = d3.range(3).map((d, i, a) => {
const t = d / (a.length - 1);
return {
t: t,
position: cubicInterpolator(t),
angle: cubicAngleInterpolator(t),
};
});
gCubic.selectAll('.rotated-point').data(rotatedPoints)
.enter()
.append('path')
.attr('d', 'M12,0 L-5,-8 L0,0 L-5,8 Z')
.attr('class', 'rotated-point')
.attr('transform', d => `translate(${d.position[0]}, ${d.position[1]}) rotate(${d.angle})`)
console.log('CUBIC');
console.log(JSON.stringify(cubic));
console.log(JSON.stringify(rotatedPoints, null, 2))
return gCubic;
}
const quadratic = { start: [0, 70], end: [200, 100], control: [160, 20] };
drawQuadratic(quadratic);
const cubic = { start: [0, 70], end: [200, 100], control1: [40, 180], control2: [160, 20] };
drawCubic(cubic);
.start-point {
fill: #fff;
stroke: #0bb;
}
.end-point {
fill: #fff;
stroke: #0bb;
}
.control-point {
fill: tomato;
}
.curve {
fill: none;
stroke: #0bb;
stroke-width: 2px;
}
.interpolated-point {
fill: #077;
}
.rotated-point {
fill: #af84e6;
stroke: #660cd0;
}
@fedulovivan
Copy link

Thank you very much for the good demo. You solution helped to implement proper rotation (orientation) of arrowheads with reactflow.dev library.

@pbeshai
Copy link
Author

pbeshai commented May 25, 2022

Awesome, thanks for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment