Last active
August 20, 2021 15:31
-
-
Save bohnacker/12d3298163c4a4852227c287e3fa7752 to your computer and use it in GitHub Desktop.
A javascript function that takes a list of points and calculates a curvy path that passes all these points. See https://hartmut-bohnacker.de/projects/points-to-curve for more information.
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
// Calculates a curve that goes through a number of points. | |
// There are lots of mathematical approaches to do so | |
// (see: https://en.wikipedia.org/wiki/Cubic_Hermite_spline). | |
// The most commonly used seems to be the Catmull–Rom spline. | |
// My approch here is not intended to be a new general | |
// solution to this problem, but it should fit some geometrical | |
// and graphical needs. See | |
// https://hartmut-bohnacker.de/projects/points-to-curve | |
// for more explanation. | |
// points: Array of points [x1, y1, x2, y2, ..., xn, yn]; must | |
// be at least 4 points | |
// returns an object {d: "...", pointsInfo: [...]} | |
// 'd' is a path string (to use directly as the 'd' attribute in | |
// an svg path element) and pointsInfo holds lots of information | |
// that was needed for calculation. | |
function pointsToCurve(points) { | |
var PI = Math.PI; | |
// first: calculate normals and angles for all points | |
var pinfo = []; | |
for (var i = 0; i < points.length; i += 2) { | |
var p = {}; | |
p.x1 = i >= 2 ? points[i - 2] : points[i]; | |
p.y1 = i >= 2 ? points[i - 1] : points[i + 1]; | |
p.x = points[i]; | |
p.y = points[i + 1]; | |
p.x3 = i < points.length - 2 ? points[i + 2] : points[i]; | |
p.y3 = i < points.length - 2 ? points[i + 3] : points[i + 1]; | |
p.d1x = p.x - p.x1; | |
p.d1y = p.y - p.y1; | |
p.a1 = Math.atan2(p.d1y, p.d1x); | |
p.l1 = Math.sqrt(p.d1x * p.d1x + p.d1y * p.d1y); | |
if (p.l1 == 0) p.l1 = 0.0000001; | |
p.d2x = p.x - p.x3; | |
p.d2y = p.y - p.y3; | |
p.a2 = Math.atan2(p.d2y, p.d2x); | |
p.l2 = Math.sqrt(p.d2x * p.d2x + p.d2y * p.d2y); | |
if (p.l2 == 0) p.l2 = 0.0000001; | |
p.d3x = p.x3 - p.x1; | |
p.d3y = p.y3 - p.y1; | |
p.l3 = Math.sqrt(p.d3x * p.d3x + p.d3y * p.d3y); | |
p.ad = angleDifference(p.a1, p.a2); | |
p.na = angleAverage([p.a1, p.a2], [1, 1]); | |
if (p.na == 0 && p.d1x == 0 && p.d1y > 0) p.na = PI; | |
p.nx = Math.cos(p.na); | |
p.ny = Math.sin(p.na); | |
// calculate length of the anchors for all points except first and last | |
if (i >= 2 && i < points.length - 2) { | |
// the normal should have length 0 if angle difference is 180 degrees | |
// or if l1 or l2 is 0. | |
var nfac = Math.sqrt(p.l1 * p.l2); | |
var nfac1 = Math.pow(2 * p.l1 / (p.l1 + p.l2), 1); | |
var nfac2 = Math.pow(2 * p.l2 / (p.l1 + p.l2), 1); | |
// If angle diff is 90 degrees, bfac should be adjust the anchor | |
// length to make an exact circle. | |
// https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves | |
var bfac = Math.abs((4 / 3) * Math.tan(p.ad / 4) * Math.SQRT1_2); | |
// lfac should be 1 if ad is 90 degrees and smaller if ad is | |
// below or above 90 | |
var lfac = Math.SQRT2 * (0.5 * PI / (Math.abs(p.ad) + 0.00001) - 1) + 1; | |
lfac *= (2 * Math.atan(20 * Math.abs(p.ad))) / PI; | |
p.f = lfac * nfac * bfac; | |
p.f1 = Math.sign(p.ad) * p.f * nfac1; | |
p.f2 = Math.sign(p.ad) * p.f * nfac2; | |
p.a1x = p.ny * p.f1; | |
p.a1y = -p.nx * p.f1; | |
p.a2x = -p.ny * p.f2; | |
p.a2y = p.nx * p.f2; | |
} | |
pinfo.push(p); | |
} | |
// adjust direction of normals where ad is close to 180 deg | |
for (var i = 2; i < pinfo.length - 2; i++) { | |
var pp = pinfo[i - 1]; | |
var p = pinfo[i]; | |
var pn = pinfo[i + 1]; | |
var vx = Math.sign(pp.ad) * pp.nx * pp.f + Math.sign(pn.ad) * pn.nx * pn.f; | |
var vy = Math.sign(pp.ad) * pp.ny * pp.f + Math.sign(pn.ad) * pn.ny * pn.f; | |
var a = Math.atan2(-Math.sign(p.ad) * vy, -Math.sign(p.ad) * vx); | |
var l = Math.sqrt(vx * vx + vy * vy); | |
var weight = Math.pow(Math.abs(p.ad) / PI, 4) * Math.atan(1 * l) / PI; | |
var nanew = angleAverage([p.na, a], [1, weight]); | |
p.nx = Math.cos(nanew); | |
p.ny = Math.sin(nanew); | |
p.a1x = p.ny * p.f1; | |
p.a1y = -p.nx * p.f1; | |
p.a2x = -p.ny * p.f2; | |
p.a2y = p.nx * p.f2; | |
} | |
// third: make d-string for svg path elements | |
var d = ["M", trim(pinfo[1].x, 2), trim(pinfo[1].y, 2)].join(' '); | |
for (var i = 1; i < pinfo.length - 1; i++) { | |
var p1 = pinfo[i]; | |
var p2 = pinfo[i + 1]; | |
var bezPoints = [" C"]; | |
bezPoints.push(trim(p1.x + p1.a2x, 2)); | |
bezPoints.push(trim(p1.y + p1.a2y, 2)); | |
bezPoints.push(trim(p2.x + p2.a1x, 2)); | |
bezPoints.push(trim(p2.y + p2.a1y, 2)); | |
bezPoints.push(trim(p2.x, 2)); | |
bezPoints.push(trim(p2.y, 2)); | |
d += bezPoints.join(" "); | |
} | |
return { d: d, pointsInfo: pinfo }; | |
// helping function that calculates the difference of two angles | |
function angleDifference(angle1, angle2) { | |
var TWO_PI = Math.PI * 2; | |
var a1 = (angle1 % TWO_PI + TWO_PI) % TWO_PI; | |
var a2 = (angle2 % TWO_PI + TWO_PI) % TWO_PI; | |
if (a2 > a1) { | |
var d1 = a2 - a1; | |
var d2 = a1 + TWO_PI - a2; | |
if (d1 <= d2) { | |
return -d1; | |
} else { | |
return d2; | |
} | |
} else { | |
var d1 = a1 - a2; | |
var d2 = a2 + TWO_PI - a1; | |
if (d1 <= d2) { | |
return d1; | |
} else { | |
return -d2; | |
} | |
} | |
} | |
// calculates the weighted average from a list of angles | |
function angleAverage(angles, weights) { | |
var resx = 0; | |
var resy = 0; | |
for (var i = 0; i < angles.length; i++) { | |
resx += Math.cos(angles[i]) * weights[i]; | |
resy += Math.sin(angles[i]) * weights[i]; | |
} | |
return Math.atan2(resy, resx); | |
} | |
// shorten floating point numbers | |
function trim(value, dig) { | |
var f = Math.pow(10, dig); | |
return Math.round(f * value) / f; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment