I am trying to implement raycasting in typescript with HTML Canvas 2D following this video: https://youtu.be/TOEi6T2mtHo. However, the rays being cast tend to point towards a single direction.
My goal is to have the rays extend out in each direction to make contact with each line like in the video.
I have tried normalizing the direction vector, and I have tried multiplying the direction of the rays with a scalar value like 10 but both did not work.
The rays are working sometimes, but mostly point in a single direction.
Here is the code for the Vector class:
export default class Vector2D {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
getX(): number {
return this.x;
}
getY(): number {
return this.y;
}
setX(x: number) {
this.x = x;
}
setY(y: number) {
this.y = y;
}
add(vector: Vector2D): Vector2D {
return new Vector2D(this.x + vector.getX(), this.y + vector.getY());
}
subtract(vector: Vector2D): Vector2D {
return new Vector2D(this.x - vector.getX(), this.y - vector.getY());
}
magnitude(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
normalize(): Vector2D {
const magnitude = this.magnitude();
this.x = this.x / magnitude;
this.y = this.y / magnitude;
return this;
}
dot(vec: Vector2D): number {
return this.x * vec.getX() + this.y * vec.getY();
}
}
Here is the code of the Ray class:
import Vector2D from "../Util/Vector2D";
import Line from "./Line";
export interface RayOptions {
context: CanvasRenderingContext2D;
position: Vector2D;
direction: Vector2D;
}
export default class Ray {
context: CanvasRenderingContext2D;
position: Vector2D;
direction: Vector2D;
constructor({ context, position, direction }: RayOptions) {
this.context = context;
this.position = position;
this.direction = direction;
}
getDirection(): Vector2D {
return this.direction;
}
getPosition(): Vector2D {
return this.position;
}
setDirection(vector: Vector2D): void {
this.direction = vector.add(this.position);
this.direction.normalize();
}
setPosition(vector: Vector2D): void {
this.position = vector;
}
draw() {
this.context.strokeStyle = "white";
this.context.save();
this.context.beginPath();
this.context.translate(this.position.getX(), this.position.getY());
this.context.moveTo(0, 0);
this.context.lineTo(this.direction.getX() * 10, this.direction.getY() * 10);
this.context.stroke();
this.context.restore();
}
cast(line: Line): Vector2D | null {
const x1 = line.getStart().getX();
const y1 = line.getStart().getY();
const x2 = line.getEnd().getX();
const y2 = line.getEnd().getY();
const x3 = this.getPosition().getX();
const y3 = this.getPosition().getX();
const x4 = this.getPosition().getX() + this.getDirection().getX();
const y4 = this.getPosition().getY() + this.getDirection().getY();
const denomenator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (denomenator === 0) {
return null;
}
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denomenator;
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denomenator;
if (t > 0 && t < 1 && u > 0) {
const x = x1 + t * (x2 - x1);
const y = y1 + t * (y2 - y1);
return new Vector2D(x, y);
}
return null;
}
}
Here is the code for the Line class:
import Vector2D from "../Util/Vector2D";
export interface LineOptions {
context: CanvasRenderingContext2D;
start: Vector2D;
end: Vector2D;
color?: string;
}
export default class Line {
context: CanvasRenderingContext2D;
start: Vector2D;
end: Vector2D;
color: string;
constructor({
context,
color = "rgb(255, 255, 0)",
start,
end
}: LineOptions) {
this.context = context;
this.start = start;
this.end = end;
this.color = color;
}
setStart(vector: Vector2D): void {
this.start = vector;
}
setEnd(vector: Vector2D): void {
this.end = vector;
}
setColor(color: string): void {
this.color = color;
}
getColor(): string {
return this.color;
}
getStart(): Vector2D {
return this.start;
}
getEnd(): Vector2D {
return this.end;
}
draw(): void {
this.context.strokeStyle = this.color;
this.context.beginPath();
this.context.moveTo(this.start.getX(), this.start.getY());
this.context.lineTo(this.end.getX(), this.end.getY());
this.context.stroke();
}
}
Here is the Player class which contains the code for rendering the lines from the rays:
import Vector2D from "../Util/Vector2D";
import Circle, { CircleOptions } from "./Circle";
import Line from "./Line";
import Ray from "./Ray";
export default class Player extends Circle {
rays: Ray[];
constructor({ context, color, position, radius, velocity }: CircleOptions) {
super({ context, color, position, radius, velocity });
this.rays = [];
this.setRays();
}
draw(): void {
this.rays.forEach((ray) => ray.draw());
}
getRays(): Ray[] {
return this.rays;
}
setPosition(vector: Vector2D): void {
super.setPosition(vector);
this.rays.forEach((ray) => ray.setPosition(vector));
}
castTo(lines: Line[]) {
this.rays.forEach((ray) => {
let record = Infinity;
let closest: Vector2D = null!;
lines.forEach((line) => {
const intersection = ray.cast(line);
if (intersection) {
const diffX = intersection.getX() - this.getPosition().getX();
const diffY = intersection.getY() - this.getPosition().getY();
const distance = Math.hypot(diffX, diffY);
if (distance < record) {
record = distance;
closest = intersection;
}
}
});
if (closest) {
const l = new Line({
context: this.context,
color: "green",
start: ray.getPosition(),
end: closest
});
l.draw();
}
});
}
private setRays() {
for (let i = 0; i < 360; i += 10) {
const rad = i * (Math.PI / 180);
this.rays.push(
new Ray({
context: this.context,
position: this.position,
direction: new Vector2D(Math.cos(rad), Math.sin(rad))
})
);
}
}
}
And finally, here is how I am updating the position for the rays:
const updatePlayer = (ev: MouseEvent) => {
if (player) {
player.setPosition(
new Vector2D(ev.clientX + offsetX, ev.clientY + offsetY)
);
}
};
window.addEventListener("mousemove", updatePlayer);
