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