1

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!

kaktus
  • 169
  • 1
  • 1
  • 11
  • Don't use a filter and see what happens to performance. – Michael Mullany Apr 06 '23 at 19:41
  • As I wrote in my question the usage of the filter does not cause the performance issues. The weird behaviour happens even if the filter is not applied and are caused by the mask. – kaktus Apr 07 '23 at 14:14

2 Answers2

0

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>
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
  • Thank you for your answer! The performance issue happens independently of the filter but are caused by the mask. Even when the filter is not used, the elements transform in a weird way, which is why I am looking for alternatives to SVG masking. – – kaktus Apr 08 '23 at 10:30
  • @kaktus: I've added a potrace tracing example. The main advantage: you'll get proper svg transparency - not need for any masking or filtering – herrstrietzel Apr 08 '23 at 17:34
0

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>
Michael Mullany
  • 30,283
  • 6
  • 81
  • 105