Tesseract Portal — 4D Hypercube Projection

This sketch renders a 4-dimensional hypercube (tesseract) that rotates through 4D space and gets mathematically projected down into the 3D WEBGL canvas as a glowing wireframe. Dragging the mouse steers the rotation speed, clicking toggles auto-rotation, and pressing R randomizes the spin, making the shape feel like a living portal.

🧪 Try This!

Experiment with the code by making these changes:

  1. Slow down and thicken the glow — Reducing rotation speed and boosting the glow stroke weight makes the tesseract feel like it's slowly pulsing rather than spinning fast.
  2. Recolor the glow to magenta — The glow pass color is set once with stroke(0, 255, 255, 50) - changing the RGB channels instantly changes the portal's hue.
  3. Shrink the tesseract — Lowering hypercubeSize scales the entire projected wireframe down, making the shape look smaller and further away.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a real tesseract - the four-dimensional analog of a cube - by generating 16 vertices with coordinates of -1 or +1 in four axes (x, y, z, w), then connecting every pair of vertices that differ in exactly one coordinate to form 32 edges. Each frame it rotates those vertices through two 4D rotation planes (x-w and y-z) using actual 4x4 rotation matrices, then perspective-projects the result into 3D coordinates that p5.js can draw with WEBGL's vertex() and beginShape(LINES). The glowing look comes from drawing the wireframe twice: once as solid white lines, then again with blendMode(ADD) and a translucent cyan stroke so overlapping edges brighten like neon.

The code is organized around a data-generation helper (generateHypercube), two small linear-algebra helpers (multiplyMatrixVector and multiplyMatrices) that do the actual 4D math, and the standard p5.js setup()/draw() pair that ties it together with mouse and keyboard interaction. Studying it teaches how higher-dimensional rotation and projection work, how to mix 2D text overlays into a WEBGL scene using push()/pop() and ortho(), and how additive blending creates a glow effect.

⚙️ How It Works

  1. When the sketch loads, preload() fetches a web font and setup() creates a full-window WEBGL canvas, then calls generateHypercube() to compute the tesseract's 16 four-dimensional corner points and the 32 edges connecting them.
  2. Every frame, draw() clears the background, advances two rotation angles (angleXW and angleYZ) if auto-rotate is on, and builds two 4x4 rotation matrices representing spins in the x-w plane and the y-z plane - planes that don't exist in ordinary 3D space.
  3. Those two matrices are multiplied together into one combined matrix, which is then applied to every 4D vertex, producing a rotated set of 4D points.
  4. Each rotated 4D point is perspective-projected into 3D by dividing its x, y, z coordinates by (projectionDistance - w), so vertices with larger w values appear to recede or bulge, giving the illusion of depth beyond the third dimension.
  5. The projected 3D vertices are connected into lines twice - a solid pass and a glowing additive-blend pass - to draw the wireframe, and finally push()/ortho()/pop() temporarily switch to a flat 2D projection so instructional text can be drawn on top of the 3D scene.
  6. Mouse drags nudge the two rotation speeds based on how fast the cursor moves, clicking toggles autoRotate, and pressing R randomizes both speeds, so the viewer directly steers the 4D spin.

🎓 Concepts You'll Learn

4D to 3D perspective projectionMatrix multiplication and rotation matricesWEBGL custom geometry with beginShape(LINES)Additive blend mode for glow effectsBitwise operators for vertex generationMixing 2D text overlays into 3D scenes with ortho()

📝 Code Breakdown

preload()

preload() is a special p5.js function that runs before setup() and pauses the sketch until all loading calls inside it (like loadFont, loadImage, loadSound) complete. This avoids the classic bug where text or images fail to appear because they hadn't finished loading yet.

function preload() {
  myFont = loadFont('https://unpkg.com/@fontsource/source-code-pro@latest/files/source-code-pro-latin-400-normal.woff');
}
Line-by-line explanation (1 lines)
myFont = loadFont('https://unpkg.com/@fontsource/source-code-pro@latest/files/source-code-pro-latin-400-normal.woff');
Downloads a font file from a CDN before the sketch starts. p5.js's preload() guarantees this finishes loading before setup() runs, which is required because WEBGL text rendering needs a font object rather than a system font.

setup()

setup() runs once when the sketch starts, and is the right place to configure the canvas, drawing defaults, and generate any data structures (like the hypercube's vertices and edges) that draw() will reuse every frame.

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  noFill();
  stroke(255);
  strokeWeight(2);
  textFont(myFont); // Set the loaded font for text
  textSize(16);

  generateHypercube(); // Generate vertices and edges
}
Line-by-line explanation (7 lines)
createCanvas(windowWidth, windowHeight, WEBGL);
Creates a canvas that fills the browser window and uses the WEBGL renderer, which is required for drawing true 3D geometry like the hypercube's edges.
noFill();
Turns off shape fill so only the wireframe outlines of edges are visible, keeping the tesseract looking like a transparent glowing structure.
stroke(255);
Sets a default white stroke color, though draw() overrides this each frame for the two rendering passes.
strokeWeight(2);
Sets a default line thickness in pixels for drawn edges.
textFont(myFont); // Set the loaded font for text
Applies the font loaded in preload() so the UI text overlay renders correctly in WEBGL mode.
textSize(16);
Sets the font size in pixels for the instructional text drawn later in draw().
generateHypercube(); // Generate vertices and edges
Calls the helper function that computes the tesseract's 16 four-dimensional vertices and the edges connecting them, filling the global vertices4D and edges arrays.

draw()

draw() runs continuously (about 60 times per second) and is where per-frame animation happens: updating angles, recomputing rotation matrices, transforming every vertex, projecting to 3D, and rendering. Splitting the wireframe into a solid pass and an additive glow pass is a common trick for making thin WEBGL lines look luminous without needing custom shaders.

🔬 This is what makes the tesseract spin on its own. What happens if you swap the += for -= on angleXW only, so the two planes rotate in opposite senses?

  if (autoRotate) {
    angleXW += rotationSpeedXW;
    angleYZ += rotationSpeedYZ;
  }

🔬 The perspective divide uses projectionDistance minus the w-coordinate. What happens visually if projectionDistance is set very close to 1 (near the vertex range of -1 to 1), making the denominator sometimes tiny?

  let scaleFactor = 1 / (projectionDistance - v[3]);
    return createVector(
      v[0] * scaleFactor * hypercubeSize,
      v[1] * scaleFactor * hypercubeSize,
      v[2] * scaleFactor * hypercubeSize
    );
function draw() {
  background(0); // Black background

  // Update rotation angles if auto-rotate is on
  if (autoRotate) {
    angleXW += rotationSpeedXW;
    angleYZ += rotationSpeedYZ;
  }

  // --- 4D Rotation ---
  // Create rotation matrices for x-w and y-z planes
  // Matrix for x-w plane rotation:
  // [ cos(a)  0  0 -sin(a) ]
  // [   0     1  0    0    ]
  // [   0     0  1    0    ]
  // [ sin(a)  0  0  cos(a) ]
  const matXW = [
    [cos(angleXW), 0, 0, -sin(angleXW)],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [sin(angleXW), 0, 0, cos(angleXW)]
  ];

  // Matrix for y-z plane rotation:
  // [   1     0    0    0    ]
  // [   0  cos(a) -sin(a)  0 ]
  // [   0  sin(a)  cos(a)  0 ]
  // [   0     0    0    1    ]
  const matYZ = [
    [1, 0, 0, 0],
    [0, cos(angleYZ), -sin(angleYZ), 0],
    [0, sin(angleYZ), cos(angleYZ), 0],
    [0, 0, 0, 1]
  ];

  // Combine rotations by multiplying matrices (order matters!)
  // Applying YZ rotation, then XW rotation
  const combinedMatrix = multiplyMatrices(matXW, matYZ);

  // Apply the combined rotation to each 4D vertex
  let rotatedVertices4D = vertices4D.map(v => multiplyMatrixVector(combinedMatrix, v));

  // --- 4D to 3D Perspective Projection ---
  // Project each rotated 4D vertex to 3D space
  // Perspective projection based on 'w' coordinate:
  // x_proj = x / (distance - w)
  // y_proj = y / (distance - w)
  // z_proj = z / (distance - w)
  let projectedVertices3D = rotatedVertices4D.map(v => {
    // Avoid division by zero or very small numbers
    let scaleFactor = 1 / (projectionDistance - v[3]);
    return createVector(
      v[0] * scaleFactor * hypercubeSize,
      v[1] * scaleFactor * hypercubeSize,
      v[2] * scaleFactor * hypercubeSize
    );
  });

  // --- Render Hypercube Wireframe ---
  // Draw edges with a base stroke
  stroke(255); // White stroke
  strokeWeight(1);
  beginShape(LINES);
  for (let edge of edges) {
    let v1 = projectedVertices3D[edge[0]];
    let v2 = projectedVertices3D[edge[1]];
    vertex(v1.x, v1.y, v1.z);
    vertex(v2.x, v2.y, v2.z);
  }
  endShape();

  // Draw edges again with additive blend mode for glow effect
  blendMode(ADD); // Switch to additive blending
  stroke(0, 255, 255, 50); // Cyan stroke with some transparency
  strokeWeight(3);
  beginShape(LINES);
  for (let edge of edges) {
    let v1 = projectedVertices3D[edge[0]];
    let v2 = projectedVertices3D[edge[1]];
    vertex(v1.x, v1.y, v1.z);
    vertex(v2.x, v2.y, v2.z);
  }
  endShape();
  blendMode(BLEND); // Reset blend mode to default

  // --- Draw UI Text (2D Overlay) ---
  // In WEBGL mode, to draw 2D text on top, we switch to 2D projection using ortho()
  push();
  ortho(); // Switch to 2D orthographic projection
  translate(-width / 2, -height / 2); // Move origin to top-left for 2D UI

  fill(255); // White text
  noStroke(); // No stroke for text

  text("Controls:", 20, 30);
  text("Drag mouse: Change rotation speed", 20, 50);
  text("Click: Toggle auto-rotate (" + (autoRotate ? "ON" : "OFF") + ")", 20, 70);
  text("Press 'R': Randomize rotation speeds", 20, 90);
  text("Rotation Speed XW: " + rotationSpeedXW.toFixed(4), 20, 120);
  text("Rotation Speed YZ: " + rotationSpeedYZ.toFixed(4), 20, 140);

  pop(); // Restore 3D projection
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

conditional Auto-Rotate Angle Update if (autoRotate) { angleXW += rotationSpeedXW; angleYZ += rotationSpeedYZ; }

Advances the two 4D rotation angles every frame only when auto-rotation is enabled

for-loop Base Wireframe Edge Loop for (let edge of edges) { let v1 = projectedVertices3D[edge[0]]; let v2 = projectedVertices3D[edge[1]]; vertex(v1.x, v1.y, v1.z); vertex(v2.x, v2.y, v2.z); }

Loops through every edge index pair and draws a 3D line segment between the two connected projected vertices

for-loop Glow Wireframe Edge Loop for (let edge of edges) { let v1 = projectedVertices3D[edge[0]]; let v2 = projectedVertices3D[edge[1]]; vertex(v1.x, v1.y, v1.z); vertex(v2.x, v2.y, v2.z); }

Redraws the same edges with additive blending and a translucent cyan stroke to create the neon glow effect

background(0); // Black background
Clears the canvas to solid black every frame, which is essential in WEBGL mode too - without it you'd see smearing or artifacts from the previous frame.
if (autoRotate) { angleXW += rotationSpeedXW; angleYZ += rotationSpeedYZ; }
Only advances the rotation angles when autoRotate is true, letting the click handler freeze the tesseract in place.
const matXW = [...]
Builds a 4x4 rotation matrix that rotates points around the x-w plane using the standard 2D rotation formula (cos/sin) placed into the x and w rows/columns.
const matYZ = [...]
Builds a second 4x4 rotation matrix, this time rotating around the y-z plane, which is closer to an ordinary 3D rotation.
const combinedMatrix = multiplyMatrices(matXW, matYZ);
Multiplies the two rotation matrices together so a single matrix can apply both rotations to a vertex in one step - order matters because matrix multiplication is not commutative.
let rotatedVertices4D = vertices4D.map(v => multiplyMatrixVector(combinedMatrix, v));
Applies the combined rotation matrix to every one of the 16 original 4D vertices, producing their new rotated positions.
let scaleFactor = 1 / (projectionDistance - v[3]);
Computes a perspective divide based on the vertex's w-coordinate - vertices with w closer to projectionDistance get scaled up more, simulating depth in the fourth dimension.
return createVector(v[0] * scaleFactor * hypercubeSize, v[1] * scaleFactor * hypercubeSize, v[2] * scaleFactor * hypercubeSize);
Converts the rotated 4D point into a 3D p5.Vector by scaling its x, y, z components by the perspective factor and the overall display size.
beginShape(LINES);
Starts a custom WEBGL geometry shape where every pair of vertex() calls draws one independent line segment.
vertex(v1.x, v1.y, v1.z);
Feeds one 3D point into the shape being built - two calls in a row form one line segment representing a tesseract edge.
blendMode(ADD); // Switch to additive blending
Changes how new pixels combine with existing ones so overlapping translucent lines add their brightness together, creating a glowing highlight where edges overlap.
stroke(0, 255, 255, 50); // Cyan stroke with some transparency
Sets a semi-transparent cyan color for the glow pass; the low alpha (50) means many overlapping lines are needed to become fully bright.
blendMode(BLEND); // Reset blend mode to default
Restores normal blending after the glow pass so it doesn't affect other drawing (like the text) later in the frame.
push();
Saves the current transformation and projection state so the following ortho() and translate() calls can be undone later.
ortho(); // Switch to 2D orthographic projection
Switches the WEBGL camera to an orthographic (non-perspective) projection, which is what lets flat 2D text sit cleanly on top of the 3D scene without warping.
translate(-width / 2, -height / 2); // Move origin to top-left for 2D UI
WEBGL's default origin is the center of the canvas; this shifts it back to the top-left corner so text(x, y) coordinates behave like normal 2D p5.js.
text("Rotation Speed XW: " + rotationSpeedXW.toFixed(4), 20, 120);
Displays the current x-w rotation speed rounded to 4 decimal places, giving the viewer live feedback on how their mouse dragging is affecting the spin.
pop(); // Restore 3D projection
Restores the 3D perspective projection and transformations that were active before the 2D text overlay, so the next frame's 3D drawing works correctly.

mousePressed()

mousePressed() is a p5.js event function that fires automatically once whenever a mouse button is pressed, making it ideal for simple toggle interactions like this one.

function mousePressed() {
  autoRotate = !autoRotate; // Toggle auto-rotate on click
}
Line-by-line explanation (1 lines)
autoRotate = !autoRotate; // Toggle auto-rotate on click
Flips the boolean autoRotate variable between true and false each time the mouse is clicked, pausing or resuming automatic spinning.

mouseDragged()

mouseDragged() fires repeatedly while the mouse button is held and moved, and pmouseX/pmouseY (the mouse position from the previous frame) let you measure velocity - a common pattern for turning drag gestures into physics-like responses.

function mouseDragged() {
  // Map mouse velocity to rotation speed changes
  let dx = mouseX - pmouseX;
  let dy = mouseY - pmouseY;

  rotationSpeedXW += dx * 0.0001;
  rotationSpeedYZ += dy * 0.0001;

  // Clamp speeds to a reasonable range
  rotationSpeedXW = constrain(rotationSpeedXW, -0.05, 0.05);
  rotationSpeedYZ = constrain(rotationSpeedYZ, -0.05, 0.05);
}
Line-by-line explanation (6 lines)
let dx = mouseX - pmouseX;
Calculates how far the mouse moved horizontally since the last frame by comparing the current position (mouseX) to the previous one (pmouseX).
let dy = mouseY - pmouseY;
Calculates the vertical mouse movement in the same way, using mouseY and pmouseY.
rotationSpeedXW += dx * 0.0001;
Nudges the x-w rotation speed based on horizontal drag distance, scaled down by a small factor so a single drag doesn't cause a huge jump in speed.
rotationSpeedYZ += dy * 0.0001;
Nudges the y-z rotation speed based on vertical drag distance in the same way.
rotationSpeedXW = constrain(rotationSpeedXW, -0.05, 0.05);
Uses p5.js's constrain() to keep the speed value from growing without bound, preventing the tesseract from spinning uncontrollably fast.
rotationSpeedYZ = constrain(rotationSpeedYZ, -0.05, 0.05);
Applies the same speed limit to the y-z rotation speed.

keyPressed()

keyPressed() is a p5.js event function triggered once per key press, and the global 'key' variable holds the character that was typed - useful for adding keyboard shortcuts like this randomize feature.

function keyPressed() {
  // Randomize rotation speeds on 'R' press
  if (key === 'R' || key === 'r') {
    rotationSpeedXW = random(-0.02, 0.02);
    rotationSpeedYZ = random(-0.02, 0.02);
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional R Key Check if (key === 'R' || key === 'r') {

Checks whether the pressed key was R in either uppercase or lowercase form

if (key === 'R' || key === 'r') {
Checks the built-in p5.js 'key' variable to see if the most recently pressed key was R, handling both uppercase and lowercase since Caps Lock or Shift changes the case.
rotationSpeedXW = random(-0.02, 0.02);
Assigns a new random rotation speed for the x-w plane between -0.02 and 0.02 using p5.js's random() function.
rotationSpeedYZ = random(-0.02, 0.02);
Assigns a new random rotation speed for the y-z plane in the same range, giving the tesseract an unpredictable new spin.

windowResized()

windowResized() is a p5.js event function that fires automatically whenever the browser window changes size, and pairing it with resizeCanvas() is the standard way to build responsive full-window sketches.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window's new dimensions whenever it changes, keeping the tesseract centered and full-screen after a resize.

generateHypercube()

This function is a data-generation step that runs once in setup(). It shows how bitwise operators can compactly enumerate combinations, and how comparing coordinate differences is a clean way to define connectivity in any-dimensional cube shapes.

🔬 This rule (diffCount === 1) defines a tesseract's edges. What happens if you change it to diffCount === 2, connecting vertices that differ in two coordinates instead - what new shape of lines appears?

      if (diffCount === 1) {
        edges.push([i, j]); // Store indices of connected vertices
      }
function generateHypercube() {
  vertices4D = [];
  edges = [];

  // Generate 16 vertices (2^4)
  // Each coordinate (x,y,z,w) can be -1 or +1
  for (let i = 0; i < 16; i++) {
    let v = [];
    v[0] = (i & 1) ? 1 : -1;    // x coordinate
    v[1] = (i & 2) ? 1 : -1;    // y coordinate
    v[2] = (i & 4) ? 1 : -1;    // z coordinate
    v[3] = (i & 8) ? 1 : -1;    // w coordinate
    vertices4D.push(v);
  }

  // Define edges: an edge connects two vertices that differ by exactly one coordinate
  for (let i = 0; i < vertices4D.length; i++) {
    for (let j = i + 1; j < vertices4D.length; j++) {
      let diffCount = 0;
      for (let k = 0; k < 4; k++) {
        if (vertices4D[i][k] !== vertices4D[j][k]) {
          diffCount++;
        }
      }
      if (diffCount === 1) {
        edges.push([i, j]); // Store indices of connected vertices
      }
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop 16-Vertex Generation Loop for (let i = 0; i < 16; i++) { let v = []; v[0] = (i & 1) ? 1 : -1; // x coordinate v[1] = (i & 2) ? 1 : -1; // y coordinate v[2] = (i & 4) ? 1 : -1; // z coordinate v[3] = (i & 8) ? 1 : -1; // w coordinate vertices4D.push(v); }

Uses bitwise AND on numbers 0-15 to generate every combination of -1/+1 across four coordinates, producing all 16 tesseract corners

for-loop Edge Detection Nested Loop for (let i = 0; i < vertices4D.length; i++) { for (let j = i + 1; j < vertices4D.length; j++) { let diffCount = 0; for (let k = 0; k < 4; k++) { if (vertices4D[i][k] !== vertices4D[j][k]) { diffCount++; } } if (diffCount === 1) { edges.push([i, j]); // Store indices of connected vertices } } }

Compares every pair of vertices coordinate by coordinate and records an edge whenever exactly one of the four coordinates differs, which is the mathematical definition of a hypercube edge

vertices4D = [];
Resets the global array that will hold all 16 four-dimensional vertex coordinates.
edges = [];
Resets the global array that will hold pairs of vertex indices representing edges.
v[0] = (i & 1) ? 1 : -1; // x coordinate
Uses the bitwise AND operator to check the 1s bit of i; if it's set the x coordinate is +1, otherwise -1 - this is a compact way to enumerate all combinations of +1/-1.
v[1] = (i & 2) ? 1 : -1; // y coordinate
Checks the 2s bit of i to decide the y coordinate, similarly toggling between -1 and +1.
v[2] = (i & 4) ? 1 : -1; // z coordinate
Checks the 4s bit of i to decide the z coordinate.
v[3] = (i & 8) ? 1 : -1; // w coordinate
Checks the 8s bit of i to decide the fourth-dimensional w coordinate, completing all four axes so that looping i from 0-15 covers every one of the 16 possible sign combinations.
for (let j = i + 1; j < vertices4D.length; j++) {
Starts the inner loop at i+1 rather than 0 so each unique pair of vertices is only compared once, avoiding duplicate edges.
if (vertices4D[i][k] !== vertices4D[j][k]) { diffCount++; }
Compares one coordinate at a time between two vertices and counts how many of the four coordinates are different.
if (diffCount === 1) { edges.push([i, j]); }
Two vertices of a hypercube are connected by an edge only if they differ in exactly one coordinate - this line captures that geometric rule and stores the pair of indices.

multiplyMatrixVector()

This is a general-purpose 4x4 matrix times 4D vector multiplication function - the core linear algebra operation that lets the sketch rotate points in four-dimensional space just like a 3x3 matrix rotates points in ordinary 3D.

function multiplyMatrixVector(matrix, vector) {
  let result = [0, 0, 0, 0];
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      result[i] += matrix[i][j] * vector[j];
    }
  }
  return result;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Matrix-Vector Multiply Loop for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { result[i] += matrix[i][j] * vector[j]; } }

Implements the standard row-by-column matrix-vector multiplication formula to rotate a 4D point

let result = [0, 0, 0, 0];
Starts with a zeroed-out 4-element array that will accumulate the rotated x, y, z, w values.
for (let i = 0; i < 4; i++) {
Loops over each of the 4 output coordinates (rows of the result).
for (let j = 0; j < 4; j++) {
Loops over each of the 4 input coordinates to sum their weighted contribution to output row i.
result[i] += matrix[i][j] * vector[j];
Multiplies each matrix row entry by the corresponding vector component and adds it to the running total - this is the textbook definition of matrix-vector multiplication.
return result;
Returns the new 4D vector after the rotation matrix has been applied.

multiplyMatrices()

Combining two rotation matrices into one via multiplication before applying them to every vertex is more efficient than rotating each vertex twice separately, and it's the same technique used in 3D graphics engines to combine transformations like rotate-then-translate.

function multiplyMatrices(m1, m2) {
  let result = Array(4).fill(0).map(() => Array(4).fill(0));
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      for (let k = 0; k < 4; k++) {
        result[i][j] += m1[i][k] * m2[k][j];
      }
    }
  }
  return result;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Matrix-Matrix Multiply Triple Loop for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { for (let k = 0; k < 4; k++) { result[i][j] += m1[i][k] * m2[k][j]; } } }

Implements standard 4x4 matrix multiplication so two separate rotations can be combined into a single transformation

let result = Array(4).fill(0).map(() => Array(4).fill(0));
Creates a fresh 4x4 grid of zeros to hold the resulting combined matrix - using .map() here ensures each row is a genuinely separate array rather than shared references.
for (let i = 0; i < 4; i++) {
Loops over each row of the result matrix.
for (let j = 0; j < 4; j++) {
Loops over each column of the result matrix.
for (let k = 0; k < 4; k++) {
Loops over the shared inner dimension used to compute the dot product between a row of m1 and a column of m2.
result[i][j] += m1[i][k] * m2[k][j];
Accumulates the dot product of row i from m1 and column j from m2 into result[i][j], which is the standard matrix multiplication formula.
return result;
Returns the new combined 4x4 matrix, which when applied to a vertex performs both original rotations in one step.

📦 Key Variables

vertices4D array

Stores the 16 original 4D vertex coordinates ([x,y,z,w] arrays) of the tesseract before any rotation is applied

let vertices4D = [];
edges array

Stores pairs of vertex indices that should be connected by a line, defining the tesseract's wireframe structure

let edges = [];
angleXW number

Tracks the current rotation angle in the x-w plane, incremented every frame to animate the spin

let angleXW = 0;
angleYZ number

Tracks the current rotation angle in the y-z plane, incremented every frame to animate the spin

let angleYZ = 0;
rotationSpeedXW number

Controls how quickly angleXW increases each frame, and is adjustable via mouse drag

let rotationSpeedXW = 0.01;
rotationSpeedYZ number

Controls how quickly angleYZ increases each frame, and is adjustable via mouse drag

let rotationSpeedYZ = 0.015;
autoRotate boolean

Toggles whether the rotation angles update automatically each frame; flipped by clicking the canvas

let autoRotate = true;
hypercubeSize number

Scales the projected 3D coordinates up or down to control the overall display size of the tesseract

let hypercubeSize = 150;
projectionDistance number

Represents the virtual viewer's distance in 4D space, used in the perspective divide that turns 4D points into 3D ones

let projectionDistance = 2;
myFont object

Holds the loaded p5.Font object needed to render text while using the WEBGL renderer

let myFont;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG draw() perspective projection

When projectionDistance minus a vertex's w coordinate approaches zero (e.g. if projectionDistance is set near 1), scaleFactor spikes toward infinity and edges can flash or fly off screen.

💡 Clamp the denominator with something like max(0.1, projectionDistance - v[3]) before dividing, or constrain projectionDistance to a safe minimum in setup().

PERFORMANCE draw()

Two full-precision rotation matrices, a matrix multiplication, and two array-mapping passes over all 16 vertices are recomputed every single frame even when autoRotate is off and nothing has changed.

💡 Cache the rotated/projected vertices and only recompute them when angleXW, angleYZ, hypercubeSize, or projectionDistance actually change.

STYLE keyPressed()

The check `key === 'R' || key === 'r'` works but p5.js also exposes `keyCode`, and comparing case-insensitively via `key.toUpperCase() === 'R'` would be slightly clearer and easier to extend to more keys.

💡 Use `if (key.toUpperCase() === 'R') { ... }` for a more scalable pattern if more keyboard shortcuts are added later.

FEATURE draw() rendering

All edges are drawn with the same uniform color and thickness, so the wireframe doesn't communicate which edges are 'closer' in the fourth dimension.

💡 Color or fade each edge based on the average w-coordinate of its two vertices (e.g. brighter for edges with higher w) to make the 4D depth more visually intuitive.

🔄 Code Flow

Code flow showing preload, setup, draw, mousepressed, mousedragged, keypressed, windowresized, generatehypercube, multiplymatrixvector, multiplymatrices

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> generatehypercube[generatehypercube] generatehypercube --> draw[draw loop] draw --> auto-rotate-update[Auto-Rotate Angle Update] draw --> base-wireframe-loop[Base Wireframe Edge Loop] base-wireframe-loop --> glow-wireframe-loop[Glow Wireframe Edge Loop] draw --> r-key-check[R Key Check] click setup href "#fn-setup" click generatehypercube href "#fn-generatehypercube" click draw href "#fn-draw" click auto-rotate-update href "#sub-auto-rotate-update" click base-wireframe-loop href "#sub-base-wireframe-loop" click glow-wireframe-loop href "#sub-glow-wireframe-loop" click r-key-check href "#sub-r-key-check" auto-rotate-update -->|if enabled| draw auto-rotate-update -->|if disabled| draw base-wireframe-loop -->|for each edge| vertex-generation-loop[Vertex Generation Loop] vertex-generation-loop --> edge-detection-loop[Edge Detection Nested Loop] edge-detection-loop --> base-wireframe-loop glow-wireframe-loop -->|for each edge| glow-wireframe-loop glow-wireframe-loop -->|draw with glow| draw r-key-check -->|if R pressed| draw r-key-check -->|if not R pressed| draw click vertex-generation-loop href "#sub-vertex-generation-loop" click edge-detection-loop href "#sub-edge-detection-loop" draw --> mousedragged[mousedragged] draw --> mousepressed[mousepressed] draw --> keypressed[keypressed] draw --> windowresized[windowresized] click mousedragged href "#fn-mousedragged" click mousepressed href "#fn-mousepressed" click keypressed href "#fn-keypressed" click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual effect does the Tesseract Portal sketch create?

The sketch visually represents a rotating tesseract (4D hypercube) projected into 3D space, featuring glowing wireframe edges that enhance its portal-like appearance.

How can users interact with the Tesseract Portal sketch?

Users can drag to steer the rotation, click to toggle auto-rotation, and press 'R' to randomize the spin of the tesseract.

What creative coding concepts does the Tesseract Portal sketch demonstrate?

This sketch demonstrates 4D rotation projections into 3D space and the use of WebGL for rendering dynamic, interactive visualizations.

Preview

Tesseract Portal — 4D Hypercube Projection - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Tesseract Portal — 4D Hypercube Projection - Code flow showing preload, setup, draw, mousepressed, mousedragged, keypressed, windowresized, generatehypercube, multiplymatrixvector, multiplymatrices
Code Flow Diagram