Implement Zooming with viewbox in nested SVGs - svg

I have been tasked to implement zooming in custom charts based on SVGs. Before i had one <svg> element. I looked into either using the transform or the viewbox approach and decided for the viewbox approach. Since i have to support zooming on just the x-axis, the charts contents will be squashed depending on the zoomFactor and need preserveAspectRatio="none". This does not look pretty for the chart labels that i used foreignObjects for. They get disorted as well and are not readable anymore. I did not find any solutions on how to apply the viewbox to just the actual chart contents, not the scales / labels.
I came up with the solution to split the chart into 3 nested svgs. The structure looks like this:
<svg> // Container SVG
<svg>...</svg> // XAxis
<svg>...</svg> // YAxis
<svg>...</svg> // ChartContent
</svg>
The viewbox will only be applied to the ChartContent svg and the svgs with the actual scales stay untouched and are just simply rerendered if needed with different labels at different positions. The desired outcome is similar to this example: https://jsfiddle.net/19h83ker/1/
Given that i have a chart to display that is as an example 4000 pixels wide and 200 pixels high, the y-axis is 40 pixels wide and 200 pixels high, the x-axis is 40 pixels high and 4000 pixels wide, how should i generally setup the viewports? If i set width="100%" and height="100%" on the ContainerSVG and ChartContent SVG, i have no scrollbar available. If i set width="4040" on the ContainerSVG and width="4000" on the ChartContent SVG, i have a scrollbar but applying the viewbox while zooming out by 100% will simply halve my svg in width and the right 50% are left blank. I dont really understand what the combination of widths / heights is in my structure, that i should be going for. Or am i making a mistake in general? Are there better ways to implement the desired outcome? I have already spent 2 days on this and dont really see any other option than these 3 nested svgs.At the end of the day panning / zooming in the 4000 pixel wide example SVG chart should be possible.

The suggestions you mentioned involve setting width and height, then using the default browser behavior for scroll and zoom. Instead, you need to intercept the appropriate events and modify the viewBox attribute.
The code below demonstrates zooming on mouse wheel events. Panning should be simpler (only the first and second parts of the viewBox need to change) but the implementation depends on what UI controls you want to use.
svg = document.getElementById("s")
var vb = [0,0,300,200]
svg.setAttribute("viewBox",vb) // vb gets converted to the string "0,0,300,40"
function zoom(wheelEvent){
let k=1.005**wheelEvent.deltaY
let ctm = svg.children[0].getScreenCTM()//If the svg is empty, this won't work
// position of the mouse in svg coords
let mx = (wheelEvent.clientX-ctm.e)/ctm.a
let my = (wheelEvent.clientY-ctm.f)/ctm.d
// To center the zoom on the mouse, we need:
// (mx - initialX)/initialWidth == (mx - finalX)/finalWidth
// finalX = mx - (mx - initalX)*(finalWidth/initialWidth)
vb[0] = mx-(mx-vb[0])*k
vb[1] = my-(my-vb[1])*k
vb[2] *= k
vb[3] *= k
svg.setAttribute("viewBox",vb)
wheelEvent.preventDefault() // prevent the page from scrolling
}
svg.addEventListener("wheel",zoom)
html,body,svg{
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
}
#s{
width: 100%;
height: 100%;
background-color: red;
}
<svg id="outer" viewBox="0 0 200 100" preserveAspectRatio="xMinYMin">
<rect x="0" y="0" width="20" height="200" />
<rect x="0" y="0" width="100%" height="20" />
<svg id="s" x="20" y="20" width="180" height="180" viewBox="0,0,300,200">
<rect x="-1000" y="-1000" width="2000" height="2000" fill="lightgray"/>
<text font-size="60" x="10" y="60" fill="blue"> hello </text>
</svg>
</svg>
If you only want scaling of the x-axis, you'll need a different value for the 'preserveAspectRatio' attribute (as well as removing the code which changes vb[1] and vb[3]).

Related

How avoid SVG from scaling as its container shrinks?

I want an SVG to not scale as its container shrinks. On scroll, my container shrinks its height (its width stays the same). That container contains an SVG that I want to not become scaled, but instead have its lower part become invisible/cut off.
I can do it if I (by CSS) use my SVG as a background, but I'd prefer to have the SVG inline in the HTML.
I have tried with various values for the SVG attribute preserveAspectRatio. I thought that the value xMidYMin slice would slice off the bottom part of my SVG (like I want), but it squashes its height instead.
My container is 245x80 px and on scroll is shrinked to 245x40 px.
My svg element has attribute viewBox set to 0 0 245 80 , and has no width or height explicitly defined.
You can use preserveAspectRatio="xMidYMin slice" for the svg element.
Please observe that the svg element has a viewBox and also a width and a height. While the aspect ratio of the viewBox is 1:1 the aspect ratio from width and height is 2:1
xMidYMin - Force uniform scaling.
Align the midpoint X value of the element's viewBox with the midpoint X value of the viewport.
Align the of the element's viewBox with the smallest Y value of the viewport.
slice - Scale the graphic such that:
the aspect ratio is preserved and
the entire viewport is covered by the viewBox
Please read more about preserveAspectRatio
In the next demo use the slider to change the height of the svg element
itr.addEventListener("input",()=>{svg.setAttribute("height",itr.value)})
svg{border:solid}
<p><input id="itr" type="range" min="10" max="200" value="100"/></p>
<svg preserveAspectRatio="xMidYMin slice" viewBox="0 0 100 100" width="200" height="100" id="svg">
<defs>
<path style="fill:gold; stroke:black; stroke-width: 8px;
stroke-linecap: round; stroke-linejoin: round;" id="smiley" d="M50,10 A40,40,1,1,1,50,90 A40,40,1,1,1,50,10 M30,40 Q36,35,42,40 M58,40 Q64,35,70,40 M30,60 Q50,75,70,60 Q50,75,30,60" />
</defs>
<use href="#smiley" />
</svg>

SVG RECT Width and Height of 100%

I am creating an SVG element, and would like to change its background color. As per this question, and as per W3C recommendations, background-color is not a standard style for SVG, but fill must be used instead. However, fill does not work and the most common solution was to create a rect element inside the svg element and make that rect element have a width and height similar to that of the svg.
So, the following is the outcome of the suggested solution:
<svg width="300" height="200">
<rect width="300" height="200" style="fill: rgb(0, 255, 0);></rect>
</svg>
I then changed that to:
<svg width="300" height="200">
<rect width="100%" height="100%" style="fill: rgb(0, 255, 0);"></rect>
</svg>
(note that I have width and height set to 100% in my second attempt).
Now my question: even though this works, is using percentages in width and height a W3C standard? Or is it a hack?
Thanks.
is using percentages in width and height a W3C standard?
Yes. According to https://www.w3.org/TR/SVG/coords.html:
The supported length unit identifiers are: em, ex, px, pt, pc, cm, mm, in, and percentages.

SVG image with a border / stroke

I'm trying to add a border around a svg image. I have tried 2 approaches but both failed...
Attempt 1 : Draws image but no border..
<image id="note-0" xlink:href="http://example.com/example.png" x="185" y="79" height="202" width="150" style="stroke:#ac0d0d; stroke-width:3;"></image>
Attempt 2 : Draws image but no border..
<defs>
<image id="image1352022098990svg" height="202" width="150" xlink:href="http://example.com/example.png"></image>
</defs>
<use xmlns="http://www.w3.org/2000/svg" id="note-0" xlink:href="#image1352022098990svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="185" y="79" height="202" width="150" style="stroke:#ac0d0d; stroke-width:3;"/>
So my question is, is it possible to define a image on a svg element and have a border/stroke around it at the same time?
Futhermore it seems i can position svg elements with translate and with the x/y attribute. Which is preffered and why?
stroke does not apply to <image> or <use>, only shapes and text. If you want to draw a border round it, draw a <rect> after the image with the same x,y,width and height as the image and give that a stroke and a fill of "none".
As to translate vs x/y - it depends on your use case.
If for some reason you cannot change the SVG elements, there is a workaround using the outline CSS property:
#note-0 {
outline: 6px solid white;
}
If you need it to wrap around a circular image (svg shape for example), and you just need some color to outline it, you may find something like this useful:
image {
filter: drop-shadow(0px 0px 1px red);
}

How does setting viewBox attribute, affects the user coordinates

I have the following code: http://jsfiddle.net/fCWJ5/1/, and following doubts regarding the viewbox.
body{margin:0;}
#test{width:200px;height:200px;border:solid red 1px;}
<body>
<div id="test">
<!-- preserveAspectRatio="xMinYMin meet" -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 150"
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full">
<g>
<rect class="drag resize" x="150" y="50" width="50" height="50" fill="#c66" />
</g>
</svg>
</div>
</body>
From the fiddle one user coordinate = .2, this I got by dividing
200/1000 (test div width / viewBox width attribute). According to
this, the rectangle should be at (30px, 10px), with a width and
height of 10px, 10px respectively. But the rectangle is at
(30px,97px), with a width and height of 10px,10px (some how height
and width is correct as per calculation.). Please point out why the
y coordinate is wrong.
Then I gave preserveAspectRatio="xMinYMin meet" as said in a svg
tutorial pdf. It was working fine for this value. But for other
value the display goes for toss. Please explain what is this. I
already asked a question regarding this
could not able to put viewbox,viewport,userspace together and get the picture.
I'm unable to understand the answer and the concept.
what will be the value of the ratio, if I didn't specified any width
and height for the svg dom element container.
I'm seeing that the ratio 1.3, (height of the test div/height
attribute of the viewBox), is not used. Should that be used for
calculating things like height,y coordinates.
The problem is the aspect ratio of your DIV does not match the aspect ratio of your viewBox. So HTML puts your SVG in the center of the DIV with the empty space above and below. Add the following to your SVG code to illustrate:
<rect x="0" y="0" width="100%" height="100%" fill="none" stroke="black"/>
This will show you the boundaries of your SVG element, while the red border you put on your DIV will show you its boundary. They don't match.
If you don't put a width or height on the SVG element then it will fill its container. In your example you set the DIV to 200px X 200px, the viewBox will then be applied effectively dividing the 200px by 1000 user units for X and 30px by 150 for the Y (because of the aspect ratio of the SVG only 15% of the DIV height is used by the SVG, 15% of 200px is 30px). Remove the width and height from the DIV and it will use the full width of the screen.
If you add my rect element you will see that your box is 1/3 (50/150 = 1/3) from the top extending 1/3 down, while also being 3/20 (150/1000 = 3/20) in from the left and extending 1/20 (50/1000 = 1/20) across.

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