]> git.mdlowis.com Git - proto/jstoys.git/commitdiff
initial commit of stripped down raycaster
authorMichael D. Lowis <mike.lowis@gentex.com>
Tue, 13 Nov 2018 23:20:04 +0000 (18:20 -0500)
committerMichael D. Lowis <mike.lowis@gentex.com>
Tue, 13 Nov 2018 23:20:04 +0000 (18:20 -0500)
raycast.html [new file with mode: 0644]

diff --git a/raycast.html b/raycast.html
new file mode 100644 (file)
index 0000000..6b014ba
--- /dev/null
@@ -0,0 +1,185 @@
+<!doctype html>
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>Raycaster</title>
+  </head>
+  <body style='background: #000; margin: 0; padding: 0; width: 100%; height: 100%;'>
+    <canvas id='display' width='1' height='1' style='width: 100%; height: 100%;' />
+    <script>
+"use strict";
+const TAU = Math.PI * 2;
+const WALK_SPEED = 5;
+const TURN_SPEED = Math.PI * 0.5;
+const MAX_VIEW_DIST = 32;
+const LIGHT_RANGE = 20;
+const FOCAL_LENGTH = 0.8;
+const VIEW_SCALER = 1.0;
+const MAP_SIZE = 32;
+
+const Controls = (()=>{
+    const self = { states: { 'left': false, 'right': false, 'forward': false, 'backward': false } };
+    const codes = { 37: 'left', 39: 'right', 38: 'forward', 40: 'backward' };
+    const onKey = (val, e)=>{
+        const state = codes[e.keyCode];
+        if (typeof state === 'undefined') return;
+        self.states[state] = val;
+        e.preventDefault && e.preventDefault();
+        e.stopPropagation && e.stopPropagation();
+    };
+    document.addEventListener('keydown', onKey.bind(self, true), false);
+    document.addEventListener('keyup', onKey.bind(self, false), false);
+    return self;
+})();
+
+const Player = ((x, y, direction)=>{
+    const self = {x: x, y: y, direction: direction};
+
+    const rotate = (angle)=>{
+        self.direction = (self.direction + angle + TAU) % (TAU);
+    };
+
+    const walk = (distance)=>{
+        const dx = Math.cos(self.direction) * distance;
+        const dy = Math.sin(self.direction) * distance;
+        if (Map.get(self.x + dx, self.y) <= 0) self.x += dx;
+        if (Map.get(self.x, self.y + dy) <= 0) self.y += dy;
+    };
+
+    self.update = (seconds)=>{
+        if (Controls.states.left) rotate(-TURN_SPEED * seconds);
+        if (Controls.states.right) rotate(TURN_SPEED * seconds);
+        if (Controls.states.forward) walk(WALK_SPEED * seconds);
+        if (Controls.states.backward) walk(-WALK_SPEED * seconds);
+    };
+
+    return self;
+})(15.3, -1.2, Math.PI * 0.3);
+
+const Map = ((size)=>{
+    let grid = new Uint8Array(size*size);
+    return {
+        get: (x, y)=>(grid[Math.floor(y) * size + Math.floor(x)] || -1),
+        randomize: ()=>(grid = grid.map((i) => (Math.random() < 0.3 ? 1 : 0))),
+    };
+})(MAP_SIZE);
+
+const Camera = (()=>{
+    const self = {};
+    let columns = [], ceiling = null, floor = null;
+    const ctx = display.getContext('2d');
+
+    const resizeView = ()=>{
+        if (self.width != window.innerWidth || self.height != window.innerHeight) {
+            self.width = display.width = Math.floor(window.innerWidth * VIEW_SCALER);
+            self.height = display.height = Math.floor(window.innerHeight * VIEW_SCALER);
+
+            /* Precalculate some trig values for each ray corresponding to each vertical line of the canvas */
+            columns = Array(self.width).fill(0).map((o, col)=>{
+                const data = {};
+                data.angle = Math.atan2((col / self.width - 0.5), FOCAL_LENGTH);
+                data.cos = Math.cos(data.angle);
+                data.sin = Math.sin(data.angle);
+                return data;
+            });
+
+            /* Generate floor and ceiling gradients */
+            ceiling = ctx.createLinearGradient(0, self.height/2, 0, 0);
+            ceiling.addColorStop(0, '#000011');
+            ceiling.addColorStop(1, '#000055');
+            floor = ctx.createLinearGradient(0, self.height/2, 0, self.height);
+            floor.addColorStop(0, '#110000');
+            floor.addColorStop(1, '#550000');
+        }
+    };
+
+    self.render = ()=>{
+        ctx.save();
+        const data = {};
+        resizeView();
+
+        ctx.fillStyle = ceiling;
+        ctx.fillRect(0, 0, self.width, self.height/2);
+        ctx.fillStyle = floor;
+        ctx.fillRect(0, self.height/2, self.width, self.height);
+
+        columns.map((col, i)=>{
+            /* pre-calculate trig functions related to the ray to cast */
+            data.angle = Player.direction + col.angle;
+            data.sin = Math.sin(data.angle);
+            data.cos = Math.cos(data.angle);
+            const obj = findWall(Player.x, Player.y, data);
+            if (obj) {
+                const z = obj.distance * col.cos;
+                const wall = { height: self.height * obj.height / z, top : 0 };
+                wall.top = (self.height / 2 * (1 + 1 / z)) - wall.height;
+
+                /* draw wall slice */
+                ctx.fillStyle = '#999999';
+                ctx.globalAlpha = 1;
+                ctx.fillRect(i, wall.top, 1, wall.height);
+
+                /* draw shadow over wall slice */
+                ctx.fillStyle = '#000000';
+                ctx.globalAlpha = Math.max((obj.distance) / LIGHT_RANGE, 0);
+                ctx.fillRect(i, wall.top, 1, wall.height);
+            }
+        });
+        ctx.restore();
+    };
+
+    const snapToGrid = (pos, dist) =>
+        ((dist > 0 ? Math.floor(pos + 1) : Math.ceil(pos - 1)) - pos);
+
+    const findWall = (x, y, data)=>{
+        let origin = { x: x, y: y, height: 0, distance: 0 };
+        while (true) {
+            let stepX = { length2: Infinity }, stepY = { length2: Infinity };
+
+            if (data.cos !== 0) {
+                const dx = snapToGrid(origin.x, data.cos), dy = (dx * (data.sin / data.cos));
+                stepX.x = origin.x + dx;
+                stepX.y = origin.y + dy;
+                stepX.length2 = dx * dx + dy * dy;
+                stepX.height = Map.get(stepX.x - (data.cos < 0 ? 1 : 0), stepX.y);
+                stepX.distance = origin.distance + Math.sqrt(stepX.length2);
+                stepX.shading = data.cos < 0 ? 2 : 0;
+                stepX.offset = stepX.y - Math.floor(stepX.y);
+            }
+
+            if (data.sin !== 0) {
+                const dx = snapToGrid(origin.y, data.sin), dy = (dx * (data.cos / data.sin));
+                stepY.x = origin.x + dy;
+                stepY.y = origin.y + dx;
+                stepY.length2 = dx * dx + dy * dy;
+                stepY.height = Map.get(stepY.x, stepY.y - (data.sin < 0 ? 1 : 0));
+                stepY.distance = origin.distance + Math.sqrt(stepY.length2);
+                stepY.shading = data.sin < 0 ? 2 : 1;
+                stepY.offset = stepY.x - Math.floor(stepY.x);
+            }
+
+            origin = (stepX.length2 < stepY.length2 ? stepX : stepY);
+            if (origin.distance > MAX_VIEW_DIST) return null;
+            if (origin.height > 0) return origin;
+        }
+    };
+
+    resizeView();
+    return self;
+})();
+
+(() => {
+    Map.randomize();
+    let time = 0;
+    const loop = (now) => {
+        const seconds = (now - time) / 1000;
+        Player.update(seconds);
+        Camera.render();
+        time = now;
+        requestAnimationFrame(loop);
+    };
+    requestAnimationFrame(loop);
+})();
+    </script>
+  </body>
+</html>