I have an ellipse in SVG.
<ellipse cx="960" cy="600" rx="700" ry="480"/>
It is possible to draw the same shape in a <path> if needed.
Is it possible to precisely position an object (e.g. a circle) along this path, with an angle or a percentage of the path ?
You can use the getPointAtLength() method to get the position of a point on a path. If you want to use percents you need to calculate the length of the path (using the getTotalLength() method)
//the total length of the ellipse path
let ellTotalLength = ell.getTotalLength();
// the percentage as the input type range value
let perc = itr.value / 100;
function setPosition(perc){
//the distance on the path where to place the little circle
let dist = ellTotalLength * perc;
//get the position of a point at the distance dist on the path
let point = ell.getPointAtLength(dist);
// set the values of the cx and cy attributes of the little circle
circ.setAttributeNS(null, "cx", point.x);
circ.setAttributeNS(null, "cy", point.y);
}
setPosition(perc)
itr.addEventListener("input",()=>{
perc = itr.value / 100;
setPosition(perc)
})
svg{border:solid; width:300px;}
ellipse{fill:none;stroke:black;stroke-width:5px}
<p><input id="itr" type="range" value="70" /></p>
<svg viewBox="0 0 2000 1200">
<ellipse id="ell" cx="960" cy="600" rx="700" ry="480" />
<circle id="circ" r="30" fill="red" />
</svg>
UPDATE
The OP is commenting:
I should have warned that I can't use any javascript, I'm looking for a pure SVG solution. Sorry for that.
Next comes a demo where I'm using SVG animations to move the little circle over the ellipse. I've transformed the ellipse in a path and I'm animating the small circle over the ellipse using <animateMotion> the animation has a duration of 10s but stops after 3 seconds which means that the circle is stopping at 30% length. If you need this to happen instantly you may use a very short duration - for example dur=".1s" end=".03s".
I hope this is what you need.
<svg viewBox="-1000 -600 2000 1200">
<path id="path" d="M-700,0 a700,480 0 1,1 1400,0a700,480 0 1,1 -1400,0" style="stroke: #00f;stroke-width:20; fill: none;">
</path>
<circle r="20" fill="red" id="circ">
<animateMotion begin="0s" dur="10s" end="3s" rotate="auto" fill="freeze" >
<mpath xlink:href="#path"></mpath>
</animateMotion>
</circle>
</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>
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
How do I most easily first scale an object, say 2 * times it's current size and then flip it vertically and horizontally, or both?
As of now, I can either set "scale(2,2)" for it to become 2 times as big as it's width and height but I can't flip it at the same with scale(-1, 1) for vertical flip.
I'm creating SVG objects programmatically, as a format to export to.
To apply both scale and flip, just list both in your transform:
transform="scale(2,2) scale(-1,1)"
Or simply combine the values:
transform="scale(-2,2)"
Of course, the issue you have with negative scales is that the objects get flipped across the origin (top left) of the SVG, so they can go off the edge of the document. You need to correct this by adding a translate as well.
So, for example, imagine we had a document that is 100×100.
<svg width="100" height="100">
<polygon points="100,0,100,100,0,100"/>
</svg>
To flip this vertically, you do:
<polygon points="100,0,100,100,0,100" transform="scale(2,-2)"/>
And to correct the movement off-screen, you can either...
(option 1) Shift it negative before the flip (so it gets flipped back on screen):
<polygon points="100,0,100,100,0,100" transform="scale(2,-2) translate(0,-100)"/>
(The translate is listed second here because transform lists are effectively applied right to left)
(option 2) Or, you can shift it positive (by the scaled size) afterwards:
<polygon points="100,0,100,100,0,100" transform="translate(0,200) scale(-2,2)"/>
Here is a demo showing vertical flip, horizontal flip and both flips
Update
To flip (in position) an already existing object that is somewhere on screen. First determine its bounding box (minX, minY, maxX, maxY), or centreX,centreY if you already know that instead.
Then prepend the following to its transform:
translate(<minX+maxX>,0) scale(-1, 1) // for flip X
translate(0,<minY+maxY>) scale(1, -1) // for flip Y
or if you have the centre you can use
translate(<2 * centreX>,0) scale(-1, 1) // for flip X
So in your example:
<rect x="75" y="75" width="50" height="50" transform="translate(-100, -100) scale(2, 2) scale(1, 1) rotate(45, 100, 100)" />
The minX+maxX comes to 200. So to flip horizontally, we prepend:
translate(200,0) scale(-1, 1)
So the final object becomes:
<rect x="75" y="75" width="50" height="50" transform="translate(200,0) scale(-1, 1) translate(-100, -100) scale(2, 2) scale(1, 1) rotate(45, 100, 100)" />
Demo here
simply add below attributes into path tag in svg
transform="scale (-1, 1)" transform-origin="center"
Eg: <path transform="scale (-1, 1)" transform-origin="center" ......./>
Meet "Tux" the pinguin. For the sake of this exercise I painted the letters "L" and "R" on his feet.
For starters, let's paint Tux in the center of our canvas. If the canvas is size 500x500, and if Tux has a size of 100x100 we have to position him at (200,200). (i.e. the center minus half its size.)
<svg width="500" height="500">
<!-- marking our border and a cross through the center -->
<rect x="0" y="0" width="500" height="500" stroke-width="2" stroke="red" fill="none"></rect>
<line x1="0" y1="0" x2="500" y2="500" stroke="red" stroke-width="2"></line>
<line x1="500" y1="0" x2="0" y2="500" stroke="red" stroke-width="2"></line>
<!-- our pinguin in the center -->
<image x="200" y="200" width="100" height="100" href="assets/pinguin.png"></image>
</svg>
Now, if we want to mirror our pinguin horizontally (switching left and right) it is tempting to just use a transform with scale(-1 1). However, our pinguin just dissappears when we try that.
<svg width="500" height="500">
...
<image ... transform="scale(-1 1)"></image>
</svg>
The reason, is that the default point of reflection (the so-called "transform-origin") for our transform is not in the center of our image, but is actually still at the (0,0) point.
The most obvious solution is to move the point of reflection to the central point of the image (250,250). (in this case, the center of our canvas).
<svg width="500" height="500">
...
<image ... transform="scale(-1 1)" transform-origin="250 250"></image>
</svg>
And resizing works exactly the same. You can do it in 2 scales or combine them in 1 scale.
<svg width="500" height="500">
<!-- use 2 scales -->
<image x="200" y="200" width="100" height="100" href="assets/pinguin.png"
transform="scale(-1 1) scale(2 2)" transform-origin="250 250">
</image>
<!-- or just multiply the values of both scales -->
<image x="200" y="200" width="100" height="100" href="assets/pinguin.png"
transform="scale(-2 2)" transform-origin="250 250">
</image>
</svg>
No solution worked for me, I'll post what I discovered:
You can use either matrix or css-like transformations. And they behave different.
Take a look at this very basic example with an original shape and different ways to flip it horizontally. Notice that depending on the translation (you may want to keep it in the same x-axis) and the type of transformation you are using, you will need to set a different x-axis transformation.
Observation:
Matrix
Same place (light green): translate with positive width size.
X translation (dark green): Expected behavior (same as light green).
CSS-like
Same place (light blue): translate with negative width size and placed after scale. The opposite order is out of viewBox (note pink shape).
X translation (dark blue): translate with negative width size plus positive translation and placed before scale. The opposite order is out of viewBox (note orange shape).
<svg
viewBox="0 0 15 30"
width="150"
height="300"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<path
id="triangle"
d="M0,5 l5,5 V0z"
/>
</defs>
<use
href="#triangle"
fill="red"
/>
<use
y="5"
href="#triangle"
transform="scale(-1, 1) translate(-5, 0)"
fill="lightBlue"
/>
<use
y="5"
href="#triangle"
transform="translate(-5, 0) scale(-1, 1)"
fill="pink"
/>
<use
y="15"
href="#triangle"
transform="matrix(-1 0 0 1 5 0)"
fill="lightGreen"
/>
<use
href="#triangle"
transform="translate(10, 0) scale(-1, 1)"
fill="darkBlue"
/>
<use
href="#triangle"
transform="scale(-1, 1) translate(10, 0)"
fill="orange"
/>
<use
href="#triangle"
transform="matrix(-1 0 0 1 15 0)"
fill="darkGreen"
/>
</svg>
Here is the Livescript-ish code snippet how you can horizontally flip and scale by any factor:
# scale is 1 by default
if mirror or scale isnt 1
[minx, miny, width, height] = svg.attributes.viewBox |> split-by ',' |> cast-each-as (Number)
s = scale
# container is the root `g` node
container.attributes.transform = if mirror
"translate(#{s * (width + minx) + minx}, #{-miny * (s-1)}) scale(#{-s},#{s})"
else
"translate(#{-minx * (s-1)}, #{-miny * (s-1)}) scale(#{s},#{s})"
if scale isnt 1
svg.attributes
..viewBox = "#{minx},#{miny},#{width * scale},#{height * scale}"
..width = "#{width * scale}"
..height = "#{height * scale}"
I would like to draw a circle with it's diameter being an odd number (7px), so I can draw a 1px line exactly through the center of the circle.
<svg width="600" height="600">
<circle cx="30" cy="30" r="3.5" fill="red" />
</svg>
Strangely, the diameter is always even, no matter what is r value.
Shift the cx and cy coordinates by .5:
<svg width="600" height="600">
<circle cx="30.5" cy="30.5" r="3.5" fill="red" />
</svg>