prevent path deformation made by preserveAspectRatio="none" - svg

I'd like to prevent <path> elements from stretching caused bypreserveAspectRatio="none" on <svg> tag.
I've found nearly perfect solution adding this attribute to <path> elements:
vector-effect="non-scaling-stroke"
But I need them to be scalable with transform="scale()" which is also disabled or more precisely their stroke-width are fixed and not scalable. So, my question is:
Is there any way to prevent stretching but not zooming?
Ok, to make it more clear: https://codepen.io/lukydorny/pen/aNYOdW
I would like to shape a path by different viewBox width and height (2nd image) but I need horizontal line to be of same width as vertical(3rd image). And then I wish I was able to scale it exactly the same way as original path(4th image) which is disabled as you can see in the last image.
Is there any way how to do it?

Yes, you can achieve this simply by defining the SVG element like this:
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="100%"
height="100%"
viewBox="0 0 1920 1080"
style="display:block">
... your SVG paths code here
</svg>
Notice the "viewBox" size should be the size of your "svg document" (maximum x and maximum y values).
This preserves the aspect ratio you define; however, be aware that your paths created should comply with the size you define, else it may cause problems.
To "zoom" the whole graphic, simply change with width & height of the SVG element, or its parent element, or use "scale()" inside the SVG, or with JavaScript as you normally would.

Would this option work?
svg {
border: solid 1px
}
<div title='original path' style='float:left;'>
<svg width=192px height=54px viewBox='0 0 200 200'>
<path style="stroke-linejoin: round; stroke-linecap: round; opacity: 1; stroke: rgb(50, 122, 97); stroke-width: 8px; fill: none;" d="m30 68.709l5,0,4.111,0,3.444,0,3.555,0,4.111,0.071,5,0.214,5.222,0.428,5.222,0.571,5,0.642,4.778,0.571,4.778,0.428,4.778,0.214,5.111,0.071,4.778,0,4.333,0,3.667,0,3.444,0,3.333,0,3.222,0,3,0,2.556,0,2.111,-0.071,1.889,-0.214,1.778,-0.499,1.778,-0.785,1.445,-1.07,1.111,-1.284,0.778,-1.498,0.556,-2.069,0.444,-2.64,0.222,-3.211,0.111,-2.997,0,-2.569,0,-1.998,0,-1.998,0,-2.283,0,-2.64,0,-2.497,0,-2.069,0,-1.569,-0.556,-1.284,-1.556,-1.07,-3,-0.785,-3.778,-0.499,-4.222,-0.214,-4.222,-0.071,-4.889,0,-5.333,0,-6,0,-5.778,-0.071,-5.778,-0.214,-5.556,-0.428,-5.556,-0.571,-5.889,-0.571,-5.889,-0.428,-5.778,-0.214,-4.445,-0.143,-3.333,-0.143,-2.444,-0.214,-2.333,-0.214,-2.333,-0.214,-2.222,-0.214,-2,-0.214,-1.778,-0.214,-1.667,-0.214,-1.778,-0.071,-1.778,0.357,-1.667,1.926,-1.222,3.781,-0.778,5.494,-0.333,5.28,-0.111,4.495,0,3.568,0,3.496,0,4.21,0,4.923,0,5.494,0,4.78,0,3.996,0,3.282,0,3.14,0,3.354,0,3.496,0.111,3.568,0.333,2.854,0.667,2.069,1.111,1.427,1.445,1.213,1.778,0.999,1.778,0.713,1.889,0.499,1.889,0.499,2.111,0.499,2.889,0.571,4.111,0.571,5.778,0.713,6.778,0.713,7.333,0.642,7,0.428,6.333,0.214,5.778,0.071,5.667,0,6.111,0,7.111,0,8.111,0,8.556,0,7.333,0,5.667,-0.071,4.444,-0.143,4.333,-0.285,4.556,-0.285,4.667,-0.285,4.556,-0.143,4.333,-0.143,4,-0.143,3.333,-0.214,2,-0.143,0,0"></path>
</svg>
</div>
<div title='original path' style='float:left;'>
<svg width=192px height=108px viewBox='0 0 200 200'>
<path style="stroke-linejoin: round; stroke-linecap: round; opacity: 1; stroke: rgb(50, 122, 97); stroke-width: 8px; fill: none;" d="m30 68.709l5,0,4.111,0,3.444,0,3.555,0,4.111,0.071,5,0.214,5.222,0.428,5.222,0.571,5,0.642,4.778,0.571,4.778,0.428,4.778,0.214,5.111,0.071,4.778,0,4.333,0,3.667,0,3.444,0,3.333,0,3.222,0,3,0,2.556,0,2.111,-0.071,1.889,-0.214,1.778,-0.499,1.778,-0.785,1.445,-1.07,1.111,-1.284,0.778,-1.498,0.556,-2.069,0.444,-2.64,0.222,-3.211,0.111,-2.997,0,-2.569,0,-1.998,0,-1.998,0,-2.283,0,-2.64,0,-2.497,0,-2.069,0,-1.569,-0.556,-1.284,-1.556,-1.07,-3,-0.785,-3.778,-0.499,-4.222,-0.214,-4.222,-0.071,-4.889,0,-5.333,0,-6,0,-5.778,-0.071,-5.778,-0.214,-5.556,-0.428,-5.556,-0.571,-5.889,-0.571,-5.889,-0.428,-5.778,-0.214,-4.445,-0.143,-3.333,-0.143,-2.444,-0.214,-2.333,-0.214,-2.333,-0.214,-2.222,-0.214,-2,-0.214,-1.778,-0.214,-1.667,-0.214,-1.778,-0.071,-1.778,0.357,-1.667,1.926,-1.222,3.781,-0.778,5.494,-0.333,5.28,-0.111,4.495,0,3.568,0,3.496,0,4.21,0,4.923,0,5.494,0,4.78,0,3.996,0,3.282,0,3.14,0,3.354,0,3.496,0.111,3.568,0.333,2.854,0.667,2.069,1.111,1.427,1.445,1.213,1.778,0.999,1.778,0.713,1.889,0.499,1.889,0.499,2.111,0.499,2.889,0.571,4.111,0.571,5.778,0.713,6.778,0.713,7.333,0.642,7,0.428,6.333,0.214,5.778,0.071,5.667,0,6.111,0,7.111,0,8.111,0,8.556,0,7.333,0,5.667,-0.071,4.444,-0.143,4.333,-0.285,4.556,-0.285,4.667,-0.285,4.556,-0.143,4.333,-0.143,4,-0.143,3.333,-0.214,2,-0.143,0,0"></path>
</svg>
</div>

Related

CSS transform-origin on <g> element for centered rotation?

I don't quite understand the transform-origin CSS rule.
If you have a plain <svg> with changing sizes (width, height) and some <rect> elements in it, how can you rotate a <g> element in it correctly?
It should be right next to the <rect>, so I am using transform: translate(x,y).
Then I'd like to rotate the group with rotate(45), but it ends up at strange places:
HTML
<div class="box">
<svg class="svg-main" width="600px" height="600px">
<rect x="0" y="50" width="90" height="40" fill="blue" stroke="blue"></rect>
<g transform="translate(100,50),rotate(7)"
transform-origin="center"
class="group">
<path d="M38.15,20.54,28.9,11.29a1.4,1.4,0,0,0-2.4,1v1.89a1.41,1.41,0,0,1-1.4,1.41H3.2A1.4,1.4,0,0,0,1.79,17v9.11a1.41,1.41,0,0,0,1.41,1.4H25.1a1.41,1.41,0,0,1,1.4,1.41v1.89a1.41,1.41,0,0,0,2.4,1l9.25-9.25A1.41,1.41,0,0,0,38.15,20.54Z" stroke="#000" strokeMiterlimit="10" strokeWidth="2"/>
</g>
</svg>
</div>
CSS
div.box {
padding: 30px;
background-color: #dfcfcf;
}
svg.svg-main {
background-color: white;
border: 2px solid #dedada;
}
svg.svg-main g.group path {
fill: rgba(215, 20, 45, 0.5);
}
JSFiddle
https://jsfiddle.net/bair_web/ajLgzoey/
Result for 17deg
Where is the origin?
Question
I would like to rotate the group with its <path> and using center for transform-origin.
MDN
How can I achieve a simple, centered rotation of a group in an SVG, which is also translated? It seems like transform-origin points to the parent element (the SVG)? So do I need to calculate the position of the<g> element relative to the <svg> elment and use it for transform-origin?
Because, when I just omit the origin, the rotated group also moves around and does not keep its center.

Sizing pattern element based on viewport

I have a large svg image that will be scrolled. I would like to add a tiled background image behind all the elements. This is currently leaving me with two problems:
I haven't found a way to keep a background image fixed, and not scroll it with the svg elements on top of it.
Assuming that's not possible, I wanted to size the tile elements to occupy 20% of the viewable area rather than 20% of the total svg size.
<svg width="3560.00px" height="1350.00px" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<pattern id="G-BG" width="20.00%" height="20.00%">
<image xlink:href="https://static.vecteezy.com/system/resources/thumbnails/000/301/800/small/jfyo_waxi_190121.jpg" x="0" y="0" width="20.00%" height="20.00%"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#G-BG)" fill-opacity="0.20"/>
<circle fill="green" cx="200" cy="200" r="100.00"/>
</svg>
If you run this little example, it sets a background composed of 5 x 5 copies of some arbitrary image, but the repeat interval of the tiling is based on 20% of the full svg size. If I try and use vw/vh units then the pattern size seems to go to zero, and so disappear.
My ideal choice would be to have a fixed 5 x 5 image that doesn't scroll, but the CSS background support doesn't seem to work for for svg.
My second choice would be to have a scrolling background image, but where there are 5 x 5 tiles visible at any one time.
Can either of these goals be achieved?
Not sure if there is anything here that might help?
html,body{
height: 100%;
}
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
.wrapper{
width: 100%;
height: 100%;
}
.wrapper svg{
background-color: 'red';
top: 0;
left:0;
position: fixed;
width: 100%;
height: 100%;
}
<div class="wrapper">
<svg xmlns="http://www.w3.org/2000/svg" >
<pattern id="pattern" width="0.2" height="0.25" viewBox="0 0 1 1" preserveAspectRatio="xMidYMid meet">
<image width="0.5" height="0.5" href="https://static.vecteezy.com/system/resources/thumbnails/000/301/800/small/jfyo_waxi_190121.jpg" />
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern)"></rect>
</svg>
</div>

SVG is generating a viewbox even though there isn't one specified

I'm trying to create an SVG play button that is only the size of the button itself, but it seems that there is some kind of viewbox being auto-generated. This phantom viewbox isn't even the same dimensions as the the play button and seems to be a 2:1 ratio.
#play-button {
border: 1px dashed gray;
}
<svg id="play-button">
<style type="text/css">
.st0{fill:none;stroke:#010101;stroke-miterlimit:10;}
.st1{fill:#010101;}
</style>
<circle id="button-border" class="st0" cx="30" cy="29.9" r="29.3"/>
<polygon id="play-triangle" class="st1" points="21.9,15.7 46.6,29.9 21.9,44.1 "/>
</svg>
How can I size the viewbox to the size of the SVG without specifying a viewBox or a height/width?
The outer SVG element is a replaced element. If you don't provide anything to go on for size you'll get the default width which is 300px and the default height which is 50% of the width so if you've not supplied a width value either, that ends up being 150px.
There are a number of ways to indicate the height and width you want. The most obvious would be height and width attributes or CSS properties of the same name but you can also use a viewBox attribute to define a width and height.
#RobertLongson's answer explains why this is happening. Here are some things you can do about it:
Your circle's r="29.3" does in effect "specify a height/width" in the markup. If you can put a value there, I would imagine you actually can the same value in the svg's width or height instead. Here's a different approach you could take with that in mind. It does require a viewbox but one that doesn't change: 0 0 100 100 just lets us use percentage values for the polygon's points. To calculate them, I did yourpointvalue/yourradius (e.g. 21.9/58.6). Using a border on the svg instead of a circle element makes this lighter weight and makes the markup easier to read. I've specified the width in the CSS, but it could also be inline; you could also only specify the height, or have it relative to a parent, etc etc.
(If you check the svg with your browser's inspector, you'll see it's the same width and height as the circle)
#play-button {
fill: #010101;
border: 1px solid #000;
border-radius: 50%;
display: block;
/* specify a width or a height either here or inline */
width: 58.6px;
}
<svg id="play-button" viewbox="0 0 100 100">
<polygon points="37.3,26.8 79.5,50 37.3,73.2" />
</svg>
If you really can't use viewbox, width, or height, and need to keep all that markup, you can achieve this with javascript. This is adapted from a solution by #Almis (but see #PaulLeBeau's take on the issue). The overflow: visible is necessary because the circle's stroke extends a little beyond the bounds of the svg. (Eventually we may be able to specify where a stroke is drawn, but not yet.)
var playButton = document.getElementById('play-button');
var boundingRect = document.getElementById('button-border').getBoundingClientRect();
playButton.style.height = boundingRect.height + 'px';
playButton.style.width = boundingRect.width + 'px';
#play-button {
border: 1px dashed gray;
overflow: visible; /* added */
}
<svg id="play-button">
<style type="text/css">
.st0 {
fill: none;
stroke: #010101;
stroke-miterlimit: 10;
}
.st1 {
fill: #010101;
}
</style>
<circle id="button-border" class="st0" cx="30" cy="29.9" r="29.3" />
<polygon id="play-triangle" class="st1" points="21.9,15.7 46.6,29.9 21.9,44.1 " />
</svg>
It looks like the answer is that the default size of an SVG is 300x150, which seems bizarre to me.
If you don't want that sizing (and you probably shouldn't rely on that default) you have to specify the size as detailed in this CSS Tricks article:
https://css-tricks.com/scale-svg/

svg with "non-scaling-stroke" ignores viewbox and uses its viewport

I'm working with an SVG that I don't want the stroke of <circle> to scale when resized. What happens when I add vector-effect="non-scaling-stroke" to the <circle> is that now the circumference becomes based on the viewport and not the original viewBox. This is a problem because when using stroke-dasharray for an animation, it's referencing the wrong circumference.
The following snippet shows a stroke-dasharray set to the circumference of two circles of the same size and the right circle showing the problem (using the viewport; effectively doubling its circumference):
svg {
width: 400px;
height: auto;
fill: none;
stroke: #000;
stroke-width: 6;
stroke-dasharray: 252
}
<body>
<svg viewBox="0 0 200 100">
<circle cx="50" cy="50" r="40" />
<circle cx="150" cy="50" r="40" vector-effect="non-scaling-stroke" />
</svg>
</body>
Is there any way to tell the <circle> to respect the viewBox and not the viewport?
This is what happens when you specify non-scaling stroke: it's "not-scaling" the "dash" in the stroke as well as the stroke itself. If you had a normal stroke dash array (rather than using it as an animation hack) - this is the behavior you would want :) There is currently no way to specify a different coordinate system just for the dash-array calculation, so javascript is your friend.

Can you control how an SVG's stroke-width is drawn?

Currently building a browser-based SVG application. Within this app, various shapes can be styled and positioned by the user, including rectangles.
When I apply a stroke-width to an SVG rect element of say 1px, the stroke is applied to the rect’s offset and inset in different ways by different browsers. This is proving to be troublesome, especially when I try to calculate the outer width and visual position of a rectangle and position it next to other elements.
For example:
Firefox adds 1px inset (bottom and left), and 1px offset (top and right)
Chrome adds 1px inset (top and left), and 1px offset (bottom and right)
My only solution so far would be to draw the actual borders myself (probably with the path tool) and position the borders behind the stroked element. But this solution is an unpleasant workaround, and I’d prefer not to go down this road if possible.
So my question is, can you control how an SVG’s stroke-width is drawn on elements?
No, you cannot specify whether the stroke is drawn inside or outside an element. I made a proposal to the SVG working group for this functionality in 2003, but it received no support (or discussion).
As I noted in the proposal,
you can achieve the same visual result as "inside" by doubling your stroke width and then using a clipping path to clip the object to itself, and
you can achieve the same visual result as 'outside' by doubling the stroke width and then overlaying a no-stroke copy of the object on top of itself.
Edit: This answer may be wrong in the future. It should be possible to achieve these results using SVG Vector Effects, by combining veStrokePath with veIntersect (for 'inside') or with veExclude (for 'outside). However, Vector Effects are still a working draft module with no implementations that I can yet find.
Edit 2: The SVG 2 draft specification includes a stroke-alignment property (with center|inside|outside possible values). This property may make it into UAs eventually.
Edit 3: Amusingly and dissapointingly, the SVG working group has removed stroke-alignment from SVG 2. You can see some of the concerns described after the prose here.
UPDATE: The stroke-alignment attribute was on April 1st, 2015 moved to a completely new spec called SVG Strokes.
As of the SVG 2.0 Editor’s Draft of February 26th, 2015 (and possibly since February 13th), the stroke-alignment property is present with the values inner, center (default) and outer.
It seems to work the same way as the stroke-location property proposed by #Phrogz and the later stroke-position suggestion. This property has been planned since at least 2011, but apart from an annotation that said
SVG 2 shall include a way to specify stroke position
, it has never been detailed in the spec as it was deferred - until now, it seems.
No browser support this property, or, as far as I know, any of the new SVG 2 features, yet, but hopefully they will soon as the spec matures. This has been a property I personally have been urging to have, and I'm really happy that it's finally there in the spec.
There seems to be some issues as to how to the property should behave on open paths as well as loops. These issues will, most probably, prolong implementations across browsers. However, I will update this answer with new information as browsers begin to support this property.
I found an easy way, which has a few restrictions, but worked for me:
define the shape in defs
define a clip path referencing the shape
use it and double the stroke with as the outside is clipped
Here a working example:
<svg width="240" height="240" viewBox="0 0 1024 1024">
<defs>
<path id="ld" d="M256,0 L0,512 L384,512 L128,1024 L1024,384 L640,384 L896,0 L256,0 Z"/>
<clipPath id="clip">
<use xlink:href="#ld"/>
</clipPath>
</defs>
<g>
<use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="#00D2B8" clip-path="url(#clip)"/>
</g>
</svg>
You can use CSS to style the order of stroke and fills. That is, stroke first and then fill second, and get the desired effect.
MDN on paint-order: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order
CSS code:
paint-order: stroke;
Here's a function that will calculate how many pixels you need to add - using the given stroke - to the top, right, bottom and left, all based on the browser:
var getStrokeOffsets = function(stroke){
var strokeFloor = Math.floor(stroke / 2), // max offset
strokeCeil = Math.ceil(stroke / 2); // min offset
if($.browser.mozilla){ // Mozilla offsets
return {
bottom: strokeFloor,
left: strokeFloor,
top: strokeCeil,
right: strokeCeil
};
}else if($.browser.webkit){ // WebKit offsets
return {
bottom: strokeCeil,
left: strokeFloor,
top: strokeFloor,
right: strokeCeil
};
}else{ // default offsets
return {
bottom: strokeCeil,
left: strokeCeil,
top: strokeCeil,
right: strokeCeil
};
}
};
As people above have noted you'll either have to recalculate an offset to the stroke's path coordinates or double its width and then mask one side or the other, because not only does SVG not natively support Illustrator's stroke alignment, but PostScript doesn't either.
The specification for strokes in Adobe's PostScript Manual 2nd edition states:
"4.5.1 Stroking:
The stroke operator draws a line of some thickness along the current path. For each straight or curved segment in the path, stroke draws a line that is centered on the segment with sides parallel to the segment." (emphasis theirs)
The rest of the specification has no attributes for offsetting the line's position. When Illustrator lets you align inside or outside, it's recalculating the actual path's offset (because it's still computationally cheaper than overprinting then masking). The path coordinates in the .ai document are reference, not what gets rastered or exported to a final format.
Because Inkscape's native format is spec SVG, it can't offer a feature the spec lacks.
Here is a work around for inner bordered rect using symbol and use.
Example: https://jsbin.com/yopemiwame/edit?html,output
SVG:
<svg>
<symbol id="inner-border-rect">
<rect class="inner-border" width="100%" height="100%" style="fill:rgb(0,255,255);stroke-width:10;stroke:rgb(0,0,0)">
</symbol>
...
<use xlink:href="#inner-border-rect" x="?" y="?" width="?" height="?">
</svg>
Note: Make sure to replace the ? in use with real values.
Background: The reason why this works is because symbol establishes a new viewport by replacing symbol with svg and creating an element in the shadow DOM. This svg of the shadow DOM is then linked into your current SVG element. Note that svgs can be nested and every svg creates a new viewport, which clips everything that overlaps, including the overlapping border. For a much more detailed overview of whats going on read this fantastic article by Sara Soueidan.
I don’t know how helpful will that be but in my case I just created another circle with border only and placed it “inside” the other shape.
This worked for me:
.btn {
border: 1px solid black;
box-shadow: inset 0 0 0 1px black;
}
A (dirty) possible solution is by using patterns,
here is an example with an inside stroked triangle :
https://jsfiddle.net/qr3p7php/5/
<style>
#triangle1{
fill: #0F0;
fill-opacity: 0.3;
stroke: #000;
stroke-opacity: 0.5;
stroke-width: 20;
}
#triangle2{
stroke: #f00;
stroke-opacity: 1;
stroke-width: 1;
}
</style>
<svg height="210" width="400" >
<pattern id="fagl" patternUnits="objectBoundingBox" width="2" height="1" x="-50%">
<path id="triangle1" d="M150 0 L75 200 L225 200 Z">
</pattern>
<path id="triangle2" d="M150 0 L75 200 L225 200 Z" fill="url(#fagl)"/>
</svg>
The solution from Xavier Ho of doubling the width of the stroke and changing the paint-order is brilliant, although only works if the fill is a solid color, with no transparency.
I have developed other approach, more complicated but works for any fill. It also works in ellipses or paths (with the later there are some corner cases with strange behaviour, for example open paths that crosses theirselves, but not much).
The trick is to display the shape in two layers. One without stroke (only fill), and another one only with stroke at double width (transparent fill) and passed through a mask that shows the whole shape, but hides the original shape without stroke.
<svg width="240" height="240" viewBox="0 0 1024 1024">
<defs>
<path id="ld" d="M256,0 L0,512 L384,512 L128,1024 L1024,384 L640,384 L896,0 L256,0 Z"/>
<mask id="mask">
<use xlink:href="#ld" stroke="#FFFFFF" stroke-width="160" fill="#FFFFFF"/>
<use xlink:href="#ld" fill="#000000"/>
</mask>
</defs>
<g>
<use xlink:href="#ld" fill="#00D2B8"/>
<use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="red" mask="url(#mask)"/>
</g>
</svg>
The easiest way I found is to add clip-path into circle
Add clip-path="circle()"
<circle id="circle" clip-path="circle()" cx="100" cy="100" r="100" fill="none" stroke="currentColor" stroke-width="5" />
Then the stroke-width="5" will magically become inner 5px stroke with absolute 100px radius.
Update 2023: The current draft renamed the attribute to stroke-align
Browser Support 2023:
See caniuse
This CSS property is not supported in any modern browser, nor are
there any known plans to support it.
Polyfill-like helper function
Based on the previous approaches to combine paint-order, mask and clip-path.
(As suggested by #Xavier Ho
#Jorg Janke)
//emulateStrokeAlign();
function emulateStrokeAlign() {
let supportsSvgStrokeAlign = CSS.supports("stroke-align", "outer") ?
true :
CSS.supports("stroke-alignment", "outer") ?
true :
false;
console.log("supportsSvgStrokeAlign", supportsSvgStrokeAlign);
if (!supportsSvgStrokeAlign) {
let ns = "http://www.w3.org/2000/svg";
let strokeAlignmentEls = document.querySelectorAll(
"*[stroke-alignment], *[stroke-align]"
);
strokeAlignmentEls.forEach((el, s) => {
let svg = el.closest("svg");
// set auto ids to prevent non-unique mask ids
let svgID = svg.id ? svg.id : "svg_" + s;
svg.id = svgID;
//create <defs> if not previously appended
let defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS(ns, "defs");
svg.insertBefore(defs, svg.children[0]);
}
let style = window.getComputedStyle(el);
let strokeWidth = parseFloat(style.strokeWidth);
let strokeAlignment = el.getAttribute("stroke-alignment") ?
el.getAttribute("stroke-alignment") :
el.getAttribute("stroke-align");
el.removeAttribute("stroke-align");
el.removeAttribute("stroke-alignment");
el.setAttribute("data-stroke-align", strokeAlignment);
let maskClipId = `mask-${svgID}-${s}`;
if (strokeAlignment === "outer") {
// create mask
let mask = document.createElementNS(ns, "mask");
mask.id = maskClipId;
let maskEl = el.cloneNode();
mask.appendChild(maskEl);
defs.appendChild(mask);
maskEl.setAttribute("fill", "#000");
mask.setAttribute("maskUnits", "userSpaceOnUse");
maskEl.setAttribute("stroke", "#fff");
maskEl.removeAttribute("stroke-opacity");
maskEl.removeAttribute("id");
maskEl.setAttribute("paint-order", "stroke");
maskEl.style.strokeWidth = strokeWidth * 2;
// clone stroke
let cloneStroke = el.cloneNode();
cloneStroke.style.fill = "none";
cloneStroke.style.strokeWidth = strokeWidth * 2;
cloneStroke.removeAttribute("id");
cloneStroke.removeAttribute("stroke-alignment");
cloneStroke.classList.add("cloneStrokeOuter");
cloneStroke.setAttribute("mask", `url(#${maskClipId})`);
el.parentNode.insertBefore(cloneStroke, el.nextElementSibling);
//remove stroke from original element
el.style.stroke = "none";
}
if (strokeAlignment === "inner") {
//create clipPath
let clipPathEl = el.cloneNode();
let clipPath = document.createElementNS(ns, "clipPath");
clipPath.id = maskClipId;
defs.appendChild(clipPath);
clipPathEl.removeAttribute("id");
clipPath.appendChild(clipPathEl);
el.setAttribute("clip-path", `url(#${maskClipId})`);
el.style.strokeWidth = strokeWidth * 2;
}
});
}
}
body {
margin: 2em;
}
svg {
width: 100%;
height: auto;
overflow: visible;
border: 1px solid #ccc;
}
body {
margin: 2em;
}
svg {
height: 20em;
overflow: visible;
border: 1px solid #ccc;
}
<p><button onclick="emulateStrokeAlign()">Emulate stroke align</button></p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 120">
<g id="myGroup" style="fill:rgb(45, 130, 255); stroke:#000; stroke-width:10; stroke-opacity:1;">
<rect id="el1" stroke-alignment="outer" x="10" y="10" width="100" height="100" />
<rect id="el2" x="140" y="10" width="100" height="100" />
<rect id="el3" stroke-alignment="inner" x="270" y="10" width="100" height="100" />
</g>
</svg>
<svg viewBox="0 0 12 6" xmlns="http://www.w3.org/2000/svg" stroke-width="0.5">
<path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5z" fill="blue" stroke-align="outer" stroke="red" stroke-opacity="0.5" stroke-linecap="butt" />
<path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5z" fill="blue" stroke-align="inner" stroke="red" stroke-opacity="0.5" />
</svg>
Hardcoded offset via paper.js offset glenzli's plugin
This approach will actually grow/shrink your <path> elements to get the desired stroke position (using the default middle stroke-alignment).
const canvas = document.createElement("canvas");
canvas.style.display='none';
document.body.appendChild(canvas);
//const canvas = document.querySelector("canvas");
paper.setup(canvas);
let strokeEls = document.querySelectorAll("*[stroke-alignment]");
strokeEls.forEach((el,i) => {
let type = el.nodeName;
let style = window.getComputedStyle(el);
let strokeWidth = parseFloat(style.strokeWidth);
let strokeAlignment = el.getAttribute('stroke-alignment');
let offset = strokeAlignment==='outer' ? strokeWidth/2 : (strokeAlignment==='inner' ? strokeWidth / -2 : 0);
// convert primitive
if(type!=='path'){
el = convertPrimitiveToPath(el);
}
let d = el.getAttribute("d");
let polyPath = new paper.Path(el.getAttribute("d"));
let dOffset = offset ? PaperOffset.offset(polyPath, offset)
.exportSVG()
.getAttribute("d") : d;
el.setAttribute("d", dOffset);
});
body{
margin:2em;
}
svg{
width:100%;
overflow:visible;
border:1px solid #ccc;
}
<svg viewBox="0 0 12 6" xmlns="http://www.w3.org/2000/svg" stroke-width="0.5">
<path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" stroke="black" fill="none" stroke-linejoin="miter"/>
<path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" fill="none" stroke-linejoin="miter" stroke-alignment="outer" stroke="red" stroke-opacity="0.5" />
<path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" stroke="black" fill="none" stroke-linejoin="round" />
<path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" fill="none" stroke-linejoin="round" stroke-alignment="inner" stroke="red" stroke-opacity="0.5" />
</svg>
<script src="https://unpkg.com/paper#0.12.15/dist/paper-full.min.js"></script>
<script src="https://unpkg.com/paperjs-offset#1.0.8/dist/paperjs-offset.js"></script>
However, the library struggles with complex shapes.

Resources