UNPKG

demon

Version:

A small 3D library for rendering a very simple .obj

295 lines (245 loc) 7.78 kB
class demon { constructor(canvas) { this.canvas = document.querySelector(canvas) this.ctx = this.canvas.getContext("2d") this.canvas.width = 2000 this.canvas.height = (2000 / window.innerWidth) * window.innerHeight this.camera = { x: 0, y: 0, z: 0, rotation: { x: 0, y: 0, z: 0 }, fov: 90 } this.objects = [] this.darkmode = false this.wireframe = false this.fill = true this.mouseX = [] this.mouseY = [] this.keys = [] document.addEventListener("keydown", (e) => { e.preventDefault() this.keys[e.key] = true }) document.addEventListener("keyup", (e) => { e.preventDefault() delete this.keys[e.key] }) this.canvas.addEventListener("mousedown", (event) => { this.isMouseDown = true this.mouseX = event.clientX this.mouseY = event.clientY }) this.canvas.addEventListener("mousemove", (event) => { if (!this.isMouseDown) { return } const deltaX = event.clientX - this.mouseX const deltaY = event.clientY - this.mouseY this.camera.rotation.x += deltaY * 0.01 this.camera.rotation.y += deltaX * 0.01 this.mouseX = event.clientX this.mouseY = event.clientY }) this.canvas.addEventListener("mouseup", () => { this.isMouseDown = false }) } load(name, x, y, z, color) { let vertices = [] let faces = [] fetch(name) .then((res) => res.text()) .then((data) => { let lines = data.split("\n") for (let line of lines) { line = line.split(" ") for (let i = 0; i < line.length; i++) { line[i] = line[i].trim() if (line[i] === "") { line.splice(i, 1) i-- } } switch (line[0]) { case "v": vertices.push({ x: line[1] * 1, y: line[2] * 1, z: line[3] * 1 }) break case "f": let arr = [] for (let i = 1; i < line.length; i++) { arr.push(vertices[line[i].split("/")[0] - 1]) } faces.push(arr) break } } }) let object = { shape: "polyhedron", faces, x: x || 0, y: y || 0, z: z || 0, rotation: { x: 0, y: 0, z: 0, center: { x: x || 0, y: y || 0, z: z || 0 }, }, color: color || "white", } this.objects.push(object) return object } renderLoop(beforeRender) { let rsThis = this function run() { beforeRender() rsThis.render() window.requestAnimationFrame(run) } window.requestAnimationFrame(run) } toDeg(rad) { return (180 * rad) / Math.PI } toRad(deg) { return (Math.PI * deg) / 180 } spaceToPixels(x, y, z) { let cw = this.canvas.width let ch = this.canvas.height let fovX = this.toRad(this.camera.fov) let fovY = 2 * Math.atan((Math.tan(fovX / 2) * ch) / cw) let width = Math.tan(fovX / 2) * z * 2 let height = Math.tan(fovY / 2) * z * 2 let ex = (x / width) * this.canvas.width let ey = (y / height) * this.canvas.height if (z < 0) { let ratio if (ex > 0) ratio = this.canvas.width / 2 / ex if (ex < 0) ratio = -(this.canvas.width / 2) / ex ex *= -1 * ratio ey *= -1 * ratio } return { x: ex, y: ey, z } } drawShape(points, stroke, fill) { if (points.reduce((a, c) => a && c.z < 0, true)) { return } this.ctx.beginPath() this.ctx.moveTo(points[0].x, points[0].y) for (let i = 1; i < points.length; i++) { this.ctx.lineTo(points[i].x, points[i].y) } this.ctx.lineTo(points[0].x, points[0].y) this.ctx.closePath() if (stroke) this.ctx.stroke() if (fill) this.ctx.fill() } render() { this.ctx.setTransform(1, 0, 0, 1, 0, 0) this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2) this.ctx.scale(1, -1) this.ctx.clearRect( -this.canvas.width / 2, -this.canvas.height / 2, this.canvas.width, this.canvas.height ) if (this.darkmode) { this.ctx.fillStyle = "#111111" this.ctx.strokeStyle = "#ffffff" this.ctx.fillRect( -this.canvas.width / 2, -this.canvas.height / 2, this.canvas.width, this.canvas.height ) } this.objects.sort((a, b) => { let ad = this.distanceFromCamera(a.x, a.y, a.z) let bd = this.distanceFromCamera(b.x, b.y, b.z) return bd - ad }) function applyMatrices(point, rotation, x, y, z) { let part1 = JSON.parse(JSON.stringify(point)) part1.y = (point.y + y - rotation.center.y) * Math.cos(rotation.x) + (point.z + z - rotation.center.z) * -Math.sin(rotation.x) part1.z = (point.y + y - rotation.center.y) * Math.sin(rotation.x) + (point.z + z - rotation.center.z) * Math.cos(rotation.x) let part2 = JSON.parse(JSON.stringify(part1)) part2.z = (part1.x + x - rotation.center.x) * -Math.sin(rotation.y) + (part1.z + z - rotation.center.z) * Math.cos(rotation.y) + z part2.x = (part1.x + x - rotation.center.x) * Math.cos(rotation.y) + (part1.z + z - rotation.center.z) * Math.sin(rotation.y) let final = JSON.parse(JSON.stringify(part2)) final.x = (part2.x + x - rotation.center.x) * Math.cos(rotation.z) + (part2.y + y - rotation.center.y) * -Math.sin(rotation.z) + x final.y = (part2.x + x - rotation.center.x) * Math.sin(rotation.z) + (part2.y + y - rotation.center.y) * Math.cos(rotation.z) + y return final } for (let object of this.objects) { let objectFaces = JSON.parse(JSON.stringify(object.faces)) objectFaces.forEach((face, index) => { face.forEach((point, index) => { let nPoint = applyMatrices( point, object.rotation, object.x, object.y, object.z ) face[index] = nPoint }) objectFaces[index] = face }) objectFaces.sort((a, b) => { let aTotal = a.reduce((a, c) => a + c.z, 0) let bTotal = b.reduce((a, c) => a + c.z, 0) return bTotal - aTotal }) for (let face of objectFaces) { let l1 = { x: face[1].x - face[0].x, y: face[1].y - face[0].y, z: face[1].z - face[0].z, } let l2 = { x: face[2].x - face[0].x, y: face[2].y - face[0].y, z: face[2].z - face[0].z, } let normal = { x: l1.y * l2.z - l1.z * l2.y, y: l1.z * l2.x - l1.x * l2.z, z: l1.x * l2.y - l1.y * l2.x, } let ax = normal.x * (face[0].x - this.camera.x) let ay = normal.y * (face[0].y - this.camera.y) let az = normal.z * (face[0].z - this.camera.z) if (ax + ay + az < 0) { let pointArr = [] face.forEach((a) => { let nPoint = this.spaceToPixels( a.x - this.camera.x, a.y - this.camera.y, a.z - this.camera.z ) pointArr.push(nPoint) }) this.ctx.fillStyle = object.color this.drawShape(pointArr, this.wireframe, this.fill) } } } } } module.exports = demon