DEV Community

Cover image for Some complex JS Math Utils front-end devs will love
Alessandro Cipolletti
Alessandro Cipolletti

Posted on

Some complex JS Math Utils front-end devs will love

Hi everyone,

I know that today there is a js library for (almost) everything and you can be fine even if you don't know exactly what's going on on your page. But if the day comes when you have to customise the code for yours needs... math can be very tricky.

And IMO it's always cool to know how things work anyway.

Starting from the basics:

const M = Math

const DEFAULT_DECIMALS = 4

const round = (n, d = DEFAULT_DECIMALS) => {
  const m = d ? M.pow(10, d) : 1
  return M.round(n * m) / m
}

const getPercOf = (value, total, decimals = DEFAULT_DECIMALS) => 
  round(!total ? value : value * 100 / total, decimals)

const percValueOf = (perc, total, decimals = DEFAULT_DECIMALS) =>
  round(!total ? perc : perc * total / 100, decimals)

const getRandomNumber = (max, float = false, decimals = DEFAULT_DECIMALS) =>
  float ? round(M.random() * max, decimals) : M.random() * max | 0

// fastest way I found to put a value between a Min and a Max
const arrayOrderNumberUp = (a, b) => a - b
const getNumberInBetween = (a, b, c) => [a, b, c].sort(arrayOrderNumberUp)[1]

// USAGE
round(12.3456789)     // ==> 12.3457
round(12.3456789, 2)  // ==> 12.35
round(12.3456789, 0)  // ==> 12

getPercOf(10, 20)             // ==> 50
getPercOf(10.1234567, 20)     // ==> 50.6173
getPercOf(10.1234567, 20, 8)  // ==> 50.6172835 

percValueOf(50, 20)          // ==> 10
percValueOf(12.345678, 100)  // ==> 12.3457
percValueOf(13.566, 33, 5)   // ==> 4.47678

getRandomNumber(255)  // ==> between 0 and 255, integer
getRandomNumber(1)    // ==> always 0
getRandomNumber(255, true)  // ==> between 0 and 255, float, 4 decimals

const MIN = 10
const MAX = 100
getNumberInBetween(50, MIN, MAX)  // ==> 50
getNumberInBetween(150, MIN, MAX) // ==> 100
getNumberInBetween(1, MIN, MAX)   // ==> 10
Enter fullscreen mode Exit fullscreen mode

I know there is nothing special here, but the point for now is to have your values with a default number of decimals. I found this very useful for multiple reasons while I was working on a React video editor.
I had different resolutions to consider (720p 1080p 4k) edited in a small 480p preview. There were drag and drop elements, and all positions were in % (to be able to export at any resolution). Just setting the number of decimals (to 4) fixed some alignment bugs, and allowed me to had a pixel-level precision.

On top of that, there is the rounding problem.
In js 10 / 3 = 3.3333333333333335. FFMPEG disagreed. Other bugs fixed just by setting the number of decimals.

(If your values are in pixels, just 1 decimal is enough. Anyway I suggest not to use just integer numbers, even for pixels)

Moving on, cartesian coordinate system. Probably you too, like me, studied that at school and then never heard of it again for years. Until it turns out you need it in order to edit an image on your webpage.

Crop, resize, drag and drop, scale, rotation, lines intersections, point projection on line, get a line between two points... Here is all I needed until now:

const translateCoords = (x, y, dx, dy) => ([x + dx, y + dy])

const getDistanceBetweenTwoPoints = (x1, y1, x2, y2, decimals = 0) =>
  round(M.sqrt(M.pow(x2 - x1, 2) + M.pow(y2 - y1, 2)), decimals)

const getMiddlePointCoords = (x1, y1, x2, y2, decimals = DEFAULT_DECIMALS) =>
  ([
    round((x1 + x2) / 2, decimals),  // middle point x
    round((y1 + y2) / 2, decimals),  // middle point y
  ])

const rotatePointCoords = (x, y, angle, decimals = DEFAULT_DECIMALS) =>
  ([
    round(x * M.cos(angle) - y * M.sin(angle)),  // new point x
    round(x * M.sin(angle) + y * M.cos(angle)),  // new point y
  ])

const convertAngleRadToDeg = (rad) => rad * 180 / M.PI
const convertAngleDegToRad = (deg) => deg * M.PI / 180

const getAngleDegBetweenTwoPoints = (x1, y1, x2, y2) =>
  convertAngleRadToDeg(getAngleRadBetweenTwoPoints(x1, y1, x2, y2))

const getAngleRadBetweenTwoPoints = (x1, y1, x2, y2) => {
  const m1 = x2 - x1
  const m2 = y2 - y1
  if (m1 > 0 && m2 > 0) { // first quadrant
    return (M.atan(m2 / m1))
  } else if (m1 < 0 && m2 > 0) { // second quadrant
    return (M.atan(m2 / m1) + PI)
  } else if (m1 < 0 && m2 < 0) { // third quadrant
    return (M.atan(m2 / m1) + PI)
  } else if (m1 > 0 && m2 < 0) { // fourth quadrant
    return (M.atan(m2 / m1) + PI * 2)
  } else {
    // multiples of 90
    if (m1 === 0) {
      if (m2 > 0) {
        return PI / 2
      } else {
        return PI * 1.5
      }
    } else {
      if (m1 > 0) {
        return 0
      } else {
        return PI
      }
    }
  }
}

const getSlopeCoefficientBetweenTwoPoints = (x1, y1, x2, y2) => (y2 - y1) / (x2 - x1)

const getLineFunctionBetweenTwoPoints = (x1, y1, x2, y2) => (x) =>
  (((x - x1) * (y2 - y1)) / (x2 - x1)) + y1

const getPerpendicularLineFunctionPassingByPoint = (slope, x1, y1) =>
  (x) => (-1 / slope) * (x - x1) + y1

const getIntersectionBetween4Points = (x1, y1, x2, y2, x3, y3, x4, y4, decimals = DEFAULT_DECIMALS) => {
  // points {x1, y1} and {x2, y2} define the first line
  // points {x3, y3} and {x4, y4} define the second line
  let ua, denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
  if (denom === 0) {
    return [false, false]
  }
  ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
  return [
    round(x1 + ua * (x2 - x1), decimals),
    round(y1 + ua * (y2 - y1), decimals),
  ]
}

const getPointProjectionOnLine = (x1, y1, x2, y2, x3, y3) => {
  // points {x1, y1} and {x2, y2} define the line
  // point {x3, y3} is the point to project on the line
  let x4, y4
  const slopeLine1 = getSlopeCoefficientBetweenTwoPoints(x1, y1, x2, y2)
  if (slopeLine1 === 0) {
    x4 = x3
    y4 = y1
  } else if (isFinite(slopeLine1)) {
    const line2 = getPerpendicularLineFunctionPassingByPoint(slopeLine1, x3, y3)
    if (x3 === x1) {
      x4 = x2
    } else {
      x4 = x1
    }
    y4 = line2(x4)
  } else {
    x4 = x1
    y4 = y3
  }
  return getIntersectionBetween4Points(x1, y1, x2, y2, x3, y3, x4, y4)
}
Enter fullscreen mode Exit fullscreen mode

I won't go into the complex details of what I wrote above. When you need this you'll know it.
But I can show a few examples of how I used some of it:

This free resize is quite simple:
Alt Text

This ratio + rotation resize is not:
Alt Text

For this one I projected mouse coords on the line defined by the two opposite image corners.

Same principle allowed me to do this too:
Alt Text



Last but not least, what about drawing a complex custom line on a canvas?
I already wrote the first of a series of articles about the complexity of drawing a really realistic line on a canvas. Simply put, there is no simple way to draw a realistic line.

The first obstacle is going from a series of lines drawn between mousemoves, to a nice curved line.
Alt Text

To fix that we need to stop using simple straight lines and use bezier curved lines instead.

Alt Text

Since ctx.quadraticCurveTo only allows simple flat line (and no custom tools), we need to calculate the bezier curve by hand:

const getQuadraticBezierCurvePointAtTime = (t, x1, y1, x2, y2, x3, y3, decimals = DEFAULT_DECIMALS) => ([
  round((1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * x2 + t * t * x3, decimals), // curve x at time t
  round((1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * y2 + t * t * y3, decimals), // curve y at time t
])

export const getQuadraticBezierCurveLength = (() => {
  let a, b, A, B, C, Sabc, A_2, A_32, C_2, BA
  return (x1, y1, x2, y2, x3, y3, decimals = DEFAULT_DECIMALS) => {
    a = {
      x: x1 - 2 * x2 + x3,
      y: y1 - 2 * y2 + y3,
    }
    b = {
      x: 2 * x2 - 2 * x1,
      y: 2 * y2 - 2 * y1,
    }
    A = 4 * (a.x * a.x + a.y * a.y)
    B = 4 * (a.x * b.x + a.y * b.y)
    C = b.x * b.x + b.y * b.y
    Sabc = 2 * M.sqrt(A+B+C)
    A_2 = M.sqrt(A)
    A_32 = 2 * A * A_2
    C_2 = 2 * M.sqrt(C)
    BA = B / A_2
    if (BA === -C_2 && a.x !=0 && a.y != 0 && b.x != 0 && b.y != 0) {
      BA += 1
    }
    return round((A_32 * Sabc + A_2 * B * (Sabc - C_2) + (4 * C * A - B * B) * M.log((2 * A_2 + BA + Sabc) / (BA + C_2))) / (4 * A_32), decimals)
  }
})()
Enter fullscreen mode Exit fullscreen mode

With that you'll be able to draw a perfectly homogeneous line, adapting its size and alpha continuously.

Alt Text

And that's it, I shared with you all I have used until now in my previous projects.
I know not everyone needs this level of math in his/her code, but I hope some of you can put it to good use.

Please leave a comment if you liked it or If you have some more math functions to share!
See you

Top comments (0)