self driving car
This commit is contained in:
@@ -0,0 +1,174 @@
|
|||||||
|
class Car {
|
||||||
|
constructor(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
controlType,
|
||||||
|
color = "blue",
|
||||||
|
maxSpeed = 4,
|
||||||
|
width = 30,
|
||||||
|
height = 50
|
||||||
|
) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
this.speed = 0;
|
||||||
|
this.acceleration = 0.2;
|
||||||
|
this.maxSpeed = maxSpeed;
|
||||||
|
this.friction = 0.05;
|
||||||
|
this.angle = 0;
|
||||||
|
this.useBrain = controlType == "AI";
|
||||||
|
if (controlType != "DUMMY") {
|
||||||
|
this.sensor = new Sensor(this);
|
||||||
|
this.brain = new NeuralNetwork([
|
||||||
|
this.sensor.rayCount,
|
||||||
|
6,
|
||||||
|
4,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
this.controls = new Controls(controlType);
|
||||||
|
this.img = new Image();
|
||||||
|
this.img.src = "car.png";
|
||||||
|
this.mask = document.createElement("canvas");
|
||||||
|
this.mask.width = width;
|
||||||
|
this.mask.height = height;
|
||||||
|
|
||||||
|
const maskCtx = this.mask.getContext("2d");
|
||||||
|
this.img.onload = () => {
|
||||||
|
maskCtx.fillStyle = color;
|
||||||
|
maskCtx.rect(0, 0, this.width, this.height);
|
||||||
|
maskCtx.fill();
|
||||||
|
|
||||||
|
maskCtx.globalCompositeOperation = "destination-atop";
|
||||||
|
maskCtx.drawImage(
|
||||||
|
this.img,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.width,
|
||||||
|
this.height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
update(roadBorders, traffic) {
|
||||||
|
if (!this.damaged) {
|
||||||
|
this.#move();
|
||||||
|
this.polygon = this.#createPolygon();
|
||||||
|
this.damaged = this.#assessDamage(roadBorders, traffic);
|
||||||
|
}
|
||||||
|
if (this.sensor) {
|
||||||
|
this.sensor.update(roadBorders, traffic);
|
||||||
|
const offsets = this.sensor.readings.map((s) =>
|
||||||
|
s == null ? 0 : 1 - s.offset
|
||||||
|
);
|
||||||
|
const outputs = NeuralNetwork.feedForward(
|
||||||
|
offsets,
|
||||||
|
this.brain
|
||||||
|
);
|
||||||
|
if (this.useBrain) {
|
||||||
|
this.controls.forward = outputs[0];
|
||||||
|
this.controls.left = outputs[1];
|
||||||
|
this.controls.right = outputs[2];
|
||||||
|
this.controls.reverse = outputs[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#assessDamage(roadBorders, traffic) {
|
||||||
|
for (let i = 0; i < roadBorders.length; i++) {
|
||||||
|
if (polyIntersect(this.polygon, roadBorders[i]))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < traffic.length; i++) {
|
||||||
|
if (polyIntersect(this.polygon, traffic[i].polygon)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#createPolygon() {
|
||||||
|
const points = [];
|
||||||
|
const rad = Math.hypot(this.width, this.height) / 2;
|
||||||
|
const alpha = Math.atan2(this.width, this.height);
|
||||||
|
points.push({
|
||||||
|
x: this.x - Math.sin(this.angle - alpha) * rad,
|
||||||
|
y: this.y - Math.cos(this.angle - alpha) * rad,
|
||||||
|
});
|
||||||
|
points.push({
|
||||||
|
x: this.x - Math.sin(this.angle + alpha) * rad,
|
||||||
|
y: this.y - Math.cos(this.angle + alpha) * rad,
|
||||||
|
});
|
||||||
|
points.push({
|
||||||
|
x:
|
||||||
|
this.x -
|
||||||
|
Math.sin(Math.PI + this.angle - alpha) * rad,
|
||||||
|
y:
|
||||||
|
this.y -
|
||||||
|
Math.cos(Math.PI + this.angle - alpha) * rad,
|
||||||
|
});
|
||||||
|
points.push({
|
||||||
|
x:
|
||||||
|
this.x -
|
||||||
|
Math.sin(Math.PI + this.angle + alpha) * rad,
|
||||||
|
y:
|
||||||
|
this.y -
|
||||||
|
Math.cos(Math.PI + this.angle + alpha) * rad,
|
||||||
|
});
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
#move() {
|
||||||
|
if (this.controls.forward) this.speed += this.acceleration;
|
||||||
|
else if (this.controls.reverse) this.speed -= this.acceleration;
|
||||||
|
if (this.speed > this.maxSpeed) this.speed = this.maxSpeed;
|
||||||
|
else if (this.speed < -this.maxSpeed / 2)
|
||||||
|
this.speed = -this.maxSpeed / 2;
|
||||||
|
if (this.speed > 0) this.speed -= this.friction;
|
||||||
|
if (this.speed < 0) this.speed += this.friction;
|
||||||
|
if (Math.abs(this.speed) < this.friction) this.speed = 0;
|
||||||
|
if (this.speed != 0) {
|
||||||
|
const flip = this.speed > 0 ? 1 : -1;
|
||||||
|
if (this.controls.left) this.angle += 0.03 * flip;
|
||||||
|
if (this.controls.right) this.angle -= 0.03 * flip;
|
||||||
|
}
|
||||||
|
this.x -= Math.sin(this.angle) * this.speed;
|
||||||
|
this.y -= Math.cos(this.angle) * this.speed;
|
||||||
|
}
|
||||||
|
draw(ctx, color, drawSensor = false) {
|
||||||
|
if (this.sensor && drawSensor) this.sensor.draw(ctx);
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(this.x, this.y);
|
||||||
|
ctx.rotate(-this.angle);
|
||||||
|
if (!this.damaged) {
|
||||||
|
ctx.drawImage(
|
||||||
|
this.mask,
|
||||||
|
-this.width / 2,
|
||||||
|
-this.height / 2,
|
||||||
|
this.width,
|
||||||
|
this.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.globalCompositeOperation = "multiply";
|
||||||
|
ctx.drawImage(
|
||||||
|
this.img,
|
||||||
|
-this.width / 2,
|
||||||
|
-this.height / 2,
|
||||||
|
this.width,
|
||||||
|
this.height
|
||||||
|
);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function polyIntersect(poly1, poly2) {
|
||||||
|
for (let i = 0; i < poly1.length; i++) {
|
||||||
|
for (let j = 0; j < poly2.length; j++) {
|
||||||
|
const touch = getIntersection(
|
||||||
|
poly1[i],
|
||||||
|
poly1[(i + 1) % poly1.length],
|
||||||
|
poly2[j],
|
||||||
|
poly2[(j + 1) % poly2.length]
|
||||||
|
);
|
||||||
|
if (touch) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Vendored
+51
@@ -0,0 +1,51 @@
|
|||||||
|
class Controls {
|
||||||
|
constructor(type) {
|
||||||
|
this.forward = false;
|
||||||
|
this.reverse = false;
|
||||||
|
this.left = false;
|
||||||
|
this.right = false;
|
||||||
|
switch (type) {
|
||||||
|
case "KEYS":
|
||||||
|
this.#addKeyboardListeners();
|
||||||
|
break;
|
||||||
|
case "DUMMY":
|
||||||
|
this.forward = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#addKeyboardListeners() {
|
||||||
|
document.onkeydown = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
this.left = true;
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
this.right = true;
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
this.forward = true;
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
this.reverse = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.onkeyup = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
this.left = false;
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
this.right = false;
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
this.forward = false;
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
this.reverse = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<title>Self-driving car - No libraries</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="carCanvas"></canvas>
|
||||||
|
<div id="verticalButtons">
|
||||||
|
<button onclick="save()">Save</button>
|
||||||
|
<button onclick="discard()">Discard</button>
|
||||||
|
</div>
|
||||||
|
<canvas id="networkCanvas"></canvas>
|
||||||
|
<script src="visualizer.js"></script>
|
||||||
|
<script src="network.js"></script>
|
||||||
|
<script src="sensor.js"></script>
|
||||||
|
<script src="utils.js"></script>
|
||||||
|
<script src="road.js"></script>
|
||||||
|
<script src="controls.js"></script>
|
||||||
|
<script src="car.js"></script>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
const carCanvas = document.getElementById("carCanvas");
|
||||||
|
carCanvas.width = 200;
|
||||||
|
|
||||||
|
const networkCanvas = document.getElementById("networkCanvas");
|
||||||
|
networkCanvas.width = 0;
|
||||||
|
|
||||||
|
const carCtx = carCanvas.getContext("2d");
|
||||||
|
const networkCtx = networkCanvas.getContext("2d");
|
||||||
|
|
||||||
|
const road = new Road(carCanvas.width / 2, carCanvas.width * 0.9, 5);
|
||||||
|
const N = 200;
|
||||||
|
const cars = generateCars(N);
|
||||||
|
const traffic = [];
|
||||||
|
let minY = 500;
|
||||||
|
let carsCount = 3;
|
||||||
|
for (let i = 0; i < carsCount; i++) {
|
||||||
|
traffic.push(
|
||||||
|
new Car(
|
||||||
|
road.getLaneCenter(Math.random() * 5),
|
||||||
|
Math.random() * (-1300 + 700) - 700,
|
||||||
|
"DUMMY",
|
||||||
|
getRandomColor(),
|
||||||
|
Math.random() * 2.9
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let bestCar = cars[0];
|
||||||
|
if (localStorage.getItem("bestBrain")) {
|
||||||
|
for (let i = 0; i < cars.length; i++) {
|
||||||
|
cars[i].brain = JSON.parse(localStorage.getItem("bestBrain"));
|
||||||
|
if (i != 0) {
|
||||||
|
NeuralNetwork.mutate(cars[i].brain, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
localStorage.setItem("bestBrain", JSON.stringify(bestCar.brain));
|
||||||
|
localStorage.setItem("bestScore", JSON.stringify(bestCar.y));
|
||||||
|
}
|
||||||
|
function discard() {
|
||||||
|
localStorage.removeItem("bestBrain");
|
||||||
|
localStorage.removeItem("bestScore");
|
||||||
|
}
|
||||||
|
function generateCars(N) {
|
||||||
|
const cars = [];
|
||||||
|
for (let i = 0; i <= N; i++) {
|
||||||
|
cars.push(new Car(road.getLaneCenter(1), -1, "AI"));
|
||||||
|
}
|
||||||
|
return cars;
|
||||||
|
}
|
||||||
|
setInterval(() => {
|
||||||
|
if (carsCount < 15) {
|
||||||
|
traffic.push(
|
||||||
|
new Car(
|
||||||
|
road.getLaneCenter(Math.random() * 3),
|
||||||
|
bestCar.y - 800,
|
||||||
|
"DUMMY",
|
||||||
|
getRandomColor(),
|
||||||
|
Math.random() * 2.9
|
||||||
|
)
|
||||||
|
);
|
||||||
|
carsCount += 1;
|
||||||
|
}
|
||||||
|
if (bestCar.y < localStorage.getItem("bestScore")) save();
|
||||||
|
if (minY > 0) minY = -minY;
|
||||||
|
minY = minY * 1.5;
|
||||||
|
}, 5000);
|
||||||
|
function animate() {
|
||||||
|
for (let i = 0; i < traffic.length; i++) {
|
||||||
|
if (
|
||||||
|
traffic[i].y > bestCar.y + 350 ||
|
||||||
|
traffic[i].y < bestCar.y - 1400
|
||||||
|
) {
|
||||||
|
traffic.splice(
|
||||||
|
i,
|
||||||
|
1,
|
||||||
|
new Car(
|
||||||
|
road.getLaneCenter(Math.random() * 3),
|
||||||
|
bestCar.y - 800,
|
||||||
|
"DUMMY",
|
||||||
|
getRandomColor(),
|
||||||
|
Math.random() * 2.9
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
traffic[i].update(road.borders, []);
|
||||||
|
}
|
||||||
|
let restart = true;
|
||||||
|
for (let i = 0; i < cars.length; i++) {
|
||||||
|
cars[i].update(road.borders, traffic);
|
||||||
|
restart = restart && cars[i].damaged;
|
||||||
|
if (minY < cars[i].y) cars[i].damaged = true;
|
||||||
|
}
|
||||||
|
if (restart) location.reload();
|
||||||
|
bestCar = cars.find((c) => c.y == Math.min(...cars.map((c) => c.y)));
|
||||||
|
carCanvas.height = window.innerHeight;
|
||||||
|
networkCanvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
carCtx.save();
|
||||||
|
carCtx.translate(0, -bestCar.y + carCanvas.height * 0.7);
|
||||||
|
road.draw(carCtx);
|
||||||
|
for (let i = 0; i < traffic.length; i++) {
|
||||||
|
traffic[i].draw(carCtx, "red");
|
||||||
|
}
|
||||||
|
carCtx.globalAlpha = 0.2;
|
||||||
|
for (let i = 0; i < cars.length; i++) {
|
||||||
|
cars[i].draw(carCtx, "blue");
|
||||||
|
}
|
||||||
|
carCtx.globalAlpha = 1;
|
||||||
|
bestCar.draw(carCtx, "blue", true);
|
||||||
|
carCtx.restore();
|
||||||
|
// Visualizer.drawNetwork(networkCtx, bestCar.brain);
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
class NeuralNetwork {
|
||||||
|
constructor(neuronCounts) {
|
||||||
|
this.levels = [];
|
||||||
|
for (let i = 0; i < neuronCounts.length - 1; i++) {
|
||||||
|
this.levels.push(
|
||||||
|
new Level(neuronCounts[i], neuronCounts[i + 1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static feedForward(givenInputs, network) {
|
||||||
|
let outputs = Level.feedForward(givenInputs, network.levels[0]);
|
||||||
|
for (let i = 1; i < network.levels.length; i++) {
|
||||||
|
outputs = Level.feedForward(outputs, network.levels[i]);
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
static mutate(network, amount = 1) {
|
||||||
|
network.levels.forEach((level) => {
|
||||||
|
for (let i = 0; i < level.biases.length; i++) {
|
||||||
|
level.biases[i] = lerp(
|
||||||
|
level.biases[i],
|
||||||
|
Math.random() * 2 - 1,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < level.weights.length; i++) {
|
||||||
|
for (let j = 0; j < level.weights.length; j++) {
|
||||||
|
level.weights[i][j] = lerp(
|
||||||
|
level.weights[i][j],
|
||||||
|
Math.random() * 2 - 1,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Level {
|
||||||
|
constructor(inputCount, outputCount) {
|
||||||
|
this.inputs = new Array(inputCount);
|
||||||
|
this.outputs = new Array(outputCount);
|
||||||
|
this.biases = new Array(outputCount);
|
||||||
|
|
||||||
|
this.weights = [];
|
||||||
|
for (let i = 0; i < inputCount; i++) {
|
||||||
|
this.weights[i] = new Array(outputCount);
|
||||||
|
}
|
||||||
|
Level.#randomize(this);
|
||||||
|
}
|
||||||
|
static #randomize(level) {
|
||||||
|
for (let i = 0; i < level.inputs.length; i++) {
|
||||||
|
for (let j = 0; j < level.outputs.length; j++) {
|
||||||
|
level.weights[i][j] = Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < level.biases.length; i++) {
|
||||||
|
level.biases[i] = Math.random() * 2 - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static feedForward(givenInputs, level) {
|
||||||
|
for (let i = 0; i < level.inputs.length; i++) {
|
||||||
|
level.inputs[i] = givenInputs[i];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < level.outputs.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < level.inputs.length; j++) {
|
||||||
|
sum += level.inputs[j] * level.weights[j][i];
|
||||||
|
}
|
||||||
|
if (sum > level.biases[i]) level.outputs[i] = 1;
|
||||||
|
else level.outputs[i] = 0;
|
||||||
|
}
|
||||||
|
return level.outputs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
class Road {
|
||||||
|
constructor(x, width, laneCount = 5) {
|
||||||
|
this.x = x;
|
||||||
|
this.width = width;
|
||||||
|
this.laneCount = laneCount;
|
||||||
|
|
||||||
|
this.left = x - width / 2;
|
||||||
|
this.right = x + width / 2;
|
||||||
|
|
||||||
|
const infinity = 1000000;
|
||||||
|
this.top = -infinity;
|
||||||
|
this.bottom = infinity;
|
||||||
|
|
||||||
|
const topLeft = { x: this.left, y: this.top };
|
||||||
|
const topRight = { x: this.right, y: this.top };
|
||||||
|
const bottomLeft = { x: this.left, y: this.bottom };
|
||||||
|
const bottomRight = { x: this.right, y: this.bottom };
|
||||||
|
this.borders = [
|
||||||
|
[topLeft, bottomLeft],
|
||||||
|
[topRight, bottomRight],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getLaneCenter(laneIndex) {
|
||||||
|
const laneWidth = this.width / this.laneCount;
|
||||||
|
return (
|
||||||
|
this.left +
|
||||||
|
laneWidth / 2 +
|
||||||
|
Math.min(laneIndex, this.laneCount - 1) * laneWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.strokeStyle = "white";
|
||||||
|
|
||||||
|
for (let i = 1; i <= this.laneCount - 1; i++) {
|
||||||
|
const x = lerp(
|
||||||
|
this.left,
|
||||||
|
this.right,
|
||||||
|
i / this.laneCount
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.setLineDash([20, 20]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, this.top);
|
||||||
|
ctx.lineTo(x, this.bottom);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
this.borders.forEach((border) => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(border[0].x, border[0].y);
|
||||||
|
ctx.lineTo(border[1].x, border[1].y);
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
class Sensor {
|
||||||
|
constructor(car) {
|
||||||
|
this.car = car;
|
||||||
|
this.rayCount = 5;
|
||||||
|
this.rayLength = 150;
|
||||||
|
this.raySpread = Math.PI / 2;
|
||||||
|
|
||||||
|
this.rays = [];
|
||||||
|
this.readings = [];
|
||||||
|
}
|
||||||
|
update(roadBorders, traffic) {
|
||||||
|
this.#castRays();
|
||||||
|
this.readings = [];
|
||||||
|
for (let i = 0; i < this.rays.length; i++) {
|
||||||
|
this.readings.push(
|
||||||
|
this.#getReading(
|
||||||
|
this.rays[i],
|
||||||
|
roadBorders,
|
||||||
|
traffic
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#getReading(ray, roadBorders, traffic) {
|
||||||
|
let touches = [];
|
||||||
|
for (let i = 0; i < roadBorders.length; i++) {
|
||||||
|
const touch = getIntersection(
|
||||||
|
ray[0],
|
||||||
|
ray[1],
|
||||||
|
roadBorders[i][0],
|
||||||
|
roadBorders[i][1]
|
||||||
|
);
|
||||||
|
if (touch) touches.push(touch);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < traffic.length; i++) {
|
||||||
|
const poly = traffic[i].polygon;
|
||||||
|
for (let j = 0; j < poly.length; j++) {
|
||||||
|
const value = getIntersection(
|
||||||
|
ray[0],
|
||||||
|
ray[1],
|
||||||
|
poly[j],
|
||||||
|
poly[(j + 1) % poly.length]
|
||||||
|
);
|
||||||
|
if (value) touches.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (touches.length == 0) return null;
|
||||||
|
const offsets = touches.map((e) => e.offset);
|
||||||
|
const minOffset = Math.min(...offsets);
|
||||||
|
return touches.find((e) => e.offset == minOffset);
|
||||||
|
}
|
||||||
|
#castRays() {
|
||||||
|
this.rays = [];
|
||||||
|
for (let i = 0; i < this.rayCount; i++) {
|
||||||
|
const rayAngle =
|
||||||
|
lerp(
|
||||||
|
this.raySpread / 2,
|
||||||
|
-this.raySpread / 2,
|
||||||
|
this.rayCount == 1
|
||||||
|
? 0.5
|
||||||
|
: i / (this.rayCount - 1)
|
||||||
|
) + this.car.angle;
|
||||||
|
const start = { x: this.car.x, y: this.car.y };
|
||||||
|
const end = {
|
||||||
|
x:
|
||||||
|
this.car.x -
|
||||||
|
Math.sin(rayAngle) * this.rayLength,
|
||||||
|
y:
|
||||||
|
this.car.y -
|
||||||
|
Math.cos(rayAngle) * this.rayLength,
|
||||||
|
};
|
||||||
|
this.rays.push([start, end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw(ctx) {
|
||||||
|
for (let i = 0; i < this.rayCount; i++) {
|
||||||
|
let end = this.rays[i][1];
|
||||||
|
if (this.readings[i]) end = this.readings[i];
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = "yellow";
|
||||||
|
ctx.moveTo(this.rays[i][0].x, this.rays[i][0].y);
|
||||||
|
ctx.lineTo(end.x, end.y);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.moveTo(this.rays[i][1].x, this.rays[i][1].y);
|
||||||
|
ctx.lineTo(end.x, end.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: darkgray;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#verticalButtons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 5px 5px 7px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: blue;
|
||||||
|
}
|
||||||
|
#carCanvas {
|
||||||
|
background: lightgray;
|
||||||
|
}
|
||||||
|
#networkCanvas {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
function lerp(A, B, t) {
|
||||||
|
return A + (B - A) * t;
|
||||||
|
}
|
||||||
|
function getIntersection(A, B, C, D) {
|
||||||
|
const tTop = (D.x - C.x) * (A.y - C.y) - (D.y - C.y) * (A.x - C.x);
|
||||||
|
const uTop = (C.y - A.y) * (A.x - B.x) - (C.x - A.x) * (A.y - B.y);
|
||||||
|
const bottom = (D.y - C.y) * (B.x - A.x) - (D.x - C.x) * (B.y - A.y);
|
||||||
|
if (bottom != 0) {
|
||||||
|
const t = tTop / bottom;
|
||||||
|
const u = uTop / bottom;
|
||||||
|
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
|
||||||
|
return {
|
||||||
|
x: lerp(A.x, B.x, t),
|
||||||
|
y: lerp(A.y, B.y, t),
|
||||||
|
offset: t,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function getRGBA(value) {
|
||||||
|
const alpha = Math.abs(value);
|
||||||
|
const R = value < 0 ? 0 : 255;
|
||||||
|
const G = R;
|
||||||
|
const B = value > 0 ? 0 : 255;
|
||||||
|
return "rgba(" + R + "," + G + "," + B + "," + alpha + ")";
|
||||||
|
}
|
||||||
|
function getRandomColor() {
|
||||||
|
const hue = 290 + Math.random() * 260;
|
||||||
|
return "hsl(" + hue + ",100%, 60%)";
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
class Visualizer{
|
||||||
|
static drawNetwork(ctx,network){
|
||||||
|
const margin=50;
|
||||||
|
const left=margin;
|
||||||
|
const top=margin;
|
||||||
|
const width=ctx.canvas.width-margin*2;
|
||||||
|
const height=ctx.canvas.height-margin*2;
|
||||||
|
|
||||||
|
const levelHeight=height/network.levels.length;
|
||||||
|
|
||||||
|
for(let i=network.levels.length-1;i>=0;i--){
|
||||||
|
const levelTop=top+
|
||||||
|
lerp(
|
||||||
|
height-levelHeight,
|
||||||
|
0,
|
||||||
|
network.levels.length==1
|
||||||
|
?0.5
|
||||||
|
:i/(network.levels.length-1)
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.setLineDash([7,3]);
|
||||||
|
Visualizer.drawLevel(ctx,network.levels[i],
|
||||||
|
left,levelTop,
|
||||||
|
width,levelHeight,
|
||||||
|
i==network.levels.length-1
|
||||||
|
?['🠉','🠈','🠊','🠋']
|
||||||
|
:[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static drawLevel(ctx,level,left,top,width,height,outputLabels){
|
||||||
|
const right=left+width;
|
||||||
|
const bottom=top+height;
|
||||||
|
|
||||||
|
const {inputs,outputs,weights,biases}=level;
|
||||||
|
|
||||||
|
for(let i=0;i<inputs.length;i++){
|
||||||
|
for(let j=0;j<outputs.length;j++){
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(
|
||||||
|
Visualizer.#getNodeX(inputs,i,left,right),
|
||||||
|
bottom
|
||||||
|
);
|
||||||
|
ctx.lineTo(
|
||||||
|
Visualizer.#getNodeX(outputs,j,left,right),
|
||||||
|
top
|
||||||
|
);
|
||||||
|
ctx.lineWidth=2;
|
||||||
|
ctx.strokeStyle=getRGBA(weights[i][j]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeRadius=18;
|
||||||
|
for(let i=0;i<inputs.length;i++){
|
||||||
|
const x=Visualizer.#getNodeX(inputs,i,left,right);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x,bottom,nodeRadius,0,Math.PI*2);
|
||||||
|
ctx.fillStyle="black";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x,bottom,nodeRadius*0.6,0,Math.PI*2);
|
||||||
|
ctx.fillStyle=getRGBA(inputs[i]);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i=0;i<outputs.length;i++){
|
||||||
|
const x=Visualizer.#getNodeX(outputs,i,left,right);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x,top,nodeRadius,0,Math.PI*2);
|
||||||
|
ctx.fillStyle="black";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x,top,nodeRadius*0.6,0,Math.PI*2);
|
||||||
|
ctx.fillStyle=getRGBA(outputs[i]);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth=2;
|
||||||
|
ctx.arc(x,top,nodeRadius*0.8,0,Math.PI*2);
|
||||||
|
ctx.strokeStyle=getRGBA(biases[i]);
|
||||||
|
ctx.setLineDash([3,3]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
if(outputLabels[i]){
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.textAlign="center";
|
||||||
|
ctx.textBaseline="middle";
|
||||||
|
ctx.fillStyle="black";
|
||||||
|
ctx.strokeStyle="white";
|
||||||
|
ctx.font=(nodeRadius*1.5)+"px Arial";
|
||||||
|
ctx.fillText(outputLabels[i],x,top+nodeRadius*0.1);
|
||||||
|
ctx.lineWidth=0.5;
|
||||||
|
ctx.strokeText(outputLabels[i],x,top+nodeRadius*0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static #getNodeX(nodes,index,left,right){
|
||||||
|
return lerp(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
nodes.length==1
|
||||||
|
?0.5
|
||||||
|
:index/(nodes.length-1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
class Visualizer{
|
||||||
|
static drawNetwork(ctx, network){
|
||||||
|
const margin = 50;
|
||||||
|
const left = margin;
|
||||||
|
const top = margin;
|
||||||
|
const width = ctx.canvas.width - margin * 2;
|
||||||
|
const height = ctx.canvas.height - margin * 2;
|
||||||
|
|
||||||
|
Visualizer.drawLevel(ctx, network.levels[0], left, top, width, height);
|
||||||
|
}
|
||||||
|
static drawLevel(ctx, level, left, top, width, height){
|
||||||
|
const right = left + width;
|
||||||
|
const bottom = top + height;
|
||||||
|
|
||||||
|
const {inputs, outputs, weights, biases} = level;
|
||||||
|
|
||||||
|
const nodeRadius = 18;
|
||||||
|
for (let i = 0; i < inputs.length; i++){
|
||||||
|
const x = lerp(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
inputs.length == 1 ? 0.5 : i/(inputs.length - 1));
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, bottom, nodeRadius * 0.9, 0 , Math.PI * 2);
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = getRGBA(biases[i]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < outputs.length; i++){
|
||||||
|
const x = lerp(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
outputs.length == 1 ? 0.5 : i / (outputs.length - 1));
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, top, nodeRadius * 0.6, 0 , Math.PI * 2);
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.arc(x, top, nodeRadius, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = getRGBA(biases[i]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let i= 0; i < inputs.length; i++){
|
||||||
|
for (let j =0; j < outputs.length; j++){
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(Visualizer.#getNodeX(inputs, i, left,right),bottom);
|
||||||
|
ctx.lineTo(Visualizer.#getNodeX(outputs, j, left,right),top);
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = getRGBA(weights[i][j]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static #getNodeX(nodes, index, left, right){
|
||||||
|
return lerp(
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
nodes.length == 1 ? 0.5 : index / (nodes.length - 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user