0
  <path transform="matrix(1,0,0,-1,0,600)" stroke-width=".074" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#a56752" d="M666.84 101.05V101.07 101.05L666.83 101.04V101.03H666.82 666.62 666.61L666.6 101.04V101.05 101.59 101.6L666.61 101.61H666.62 666.82 666.83V101.6L666.84 101.59V101.56 101.59 101.6L666.83 101.61 666.82 101.62H666.62L666.61 101.61 666.6 101.6 666.59 101.59V101.05L666.6 101.03 666.61 101.02H666.62 666.82 666.83L666.84 101.03V101.05"/>

  <path transform="matrix(1,0,0,-1,0,600)" stroke-width=".074" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#a56752" d="M 666.84 101.05 L 666.84 101.07 L 666.84 101.05 L 666.83 101.04 L 666.83 101.03 L 666.82 101.03 L 666.62 101.03 L 666.61 101.03 L 666.6 101.04 L 666.6 101.05 L 666.6 101.59 L 666.6 101.6 L 666.61 101.61 L 666.62 101.61 L 666.82 101.61 L 666.83 101.61 L 666.83 101.6 L 666.84 101.59 L 666.84 101.56 L 666.84 101.59 L 666.84 101.6 L 666.83 101.61 L 666.82 101.62 L 666.62 101.62 L 666.61 101.61 L 666.6 101.6 L 666.59 101.59 L 666.59 101.05 L 666.6 101.03 L 666.61 101.02 L 666.62 101.02 L 666.82 101.02 L 666.83 101.02 L 666.84 101.03 L 666.84 101.05 "/>

Above path transform are identical to each other, just one of it displays it in a more simplified manner which includes (L, V, H) which stands for vertical and horizontal. May I ask how to expand the simplified version(First Line) into a more expanded form version (Second Line) ? I have a hard time understanding the path transform to expand it as shown in the second Line? Any help will be very appreaciated, I really want to try to understand :).

1 Answers1

0

Commands like V, H, S and T are commonly referred to as shorthand commands since they repeat or reflect a previous command property.

Horizontal or vertical shorthands H and V

M0 10 L50 10 L50 100

equals

M0 10 H50 V100

V and H will take the previous command's x or y values to draw a vertical or horizontal line.

S and T are cubic or quadratic curve shorthands reflecting previous C or Q control points.

Convert shorthands to "lonhghands"

Many svg libraries (e.g. fontello svgpath toolkit, svg path commander) include conversion methods.

I'm using getPathData() polyfill (based on the official working draft)

1. Quick solution: Normalize via getPathData({normalize:true})

getPathData() has a normalize option, that converts all commands to:

  • all absolute

  • quadratic Q, T and A (arctos) are converted to cubic C commands

  • cubic shorthands S become C

  • implicit/repeated commands are also decomposed: L 100 100 150 15 => L 100 100 L 150 15

    let pathData = path.getPathData({normalize:true}); path.setPathData(pathData)

2. Convert shorthands to longhand notation (retain quadratic or arc)

The conversion is applied in 2 steps:

  • conversion to absolute commands via pathDataToAbsolute() helper
  • pathDataToLonghands() loops through all commands and adds/calculates missing coordinates for longhands

let pathData = path.getPathData();
// decompose shorthands
pathData = pathDataToLonghands(pathData);
path.setPathData(pathData)
outputLong.value = path.getAttribute('d')


// normalized to cubics
let pathDataCubic = path.getPathData({
  normalize: true
});
pathDataCubic = roundPathData(pathDataCubic, 2);
path.setPathData(pathDataCubic)

outputNorm.value = path.getAttribute('d')


/**
 * round pathData
 */
function roundPathData(pathData, decimals = 3) {
  pathData.forEach((com, c) => {
    if (decimals >= 0) {
      com.values.forEach((val, v) => {
        pathData[c].values[v] = +val.toFixed(decimals);
      });
    }
  });
  return pathData;
}



/**
 * decompose/convert shorthands to "longhand" commands:
 * H, V, S, T => L, L, C, Q
 * reversed method: pathDataToShorthands()
 */
function pathDataToLonghands(pathData, decimals = -1) {
  pathData = pathDataToAbsolute(pathData, decimals);
  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, 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, unlink = false) {
  // remove object reference
  pathData = unlink ? JSON.parse(JSON.stringify(pathData)) : pathData;

  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) {
      cmd.values = values.map((val) => {
        return +val.toFixed(decimals);
      });
    }
  }
  // round M (starting point)
  if (decimals >= 0) {
    [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
  }
  return pathData;
}
body {
  padding: 2em;
  font-family: sans-serif;
}

svg {
  border: 1px solid #ccc;
  overflow: visible;
  width: 50%;
}

textarea {
  min-height: 20em;
  width: 100%;
}
<svg id="svg" class="svgPreview" viewBox="3 7 80 70">
<path id="path" d="M3,7 L13,7 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,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z" fill="#cccccc" stroke="#000000" stroke-width="0" stroke-linecap="butt"></path>
 </svg>

<h3>Longhand output</h3>
<textarea id="outputLong"></textarea>

<h3>Normalized output (only M, L, C, Z)</h3>
<textarea id="outputNorm"></textarea>

<p>Svg by @Phrogz: http://phrogz.net/svg/convert_path_to_absolute_commands.svg
</p>
<p>https://stackoverflow.com/questions/9677885/convert-svg-path-to-absolute-commands#9677915</p>

<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>

You could also play around with my experimental custom version of getpathData() polyfill.
It adds some helper functions for conversion tasks like absolute to relative, shorthands to longhands, rounding etc.

let pathData = path.getPathDataOpt();
// decompose shorthands
path.setPathData(pathData)
outputLong.value = path.getAttribute('d')

// convert to optimized: all relative, rounded
let options = {
  relative: true,
  decimlas: 1
}
let dMin = getDopt(pathData, options);
outputMin.value = dMin
body {
  padding: 2em;
  font-family: sans-serif;
}

svg {
  border: 1px solid #ccc;
  overflow: visible;
  width: 50%;
}

textarea {
  min-height: 20em;
  width: 100%;
}
<svg id="svg" class="svgPreview" viewBox="3 7 80 70">
<path id="path" d="M3,7 L13,7 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,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z" fill="#cccccc" stroke="#000000" stroke-width="0" stroke-linecap="butt"></path>
 </svg>

<h3>Longhand output</h3>
<textarea id="outputLong"></textarea>

<h3>Shorthand and relative output</h3>
<textarea id="outputMin"></textarea>

<p>Svg by @Phrogz: http://phrogz.net/svg/convert_path_to_absolute_commands.svg
</p>
<p>https://stackoverflow.com/questions/9677885/convert-svg-path-to-absolute-commands#9677915</p>

<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.getPathDataCustom.js"></script>
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34