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
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 https://4-dog-problem.netlify.app/ 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 = Date.now() + this.interval;
timeout = setTimeout(step, this.interval);
}
this.stop = function() {
isRunning = false;
}
function step() {
if(!isRunning) {
clearTimeout(timeout);
} else {
let drift = Date.now() - expected;
callback();
expected += that.interval;
timeout = setTimeout(step, Math.max(0, that.interval-drift));
}
}
}
}
export default ImprovedSetInterval;
Coordinates.tsx
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];
}
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));
}
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) {
setTimeOfPursuit(time);
}
}
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: https://4-dog-problem.netlify.app/
--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--
- Follow my Blog: https://dev.to/sebcodestheweb
- Youtube: www.youtube.com/channel/UCikWIcChAOSwoc2qpbZ6iIA
- Github: https://github.com/SebCodesTheWeb
- Twitter: https://twitter.com/sebcodestheweb
This does not cover all the code, check out the source code: https://github.com/SebCodesTheWeb/pursuit-curve-calculator
Top comments (0)