Created
June 4, 2014 07:33
-
-
Save azappella/e092d7e08fffe49e4979 to your computer and use it in GitHub Desktop.
Raycaster
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<title>Raycaster Demo - PlayfulJS</title> | |
</head> | |
<body style='background: #000; margin: 0; padding: 0'> | |
<canvas id='display' width='1' height='1' style='width: 100%; height: 100%;' /> | |
<script> | |
var CIRCLE = Math.PI * 2; | |
var MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) | |
function Controls() { | |
this.codes = { 37: 'left', 39: 'right', 38: 'forward', 40: 'backward' }; | |
this.states = { 'left': false, 'right': false, 'forward': false, 'backward': false }; | |
document.addEventListener('keydown', this.onKey.bind(this, true), false); | |
document.addEventListener('keyup', this.onKey.bind(this, false), false); | |
document.addEventListener('touchstart', this.onTouch.bind(this), false); | |
document.addEventListener('touchmove', this.onTouch.bind(this), false); | |
document.addEventListener('touchend', this.onTouchEnd.bind(this), false); | |
} | |
Controls.prototype.onTouch = function(e) { | |
var t = e.touches[0]; | |
this.onTouchEnd(e); | |
if (t.pageY < window.innerHeight * 0.5) this.onKey(true, { keyCode: 38 }); | |
else if (t.pageX < window.innerWidth * 0.5) this.onKey(true, { keyCode: 37 }); | |
else if (t.pageY > window.innerWidth * 0.5) this.onKey(true, { keyCode: 39 }); | |
}; | |
Controls.prototype.onTouchEnd = function(e) { | |
this.states = { 'left': false, 'right': false, 'forward': false, 'backward': false }; | |
e.preventDefault(); | |
e.stopPropagation(); | |
}; | |
Controls.prototype.onKey = function(val, e) { | |
var state = this.codes[e.keyCode]; | |
if (typeof state === 'undefined') return; | |
this.states[state] = val; | |
e.preventDefault && e.preventDefault(); | |
e.stopPropagation && e.stopPropagation(); | |
}; | |
function Bitmap(src, width, height) { | |
this.image = new Image(); | |
this.image.src = src; | |
this.width = width; | |
this.height = height; | |
} | |
function Player(x, y, direction) { | |
this.x = x; | |
this.y = y; | |
this.direction = direction; | |
this.weapon = new Bitmap('../knife_hand.png', 319, 320); | |
this.paces = 0; | |
} | |
Player.prototype.rotate = function(angle) { | |
this.direction = (this.direction + angle + CIRCLE) % (CIRCLE); | |
}; | |
Player.prototype.walk = function(distance, map) { | |
var dx = Math.cos(this.direction) * distance; | |
var dy = Math.sin(this.direction) * distance; | |
if (map.get(this.x + dx, this.y) <= 0) this.x += dx; | |
if (map.get(this.x, this.y + dy) <= 0) this.y += dy; | |
this.paces += distance; | |
}; | |
Player.prototype.update = function(controls, map, seconds) { | |
if (controls.left) this.rotate(-Math.PI * seconds); | |
if (controls.right) this.rotate(Math.PI * seconds); | |
if (controls.forward) this.walk(3 * seconds, map); | |
if (controls.backward) this.walk(-3 * seconds, map); | |
}; | |
function Map(size) { | |
this.size = size; | |
this.wallGrid = new Uint8Array(size * size); | |
this.skybox = new Bitmap('../deathvalley_panorama.jpg', 4000, 1290); | |
this.wallTexture = new Bitmap('../wall_texture.jpg', 1024, 1024); | |
this.light = 0; | |
} | |
Map.prototype.get = function(x, y) { | |
x = Math.floor(x); | |
y = Math.floor(y); | |
if (x < 0 || x > this.size - 1 || y < 0 || y > this.size - 1) return -1; | |
return this.wallGrid[y * this.size + x]; | |
}; | |
Map.prototype.randomize = function() { | |
for (var i = 0; i < this.size * this.size; i++) { | |
this.wallGrid[i] = Math.random() < 0.3 ? 1 : 0; | |
} | |
}; | |
Map.prototype.cast = function(point, angle, range) { | |
var self = this; | |
var sin = Math.sin(angle); | |
var cos = Math.cos(angle); | |
var noWall = { length2: Infinity }; | |
return ray({ x: point.x, y: point.y, height: 0, distance: 0 }); | |
function ray(origin) { | |
var stepX = step(sin, cos, origin.x, origin.y); | |
var stepY = step(cos, sin, origin.y, origin.x, true); | |
var nextStep = stepX.length2 < stepY.length2 | |
? inspect(stepX, 1, 0, origin.distance, stepX.y) | |
: inspect(stepY, 0, 1, origin.distance, stepY.x); | |
if (nextStep.distance > range) return [origin]; | |
return [origin].concat(ray(nextStep)); | |
} | |
function step(rise, run, x, y, inverted) { | |
if (run === 0) return noWall; | |
var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x; | |
var dy = dx * (rise / run); | |
return { | |
x: inverted ? y + dy : x + dx, | |
y: inverted ? x + dx : y + dy, | |
length2: dx * dx + dy * dy | |
}; | |
} | |
function inspect(step, shiftX, shiftY, distance, offset) { | |
var dx = cos < 0 ? shiftX : 0; | |
var dy = sin < 0 ? shiftY : 0; | |
step.height = self.get(step.x - dx, step.y - dy); | |
step.distance = distance + Math.sqrt(step.length2); | |
if (shiftX) step.shading = cos < 0 ? 2 : 0; | |
else step.shading = sin < 0 ? 2 : 1; | |
step.offset = offset - Math.floor(offset); | |
return step; | |
} | |
}; | |
Map.prototype.update = function(seconds) { | |
if (this.light > 0) this.light = Math.max(this.light - 10 * seconds, 0); | |
else if (Math.random() * 5 < seconds) this.light = 2; | |
}; | |
function Camera(canvas, resolution, fov) { | |
this.ctx = canvas.getContext('2d'); | |
this.width = canvas.width = window.innerWidth * 0.5; | |
this.height = canvas.height = window.innerHeight * 0.5; | |
this.resolution = resolution; | |
this.spacing = this.width / resolution; | |
this.fov = fov; | |
this.range = MOBILE ? 8 : 14; | |
this.lightRange = 5; | |
this.scale = (this.width + this.height) / 1200; | |
} | |
Camera.prototype.render = function(player, map) { | |
this.drawSky(player.direction, map.skybox, map.light); | |
this.drawColumns(player, map); | |
this.drawWeapon(player.weapon, player.paces); | |
}; | |
Camera.prototype.drawSky = function(direction, sky, ambient) { | |
var width = this.width * (CIRCLE / this.fov); | |
var left = -width * direction / CIRCLE; | |
this.ctx.save(); | |
this.ctx.drawImage(sky.image, left, 0, width, this.height); | |
if (left < width - this.width) { | |
this.ctx.drawImage(sky.image, left + width, 0, width, this.height); | |
} | |
if (ambient > 0) { | |
this.ctx.fillStyle = '#ffffff'; | |
this.ctx.globalAlpha = ambient * 0.1; | |
this.ctx.fillRect(0, this.height * 0.5, this.width, this.height * 0.5); | |
} | |
this.ctx.restore(); | |
}; | |
Camera.prototype.drawColumns = function(player, map) { | |
this.ctx.save(); | |
for (var column = 0; column < this.resolution; column++) { | |
var angle = this.fov * (column / this.resolution - 0.5); | |
var ray = map.cast(player, player.direction + angle, this.range); | |
this.drawColumn(column, ray, angle, map); | |
} | |
this.ctx.restore(); | |
}; | |
Camera.prototype.drawWeapon = function(weapon, paces) { | |
var bobX = Math.cos(paces * 2) * this.scale * 6; | |
var bobY = Math.sin(paces * 4) * this.scale * 6; | |
var left = this.width * 0.66 + bobX; | |
var top = this.height * 0.6 + bobY; | |
this.ctx.drawImage(weapon.image, left, top, weapon.width * this.scale, weapon.height * this.scale); | |
}; | |
Camera.prototype.drawColumn = function(column, ray, angle, map) { | |
var ctx = this.ctx; | |
var texture = map.wallTexture; | |
var left = Math.floor(column * this.spacing); | |
var width = Math.ceil(this.spacing); | |
var hit = -1; | |
while (++hit < ray.length && ray[hit].height <= 0); | |
for (var s = ray.length - 1; s >= 0; s--) { | |
var step = ray[s]; | |
var rainDrops = Math.pow(Math.random(), 3) * s; | |
var rain = (rainDrops > 0) && this.project(0.1, angle, step.distance); | |
if (s === hit) { | |
var textureX = Math.floor(texture.width * step.offset); | |
var wall = this.project(step.height, angle, step.distance); | |
ctx.globalAlpha = 1; | |
ctx.drawImage(texture.image, textureX, 0, 1, texture.height, left, wall.top, width, wall.height); | |
ctx.fillStyle = '#000000'; | |
ctx.globalAlpha = Math.max((step.distance + step.shading) / this.lightRange - map.light, 0); | |
ctx.fillRect(left, wall.top, width, wall.height); | |
} | |
ctx.fillStyle = '#ffffff'; | |
ctx.globalAlpha = 0.15; | |
while (--rainDrops > 0) ctx.fillRect(left, Math.random() * rain.top, 1, rain.height); | |
} | |
}; | |
Camera.prototype.project = function(height, angle, distance) { | |
var z = distance * Math.cos(angle); | |
var wallHeight = this.height * height / z; | |
var bottom = this.height / 2 * (1 + 1 / z); | |
return { | |
top: bottom - wallHeight, | |
height: wallHeight | |
}; | |
}; | |
function GameLoop() { | |
this.frame = this.frame.bind(this); | |
this.lastTime = 0; | |
this.callback = function() {}; | |
} | |
GameLoop.prototype.start = function(callback) { | |
this.callback = callback; | |
requestAnimationFrame(this.frame); | |
}; | |
GameLoop.prototype.frame = function(time) { | |
var seconds = (time - this.lastTime) / 1000; | |
this.lastTime = time; | |
if (seconds < 0.2) this.callback(seconds); | |
requestAnimationFrame(this.frame); | |
}; | |
var display = document.getElementById('display'); | |
var player = new Player(15.3, -1.2, Math.PI * 0.3); | |
var map = new Map(32); | |
var controls = new Controls(); | |
var camera = new Camera(display, MOBILE ? 160 : 320, Math.PI * 0.4); | |
var loop = new GameLoop(); | |
map.randomize(); | |
loop.start(function frame(seconds) { | |
map.update(seconds); | |
player.update(controls.states, map, seconds); | |
camera.render(player, map); | |
}); | |
</script> | |
<script> | |
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ | |
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), | |
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) | |
})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); | |
ga('create', 'UA-50885475-1', 'playfuljs.com'); | |
ga('send', 'pageview'); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment