Compare commits
7 Commits
evenlySpac
...
PWA
| Author | SHA1 | Date | |
|---|---|---|---|
|
fb8cf9bbfc
|
|||
|
402b147654
|
|||
|
e9d3b71f28
|
|||
|
e593efafa4
|
|||
|
7d02789a39
|
|||
|
b1b67fb0bd
|
|||
|
62bb28ca9d
|
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# MuneBase Swerve Sim
|
||||||
|
|
||||||
|
A browser-based simulator for Swerve Drive robots.
|
||||||
|
|
||||||
|
Live demo: https://swerve-vis.munebase.dev
|
||||||
|
|
||||||
|
|
||||||
|
You can host the project locally with a simple static server and open it in your browser:
|
||||||
|
|
||||||
|
- Python 3 (built-in):
|
||||||
|
|
||||||
|
python3 -m http.server 8000
|
||||||
|
|
||||||
|
Then open: http://localhost:8000
|
||||||
|
|
||||||
|
- Node.js (with npx):
|
||||||
|
|
||||||
|
npx http-server -c-1 .
|
||||||
|
|
||||||
|
Open `index.html` from the project root in your browser if your editor provides a static preview.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
|
||||||
|
- Third-party scripts are in the `vendor/` directory.
|
||||||
|
|
||||||
|
License
|
||||||
|
|
||||||
|
This repository is provided as-is.
|
||||||
22
icons/192.svg
Normal file
22
icons/192.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<svg width="192" height="192" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="192" height="192" fill="#16213e"/>
|
||||||
|
<circle cx="96" cy="96" r="70" fill="none" stroke="#0f3460" stroke-width="3"/>
|
||||||
|
|
||||||
|
<!-- Swerve modules (4 corners) -->
|
||||||
|
<circle cx="56" cy="56" r="12" fill="#e94560"/>
|
||||||
|
<circle cx="136" cy="56" r="12" fill="#e94560"/>
|
||||||
|
<circle cx="56" cy="136" r="12" fill="#e94560"/>
|
||||||
|
<circle cx="136" cy="136" r="12" fill="#e94560"/>
|
||||||
|
|
||||||
|
<!-- Velocity arrows -->
|
||||||
|
<line x1="56" y1="56" x2="56" y2="40" stroke="#ff6b6b" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<line x1="136" y1="56" x2="136" y2="40" stroke="#ff6b6b" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<line x1="56" y1="136" x2="56" y2="152" stroke="#ff6b6b" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<line x1="136" y1="136" x2="136" y2="152" stroke="#ff6b6b" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Center point -->
|
||||||
|
<circle cx="96" cy="96" r="6" fill="#4ecca3"/>
|
||||||
|
|
||||||
|
<!-- Robot frame -->
|
||||||
|
<polygon points="56,56 136,56 136,136 56,136" fill="none" stroke="#4ecca3" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
41
icons/512.svg
Normal file
41
icons/512.svg
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="512" height="512" fill="#16213e"/>
|
||||||
|
<circle cx="256" cy="256" r="190" fill="none" stroke="#0f3460" stroke-width="8"/>
|
||||||
|
|
||||||
|
<!-- Swerve modules (4 corners) -->
|
||||||
|
<circle cx="150" cy="150" r="32" fill="#e94560"/>
|
||||||
|
<circle cx="362" cy="150" r="32" fill="#e94560"/>
|
||||||
|
<circle cx="150" cy="362" r="32" fill="#e94560"/>
|
||||||
|
<circle cx="362" cy="362" r="32" fill="#e94560"/>
|
||||||
|
|
||||||
|
<!-- Velocity arrows -->
|
||||||
|
<line x1="150" y1="150" x2="150" y2="100" stroke="#ff6b6b" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<polygon points="150,90 140,110 160,110" fill="#ff6b6b"/>
|
||||||
|
|
||||||
|
<line x1="362" y1="150" x2="362" y2="100" stroke="#ff6b6b" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<polygon points="362,90 352,110 372,110" fill="#ff6b6b"/>
|
||||||
|
|
||||||
|
<line x1="150" y1="362" x2="150" y2="412" stroke="#ff6b6b" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<polygon points="150,422 140,402 160,402" fill="#ff6b6b"/>
|
||||||
|
|
||||||
|
<line x1="362" y1="362" x2="362" y2="412" stroke="#ff6b6b" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<polygon points="362,422 352,402 372,402" fill="#ff6b6b"/>
|
||||||
|
|
||||||
|
<!-- Center point -->
|
||||||
|
<circle cx="256" cy="256" r="16" fill="#4ecca3"/>
|
||||||
|
|
||||||
|
<!-- Robot frame -->
|
||||||
|
<polygon points="150,150 362,150 362,362 150,362" fill="none" stroke="#4ecca3" stroke-width="6"/>
|
||||||
|
|
||||||
|
<!-- Grid lines in background -->
|
||||||
|
<g opacity="0.2" stroke="#4ecca3" stroke-width="1">
|
||||||
|
<line x1="100" y1="100" x2="100" y2="412"/>
|
||||||
|
<line x1="200" y1="100" x2="200" y2="412"/>
|
||||||
|
<line x1="300" y1="100" x2="300" y2="412"/>
|
||||||
|
<line x1="400" y1="100" x2="400" y2="412"/>
|
||||||
|
<line x1="100" y1="100" x2="412" y2="100"/>
|
||||||
|
<line x1="100" y1="200" x2="412" y2="200"/>
|
||||||
|
<line x1="100" y1="300" x2="412" y2="300"/>
|
||||||
|
<line x1="100" y1="400" x2="412" y2="400"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
49
index.html
49
index.html
@ -4,7 +4,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#16213e">
|
||||||
|
<meta name="description"
|
||||||
|
content="Interactive simulation of a swerve drive robot with configurable size, speeds, and number of wheels">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icons/512.svg">
|
||||||
<title>Swerve Drive Visualizer</title>
|
<title>Swerve Drive Visualizer</title>
|
||||||
|
|
||||||
|
<!-- PWA Manifest -->
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -199,7 +207,7 @@ if scale < 1:
|
|||||||
<legend>Translation & Rotation</legend>
|
<legend>Translation & Rotation</legend>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button id="control-mode-toggle" type="button">Switch to Keyboard Controls (WASD + QE)</button>
|
<button id="control-mode-toggle" type="button">Switch to Keyboard/Joystick Controls</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="slider-controls">
|
<div id="slider-controls">
|
||||||
@ -313,6 +321,45 @@ if scale < 1:
|
|||||||
|
|
||||||
<script type="module" src="vendor/lucio/graham-scan.mjs"></script>
|
<script type="module" src="vendor/lucio/graham-scan.mjs"></script>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
|
|
||||||
|
<!-- Register Service Worker for PWA -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then((registration) => {
|
||||||
|
console.log('Service Worker registered successfully:', registration.scope);
|
||||||
|
|
||||||
|
// Check for updates periodically
|
||||||
|
setInterval(() => {
|
||||||
|
registration.update();
|
||||||
|
}, 60000); // Check every minute
|
||||||
|
|
||||||
|
// Handle updates
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
const newWorker = registration.installing;
|
||||||
|
newWorker.addEventListener('statechange', () => {
|
||||||
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
|
// New service worker available, prompt user to reload
|
||||||
|
if (confirm('A new version is available! Reload to update?')) {
|
||||||
|
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('Service Worker registration failed:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle controller change (new service worker activated)
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
22
manifest.json
Normal file
22
manifest.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "MuneBase Swerve Drive Sim",
|
||||||
|
"short_name": "Swerve Sim",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#16213e",
|
||||||
|
"background_color": "#16213e",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/192.svg",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/512.svg",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
218
script.js
218
script.js
@ -4,6 +4,81 @@
|
|||||||
|
|
||||||
import GrahamScan from "./vendor/lucio/graham-scan.mjs";
|
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 = rootStyles.getPropertyValue('--primary-dark-blue');
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(this.nubX, this.nubY, this.radius / 3, 0, Math.PI * 2);
|
||||||
|
this.ctx.fillStyle = rootStyles.getPropertyValue('--accent-blue');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.nubX = this.x;
|
||||||
|
this.nubY = this.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
|
// 2D vector class to make some of the math easier
|
||||||
class Vec2D {
|
class Vec2D {
|
||||||
constructor(x, y) {
|
constructor(x, y) {
|
||||||
@ -269,6 +344,15 @@ const PresetConfigs = {
|
|||||||
* BEGIN DOM VARIABLES
|
* 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
|
// Get all control elements
|
||||||
const vxSlider = document.getElementById('vx-slider');
|
const vxSlider = document.getElementById('vx-slider');
|
||||||
const vySlider = document.getElementById('vy-slider');
|
const vySlider = document.getElementById('vy-slider');
|
||||||
@ -379,17 +463,21 @@ controlModeToggle.addEventListener('click', () => {
|
|||||||
vxOutput.textContent = '0';
|
vxOutput.textContent = '0';
|
||||||
vyOutput.textContent = '0';
|
vyOutput.textContent = '0';
|
||||||
omegaOutput.textContent = '0';
|
omegaOutput.textContent = '0';
|
||||||
|
leftJoystick.setIsVisible(true);
|
||||||
|
rightJoystick.setIsVisible(true);
|
||||||
} else {
|
} else {
|
||||||
// Switch to slider mode
|
// Switch to slider mode
|
||||||
sliderControls.style.display = 'block';
|
sliderControls.style.display = 'block';
|
||||||
keyboardControls.style.display = 'none';
|
keyboardControls.style.display = 'none';
|
||||||
controlModeToggle.textContent = 'Switch to Keyboard Controls (WASD + QE)';
|
controlModeToggle.textContent = 'Switch to Keyboard/Joystick Controls';
|
||||||
|
|
||||||
// Reset manual input state
|
// Reset manual input state
|
||||||
Object.keys(keyState).forEach(key => keyState[key] = false);
|
Object.keys(keyState).forEach(key => keyState[key] = false);
|
||||||
manualInputVelX = 0;
|
manualInputVelX = 0;
|
||||||
manualInputVelY = 0;
|
manualInputVelY = 0;
|
||||||
manualInputOmega = 0;
|
manualInputOmega = 0;
|
||||||
|
leftJoystick.setIsVisible(false);
|
||||||
|
rightJoystick.setIsVisible(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -552,6 +640,105 @@ applyCustomBtn.addEventListener('click', () => {
|
|||||||
updateModuleDisplays(robot);
|
updateModuleDisplays(robot);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function convertTouchToCanvas(inCoords) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate the scale factor between display size and canvas internal size
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
// Convert client coordinates to canvas coordinates, accounting for scaling
|
||||||
|
const x = (inCoords.x - rect.left) * scaleX - canvas.width / 2;
|
||||||
|
const y = (inCoords.y - rect.top) * scaleY - 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch event listeners for mobile/tablet support
|
||||||
|
canvas.addEventListener('touchstart', (event) => {
|
||||||
|
event.preventDefault(); // Prevent scrolling and default touch behavior
|
||||||
|
|
||||||
|
for (let i = 0; i < event.touches.length; i++) {
|
||||||
|
const touch = event.touches[i];
|
||||||
|
const canvasCoords = convertTouchToCanvas({ x: touch.clientX, y: touch.clientY });
|
||||||
|
// alert(`X: ${canvasCoords.x}, Y: ${canvasCoords.y}`);
|
||||||
|
leftJoystick.checkShouldActivate(canvasCoords.x, canvasCoords.y);
|
||||||
|
rightJoystick.checkShouldActivate(canvasCoords.x, canvasCoords.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('touchmove', (event) => {
|
||||||
|
event.preventDefault(); // Prevent scrolling while using joysticks
|
||||||
|
|
||||||
|
for (let i = 0; i < event.touches.length; i++) {
|
||||||
|
const touch = event.touches[i];
|
||||||
|
const canvasCoords = convertTouchToCanvas({ x: touch.clientX, y: touch.clientY });
|
||||||
|
leftJoystick.processTouch(canvasCoords.x, canvasCoords.y);
|
||||||
|
rightJoystick.processTouch(canvasCoords.x, canvasCoords.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('touchend', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// If no touches remain, deactivate both joysticks
|
||||||
|
if (event.touches.length === 0) {
|
||||||
|
leftJoystick.deactivate();
|
||||||
|
rightJoystick.deactivate();
|
||||||
|
} else {
|
||||||
|
// Check if the remaining touches are still in range of the joysticks
|
||||||
|
let leftStillActive = false;
|
||||||
|
let rightStillActive = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < event.touches.length; i++) {
|
||||||
|
const touch = event.touches[i];
|
||||||
|
const canvasCoords = convertTouchToCanvas({ x: touch.clientX, y: touch.clientY });
|
||||||
|
|
||||||
|
if (leftJoystick.touchInRange(canvasCoords.x, canvasCoords.y)) {
|
||||||
|
leftStillActive = true;
|
||||||
|
}
|
||||||
|
if (rightJoystick.touchInRange(canvasCoords.x, canvasCoords.y)) {
|
||||||
|
rightStillActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!leftStillActive) {
|
||||||
|
leftJoystick.deactivate();
|
||||||
|
leftJoystick.reset();
|
||||||
|
}
|
||||||
|
if (!rightStillActive) {
|
||||||
|
rightJoystick.deactivate();
|
||||||
|
rightJoystick.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('touchcancel', (event) => {
|
||||||
|
// Handle touch cancellation (e.g., when system interrupts)
|
||||||
|
leftJoystick.deactivate();
|
||||||
|
leftJoystick.reset();
|
||||||
|
rightJoystick.deactivate();
|
||||||
|
rightJoystick.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* END LISTENER CODE
|
* END LISTENER CODE
|
||||||
* BEGIN DYNAMIC DOM FUNCTIONS
|
* BEGIN DYNAMIC DOM FUNCTIONS
|
||||||
@ -696,12 +883,8 @@ function updateModuleDisplays(robot) {
|
|||||||
* BEGIN ANIMATION CODE
|
* BEGIN ANIMATION CODE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get the canvas and context as constants
|
const leftJoystick = new Joystick(ctx, -250, 250, 100);
|
||||||
const canvas = document.getElementById('swerve-canvas');
|
const rightJoystick = new Joystick(ctx, 250, 250, 100);
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
// Get CSS variables for use in canvas
|
|
||||||
const rootStyles = getComputedStyle(document.documentElement);
|
|
||||||
|
|
||||||
function drawGrid(ctx, sideLength, gridSquareSize, xOffset, yOffset) {
|
function drawGrid(ctx, sideLength, gridSquareSize, xOffset, yOffset) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@ -806,8 +989,12 @@ function drawRobot(ctx, robot, heading) {
|
|||||||
ctx.restore(); // Restore to remove rotation
|
ctx.restore(); // Restore to remove rotation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Initialize Variables
|
// Initialize Variables
|
||||||
|
// Joysticks
|
||||||
|
const supportsTouch = (navigator.maxTouchPoints > 0);
|
||||||
|
|
||||||
|
|
||||||
|
// General robot
|
||||||
const robotSize = 200;
|
const robotSize = 200;
|
||||||
const defaultModulePositions = PresetConfigs.fourWheelSquare(robotSize);
|
const defaultModulePositions = PresetConfigs.fourWheelSquare(robotSize);
|
||||||
const robot = new SwerveDrive(defaultModulePositions, "4-Wheel Square");
|
const robot = new SwerveDrive(defaultModulePositions, "4-Wheel Square");
|
||||||
@ -829,9 +1016,15 @@ function animate() {
|
|||||||
|
|
||||||
// Update speeds based on control mode
|
// Update speeds based on control mode
|
||||||
if (isManualInputMode) {
|
if (isManualInputMode) {
|
||||||
xSpeed = manualInputVelX;
|
const maxSpeed = parseFloat(keyboardMaxSpeed.value);
|
||||||
ySpeed = -manualInputVelY; // Negative because canvas Y axis is inverted
|
const maxRotation = parseFloat(keyboardMaxRotation.value);
|
||||||
turnSpeed = manualInputOmega;
|
// 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 {
|
} else {
|
||||||
xSpeed = parseFloat(vxSlider.value);
|
xSpeed = parseFloat(vxSlider.value);
|
||||||
ySpeed = -parseFloat(vySlider.value);
|
ySpeed = -parseFloat(vySlider.value);
|
||||||
@ -870,6 +1063,9 @@ function animate() {
|
|||||||
drawGrid(ctx, canvas.width * 2, gridSquareSize, xGridOffset, yGridOffset);
|
drawGrid(ctx, canvas.width * 2, gridSquareSize, xGridOffset, yGridOffset);
|
||||||
drawRobot(ctx, robot, robot.gyroHeading);
|
drawRobot(ctx, robot, robot.gyroHeading);
|
||||||
|
|
||||||
|
leftJoystick.draw();
|
||||||
|
rightJoystick.draw();
|
||||||
|
|
||||||
// Do it all over again
|
// Do it all over again
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|||||||
@ -361,4 +361,10 @@ button:hover {
|
|||||||
.readout .value {
|
.readout .value {
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
33
sw.js
Normal file
33
sw.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const CACHE_NAME = 'v1';
|
||||||
|
const ASSETS = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/script.js',
|
||||||
|
'/styles.css',
|
||||||
|
'/manifest.json',
|
||||||
|
'/icons/192.svg',
|
||||||
|
'/icons/512.svg',
|
||||||
|
'/vendor/lucio/graham-scan.mjs'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (e) => {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (e) => {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.keys().then(keys =>
|
||||||
|
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (e) => {
|
||||||
|
e.respondWith(
|
||||||
|
caches.match(e.request).then(response => response || fetch(e.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user