Is there a Joystick Node?

I see a button, a numeric entry, a slider, and some other things, but I don't see a joystick. Is there one?

I'm looking for a 2-dimensional slider of sorts, that I can drag with the mouse and adjust both X and Y coordinates simultaneously. I'll hook it up to a VISCA node to command the pan/tilt of a remote camera.

Bonus points if it returns to center when released, or can be wired to do that.
Though it could also be interesting to not return to center and command the absolute position of the camera instead. I guess either way can work.

You should be able to build something in a UI_template using this....

1 Like

Okay, I've looked through that, and I can see some similarities to what I did in C++ and Qt a few years ago, but I'm not an HTML or JS guy at all! And I seem to be beating my head against a wall in putting it together with the UI_template node.

I gather that the Joystick example is an HTML layout that uses a separate JS module to do all the work, which is fine, except that the UI_template node doesn't take JS?

There's a Function node that does take JS, but it only runs when it receives a message?
I guess I can give it a constant stream of dummy messages - the Inject node can do that - but it seems like something ought to work like the UI_slider node, as in completely standalone with messages being sent as the user moves it.

And besides all that, how do I get the mouse or touch coordinates anyway? Do the events still work in Node-RED that the Joystick example relies on?

Sure it does - HTML can have a <script> tag (so can the ui_template)

The function node ran is server (NOT client side / dashboard)

Read the help info and search the forum - it is perfectly possible (and has been covered lots and lots of times - search ui template send message)

the joystick demo runs client side (has nothing to do with node-red). If you make it work in the ui_template (i.e. the browser) and hook up the events to send messages to node red - yes.

Here is a demo in node-red...

[{"id":"1d2a0be.ace77f4","type":"ui_template","z":"64ce1d00.d049e4","group":"dce9e7a2.d20c78","name":"joy.js","order":4,"width":0,"height":0,"format":"<script id=\"joy\">\n    console.log(\"joystick code\")\n /*\n * Name          : joy.js\n * @author       : Roberto D'Amico (Bobboteck)\n * Last modified : 09.06.2020\n * Revision      : 1.1.6\n *\n * Modification History:\n * Date         Version     Modified By\t\tDescription\n * 2020-06-09\t1.1.6\t\tRoberto D'Amico\tFixed Issue #10 and #11\n * 2020-04-20\t1.1.5\t\tRoberto D'Amico\tCorrect: Two sticks in a row, thanks to @liamw9534 for the suggestion\n * 2020-04-03               Roberto D'Amico Correct: InternalRadius when change the size of canvas, thanks to @vanslipon for the suggestion\n * 2020-01-07\t1.1.4\t\tRoberto D'Amico Close #6 by implementing a new parameter to set the functionality of auto-return to 0 position\n * 2019-11-18\t1.1.3\t\tRoberto D'Amico\tClose #5 correct indication of East direction\n * 2019-11-12   1.1.2       Roberto D'Amico Removed Fix #4 incorrectly introduced and restored operation with touch devices\n * 2019-11-12   1.1.1       Roberto D'Amico Fixed Issue #4 - Now JoyStick work in any position in the page, not only at 0,0\n * \n * The MIT License (MIT)\n *\n *  This file is part of the JoyStick Project (https://github.com/bobboteck/JoyStick).\n *\tCopyright (c) 2015 Roberto D'Amico (Bobboteck).\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n * \n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * @desc Principal object that draw a joystick, you only need to initialize the object and suggest the HTML container\n * @costructor\n * @param container {String} - HTML object that contains the Joystick\n * @param parameters (optional) - object with following keys:\n *\ttitle {String} (optional) - The ID of canvas (Default value is 'joystick')\n * \twidth {Int} (optional) - The width of canvas, if not specified is setted at width of container object (Default value is the width of container object)\n * \theight {Int} (optional) - The height of canvas, if not specified is setted at height of container object (Default value is the height of container object)\n * \tinternalFillColor {String} (optional) - Internal color of Stick (Default value is '#00AA00')\n * \tinternalLineWidth {Int} (optional) - Border width of Stick (Default value is 2)\n * \tinternalStrokeColor {String}(optional) - Border color of Stick (Default value is '#003300')\n * \texternalLineWidth {Int} (optional) - External reference circonference width (Default value is 2)\n * \texternalStrokeColor {String} (optional) - External reference circonference color (Default value is '#008000')\n * \tautoReturnToCenter {Bool} (optional) - Sets the behavior of the stick, whether or not, it should return to zero position when released (Default value is True and return to zero)\n */\nvar JoyStick = (function (container, parameters) {\n    parameters = parameters || {};\n    var title = (typeof parameters.title === \"undefined\" ? \"joystick\" : parameters.title),\n        width = (typeof parameters.width === \"undefined\" ? 0 : parameters.width),\n        height = (typeof parameters.height === \"undefined\" ? 0 : parameters.height),\n        internalFillColor = (typeof parameters.internalFillColor === \"undefined\" ? \"#00AA00\" : parameters.internalFillColor),\n        internalLineWidth = (typeof parameters.internalLineWidth === \"undefined\" ? 2 : parameters.internalLineWidth),\n        internalStrokeColor = (typeof parameters.internalStrokeColor === \"undefined\" ? \"#003300\" : parameters.internalStrokeColor),\n        externalLineWidth = (typeof parameters.externalLineWidth === \"undefined\" ? 2 : parameters.externalLineWidth),\n        externalStrokeColor = (typeof parameters.externalStrokeColor === \"undefined\" ? \"#008000\" : parameters.externalStrokeColor),\n        autoReturnToCenter = (typeof parameters.autoReturnToCenter === \"undefined\" ? true : parameters.autoReturnToCenter);\n\n    // Create Canvas element and add it in the Container object\n    var objContainer = document.getElementById(container);\n    var canvas = document.createElement(\"canvas\");\n    canvas.id = title;\n    if (width === 0) { width = objContainer.clientWidth; }\n    if (height === 0) { height = objContainer.clientHeight; }\n    canvas.width = width;\n    canvas.height = height;\n    objContainer.appendChild(canvas);\n    var context = canvas.getContext(\"2d\");\n\n    var pressed = 0; // Bool - 1=Yes - 0=No\n    var circumference = 2 * Math.PI;\n    var internalRadius = (canvas.width - ((canvas.width / 2) + 10)) / 2;\n    var maxMoveStick = internalRadius + 5;\n    var externalRadius = internalRadius + 30;\n    var centerX = canvas.width / 2;\n    var centerY = canvas.height / 2;\n    var directionHorizontalLimitPos = canvas.width / 10;\n    var directionHorizontalLimitNeg = directionHorizontalLimitPos * -1;\n    var directionVerticalLimitPos = canvas.height / 10;\n    var directionVerticalLimitNeg = directionVerticalLimitPos * -1;\n    // Used to save current position of stick\n    var movedX = centerX;\n    var movedY = centerY;\n\n    // Check if the device support the touch or not\n    if (\"ontouchstart\" in document.documentElement) {\n        canvas.addEventListener(\"touchstart\", onTouchStart, false);\n        canvas.addEventListener(\"touchmove\", onTouchMove, false);\n        canvas.addEventListener(\"touchend\", onTouchEnd, false);\n    }\n    else {\n        canvas.addEventListener(\"mousedown\", onMouseDown, false);\n        canvas.addEventListener(\"mousemove\", onMouseMove, false);\n        canvas.addEventListener(\"mouseup\", onMouseUp, false);\n    }\n    // Draw the object\n    drawExternal();\n    drawInternal();\n\n    /******************************************************\n     * Private methods\n     *****************************************************/\n\n    /**\n     * @desc Draw the external circle used as reference position\n     */\n    function drawExternal() {\n        context.beginPath();\n        context.arc(centerX, centerY, externalRadius, 0, circumference, false);\n        context.lineWidth = externalLineWidth;\n        context.strokeStyle = externalStrokeColor;\n        context.stroke();\n    }\n\n    /**\n     * @desc Draw the internal stick in the current position the user have moved it\n     */\n    function drawInternal() {\n        context.beginPath();\n        if (movedX < internalRadius) { movedX = maxMoveStick; }\n        if ((movedX + internalRadius) > canvas.width) { movedX = canvas.width - (maxMoveStick); }\n        if (movedY < internalRadius) { movedY = maxMoveStick; }\n        if ((movedY + internalRadius) > canvas.height) { movedY = canvas.height - (maxMoveStick); }\n        context.arc(movedX, movedY, internalRadius, 0, circumference, false);\n        // create radial gradient\n        var grd = context.createRadialGradient(centerX, centerY, 5, centerX, centerY, 200);\n        // Light color\n        grd.addColorStop(0, internalFillColor);\n        // Dark color\n        grd.addColorStop(1, internalStrokeColor);\n        context.fillStyle = grd;\n        context.fill();\n        context.lineWidth = internalLineWidth;\n        context.strokeStyle = internalStrokeColor;\n        context.stroke();\n    }\n\n    /**\n     * @desc Events for manage touch\n     */\n    function onTouchStart(event) {\n        pressed = 1;\n    }\n\n    // function onTouchMove(event) {\n    //     // Prevent the browser from doing its default thing (scroll, zoom)\n    //     event.preventDefault();\n    //     if (pressed === 1 && event.targetTouches[0].target === canvas) {\n    //         movedX = event.targetTouches[0].pageX;\n    //         movedY = event.targetTouches[0].pageY;\n    //         // Manage offset\n    //         if (canvas.offsetParent.tagName.toUpperCase() === \"BODY\") {\n    //             movedX -= canvas.offsetLeft;\n    //             movedY -= canvas.offsetTop;\n    //         }\n    //         else {\n    //             movedX -= canvas.offsetParent.offsetLeft;\n    //             movedY -= canvas.offsetParent.offsetTop;\n    //         }\n    //         // Delete canvas\n    //         context.clearRect(0, 0, canvas.width, canvas.height);\n    //         // Redraw object\n    //         drawExternal();\n    //         drawInternal();\n    //     }\n    // }\n  function onTouchMove(event)\n  {\n    // Prevent the browser from doing its default thing (scroll, zoom)\n    event.preventDefault();\n    if(pressed === 1 && event.targetTouches[0].target === canvas)\n    {\n      movedX = event.targetTouches[0].offsetX;\n      movedY = event.targetTouches[0].offsetY;\n      // Delete canvas\n      context.clearRect(0, 0, canvas.width, canvas.height);\n      // Redraw object\n      drawExternal();\n      drawInternal();\n    }\n  } \n    function onTouchEnd(event) {\n        pressed = 0;\n        // If required reset position store variable\n        if (autoReturnToCenter) {\n            movedX = centerX;\n            movedY = centerY;\n        }\n        // Delete canvas\n        context.clearRect(0, 0, canvas.width, canvas.height);\n        // Redraw object\n        drawExternal();\n        drawInternal();\n        //canvas.unbind('touchmove');\n    }\n\n    /**\n     * @desc Events for manage mouse\n     */\n    function onMouseDown(event) {\n        pressed = 1;\n    }\n\n    // function onMouseMove(event) {\n    //     if (pressed === 1) {\n    //         movedX = event.pageX;\n    //         movedY = event.pageY;\n    //         // Manage offset\n    //         if (canvas.offsetParent.tagName.toUpperCase() === \"BODY\") {\n    //             movedX -= canvas.offsetLeft;\n    //             movedY -= canvas.offsetTop;\n    //         }\n    //         else {\n    //             movedX -= canvas.offsetParent.offsetLeft;\n    //             movedY -= canvas.offsetParent.offsetTop;\n    //         }\n    //         // Delete canvas\n    //         context.clearRect(0, 0, canvas.width, canvas.height);\n    //         // Redraw object\n    //         drawExternal();\n    //         drawInternal();\n    //     }\n    // }\n  function onMouseMove(event) \n  {\n    if(pressed === 1)\n    {\n      movedX = event.offsetX;\n      movedY = event.offsetY;\n      // Delete canvas\n      context.clearRect(0, 0, canvas.width, canvas.height);\n      // Redraw object\n      drawExternal();\n      drawInternal();\n    }\n  }\n\n    function onMouseUp(event) {\n        pressed = 0;\n        // If required reset position store variable\n        if (autoReturnToCenter) {\n            movedX = centerX;\n            movedY = centerY;\n        }\n        // Delete canvas\n        context.clearRect(0, 0, canvas.width, canvas.height);\n        // Redraw object\n        drawExternal();\n        drawInternal();\n        //canvas.unbind('mousemove');\n    }\n\n    /******************************************************\n     * Public methods\n     *****************************************************/\n\n    /**\n     * @desc The width of canvas\n     * @return Number of pixel width \n     */\n    this.GetWidth = function () {\n        return canvas.width;\n    };\n\n    /**\n     * @desc The height of canvas\n     * @return Number of pixel height\n     */\n    this.GetHeight = function () {\n        return canvas.height;\n    };\n\n    /**\n     * @desc The X position of the cursor relative to the canvas that contains it and to its dimensions\n     * @return Number that indicate relative position\n     */\n    this.GetPosX = function () {\n        return movedX;\n    };\n\n    /**\n     * @desc The Y position of the cursor relative to the canvas that contains it and to its dimensions\n     * @return Number that indicate relative position\n     */\n    this.GetPosY = function () {\n        return movedY;\n    };\n\n    /**\n     * @desc Normalizzed value of X move of stick\n     * @return Integer from -100 to +100\n     */\n    this.GetX = function () {\n        return (100 * ((movedX - centerX) / maxMoveStick)).toFixed();\n    };\n\n    /**\n     * @desc Normalizzed value of Y move of stick\n     * @return Integer from -100 to +100\n     */\n    this.GetY = function () {\n        return ((100 * ((movedY - centerY) / maxMoveStick)) * -1).toFixed();\n    };\n\n    /**\n     * @desc Get the direction of the cursor as a string that indicates the cardinal points where this is oriented\n     * @return String of cardinal point N, NE, E, SE, S, SW, W, NW and C when it is placed in the center\n     */\n    this.GetDir = function () {\n        var result = \"\";\n        var orizontal = movedX - centerX;\n        var vertical = movedY - centerY;\n\n        if (vertical >= directionVerticalLimitNeg && vertical <= directionVerticalLimitPos) {\n            result = \"C\";\n        }\n        if (vertical < directionVerticalLimitNeg) {\n            result = \"N\";\n        }\n        if (vertical > directionVerticalLimitPos) {\n            result = \"S\";\n        }\n\n        if (orizontal < directionHorizontalLimitNeg) {\n            if (result === \"C\") {\n                result = \"W\";\n            }\n            else {\n                result += \"W\";\n            }\n        }\n        if (orizontal > directionHorizontalLimitPos) {\n            if (result === \"C\") {\n                result = \"E\";\n            }\n            else {\n                result += \"E\";\n            }\n        }\n\n        return result;\n    };\n});\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"global","x":1730,"y":100,"wires":[[]]},{"id":"f4c1920a.492c5","type":"ui_template","z":"64ce1d00.d049e4","group":"dea21ffd.1483","name":"joystick 1","order":4,"width":"6","height":"12","format":"<div id=\"joy1\" style=\"width:200px;height:200px;margin:50px\"></div>\nPosizione X:<input id=\"joy1PosizioneX\" type=\"text\" /><br />\nPosizione Y:<input id=\"joy1PosizioneY\" type=\"text\" /><br />\nDirezione:<input id=\"joy1Direzione\" type=\"text\" /><br />\nX :<input id=\"joy1X\" type=\"text\" /></br>\nY :<input id=\"joy1Y\" type=\"text\" />\n\n<script>\n\n(function(scope) {\n    console.log(\"joy1 demo scope\");\n    var Joy1PosMemory = {};\n    var Joy1 = new JoyStick('joy1');\n    setInterval(function(){ \n        var Joy1Current = {};\n        Joy1Current.inX = Joy1.GetPosX(); \n        Joy1Current.inY = Joy1.GetPosY(); \n        Joy1Current.dir =Joy1.GetDir();\n        Joy1Current.x =Joy1.GetX();\n        Joy1Current.y =Joy1.GetY();\n        if (\n            Joy1Current.inX != Joy1PosMemory.inX || \n            Joy1Current.inY != Joy1PosMemory.inY || \n            Joy1Current.dir != Joy1PosMemory.dir || \n            Joy1Current.x != Joy1PosMemory.x || \n            Joy1Current.y != Joy1PosMemory.y  \n        ) {\n            ///value has changed - send msg to node-red\n            scope.send ({topic: \"joy1\", payload: Joy1Current} );\n        }\n        Joy1PosMemory = {...Joy1Current};//copy current to memory var\n    }, 50);\n\n    //************************ these can be removed *****************\n    var joy1IinputPosX = document.getElementById(\"joy1PosizioneX\");\n    var joy1InputPosY = document.getElementById(\"joy1PosizioneY\");\n    var joy1Direzione = document.getElementById(\"joy1Direzione\");\n    var joy1X = document.getElementById(\"joy1X\");\n    var joy1Y = document.getElementById(\"joy1Y\");\n\n    setInterval(function(){ joy1IinputPosX.value=Joy1.GetPosX(); }, 50);\n    setInterval(function(){ joy1InputPosY.value=Joy1.GetPosY(); }, 50);\n    setInterval(function(){ joy1Direzione.value=Joy1.GetDir(); }, 50);\n    setInterval(function(){ joy1X.value=Joy1.GetX(); }, 50);\n    setInterval(function(){ joy1Y.value=Joy1.GetY(); }, 50);\n    //***************************************************************\n})(scope);\n\n\n\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":1740,"y":140,"wires":[["c2dc33fb.97d72"]]},{"id":"c2dc33fb.97d72","type":"debug","z":"64ce1d00.d049e4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1770,"y":200,"wires":[]},{"id":"dce9e7a2.d20c78","type":"ui_group","name":"Object detection","tab":"5132060d.4cde48","order":1,"disp":true,"width":"7","collapse":false},{"id":"dea21ffd.1483","type":"ui_group","name":"Pump Status","tab":"dd590918.d334c8","order":1,"disp":true,"width":"6","collapse":false},{"id":"5132060d.4cde48","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false},{"id":"dd590918.d334c8","type":"ui_tab","name":"Pool Control","icon":"dashboard","disabled":false,"hidden":false}]
3 Likes

Nice Steve!!!!

I think I get it now. Thanks! I was getting hung up on the difference between a webpage with a traditional file structure behind it, and the collection of independent items that most graphical languages that I've used seem to be.
(and not being an HTML or JS guy, I was also unfamiliar with the <script> tag)

I'm used to a graphical language being completely unaware of other nodes in the graph, except that messages "magically appear" at their inputs. From the way that your example is structured (and obviously works on my end too), I'm guessing that's not the case for Node-RED? The nodes are aware of each other, through more than just their graphical connections?

My first-glance interpretation says that the unconnected node that has the actual joystick in it, can be deleted with no change at all in functionality, simply because it's not connected to anything. So I didn't think about structuring a flow that way. But clearly it's needed here, despite that non-connection!
(Maybe I can copy/paste everything into the same node, just to eliminate that source of confusion for me - 'cause I know I'm going to delete it at some point in the name of "cleaning up" - at the expense of some clutter in that one node?)

At any rate, you've given me something to play with, and to go back to when I inevitably screw it up beyond my ability to repair. Thanks again!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.