Created
January 23, 2025 23:22
-
-
Save henrycunh/d56f03ace35c96b87c3bcc7624efaa46 to your computer and use it in GitHub Desktop.
ball thingy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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