-/* ===================== SETUP ===================== */
-const game = document.getElementById("game");
-game.width = 800;
-game.height = 800;
-const ctx = game.getContext("2d");
-
-const BACKGROUND = "#101010";
-const FOREGROUND = "#50FF50";
-const TEXT = "#FFFFFF";
-
-const FPS = 60;
-const NEAR = 0.1;
-
-/* ===================== GEOMETRY ===================== */
-const vertices = [
- {x: 0.25, y: 0.25, z: 0.25},
- {x: -0.25, y: 0.25, z: 0.25},
- {x: -0.25, y: -0.25, z: 0.25},
- {x: 0.25, y: -0.25, z: 0.25},
- {x: 0.25, y: 0.25, z: -0.25},
- {x: -0.25, y: 0.25, z: -0.25},
- {x: -0.25, y: -0.25, z: -0.25},
- {x: 0.25, y: -0.25, z: -0.25},
-];
-
-const edges = [
- [0,1,2,3],
- [4,5,6,7],
- [0,4],[1,5],[2,6],[3,7]
-];
-
-/* ===================== OBJECT ===================== */
-let objectPos = { x: 0, y: 0, z: 2 };
-let angle = 0;
-
-const AXIS_MODES = [
- ["x"], ["y"], ["z"],
- ["x","y"], ["y","z"], ["x","z"],
- ["x","y","z"]
-];
-let axisModeIndex = AXIS_MODES.length-1;
-let activeAxes = AXIS_MODES[axisModeIndex];
-
-/* ===================== CAMERA ===================== */
-let camera = {
- position: { x: 0, y: 0, z: 0 },
- rotation: [1,0,0,0],
- speed: 0.05,
- sensitivity: 0.002
-};
-
-/* ===================== INPUT ===================== */
-const keys = {};
-window.addEventListener("keydown", e => keys[e.key.toLowerCase()] = true);
-window.addEventListener("keyup", e => keys[e.key.toLowerCase()] = false);
-
-/* ===================== QUATERNIONS ===================== */
-function quatFromAxisAngle(axis, angle) {
- const h = angle / 2;
- const s = Math.sin(h);
- return [Math.cos(h), axis.x*s, axis.y*s, axis.z*s];
-}
+// Meshes -------------------------------------------------
+const CUBE_MESH = [
+ // front face
+ {x: -1, y: 1, z: -1},
+ {x: 1, y: 1, z: -1},
+ {x: 1, y: -1, z: -1},
+
+ {x: 1, y: -1, z: -1},
+ {x: -1, y: -1, z: -1},
+ {x: -1, y: 1, z: -1},
+
+ // backface
+ {x: -1, y: 1, z: 1},
+ {x: 1, y: 1, z: 1},
+ {x: 1, y: -1, z: 1},
+
+ {x: 1, y: -1, z: 1},
+ {x: -1, y: -1, z: 1},
+ {x: -1, y: 1, z: 1},
+
+
+ // left face
+ {x: -1, y: 1, z: -1},
+ {x: -1, y: 1, z: 1},
+ {x: -1, y: -1, z: 1},
+
+ {x: -1, y: -1, z: 1},
+ {x: -1, y: -1, z: -1},
+ {x: -1, y: 1, z: -1},
+
+ // right face
+ {x: 1, y: 1, z: -1},
+ {x: 1, y: 1, z: 1},
+ {x: 1, y: -1, z: 1},
+
+ {x: 1, y: -1, z: 1},
+ {x: 1, y: -1, z: -1},
+ {x: 1, y: 1, z: -1},
+
+ // top face
+ {x: -1, y: 1, z: -1},
+ {x: 1, y: 1, z: -1},
+ {x: -1, y: 1, z: 1},
+
+ // bottom face
+ {x: -1, y: -1, z: -1},
+ {x: 1, y: -1, z: -1},
+ {x: -1, y: -1, z: 1},
+
+]
+
+const PYRAMID_MESH = [
+ // front face
+ {x: 0, y: 1, z: 0},
+ {x: -1, y: -1, z: 1},
+ {x: 1, y: -1, z: 1},
+
+ // right face
+ {x: 0, y: 1, z: 0},
+ {x: 1, y: -1, z: 1},
+ {x: 1, y: -1, z: -1},
+
+ // back face
+ {x: 0, y: 1, z: 0},
+ {x: 1, y: -1, z: -1},
+ {x: -1, y: -1, z: -1},
+
+ // left face
+ {x: 0, y: 1, z: 0},
+ {x: -1, y: -1, z: -1},
+ {x: -1, y: -1, z: 1},
+
+ // bottom (two triangles)
+ {x: -1, y: -1, z: 1},
+ {x: 1, y: -1, z: 1},
+ {x: 1, y: -1, z: -1},
+
+ {x: 1, y: -1, z: -1},
+ {x: -1, y: -1, z: -1},
+ {x: -1, y: -1, z: 1},
+]
+
+const DIAMOND_MESH = [
+ // top
+ {x: 0,y: 1,z: 0},{x:-1,y: 0,z: 1},{x: 1,y: 0,z: 1},
+ {x: 0,y: 1,z: 0},{x: 1,y: 0,z: 1},{x: 1,y: 0,z:-1},
+ {x: 0,y: 1,z: 0},{x: 1,y: 0,z:-1},{x:-1,y: 0,z:-1},
+ {x: 0,y: 1,z: 0},{x:-1,y: 0,z:-1},{x:-1,y: 0,z: 1},
+
+ // bottom
+ {x: 0,y:-1,z: 0},{x: 1,y: 0,z: 1},{x:-1,y: 0,z: 1},
+ {x: 0,y:-1,z: 0},{x: 1,y: 0,z:-1},{x: 1,y: 0,z: 1},
+ {x: 0,y:-1,z: 0},{x:-1,y: 0,z:-1},{x: 1,y: 0,z:-1},
+ {x: 0,y:-1,z: 0},{x:-1,y: 0,z: 1},{x:-1,y: 0,z:-1},
+]
+
+const TETRAHEDRON_MESH = [
+ // face 1
+ {x: 1, y: 1, z: 1},
+ {x: -1, y: -1, z: 1},
+ {x: -1, y: 1, z: -1},
+
+ // face 2
+ {x: 1, y: 1, z: 1},
+ {x: -1, y: -1, z: 1},
+ {x: 1, y: -1, z: -1},
+
+ // face 3
+ {x: 1, y: 1, z: 1},
+ {x: -1, y: 1, z: -1},
+ {x: 1, y: -1, z: -1},
+
+ // face 4 (base)
+ {x: -1, y: -1, z: 1},
+ {x: -1, y: 1, z: -1},
+ {x: 1, y: -1, z: -1},
+]
+
+const OCTAHEDRON_MESH = [
+ {x: 0,y: 1,z: 0},{x:-1,y: 0,z: 0},{x: 0,y: 0,z: 1},
+ {x: 0,y: 1,z: 0},{x: 0,y: 0,z: 1},{x: 1,y: 0,z: 0},
+ {x: 0,y: 1,z: 0},{x: 1,y: 0,z: 0},{x: 0,y: 0,z:-1},
+ {x: 0,y: 1,z: 0},{x: 0,y: 0,z:-1},{x:-1,y: 0,z: 0},
+
+ {x: 0,y:-1,z: 0},{x: 0,y: 0,z: 1},{x:-1,y: 0,z: 0},
+ {x: 0,y:-1,z: 0},{x: 1,y: 0,z: 0},{x: 0,y: 0,z: 1},
+ {x: 0,y:-1,z: 0},{x: 0,y: 0,z:-1},{x: 1,y: 0,z: 0},
+ {x: 0,y:-1,z: 0},{x:-1,y: 0,z: 0},{x: 0,y: 0,z:-1},
+]
+
+// Global constant variables ------------------------------
+const CTX = game.getContext("2d")
+const VERTSIZE = 10
+const FOREGROUND_COLOR = "#b3ff66"
+const BOUNDRIES_COLOR = "#FFFFFF"
+const TEXT_COLOR = "#FFFFFF"
+const AXIS_MODES = ["none", "yz", "xz", "xy"]
+const KEYS = {};
+const NEAR = 0.1
+const FPS = 60
+const DT = 1/FPS
+
+// Other global variables ----------------------------------
+let meshes = [CUBE_MESH,PYRAMID_MESH, DIAMOND_MESH, TETRAHEDRON_MESH, OCTAHEDRON_MESH]
+let mesh = meshes[0]
+let inObject = false
+let canMoveObject = false
+let angle = Math.PI
+let position = {x: 0, y: 0, z: 8}
+let rotation = "none"
+let mouseDown = false
+let mousePos = {x: 0, y: 0}
+let showCollisionBox = true
+let moveObject = false
+let objectScale = 0.25
+let triangle = []
+let ri = 0
+let mi = 0
+let ti = -1
+
+// Canvas changes ------------------------------------------
+game.width = 800
+game.height = 800
+
+// Controls ------------------------------------------------
+window.addEventListener("keydown", e => {
+ if (KEYS[e.key.toLowerCase()]) return
+ KEYS[e.key.toLowerCase()] = true
+
+ if (KEYS["r"]) {
+ changeRotation()
+ }
+ if (KEYS["d"]) {
+ changeMesh()
+ }
+});
+
+window.addEventListener("keyup", e => KEYS[e.key.toLowerCase()] = false);
+
+game.addEventListener("mousedown", e => {
+ if (e.button === 0) { // left mouse button
+ mouseDown = true
+ }
+})
+
+game.addEventListener("mouseup", e => {
+ if (e.button === 0) {
+ mouseDown = false
+ }
+})
+
+game.addEventListener("mousemove", e => {
+ let m = getMousePos(e)
+ if (canMoveObject) {
+ position.x = (m.x/game.width * 2 - 1) * position.z
+ position.y = ((1 - m.y/game.height) * 2 - 1) * position.z
+ }
+ mousePos.x = m.x;
+ mousePos.y = m.y;
+})
+
+game.addEventListener("wheel", e => {
+ e.preventDefault() // prevent page from scrolling
-function quatMultiply(a, b) {
- const [w1,x1,y1,z1] = a;
- const [w2,x2,y2,z2] = b;
- return [
- w1*w2 - x1*x2 - y1*y2 - z1*z2,
- w1*x2 + x1*w2 + y1*z2 - z1*y2,
- w1*y2 - x1*z2 + y1*w2 + z1*x2,
- w1*z2 + x1*y2 - y1*x2 + z1*w2
- ];
+ if (e.deltaY < 0) {
+ position.z += 0.4
+ } else if (e.deltaY > 0) {
+ position.z -= 0.4
+ }
+})
+
+// Control Functions ---------------------------------------
+function getMousePos(e) {
+ const rect = game.getBoundingClientRect()
+
+ return {
+ x: (e.clientX - rect.left) * (game.width / rect.width),
+ y: (e.clientY - rect.top) * (game.height / rect.height),
+ }
}
-function quatConjugate(q) {
- return [q[0], -q[1], -q[2], -q[3]];
+// Screen drawing functions --------------------------------
+function clear() {
+ CTX.fillStyle = "black"
+ CTX.fillRect(0, 0, game.width, game.height)
+
+ CTX.fillStyle = TEXT_COLOR;
+ CTX.font = "14px monospace";
+ CTX.fillText("press r to change rotation", 10, 18);
+ CTX.fillText("press d to change mesh", 10, 36);
+ CTX.fillText("click and drag to move object" , 10, 54);
+ CTX.fillText("scroll to change objects z position" , 10, 72);
}
-function quatRotateVector(q, v) {
- const qv = [0, v.x, v.y, v.z];
- const r = quatMultiply(quatMultiply(q, qv), quatConjugate(q));
- return { x: r[1], y: r[2], z: r[3] };
+function point({x, y}, color) {
+ CTX.fillStyle = color
+ CTX.fillRect(x - VERTSIZE / 2,y - VERTSIZE/2, VERTSIZE, VERTSIZE)
}
-/* ===================== CAMERA VECTORS ===================== */
-function cameraForward() {
- return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+function line(a, b, color) {
+ CTX.strokeStyle = color
+ CTX.lineWidth = 2
+ CTX.beginPath()
+ CTX.moveTo(a.x , a.y )
+ CTX.lineTo(b.x , b.y)
+ CTX.stroke()
}
-function cameraRight() {
- return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+
+// Mesh functions ------------------------------------------
+function changeMesh() {
+ mesh = meshes[(mi += 1)%meshes.length]
}
-function cameraUp() {
- return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+
+// Rotation functions --------------------------------------
+function rotate({x, y, z}, axis, angle) {
+ // rotating a vector
+ const c = Math.cos(angle)
+ const s = Math.sin(angle)
+ switch (axis) {
+ case "none": return {x: x, y: y, z: z}
+ case "yz": return {x, y:y*c-z*s, z: y*s+z*c} // x-axis
+ case "xz": return {x: x*c-z*s, y, z: x*s+z*c} // y-axis
+ case "xy": return {x: x*c-y*s, y: x*s+y*c, z} // z-axis
+ }
}
-function applyCamera(p) {
- const v = {
- x: p.x - camera.position.x,
- y: p.y - camera.position.y,
- z: p.z - camera.position.z
- };
- return quatRotateVector(quatConjugate(camera.rotation), v);
+function changeRotation() {
+ rotation = AXIS_MODES[(ri += 1)%AXIS_MODES.length]
}
-/* ===================== PROJECTION ===================== */
-function project(p) {
- return { x: p.x / p.z, y: p.y / p.z };
+// Graphics pipeline functions -----------------------------
+function project({ x, y, z }) {
+ const eps = 0.001
+ return { x: x / Math.max(z, eps), y: y / Math.max(z, eps), z }
}
-function screen(p) {
+
+function screen({x, y}) {
+ // -1 .. 1 -> 0 .. w/h
return {
- x: (p.x + 1) * 0.5 * game.width,
- y: (1 - (p.y + 1) * 0.5) * game.height
- };
+ x: ((x + 1) * 0.5 * game.width) ,
+ y: ((1 - (y + 1) * 0.5) * game.width),
+ }
}
-/* ===================== DRAW ===================== */
-function clear() {
- ctx.fillStyle = BACKGROUND;
- ctx.fillRect(0,0,game.width,game.height);
-
- ctx.fillStyle = TEXT;
- ctx.font = "14px monospace";
- ctx.fillText("WASD: move | QE: up/down", 10, 18);
- ctx.fillText("Mouse: look | Click canvas to lock", 10, 36);
- ctx.fillText("Click canvas again: change cube axes", 10, 54);
- ctx.fillText("Cube axes: " + activeAxes.join(","), 10, 72);
-}
+function makeTriangles (vert, color) {
+ ti = (ti + 1) % 3
+ triangle.push(vert)
-function line(a,b) {
- ctx.strokeStyle = FOREGROUND;
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(a.x,a.y);
- ctx.lineTo(b.x,b.y);
- ctx.stroke();
+ if (ti != 2) return
+ for (let ti = 0; ti < 3; ++ti) {
+ let a = triangle[ti]
+ let b = triangle[(ti+1)%3]
+ // if (a.z <= NEAR || b.z <= NEAR) continue
+ line(a, b, color)
+ }
+ triangle = []
}
-/* ===================== LOOP ===================== */
-function frame() {
- angle += Math.PI / FPS;
-
- // --- cube rotation quaternion ---
- let q = [1,0,0,0];
- for (const ax of activeAxes) {
- if (ax==="x") q = quatMultiply(quatFromAxisAngle({x:1,y:0,z:0}, angle), q);
- if (ax==="y") q = quatMultiply(quatFromAxisAngle({x:0,y:1,z:0}, angle), q);
- if (ax==="z") q = quatMultiply(quatFromAxisAngle({x:0,y:0,z:1}, angle), q);
+function translate(p1, p2) {
+ return {
+ x: p1.x + p2.x,
+ y: p1.y + p2.y,
+ z: p1.z + p2.z
}
+}
- // --- camera movement ---
- if (keys["w"]) {
- const f = cameraForward();
- camera.position.x += f.x * camera.speed;
- camera.position.y += f.y * camera.speed;
- camera.position.z += f.z * camera.speed;
- }
- if (keys["s"]) {
- const f = cameraForward();
- camera.position.x -= f.x * camera.speed;
- camera.position.y -= f.y * camera.speed;
- camera.position.z -= f.z * camera.speed;
- }
- if (keys["a"]) {
- const r = cameraRight();
- camera.position.x -= r.x * camera.speed;
- camera.position.y -= r.y * camera.speed;
- camera.position.z -= r.z * camera.speed;
- }
- if (keys["d"]) {
- const r = cameraRight();
- camera.position.x += r.x * camera.speed;
- camera.position.y += r.y * camera.speed;
- camera.position.z += r.z * camera.speed;
- }
- if (keys["q"]) {
- const u = cameraUp();
- camera.position.x -= u.x * camera.speed;
- camera.position.y -= u.y * camera.speed;
- camera.position.z -= u.z * camera.speed;
- }
- if (keys["e"]) {
- const u = cameraUp();
- camera.position.x += u.x * camera.speed;
- camera.position.y += u.y * camera.speed;
- camera.position.z += u.z * camera.speed;
+// Bounds checking functions -------------------------------
+function computeBounds(mesh) {
+ let min = {x: Infinity, y: Infinity}
+ let max = {x: -Infinity, y: -Infinity}
+
+ for (const v of mesh) {
+ min.x = Math.min(min.x, v.x)
+ min.y = Math.min(min.y, v.y)
+
+ max.x = Math.max(max.x, v.x)
+ max.y = Math.max(max.y, v.y)
}
+ return {min, max}
+}
- clear();
+function boundingBoxCorners({min, max}) {
+ return {
+ bottom: {left: {x: min.x, y: max.y}, right: {x: max.x, y: max.y}},
+ top: {left: {x: min.x, y: min.y}, right: {x: max.x, y: min.y}},
- for (const e of edges) {
- for (let i=0;i<e.length;i++) {
- let a = vertices[e[i]];
- let b = vertices[e[(i+1)%e.length]];
+ }
+}
- a = quatRotateVector(q, a);
- b = quatRotateVector(q, b);
+// Game loop ----------------------------------------------
+function frame() {
+ clear()
+ angle += Math.PI * DT
+ let translated_mesh = []
- a = { x:a.x+objectPos.x, y:a.y+objectPos.y, z:a.z+objectPos.z };
- b = { x:b.x+objectPos.x, y:b.y+objectPos.y, z:b.z+objectPos.z };
+ for (let i = 0; i < mesh.length; ++i) {
+ vert = mesh[i]
- a = applyCamera(a);
- b = applyCamera(b);
+ const translated = translate(rotate(vert, rotation, angle), position)
+ const projected = project(translated)
+ let obj = screen(projected)
+ translated_mesh.push(obj)
- if (a.z<=NEAR || b.z<=NEAR) continue;
- line(screen(project(a)), screen(project(b)));
- }
+ // point(obj, FOREGROUND_COLOR)
+ makeTriangles(obj, FOREGROUND_COLOR)
+
+ }
+ const bounds = computeBounds(translated_mesh)
+ const rect = boundingBoxCorners(bounds)
+
+ if (showCollisionBox) {
+ line(rect.bottom.left, rect.bottom.right, BOUNDRIES_COLOR)
+ line(rect.top.left, rect.top.right, BOUNDRIES_COLOR)
+ line(rect.bottom.left, rect.top.left, BOUNDRIES_COLOR)
+ line(rect.bottom.right, rect.top.right, BOUNDRIES_COLOR)
}
- setTimeout(frame, 1000/FPS);
-}
-frame();
+ inObject = (
+ mousePos.x > rect.top.left.x
+ &&
+ mousePos.x < rect.top.right.x
+ &&
+ mousePos.y > rect.top.left.y
+ &&
+ mousePos.y < rect.bottom.right.y
+ )
-/* ===================== INTERACTION ===================== */
-game.onclick = () => {
- game.requestPointerLock();
- axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length;
- activeAxes = AXIS_MODES[axisModeIndex];
-};
+ if (mouseDown && inObject) canMoveObject = true
-window.addEventListener("mousemove", e => {
- if (document.pointerLockElement !== game) return;
+ if (!mouseDown) canMoveObject = false
- const yaw = quatFromAxisAngle({x:0,y:1,z:0}, -e.movementX * camera.sensitivity);
- const pitch = quatFromAxisAngle({x:1,y:0,z:0}, -e.movementY * camera.sensitivity);
+ setTimeout(frame, 1000/FPS)
+}
- camera.rotation = quatMultiply(yaw, camera.rotation);
- camera.rotation = quatMultiply(camera.rotation, pitch);
-});
+frame()