How can I get the new size and coordinate properties of an image element so the parent group container BBOX will supposed to change to new target values.
Here's my SVG:
<svg width="500"height ="500" xmlns="http://www.w3.org/2000/svg">
<g id="my_grp">
<image width="350" height="150" transform="translate(100 100) rotate(35 175 75)" href="https://via.placeholder.com/350x150"></image>
</g>
</svg>
From that example I got the initial bounding box of "my_grp" using document.getElementById('my_grp').getBBox() which is:
[object SVGRect] {
height: 323.62457275390625,
width: 372.73968505859375,
x: 88.63015747070312,
y: 13.18772029876709
}
I want to resize my_grp box by 30% and move it to (30, 40) coordinates. How can I change the image properties to achieve it?
You may check https://jsfiddle.net/ybjL0zmt/.
Thanks!
You can get the help of the SVGMatrix interface to compute the neccessary transformation. Starting with the identity matrix, you then move the coordinate system around to draw the group content into:
const group = document.querySelector('#my_grp');
const bbox = group.getBBox();
// initialize an identity matrix
const matrix = document.querySelector('svg').createSVGMatrix();
const {a, b, c, d, e, f} = matrix
.translate(30, 40)
.scale(0.7)
.translate(-bbox.x, -bbox.y);
const transform = 'matrix(' + [a, b, c, d, e, f].join(',') + ')';
group.setAttribute('transform', transform);
<svg width="500"height ="500" xmlns="http://www.w3.org/2000/svg">
<path d="M30 150V40H200" style="fill:none;stroke:black;stroke-dasharray:5 5" />
<g id="my_grp">
<image width="350" height="150" transform="translate(100 100) rotate(35 175 75)" href="https://via.placeholder.com/350x150"></image>
</g>
</svg>
Related
I am trying to mimic the behavior of "stroke alignment" in an SVG object. While there is a working draft for stroke-alignment in the spec, this has not actually been implemented (despite being drafted in 2015).
Example of non-working stroke-alignment:
The blue square should have stroke inside, the red outside, but they're both the same
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="100">
<g id="myGroup" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 50,0 50,50 0,50" style="fill:blue;stroke:black;stroke-width:4;stroke-alignment:inner"></polygon>
<polygon id="myPoly1" transform="translate(75 0)" points="0,0 50,0 50,50 0,50" style="fill:red;stroke:black;stroke-width:4;stroke-alignment:outer"></polygon>
</g>
</svg>
My approach to mimicking this behavior is to create a duplicate SVG object using the <use> element, setting a stroke property on the copy and scaling it slightly up or down depending on whether it's an inner or outer stroke alignment (default is center)
For example:
The scale and transform for the <use> element gets worse the farther from origin
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="400">
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly1" transform="translate(110 110)" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly2" transform="translate(220 220)" points="0,0 100,0 100,100 0,100"></polygon>
<use id="myPolyCopy0" vector-effect="non-scaling-stroke" href="#myPoly0" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
<use id="myPolyCopy1" vector-effect="non-scaling-stroke" href="#myPoly1" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
<use id="myPolyCopy2" vector-effect="non-scaling-stroke" href="#myPoly2" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
</g>
</svg>
As you can see from the above example, the relative positioning of the <use> element goes awry, and gets worse the farther away from the origin it gets.
Naively, I assume that the transform property of the <use> element acts upon the SVG shape referenced in its href, but that seems not to be the case.
In my example, I'm scaling a 100x100 square by a factor of 1.04, which should result in a 104x104 square (4px width of the stroke). I'm then translating back by -2px to position the stroke on the outside of the source shape, thereby mimicking an outer stroke alignment.
This works as expected if the source shape is at origin (relative to the container group), but goes bonkers if the source shape is translated away from the origin.
My brain says this should work, but my browser says no bueno.
Anyone got any clues?
So, it turns out that the transform applied to the <use> element will be applied to the existing transform of the source element.
This means that the scale transform applied to the <use> element will also scale its translate matrix.
For example:
If the source element has translate(100 100), applying a scale(1.1 1.1) on the <use> copy will cause it to have a translate with the value (110,110)
This means to move the copy with the stroke value back to the correct location, you need to move the copy far enough back to overcome this "scaled translation".
This may well be expected behavior, but it was not intuitive to me (I may need to RTFM). Overall this approach "works", but feels complicated and hacky.
Working sample:
const strokeWidth = 8;
const poly1Translate = {
x: 150,
y: 20
};
const poly2Translate = {
x: 300,
y: 40
};
const poly1 = document.getElementById("myPoly1");
const poly2 = document.getElementById("myPoly2");
const polyCopy0 = document.getElementById("myPolyCopy0");
const polyCopy1 = document.getElementById("myPolyCopy1");
const polyCopy2 = document.getElementById("myPolyCopy2");
const styleString = `fill:none;stroke:red;stroke-opacity:0.5;stroke-width:${strokeWidth};`;
poly1.setAttribute(
"transform",
`translate(${poly1Translate.x} ${poly1Translate.y})`
);
poly2.setAttribute(
"transform",
`translate(${poly2Translate.x} ${poly2Translate.y})`
);
polyCopy0.setAttribute("style", styleString);
polyCopy1.setAttribute("style", styleString);
polyCopy2.setAttribute("style", styleString);
// Use the boundingbox to get the dimensions
const poly1BBox = poly1.getBBox();
const poly2BBox = poly2.getBBox();
let halfStrokeWidth = strokeWidth / 2;
// stroke-alignment:outside
// Scale the copy to be strokeWidth pixels larger
let scaleOutsideX = 1+strokeWidth/poly1BBox.width;
let scaleOutsideY = 1+strokeWidth/poly1BBox.height;
// Move the copy to the same scale property based on the current translation
// This will position the stroke at the correct origin point, and we need to
// deduct a further half of the stroke width to position it fully on the outside
let translateOutsideX = -((poly1Translate.x * scaleOutsideX - poly1Translate.x) + halfStrokeWidth);
let translateOutsideY = -((poly1Translate.y * scaleOutsideY - poly1Translate.y) + halfStrokeWidth);
polyCopy1.setAttribute('transform', `translate(${translateOutsideX} ${translateOutsideY}) scale(${scaleOutsideX} ${scaleOutsideY})`);
// stroke-alignment:inside
let scaleInsideX = 1-strokeWidth/poly2BBox.width;
let scaleInsideY = 1-strokeWidth/poly2BBox.height;
let translateInsideX = poly2Translate.x * scaleOutsideX - poly2Translate.x + halfStrokeWidth;
let translateInsideY = poly2Translate.y * scaleOutsideY - poly2Translate.y + halfStrokeWidth;
polyCopy2.setAttribute('transform', `translate(${translateInsideX} ${translateInsideY}) scale(${scaleInsideX} ${scaleInsideY})`);
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="160">
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly1" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly2" points="0,0 100,0 100,100 0,100"></polygon>
<use id="myPolyCopy0" vector-effect="non-scaling-stroke" href="#myPoly0"></use>
<use id="myPolyCopy1" vector-effect="non-scaling-stroke" href="#myPoly1"></use>
<use id="myPolyCopy2" vector-effect="non-scaling-stroke" href="#myPoly2"></use>
</g>
</svg>
UPDATE
After noticing the following comment in the Figma website:
Inside and outside stroke are actually implemented by doubling the stroke weight and masking the stroke by the fill. This means inside-aligned stroke will never draw strokes outside the fill and outside-aligned stroke will never draw strokes inside the fill.
I implemented a similar method using a combination of <clipPath> and <mask>
.stroke {
fill:none;
stroke:red;
stroke-opacity:0.5;
}
.stroke-center {
stroke-width:8;
}
.stroke-inout {
stroke-width:16;
}
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="160">
<defs>
<rect id="stroke-mask" width="500" height="160" fill="white"/>
</defs>
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20,20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100" transform="translate(0,20)"></polygon>
<polygon id="myPoly1" points="0,0 100,0 100,100 0,100" transform="translate(150,20)"></polygon>
<polygon id="myPoly2" points="0,0 100,0 100,100 0,100" transform="translate(300,20)"></polygon>
<mask id="mask">
<use href="#stroke-mask"/>
<use href="#myPoly1" fill="black"/>
</mask>
<clipPath id="clip">
<use href="#myPoly2"/>
</clipPath>
<use id="myPolyCopy0" class="stroke stroke-center" href="#myPoly0"></use>
<use id="myPolyCopy1" class="stroke stroke-inout" href="#myPoly1" mask="url(#mask)"></use>
<use id="myPolyCopy2" class="stroke stroke-inout" href="#myPoly2" clip-path="url(#clip)"></use>
</g>
</svg>
The idea here is, to achieve the equivalent of:
stroke-align:center: is the default behavior, do nothing
stroke-align:inner: Create a clipPath using the source object to which you want to apply the inner stroke, then with the <use> element, create a copy of this with a stroke twice the width you actually want, and set the clip-path of the copy to be the clipPath created from the source object. This will effectively clip everything outside the clipPath, thereby clipping the "outside half" of the double-width stroke
stroke-align:outer: There isn't an equivalent of clipPath which will clip everything inside the path (sadly), so the way to achieve this is to use a <mask>, but the same principle applies as for inner. Create a <mask> based on the source object, create a copy with a double-width stroke, then use the mask to clip everything inside the mask, thereby clipping the "inside half" of the double-width stroke
This question already has answers here:
SVG rounded corner
(15 answers)
Closed last year.
I could use a little help with setting border-radius on each side of rectangle . Here’s the current code rectangle svg path tag
`M0,53H415.4285583496094V57H0V53Z`
I want to give the each corner of the rectangle a rounded shape. How is it possible?
I am not able to apply like border radius properly. I already try using SVG path generator, but still not really understand how to use that to make such a border radius on that
You can't apply a border radius on a <path> element.
But it can be set for <rect> primitives. See Mdn Docs: rect.
You would define the border radius via rx and ry properties:
<rect x="0" y="53" width="415.43" height="4" rx="2" ry="2" />
If you need to convert to convert a rect to a path element, you could use the pathdata polyfill by Jarek Foksa
// convert rect to path
let rect = document.querySelector('rect');
let rectPath = rect.getPathData({normalize:true})
{normalize:true} will return an array of path commands using only a reduced set of command types (M, L, C, Z – with absolute coordinates).
This option can also be used to convert primitives like rect, circle, polygon, line etc. to path d data. So you will have to create a new path element an set the retrieved pathdata to the new path's d attribute.
let path = document.querySelector('path')
//let bb = path.getBBox()
//console.log(bb)
let rect = document.querySelector('rect');
// convert rect to path
let rectPath = rect.getPathData({
normalize: true
})
let newSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
newSvg.setAttribute('viewBox', '0 0 415.43 100');
let rectPathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
rectPathEl.setPathData(rectPath)
document.body.appendChild(newSvg);
newSvg.appendChild(rectPathEl);
console.log(rectPathEl.getAttribute('d'))
svg {
display: block;
width: 30em
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.3/path-data-polyfill.js"></script>
<p>Path </p>
<svg viewBox="0 0 415.43 100">
<path d="M0,53 H415.4285583496094 V57 H0 V53 Z" />
</svg>
<p>Rect </p>
<svg viewBox="0 0 415.43 100">
<rect x="0" y="53" width="415.43" height="4" rx="2" ry="2" />
</svg>
<p>Rect converted to path</p>
A <path> element cannot really have radii at its corners. You can of course generate any kind of rounded path by defining suitable curve segments (C and S in the d attribute). Alternatively you can replace the <path> by a <rect> element, as I have done in the following snippet. I changed the height to 10 times the amount in order to make the rounding with a radius of 4 possible
<svg viewbox="0 0 500 100" fill="none" stroke="black">
<rect x="0" y="53" width="415.4286" height="40" rx="4" ry="4">
</svg>
I want to have a curved text along a path (half circle) in SVG. I have followed this tutorial, which is great: https://css-tricks.com/snippets/svg/curved-text-along-path/
The problem is that the path presented there works only for this specific text - Dangerous Curves Ahead. If you leave only Dangerous word that's what happens: https://codepen.io/anon/pen/pqqVGa - it no longer works (the text is no more evenly spreaded across the path).
I want to have it work regardless of text length. How to achieve that?
Using the attributes lengthAdjust and textLength you can adjust the length of the text and the height of the letters, thereby placing the text of the desired length on a segment of a fixed length
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300">
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="400" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="1%" xlink:href="#path1">
<tspan dy="-10"> very long text very long text very long text </tspan>
</textPath>
</text>
</svg>
Using the attribute startOffset =" 10% " you can adjust the position of the first character of the phrase
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300" >
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="400" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="15%" xlink:href="#path1">
<tspan dy="-10"> very long text very long text very long text </tspan>
</textPath>
</text>
</svg>
and make animation using this attribute (click canvas)
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300">
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="200" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="-100%" xlink:href="#path1">
<tspan dy="-10"> Very long text Very long text Very long text </tspan>
<animate
begin="svg1.click"
dur="15s"
attributeName="startOffset"
values="-100%;1%;1%;100%;1%;1%;-100%"
repeatCount="5"/>
</textPath>
</text>
<text x="200" y="150" font-size="24" fill="orange" >Click me </text>
</svg>
This is assuming that the initial text size (35) is too small.
let curveLength = curve.getTotalLength();
let fs = 35;//the initial font size
test.setAttributeNS(null, "style", `font-size:${fs}px`)
while(test.getComputedTextLength() < curveLength){
fs++
test.setAttributeNS(null, "style", `font-size:${fs}px`)
}
body {
background-color: #333;
}
text {
fill: #FF9800;
}
<svg viewBox="0 0 500 500">
<path id="curve" d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97" />
<text id="test">
<textPath xlink:href="#curve">
Dangerous
</textPath>
</text>
</svg>
UPDATE
The OP is commenting:
Thanks for the response. Instead of adjusting the font size, I would prefer to create a new path that is longer / smaller and matches the text width. Not sure how to do this tho. – feerlay
Please read the comments in the code. In base of the length of the text I'm calculating the new path, but I'm assuming a lot of things: I'm assuming the new path starts in the same point as the old one.
let textLength = test.getComputedTextLength();
// the center of the black circle
let c = {x:250,y:266}
// radius of the black circle
let r = 211;
// the black arc starts at point p1
let p1 = {x:73.2,y:150}
// the black arc ends at point p2
let p2 = {x:426.8,y:150}
// distance between p1 and p2
let d = dist(p1, p2);
// the angle of the are begining at p1 and ending at p2
let angle = Math.asin(.5*d/r);
// the radius of the new circle
let newR = textLength / angle;
// the distance between p1 and the new p2
let newD = 2 * Math.sin(angle/2) * newR;
// the new attribute c for the path #curve
let D = `M${p1.x},${p1.y} A`
D += `${newR}, ${newR} 0 0 1 ${p1.x + newD},${p1.y} `
document.querySelector("#curve").setAttributeNS(null,"d",D);
// a function to calculate the distance between two points
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
body {
background-color: #333;
}
text {
fill: #FF9800;
};
<svg viewBox="0 0 500 500">
<path id="black_circle" d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97" />
<path id ="curve" d="M73.2,150 A 211,211 0 0 1 426.8,150" fill="#777" />
<text id="test">
<textPath xlink:href="#curve">
Dangerous curves
</textPath>
</text>
</svg>
Question: How can I translate the center of a SVG group element to the center of the root SVG. I tried to use transform="translate(x,y)" on the <g> element, but this transformation will only translate with respect to the top left corner of the group element.
Example case and goal: In the following SVG, the two rectangles <rect> are grouped together with <g>. Assume we don't know the position, size, and which types are elements are inside the group. We only know the width/height of the parent SVG. Goal is to translate the center of the group (bounding box of the two rectangles) to the center of the root SVG. The issue is that we don't know the height/width of the "bounding box" which is the group itself, thus when using transform="translate(x,y)" alone won't get us to the center of the root SVG.
<svg width="500px" height="300px" preserveAspectRatio="none" viewBox="0,0,5.0,3.0">
<g transform="translate(0,0)">
<rect x="1" y="0.25" width="0.5" height="0.5" fill="green" />
<rect x="1.25" y="1" width="0.5" height="0.5" fill="red" />
</g>
</svg>
Requirements:
The solution can only use pure SVG. CSS or external libraries can NOT be used.
Using Python to do basic calculation is okay. However, remember we don't what elements are inside the <g>.
The coordinate system for the root SVG (viewBox, width, height) must not be change because in example use case, these coordinate system are used for conversion of real world spatial units (ex: millimeters) to pixels for the end application.
You need some way to do calculations. I'm using Javascript:
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.querySelector("svg");
// the viewBox of the svg element splited
let svgVB = svg.getAttribute("viewBox").split(/[ ,]+/);
let o = test.getBBox();
let oCenter = {};//the center of the g#test element
oCenter.x = o.x + o.width/2;
oCenter.y = o.y + o.height/2;
// the valuefor the transform attribute
let tr = `
translate(${-oCenter.x},${-oCenter.y})
translate(${svgVB[2]/2},${svgVB[3]/2})`;
test.setAttributeNS(null, "transform",tr);
// for debugging I'm drawing the bounding box
bbox.setAttributeNS(null, "transform",tr);
function drawRect(o, parent) {
let rect = document.createElementNS(SVG_NS, 'rect');
for (let name in o) {
rect.setAttributeNS(null, name, o[name]);
}
parent.appendChild(rect);
return rect;
}
drawRect(o, bbox);
svg{border:1px solid;}
<svg width="500px" height="300px" preserveAspectRatio="none" viewBox="0,0,5.0,3.0">
<g id="bbox"></g>
<g id="test" transform="translate(0 0)">
<rect x="1" y="0.25" width="0.5" height="0.5" fill="green" />
<rect x="1.95" y="1" width="0.5" height="0.5" fill="red" />
</g>
</svg>
I hope it helps
I'm writing an Amcharts plugin that puts a pie chart slice label on the slice's path. I do this by adding IDs to the slice <path>, moving the <text> in the label inside a <textPath> that references that <path>. The output looks correct but the text is not visible. It doesn't seem to be a browserism because several SVG validators do the same thing. Any idea why the <textPath> isn't being displayed?
I manipulate the chart data like so:
var chart = event.chart;
var div = chart.div;
var divId = div.id;
var chartData = chart.chartData;
chart.container.container.setAttribute("xmlns","http://www.w3.org/2000/svg");
chart.container.container.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
for(var i = 0; i < chartData.length; i++) {
if(chartData[i].dataContext.wrapLabel) {
/**
* Create an ID for the <path> that the label will wrap onto
*/
chartData[i].wedge.node.firstChild.setAttribute("id",divId + "-" + i);
// create the textPath element and set its href to the id we added to the path
var n = document.createElement('textPath');
n.setAttribute("xlink:href","#" + divId + "-" + i);
// Now move all of the tspan nodes underneath the textPath node
while(chartData[i].label.node.hasChildNodes()) {
n.appendChild(chartData[i].label.node.firstChild);
}
// and then append the textPath node to the <text> node
chartData[i].label.node.appendChild(n);
}
}
This produces the following SVG:
<svg version="1.1" style="position: absolute; width: 1000px; height: 1000px; top: -0.457382px; left: -0.002841px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g opacity="1" aria-label=": 24.38% 84 " visibility="visible" transform="translate(0,0)">
<path cs="1000,1000" d=" M450.03754782613004,498.06263767618003 L375.0938695653251,495.1565941904501 A125,125,0,0,1,499.99999999999994,375 L500,450 A50,50,0,0,0,450.03754782613004,498.06263767618003 Z" fill="#741010" stroke="#000" stroke-width="2" stroke-opacity="1" fill-opacity="1" id="chart2-0"></path>
</g>
<g visibility="visible" transform="translate(0,0)" opacity="1">
<text y="5" fill="#fff" font-family="Verdana" font-size="9px" opacity="1" text-anchor="middle" transform="translate(552,554)" style="cursor: default;" visibility="visible">
<textpath xlink:href="#chart2-0">
<tspan y="5" x="0">$260.5B</tspan>
<tspan y="16" x="0">Satellite</tspan>
<tspan y="27" x="0">Industry</tspan>
</textpath>
</text>
</g>
</svg>
(Edited down for brevity.)
You'd be wanting
n.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href","#" + divId + "-" + i);
because you can only set attributes in the null namespace with setAttribute.
Nor can you set a namespace with setAttribute so I'm not sure what you're trying to achieve with
chart.container.container.setAttribute("xmlns","http://www.w3.org/2000/svg");
chart.container.container.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
The fact that the textPath element is entirely lower case in the output suggests that something has gone wrong in its creation. Either you incorrectly wrote it in lower case or you've created it (and possibly all your other elements) in the wrong namespace.
See the MDN article for more details about namespaces.