Curve Length Calculus with Functional JS
You’re on a holiday trip to the peak this weekend, instead of riding on linear road you found that the road to the top is winding sideways, oscillating that way to make your trip less climbing. If you look from way up in the sky, you’d realize that the curve follows certain pattern that resembles sine function. With calculus, you’d be familiar with derivation and integration of a function to find its slope and area. But what if you want to stretch out that curve and measure the length from certain point to another? Surely there’s a formula for that:
That formula surely will make your work easier since you only need to find the 1) derivative, 2) simplify it, 3) integrate it and 4) plug the numbers in to get the result. But some of the time derivating and inegrating a function is not as easily as you think would be. There’s another trick through exhaustion — principally similar to Riemann Sum— which can be used to find the curve length.
You know that Pythagoras prompt a theorem that the square of hypotenuse of a triangle is equal to the sum of the other two sides squared. If you slice a curve to certain number you’d realize that each slice of that curve is just the hypotenuse of a triangle. So you can take advantage of this perception to think of that any curve is just a summation of many hypotenuses. With infinitesimal quantity of hypotenuse you’d get the closest approximation to the real lengh of that curve. That method is representable in this equation:
So to apply that algorithm to functional JS code, we ought to do this:
- Set a large number of slices between the desired start and end
- Treat each sliced curve as the hypotenuse of a triangle that has width and height according to given function
- Sum all hypotenuses and present the result
Through experimentation, trial and errors, here’s the code I came up with:
let withAs = (obj, cb) => cb(obj),
makeArray = n => [...Array(n).keys()],
sum = arr => arr.reduce((r, i) => r + i),
sub = arr => arr.reduce((r, i) => r - i),
curveLength = obj => withAs(
(obj.end - obj.start) / obj.slices,
width => sum(
makeArray(obj.slices)
.map(i => Math.pow(sum([
Math.pow(width, 2),
Math.pow(sub([
obj.func(width * (i + 1)),
obj.func(width * i),
]), 2)
]), 1/2))
)
)
withAs
is a callback function to pass object around inside a function. makeArray is a function to create an array in certain length. Both sum
and sub
are self-explanatory, they are used for reducing an array of numbers to single number. curveLength
is a function whence given an object with specified properties, shall return an approximate value close to the actual curve length of given function. Now lets test with a linear function y = x
with x
ranged from 2 to 3:
curveLength({
start: 2, end: 3, slices: 100,
func: x => x
}) // get 1.4142 or root of 2
As we expected, the function call yields 1.4142
which is roughly equal to the root of 2. And now back to our curiosity of finding the actual length of a winding road that follows the y = sin(2x)
function from 0 to 5:
curveLength({
start: 0, end: 5, slices: 100,
func: x => Math.sin(2 * x)
}) // got 8.5190
According to curveLength function, the actual length should be close to 8.52
point of distance, which is reasonable since the straight road — which supposedly shorter — is 5
point of distance. If you need some additional explanation of how this function works, here’s the documentation:
// How it works
curveLength = obj => withAs(
// make so many slices
(obj.end - obj.start) / obj.slices,
width => sum(
makeArray(obj.slices)
// find hypotenuse of each triangle
.map(i => Math.pow(sum([ // square of c is sum of ..
Math.pow(width, 2), // square of a and
Math.pow(sub([ // square of b
obj.func(width * (i + 1)),
obj.func(width * i), // height of triangle
]), 2)
]), 1/2))
) // sum every hypotenuse
)
I hope it helps you in some ways.