# Having fun with SVG - a customizable camera widget

**URL:** <https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045>\
**Category:** Share Your Projects\
**Created:** [26 December 2020 18:40 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045 "2020-12-26T18:40:30Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![BartButenaers](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/bartbutenaers/32/10476_2.png) [@BartButenaers](https://discourse.nodered.org/u/BartButenaers)\
**Post date:** [26 December 2020 18:40 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/1 "2020-12-26T18:40:30Z")

</div>

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.

1. The camera _ **definition** _ contains a series of custom [CSS variables](https://medium.com/techradiant/understanding-css-variables-a0f956b281e0) (whose name start with `--`):

2. 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:

3. 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:

 ![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/9/0/900ec4da67361992c4d512847c9d14899712d1c3.png)

```auto
[{"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:

 ![svg-camera-widget](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/4/e/4e4accef01996ce8bb272d26ac0d60c746fdb1cb.gif)

P.S. I also tried to add a _ **thumbnail image** _ to my camera definition, like in this [discussion](https://discourse.nodered.org/t/having-fun-with-svg-pushing-images/37029/3). 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

---

<div class="post-metadata">

**Author:** ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)\
**Post date:** [26 December 2020 22:05 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/2 "2020-12-26T22:05:54Z")

</div>

Very nice and potentially usable with the uibuilder SVG example I wrote when you released the Dashboard SVG node.

I'm surprised you can't automate the xlink though. I seem to remember that in uibuilder with Vue pretty much anything could be data driven inside the SVG. I would have though that would hold true for Angular as well.

---

<div class="post-metadata">

**Author:** ![BartButenaers](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/bartbutenaers/32/10476_2.png) [@BartButenaers](https://discourse.nodered.org/u/BartButenaers)\
**Post date:** [26 December 2020 22:51 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/3 "2020-12-26T22:51:56Z")

</div>

> [@TotallyInformation](#):
>
> Very nice and potentially usable with the uibuilder

Hi Julian, thanks for joining.  
Sometimes I get the impression that all roads lead to uibuilder...  
The uibuilder team's sales manager must be near to a burnout 😉

> [@TotallyInformation](#):
>
> I'm surprised you can't automate the xlink though.

I tried everything within my powers (to set a new value in the `xlink:href` attribute of the `image` element inside the definition), but nothing helps:

1. `xlink:href` is not a css attribute, so cannot access it e.g. using a css variable (as you can see in this Stackoverflow [discussion](https://stackoverflow.com/questions/21205096/set-the-xlinkhref-attribute-of-an-svg-image-element-with-css)).

2. The `use` element uses the definition as a _ **shadow dom** _, so the shadow dom will be rendered (instead of the element's children):

3. As you can see in the above screenshot, the shadow dom is _ **closed** _. Following this Stackoverflow [discussion](https://stackoverflow.com/questions/63508259/html-svg-reuse-a-group-g-with-use-and-change-attributes-of-inner-elements-in/63508396#63508396) this means the shadow dom elements are not accessible via Javascript.

4. It is also not possible to access the shadow dom elements via _ **CSS selectors** _:

5. From this [article](https://tympanus.net/codrops/2015/07/16/styling-svg-use-content-css/) I understand there are not much cross-browser solutions:

First I though I could perhaps - to solve point 4 - use the [query-selector-shadow-dom](https://www.npmjs.com/package/query-selector-shadow-dom) package in the SVG node (instead of the standard querySelectorAll function), however I don't think that can work (due to the previous points).

But if you have any ideas or workarounds, please let me know!!!

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [26 December 2020 23:07 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/4 "2020-12-26T23:07:35Z")

</div>

The definition works as content storage of something to be used later on. It is against the logic that you try to change it.  
At 1 past midnight I don't have solution to provide of course 😛

---

<div class="post-metadata">

**Author:** ![BartButenaers](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/bartbutenaers/32/10476_2.png) [@BartButenaers](https://discourse.nodered.org/u/BartButenaers)\
**Post date:** [26 December 2020 23:16 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/5 "2020-12-26T23:16:27Z")

</div>

> [@hotNipi](#):
>
> The definition works as content storage of something to be used later on

Yes, but initially I had hoped that the definition's dom tree would be copied for each `use` instance. But that isn't the case, so therefore indeed I don't think I can solve this...  
Would have been nice if I could have added the video thumbnail viewer inside the definition. Because it would have been very easy for users to setup video surveillance on their floorplans. But don't think I will be able to go all the way ...

---

<div class="post-metadata">

**Author:** ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)\
**Post date:** [26 December 2020 23:27 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/6 "2020-12-26T23:27:15Z")

</div>

> [@BartButenaers](#):
>
> Sometimes I get the impression that all roads lead to uibuilder...

Well speaking as an unbiased observer, I think you might be right! Good enough for the Romans, good enough for me.

> [@BartButenaers](#):
>
> The uibuilder team's sales manager must be near to a burnout

Thankfully, the market is fairly small so I only need to advertise in one place 😉

And of course, it pretty much sells itself to the right audience 😃 Still, he doesn't get to rest just because it is Christmas 🤣

To be honest, I never thought I'd write some software that consistently averages 2k downloads a month! Nearly 84k downloads so far.

> [@BartButenaers](#):
>
> I tried everything within my powers (to set a new value in the `xlink:href` attribute of the `image` element inside the definition), but nothing helps:

Well, they will be much greater than mine!

Perhaps there is another way to achieve the same _look_ though?

---

<div class="post-metadata">

**Author:** ![BartButenaers](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/bartbutenaers/32/10476_2.png) [@BartButenaers](https://discourse.nodered.org/u/BartButenaers)\
**Post date:** [26 December 2020 23:58 UTC](https://discourse.nodered.org/t/having-fun-with-svg-a-customizable-camera-widget/38045/7 "2020-12-26T23:58:42Z")

</div>

> [@BartButenaers](#):
>
> It is also not possible to access the shadow dom elements via _ **CSS selectors** _ :

One last thing about this. This [article](https://blog.revillweb.com/open-vs-closed-shadow-dom-9f3d7427d1af) explains the difference between an open and closed shadow dom. With an open shadow dom, you could find an element (and manipulate it) of the shadow dom via the `shadowRoot`. Suppose the shadow dom tree would have been open, then I assume I could have searched through the shadow dom tree like this:

```auto
// Get the "use" element in the dom tree (= shadow host element)
var myUseElement = document.querySelectorAll("#my_camera");

// Get the root node of the shadow dom tree
var shadowRoot = myUseElement.shadowRoot;

// Search through the open shadow dom tree, and find the element with tag name "image"
var my ImageElement = shadowRoot.querySelectorAll("image");

```

But now it doesn't work that way. Because the shadow dom is closed, the shadowRoot will be `null`:

![image](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/6/0/60f324fd9d1ab5c5322367d30f2f03ddbe7c509e.png)

So I have no access to the closed shadow dom tree 🙄
