Created
September 25, 2020 14:27
-
-
Save lukem512/9a9774195bf8a3246f009595ce679b3f to your computer and use it in GitHub Desktop.
LukeCaster (a simple JavaScript raycaster)
This file contains 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
<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> |
This file contains 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
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