--- /dev/null
+<!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>