Search code examples
csssvgcss-transforms

Multiple overlapping svg paths create unwanted effect


I have created a tree-like structure of cards with lines between parents and children. The lines are svg paths like so:

const newpath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        newpath.setAttributeNS(
            null,
            'd',
            `M${parentX} ${parentY} h10 a${arcRadius},${arcRadius} 0 0 1 ${arcRadius},${arcRadius} v ${
                childY - arcRadius * 2
            } a${arcRadius},${arcRadius} 0 0 0 ${arcRadius},${arcRadius} h12`
        );
        newpath.setAttributeNS(null, 'stroke', 'black');
        newpath.setAttributeNS(null, 'stroke-width', '1');
        newpath.setAttributeNS(null, 'opacity', '1');
        newpath.setAttributeNS(null, 'fill', 'none');

Now this means that for 1 parent with 3 children, 3 paths are created. All 3 overlaps to the first child, 2 paths overlaps to the second child and finally there's 1 path to the third child. The whole tree is zoomable by adjusting the transform: scale() property. Now when I zoom out the lines are darker where they overlap. Is there any way to get rid of this behaviour except drawing the lines only from child to child?

A similiar question has been asked before here: Why do SVG lines/ paths on top of each other create a different stroke? but the accepted solution does not work for me.

enter image description here

enter image description here


Solution

  • Unfortunately, there is no perfect solution.

    A workaround providing a decent balance between crispness and smooth edges might be to apply a svg feComponentTransfer filter to enhance the contrast of the alpha channel.

    svg{
      border: 1px solid #ccc;
      width: 70px;
    }
    
    path{
      stroke-width:1px;
      fill:none;
    }
    
    .crisp path{
      shape-rendering: crispEdges
    }
    
    .contrast
    path{
       filter:url(#enhanceContrast);
    }
    <h3>Unedited</h3>
    
    <svg  xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" >
    <path  stroke="#000"  d="M46.694,63.025c-6.627,0-12-5.373-12-12V16.248"/>
    <path  stroke="#000"  d="M46.694,76.536c-6.627,0-12-5.373-12-12V29.758"/>
    <path  stroke="#000"  d="M46.694,89.753c-6.627,0-12-5.373-12-12V42.975"/>
    </svg>
    
    <h3>shape-rendering: crispedges</h3>
    
    <svg class="crisp" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" >
    <path  stroke="#000"  d="M46.694,63.025c-6.627,0-12-5.373-12-12V16.248"/>
    <path  stroke="#000"  d="M46.694,76.536c-6.627,0-12-5.373-12-12V29.758"/>
    <path  stroke="#000"  d="M46.694,89.753c-6.627,0-12-5.373-12-12V42.975"/>
    </svg>
    
    <h3>Integer coordinates</h3>
    
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
    <path  stroke="#000" d="M 47 63
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 77
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 90
    c -7 0 -12 -5 -12 -12
    v -35"/>
    </svg>
    
    <h3>Duplicate strokes</h3>
    
    <svg  xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
    <path  stroke="#000" d="M 47 63
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 77
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 90
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 63
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 77
    c -7 0 -12 -5 -12 -12
    v -35"/>
    <path  stroke="#000" d="M 47 90
    c -7 0 -12 -5 -12 -12
    v -35"/>
        
    </svg>
    
    <h3>Enhance contrast (svg filter)</h3>
    
    <svg class="contrast" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" >
    <path  stroke="#000"  d="M46.694,63.025c-6.627,0-12-5.373-12-12V16.248"/>
    <path  stroke="#000"  d="M46.694,76.536c-6.627,0-12-5.373-12-12V29.758"/>
    <path  stroke="#000"  d="M46.694,89.753c-6.627,0-12-5.373-12-12V42.975"/>
    </svg>
    
    
    
    <svg xmlns="http://www.w3.org/2000/svg" style="position: fixed; width:0; height:0; overflow:hidden;">
        <filter id="enhanceContrast" x="0" y="0" width="100%" height="100%">
          <feComponentTransfer>
            <feFuncA type="gamma" amplitude="1" exponent="1" offset="0"></feFuncA>        
          </feComponentTransfer>
          </filter>
    </svg>

    The above example compares different workarounds:

    • shape-rendering: crispEdges – perfect for straight lines but produces jagged edges on curves
    • Integer coordinates: rounding path command values to integers can often mitigate the undesired effect of differing stroke widths
    • Duplicate strokes: another approach is to duplicate paths – this will prevent "lighter" renderings but also results in a visually thicker stroke width
    • enhance alpha contrast via svg feComponentTransfer filter: we're basically reducing the number of semi-transparent pixels.