components/widget/Joystick.vue

  1. <template>
  2. <!-- Joystick widget -->
  3. <div
  4. id="joystick"
  5. class="h-100 w-100">
  6. <!-- Joystick canvas -->
  7. <canvas
  8. ref="canvas"
  9. class="h-100 m-100"
  10. @mousedown="onMouseDown"
  11. @mouseup="onMouseUp"
  12. @mousemove="onMouseMove"
  13. @mouseout="onMouseOut" />
  14. </div>
  15. </template>
  16. <script>
  17. /**
  18. * Vue SFC used as a widget that draws an joystick that the user
  19. * can use to send teleoperation control to the robot that it is
  20. * connected to. Takes 2 absolute values in props to set the max
  21. * value of a command and a bus to send the event (new joystick value).
  22. * This widget has the following dependencies : Bootstrap-Vue for styling.
  23. *
  24. * @module widget/Joystick
  25. * @vue-prop {Boolean} enable - Enable the sending of joystick data.
  26. * @vue-prop {Number} absoluteMaxX - Max x value of the joystick coordinate.
  27. * @vue-prop {Number} absoluteMaxY - Max y value of the joystick coordinate.
  28. * @vue-prop {Vue} bus - Vue bus use to emit event to other components.
  29. * @vue-event {Object} joystick-position-change - Emit joystick data to be sent to robot.
  30. * @vue-data {Number} x - Horizontal coordinate of the joystick.
  31. * @vue-data {Number} y - Vertical coordinate of the joystick.
  32. * @vue-data {Number} loopIntervalId - Refresh canvas loop (timer).
  33. * @vue-data {Number} positionChangeIntervalId - Joystick updating loop (timer).
  34. * @vue-data {HTMLCanvasElement} canvas - HTML element of the canvas.
  35. * @vue-data {HTMLCanvasElement} context - Canvas 2d context.
  36. * @vue-data {Number} radiusRatio - Size of the joystick in ratio of available space.
  37. * @vue-data {HTMLElement} joystickElement - HTML element of the joystick.
  38. * @vue-data {Boolean} isMouseDown - Keep a manual trace of click in canvas for drawing.
  39. * @vue-data {Number} canvasRefreshRate - Number of time to update canvas for 1 sec.
  40. * @vue-data {Number} operatorCommandInterval - Time in ms between the joystick postion update.
  41. */
  42. /**
  43. * @author Edouard Legare <edouard.legare@usherbrooke.ca>,
  44. * @author Valerie Gauthier <valerie.gauthier4@usherbrooke.ca>,
  45. * @version 1.0.0
  46. */
  47. import Vue from 'vue';
  48. export default {
  49. name: 'joystick',
  50. props: {
  51. enable: {
  52. type: Boolean,
  53. required: true,
  54. },
  55. absoluteMaxX: {
  56. type: Number,
  57. required: true,
  58. },
  59. absoluteMaxY: {
  60. type: Number,
  61. required: true,
  62. },
  63. bus: {
  64. type: Vue,
  65. required: true,
  66. },
  67. },
  68. data() {
  69. return {
  70. x: null,
  71. y: null,
  72. loopIntervalId: null,
  73. positionChangeIntervalId: null,
  74. canvas: null,
  75. context: null,
  76. radiusRatio: 0.75,
  77. joystickElement: null,
  78. isMouseDown: false,
  79. canvasRefreshRate: 60.0, // Hz
  80. operatorCommandInterval: 100, // ms
  81. };
  82. },
  83. mounted() {
  84. this.joystickElement = document.getElementById('joystick');
  85. this.canvas = this.$refs.canvas;
  86. this.context = this.canvas.getContext('2d');
  87. this.init();
  88. },
  89. destroyed() {
  90. clearInterval(this.loopIntervalId);
  91. clearInterval(this.positionChangeIntervalId);
  92. },
  93. methods: {
  94. /**
  95. * Initilisation of component and timer.
  96. * @method
  97. */
  98. init() {
  99. // Timer refreshing the canvas
  100. this.loopIntervalId = setInterval(() => {
  101. this.setCanvasSize();
  102. this.findCenterCanvas();
  103. this.drawCanvas();
  104. }, 1000 / this.canvasRefreshRate);
  105. // Timer sending joystick position
  106. this.positionChangeIntervalId = setInterval(() => {
  107. this.emitJoystickPosition();
  108. }, this.operatorCommandInterval);
  109. },
  110. /**
  111. * Find the center of canvas
  112. * @method
  113. */
  114. findCenterCanvas() {
  115. if (this.x === null || this.y === null || !this.isMouseDown) {
  116. this.x = this.getCenterX();
  117. this.y = this.getCenterY();
  118. }
  119. },
  120. /**
  121. * Callback for the mouse down event.
  122. * @method
  123. * @param {HTMLElement} event - The html event given by the click.
  124. */
  125. onMouseDown(event) {
  126. if (event.button === 0) {
  127. this.updateJoystickPositionFromMouseEvent(event);
  128. this.isMouseDown = true;
  129. }
  130. },
  131. /**
  132. * Callback for the mouse up event.
  133. * @method
  134. * @param {HTMLElement} event - The html event given by the click.
  135. */
  136. onMouseUp(event) {
  137. if (event.button === 0) {
  138. this.x = this.getCenterX();
  139. this.y = this.getCenterY();
  140. this.isMouseDown = false;
  141. if (this.enable) {
  142. this.emitJoystickPosition();
  143. }
  144. }
  145. },
  146. /**
  147. * Callback for the mouse move event.
  148. * @method
  149. * @param {HTMLElement} event - The html event given by the click.
  150. */
  151. onMouseMove(event) {
  152. if (this.isMouseDown) {
  153. this.updateJoystickPositionFromMouseEvent(event);
  154. }
  155. },
  156. /**
  157. * Callback for the mouse out event.
  158. * @method
  159. * @param {HTMLElement} event - The html event given by the click.
  160. */
  161. onMouseOut(event) {
  162. this.x = this.getCenterX();
  163. this.y = this.getCenterY();
  164. this.isMouseDown = false;
  165. if (this.enable) {
  166. this.emitJoystickPosition();
  167. }
  168. },
  169. /**
  170. * Update the joystick position with the given event.
  171. * @method
  172. * @param {HTMLElement} event - The html event given by the click.
  173. */
  174. updateJoystickPositionFromMouseEvent(event) {
  175. const rect = this.canvas.getBoundingClientRect();
  176. this.x = event.clientX - rect.left;
  177. this.y = event.clientY - rect.top;
  178. const centerX = this.getCenterX();
  179. const centerY = this.getCenterY();
  180. const deltaX = this.x - centerX;
  181. const deltaY = this.y - centerY;
  182. const radius = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY));
  183. const maxRadius = this.getCanvasRadius() - this.getJoystickRadius();
  184. if (radius > maxRadius) {
  185. const ratio = maxRadius / radius;
  186. this.x = (deltaX * ratio) + centerX;
  187. this.y = (deltaY * ratio) + centerY;
  188. }
  189. },
  190. /**
  191. * Calls the different methods to draw the canvas.
  192. * @method
  193. */
  194. drawCanvas() {
  195. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  196. this.drawBackground();
  197. this.drawJoystick();
  198. },
  199. /**
  200. * Draws the joystick movable circle.
  201. * @method
  202. */
  203. drawJoystick() {
  204. if (this.isMouseDown) {
  205. this.context.fillStyle = 'rgba(0, 0, 0, 0.75)';
  206. } else {
  207. this.context.fillStyle = '#000000';
  208. }
  209. this.context.beginPath();
  210. this.context.arc(this.x, this.y, this.getJoystickRadius(), 0, 2 * Math.PI);
  211. this.context.fill();
  212. },
  213. /**
  214. * Draws the joystick background.
  215. * @method
  216. */
  217. drawBackground() {
  218. const centerX = this.getCenterX();
  219. const centerY = this.getCenterY();
  220. const radius = this.getCanvasRadius();
  221. // Draw the background circle
  222. this.context.fillStyle = '#87CEEB';
  223. this.context.beginPath();
  224. this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
  225. this.context.fill();
  226. const pointOffset = radius / 8;
  227. const halfPointOffset = pointOffset / 2;
  228. // draw center cross
  229. this.context.lineWidth = 2;
  230. this.context.strokeStyle = '#4682B4';
  231. this.context.beginPath();
  232. this.context.moveTo(centerX, centerY - pointOffset);
  233. this.context.lineTo(centerX, centerY + pointOffset);
  234. this.context.stroke();
  235. this.context.beginPath();
  236. this.context.moveTo(centerX - pointOffset, centerY);
  237. this.context.lineTo(centerX + pointOffset, centerY);
  238. this.context.stroke();
  239. // draw the up triangle
  240. const upTriangleStartY = centerY - ((3 * radius) / 4);
  241. this.context.fillStyle = '#4682B4';
  242. this.context.beginPath();
  243. this.context.moveTo(centerX, upTriangleStartY);
  244. this.context.lineTo(centerX - halfPointOffset, upTriangleStartY + pointOffset);
  245. this.context.lineTo(centerX + halfPointOffset, upTriangleStartY + pointOffset);
  246. this.context.fill();
  247. // draw the down triangle
  248. const downTriangleStartY = centerY + ((3 * radius) / 4);
  249. this.context.beginPath();
  250. this.context.moveTo(centerX, downTriangleStartY);
  251. this.context.lineTo(centerX - halfPointOffset, downTriangleStartY - pointOffset);
  252. this.context.lineTo(centerX + halfPointOffset, downTriangleStartY - pointOffset);
  253. this.context.fill();
  254. // draw the left triangle
  255. const leftTriangleStartX = centerX - ((3 * radius) / 4);
  256. this.context.beginPath();
  257. this.context.moveTo(leftTriangleStartX, centerY);
  258. this.context.lineTo(leftTriangleStartX + pointOffset, centerY - halfPointOffset);
  259. this.context.lineTo(leftTriangleStartX + pointOffset, centerY + halfPointOffset);
  260. this.context.fill();
  261. // draw the right triangle
  262. const rightTriangleStartX = centerX + ((3 * radius) / 4);
  263. this.context.beginPath();
  264. this.context.moveTo(rightTriangleStartX, centerY);
  265. this.context.lineTo(rightTriangleStartX - pointOffset, centerY - halfPointOffset);
  266. this.context.lineTo(rightTriangleStartX - pointOffset, centerY + halfPointOffset);
  267. this.context.fill();
  268. },
  269. /**
  270. * Set the size of the canvas.
  271. * @method
  272. */
  273. setCanvasSize() {
  274. this.canvas.width = this.joystickElement.clientWidth;
  275. this.canvas.height = this.joystickElement.clientHeight;
  276. },
  277. /**
  278. * Get the central horizontal value of canvas.
  279. * @method
  280. * @returns {Number} Canvas width divided by 2.
  281. */
  282. getCenterX() {
  283. return this.canvas.width / 2;
  284. },
  285. /**
  286. * Get the central vertical value of canvas.
  287. * @method
  288. * @returns {Number} Canvas height divided by 2.
  289. */
  290. getCenterY() {
  291. return this.canvas.height / 2;
  292. },
  293. /**
  294. * Get the center of the joystick's canvas.
  295. * @method
  296. * @returns {Number} Radius times the lowest value between height or width divided by 2.
  297. */
  298. getCanvasRadius() {
  299. return (this.radiusRatio * Math.min(this.canvas.width, this.canvas.height)) / 2;
  300. },
  301. /**
  302. * Get the joystick radius.
  303. * @method
  304. * @returns {Number} Canvas radius divided by 6.
  305. */
  306. getJoystickRadius() {
  307. return this.getCanvasRadius() / 6;
  308. },
  309. /**
  310. * Emit the joystick position to be sent to robot.
  311. * @method
  312. */
  313. emitJoystickPosition() {
  314. const event = {
  315. x: ((this.x - this.getCenterX()) * this.absoluteMaxX)
  316. / (this.getCanvasRadius() - this.getJoystickRadius()),
  317. y: ((this.y - this.getCenterY()) * this.absoluteMaxY)
  318. / (this.getCanvasRadius() - this.getJoystickRadius()),
  319. };
  320. if (this.enable) {
  321. this.bus.$emit('joystick-position-change', event);
  322. }
  323. },
  324. },
  325. };
  326. </script>
  327. <style>
  328. .inner-joystick-container{
  329. padding:10px;
  330. height: 100%;
  331. width: 100%;
  332. }
  333. </style>