I want to create an SVG donut shape (circle with another empty circle inside). I want to be able to access & resize both circles, eg via their id attributes. This will allow for animation.
I have considered three approaches but none are that great:
complex path: does not allow for access of the inner circle via #id
outline stroke: possible but complicated for my purpose (would have to reposition as I increase stroke)
clippath/mask: Doesn't work like a compound path, only an outer box
Is there a way of doing this?
Probably the easiest way would be with masks.
If you are working with a set of discrete donut sizes, you could use CSS and a mask for each size:
<svg width="500" height="500">
<defs>
<mask id="bigmask">
<rect width="100%" height="100%" fill="white"/>
<circle cx="250" cy="250" r="50"/>
</mask>
<mask id="smallmask">
<circle cx="250" cy="250" r="150" fill="white"/>
<circle cx="250" cy="250" r="100"/>
</mask>
</defs>
<circle id="donut" cx="250" cy="250" r="200" mask="url(#bigmask)"/>
</svg>
CSS:
#donut:hover
{
mask: url(#smallmask);
}
Demo here
Unfortunately you can't modify the size of circles with CSS. "r" is not (yet) a property that can be manipulated with CSS. So you will need to either use SMIL (SVG) animation, or manipulate your mask circles with javascript:
<svg width="500" height="500">
<defs>
<mask id="donutmask">
<circle id="outer" cx="250" cy="250" r="200" fill="white"/>
<circle id="inner" cx="250" cy="250" r="50"/>
</mask>
</defs>
<circle id="donut" cx="250" cy="250" r="200" mask="url(#donutmask)"/>
</svg>
JS
$("#donut").mouseenter(function(evt) {
$("#outer").attr("r", 100 + Math.random() * 100);
$("#inner").attr("r", 100 - Math.random() * 50);
});
Demo here
Although BigBadaboom's answer is the best way IMO, if you want to use a compound path, it's possible to animate by rewriting the path's d attribute each frame like this:
// get svg path coordinates for a ring
ring:function(x, y, ir, or) {
var path =
'M'+x+' '+(y+or)+'A'+or+' '+or+' 0 1 1 '+(x+0.001)+' '+(y+or) // outer
+ 'M'+x+' '+(y+ir)+'A'+ir+' '+ir+' 0 1 0 '+(x-0.001)+' '+(y+ir) // inner
;
return path;
}
Related
I am working with a SVG element as following.
The pattern of this SVG is that the SVG element circle in this case would always have some parent element <g><g></g></g> in this case and the parent element may or may not have some sort of transform attributes and the element itself may or may not have some sort of transform attributes.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<rect x="0" y="0" width="1280" height="720" fill="ghostwhite"></rect>
<g transform="translate(10,10)">
<g transform="translate(100 100)">
<circle id="circ" r="20" cx="25" cy="25" fill="green"
transform="translate (100,400) scale(2)" />
</g>
</g>
</svg>
</body>
</html>
I need to know the absolute x,y(transformMatrix.e, transformMatrix.f) coordinate of the circle element that equates to the cumulative transform of each of the attributes coming from any of its parent elements plus its own transform element.
So, in this case, I want javascript to return x=10+100+25*2+100=260 and y=10+100+25*2+400=560.
I tried using getCTM() as my understanding was it takes into account the effect of all cumulative transform for a particular element and tried it like this. I also took help from this post to convert DOMMatrix to svgMatrix.
However, it does not quite do the job. The javascript returns convertedMatrix.e=210 and convertedMatrix.f=510. I don't know where this is going wrong in using getCTM().
I can't figure out how can I achieve the desired using this approach and what adjustments do I need to make here?
const toElement = document.querySelector('svg');
const fromElement = document.querySelector('circle')
const toCTM = toElement.getCTM();
const fromCTM = fromElement.getCTM();
const convertedMatrix = toCTM.inverse().multiply(fromCTM);
console.log(convertedMatrix);
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<rect x="0" y="0" width="1280" height="720" fill="ghostwhite"></rect>
<g transform="translate(10,10)">
<g transform="translate(100 100)">
<circle id="circ" r="20" cx="25" cy="25" fill="green"
transform="translate (100,400) scale(2)" />
</g>
</g>
</svg>
</body>
<script type="text/javascript">
</script>
</html>
The transform matrix is not the same as the result of its application to a point. The transform list
translate(10,10) translate(100 100) translate(100,400) scale(2)
consolidates to (note the a and d values)
matrix(2, 0, 0, 2, 210, 510)
and make this computation:
cx' = cx * 2 + 210
cy' = cy * 2 + 510
For a point (25, 25), this computes to
25 * 2 + 210 = 260
25 * 2 + 510 = 510
just like you expected.
In the form of a script, you need to construct a point that the matrix can be applied to. Then read its coordinates. Don't bother about the old interfaces SVGMatrix and SVGPoint, that is a problem of ten years past.
What you need to bother about is the result of implicit transformations by the viewBox and preserveAspectRatio attributes. They are included in the matrix produced by .getCTM().
If you want to exclude the implicit transformation, you need to apply it inversely, which is what the code you took from the linked answer does.
Note: I am a bit stumped to find that Firefox returns null on svg.getCTM(). That is a bug. Chrome handles that case correctly.
const svg = document.querySelector('svg');
const matrixViewBox = svg.getCTM();
const circle = document.querySelector('circle');
const matrixCircle = circle.getCTM();
const x = circle.cx.baseVal.value; // takes care of unit conversion
const y = circle.cy.baseVal.value;
console.log(new DOMPoint(x, y).matrixTransform(matrixCircle));
matrixFromCircleToViewBox = matrixViewBox.inverse().multiply(matrixCircle);
console.log(new DOMPoint(x, y).matrixTransform(matrixFromCircleToViewBox));
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 1280 720">
<rect x="0" y="0" width="1280" height="720" fill="ghostwhite"></rect>
<g transform="translate(10,10)">
<g transform="translate(100 100)">
<circle id="circ" r="20" cx="25" cy="25" fill="green"
transform="translate (100,400) scale(2)" />
</g>
</g>
</svg>
I know how to use SVG masks to completely "cut out" the mask in another shape, if the mask is monochrome.
How can I use a multicolored SVG definition X as the mask so that the outer shape of X defines the "hole" to be cut out?
Here are three images that illustrate what I am trying to achieve:
svg #1 to be used as mask
svg #2 on which the outer shape of #1 should be used as a cut-out
result
Creating a white-filled version of the shape as #enxaneta proposed is not applicable to my problem, as I have many "complicated" external SVG definitions, and I don't want to change every single one of them.
Is there another, simpler way to achieve what I want?
You need to define your paths with no fill. Then you use your paths for the mask and fill them with white. To draw the image you fill those paths with the colors of your choice.
svg{border:1px solid; width:49vw}
svg:nth-child(2){background:red;}
mask use{fill:white;}
<svg viewBox="0 0 100 50">
<defs>
<polygon id="a" points="30,5 70,20 75,40 20,20" />
<circle id="b" cx="50" cy="25" r="15" />
<circle id="c" cx="60" cy="35" r="10" />
<mask id="m">
<use xlink:href="#a"/>
<use xlink:href="#b"/>
<use xlink:href="#c"/>
</mask>
</defs>
<g id="complexShape">
<use xlink:href="#a" fill="lightblue" />
<use xlink:href="#b" fill="gold"/>
<use xlink:href="#c" fill="red"/>
</g>
</svg>
<svg viewBox="0 0 100 50">
<rect width="100" height="50" style="mask: url(#m)" />
</svg>
The colour of a mask determines the final opacity of the masked object at that point. The R, G, B, and A components of the mask colour are combined in a formula to determine a luminance value that is used to set the final transparency that the mask will be a that point. So, for example, if the mask is red, the final masked result will be semi transparent.
There is no way to make a coloured object be a solid (not translucent) mask. Only full white will do that.
Update
Assuming you have an external SVG image that looks like the following:
<svg viewBox="0 0 100 50">
<polygon id="a" points="30,5 70,20 75,40 20,20" fill="lightblue"/>
<circle id="b" cx="50" cy="25" r="15" fill="gold"/>
<circle id="c" cx="60" cy="35" r="10" fill="red" stroke="blue" stroke-width="4"/>
</svg>
You can turn this into a "mask" version by adding three lines to the start of your SVG.
<svg viewBox="0 0 100 50">
<filter id="blacken"><feFlood flood-color="black"/><feComposite operator="in" in2="SourceGraphic"/></filter>
<style>svg :not(#maskbg) { filter: url(#blacken); }</style>
<rect id="maskbg" x="-100%" y="-100%" width="300%" height="300%" fill="white"/>
<polygon id="a" points="30,5 70,20 75,40 20,20" fill="lightblue"/>
<circle id="b" cx="50" cy="25" r="15" fill="gold"/>
<circle id="c" cx="60" cy="35" r="10" fill="red" stroke="blue" stroke-width="4"/>
</svg>
This is something that could easily be scripted. This method should work for almost all SVGs.
Once you have all the mask variants built, you can apply them using mask-image.
https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image
I'm using SVG.js to draw shapes. One of the shapes needs to be a circle with a radius line so so get that behavior, I'm using an ellipse and a line inside of a "g" element.
The trouble is, whenever I reference that g element and try to get the bbox, I always get a value like this:
{
x: 0,
y: 0,
width: widthOfBox,
height: heightOfBox,
cx: widthOfBox/2,
cy: heightOfBox/2
}
x and y should NOT be 0.
Below is the element in question that is clearly not at (0,0) but seems to be reporting that it is.
<g id="MLG1008" stroke="#000000" stroke-width="2" fill="#000000" transform="translate(100 200)" width="993.3559281546569" height="993.3559281546569" x="12.963409423828125" y="-290.0365905761719">
<ellipse id="MLEllipse1009" rx="496.67796407732845" ry="496.67796407732845" cx="496.67796407732845" cy="496.67796407732845" stroke="#000000" stroke-width="2"></ellipse>
<line id="MLLine1010" x1="496.67796407732845" y1="496.67796407732845" x2="801.6779640773284" y2="888.6779640773284" stroke="#000000" stroke-width="2"></line>
</g>
Is there a way to define the values that bbox should have? Or a way to set the values that bbox() looks at?
From W3 specs on getBBox
SVGRect getBBox()
Returns the tight bounding box in current user space (i.e., after application of the ‘transform’ attribute, if any) on the geometry of all contained graphics elements, exclusive of stroking, clipping, masking and filter effects). [...]
Emphasis mine
As you can see in this definition, the SVGRect returned by getBBox is calculated against the contained graphics elements, this means that the translation you applied on your <g> element will not be taken into account in this SVGRect, since the transformation is applied on the element itself, and not on its graphic content.
You could get these x and y values by wrapping your transformed <g> element inside an other <g> from which you would call its getBBox() method,
console.log(document.getElementById("wrapper").getBBox());
<svg viewBox="0 0 1500 1500" width="300" height="300">
<g id="wrapper">
<g id="MLG1008" stroke="#000000" stroke-width="2" fill="#000000" transform="translate(100 200)">
<ellipse id="MLEllipse1009" rx="496.67796407732845" ry="496.67796407732845" cx="496.67796407732845" cy="496.67796407732845" stroke="#000000" stroke-width="2"></ellipse>
<line id="MLLine1010" x1="496.67796407732845" y1="496.67796407732845" x2="801.6779640773284" y2="888.6779640773284" stroke="#000000" stroke-width="2"></line>
</g>
</g>
</svg>
or even on the parent <svg> if all its graphic elements are inside this <g>:
console.log(document.querySelector("svg").getBBox());
<svg viewBox="0 0 1500 1500" width="300" height="300">
<g id="MLG1008" stroke="#000000" stroke-width="2" fill="#000000" transform="translate(100 200)">
<ellipse id="MLEllipse1009" rx="496.67796407732845" ry="496.67796407732845" cx="496.67796407732845" cy="496.67796407732845" stroke="#000000" stroke-width="2"></ellipse>
<line id="MLLine1010" x1="496.67796407732845" y1="496.67796407732845" x2="801.6779640773284" y2="888.6779640773284" stroke="#000000" stroke-width="2"></line>
</g>
</svg>
Also, even if unrelated with your issue, note that <g> element doesn't have width, height, x nor y attributes.
Because your <g> positioned relative to <svg>. You need to use transform="translate(x, y)" to change the element position.
I created a SVG file contains 5 polygons, then I need to embed Javascript so 4 of the polygons' color changes to Red when mouseover, and when mouseout, the color changes to Green. I tried to write the code but it didn't work, what could be the problem? Thanks for help and tips!
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="26cm" height="24cm" viewBox="0 0 2600 2400" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink= "http://www.w3.org/1999/xlink">
<script type="text/javascript">
<![CDATA[
document.getElementById("test").onmouseover = function(){changeColor()};
function changeColor() {
document.getElementById("test").style.color = "red";
}
document.getElementById("test").onmouseout = function(){changeColor()};
function changeColor() {
document.getElementById("test").style.color = "green";
}
]]>
</script>
<circle cx="1600" cy="700" r="600" fill="yellow" stroke="black" stroke-width="3"/>
<ellipse id="test" cx="1300" cy="500" rx="74" ry="120" fill="blue" stroke="black" stroke-width="3" onmouseover="javascript:red();" onmouseout="javascript:green();"/>
<ellipse id="test" cx="1850" cy="500" rx="74" ry="120" fill="blue" stroke="black" stroke-width="3" onmouseover="javascript:red();" onmouseout="javascript:green();"/>
<rect id="test" x="1510" y="650" width="160" height="160" fill="blue" stroke="black" stroke-width="3" onmouseover="javascript:red();" onmouseout="javascript:green();"/>
<polygon id="test" points="1320,800 1370,1080 1820,1080 1870,800 1820,1000 1370,1000" name="mouth" fill="blue" stroke="black" stroke-width="3" onmouseover="javascript:red();" onmouseout="javascript:green();"/>
</svg>
For what you are doing I would recommend using pure CSS.
Here is some working code.
svg:hover .recolor {
fill: red;
}
As you see, you can just use the :hover event in CSS to recolor the necessary elements. And set them to your default color (green), which will take effect when the user is not hovered.
You have various errors
you've two functions called changeColor, functions must have unique names
SVG does not use color to colour elements, instead it uses fill (and stroke).
id values must be unique, you probably want to replace id by class and then use getElementsByClassName instead of getElementById. If you do that you'll need to cope with more than one element though. I've not completed that part, you should try it yourself so you understand what's going on.
I've removed all but one id from my version so you can see it working on the left eye.
document.getElementById("test").onmouseover = function(){changeColor()};
function changeColor() {
document.getElementById("test").style.fill = "red";
}
document.getElementById("test").onmouseout = function(){changeColor2()};
function changeColor2() {
document.getElementById("test").style.fill = "green";
}
<svg width="26cm" height="24cm" viewBox="0 0 2600 2400" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink= "http://www.w3.org/1999/xlink">
<circle cx="1600" cy="700" r="600" fill="yellow" stroke="black" stroke-width="3"/>
<ellipse id="test" cx="1300" cy="500" rx="74" ry="120" fill="blue" stroke="black" stroke-width="3"/>
<ellipse cx="1850" cy="500" rx="74" ry="120" fill="blue" stroke="black" stroke-width="3" />
<rect x="1510" y="650" width="160" height="160" fill="blue" stroke="black" stroke-width="3" />
<polygon points="1320,800 1370,1080 1820,1080 1870,800 1820,1000 1370,1000" name="mouth" fill="blue" stroke="black" stroke-width="3"/>
</svg>
I am new on stackoverflow.
I face a problem in svg code. I want to draw a container with a background image but when I set an image it breaks into 4 parts and gives a white space in mid of container.
This is my SVG code:
<svg id="do-as-cards" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0,0,320,340" preserveAspectRatio="xMidYMin">
<defs>
<pattern id="imgDo" preserveAspectRatio="true" patternUnits="userSpaceOnUse" y="0" x="0" width="240" height="120" >
<image xlink:href="http://s22.postimg.org/ekd89tb8x/image.png" x="0" y="0" width="407px" height="220px" />
</pattern>
<pattern id="imgAs" preserveAspectRatio="true" patternUnits="userSpaceOnUse" y="0" x="0" width="240" height="120" >
<image xlink:href="http://s22.postimg.org/72zfguwc1/image.png" x="0" y="0" width="407px" height="220px" />
</pattern>
</defs>
<g transform="translate(160,86)">
<g id="doCard" class="animatedCard" transform="matrix(1 0 0 1 0 0)" onclick="spin()">
<path class="cardOutline" d="m -150,-60 c 0,-10 10,-20 20,-20 l260,0 c 10,0 20,10 20,20 l 0,120 c 0,10 -10,20 -20,20 l -260,0 c -10,0 -20,-10 -20,-20 l 0,-120 z" />
<foreignObject id="do" class="cardFace" x="-120" y="-60" width="240" height="120"></foreignObject>
</g>
</g>
<g transform="translate(160,253)">
<g id="asCard" class="animatedCard" transform="matrix(1 0 0 1 0 0)" onclick="spin()">
<path class="cardOutline" id="as_path" d="m -150,-60 c 0,-10 10,-20 20,-20 l260,0 c 10,0 20,10 20,20 l 0,120 c 0,10 -10,20 -20,20 l -260,0 c -10,0 -20,-10 -20,-20 l 0,-120 z"/>
<foreignObject id="as" class="cardFace" x="-120" y="-60" width="240" height="120"></foreignObject>
</g>
</g>
</svg>
You can see this code in running stage by using this url
I have already tried the following:
How to set a SVG rect element's background image?
Fill SVG path element with a background-image
Using a <pattern> may not be the best way to do what you want. It can be done though.
If you are using a pattern, stick to the default patternUnits (objectBoundingBox), and set width and height to 1. Then set the width and height of your image to the max width or height of the region you are trying to fill. In the case of your example shapes, that appears to be 300. Then adjust the x and y of the <image> so it is centred in your shape.
<pattern id="imgDo" y="0" x="0" width="1" height="1" >
<image xlink:href="http://s22.postimg.org/ekd89tb8x/image.png" x="0" y="-75" width="300" height="300" />
</pattern>
Demo: http://jsfiddle.net/TRLa7/1/
Personally, I would use a <clipPath> for this situation. Use your path shape as the clipPath for the image. You will need to add an additional copy of the <path> to apply your stroke effects etc. You can define your card(s) in the <defs> section and then use a <use> to instantiate each card.
Demo: http://jsfiddle.net/TRLa7/2/