Skip to content

Instantly share code, notes, and snippets.

@henrycunh
Created January 23, 2025 23:22
Show Gist options
  • Save henrycunh/d56f03ace35c96b87c3bcc7624efaa46 to your computer and use it in GitHub Desktop.
Save henrycunh/d56f03ace35c96b87c3bcc7624efaa46 to your computer and use it in GitHub Desktop.
ball thingy
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Rotating 4D Tesseract with Bouncing Ball</title>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: #111;
height: 100%;
}
canvas {
display: block;
background: #111;
}
</style>
</head>
<body>
<canvas id="tesseractCanvas"></canvas>
<script>
// ============= Configurable Parameters =============
const CANVAS_ID = "tesseractCanvas";
const EDGE_COLOR = "#CCCCCC";
const FACE_GLOW_COLOR = "rgba(255, 255, 0, "; // alpha appended later
const BALL_COLOR = "red";
const BALL_RADIUS_4D = 0.15; // Radius of the ball in 4D space
const ROTATION_SPEED_XW = 0.01; // Speed of rotation in x-w plane
const ROTATION_SPEED_YW = 0.013; // Speed of rotation in y-w plane
const ROTATION_SPEED_ZW = 0.017; // Speed of rotation in z-w plane
const HIGHLIGHT_DECAY = 0.92; // Glow decay factor per frame
const TESSERACT_BOUND = 1; // Half-size of the tesseract in each dimension
// For the 4D->3D perspective
const W_CAMERA_OFFSET = 2.5; // Camera offset in w-dimension
// For the 3D->2D perspective
const FOV = 3.0; // Higher means more perspective
const VIEW_SIZE = 600; // Pixel scaling factor
// ============= Canvas and Context Setup =============
const canvas = document.getElementById(CANVAS_ID);
const ctx = canvas.getContext("2d");
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
// ============= Define the Tesseract Vertices =============
// A tesseract has 16 vertices, each coordinate is ±1
const vertices4D = [];
for (let x of [-TESSERACT_BOUND, TESSERACT_BOUND]) {
for (let y of [-TESSERACT_BOUND, TESSERACT_BOUND]) {
for (let z of [-TESSERACT_BOUND, TESSERACT_BOUND]) {
for (let w of [-TESSERACT_BOUND, TESSERACT_BOUND]) {
vertices4D.push([x, y, z, w]);
}
}
}
}
// ============= Define the Edges =============
// Two vertices share an edge if they differ in exactly one coordinate
const edges = [];
for (let i = 0; i < vertices4D.length; i++) {
for (let j = i + 1; j < vertices4D.length; j++) {
const v1 = vertices4D[i], v2 = vertices4D[j];
// Count coordinate differences
let diffCount = 0;
for (let k = 0; k < 4; k++) {
if (v1[k] !== v2[k]) diffCount++;
}
if (diffCount === 1) {
edges.push([i, j]);
}
}
}
// ============= Faces (bounding planes) for Glow =============
// We'll maintain an alpha glow for each of the 8 bounding planes.
const faceGlow = {
"x=+1": 0,
"x=-1": 0,
"y=+1": 0,
"y=-1": 0,
"z=+1": 0,
"z=-1": 0,
"w=+1": 0,
"w=-1": 0,
};
// Precompute which edges belong to which bounding plane face
// E.g., the face "x=+1" includes edges whose both endpoints have x=+1.
const faceEdges = {
"x=+1": [],
"x=-1": [],
"y=+1": [],
"y=-1": [],
"z=+1": [],
"z=-1": [],
"w=+1": [],
"w=-1": []
};
function approxEquals(a, b) {
// Because of floating precision, we can check with a small epsilon
return Math.abs(a - b) < 1e-9;
}
// Map each vertex to its 4D coordinate for easy checks:
function belongsToFace(vertex, faceLabel) {
// faceLabel is something like "x=+1"
const [dim, sign] = faceLabel.split("=");
const axis = dim; // 'x','y','z','w'
const targetVal = parseFloat(sign);
let idx = 0;
if (axis === 'x') idx = 0;
if (axis === 'y') idx = 1;
if (axis === 'z') idx = 2;
if (axis === 'w') idx = 3;
return approxEquals(vertex[idx], targetVal);
}
// Precompute edges for each face
Object.keys(faceEdges).forEach(faceLabel => {
for (let [i, j] of edges) {
const v1 = vertices4D[i], v2 = vertices4D[j];
if (belongsToFace(v1, faceLabel) && belongsToFace(v2, faceLabel)) {
faceEdges[faceLabel].push([i, j]);
}
}
});
// ============= Ball State =============
let ball4D = {
x: 0,
y: 0,
z: 0,
w: 0
};
let ballVel = {
x: 0.01,
y: 0.015,
z: 0.012,
w: 0.017
};
// ============= Rotation Angles =============
let angleXW = 0;
let angleYW = 0;
let angleZW = 0;
// ============= 4D Rotation Functions =============
// Rotate in x-w plane by 'theta'
function rotateXW(vec, theta) {
const [x, y, z, w] = vec;
const cosT = Math.cos(theta);
const sinT = Math.sin(theta);
// (x, w) -> (x*cosT - w*sinT, x*sinT + w*cosT)
return [
x * cosT - w * sinT,
y,
z,
x * sinT + w * cosT
];
}
// Rotate in y-w plane by 'theta'
function rotateYW(vec, theta) {
const [x, y, z, w] = vec;
const cosT = Math.cos(theta);
const sinT = Math.sin(theta);
// (y, w) -> (y*cosT - w*sinT, y*sinT + w*cosT)
return [
x,
y * cosT - w * sinT,
z,
y * sinT + w * cosT
];
}
// Rotate in z-w plane by 'theta'
function rotateZW(vec, theta) {
const [x, y, z, w] = vec;
const cosT = Math.cos(theta);
const sinT = Math.sin(theta);
// (z, w) -> (z*cosT - w*sinT, z*sinT + w*cosT)
return [
x,
y,
z * cosT - w * sinT,
z * sinT + w * cosT
];
}
// Apply all rotations in order
function rotate4D(vec) {
let v = rotateXW(vec, angleXW);
v = rotateYW(v, angleYW);
v = rotateZW(v, angleZW);
return v;
}
// ============= Projection Functions =============
// 4D -> 3D perspective (using w)
function project4Dto3D(vec4) {
const [x, y, z, w] = vec4;
// We treat w as a dimension that affects scaling in 3D
// A simple perspective approach: scale by 1 / (W_CAMERA_OFFSET - w)
const denom = (W_CAMERA_OFFSET - w);
const scale = (denom === 0) ? 1e6 : 1 / denom; // avoid divide by zero
return [
x * scale,
y * scale,
z * scale
];
}
// 3D -> 2D perspective for final canvas coordinates
function project3Dto2D(vec3) {
const [x, y, z] = vec3;
// Another perspective factor: scale by FOV/(FOV - z)
const denom = (FOV - z);
const scale = (denom === 0) ? 1e6 : FOV / denom;
const screenX = x * scale * VIEW_SIZE + canvas.width / 2;
const screenY = -y * scale * VIEW_SIZE + canvas.height / 2;
// (Note the minus on y so that +y is upward on the canvas)
return [screenX, screenY, scale];
}
// Helper to get final 2D projection from 4D vector
function project4Dto2D(vec4) {
const vec3 = project4Dto3D(vec4);
return project3Dto2D(vec3);
}
// ============= Animation Loop =============
function animate() {
// 1. Update rotation angles
angleXW += ROTATION_SPEED_XW;
angleYW += ROTATION_SPEED_YW;
angleZW += ROTATION_SPEED_ZW;
// 2. Update ball position in 4D
ball4D.x += ballVel.x;
ball4D.y += ballVel.y;
ball4D.z += ballVel.z;
ball4D.w += ballVel.w;
// 3. Check collisions with tesseract bounding planes ±1
// We compare each coordinate, factoring in BALL_RADIUS_4D
// X collisions
if (ball4D.x > TESSERACT_BOUND - BALL_RADIUS_4D) {
ball4D.x = TESSERACT_BOUND - BALL_RADIUS_4D;
ballVel.x *= -1;
faceGlow["x=+1"] = 1.0;
} else if (ball4D.x < -TESSERACT_BOUND + BALL_RADIUS_4D) {
ball4D.x = -TESSERACT_BOUND + BALL_RADIUS_4D;
ballVel.x *= -1;
faceGlow["x=-1"] = 1.0;
}
// Y collisions
if (ball4D.y > TESSERACT_BOUND - BALL_RADIUS_4D) {
ball4D.y = TESSERACT_BOUND - BALL_RADIUS_4D;
ballVel.y *= -1;
faceGlow["y=+1"] = 1.0;
} else if (ball4D.y < -TESSERACT_BOUND + BALL_RADIUS_4D) {
ball4D.y = -TESSERACT_BOUND + BALL_RADIUS_4D;
ballVel.y *= -1;
faceGlow["y=-1"] = 1.0;
}
// Z collisions
if (ball4D.z > TESSERACT_BOUND - BALL_RADIUS_4D) {
ball4D.z = TESSERACT_BOUND - BALL_RADIUS_4D;
ballVel.z *= -1;
faceGlow["z=+1"] = 1.0;
} else if (ball4D.z < -TESSERACT_BOUND + BALL_RADIUS_4D) {
ball4D.z = -TESSERACT_BOUND + BALL_RADIUS_4D;
ballVel.z *= -1;
faceGlow["z=-1"] = 1.0;
}
// W collisions
if (ball4D.w > TESSERACT_BOUND - BALL_RADIUS_4D) {
ball4D.w = TESSERACT_BOUND - BALL_RADIUS_4D;
ballVel.w *= -1;
faceGlow["w=+1"] = 1.0;
} else if (ball4D.w < -TESSERACT_BOUND + BALL_RADIUS_4D) {
ball4D.w = -TESSERACT_BOUND + BALL_RADIUS_4D;
ballVel.w *= -1;
faceGlow["w=-1"] = 1.0;
}
// 4. Draw
drawScene();
// 5. Request next frame
requestAnimationFrame(animate);
}
// ============= Draw Scene =============
function drawScene() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Rotate and project all vertices
const projectedPoints = [];
for (let i = 0; i < vertices4D.length; i++) {
const rotated = rotate4D(vertices4D[i]);
const projected = project4Dto2D(rotated);
projectedPoints.push(projected);
}
// Draw all edges in base color (slightly darker)
ctx.strokeStyle = EDGE_COLOR;
ctx.lineWidth = 1;
ctx.beginPath();
edges.forEach(([i, j]) => {
const [sx1, sy1] = projectedPoints[i];
const [sx2, sy2] = projectedPoints[j];
ctx.moveTo(sx1, sy1);
ctx.lineTo(sx2, sy2);
});
ctx.stroke();
// Draw highlighted faces (their edges) if glow alpha > 0
Object.keys(faceGlow).forEach(faceLabel => {
const alpha = faceGlow[faceLabel];
if (alpha > 0.01) {
ctx.strokeStyle = FACE_GLOW_COLOR + alpha + ")";
ctx.lineWidth = 2;
ctx.beginPath();
faceEdges[faceLabel].forEach(([i, j]) => {
const [sx1, sy1] = projectedPoints[i];
const [sx2, sy2] = projectedPoints[j];
ctx.moveTo(sx1, sy1);
ctx.lineTo(sx2, sy2);
});
ctx.stroke();
// Decay alpha
faceGlow[faceLabel] *= HIGHLIGHT_DECAY;
}
});
// Draw the ball
// First, rotate and project the ball's 4D position
const ballRotated = rotate4D([ball4D.x, ball4D.y, ball4D.z, ball4D.w]);
const [bx, by, bScale] = project4Dto2D(ballRotated);
// We use bScale to approximate how big the ball should appear
// (this is not a perfectly realistic 4D->2D size, but a decent effect)
const ballRadiusOnScreen = BALL_RADIUS_4D * bScale * VIEW_SIZE;
ctx.fillStyle = BALL_COLOR;
ctx.beginPath();
ctx.arc(bx, by, Math.max(1, ballRadiusOnScreen), 0, 2 * Math.PI);
ctx.fill();
}
// Start the animation
animate();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Tesseract with Bouncing Ball (All 6 Planes)</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #000;
overflow: hidden;
}
canvas {
display: block;
background: #000;
}
</style>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script>
(function() {
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// Resize canvas to full window
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// -----------------------------
// 1. Define 4D Tesseract Data
// -----------------------------
// All vertices of a 4D hypercube (tesseract) in local space: each coordinate is ±1.
// There are 16 vertices total (2^4).
const vertices4D = [];
for (let x = -1; x <= 1; x += 2) {
for (let y = -1; y <= 1; y += 2) {
for (let z = -1; z <= 1; z += 2) {
for (let w = -1; w <= 1; w += 2) {
vertices4D.push([x, y, z, w]);
}
}
}
}
// Edges of the tesseract: connect vertices that differ by exactly one coordinate.
const edges = [];
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]);
}
}
}
// Each of the 8 bounding "faces" in 4D (x=±1, y=±1, z=±1, w=±1).
// We'll store which edges belong to each face, so we can "glow" those edges.
const faceEdges = {
xPlus: [], xMinus: [],
yPlus: [], yMinus: [],
zPlus: [], zMinus: [],
wPlus: [], wMinus: []
};
// Helper: checks if a vertex belongs to faceName, e.g. "xPlus" => x=+1.
function belongsToFace(vertex, faceName) {
const [x, y, z, w] = vertex;
switch(faceName) {
case "xPlus": return x === 1;
case "xMinus": return x === -1;
case "yPlus": return y === 1;
case "yMinus": return y === -1;
case "zPlus": return z === 1;
case "zMinus": return z === -1;
case "wPlus": return w === 1;
case "wMinus": return w === -1;
default: return false;
}
}
// Build faceEdges by checking edges whose both endpoints belong to a given face.
const faceNames = ["xPlus","xMinus","yPlus","yMinus","zPlus","zMinus","wPlus","wMinus"];
for (let f of faceNames) {
for (let e = 0; e < edges.length; e++) {
const [i, j] = edges[e];
if (belongsToFace(vertices4D[i], f) && belongsToFace(vertices4D[j], f)) {
faceEdges[f].push(e);
}
}
}
// Glow amounts for each face (0 to 1)
const collisionGlow = {
xPlus: 0, xMinus: 0,
yPlus: 0, yMinus: 0,
zPlus: 0, zMinus: 0,
wPlus: 0, wMinus: 0
};
// ---------------------------------
// 2. Ball Physics in 4D
// ---------------------------------
// We'll keep the tesseract in local coordinate space, with the ball
// bouncing in the [-1,1] range in each dimension.
const ball = {
x: 0,
y: 0,
z: 0,
w: 0,
vx: 0.015,
vy: 0.01,
vz: 0.012,
vw: 0.008,
radius: 0.2
};
// ---------------------------------
// 3. 4D Rotation in All 6 Planes
// ---------------------------------
// We have 6 independent planes in 4D: XY, XZ, XW, YZ, YW, ZW
let angleXY = 0;
let angleXZ = 0;
let angleXW = 0;
let angleYZ = 0;
let angleYW = 0;
let angleZW = 0;
// Speeds for each plane
const speedXY = 0.008;
const speedXZ = 0.003;
const speedXW = 0.004;
const speedYZ = 0.005;
const speedYW = 0.003;
const speedZW = 0.002;
// Rotate a point in plane (a,b) by theta in 4D
function rotatePlane(point, a, b, theta) {
const c = Math.cos(theta);
const s = Math.sin(theta);
const pa = point[a];
const pb = point[b];
point[a] = pa * c - pb * s;
point[b] = pa * s + pb * c;
}
// Update angles each frame
function updateRotationAngles() {
angleXY += speedXY;
angleXZ += speedXZ;
angleXW += speedXW;
angleYZ += speedYZ;
angleYW += speedYW;
angleZW += speedZW;
}
// Apply all 6 plane rotations to a single [x,y,z,w] point
function apply4DRotations(point) {
rotatePlane(point, 0, 1, angleXY); // XY
rotatePlane(point, 0, 2, angleXZ); // XZ
rotatePlane(point, 0, 3, angleXW); // XW
rotatePlane(point, 1, 2, angleYZ); // YZ
rotatePlane(point, 1, 3, angleYW); // YW
rotatePlane(point, 2, 3, angleZW); // ZW
}
// ---------------------------------
// 4. Projection from 4D -> 3D -> 2D
// ---------------------------------
// "4D camera" distance
const dist4D = 3.0;
// "3D camera" distance
const cameraDist = 4.0;
// ---------------------------------
// 5. Animation Loop
// ---------------------------------
function animate() {
requestAnimationFrame(animate);
// 1) Update ball physics in 4D
ball.x += ball.vx;
ball.y += ball.vy;
ball.z += ball.vz;
ball.w += ball.vw;
// Check collisions with ±1 in each dimension
checkCollision("xPlus", ball.x, +1);
checkCollision("xMinus", ball.x, -1);
checkCollision("yPlus", ball.y, +1);
checkCollision("yMinus", ball.y, -1);
checkCollision("zPlus", ball.z, +1);
checkCollision("zMinus", ball.z, -1);
checkCollision("wPlus", ball.w, +1);
checkCollision("wMinus", ball.w, -1);
// 2) Update glow fade
for (let f of faceNames) {
collisionGlow[f] = Math.max(0, collisionGlow[f] - 0.02);
}
// 3) Update tesseract rotation angles
updateRotationAngles();
// 4) Clear the screen
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 5) Project and draw everything
const projectedPoints = projectTesseract();
drawEdges(projectedPoints);
drawBall();
}
// Start animation
animate();
// ---------------------------------
// 6. Helper Functions
// ---------------------------------
// Check collision with face = ±1 in a particular dimension
function checkCollision(faceName, coord, limit) {
if (limit > 0) {
// +1 face
if (coord > limit - ball.radius) {
if (faceName === "xPlus") ball.vx = -Math.abs(ball.vx);
if (faceName === "yPlus") ball.vy = -Math.abs(ball.vy);
if (faceName === "zPlus") ball.vz = -Math.abs(ball.vz);
if (faceName === "wPlus") ball.vw = -Math.abs(ball.vw);
collisionGlow[faceName] = 1.0; // trigger glow
}
} else {
// -1 face
if (coord < limit + ball.radius) {
if (faceName === "xMinus") ball.vx = +Math.abs(ball.vx);
if (faceName === "yMinus") ball.vy = +Math.abs(ball.vy);
if (faceName === "zMinus") ball.vz = +Math.abs(ball.vz);
if (faceName === "wMinus") ball.vw = +Math.abs(ball.vw);
collisionGlow[faceName] = 1.0; // trigger glow
}
}
}
// Rotate the entire tesseract in all 6 planes, then project each vertex from 4D to 2D
function projectTesseract() {
const points2D = [];
for (let i = 0; i < vertices4D.length; i++) {
// Clone
const p = [...vertices4D[i]];
// Apply 4D rotations
apply4DRotations(p);
// 4D -> 3D perspective
let wFactor = (dist4D - p[3]);
if (Math.abs(wFactor) < 0.001) wFactor = 0.001; // avoid near-zero division
const scale4D = 1 / wFactor;
let x = p[0] * scale4D;
let y = p[1] * scale4D;
let z = p[2] * scale4D;
// 3D -> 2D perspective
let zFactor = (cameraDist - z);
if (Math.abs(zFactor) < 0.001) zFactor = 0.001;
const scale3D = cameraDist / zFactor;
const X = x * scale3D;
const Y = y * scale3D;
// Convert to canvas coords
const screenX = X * 100 + canvas.width / 2;
const screenY = Y * 100 + canvas.height / 2;
points2D.push([screenX, screenY]);
}
return points2D;
}
// Draw all edges, applying glow based on collisions
function drawEdges(projectedPoints) {
ctx.lineWidth = 2;
for (let e = 0; e < edges.length; e++) {
// Base color
let r = 200, g = 200, b = 200;
// Check if this edge belongs to any glowing face
for (let f of faceNames) {
if (faceEdges[f].includes(e)) {
const glow = collisionGlow[f];
if (glow > 0) {
const faceColor = colorForFace(f);
// Blend base color with face color
r = r * (1 - glow) + faceColor[0] * glow;
g = g * (1 - glow) + faceColor[1] * glow;
b = b * (1 - glow) + faceColor[2] * glow;
}
}
}
const [i, j] = edges[e];
const [x1, y1] = projectedPoints[i];
const [x2, y2] = projectedPoints[j];
ctx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
}
// Simple face color assignment
function colorForFace(faceName) {
switch (faceName) {
case "xPlus": return [255, 80, 80];
case "xMinus": return [255, 80, 80];
case "yPlus": return [ 80, 255, 80];
case "yMinus": return [ 80, 255, 80];
case "zPlus": return [ 80, 200, 255];
case "zMinus": return [ 80, 200, 255];
case "wPlus": return [255, 180, 80];
case "wMinus": return [255, 180, 80];
default: return [255, 255, 255];
}
}
// Draw the ball in the same 4D-rotated and projected space
function drawBall() {
let p = [ball.x, ball.y, ball.z, ball.w];
// apply the 4D rotations
apply4DRotations(p);
// 4D->3D
let wFactor = (dist4D - p[3]);
if (Math.abs(wFactor) < 0.001) wFactor = 0.001;
const scale4D = 1 / wFactor;
let bx = p[0] * scale4D;
let by = p[1] * scale4D;
let bz = p[2] * scale4D;
// 3D->2D
let zFactor = (cameraDist - bz);
if (Math.abs(zFactor) < 0.001) zFactor = 0.001;
const scale3D = cameraDist / zFactor;
const X = bx * scale3D;
const Y = by * scale3D;
const screenX = X * 100 + canvas.width / 2;
const screenY = Y * 100 + canvas.height / 2;
// scale the ball's radius in perspective
const r = ball.radius * scale4D * scale3D * 100;
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(screenX, screenY, Math.max(2, r), 0, Math.PI * 2);
ctx.fill();
}
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment