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 utilities;
|
||||
|
||||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: white;
|
||||
color:black
|
||||
color: black;
|
||||
}
|
||||
|
||||
.controller-back {
|
||||
width: 10em;
|
||||
height: 10em;
|
||||
background: red;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
}
|
||||
.controller-joystick {
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.controller-joystick-ins {
|
||||
position: relative;
|
||||
/* left: -50%; */
|
||||
|
@ -41,3 +30,9 @@ input {
|
|||
/* heigth: 10em; */
|
||||
background: gray;
|
||||
}
|
||||
|
||||
.joystick-bank {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
height: 20em;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,44 @@
|
|||
<script>
|
||||
function clamp(v, min, max) {
|
||||
return Math.min(Math.max(v, min), max);
|
||||
}
|
||||
function sign(x) {
|
||||
if (x == 0) { return 0; }
|
||||
if (x > 0) { return 1; }
|
||||
return -1;
|
||||
}
|
||||
import Joystick from './Joystick.svelte';
|
||||
import WebSocketService from './WebSocketService';
|
||||
import { clamp, areSameObjects } from './utils'
|
||||
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 axisBindings = ['left_joystick_x','left_joystick_y','l2','right_joystick_x','right_joystick_y','r2','cross_x','cross_y']
|
||||
const WEBSOCKET_URL = "http://localhost:1567";
|
||||
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) {
|
||||
let tmpState = navigator.getGamepads()[currentGamepadIndex];
|
||||
|
@ -28,111 +51,44 @@
|
|||
};
|
||||
}
|
||||
|
||||
let l2initialized;
|
||||
export let gpDictState;
|
||||
export let prettyGpState;
|
||||
export let direction;
|
||||
export let normalizedVec = {x: 0, y: 0};
|
||||
export let angleFullRangeDeg = 0;
|
||||
export let finalSpeed = 0;
|
||||
|
||||
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 updateRangesMap(inputRangesMap) {
|
||||
if (!currentWs) return;
|
||||
// console.log(Object.entries(inputRangesMap))
|
||||
Object.entries(inputRangesMap).map(([name, range]) => {
|
||||
console.log(name, range)
|
||||
currentWs.send(
|
||||
"set_range",
|
||||
{name, min: range[0], max: range[1]}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function initGamepads() {
|
||||
console.log("init gamepads");
|
||||
console.log("> Initializing gamepads...");
|
||||
window.addEventListener("gamepadconnected", (e) => {
|
||||
const gp = navigator.getGamepads()[e.gamepad.index];
|
||||
currentGamepadIndex = gp.index;
|
||||
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) => {
|
||||
console.log(e.gamepad);
|
||||
console.log("> Gamepad disconnected!", e.gamepad);
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (currentGamepadIndex === -1) {
|
||||
console.warn("> No gamepad found!");
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function getGamepadStateAsDict(arrayState) {
|
||||
return {
|
||||
axes: Object.fromEntries(axisBindings.map((n, i) => [n, arrayState.axes[i]])),
|
||||
buttons: Object.fromEntries(buttonBindings.map((n, i) => [n, arrayState.buttons[i]]))
|
||||
axes: Object.fromEntries(AXIS_BINDINGS.map((n, i) => [n, arrayState.axes[i]])),
|
||||
buttons: Object.fromEntries(BUTTON_BINDINGS.map((n, i) => [n, arrayState.buttons[i]]))
|
||||
}
|
||||
}
|
||||
|
||||
function renderJoystick() {
|
||||
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) {
|
||||
function processNewGamepadState(receivedState) {
|
||||
gpDictState = getGamepadStateAsDict(lastGpState);
|
||||
|
||||
if (!l2initialized && gpDictState.axes.l2 != 0) {
|
||||
|
@ -141,44 +97,10 @@
|
|||
|
||||
prettyGpState = JSON.stringify(gpDictState, " ", 4);
|
||||
|
||||
// pre process the right joystick
|
||||
let vec = {x: gpDictState.axes.right_joystick_x, y: 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();
|
||||
leftJoystick.onGamepadJoystickChange(gpDictState.axes.left_joystick_x, gpDictState.axes.left_joystick_y);
|
||||
rightJoystick.onGamepadJoystickChange(gpDictState.axes.right_joystick_x, gpDictState.axes.right_joystick_y);
|
||||
}
|
||||
|
||||
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() {
|
||||
if (currentGamepadIndex >= 0) {
|
||||
|
@ -188,123 +110,152 @@
|
|||
if (lastGpState == null || !areSameObjects(lastGpState, receivedState)) {
|
||||
// console.log("changed")
|
||||
lastGpState = receivedState;
|
||||
onGamepadChange(receivedState);
|
||||
twoScene.render();
|
||||
processNewGamepadState(receivedState);
|
||||
}
|
||||
}
|
||||
window.requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const sceneContainer = document.getElementById("joy2");
|
||||
twoScene = new Two({}).appendTo(sceneContainer);
|
||||
|
||||
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);
|
||||
function updateNavigation(angle, distance, zoneIndex) {
|
||||
if (distance == 0) {
|
||||
currentWs.send("stop_robot", {})
|
||||
return
|
||||
}
|
||||
|
||||
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);
|
||||
twoScene.add(rightJoystick.pointer);
|
||||
twoScene.add(mousePointer);
|
||||
// update the translation direction
|
||||
direction = ZONE_DIRECTIONS_MAPPING[zoneIndex];
|
||||
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();
|
||||
currentWs = new WebSocketService("http://192.168.1.128:1567");
|
||||
|
||||
// setup websocket
|
||||
currentWs = new WebSocketService(WEBSOCKET_URL);
|
||||
currentWs.isEnabled = REAL_SEND_MODE;
|
||||
currentWs.start()
|
||||
currentWs.on('connectionUpdated', 'connection', e => {
|
||||
console.log(e.detail);
|
||||
});
|
||||
|
||||
// handle mouse events
|
||||
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();
|
||||
}
|
||||
joystickBankEnabled = true;
|
||||
|
||||
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>
|
||||
|
||||
<h2>Controller</h2>
|
||||
<div class="grid grid-cols-2">
|
||||
<div>
|
||||
<h2>Robot Controller</h2>
|
||||
<pre>
|
||||
angle: {angleFullRangeDeg.toFixed(2)} deg
|
||||
dir: {direction}
|
||||
speed: {finalSpeed}
|
||||
vec: {normalizedVec.x.toFixed(4)} {normalizedVec.y.toFixed(4)}
|
||||
</pre>
|
||||
<pre style="opacity: 0.6">joystick: {prettyGpState}</pre>
|
||||
<pre style="opacity: 0.6; font-size: 0.5em">joystick: {prettyGpState}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <div class="controller-back"> -->
|
||||
<!-- <div id="joy" class="controller-joystick"> -->
|
||||
<!-- <div class="controller-joystick-ins"></div> -->
|
||||
{#if joystickBankEnabled}
|
||||
<div class="joystick-bank">
|
||||
|
||||
<!-- <div id="joy1"> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div style="opacity: 0.5" id="joy2"> -->
|
||||
<!-- </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>
|
||||
|
|
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 */
|
||||
this.isEnabled = true
|
||||
this.recovery = false
|
||||
this.isConnected = false
|
||||
this.handlersList = []
|
||||
|
@ -119,7 +120,7 @@ export default class WebSocketService extends EventTarget {
|
|||
|
||||
send(cmd, args = {}) {
|
||||
const payload = {cmd, args};
|
||||
if (!this.ws.readyState) {
|
||||
if (!this.ws.readyState || !this.ws.isEnabled) {
|
||||
console.log("Would have sent", payload)
|
||||
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