Mapping SVG ARCTO to HTML Canvas ARCTO

2019-04-10 13:21发布

ARCTO in SVG specification is quite different from the one we have in Canvas. I have a use case where I will have the data as per SVG spec but I need to draw that on Canvas.

I tried this but I guess my geometry is weak. Can you please help?

4条回答
萌系小妹纸
2楼-- · 2019-04-10 13:29

The following code segment was extracted from the relevant section of Gabe Lerner's comprehensive CANVG package (see https://github.com/canvg/canvg) for any of you out there, who like me, might not want the whole nine yards of Gabe's package. Unlike the earlier solutions it is not an approximation, it is the exact equivalent of the SVG arc path element for which I would like to thank Gabe enormously.

One further point is that if you have already applied some scaling and/or translation to the canvas prior to the plotting of the path, you will need to factor this into the parameters of the two calls to Context.translate and also into the radius parameter of the call to Context.arc

function drawSVGarcOnCanvas (Context,lastX,lastY,rx,ry,xAxisRotation,largeArcFlag,sweepFlag,x,y)
{
    //--------------------
    // rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y
    // are the 6 data items in the SVG path declaration following the A
    //
    // lastX and lastY are the previous point on the path before the arc
    //--------------------
    // useful functions
    var m   = function (   v) {return Math.sqrt (Math.pow (v[0],2) + Math.pow (v[1],2))};
    var r   = function (u, v) {return ( u[0]*v[0] + u[1]*v[1]) / (m(u) * m(v))};
    var ang = function (u, v) {return ((u[0]*v[1] < u[1]*v[0])? -1 : 1) * Math.acos (r (u,v))};
    //--------------------

var currpX =  Math.cos (xAxisRotation) * (lastX - x) / 2.0 + Math.sin (xAxisRotation) * (lastY - y) / 2.0 ;
var currpY = -Math.sin (xAxisRotation) * (lastX - x) / 2.0 + Math.cos (xAxisRotation) * (lastY - y) / 2.0 ;

var l = Math.pow (currpX,2) / Math.pow (rx,2) + Math.pow (currpY,2) / Math.pow (ry,2);
if (l > 1) {rx *= Math.sqrt (l); ry *= Math.sqrt (l)};
var s = ((largeArcFlag == sweepFlag)? -1 : 1) * Math.sqrt 
      (( (Math.pow (rx,2) * Math.pow (ry    ,2)) - (Math.pow (rx,2) * Math.pow (currpY,2)) - (Math.pow (ry,2) * Math.pow (currpX,2))) 
       / (Math.pow (rx,2) * Math.pow (currpY,2) +   Math.pow (ry,2) * Math.pow (currpX,2)));
if (isNaN (s)) s = 0 ;

var cppX = s *  rx * currpY / ry ;
var cppY = s * -ry * currpX / rx ;
var centpX = (lastX + x) / 2.0 + Math.cos (xAxisRotation) * cppX - Math.sin (xAxisRotation) * cppY ;
var centpY = (lastY + y) / 2.0 + Math.sin (xAxisRotation) * cppX + Math.cos (xAxisRotation) * cppY ;

var ang1 = ang ([1,0], [(currpX-cppX)/rx,(currpY-cppY)/ry]);
var a = [(  currpX-cppX)/rx,(currpY-cppY)/ry];
var b = [(-currpX-cppX)/rx,(-currpY-cppY)/ry];
var angd = ang (a,b);
if (r (a,b) <= -1) angd = Math.PI;
if (r (a,b) >=  1) angd = 0;

var rad = (rx > ry)? rx : ry;
var sx  = (rx > ry)? 1 : rx / ry;
var sy  = (rx > ry)? ry / rx : 1;

Context.translate (centpX,centpY);
Context.rotate (xAxisRotation);
Context.scale (sx, sy);
Context.arc (0, 0, rad, ang1, ang1 + angd, 1 - sweepFlag);
Context.scale (1/sx, 1/sy);
Context.rotate (-xAxisRotation);
Context.translate (-centpX, -centpY);
};      
查看更多
一纸荒年 Trace。
3楼-- · 2019-04-10 13:31

When trying to map "M100,100 a25,50 -30 0,1 50,-25" to canvas use my function. Granted I wrote this with circlular arcs in mind.

ellipse(100,100,50,-25,50,false);

function ellipse(x1, y1, x2, y2, radius, clockwise) {

var cBx = (x1 + x2) / 2;    //get point between xy1 and xy2
var cBy = (y1 + y2) / 2;
var aB = Math.atan2(y1 - y2, x1 - x2);  //get angle to bulge point in radians
if (clockwise) { aB += (90 * (Math.PI / 180)); }
else { aB -= (90 * (Math.PI / 180)); }
var op_side = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) / 2;
var adj_side = Math.sqrt(Math.pow(radius, 2) - Math.pow(op_side, 2));

if (isNaN(adj_side)) {
    adj_side = Math.sqrt(Math.pow(op_side, 2) - Math.pow(radius, 2));
}

var Cx = cBx + (adj_side * Math.cos(aB));            
var Cy = cBy + (adj_side * Math.sin(aB));
var startA = Math.atan2(y1 - Cy, x1 - Cx);       //get start/end angles in radians
var endA = Math.atan2(y2 - Cy, x2 - Cx);
var mid = (startA + endA) / 2;
var Mx = Cx + (radius * Math.cos(mid));
var My = Cy + (radius * Math.sin(mid));
context.arc(Cx, Cy, radius, startA, endA, clockwise);
}
查看更多
迷人小祖宗
4楼-- · 2019-04-10 13:38

I had the same problem so I came across this post. The Implementation Requirements Appendix of the W3C SVG definition tells exactly how to convert form (they call it) end point parameterization to the center parameterization and back:

The SVG arc (end point parameterization) is described by:

  • x1/y1: The start position (the position of the last path command)
  • x2/y2: The end position of the arc (the x and y values of this path command)
  • rx/ry: The x and y radius
  • φ: The rotation angle
  • fA: The large arc flag (1 or 0, whether to use the larg or the small arc)
  • fS: The sweep flag (whether to go clockwise or anticlockwise)

The canvas arc uses (center point parameterization):

  • cx/cy: The center point of the ellipse
  • rx/ry: The x and y radius
  • φ: The rotation angle
  • θ1: The start angle of the ellipse (before rotation)
  • Δθ: The angle distance to use of the ellipse (direction depends on the sweep flag fS, you can also calculate the end point θ2 which may be even better)

Convert from SVG to Canvas

This means converting from SVG to canvas you can use the following equations (taken directly from the given url from W3C):

  1. Compute (x1′, y1′) (Equation F.6.5.1)

    Equation F.6.5.1

  2. Compute (cx′, cy′) (Equation F.6.5.2)

    enter image description here

    where the + sign is chosen if fA ≠ fS, and the − sign is chosen if fA = fS.

  3. Compute (cx, cy) from (cx′, cy′) (Equation F.6.5.3)

    enter image description here

  4. Compute θ1 and Δθ (Equations F.6.5.5 and F.6.5.6)

    Edit: I am now using other equations, have a look at the bottom

    enter image description here

    enter image description here

    where θ1 is fixed in the range −360° < Δθ < 360° such that:

    if fS = 0, then Δθ < 0,

    else if fS = 1, then Δθ > 0.

    In other words, if fS = 0 and the right side of (F.6.5.6) is greater than 0, then subtract 360°, whereas if fS = 1 and the right side of (F.6.5.6) is less than 0, then add 360°. In all other cases leave it as is.

Copyright © 16 August 2011 World Wide Web Consortium, (MIT, ERCIM, Keio, Beihang). http://www.w3.org/Consortium/Legal/2015/doc-license

Edit: Modified equations for step 4.

I am now using the following equations for determining θ1 and Δθ:

enter image description here

enter image description here

This is simply the vectors between the start and end point of the arc and the center point. The φ is subtracted because of the angle is calculated before rotation. You may just leave this away if needed.

I received wrong results of the given equations but this may also be a bug in my implementation. When trying to find the bug I was thinking about what W3C is doing here. I was looking on how to calculate the angles and this was the first thing I thought about. This is leading to the correct results for me.

Convert from Canvas to SVG

I also ran into problems when using the W3C equations when converting back. This may be because of changing the angles. For getting from Canvas to SVG you need to convert the start and end angles (θ1 and θ2 = θ1 + Δθ) together with the center point to the intersections of the arc. Those are the start and end points of the SVG arc.

  1. Compute (x1', y1') and (x2', y2')

    enter image description here

    This is calculating the intersection for the line which is defined by the given angle θ12 in the rotated coordinate system. For the x coordinate the + sign should be chosen, when the -π/2 ≤ θ ≤ π/2. The + sign for the y coordinate should be chosen when 0 ≤ θ ≤ π.

  2. Compute (x1, y1) and (x2, y2)

    enter image description here

    The x and y coordinate of the start and end points can then be calculated by rotating back the rotation angle φ and translating the vector to the center of the ellipse.

  3. Find the flags

    The flags can easily be determined: fA is 1 if Δθ is greater than 180°, fS is 1 if Δθ is greater than 0°.

查看更多
再贱就再见
5楼-- · 2019-04-10 13:40

The difference between svg ellipse and canvas arc is that you have 2 radiuses in svg and only one in arcTo. Then you also need to rotate your arc on specific angle in canvas. To emulate 2 radiuses you need to make an arc with the given coordinates having the smallest radius. Then you need to scale this arc in a specific direction with coefficient(rx/ry). And now you need only to rotate. But in this approach is really hard to figurate out which part of the ellipse you want to show because it depends on the large-arc-flag and sweep-flag in svg spec. Another problem is to limit your arc by end coordinates(from svg spec). So by arcTo you can build a maximum of half of an ellipse, I guess.

You also may use a bezierCurveTo(x0,y0,x1,y1,x2,y2) to draw a part of an ellipse, if you have coordinates of 3 control points on your ellipse. With this approach you can build any segment of an ellipse. Of course, for segments more than PI you will need at least two curves

From the SVG spec you have (rx ry x-axis-rotation large-arc-flag sweep-flag x y). So the sample path would be like that:

  M100,100 a25,50 -30 0,1 50,-25

Here you may find how bezier curves should be drawn.

Now you have a context point (which is 100,100), and an end point (which is 100+50,100-25) You need to calculate control points before rotation to -30 degrees.

Here the an example that works for me:

$(document).ready(function(){
        var startX = 100;
        var startY = 100;
        var dX = 50;
        var dY = -25;
        var angle = -30;
        var rx = 25;
        var ry = 50;
        var svg = Raphael($('#svg')[0], 200, 200);

        var path = "M" +startX + "," + startY + " a" + rx + "," + ry + " " + angle + " 0,1" + " " + dX + "," +dY;
        svg.path(path).attr({"stroke-width" : 2, "stroke" : "#FFFFFF"});

        var kappa = .5522848,
        ox = rx*kappa,
        oy = ry*kappa,
        xm = startX + rx,       // x-middle
        ym = startY + ry;       // y-middle
        var canvas = document.getElementById("canvas");
        var ctx = canvas.getContext("2d");
        ctx.moveTo(startX,startY);
        ctx.bezierCurveTo(startX, startY - oy, startX + ox, startY - ry, startX + rx, startY - ry);
        ctx.bezierCurveTo(startX + rx + ox, startY - ry, startX + 2*rx, startY - oy, startX + dX, startY + dY);
        ctx.stroke();
    });

markup is simply:

<div id="svg" style="border: 1px solid black;position : absolute;top : 50px;left : 50px;"></div>
<canvas id="canvas" width="200px" height="200px" style="border: 1px solid black;position : absolute;top : 300px;left : 50px;"></canvas>

the curves are not similar because I didnt rotate the control points to -30 degrees. But I believe that it is the only thing that you need to do. Because if you will put angle = 0. They will be similar You may use this article to get the mathematics for rotation.

PS: I took some parts of code from this answer

查看更多
登录 后发表回答