Skip to content

Instantly share code, notes, and snippets.

@lukem512
Created September 25, 2020 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukem512/9a9774195bf8a3246f009595ce679b3f to your computer and use it in GitHub Desktop.
Save lukem512/9a9774195bf8a3246f009595ce679b3f to your computer and use it in GitHub Desktop.
LukeCaster (a simple JavaScript raycaster)
<html>
<head>
<title>LukeCaster</title>
<script type="text/javascript" src="raycaster.js"></script>
<style type="text/css">
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Montserrat', sans-serif;
}
.small {
font-size: 0.75rem;
margin: 0.35rem;
}
</style>
</head>
<body>
<canvas id="canvas" width="600" height="400">
</canvas>
<div class="small">Click any two points to draw a boundary.</div>
<script type="text/javascript">
// Set up a global object for the application
window.lukecaster = {
boundaries: [],
bulbs: []
}
// Constant handles on the canvas
const canvas = document.querySelector("#canvas")
const ctx = canvas.getContext("2d")
// Create a bulb with equally-spaced rays radiation from it
// The theta angle for each ray is defined between -Pi and Pi
function createBulb(x = 100, y = 100, numRays = 12) {
let delta = 2 * Math.PI / numRays
return {
position: new Point(x, y),
rays: Array.from({ length: numRays }, (_, i) => new Ray(x, y, i * delta - Math.PI))
}
}
function updateCanvas() {
// Blank backdrop
ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Paint the boundaries
window.lukecaster.boundaries.forEach(boundary => boundary.paint(ctx, { color: "white" }))
// Paint the bulbs
window.lukecaster.bulbs.forEach(bulb => {
bulb.position.paint(ctx, { color: "gold" })
bulb.rays.forEach((ray, i) => ray.paint(ctx, { length: 800, boundaries: window.lukecaster.boundaries }))
})
}
// Listener, used to create boundaries
canvas.addEventListener("click", e => {
// Current mouse point
let thisPoint = new Point(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop)
if (window.lukecaster.placing) {
window.lukecaster.placing = false
window.lukecaster.boundaries.push(new Boundary(window.lukecaster.lastPoint, thisPoint))
updateCanvas()
} else {
window.lukecaster.placing = true
thisPoint.paint(ctx, { color: "white" })
}
// Update the last-clicked point
window.lukecaster.lastPoint = thisPoint
})
// Construct the starting landscape
// let boundary = new Boundary(new Point(400, 80), new Point(500, canvas.height - 50))
// window.lukecaster.boundaries.push(boundary)
// Add a lightsource
let bulb = createBulb(300, 200, 360)
window.lukecaster.bulbs.push(bulb)
// Initial paint
updateCanvas()
</script>
</body>
</html>
class Point {
constructor(x, y) {
this._x = x
this._y = y
}
get x() {
return this._x
}
get y() {
return this._y
}
set x(x) {
this._x = x
}
set y(y) {
this._y = y
}
// Determine equality betwee two points
equals(point) {
return this.x === point.x && this.y === point.y
}
// Find the distance to a specified point
distanceTo(point) {
let dx = this.x - point.x
let dy = this.y - point.y
let x2 = dx * dx
let y2 = dy * dy
return Math.sqrt(x2 + y2)
}
paint(ctx, { diameter = 5, color = "black" }) {
let radius = Math.round(diameter / 2)
ctx.fillStyle = color
ctx.fillRect(this.x - radius, this.y - radius, diameter, diameter)
}
}
class Ray extends Point {
constructor(x, y, theta) {
super(x, y)
this._theta = theta
}
get theta() {
return this._theta
}
set theta(theta) {
this._theta = theta
}
// Determine where a ray will strike and illuminate
// a specified boundary line.
// Returns a point if one exists, null otherwise
illuminationPoint(boundary, length) {
let rayPoint = this.pointOnRay(length)
// Points on ray, (x1, y1) (x2, y2)
let x1 = this.x
let y1 = this.y
let x2 = rayPoint.x
let y2 = rayPoint.y
// Points on boundary, (x3, y3) (x4, y4)
let x3 = boundary.a.x
let y3 = boundary.a.y
let x4 = boundary.b.x
let y4 = boundary.b.y
// Check for zero-length lines
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return null
}
// Find intersection
let denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
// Are lines parallel?
if (denom === 0) {
return null
}
let ua = ((x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3)) / denom
let ub = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / denom
// Lines do not intersect along specified segments
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return null
}
let px = x1 + ua * (x2 - x1)
let py = y1 + ua * (y2 - y1)
return new Point(px, py)
}
// Determine whether the ray will strike and illuminate
// a specified boundary line
willIlluminate(boundary, length) {
return this.illuminationPoint(boundary, length) !== null
}
pointOnRay(distance = 200) {
let dx = distance * Math.cos(this.theta)
let dy = distance * Math.sin(this.theta)
return new Point(this.x + dx, this.y + dy)
}
paint(ctx, { thickness = 1, color = "LightGoldenRodYellow", length = 100, boundaries = [] }) {
let endPoint
let illuminationPoints = boundaries.filter(x => this.willIlluminate(x, length)).map(x => this.illuminationPoint(x, length))
// If there are illuminated boundary points, find the closest one and end the ray there
// Otherwise, illuminate the full length of the ray
if (illuminationPoints.length) {
endPoint = illuminationPoints.reduce((closest, cur) => {
if (cur.distanceTo(this) < closest.distanceTo(this)) {
return cur
}
return closest
}, illuminationPoints[0])
} else {
endPoint = this.pointOnRay(length)
}
ctx.lineWidth = thickness
ctx.lineCap = "round"
ctx.strokeStyle = color
ctx.beginPath()
ctx.moveTo(this.x, this.y)
ctx.lineTo(endPoint.x, endPoint.y)
ctx.closePath()
ctx.stroke()
}
}
class Boundary {
// Accepts two Points, a start and end position
constructor(a, b) {
this._a = a
this._b = b
}
get a() {
return this._a
}
get b() {
return this._b
}
set a(point) {
if (point instanceof Point) {
this._a = point
}
}
set b(point) {
if (point instanceof Point) {
this._b = point
}
}
get xLength() {
return Math.abs(this.a.x - this.b.x)
}
get yLength() {
return Math.abs(this.a.y - this.b.y)
}
get length() {
return this.a.distanceTo(this.b)
}
paint(ctx, { thickness = 3, color = "black" }) {
ctx.lineWidth = thickness
ctx.lineCap = "round"
ctx.strokeStyle = color
ctx.beginPath()
ctx.moveTo(this.a.x, this.a.y)
ctx.lineTo(this.b.x, this.b.y)
ctx.stroke()
ctx.closePath()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment