]> nyquistdev.net Git - basic-3d-example.git/commitdiff
first commit main
authorA-Larsen <austin.larsen@mailfence.com>
Thu, 15 Oct 2026 01:17:31 +0000 (21:17 -0400)
committerA-Larsen <austin.larsen@mailfence.com>
Thu, 15 Oct 2026 01:17:31 +0000 (21:17 -0400)
docinfo.conf
index.js

index fc4596cf4a3a8b88318a6eb579d4261dc02299ee..57fa5b90f8b0b0d4ab73553141e764c824f37346 100644 (file)
@@ -1,3 +1,3 @@
 #!/usr/bin/env bash
 HOSTNAME=nyquistdev.net
-DOCINFODIR=/usr/local/share/asciidoctor-styles/dark1/
+DOCINFODIR=/usr/local/share/readme-to-html
index 8bfa57771ff7d36dec06b5eab7e6a54bf4645e71..17f28704b3424b7c205831d93befbaa10666f85c 100644 (file)
--- a/index.js
+++ b/index.js
-const canvas = document.getElementById("game");
-const ctx = canvas.getContext("2d")
 
-const BACKGROUND = "#000000";
-const FOREGROUND = "#00FF00";
+/* ===================== 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},
+];
 
-// let debug = true;
-let debug = false;
-
-let debugColors = [
-    "#FF0000",
-    "#00FF00",
-    "#0000FF",
-]
-
-const highContrastColors = [
-  "#FFFF00", // 1. Bright Yellow
-  "#FF00FF", // 2. Magenta
-  "#00FFFF", // 3. Cyan / Aqua
-  "#FFA500", // 4. Pure Orange
-  "#800080", // 5. Deep Purple
-  "#008080", // 6. Teal
-  "#A52A2A", // 7. Brown
-  "#800000", // 8. Maroon
-  "#008000", // 9. Forest Green
-  "#000080", // 10. Navy Blue
-  "#808000", // 11. Olive
-  "#C71585", // 12. Hot Pink
-  "#FFC0CB", // 13. Light Pink
-  "#7FFFD4", // 14. Aquamarine
-  "#FFE4E1", // 15. Misty Rose
-  "#F0E68C", // 16. Khaki
-  "#E6E6FA", // 17. Lavender
-  "#B0C4DE", // 18. Light Steel Blue
-  "#FF4500", // 19. Red-Orange
-  "#1E90FF", // 20. Dodger Blue
-  "#FFD700", // 21. Gold
-  "#9370DB", // 22. Medium Purple
-  "#8B008B", // 23. Dark Magenta
-  "#556B2F", // 24. Dark Olive Green
-  "#FF8C00", // 25. Dark Orange
-  "#48D1CC", // 26. Medium Turquoise
-  "#32CD32", // 27. Bright Green
-  "#2F4F4F", // 28. Dark Slate Gray
-  "#98FF98", // 29. Mint Green
-  "#FF7F50", // 30. Coral
-  "#4B0082", // 31. Indigo
-  "#D2B48C", // 32. Tan / Sand
-  "#FF7F55", // 30. Coral
+const edges = [
+    [0,1,2,3],
+    [4,5,6,7],
+    [0,4],[1,5],[2,6],[3,7]
 ];
 
-canvas.width = 800;
-canvas.height = 800;
+/* ===================== OBJECT ===================== */
+let objectPos = { x: 0, y: 0, z: 2 };
+let angle = 0;
 
-function clear() {
-    ctx.fillStyle = BACKGROUND;
-    ctx.fillRect(0, 0, canvas.width, canvas.height);
+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];
 }
 
-function line(p1, p2) {
-    ctx.lineWidth = 8;
-    if (!debug) ctx.strokeStyle = FOREGROUND;
-    ctx.beginPath();
-    ctx.moveTo(p1.x, p1.y);
-    ctx.lineTo(p2.x, p2.y);
-    ctx.stroke();
+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
+    ];
 }
 
-function getMagnitude(a, b) {
-    let x = (b.x - a.x);
-    let y = (b.y - a.y);
-    return Math.sqrt(x * x + y * y);
+function quatConjugate(q) {
+    return [q[0], -q[1], -q[2], -q[3]];
 }
 
-function interpolate(a, b, y) {
-    let startingPos = a.x;
-    let incX = (b.x - a.x) / (b.y - a.y);
-    let numberOfyIncs = y - a.y;
-    return startingPos + incX * numberOfyIncs;
+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 drawTriangle(coords, color) {
-    coords.sort((a,b) => a.y - b.y);
-
-    let blueisSteeperThanRed = 
-        (coords[2].y - coords[0].y) * (coords[1].x - coords[0].x) >
-        (coords[1].y - coords[0].y) * (coords[2].x - coords[0].x)
+/* ===================== CAMERA VECTORS ===================== */
+function cameraForward() {
+    return quatRotateVector(camera.rotation, {x:0,y:0,z:1});
+}
+function cameraRight() {
+    return quatRotateVector(camera.rotation, {x:1,y:0,z:0});
+}
+function cameraUp() {
+    return quatRotateVector(camera.rotation, {x:0,y:1,z:0});
+}
 
-    for (let y = coords[0].y; y <= coords[2].y; ++y) {
+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);
+}
 
-        let x1 = interpolate(coords[0], coords[2], y);
+/* ===================== PROJECTION ===================== */
+function project(p) {
+    return { x: p.x / p.z, y: p.y / p.z };
+}
+function screen(p) {
+    return {
+        x: (p.x + 1) * 0.5 * game.width,
+        y: (1 - (p.y + 1) * 0.5) * game.height
+    };
+}
 
-        let x2 = (y < coords[1].y) ? 
-            interpolate(coords[0], coords[1], y) :
-            interpolate(coords[1], coords[2], y);
+/* ===================== 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);
+}
 
-        let fill = (i, y) => {
-            ctx.fillStyle = color;
-            ctx.fillRect(i, y, 2, 2);
-        }
+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 (blueisSteeperThanRed) {
-            for (let i = x1; i < x2; ++i) fill(i, y);
-        } else {
-            for (let i = x1; i > x2; --i) fill(i, y);
+/* ===================== 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);
     }
 
-    if (debug) {
-        for (const i in coords) {
-            ctx.strokeStyle = debugColors[i];
-            let a = coords[i];
-            let b = coords[ (i + 1) % coords.length];
-            line(a, b);
-        }
-
+    // --- 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;
     }
 
+    clear();
 
-}
+    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);
 
-clear();
-let moveX = 0;
-
-let triangleCoords = [
-  // --- CENTER CRADLE (Core focal points) ---
-  [{x: 400, y: 400}, {x: 300, y: 300}, {x: 500, y: 250}], // High acute top wedge
-  [{x: 400, y: 400}, {x: 500, y: 250}, {x: 550, y: 420}], // Narrow vertical sliver
-  [{x: 400, y: 400}, {x: 550, y: 420}, {x: 480, y: 550}], // Rotated downward blade
-  [{x: 400, y: 400}, {x: 480, y: 550}, {x: 280, y: 480}], // Broad foundational center piece
-  [{x: 400, y: 400}, {x: 280, y: 480}, {x: 300, y: 300}], // Left-leaning internal wedge
-
-  // --- NORTH FLANK (Radiating to the top edge: y = 0) ---
-  [{x: 300, y: 300}, {x: 0,   y: 0},   {x: 250, y: 0}],
-  [{x: 300, y: 300}, {x: 250, y: 0},   {x: 500, y: 0}],
-  [{x: 300, y: 300}, {x: 500, y: 0},   {x: 500, y: 250}], // Massive sweeping North-East shard
-  [{x: 500, y: 250}, {x: 500, y: 0},   {x: 700, y: 0}],
-  [{x: 500, y: 250}, {x: 700, y: 0},   {x: 800, y: 150}], // Long, sharp slope
-
-  // --- EAST FLANK (Radiating to the right edge: x = 800) ---
-  [{x: 500, y: 250}, {x: 800, y: 150}, {x: 550, y: 420}],
-  [{x: 550, y: 420}, {x: 800, y: 150}, {x: 800, y: 450}], // Tight accent block
-  [{x: 550, y: 420}, {x: 800, y: 450}, {x: 800, y: 600}], 
-  [{x: 550, y: 420}, {x: 800, y: 600}, {x: 480, y: 550}], // Giant horizontal slice
-
-  // --- SOUTH FLANK (Radiating to the bottom edge: y = 800) ---
-  [{x: 480, y: 550}, {x: 800, y: 600}, {x: 800, y: 800}], // Clears the bottom right corner
-  [{x: 480, y: 550}, {x: 800, y: 800}, {x: 550, y: 800}],
-  [{x: 480, y: 550}, {x: 550, y: 800}, {x: 350, y: 800}], // Long vertical grounding anchor
-  [{x: 480, y: 550}, {x: 350, y: 800}, {x: 150, y: 800}],
-  [{x: 480, y: 550}, {x: 150, y: 800}, {x: 280, y: 480}], // Sweeping South-West transitional floor
-
-  // --- WEST FLANK (Radiating to the left edge: x = 0) ---
-  [{x: 280, y: 480}, {x: 150, y: 800}, {x: 0,   y: 800}], // Clears the bottom left corner
-  [{x: 280, y: 480}, {x: 0,   y: 800}, {x: 0,   y: 500}],
-  [{x: 280, y: 480}, {x: 0,   y: 500}, {x: 0,   y: 300}], // Tall vertical perimeter guard
-  [{x: 280, y: 480}, {x: 0,   y: 300}, {x: 300, y: 300}], // Steep internal left block
-
-  // --- NORTH-WEST FILLER ZONE (Completing upper left frame gaps) ---
-  [{x: 300, y: 300}, {x: 0,   y: 300}, {x: 0,   y: 120}],
-  [{x: 300, y: 300}, {x: 0,   y: 120}, {x: 0,   y: 0}],
-
-  // --- OUTER SUBDIVISIONS (Adding complexity to the borders for visual contrast) ---
-  [{x: 700, y: 0},   {x: 800, y: 0},   {x: 800, y: 150}], // Top right micro-corner
-  [{x: 550, y: 800}, {x: 680, y: 800}, {x: 480, y: 550}], // Tiny baseline shard A
-  [{x: 680, y: 800}, {x: 800, y: 800}, {x: 480, y: 550}], // Tiny baseline shard B
-  [{x: 350, y: 800}, {x: 250, y: 800}, {x: 480, y: 550}], // Tiny baseline shard C
-  [{x: 250, y: 800}, {x: 150, y: 800}, {x: 480, y: 550}], // Tiny baseline shard D
-  [{x: 0,   y: 500}, {x: 0,   y: 650}, {x: 280, y: 480}], // Left margin splitter
-  [{x: 0,   y: 650}, {x: 0,   y: 800}, {x: 280, y: 480}], // Left floor splitter
-  [{x: 0,   y: 300}, {x: 0,   y: 200}, {x: 300, y: 300}], // Left ceiling splitter
-  [{x: 0,   y: 200}, {x: 0,   y: 0},   {x: 300, y: 300}]  // Left corner lock
-];
+            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 };
 
-// drawTriangle([{x: moveX + 200, y: 200}, {x: moveX + 600, y: 310},{x: moveX + 450, y: 400}]);
-// drawTriangle([{x: 120, y: 200}, {x:200, y: 310},{x: 450, y: 400}], "#FF0000");
-// drawTriangle([{x: 200, y: 200}, {x:600, y: 230},{x: 250, y: 300}]);
+            a = applyCamera(a);
+            b = applyCamera(b);
 
+            if (a.z<=NEAR || b.z<=NEAR) continue;
+            line(screen(project(a)), screen(project(b)));
+        }
+    }
 
-for (const i in triangleCoords) {
-    drawTriangle(triangleCoords[i], highContrastColors[i % highContrastColors.length]);
-    await new Promise(r => setTimeout(r, 2000));
+    setTimeout(frame, 1000/FPS);
 }
+frame();
+
+/* ===================== INTERACTION ===================== */
+game.onclick = () => {
+    game.requestPointerLock();
+    axisModeIndex = (axisModeIndex + 1) % AXIS_MODES.length;
+    activeAxes = AXIS_MODES[axisModeIndex];
+};
+
+window.addEventListener("mousemove", e => {
+    if (document.pointerLockElement !== game) return;
+
+    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);
+
+    camera.rotation = quatMultiply(yaw, camera.rotation);
+    camera.rotation = quatMultiply(camera.rotation, pitch);
+});