411 lines
22 KiB
HTML
411 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<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>
|
||
|
||
<body>
|
||
<header>
|
||
<h1>Swerve Drive Movement Visualizer</h1>
|
||
<p>Interactive simulation of a swerve drive robot with configurable size, speeds, and number of wheels</p>
|
||
</header>
|
||
<main>
|
||
<section class="documentation">
|
||
<h2>About This Project</h2>
|
||
<details>
|
||
<summary>How To Use</summary>
|
||
<div class="documentation-content">
|
||
<h3>Getting Started</h3>
|
||
<p>This interactive visualizer demonstrates how a swerve drive robot moves based on commanded
|
||
velocities. Use the controls to experiment with different configurations and movement patterns.
|
||
</p>
|
||
|
||
<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>
|
||
<li><strong>Move Forward/Backward:</strong> Controls the robot's velocity in the Y direction
|
||
(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>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>
|
||
<ul>
|
||
<li><strong>2-Wheel:</strong> Differential drive arrangement</li>
|
||
<li><strong>3-Wheel Triangle:</strong> Three modules in an equilateral triangle</li>
|
||
<li><strong>4-Wheel Square:</strong> Classic square configuration</li>
|
||
<li><strong>4-Wheel Rectangle:</strong> Rectangular configuration for longer robots</li>
|
||
<li><strong>6-Wheel Hexagon:</strong> Hexagonal arrangement</li>
|
||
<li><strong>8-Wheel Octagon:</strong> Octagonal arrangement</li>
|
||
<li><strong>8-Wheel Square:</strong> Double-layered square with inner and outer modules</li>
|
||
<li><strong>12-Wheel Hexagon:</strong> Double-layered hexagonal arrangement</li>
|
||
<li><strong>16-Wheel Octagon:</strong> Double-layered octagonal arrangement</li>
|
||
</ul>
|
||
|
||
<h3>Custom Configurations</h3>
|
||
<p>Create your own robot configuration:</p>
|
||
<ol>
|
||
<li>Enter the desired number of modules (Minimum of 2)</li>
|
||
<li>Click <strong>Generate Position Inputs</strong> to create input fields</li>
|
||
<li>For each module, specify:
|
||
<ul>
|
||
<li><strong>Module Name:</strong> A label for the module</li>
|
||
<li><strong>X Position:</strong> Distance from robot center (pixels, positive = right)
|
||
</li>
|
||
<li><strong>Y Position:</strong> Distance from robot center (pixels, positive = up)</li>
|
||
</ul>
|
||
</li>
|
||
<li>Click <strong>Apply Custom Configuration</strong> to see your design</li>
|
||
<li>Use <strong>Remove Position Inputs</strong> to clear the custom fields. This does not reset
|
||
the robot, only clears the input box</li>
|
||
</ol>
|
||
|
||
<h3>Understanding the Visualization</h3>
|
||
<ul>
|
||
<li><strong>Robot Frame:</strong> The filled polygon connecting the outer-most module positions
|
||
</li>
|
||
<li><strong>Modules:</strong> Circular markers at each wheel position</li>
|
||
<li><strong>Velocity Arrows:</strong> Red arrows showing the direction and magnitude of each
|
||
module's velocity</li>
|
||
<li><strong>Grid:</strong> Moves relative to the robot to show field-relative motion</li>
|
||
<li><strong>Gyro Heading:</strong> The current rotation angle of the robot in degrees</li>
|
||
</ul>
|
||
|
||
<h3>Module States Panel</h3>
|
||
<p>Displays real-time information for each module:</p>
|
||
<ul>
|
||
<li><strong>Angle:</strong> The direction the module is pointing (in degrees)</li>
|
||
<li><strong>Speed:</strong> The velocity of the module (in pixels/second)</li>
|
||
</ul>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary>Explanation of Swerve Kinematics</summary>
|
||
<div class="documentation-content">
|
||
<h3>What is Swerve Drive?</h3>
|
||
<p>Swerve drive (also called holonomic drive) is a drivetrain design where each wheel module can
|
||
independently rotate and drive in any direction. This allows the robot to move in any direction
|
||
while simultaneously rotating, providing exceptional maneuverability.</p>
|
||
|
||
<h3>Kinematic Equations</h3>
|
||
<p>The simulator calculates each module's state using inverse kinematics. Given a desired robot
|
||
velocity (v<sub>x</sub>, v<sub>y</sub>) and rotation rate (ω), we calculate each module's
|
||
required velocity.</p>
|
||
|
||
<h4>Field-Relative vs Robot-Relative</h4>
|
||
<p>This simulator uses <strong>field-relative control</strong>, meaning the velocity commands are
|
||
relative to the field, not the robot's current orientation. The inputs are transformed to
|
||
robot-relative coordinates using the current gyro heading:</p>
|
||
<pre>
|
||
v<sub>robot_x</sub> = v<sub>field_x</sub> × cos(-θ) - v<sub>field_y</sub> × sin(-θ)
|
||
v<sub>robot_y</sub> = v<sub>field_x</sub> × sin(-θ) + v<sub>field_y</sub> × cos(-θ)
|
||
</pre>
|
||
<p>Where θ is the robot's heading angle (gyro reading).</p>
|
||
|
||
<h4>Module Velocity Calculation</h4>
|
||
<p>For each module at position (x<sub>i</sub>, y<sub>i</sub>) relative to the robot's center of
|
||
rotation:</p>
|
||
<ol>
|
||
<li><strong>Translation component:</strong> The robot's linear velocity (v<sub>robot_x</sub>,
|
||
v<sub>robot_y</sub>)</li>
|
||
<li><strong>Rotation component:</strong> Perpendicular to the position vector, with magnitude
|
||
proportional to distance from center:
|
||
<pre>
|
||
v<sub>rot_x</sub> = -y<sub>i</sub> × ω
|
||
v<sub>rot_y</sub> = x<sub>i</sub> × ω
|
||
</pre>
|
||
</li>
|
||
<li><strong>Combined velocity:</strong> Vector sum of translation and rotation:
|
||
<pre>
|
||
v<sub>module_x</sub> = v<sub>robot_x</sub> + v<sub>rot_x</sub>
|
||
v<sub>module_y</sub> = v<sub>robot_y</sub> + v<sub>rot_y</sub>
|
||
</pre>
|
||
</li>
|
||
</ol>
|
||
|
||
<h4>Module Angle and Speed</h4>
|
||
<p>From the module's velocity vector, we calculate:</p>
|
||
<ul>
|
||
<li><strong>Speed:</strong> The magnitude of the velocity vector: √(v<sub>x</sub>² +
|
||
v<sub>y</sub>²)</li>
|
||
<li><strong>Angle:</strong> The direction of the velocity vector: arctan2(v<sub>y</sub>,
|
||
v<sub>x</sub>)</li>
|
||
</ul>
|
||
|
||
<h4>Speed Normalization</h4>
|
||
<p>If any module's calculated speed exceeds the maximum allowed speed, all module velocities are
|
||
scaled proportionally. This preserves the movement direction while respecting hardware limits:
|
||
</p>
|
||
<pre>
|
||
scale = max_speed / max(calculated_speeds)
|
||
if scale < 1:
|
||
all_module_speeds × scale
|
||
</pre>
|
||
|
||
<h3>Gyro Integration</h3>
|
||
<p>The robot's heading (gyro angle) is continuously updated by integrating the rotation rate:</p>
|
||
<pre>
|
||
θ<sub>new</sub> = θ<sub>old</sub> + ω × Δt
|
||
</pre>
|
||
<p>Where Δt is the time step. The heading is normalized to stay within the range [-π, π].</p>
|
||
|
||
<h3>Real-World Applications</h3>
|
||
<p>Swerve drive systems are commonly used in:</p>
|
||
<ul>
|
||
<li><strong>FRC (FIRST Robotics Competition):</strong> For competitive robots requiring precise
|
||
positioning</li>
|
||
<li><strong>Industrial AGVs:</strong> Automated guided vehicles in warehouses</li>
|
||
<li><strong>Research Platforms:</strong> Mobile robots requiring omnidirectional movement</li>
|
||
</ul>
|
||
|
||
<h3>Key Advantages</h3>
|
||
<ul>
|
||
<li>True holonomic motion (can move in any direction without rotating)</li>
|
||
<li>Can translate and rotate simultaneously</li>
|
||
<li>Excellent maneuverability in constrained spaces</li>
|
||
<li>No "drift" or unwanted rotation during translation</li>
|
||
</ul>
|
||
|
||
<h3>Implementation Considerations</h3>
|
||
<ul>
|
||
<li><strong>Mechanical Complexity:</strong> Each module requires two motors (drive and steering)
|
||
</li>
|
||
<li><strong>Control Complexity:</strong> Requires coordinated control of all modules</li>
|
||
<li><strong>Sensor Requirements:</strong> Absolute encoders recommended for module angles</li>
|
||
<li><strong>Cost:</strong> More expensive than traditional drivetrains</li>
|
||
</ul>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
<section class="visualization-canvas">
|
||
<h2>Robot Visualization</h2>
|
||
<canvas id="swerve-canvas" width="800" height="800"></canvas>
|
||
</section>
|
||
|
||
<section class="control-panel">
|
||
<h2>Drive Controls</h2>
|
||
|
||
<fieldset>
|
||
<legend>Translation & Rotation</legend>
|
||
|
||
<div class="control-group">
|
||
<button id="control-mode-toggle" type="button">Switch to Slider Controls</button>
|
||
</div>
|
||
|
||
<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">
|
||
<output id="vy-value">0</output>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<label for="vx-slider">Strafe Left/Right (pixels/s)</label>
|
||
<input type="range" id="vx-slider" min="-300" max="300" step="10" value="0">
|
||
<output id="vx-value">0</output>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<label for="omega-slider">Rotation (rad/s)</label>
|
||
<input type="range" id="omega-slider" min="-3" max="3" step="0.1" value="0">
|
||
<output id="omega-value">0</output>
|
||
</div>
|
||
|
||
<button id="reset-btn" type="button">Reset Controls</button>
|
||
</div>
|
||
|
||
<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">
|
||
<output id="keyboard-max-speed-value">150</output>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<label for="keyboard-max-rotation">Max Rotation (rad/s)</label>
|
||
<input type="range" id="keyboard-max-rotation" min="0.5" max="3" step="0.1" value="1.5">
|
||
<output id="keyboard-max-rotation-value">1.5</output>
|
||
</div>
|
||
|
||
<div class="keyboard-instructions">
|
||
<h4>Keyboard Controls:</h4>
|
||
<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>
|
||
<p><em>Click on the canvas area to focus for keyboard input</em></p>
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<fieldset>
|
||
<legend>Performance Limits</legend>
|
||
|
||
<div class="control-group">
|
||
<label for="max-speed-slider">Max Module Speed (pixels/s)</label>
|
||
<input type="range" id="max-speed-slider" min="200" max="1000" step="10" value="400">
|
||
<output id="max-speed-value">0</output>
|
||
</div>
|
||
</fieldset>
|
||
</section>
|
||
<section class="config-panel">
|
||
<h2>Robot Configuration</h2>
|
||
<fieldset>
|
||
<legend>Quick Presets</legend>
|
||
<div class="preset-buttons">
|
||
<button id="preset-2wheel" type="button">2-Wheel</button>
|
||
<button id="preset-3wheel" type="button">3-Wheel Triangle</button>
|
||
<button id="preset-4wheel" type="button">4-Wheel Square</button>
|
||
<button id="preset-4rect" type="button">4-Wheel Rectangle</button>
|
||
<button id="preset-6wheel" type="button">6-Wheel Hexagon</button>
|
||
<button id="preset-8wheel" type="button">8-Wheel Octagon</button>
|
||
<button id="preset-8square" type="button">8-Wheel Square</button>
|
||
<button id="preset-12hex" type="button">12-Wheel Hexagon</button>
|
||
<button id="preset-16oct" type="button">16-Wheel Octogon</button>
|
||
</div>
|
||
</fieldset>
|
||
<fieldset>
|
||
<legend>Custom Configuration</legend>
|
||
|
||
<div class="control-group">
|
||
<label for="module-count">Number of Modules</label>
|
||
<input type="number" id="module-count" min="2" max="64" value="4" step="1">
|
||
</div>
|
||
|
||
<button id="generate-inputs-btn" type="button">Generate Position Inputs</button>
|
||
<button id="delete-inputs-btn" type="button">Remove Position Inputs</button>
|
||
|
||
<div id="module-position-inputs" class="position-inputs">
|
||
<!-- Dynamically generated position inputs will appear here -->
|
||
</div>
|
||
|
||
<button id="apply-custom-btn" type="button" style="display: none;">Apply Custom Configuration</button>
|
||
</fieldset>
|
||
</section>
|
||
<section class="module-states">
|
||
<h2>Module States</h2>
|
||
<div id="current-config-info" class="config-info">
|
||
Current Configuration: <strong id="config-name">4-Wheel Rectangle</strong>
|
||
(<span id="module-count-display">4</span> modules)
|
||
<br>
|
||
Gyro Heading: <strong id="gyro-heading-display">0.0°</strong>
|
||
</div>
|
||
<div class="module-grid" id="module-grid">
|
||
<!-- Dynamically generated module data will appear here -->
|
||
</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> |