Coordinates, Geometry and Putting It Together

Table of Contents

  1. Where Is Everything on the Screen?
  2. Distance and Pythagoras
  3. Degrees and Radians
  4. Points on a Circle: sin and cos
  5. lerp: Moving Smoothly Between Two Values
  6. clamp and mapRange
  7. Recipe: The geometry Module
  8. Capstone: The mathlab Report
  9. The Series in One Table
  10. Where to Go Next
  11. 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.

TurnDegreesRadians
Quarter90Math.PI / 2 (1.5708)
Half180Math.PI (3.1416)
Full3602 * 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

ChapterMath ideaWhere you will meet it
1. Why Programmers Need MathMath as a tool, plus the mathlab setupEverywhere, once you start noticing it
2. Numbers and Number SystemsBinary, hex, floating-point errorCSS hex colors, memory addresses, 0.1 + 0.2
3. Bitwise Operations and FlagsAND, OR, XOR, shifts on bitsUnix permissions, feature flags, network masks
4. Boolean LogicTruth tables, De Morgan's lawsComplex if conditions, access rules, search filters
5. Sets and Set OperationsUnion, intersection, differenceSQL joins, tag filters, "who is in A but not B"
6. Modular ArithmeticRemainder, wrapping, check digitsCarousels, hash tables, card numbers, clocks
7. Integers, Overflow and PrecisionLimits of numbers, BigInt, centsMoney, large IDs, counters that wrap
8. Functions, Growth and LogarithmsPowers, logs, doublingBinary search, storage planning, growth charts
9. Big-O NotationHow work grows with input sizeChoosing data structures, spotting slow loops
10. CountingRule of product, permutations, combinationsPassword strength, test cases, pagination
11. Probability BasicsChance, expected value, simulationRandom IDs, A/B tests, security tokens, games
12. Coordinates and GeometryDistance, angles, interpolationUI 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 with node --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

  1. Nearest point. Write nearest(origin, points) that returns the point from an array that is closest to origin, using distance. Test it with three shops and a user's position.
  2. 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 (y negative), and at 15 minutes straight right. Remember the 90 degree shift and that one minute is 6 degrees.
  3. 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.
  4. Safe progress. Write progressPercent(done, total) using mapRange and clamp. It should return 0 when total is 0 (no divide-by-zero surprise) and never go beyond 100. Test with (50, 200), (250, 200), and (0, 0).
  5. Add to the report. Add a section to report.js that uses an idea from chapter 4 (truthTable or the De Morgan check) and one from chapter 9 (Set.has versus array.includes timing). Run it and confirm every section still prints.
  6. Your own helper. Pick one idea from the recap table that you want to remember, such as wrap for 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!