Coordinates, Geometry and Putting It Together
Table of Contents
- Where Is Everything on the Screen?
- Distance and Pythagoras
- Degrees and Radians
- Points on a Circle: sin and cos
- lerp: Moving Smoothly Between Two Values
- clamp and mapRange
- Recipe: The geometry Module
- Capstone: The mathlab Report
- The Series in One Table
- Where to Go Next
- Try It Yourself
1. Where Is Everything on the Screen?
Every pixel on your screen has an address made of two numbers: x
(how far across) and y (how far down). A point is written (x, y).
This system is called coordinates, and you already use it whenever you
write CSS like left: 40px; top: 120px or draw on a canvas.
There is one twist compared with the graphs you drew in school. On a
screen, the origin (0, 0) is the top-left corner, and y grows
downward:
(0,0) ──────────► x grows right
│
│ (100, 50)
│ •
▼
y grows DOWN
So the point (100, 50) is 100 pixels right and 50 pixels below the
top edge. In school maths, "up" was positive. On a screen, "down" is
positive. This single fact explains many upside-down bugs, and it matters
again when we use angles below.
In JavaScript, a point is naturally an object:
const a = { x: 10, y: 20 };
const b = { x: 40, y: 60 };
2. Distance and Pythagoras
How far apart are a and b? Think of a right triangle. Move across by
dx = 40 - 10 = 30, and down by dy = 60 - 20 = 40. The straight line from
a to b is the longest side of that triangle. The Pythagorean theorem
says:
distance = sqrt( dx*dx + dy*dy )
sqrt means square root: the number that gives dx*dx + dy*dy when
multiplied by itself. For our points: sqrt(30*30 + 40*40) = sqrt(2500) = 50.
const dx = b.x - a.x;
const dy = b.y - a.y;
console.log(Math.sqrt(dx * dx + dy * dy)); // 50
console.log(Math.hypot(dx, dy)); // 50
Math.hypot ("hypotenuse") does the same job in one call and is safer with
very large or very small numbers. Use it.
Where you will need distance:
- Is a click within 20 pixels of a button's center?
- Which store is nearest to the user's location (on a small map)?
- Did two game objects collide?
A small speed-up: if you only need to compare distances ("is A closer
than B?"), you can skip the square root and compare dx*dx + dy*dy
directly, because the square root never changes which is bigger.
3. Degrees and Radians
You know angles in degrees: a full turn is 360. JavaScript's Math.sin
and Math.cos, along with most graphics APIs, use radians instead.
A radian measures an angle by how far you travel along a circle's edge, and
a full turn is 2 * Math.PI (about 6.2832) radians.
| Turn | Degrees | Radians |
|---|---|---|
| Quarter | 90 | Math.PI / 2 (1.5708) |
| Half | 180 | Math.PI (3.1416) |
| Full | 360 | 2 * Math.PI (6.2832) |
Math.PI is the constant pi, about 3.14159. Converting uses the fact that
180 degrees equals pi radians:
function degToRad(degrees) {
return (degrees * Math.PI) / 180;
}
function radToDeg(radians) {
return (radians * 180) / Math.PI;
}
console.log(degToRad(180)); // 3.141592653589793
console.log(degToRad(90)); // 1.5707963267948966
console.log(radToDeg(Math.PI / 2)); // 90
The most common bug in this area is passing 90 straight to Math.sin and
expecting a quarter turn. JavaScript reads it as 90 radians, which is
many full turns around. Always convert first. (CSS is a mixed bag: it accepts
both, such as rotate(90deg) or rotate(1.57rad).)
4. Points on a Circle: sin and cos
Picture a circle with its center at (cx, cy) and a radius r. An angle
picks a spot on its edge. The point at that angle is:
x = cx + r * cos(angle)
y = cy + r * sin(angle)
You do not need the full theory to use this. cos and sin take an angle
(in radians) and return a number between -1 and 1 that tells you how far
across and how far down that angle points, on a circle of radius 1. Multiply
by r to scale up, and add the center to move it into place.
const cx = 100;
const cy = 100;
const r = 50;
function pointAt(degrees) {
const rad = degToRad(degrees);
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
}
console.log(pointAt(0)); // { x: 150, y: 100 } right of the center
console.log(pointAt(90)); // { x: 100, y: 150 } BELOW the center
console.log(pointAt(180)); // { x: 50, y: 100 } left of the center
The 90 degrees result is the screen-coordinates twist from section 1:
because y grows downward, increasing the angle moves clockwise on
screen, and 90 points down. In a school graph it would point up.
Floating-point numbers also leave tiny crumbs. pointAt(270) gives
y: 50 and an x of 99.99999999999999, not exactly 100. This is the
same precision effect from Integers, Overflow and
Precision.
Round with toFixed(1) when printing, or compare with nearlyEqual.
A clock face is a nice use. To place 12 hour marks, step 30 degrees each time (360 / 12). Subtract 90 so that mark 0 sits at the top, not the right:
for (let hour = 0; hour < 12; hour++) {
const p = pointOnCircle(0, 0, 100, hour * 30 - 90);
console.log(hour, p.x.toFixed(1), p.y.toFixed(1));
}
// 0 0.0 -100.0 (12 o'clock, top)
// 3 100.0 0.0 (3 o'clock, right)
// 6 0.0 100.0 (6 o'clock, bottom)
// 9 -100.0 0.0 (9 o'clock, left)
(pointOnCircle is the function we write in the recipe below.) The same
loop positions loading spinners, radial menus, pie chart labels, and
orbiting animations.
5. lerp: Moving Smoothly Between Two Values
Lerp stands for linear interpolation, which is a long name for a
simple idea: find the value that is some fraction of the way between a
and b. You pass a fraction t from 0 to 1:
function lerp(a, b, t) {
return a + (b - a) * t;
}
console.log(lerp(0, 100, 0)); // 0 (start)
console.log(lerp(0, 100, 0.25)); // 25 (a quarter of the way)
console.log(lerp(10, 20, 0.5)); // 15 (halfway)
console.log(lerp(100, 0, 0.3)); // 70 (works going downward too)
How it works: b - a is the full distance, * t takes your fraction of
it, and a + starts from the beginning. At t = 0 you are at a, and at
t = 1 you are at b.
Everywhere something moves smoothly, there is a lerp: a progress bar
filling, a color fading, a menu sliding in, a camera following a player.
To animate, you increase t a little on each frame:
// Slide from x = 0 to x = 300 over 5 steps.
for (let step = 0; step <= 5; step++) {
console.log(lerp(0, 300, step / 5));
}
// 0, 60, 120, 180, 240, 300
A popular variant called "smoothing" repeats current = lerp(current, target, 0.5)
every frame, so the value approaches its target quickly at first and then
settles gently: 50, 75, 87.5, 93.75, 96.875 when chasing 100.
Lerp also works on colors. Do it on each of the red, green, and blue channels from Numbers and Number Systems and you get a smooth fade between two hex colors.
6. clamp and mapRange
Real values often misbehave. A drag handle can leave the screen. A percentage can reach 104% because of a rounding slip. clamp forces a value to stay inside a range:
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
console.log(clamp(150, 0, 100)); // 100 (too big, pulled down to the max)
console.log(clamp(-5, 0, 100)); // 0 (too small, pulled up to the min)
console.log(clamp(42, 0, 100)); // 42 (already inside, unchanged)
Read it as: first make sure the value is at least min (Math.max), then
make sure it is at most max (Math.min).
mapRange converts a value from one scale to another. It finds how far
along the input range the value is (a fraction, like t), then uses lerp to
place that fraction on the output range:
function mapRange(value, inMin, inMax, outMin, outMax) {
const t = (value - inMin) / (inMax - inMin);
return lerp(outMin, outMax, t);
}
console.log(mapRange(30, 0, 60, 0, 100)); // 50 (30 of 60 seconds -> 50%)
console.log(mapRange(25, 0, 100, 200, 300)); // 225
console.log(mapRange(-40, -40, 0, 0, 1)); // 0
Use it for progress bars (bytes uploaded to a percentage), sliders (a position in pixels to a value like volume), and unit conversions between two linear scales, such as Celsius to a thermometer's height.
Combine both for a robust progress bar. If 500,000 of 2,000,000 bytes have uploaded:
const percent = clamp(mapRange(500000, 0, 2000000, 0, 100), 0, 100);
console.log(percent); // 25
If the upload counter ever overshoots because of a retry, clamp keeps
the bar from drawing past 100%.
7. Recipe: The geometry Module
Create src/geometry.js in mathlab:
// src/geometry.js — points, angles, and smooth movement.
export function distance(a, b) {
return Math.hypot(b.x - a.x, b.y - a.y);
}
export function degToRad(degrees) {
return (degrees * Math.PI) / 180;
}
export function radToDeg(radians) {
return (radians * 180) / Math.PI;
}
// Screen coordinates: y grows downward, so 90 degrees points DOWN.
export function pointOnCircle(cx, cy, radius, degrees) {
const rad = degToRad(degrees);
return {
x: cx + radius * Math.cos(rad),
y: cy + radius * Math.sin(rad),
};
}
export function lerp(a, b, t) {
return a + (b - a) * t;
}
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
export function mapRange(value, inMin, inMax, outMin, outMax) {
const t = (value - inMin) / (inMax - inMin);
return lerp(outMin, outMax, t);
}
Add the last export to src/index.js, so that it re-exports all eleven
modules:
export * from "./geometry.js";
Try it in demo.js:
import {
distance, pointOnCircle, lerp, clamp, mapRange,
} from "./src/index.js";
console.log(distance({ x: 0, y: 0 }, { x: 3, y: 4 })); // 5
console.log(pointOnCircle(100, 100, 50, 0)); // { x: 150, y: 100 }
console.log(lerp(0, 100, 0.25)); // 25
console.log(clamp(150, 0, 100)); // 100
console.log(mapRange(30, 0, 60, 0, 100)); // 50
8. Capstone: The mathlab Report
Over eleven chapters you built a module each. Now bring them together.
Create report.js at the root of mathlab, next to demo.js. It imports
from src/index.js, which re-exports everything, and runs a short demo of
each module under a heading.
// report.js — one tour of everything in mathlab.
import {
// chapters 2-9
toHex, nearlyEqual,
READ, WRITE, EXEC, hasFlag, setFlag,
union, intersection,
mod, nextIndex,
toCents, formatMoney,
stepsToBinarySearch,
linearSearch, binarySearch, time,
// chapters 10-12
permutations, combinations, passwordSpace, pageCount,
randomInt, expectedValue, birthdayCollision,
distance, pointOnCircle, lerp, clamp, mapRange,
} from "./src/index.js";
function section(title) {
console.log(`\n== ${title} ==`);
}
section("2. Number systems");
console.log("255 in hex:", toHex(255));
console.log("0.1 + 0.2 nearly 0.3:", nearlyEqual(0.1 + 0.2, 0.3));
section("3. Bit flags");
let perms = READ;
perms = setFlag(perms, WRITE);
console.log("can write:", hasFlag(perms, WRITE));
console.log("can exec:", hasFlag(perms, EXEC));
section("5. Sets");
const a = new Set(["km", "en", "fr"]);
const b = new Set(["km", "zh"]);
console.log("union:", [...union(a, b)]);
console.log("intersection:", [...intersection(a, b)]);
section("6. Modular arithmetic");
console.log("mod(-1, 5):", mod(-1, 5));
console.log("next slide after the last of 4:", nextIndex(3, 4));
section("7. Money");
console.log("toCents('4.50'):", toCents("4.50"));
console.log("formatMoney:", formatMoney(450, "USD"));
section("8-9. Growth and searching");
console.log("steps to search 1,000,000 items:", stepsToBinarySearch(1_000_000));
const sorted = Array.from({ length: 1_000_000 }, (_, i) => i);
console.log("linear:", linearSearch(sorted, 999_999), "binary:", binarySearch(sorted, 999_999));
console.log("linear took more than binary:",
time(() => linearSearch(sorted, 999_999)).ms >
time(() => binarySearch(sorted, 999_999)).ms);
section("10. Counting");
console.log("PINs (4 digits):", passwordSpace(10, 4));
console.log("podiums from 8 runners:", permutations(8, 3));
console.log("lottery 6 of 49:", combinations(49, 6));
console.log("pages for 95 items:", pageCount(95, 10));
section("11. Probability");
const roll = randomInt(1, 6);
console.log("a die roll is 1 to 6:", roll >= 1 && roll <= 6);
console.log("expected die value:",
expectedValue([1, 2, 3, 4, 5, 6].map((value) => ({ value, probability: 1 / 6 }))));
console.log("23 people share a birthday:", birthdayCollision(23).toFixed(3));
section("12. Geometry");
console.log("distance (0,0)->(3,4):", distance({ x: 0, y: 0 }, { x: 3, y: 4 }));
const p = pointOnCircle(100, 100, 50, 0);
console.log("point on circle at 0 degrees:", p.x, p.y);
console.log("lerp(0, 100, 0.25):", lerp(0, 100, 0.25));
console.log("clamp(150, 0, 100):", clamp(150, 0, 100));
console.log("mapRange(30, 0, 60, 0, 100):", mapRange(30, 0, 60, 0, 100));
Run it from the mathlab folder:
node report.js
You should see something like this. The lines for chapters 2 to 9 depend
on exactly how you wrote those modules (for example, whether toHex
returns "ff" or "0xff"), so yours may differ slightly. The last four
sections come from this and the previous two chapters and should match:
== 2. Number systems ==
255 in hex: ff
0.1 + 0.2 nearly 0.3: true
== 3. Bit flags ==
can write: true
can exec: false
== 5. Sets ==
union: [ 'km', 'en', 'fr', 'zh' ]
intersection: [ 'km' ]
== 6. Modular arithmetic ==
mod(-1, 5): 4
next slide after the last of 4: 0
== 7. Money ==
toCents('4.50'): 450
formatMoney: $4.50
== 8-9. Growth and searching ==
steps to search 1,000,000 items: 20
linear: 999999 binary: 999999
linear took more than binary: true
== 10. Counting ==
PINs (4 digits): 10000n
podiums from 8 runners: 336n
lottery 6 of 49: 13983816n
pages for 95 items: 10
== 11. Probability ==
a die roll is 1 to 6: true
expected die value: 3.5
23 people share a birthday: 0.507
== 12. Geometry ==
distance (0,0)->(3,4): 5
point on circle at 0 degrees: 150 100
lerp(0, 100, 0.25): 25
clamp(150, 0, 100): 100
mapRange(30, 0, 60, 0, 100): 50
If an import fails with does not provide an export named ..., the name in
report.js does not match the one your module exports. Open that module,
check the spelling, and fix whichever side is wrong. If a line prints
undefined or throws, go back to that chapter's recipe and compare. That
is the normal way to find a bug in a library: run it, read the output, and
trace the difference.
You now have a working library with eleven modules and one script that exercises all of them. This is the same shape as a real project: small modules, one entry point, and a smoke test that tells you quickly whether everything still works.
9. The Series in One Table
| Chapter | Math idea | Where you will meet it |
|---|---|---|
| 1. Why Programmers Need Math | Math as a tool, plus the mathlab setup | Everywhere, once you start noticing it |
| 2. Numbers and Number Systems | Binary, hex, floating-point error | CSS hex colors, memory addresses, 0.1 + 0.2 |
| 3. Bitwise Operations and Flags | AND, OR, XOR, shifts on bits | Unix permissions, feature flags, network masks |
| 4. Boolean Logic | Truth tables, De Morgan's laws | Complex if conditions, access rules, search filters |
| 5. Sets and Set Operations | Union, intersection, difference | SQL joins, tag filters, "who is in A but not B" |
| 6. Modular Arithmetic | Remainder, wrapping, check digits | Carousels, hash tables, card numbers, clocks |
| 7. Integers, Overflow and Precision | Limits of numbers, BigInt, cents | Money, large IDs, counters that wrap |
| 8. Functions, Growth and Logarithms | Powers, logs, doubling | Binary search, storage planning, growth charts |
| 9. Big-O Notation | How work grows with input size | Choosing data structures, spotting slow loops |
| 10. Counting | Rule of product, permutations, combinations | Password strength, test cases, pagination |
| 11. Probability Basics | Chance, expected value, simulation | Random IDs, A/B tests, security tokens, games |
| 12. Coordinates and Geometry | Distance, angles, interpolation | UI layout, animation, charts, maps, games |
10. Where to Go Next
You can now read a piece of code and ask the right question: is this about bits, sets, remainders, growth, counting, or chance? Naming the kind of math is half the battle.
Here is how to keep building:
- Start Here roadmap — see the whole learning path and pick your next track: frontend, backend, security, and more.
- Data Structures & Algorithms (planned) — takes the Big-O thinking from chapters 8 and 9 and turns it into practice: arrays, hash tables, trees, sorting, and searching, where Big-O decides which choice is right.
- Math for AI (planned) — continues with the topics deliberately left out here: vectors, matrices, and a gentle look at calculus, the math behind machine learning.
- Grow
mathlab. Add a test file withnode --test, or write a tiny command-line tool that calls your modules. A small project you keep using teaches more than another tutorial.
11. Try It Yourself
- Nearest point. Write
nearest(origin, points)that returns the point from an array that is closest toorigin, usingdistance. Test it with three shops and a user's position. - Clock hands. Write
handPosition(minutes, length)that returns the end point of a minute hand for a clock centered on(0, 0). At 0 minutes it should point straight up (ynegative), and at 15 minutes straight right. Remember the 90 degree shift and that one minute is 6 degrees. - Color fade. Write
lerpColor(c1, c2, t)that fades between two{ r, g, b }colors by lerping each channel and rounding. Use it to print five colors between black and white. - Safe progress. Write
progressPercent(done, total)usingmapRangeandclamp. It should return 0 whentotalis 0 (no divide-by-zero surprise) and never go beyond 100. Test with(50, 200),(250, 200), and(0, 0). - Add to the report. Add a section to
report.jsthat uses an idea from chapter 4 (truthTableor the De Morgan check) and one from chapter 9 (Set.hasversusarray.includestiming). Run it and confirm every section still prints. - Your own helper. Pick one idea from the recap table that you want to
remember, such as
wrapfor a carousel or the birthday collision for short coupon codes, and write a small, real function for a project of your own using it.
Congratulations on finishing Math for Programmers. You started with
0.1 + 0.2 !== 0.3 and now you can count possibilities, size up an
algorithm, place a point on a circle, and build a library that proves it.
When you are ready, head back to the Start Here roadmap to
choose your next series. Data Structures & Algorithms and Math for AI will
build directly on what you practiced here, and you already have the
vocabulary for both.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!