Compare commits
9 Commits
e593efafa4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
de733edf48
|
|||
|
c72b6e5f1a
|
|||
|
64bd500161
|
|||
|
77bcfd3196
|
|||
|
c1aa7d2086
|
|||
|
c225ee696a
|
|||
|
fb8cf9bbfc
|
|||
|
402b147654
|
|||
|
e9d3b71f28
|
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 |
105
index.html
105
index.html
@ -4,7 +4,15 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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>
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
@ -24,7 +32,43 @@
|
||||
velocities. Use the controls to experiment with different configurations and movement patterns.
|
||||
</p>
|
||||
|
||||
<h3>Drive Controls</h3>
|
||||
<h3>Control Modes</h3>
|
||||
<p>The simulator offers two control modes. Switch between them using the <strong>Switch to
|
||||
Keyboard/Joystick Controls</strong> or <strong>Switch to Slider Controls</strong> button.
|
||||
</p>
|
||||
|
||||
<h4>Keyboard/Joystick Mode (Default)</h4>
|
||||
<p>Control the robot using keyboard keys or on-screen joysticks (if on a touch enabled device):</p>
|
||||
<ul>
|
||||
<li><strong>Keyboard Controls:</strong>
|
||||
<ul>
|
||||
<li><strong>W:</strong> Move Forward</li>
|
||||
<li><strong>A:</strong> Strafe Left</li>
|
||||
<li><strong>S:</strong> Move Backward</li>
|
||||
<li><strong>D:</strong> Strafe Right</li>
|
||||
<li><strong>Q:</strong> Rotate Counter-Clockwise</li>
|
||||
<li><strong>E:</strong> Rotate Clockwise</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>On-Screen Joysticks (Touch enabled devices only):</strong>
|
||||
<ul>
|
||||
<li><strong>Left Joystick:</strong> Controls translation (movement in X and Y
|
||||
directions)</li>
|
||||
<li><strong>Right Joystick:</strong> Controls rotation (turning)</li>
|
||||
<li>Touch or click and drag within the joystick circles to control the robot</li>
|
||||
<li>Joysticks take priority when active; otherwise, keyboard controls are used</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Max Speed:</strong> Sets the maximum translation speed for keyboard/joystick input
|
||||
</li>
|
||||
<li><strong>Max Rotation:</strong> Sets the maximum rotation speed for keyboard/joystick input
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<h4>Slider Mode</h4>
|
||||
<p>Use sliders to set precise velocity values:</p>
|
||||
<ul>
|
||||
<li><strong>Strafe Left/Right:</strong> Controls the robot's velocity in the X direction
|
||||
(field-relative). Positive values move right, negative values move left.</li>
|
||||
@ -32,11 +76,16 @@
|
||||
(field-relative). Positive values move forward, negative values move backward.</li>
|
||||
<li><strong>Rotation:</strong> Controls the robot's angular velocity (turn rate) in radians per
|
||||
second. Positive values rotate counter-clockwise.</li>
|
||||
<li><strong>Max Module Speed:</strong> Sets the maximum speed limit for any individual swerve
|
||||
module. If calculated speeds exceed this, all modules are scaled proportionally.</li>
|
||||
<li><strong>Reset Controls:</strong> Returns all velocity sliders to zero.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Performance Limits</h3>
|
||||
<ul>
|
||||
<li><strong>Max Module Speed:</strong> Sets the maximum speed limit for any individual swerve
|
||||
module. If calculated speeds exceed this, all modules are scaled proportionally to maintain
|
||||
the intended direction of movement.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Preset Configurations</h3>
|
||||
<p>Choose from 9 pre-built robot configurations ranging from 2 to 16 wheels. Each preset
|
||||
demonstrates different module arrangements:</p>
|
||||
@ -199,10 +248,10 @@ if scale < 1:
|
||||
<legend>Translation & Rotation</legend>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="control-mode-toggle" type="button">Switch to Keyboard/Joystick Controls</button>
|
||||
<button id="control-mode-toggle" type="button">Switch to Slider Controls</button>
|
||||
</div>
|
||||
|
||||
<div id="slider-controls">
|
||||
<div id="slider-controls" style="display: none;">
|
||||
<div class="control-group">
|
||||
<label for="vy-slider">Move Forward/Backward (pixels/s)</label>
|
||||
<input type="range" id="vy-slider" min="-300" max="300" step="10" value="0">
|
||||
@ -224,7 +273,7 @@ if scale < 1:
|
||||
<button id="reset-btn" type="button">Reset Controls</button>
|
||||
</div>
|
||||
|
||||
<div id="keyboard-controls" style="display: none;">
|
||||
<div id="keyboard-controls">
|
||||
<div class="control-group">
|
||||
<label for="keyboard-max-speed">Max Speed (pixels/s)</label>
|
||||
<input type="range" id="keyboard-max-speed" min="50" max="300" step="10" value="150">
|
||||
@ -309,10 +358,54 @@ if scale < 1:
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<footer id="site-footer">
|
||||
<a href="https://git.munebase.dev/Munelit/swerve-visualizer">Source for this site can be found here.</a>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="vendor/lucio/graham-scan.mjs"></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>
|
||||
|
||||
</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"
|
||||
}
|
||||
]
|
||||
}
|
||||
47
script.js
47
script.js
@ -5,7 +5,7 @@
|
||||
import GrahamScan from "./vendor/lucio/graham-scan.mjs";
|
||||
|
||||
class Joystick {
|
||||
constructor(ctx, x, y, radius) {
|
||||
constructor(ctx, x, y, radius, visibleByDefault) {
|
||||
this.ctx = ctx;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
@ -13,7 +13,7 @@ class Joystick {
|
||||
this.nubX = x;
|
||||
this.nubY = y;
|
||||
this.active = false;
|
||||
this.visible = false;
|
||||
this.visible = visibleByDefault;
|
||||
}
|
||||
|
||||
draw() {
|
||||
@ -45,9 +45,13 @@ class Joystick {
|
||||
|
||||
deactivate() {
|
||||
this.active = false;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
touchInRange(x, y) {
|
||||
if (!this.visible)
|
||||
return false;
|
||||
|
||||
const deltaX = x - this.x;
|
||||
const deltaY = y - this.y;
|
||||
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
@ -398,7 +402,7 @@ const preset16OctBtn = document.getElementById('preset-16oct');
|
||||
*/
|
||||
|
||||
// Control mode state
|
||||
let isManualInputMode = false; // true = keyboard/gamepad mode, false = slider mode
|
||||
let isManualInputMode = true; // true = keyboard/gamepad mode, false = slider mode
|
||||
|
||||
// Keyboard state tracking
|
||||
const keyState = {
|
||||
@ -446,6 +450,9 @@ keyboardMaxRotation.addEventListener('input', (e) => {
|
||||
});
|
||||
keyboardMaxRotationOutput.textContent = parseFloat(keyboardMaxRotation.value);
|
||||
|
||||
|
||||
const supportsTouch = (navigator.maxTouchPoints > 0);
|
||||
|
||||
// Control mode toggle
|
||||
controlModeToggle.addEventListener('click', () => {
|
||||
isManualInputMode = !isManualInputMode;
|
||||
@ -463,13 +470,15 @@ controlModeToggle.addEventListener('click', () => {
|
||||
vxOutput.textContent = '0';
|
||||
vyOutput.textContent = '0';
|
||||
omegaOutput.textContent = '0';
|
||||
leftJoystick.setIsVisible(true);
|
||||
rightJoystick.setIsVisible(true);
|
||||
if (supportsTouch) {
|
||||
leftJoystick.setIsVisible(true);
|
||||
rightJoystick.setIsVisible(true);
|
||||
}
|
||||
} else {
|
||||
// Switch to slider mode
|
||||
sliderControls.style.display = 'block';
|
||||
keyboardControls.style.display = 'none';
|
||||
controlModeToggle.textContent = 'Switch to Keyboard/Joystick Controls';
|
||||
controlModeToggle.textContent = supportsTouch ? 'Switch to Keyboard/Joystick Controls' : 'Switch to Keyboard';
|
||||
|
||||
// Reset manual input state
|
||||
Object.keys(keyState).forEach(key => keyState[key] = false);
|
||||
@ -883,8 +892,8 @@ function updateModuleDisplays(robot) {
|
||||
* BEGIN ANIMATION CODE
|
||||
*/
|
||||
|
||||
const leftJoystick = new Joystick(ctx, -250, 250, 100);
|
||||
const rightJoystick = new Joystick(ctx, 250, 250, 100);
|
||||
const leftJoystick = new Joystick(ctx, -250, 250, 100, supportsTouch);
|
||||
const rightJoystick = new Joystick(ctx, 250, 250, 100, supportsTouch);
|
||||
|
||||
function drawGrid(ctx, sideLength, gridSquareSize, xOffset, yOffset) {
|
||||
ctx.save();
|
||||
@ -990,9 +999,6 @@ function drawRobot(ctx, robot, heading) {
|
||||
}
|
||||
|
||||
// Initialize Variables
|
||||
// Joysticks
|
||||
const supportsTouch = (navigator.maxTouchPoints > 0);
|
||||
|
||||
|
||||
// General robot
|
||||
const robotSize = 200;
|
||||
@ -1018,13 +1024,20 @@ function animate() {
|
||||
if (isManualInputMode) {
|
||||
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;
|
||||
// Combine keyboard and joystick inputs
|
||||
const keyboardX = manualInputVelX;
|
||||
const keyboardY = -manualInputVelY; // Negative because canvas Y axis is inverted
|
||||
const keyboardOmega = manualInputOmega;
|
||||
|
||||
const joystickX = leftJoystick.getX() * maxSpeed;
|
||||
const joystickY = -leftJoystick.getY() * maxSpeed;
|
||||
const joystickOmega = rightJoystick.getX() * maxRotation;
|
||||
|
||||
// Use joystick if active, otherwise use keyboard
|
||||
xSpeed = leftJoystick.active ? joystickX : keyboardX;
|
||||
ySpeed = leftJoystick.active ? joystickY : keyboardY;
|
||||
turnSpeed = rightJoystick.active ? joystickOmega : keyboardOmega;
|
||||
} else {
|
||||
xSpeed = parseFloat(vxSlider.value);
|
||||
ySpeed = -parseFloat(vySlider.value);
|
||||
|
||||
@ -232,6 +232,8 @@ tr:hover {
|
||||
margin-top: var(--spacing-large);
|
||||
border-top: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
grid-row: 4 / 5;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
/* Additions for this page */
|
||||
|
||||
33
sw.js
Normal file
33
sw.js
Normal file
@ -0,0 +1,33 @@
|
||||
const CACHE_NAME = 'v2';
|
||||
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