skip to Main Content

I am working on a web project where I display dynamically rendered items which I would like to display in a very specific way: They should be masked with a black and white image and then be applied an SVG filter.
The mask images are highly contrasted black and white jpgs which are created in the backend from the images which the users upload. This is an example of such an item:

svg {
  width: 100px;
  height: 100px;
  filter: url("#outline");
  padding: 1px;
  transition: all 800ms;
}

svg:hover {
  transform: rotate(90deg);
}

.wrapper {
  position: absolute;
  padding: 10px;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: black;
}
<div class="wrapper">
   <svg
      filter="url(#outline)"
      width="100%"
      height="100%"
    >
      <defs>
        <mask id="mask">
          <image
            x="0"
            y="0"
            width="100%"
            height="100%"
            xlink:href="https://i.ibb.co/pL9J807/test.png"
          />
        </mask>

        <filter id="outline" color-interpolation-filters="sRGB">
          <feDropShadow
            dx="0"
            dy="0"
            stdDeviation="1.25"
            in="SourceGraphic"
            flood-color="white"
            result="blur"
            flood-opacity="10"
          ></feDropShadow>
          <feColorMatrix
            mode="matrix"
            values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 55 -5"
            in="blur"
          ></feColorMatrix>
        </filter>
      </defs>

      <rect
        width="100%"
        height="100%"
        mask="url('#mask')"
        fill="red"
      />
    </svg>
  
</div>

There is additionally an intro animation, which makes the items rotate. It works perfectly fine in Chrome. In Safari, however, the masking creates performance issues, making the masked elements jump and rotate in an unexpected way even after the intro animation is done. Apparently, Safari does not do too well with SVG masks, hence I am looking for an alternative to mask single-colored divs/rects with black and white images.

Is there a way to use CSS masks that also works with black/white as input instead of black/transparent? Or is there maybe something about the SVG setup that could be changed in order to icrease performance? I also included the SVG filter in the code snippet although the performance issues arise even when it is removed as it might be relevant when thinking about alternatives.

I am grateful for any hints!

2

Answers


  1. As already implied by Michael Mullany

    SVG filters are notorious for negatively affecting rendering performance.

    Workarounds to mitigate jittery animations/transitions

    • apply the filter only to certain elements – instead of the whole parent svg.
    • specify or limit the css properties, that should be transitioned like so:
      transition: transform 800ms;
    svg {
      width: 100px;
      height: 100px;
      padding: 1px;
      transition: transform 800ms;
      overflow: visible;
    }
    
    svg:hover {
      transform: rotate(90deg);
    }
    
    .filterOutline {
      filter: url("#outline");
    }
    
    .wrapper {
      position: absolute;
      padding: 10px;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: black;
    }
    <div class="wrapper">
      <svg width="100%" height="100%">
          <defs>
            <mask id="mask">
              <image
                x="0"
                y="0"
                width="100%"
                height="100%"
                xlink:href="https://i.ibb.co/pL9J807/test.png"
              />
            </mask>
    
            <filter id="outline" color-interpolation-filters="sRGB">
              <feDropShadow
                dx="0"
                dy="0"
                stdDeviation="1.25"
                in="SourceGraphic"
                flood-color="white"
                result="blur"
                flood-opacity="10"
              ></feDropShadow>
              <feColorMatrix
                mode="matrix"
                values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 55 -5"
                in="blur"
              ></feColorMatrix>
            </filter>
          </defs>
    
         <g class="filterOutline">
          <rect
            width="100%"
            height="100%"
            mask="url('#mask')"
            fill="red"
          />
           </g>
        </svg>
    
    </div>

    Maybe vectorize mask images provided by users?

    Applying a <mask> is usually more expensive than applying a <clip-path> since masks also support semi-transparent areas introducing a more complex alpha transparency rendering context.

    Provided all user uploads are "high-contrast" or perfect black and white images (… or will be converted to these) – you might even try to vectorize these files via something like potrace (available for pretty much any language – here’s just a javaScript example).

    Trace/vectorize by potrace

    Ideally you could convert the uploaded images server-side.

    Provided, all images are similarly simple and clean as the example png, this might be a valid alternative.

    The advantage of this approach – you’ll get a svg already including the desired transparency. So there’s no need to clip or mask anything.

    Besides you could apply regular fill and stroke properties – no need to emulate fills or strokes with a filter.

    enter image description here

    // draw png to canvas
    let canvas = document.createElement("canvas");
    canvasWrp.appendChild(canvas);
    
    let ctx = canvas.getContext("2d");
    let scale = 2;
    //let imgUrl = 'https://i.ibb.co/pL9J807/test.png';
    let imgUrl ="data:image/png;base64,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";
    
    var img = new Image();
    img.src = imgUrl;
    img.crossOrigin = "anonymous";
    
    img.onload = function() {
      let width = img.naturalWidth * scale;
      let height = img.naturalHeight * scale;
      canvas.width = width;
      canvas.height = height;
    
      // invert
      ctx.filter = 'invert(1)';
      ctx.drawImage(img, 0, 0, width, height);
      let dataUrl = canvas.toDataURL();
    
    
      /**
      * trace/vectorize
      * adjust parameters to get desired curve smoothing
      * See: https://github.com/kilobtye/potrace/blob/master/potrace.js#LC7
      */
      Potrace.setParameter({
        turnpolicy: 'minority',
        turdsize: 2,
        optcurve: true,
        alphamax: 1,
        opttolerance: 0.75
      });
    
      // load
      Potrace.loadImageFromUrl(dataUrl);
    
      let opt_type = 'curve';
    
      Potrace.process(() => {
        let svg = new DOMParser().parseFromString(Potrace.getSVG(1), 'image/svg+xml').querySelector('svg');
        let path = svg.querySelector('path');
        path.removeAttribute('fill');
        path.removeAttribute('fill-rule');
        path.removeAttribute('stroke');
        let [w, h] = [svg.width.baseVal.value, svg.height.baseVal.value];
        svg.setAttribute('viewBox', [0, 0, w, h].join(' '))
        svg.removeAttribute('id');
        svg.removeAttribute('version');
        svg.removeAttribute('width');
        svg.removeAttribute('height');
        svgdiv.appendChild(svg)
    
        // clone
        let svgClone = svg.cloneNode(true)
        svgdivAni.appendChild(svgClone)
    
      });
    
    };
    body {
      margin: 30px;
      background-color: #fff;
      background-image: url("data:image/svg+xml, %3Csvg viewBox='0 0 10 9' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M0 0 L10 0 L10 9' stroke-width='1' fill='none' stroke='%23eee' /%3E%3C/svg%3E");
      background-repeat: repeat;
      background-size: 20px;
    }
    
    article {
      display: flex;
      gap: 2em;
    }
    
    article>* {
      flex: 1;
      width: 100%;
      height: 100%;
    }
    
    .imgWrp {
      width: 50%;
    }
    
    canvas,
    svg {
      width: 100%
    }
    
    .svgdiv path:hover {
      fill: #000;
      stroke-width: 0.5%;
      stroke: transparent;
      marker-start: url(#markerStart);
      marker-mid: url(#markerRound);
    }
    
    svg {
      overflow: visible;
    }
    
    .svgdivAni path {
      fill: red;
      stroke: #000;
      stroke-width: 5;
      paint-order: fill;
      transition: 0.3s;
      transform-origin: center;
    }
    
    .svgdivAni:hover path {
      transform: rotate(180deg);
    }
    <article>
    
      <div class="imgWrp" id="canvasWrp"> </div>
      <div class="imgWrp svgdiv" id="svgdiv"> </div>
      <div class="imgWrp svgdivAni" id="svgdivAni"> </div>
    </article>
    
    
    <!-- just to illustrate the number of commands according to curretn smoothing parameters -->
    <svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
        <defs>
          <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
            markerWidth="10" markerHeight="10" orient="auto-start-reverse">
            <circle cx="5" cy="5" r="5" fill="green"></circle>
          </marker>
          <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
            markerWidth="10" markerHeight="10" orient="auto-start-reverse">
            <circle cx="5" cy="5" r="2.5" fill="red"></circle>
          </marker>
        </defs>
      </svg>
    
    
    <script src="https://cdn.jsdelivr.net/gh/kilobtye/potrace@master/potrace.js"></script>
    Login or Signup to reply.
  2. You can move the mask into the filter using feComposite/in and try to animate it there. Now it used to be that Safari didn’t support feComposite when it was applied via CSS (rather than an SVG attribute) – but hopefully that has changed.

    svg {
      width: 100px;
      height: 100px;
      filter: url("#outline");
      padding: 1px;
      transition: all 800ms;
    }
    
    svg:hover {
      transform: rotate(90deg);
    }
    
    .wrapper {
      position: absolute;
      padding: 10px;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: black;
    }
    <div class="wrapper">
       <svg
          filter="url(#outline)"
          width="100%"
          height="100%">
          <defs>
    
    
            <filter id="outline" color-interpolation-filters="sRGB">     
               <feImage
                x="0%"
                y="0%"
                width="100%"
                height="100%"
                xlink:href="https://i.ibb.co/pL9J807/test.png" />
              <feColorMatrix type="luminanceToAlpha" result="fil-mask"/>
                                                                               
              <feComposite operator="in" in="SourceGraphic" in2="fil-mask" result="inter"/>
              
              <feDropShadow
                dx="0"
                dy="0"
                stdDeviation="1.25"
                in="inter"
                flood-color="white"
                result="blur"
                flood-opacity="10" />
              
              <feColorMatrix
                mode="matrix"
                values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 55 -5"
                in="blur" />
              
            </filter>
          </defs>
    
          <rect
            width="100%"
            height="100%"
            fill="red"
          />
        </svg>
      
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search