a scrappy gimbal that insults you in shakespearean english

feat: use joystick

+69 -28
+2 -2
server/index.ts
··· 8 8 9 9 app.post("/motor1", (req, res) => { 10 10 const speed = req.body.speed; 11 - const command = `1 ${speed}\n`; 11 + const command = `1 ${speed}\r`; 12 12 serialPort.write(command); 13 13 res.send("OK"); 14 14 }); 15 15 16 16 app.post("/motor2", (req, res) => { 17 17 const speed = req.body.speed; 18 - const command = `2 ${speed}\n`; 18 + const command = `2 ${speed}\r`; 19 19 serialPort.write(command); 20 20 res.send("OK"); 21 21 });
+67 -26
src/index.ts
··· 5 5 let videoElement: HTMLVideoElement; 6 6 let lastError = 0; 7 7 let integral = 0; 8 + let isDragging = false; 9 + let joystickX = 0; 10 + let joystickY = 0; 8 11 9 12 // PID constants 10 13 const Kp = 0.5; ··· 26 29 <video id="webcam" width="640" height="480" autoplay muted></video> 27 30 <canvas id="overlay" style="position: absolute;"></canvas> 28 31 29 - <div> 30 - <label>Left Motor (1) Rotations:</label> 31 - <input type="range" id="motor1" min="-10" max="10" step="0.1" value="0"> 32 - <span id="motor1Value">0</span> 33 - <button id="send1">Send</button> 32 + <div id="joystick" style="width: 200px; height: 200px; border: 2px solid black; border-radius: 50%; position: relative; margin: 20px;"> 33 + <div id="joystickHandle" style="width: 20px; height: 20px; background: red; border-radius: 50%; position: absolute; top: 90px; left: 90px; cursor: pointer;"></div> 34 34 </div> 35 - 36 35 <div> 37 - <label>Right Motor (2) Rotations:</label> 38 - <input type="range" id="motor2" min="-10" max="10" step="0.1" value="0"> 39 - <span id="motor2Value">0</span> 40 - <button id="send2">Send</button> 36 + <span>Motor Values - X: </span><span id="motor1Value">0</span> 37 + <span>, Y: </span><span id="motor2Value">0</span> 38 + <button id="sendJoystick">Send</button> 41 39 </div> 42 40 </div> 43 41 <div style="width: 300px; padding: 20px; border-left: 1px solid #ccc; overflow-y: auto;"> ··· 174 172 } 175 173 } 176 174 175 + function updateJoystickPosition(x: number, y: number) { 176 + const joystick = document.getElementById("joystick"); 177 + const handle = document.getElementById("joystickHandle"); 178 + if (!joystick || !handle) return; 179 + 180 + const bounds = joystick.getBoundingClientRect(); 181 + const radius = bounds.width / 2; 182 + 183 + // Calculate relative position from center 184 + const relX = x - bounds.left - radius; 185 + const relY = y - bounds.top - radius; 186 + 187 + // Calculate distance from center 188 + const distance = Math.sqrt(relX * relX + relY * relY); 189 + 190 + // Normalize to radius if outside circle 191 + const normalizedX = distance > radius ? (relX / distance) * radius : relX; 192 + const normalizedY = distance > radius ? (relY / distance) * radius : relY; 193 + 194 + // Update handle position 195 + handle.style.left = normalizedX + radius - 10 + "px"; 196 + handle.style.top = normalizedY + radius - 10 + "px"; 197 + 198 + // Update values (-0.5 to 0.5 range) 199 + joystickX = normalizedX / (radius * 2); 200 + joystickY = normalizedY / (radius * 2); 201 + 202 + document.getElementById("motor1Value")!.textContent = joystickX.toFixed(2); 203 + document.getElementById("motor2Value")!.textContent = joystickY.toFixed(2); 204 + } 205 + 177 206 function defaultPageRender() { 178 207 const app = document.querySelector<HTMLDivElement>("#app"); 179 208 if (!app) throw new Error("App element not found"); ··· 181 210 182 211 videoElement = document.getElementById("webcam") as HTMLVideoElement; 183 212 184 - document.getElementById("motor1")?.addEventListener("input", (e) => { 185 - const value = (e.target as HTMLInputElement).value; 186 - const display = document.getElementById("motor1Value"); 187 - if (display) display.textContent = value; 188 - }); 213 + const joystick = document.getElementById("joystick"); 214 + const handle = document.getElementById("joystickHandle"); 215 + 216 + if (joystick && handle) { 217 + handle.addEventListener("mousedown", () => { 218 + isDragging = true; 219 + }); 220 + 221 + document.addEventListener("mousemove", (e) => { 222 + if (isDragging) { 223 + updateJoystickPosition(e.clientX, e.clientY); 224 + } 225 + }); 189 226 190 - document.getElementById("motor2")?.addEventListener("input", (e) => { 191 - const value = (e.target as HTMLInputElement).value; 192 - const display = document.getElementById("motor2Value"); 193 - if (display) display.textContent = value; 194 - }); 227 + document.addEventListener("mouseup", () => { 228 + if (isDragging) { 229 + isDragging = false; 230 + } 231 + }); 232 + } 195 233 196 234 document.getElementById("connect")?.addEventListener("click", connectSerial); 197 235 document ··· 201 239 await startWebcam(); 202 240 trackFaces(); 203 241 }); 204 - document.getElementById("send1")?.addEventListener("click", () => { 205 - const value = (document.getElementById("motor1") as HTMLInputElement).value; 206 - sendMotorCommand(1, parseFloat(value)); 242 + document.getElementById("sendJoystick")?.addEventListener("click", () => { 243 + sendMotorCommand(1, joystickX); 244 + sendMotorCommand(2, joystickY); 207 245 }); 208 - document.getElementById("send2")?.addEventListener("click", () => { 209 - const value = (document.getElementById("motor2") as HTMLInputElement).value; 210 - sendMotorCommand(2, parseFloat(value)); 246 + 247 + document.addEventListener("keydown", (e) => { 248 + if (e.key === "Enter") { 249 + sendMotorCommand(1, joystickX); 250 + sendMotorCommand(2, joystickY); 251 + } 211 252 }); 212 253 } 213 254