I have two identical paths, but stroked differently: https://jsfiddle.net/vzbdcupf/
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" width="300" height="300">
<style>
.s0 { fill: none; stroke: red ; stroke-width: 80 }
.s1 { fill: none; stroke: black; stroke-width: 4 }
</style>
<path class="s0" d="m100 100c20 7 33-1 36-25 2-13-71 13-36 25z"/>
<path class="s1" d="m100 100c20 7 33-1 36-25 2-13-71 13-36 25z"/>
</svg>
When I stroke the path with red, the stroke-width is huge, and there should not be any "hole" inside it. Why is there a hole?
I think it is related to the rendering algorithm (stroking is converted into filling paths, and the inner path gets "reflected"). But how do you explain it in terms of the SVG specification, to be able to say the rendering is correct?
Not an explanation why this happens but a possible workaround:
Some observations on the occurrence of this rendering:
appears on paths containing curve commands (c, s, q etc.)
won't appear on primitives like <circle>, <polygon> or paths using only line commands like l, h, v
Applying a dashed stroke seems to fix this rendering issue:
pathLength="100"
stroke-dasharray="100 0"
stroke-linecap="round"
Example:
function applyDash() {
const svg = document.querySelector('svg');
const paths = svg.querySelectorAll('path');
paths.forEach(function(path) {
path.setAttribute('pathLength', 100);
path.setAttribute('stroke-dasharray', '100 0');
path.setAttribute('stroke-linecap', 'round');
})
}
<p><button type="button" onclick="applyDash()"> Apply dash fix</button></p>
<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="300px" viewBox="0 0 300 300">
<path fill="none" stroke="#FF1746" stroke-width="80" d="M58,100c10,3.5,18.3,3.3,24.4-0.9S92.5,87,94,75 c1-6.5-16.8-3.3-30.4,3.1S40.5,94,58,100z" />
<path fill="none" stroke="#FF1746" stroke-width="80" d="M211.6,78.1l-14.5,11.1L206,100l13.6,2.4l10.7-3.3
l7.5-9.1l4.1-15l-9.7-3.3L211.6,78.1L211.6,78.1z" />
<circle fill="none" stroke="#FF1746" stroke-width="80" cx="230" cy="232.5" r="9.8" />
<path fill="none" stroke="#FF1746" stroke-width="80" d="M77.1,232.5c0,5.4-4.4,9.8-9.8,9.8
s-9.8-4.4-9.8-9.8s4.4-9.8,9.8-9.8S77.1,227.1,77.1,232.5z" />
</svg>
Your guess as to what is happening is basically correct. When 2D graphics engines "stroke" a path, what they actually do is effectively convert the stroke into a filled path of its own. When the path that is being stroked has tight corners, larger stroke widths can cause the inside borders of the stroke path to overlap each other. The result is the same as if any path intersects with itself - it can cause holes. With actual fills, you can control how those holes are rendered with the fill-rule property. Unfortunately, there is no such property for strokes.
The SVG 2 specification briefly mentions this phenomenon here, but ultimately leaves it up to implementers how they want to deal this situation.
Consider following code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="2000" height="2000" viewBox="-1000 -1000 2000 2000"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style>text { font-size: 60px; }</style>
<style>path { fill: none; stroke: black; }</style>
<style>textPath { text-anchor: middle; }</style>
<path d="M -258.8190, -965.9258 a 1000, 1000 0 0,1 517.6381, 0" id="ttO"/>
<use xlink:href="#ttO" transform="scale(.85)" id="ttI"/>
</defs>
<use xlink:href="#ttO"/>
<text dy="55"><textPath xlink:href="#ttO" startOffset="50%">Hello</textPath></text>
<use xlink:href="#ttI"/>
<text dy="25"><textPath xlink:href="#ttI" startOffset="50%">World!</textPath></text>
</svg>
... and the resulting image:
.
The explicitly defined path (ttO) and the textPath (Hello) using that path works fine. To define the smaller concentric arc (ttI), rather than calculating coordinates, I like to use transform. This works fine. However, I cannot use this second arc 'ttI' in the second textPath, at least Chrome doesn't render the second textPath. Is this because 'use' cannot be used to define new ids? What is the best way to do this?
If it was just this example, probably I can define one path halfway between two arcs and use that to draw textPaths with +/- offsets and draw the two arcs as transforms of the defied path. My goal is to extend the pattern I attempted to create more paths/textPaths at other angles / distances from the center with different languages to form a circular graphic. Without defining transformed paths, I have to explicitly specify at least one arc for each language.
This is how I would do it: I would create a text element using the same text path and I would scale the text - in this case transform="scale(.7,.7)". Also since I want the text to be the same size as the unscaled one I would use a different font size: 1em for the scaled text and .7em for the unscaled. Of coarse this is just an example. You can pick your font size making sure the font size for the scaled text is bigger than the font-size of the unscaled one by the same amount as the transformation.
Please observe that for the path I'm using vector-effect="non-scaling-stroke"so that that stroke appears the same in both the scaled and unscaled one.
svg{border:solid}
path{fill:none;stroke:black;}
<svg viewBox="-110 -110 220 220" >
<g>
<path id="path0" d="M90,0A90,90 0 0 1 -90,0A90,90 0 0 1 90,0" vector-effect="non-scaling-stroke"/>
<text font-size=".7em" >
<textPath xlink:href="#path0" startOffset="75%" text-anchor="middle">Hello World!</textPath>
</text>
</g>
<g transform="scale(.7,.7)">
<text font-size="1em" >
<textPath xlink:href="#path0" startOffset="75%" text-anchor="middle">Hello World!</textPath>
</text>
<use xlink:href="#path0"/>
</g>
</svg>
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>
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
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.