8

I would like to simulate and "inside stroke" on an svg path. I have an svg map with multiple complex paths (countries) each with a different fill color stroke. And i would like to add a "fake inside stroke" in the first one. I managed to get a few things done with the inner-shadow trick (with gaussian blur filter) but can't manage to have it as "non-blurry".

The ideal solution would be as an svg filter so i can apply it dynamicaly via JS without changing path or manipulating dom.

Thanks a lot ! Edit 1 : So far i tried this trick but the fake shadow is sometimes over the stroke and always blurry so i'm not sure that's even the best way ...

  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="150" height="150" style="transform:scale(2);transform-origin:0 0 ">
     <defs>
  <filter id='inset' x='-50%' y='-50%' width='200%' height='200%'>
  
    <feFlood fill-color="black"/>
    <feComposite in2="SourceAlpha" operator="out"/>
    
    <feGaussianBlur stdDeviation='10' edgeMode="none" />
    <feOffset dx='0' dy='0' result='offsetblur'/>
    <feFlood flood-color='#00ff00' result='color'/>
    <feComposite in2='offsetblur' operator='in'/>
    <feComposite in2='SourceAlpha' operator='in' />
    <feMerge>
      <feMergeNode in='SourceGraphic'/>
        <feMergeNode/>
    </feMerge>
  </filter> 
       
	</defs>
  
<path class="st0" d="M144.7,126.2l-2.8,8.8l-3.9-2.3l-2-7.7l1.7-4.3l5.5-4.4L144.7,126.2z M93.5,24.2l6,6.3l4.4-1l7.5,6l1.9,1.1
	l2.5-0.3l4,3.4l12.3,2.4l-4.3,8.9l-1.1,9.1l-2.4,2.2l-3.9-1.2l0.3,3.2l-6.3,7l-0.1,5.6l4.1-1.9l2.9,5.4L121,84l2.5,4.6l-3,3.7
	l2.2,9.3l4.6,1.5l-1,5.1l-7.8,6.6l-16.9-3.2l-12.5,3.8l-1,7l-9.9,1.5l-9.6-5.3l-3.1,2.5l-15.8-5.3l-3.4-4.6l4.4-7.1l1.6-24.1
	l-8.8-13l-6.3-6.4l-13.1-4.9l-0.9-9.4l11.1-2.8L48.9,47l-2.7-14.8l8.1,5.7l20-10.3l2.6-11l7.5-2.8l1.3,4.8l4,0.2L93.5,24.2z" stroke-width="1" fill="#00ffff"  stroke="#FF0000" filter="url(#inset)"/>
</svg>

0

4 Answers 4

9

If you want clear shape, you should use SVG transform instead of applying CSS transform to svg element.

And when you draw "inside stroke", the feMorphorogy element is useful. This reduces(or increases) paint area of target shape, thus you can draw "fake inside/outside" stroke.

<svg xmlns="http://www.w3.org/2000/svg" 
  xmlns:xlink="http://www.w3.org/1999/xlink" width="300" height="300">
  <defs>
    <filter id='inset' x='-50%' y='-50%' width='200%' height='200%'>
      <!--outside-stroke-->
      <feFlood flood-color="red" result="outside-color"/>
      <feMorphology in="SourceAlpha" operator="dilate" radius="2"/>
      <feComposite in="outside-color" operator="in" result="outside-stroke"/>
      <!--inside-stroke-->
      <feFlood flood-color="blue" result="inside-color"/>
      <feComposite in2="SourceAlpha" operator="in" result="inside-stroke"/>
      <!--fill-area-->
      <feMorphology in="SourceAlpha" operator="erode" radius="2"/>
      <feComposite in="SourceGraphic" operator="in" result="fill-area"/>
      <!--merge graphics-->
      <feMerge>
        <feMergeNode in="outside-stroke"/>
        <feMergeNode in="inside-stroke"/>
        <feMergeNode in="fill-area"/>
      </feMerge>
    </filter>
  </defs>
  <g transform="scale(2)">
    <path class="st0" d="M144.7,126.2l-2.8,8.8l-3.9-2.3l-2-7.7l1.7-4.3l5.5-4.4L144.7,126.2z M93.5,24.2l6,6.3l4.4-1l7.5,6l1.9,1.1
  l2.5-0.3l4,3.4l12.3,2.4l-4.3,8.9l-1.1,9.1l-2.4,2.2l-3.9-1.2l0.3,3.2l-6.3,7l-0.1,5.6l4.1-1.9l2.9,5.4L121,84l2.5,4.6l-3,3.7
  l2.2,9.3l4.6,1.5l-1,5.1l-7.8,6.6l-16.9-3.2l-12.5,3.8l-1,7l-9.9,1.5l-9.6-5.3l-3.1,2.5l-15.8-5.3l-3.4-4.6l4.4-7.1l1.6-24.1
  l-8.8-13l-6.3-6.4l-13.1-4.9l-0.9-9.4l11.1-2.8L48.9,47l-2.7-14.8l8.1,5.7l20-10.3l2.6-11l7.5-2.8l1.3,4.8l4,0.2L93.5,24.2z"
fill="#00ffff" filter="url(#inset)"/>
  </g>
</svg>

0
7

This does what you want. Please note that it depends on the stroke being a single color that's distinct from any of the fill colors (in this case 100% red - you can change the stroke color to anything you want but the filter gets more complicated).

You can adjust the color of the "fake" inner stroke by altering the values in the last column of the final feColorMatrix. Right now, it's 100% blue. (You can also use feMorphology to create this - as in def's answer - but that approach does not preserve the original mitering.)

  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="150" height="150" style="transform:scale(2);transform-origin:0 0 ">
     <defs>
  <filter id='fake-stroke' x='-50%' y='-50%' width='200%' height='200%' color-interpolation-filters="sRGB">
  
   <!-- select just the red outline and zero out the opacity of everything that's not 100% red. -->
   <feColorMatrix type="matrix" values="1 0 0 0 0 
                                        0 0 0 0 0 
                                        0 0 0 0 0 
                                        255 -255 -255 -254 0" result="outline-only"/>
    <feGaussianBlur stdDeviation="1"/>

   <!-- select just the blur - not the original stroke. -->
    <feComposite operator="out" in2="outline-only"/>

   <!-- select just the blur that overlaps the original content -->
    <feComposite operator="in" in2="SourceGraphic" />

   <!-- increase its opacity to 100% except the most blurred - to fake anti-aliasing -->
    <feComponentTransfer>
      <feFuncA type="table" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"/>
    </feComponentTransfer>

   <!-- change the color of the fake stroke to the desired value -->
    <feColorMatrix type="matrix" values ="0 0 0 0 0
                                          0 0 0 0 0
                                          0 0 0 0 1 
                                          0 0 0 1 0"/>
   <!-- put it on top of the original -->
    <feComposite operator="over" in2="SourceGraphic"/>

  </filter> 
       
	</defs>
  
<path class="st0" d="M144.7,126.2l-2.8,8.8l-3.9-2.3l-2-7.7l1.7-4.3l5.5-4.4L144.7,126.2z M93.5,24.2l6,6.3l4.4-1l7.5,6l1.9,1.1
	l2.5-0.3l4,3.4l12.3,2.4l-4.3,8.9l-1.1,9.1l-2.4,2.2l-3.9-1.2l0.3,3.2l-6.3,7l-0.1,5.6l4.1-1.9l2.9,5.4L121,84l2.5,4.6l-3,3.7
	l2.2,9.3l4.6,1.5l-1,5.1l-7.8,6.6l-16.9-3.2l-12.5,3.8l-1,7l-9.9,1.5l-9.6-5.3l-3.1,2.5l-15.8-5.3l-3.4-4.6l4.4-7.1l1.6-24.1
	l-8.8-13l-6.3-6.4l-13.1-4.9l-0.9-9.4l11.1-2.8L48.9,47l-2.7-14.8l8.1,5.7l20-10.3l2.6-11l7.5-2.8l1.3,4.8l4,0.2L93.5,24.2z" stroke-width="2" fill="#00ffff"  stroke="#FF0000" filter="url(#fake-stroke)"/>
</svg>

1
  • Thanks a lot, i tried using it but failed at updating the matrix values dynamically in JS, the other solution is more straightforward for my use case.
    – Flunch
    Commented Apr 3, 2017 at 12:55
4

I tried both of the existing answers and I found that they altered the shape of the inner contour in a messy way. My solution below covers the requirements except that the solution would ideally use an SVG filter. I used the clip-path feature instead as it allowed me to preserve the correct miters and also not result in a blurry inset.

Comparison of existing solutions to my solution

  1. defghi1977's solution results in odd outline contours that do not match the original shape.
  2. Michael Mullany's solution results in a good outer contour but the inner one is heavily blurred.
  3. My solution results in sharp contours and correct miters.

SVG markup

<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
                   xmlns:xlink="http://www.w3.org/1999/xlink"
                   width="600" height="300" viewbox="0 0 300 150">
  <defs>
    <clipPath id="inset1">
      <path id="area1" d="M144.7,126.2l-2.8,8.8l-3.9-2.3l-2-7.7l1.7-4.3l5.5-4.4L144.7,
                          126.2z M93.5,24.2l6,6.3l4.4-1l7.5,6l1.9,1.1l2.5-0.3l4,3.4l
                          12.3,2.4l-4.3,8.9l-1.1,9.1l-2.4,2.2l-3.9-1.2l0.3,3.2l-6.3,7l
                          -0.1,5.6l4.1-1.9l2.9,5.4L121,84l2.5,4.6l-3,3.7l2.2,9.3l4.6,
                          1.5l-1,5.1l-7.8,6.6l-16.9-3.2l-12.5,3.8l-1,7l-9.9,1.5l-9.6
                          -5.3l-3.1,2.5l-15.8-5.3l-3.4-4.6l4.4-7.1l1.6-24.1l-8.8-13l
                          -6.3-6.4l-13.1-4.9l-0.9-9.4l11.1-2.8L48.9,47l-2.7-14.8l8.1,
                          5.7l20-10.3l2.6-11l7.5-2.8l1.3,4.8l4,0.2L93.5,24.2z"/>
    </clipPath>
  </defs>

  <use href="#area1" stroke-width="6" fill="cyan" stroke="blue" clip-path="url(#inset1)"/>
  <use href="#area1" stroke-width="2" fill="none" stroke="red"/>

</svg>

1
  • This is a great solution. Man, this tells me that I really should be using clipPath more!
    – Todd Main
    Commented Feb 27 at 23:25
2

Iterating from Don's idea here you might need to further separate each line and even add a fill with no overlaps -- perhaps because you need some transparency or texture.

If moving the shape into defs is possible in given use-case and interactivity is not strictly necessary, it is possible to further derive disjunct masks from it that each cover distinct area: "outer border" (beach), "inner border" (cliffs), and even "fill" (inland):

:root {
  background: dimgray;
  color: snow;
}
svg {
  --a: darkslategray;
  --b: teal;
  background-image: repeating-conic-gradient(var(--a) 0 25%, var(--b) 0 50%);
  background-size: 1em 1em;
}
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
  width="600" height="300" viewbox="0 0 300 150">

  <use
    href="#canvas"
    mask="url(#beach)"
    fill="darkblue"
    opacity=".6"
  />
  <use
    href="#canvas"
    mask="url(#cliffs)"
    fill="red"
    opacity=".4"
  />
  <use
    href="#canvas"
    mask="url(#inland)"
    fill="lime"
    opacity=".3"
  />

  <defs>
    <!--
      Both "stroke halves" in a single stroke-width:
    -->
    <g id="coast">
      <use
        href="#shoreline"
        stroke-width="10"
      />
    </g>
    <!--
      Area with the outer half of the stroke:
    -->
    <mask id="beach">
      <use
        href="#coast"
        stroke="white"
      />
      <use
        href="#shoreline"
        fill="black"
      />
    </mask>
    <!--
      For cutting the outer half of the stroke:
    -->
    <clipPath id="sea">
      <use
        href="#shoreline"
      />
    </clipPath>
    <!--
      Area with the inner half of the stroke:
    -->
    <mask id="cliffs">
      <use
        href="#coast"
        stroke="white"
        clip-path="url(#sea)"
      />
    </mask>
    <!--
      Area inside inner stroke:
    -->
    <mask id="inland">
      <use
        href="#coast"
        stroke="black"
        fill="white"
      />
    </mask>
    <!--
      Viewport cover:
    -->
    <rect
      id="canvas"
      width="100%"
      height="100%"
    />
    <!--
      The shape:
    -->
    <path
      id="shoreline"
      d="m144.7 126.2-2.8 8.8-3.9-2.3-2-7.7 1.7-4.3 5.5-4.4
         1.5 9.9zm-51.2-102 6 6.3 4.4-1 7.5 6 1.9 1.1 2.5-.3
         4 3.4 12.3 2.4-4.3 8.9-1.1 9.1-2.4 2.2-3.9-1.2.3
         3.2-6.3 7-.1 5.6 4.1-1.9 2.9 5.4-.3 3.6 2.5 4.6-3
         3.7 2.2 9.3 4.6 1.5-1 5.1-7.8 6.6-16.9-3.2-12.5
         3.8-1 7-9.9 1.5-9.6-5.3-3.1 2.5-15.8-5.3-3.4-4.6
         4.4-7.1L52.3 80l-8.8-13-6.3-6.4-13.1-4.9-.9-9.4
         11.1-2.8L48.9 47l-2.7-14.8 8.1 5.7 20-10.3 2.6-11
         7.5-2.8 1.3 4.8 4 .2 3.8 5.4z"
    />
  </defs>
</svg>

Semi-transparent map-like picture on chequerboard pattern with large and small island outlined with dark blue and red lines. Larger island has green inland fill.

(Credits:) See nice Drawing inner/outer strokes in SVG (clips and masks) article by Alex Chan demonstrating this technique.

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.