htmlcsssvg

How can I apply a mask from an inline SVG that will work in Chrome?


Update: This question seems to be obsolete as both snippets now work fine in Chrome.


I want to clip my element using an SVG shape which is defined in the same HTML file (an inline SVG).

It works with clip-path:

div {
  width: 200px;
  height: 200px;
  background-color: red;
  clip-path: url("#c");
}
<div>
  <svg>
    <defs>
      <clippath id="c">
        <circle cx="100" cy="100" r="50" />
      </clippath>
    </defs>
  </svg>
</div>

But when using a mask, although this works fine in Firefox, it applies no masking in Chrome:

div {
  width: 200px;
  height: 200px;
  background-color: red;
  mask: url("#m");
}
<div>
  <svg>
    <defs>
      <mask id="m">
        <circle cx="100" cy="100" r="50" fill="white" />
      </mask>
    </defs>
  </svg>
</div>

Searching around (example), it seems as though Chrome does not expect the mask to refer to a definition, but instead to an entire image. Is there a way to refer to an entire image if it has been inlined, though? Or, is there anything else I could do to apply a mask from an inline element?


Solution

  • Here is an ugly js solution, which will

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    const mask = document.querySelector("mask");
    svg.innerHTML = mask.innerHTML;
    const markup = new XMLSerializer().serializeToString(svg);
    const url = `data:image/svg+xml;charset=utf8,${ encodeURIComponent(markup) }`;
    const div = document.getElementById('mask-me');
    div.style.webkitMaskImage = `url('${ url }')`;
    #mask-me {
      width: 200px;
      height: 200px;
      background-color: red;
      /**
        This will be overidden by -webkit-mask-image even in Firefox.
        We could have avoided it with a CSS variable, e.g
        -webkit-mask-image: var(--workaround-url);
        but Safari acts in all ways as if they did support inline SVG as mask,
        when they don't.
        So, for them, we have to use the workaround everywhere...
      mask-image: url(#m);
      **/
    }
    <div id="mask-me">
      <svg>
        <defs>
          <mask id="m">
            <circle cx="100" cy="100" r="50" fill="white" />
          </mask>
        </defs>
      </svg>
    </div>


    For the ones needing the mask-mode: luminance feature, you need to do it manually in the generated SVG image through a <feColorMatrix type="luminanceToAlpha"/>:

    const svgNS = "http://www.w3.org/2000/svg";
    const svg = document.createElementNS(svgNS, "svg");
    // create a <filter><feColorMatrix type="luninanceToAllpha"/>
    const alpha = document.createElementNS(svgNS, "feColorMatrix");
    alpha.setAttribute("type", "luminanceToAlpha");
    const filter = document.createElementNS(svgNS, "filter");
    filter.setAttribute("id", "alpha");
    filter.append(alpha);
    svg.append(filter);
    // apply the filter on all the <mask> content
    const g = document.createElementNS(svgNS, "g");
    g.setAttribute("filter", "url(#alpha)");
    // luminanceToAlpha disregards semi-transparent pixels,
    // so we add a black background to have no such thing 
    const bg = document.createElementNS(svgNS, "rect");
    bg.setAttribute("width", "100%");
    bg.setAttribute("height", "100%");
    g.append(bg);
    
    var mask = document.querySelector('mask').cloneNode(true);
    g.append(...mask.children);
    svg.append(g);
    
    const markup = new XMLSerializer().serializeToString(svg);
    const url = "data:image/svg+xml;charset=utf8," + encodeURIComponent(markup);
    const div = document.getElementById("mask-me");
    div.style.webkitMaskImage = `url('${ url }')`;
    .border {
      display: inline-block;
      border: 1px solid;
    }
    #mask-me {
      width: 300px;
      height: 200px;
      background-color: red;
      /** Safari doesn't really support mask-image using inline SVG either...
        mask-image: url(#m);
        mask-mode: luminance;
      **/
    }
    <div id="mask-me">
      <svg width="0">
        <defs>
          <mask id="m">
            <circle cx="100" cy="100" r="50" fill="white" />
            <rect x="90" y="90" width="20" height="20" fill="black" />
          </mask>
        </defs>
      </svg>
    </div>