<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oleksii</title>
    <description>The latest articles on DEV Community by Oleksii (@svg_garagemade).</description>
    <link>https://dev.to/svg_garagemade</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4068207%2F4fb09d7b-fb1e-42b0-b69c-822d981aa95d.png</url>
      <title>DEV Community: Oleksii</title>
      <link>https://dev.to/svg_garagemade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/svg_garagemade"/>
    <language>en</language>
    <item>
      <title>Converting SVG strokes to fills in the browser: the geometry problem nobody warns you about</title>
      <dc:creator>Oleksii</dc:creator>
      <pubDate>Sat, 08 Aug 2026 01:50:16 +0000</pubDate>
      <link>https://dev.to/svg_garagemade/converting-svg-strokes-to-fills-in-the-browser-the-geometry-problem-nobody-warns-you-about-9ko</link>
      <guid>https://dev.to/svg_garagemade/converting-svg-strokes-to-fills-in-the-browser-the-geometry-problem-nobody-warns-you-about-9ko</guid>
      <description>&lt;p&gt;A while back I was building an icon font with IcoMoon. I dragged in my SVGs, hit generate, and about half the icons came out wrong — hollow outlines, missing pieces, shapes that looked nothing like the source.&lt;/p&gt;

&lt;p&gt;The reason turned out to be simple, and it's a trap a lot of tooling sets for you: &lt;strong&gt;my icons were drawn with strokes, and IcoMoon wants fills.&lt;/strong&gt; It builds glyphs from filled outlines, so a shape that's really "a thin path rendered at 2px" has no filled area to turn into a glyph. It just… disappears.&lt;/p&gt;

&lt;p&gt;The fix is an operation you'll see called "outline stroke," "stroke to path," or "stroke to fill": take a stroked path and replace it with a filled shape that traces the outline of that stroke. Illustrator has it. Inkscape has it (&lt;code&gt;Path → Stroke to Path&lt;/code&gt;). I wanted it in the browser, no install, no upload — so I built it. And it turned out to be a much deeper geometry problem than "just draw the outline."&lt;/p&gt;

&lt;h2&gt;
  
  
  Stroke vs fill: why anything cares
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;stroke&lt;/strong&gt; is a path plus an instruction: &lt;em&gt;draw me at width N&lt;/em&gt;. The geometry stored in the file is a thin centerline; the thickness is a rendering property.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;fill&lt;/strong&gt; is a closed shape painted solid. The thickness &lt;em&gt;is&lt;/em&gt; the geometry.&lt;/p&gt;

&lt;p&gt;On screen they can look identical. Structurally they're completely different, and a surprising number of tools only understand fills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Icon-font generators&lt;/strong&gt; (IcoMoon and friends) build glyphs from filled contours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cutting machines&lt;/strong&gt; — Cricut, laser cutters, vinyl plotters — cut along fill contours. A stroke is either ignored or cut down its centerline, which is not what you drew.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font editors&lt;/strong&gt; and some &lt;strong&gt;game engines&lt;/strong&gt; want outlines, not stroked paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Older PDF and print pipelines&lt;/strong&gt; choke on complex strokes (dashes, variable joins).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever one of these meets a stroke, you get empty shapes or wrong output. Converting stroke → fill up front makes the asset portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach, and why it falls apart
&lt;/h2&gt;

&lt;p&gt;The obvious idea: offset the path by half the stroke width to each side and close it off. Two parallel copies, join the ends, done.&lt;/p&gt;

&lt;p&gt;It falls apart almost immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Corners.&lt;/strong&gt; At every vertex the two offset edges have to be joined — and SVG defines three different joins (&lt;code&gt;miter&lt;/code&gt;, &lt;code&gt;round&lt;/code&gt;, &lt;code&gt;bevel&lt;/code&gt;) with a &lt;code&gt;miter-limit&lt;/code&gt; that clips sharp spikes. A naive offset produces gaps or overshoots at every corner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line ends.&lt;/strong&gt; &lt;code&gt;butt&lt;/code&gt;, &lt;code&gt;round&lt;/code&gt;, and &lt;code&gt;square&lt;/code&gt; caps each need their own end geometry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-overlap.&lt;/strong&gt; When a path curves back on itself, the offset outline crosses itself. You don't want the crossing — you want the &lt;em&gt;union&lt;/em&gt; of the swept area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holes.&lt;/strong&gt; A closed stroked shape has an inner and outer contour. Get the winding wrong and the hole fills in solid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this isn't an offset problem, it's a &lt;strong&gt;polygon offsetting with union&lt;/strong&gt; problem. That's a well-studied thing, and the reference implementation in this space is Angus Johnson's Clipper. I used the JS port, &lt;code&gt;clipper-lib&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual pipeline
&lt;/h2&gt;

&lt;p&gt;Clipper works on integer polygons, so getting an SVG path into it is most of the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Normalize the path
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;svgpath&lt;/code&gt; flattens all the shorthand and arc commands down to something uniform — absolute coordinates, expanded shorthands, arcs converted to cubic Béziers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svgpathMod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svgpath&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svgpath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svgpathMod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;svgpathMod&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;svgpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="c1"&gt;// absolute coordinates&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unshort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// expand S/T shorthand&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unarc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// arcs → cubic béziers&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// now every segment is M / L / C / Q / Z&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Flatten curves to polylines
&lt;/h3&gt;

&lt;p&gt;Clipper doesn't know about Béziers — it needs points. Rather than sampling every curve at a fixed step (which over-samples flat curves and under-samples tight ones), flatten &lt;em&gt;adaptively&lt;/em&gt;: recursive subdivision with a flatness test, so a curve only gets split where it's actually bending.&lt;/p&gt;

&lt;p&gt;The tolerance is scaled to the artwork so a tiny icon and a huge illustration both stay smooth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// flatten tolerance ≈ 0.06% of the viewBox diagonal, floored&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0006&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps the polygonal approximation well under a pixel at normal sizes, with a hard recursion cap so a pathological curve can't blow the stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Scale to integers
&lt;/h3&gt;

&lt;p&gt;Clipper's robustness comes from integer arithmetic, so multiply everything by a fixed factor before feeding it in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// → 1e-4 unit precision&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SCALE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SCALE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Offset by half the stroke width
&lt;/h3&gt;

&lt;p&gt;This is where the real work happens — and where the naive version was hopeless. &lt;code&gt;ClipperOffset&lt;/code&gt; takes the miter limit and an arc tolerance, and you map SVG's joins and caps onto Clipper's &lt;code&gt;JoinType&lt;/code&gt;/&lt;code&gt;EndType&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JoinType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EndType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Clipper&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JoinType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jtRound&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bevel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JoinType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jtSquare&lt;/span&gt; &lt;span class="c1"&gt;// Clipper has no exact bevel; square is closest&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JoinType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jtMiter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;linecap&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;EndType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;etOpenRound&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;linecap&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;square&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;EndType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;etOpenSquare&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EndType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;etOpenButt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;strokeWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SCALE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// offset outward&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;co&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Clipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClipperOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;miterLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;co&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subpathClosed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;EndType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;etClosedLine&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;co&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The offset delta is half the stroke width (a stroke is centered on its path, so it extends &lt;code&gt;width/2&lt;/code&gt; each way). The arc tolerance scales with the stroke width, so round joins on thick strokes get proportionally more segments and stay smooth.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Back to a path — and let winding carve the holes
&lt;/h3&gt;

&lt;p&gt;Clipper returns oriented polygons: outer contours wound one way, holes wound the other. Concatenate them all into a single &lt;code&gt;d&lt;/code&gt;, and SVG's default &lt;code&gt;fill-rule: nonzero&lt;/code&gt; reads the orientation and punches the holes out automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`M&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`L&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;fill-rule&lt;/code&gt; is set — the initial &lt;code&gt;nonzero&lt;/code&gt; is exactly what you want, &lt;em&gt;because&lt;/em&gt; Clipper hands back correctly oriented contours. That's the quiet payoff of using a real offsetting engine instead of hand-rolling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that bite
&lt;/h2&gt;

&lt;p&gt;A few things are worth knowing before you ship this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bevel joins are approximated.&lt;/strong&gt; Clipper has no true bevel, so I substitute &lt;code&gt;jtSquare&lt;/code&gt;. Close, not identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output is polyline-only.&lt;/strong&gt; Curves are flattened and never re-fitted to Béziers, so fidelity is bounded by your flatten tolerance. At ~0.06% of the diagonal it's sub-pixel, but it &lt;em&gt;is&lt;/em&gt; an approximation — I haven't formally measured a symmetric-difference error, so I won't quote a number I can't back up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One join type per path.&lt;/strong&gt; Clipper takes a single &lt;code&gt;JoinType&lt;/code&gt; per path, so a &lt;code&gt;stroke-linejoin&lt;/code&gt; can't vary per vertex. SVG 2's &lt;code&gt;arcs&lt;/code&gt; join isn't supported and silently degrades to miter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vector-effect: non-scaling-stroke&lt;/code&gt; gets baked&lt;/strong&gt; at the current scale — the whole point of the operation is to freeze the stroke into geometry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradient and pattern strokes&lt;/strong&gt; keep their paint reference; object-bounding-box gradients can shift once the geometry changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashed strokes work&lt;/strong&gt; — you split the centerline into its "on" runs first, then offset each run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One note on the dependency
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clipper-lib&lt;/code&gt; is excellent at the actual math, but it's unmaintained — the last release was around 2017. It's battle-tested and correct, which is why I kept it, but if you're starting fresh, look at &lt;strong&gt;Clipper2&lt;/strong&gt; (the newer port from the same author). Being honest about the state of your dependencies is cheaper than pretending everything's pristine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser part
&lt;/h2&gt;

&lt;p&gt;The reason I went down this road at all is that I wanted it to run entirely client-side — the SVG is parsed, flattened, offset and re-serialized in the page, and never touches a server. It's now one of the tools in &lt;a href="https://svg.garagemade.app" rel="noopener noreferrer"&gt;garagemade&lt;/a&gt;, a free, browser-based SVG editor I've been building. But the geometry above is the interesting part, and it's the same whether you build it into an app or a one-off script.&lt;/p&gt;

&lt;p&gt;If you've fought stroke-to-fill from a different angle — a cleaner curve refit, a better join approximation — I'd genuinely like to hear it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>svg</category>
      <category>graphics</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
