Now, before you say anything – this gauge is different to the rest.. It is all mine and it EVOLVING if you’ve seen this blog entry before today…
What you see here is not finished but it works (and a lot prettier when you see it running). It scales without internal adjustments other than the gauge size as I need something that will easily adapt to differing Node-Red template blocks.
There are several images on my site – don’t use them permanently please – grab them for yourself – they likely will change this week anyway.
As the dials move below the (circular) set-points the LEDS will go on and off – one triggers BELOW the set-point (heat), the other ABOVE (dehumidifier).
In the process of making this I’ve made a number of dials and centres – all in PowerPoint – very simple.
I call these three metalCarvedCentre.png, greenCarveCentre.png and greyCentre.png respectively. I don’t plan to make a lot as there must be millions of needles and centres out there already.
If you poke the test values, you should see the gauge needles move smoothly to their destination. So I’m really happy about (and this happens a LOT it seems) having to update the whole thing every 20ms for animation when all I want is to update the dials (and an LCD panel eventually) – but I can’t find LAYERS anywhere – someone’s done a layering system but it looks awfully complicated. However, this does work and I note others simply redraw the lot for animation!
A timer runs constantly but does nothing unless a change is made and the values are then incremented or decremented until they match the required values.
I think this will make a fine addition to the visual tools we have but still needs much tidying up so no comments about code quality please.
To use the font, I did this..
and then just referred to DOTMAT as you would any font. Now there are all sorts of warnings on the web about ensuring fonts are loaded first and working with CANVAS etc. – but somehow, in Node-Red, it all just works… which is nice and keeps the code simple.
And that’s it for now. I’m working on improving the code.
[{"id":"22bad52a.82347a","type":"ui_template","z":"c552e8d2.712b48","group":"40cf30b4.d9549","name":"MyGauge","order":0,"width":"6","height":"6","format":"<style>\n @font-face {\n font-family: \"DOTMAT\";\n src: url(\"/myfonts/dotsalfn.woff\") format('woff');\n } \n</style>\n\n<script>\n var showNeedle2 = true;\nvar showLED1 = true;\nvar showLED2 = true;\nvar needleWidth1 = 1;\nvar needleWidth2 = 1;\n\nvar set1 = 0;\nvar set2 = 0;\nvar value1 = 0;\nvar value2 = 0;\n\nvar setpoint1 = -1;\nvar setpoint2 = -1;\nvar degrees = -1;\nvar degrees2 = -1;\n\nvar title = \"Pete's Aircon\";\nvar subTitle = \"Hmm1\";\nvar ledTitle1 = \"DEHUM\";\nvar ledTitle2 = \"HEATING\";\n\nvar needle1 = new Image();\nvar needle2 = new Image();\nvar centre = new Image();\n\n\n\nvar direction1 = 1;\nvar direction2 = 1;\n\n(function(scope) {\n scope.$watch('msg', function(msg) {\n if (typeof(msg.value1) != \"undefined\") value1 = msg.value1;\n if (typeof(msg.value2) != \"undefined\") value2 = msg.value2;\n if (typeof(msg.set1) != \"undefined\") set1 = msg.set1;\n if (typeof(msg.set2) != \"undefined\") set2 = msg.set2;\n });\n})(scope);\n\n\n\n\nfunction n(n) {\n return n > 9 ? \"\" + n : \"0\" + n;\n}\n\n\nfunction init() {\n needle1.src = \"http://www.scargill.net/things/needles/redCurvedNeedle.png\";\n needle2.src = \"http://www.scargill.net/things/needles/greenCurvedNeedle.png\";\n centre.src = \"http://www.scargill.net/things/needles/greyCarvedCentre.png\";\n canvas = document.getElementById(\"fred\");\n ctx = canvas.getContext(\"2d\");\n cX = Math.floor(canvas.width / 2);\n cY = Math.floor(canvas.height / 2);\n dX = cX / 175; // divisor for centrepiece sizing\n dY = cY / 175;\n\n setInterval(draw, 50);\n\n}\n\n// draw a wedge\nfunction drawWedge(percent, color, count) {\n var arcRadians = ((percent / 100) * 360) * (Math.PI / 180),\n startAngle = totalArc,\n endAngle = totalArc - arcRadians;\n ctx.save();\n ctx.beginPath();\n ctx.moveTo(cX, cY);\n ctx.arc(cX, cY, radius_outer, startAngle, endAngle, true);\n /** cut out the inner section by going in the opposite direction **/\n ctx.fillStyle = color;\n ctx.arc(cX, cY, radius_inner, endAngle, startAngle, false);\n ctx.closePath();\n ctx.fill()\n ctx.restore();\n totalArc -= arcRadians;\n}\n\nfunction drawbit(i, colr) {\n if (i & 1) drawWedge(1.1, colr, i);\n else drawWedge(0.2, \"#cccccc\", i);\n}\n\n// draw the donut one wedge at a time\nfunction drawDonut() {\n var r, g, b;\n b = 0;\n g = 0;\n r = 255;\n for (var i = 0; i < 100; i++) {\n var r, g, b;\n if (i < 40) {\n g += 8;\n }\n if ((i > 40) && (i < 70)) {\n g -= 8;\n r -= 12;\n }\n if (i > 70) {\n g -= 8;\n b += 12;\n }\n drawbit(i, \"rgba(\" + r + \",\" + g + \",\" + b + \",1)\");\n }\n}\n\nfunction roundRect(ctx, x, y, width, height, radius, fill, stroke) {\n if (typeof stroke == 'undefined') {\n stroke = true;\n }\n if (typeof radius === 'undefined') {\n radius = 5;\n }\n if (typeof radius === 'number') {\n radius = {\n tl: radius,\n tr: radius,\n br: radius,\n bl: radius\n };\n } else {\n var defaultRadius = {\n tl: 0,\n tr: 0,\n br: 0,\n bl: 0\n };\n for (var side in defaultRadius) {\n radius[side] = radius[side] || defaultRadius[side];\n }\n }\n ctx.beginPath();\n ctx.moveTo(x + radius.tl, y);\n ctx.lineTo(x + width - radius.tr, y);\n ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);\n ctx.lineTo(x + width, y + height - radius.br);\n ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);\n ctx.lineTo(x + radius.bl, y + height);\n ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);\n ctx.lineTo(x, y + radius.tl);\n ctx.quadraticCurveTo(x, y, x + radius.tl, y);\n ctx.closePath();\n if (fill) {\n ctx.fill();\n }\n if (stroke) {\n ctx.stroke();\n }\n}\n\nfunction drawCircle() {\n ctx.save();\n /** outer ring **/\n ctx.beginPath();\n ctx.moveTo(cX, cY);\n ctx.shadowBlur = 5 * dX;\n ctx.shadowColor = \"rgba(40,40,40,1)\";\n ctx.arc(cX, cY, radius + (8 * dX), 0, 2 * Math.PI, false);\n ctx.arc(cX, cY, radius + (6 * dX), 0, 2 * Math.PI, true);\n ctx.closePath();\n ctx.fillStyle = \"rgba(40,40,40,1)\";\n ctx.fill();\n ctx.restore();\n \n \n // do an arc of numbers...\n ctx.save();\n ctx.translate(canvas.width / 2, canvas.height / 2);\n ctx.rotate(-140 * (Math.PI / 180));\n ctx.font = \"bold \" + String(Math.floor(cX / 13)) + \"px Helvetica\";\n ctx.textAlign = 'center';\n ctx.fillStyle = '#000000';\n for (var a=0;a<=100; a+=10)\n {\n ctx.rotate(23 * (Math.PI / 180));\n ctx.fillText(n(a), 0, -(cY*0.65)); \n }\n ctx.restore();\n \n\n ctx.save();\n\n if (showLED1 == true) {\n /** Sub label 1 **/\n ctx.font = \"bold \" + String(Math.floor(cX / 14)) + \"px Helvetica\";\n\n // ctx.font = \"bold \" + String(Math.floor(cX / 14)) + \"px Helvetica\";\n ctx.textAlign = 'center';\n ctx.fillStyle = '#8A8A8A';\n ctx.fillText(ledTitle2, cX + (cX / 2.5), cY - (cY / 9), (cX / 1));\n if (degrees < setpoint2) {\n ctx.beginPath(); // red led - size and scale need to be related to canvas - currently fixed\n ctx.arc(cX + (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n ctx.closePath();\n ctx.shadowBlur = 20;\n ctx.shadowColor = \"rgba(255,0,0,1)\";\n ctx.fillStyle = \"rgba(255,0,0,1)\";\n ctx.fill();\n ctx.restore();\n } else {\n ctx.beginPath(); // red led - size and scale need to be related to canvas - currently fixed\n ctx.arc(cX + (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n ctx.closePath();\n ctx.fillStyle = \"rgba(140,40,40,1)\";\n ctx.fill();\n ctx.lineWidth = 2 * dX;\n ctx.strokeStyle = 'rgba(40,0,0,0.6)';\n ctx.stroke();\n ctx.restore();\n }\n }\n if (showLED2 == true) {\n /** Sub label 1 **/\n ctx.font = \"bold \" + String(Math.floor(cX / 14)) + \"px Helvetica\";\n ctx.textAlign = 'center';\n ctx.fillStyle = '#8A8A8A';\n ctx.fillText(ledTitle1, cX - (cX / 2.5), cY - (cY / 9), (cX / 1));\n\n if (degrees2 > setpoint1) {\n ctx.save(); // green led - size and scale need to be related to canvas - currently fixed\n ctx.beginPath();\n ctx.arc(cX - (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n ctx.closePath();\n ctx.shadowBlur = 20;\n ctx.shadowColor = \"rgba(0,200,0,1)\";\n ctx.fillStyle = \"rgba(0,200,0,1)\";\n ctx.fill();\n ctx.restore();\n } else {\n ctx.save(); // green led - size and scale need to be related to canvas - currently fixed\n ctx.beginPath();\n ctx.arc(cX - (cX / 2.5), cY, (cX / 15), 0, 2 * Math.PI, false);\n ctx.closePath();\n ctx.fillStyle = \"rgba(40,110,40,1)\";\n ctx.fill();\n ctx.lineWidth = 2 * dX;\n ctx.strokeStyle = 'rgba(0,40,0,0.6)';\n ctx.stroke();\n ctx.restore();\n }\n }\n\n /** Main label **/\n ctx.save();\n\n ctx.beginPath;\n roundRect(ctx, cX - (cX / 1.7), cY + (cY / 3), cX + (cX / 5.1), cY - (cY / 1.5), dX * 10, true);\n ctx.clip()\n\n ctx.beginPath;\n ctx.strokeStyle = 'black';\n ctx.lineWidth = 5;\n ctx.shadowBlur = 15;\n ctx.shadowColor = 'black';\n ctx.shadowOffsetX = 0;\n ctx.shadowOffsetY = 0;\n ctx.fillStyle = \"rgba(255, 255, 180, .8)\";\n roundRect(ctx, cX - (cX / 1.7) - 4, cY + (cY / 3) - 4, cX + (cX / 5.1) + 8, cY - (cY / 1.5) + 8, dX * 10, true);\n\n ctx.restore();\n ctx.save();\n\n ctx.font = \"bold \" + String(Math.floor(cX / 8)) + \"px DOTMAT\";\n ctx.textAlign = 'center';\n ctx.fillStyle = '#8A8A8A';\n ctx.fillText(title, cX, cY + (cY / 2.05));\n\n /** Sub label **/\n ctx.font = \"bold \" + String(Math.floor(cX / 12)) + \"px Helvetica\";\n ctx.textAlign = 'center';\n ctx.fillStyle = '#8A8A8A';\n ctx.fillText(subTitle, cX, cY + (cY / 1.65));\n ctx.restore();\n}\n\nfunction draw() {\n\n if ((set1 == setpoint1) && (set2 == setpoint2) && (value1 == degrees) && (value2 == degrees2)) return;\n\n if (set1 > setpoint1) setpoint1++;\n else if (set1 < setpoint1) setpoint1--;\n if (set2 > setpoint2) setpoint2++;\n else if (set2 < setpoint2) setpoint2--;\n\n if (value1 > degrees) degrees++;\n else if (value1 < degrees) degrees--;\n if (value2 > degrees2) degrees2++;\n else if (value2 < degrees2) degrees2--;\n\n\n width = 18 * dX,\n radius = cX * .9,\n radius_outer = cX * .9,\n radius_inner = (radius - width) - (11 * dX),\n kerning = 0.04,\n color_alpha = 0.3;\n totalArc = .47; // starting point for the arc\n\n ctx.save();\n // Radii of the white glow.\n innerRadius = 20 * dX;\n outerRadius = canvas.height / 2;\n gradient = ctx.createRadialGradient(canvas.width / 2, canvas.height / 2, innerRadius, canvas.width / 2, canvas.height / 2, outerRadius);\n gradient.addColorStop(0, 'white');\n gradient.addColorStop(1, '#bbbbbb');\n ctx.arc(canvas.width / 2, canvas.height / 2, canvas.height / 2 - (10 * dX), 0, 2 * Math.PI);\n ctx.fillStyle = gradient;\n ctx.fill();\n ctx.restore();\n\n drawCircle();\n drawDonut();\n \n // Humid Circle\n ctx.save();\n ctx.beginPath();\n ctx.translate(canvas.width / 2, canvas.height / 2);\n tdegrees = -204 + (degrees2 * 227 / 100)\n ctx.rotate((233 / 100 * setpoint1 - 118) * (Math.PI / 180));\n ctx.beginPath();\n ctx.arc(0, -(cY*0.82),cY*0.05, 0, 2 * Math.PI, false);\n ctx.fillStyle = 'green';\n ctx.fill();\n ctx.lineWidth = 1.5;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n ctx.restore();\n\n // Temperature Circle\n ctx.save(); \n ctx.beginPath();\n ctx.translate(canvas.width / 2, canvas.height / 2);\n tdegrees = -204 + (degrees2 * 227 / 100)\n ctx.rotate((233 / 100 * setpoint2 - 118) * (Math.PI / 180));\n //ctx.beginPath();\n ctx.arc(0, -(cY*0.82),cY*0.05, 0, 2 * Math.PI, false);\n ctx.fillStyle = 'red';\n ctx.fill();\n ctx.lineWidth = 1.5;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n ctx.restore();\n\n // Save the current drawing state\n ctx.save();\n ctx.beginPath();\n ctx.translate(canvas.width / 2, canvas.height / 2);\n tdegrees = -204 + (degrees * 227 / 100)\n ctx.rotate(tdegrees * (Math.PI / 180));\n\n // shadow on lines??\n ctx.shadowBlur = 4;\n ctx.shadowColor = \"rgba(0,0,0,0.2)\";\n ctx.shadowOffsetX = 5 * dX;\n ctx.shadowOffsetY = 5 * dX;\n\n\n ctx.drawImage(needle1, (canvas.width / 7) - (canvas.width / 4), -(canvas.height * needleWidth1 / 80), (canvas.height / 2), (canvas.width * needleWidth1 / 40));\n // Restore the previous drawing state\n ctx.restore();\n\n if (showNeedle2 == true) {\n // Save the current drawing state\n ctx.save();\n ctx.beginPath();\n ctx.translate(canvas.width / 2, canvas.height / 2);\n tdegrees = -204 + (degrees2 * 227 / 100)\n ctx.rotate(tdegrees * (Math.PI / 180));\n // shadow on lines??\n ctx.shadowBlur = 4;\n ctx.shadowColor = \"rgba(0,0,0,0.2)\";\n ctx.shadowOffsetX = 5 * dX;\n ctx.shadowOffsetY = 5 * dX;\n ctx.drawImage(needle2, (canvas.width / 7) - (canvas.width / 4), -(canvas.height * needleWidth2 / 80), (canvas.height / 2), (canvas.width * needleWidth2 / 40));\n ctx.restore();\n }\n ctx.save();\n ctx.beginPath();\n ctx.translate(canvas.width / 2, canvas.height / 2);\n // draw the centre bit then restore the previous drawing state\n ctx.drawImage(centre, (0 - (centre.height / 4)) * dX, (0 - (centre.width / 4)) * dY, (centre.height / 2) * dX, (centre.width / 2) * dY);\n ctx.restore();\n subTitle = \"Temp=\" + n(Math.floor(degrees)) + \"c \" + \"Hum=\" + n(Math.floor(degrees2)) + \"% \";\n}\n\ninit();\n\n</script>\n\n<canvas id = \"fred\"\nwidth = 310 height = 310 > </canvas>","storeOutMessages":true,"fwdInMessages":true,"x":980,"y":2480,"wires":[[]]},{"id":"fbecd5a1.989ac8","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"22","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2340,"wires":[["9f9e89fe.db3518"]]},{"id":"6e98eeac.504cf","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"35","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2380,"wires":[["9f9e89fe.db3518"]]},{"id":"9f9e89fe.db3518","type":"function","z":"c552e8d2.712b48","name":"msg.value1","func":"msg.value1=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":2360,"wires":[["22bad52a.82347a"]]},{"id":"10d0f056.a552e","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"11","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2420,"wires":[["9eb8ebf8.be3108"]]},{"id":"ae37427b.2f8b3","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"60","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2460,"wires":[["9eb8ebf8.be3108"]]},{"id":"9eb8ebf8.be3108","type":"function","z":"c552e8d2.712b48","name":"msg.value2","func":"msg.value2=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":2440,"wires":[["22bad52a.82347a"]]},{"id":"2251f312.3a575c","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"10","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2500,"wires":[["7c5ad2de.3c767c"]]},{"id":"ae3f6f46.2aed8","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"20","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2540,"wires":[["7c5ad2de.3c767c"]]},{"id":"7c5ad2de.3c767c","type":"function","z":"c552e8d2.712b48","name":"msg.set1","func":"msg.set1=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":780,"y":2520,"wires":[["22bad52a.82347a"]]},{"id":"4a2ac4f5.5906dc","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"30","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2580,"wires":[["f6ffef5f.562ca"]]},{"id":"8f5b3775.03c018","type":"inject","z":"c552e8d2.712b48","name":"","topic":"","payload":"40","payloadType":"str","repeat":"","crontab":"","once":false,"x":630,"y":2620,"wires":[["f6ffef5f.562ca"]]},{"id":"f6ffef5f.562ca","type":"function","z":"c552e8d2.712b48","name":"msg.set2","func":"msg.set2=msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":780,"y":2600,"wires":[["22bad52a.82347a"]]},{"id":"40cf30b4.d9549","type":"ui_group","z":"","name":"testa","tab":"66a97521.af8dac","disp":true,"width":"6"},{"id":"66a97521.af8dac","type":"ui_tab","z":"","name":"testa","icon":"dashboard"}]