Using SVG with IDs for subcomponents - javascript

I am trying (in javascript) to access parts of a list of SVG components in a little test page, but I am not sure I can achieve what I want this way. The main question is:
Can I have sub-components having the same id in the two SVG top components?
In the code hereafter I want to change the color inside the first disk and the first rectangle. Here is what I tried, but it is not working.
Any tip would be appreciated.
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name=viewport content='width=device-width, initial-scale=1'>
<title>SVG-ID-Test</title>
</head>
<body>
<svg id="theSVGOne" width="200" height="300" fill="#d55">
<circle id="theCircle" cx="100" cy="75" r="50"
stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30"
stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<svg id="theSVGTwo" width="200" height="200">
<circle id="theCircle" cx="100" cy="75" r="50"
stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30"
stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<div id="status"></div>
<script type='text/javascript'>
let svgOne = document.getElementById('theSVGOne')
let svgTwo = document.getElementById('theSVGTwo')
let statusPane = document.getElementById('status')
statusPane.innerHTML = 'svgOne => '+svgOne.childElementCount.toString()
let circOne = svgOne.firstChild
let rctOne = svgOne.lastChild
circOne.setAttribute(('fill', '#ec3'))
rctOne.setAttribute(('fill', '#e3c'))
</script>
</body>

You can use a class instead of id on the child elements (you can have as many duplicate class attributes as you want in the document) and then use querySelector on each svg to target specific children.
let svgOne = document.getElementById('theSVGOne')
let svgTwo = document.getElementById('theSVGTwo')
let statusPane = document.getElementById('status')
statusPane.innerHTML = 'svgOne => '+svgOne.childElementCount.toString()
let circOne = svgOne.querySelector(".theCircle")
let rctOne = svgOne.querySelector(".theRectangle")
circOne.setAttribute('fill', '#ec3')
rctOne.setAttribute('fill', '#e3c')
let circTwo = svgTwo.querySelector(".theCircle")
let rctTwo = svgTwo.querySelector(".theRectangle")
circTwo.setAttribute('fill', 'blue')
rctTwo.setAttribute('fill', 'green')
<svg id="theSVGOne" width="200" height="300" fill="#d55">
<circle class="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect class="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<svg id="theSVGTwo" width="200" height="200">
<circle class="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect class="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<div id="status"></div>
Don't use IDs
While technically you can have duplicate ids in the DOM (and make it work), this is very bad practise. Id stands for identifier and should be unique to be able to "tell them apart". You can't do that with identical ids (which theCircle are we talking about?).
Problem is, html won't complain about duplicate ids (no errors are thrown).
To actually get feedback on right or wrong markup you can go to w3's validator and check your html there.
Eg. Error: Duplicate ID theCircle.
Working (but wrong) example using id:
let svgOne = document.getElementById('theSVGOne')
let svgTwo = document.getElementById('theSVGTwo')
let statusPane = document.getElementById('status')
statusPane.innerHTML = 'svgOne => '+svgOne.childElementCount.toString()
let circOne = svgOne.getElementById("theCircle")
let rctOne = svgOne.getElementById("theRectangle")
circOne.setAttribute('fill', '#ec3')
rctOne.setAttribute('fill', '#e3c')
let circTwo = svgTwo.getElementById("theCircle")
let rctTwo = svgTwo.getElementById("theRectangle")
circTwo.setAttribute('fill', 'pink')
rctTwo.setAttribute('fill', 'teal')
<svg id="theSVGOne" width="200" height="300" fill="#d55">
<circle id="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<svg id="theSVGTwo" width="200" height="200">
<circle id="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<div id="status"></div>

Related

How can I add "Loading" text when i click on a button in angular.js?

Here is the progress bar(Loading) as I know,
<svg class="center-block progress-bar-round" width="200" height="200">
<circle cx="100" cy="100" r="90" fill="none" stroke="#F8F8FF" stroke-width="8"/>
<circle cx="100" cy="100" r="90" fill="none" id="loader" class=""
stroke="#209e91" stroke-width="8" stroke-dasharray="0,20000"
transform="rotate(-90,100,100)" stroke-linecap="round"/>
<text text-anchor="middle" class="loading" x="100" y="90">Loading...</text>
<text class="percentage" text-anchor="middle" x="100" y="130">{{progress}}%</text>
</svg>
In a page, lets say there is "Continue" button, but i dont know how to connect this Loading text with that button,
How could I do it?
You can use some kind of boolean in your controller like
$scope.loading = false
Then in your html your button, and your svg, which is hidden until $scope.loading is true with ng-if
<button ng-click="continue()" ng-if="!loading">Continue</button>
<svg ng-if='loading' class="center-block progress-bar-round" width="200" height="200">
<circle cx="100" cy="100" r="90" fill="none" stroke="#F8F8FF" stroke-width="8"/>
<circle cx="100" cy="100" r="90" fill="none" id="loader" class=""
stroke="#209e91" stroke-width="8" stroke-dasharray="0,20000"
transform="rotate(-90,100,100)" stroke-linecap="round"/>
<text text-anchor="middle" class="loading" x="100" y="90">Loading...</text>
<text class="percentage" text-anchor="middle" x="100" y="130">{{progress}}%</text>
</svg>
Then finally in your controller
$scope.continue = function() {
$scope.loading = true;
// do the loading stuff, then when done
// ...
$scope.loading = false;
}

How to stroke SVG elements together, as a single element [duplicate]

I have two shapes: circle and rectangle. Want to convert them into one figure. Are there any ways to do that in SVG code?
<svg width="400" height="400">
<defs>
<g id="shape" fill="none" stroke="red">
<rect x="40" y="50" width="40" height="70" />
<circle cx="50" cy="50" r="50" />
</g>
</defs>
<use xlink:href="#shape" x="50" y="50" />
<use xlink:href="#shape" x="200" y="50" />
</svg>
Like this:
For anyone looking for the answer to the actual question of how to combine two outlined shapes into a single outlined shape (rather than putting a drop shadow on the combined shape), here is a possible solution:
<svg width="400" height="400">
<defs>
<rect id="canvas" width="100%" height="100%" fill="white" />
<rect id="shape1" x="40" y="50" width="40" height="70" />
<circle id="shape2" cx="50" cy="50" r="50" />
<mask id="shape1-cutout">
<use href="#canvas" />
<use href="#shape1" />
</mask>
<mask id="shape2-cutout">
<use href="#canvas" />
<use href="#shape2" />
</mask>
</defs>
<use href="#shape1" stroke="red" fill="none" mask="url(#shape2-cutout)" />
<use href="#shape2" stroke="red" fill="none" mask="url(#shape1-cutout)" />
</svg>
This essentially draws the circle with the rectangle shape cut out of it and draws the rectangle with the circle cut out of it. When you place these "punched out" shapes one on top of the other, you get what appears to be a single outlined shape.
Here's what the SVG actually does:
It defines a white rectangle called "canvas" that is the same size as the SVG.
It defines the two shapes that are to be combined ("shape1" and "shape2").
It defines a mask for each shape that combines the canvas (which has a fill of white) with the shape (which has a fill of black by default). Note that when you apply a mask to a shape, the part of the shape that corresponds to the white area of the mask is shown, while the part that corresponds with black part is hidden.
It draws each shape with the the mask of the other shape applied.
You can make a <mask> or a <clipPath> from the two shapes and then use that to mask a third shape. You can then apply your drop shadow to that.
<svg width="400" height="400">
<defs>
<clipPath id="shape">
<rect x="40" y="50" width="40" height="70" />
<circle cx="50" cy="50" r="50" />
</clipPath>
<filter id="shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="3" dy="3"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<g filter="url(#shadow)">
<rect width="100%" height="100%" fill="red"
clip-path="url(#shape)"/>
</g>
</svg>
Note: if you are wondering why we are applying the drop shadow to a parent <g> here, it is because if we applied it directly to the <rect>, the drop shadow would be subject to the clip also.
What's wrong with just a dropshadow on a group around the shapes?
<svg width="400" height="400">
<defs>
<filter id="shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="3" dy="3"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<g filter="url(#shadow)">
<rect x="40" y="50" width="40" height="70" fill="red"/>
<circle cx="50" cy="50" r="50" fill="red"/>
</g>
</svg>
Note: Be aware that this solution is only giving you the outer part of the contour.
A little variation of #devuxer's answer. A shadow can be created by a mask that can then be applied to a black shape. In https://stackoverflow.com/a/50884092/275195 two masks are created with set theory (A-B)+(B-A). However in the rendering engine a pixel raster is used for masks and antialiasing is applied. This leads to half-transparent pixels in the mask at the boundary of A and B. This can be avoided by applying the shaped to one mask like this (basically outline of A+B minus A+B):
<svg>
<!-- Masks seem to be pre-filled with black -->
<mask id="union">
<!-- Whatever is white in the mask will be visible of the shape the mask is applied to. -->
<rect id="A" x="20" y="20" width="50" height="100" stroke="white" fill="none"></rect>
<!-- Putting a second (, third, ...) shape into the mask will draw the shape over any existing shapes. -->
<circle id="B" cx="80" cy="60" r="30" stroke="white" fill="none"></circle>
<!-- Now we delete the inner part of the shapes by drawing black over them. -->
<rect id="Ainner" x="20" y="20" width="50" height="100" fill="black"></rect>
<circle id="Binner" cx="80" cy="60" r="30" fill="black"></circle>
</mask>
<!-- Applied to a full-screen red rectangle it cuts out the strokes from the mask. -->
<rect id="A+B" x="0" y="0" width="100%" height="100%" fill="red" mask="url(#union)"></rect>
</svg>
For everyone who wants both fill and border, here is a combination of devuxers and pascals posts
<svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
<defs>
<rect id="canvas" width="100%" height="100%" fill="white" />
<rect id="rect" x="50" y="60" width="40" height="70" />
<circle id="circle" cx="50" cy="50" r="40" />
<clipPath id="shape">
<use href="#rect" />
<use href="#circle" />
</clipPath>
<mask id="maskRect">
<use href="#canvas" />
<use href="#rect" />
</mask>
<mask id="maskCircle">
<use href="#canvas" />
<use href="#circle" />
</mask>
</defs>
<rect width="100%" height="100%" fill="red" clip-path="url(#shape)" />
<use href="#rect" stroke="green" stroke-width="2" fill="none" mask="url(#maskCircle)" />
<use href="#circle" stroke="green" stroke-width="2" fill="none" mask="url(#maskRect)" />
</svg>
With the feMorphology filter you can grow and shrink objects (MDN). Combining them, you can create a simply reusable outline: make an increased copy of the original, paint it, and place it under the shrunk original.
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
<defs>
<filter id="outline">
<!-- Make a copy of the original,
grow it by half the stroke width,
call it outer -->
<feMorphology
operator="dilate"
radius="1"
result="outer"
/>
<!-- Make another copy of the original,
shrink it by half the stroke width,
call it inner -->
<feMorphology
in="SourceGraphic"
operator="erode"
radius="1"
result="inner"
/>
<!-- Create a color layer... -->
<feFlood
flood-color="black"
flood-opacity="1"
result="outlineColor"
/>
<!-- ...and crop it by outer -->
<feComposite
operator="in"
in="outlineColor"
in2="outer"
result="coloredOuter"
/>
<!-- Place the shrunk original over it -->
<feComposite
operator="over"
in="inner"
in2="coloredOuter"
/>
</filter>
</defs>
<g filter="url(#outline)">
<rect x="50" y="60" width="40" height="70" fill="red" />
<circle cx="60" cy="60" r="50" fill="red" />
</g>
</svg>
If you want to have it transparent, replace the operator in the last feComposite with xor:
<!-- Knock off the shrunk one -->
<feComposite
operator="xor"
in="inner"
in2="coloredOuter"
/>
The only caveat: fatter strokes - ie larger dilate and erode radii - on curves may look distorted, with 1px + 1px it is still fine.

How can I echo and execute HTML / SVG in rmarkdown without repeating the code?

For teaching purposes I would like to echo and evaluate HTML/SVG in my rmarkdown documents. Is there a way to do this without repeating code? This is my current method with code repeats (syntax highlighting is off so ignore that):
``` html
<h3>This is my SVG.</h3>
```
<h3>This is my SVG.</h3> <!-- repeated -->
``` html
<svg width="200" height="100">
<rect x="0" y="0" width="200" height="100" fill="lightblue"></rect>
<circle cx="75" cy="50" r="20" fill="blue"></circle>
<circle id="c2" cx="125" cy="50" r="20" fill="blue"></circle>
</svg>
```
<svg width="200" height="100"> <!-- repeated -->
<rect x="0" y="0" width="200" height="100" fill="lightblue"></rect>
<circle cx="75" cy="50" r="20" fill="blue"></circle>
<circle id="c2" cx="125" cy="50" r="20" fill="blue"></circle>
</svg>
``` javascript
<script src="https://d3js.org/d3.v5.min.js"></script>
```
<script src="https://d3js.org/d3.v5.min.js"></script> <!-- repeated -->
```{js}
// not repeated: echos and executes
d3.select("circle#c2").attr("fill", "red");
```
The rendered page looks like this:
(Note that I don't have this problem with JavaScript using the {js} engine, though I don't know how to include attributes in the <script> tag without repeating the line.)

Scale overriding SVG width and height attributes

I have an SVG element with a defined width and height, like <svg width="100px" height="100px"></svg>, filled with various elements.
I want to have a kind of "zoom" feature, where a particular region of the SVG is zoomed in on to fill the whole SVG element.
I planned to do this with the scale and translate attributes, i.e. by applying scale(x) to the SVG element and then calculating what I need to translate by in order to have the desired region remain visible.
I expected this would keep the SVG at 100x100px and simply hide any element outside this region. However, this doesn't happen; the whole SVG element just gets bigger instead, even though the dimensions are explicitly defined as attributes.
Clearly I'm misunderstanding the way that scaling and SVG dimensions work, does anyone know how I can achieve what I'm trying to do here?
You can warp the svg with a div element and use overflow: hidden.
<div style="width: 300px; height: 300px; overflow: hidden">
<svg width="100" height="100" style="transform: scale(4);">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
</div>
Do you mean something like this?
function setViewBox(vbx){
svg.setAttribute("viewBox",vbx)
}
<svg viewBox="0 0 100 100" width="200px" height="200px" id="svg">
<rect x="0" y="0" width="100" height="100" stroke="black" fill="white" onclick="setViewBox('0 0 100 100')"/>
<circle cx="25" cy="25" r="25" fill="red" onclick="setViewBox('0 0 50 50')"/>
<rect x="60" y="10" width="30" height="30" fill="green" onclick="setViewBox('50 0 50 50')"/>
<rect x="10" y="60" width="30" height="30" fill="blue" transform="rotate(45,25,75)" onclick="setViewBox('0 50 50 50')"/>
<path d="M50 100L75 50L100 100z" fill="yellow" onclick="setViewBox('50 50 50 50')"/>
</svg>
or more like that?
var last=null
function setTransform(evt,trs){
reset()
svg.appendChild(evt.target)
evt.target.setAttribute("transform","scale(2 2) translate("+trs+")")
last=evt.target
}
function reset(){
if(last) last.removeAttribute("transform")
}
<svg viewBox="0 0 100 100" width="200px" height="200px" id="svg">
<rect x="0" y="0" width="100" height="100" stroke="black" fill="white" onclick="reset()"/>
<circle cx="25" cy="25" r="25" fill="red" onclick="setTransform(event,'0 0')"/>
<rect x="60" y="10" width="30" height="30" fill="green" onclick="setTransform(event,'-50 0')"/>
<rect x="10" y="60" width="30" height="30" fill="blue" transform="rotate(45,25,75)" onclick="setTransform(event,'0 -50')"/>
<path d="M50 100L75 50L100 100z" fill="yellow" onclick="setTransform(event,'-50 -50')"/>
</svg>

Captain America with svg

How I can do this star right with svg? I must use svg and I try with points but not work. I mustn't use element path. Thanks.
<!DOCTYPE html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<polygon points="50,25 30,80 75,40 25,40 70,70" style="fill:white;"/>
</svg>
</body>
</html>
If you want to get the most accurate points for the pentastar, you can get them easily from the underlying pentagon.
A simple js function to obtain these points is something like this (REPL, which by the way you can use for polygons with any n edges):
var n = 5;
var points = [];
for (var i=0; i < n; i++) {
var x = 50;
var y = -50;
var r = 25;
points.push([x + r * Math.sin(2 * Math.PI * i / n),
y + r * Math.cos(2 * Math.PI * i / n)]);
}
Result is the pentagon points clock-wise, starting at the top (use all values as positive ones):
[ [ 50, -25 ],
[ 73.77641290737884, -42.27457514062631 ],
[ 64.69463130731182, -70.22542485937369 ],
[ 35.30536869268818, -70.22542485937369 ],
[ 26.22358709262116, -42.27457514062632 ] ]
Your order would be points[x], where x = 0, 3, 1, 4, 2.
And using them for your example rounded to the nearest pixel:
<!DOCTYPE html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<polygon points="50,25 35,70 73,42 26,42 65,70" style="fill:white;"/>
</svg>
</body>
</html>
This seems a bit closer.
<!DOCTYPE html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<polygon points="50,25 35,70 73,42 26,42 65,70" style="fill:white;"/>
</svg>
</body>
</html>
updated to use #frhd's number, make community wiki,
please see his answer for calculations
Here with path
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<!-- <polygon points="50,25 30,80 75,40 25,40 70,70" style="fill:white;"/> -->
<path fill="#fff" d="m50,25 5,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z" />
</svg>

Categories

Resources