Points and vectors
In the previous post, we took a closer look at how we represent problems numerically. In this post, we'll intoduce some additional "type safety" into this prcess by distinguishing between two types of quantities: points and vectors. You might also call these absolute and relative quantities, or affine and vector quantities. In the next post, we'll use this perspective to build a draggable popup.
Introduction
Let's start with the example of temperature. Suppose the temperatures in a given week are as follows:
M | T | W | R | F | |
---|---|---|---|---|---|
Looking at the Celsius row, we might be tempted to say something like "it was twice as hot on Wednesday as it was on Tuesday", having the equation
in mind. But if we write the same equation in Fahrenheit, we'd get
which is wrong!
On the other hand, we could say "it got twice as hot from Monday to Wednesday as it did from Monday to Tuesday". In the Celsius case, we'd be thinking of the equation
in the Fahrenheit case, we'd have the equation
and this equation is true.
What's going on?
Operations
There are two distinct things we can mean by "temperature": an absolute temperature (how hot an object is, or the temperature outside), or a relative temperature, representing the difference between two absolute temperatures. The formulas for converting between these are different: for relative temperatures, we just multiply or divide by whereas for absolute temperatures we also have to offset by
Here are some more examples of absolute vs relative quantities:
-
an absolute time is a datetime like "3:23pm April 19 1943"; a relative time is a duration, like "4 minutes and 33 seconds".
-
an absolute position is a specific point in space, like the top of the Eiffel tower. A relative position is something like "2km north and 500m east".
We will call relative quantities vectors, and absolute quantities points. The "allowed" (or "meaningful") operations between these are
Here are some demos of that in 2d and 3d space:
SourceWhen you're working with numbers (i.e. once you've picked a coordinate system), you can't really tell the difference between points and vectors: you can add and multiply points willy-nilly. You'll run into problems, though, when you try to change coordinate systems: if you do an "illegal" operation, you'll get different answers in different coordinate systems.
Let's make a definition.
Let's try to give a slightly more precise definition (although I don't want to be fully rigorous in the main series).
If vectors represent differences between points, we might think to define points first, and then vectors. But look back at the operations we can do: all the operations involving points also involve vectors, but adding and scaling of vectors doesn't involve points. So we can talk about vectors without mentioning points, but not vice versa.
Second, if the distinguishing property of vectors is that they can be added together with other vectors, and scaled by numbers to produce other vectors, then it doesn't make sense to talk about individual vectors in isolation.
These operations are required to satisfy several axioms, such as and
It might be startling to think of functions as vectors—but this perspective turns out to be very useful, for example when solving systems of differential equations. One of the benefits of the abstract perspective is that it allows us to extend the tools of linear algebra to situations we wouldn't have originally thought of.
In most situations there will be an obvious choice for the "zero vector", "addition", and "scalar multiplication". However, it should be stressed that these are additional data that needs to be specified along with the set
Code
Graphics
Every graphics library will have methods for dealing with vectors. For example, in THREE.js the relevant class is THREE.Vector3
.
Few (if any) graphics libraries explicitly make the distinction between points and vectors. Still, remembering the distinction is helpful for understanding how to convert between different coordinate systems.
In computer graphics, the ambient "affine space" is called world space. The vector space centered at a particular object is called object space. (See below for what I mean by "centered at".)
DOM
Another example of points vs vectors comes from my favorite DOM method, getBoundingClientRect()
. The return type of this is a DOMRect
, which (ignoring readonly
) is
interface DOMRect {
height: number;
width: number;
x: number;
y: number;
readonly bottom: number;
readonly left: number;
readonly right: number;
readonly top: number;
}
To distinguish between points and vectors, we could type this more precisely as
interface DOMRect {
height: Vector1;
width: Vector1;
x: Point1;
y: Point1;
readonly bottom: Point1;
readonly left: Point1;
readonly right: Point1;
readonly top: Point1;
}
Here a Vector1
and a Point1
are both just number
s, but we're not allowed to add two Point1
s together.
TypeScript
The above discussion of axioms and operations might have been a little abstract or confusing. The following code is an approximate TypeScript equivalent of what I was saying above. In particular, note that the operations add()
, scale()
, sub()
have to be specified/implemented.
// helpers
// https://stackoverflow.com/a/43674389
interface Type<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): T;
}
function staticImplements<T>() {
return <U extends T>(constructor: U, _context: ClassDecoratorContext) => {
constructor;
};
}
// vector types
interface Vector<V> {
/** Add two vectors together */
add(other: V): V;
/** Scale a vector by a number */
scale(factor: number): V;
}
interface VectorStatic<V> extends Type<Vector<V>> {
zero: V;
}
// implementation
@staticImplements<VectorStatic<Vector2>>()
export class Vector2 {
constructor(
public x: number,
public y: number,
) {}
static zero = new Vector2(0, 0);
/** Add two vectors together */
add(other: Vector2) {
return new Vector2(this.x + other.x, this.y + other.y);
}
/** Scale a vector by a number */
scale(factor: number) {
return new Vector2(this.x * factor, this.y * factor);
}
/** Whether two vectors are equal */
static eq(u: Vector2, v: Vector2) {
return u.x === v.x && u.y === v.y;
}
}
// affine types
interface Affine<V, A> {
/** Add a vector to a point to get another point */
add(other: V): A;
/** Subtract two points to get a vector */
sub(other: A): V;
}
@staticImplements<Type<Affine<Vector2, Point2>>>()
export class Point2 {
constructor(
public x: number,
public y: number,
) {}
/** Add a vector to a point to get another point */
add(other: Vector2) {
return new Point2(this.x + other.x, this.y + other.y);
}
/** Subtract two points to get a vector */
sub(other: Point2) {
return new Vector2(this.x - other.x, this.y * other.y);
}
/** Whether this point is equal to another */
eq(other: Point2) {
return this.x === other.x && this.y === other.y;
}
}
Note we have to do a little bit of decorator weirdness to express this in TypeScript. Also, TypeScript has no way of expressing the constraints (axioms) these operations need to specify, e.g.
The closest code equivalent would be
function assertVectorSpaceAxioms(u: Vector2, v: Vector2, w: Vector2, r: number) {
// zero vector is identity element for addition
// u + 0 = u
assert(Vector2.eq(
u.add(Vector2.zero),
u,
);
// 0 + u = u
assert(Vector2.eq(
Vector2.zero.add(u), // 0 + v
u,
);
// associativity: (u + v) + w = u + (v + w)
assert(Vector2.eq(
u.add(v).add(w), // (u + v) + w
u.add(v.add(w)), // u + (v + w)
));
// commutativity: u + v = v + u
assert(Vector2.eq(
u.add(v),
v.add(u)
));
// ...and so on (see Exercises)
}
These limitations are deficiencies of TypeScript, not of mathematics.
Dictionary
One of the reasons the distinction between points and vectors is confusing is that it's possible to convert between the two. However, there are many different ways to do this—more precisely, every point gives a different way to convert between points and vectors. It works as follows:
Let be a vector space, let be an affine space over and let be a point of The choice of gives a way to go back and forth between and
We include the subscript to emphasize that these functions depend on the choice of "origin" point These functions are bijections:
so no information is lost in this process.
The rub is that this dictionary depends on the chosen point . In some situations there may be an obvious "origin" point, but in others there may be none, and in most there are several. For example, in computer graphics our "default" origin is the top-left corner of the screen. But if you want to animate a fireball circling around a character, you want to position the fireball relative to that character, meaning the center of that character's bounding box will be your origin.
Note that every vector space is an affine space over itself: we already know how to add vectors to vectors, and we can define subtraction of vectors by combining addition and scalar multiplication: