Hi folks,
I'm already playing with the idea of having some kind of library of customizable SVG widgets (camera, sprinkler ...) which can be used in floorplans. To demonstrate what I mean, I have create a customizable camera widget that can be used in the SVG node.
-
The camera definition contains a series of custom CSS variables (whose name start with
--
):<defs> <g id="circle_definition"> <circle style="--arc-radians:calc(2 * 3.1416 * 10 * var(--arc-angle) / 360); --arc-start:calc((var(--arc-center) - (var(--arc-angle)) / 2) * 1deg); --arc-scale:calc(var(--arc-radius) / 10 / 2); transform:scale(var(--arc-scale)) rotate(var(--arc-start)); opacity:var(--arc-opacity);" cx="0" cy="0" r="10" fill="transparent" stroke="var(--arc-color)" stroke-width="20" stroke-dasharray="var(--arc-radians) calc(2 * 3.1416 * 10)"></circle> <circle style="transform:scale(var(--icon-size))" cx="0" cy="0" r="1" fill="white" stroke-width="0"></circle> <text x="0" y="0" font-family="FontAwesome" fill="var(--icon-color)" stroke="none" font-size="var(--icon-size)" text-anchor="middle" alignment-baseline="middle" stroke-width="1">fa-video-camera</text> </g> </defs>
Some remarks:
- For those wondering: "yes" I was a hell for me to get this working. CSS is not my BFF
- There are other ways to create definitions, like a
symbol
. However IMHO a simple group (without viewbox) was much more flexible for my purpose. So I gave up experimenting with symbols... - Perhaps I should apply default values to all those CSS variables...
-
You can (re)use the above definitions multiple times in a single drawing. Just specify an id, a location (x, y) and values for the CSS variables:
<use xlink:href="#circle_definition" href="#circle_definition" id="my_camera" x="680" y="270" style=" --arc-angle:90; --arc-radius:140; --arc-center:225; --arc-color:blue; --arc-opacity:0.3; --icon-size:25; --icon-color:blue;" />
-
Now you can override those (default) values from step 2, by setting new values via your Node-RED flow. For example when you PTZ control your camera - using my Onvif nodes - you could also move the camera on the floorplan ...
The following flow demonstrates how to change the CSS variables via sliders/colorpickers in the dashoard:
[{"id":"663c0bc1.051714","type":"ui_svg_graphics","z":"7f1827bd.8acfe8","group":"5ae1b679.de89c8","order":9,"width":"12","height":"9","svgString":"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" preserveAspectRatio=\"none meet\" x=\"0\" y=\"0\" viewBox=\"0 0 900 710\" width=\"100%\" height=\"100%\">\n <defs>\n <g id=\"circle_definition\">\n <!-- https://glennmccomb.com/articles/building-a-pure-css-animated-svg-spinner/#:~:text=When%20applied%20to%20an%20SVG,depending%20on%20the%20circle's%20radius.&text=A%20stroke%2Ddasharray%20of%20157,with%20r%3D%2245%22%20. -->\n <!-- https://www.geeksforgeeks.org/arc-length-angle/ -->\n <circle style=\"--arc-radians:calc(2 * 3.1416 * 10 * var(--arc-angle) / 360);\n --arc-start:calc((var(--arc-center) - (var(--arc-angle)) / 2) * 1deg);\n --arc-scale:calc(var(--arc-radius) / 10 / 2); \n transform:scale(var(--arc-scale)) rotate(var(--arc-start));\n opacity:var(--arc-opacity);\n \" cx=\"0\" cy=\"0\" r=\"10\" fill=\"transparent\" stroke=\"var(--arc-color)\" stroke-width=\"20\" stroke-dasharray=\"var(--arc-radians) calc(2 * 3.1416 * 10)\"></circle>\n <circle style=\"transform:scale(var(--icon-size))\" cx=\"0\" cy=\"0\" r=\"1\" fill=\"white\" stroke-width=\"0\"></circle>\n <text x=\"0\" y=\"0\" font-family=\"FontAwesome\" fill=\"var(--icon-color)\" stroke=\"none\" font-size=\"var(--icon-size)\" text-anchor=\"middle\" alignment-baseline=\"middle\" stroke-width=\"1\">fa-video-camera</text>\n </g>\n </defs>\n <image width=\"100%\" height=\"100%\" id=\"background\" xlink:href=\"https://www.roomsketcher.com/wp-content/uploads/2016/10/1-Bedroom-Floor-Plans.jpg\" />\n <!-- The x and y properties define an additional transformation (translate(x,y) on the group element. -->\n <!-- See https://svgwg.org/svg2-draft/single-page.html#struct-UseLayout -->\n <use xlink:href=\"#circle_definition\" href=\"#circle_definition\" id=\"my_camera\" x=\"680\" y=\"270\" style=\"--arc-angle:90;\n --arc-radius:140;\n --arc-center:225;\n --arc-color:blue;\n --arc-opacity:0.3;\n --icon-size:25;\n --icon-color:blue;\" />\n</svg>","clickableShapes":[],"javascriptHandlers":[],"smilAnimations":[{"id":"","targetId":"","classValue":"","attributeName":"transform","transformType":"rotate","fromValue":"","toValue":"","trigger":"msg","duration":"1","durationUnit":"s","repeatCount":"0","end":"restore","delay":"1","delayUnit":"s","custom":""}],"bindings":[],"showCoordinates":true,"autoFormatAfterEdit":false,"showBrowserErrors":true,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":false,"outputField":"payload","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panning":"both","zooming":"enabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":"200","name":"SVG with Javascript","x":1120,"y":460,"wires":[[]]},{"id":"c2e22011.78a16","type":"ui_slider","z":"7f1827bd.8acfe8","name":"","label":"--arc-radius","tooltip":"","group":"5ae1b679.de89c8","order":1,"width":0,"height":0,"passthru":true,"outs":"all","topic":"--arc-radius","min":"50","max":"240","step":"5","x":490,"y":460,"wires":[["5e94e0a9.f87bf"]]},{"id":"90f9f668.be25c8","type":"ui_slider","z":"7f1827bd.8acfe8","name":"","label":"--arc-opacity","tooltip":"","group":"5ae1b679.de89c8","order":4,"width":0,"height":0,"passthru":true,"outs":"all","topic":"--arc-opacity","min":"0","max":"1","step":"0.05","x":490,"y":640,"wires":[["5e94e0a9.f87bf"]]},{"id":"9ac69b92.2002e8","type":"ui_slider","z":"7f1827bd.8acfe8","name":"","label":"--icon-size","tooltip":"","group":"5ae1b679.de89c8","order":7,"width":0,"height":0,"passthru":true,"outs":"all","topic":"--icon-size","min":"20","max":"40","step":"1","x":490,"y":700,"wires":[["5e94e0a9.f87bf"]]},{"id":"5e94e0a9.f87bf","type":"change","z":"7f1827bd.8acfe8","name":"Update CSS variable","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\t \"command\":\"update_style\",\t \"selector\":\"#my_camera\",\t \"attributeName\":$.topic,\t \"attributeValue\":$.payload\t}","tot":"jsonata"},{"t":"delete","p":"topic","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":460,"wires":[["663c0bc1.051714"]]},{"id":"d13d682b.0da4e8","type":"inject","z":"7f1827bd.8acfe8","name":"Default radius","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"140","payloadType":"num","x":280,"y":460,"wires":[["c2e22011.78a16"]]},{"id":"7a2943eb.86efcc","type":"inject","z":"7f1827bd.8acfe8","name":"Default opacity","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"0.3","payloadType":"num","x":280,"y":640,"wires":[["90f9f668.be25c8"]]},{"id":"c01c7ed6.4fe48","type":"inject","z":"7f1827bd.8acfe8","name":"Default icon size","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"25","payloadType":"num","x":290,"y":700,"wires":[["9ac69b92.2002e8"]]},{"id":"adeba630.0fa0c8","type":"ui_slider","z":"7f1827bd.8acfe8","name":"","label":"--arc-angle","tooltip":"","group":"5ae1b679.de89c8","order":2,"width":0,"height":0,"passthru":true,"outs":"all","topic":"--arc-angle","min":"0","max":"360","step":"2","x":490,"y":520,"wires":[["5e94e0a9.f87bf"]]},{"id":"62229620.48cdc8","type":"inject","z":"7f1827bd.8acfe8","name":"Default angle","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"90","payloadType":"num","x":280,"y":520,"wires":[["adeba630.0fa0c8"]]},{"id":"c9e6880c.95d8f8","type":"ui_slider","z":"7f1827bd.8acfe8","name":"","label":"--arc-center","tooltip":"","group":"5ae1b679.de89c8","order":3,"width":0,"height":0,"passthru":true,"outs":"all","topic":"--arc-center","min":"0","max":"360","step":"2","x":490,"y":580,"wires":[["5e94e0a9.f87bf"]]},{"id":"44840ddf.3c0824","type":"inject","z":"7f1827bd.8acfe8","name":"Default center","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"225","payloadType":"num","x":280,"y":580,"wires":[["c9e6880c.95d8f8"]]},{"id":"549507b4.9af478","type":"ui_colour_picker","z":"7f1827bd.8acfe8","name":"","label":"--arc-color","group":"5ae1b679.de89c8","format":"hex","outformat":"string","showSwatch":true,"showPicker":false,"showValue":false,"showHue":false,"showAlpha":false,"showLightness":true,"square":"false","dynOutput":"false","order":5,"width":0,"height":0,"passthru":false,"topic":"--arc-color","x":490,"y":760,"wires":[["24093c69.0c72f4"]]},{"id":"24093c69.0c72f4","type":"function","z":"7f1827bd.8acfe8","name":"\"#\"...","func":"// The hex color value should have a \"#\" as prefix ...\nmsg.payload = \"#\" + msg.payload;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":650,"y":760,"wires":[["5e94e0a9.f87bf"]]},{"id":"8f3a39ff.bd3a88","type":"inject","z":"7f1827bd.8acfe8","name":"Default arc color","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"blue","payloadType":"str","x":290,"y":760,"wires":[["549507b4.9af478"]]},{"id":"bcad582f.7f62a8","type":"ui_colour_picker","z":"7f1827bd.8acfe8","name":"","label":"--icon-color","group":"5ae1b679.de89c8","format":"hex","outformat":"string","showSwatch":true,"showPicker":false,"showValue":false,"showHue":false,"showAlpha":false,"showLightness":true,"square":"false","dynOutput":"false","order":8,"width":0,"height":0,"passthru":false,"topic":"--icon-color","x":490,"y":820,"wires":[["24093c69.0c72f4"]]},{"id":"7296c6a.74ff438","type":"inject","z":"7f1827bd.8acfe8","name":"Default icon color","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"blue","payloadType":"str","x":290,"y":820,"wires":[["bcad582f.7f62a8"]]},{"id":"5ae1b679.de89c8","type":"ui_group","z":"","name":"Camera angle demo","tab":"3667e211.c08f0e","order":1,"disp":true,"width":"12","collapse":false},{"id":"3667e211.c08f0e","type":"ui_tab","name":"Home","icon":"dashboard","order":1,"disabled":false,"hidden":false}]
Which gives the following result:
P.S. I also tried to add a thumbnail image to my camera definition, like in this discussion. However that seems not possible: the SVG image element has a xlink:href
attribute (which I need to update to push new images to it). However that is not a CSS property, so I cannot use a CSS variable for it. Moreover I cannot access that image element via Javascript, since the definition is "use"d in a closed shadow dom (which means you cannot access it)
Not sure what would be the best way to have the community cooperate to share such customizable widgets (sprinklers, ...)?
As always, all 'constructive' ideas are welcome!
Hope you like it as much as I do...
Bart