From: A-Larsen Date: Thu, 15 Oct 2026 01:17:31 +0000 (-0400) Subject: first commit X-Git-Url: https://nyquistdev.net/git/?a=commitdiff_plain;h=HEAD;p=basic-3d-example.git first commit --- diff --git a/docinfo.conf b/docinfo.conf index fc4596c..57fa5b9 100644 --- a/docinfo.conf +++ b/docinfo.conf @@ -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 diff --git a/index.js b/index.js index 8bfa577..17f2870 100644 --- a/index.js +++ b/index.js @@ -1,186 +1,229 @@ -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 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); +});