不同原点的变换序列在 SVG 中如何工作?

问题描述 投票:0回答:1

为了准备用于生产环境或发布的矢量图像,我希望将 SVG 文件的代码统一为一种形式。

简单地说,我有一个包含 SVG 文件的文件夹,我需要转换图像并将视图数组中的所有图像组成一个 JSON 文件(或 JS 文件),如下所示 https://github.com/arallsopp/hass-hue-icons/blob/main/dist/hass-hue-icons.js

图像本身是由不同的人使用不同的软件解决方案(inkscape、coreldraw、illustrator)绘制的。 通常,矢量图像的作者不太关心文件大小等属性。但我想摆脱 SVG 文件中的冗余代码,为代码提供学术上的基本形式,其中每个引理都有特定的功能。

对于重建单色字形,仅存储单个

d
元素的
<path/>
属性的值非常重要。 SVG 字形文件中的其他内容对于所有图像都是通用的。 可以为单个文件中的所有图像设置填充颜色或描边粗细等参数。从外部将所有图像集中在一个地方。

当然,要将图像代码获取到此表单,您必须手动编辑每个图像。 对于简单的图像,这变成了一件单调的苦差事,但由于累积结果的可见性,这样做很好。 但有时矢量图像编辑器生成的代码结构是非常难以手动解释的代码结构。 有时这些程序会将此类代码保存到 SVG 文件中,这几乎不可能手动更改。

特别是多层

matrix()
变换,需要计算每个路径点的坐标,在渲染时计算机很容易计算,但对于手动编辑来说却很头疼。

对于这种情况,我决定编写一个脚本,帮助将 SVG 文件转换为具有针对

<path/>
的所有点预先计算的变换的形式。

写这样的脚本只花了很少的时间。 研究 SVG 和 CSS 规范的所有版本需要花费更多时间。 - 相同规范的不同版本处理不同的问题,并且在许多地方它们相互引用。

文档中很好地描述了 2D 和 3D 变换的原理。 但在我看来,有一点似乎没有完全公开。 通过将变换矩阵相乘,可以轻松计算一系列连续变换的 CTM 矩阵。 但是如果每个变换都有自己不同的原点,那么 CTM 矩阵是如何计算的呢?

有了一点Canvas API的经验,不难猜测,只需减去变换前的原点坐标并加上变换后的原点坐标,就像

translate(-originX, -originY)
translate(originX, originY)

但是如果在单个元素中列出多个变换呢?

例如,

transform=“skewX(45) translate(-100, 0) skewY(-45) skewX(25)” transform-origin=“500 350”

这里,是应该在

skewX(45)
之前和
skewX(25)
之后考虑原点,还是应该为每个变换单独考虑原点?点会随着空间扭曲而变化吗?

为了测试算法,我制作了一个小型 SVG 文件,其中包含要转换的元素。 页面加载后一秒钟,转换脚本就会运行并破坏原始图像。 我需要更改代码才能获得原始图像。

UPD 为代码添加了注释。

// regular expression to parse numbers
const rxNumberStr = '[+-]?(?:(?:[0-9]+\\.[0-9]+)|(?:\\.[0-9]+)|(?:[0-9]+))(?:[eE][+-]?[0-9]+)?';
// regular expression to  parse next lemma from d attribute of <path> 
const rxDLemmaStr = `[MmLlHhVvCcSsQqTtAaZz]|${rxNumberStr}`;

// D-attribute string parser
// https://www.w3.org/TR/SVG/paths.html#PathData
// https://www.w3.org/TR/SVG/paths.html#PathDataBNF
class D {
  // Result of parsing a D-attribute string
  // path: [[cmd, new D.P({x,y}), ...], ...]
  path = [];
  constructor(d) {
    let rx = new RegExp(rxDLemmaStr, 'g');
    let ps = d.match(rx).map(v => ('MmLlHhVvCcSsQqTtAaZz'.indexOf(v) > -1 ? v : parseFloat(v)));
    let cmd, move, ref = D.P.abs(0, 0);
    for (let i = 0, len = ps.length; i < len; ) {
      if (isNaN(ps[i])) cmd = ps[i++];
      this.path.push(({
        m: () => (ref = D.P.rel(ps[i++], ps[i++], ref), ps[i - 3] == cmd ? ['M', move = ref] : ['L', ref]),
        M: () => (ref = D.P.rel(ps[i++], ps[i++], ref), ps[i - 3] == cmd ? ['M', move = ref] : ['L', ref]),
        l: () => ['L', (ref = D.P.rel(ps[i++], ps[i++], ref))],
        L: () => ['L', (ref = D.P.abs(ps[i++], ps[i++], ref))],
        h: () => ['H', (ref = D.P.rel(ps[i++], 0, ref))],
        H: () => ['H', (ref = D.P.abs(ps[i++], ref.y, ref))],
        v: () => ['V', (ref = D.P.rel(0, ps[i++], ref))],
        V: () => ['V', (ref = D.P.abs(ref.x, ps[i++], ref))],
        c: () => ['C', D.P.rel(ps[i++], ps[i++], ref), D.P.rel(ps[i++], ps[i++], ref), (ref = D.P.rel(ps[i++], ps[i++], ref))],
        C: () => ['C', D.P.abs(ps[i++], ps[i++], ref), D.P.abs(ps[i++], ps[i++], ref), (ref = D.P.abs(ps[i++], ps[i++], ref))],
        s: () => ['S', D.P.rel(ps[i++], ps[i++], ref), (ref = D.P.rel(ps[i++], ps[i++], ref))],
        S: () => ['S', D.P.abs(ps[i++], ps[i++], ref), (ref = D.P.abs(ps[i++], ps[i++], ref))],
        q: () => ['Q', D.P.rel(ps[i++], ps[i++], ref), (ref = D.P.rel(ps[i++], ps[i++], ref))],
        Q: () => ['Q', D.P.abs(ps[i++], ps[i++], ref), (ref = D.P.abs(ps[i++], ps[i++], ref))],
        t: () => ['T', (ref = D.P.rel(ps[i++], ps[i++], ref))],
        T: () => ['T', (ref = D.P.abs(ps[i++], ps[i++], ref))],
        a: () => ['A', ps[i++], ps[i++], ps[i++], ps[i++], ps[i++], (ref = D.P.rel(ps[i++], ps[i++], ref))],
        A: () => ['A', ps[i++], ps[i++], ps[i++], ps[i++], ps[i++], (ref = D.P.abs(ps[i++], ps[i++], ref))],
        z: () => (ref = move, ['Z']),
        Z: () => (ref = move, ['Z']),
      })[cmd]());
    }
  }
  // Compile array with absolute coordinates for new <path d="..." />
  get abs() {
    return this.path.map(s => s.map((v, i) => (!i ? v : { H: v.x, V: v.y }[s[0]] || v.abs))).flat(3);
  }
  // Compile array with relative coordinates for new <path d="..." />
  get rel() {
    return this.path.map(s => s.map((v, i) => !i ? v.toLowerCase() : { H: v.x - v.ref.x, V: v.y - v.ref.y }[s[0]] || v.rel)).flat(3);
  }
  // Compile array with transformed absolute coordinates for new <path d="..." />
  transform(matrix) {
    return this.path.map(s => s.map((v, i) => (!i ? v.replace(/[HV]/i,'L') : v.transform(matrix)))).flat(3);
  }
  // Path Point class 
  static get P() {
    return class P {
      x;
      y;
      // Reference point for relative coordinates
      ref;
      constructor(props) {
        Object.assign(this, props);
      }
      // Absolute coordinates
      get abs() {
        return [this.x, this.y];
      }
      // Relative coordinates
      get rel() {
        return [this.x - this.ref.x, this.y - this.ref.y];
      }
      // Transform point with matrix
      transform([a, b, c, d, e, f]) {
        return [this.x * a + this.y * c + e, this.x * b + this.y * d + f];
      }
      // Create point from absolute coordinates
      static abs = (x, y, ref) => new D.P({ x, y, ref });
      // Create point from relative coordinates
      static rel = (x, y, ref) => new D.P({ x: ref.x + x, y: ref.y + y, ref });
    };
  }
}

// Matrix multiplication
// https://en.wikipedia.org/wiki/Matrix_multiplication
const multiply = ([a1, b1, c1, d1, e1, f1], [a2, b2, c2, d2, e2, f2]) => [
    a1 * a2 + c1 * b2,
    b1 * a2 + d1 * b2,
    a1 * c2 + c1 * d2,
    b1 * c2 + d1 * d2,
    a1 * e2 + c1 * f2 + e1,
    b1 * e2 + d1 * f2 + f1,
  ];

// Consolidate matrix chain
const consolidate = matrices => matrices.reduceRight((acc, m) => multiply(m, acc), [1, 0, 0, 1, 0, 0]);

// Read SVG element attributes
// https://developer.mozilla.org/en-US/docs/Web/API/SVGElement/attributes
// returns object with attributes
const readAttrs = (element) => [...element.attributes].reduce((acc, { name, value }) => ((acc[name] = value), acc), {});
// Write SVG element attributes
const writeAttrs = (element, attrs) => Object.entries(attrs).forEach(([attr, value]) => element.setAttribute(attr, value));

// Process SVG shape element
// Shape elements are replaced with new <path d="..." /> element
// Shape element attributes are copied to new <path d="..." /> element
// Shape element must not have child elements
// List of shape elements <circle>, <ellipse>, <line>, <path>, <polygon>, <polyline>, <rect>
function processShape(shape) {
  const shape2path = {
    circle: ({ cx, cy, r, ...attrs }) => [
      `M ${[cx - r, cy]} A ${r} ${r} 0 1 1 ${[cx + r, cy]} A ${r} ${r} 0 1 1 ${[cx - r, cy]} Z`,
      attrs,
    ],
    line: ({ x1, y1, x2, y2, ...attrs }) => [`M ${[x1, y1]} L ${[x2, y2]}`, attrs],
    rect: ({ x, y, width, height, rx = 0, ry = 0, ...attrs }) =>
      rx == 0 && ry == 0
        ? [`M ${[x, y]} h ${width} v ${height} h -${width} Z`, attrs]
        : [
            `M ${[x + rx, y]} h  ${width - 2 * rx}  a ${rx} ${ry} 0 0 1  ${rx}  ${ry}
                              v  ${height - 2 * ry} a ${rx} ${ry} 0 0 1 -${rx}  ${ry}
                              h -${width - 2 * rx}  a ${rx} ${ry} 0 0 1 -${rx} -${ry}
                              v -${height - 2 * ry} a ${rx} ${ry} 0 0 1  ${rx} -${ry} Z`,
            attrs,
          ],
    ellipse: ({ cx, cy, rx, ry, ...attrs }) => [
      `M ${[cx - rx, cy]} A ${rx} ${ry} 0 1 1 ${[cx + 2 * rx, cy]}
                          A ${rx} ${ry} 0 1 1 ${[cx + rx, cy - ry]}
                          A ${rx} ${ry} 0 1 1 ${[cx + rx, cy + ry]}
                          A ${rx} ${ry} 0 1 1 ${[cx - rx, cy]} Z`,
      attrs,
    ],
    polygon: ({ points, ...attrs }) => [`M ${points} Z`, attrs],
    polyline: ({ points, ...attrs }) => [`M ${points}`, attrs],
  };

  // return if shape inside <pattern>, <mask> or <clipPath>
  if (shape.closest('pattern') || shape.closest('mask') || shape.closest('clipPath')) {
    return;
  }

  // create d value for new <path>
  let [d, attrs] = shape2path[shape.tagName](readAttrs(shape));
  // create new <path>
  let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  // write attributes from object to <path>
  writeAttrs(path, attrs);
  // set attribute "d"
  path.setAttribute('d', d);
  // replace shape with <path>
  shape.replaceWith(path);
  return path;
}

// process <path /> element
function processPath(path){
  // returns all parent Elements
  const getParents = (element)=>{
    let parents = [];
    while(element !== document.documentElement){
      parents.push(element = element.parentNode);
    }
    return parents;
  }
  // parse transform matrix
  const getMatrix = (transform) => {
    let [a, b, c, d, e, f] = transform.match(/^matrix\(([^)]+)\)$/)[1].split(',').map(Number);
    return [a, b, c, d, e, f];
  }
  // parse transform origin
  const getOrigin = (transformOrigin) => {
    let [x, y] = transformOrigin.split(' ').map(value=>value.trim().replace(/[a-z]+$/i,'')).map(Number);
    return [x, y];
  }
  // calc CTM matrix from transform and origin
  const getCTM = (element) => {
    // very useful function for read computed style
    let style = getComputedStyle(element);
    // no matter how exactly the transform is specified
    // (in attributes or in styles or in individual classes),
    // the function returns transform always in a matrix form
    let transform = style.getPropertyValue('transform');
    let transformOrigin = style.getPropertyValue('transform-origin');
    console.log(transform);
    // if no transform is acquired
    if(!transform || transform == 'none') return [1,0,0,1,0,0];
    // parse transform matrix
    let matrix = getMatrix(transform);
    // parse origin
    let origin = getOrigin(transformOrigin);
    // return consolidate matrix
    return consolidate([[1,0,0,1,...origin], matrix,[1,0,0,1,...origin.map(v=>-v)]]);
    // this variant returns original matrix;
    // return matrix;
  }
  
  // return if <path> inside <pattern>, <mask> or <clipPath>
  if (path.closest('pattern') || path.closest('mask') || path.closest('clipPath')) {
    return;
  }

  // read element transform matrix
  const elementCTM = getCTM(path);
  // get all parent elements
  const parents = getParents(path);
  // clone element transform matrix array
  let resultCTM = [...elementCTM];
  // for each parent
  parents.forEach((parent) => {
    // read parent transform matrix
    let parentCTM = getCTM(parent);
    // calc CTM
    resultCTM = consolidate([resultCTM, parentCTM]);
  });
  
  console.log('resultCTM',resultCTM);

  // read d attribute, parse it, transform each point and join to single string
  let d = new D(path.getAttribute('d')).transform(resultCTM).join(' ');
  // this variant returns absolute coordinates without transform
  //let d = new D(path.getAttribute('d')).abs.join(' ');
  // remove transform attributes
  path.removeAttribute('transform');
  path.removeAttribute('transform-origin');
  // force init transform matrix
  path.style.setProperty('transform','matrix(1,0,0,1,0,0)');
  path.style.setProperty('transform-origin', '0 0');
  // set d attribute
  path.setAttribute('d', d);
  // find root element
  let svg = path.closest('svg');
  if(svg){
    // remove element from parent
    path.parentNode.removeChild(path);
    // add element to root
    svg.appendChild(path);
  }
}

// Process <use /> element
function  processUse(element) {
  // read attributes to object
  let attrs = readAttrs(element);
  // get href attribute value
  let sel = attrs.href;
  // find referenced element by href="#id"
  let ref = document.querySelector(sel);
  // clone referenced element
  let clone = ref.cloneNode(true);
  // remove id attribute from clone
  clone.removeAttribute('id');
  // remove href from attribute object
  delete attrs.href;
  // write attributes from object
  writeAttrs(clone, attrs);
  // replace <use> element to clone
  element.replaceWith(clone);
  return clone;
}


// Create matrix from transform attribute
// This fuction does not used.
// Use of getComputedStyle() completely replaces this implementation
function parseTransform (transform) {
  const rxTransformNameStr = '(?:(?:translate|scale|skew)[XY]?)|matrix|rotate';

  const rad = a => a / 57.29577951308232;

  const { sin, cos, tan } = Math;

  const matricies = {
    identity: () => [1, 0, 0, 1, 0, 0],
    translate: (tx, ty) => [1, 0, 0, 1, tx, ty],
    translateX: tx => [1, 0, 0, 1, tx, 0],
    translateY: ty => [1, 0, 0, 1, 0, ty],
    scale: (sx, sy = sx, cx = 0, cy = 0) => [sx, 0, 0, sy, cx * (1 - sx), cy * (1 - sy)],
    scaleX: (sx, cx = 0) => [sx, 0, 0, 1, cx * (1 - sx), 0],
    scaleY: (sy, cy = 0) => [1, 0, 0, sy, 0, cy * (1 - sy)],
    rotate: (a, cx = 0, cy = 0) => {
      let [s, c] = [sin(rad(a)), cos(rad(a))];
      return [c, s, -s, c, cx * (1 - c) + cy * s, cy * (1 - c) - cx * s];
    },
    skew: (ax, ay = ax) => [1, tan(rad(ay)), tan(rad(ax)), 1, 0, 0],
    skewX: a => [1, 0, tan(rad(a)), 1, 0, 0],
    skewY: a => [1, tan(rad(a)), 0, 1, 0, 0],
    matrix: (a, b, c, d, e, f) => [a, b, c, d, e, f],
  };

  let chain = [...transform.matchAll(new RegExp(`(${rxTransformNameStr})\\(([0-9eE,\\.\\s+-]+)\\)`, 'g'))].map(
    ([, fn, args]) => [fn, [...args.matchAll(new RegExp(rxNumberStr, 'g'))].map(([num]) => parseFloat(num))]
  );

  // For example,
  //    chain = [
  //      [ 'matrix', [ 1, 0, 0, 1, 0, 0 ] ],
  //      [ 'translate', [ 100, 100 ] ],
  //      [ 'rotate', [ 45 ] ],
  //      [ 'scale', [ 2, 2 ] ],
  //    ]

  chain = chain.map(([fn, args]) => matrices[fn](...args));

  // chain = [
  //   [ 1, 0, 0, 1, 0, 0 ],
  //   [ 1, 0, 0, 1, 100, 100 ],
  //   [ 0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 0, 0 ],
  //   [ 2, 0, 0, 2, 0, 0 ]
  // ]

  // returns single matrix
  return consolidate(chain);
}

setTimeout(()=>{

// Replace all <use /> elements to clone elements referenced by the <use>
document.querySelectorAll('use').forEach(processUse);

// Replace all shape elements to <path />
document.querySelectorAll('line, rect, circle, ellipse, polygon, polyline').forEach(processShape);

// Process all <path /> elements
document.querySelectorAll('path').forEach(processPath);   // <--- PROBLEM HERE

}, 2000);
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">

  <g transform="translate(-100,-100)">

    <g id="hexagon" transform="translate(250,220) scale(1,.5)">
      <line id="line" x1="200" x2="500" y1="100" y2="100" stroke="#000" stroke-width="5" />
      <g transform="rotate(-120)" transform-origin="500 100">
        <use href="#line" />
        <g transform="rotate(-120)" transform-origin="200 100">
          <use href="#line" />
          <g transform="rotate(-120)" transform-origin="500 100">
            <use href="#line" />
            <g transform="rotate(-120)" transform-origin="200 100">
              <use href="#line" />
              <g transform="rotate(-120)" transform-origin="500 100">
                <use href="#line" />
              </g>
            </g>
          </g>
        </g>
      </g>
    </g>


    <g id="hexagon2" transform="translate(250,720) scale(1,.5)">
      <line id="line2" x1="200" x2="500" y1="100" y2="100" stroke="#000" stroke-width="5" />
      <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
        <use href="#line2" />
        <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
          <use href="#line2" />
          <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
            <use href="#line2" />
            <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
              <use href="#line2" />
              <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
                <use href="#line2" />
              </g>
            </g>
          </g>
        </g>
      </g>
    </g>

    <use href="#hexagon" transform="scale(1,-1) translate(0,-1200)" />

    <rect x="500" y="500" width="200" height="400" fill="#0008" stroke="#000" stroke-width="5" />
    <g transform="translate(-100,-100) skewY(-45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0004" stroke="#000" stroke-width="5" />
    </g>
    <g transform="translate(-100,-100) skewY(45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0008" stroke="#000" stroke-width="5" />
    </g>
    <g transform="translate(200,-200) skewY(45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0004" stroke="#000" stroke-width="5" />
    </g>
    <g transform="translate(200,0) skewY(-45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0008" stroke="#000" stroke-width="5" />
    </g>
    <rect x="500" y="300" width="200" height="400" fill="#0004" stroke="#000" stroke-width="5" />
  </g>

</svg>

javascript math svg transform glyphicons
1个回答
0
投票

我做到了!

因此,工作代码如下。这个答案是通过 NetSurf 浏览器 SVG 渲染器的源代码实现的。

此外,herrstrietzel对我帮助很大。将

<use>
包裹在
<g>
内的尖端恰到好处。

就我而言,不需要文本渲染。但如果有人需要在

<path>
中渲染文本,那么这个问题可以在 opentype 库的帮助下解决。这个图书馆不是很出名,但它很有魔力。为了应用文本转换,必须在处理所有
<path>
之前在
<path>
中渲染测试。

下一步是什么?要么任务需要更复杂,要么我根本不知道。如果有人有更优雅的解决方案或在我的代码中发现错误,请告诉我。你可能会得到赏金。

    // regular expression to parse numbers
const rxNumberStr = '[+-]?(?:(?:[0-9]+\\.[0-9]+)|(?:\\.[0-9]+)|(?:[0-9]+))(?:[eE][+-]?[0-9]+)?';
// regular expression to  parse next lemma from d attribute of <path> 
const rxDLemmaStr = `[MmLlHhVvCcSsQqTtAaZz]|${rxNumberStr}`;

// D-attribute string parser
// https://www.w3.org/TR/SVG/paths.html#PathData
// https://www.w3.org/TR/SVG/paths.html#PathDataBNF
class D {
  // Result of parsing a D-attribute string
  // path: [[cmd, new D.P({x,y}), ...], ...]
  path = [];
  constructor(d) {
    let rx = new RegExp(rxDLemmaStr, 'g');
    let ps = d.match(rx).map(v => ('MmLlHhVvCcSsQqTtAaZz'.indexOf(v) > -1 ? v : parseFloat(v)));
    let cmd, move, ref = D.P.abs(0, 0);
    for (let i = 0, len = ps.length; i < len; ) {
      if (isNaN(ps[i])) cmd = ps[i++];
      this.path.push(({
        m: () => (ref = D.P.rel(ps[i++], ps[i++], ref), ps[i - 3] == cmd ? ['M', move = ref] : ['L', ref]),
        M: () => (ref = D.P.rel(ps[i++], ps[i++], ref), ps[i - 3] == cmd ? ['M', move = ref] : ['L', ref]),
        l: () => ['L', (ref = D.P.rel(ps[i++], ps[i++], ref))],
        L: () => ['L', (ref = D.P.abs(ps[i++], ps[i++], ref))],
        h: () => ['H', (ref = D.P.rel(ps[i++], 0, ref))],
        H: () => ['H', (ref = D.P.abs(ps[i++], ref.y, ref))],
        v: () => ['V', (ref = D.P.rel(0, ps[i++], ref))],
        V: () => ['V', (ref = D.P.abs(ref.x, ps[i++], ref))],
        c: () => ['C', D.P.rel(ps[i++], ps[i++], ref), D.P.rel(ps[i++], ps[i++], ref), (ref = D.P.rel(ps[i++], ps[i++], ref))],
        C: () => ['C', D.P.abs(ps[i++], ps[i++], ref), D.P.abs(ps[i++], ps[i++], ref), (ref = D.P.abs(ps[i++], ps[i++], ref))],
        s: () => ['S', D.P.rel(ps[i++], ps[i++], ref), (ref = D.P.rel(ps[i++], ps[i++], ref))],
        S: () => ['S', D.P.abs(ps[i++], ps[i++], ref), (ref = D.P.abs(ps[i++], ps[i++], ref))],
        q: () => ['Q', D.P.rel(ps[i++], ps[i++], ref), (ref = D.P.rel(ps[i++], ps[i++], ref))],
        Q: () => ['Q', D.P.abs(ps[i++], ps[i++], ref), (ref = D.P.abs(ps[i++], ps[i++], ref))],
        t: () => ['T', (ref = D.P.rel(ps[i++], ps[i++], ref))],
        T: () => ['T', (ref = D.P.abs(ps[i++], ps[i++], ref))],
        a: () => ['A', ps[i++], ps[i++], ps[i++], ps[i++], ps[i++], (ref = D.P.rel(ps[i++], ps[i++], ref))],
        A: () => ['A', ps[i++], ps[i++], ps[i++], ps[i++], ps[i++], (ref = D.P.abs(ps[i++], ps[i++], ref))],
        z: () => (ref = move, ['Z']),
        Z: () => (ref = move, ['Z']),
      })[cmd]());
    }
  }
  // Compile array with absolute coordinates for new <path d="..." />
  get abs() {
    return this.path.map(s => s.map((v, i) => (!i ? v : { H: v.x, V: v.y }[s[0]] || v.abs))).flat(3);
  }
  // Compile array with relative coordinates for new <path d="..." />
  get rel() {
    return this.path.map(s => s.map((v, i) => !i ? v.toLowerCase() : { H: v.x - v.ref.x, V: v.y - v.ref.y }[s[0]] || v.rel)).flat(3);
  }
  // Compile array with transformed absolute coordinates for new <path d="..." />
  transform(matrix) {
    return this.path.map(s => s.map((v, i) => (!i ? v.replace(/[HV]/i,'L') : v.transform(matrix)))).flat(3);
  }
  // Path Point class 
  static get P() {
    return class P {
      x;
      y;
      // Reference point for relative coordinates
      ref;
      constructor(props) {
        Object.assign(this, props);
      }
      // Absolute coordinates
      get abs() {
        return [this.x, this.y];
      }
      // Relative coordinates
      get rel() {
        return [this.x - this.ref.x, this.y - this.ref.y];
      }
      // Transform point with matrix
      transform([a, b, c, d, e, f]) {
        return [this.x * a + this.y * c + e, this.x * b + this.y * d + f];
      }
      // Create point from absolute coordinates
      static abs = (x, y, ref) => new D.P({ x, y, ref });
      // Create point from relative coordinates
      static rel = (x, y, ref) => new D.P({ x: ref.x + x, y: ref.y + y, ref });
    };
  }
}

// Matrix multiplication
// https://en.wikipedia.org/wiki/Matrix_multiplication
const multiply = ([a1, b1, c1, d1, e1, f1], [a2, b2, c2, d2, e2, f2]) => [
    a1 * a2 + c1 * b2,
    b1 * a2 + d1 * b2,
    a1 * c2 + c1 * d2,
    b1 * c2 + d1 * d2,
    a1 * e2 + c1 * f2 + e1,
    b1 * e2 + d1 * f2 + f1,
  ];

// Consolidate matrix chain
const consolidate = matrices => matrices.reduce((acc, m) => multiply(acc,m), [1, 0, 0, 1, 0, 0]);

// Read SVG element attributes
// https://developer.mozilla.org/en-US/docs/Web/API/SVGElement/attributes
// returns object with attributes
const readAttrs = (element) => [...element.attributes].reduce((acc, { name, value }) => ((acc[name] = value), acc), {});
// Write SVG element attributes
const writeAttrs = (element, attrs) => Object.entries(attrs).forEach(([attr, value]) => element.setAttribute(attr, value));

// Process SVG shape element
// Shape elements are replaced with new <path d="..." /> element
// Shape element attributes are copied to new <path d="..." /> element
// Shape element must not have child elements
// List of shape elements <circle>, <ellipse>, <line>, <path>, <polygon>, <polyline>, <rect>
function processShape(shape) {
  const shape2path = {
    circle: ({ cx, cy, r, ...attrs }) => [
      `M ${[cx - r, cy]} A ${r} ${r} 0 1 1 ${[cx + r, cy]} A ${r} ${r} 0 1 1 ${[cx - r, cy]} Z`,
      attrs,
    ],
    line: ({ x1, y1, x2, y2, ...attrs }) => [`M ${[x1, y1]} L ${[x2, y2]}`, attrs],
    rect: ({ x, y, width, height, rx = 0, ry = 0, ...attrs }) =>
      rx == 0 && ry == 0
        ? [`M ${[x, y]} h ${width} v ${height} h -${width} Z`, attrs]
        : [
            `M ${[x + rx, y]} h  ${width - 2 * rx}  a ${rx} ${ry} 0 0 1  ${rx}  ${ry}
                              v  ${height - 2 * ry} a ${rx} ${ry} 0 0 1 -${rx}  ${ry}
                              h -${width - 2 * rx}  a ${rx} ${ry} 0 0 1 -${rx} -${ry}
                              v -${height - 2 * ry} a ${rx} ${ry} 0 0 1  ${rx} -${ry} Z`,
            attrs,
          ],
    ellipse: ({ cx, cy, rx, ry, ...attrs }) => [
      `M ${[cx - rx, cy]} A ${rx} ${ry} 0 1 1 ${[cx + 2 * rx, cy]}
                          A ${rx} ${ry} 0 1 1 ${[cx + rx, cy - ry]}
                          A ${rx} ${ry} 0 1 1 ${[cx + rx, cy + ry]}
                          A ${rx} ${ry} 0 1 1 ${[cx - rx, cy]} Z`,
      attrs,
    ],
    polygon: ({ points, ...attrs }) => [`M ${points} Z`, attrs],
    polyline: ({ points, ...attrs }) => [`M ${points}`, attrs],
  };

  // return if shape inside <pattern>, <mask> or <clipPath>
  if (shape.closest('pattern') || shape.closest('mask') || shape.closest('clipPath')) {
    return;
  }

  // create d value for new <path>
  let [d, attrs] = shape2path[shape.tagName](readAttrs(shape));
  // create new <path>
  let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  // write attributes from object to <path>
  writeAttrs(path, attrs);
  // set attribute "d"
  path.setAttribute('d', d);
  // replace shape with <path>
  shape.replaceWith(path);
  return path;
}

// process <path /> element
function processPath(path){
  // returns all parent Elements
  const getParents = (element)=>{
    let parents = [];
    while(element !== document.documentElement){
      parents.push(element = element.parentNode);
    }
    return parents;
  }
  // parse transform matrix
  const getMatrix = (transform) => {
    let [a, b, c, d, e, f] = transform.match(/^matrix\(([^)]+)\)$/)[1].split(',').map(Number);
    return [a, b, c, d, e, f];
  }
  // parse transform origin
  const getOrigin = (transformOrigin) => {
    // TODO Parse origin coordinates with units
    // from https://source.netsurf-browser.org/libsvgtiny.git/tree/src/svgtiny.c#n1816
    let [x, y] = transformOrigin.split(' ').map(value=>value.trim().replace(/[a-z]+$/i,'')).map(Number);
    return [x, y];
  }
  // calc origed matrix from transform and origin
  const getOTM = (element) => {
    // very useful function for read computed style
    let style = getComputedStyle(element);
    // no matter how exactly the transform is specified
    // (in attributes or in styles or in individual classes),
    // the function returns transform always in a matrix form
    let transform = style.getPropertyValue('transform');
    let transformOrigin = style.getPropertyValue('transform-origin');
    // if no transform is acquired
    if(!transform || transform == 'none') return [1,0,0,1,0,0];
    // parse transform matrix
    let matrix = getMatrix(transform);
    // parse origin
    let origin = getOrigin(transformOrigin);
    // return consolidate matrix
    return consolidate([
      [1,0,0,1,...origin],            // + origin
      matrix,
      [1,0,0,1,...origin.map(v=>-v)]  // - origin
    ]);
  }
  
  // return if <path> inside <pattern>, <mask> or <clipPath>
  if (path.closest('pattern') || path.closest('mask') || path.closest('clipPath')) {
    return;
  }

  // read element transform matrix
  const elementOTM = getOTM(path);
  // get all parent elements
  const parents = getParents(path);
  // clone element transform matrix array
  let resultCTM = [1,0,0,1,0,0];
  // for each parent
  parents.reverse().forEach((parent) => {
    // read parent transform matrix
    let parentOTM = getOTM(parent);
    // calc CTM
    resultCTM = consolidate([resultCTM,parentOTM]);
  });

  consolidate([resultCTM,elementOTM])
  
  // read d attribute, parse it, transform each point and join to single string
  let d = new D(path.getAttribute('d')).transform(resultCTM).join(' ');
  // this variant returns absolute coordinates without transform
  //let d = new D(path.getAttribute('d')).abs.join(' ');
  // remove transform attributes
  path.removeAttribute('transform');
  path.removeAttribute('transform-origin');
  // force init transform matrix
  //path.style.setProperty('transform','matrix(1,0,0,1,0,0)');
  path.style.setProperty('transform-origin', '0 0');
  // set d attribute
  path.setAttribute('d', d);
  // find root element
  let svg = path.closest('svg');
  if(svg){
    // remove element from parent
    path.parentNode.removeChild(path);
    // add element to root
    svg.appendChild(path);
  }
  return path;
}

// Process <use /> element
function  processUse(element) {
  // read attributes to object
  let attrs = readAttrs(element);
  // get href attribute value
  let sel = attrs.href;
  // find referenced element by href="#id"
  let ref = document.querySelector(sel);
  // clone referenced element
  let clone = ref.cloneNode(true);
  // remove id attribute from clone
  clone.removeAttribute('id');
  // remove href from attribute object
  delete attrs.href;
  // Create new <g>
  let group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  // write attributes from object
  writeAttrs(group, attrs);
  // add clone to <g>
  group.appendChild(clone);
  // replace <use> element to <g>
  element.replaceWith(group);
  return clone;
}


// Create matrix from transform attribute
// This fuction does not used.
// Use of getComputedStyle() completely replaces this implementation
function parseTransform (transform) {
  const rxTransformNameStr = '(?:(?:translate|scale|skew)[XY]?)|matrix|rotate';

  const rad = a => a / 57.29577951308232;

  const { sin, cos, tan } = Math;

  const matricies = {
    identity: () => [1, 0, 0, 1, 0, 0],
    translate: (tx, ty) => [1, 0, 0, 1, tx, ty],
    translateX: tx => [1, 0, 0, 1, tx, 0],
    translateY: ty => [1, 0, 0, 1, 0, ty],
    scale: (sx, sy = sx, cx = 0, cy = 0) => [sx, 0, 0, sy, cx * (1 - sx), cy * (1 - sy)],
    scaleX: (sx, cx = 0) => [sx, 0, 0, 1, cx * (1 - sx), 0],
    scaleY: (sy, cy = 0) => [1, 0, 0, sy, 0, cy * (1 - sy)],
    rotate: (a, cx = 0, cy = 0) => {
      let [s, c] = [sin(rad(a)), cos(rad(a))];
      return [c, s, -s, c, cx * (1 - c) + cy * s, cy * (1 - c) - cx * s];
    },
    skew: (ax, ay = ax) => [1, tan(rad(ay)), tan(rad(ax)), 1, 0, 0],
    skewX: a => [1, 0, tan(rad(a)), 1, 0, 0],
    skewY: a => [1, tan(rad(a)), 0, 1, 0, 0],
    matrix: (a, b, c, d, e, f) => [a, b, c, d, e, f],
  };

  let chain = [...transform.matchAll(new RegExp(`(${rxTransformNameStr})\\(([0-9eE,\\.\\s+-]+)\\)`, 'g'))].map(
    ([, fn, args]) => [fn, [...args.matchAll(new RegExp(rxNumberStr, 'g'))].map(([num]) => parseFloat(num))]
  );

  // For example,
  //    chain = [
  //      [ 'matrix', [ 1, 0, 0, 1, 0, 0 ] ],
  //      [ 'translate', [ 100, 100 ] ],
  //      [ 'rotate', [ 45 ] ],
  //      [ 'scale', [ 2, 2 ] ],
  //    ]

  chain = chain.map(([fn, args]) => matrices[fn](...args));

  // chain = [
  //   [ 1, 0, 0, 1, 0, 0 ],
  //   [ 1, 0, 0, 1, 100, 100 ],
  //   [ 0.7071067811865476, 0.7071067811865475, -0.7071067811865475, 0.7071067811865476, 0, 0 ],
  //   [ 2, 0, 0, 2, 0, 0 ]
  // ]

  // returns single matrix
  return consolidate(chain);
}

function groupPathList(pathList){
  let groupList = [];

  const compareStyleEquals = (s1,s2) => Object.entries(s1).every(([k,v])=>'d'==k?true:v==s2[k]);

  for(let path of pathList){
    let pathStyle = getComputedStyle(path);
    let groupFound = false;
    for(let group of groupList){
      if(compareStyleEquals(group.style,pathStyle)){
        group.list.push(path);
        groupFound = true;
        break;
      }
    }
    if(!groupFound){
      groupList.push({
        style: pathStyle,
        list: [path],
      });
    }
  }
  return groupList;
}

function ungroupPathList(groupList){
  let pathList = [];
  for(let group of groupList){
    let newPath = document.createElementNS('http://www.w3.org/2000/svg','path');
    Object.entries(getComputedStyle(group.list[0])).forEach(([k,v])=>{if(k!=='d')newPath.style.setProperty(k,v)});
    let d = group.list.map(path=>(path.parentNode.removeChild(path),path.getAttribute('d'))).join(' ');
    d = d.replace(/[0-9]+\.[0-9]+/g, num => parseFloat(num).toFixed(1));
    newPath.setAttribute('d', d);
    // this varian to run outside <svg>
    document.querySelector('svg').appendChild(newPath);
    // this variant to run inside <svg>
    //document.documentElement.appendChild(newPath);
    pathList.push(newPath);
  }
  return pathList;
}

setTimeout(()=>{

  // TODO: Parse <svg width="" height="" viewBox=""> to common CTM

  // Replace all <use /> elements to clone elements referenced by the <use>
  document.querySelectorAll('use').forEach(processUse);

  // Replace all shape elements to <path />
  document.querySelectorAll('line, rect, circle, ellipse, polygon, polyline').forEach(processShape);

  // Process all <path /> elements
  let pathList = [...document.querySelectorAll('path')].map(processPath).filter(Boolean);

  // Group all <path> into groups by style values
  let groupList = groupPathList(pathList);

  // Convert each group into single <path>
  pathList = ungroupPathList(groupList);

  // Output new values
  console.log(JSON.stringify(pathList.map(path=>new D(path.getAttribute('d')).rel.join(' ')),null,2));

},1000);
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">

  <g transform="translate(-100,-100)">

    <g id="hexagon" transform="translate(250,220) scale(1,.5)">
      <line id="line" x1="200" x2="500" y1="100" y2="100" stroke="#000" stroke-width="5" />
      <g transform="rotate(-120)" transform-origin="500 100">
        <use href="#line" />
        <g transform="rotate(-120)" transform-origin="200 100">
          <use href="#line" />
          <g transform="rotate(-120)" transform-origin="500 100">
            <use href="#line" />
            <g transform="rotate(-120)" transform-origin="200 100">
              <use href="#line" />
              <g transform="rotate(-120)" transform-origin="500 100">
                <use href="#line" />
              </g>
            </g>
          </g>
        </g>
      </g>
    </g>


    <g id="hexagon2" transform="translate(250,720) scale(1,.5)">
      <line id="line2" x1="200" x2="500" y1="100" y2="100" stroke="#000" stroke-width="5" />
      <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
        <use href="#line2" />
        <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
          <use href="#line2" />
          <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
            <use href="#line2" />
            <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
              <use href="#line2" />
              <g transform="rotate(60) translate(-300,0)" transform-origin="200 100">
                <use href="#line2" />
              </g>
            </g>
          </g>
        </g>
      </g>
    </g>

    <use href="#hexagon" transform="scale(1,-1) translate(0,-1200)" />

    <rect x="500" y="500" width="200" height="400" fill="#0008" stroke="#000" stroke-width="5" />
    <g transform="translate(-100,-100) skewY(-45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0004" stroke="#000" stroke-width="5" />
    </g>
    <g transform="translate(-100,-100) skewY(45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0008" stroke="#000" stroke-width="5" />
    </g>
    <g transform="translate(200,-200) skewY(45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0004" stroke="#000" stroke-width="5" />
    </g>
    <g transform="translate(200,0) skewY(-45)" transform-origin="500 500">
      <rect x="500" y="500" width="100" height="400" fill="#0008" stroke="#000" stroke-width="5" />
    </g>
    <rect x="500" y="300" width="200" height="400" fill="#0004" stroke="#000" stroke-width="5" />
  </g>

</svg>

© www.soinside.com 2019 - 2024. All rights reserved.