Search code examples
react-nativesvgreact-native-svg

How to calculate viewbox for an individual path in a svg in react native


I am trying to make every path of svg as independent svg. I wasn't able to find any method to directy get the viewbox for a path.

take this svg as example:

import Svg, { Path } from 'react-native-svg';
const SVGComponent = () => {
  return (
    <View style={{ borderColor: 'red', borderWidth: 1 }}>
      <Svg width={268} height={134} viewBox="0 0 268 134" fill="none">
        <Path
          d="M135.5 132.5V2.74503L266.5 86.8197V132.5H135.5Z"
          fill="#4C4ACF"
          stroke="white"
          strokeWidth={3}
        />
        <Path
          d="M132.5 131.651L3.3555 71.7382L132.5 2.50607L132.5 131.651Z"
          fill="#4C4ACF"
          stroke="white"
          strokeWidth={3}
        />
      </Svg>
    </View>
  );
};

I want to divide the svg like this:

  • original svg
  • after breaking the svg into sub components
  • svg component 1
  • svg component 2

I did make a function to get the viewbox for a path, it did work! but when i triend it on this path

 <Path
     d="M135.5 132.5V2.74503L266.5 86.8197V132.5H135.5Z"
     fill="#4C4ACF"
     stroke="white"
     strokeWidth={3}
        />

I got null height.

here is the link to full code: https://snack.expo.dev/@arjun331/joyous-pizza


Solution

  • Actually you need an equivalent to native browser method getBBox().

    Your current function can't calculate a viewBox because the second path contains shorthand commands (v, h) - so there will be missing x or y coordinates.
    It will also fail when a d pathdata contains relative commands.

    Normalize pathData

    The following helper function is heavily inspired by Jarek Foksa's getpathData() polyfill and works like this:

    1. parse the d attribute to a pathData array
    2. convert all commands to absolute
    3. Convert all shorthand (like v, h) commands to their longhand counterpart L

    Now you can easily calculate a bounding box by getting the polygon's x and y extrema – just like you did before

    function getBBoxFromD(d) {
      // parse to pathdata array
      let pathData = parseDtoPathData(d);
      // to longhands
      pathData = pathDataToLonghands(pathData);
    
      // get polygon points
      let polyPoints = pathDataToPolygonPoints(pathData);
      let bboxPoly = getPolygonBBox(polyPoints);
      return bboxPoly;
    }
    
    /**
     * create pathData from d attribute
     **/
    function parseDtoPathData(d, normalize = false) {
      // sanitize d string
      let commandsString = d
        .replace(/[\n\r\t]/g, "")
        .replace(/,/g, " ")
        .replace(/(\d+)(\-)/g, "$1 $2")
        .replace(/(\.)(\d+)(\.)(\d+)/g, "$1$2 $3$4")
        .replace(/(\.)(\d+)(\.)(\d+)/g, "$1$2 $3$4")
        .replace(/( )(0)(\d+)/g, "$1 $2 $3")
        // add space between all valid command letters and values - excludes scientific e notation
        .replace(/([mlcsqtahvz])/gi, "|$1 ")
        .replace(/\s{2,}/g, " ")
        .trim();
      let commands = commandsString
        .split("|")
        .filter(Boolean)
        .map((val) => {
          return val.trim();
        });
      // compile pathData
      let pathData = [];
      for (let i = 0; i < commands.length; i++) {
        let com = commands[i].split(" ");
        let type = com.shift();
        let typeLc = type.toLowerCase();
        let isRelative = type === typeLc ? true : false;
        // convert to numbers
        let values = com.map((val) => {
          return +val;
        });
        // analyze repeated (shorthanded) commands
        let chunks = [];
        let repeatedType = type;
        // maximum values for a specific command type
        let maxValues = 2;
        switch (typeLc) {
          case "v":
          case "h":
            maxValues = 1;
            if (typeLc === "h") {
              repeatedType = isRelative ? "h" : "H";
            } else {
              repeatedType = isRelative ? "v" : "V";
            }
            break;
          case "m":
          case "l":
          case "t":
            maxValues = 2;
            repeatedType =
              typeLc !== "t" ? (isRelative ? "l" : "L") : isRelative ? "t" : "T";
            if (i === 0) {
              type = "M";
            }
            break;
          case "s":
          case "q":
            maxValues = 4;
            repeatedType =
              typeLc !== "q" ? (isRelative ? "s" : "S") : isRelative ? "q" : "Q";
            break;
          case "c":
            maxValues = 6;
            repeatedType = isRelative ? "c" : "C";
            break;
          case "a":
            maxValues = 7;
            repeatedType = isRelative ? "a" : "A";
            break;
            // z closepath
          default:
            maxValues = 0;
        }
        // if string contains repeated shorthand commands - split them
        const arrayChunks = (array, chunkSize = 2) => {
          let chunks = [];
          for (let i = 0; i < array.length; i += chunkSize) {
            let chunk = array.slice(i, i + chunkSize);
            chunks.push(chunk);
          }
          return chunks;
        };
        chunks = arrayChunks(values, maxValues);
        // add 1st/regular command
        let chunk0 = chunks.length ? chunks[0] : [];
        pathData.push({
          type: type,
          values: chunk0
        });
        // add repeated commands
        if (chunks.length > 1) {
          for (let c = 1; c < chunks.length; c++) {
            pathData.push({
              type: repeatedType,
              values: chunks[c]
            });
          }
        }
      }
      return pathData;
    }
    /**
     * decompose/convert shorthands to "longhand" commands:
     * H, V, S, T => L, L, C, Q
     */
    function pathDataToLonghands(pathData) {
      pathData = pathDataToAbsolute(pathData);
      let pathDataLonghand = [];
      let comPrev = {
        type: "M",
        values: pathData[0].values
      };
      pathDataLonghand.push(comPrev);
      for (let i = 1; i < pathData.length; i++) {
        let com = pathData[i];
        let type = com.type;
        let values = com.values;
        let valuesL = values.length;
        let valuesPrev = comPrev.values;
        let valuesPrevL = valuesPrev.length;
        let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
        let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
        let [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        switch (type) {
          case "H":
            comPrev = {
              type: "L",
              values: [values[0], prevY]
            };
            break;
          case "V":
            comPrev = {
              type: "L",
              values: [prevX, values[0]]
            };
            break;
          case "T":
            [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
            [prevX, prevY] = [
              valuesPrev[valuesPrevL - 2],
              valuesPrev[valuesPrevL - 1]
            ];
            // new control point
            cpN1X = prevX + (prevX - cp1X);
            cpN1Y = prevY + (prevY - cp1Y);
            comPrev = {
              type: "Q",
              values: [cpN1X, cpN1Y, x, y]
            };
            break;
          case "S":
            [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
            [cp2X, cp2Y] =
            valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
            [prevX, prevY] = [
              valuesPrev[valuesPrevL - 2],
              valuesPrev[valuesPrevL - 1]
            ];
            // new control points
            cpN1X = 2 * prevX - cp2X;
            cpN1Y = 2 * prevY - cp2Y;
            cpN2X = values[0];
            cpN2Y = values[1];
            comPrev = {
              type: "C",
              values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
            };
            break;
          default:
            comPrev = {
              type: type,
              values: values
            };
        }
        pathDataLonghand.push(comPrev);
      }
      return pathDataLonghand;
    }
    
    
    function pathDataToAbsolute(pathData, decimals = -1) {
      let M = pathData[0].values;
      let x = M[0],
        y = M[1],
        mx = x,
        my = y;
      // loop through commands
      for (let i = 1; i < pathData.length; i++) {
        let cmd = pathData[i];
        let type = cmd.type;
        let typeAbs = type.toUpperCase();
        let values = cmd.values;
        if (type != typeAbs) {
          type = typeAbs;
          cmd.type = type;
          // check current command types
          switch (typeAbs) {
            case "A":
              values[5] = +(values[5] + x);
              values[6] = +(values[6] + y);
              break;
            case "V":
              values[0] = +(values[0] + y);
              break;
            case "H":
              values[0] = +(values[0] + x);
              break;
            case "M":
              mx = +values[0] + x;
              my = +values[1] + y;
            default:
              // other commands
              if (values.length) {
                for (let v = 0; v < values.length; v++) {
                  // even value indices are y coordinates
                  values[v] = values[v] + (v % 2 ? y : x);
                }
              }
          }
        }
        // is already absolute
        let vLen = values.length;
        switch (type) {
          case "Z":
            x = +mx;
            y = +my;
            break;
          case "H":
            x = values[0];
            break;
          case "V":
            y = values[0];
            break;
          case "M":
            mx = values[vLen - 2];
            my = values[vLen - 1];
          default:
            x = values[vLen - 2];
            y = values[vLen - 1];
        }
      }
      // round coordinates
      if (decimals >= 0) {
        pathData = roundPathData(pathData, decimals);
      }
      return pathData;
    }
    
    
    // get polygon bbox
    function getPolygonBBox(polyPoints) {
      let xArr = [];
      let yArr = [];
      polyPoints.forEach((point) => {
        xArr.push(point.x);
        yArr.push(point.y);
      });
      let xmin = Math.min(...xArr);
      let xmax = Math.max(...xArr);
      let ymin = Math.min(...yArr);
      let ymax = Math.max(...yArr);
      return {
        x: xmin,
        y: ymin,
        width: xmax - xmin,
        height: ymax - ymin
      };
    }
    /**
     * convert path d to polygon point array
     */
    function pathDataToPolygonPoints(
      pathData
    ) {
      let points = [];
      pathData.forEach((com, c) => {
        let type = com.type;
        let values = com.values;
        let valL = values.length;
    
        // M
        if (c === 0) {
          let M = {
            x: pathData[0].values[valL - 2],
            y: pathData[0].values[valL - 1]
          };
          points.push(M);
        }
        if (valL && c > 0) {
          let prev = pathData[c - 1];
          // linetos
          if (type === "L") {
            points.push({
              x: values[valL - 2],
              y: values[valL - 1]
            });
          }
        }
      });
      return points;
    }
    <svg id="svg" viewBox="135.5 2.745029926300049 131 129.75497436523438">
        <path id="path" d="M135.5 132.5V2.74503L266.5 86.8197V132.5H135.5Z "></path>
    </svg>
    
    <script>
      window.addEventListener('DOMContentLoaded', e => {
        let d = path.getAttribute("d");
        // polygon bbox
        let bboxPoly = getBBoxFromD(d)
        //compare with native bbox browser method
        let bbox = path.getBBox();
        console.log(bbox);
        console.log(bboxPoly);
      })
    </script>

    If you're only working with polygons and don't use any Bézier curves or Arc commands – you can stop here.

    Calculate Boundingbox for Béziers or Arcs

    We're also calculating the bbox from an array of polygon points.
    In this case we need to split any curved segments to improve accuracy.
    The problem: control points are usually "off-path" - so we can't use them for the polygon extrema calculation.

    curved object

    In addition to the previous normalization we also need to:

    • convert A Arcs to cubic béziers
    • split curve segments multiple times. The more you split segments - the higher the accuracy

    enter image description here

    function getBBoxFromD(d, accuracy = 4) {
      let pathData = parseDtoPathData(d);
      pathData = pathDataToLonghands(pathData);
      pathData = convertArcsToCubics(pathData);
      // calcultate polygon points
      let polyPoints = pathDataToPolygonPoints(pathData, true, accuracy);
      let bboxPoly = getPolygonBBox(polyPoints);
      return bboxPoly;
    }
    
    function parseDtoPathData(d, normalize = false) {
      let commandsString = d
        .replace(/[\n\r\t]/g, "")
        .replace(/,/g, " ")
        .replace(/(\d+)(\-)/g, "$1 $2")
        .replace(/(\.)(\d+)(\.)(\d+)/g, "$1$2 $3$4")
        .replace(/(\.)(\d+)(\.)(\d+)/g, "$1$2 $3$4")
        .replace(/( )(0)(\d+)/g, "$1 $2 $3")
        .replace(/([mlcsqtahvz])/gi, "|$1 ")
        .replace(/\s{2,}/g, " ")
        .trim();
      let commands = commandsString
        .split("|")
        .filter(Boolean)
        .map((val) => {
          return val.trim();
        });
      let pathData = [];
      for (let i = 0; i < commands.length; i++) {
        let com = commands[i].split(" ");
        let type = com.shift();
        let typeLc = type.toLowerCase();
        let isRelative = type === typeLc ? true : false;
        let values = com.map((val) => {
          return +val;
        });
        let chunks = [];
        let repeatedType = type;
        let maxValues = 2;
        switch (typeLc) {
          case "v":
          case "h":
            maxValues = 1;
            if (typeLc === "h") {
              repeatedType = isRelative ? "h" : "H";
            } else {
              repeatedType = isRelative ? "v" : "V";
            }
            break;
          case "m":
          case "l":
          case "t":
            maxValues = 2;
            repeatedType =
              typeLc !== "t" ? (isRelative ? "l" : "L") : isRelative ? "t" : "T";
            if (i === 0) {
              type = "M";
            }
            break;
          case "s":
          case "q":
            maxValues = 4;
            repeatedType =
              typeLc !== "q" ? (isRelative ? "s" : "S") : isRelative ? "q" : "Q";
            break;
          case "c":
            maxValues = 6;
            repeatedType = isRelative ? "c" : "C";
            break;
          case "a":
            maxValues = 7;
            repeatedType = isRelative ? "a" : "A";
            break;
          default:
            maxValues = 0;
        }
        const arrayChunks = (array, chunkSize = 2) => {
          let chunks = [];
          for (let i = 0; i < array.length; i += chunkSize) {
            let chunk = array.slice(i, i + chunkSize);
            chunks.push(chunk);
          }
          return chunks;
        };
        chunks = arrayChunks(values, maxValues);
        let chunk0 = chunks.length ? chunks[0] : [];
        pathData.push({
          type: type,
          values: chunk0
        });
        if (chunks.length > 1) {
          for (let c = 1; c < chunks.length; c++) {
            pathData.push({
              type: repeatedType,
              values: chunks[c]
            });
          }
        }
      }
      return pathData;
    }
    
    function pathDataToLonghands(pathData) {
      pathData = pathDataToAbsolute(pathData);
      let pathDataLonghand = [];
      let comPrev = {
        type: "M",
        values: pathData[0].values
      };
      pathDataLonghand.push(comPrev);
      for (let i = 1; i < pathData.length; i++) {
        let com = pathData[i];
        let type = com.type;
        let values = com.values;
        let valuesL = values.length;
        let valuesPrev = comPrev.values;
        let valuesPrevL = valuesPrev.length;
        let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
        let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
        let [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        switch (type) {
          case "H":
            comPrev = {
              type: "L",
              values: [values[0], prevY]
            };
            break;
          case "V":
            comPrev = {
              type: "L",
              values: [prevX, values[0]]
            };
            break;
          case "T":
            [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
            [prevX, prevY] = [
              valuesPrev[valuesPrevL - 2],
              valuesPrev[valuesPrevL - 1]
            ];
            cpN1X = prevX + (prevX - cp1X);
            cpN1Y = prevY + (prevY - cp1Y);
            comPrev = {
              type: "Q",
              values: [cpN1X, cpN1Y, x, y]
            };
            break;
          case "S":
            [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
            [cp2X, cp2Y] =
            valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
            [prevX, prevY] = [
              valuesPrev[valuesPrevL - 2],
              valuesPrev[valuesPrevL - 1]
            ];
            cpN1X = 2 * prevX - cp2X;
            cpN1Y = 2 * prevY - cp2Y;
            cpN2X = values[0];
            cpN2Y = values[1];
            comPrev = {
              type: "C",
              values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
            };
            break;
          default:
            comPrev = {
              type: type,
              values: values
            };
        }
        pathDataLonghand.push(comPrev);
      }
      return pathDataLonghand;
    }
    
    function convertArcsToCubics(pathData) {
      let pathdataNew = [pathData[0]];
      for (let i = 1; i < pathData.length; i++) {
        let com = pathData[i];
        let comPrev = pathData[i - 1];
        let valuesPrev = comPrev.values;
        let valuesPrevL = valuesPrev.length;
        let [prevX, prevY] = [
          valuesPrev[valuesPrevL - 2],
          valuesPrev[valuesPrevL - 1]
        ];
        if (com.type === "A") {
          com = pathDataArcToCubic([prevX, prevY], com.values);
        }
        pathdataNew.push(com);
      }
      return pathdataNew.flat();
    }
    
    function pathDataToAbsolute(pathData, decimals = -1) {
      let M = pathData[0].values;
      let x = M[0],
        y = M[1],
        mx = x,
        my = y;
      // loop through commands
      for (let i = 1; i < pathData.length; i++) {
        let cmd = pathData[i];
        let type = cmd.type;
        let typeAbs = type.toUpperCase();
        let values = cmd.values;
        if (type != typeAbs) {
          type = typeAbs;
          cmd.type = type;
          // check current command types
          switch (typeAbs) {
            case "A":
              values[5] = +(values[5] + x);
              values[6] = +(values[6] + y);
              break;
            case "V":
              values[0] = +(values[0] + y);
              break;
            case "H":
              values[0] = +(values[0] + x);
              break;
            case "M":
              mx = +values[0] + x;
              my = +values[1] + y;
            default:
              // other commands
              if (values.length) {
                for (let v = 0; v < values.length; v++) {
                  // even value indices are y coordinates
                  values[v] = values[v] + (v % 2 ? y : x);
                }
              }
          }
        }
        // is already absolute
        let vLen = values.length;
        switch (type) {
          case "Z":
            x = +mx;
            y = +my;
            break;
          case "H":
            x = values[0];
            break;
          case "V":
            y = values[0];
            break;
          case "M":
            mx = values[vLen - 2];
            my = values[vLen - 1];
          default:
            x = values[vLen - 2];
            y = values[vLen - 1];
        }
      }
      // round coordinates
      if (decimals >= 0) {
        pathData = roundPathData(pathData, decimals);
      }
      return pathData;
    }
    
    function pathDataArcToCubic(p0, comValues, recursive = false) {
      if (Array.isArray(p0)) {
        p0 = {
          x: p0[0],
          y: p0[1]
        };
      }
      let [r1, r2, angle, largeArcFlag, sweepFlag, x2, y2] = comValues;
      let [x1, y1] = [p0.x, p0.y];
      const degToRad = (degrees) => {
        return (Math.PI * degrees) / 180;
      };
      const rotate = (x, y, angleRad) => {
        let X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
        let Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
        return {
          x: X,
          y: Y
        };
      };
      let angleRad = degToRad(angle);
      let params = [];
      let x, y, f1, f2, cx, cy, h;
      if (recursive) {
        f1 = recursive[0];
        f2 = recursive[1];
        cx = recursive[2];
        cy = recursive[3];
      } else {
        let p1 = rotate(x1, y1, -angleRad);
        x1 = p1.x;
        y1 = p1.y;
        let p2 = rotate(x2, y2, -angleRad);
        x2 = p2.x;
        y2 = p2.y;
        x = (x1 - x2) / 2;
        y = (y1 - y2) / 2;
        h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2);
        if (h > 1) {
          h = Math.sqrt(h);
          r1 = h * r1;
          r2 = h * r2;
        }
        let sign = largeArcFlag === sweepFlag ? -1 : 1;
        let r1Pow = r1 * r1;
        let r2Pow = r2 * r2;
        let left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x;
        let right = r1Pow * y * y + r2Pow * x * x;
        let k = sign * Math.sqrt(Math.abs(left / right));
        cx = (k * r1 * y) / r2 + (x1 + x2) / 2;
        cy = (k * -r2 * x) / r1 + (y1 + y2) / 2;
        f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
        f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
        if (x1 < cx) {
          f1 = Math.PI - f1;
        }
        if (x2 < cx) {
          f2 = Math.PI - f2;
        }
        if (f1 < 0) {
          f1 = Math.PI * 2 + f1;
        }
        if (f2 < 0) {
          f2 = Math.PI * 2 + f2;
        }
        if (sweepFlag && f1 > f2) {
          f1 = f1 - Math.PI * 2;
        }
        if (!sweepFlag && f2 > f1) {
          f2 = f2 - Math.PI * 2;
        }
      }
      let df = f2 - f1;
      if (Math.abs(df) > (Math.PI * 120) / 180) {
        let f2old = f2;
        let x2old = x2;
        let y2old = y2;
        f2 =
          sweepFlag && f2 > f1 ?
          (f2 = f1 + ((Math.PI * 120) / 180) * 1) :
          (f2 = f1 + ((Math.PI * 120) / 180) * -1);
        x2 = cx + r1 * Math.cos(f2);
        y2 = cy + r2 * Math.sin(f2);
        params = pathDataArcToCubic(
          [x2, y2], [r1, r2, angle, 0, sweepFlag, x2old, y2old], [f2, f2old, cx, cy]
        );
      }
      df = f2 - f1;
      let c1 = Math.cos(f1);
      let s1 = Math.sin(f1);
      let c2 = Math.cos(f2);
      let s2 = Math.sin(f2);
      let t = Math.tan(df / 4);
      let hx = (4 / 3) * r1 * t;
      let hy = (4 / 3) * r2 * t;
      let m1 = [x1, y1];
      let m2 = [x1 + hx * s1, y1 - hy * c1];
      let m3 = [x2 + hx * s2, y2 - hy * c2];
      let m4 = [x2, y2];
      m2[0] = 2 * m1[0] - m2[0];
      m2[1] = 2 * m1[1] - m2[1];
      if (recursive) {
        return [m2, m3, m4].concat(params);
      } else {
        params = [m2, m3, m4].concat(params);
        let commands = [];
        for (var i = 0; i < params.length; i += 3) {
          r1 = rotate(params[i][0], params[i][1], angleRad);
          r2 = rotate(params[i + 1][0], params[i + 1][1], angleRad);
          r3 = rotate(params[i + 2][0], params[i + 2][1], angleRad);
          commands.push({
            type: "C",
            values: [r1.x, r1.y, r2.x, r2.y, r3.x, r3.y]
          });
        }
        return commands;
      }
    }
    // get polygon bbox
    function getPolygonBBox(polyPoints) {
      let xArr = [];
      let yArr = [];
      polyPoints.forEach((point) => {
        xArr.push(point.x);
        yArr.push(point.y);
      });
      let xmin = Math.min(...xArr);
      let xmax = Math.max(...xArr);
      let ymin = Math.min(...yArr);
      let ymax = Math.max(...yArr);
      return {
        x: xmin,
        y: ymin,
        width: xmax - xmin,
        height: ymax - ymin
      };
    }
    /**
     * convert path d to polygon point array
     */
    function pathDataToPolygonPoints(
      pathData,
      addControlPointsMid = false,
      splitNtimes = 0,
      splitLines = false
    ) {
      let points = [];
      pathData.forEach((com, c) => {
        let type = com.type;
        let values = com.values;
        let valL = values.length;
        let splitStep = splitNtimes ?
          0.5 / splitNtimes :
          addControlPointsMid ?
          0.5 :
          0;
        let split = splitStep;
        // M
        if (c === 0) {
          let M = {
            x: pathData[0].values[valL - 2],
            y: pathData[0].values[valL - 1]
          };
          points.push(M);
        }
        if (valL && c > 0) {
          let prev = pathData[c - 1];
          let prevVal = prev.values;
          let prevValL = prevVal.length;
          let p0 = {
            x: prevVal[prevValL - 2],
            y: prevVal[prevValL - 1]
          };
          // cubic curves
          if (type === "C") {
            if (prevValL) {
              let cp1 = {
                x: values[valL - 6],
                y: values[valL - 5]
              };
              let cp2 = {
                x: values[valL - 4],
                y: values[valL - 3]
              };
              let p = {
                x: values[valL - 2],
                y: values[valL - 1]
              };
              if (addControlPointsMid && split) {
                // split cubic curves
                for (let s = 0; split < 1 && s < 9999; s++) {
                  let midPoint = getPointAtCubicSegmentLength(
                    p0,
                    cp1,
                    cp2,
                    p,
                    split
                  );
                  points.push(midPoint);
                  split += splitStep;
                }
              }
              points.push({
                x: values[valL - 2],
                y: values[valL - 1]
              });
            }
          }
          // quadratic curves
          else if (type === "Q") {
            if (prevValL) {
              let cp1 = {
                x: values[valL - 4],
                y: values[valL - 3]
              };
              let p = {
                x: values[valL - 2],
                y: values[valL - 1]
              };
              if (addControlPointsMid && split) {
                // split cubic curves
                for (let s = 0; split < 1 && s < 9999; s++) {
                  let midPoint = getPointAtQuadraticSegmentLength(
                    p0,
                    cp1,
                    p,
                    split
                  );
                  points.push(midPoint);
                  split += splitStep;
                }
              }
              points.push({
                x: values[valL - 2],
                y: values[valL - 1]
              });
            }
          }
          // linetos
          else if (type === "L") {
            if (splitLines) {
              let p1 = {
                x: prevVal[prevValL - 2],
                y: prevVal[prevValL - 1]
              };
              let p2 = {
                x: values[valL - 2],
                y: values[valL - 1]
              };
              if (addControlPointsMid && split) {
                for (let s = 0; split < 1; s++) {
                  let midPoint = interpolatedPoint(p1, p2, split);
                  points.push(midPoint);
                  split += splitStep;
                }
              }
            }
            points.push({
              x: values[valL - 2],
              y: values[valL - 1]
            });
          }
        }
      });
      return points;
    }
    // Linear  interpolation (LERP) 
    function interpolatedPoint(p1, p2, t = 0.5) {
      if (Array.isArray(p1)) {
        p1.x = p1[0];
        p1.y = p1[1];
      }
      if (Array.isArray(p2)) {
        p2.x = p2[0];
        p2.y = p2[1];
      }
      let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
      return {
        x: x,
        y: y
      };
    }
    
    // calculate single points on segments
    function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t) {
      let t1 = 1 - t;
      return {
        x: t1 ** 3 * p0.x +
          3 * t1 ** 2 * t * cp1.x +
          3 * t1 * t ** 2 * cp2.x +
          t ** 3 * p.x,
        y: t1 ** 3 * p0.y +
          3 * t1 ** 2 * t * cp1.y +
          3 * t1 * t ** 2 * cp2.y +
          t ** 3 * p.y
      };
    }
    
    function getPointAtQuadraticSegmentLength(p0, cp1, p, t = 0.5) {
      let t1 = 1 - t;
      return {
        x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
        y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
      };
    }
    <svg id="svg" viewBox="0 0 100 100">
      <path id="path" d="m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15 A 5,5 45 1 0 40,20  a5,5 20 0 1 -10,-10Z "></path>
    </svg>
    <script>
      window.addEventListener('DOMContentLoaded', e => {
        let d = path.getAttribute("d");
        let bboxPoly = getBBoxFromD(d, 4)
        //compare with browser method
        let bbox = path.getBBox();
        console.log(bbox);
        console.log(bboxPoly);
      })
    </script>