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.
Last active
May 25, 2022 17:34
-
-
Save pbeshai/72c446033a98f99ce1e1371c6eee9644 to your computer and use it in GitHub Desktop.
Compute points and angles along a bezier curve
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit | |
height: 320 | |
border: no |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJxdWFkcmF0aWNQYXRoIiwicSIsInN0YXJ0IiwiY29udHJvbCIsImVuZCIsImludGVycG9sYXRlUXVhZHJhdGljQmV6aWVyIiwidCIsIk1hdGgiLCJwb3ciLCJpbnRlcnBvbGF0ZVF1YWRyYXRpY0JlemllckFuZ2xlIiwiY29uc3QiLCJ0YW5nZW50WCIsInRhbmdlbnRZIiwiYXRhbjIiLCJQSSIsImN1YmljUGF0aCIsImMiLCJjb250cm9sMSIsImNvbnRyb2wyIiwiaW50ZXJwb2xhdGVDdWJpY0JlemllciIsImludGVycG9sYXRlQ3ViaWNCZXppZXJBbmdsZSIsImRyYXdRdWFkcmF0aWMiLCJxdWFkcmF0aWMiLCJnUXVhZHJhdGljIiwiZyIsImFwcGVuZCIsImF0dHIiLCJ0ZXh0IiwicXVhZHJhdGljSW50ZXJwb2xhdG9yIiwiaW50ZXJwb2xhdGVkUG9pbnRzIiwiZDMiLCJyYW5nZSIsIm1hcCIsImQiLCJpIiwiYSIsImxlbmd0aCIsInNlbGVjdEFsbCIsImRhdGEiLCJlbnRlciIsInF1YWRyYXRpY0FuZ2xlSW50ZXJwb2xhdG9yIiwicm90YXRlZFBvaW50cyIsInBvc2l0aW9uIiwiYW5nbGUiLCJjb25zb2xlIiwibG9nIiwiSlNPTiIsInN0cmluZ2lmeSIsImRyYXdDdWJpYyIsImN1YmljIiwiZ0N1YmljIiwiY3ViaWNJbnRlcnBvbGF0b3IiLCJjdWJpY0FuZ2xlSW50ZXJwb2xhdG9yIiwid2lkdGgiLCJoZWlnaHQiLCJwYWRkaW5nIiwidG9wIiwicmlnaHQiLCJib3R0b20iLCJsZWZ0Iiwic3ZnIiwic2VsZWN0Il0sIm1hcHBpbmdzIjoiQUFjQSxRQUFTQSxlQUFjQyxHQUN0QixNQUFPLElBQUVBLEVBQUFDLE1BQUksR0FBQSxJQUFRRCxFQUFBQyxNQUFFLEdBQUEsS0FBSUQsRUFBQUUsUUFBUSxHQUFBLElBQUdGLEVBQUVFLFFBQUUsR0FBUSxJQUFFRixFQUFBRyxJQUFFLEdBQUEsSUFBSUgsRUFBQUcsSUFBTyxHQUlsRSxRQUFTQyw0QkFBMkJILEVBQU9DLEVBQVNDLEdBRW5ELE1BQU8sVUFBc0JFLEdBQzVCLE9BQ0VDLEtBQUtDLElBQUksRUFBSUYsRUFBRyxHQUFLSixFQUFNLEdBQ3hCLEdBQUssRUFBSUksR0FBS0EsRUFBSUgsRUFBUSxHQUMxQkksS0FBS0MsSUFBSUYsRUFBRyxHQUFLRixFQUFJLEdBQ3JCRyxLQUFLQyxJQUFJLEVBQUlGLEVBQUcsR0FBS0osRUFBTSxHQUMzQixHQUFLLEVBQUlJLEdBQUtBLEVBQUlILEVBQVEsR0FDMUJJLEtBQUtDLElBQUlGLEVBQUcsR0FBS0YsRUFBSSxLQU01QixRQUFTSyxpQ0FBZ0NQLEVBQU9DLEVBQVNDLEdBRXhELE1BQU8sVUFBc0JFLEdBQzVCSSxHQUFNQyxHQUFhLEdBQUssRUFBS0wsSUFBSUgsRUFBVSxHQUFHRCxFQUFRLElBQzVDLEVBQUlJLEdBQUtGLEVBQUksR0FBS0QsRUFBUSxJQUM5QlMsRUFBYSxHQUFLLEVBQUtOLElBQUlILEVBQVUsR0FBR0QsRUFBUSxJQUM1QyxFQUFJSSxHQUFLRixFQUFJLEdBQUtELEVBQVEsR0FFcEMsT0FBT0ksTUFBS00sTUFBTUQsRUFBVUQsSUFBYSxJQUFNSixLQUFLTyxLQUl0RCxRQUFTQyxXQUFVQyxHQUNsQixNQUFPLElBQUVBLEVBQUFkLE1BQUksR0FBQSxJQUFRYyxFQUFBZCxNQUFFLEdBQUEsS0FBSWMsRUFBQUMsU0FBUSxHQUFBLElBQUdELEVBQUdDLFNBQVMsR0FBRSxJQUFDRCxFQUFBRSxTQUFNLEdBQUEsSUFBQUYsRUFBU0UsU0FBSSxHQUFBLElBQUlGLEVBQUFaLElBQUEsR0FBUSxJQUFHWSxFQUFBWixJQUFFLEdBSTFGLFFBQVNlLHdCQUF1QmpCLEVBQU9lLEVBQVVDLEVBQVVkLEdBRTFELE1BQU8sVUFBc0JFLEdBQzVCLE9BQ0tDLEtBQUtDLElBQUksRUFBSUYsRUFBRyxHQUFLSixFQUFNLEdBQzNCLEVBQUlLLEtBQUtDLElBQUksRUFBSUYsRUFBRyxHQUFLQSxFQUFJVyxFQUFTLEdBQ3RDLEdBQUssRUFBSVgsR0FBS0MsS0FBS0MsSUFBSUYsRUFBRyxHQUFLWSxFQUFTLEdBQzNDWCxLQUFLQyxJQUFJRixFQUFHLEdBQUtGLEVBQUksR0FDckJHLEtBQUtDLElBQUksRUFBSUYsRUFBRyxHQUFLSixFQUFNLEdBQ3hCLEVBQUlLLEtBQUtDLElBQUksRUFBSUYsRUFBRyxHQUFLQSxFQUFJVyxFQUFTLEdBQ3RDLEdBQUssRUFBSVgsR0FBS0MsS0FBS0MsSUFBSUYsRUFBRyxHQUFLWSxFQUFTLEdBQzNDWCxLQUFLQyxJQUFJRixFQUFHLEdBQUtGLEVBQUksS0FNekIsUUFBU2dCLDZCQUE0QmxCLEVBQU9lLEVBQVVDLEVBQVVkLEdBRS9ELE1BQU8sVUFBc0JFLEdBQzVCSSxHQUFNQyxHQUFjLEVBQUdKLEtBQUtDLElBQUssRUFBSUYsRUFBSSxJQUFJVyxFQUFXLEdBQUdmLEVBQVEsSUFDekQsR0FBSyxFQUFJSSxHQUFLQSxHQUFLWSxFQUFTLEdBQUtELEVBQVMsSUFDMUMsRUFBSVYsS0FBS0MsSUFBSUYsRUFBRyxJQUFNRixFQUFJLEdBQUtjLEVBQVMsSUFDNUNOLEVBQWMsRUFBR0wsS0FBS0MsSUFBSyxFQUFJRixFQUFJLElBQUlXLEVBQVcsR0FBR2YsRUFBUSxJQUN6RCxHQUFLLEVBQUlJLEdBQUtBLEdBQUtZLEVBQVMsR0FBS0QsRUFBUyxJQUMxQyxFQUFJVixLQUFLQyxJQUFJRixFQUFHLElBQU1GLEVBQUksR0FBS2MsRUFBUyxHQUVsRCxPQUFPWCxNQUFLTSxNQUFNRCxFQUFVRCxJQUFhLElBQU1KLEtBQUtPLEtBTXRELFFBQVNPLGVBQWNDLEdBQ3RCWixHQUFNYSxHQUFlQyxFQUFBQyxPQUFPLEtBQzFCQyxLQUFLLFFBQVMsWUFFaEJILEdBQVdFLE9BQU8sUUFDaEJFLEtBQUssb0JBQ0xELEtBQUssS0FBTSxTQUdiSCxFQUFXRSxPQUFPLFVBQ2hCQyxLQUFLLElBQUssR0FDVkEsS0FBSyxRQUFTLGVBQ2RBLEtBQUssS0FBTUosRUFBVXBCLE1BQU0sSUFDM0J3QixLQUFLLEtBQU1KLEVBQVVwQixNQUFNLElBRTdCcUIsRUFBV0UsT0FBTyxVQUNoQkMsS0FBSyxJQUFLLEdBQ1ZBLEtBQUssUUFBUyxhQUNkQSxLQUFLLEtBQU1KLEVBQVVsQixJQUFJLElBQ3pCc0IsS0FBSyxLQUFNSixFQUFVbEIsSUFBSSxJQUUzQm1CLEVBQVdFLE9BQU8sVUFDaEJDLEtBQUssSUFBSyxHQUNWQSxLQUFLLFFBQVMsaUJBQ2RBLEtBQUssS0FBTUosRUFBVW5CLFFBQVEsSUFDN0J1QixLQUFLLEtBQU1KLEVBQVVuQixRQUFRLElBRy9Cb0IsRUFBV0UsT0FBTyxRQUNoQkMsS0FBSyxRQUFTLFNBQ2RBLEtBQUssSUFBSzFCLGNBQWNzQixHQUUxQlosSUFBTWtCLEdBQXdCdkIsMkJBQTJCaUIsRUFBVXBCLE1BQU9vQixFQUFVbkIsUUFBU21CLEVBQVVsQixLQUNqR3lCLEVBQXVCQyxHQUFDQyxNQUFRLElBQUVDLElBQUksU0FBQUMsRUFBQUMsRUFBQUMsR0FBRSxNQUFHUCxHQUFRSyxHQUFBRSxFQUFBQyxPQUF1QixLQUVoRmIsR0FBV2MsVUFBVSx1QkFBdUJDLEtBQUtULEdBQy9DVSxRQUNDZCxPQUFPLFVBQ05DLEtBQUssUUFBUyxzQkFDZEEsS0FBSyxJQUFLLEdBQ1ZBLEtBQUssS0FBTSxTQUFBTyxHQUFBLE1BQUFBLEdBQUEsS0FDWFAsS0FBSyxLQUFNLFNBQUFPLEdBQUEsTUFBQUEsR0FBQSxJQUdmdkIsSUFBTThCLEdBQTZCL0IsZ0NBQWdDYSxFQUFVcEIsTUFBT29CLEVBQVVuQixRQUFTbUIsRUFBVWxCLEtBRTNHcUMsRUFBa0JYLEdBQUNDLE1BQVEsR0FBQ0MsSUFBSSxTQUFBQyxFQUFBQyxFQUFBQyxHQUNyQ3pCLEdBQU9KLEdBQUkyQixHQUFNRSxFQUFBQyxPQUFXLEVBQzVCLFFBQ0M5QixFQUFHQSxFQUNIb0MsU0FBVWQsRUFBc0J0QixHQUNoQ3FDLE1BQU9ILEVBQTJCbEMsS0FnQnBDLE9BWkFpQixHQUFXYyxVQUFVLGtCQUFrQkMsS0FBS0csR0FDMUNGLFFBQ0NkLE9BQU8sUUFDTkMsS0FBSyxJQUFLLDZCQUNWQSxLQUFLLFFBQVMsaUJBQ2RBLEtBQUssWUFBYSxTQUFBTyxHQUFBLE1BQUEsYUFBRUEsRUFBQVMsU0FBRyxHQUFBLEtBQVdULEVBQUFTLFNBQUksR0FBUSxZQUFNVCxFQUFJLE1BQUEsTUFHNURXLFFBQVFDLElBQUksYUFDWkQsUUFBUUMsSUFBSUMsS0FBS0MsVUFBVXpCLElBQzNCc0IsUUFBUUMsSUFBSUMsS0FBS0MsVUFBVU4sRUFBZSxLQUFNLElBRXpDbEIsRUFJUixRQUFTeUIsV0FBVUMsR0FDbEJ2QyxHQUFNd0MsR0FBVzFCLEVBQUFDLE9BQU8sS0FDdEJDLEtBQUssUUFBUyxTQUNkQSxLQUFLLFlBQWEsb0JBRXBCd0IsR0FBT3pCLE9BQU8sUUFDWkUsS0FBSyxnQkFDTEQsS0FBSyxLQUFNLFNBR2J3QixFQUFPekIsT0FBTyxVQUNaQyxLQUFLLElBQUssR0FDVkEsS0FBSyxRQUFTLGVBQ2RBLEtBQUssS0FBTXVCLEVBQU0vQyxNQUFNLElBQ3ZCd0IsS0FBSyxLQUFNdUIsRUFBTS9DLE1BQU0sSUFFekJnRCxFQUFPekIsT0FBTyxVQUNaQyxLQUFLLElBQUssR0FDVkEsS0FBSyxRQUFTLGFBQ2RBLEtBQUssS0FBTXVCLEVBQU03QyxJQUFJLElBQ3JCc0IsS0FBSyxLQUFNdUIsRUFBTTdDLElBQUksSUFFdkI4QyxFQUFPekIsT0FBTyxVQUNaQyxLQUFLLElBQUssR0FDVkEsS0FBSyxRQUFTLGlCQUNkQSxLQUFLLEtBQU11QixFQUFNaEMsU0FBUyxJQUMxQlMsS0FBSyxLQUFNdUIsRUFBTWhDLFNBQVMsSUFFNUJpQyxFQUFPekIsT0FBTyxVQUNaQyxLQUFLLElBQUssR0FDVkEsS0FBSyxRQUFTLGlCQUNkQSxLQUFLLEtBQU11QixFQUFNL0IsU0FBUyxJQUMxQlEsS0FBSyxLQUFNdUIsRUFBTS9CLFNBQVMsSUFHNUJnQyxFQUFPekIsT0FBTyxRQUNaQyxLQUFLLFFBQVMsU0FDZEEsS0FBSyxJQUFLWCxVQUFVa0MsR0FFdEJ2QyxJQUFNeUMsR0FBb0JoQyx1QkFBdUI4QixFQUFNL0MsTUFBTytDLEVBQU1oQyxTQUFVZ0MsRUFBTS9CLFNBQVUrQixFQUFNN0MsS0FDOUZ5QixFQUF1QkMsR0FBQ0MsTUFBUSxJQUFFQyxJQUFJLFNBQUFDLEVBQUFDLEVBQUFDLEdBQUUsTUFBR2dCLEdBQVFsQixHQUFBRSxFQUFBQyxPQUFtQixLQUU1RWMsR0FBT2IsVUFBVSx1QkFBdUJDLEtBQUtULEdBQzNDVSxRQUNDZCxPQUFPLFVBQ05DLEtBQUssUUFBUyxzQkFDZEEsS0FBSyxJQUFLLEdBQ1ZBLEtBQUssS0FBTSxTQUFBTyxHQUFBLE1BQUFBLEdBQUEsS0FDWFAsS0FBSyxLQUFNLFNBQUFPLEdBQUEsTUFBQUEsR0FBQSxJQUdmdkIsSUFBTTBDLEdBQXlCaEMsNEJBQTRCNkIsRUFBTS9DLE1BQU8rQyxFQUFNaEMsU0FBVWdDLEVBQU0vQixTQUFVK0IsRUFBTTdDLEtBRXhHcUMsRUFBa0JYLEdBQUNDLE1BQVEsR0FBQ0MsSUFBSSxTQUFBQyxFQUFBQyxFQUFBQyxHQUNyQ3pCLEdBQU9KLEdBQUkyQixHQUFNRSxFQUFBQyxPQUFXLEVBQzVCLFFBQ0M5QixFQUFHQSxFQUNIb0MsU0FBVVMsRUFBa0I3QyxHQUM1QnFDLE1BQU9TLEVBQXVCOUMsS0FnQmhDLE9BWkE0QyxHQUFPYixVQUFVLGtCQUFrQkMsS0FBS0csR0FDdENGLFFBQ0NkLE9BQU8sUUFDTkMsS0FBSyxJQUFLLDZCQUNWQSxLQUFLLFFBQVMsaUJBQ2RBLEtBQUssWUFBYSxTQUFBTyxHQUFBLE1BQUEsYUFBRUEsRUFBQVMsU0FBRyxHQUFBLEtBQVdULEVBQUFTLFNBQUksR0FBUSxZQUFNVCxFQUFJLE1BQUEsTUFHNURXLFFBQVFDLElBQUksU0FDWkQsUUFBUUMsSUFBSUMsS0FBS0MsVUFBVUUsSUFDM0JMLFFBQVFDLElBQUlDLEtBQUtDLFVBQVVOLEVBQWUsS0FBTSxJQUV6Q1MsRUFwT1J4QyxHQUFNMkMsT0FBUSxJQUNSQyxPQUFTLElBQ1RDLFNBQVlDLElBQU8sR0FBRUMsTUFBUyxHQUFFQyxPQUFVLEdBQUVDLEtBQVEsSUFHcERDLElBQVE5QixHQUFDK0IsT0FBTyxRQUFRcEMsT0FBTyxPQUNsQ0MsS0FBSyxRQUFTMkIsT0FDZDNCLEtBQUssU0FBVTRCLFFBRVg5QixFQUFHb0MsSUFBSW5DLE9BQU8sS0FDbEJDLEtBQUssWUFBYSxhQUFXNkIsUUFBVSxLQUFBLEtBQUlBLFFBQUssSUFBQSxJQStObkRsQyxnQkFEb0JuQixPQUFTLEVBQUksSUFBR0UsS0FBTSxJQUFLLEtBQU1ELFNBQVUsSUFBTyxNQUl0RTZDLFdBRGdCOUMsT0FBUyxFQUFJLElBQUdFLEtBQU0sSUFBSyxLQUFNYSxVQUFhLEdBQUUsS0FBTUMsVUFBVyxJQUFPIiwiZmlsZSI6InNjcmlwdC5qcyIsInNvdXJjZXNDb250ZW50IjpbIlxuY29uc3Qgd2lkdGggPSA1MDA7XG5jb25zdCBoZWlnaHQgPSAzMDA7XG5jb25zdCBwYWRkaW5nID0geyB0b3A6IDEwLCByaWdodDogMTAsIGJvdHRvbTogMTAsIGxlZnQ6IDEwIH07XG5cblxuY29uc3Qgc3ZnID0gZDMuc2VsZWN0KCdib2R5JykuYXBwZW5kKCdzdmcnKVxuXHRcdC5hdHRyKCd3aWR0aCcsIHdpZHRoKVxuXHRcdC5hdHRyKCdoZWlnaHQnLCBoZWlnaHQpO1xuXG5jb25zdCBnID0gc3ZnLmFwcGVuZCgnZycpXG5cdFx0LmF0dHIoJ3RyYW5zZm9ybScsIGB0cmFuc2xhdGUoJHtwYWRkaW5nLmxlZnR9LCAke3BhZGRpbmcudG9wfSlgKVxuXG5cbmZ1bmN0aW9uIHF1YWRyYXRpY1BhdGgocSkge1xuXHRyZXR1cm4gYE0ke3Euc3RhcnRbMF19LCR7cS5zdGFydFsxXX0gUSR7cS5jb250cm9sWzBdfSwke3EuY29udHJvbFsxXX0gJHtxLmVuZFswXX0sJHtxLmVuZFsxXX1gO1xufVxuXG4vLyBCKHQpID0gKDEgLSB0KV4yUDAgKyAyKDEgLSB0KXRQMSArIHReMlAyXG5mdW5jdGlvbiBpbnRlcnBvbGF0ZVF1YWRyYXRpY0JlemllcihzdGFydCwgY29udHJvbCwgZW5kKSB7XG5cdC8vIDAgPD0gdCA8PSAxXG5cdHJldHVybiBmdW5jdGlvbiBpbnRlcnBvbGF0b3IodCkge1xuXHRcdHJldHVybiBbXG5cdFx0XHQoTWF0aC5wb3coMSAtIHQsIDIpICogc3RhcnRbMF0pICtcbiAgICAgICgyICogKDEgLSB0KSAqIHQgKiBjb250cm9sWzBdKSArXG4gICAgICAoTWF0aC5wb3codCwgMikgKiBlbmRbMF0pLFxuICAgICAgKE1hdGgucG93KDEgLSB0LCAyKSAqIHN0YXJ0WzFdKSArXG4gICAgICAoMiAqICgxIC0gdCkgKiB0ICogY29udHJvbFsxXSkgK1xuICAgICAgKE1hdGgucG93KHQsIDIpICogZW5kWzFdKSxcblx0XHRdO1xuXHR9O1xufVxuXG4vLyBCJyh0KSA9IDIoMSAtIHQpKFAxIC0gUDApICsgMnQoUDIgLSBQMSlcbmZ1bmN0aW9uIGludGVycG9sYXRlUXVhZHJhdGljQmV6aWVyQW5nbGUoc3RhcnQsIGNvbnRyb2wsIGVuZCkge1xuXHQvLyAwIDw9IHQgPD0gMVxuXHRyZXR1cm4gZnVuY3Rpb24gaW50ZXJwb2xhdG9yKHQpIHtcblx0XHRjb25zdCB0YW5nZW50WCA9ICgyICogKDEgLSB0KSAqIChjb250cm9sWzBdIC0gc3RhcnRbMF0pKSArXG5cdFx0XHRcdFx0XHRcdFx0XHRcdCAoMiAqIHQgKiAoZW5kWzBdIC0gY29udHJvbFswXSkpO1xuXHRcdGNvbnN0IHRhbmdlbnRZID0gKDIgKiAoMSAtIHQpICogKGNvbnRyb2xbMV0gLSBzdGFydFsxXSkpICtcblx0XHRcdFx0XHRcdFx0XHRcdFx0ICgyICogdCAqIChlbmRbMV0gLSBjb250cm9sWzFdKSk7XG5cblx0XHRyZXR1cm4gTWF0aC5hdGFuMih0YW5nZW50WSwgdGFuZ2VudFgpICogKDE4MCAvIE1hdGguUEkpO1xuXHR9XG59XG5cbmZ1bmN0aW9uIGN1YmljUGF0aChjKSB7XG5cdHJldHVybiBgTSR7Yy5zdGFydFswXX0sJHtjLnN0YXJ0WzFdfSBDJHtjLmNvbnRyb2wxWzBdfSwke2MuY29udHJvbDFbMV19ICR7Yy5jb250cm9sMlswXX0sJHtjLmNvbnRyb2wyWzFdfSAke2MuZW5kWzBdfSwke2MuZW5kWzFdfWA7XG59XG5cbi8vIEIodCkgPSAoMSAtIHQpXjNQMCArIDMoMSAtIHQpXjJ0UDEgKyAzKDEgLSB0KXReMlAyICsgdF4zUDNcbmZ1bmN0aW9uIGludGVycG9sYXRlQ3ViaWNCZXppZXIoc3RhcnQsIGNvbnRyb2wxLCBjb250cm9sMiwgZW5kKSB7XG5cdC8vIDAgPD0gdCA8PSAxXG5cdHJldHVybiBmdW5jdGlvbiBpbnRlcnBvbGF0b3IodCkge1xuXHRcdHJldHVybiBbXG4gICAgICAoTWF0aC5wb3coMSAtIHQsIDMpICogc3RhcnRbMF0pICtcbiAgICAgICgzICogTWF0aC5wb3coMSAtIHQsIDIpICogdCAqIGNvbnRyb2wxWzBdKSArXG4gICAgICAoMyAqICgxIC0gdCkgKiBNYXRoLnBvdyh0LCAyKSAqIGNvbnRyb2wyWzBdKSArXG5cdFx0XHQoTWF0aC5wb3codCwgMykgKiBlbmRbMF0pLFxuXHRcdFx0KE1hdGgucG93KDEgLSB0LCAzKSAqIHN0YXJ0WzFdKSArXG4gICAgICAoMyAqIE1hdGgucG93KDEgLSB0LCAyKSAqIHQgKiBjb250cm9sMVsxXSkgK1xuICAgICAgKDMgKiAoMSAtIHQpICogTWF0aC5wb3codCwgMikgKiBjb250cm9sMlsxXSkgK1xuXHRcdFx0KE1hdGgucG93KHQsIDMpICogZW5kWzFdKSxcblx0XHRdO1xuXHR9O1xufVxuXG4vLyBCJyh0KSA9IDMoMS0gdCleMihQMSAtIFAwKSArIDYoMSAtIHQpdChQMiAtIFAxKSArIDN0XjIoUDMgLSBQMilcbmZ1bmN0aW9uIGludGVycG9sYXRlQ3ViaWNCZXppZXJBbmdsZShzdGFydCwgY29udHJvbDEsIGNvbnRyb2wyLCBlbmQpIHtcblx0Ly8gMCA8PSB0IDw9IDFcblx0cmV0dXJuIGZ1bmN0aW9uIGludGVycG9sYXRvcih0KSB7XG5cdFx0Y29uc3QgdGFuZ2VudFggPSAgKDMgKiBNYXRoLnBvdygxIC0gdCwgMikgKiAoY29udHJvbDFbMF0gLSBzdGFydFswXSkpICtcblx0XHRcdFx0XHRcdFx0XHRcdFx0XHQoNiAqICgxIC0gdCkgKiB0ICogKGNvbnRyb2wyWzBdIC0gY29udHJvbDFbMF0pKSArXG5cdFx0XHRcdFx0XHRcdFx0XHRcdFx0KDMgKiBNYXRoLnBvdyh0LCAyKSAqIChlbmRbMF0gLSBjb250cm9sMlswXSkpO1xuXHRcdGNvbnN0IHRhbmdlbnRZID0gICgzICogTWF0aC5wb3coMSAtIHQsIDIpICogKGNvbnRyb2wxWzFdIC0gc3RhcnRbMV0pKSArXG5cdFx0XHRcdFx0XHRcdFx0XHRcdFx0KDYgKiAoMSAtIHQpICogdCAqIChjb250cm9sMlsxXSAtIGNvbnRyb2wxWzFdKSkgK1xuXHRcdFx0XHRcdFx0XHRcdFx0XHRcdCgzICogTWF0aC5wb3codCwgMikgKiAoZW5kWzFdIC0gY29udHJvbDJbMV0pKTtcblxuXHRcdHJldHVybiBNYXRoLmF0YW4yKHRhbmdlbnRZLCB0YW5nZW50WCkgKiAoMTgwIC8gTWF0aC5QSSk7XG5cdH1cbn1cblxuXG4vLyBkcmF3IGEgcXVhZHJhdGljIGJlemllciBjdXJ2ZVxuZnVuY3Rpb24gZHJhd1F1YWRyYXRpYyhxdWFkcmF0aWMpIHtcblx0Y29uc3QgZ1F1YWRyYXRpYyA9IGcuYXBwZW5kKCdnJylcblx0XHQuYXR0cignY2xhc3MnLCAncXVhZHJhdGljJyk7XG5cblx0Z1F1YWRyYXRpYy5hcHBlbmQoJ3RleHQnKVxuXHRcdC50ZXh0KCdRdWFkcmF0aWMgQmV6aWVyJylcblx0XHQuYXR0cignZHknLCAnMC40ZW0nKVxuXG5cdC8vIGRyYXcgdGhlIHBvaW50c1xuXHRnUXVhZHJhdGljLmFwcGVuZCgnY2lyY2xlJylcblx0XHQuYXR0cigncicsIDUpXG5cdFx0LmF0dHIoJ2NsYXNzJywgJ3N0YXJ0LXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBxdWFkcmF0aWMuc3RhcnRbMF0pXG5cdFx0LmF0dHIoJ2N5JywgcXVhZHJhdGljLnN0YXJ0WzFdKVxuXG5cdGdRdWFkcmF0aWMuYXBwZW5kKCdjaXJjbGUnKVxuXHRcdC5hdHRyKCdyJywgNSlcblx0XHQuYXR0cignY2xhc3MnLCAnZW5kLXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBxdWFkcmF0aWMuZW5kWzBdKVxuXHRcdC5hdHRyKCdjeScsIHF1YWRyYXRpYy5lbmRbMV0pXG5cblx0Z1F1YWRyYXRpYy5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0LmF0dHIoJ3InLCAzKVxuXHRcdC5hdHRyKCdjbGFzcycsICdjb250cm9sLXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBxdWFkcmF0aWMuY29udHJvbFswXSlcblx0XHQuYXR0cignY3knLCBxdWFkcmF0aWMuY29udHJvbFsxXSlcblxuXHQvLyBkcmF3IHRoZSBwYXRoXG5cdGdRdWFkcmF0aWMuYXBwZW5kKCdwYXRoJylcblx0XHQuYXR0cignY2xhc3MnLCAnY3VydmUnKVxuXHRcdC5hdHRyKCdkJywgcXVhZHJhdGljUGF0aChxdWFkcmF0aWMpKTtcblxuXHRjb25zdCBxdWFkcmF0aWNJbnRlcnBvbGF0b3IgPSBpbnRlcnBvbGF0ZVF1YWRyYXRpY0JlemllcihxdWFkcmF0aWMuc3RhcnQsIHF1YWRyYXRpYy5jb250cm9sLCBxdWFkcmF0aWMuZW5kKTtcblx0Y29uc3QgaW50ZXJwb2xhdGVkUG9pbnRzID0gZDMucmFuZ2UoMTApLm1hcCgoZCwgaSwgYSkgPT4gcXVhZHJhdGljSW50ZXJwb2xhdG9yKGQgLyAoYS5sZW5ndGggLSAxKSkpO1xuXG5cdGdRdWFkcmF0aWMuc2VsZWN0QWxsKCcuaW50ZXJwb2xhdGVkLXBvaW50JykuZGF0YShpbnRlcnBvbGF0ZWRQb2ludHMpXG5cdFx0LmVudGVyKClcblx0XHRcdC5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0XHRcdC5hdHRyKCdjbGFzcycsICdpbnRlcnBvbGF0ZWQtcG9pbnQnKVxuXHRcdFx0XHQuYXR0cigncicsIDMpXG5cdFx0XHRcdC5hdHRyKCdjeCcsIGQgPT4gZFswXSlcblx0XHRcdFx0LmF0dHIoJ2N5JywgZCA9PiBkWzFdKTtcblxuXG5cdGNvbnN0IHF1YWRyYXRpY0FuZ2xlSW50ZXJwb2xhdG9yID0gaW50ZXJwb2xhdGVRdWFkcmF0aWNCZXppZXJBbmdsZShxdWFkcmF0aWMuc3RhcnQsIHF1YWRyYXRpYy5jb250cm9sLCBxdWFkcmF0aWMuZW5kKTtcblxuXHRjb25zdCByb3RhdGVkUG9pbnRzID0gZDMucmFuZ2UoMykubWFwKChkLCBpLCBhKSA9PiB7XG5cdFx0Y29uc3QgdCA9IGQgLyAoYS5sZW5ndGggLSAxKTtcblx0XHRyZXR1cm4ge1xuXHRcdFx0dDogdCxcblx0XHRcdHBvc2l0aW9uOiBxdWFkcmF0aWNJbnRlcnBvbGF0b3IodCksXG5cdFx0XHRhbmdsZTogcXVhZHJhdGljQW5nbGVJbnRlcnBvbGF0b3IodCksXG5cdFx0fTtcblx0fSk7XG5cblx0Z1F1YWRyYXRpYy5zZWxlY3RBbGwoJy5yb3RhdGVkLXBvaW50JykuZGF0YShyb3RhdGVkUG9pbnRzKVxuXHRcdC5lbnRlcigpXG5cdFx0XHQuYXBwZW5kKCdwYXRoJylcblx0XHRcdFx0LmF0dHIoJ2QnLCAnTTEyLDAgTC01LC04IEwwLDAgTC01LDggWicpXG5cdFx0XHRcdC5hdHRyKCdjbGFzcycsICdyb3RhdGVkLXBvaW50Jylcblx0XHRcdFx0LmF0dHIoJ3RyYW5zZm9ybScsIGQgPT4gYHRyYW5zbGF0ZSgke2QucG9zaXRpb25bMF19LCAke2QucG9zaXRpb25bMV19KSByb3RhdGUoJHtkLmFuZ2xlfSlgKVxuXG5cblx0Y29uc29sZS5sb2coJ1FVQURSQVRJQycpO1xuXHRjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShxdWFkcmF0aWMpKTtcblx0Y29uc29sZS5sb2coSlNPTi5zdHJpbmdpZnkocm90YXRlZFBvaW50cywgbnVsbCwgMikpXG5cblx0cmV0dXJuIGdRdWFkcmF0aWM7XG59XG5cblxuZnVuY3Rpb24gZHJhd0N1YmljKGN1YmljKSB7XG5cdGNvbnN0IGdDdWJpYyA9IGcuYXBwZW5kKCdnJylcblx0XHQuYXR0cignY2xhc3MnLCAnY3ViaWMnKVxuXHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCAndHJhbnNsYXRlKDI1MCwgMCknKTtcblxuXHRnQ3ViaWMuYXBwZW5kKCd0ZXh0Jylcblx0XHQudGV4dCgnQ3ViaWMgQmV6aWVyJylcblx0XHQuYXR0cignZHknLCAnMC40ZW0nKVxuXG5cdC8vIGRyYXcgdGhlIHBvaW50c1xuXHRnQ3ViaWMuYXBwZW5kKCdjaXJjbGUnKVxuXHRcdC5hdHRyKCdyJywgNSlcblx0XHQuYXR0cignY2xhc3MnLCAnc3RhcnQtcG9pbnQnKVxuXHRcdC5hdHRyKCdjeCcsIGN1YmljLnN0YXJ0WzBdKVxuXHRcdC5hdHRyKCdjeScsIGN1YmljLnN0YXJ0WzFdKVxuXG5cdGdDdWJpYy5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0LmF0dHIoJ3InLCA1KVxuXHRcdC5hdHRyKCdjbGFzcycsICdlbmQtcG9pbnQnKVxuXHRcdC5hdHRyKCdjeCcsIGN1YmljLmVuZFswXSlcblx0XHQuYXR0cignY3knLCBjdWJpYy5lbmRbMV0pXG5cblx0Z0N1YmljLmFwcGVuZCgnY2lyY2xlJylcblx0XHQuYXR0cigncicsIDMpXG5cdFx0LmF0dHIoJ2NsYXNzJywgJ2NvbnRyb2wtcG9pbnQnKVxuXHRcdC5hdHRyKCdjeCcsIGN1YmljLmNvbnRyb2wxWzBdKVxuXHRcdC5hdHRyKCdjeScsIGN1YmljLmNvbnRyb2wxWzFdKVxuXG5cdGdDdWJpYy5hcHBlbmQoJ2NpcmNsZScpXG5cdFx0LmF0dHIoJ3InLCAzKVxuXHRcdC5hdHRyKCdjbGFzcycsICdjb250cm9sLXBvaW50Jylcblx0XHQuYXR0cignY3gnLCBjdWJpYy5jb250cm9sMlswXSlcblx0XHQuYXR0cignY3knLCBjdWJpYy5jb250cm9sMlsxXSlcblxuXHQvLyBkcmF3IHRoZSBwYXRoXG5cdGdDdWJpYy5hcHBlbmQoJ3BhdGgnKVxuXHRcdC5hdHRyKCdjbGFzcycsICdjdXJ2ZScpXG5cdFx0LmF0dHIoJ2QnLCBjdWJpY1BhdGgoY3ViaWMpKTtcblxuXHRjb25zdCBjdWJpY0ludGVycG9sYXRvciA9IGludGVycG9sYXRlQ3ViaWNCZXppZXIoY3ViaWMuc3RhcnQsIGN1YmljLmNvbnRyb2wxLCBjdWJpYy5jb250cm9sMiwgY3ViaWMuZW5kKTtcblx0Y29uc3QgaW50ZXJwb2xhdGVkUG9pbnRzID0gZDMucmFuZ2UoMTApLm1hcCgoZCwgaSwgYSkgPT4gY3ViaWNJbnRlcnBvbGF0b3IoZCAvIChhLmxlbmd0aCAtIDEpKSk7XG5cblx0Z0N1YmljLnNlbGVjdEFsbCgnLmludGVycG9sYXRlZC1wb2ludCcpLmRhdGEoaW50ZXJwb2xhdGVkUG9pbnRzKVxuXHRcdC5lbnRlcigpXG5cdFx0XHQuYXBwZW5kKCdjaXJjbGUnKVxuXHRcdFx0XHQuYXR0cignY2xhc3MnLCAnaW50ZXJwb2xhdGVkLXBvaW50Jylcblx0XHRcdFx0LmF0dHIoJ3InLCAzKVxuXHRcdFx0XHQuYXR0cignY3gnLCBkID0+IGRbMF0pXG5cdFx0XHRcdC5hdHRyKCdjeScsIGQgPT4gZFsxXSk7XG5cblxuXHRjb25zdCBjdWJpY0FuZ2xlSW50ZXJwb2xhdG9yID0gaW50ZXJwb2xhdGVDdWJpY0JlemllckFuZ2xlKGN1YmljLnN0YXJ0LCBjdWJpYy5jb250cm9sMSwgY3ViaWMuY29udHJvbDIsIGN1YmljLmVuZCk7XG5cblx0Y29uc3Qgcm90YXRlZFBvaW50cyA9IGQzLnJhbmdlKDMpLm1hcCgoZCwgaSwgYSkgPT4ge1xuXHRcdGNvbnN0IHQgPSBkIC8gKGEubGVuZ3RoIC0gMSk7XG5cdFx0cmV0dXJuIHtcblx0XHRcdHQ6IHQsXG5cdFx0XHRwb3NpdGlvbjogY3ViaWNJbnRlcnBvbGF0b3IodCksXG5cdFx0XHRhbmdsZTogY3ViaWNBbmdsZUludGVycG9sYXRvcih0KSxcblx0XHR9O1xuXHR9KTtcblxuXHRnQ3ViaWMuc2VsZWN0QWxsKCcucm90YXRlZC1wb2ludCcpLmRhdGEocm90YXRlZFBvaW50cylcblx0XHQuZW50ZXIoKVxuXHRcdFx0LmFwcGVuZCgncGF0aCcpXG5cdFx0XHRcdC5hdHRyKCdkJywgJ00xMiwwIEwtNSwtOCBMMCwwIEwtNSw4IFonKVxuXHRcdFx0XHQuYXR0cignY2xhc3MnLCAncm90YXRlZC1wb2ludCcpXG5cdFx0XHRcdC5hdHRyKCd0cmFuc2Zvcm0nLCBkID0+IGB0cmFuc2xhdGUoJHtkLnBvc2l0aW9uWzBdfSwgJHtkLnBvc2l0aW9uWzFdfSkgcm90YXRlKCR7ZC5hbmdsZX0pYClcblxuXG5cdGNvbnNvbGUubG9nKCdDVUJJQycpO1xuXHRjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShjdWJpYykpO1xuXHRjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShyb3RhdGVkUG9pbnRzLCBudWxsLCAyKSlcblxuXHRyZXR1cm4gZ0N1YmljO1xufVxuXG5cbmNvbnN0IHF1YWRyYXRpYyA9IHsgc3RhcnQ6IFswLCA3MF0sIGVuZDogWzIwMCwgMTAwXSwgY29udHJvbDogWzE2MCwgMjBdIH07XG5kcmF3UXVhZHJhdGljKHF1YWRyYXRpYyk7XG5cbmNvbnN0IGN1YmljID0geyBzdGFydDogWzAsIDcwXSwgZW5kOiBbMjAwLCAxMDBdLCBjb250cm9sMTogWzQwLCAxODBdLCBjb250cm9sMjogWzE2MCwgMjBdIH07XG5kcmF3Q3ViaWMoY3ViaWMpO1xuIl19 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
Awesome, thanks for sharing!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you very much for the good demo. You solution helped to implement proper rotation (orientation) of arrowheads with reactflow.dev library.