Vector3

In which you learn how to navigate space and start implementing a utility class to represent locations and directions in 3D.

Cartesian Space

Space is the stage on which visual programs tell their stories. In order to situate objects at particular locations, a program's space must have addresses. Physical spaces are addressed using streets and house numbers or latitudes and longitudes. Virtual spaces are frequently addressed using a Cartesian grid. A location on a Cartesian grid is a sequence of numbers describing how far the location is from some anchor point in the grid called the origin. These distances from the origin are called coordinates.

Three-dimensional Cartesian coordinates are often written in vector form:

$$ \begin{aligned} \text{origin} &= \begin{bmatrix}0 & 0 & 0\end{bmatrix} \\ \text{target} &= \begin{bmatrix}-5 & 8 & 2\end{bmatrix} \end{aligned} $$

Each individual number describes the distance from the origin along a single axis of the coordinate system. Different disciplines label these axes differently. This book follows these conventions, which are found in many computer graphics systems:

For example, the location \(\text{target}\) is -5 units left of the origin, 8 units above the origin, and 2 units in front of the origin.

You already know these terms because you have been dealing with Cartesian coordinate systems for many years. But now it's time to start programming with them.

Vector3

Create a new file named vector.js in a project and define a class named Vector3:

export class Vector3 {
  constructor(x, y, z) {
    this.coordinates = [x, y, z];
  }

  toString() {
    return `[${this.coordinates[0]}, ${this.coordinates[1]}, ${this.coordinates[2]}]`;
  }
}

This book makes no attempt to teach you JavaScript. Read the code and work to understand it. Consult books, the web, and the humans around you if you have questions.

To test the constructor and toString method, create another file named test-vector.js and add this code:

import {Vector3} from './vector.js';

const vector = new Vector3(1, 2, 3);
console.log(vector.toString());

Run the tester in your terminal with this command:

node test-vector.js

A good vector class provides getters and setters for the individual coordinates. Place these definitions inside the Vector3 class:

get x() {
  return this.coordinates[0];
}

get y() {
  return this.coordinates[1];
}

get z() {
  return this.coordinates[2];
}

set x(value) {
  this.coordinates[0] = value;
}

set y(value) {
  this.coordinates[1] = value;
}

set z(value) {
  this.coordinates[2] = value;
}

These getters and setters make the class appear to have public instance variables named x, y, and z. That way you can write code like this:

console.log(vector.x, vector.y, vector.z);

vector.x = 0;
vector.z = vector.y;
console.log(vector.x, vector.y, vector.z);

Append these lines to test-vector.js and run the script.

These properties are just an illusion. Behind the scenes, vector.x = 0 turns into a setter function call.

Vectors vs. Positions

In physics, a vector is a relative direction or offset. The starting location to which the vector is applied is not specified. A position, on the other hand, is an absolute location. In computer graphics, you might use a vector to represent the direction in which a player is jumping and a position to represent the player's current location.

Both vectors and positions are sequences of three numbers. Because of their similarity, your Vector3 class will serve both notions. Technically, some operations that you'll add to this class won't be supported by both types. But many graphics developers overlook their differences.

Magnitude

You will eventually need to be able to compute a vector's magnitude. Magnitude is the length of a straight line going from the origin to the vector's coordinates. In two dimensions, you have computed magnitude using the Pythagorean theorem. The x-coordinate is the length of one side, and the y-coordinate is the length of the other. The straight line is the hypotenuse, which has this length:

$$ \text{magnitude} = \sqrt{x^2 + y^2} $$

The Pythagorean theorem extends to three dimensions:

$$ \text{magnitude} = \sqrt{x^2 + y^2 + z^2} $$

This value is reasonably implemented with a getter:

get magnitude() {
  return Math.sqrt(
    this.coordinates[0] * this.coordinates[0] +
    this.coordinates[1] * this.coordinates[1] +
    this.coordinates[2] * this.coordinates[2]
  );
}

Add this getter to your Vector3 class and add tests to print the magnitudes of the following vectors:

Reason about the results you see.

Immutability

Though you recently added three setters to your Vector3 class, you are encouraged to keep your classes immutable as much as possible. That is, instead of mutating the state of an object, give back a new instance with the modified state. For example, this invert function is mutable:

invert() {
  this.coordinates[0] = -this.coordinates[0];
  this.coordinates[1] = -this.coordinates[1];
  this.coordinates[2] = -this.coordinates[2];
}

This inverse function is immutable:

inverse() {
  return new Vector3(-this.coordinates[0], -this.coordinates[1], -this.coordinates[2]);
}

Immutable classes are easier to depend on because they don't change in unpredictable ways. When you write code that relies on a mutable object, you must live in the fear that the object could change at any moment.

Exercises

Add the following methods to your Vector3 class and test their results:

add(that), which returns a new Vector3 that is the sum of this and that. The parameter that is assumed to be another Vector3.
scalarMultiply(scalar), which returns a new Vector3 that is a scaled version of this. Each coordinate is scaled, or multiplied, by scalar.
normalize(), which returns a new Vector3 in which each coordinate of this is divided by the vector's magnitude. What is the magnitude of a normalized vector?

You could use your Vector3 class to define the positions array in hello-orange, like this perhaps:

const positions = [
  new Vector3(0, 0, 0),
  new Vector3(0.5, 0.5, 0.5),
];

This will fail, however, because the VertexAttributes class expects a flat array of numbers. If you really want to use Vector3, flatten the vectors using the flatMap function:

const positions = [
  new Vector3(0, 0, 0),
  new Vector3(0.5, 0.5, 0),
].flatMap(vector => vector.coordinates);

You may want to study up on flatMap if you haven't seen it before. Ignoring it and going with the original assignment to positions is also fine.

Once you're done coding, commit and push your code to Git.