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(); } }