diff --git a/script.js b/script.js index b4c8ee9..56be202 100644 --- a/script.js +++ b/script.js @@ -4,6 +4,76 @@ import GrahamScan from "./vendor/lucio/graham-scan.mjs"; +class Joystick { + constructor(ctx, x, y, radius) { + this.ctx = ctx; + this.x = x; + this.y = y; + this.radius = radius; + this.nubX = x; + this.nubY = y; + this.active = false; + this.visible = false; + } + + draw() { + if (!this.visible) + return; + + this.ctx.save(); + + this.ctx.beginPath(); + this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + this.ctx.fillStyle = '#5554'; + this.ctx.fill(); + + + this.ctx.beginPath(); + this.ctx.arc(this.nubX, this.nubY, this.radius / 3, 0, Math.PI * 2); + this.ctx.fillStyle = '#445bcaff'; + this.ctx.fill(); + + this.ctx.restore(); + } + + checkShouldActivate(x, y) { + if (!this.touchInRange(x, y)) + return; + this.active = true; + this.processTouch(x, y); + } + + deactivate() { + this.active = false; + } + + touchInRange(x, y) { + const deltaX = x - this.x; + const deltaY = y - this.y; + const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + return dist <= this.radius; + } + + processTouch(x, y) { + if (this.touchInRange(x, y) && this.active) { + this.nubX = x; + this.nubY = y; + } + } + + getX() { + return (this.nubX - this.x) / this.radius; + } + + getY() { + return (this.y - this.nubY) / this.radius; + } + + setIsVisible(visible) { + this.visible = visible; + } +} + // 2D vector class to make some of the math easier class Vec2D { constructor(x, y) { @@ -269,6 +339,15 @@ const PresetConfigs = { * BEGIN DOM VARIABLES */ +// Get canvas +const canvas = document.getElementById('swerve-canvas'); + +// Get the canvas context as constant +const ctx = canvas.getContext('2d'); + +// Get CSS variables for use in canvas +const rootStyles = getComputedStyle(document.documentElement); + // Get all control elements const vxSlider = document.getElementById('vx-slider'); const vySlider = document.getElementById('vy-slider'); @@ -379,6 +458,8 @@ controlModeToggle.addEventListener('click', () => { vxOutput.textContent = '0'; vyOutput.textContent = '0'; omegaOutput.textContent = '0'; + leftJoystick.setIsVisible(true); + rightJoystick.setIsVisible(true); } else { // Switch to slider mode sliderControls.style.display = 'block'; @@ -390,6 +471,8 @@ controlModeToggle.addEventListener('click', () => { manualInputVelX = 0; manualInputVelY = 0; manualInputOmega = 0; + leftJoystick.setIsVisible(false); + rightJoystick.setIsVisible(false); } }); @@ -552,6 +635,30 @@ applyCustomBtn.addEventListener('click', () => { updateModuleDisplays(robot); }); +function convertTouchToCanvas(inCoords) { + const rect = canvas.getBoundingClientRect(); + const x = inCoords.x - rect.left - canvas.width / 2; + const y = inCoords.y - rect.top - canvas.height / 2; + return { x, y }; +} + +canvas.addEventListener('mousedown', (event) => { + const canvasCoords = convertTouchToCanvas({ x: event.clientX, y: event.clientY }); + leftJoystick.checkShouldActivate(canvasCoords.x, canvasCoords.y); + rightJoystick.checkShouldActivate(canvasCoords.x, canvasCoords.y); +}); + +canvas.addEventListener('mousemove', (event) => { + const canvasCoords = convertTouchToCanvas({ x: event.clientX, y: event.clientY }); + leftJoystick.processTouch(canvasCoords.x, canvasCoords.y); + rightJoystick.processTouch(canvasCoords.x, canvasCoords.y); +}); + +canvas.addEventListener('mouseup', (event) => { + leftJoystick.deactivate(); + rightJoystick.deactivate(); +}); + /* * END LISTENER CODE * BEGIN DYNAMIC DOM FUNCTIONS @@ -696,12 +803,8 @@ function updateModuleDisplays(robot) { * BEGIN ANIMATION CODE */ -// Get the canvas and context as constants -const canvas = document.getElementById('swerve-canvas'); -const ctx = canvas.getContext('2d'); - -// Get CSS variables for use in canvas -const rootStyles = getComputedStyle(document.documentElement); +const leftJoystick = new Joystick(ctx, -250, 250, 100); +const rightJoystick = new Joystick(ctx, 250, 250, 100); function drawGrid(ctx, sideLength, gridSquareSize, xOffset, yOffset) { ctx.save(); @@ -806,8 +909,12 @@ function drawRobot(ctx, robot, heading) { ctx.restore(); // Restore to remove rotation } - // Initialize Variables +// Joysticks +const supportsTouch = (navigator.maxTouchPoints > 0); + + +// General robot const robotSize = 200; const defaultModulePositions = PresetConfigs.fourWheelSquare(robotSize); const robot = new SwerveDrive(defaultModulePositions, "4-Wheel Square"); @@ -827,11 +934,20 @@ function animate() { ctx.save(); ctx.translate(canvas.width / 2, canvas.height / 2); + leftJoystick.draw(); + rightJoystick.draw(); + // Update speeds based on control mode if (isManualInputMode) { - xSpeed = manualInputVelX; - ySpeed = -manualInputVelY; // Negative because canvas Y axis is inverted - turnSpeed = manualInputOmega; + const maxSpeed = parseFloat(keyboardMaxSpeed.value); + const maxRotation = parseFloat(keyboardMaxRotation.value); + // xSpeed = manualInputVelX; + // ySpeed = -manualInputVelY; // Negative because canvas Y axis is inverted + // turnSpeed = manualInputOmega; + + xSpeed = leftJoystick.getX() * maxSpeed; + ySpeed = -leftJoystick.getY() * maxSpeed; + turnSpeed = rightJoystick.getX() * maxRotation; } else { xSpeed = parseFloat(vxSlider.value); ySpeed = -parseFloat(vySlider.value);