7

I want to convert an svg as an icon font element in my HTML/CSS using icomoon app

<svg width="325" height="350" viewBox="0 0 325 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M205.43 59.38C205.43 26.5853 232.015 0 264.81 0C297.605 0 324.19 26.5853 324.19 59.38C324.19 92.1747 297.605 118.76 264.81 118.76C246.525 118.76 230.17 110.495 219.277 97.4975L116.536 157.274C117.985 162.41 118.76 167.83 118.76 173.43C118.76 178.348 118.162 183.127 117.035 187.697L222.002 248.758C232.806 237.522 247.991 230.53 264.81 230.53C297.605 230.53 324.19 257.115 324.19 289.91C324.19 322.705 297.605 349.29 264.81 349.29C232.015 349.29 205.43 322.705 205.43 289.91C205.43 280.791 207.486 272.152 211.159 264.431L109.497 205.292C98.9566 221.836 80.4498 232.81 59.38 232.81C26.5853 232.81 0 206.225 0 173.43C0 140.635 26.5853 114.05 59.38 114.05C79.7735 114.05 97.7659 124.331 108.457 139.992L209.556 81.1716C206.893 74.4248 205.43 67.0733 205.43 59.38ZM264.81 19C242.509 19 224.43 37.0787 224.43 59.38C224.43 81.6813 242.509 99.76 264.81 99.76C287.111 99.76 305.19 81.6813 305.19 59.38C305.19 37.0787 287.111 19 264.81 19ZM59.38 133.05C37.0787 133.05 19 151.129 19 173.43C19 195.731 37.0787 213.81 59.38 213.81C81.6813 213.81 99.76 195.731 99.76 173.43C99.76 151.129 81.6813 133.05 59.38 133.05ZM224.43 289.91C224.43 267.609 242.509 249.53 264.81 249.53C287.111 249.53 305.19 267.609 305.19 289.91C305.19 312.211 287.111 330.29 264.81 330.29C242.509 330.29 224.43 312.211 224.43 289.91Z" fill="#0D0F13"/>
</svg>

But every time I generate the icon font file one of the three circles is filled black in icomoon preview and when used in HTML:

<span class="icon icon-share">

enter image description here

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
nart yam
  • 81
  • 4

1 Answers1

9

This rendering issue is caused by non ideal path directions.

path directions

You icon is a compound path – it contains multiple subpaths (the cut out "holes").

Usually, you define which shapes should be interpreted as solid or cut-out by switching path directions:
E.g. if the outer shape's path commands have a clockwise path direction – the inner ones should use a counter-clockwise direction.

It doesn't matter if you're using clockwise vs. counter-clockwise or the other way around. Inner (cut out) paths just have to use the opposite direction.
Also explained in this article: The Winding Order of the Fill Rule

Svg: fill-rule="evenodd

The original svg file renders fine since it has a fill-rule="evenodd" applied to the path element.

Apparently, icomoon font generator expects your icons to have the aforementioned alternating directions.
In other words it can't use fill-rules when converting svg to font files.

Fix path directions

Here is a helper script to fix inner paths.

const svg = document.querySelector('svg');
const path = svg.querySelector('path');

function fixPath(path) {
  // get pathData array
  let pathData = path.getPathData({
    normalize: true
  });
  // split sub paths
  let pathDataSubArr = splitSubpaths(pathData);
  let fixedPathData = fixInnerPathDirections(path, pathDataSubArr);
  path.setPathData(fixedPathData);
}


/**
 * helpers
 */
function fixInnerPathDirections(path, pathDataSubArr) {
  let svg = path.closest('svg');
  let fixedPathData = [];
  let subPathEls = [];
  let bbO = path.getBBox();
  let [xO, yO, wO, hO, rO, bO] = [bbO.x, bbO.y, bbO.width, bbO.height, (bbO.x + bbO.width), (bbO.y + bbO
    .height)];
  let outerPathData = pathDataSubArr[0];
  let outerPathTmp = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  outerPathTmp.setPathData(outerPathData);
  outerPathTmp.classList.add('outer');
  let outerClockwise = isClockwise(outerPathTmp)

  pathDataSubArr.forEach(function(pathDataSub, i) {
    // create temporary subpath elements for checking positions
    let subPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    subPath.setPathData(pathDataSub);
    subPath.setAttribute('stroke', 'red');
    subPath.setAttribute('stroke-width', '1');
    subPath.setAttribute('fill', 'none');
    svg.appendChild(subPath);
    subPathEls.push(subPath);
    let bb = subPath.getBBox();
    let [x, y, w, h, r, b] = [bb.x, bb.y, bb.width, bb.height, (bb.x + bb.width), (bb.y + bb
      .height)];
    // remove temporary subpaths
    subPath.remove();

    if (i > 0) {
      // is subpath within outer path
      if (x > xO && y > yO && r < rO && b < bO) {
        let isClockwiseInner = isClockwise(subPath);
        // if subpath has same direction as outer path: reverse direction
        if (isClockwiseInner == outerClockwise) {
          pathDataSub = reversePathData(pathDataSub);
        }
      }
    }
    fixedPathData = fixedPathData.concat(pathDataSub);
  })
  return fixedPathData;
}


function reversePathData(pathData) {
  let M = pathData[0];
  let newPathData = [M];
  // split subpaths
  let subPathDataArr = splitSubpaths(pathData);
  subPathDataArr.forEach(function(subPathData, s) {
    let subPathDataL = subPathData.length;
    let closed = subPathData[subPathDataL - 1]['type'] == 'Z' ? true : false;
    let subM = subPathData[0]['values'];

    // insert Lineto if last path segment has created by z
    let lastCom = closed ? subPathData[subPathDataL - 2] : subPathData[subPathDataL - 1];
    let lastComL = lastCom['values'].length;
    let lastXY = [lastCom['values'][lastComL - 2], lastCom['values'][lastComL - 1]];
    let diff = Math.abs(subM[0] - lastXY[0]);
    if (diff > 1 && closed) {
      subPathData.pop();
      subPathData.push({
        'type': 'L',
        'values': [subM[0], subM[1]]
      });
      subPathData.push({
        'type': 'Z',
        'values': []
      });
    }
    subPathData.forEach(function(com, i) {
      // reverse index
      let subpathDataL = subPathData.length;
      let indexR = subpathDataL - 1 - i;
      let comR = subPathData[indexR];
      let comF = subPathData[i];
      let [typeR, valuesR] = [comR['type'], comR['values']];
      let [typeF, valuesF] = [comF['type'], comF['values']];
      if (typeF == 'M' && s > 0) {
        newPathData.push(comF);
      } else if (typeR != 'M' && typeR != 'Z') {
        indexR--;
        let prevCom = i > 0 ? subPathData[indexR] : subPathData[subpathDataL - 1 - i];
        let prevVals = prevCom ? (prevCom['values'] ? prevCom['values'] : [0, 0]) : [];
        prevVals = prevCom['values'];
        let prevValsL = prevVals.length;
        let newCoords = [];

        if (typeR == 'C') {
          newCoords = [
            valuesR[2], valuesR[3],
            valuesR[0], valuesR[1],
            prevVals[prevValsL - 2], prevVals[prevValsL - 1]
          ];
          if (!closed) {
            let nextVals = i < subpathDataL - 1 ? subPathData[i + 1]['values'] : lastXY;
            let lastCX = (i < subpathDataL - 2) ? nextVals[prevValsL - 2] : subM[0];
            let lastCY = (i < subpathDataL - 2) ? nextVals[prevValsL - 1] : subM[1];
            newCoords[4] = lastCX;
            newCoords[5] = lastCY;
          }
        } else {
          newCoords = [prevVals[prevValsL - 2], prevVals[prevValsL - 1]];
        }
        newPathData.push({
          'type': typeR,
          'values': newCoords
        });
      }
    })
    //use last coordinates as M values if path isn't closed
    if (!closed) {
      newPathData[0]['values'] = lastXY;
    }
    if (closed) {
      newPathData.push({
        'type': 'Z',
        'values': []
      });
    }
  });
  return newPathData;
}

/**
 * check path direction of polygon
 * based on answer:
 * @mpen: https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order/1180256#answer-46613578
 */
function isClockwise(path, divisions = 50) {
  var length = path.getTotalLength();
  let p1 = path.getPointAtLength(0);
  let A = [p1.x, p1.y];
  let p2 = path.getPointAtLength(length * 0.25);
  let B = [p2.x, p2.y];
  let p3 = path.getPointAtLength(length * 0.75);
  let C = [p3.x, p3.y];
  let poly = [A, B, C];
  let end = poly.length - 1;
  let sum = poly[end][0] * poly[0][1] - poly[0][0] * poly[end][1];
  for (let i = 0; i < end; ++i) {
    const n = i + 1;
    sum += poly[i][0] * poly[n][1] - poly[n][0] * poly[i][1];
  }
  let cw = sum > 0 ? true : false;
  return cw;
}


function splitSubpaths(pathData) {
  let pathDataL = pathData.length;
  let subPathArr = [];
  let subPathMindex = [];
  pathData.forEach(function(com, i) {
    let [type, values] = [com['type'], com['values']];
    if (type == 'M') {
      subPathMindex.push(i);
    }
  });
  //split segments after M command
  subPathMindex.forEach(function(index, i) {
    let n = subPathMindex[i + 1];
    let thisSeg = pathData.slice(index, n);
    subPathArr.push(thisSeg)
  })
  return subPathArr;
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>


<p><button type="button" onclick="fixPath(path)">Fix path directions</button></p>
<svg width="325" height="350" viewBox="0 0 325 350" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="
        M205.43 59.38C205.43 26.5853 232.015 0 264.81 0C297.605 0 324.19 26.5853 324.19 59.38C324.19 92.1747 297.605 118.76 264.81 118.76C246.525 118.76 230.17 110.495 219.277 97.4975L116.536 157.274C117.985 162.41 118.76 167.83 118.76 173.43C118.76 178.348 118.162 183.127 117.035 187.697L222.002 248.758C232.806 237.522 247.991 230.53 264.81 230.53C297.605 230.53 324.19 257.115 324.19 289.91C324.19 322.705 297.605 349.29 264.81 349.29C232.015 349.29 205.43 322.705 205.43 289.91C205.43 280.791 207.486 272.152 211.159 264.431L109.497 205.292C98.9566 221.836 80.4498 232.81 59.38 232.81C26.5853 232.81 0 206.225 0 173.43C0 140.635 26.5853 114.05 59.38 114.05C79.7735 114.05 97.7659 124.331 108.457 139.992L209.556 81.1716C206.893 74.4248 205.43 67.0733 205.43 59.38Z
        
        M264.81 19C242.509 19 224.43 37.0787 224.43 59.38C224.43 81.6813 242.509 99.76 264.81 99.76C287.111 99.76 305.19 81.6813 305.19 59.38C305.19 37.0787 287.111 19 264.81 19Z
        
        M59.38 133.05C37.0787 133.05 19 151.129 19 173.43C19 195.731 37.0787 213.81 59.38 213.81C81.6813 213.81 99.76 195.731 99.76 173.43C99.76 151.129 81.6813 133.05 59.38 133.05Z
        
        
        M224.43 289.91C224.43 267.609 242.509 249.53 264.81 249.53C287.111 249.53 305.19 267.609 305.19 289.91C305.19 312.211 287.111 330.29 264.81 330.29C242.509 330.29 224.43 312.211 224.43 289.91Z
        
        " fill="#0D0F13" />
    </svg>

(Inspect the element in your dev tools to get the new path)

How it works

  1. path commands are parsed (using path-data-polyfill by Jarek Foksa)
    1.2 the path data is retrieved in a normalized way (path.getPathData({normalize: true}):
    all commands will be converted to a reduced set using only absolute M, L, C, Z commands.
  2. split up path data into sub paths (each sub path starts with a new M command)
  3. check if sub paths are intersecting with the outer shape
  4. check directions between outer and inner sub paths (using the isClockwise(path) helper)
  5. reverse sub path direction if needed (reversePathData(pathdata))
  6. concatenate sub paths and overwrite the original d property

Codepen example

You might also use this codepen helper: Fix svg compound path direction
This script will keep the original command types – so A, Q commands won't be converted to cubic béziers.

HTML/CSS example

@font-face {
    font-family: 'icomoon';
    src: url('data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAKsAA0AAAAABpgAAAJWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACCXhEICoIwgicLDgABNgIkAxgEIAWDGwdFG5gFyJ4FdpMzjFBuZhHDYhE2Lyuvefjx8LVWvt89OwuoLkDsUKWigS2BIwnAKmwRyqeiD1z8VXT0WrS2WSR7w9M/YpFmHip+uESqS2jQkv/fzbMtejpWgbRBCusLh5mNJZxQLwq8+J+4Gbcs0ayoyySyNm4KAw7HxADTzsMwkLHPpyWQZQUKD0Zv7Xy3GOQpg6RIkmGRRZTNaBEChl5xBP7+oo23DrQwAJIQKkKBhJyGYq1CepJu3KLi+GdF1DGd6Klcj0blY/2GbQYFAcSpCPnkiQjsIBSYSGQ1aKhJkjB1VP8HxD9n+u8YsSw0AIRMlEQGIiEBAIICrg7RJwElaKGKPeAyoAAEQihyzqva7VbQSyfP/JuzIacHG9Ph+enDBacfK4dHbovJUxU8A6u/WJQ7Xqm9+nF6qDkbiclwo2VkBgb1ANjxPsxY2H3AZ4bRsplS0Zw5z18lt1pWnMqpZVxycbNKnuXD1oYHbk6mZGvfmfmdMLq4AgJQAH3+SAIPQNz+esn3KSVZ/Z/wwqyM0of8OfegJoEw3uUoQP4sfyGUBSC2AvhN/opAaTcRSAAA6HgjQKjqIZBUzUUgq1qPQKFqBwIVTacQKFXdF6BhvJcsirrQIQIJGQtkZClQIBuxigFprNRxvmfDSncH5TTVNTU14OCqn9JsAJswRX01GR14mZYGrQlL5G2Yjq6yW8/JxgFnVkarTN86GzQ19OgVMQ1MR0YPk6eXNaYXISRh2uCeHNrBzTrX/NNhDoTEti+JJEWWoyj3Hxm3Sk58/FjZQZy3Ai5jAAAA') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: swap;
}

.icon{
  font-family: 'icomoon';
  font-size:5vw
}

.icon-share:before {
  content: "\e900";
}
<span class="icon icon-share">

Alternative: paper.js:

If your app already employs paper.js you can also use its reorient() method to fix path directions. See related post "Paper.js reorient: SVG subpaths are lost"

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
  • Excellent reply and thanks for the edit on the question as well. – Kyll Mar 30 '23 at 10:28
  • 1
    My pleasure! Glad my obsession with sub path directions was helpful =) BTW: there's also a way to fix directions in [paper.js](http://paperjs.org/reference/compoundpath/#reorient) but you might not want to load this extra library - unless you need other path operations. I've updated the info. – herrstrietzel Mar 30 '23 at 15:20