refactor: abstracted Joystick component with custom zones
This commit is contained in:
parent
17882b277a
commit
acefcb0cd8
5 changed files with 441 additions and 237 deletions
21
src/app.css
21
src/app.css
|
@ -2,28 +2,17 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
|
||||||
background: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color:black
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controller-back {
|
|
||||||
width: 10em;
|
|
||||||
height: 10em;
|
|
||||||
background: red;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.controller-joystick {
|
.controller-joystick {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controller-joystick-ins {
|
.controller-joystick-ins {
|
||||||
position: relative;
|
position: relative;
|
||||||
/* left: -50%; */
|
/* left: -50%; */
|
||||||
|
@ -41,3 +30,9 @@ input {
|
||||||
/* heigth: 10em; */
|
/* heigth: 10em; */
|
||||||
background: gray;
|
background: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.joystick-bank {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
height: 20em;
|
||||||
|
}
|
||||||
|
|
|
@ -1,21 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
function clamp(v, min, max) {
|
import Joystick from './Joystick.svelte';
|
||||||
return Math.min(Math.max(v, min), max);
|
import WebSocketService from './WebSocketService';
|
||||||
}
|
import { clamp, areSameObjects } from './utils'
|
||||||
function sign(x) {
|
|
||||||
if (x == 0) { return 0; }
|
|
||||||
if (x > 0) { return 1; }
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Two from 'two.js';
|
|
||||||
import WebSocketService from '$lib/WebSocketService.js'
|
|
||||||
|
|
||||||
const realSendMode = true;
|
|
||||||
const maxSpeed = 50;
|
|
||||||
|
|
||||||
const buttonBindings = ['x','o','triangle','square','l1','r1','l2','r2','share','options','home','leftstickpress','rightstickpress']
|
const WEBSOCKET_URL = "http://localhost:1567";
|
||||||
const axisBindings = ['left_joystick_x','left_joystick_y','l2','right_joystick_x','right_joystick_y','r2','cross_x','cross_y']
|
const REAL_SEND_MODE = true;
|
||||||
|
const MAX_SPEED = 50;
|
||||||
|
const MOTORS = ["frontLeft", "frontRight", "backLeft", "backRight"];
|
||||||
|
|
||||||
|
const BUTTON_BINDINGS = ['x','o','triangle','square','l1','r1','l2','r2','share','options','home','leftstickpress','rightstickpress']
|
||||||
|
const AXIS_BINDINGS = ['left_joystick_x','left_joystick_y','l2','right_joystick_x','right_joystick_y','r2','cross_x','cross_y']
|
||||||
|
|
||||||
|
const ZONE_DIRECTIONS_MAPPING = ["E", "NE", "N", "NW", "W", "SW", "S", "SE"];
|
||||||
|
|
||||||
|
let joystickBankEnabled = false;
|
||||||
|
|
||||||
|
let rotationZones = [];
|
||||||
|
let translationZones = [];
|
||||||
|
|
||||||
|
let l2initialized;
|
||||||
|
let gpDictState;
|
||||||
|
let prettyGpState;
|
||||||
|
|
||||||
|
let direction;
|
||||||
|
let normalizedVec = {x: 0, y: 0};
|
||||||
|
let angleFullRangeDeg = 0;
|
||||||
|
let finalSpeed = 0;
|
||||||
|
|
||||||
|
let inputRangesMap = Object.fromEntries(MOTORS.map(m => [m, [307, 410]]));
|
||||||
|
|
||||||
|
let leftJoystick;
|
||||||
|
let rightJoystick;
|
||||||
|
|
||||||
|
let lastGpState = null;
|
||||||
|
let currentWs = null;
|
||||||
|
|
||||||
|
let currentGamepadIndex = -1;
|
||||||
|
|
||||||
|
|
||||||
function getGamepadState(gpIndex) {
|
function getGamepadState(gpIndex) {
|
||||||
let tmpState = navigator.getGamepads()[currentGamepadIndex];
|
let tmpState = navigator.getGamepads()[currentGamepadIndex];
|
||||||
|
@ -28,111 +51,44 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let l2initialized;
|
function updateRangesMap(inputRangesMap) {
|
||||||
export let gpDictState;
|
if (!currentWs) return;
|
||||||
export let prettyGpState;
|
// console.log(Object.entries(inputRangesMap))
|
||||||
export let direction;
|
Object.entries(inputRangesMap).map(([name, range]) => {
|
||||||
export let normalizedVec = {x: 0, y: 0};
|
console.log(name, range)
|
||||||
export let angleFullRangeDeg = 0;
|
currentWs.send(
|
||||||
export let finalSpeed = 0;
|
"set_range",
|
||||||
|
{name, min: range[0], max: range[1]}
|
||||||
export let mouseControlled;
|
)
|
||||||
|
})
|
||||||
const zone_directions = ["E", "NE", "N", "NW", "W", "SW", "S", "SE"];
|
}
|
||||||
let twoScene = null;
|
|
||||||
let zonesGroup = null;
|
|
||||||
let leftJoystick = {pointer: null, bg: null};
|
|
||||||
let rightJoystick = {pointer: null, bg: null};
|
|
||||||
|
|
||||||
let lastGpState = null;
|
|
||||||
let currentWs = null;
|
|
||||||
|
|
||||||
let currentGamepadIndex = -1;
|
|
||||||
|
|
||||||
function initGamepads() {
|
function initGamepads() {
|
||||||
console.log("init gamepads");
|
console.log("> Initializing gamepads...");
|
||||||
window.addEventListener("gamepadconnected", (e) => {
|
window.addEventListener("gamepadconnected", (e) => {
|
||||||
const gp = navigator.getGamepads()[e.gamepad.index];
|
const gp = navigator.getGamepads()[e.gamepad.index];
|
||||||
currentGamepadIndex = gp.index;
|
currentGamepadIndex = gp.index;
|
||||||
l2initialized = false;
|
l2initialized = false;
|
||||||
console.log(`Gamepad connected at index ${gp.index}: ${gp.id} with ${gp.buttons.length} buttons, ${gp.axes.length} axes.`);
|
console.log(`> Gamepad connected at index ${gp.index}: ${gp.id} with ${gp.buttons.length} buttons, ${gp.axes.length} axes.`);
|
||||||
});
|
});
|
||||||
window.addEventListener("gamepaddisconnected", (e) => {
|
window.addEventListener("gamepaddisconnected", (e) => {
|
||||||
console.log(e.gamepad);
|
console.log("> Gamepad disconnected!", e.gamepad);
|
||||||
});
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
if (currentGamepadIndex === -1) {
|
||||||
|
console.warn("> No gamepad found!");
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGamepadStateAsDict(arrayState) {
|
function getGamepadStateAsDict(arrayState) {
|
||||||
return {
|
return {
|
||||||
axes: Object.fromEntries(axisBindings.map((n, i) => [n, arrayState.axes[i]])),
|
axes: Object.fromEntries(AXIS_BINDINGS.map((n, i) => [n, arrayState.axes[i]])),
|
||||||
buttons: Object.fromEntries(buttonBindings.map((n, i) => [n, arrayState.buttons[i]]))
|
buttons: Object.fromEntries(BUTTON_BINDINGS.map((n, i) => [n, arrayState.buttons[i]]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderJoystick() {
|
function processNewGamepadState(receivedState) {
|
||||||
const scaleFactor = 100;
|
|
||||||
rightJoystick.pointer.translation.x = 200 + normalizedVec.x*scaleFactor;
|
|
||||||
rightJoystick.pointer.translation.y = 200 + normalizedVec.y*scaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNavigation(angleFullRange) {
|
|
||||||
const intensity = Math.hypot(normalizedVec.x, normalizedVec.y)
|
|
||||||
if (intensity == 0) {
|
|
||||||
currentWs.send("stop_robot", {})
|
|
||||||
zonesGroup.children.forEach(zone => {
|
|
||||||
zone.opacity = 1
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
function getZoneDirection(angle) {
|
|
||||||
const nbOfZones = zone_directions.length;
|
|
||||||
const delta = 2*Math.PI/nbOfZones;
|
|
||||||
const offset = delta/2;
|
|
||||||
for (let i = 0; i < nbOfZones; i++) {
|
|
||||||
let lb = i*delta+offset;
|
|
||||||
let ub = (i+1)*delta+offset;
|
|
||||||
// console.log(lb, angle, ub)
|
|
||||||
if (lb >= angle && angle <= ub) {
|
|
||||||
return zone_directions[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "E";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// let angleFullRange = -Math.abs(Math.acos(normalizedVec.x))*sign(Math.asin(normalizedVec.y));
|
|
||||||
angleFullRangeDeg = angleFullRange * 180/Math.PI;
|
|
||||||
|
|
||||||
direction = getZoneDirection(angleFullRange)
|
|
||||||
|
|
||||||
zonesGroup.children.forEach((zone, i) => {
|
|
||||||
if (zone_directions.indexOf(direction) == i) {
|
|
||||||
zone.opacity = 0.5
|
|
||||||
} else {
|
|
||||||
zone.opacity = 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// console.log("new angle", angleFullRange * 180/Math.PI, direction)
|
|
||||||
|
|
||||||
if (realSendMode) {
|
|
||||||
currentWs.send(
|
|
||||||
"set_direction",
|
|
||||||
{"direction": direction}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSpeed(speed) {
|
|
||||||
// console.log("speed", speed)
|
|
||||||
finalSpeed = clamp(speed, 0, maxSpeed)
|
|
||||||
// set speed
|
|
||||||
if (realSendMode) {
|
|
||||||
currentWs.send("set_speed", {"speed": finalSpeed})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onGamepadChange(receivedState) {
|
|
||||||
gpDictState = getGamepadStateAsDict(lastGpState);
|
gpDictState = getGamepadStateAsDict(lastGpState);
|
||||||
|
|
||||||
if (!l2initialized && gpDictState.axes.l2 != 0) {
|
if (!l2initialized && gpDictState.axes.l2 != 0) {
|
||||||
|
@ -141,44 +97,10 @@
|
||||||
|
|
||||||
prettyGpState = JSON.stringify(gpDictState, " ", 4);
|
prettyGpState = JSON.stringify(gpDictState, " ", 4);
|
||||||
|
|
||||||
// pre process the right joystick
|
leftJoystick.onGamepadJoystickChange(gpDictState.axes.left_joystick_x, gpDictState.axes.left_joystick_y);
|
||||||
let vec = {x: gpDictState.axes.right_joystick_x, y: gpDictState.axes.right_joystick_y};
|
rightJoystick.onGamepadJoystickChange(gpDictState.axes.right_joystick_x, gpDictState.axes.right_joystick_y);
|
||||||
let angle = Math.atan2(vec.y, vec.x);
|
|
||||||
let reach = clamp(Math.hypot(vec.x, vec.y), 0, 1);
|
|
||||||
|
|
||||||
// create the normalized vector
|
|
||||||
normalizedVec = {x: reach*Math.cos(angle), y: reach*Math.sin(angle)};
|
|
||||||
|
|
||||||
handleJoystickChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleJoystickChange() {
|
|
||||||
let reach = Math.hypot(normalizedVec.x, normalizedVec.y);
|
|
||||||
// convert angle that is sometimes from 0 to -180 and sometimes from 0 to 180 deg
|
|
||||||
// to something from 0 to 360 full circle
|
|
||||||
let angle = Math.atan2(normalizedVec.y, normalizedVec.x);
|
|
||||||
let angleFullRange = (reach == 0) ? 0 : (angle < 0) ? -angle : Math.PI + (Math.PI - angle);
|
|
||||||
|
|
||||||
console.log({
|
|
||||||
angle: angle*180/Math.PI,
|
|
||||||
angleFullRange: angleFullRange*180/Math.PI,
|
|
||||||
reach, normalizedVec
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log(vec.x, vec.y, angle, angle *180/Math.PI)
|
|
||||||
|
|
||||||
renderJoystick();
|
|
||||||
updateNavigation(angleFullRange);
|
|
||||||
|
|
||||||
const maxSpeed = 50;
|
|
||||||
let speedForced = l2initialized ? Math.round(((gpDictState.axes.l2 + 1)/2)*maxSpeed) : 0;
|
|
||||||
let speedNormal = Math.round(0.50*reach*maxSpeed);
|
|
||||||
updateSpeed(Math.max(speedNormal, speedForced));
|
|
||||||
}
|
|
||||||
|
|
||||||
function areSameObjects(a, b) {
|
|
||||||
return JSON.stringify(a) == JSON.stringify(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loop() {
|
function loop() {
|
||||||
if (currentGamepadIndex >= 0) {
|
if (currentGamepadIndex >= 0) {
|
||||||
|
@ -188,123 +110,152 @@
|
||||||
if (lastGpState == null || !areSameObjects(lastGpState, receivedState)) {
|
if (lastGpState == null || !areSameObjects(lastGpState, receivedState)) {
|
||||||
// console.log("changed")
|
// console.log("changed")
|
||||||
lastGpState = receivedState;
|
lastGpState = receivedState;
|
||||||
onGamepadChange(receivedState);
|
processNewGamepadState(receivedState);
|
||||||
twoScene.render();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.requestAnimationFrame(loop);
|
window.requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
function updateNavigation(angle, distance, zoneIndex) {
|
||||||
const sceneContainer = document.getElementById("joy2");
|
if (distance == 0) {
|
||||||
twoScene = new Two({}).appendTo(sceneContainer);
|
currentWs.send("stop_robot", {})
|
||||||
|
return
|
||||||
rightJoystick.bg = new Two.Circle(
|
|
||||||
200, 200, 100
|
|
||||||
);
|
|
||||||
twoScene.add(rightJoystick.bg);
|
|
||||||
|
|
||||||
rightJoystick.pointer = new Two.Circle(
|
|
||||||
200, 200, 10
|
|
||||||
);
|
|
||||||
rightJoystick.pointer.fill = "red";
|
|
||||||
rightJoystick.pointer.stroke = "rgba(0, 0, 0, 0.25)";
|
|
||||||
|
|
||||||
|
|
||||||
let nbOfZones = 8;
|
|
||||||
let delta = 2*Math.PI/nbOfZones;
|
|
||||||
let offset = delta/2;
|
|
||||||
|
|
||||||
zonesGroup = new Two.Group();
|
|
||||||
for (var i = 0; i < nbOfZones; i++) {
|
|
||||||
let arc = new Two.ArcSegment(
|
|
||||||
200, 200, 0, 100,
|
|
||||||
-i*delta+offset, -(i+1)*delta+offset
|
|
||||||
);
|
|
||||||
arc.fill = "green";
|
|
||||||
zonesGroup.add(arc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mousePointer = new Two.Circle(0, 0, 4);
|
angleFullRangeDeg = angle * 180/Math.PI;
|
||||||
|
normalizedVec = {x: distance*Math.cos(angle), y: distance*Math.sin(angle)}
|
||||||
|
|
||||||
twoScene.add(zonesGroup);
|
// update the translation direction
|
||||||
twoScene.add(rightJoystick.pointer);
|
direction = ZONE_DIRECTIONS_MAPPING[zoneIndex];
|
||||||
twoScene.add(mousePointer);
|
currentWs.send(
|
||||||
|
"set_direction",
|
||||||
|
{"direction": direction}
|
||||||
|
)
|
||||||
|
|
||||||
twoScene.render();
|
// update the speed
|
||||||
|
// TODO: Have the speed overwrite by non-gamepad users
|
||||||
|
let speedForced = l2initialized ? Math.round(((gpDictState.axes.l2 + 1)/2)*MAX_SPEED) : 0;
|
||||||
|
let speedNormal = Math.round(0.50*distance*MAX_SPEED);
|
||||||
|
let speed = Math.max(speedNormal, speedForced);
|
||||||
|
finalSpeed = clamp(speed, 0, MAX_SPEED);
|
||||||
|
|
||||||
let controller_index = 0;
|
currentWs.send("set_speed", {"speed": finalSpeed});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTranslationZones() {
|
||||||
|
let nbOfZones = 8;
|
||||||
|
let delta = 2*Math.PI/nbOfZones;
|
||||||
|
let offset = -delta/2;
|
||||||
|
let zones = [];
|
||||||
|
for (var i = 0; i < nbOfZones; i++) {
|
||||||
|
zones.push({
|
||||||
|
from: i*delta+offset,
|
||||||
|
to: (i+1)*delta+offset
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return zones;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
translationZones = generateTranslationZones();
|
||||||
|
rotationZones = [
|
||||||
|
{ from: -(6/8)*Math.PI, to: -(2/8)*Math.PI },
|
||||||
|
{ to: (6/8)*Math.PI, from: (2/8)*Math.PI },
|
||||||
|
]
|
||||||
|
|
||||||
initGamepads();
|
initGamepads();
|
||||||
currentWs = new WebSocketService("http://192.168.1.128:1567");
|
|
||||||
|
|
||||||
|
// setup websocket
|
||||||
|
currentWs = new WebSocketService(WEBSOCKET_URL);
|
||||||
|
currentWs.isEnabled = REAL_SEND_MODE;
|
||||||
currentWs.start()
|
currentWs.start()
|
||||||
currentWs.on('connectionUpdated', 'connection', e => {
|
currentWs.on('connectionUpdated', 'connection', e => {
|
||||||
console.log(e.detail);
|
console.log(e.detail);
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle mouse events
|
joystickBankEnabled = true;
|
||||||
sceneContainer.onmousemove = (e) => {
|
|
||||||
if (!mouseControlled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(e);
|
|
||||||
let rect = sceneContainer.getBoundingClientRect();
|
|
||||||
let x = e.clientX - rect.left;
|
|
||||||
let y = e.clientY - rect.top;
|
|
||||||
mousePointer.translation.x = x;
|
|
||||||
mousePointer.translation.y = y;
|
|
||||||
// compute the position inside the circle
|
|
||||||
let circle = {x: 200, y: 200, radius: 100};
|
|
||||||
|
|
||||||
let relVec = {x: x-circle.x, y: y-circle.y};
|
|
||||||
|
|
||||||
// console.log("rel circle", relVec);
|
|
||||||
if (Math.hypot(relVec.x, relVec.y) < 2*circle.radius) {
|
|
||||||
// console.log("inside circle")
|
|
||||||
let vec = {x: relVec.x/circle.radius, y: relVec.y/circle.radius};
|
|
||||||
let angle = Math.atan2(vec.y, vec.x);
|
|
||||||
let reach = clamp(Math.hypot(vec.x, vec.y), 0, 1);
|
|
||||||
normalizedVec = {x: reach*Math.cos(angle), y: reach*Math.sin(angle)};
|
|
||||||
handleJoystickChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
twoScene.render();
|
|
||||||
}
|
|
||||||
sceneContainer.onmousedown = () => {
|
|
||||||
mouseControlled = true;
|
|
||||||
mousePointer.opacity = 1;
|
|
||||||
}
|
|
||||||
sceneContainer.onmouseup = () => {
|
|
||||||
mouseControlled = false;
|
|
||||||
normalizedVec = {x: 0, y: 0};
|
|
||||||
handleJoystickChange();
|
|
||||||
mousePointer.opacity = 0;
|
|
||||||
twoScene.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
loop();
|
loop();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onLeftJoystickChange({ angle, distance, zone }) {
|
||||||
|
console.log("onLeftJoystickChange", angle, distance, zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRightJoystickChange({ angle, distance, zone }) {
|
||||||
|
updateNavigation(angle, distance, zone);
|
||||||
|
console.log("onRightJoystickChange", angle, distance, zone);
|
||||||
|
|
||||||
|
// updateSpeed(distance);
|
||||||
|
// updateNavigation(angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateRangesMap(inputRangesMap);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2>Controller</h2>
|
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
|
<h2>Robot Controller</h2>
|
||||||
<pre>
|
<pre>
|
||||||
angle: {angleFullRangeDeg.toFixed(2)} deg
|
angle: {angleFullRangeDeg.toFixed(2)} deg
|
||||||
dir: {direction}
|
dir: {direction}
|
||||||
speed: {finalSpeed}
|
speed: {finalSpeed}
|
||||||
vec: {normalizedVec.x.toFixed(4)} {normalizedVec.y.toFixed(4)}
|
vec: {normalizedVec.x.toFixed(4)} {normalizedVec.y.toFixed(4)}
|
||||||
</pre>
|
</pre>
|
||||||
<pre style="opacity: 0.6">joystick: {prettyGpState}</pre>
|
<pre style="opacity: 0.6; font-size: 0.5em">joystick: {prettyGpState}</pre>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<!-- <div class="controller-back"> -->
|
{#if joystickBankEnabled}
|
||||||
<!-- <div id="joy" class="controller-joystick"> -->
|
<div class="joystick-bank">
|
||||||
<!-- <div class="controller-joystick-ins"></div> -->
|
|
||||||
|
<!-- <div id="joy1"> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
|
<!-- <div style="opacity: 0.5" id="joy2"> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
<div id="joy2">
|
<Joystick
|
||||||
|
zonesDescriptions={rotationZones}
|
||||||
|
onPositionChangeCallback={onLeftJoystickChange}
|
||||||
|
bind:this={leftJoystick}
|
||||||
|
/>
|
||||||
|
<Joystick
|
||||||
|
zonesDescriptions={translationZones}
|
||||||
|
onPositionChangeCallback={onRightJoystickChange}
|
||||||
|
bind:this={rightJoystick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Range settings</h3>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<div>frontLeft:</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<input type="number" bind:value={inputRangesMap['frontLeft'][0]} />
|
||||||
|
<input type="number" bind:value={inputRangesMap['frontLeft'][1]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<div>frontRight:</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<input type="number" bind:value={inputRangesMap['frontRight'][0]} />
|
||||||
|
<input type="number" bind:value={inputRangesMap['frontRight'][1]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<div>backLeft:</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<input type="number" bind:value={inputRangesMap['backLeft'][0]} />
|
||||||
|
<input type="number" bind:value={inputRangesMap['backLeft'][1]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<div>backRight:</div>
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<input type="number" bind:value={inputRangesMap['backRight'][0]} />
|
||||||
|
<input type="number" bind:value={inputRangesMap['backRight'][1]} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
230
src/lib/Joystick.svelte
Normal file
230
src/lib/Joystick.svelte
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import Two from 'two.js';
|
||||||
|
import { sign, clamp } from './utils'
|
||||||
|
|
||||||
|
export let onPositionChangeCallback = (angle, distance) => {};
|
||||||
|
// list of the zones, defined by their start angle, their end angle and an optional identifier
|
||||||
|
export let zonesDescriptions = [];
|
||||||
|
|
||||||
|
const BUTTON_BINDINGS = ['x','o','triangle','square','l1','r1','l2','r2','share','options','home','leftstickpress','rightstickpress']
|
||||||
|
const AXIS_BINDINGS = ['left_joystick_x','left_joystick_y','l2','right_joystick_x','right_joystick_y','r2','cross_x','cross_y']
|
||||||
|
|
||||||
|
let joystickCircleX = 130;
|
||||||
|
let joystickCircleY = 130;
|
||||||
|
let joystickCircleRad = 100;
|
||||||
|
|
||||||
|
let joystickContainer;
|
||||||
|
let twoScene = null;
|
||||||
|
|
||||||
|
// two.js elements
|
||||||
|
let zonesGroup = null;
|
||||||
|
let joystickGroup = null;
|
||||||
|
let joystickBack = null;
|
||||||
|
let joystickPointer = null;
|
||||||
|
let mousePointer = null;
|
||||||
|
|
||||||
|
let currentGamepadIndex = -1;
|
||||||
|
let normalizedVec = {x: 0, y: 0};
|
||||||
|
|
||||||
|
let mouseControlled = false;
|
||||||
|
|
||||||
|
export function onGamepadJoystickChange(x, y) {
|
||||||
|
// pre process the right joystick
|
||||||
|
let angle = Math.atan2(y, x);
|
||||||
|
let reach = clamp(Math.hypot(x, y), 0, 1);
|
||||||
|
|
||||||
|
// create the normalized vector
|
||||||
|
normalizedVec = {x: reach*Math.cos(angle), y: reach*Math.sin(angle)};
|
||||||
|
handleJoystickChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJoystick() {
|
||||||
|
joystickPointer.translation.x = normalizedVec.x*joystickCircleRad;
|
||||||
|
joystickPointer.translation.y = normalizedVec.y*joystickCircleRad;
|
||||||
|
twoScene.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the index of the zone currently pointed by the pointer
|
||||||
|
function getZoneFromAngle(angle) {
|
||||||
|
for (var i = 0; i < zonesDescriptions.length; i++) {
|
||||||
|
let zone = zonesDescriptions[i];
|
||||||
|
if (angle >= zone.from && angle <= zone.to) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJoystickChange() {
|
||||||
|
// distance between 0 and 1
|
||||||
|
let reach = Math.hypot(normalizedVec.x, normalizedVec.y);
|
||||||
|
// convert angle that is sometimes from 0 to -180 and sometimes from 0 to 180 deg
|
||||||
|
// to something from 0 to 360 full circle
|
||||||
|
let angle = Math.atan2(normalizedVec.y, normalizedVec.x);
|
||||||
|
let angleFullRange = (reach == 0) ? 0 : (angle < 0) ? -angle : Math.PI + (Math.PI - angle);
|
||||||
|
|
||||||
|
console.log('infos in degs', {
|
||||||
|
angle: angle*180/Math.PI,
|
||||||
|
angleFullRange: angleFullRange*180/Math.PI,
|
||||||
|
reach, normalizedVec
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedZoneIndex = -1;
|
||||||
|
|
||||||
|
if (reach == 0) {
|
||||||
|
zonesGroup.children.forEach(zone => {
|
||||||
|
zone.opacity = 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (reach > 0) {
|
||||||
|
selectedZoneIndex = getZoneFromAngle(angleFullRange)
|
||||||
|
zonesGroup.children.forEach((zone, i) => {
|
||||||
|
zone.opacity = (i === selectedZoneIndex) ? 0.5 : 1;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderJoystick()
|
||||||
|
onPositionChangeCallback({
|
||||||
|
angle: angleFullRange,
|
||||||
|
distance: reach,
|
||||||
|
zone: selectedZoneIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeScene() {
|
||||||
|
if (!joystickContainer) return null
|
||||||
|
|
||||||
|
constructScene()
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructScene() {
|
||||||
|
joystickContainer.innerHTML = '';
|
||||||
|
console.log("construct scene");
|
||||||
|
|
||||||
|
let rect = joystickContainer.getBoundingClientRect()
|
||||||
|
let [width, height] = [rect.width, rect.height];
|
||||||
|
|
||||||
|
twoScene = new Two({
|
||||||
|
width, height
|
||||||
|
}).appendTo(joystickContainer);
|
||||||
|
twoScene.renderer.domElement.style.background = 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
|
joystickGroup = new Two.Group();
|
||||||
|
joystickGroup.translation.x = width/2;
|
||||||
|
joystickGroup.translation.y = height/2;
|
||||||
|
|
||||||
|
joystickBack = new Two.Circle(
|
||||||
|
0, 0, joystickCircleRad
|
||||||
|
);
|
||||||
|
joystickBack.fill = "rgba(236, 240, 241,1.0)";
|
||||||
|
joystickBack.stroke = "rgba(52, 73, 94,1.0)";
|
||||||
|
|
||||||
|
joystickPointer = new Two.Circle(
|
||||||
|
0, 0, 10
|
||||||
|
);
|
||||||
|
joystickPointer.fill = "rgba(231, 76, 60, 0.8)";
|
||||||
|
joystickPointer.stroke = "rgba(44, 62, 80, 0.5)";
|
||||||
|
joystickPointer.linewidth = 2;
|
||||||
|
|
||||||
|
let nbOfZones = 8;
|
||||||
|
let delta = 2*Math.PI/nbOfZones;
|
||||||
|
let offset = delta/2;
|
||||||
|
|
||||||
|
zonesGroup = new Two.Group();
|
||||||
|
let zonesLabelGroups = new Two.Group();
|
||||||
|
zonesDescriptions.forEach((zone, index) => {
|
||||||
|
let arc = new Two.ArcSegment(
|
||||||
|
0, 0, 0, joystickCircleRad,
|
||||||
|
-zone.from, -zone.to
|
||||||
|
);
|
||||||
|
let a = (-zone.from + -zone.to)/2;
|
||||||
|
const b = 1.2;
|
||||||
|
zonesLabelGroups.add(new Two.Text(
|
||||||
|
`${index}`,
|
||||||
|
joystickCircleRad*b*Math.cos(a), joystickCircleRad*b*Math.sin(a)
|
||||||
|
))
|
||||||
|
arc.fill = "rgba(41, 128, 185,1.0)";
|
||||||
|
arc.stroke = "rgba(44, 62, 80,1.0)"
|
||||||
|
zonesGroup.add(arc);
|
||||||
|
})
|
||||||
|
|
||||||
|
mousePointer = new Two.Circle(0, 0, 4);
|
||||||
|
mousePointer.opacity = 0;
|
||||||
|
|
||||||
|
joystickGroup.add(joystickBack);
|
||||||
|
joystickGroup.add(zonesGroup);
|
||||||
|
joystickGroup.add(zonesLabelGroups);
|
||||||
|
joystickGroup.add(joystickPointer);
|
||||||
|
|
||||||
|
// marker to test for position
|
||||||
|
// joystickGroup.add(new Two.Circle(0, 0, 5));
|
||||||
|
|
||||||
|
twoScene.add(joystickGroup);
|
||||||
|
twoScene.add(mousePointer);
|
||||||
|
|
||||||
|
twoScene.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
console.log('zonesDescriptions', zonesDescriptions);
|
||||||
|
|
||||||
|
let resizeObserver = (new ResizeObserver(() => resizeScene()));
|
||||||
|
resizeObserver.observe(joystickContainer);
|
||||||
|
|
||||||
|
constructScene();
|
||||||
|
|
||||||
|
function disableMouse() {
|
||||||
|
mouseControlled = false;
|
||||||
|
normalizedVec = {x: 0, y: 0};
|
||||||
|
handleJoystickChange();
|
||||||
|
mousePointer.opacity = 0;
|
||||||
|
twoScene.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle mouse events
|
||||||
|
joystickContainer.onmousemove = (e) => {
|
||||||
|
if (!mouseControlled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.log(e);
|
||||||
|
let rect = joystickContainer.getBoundingClientRect();
|
||||||
|
let x = e.clientX - rect.left;
|
||||||
|
let y = e.clientY - rect.top;
|
||||||
|
mousePointer.translation.x = x;
|
||||||
|
mousePointer.translation.y = y;
|
||||||
|
// compute the position inside the circle
|
||||||
|
let circle = {
|
||||||
|
x: joystickGroup.translation.x,
|
||||||
|
y: joystickGroup.translation.y,
|
||||||
|
radius: joystickCircleRad
|
||||||
|
};
|
||||||
|
let relVec = {x: x-circle.x, y: y-circle.y};
|
||||||
|
|
||||||
|
// console.log("rel circle", relVec);
|
||||||
|
// if (Math.hypot(relVec.x, relVec.y) < 2*circle.radius) {
|
||||||
|
// console.log("inside circle")
|
||||||
|
let vec = {x: relVec.x/circle.radius, y: relVec.y/circle.radius};
|
||||||
|
let angle = Math.atan2(vec.y, vec.x);
|
||||||
|
let reach = clamp(Math.hypot(vec.x, vec.y), 0, 1);
|
||||||
|
normalizedVec = {x: reach*Math.cos(angle), y: reach*Math.sin(angle)};
|
||||||
|
handleJoystickChange();
|
||||||
|
// }
|
||||||
|
|
||||||
|
twoScene.render();
|
||||||
|
}
|
||||||
|
joystickContainer.onmousedown = () => {
|
||||||
|
mouseControlled = true;
|
||||||
|
mousePointer.opacity = 1;
|
||||||
|
}
|
||||||
|
joystickContainer.onmouseup = () => {
|
||||||
|
disableMouse();
|
||||||
|
}
|
||||||
|
joystickContainer.onmouseleave = () => {
|
||||||
|
disableMouse();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={joystickContainer} class="joystick-container">
|
||||||
|
</div>
|
|
@ -16,6 +16,7 @@ export default class WebSocketService extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Various other things */
|
/* Various other things */
|
||||||
|
this.isEnabled = true
|
||||||
this.recovery = false
|
this.recovery = false
|
||||||
this.isConnected = false
|
this.isConnected = false
|
||||||
this.handlersList = []
|
this.handlersList = []
|
||||||
|
@ -119,7 +120,7 @@ export default class WebSocketService extends EventTarget {
|
||||||
|
|
||||||
send(cmd, args = {}) {
|
send(cmd, args = {}) {
|
||||||
const payload = {cmd, args};
|
const payload = {cmd, args};
|
||||||
if (!this.ws.readyState) {
|
if (!this.ws.readyState || !this.ws.isEnabled) {
|
||||||
console.log("Would have sent", payload)
|
console.log("Would have sent", payload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
27
src/lib/utils.ts
Normal file
27
src/lib/utils.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Return the clamped value of a number in a bounded interval
|
||||||
|
*/
|
||||||
|
export function clamp(v: number, min: number, max: number): number {
|
||||||
|
return Math.min(Math.max(v, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the sign as integer of a number
|
||||||
|
*/
|
||||||
|
export function sign(x: number): number {
|
||||||
|
if (x == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (x > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crude way to tell if two objects are the same
|
||||||
|
*/
|
||||||
|
export function areSameObjects(a: any, b: any): boolean {
|
||||||
|
return JSON.stringify(a) == JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue