Dady
Dady

Reputation: 113

Simplify any SVG path to create a solid black shape (UPDATED)

Is it possible to transform any SVG into a solid black shape in an automated way (in javascript or otherwise) by simplifying the path?

Is there a library to do this, or is there another way?

Let's take this SVG as an example:

enter image description here

<svg height="512" viewBox="0 0 16.933 16.933" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757"/><path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c"/><path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#ffea54"/><path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b"/></svg>

Here's what I'd like to achieve:

enter image description here

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362.76 480.62"><g ><path d="m307.02,320.09C411.21,221.17,369.32,3.28,181.41,0-6.38,3.3-48.38,220.77,55.44,319.82l-47.2,112.57c-1.7,4.08.23,8.78,4.31,10.48.97,4.42,59.38-22.24,62.03-22.45l22.13,55.18c2.69,6.65,12.03,6.71,14.82.12l45.88-109.4c7.71.95,15.69,1.52,23.97,1.67,8.29-.14,16.28-.72,23.99-1.67,9.72,23.16,45.91,109.4,45.91,109.4,1.74,4.07,6.45,5.96,10.52,4.22,3.97,2.43,24.7-57.52,26.4-59.52l56,22.47c6.34,2.72,13.15-4.25,10.34-10.52-2.54-1.46-31.18-81.18-47.53-112.26Z"/></g></svg>

UPDATE

I don't think I was specific enough in my request.

My aim is for the shapes to really merge into the path.

The aim is to be able to "stickerize" (a white outline around the main shape) any SVG shape afterwards.

And the first step in achieving this is to be able to merge the different paths of an SVP to create a kind of mask.

enter image description here

Upvotes: 0

Views: 503

Answers (5)

herrstrietzel
herrstrietzel

Reputation: 17195

For simple svg you could use paper.js boolean operator unite as explained here ("Merging two bezier-based shapes into one to create a new outline").

Another approach would be re-tracing the image via potrace.

Sounds pretty "brute-force" but solves quite a lot of problems:

  • strokes and offsets: approximate offset paths is quite complex - there is no exact calculation
  • shapes or primitives like <rect>, <circle> etc. usually need to be converted to <path> when calculating merged paths from pathData
  • transforms like scale, translate also complicate a computational approach.

See also working example on codepen.

/**
 * svg to canvas
 */
async function svg2PngDataUrl(el, strokeWidth, strokeLineJoin, strokeLineCap) {
  /**
   * clone svg to add width and height
   * for better compatibility
   * without affecting the original svg
   */
  let svgClone = el.cloneNode(true);

  // add/expand stroke
  document.body.append(svgClone)
  addStrokeAndBlackFill(svgClone, strokeWidth, strokeLineJoin, strokeLineCap)

  // get dimensions
  let {
    width,
    height
  } = el.getBBox();
  let w = el.viewBox.baseVal.width ?
    svgClone.viewBox.baseVal.width :
    el.width.baseVal.value ?
    el.width.baseVal.value :
    width;
  let h = el.viewBox.baseVal.height ?
    svgClone.viewBox.baseVal.height :
    el.height.baseVal.value ?
    el.height.baseVal.value :
    height;


  // autoscale for better tracing results
  let sidelength = Math.min(w, h)
  let minWidth = 1000
  let scale = minWidth / sidelength > 1 ? minWidth / sidelength : 1;


  // apply scaling
  [w, h] = [w * scale, h * scale];
  // add width and height for firefox compatibility
  svgClone.setAttribute("width", w);
  svgClone.setAttribute("height", h);

  // create canvas
  let canvas = document.createElement("canvas");
  canvas.width = w;
  canvas.height = h;

  // create blob and object URL
  let svgString = new XMLSerializer().serializeToString(svgClone);
  let blob = new Blob([svgString], {
    type: "image/svg+xml"
  });
  let blobURL = URL.createObjectURL(blob);
  let tmpImg = new Image();
  tmpImg.src = blobURL;
  tmpImg.width = w;
  tmpImg.height = h;
  tmpImg.crossOrigin = "anonymous";
  await tmpImg.decode();
  let ctx = canvas.getContext("2d");
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, w, h);

  // apply filter to enhance contrast
  ctx.filter = "brightness(0%)";
  ctx.drawImage(tmpImg, 0, 0, w, h);

  //create img data URL
  let dataUrl = canvas.toDataURL();

  // remove clone
  svgClone.remove()
  return dataUrl;
}



function addStrokeAndBlackFill(svg, strokeWidthNew = 1, strokeLineJoin = 'round', strokeLineCap = "round") {
  let els = svg.querySelectorAll('path, circle, rect, polygon, line, polyline, text')

  els.forEach(el => {
    let strokeWidthEl = strokeWidthNew
    let css = window.getComputedStyle(el)
    let stroke = css.getPropertyValue('stroke')
    let strokeWidth = css.getPropertyValue('stroke-width')
    if (stroke !== 'none') {
      strokeWidthEl += parseFloat(strokeWidth)
    }

    el.style.fill = '#000'
    el.style.stroke = '#000'
    el.style.strokeWidth = strokeWidthEl + 'px'
    el.style.strokeLinejoin = strokeLineJoin
    el.style.strokeLinecap = strokeLineCap
  })
}
<script src="https://cdn.jsdelivr.net/gh/kilobtye/potrace@master/potrace.js"></script>

<svg viewBox="0 0 16.933 16.933" xmlns="http://www.w3.org/2000/svg" overflow="visible">
  <path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757" />
  <path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c" />
  <path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#fff" />
  <path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b" />

</svg>

<script>
  let svg = document.querySelector("svg");
  let strokeWidth = 0.75;
  let strokeLineJoin = 'round';
  let strokeLineCap = 'round';
  window.addEventListener('DOMContentLoaded', async(e) => {
    let width = svg.viewBox.baseVal.width ? svg.viewBox.baseVal.width : svg.width.baseVal.value;
    let dataUrl = await svg2PngDataUrl(svg, strokeWidth, strokeLineJoin, strokeLineCap);
    let tracingOptions = {
      turnpolicy: "majority",
      turdsize: 1,
      optcurve: true,
      alphamax: 1,
      opttolerance: 1
    };
    /**
     * trace img from data URl
     */
    let options = {
      turnpolicy: "minority",
      turdsize: 2,
      optcurve: true,
      alphamax: 1,
      opttolerance: 0.75,
    };
    // set parameters
    Potrace.setParameter(options);
    Potrace.loadImageFromUrl(dataUrl);
    Potrace.process(function() {
      // scale back
      let {
        renderedWidth,
        renderedHeight
      } = {
        renderedWidth: Potrace.img.width,
        renderedHeight: Potrace.img.height
      }
      scale = renderedWidth ? renderedWidth / width : 1
      
      // get path and prepend it
      let tracedSVG = Potrace.getSVG(1 / scale);
      let tracedPath = new DOMParser().parseFromString(tracedSVG, 'text/html').querySelector('path')
      tracedPath.removeAttribute('fill-rule')
      tracedPath.setAttribute('stroke', '#000')
      tracedPath.setAttribute('stroke-width', '0.1')
      tracedPath.setAttribute('fill', '#fff')
      svg.insertBefore(tracedPath, svg.children[0])
    });

  })
</script>

How it works

  1. we draw the svg to a <canvas> (svg2PngDataUrl()). This helper also checks the svg size. If it's below 1000x1000 units we draw scaled version on canvas to get a better tracing accuracy.
  2. we enhance contrast via brightness filter to get a black/white bitmap also rendering bright colors as black
  3. we trace the bitmap via potrace
  4. scale the image back to it't initial dimensions when retrieving the svg Potrace.getSVG(1 * scale)

The above function won't adjust the canvas size to fit larger stroke-widths. As long as your viewBox is large enough it should work fine.

Upvotes: 0

chrwahl
chrwahl

Reputation: 13090

A filter could be used in that case. The <feMorphology> element will create a "border" around the shape. And then it can be combined with a drop shadow.

<svg height="300" viewBox="0 0 18.933 18.933" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="f1">
      <feFlood flood-color="white" result="white" />
      <feMorphology in="SourceAlpha" operator="dilate" radius=".4" result="border" />
      <feComposite in="white" in2="border" operator="in" result="whiteborder" />
      <feDropShadow dx=".1" dy=".1" stdDeviation=".2" result="shadow" />
      <feMerge>
        <feMergeNode in="whiteborder" />
        <feMergeNode in="shadow" />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
  </defs>
  <g filter="url(#f1)" transform="translate(1 1)">
    <path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757"/>
    <path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c"/>
    <path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#ffea54"/>
    <path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b"/>
  </g>
</svg>

Upvotes: 0

chrwahl
chrwahl

Reputation: 13090

SVG is an XML document, so you can make a XSLTProcessor that transforms the entire SVG. I know that it is still four different paths. Well, you could also concatenate them into one path using XSL, but the path would still have the same outline as the original four.

In the example there is a special XSL template that takes the fill attribute and gives it the value "black".

const parser = new DOMParser();
const xsltProcessor = new XSLTProcessor();

const xslText = `<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns="http://www.w3.org/2000/svg"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes" />
  <xsl:template match="*">
    <xsl:element name="{name()}">
      <xsl:apply-templates select="attribute::*" />
      <xsl:apply-templates select="*" />
    </xsl:element>
  </xsl:template>
  <xsl:template match="attribute::fill">
    <!--if a fill attribute, make the value "black"-->
    <xsl:attribute name="{name()}">
      <xsl:value-of select="'black'"/>
    </xsl:attribute>
  </xsl:template>
  <xsl:template match="attribute::*">
    <xsl:attribute name="{name()}">
      <xsl:value-of select="."/>
    </xsl:attribute>
  </xsl:template>
</xsl:stylesheet>`;
const xslStylesheet = parser.parseFromString(xslText, "application/xml");
xsltProcessor.importStylesheet(xslStylesheet);

const svgElm = document.querySelector('svg');
const xmlDoc = parser.parseFromString(svgElm.outerHTML, 'image/svg+xml');

const newXmlDoc = xsltProcessor.transformToDocument(xmlDoc, document);
document.body.innerHTML += newXmlDoc.documentElement.outerHTML;
<svg viewBox="0 0 16.933 16.933" width="100" xmlns="http://www.w3.org/2000/svg">
  <path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757"/>
  <path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c"/>
  <path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#ffea54"/>
  <path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b"/>
</svg>

Upvotes: 0

chrwahl
chrwahl

Reputation: 13090

I don't know if you find this too simple a solution, but you could use the !important keyword to make all fills black.

svg *  {
  fill: black !important;
}
<svg height="512" viewBox="0 0 16.933 16.933" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757"/><path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c"/><path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#ffea54"/><path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b"/></svg>

Upvotes: 0

lusc
lusc

Reputation: 1406

If it is enough for the SVG to be rendered black, you can use clip-path with a combination of CSS and JavaScript.

The core approach here is to wrap the SVG paths in <clipPath> and use CSS's clip-path to then change the shape of a <div> to be the shape of the SVG.

The snippets assume that your SVG is a string. But if your SVG is already in the html, you can use querySelector instead to select the <svg> element.

This also assumes that there is only one SVG that this applies to. If that is untrue, you have to use clipNodePath.id = randomId for each SVG and use element.style.clipPath = url("#`${randomId}`"); instead. That approach can be found in the collapsed snippet.

const rawSvg = '<svg height="512" viewBox="0 0 16.933 16.933" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757"/><path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c"/><path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#ffea54"/><path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b"/></svg>';

const parsed = new DOMParser().parseFromString(rawSvg, 'image/svg+xml');
const svgNode = parsed.rootElement;

// Or if applicable
// const svgNode = document.querySelector('#some-id');

const clipPathNode = parsed.createElementNS("http://www.w3.org/2000/svg", "clipPath");
clipPathNode.id = 'abc';
clipPathNode.append(...svgNode.children);
svgNode.append(clipPathNode);
document.body.append(svgNode);
div {
  clip-path: url("#abc");
  background: black;
  width: 5em;
  height: 5em;
}

body {
  display: flex;
  align-items: start;
  justify-content: start;
}
<div></div>

Or with random IDs if there are multiple black SVGs per page:

const rawSvg = '<svg height="512" viewBox="0 0 16.933 16.933" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M12.289 10.724a.265.265 0 0 0-.168.066 5.555 5.555 0 0 1-2.74 1.303.265.265 0 0 0-.2.363l1.597 3.806a.265.265 0 0 0 .489-.004l.732-1.825 1.852.743a.265.265 0 0 0 .342-.348l-1.653-3.942a.265.265 0 0 0-.251-.162zm-7.897.165-1.652 3.94a.265.265 0 0 0 .343.348l1.851-.744.732 1.825c.089.22.398.222.49.004l1.598-3.81a.265.265 0 0 0-.2-.363 5.556 5.556 0 0 1-2.743-1.297.265.265 0 0 0-.419.097z" fill="#ff5757"/><path d="M8.467.529C5.109.529 2.38 3.257 2.38 6.615s2.728 6.084 6.086 6.084 6.086-2.726 6.086-6.084S11.825.529 8.467.529z" fill="#ffcb3c"/><path d="M8.467 1.851a4.767 4.767 0 0 0-4.762 4.764c0 2.627 2.135 4.762 4.762 4.762s4.762-2.135 4.762-4.762A4.767 4.767 0 0 0 8.467 1.85z" fill="#ffea54"/><path d="M8.465 3.576a.265.265 0 0 0-.229.172l-.7 1.857-1.987.06a.265.265 0 0 0-.156.471L6.94 7.38l-.554 1.906a.265.265 0 0 0 .4.295l1.658-1.09 1.643 1.117a.265.265 0 0 0 .404-.289L9.97 7.402l1.568-1.215a.265.265 0 0 0-.148-.475L9.404 5.62l-.672-1.87a.265.265 0 0 0-.267-.175z" fill="#feaa2b"/></svg>';

const parsed = new DOMParser().parseFromString(rawSvg, 'image/svg+xml');
const svgNode = parsed.rootElement;

const randomId = 'abc'; // replace with random id generator

const clipPathNode = parsed.createElementNS("http://www.w3.org/2000/svg", "clipPath");
clipPathNode.id = randomId;
clipPathNode.append(...svgNode.children);
svgNode.append(clipPathNode);
document.body.append(svgNode);

const targetNode = document.createElement('div');
targetNode.className = 'black-shape';
targetNode.style.clipPath = `url("#${randomId}"`;
document.body.append(targetNode);
.black-shape {
  background: black;
  width: 5em;
  height: 5em;
}

body {
  display: flex;
  align-items: start;
  justify-content: start;
}

Upvotes: 1

Related Questions