Tag Archives: My First Gauge

What, ANOTHER Gauge?

Now, before you say anything – this gauge is different to the rest.. It is all mine and it EVOLVING (last update Feb 03) if you’ve seen this blog entry before today…

Gauges: Peter Scargill's GaugeI’ve spent a lot of time ploughing through code, some of it years old (SteelSeries) and though many of these designs are beautiful, most are not that well supported if at all and some comprise libraries that are kitchen sink jobs that take some learning, so much so that recently, in a fit of peak, after failing to figure out how to add a simple second dial or eliminate the wasted space of that metal outer ring in SteelSeries (it is easy to make it disappear but not the space it occupies) that I decided to “give it a go” myself.

What you see here is not finished but it works (and is 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.

Images: There are several images on my site – don’t use them permanently please – grab them for yourself – they likely will change 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.

Centre 1 by Peter ScargillCentre 2 by Peter ScargillCentre 2 by Peter Scargill

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.

tmpFF04Glowing rings are easy to add… and I’m working on an LCD display background image that’s hopefully a little more convincing than the canvas-manufactured version I’m using right now.

Glowing rings by Peter Scargill - in PowerPoint

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.

What I HAVE done elsewhere is use Z-laying with multiple canvases to achieve layering as you will see in the link below. Originally I had 3 layers – the background, the bits that change and the knob over the needle – all of which SHOULD have worked perfectly – but for reasons beyond me the knob didn’t always show. I moved it in with the dial and all is well – so only two identical canvas units are needed. This seems like a good general way forward. I will change my main dial code to follow this pattern eventually when I've pondered what effect this will have on interfacing.  Anyway, I put this together from scratch and it seems to work ok, Take a look at this.. https://jsfiddle.net/scargill/r4qstas2/6/

tmp42D9

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.

Fonts: Fonts are always a thing – and the normal web ones are today considered to be utterly pants. So – TTF fonts are good but also large – so I found this online convertor. You simply drag a TTF file of your choosing (clearly if for commercial purposes, you need to make sure it is free) and you get the option to download from a link – it works. So I wanted a nice dot matrix font for the little display here – and I found “Dots all for Now JL” which is pretty but also 44K. I ran it through the convertor and in less than a minute I was up and running with a font that is only 7k.

To use the font, I did this..

<style type="text/css">
@font-face {
font-family: "DOTMAT";
src: url("/myfonts/dotsalfn.woff") format('woff');
}
</style>

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"}]
Facebooktwittergoogle_pluspinterestlinkedin