Search code examples
svg

How can I do a drop shadow inside a SVG mask?


I've got an SVG that I'm working on, and I've made a border around my design and "hollowed out" the middle using a mask. Now I'd like to apply a drop shadow to the inside of the border to make it look like it's sitting just above the rest of the elements.

Here's my SVG so far:

<svg xmlns="http://www.w3.org/2000/svg" width="640" height="640">
    <defs>
        <!-- The bezel -->
        <mask id="bezelMask">
            <!-- Make everything fully opaque -->
            <rect width="100%" height="100%" fill="white" />
            <!-- Then make a circle with a smaller radius -->
            <circle cx="50%" cy="50%" r="43.75%" fill="black" />
            <!-- Then unmask the bottom part of the bezel -->
            <rect width="100%" height="6.25%" y="65%" fill="white" />
            <!-- And finally, mask everything below that -->
            <rect width="100%" height="35%" y="71.25%" fill="black" />
        </mask>
    </defs>
    <g id="circular-face">
        <!-- Add the bezel -->
        <circle id="bezel" r="50%" height="100%" cx="50%" cy="50%" fill="red" mask="url(#bezelMask)" />
    </g>
</svg>

I've tried a few different things (<feDropShadow>, CSS styling etc.) but I can't seem to get it to do anything on the masked out portion.

I'm thinking that I need to convert my shape into a path, or at least make a path that sits underneath to make the shadow show up, but now I'm just curious if it's possible to do a drop shadow on the inside of an SVG mask

So can I make a drop shadow that affects the inside portion of a masked out shape?


Solution

  • As commented by Michael Mullany: you need a wrapping <g> element to apply a filter to a masked (or clipped) element. Otherwise, the filter result get's "cut-out" as well.

    To apply the inset shadow to the inner shape only you need an extra <mask> defining only this area. Then you can apply a custom inset-shadow filter.
    See also "How to create an inset shadow(transparent background) in SVG?".

    body {
      background: #ccc
    }
    
    .innerShadow {
      fill: white;
      filter: url(#insetShadow);
    }
    <svg xmlns="http://www.w3.org/2000/svg" width="640" height="640">
      <defs>
        <!-- The bezel -->
        <mask id="bezelMask">
          <!-- Make everything fully opaque -->
          <rect width="100%" height="100%" fill="white" />
          <!-- Then make a circle with a smaller radius -->
          <circle cx="50%" cy="50%" r="43.75%" fill="black" />
          <!-- Then unmask the bottom part of the bezel -->
          <rect width="100%" height="6.25%" y="65%" fill="white" />
          <rect width="100%" height="35%" y="71.25%" fill="black" />
        </mask>
    
    <!-- mask for inner shape -->
        <mask id="maskInner">
          <rect width="100%" height="100%" fill="black" />
          <circle cx="50%" cy="50%" r="43.75%" fill="white" />
          <rect width="100%" height="100%" y="65%" fill="black" />
        </mask>
    
     <!-- based on https://stackoverflow.com/questions/53502987/how-to-create-an-inset-shadowtransparent-background-in-svg#53503687 -->
        <filter id="insetShadow" width="200%" height="200%">
          <!-- Shadow Offset -->
          <feOffset dx="10" dy="10" />
          <!-- Shadow Blur -->
          <feGaussianBlur stdDeviation="10" result="offset-blur" />
          <!-- Invert the drop shadow to create an inner shadow -->
          <feComposite operator="out" in="SourceGraphic" result="inverse" />
          <!-- Color & Opacity -->
          <feFlood flood-color="black" flood-opacity=".75" result="color" />
          <!-- Clip color inside shadow -->
          <feComposite operator="in" in="color" in2="inverse" result="shadow" />
          <!-- Shadow Opacity -->
          <feComponentTransfer in="shadow" result="shadow">
            <feFuncA type="linear" slope="1" />
          </feComponentTransfer>
    
        </filter>
    
      </defs>
    
      <circle id="bezel" r="50%" height="100%" cx="50%" cy="50%" fill="red" mask="url(#bezelMask)" />  
      <!-- inset shadow area -->
      <g class="innerShadow">
         <circle id="inner" r="50%" height="100%" cx="50%" cy="50%" fill="white" mask="url(#maskInner)"  />
      </g>
    
    </svg>

    If you don't need to animate the mask you may also opt for a clip-path instead:

    body {
      background: #ccc
    }
    
    
    .innerShadow {
      fill: white;
      filter: url(#insetShadow);
    }
    <h3>Clip-path</h3>
    <svg width="640" height="640">
    <!-- add rectangular path in counter clockwise drawing direction -->
    <path d="M0 0 v640  h640 v-640 z
    M600 320c0 33.7-6 66.1-16.9 96h-526.2c-10.9-29.9-16.9-62.3-16.9-96 0-154.6 125.4-280 280-280s280 125.4 280 280z"  />
    </svg>
    
    <h3>Inner shape</h3>
    <svg width="640" height="640">
       <path d="M600 320c0 33.7-6 66.1-16.9 96h-526.2c-10.9-29.9-16.9-62.3-16.9-96 0-154.6 125.4-280 280-280s280 125.4 280 280z" id="arc" />
    </svg>
    
    
    
    <h3>Clipped SVG</h3>
    <svg width="640" height="640">
      <defs>
      <!-- inset shadow -->
        <filter id="insetShadow" width="200%" height="200%">
          <feOffset dx="10" dy="10" />
          <feGaussianBlur stdDeviation="10"  result="offset-blur" />
          <feComposite operator="out" in="SourceGraphic" result="inverse" />
          <feFlood flood-color="black" flood-opacity=".75" result="color" />
          <feComposite operator="in" in="color" in2="inverse" result="shadow" />
          <feComponentTransfer in="shadow" result="shadow">
            <feFuncA type="linear" slope="1" />
          </feComponentTransfer>
        </filter>
      
      
      <!-- inner shape -->
       <path d="M600 320c0 33.7-6 66.1-16.9 96h-526.2c-10.9-29.9-16.9-62.3-16.9-96 0-154.6 125.4-280 280-280s280 125.4 280 280z" id="arc" />
        
        <!-- clip-path: negative shape-->
        <clipPath id="clip">
          <!-- add rectangular path in counter clockwise drawing direction -->
             <path d="M0 0 v640  h640 v-640 z
                      M600 320c0 33.7-6 66.1-16.9 96h-526.2c-10.9-29.9-16.9-62.3-16.9-96 0-154.6 125.4-280 280-280s280 125.4 280 280z"  />
        </clipPath>
      </defs>
      
      <!-- border -->
      <use href="#arc" clip-path="url(#clip)" stroke="red" fill="none" stroke-width="80"/>
      <!--inset shadow-->
      <use href="#arc" class="innerShadow"/>
    
    </svg>

    In this case we create a <path> element for the inner shape in a graphic editor like inkscape using path-operations like subtract.
    We can make this path reusable by creating a <defs> element. The visible stroke can be drawn by an actual stroked instance of the inner shape which is cut-out by a clip-path.

    While this approach may appear quite complicated we get a slightly more compact SVG and can circumvent some safari/webkit related mask rendering issues (e.g sometimes rendering these at a fixed resolution resulting in blurry renderings when zooming).