Reputation: 113
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:
<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:
<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.
Upvotes: 0
Views: 503
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:
<rect>
, <circle>
etc. usually need to be converted to <path>
when calculating merged paths from pathDataSee 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>
<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.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
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
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
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
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