refactor: abstracted Joystick component with custom zones

This commit is contained in:
Matthieu Bessat 2023-03-26 15:35:25 +02:00
parent 17882b277a
commit acefcb0cd8
5 changed files with 441 additions and 237 deletions

View file

@ -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;
}

View file

@ -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
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}; joystickBankEnabled = true;
// 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> --> <!-- <div id="joy1"> -->
<!-- </div> --> <!-- </div> -->
<div id="joy2"> <!-- <div style="opacity: 0.5" id="joy2"> -->
<!-- </div> -->
<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
View 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>

View file

@ -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
View 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);
}