SVG:将弧线转换为三次贝塞尔曲线

11

我正在尝试做一些看起来很简单的事情:将SVG路径中的所有弧替换为三次贝塞尔曲线。

这个链接:http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes并没有帮助我,因为它并没有提到转换方面的内容。

我知道如何制作简单的弧,但是SVG弧确实有很多参数。

所以我需要的基本上只是一个算法,它接受:

rx ry x-axis-rotation large-arc-flag sweep-flag x y

(还有弧线的起点)

并计算:

x1 y1 x2 y2 x y

(当然,起点x和y保持相同的值......)
有人知道类似这样的东西吗?
提前感谢!:-)
6个回答

10
原来这是不可能的,至少从数学上讲是不行的。你不能用普通的三次贝塞尔曲线来绘制圆弧,总会有误差,所以根据圆弧的角度,你可能需要多于一个贝塞尔曲线。
话虽如此,对于四分之一圆弧,三次贝塞尔曲线表现得相当出色,因此如果你的圆弧小于这个角度,你可以使用单个曲线,如果你的圆弧更宽,你可以将其分成一个合理数量的曲线(介于四分之一和半圆之间?使用两条曲线。在半圆和三分之一之间?则使用三条曲线。完整圆?四条曲线。相当简单)。
那么,需要做多少工作呢?事实证明,如果你必须从头开始做:需要相当多的工作,但你可以直接跳转到“好的,我需要什么最终公式”然后就变得相对简单了。
如果我们有角度phi,则我们的圆弧的三次曲线近似值(假设我们将圆弧的起点与x轴对齐(使得圆弧从y=0开始,且逆时针运行),并且我们有一个弧半径R)为:
start coordinate = {
  x: R,
  y: 0
}

control point 1 = {
  x: R,
  y: R * 4/3 * tan( phi / 4)
}

control point 2 = {
  x: R * ( cos(phi) + 4/3 * tan( phi/4 ) * sin(phi) ),
  y: R * ( sin(phi) - 4/3 * tan( phi/4 ) * cos(phi) ),
}

end coordinate = {
  x: R * cos(phi),
  y: R * sin(phi)
}

数学!但实际上只是“输入角度,获得所需坐标”。简单易懂!

但是如果我们的弧不与x轴对齐怎么办呢?我们会使用愚蠢的“旋转+平移”方法来对齐我们的弧,然后在完成后按照相反的顺序运行旋转/平移。这里有解释


1
这看起来太简单了,为什么我们只是“对齐”弧的起点呢?它必须恰好在上一段结束的地方...此外,近似也是可以接受的,将其分割成几个部分的方法完全没问题 :) - Vogel Vogel
1
诀窍在于你不需要“精确”。在计算机屏幕上第二或第三位小数处偏差,这会四舍五入为整数,绝对是无关紧要的。如果你想使用这些特定的公式,你需要对齐弧线。如果不这样做,你将需要计算很多数学(请参见链接的Primer部分中的黑盒子),而这些可以通过RT非常高效地绕过,然后基于对齐的弧线抽象出s/c1/c2/e坐标,最后应用反向RT。 - Mike 'Pomax' Kamermans
1
好的 :) 不过,如果我有一个像(5,7)这样的点,我只需要进行平移(0,-7),就可以到达x轴。不需要旋转 :/ - Vogel Vogel
2
更复杂的是,弧线不是圆的一部分,而是椭圆。因此,它实际上有两个半径rx和ry。 然而,假设一个单一半径为3,像(5,7)这样的坐标可以简单地通过(-2, -7)来平移,变成(3, 0),并且没有必要进行旋转(?): - Vogel Vogel
3
为了进行椭圆映射,您需要找到长/短轴,沿着短轴将椭圆缩放为圆形,进行弧近似,然后沿着短轴再次进行缩放。需要多做一些工作。 - Mike 'Pomax' Kamermans
显示剩余3条评论

8
大多数SVG渲染库都需要这样做,因为2D图形库似乎不直接支持相对于X轴旋转的弧线。因此,您可以查找代码,比如在Batik中。或者查看我的SVG库中的arcTo()方法(也借鉴了Batik):AndroidSVG / SVGAndroidRenderer.java / line 2889。它是Java编写的,但应该很容易转换为JS。

链接已失效。 - Googlebot
新的URL地址:https://github.com/BigBadaboom/androidsvg/blob/master/androidsvg/src/main/java/com/caverock/androidsvg/utils/SVGAndroidRenderer.java - SimplSam
我已经更新了我的回答中的链接。谢谢SimplSam。 - Paul LeBeau

4
你可以查看下面的实现链接:

当使用polyfill时,你只需要从元素中调用getPathData

svgGeometryElement.getPathData( {normalize: true} )

实施基于下一个标准: W3.org


这些函数中的“_recursive”参数是什么?它们没有文档,有点难用。 - BadZen
当超过某个角度/弧度阈值时,recursive参数在内部使用。一个A圆弧命令几乎可以用一个命令画出一个完整的圆,而立方体近似需要4个线段/命令才能获得合理的精度。因此,您需要递归运行转换函数以获取多个C命令。请原谅我,我无法更优雅地解释它 - 我也一直在想这个问题 ;) - herrstrietzel
@Ievgen:值得一提的是:Jarek Foksa的polyfill...希望getPathData()的本地浏览器实现可以通过添加normalize参数来轻松应用此转换,例如svgGeometryElement.getPathData( {normalize: true} ) - herrstrietzel

3
你还可以查看来自SnapSVG的此函数,它可能已被Adobe Illustrator某种方式使用,用于将圆弧转换为三次贝塞尔曲线命令。它也在SVGO(优化器)中使用。
我仍在尝试解读它,但标准实际上对此很有帮助。

0

所选答案提供了部分解决圆弧问题的方案。在研究该主题时,找到了一个从 SVG路径提取出来的npm函数,可以确切地解决此问题,这里有一个快速codesandbox,希望能帮助到某些人。

arcpath.setAttribute("d", "M 0 200 A 100 20 0 1 0 100 200");
const options = {
  px: 0,
  py: 200,
  cx: 100,
  cy: 200,
  rx: 100,
  ry: 20,
  xAxisRotation: 0,
  largeArcFlag: 1,
  sweepFlag: 0
};

arc to cubic curves


0

立方近似与完美圆/椭圆元素

正如Mike 'Pomax' Kamermans和其他人所解释的那样,我们只能对圆形或椭圆弧进行近似

大多数转换函数/助手,如Dmitry Baranovskiy的snap.svg/raphael.js中的a2c(),将弧分割成每个90°的段,从而产生的渲染与原始弧不可视地区分。

然而,您可能偶尔需要更高的精度,例如长度或面积计算。

...所以对于上述应用,您可能根本不应该使用立方贝塞尔的粗略近似,而应该选择像<circle>这样的基本元素,或者使用A弧命令来绘制<path>元素,对吗?

<circle>这样的基本元素或使用A的路径可能不会返回更准确的值

原来,原生支持的浏览器方法 getTotalLength() 在应用于基本图形或 A arcto 路径命令时,返回的长度并不准确。
此外,不同浏览器之间存在明显的偏差。

// circle primitive
let rx = 20;
c1Math = 2 * Math.PI * rx;
c1Method = circle.getTotalLength();
cCircleMath.textContent = c1Math;
cCircleMethod.textContent = c1Method;
cCircleDiff.textContent = c1Math - c1Method;

// circle primitive
let ry = 30;
c2MathEllipse = getEllipseLength(rx, ry);
c2Method = ellipse.getTotalLength();
cEllipseMath.textContent = c2MathEllipse;
cEllipseMethod.textContent = c2Method;
cEllipseDiff.textContent = c2MathEllipse - c2Method;

//cubic simple
cMethod2C = pathCircleC.getTotalLength();
cCircleMethod2C.textContent = cMethod2C;
cCircleDiff2C.textContent = c1Math - cMethod2C;

cMethod2EllipseC = pathEllipseC.getTotalLength();
cEllipseMethod2C.textContent = cMethod2EllipseC;
cEllipseDiff2C.textContent = c2MathEllipse - cMethod2EllipseC;

//arcto
cMethod2 = pathCircle.getTotalLength();
cCircleMethod2.textContent = cMethod2;
cCircleDiff2.textContent = c1Math - cMethod2;

cMethod2Ellipse = pathEllipse.getTotalLength();
cEllipseMethod2.textContent = cMethod2Ellipse;
cEllipseDiff2.textContent = c2MathEllipse - cMethod2Ellipse;

//cubic
let options = {
  convertQuadratic: true,
  convertArc: true,
  unshort: true,
  arcAccuracy: 1
};
let pathDataCircle = parseDtoPathData(pathCircleCubic.getAttribute("d"));
pathDataCircle = normalizePathData(pathDataCircle, options);
pathCircleCubic.setAttribute("d", pathDataToD(pathDataCircle));
let cCircle3 = pathCircleCubic.getTotalLength();
cCircleMethod3.textContent = cCircle3;
cCircleDiff3.textContent = c1Math - cCircle3;

let pathDataEllipse = parseDtoPathData(pathEllipseCubic.getAttribute("d"));
pathDataEllipse = normalizePathData(pathDataEllipse, options);
pathEllipseCubic.setAttribute("d", pathDataToD(pathDataEllipse));

let cEllipse3 = pathEllipseCubic.getTotalLength();
cEllipseMethod3.textContent = cEllipse3;
cEllipseDiff3.textContent = c2MathEllipse - cEllipse3;

//HQ

options.arcAccuracy = 2;

let pathDataCircle2 = parseDtoPathData(pathCircleCubic2.getAttribute("d"));
pathDataCircle2 = normalizePathData(pathDataCircle2, options);
pathCircleCubic2.setAttribute("d", pathDataToD(pathDataCircle2));

let cCircle4 = pathCircleCubic2.getTotalLength();
cCircleMethod4.textContent = cCircle4;
cCircleDiff4.textContent = c1Math - cCircle4;


let pathDataEllipse2 = parseDtoPathData(pathEllipseCubic2.getAttribute("d"));
pathDataEllipse2 = normalizePathData(pathDataEllipse2, options);
pathEllipseCubic2.setAttribute("d", pathDataToD(pathDataEllipse2));

let cEllipse4 = pathEllipseCubic2.getTotalLength();
cEllipseMethod4.textContent = cEllipse4;
cEllipseDiff4.textContent = c2MathEllipse - cEllipse4;



/**
 * Ramanujan approximation
 * based on: https://www.mathsisfun.com/geometry/ellipse-perimeter.html#tool
 */
function getEllipseLength(rx, ry) {
  // is circle
  if (rx === ry) {
    //console.log('is circle')
    return 2 * Math.PI * rx;
  }
  let h = Math.pow((rx - ry) / (rx + ry), 2);
  let length =
    Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)));
  return length;
}
<style>
body{
    font-family:sans-serif;
}
svg{
  border:1px solid #ccc;
  overflow:visible;
}

.diff{
  color:red
}

path {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
  fill: transparent;
  fill: #000;
  stroke-width: 0.25% !important;
}
</style>

<h3>1. Primitives</h3>
<svg id="svg" viewBox="0 0 100 60"> 
  <circle id="circle" cx="30" cy="30" r="20"/>
  <ellipse id="ellipse" cx="75" cy="30" rx="20" ry="30"/>
</svg>

<p class="result">
Circumference Circle (Math): <span id="cCircleMath"></span>  <br />  
Circumference Circle (getTotallength): <span id="cCircleMethod"></span>  <br />
Difference: <span class="diff" id="cCircleDiff"></span>  <br />
Circumference ellipse (Math): <span id="cEllipseMath"></span>  <br />  
Circumference ellipse (getTotallength): <span id="cEllipseMethod"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff"></span>  <br />
</p>


<h3>2. Paths Arctos</h3>
<svg viewBox="0 0 100 60"> 
  <path id="pathCircle" d="M50 30A20 20 0 0 1 30 50A20 20 0 0 1 10 30A20 20 0 0 1 30 10A20 20 0 0 1 50 30Z"></path>
  <path id="pathEllipse" d="M95 30A20 30 0 0 1 75 60A20 30 0 0 1 55 30A20 30 0 0 1 75 0A20 30 0 0 1 95 30Z"></path>
</svg>
<p class="result">
Circumference Circle (getTotallength): <span id="cCircleMethod2"></span>  <br />
Difference: <span class="diff" id="cCircleDiff2"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod2"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff2"></span>  <br />
</p>



<h3>3.1 Paths Cubic: a2c(); Dmitry Baranovskiy</h3>
<svg viewBox="0 0 100 60"> 
  <path id="pathCircleC" d="M50 30C50 41.046 41.046 50 30 50C18.954 50 10 41.046 10 30C10 18.954 18.954 10 30 10C41.046 10 50 18.954 50 30Z"></path>
  <path id="pathEllipseC" d="M95 30C95 46.569 86.046 60 75 60C63.954 60 55 46.569 55 30C55 13.431 63.954 0 75 0C86.046 0 95 13.431 95 30Z"></path>
</svg>
<p class="result">
Circumference Circle (getTotallength): <span id="cCircleMethod2C"></span>  <br />
Difference: <span class="diff" id="cCircleDiff2C"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod2C"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff2C"></span>  <br />
</p>


<h3>3.2 Paths Cubic Beziers (k=0.551785 for 90°)</h3>
<svg  viewBox="0 0 100 60"> 
  <path id="pathCircleCubic" d="M50 30A20 20 0 0 1 30 50A20 20 0 0 1 10 30A20 20 0 0 1 30 10A20 20 0 0 1 50 30Z"></path>
  <path id="pathEllipseCubic" d="M95 30A20 30 0 0 1 75 60A20 30 0 0 1 55 30A20 30 0 0 1 75 0A20 30 0 0 1 95 30Z"></path>
</svg>

<p class="result">
Circumference Circle (getTotallength): <span id="cCircleMethod3"></span>  <br />
Difference: <span class="diff" id="cCircleDiff3"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod3"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff3"></span>  <br />
</p>


<h3>4. Paths Cubic Beziers - more segments</h3>
<svg  viewBox="0 0 100 60"> 
  <path id="pathCircleCubic2" d="M50 30A20 20 0 0 1 30 50A20 20 0 0 1 10 30A20 20 0 0 1 30 10A20 20 0 0 1 50 30Z"></path>
  <path id="pathEllipseCubic2" d="M95 30A20 30 0 0 1 75 60A20 30 0 0 1 55 30A20 30 0 0 1 75 0A20 30 0 0 1 95 30Z"></path>
</svg>

<p class="result">
Circumference Ellipse (getTotallength): <span id="cCircleMethod4"></span>  <br />
Difference: <span class="diff" id="cCircleDiff4"></span>  <br />
Circumference ellipse (getTotallength): <span id="cEllipseMethod4"></span>  <br />
Difference: <span class="diff" id="cEllipseDiff4"></span>  <br />
</p>


<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>

      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>


<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>

      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>


<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.parseToPathData.js"></script>

虽然人们可能期望使用“无损”<circle>A arcto表示来获得最佳结果,但更好的三次近似可能实际上会得到更准确的长度结果。
换句话说,更准确的基于几何的元素/符号的(预期的/理论上的)优势可能会被证明是具有欺骗性的。
Chrome对于圆形(r=20)的长度结果如下:
长度 结果
圆周长(2π*r) 125.66370614359172
圆周长(getTotallength) 124.85393524169922
差异 0.8097709018925059
Firefox更加精确。
长度 结果
圆周长(2π*r) 125.66370614359172
圆周长(getTotallength) 125.68115997314453
差异 -0.01745382955280661

自定义弧到三次贝塞尔曲线的辅助函数

根据贝塞尔曲线入门指南:§42 圆弧和三次贝塞尔曲线 以下辅助函数通过以下方式调整精度:

  • 将弧分割为更多/更小的片段
  • 通过使用优化的k常数来优化某些弧(例如四分之一圆)。

示例1:从点转换

/** 
 * convert arctocommands to cubic bezier
 * based on a2c.js
 * https://github.com/fontello/svgpath/blob/master/lib/a2c.js
 * returns pathData array
 */

function arcToBezier(p0, values, splitSegments = 1, quadratic = false) {
  p0 = Array.isArray(p0) ? {
    x: p0[0],
    y: p0[1]
  } : p0;
  const TAU = Math.PI * 2;
  let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;

  if (rx === 0 || ry === 0) {
    return []
  }

  let phi = rotation ? rotation * TAU / 360 : 0;
  let sinphi = phi ? Math.sin(phi) : 0
  let cosphi = phi ? Math.cos(phi) : 1
  let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2
  let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2

  if (pxp === 0 && pyp === 0) {
    return []
  }
  rx = Math.abs(rx)
  ry = Math.abs(ry)
  let lambda =
    pxp * pxp / (rx * rx) +
    pyp * pyp / (ry * ry)
  if (lambda > 1) {
    let lambdaRt = Math.sqrt(lambda);
    rx *= lambdaRt
    ry *= lambdaRt
  }


  /** 
   * parametrize arc to 
   * get center point start and end angles
   */
  let rxsq = rx * rx,
    rysq = rx === ry ? rxsq : ry * ry

  let pxpsq = pxp * pxp,
    pypsq = pyp * pyp
  let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)

  if (radicant <= 0) {
    radicant = 0
  } else {
    radicant /= (rxsq * pypsq) + (rysq * pxpsq)
    radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1)
  }

  let centerxp = radicant ? radicant * rx / ry * pyp : 0
  let centeryp = radicant ? radicant * -ry / rx * pxp : 0
  let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2
  let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2

  let vx1 = (pxp - centerxp) / rx
  let vy1 = (pyp - centeryp) / ry
  let vx2 = (-pxp - centerxp) / rx
  let vy2 = (-pyp - centeryp) / ry

  // get start and end angle
  const vectorAngle = (ux, uy, vx, vy) => {
    let dot = +(ux * vx + uy * vy).toFixed(9)
    if (dot === 1 || dot === -1) {
      return dot === 1 ? 0 : Math.PI
    }
    dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot)
    let sign = (ux * vy - uy * vx < 0) ? -1 : 1
    return sign * Math.acos(dot);
  }

  let ang1 = vectorAngle(1, 0, vx1, vy1),
    ang2 = vectorAngle(vx1, vy1, vx2, vy2)

  if (sweepFlag === 0 && ang2 > 0) {
    ang2 -= Math.PI * 2
  } else if (sweepFlag === 1 && ang2 < 0) {
    ang2 += Math.PI * 2
  }

  let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0)

  // increase segments for more accureate length calculations
  splitSegments = quadratic ? splitSegments * 2 : splitSegments;
  let segments = ratio * splitSegments;
  ang2 /= segments
  let pathData = [];


  // If 90 degree circular arc, use a constant
  // https://pomax.github.io/bezierinfo/#circles_cubic
  // k=0.551784777779014

  const angle90 = 1.5707963267948966;
  const k = 0.551785
  let a = ang2 === angle90 ? k :
    (
      ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
    );

  let cos2 = ang2 ? Math.cos(ang2) : 1;
  let sin2 = ang2 ? Math.sin(ang2) : 0;
  let type = !quadratic ? 'C' : 'Q';

  const approxUnitArc = (ang1, ang2, a, cos2, sin2) => {
    let x1 = ang1 != ang2 ? Math.cos(ang1) : cos2;
    let y1 = ang1 != ang2 ? Math.sin(ang1) : sin2;
    let x2 = Math.cos(ang1 + ang2);
    let y2 = Math.sin(ang1 + ang2);

    return [{
        x: x1 - y1 * a,
        y: y1 + x1 * a
      },
      {
        x: x2 + y2 * a,
        y: y2 - x2 * a
      },
      {
        x: x2,
        y: y2
      }
    ];
  }

  for (let i = 0; i < segments; i++) {
    let com = {
      type: type,
      values: []
    }
    let curve = approxUnitArc(ang1, ang2, a, cos2, sin2);

    curve.forEach((pt) => {
      let x = pt.x * rx
      let y = pt.y * ry
      com.values.push(cosphi * x - sinphi * y + centerx, sinphi * x + cosphi * y + centery)
    })

    //convert to quadratic
    if (quadratic) {
      let p = {
        x: com.values[4],
        y: com.values[5]
      }
      let cp1 = {
        x: (com.values[0] - p0.x) * (1 + c) + p0.x,
        y: (com.values[1] - p0.y) * (1 + c) + p0.y
      };
      com.values = [cp1.x, cp1.y, p.x, p.y]
      p0 = p
    }

    pathData.push(com);
    ang1 += ang2
  }

  return pathData;

}


/**
 * serialize pathData array to 
 * d attribute string 
 */
function pathDataToD(pathData, decimals = -1, minify = false) {
  // implicit l command
  if (pathData[1].type === "l" && minify) {
    pathData[0].type = "m";
  }
  let d = `${pathData[0].type}${pathData[0].values.join(" ")}`;

  for (let i = 1; i < pathData.length; i++) {
    let com0 = pathData[i - 1];
    let com = pathData[i];

    let type = (com0.type === com.type && minify) ?
      " " :
      ((com0.type === "m" && com.type === "l") ||
        (com0.type === "M" && com.type === "l") ||
        (com0.type === "M" && com.type === "L")) &&
      minify ?
      " " : com.type;

    // round
    if (decimals >= 0) {
      com.values = com.values.map(val => {
        return +val.toFixed(decimals)
      })
    }

    //type = com.type;
    d += `${type}${com.values.join(" ")}`;
  }

  d = minify ?
    d
    .replaceAll(" 0.", " .")
    .replaceAll(" -", "-")
    .replace(/\s+([A-Za-z])/g, "$1")
    .replaceAll("Z", "z") :
    d;
  return d;
}

/**
 * convert quadratic commands to cubic
 */
function pathDataQuadratic2Cubic(p0, com) {
  if (Array.isArray(p0)) {
    p0 = {
      x: p0[0],
      y: p0[1]
    }
  }
  let cp1 = {
    x: p0.x + 2 / 3 * (com[0] - p0.x),
    y: p0.y + 2 / 3 * (com[1] - p0.y)
  }
  let cp2 = {
    x: com[2] + 2 / 3 * (com[0] - com[2]),
    y: com[3] + 2 / 3 * (com[1] - com[3])
  }
  return ({
    type: "C",
    values: [cp1.x, cp1.y, cp2.x, cp2.y, com[2], com[3]]
  });
}
svg {
  border: 1px solid #ccc;
  overflow: visible;
}

.marker {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
}
<p>
  <br>Circumference: <span id="arcLength"></span>
  <br>Difference: <span id="diff"></span>
</p>

<svg viewBox="0 0 60 60">
  <path id="pathCircle" d="M 50 30
           A20 20 0 0 1 10 30
           " stroke="#000" fill="none" stroke-width="1%"></path>
  <path id="pathCircleCubic" class="marker" d="" stroke="red" fill="none" stroke-width="0.5%"></path>
  
<!-- markers to show commands -->
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>
      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>



<script>
  // starting point M
  let p0 = [50, 30]
  // Arc command
  let values = [20, 20, 20, 0, 1, 10, 30]

  // expected mathematical circuimference
  let r = 20
  let circumference = Math.PI * r
  let accuracy = 2


  window.addEventListener('DOMContentLoaded', e => {
    // get pathdata converting arc to cubic bezier
    let pathDataArc = arcToBezier(p0, values, accuracy);
    // apply
    let d = `M ${p0.join(' ')} ` + pathDataToD(pathDataArc)
    pathCircleCubic.setAttribute('d', d)


    let pathLength = +pathCircleCubic.getTotalLength().toFixed(5)
    arcLength.textContent = pathLength + ' / ' + circumference.toFixed(5)
    diff.textContent = circumference - pathLength

  })
</script>

例子2:从pathData转换

这个例子将前面的arcToBezier()函数封装在一个路径数据解析和规范化的辅助函数中。

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

.marker
 {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
}
<svg viewBox="0 0 100 100">
  <path id="pathEllipse" d="M95 30
                            A20 30 45 01 75 60
                            A20 30 45 11 95 30
                            Z" fill="none" stroke-width="1" stroke="#000"></path>

  <path id="pathEllipseC" class="marker" d="" fill="none" stroke="red" stroke-width="0.5"></path>
  <!-- markers to show commands -->
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green"></circle>
      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red"></circle>
      </marker>
  </defs>
</svg>

<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/svgHelpers@main/js/pathData.parseToPathData.js"></script>


<script>
  window.addEventListener('DOMContentLoaded', e => {
    //parse
    let d = pathEllipse.getAttribute('d')
    let pathData = parseDtoPathData(d);
    //cubic
    let options = {
      convertArc: true,
      // optional: split arc segments
      arcAccuracy: 1,
      // convert shorthands
      unshort: true,
    };
    pathData = normalizePathData(pathData, options);
    pathEllipseC.setAttribute('d', pathDataToD(pathData))
  })
</script>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接