DEV Community

Cover image for Mathematical Beauty in Action | Pursuit Curves

Posted on

Mathematical Beauty in Action | Pursuit Curves

Have you heard of pursuit curves?

They are this really cool thing were a thing chases another thing. And then that thing can chase another thing and on so on. If you plot out their paths then it forms a beautiful image, like the one above.

(Check out awesome timelapse at 33:13)
I made an app, in which one can simulate one of this pursuit curves adjusting some parameters.

Problem Statement

Square with arrows
In a square of side length 1, there are 4 dogs, each standing in their own corner. All dogs start running at the same time with the same speed, towards the dog next to them clockwise. How long does it take before the dogs catch up to each other?

You can view the deployed app at and play around yourself :)

My Solution

An analytical solution felt hard, and I wanted to leverage my knowledge of programming. So I thought I would make a numerical solution in TypeScript, and use React to build out a UI. My two main files are:

  • AccurateTimer.tsx
  • Coordinates.tsx

AccurateTimer.tsx is simply an improved SetInterval, to call functions repeatedly after a certain time period. It adds nice features like start and stop methods. But most importantly it adjusts for drifting, which is important when one wants to make an accurate simulation.

class ImprovedSetInterval {
  interval: number
  start: () => void
  stop: () => void

  constructor(callback: () => void, interval: number) {
    let that = this;
    let expected: number;
    let timeout: NodeJS.Timeout;
    let isRunning: boolean;
    this.interval = interval;

    this.start = function() {
      isRunning = true;
      expected = + this.interval;
      timeout = setTimeout(step, this.interval);

    this.stop = function() {
      isRunning = false;

    function step() {
      if(!isRunning) {
      } else {
        let drift = - expected;
        expected += that.interval;
        timeout = setTimeout(step, Math.max(0, that.interval-drift));

export default ImprovedSetInterval;
Enter fullscreen mode Exit fullscreen mode


Now we get into the real shit!
There is a lot of code but I'm going to try to cover the important stuff.

function getVelocity(point1: number[], point2: number[]): number[] {

    //Canceling out if at somewhat same point
    if(Math.abs(point2[0] - point1[0]) < 0.001 && Math.abs(point2[1] - point1[1]) < 0.001) return [0, 0];

    //Angle is computed using arctangent delta x over delta y
    let angle: number = Math.atan((point2[0]-point1[0])/(point2[1]-point1[1]));

    //Computing components of hyptonuse
    let dx: number = Math.sin(angle) * STEP_SIZE;
    let dy: number = Math.cos(angle) * STEP_SIZE;

    //Straight Down
    if(point2[1] < point1[1] && point2[0] - point1[0] === 0) {
      dy = -dy;

    //Third Quadrant
    if(point2[0] < point1[0] && point2[1] < point1[1]) { 
      dx = -dx;
      dy = -dy;

    //Fourth Quadrant
    if(point2[0] > point1[0] && point2[1] < point1[1]) { 
      dx = -dx;
      dy = -dy;

    return [dx, dy];
Enter fullscreen mode Exit fullscreen mode

So what is really happening here is that I'm trying to compute the velocity vector, with the direction and distance that point1 will travel in direction of point2. There are som edge cases, third quadrant, fourth quadrant, I cover them briefly in the youtube video but basically it's because the formula to compute the angle changes slightly depending on the position between the two points.

function updatePosition () {
    trackPositions.push([[...p1], [...p2], [...p3], [...p4]]);

    //Getting in which direction to move
    dp1 = getVelocity(p1, p2);  
    dp2 = getVelocity(p2, p3);  
    dp3 = getVelocity(p3, p4); 
    dp4 = getVelocity(p4, p1);   

    //Moving by that velocity(split by x and y direction)
    p1[0] += dp1[0];
    p1[1] += dp1[1];

    p2[0] += dp2[0];
    p2[1] += dp2[1];

    p3[0] += dp3[0];
    p3[1] += dp3[1]; 

    p4[0] += dp4[0];
    p4[1] += dp4[1];

    length += Math.sqrt(Math.pow(dp1[0], 2) + Math.pow(dp1[1], 2));

Enter fullscreen mode Exit fullscreen mode

updatePosition is what is getting called over each iteration. As you can see it updates the values of the arrays p1, p2, p3, p4. All on the format pn=[xn, yn]. It creates an velocity vector from the getVelocity function, and then increments the x and y values. It then stores all the positions in time form each peach in the trackPositions array.

let time = 0;
function drawFigure() {
  time += 1000/FPS; 
  let currentIndex = Math.floor((time/1000)/TIME_PER_STEP); //Getting what the position should be after time milliseconds
  if(currentIndex > trackPositions.length) {
Enter fullscreen mode Exit fullscreen mode

Finally we compute the actual time it takes for the dogs to reach the center. This is done by iterating over each index of the trackPositions array, then adding upp the time step over how long each steps take, until it reaches the end of the array, where TIME_PER_STEP = SQUARE_WIDTH * 1/N * 1/Velocity. The drawFigure function is called repeatedly with the improvedSetInterval every 1000/FPS ms.

Final Answer

This gives the final answer that it takes the dogs exactly 1 second, if they run with a constant speed of 1 m/s in a square with length 1m. This is interesting, because it means it takes the same time for the dogs to run straight across each other, as it would if they ran in the spirally pursuit curve.

True beauty

I added some nice animations in the app, which in my opinion form some really beautiful shapes, you can play around yourself:

--About Me--

Hi! I'm Sebastian and I'm a high school student from Sweden. I love math and coding. Want to show my work and explain my journey as I'm developing.

--Social Media--

This does not cover all the code, check out the source code:

Top comments (0)