Search code examples
svgsvg-filters

SVG filter relative to bounding box with possibly zero length side, calc() alternative?


This is somewhat of a continuation of Humongous height value for <filter> not preventing cutoff: I am still trying to apply a <filter>on a <path> but I am having problems with things being clipped.

The problem was solved in the other thread by moving the center of the filter canvas using the x/y attributes on the <filter>, still everything was in percent and therefore relative to the size of the thing you are trying to apply the effect on, but the problem is that a side length can be 0 even in cases where you see something, see e.g. the top line in the following examples:

.pathWrapper path {
  stroke: grey;
  fill: none;
  stroke-width: 1.5;
  marker-start: url(#circle);
  marker-end: url(#arrow);
}

.pathWrapper:hover {
  filter: url(#colorFilter);
}
<svg style="height:400px;width:100%;background-color:LightCyan">

<defs>
  <filter id="colorFilter" x="-300%" y="-300%" width="600%" height="600%">
    <feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"></feGaussianBlur>
    <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 3 0" result="lightenedBlur"></feColorMatrix>
    <feMerge>
      <feMergeNode in="lightenedBlur"></feMergeNode>
      <feMergeNode in="SourceGraphic"></feMergeNode>
    </feMerge>
  </filter>
  <marker id="arrow" viewBox="0 -5 10 10" refX="0" refY="0" markerWidth="8" markerHeight="8" orient="auto" style="fill: grey;">
    <path d="M0,-5L10,0L0,5"></path>
  </marker>
  <marker id="circle" viewBox="0 -4 8 8" refX="0" refY="0" markerWidth="8" markerHeight="8" orient="auto-start-reverse" style="fill: grey;"><circle r="4" cx="4"></circle></marker>
</defs>

<g transform="scale(2)">

  <g class="pathWrapper" transform="translate(70,20)">
    <path d="M52,10L45,10L-30,10L-37,10"></path>
  </g>
  
  <g class="pathWrapper" transform="translate(70,50)">
    <path d="M42,20L35,20L30,10L-30,10L-37,10"></path>
  </g>

  <g class="pathWrapper" transform="translate(200,20)">
    <path d="M42,140L35,140L-30,70L-30,10L-30,10L-37,10"></path>
  </g>

</g>

</svg>

Hover over the lines to see the filter effect applied there. You will see that the top line becomes invisible on hovering. This is because the bounding box height is 0, stroke width and marker knickknacks do not count:

height zero demo

I could use absolute units instead, e.g. like in the following example:

.pathWrapper path {
  stroke: grey;
  fill: none;
  stroke-width: 1.5;
  marker-start: url(#circle);
  marker-end: url(#arrow);
}

.pathWrapper:hover {
  filter: url(#colorFilter);
}
<svg style="height:400px;width:100%;background-color:LightCyan">

<defs>
  <filter id="colorFilter" filterUnits="userSpaceOnUse" x="-125" y="-125" width="250" height="250">
    <feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"></feGaussianBlur>
    <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 3 0" result="lightenedBlur"></feColorMatrix>
    <feMerge>
      <feMergeNode in="lightenedBlur"></feMergeNode>
      <feMergeNode in="SourceGraphic"></feMergeNode>
    </feMerge>
  </filter>
  <marker id="arrow" viewBox="0 -5 10 10" refX="0" refY="0" markerWidth="8" markerHeight="8" orient="auto" style="fill: grey;">
    <path d="M0,-5L10,0L0,5"></path>
  </marker>
  <marker id="circle" viewBox="0 -4 8 8" refX="0" refY="0" markerWidth="8" markerHeight="8" orient="auto-start-reverse" style="fill: grey;"><circle r="4" cx="4"></circle></marker>
</defs>

<g transform="scale(2)">

  <g class="pathWrapper" transform="translate(70,20)">
    <path d="M52,10L45,10L-30,10L-37,10"></path>
  </g>
  
  <g class="pathWrapper" transform="translate(70,50)">
    <path d="M42,20L35,20L30,10L-30,10L-37,10"></path>
  </g>

  <g class="pathWrapper" transform="translate(200,20)">
    <path d="M42,140L35,140L-30,70L-30,10L-30,10L-37,10"></path>
  </g>

</g>

</svg>

The problem is that in my use case the sizes of the elements which have the effect applied to can vary quite a lot, so I would have to put an enormous value in there or otherwise there is always the change of it being not large enough (as shown in the example for the line spanning the largest height).

I found would that using CSS calc() could be a solution:

#colorFilter {
  width: calc(100% + 100);
  height: calc(100% + 100);
  x: calc(-50% - 50);
  y: calc(-50% - 50);
}

.pathWrapper path {
  stroke: grey;
  fill: none;
  stroke-width: 1.5;
  marker-start: url(#circle);
  marker-end: url(#arrow);
}

.pathWrapper:hover {
  filter: url(#colorFilter);
}
<svg style="height:400px;width:100%;background-color:LightCyan">

<defs>
  <filter id="colorFilter" filterUnits="userSpaceOnUse">
    <feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"></feGaussianBlur>
    <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 3 0" result="lightenedBlur"></feColorMatrix>
    <feMerge>
      <feMergeNode in="lightenedBlur"></feMergeNode>
      <feMergeNode in="SourceGraphic"></feMergeNode>
    </feMerge>
  </filter>
  <marker id="arrow" viewBox="0 -5 10 10" refX="0" refY="0" markerWidth="8" markerHeight="8" orient="auto" style="fill: grey;">
    <path d="M0,-5L10,0L0,5"></path>
  </marker>
  <marker id="circle" viewBox="0 -4 8 8" refX="0" refY="0" markerWidth="8" markerHeight="8" orient="auto-start-reverse" style="fill: grey;"><circle r="4" cx="4"></circle></marker>
</defs>

<g transform="scale(2)">

  <g class="pathWrapper" transform="translate(70,20)">
    <path d="M52,10L45,10L-30,10L-37,10"></path>
  </g>
  
  <g class="pathWrapper" transform="translate(70,50)">
    <path d="M42,20L35,20L30,10L-30,10L-37,10"></path>
  </g>

  <g class="pathWrapper" transform="translate(200,20)">
    <path d="M42,140L35,140L-30,70L-30,10L-30,10L-37,10"></path>
  </g>

</g>

</svg>

This works in current versions of Google Chrome and Mozilla Firefox but does not appear to work in Micosoft Edge or IE11 (and from a bit of searching it seems as if calc() support is already quite touchy even for HTML content, see the Known issues section at https://caniuse.com/#search=calc).

So does an better alternative to the calc() approach exist?

(Maybe it's worth noting that I'm working with dynamically d3.js generated content.)


Solution

  • As you are working with generated content in d3, the best approach would be to insert an invisible rect into g.pathWrapper that covers the needed filter area.

    I'm winging it here, you may have to adapt this to your logic (and maybe exchange the ES6 constructs for their ES5 equivalents). Suppose you have a list of points you are generating your path data from:

    var points = [[52,10], [45,10], [-30,10], [-37,10]];
    
    // get the min/max values
    var x1 = d3.min(points,(p) => p[0]),
        x2 = d3.max(points,(p) => p[0]),
        y1 = d3.min(points,(p) => p[1]),
        y2 = d3.max(points,(p) => p[1]);
    
    // construct the path
    var path = d3.path();
    path.moveto(...points[0]);
    for (var point of points.slice(1)) {
         path.lineto(...point);
    }
    
    // wrapper
    var wrapper = d3.select('svg > g').append('g')
        .classed('pathWrapper');
    
    // invisible rect with +5px in each direction as filter region
    wrapper.append('rect')
        .attr('fill', 'none')
        .attr('x', x1 - 5)
        .attr('y', y1 - 5)
        .attr('width', x2 - x1 + 10)
        .attr('height', y2 - y1 + 10);
    
    // and the path itself
    wrapper.append('path')
       .attr('d', path);
    

    After that, the defaults for the filter effects region should work as-is.