180 lines
4.6 KiB
JavaScript
180 lines
4.6 KiB
JavaScript
export class LineFollowerSim {
|
|
constructor(canvas, updateBoxFn) {
|
|
this.canvas = canvas;
|
|
this.ctx = canvas.getContext("2d");
|
|
|
|
this.box = {
|
|
x: 250,
|
|
y: 150,
|
|
width: 40,
|
|
height: 20,
|
|
speed: 0.4,
|
|
dir: 0
|
|
};
|
|
|
|
this.lineX = 250;
|
|
this.lineTargetX = 250;
|
|
this.lineWidth = 12;
|
|
|
|
|
|
const bbThresholdSlider = document.getElementById("bbThresholdSlider");
|
|
const bbTurnSpeedSlider = document.getElementById("bbTurnSpeedSlider");
|
|
const bbThresholdValue = document.getElementById("bbThresholdValue");
|
|
const bbTurnSpeedValue = document.getElementById("bbTurnSpeedValue");
|
|
this.threshold = parseFloat(bbThresholdSlider.value);
|
|
|
|
const resetBtn = document.getElementById("resetBangBangBtn");
|
|
resetBtn.addEventListener("click", () => {
|
|
// Put the box instantly on the line
|
|
this.box.x = this.lineX;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
this.lastUpdateTime = 0;
|
|
this.updateInterval = 50; // ms
|
|
|
|
|
|
this.updateBox = updateBoxFn || this.defaultUpdateBox.bind(this);
|
|
this.loop = this.loop.bind(this);
|
|
}
|
|
|
|
updateCodeBlock() {
|
|
this.code = `
|
|
if left_color < ${this.threshold}:
|
|
motor(-${this.turnAmount}, ${this.turnAmount}) # Turn left
|
|
elif right_color < ${this.threshold}:
|
|
motor(${this.turnAmount}, -${this.turnAmount}) # Turn right
|
|
else:
|
|
motor(50, 50) # Go straight
|
|
`
|
|
document.getElementById('code-block').textContent = this.code;
|
|
}
|
|
|
|
updateLine() {
|
|
if (Math.random() < 0.02) {
|
|
this.lineTargetX = 250 + (Math.random() - 0.5) * 200;
|
|
}
|
|
|
|
this.lineX += (this.lineTargetX - this.lineX) * 0.0025;
|
|
}
|
|
|
|
lineProximity(x, lineX) {
|
|
const distance = Math.abs(x - lineX);
|
|
const maxDistance = 20;
|
|
const maxValue = 1024;
|
|
|
|
if (distance > maxDistance) return 0;
|
|
|
|
// Reverse the falloff: closer = lower, farther = higher
|
|
const value = 100 + (distance / maxDistance) * (maxValue - 100);
|
|
return Math.round(value);
|
|
}
|
|
|
|
|
|
defaultUpdateBox() {
|
|
|
|
const now = performance.now();
|
|
if (now - this.lastUpdateTime < this.updateInterval) return;
|
|
this.lastUpdateTime = now;
|
|
|
|
|
|
const bbLeftColorValue = document.getElementById("leftColor");
|
|
const bbRightColorValue = document.getElementById("rightColor");
|
|
|
|
const bbThresh = parseFloat(bbThresholdSlider.value);
|
|
const bbTurnSpeed = parseFloat(bbTurnSpeedSlider.value);
|
|
|
|
bbThresholdValue.textContent = bbThresh.toFixed(0);
|
|
bbTurnSpeedValue.textContent = bbTurnSpeed.toFixed(2);
|
|
this.threshold = bbThresh.toFixed(0);
|
|
this.turnAmount = bbTurnSpeed.toFixed(0);
|
|
const leftSensor = this.box.x - this.box.width / 2;
|
|
const rightSensor = this.box.x + this.box.width / 2;
|
|
|
|
this.updateCodeBlock();
|
|
|
|
const onLine = x => x >= this.lineX - this.lineWidth / 2 && x <= this.lineX + this.lineWidth / 2;
|
|
|
|
let leftColor = this.lineProximity(leftSensor, this.lineX);
|
|
let rightColor = this.lineProximity(rightSensor, this.lineX);
|
|
if (leftColor == 0) {
|
|
leftColor = 1024; // No line detected
|
|
}
|
|
if (rightColor == 0) {
|
|
rightColor = 1024; // No line detected
|
|
}
|
|
|
|
bbLeftColorValue.textContent = leftColor;
|
|
bbRightColorValue.textContent = rightColor;
|
|
|
|
const leftOnLine = onLine(leftSensor);
|
|
const rightOnLine = onLine(rightSensor);
|
|
|
|
if (leftColor < bbThresh) {
|
|
this.box.dir -= (bbTurnSpeed * 0.01);
|
|
if (this.box.dir < -5) {
|
|
this.box.dir = -5; // Limit the turn speed
|
|
}
|
|
} else if (rightColor < bbThresh) {
|
|
this.box.dir += bbTurnSpeed * 0.01;
|
|
if (this.box.dir > 5) {
|
|
this.box.dir = 5; // Limit the turn speed
|
|
}
|
|
} else {
|
|
|
|
if (this.box.dir > 1) {
|
|
this.box.dir -= 0.1;
|
|
} else if (this.box.dir < -1) {
|
|
this.box.dir += 0.1;
|
|
} else {
|
|
this.box.dir = 0;
|
|
}
|
|
}
|
|
console.log(this.box.dir);
|
|
|
|
this.box.x += this.box.dir * this.box.speed;
|
|
}
|
|
|
|
draw() {
|
|
const ctx = this.ctx;
|
|
const { x, y, width, height } = this.box;
|
|
|
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Line
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.lineX, 0);
|
|
ctx.lineTo(this.lineX, this.canvas.height);
|
|
ctx.strokeStyle = "blue";
|
|
ctx.lineWidth = this.lineWidth;
|
|
ctx.stroke();
|
|
|
|
// Box
|
|
ctx.fillStyle = "red";
|
|
ctx.fillRect(x - width / 2, y - height / 2, width, height);
|
|
|
|
// Sensors
|
|
ctx.fillStyle = "black";
|
|
ctx.beginPath();
|
|
ctx.arc(x - width / 2, y - height / 2, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(x + width / 2, y - height / 2, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
loop() {
|
|
this.updateLine();
|
|
this.updateBox();
|
|
this.draw();
|
|
requestAnimationFrame(this.loop);
|
|
}
|
|
|
|
start() {
|
|
this.loop();
|
|
}
|
|
}
|