This is the Unit Circle. A representation of all the major Trigonometry functions in one place. Some of you might note that this appears to be going in the wrong direction from what you could expect from general mathematics. That is because Javascript has it's Y axis going down, not up. This does not effectively change anything else, and so I have elected not to flip my poles.
I created this in Javascript because I have been making my own drawing and maths library and have been trying to learn Trigonometry. After seeing a reddit post about the Unit Circle, I decided to make my own representation not only to make something interactive, but to fully learn the concepts myself. You can find all kinds of great information on the Wikipedia page.
I'm going to write my understanding of some concepts. This is not for someone who is new to Javascript, and not even necessarily someone new to trigonometry. It's for someone, like me, that is still wondering how to do trigonometry in Javascript. Let's start with some basics.
It all starts with an angle and a radius. If you have these two values, you can get all the others.
let a = 1.3;
let r = 100;
In javascript all trig functions default to radians. This means you need to understand those a bit. A Radian is the percentage of a circle's perimeter that would be walked by one radius of the circle. Most people have at least heard of PI. It's a value of about 3.14, but more importantly that number represents how many of the radius would be present if half the circumference of a circle were unrolled like toilet paper.
const circumference = r => Math.PI*2*r;
Example:
circumference(r); // 628.3185307179587
So this means we use radians for our angles, but it can still be useful to know how to convert radians to degrees, and degrees back to radians. Especially if you ever have to deal with user input or tell the user an angle, everyone will probably have a better time with degrees instead of radians.
const degreesToRadians = a => a*Math.PI/180;
const radiansToDegrees = a => a*180/Math.PI;
Example:
radiansToDegrees(a); // 74.48451336700703
degreesToRadians(45); // 0.7853981633974483
Of course now that we can convert from radians and degrees, it's also important to understand the difference between signed and unsigned numbers. A circle's angle can be represented by either numbers between 0 and 360 or by numbers between -180 and 180. Indeed, when you're making your own angles, you might tend to keep within 0 and 360, an unsigned number. But there are certain functions we'll discuss that will deal with angles as signed values instead.
We can make a few functions to help us convert between these types of values.
const unsignNumber = max => n => n<0?n+max:n;
const unsingRadian = unsignNumber(Math.PI*2);
const unsignDegree = unsignNumber(360);
const signNumber = max => n => n>max*0.5?n%max-max:n;
const signRadian = signNumber(Math.PI*2);
const signDegree = signNumber(360);
Example:
unsignNumber(10)(-2); // 8
unsingRadian(-2.583); // 3.700185307179586
signNumber(10)(6); // -4
signDegree(280); // -80
The unsignNumber
function expects that it's being given a number between -max*0.5 and max*0.5, while the signNumber
function is expecting only a number between 0 and max.
Having nothing to do directly with trigonometry, curried functions allow us to make functions that return functions. This can be useful to make generic functions which can then be turned into more specific functions. Here we curry a unsignNumber and signNumber function, so that when called first, it takes a number for a true maximum value and returns a function which then takes a second number to map into that range.
So for instance, the previous signNumber function could be written in a more complex manner such as this.
const unsignNumber = function(max) {
return function(n) {
if(n < 0) {
return n + max;
} else {
return n;
}
}
}
But we're moving a bit off the green right now. Let's get back to Trigonometry.
Sine and Cosine are the backbone of Trigonometry. They represent, for us, getting from angles to line widths. Because for javascript and drawing purposes, The reason we need trigonometry is to get x and y coordinates on a canvas. So if we have an angle and a radius we can turn that into x and y.
let sina = Math.sin(a); // 0.963558185417193
let cosa = Math.cos(a); // 0.26749882862458735
Let's look at an image.
So this is a triangle with a right angle. Let's call side a
the Hypotenuse, because it's opposite the right angle. Now let's consider angle C
. Its opposite line c
we'll call the opposite, and its adjacent side b
we'll call the adjacent. Side a
is still the hypotenuse.
So Sine for mathematics is a representation of a ratio of opposite over hypotenuse. Cosine is adjacent over hypotenuse. Tangent is opposite over adjacent. For the 2d graphics realm this translates into two very important things, x and y. If we consider the hypotenuse to be the radius of our circle, than sin(C)
is going to be a number between 0 and 1, a percentage. So will cos(C)
. What that number really means for us is a percentage of the hypotenuse, or a percentage of our radius. That means that if a
is our circle's radius and C
is our circle's center, than sin(C)a
represents the y
and cos(C)a
the x
of the position of point B
.
The X and Y coordinates are everything in positional math. I know I said earlier all we needed was an angle and a radius. That gets us the other sides of a hypothetical triangle, but in order to get positions, we do have to also have some where to start. We need a center x and a center y of our circle. Let's consider our space size and then get the center of it.
let cw = 400;
let ch = 400;
let cx = cw*0.5; // 200
let cy = ch*0.5; // 200
I read once that multiplication math is technically faster for Javascript than division math, and ever since then I have always used *0.5 for half instead of /2. I actually have no idea of the validity of that statement. But if you were to look through my code, you would find a lot of *0.5 and not a lot of /2.
let tana = Math.tan(a); // 3.6021024479679786
That's all I have to say about that.
These are the other trigonometry functions, but technically they're just the inverse of the first ones. They just also have new and different names. They're not actually built into Javascript, so let's do a bad thing and just add them onto the Math object.
Math.cot = a => 1/Math.tan(a);
Math.sec = a => 1/Math.cos(a);
Math.csc = a => 1/Math.sin(a);
let cota = Math.cot(a); // 0.27761564654112514
let seca = Math.sec(a); // 3.738334127075442
let csca = Math.csc(a); // 1.0378200456748015
In the Unit circle, these functions values can all be said to be a percentage of the radius. And they can be seen to map out into a number of useful numbers. The Tangent is a line that touches the outside of the circle at a right angle from the radius and travels toward Y0. The Secant represents the distance from the X0 that the tangent touches the X axis. The CoTangent moves away from the Tangent towards X0, and the CoSecant is length at the point it touches the Y axis.
So what should we have now? We can convert between radians and degrees. We can convert between signed and unsigned values. If we have an angle we can get the ratios for opposite and adjacent lines. And if we have a radius, we can turn those ratios into length values. If we have a center x and y, that means we can find the position of a point on a circle with a radius based on a current angle. Let's just see all that together.
let cw = 400; // Canvas Width
let ch = 400; // Canvas Height
let cx = cw*0.5; // Center X: 200
let cy = ch*0.5; // Center Y: 200
let a = 0.93; // An artbitrary Angle
let ad = radiansToDegrees(a); // The angle in degrees: 53.285074947166564
let r = 100; // A radius
let dx = cx+Math.cos(a)*r; // The x on the circle circumference: 259.7833982287298
let dy = cy+Math.sin(a)*r; // The y on the circle circumference: 280.1619940883777
So let's make ourself a function specifically for getting that xy position on the circle and let's make a simple function that outputs a vector.
const xy = (x,y) => ({x,y});
Side note: When doing arrow functions in ES6 Javascript syntax, and you need to return an object, you have to contain the object declaration inside some parenthesis to distinguish from the normal brace block that a function might have.
const getSatelliteXY = (x,y,a,d) => xy(x+Math.cos(a)*d,y+Math.sin(a)*d);
Example:
getSatelliteXY(cx,cy,1.3,r); // {x: 226.74988286245872, y: 296.3558185417193}
So this getSatelliteXY expects a center x and y, an angle in radians, and a radius (distance). This will return a 2d vector, and means we don't have to think about that kind of thing nearly as much later.
So we can go forward from the center now, what about the other direction? It might be nice to take a vector and turn that into a radius and an angle. You as the developer will usually work center out from whatever point you need to get, but when dealing with user input, you might need to turn a cursor position into an angle and distance.
const pointDistance = (x1,y1,x2,y2) => Math.hypot(x1-x2,y1-y2);
Example:
pointDistance(cx,cy,254,48); // 161.3071604114337
This pointDistance function takes two vectors and returns the distance between them. This is the Pythagorean theorem in practice, which states: a2 + b2 = c2. You square the x distance, you square the y distance, you add them together and return the square root of those values. Javascript just happens to have a built in function that does this called hypot.
const angleFromPoints = (cx,cy,dx,dy) => Math.atan2(dy - cy, dx - cx);
Example:
angleFromPoints(cx,cy,254,48); // -1.2294404415410498
Ok, so go back to the top of this page and look at the Radius/Tan/Sec triangle. The way that triangles work is that they have angles that add up to 180°. If we're dealing with a right angle at any time in our triangle, then we know that one acute angle is 90 minus the other acute angle. And if we're dealing with the triangles of radius/sin/cos and sin/tan/secant-cos, those two triangles are essentially the same, except the outer triangle is rotated -90° and of a different scale. But what we can know, is that if we can find the angle of the tan/sec then the circle unit angle will be 90 minus that number. That's exactly what atan2 does. It turns two points into sin and cos and the implied distance, and then uses those numbers to get the correct angle. The value returned by this function will be a signed radian.
Speaking of which, let's make our own function to turn three sides into an angle. Let's also bring back our triangle drawing for reference.
const angleFromSides = (a,b,c) => Math.acos((c*c+a*a-b*b)/(2*c*a));
Example:
angleFromSides(300,400,500); // 0.9272952180016123
If we take our triangle drawing, and we consider that at some point in our application we have three sides of a triangle, we might want to turn that into an angle. The angleFromSides function will return the B angle in radians. Using more of Pythagoras' ideas we can add and subtract the squares of our sides, and then divide that by two times the other sides from the one we want. This value ends up being the Cosine of angle B, and running that value through an ArcCosine function will get us the radian value of the angle B. Realize that this is a function for theoretical triangles, and so if we want a different angle, we just rotate the abc values clockwise.
Working with positions is best and most often done using percentages.
const within = (min,max) => n => unsignNumber(max-min)((n-min)%(max-min))+min;
const withinCircle = within(0,360);
Example:
within(0,10)(14) // 4
within(4,8)(2) // 6
withinCircle(-30) // 330
The function within
is useful for looping within a range. It's useful all the time, and as a curried function can easily be turned into a circular loop. It can also be used to loop within an array. The presumption is that it gets passed a possibly signed value, and turns it into a true value.
const toward = (min,max) => n => n*(max-min)+min;
const positionToward = (x1,y1,x2,y2,p) => xy(toward(x1,x2)(p),toward(y1,y2)(p));
Example:
toward(14,20)(0.5) // 17
positionToward(30,0,10,20,0.75) // {x: 15, y: 15}
toward
is a curried function which gives a value between a range based on a percentage. This is extremely useful when trying to get a value inbetween two positions. In fact, we make a positionToward
function which can be passed two vectors and get an even distance between them.
Ok. So like... um... I don't think anyone knows how this works. It's a magical theoretical maths function. You use it in a number of places and it just does stuff.
const vxs = (x0,y0,x1,y1) => (x0*y1) - (x1*y0);
Just look at what it does, and realize it's definitely something some mathemetician found by just messing with numbers at 3 in the morning. So then once we have that function we can do some other interesting things, that because we don't fully understand the vxs, we won't fully understand why this stuff works... but it does.
const intersect = (x0,y0,x1,y1,x2,y2,x3,y3) => xy(
vxs(vxs(x0,y0,x1,y1),x0-x1,vxs(x2,y2,x3,y3),x2-x3) / vxs(x0-x1,y0-y1,x2-x3,y2-y3),
vxs(vxs(x0,y0,x1,y1),y0-y1,vxs(x2,y2,x3,y3),y2-y3) / vxs(x0-x1,y0-y1,x2-x3,y2-y3)
);
Example:
intersect(30,5,10,20,0,-5,10,0) // {x: 26, y: 8}
I mean look at that... What is that even? I don't know. But it returns a vector of the position where two lines cross each other. Good enough for me.
const pointSide = (px,py,x0,y0,x1,y1) => vxs(x1-x0,y1-y0,px-x0,py-y0);
Example:
pointSide(30,0,10,20,45,5); // -400
The pointSide
is a very interesting function that returns a positive or negative value. This value means that, given a line and a single vector, if the line is a Unit Circle Angle, would moving the angle toward the given vector be a clockwise or counter-clockwise motion? If the return value is positive, the vector is on the clockwise side of the line, if the return is negative, it's on the counter-clockwise side of the line.
Another really good function to know about is a clamp.
const clamp = (min,max) => n => n>max?max:n<min?min:n;
let clampWidth = clamp(0,cw);
let clampHeight = clamp(0,ch);
Example;
clamp(0,10)(15); // 10
clamp(37,42)(15); // 37
clampWidth(415); // 400
The clamp
function is yet another curried function, which takes a range, and then keeps a given number within that range. You can see that currying it into width and height clamps is easy enough, and quite useful for keeping your visible math within the bounds of your canvas. Notice I made those two extra functions let instead of const. It's thoroughly possible you'd want to update those functions at sometime with new heights, so probably wouldn't ever want to hamstring yourself with immutable versions.