Подтвердить что ты не робот

Как я могу вычислить площадь кривой безье?

Учитывая следующий путь (например), который описывает кубическую беззерновую кривую SVG: "M300,140C300,40,500,40,500,140", и предполагая, что прямая линия соединяет конечные точки 300,140 - 500,140 (закрывая область под кривой), можно ли вычислить площадь, заключенную таким образом?

Может ли кто-нибудь предложить формулу (или JavaScript) для этого?

4b9b3361

Ответ 1

Преобразовать путь к полигону произвольной точности, а затем вычислить область многоугольника.

Интерактивная демонстрация: Область пути через подразделение

            Screenshot of Demo

В своей основе вышеупомянутое демо использует функции для адаптивного разделения пути на многоугольник и вычисляет площадь многоугольника:

// path:      an SVG <path> element
// threshold: a 'close-enough' limit (ignore subdivisions with area less than this)
// segments:  (optional) how many segments to subdivisions to create at each level
// returns:   a new SVG <polygon> element
function pathToPolygonViaSubdivision(path,threshold,segments){
  if (!threshold) threshold = 0.0001; // Get really, really close
  if (!segments)  segments = 3;       // 2 segments creates 0-area triangles

  var points = subdivide( ptWithLength(0), ptWithLength( path.getTotalLength() ) );
  for (var i=points.length;i--;) points[i] = [points[i].x,points[i].y];

  var doc  = path.ownerDocument;
  var poly = doc.createElementNS('http://www.w3.org/2000/svg','polygon');
  poly.setAttribute('points',points.join(' '));
  return poly;

  // Record the distance along the path with the point for later reference
  function ptWithLength(d) {
    var pt = path.getPointAtLength(d); pt.d = d; return pt;
  }

  // Create segments evenly spaced between two points on the path.
  // If the area of the result is less than the threshold return the endpoints.
  // Otherwise, keep the intermediary points and subdivide each consecutive pair.
  function subdivide(p1,p2){
    var pts=[p1];
    for (var i=1,step=(p2.d-p1.d)/segments;i<segments;i++){
      pts[i] = ptWithLength(p1.d + step*i);
    }
    pts.push(p2);
    if (polyArea(pts)<=threshold) return [p1,p2];
    else {
      var result = [];
      for (var i=1;i<pts.length;++i){
        var mids = subdivide(pts[i-1], pts[i]);
        mids.pop(); // We'll get the last point as the start of the next pair
        result = result.concat(mids)
      }
      result.push(p2);
      return result;
    }
  }

  // Calculate the area of an polygon represented by an array of points
  function polyArea(points){
    var p1,p2;
    for(var area=0,len=points.length,i=0;i<len;++i){
      p1 = points[i];
      p2 = points[(i-1+len)%len]; // Previous point, with wraparound
      area += (p2.x+p1.x) * (p2.y-p1.y);
    }
    return Math.abs(area/2);
  }
}
// Return the area for an SVG <polygon> or <polyline>
// Self-crossing polys reduce the effective 'area'
function polyArea(poly){
  var area=0,pts=poly.points,len=pts.numberOfItems;
  for(var i=0;i<len;++i){
    var p1 = pts.getItem(i), p2=pts.getItem((i+-1+len)%len);
    area += (p2.x+p1.x) * (p2.y-p1.y);
  }
  return Math.abs(area/2);
}

Ниже приведен оригинальный ответ, в котором используется другой (неадаптивный) метод преобразования <path> в <polygon>.

Интерактивная демонстрация: http://phrogz.net/svg/area_of_path.xhtml

          Screenshot of Demo

В своей основе вышеприведенное демо использует функции приближение пути с полигоном и вычисляет площадь многоугольника.

// Calculate the area of an SVG polygon/polyline
function polyArea(poly){
  var area=0,pts=poly.points,len=pts.numberOfItems;
  for(var i=0;i<len;++i){
    var p1 = pts.getItem(i), p2=pts.getItem((i+len-1)%len);
    area += (p2.x+p1.x) * (p2.y-p1.y);
  }
  return Math.abs(area/2);
}

// Create a <polygon> approximation for an SVG <path>
function pathToPolygon(path,samples){
  if (!samples) samples = 0;
  var doc = path.ownerDocument;
  var poly = doc.createElementNS('http://www.w3.org/2000/svg','polygon');

  // Put all path segments in a queue
  for (var segs=[],s=path.pathSegList,i=s.numberOfItems-1;i>=0;--i)
    segs[i] = s.getItem(i);
  var segments = segs.concat();

  var seg,lastSeg,points=[],x,y;
  var addSegmentPoint = function(s){
    if (s.pathSegType == SVGPathSeg.PATHSEG_CLOSEPATH){

    }else{
      if (s.pathSegType%2==1 && s.pathSegType>1){
        x+=s.x; y+=s.y;
      }else{
        x=s.x; y=s.y;
      }          
      var last = points[points.length-1];
      if (!last || x!=last[0] || y!=last[1]) points.push([x,y]);
    }
  };
  for (var d=0,len=path.getTotalLength(),step=len/samples;d<=len;d+=step){
    var seg = segments[path.getPathSegAtLength(d)];
    var pt  = path.getPointAtLength(d);
    if (seg != lastSeg){
      lastSeg = seg;
      while (segs.length && segs[0]!=seg) addSegmentPoint( segs.shift() );
    }
    var last = points[points.length-1];
    if (!last || pt.x!=last[0] || pt.y!=last[1]) points.push([pt.x,pt.y]);
  }
  for (var i=0,len=segs.length;i<len;++i) addSegmentPoint(segs[i]);
  for (var i=0,len=points.length;i<len;++i) points[i] = points[i].join(',');
  poly.setAttribute('points',points.join(' '));
  return poly;
}

Ответ 2

Я колебался просто комментировать или полный ответ. Но простой поиск в Google по "кривой Безье" приводит к первым трем ссылкам (первая - это тот же пост):

http://objectmix.com/graphics/133553-area-closed-bezier-curve.htmlархиве)

что обеспечивает решение в замкнутой форме, используя теорему расходимости. Я удивлен, что эта ссылка не была найдена ОП.

Копирование текста на случай, если сайт закроется, и указание автора ответа Калле Рутанен:

Интересная проблема. Для любой кусочно дифференцируемой кривой в 2D следующая общая процедура дает вам область внутри кривой/серии кривых. Для полиномиальных кривых (кривых Безье) вы получите решения в замкнутой форме.

Пусть g (t) - кусочно-дифференцируемая кривая с 0 <= t <= 1. g (t) ориентирована по часовой стрелке и g (1) = g (0).

Пусть F (x, y) = [x, y]/2

Тогда div (F (x, y)) = 1, где div для дивергенции.

Теперь теорема о расходимости дает вам площадь внутри замкнутой кривой g (t) в виде интеграла по прямой вдоль кривой:

int (точка (F (g (t)), perp (g '(t))) dt, t = 0..1) = (1/2) * int (точка (g (t), perp (g') (t))) dt, t = 0..1)

perp (x, y) = (-y, x)

где int для интеграции, 'для дифференциации и точка для точечного продукта. Интеграция должна быть разделена на части, соответствующие сегментам гладкой кривой.

Теперь для примеров. Возьмем степень Безье 3 и одну такую кривую с контрольными точками (x0, y0), (x1, y1), (x2, y2), (x3, y3). Интеграл по этой кривой:

I: = 3/10 * y1 * x0 - 3/20 * y1 * x2 - 3/20 * y1 * x3 - 3/10 * y0 * x1 - 3/20 * y0 * x2 - 1/20 * y0 * x3 + 3/20 * y2 * x0 + 3/20 * y2 * x1 - 3/10 * y2 * x3 + 1/20 * y3 * x0 + 3/20 * y3 * x1 + 3/10 * y3 * x2

Рассчитайте это для каждой кривой в последовательности и сложите их. Сумма - это площадь, окруженная кривыми (при условии, что кривые образуют петлю).

Если кривая состоит только из одной кривой Безье, то она должна быть x3 = x0 и y3 = y0, а площадь равна:

Площадь: = 3/20 * y1 * x0 - 3/20 * y1 * x2 - 3/20 * y0 * x1 + 3/20 * y0 * x2 - 3/20 * y2 * x0 + 3/20 * y2 * x1

Надеюсь, я не делал ошибок.

-
Калле Рутанен
http://kaba.hilvi.org

Ответ 3

Во-первых, я не настолько знаком с кривыми Безье, но я знаю, что они являются непрерывными функциями. Если вы убедитесь, что кубическая кривая не пересекается сама по себе, вы можете интегрировать ее в замкнутой форме (я имею в виду с помощью аналитических интегралов) в данном охватывающем домене ([ab]) и вычесть площадь треугольника, которая формируется концом соединяющий прямую линию и ось X. В случае пересечения кривой Безье и конца, соединяющего прямую линию, вы можете разделить на секции и попытаться рассчитать каждую область отдельно согласованным образом.

Для меня подходящими поисковыми терминами являются "интеграция непрерывных функций" "интегралы" "площадь под функцией" "исчисление"

Конечно, вы можете генерировать дискретные данные с помощью кривой Безье fn и получать дискретные данные X-Y и вычислять интеграл приблизительно.

Descriptive drawing

Ответ 4

Мне нравится решение в принятом ответе Phrogz, но я также посмотрел немного дальше и нашел способ сделать то же самое с Paper.js, используя свойство класса и area CompoundPath. См. Мою демо-версию Paper.js.

Результат (площадь поверхности = 11856) является таким же, как с демографией Phrogz при использовании порога 0, но обработка кажется намного быстрее! Я знаю, что это слишком сложно загрузить Paper.js, чтобы рассчитать площадь поверхности, но если вы планируете внедрить фреймворк или почувствовать себя как исследование того, как Paper.js делает это...

Ответ 5

У меня была такая же проблема, но я не использую javascript, поэтому я не могу использовать принятый ответ @Phrogz. Кроме того, SVGPathElement.getPointAtLength() который используется в принятом ответе, не рекомендуется в соответствии с Mozilla.

При описании кривой Безье с точками (x0/y0), (x1/y1), (x2/y2) и (x3/y3) (где (x0/y0) - начальная точка, а (x3/y3) - конец пункт) вы можете использовать параметризованную форму:

Parametrized form of a cubic bezier curve (источник: Википедия)

где B (t) является точкой на кривой Безье, а P i - точкой, определяющей кривую Безье (см. выше, P 0 является начальной точкой,...). t - бегущая переменная с 0 ≤ t ≤ 1.

Эта форма упрощает аппроксимацию кривой Безье: вы можете создать столько точек, сколько захотите, используя t= i/n точек. (Обратите внимание, что вы должны добавить начало и конец). Результатом является многоугольник. Затем вы можете использовать формуляр для шнурка (как это сделал @Phrogz в своем решении), чтобы вычислить площадь. Обратите внимание, что для формуляра шнурка важен порядок точек. При использовании t в качестве параметра порядок всегда будет правильным.

Interactive demo preview

Для соответствия этому вопросу приведен интерактивный пример во фрагменте кода, также написанный на javascript. Это может быть принято на других языках. Он не использует никаких специфических команд javascript (или svg) (за исключением рисунков). Обратите внимание, что для этого требуется браузер, который поддерживает HTML5.

/**
 *  Approximate the bezier curve points.
 *
 *  @param bezier_points: object, the points that define the
 *                          bezier curve
 *  @param point_number:  int, the number of points to use to
 *                          approximate the bezier curve
 *
 *  @return Array, an array which contains arrays where the 
 *    index 0 contains the x and the index 1 contains the 
 *     y value as floats
 */
function getBezierApproxPoints(bezier_points, point_number){
  var approx_points = [];
  // add the starting point
  approx_points.push([bezier_points["x0"], bezier_points["y0"]]);
  
  // implementation of the bezier curve as B(t), for futher
  // information visit 
  // https://wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
  var bezier = function(t, p0, p1, p2, p3){
    return Math.pow(1 - t, 3) * p0 + 
      3 * Math.pow(1 - t, 2) * t * p1 + 
      3 * (1 - t) * Math.pow(t, 2) * p2 + 
      Math.pow(t, 3) * p3;
  };
  
  // Go through the number of points, divide the total t (which is 
  // between 0 and 1) by the number of points. (Note that this is 
  // point_number - 1 and starting at i = 1 because of adding the
  // start and the end points.)
  // Also note that using the t parameter this will make sure that 
  // the order of the points is correct.
  for(var i = 1; i < point_number - 1; i++){
    let t = i / (point_number - 1);
    approx_points.push([
      // calculate the value for x for the current t
      bezier(
        t, 
        bezier_points["x0"], 
        bezier_points["x1"], 
        bezier_points["x2"], 
        bezier_points["x3"]
      ),
      // calculate the y value
      bezier(
        t, 
        bezier_points["y0"], 
        bezier_points["y1"], 
        bezier_points["y2"], 
        bezier_points["y3"]
      )
    ]);
  }
  
  // Add the end point. Note that it is important to do this 
  // **after** the other points. Otherwise the polygon will 
  // have a weird form and the shoelace formular for calculating
  // the area will get a weird result.
  approx_points.push([bezier_points["x3"], bezier_points["y3"]]);
  
  return approx_points;
}

/**
 *  Get the bezier curve values of the given path.
 *
 *  The returned array contains objects where each object 
 *  describes one cubic bezier curve. The x0/y0 is the start 
 *  point and the x4/y4 is the end point. x1/y1 and x2/y2 are 
 *  the control points.
 *
 *  Note that a path can also contain other objects than 
 *  bezier curves. Arcs, quadratic bezier curves and lines 
 *  are ignored.
 *
 *  @param svg:     SVGElement, the svg
 *  @param path_id: String, the id of the path element in the
 *                    svg
 *
 *  @return array, an array of plain objects where each 
 *    object represents one cubic bezier curve with the values 
 *    x0 to x4 and y0 to y4 representing the x and y 
 *    coordinates of the points
 */
function getBezierPathPoints(svg, path_id){
  var path = svg.getElementById(path_id);
  if(path === null || !(path instanceof SVGPathElement)){
    return [];
  }
  
  var path_segments = path.pathSegList;
  var points = [];
  
  var x = 0;
  var y = 0;
  for(index in path_segments){
    if(path_segments[index]["pathSegTypeAsLetter"] == "C"){
      let bezier = {};
      // start is the end point of the last element
      bezier["x0"] = x;
      bezier["y0"] = y;
      bezier["x1"] = path_segments[index]["x1"];
      bezier["y1"] = path_segments[index]["y1"];
      bezier["x2"] = path_segments[index]["x2"];
      bezier["y2"] = path_segments[index]["y2"];
      bezier["x3"] = path_segments[index]["x"];
      bezier["y3"] = path_segments[index]["y"];
      points.push(bezier);
    }
    
    x = path_segments[index]["x"];
    y = path_segments[index]["y"];
  }
  
  return points;
}

/**
 *  Calculate the area of a polygon. The pts are the 
 *  points which define the polygon. This is
 *  implementing the shoelace formular.
 *
 *  @param pts: Array, the points
 *
 *  @return float, the area
 */
function polyArea(pts){
  var area = 0;
  var n = pts.length;
  for(var i = 0; i < n; i++){
    area += (pts[i][1] + pts[(i + 1) % n][1]) * (pts[i][0] - pts[(i + 1) % n][0]);
  }
  return Math.abs(area / 2);
}

// only for the demo
(function(){
  document.getElementById('number_of_points').addEventListener('change', function(){
    var svg = document.getElementById("svg");
    var bezier_points = getBezierPathPoints(svg, "path");
    // in this example there is only one bezier curve
    bezier_points = bezier_points[0];

    // number of approximation points
    var approx_points_num = parseInt(this.value);
    var approx_points = getBezierApproxPoints(bezier_points, approx_points_num);

    var doc = svg.ownerDocument;

    // remove polygon
    var polygons;
    while((polygons = doc.getElementsByTagName("polygon")).length > 0){
      polygons[0].parentNode.removeChild(polygons[0]);
    }

    // remove old circles
    var circles;
    while((circles = doc.getElementsByTagName("circle")).length > 0){
      circles[0].parentNode.removeChild(circles[0]);
    }

    // add new circles and create polygon
    var polygon_points = [];
    for(var i = 0; i < approx_points.length; i++){
      let circle = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
      circle.setAttribute('cx', approx_points[i][0]);
      circle.setAttribute('cy', approx_points[i][1]);
      circle.setAttribute('r', 1);
      circle.setAttribute('fill', '#449944');
      svg.appendChild(circle);
      polygon_points.push(approx_points[i][0], approx_points[i][1]);
    }

    var polygon = doc.createElementNS('http://www.w3.org/2000/svg', 'polygon');
    polygon.setAttribute("points", polygon_points.join(" "));
    polygon.setAttribute("stroke", "transparent");
    polygon.setAttribute("fill", "#cccc0099");
    svg.appendChild(polygon);

    doc.querySelector("output[name='points']").innerHTML = approx_points_num;
    doc.querySelector("output[name='area']").innerHTML = polyArea(approx_points);
  });
  
  var event = new Event("change");
  document.getElementById("number_of_points").dispatchEvent(event);
})();
<html>
  <body>
    <div style="width: 100%; text-align: center;">
      <svg width="250px" height="120px" viewBox="-5 -5 45 30" id="svg">
        <path d="M 0 0 C 10 15 50 40 30 0 Z" fill="transparent" stroke="black" id="path" />
      </svg>
      <br />
      <input type="range" min="3" max="100" value="5" class="slider" id="number_of_points">
      <br />
      Approximating with 
      <output name="points" for="number_of_points"></output>
      points, area is
      <output name="area"></output>
    </div>
  </body>
</html>

Ответ 6

<html>
<body>
<!--Square area covered by radius vector of a point moving in 2D plane is 1/2*integral[(x-xc)*dy/dt - (y-yc)*dx/dt]dt .
Here xc and yc are coordinates of the origin point (center).
Derivation for the case of Bezier curves is rather cumbersome but possible.
See functions squareAreaQuadr and squareAreaCubic below.
I have tested and retested these formulae, rather sure, that there are no mistakes.
This signature gives positive square area for clockwise rotation in SVG coordinates plane.-->
<h1>Bezier square area</h1>
<p id="q">Quadratic: S = </p>

<svg  height="500" width="500">
<rect width="500" height="500" style="fill:none; stroke-width:2; stroke:black" />
<path id="quadr" fill="lightgray" stroke="red" stroke-width="1" />
<circle id="q_center" r="5" fill="black" />
</svg>

<script>
var xc=0.1, yc=0.2, x0=0.9, y0=0.1, x1=0.9, y1=0.9, x2=0.1, y2=0.9;
var quadr = document.getElementById("quadr");
quadr.setAttribute("d", "M "+xc*500+" "+yc*500+" L "+x0*500+" "+y0*500+" Q "+x1*500+" "+y1*500+" "+x2*500+" "+y2*500+" L "+xc*500+" "+yc*500);
var center = document.getElementById("q_center");
q_center.setAttribute("cx", xc*500);
q_center.setAttribute("cy", yc*500);

function squareAreaQuadr(xc, yc, x0, y0, x1, y1, x2, y2)
    {
    var s = 1/2*( (x0-xc)*(y1-y0) + (x2-xc)*(y2-y1) - (y0-yc)*(x1-x0) - (y2-yc)*(x2-x1) ) +
    1/12*( (x2-x0)*(2*y1-y0-y2) - (y2-y0)*(2*x1-x0-x2) );
    return s;
    }

var s = squareAreaQuadr(xc, yc, x0, y0, x1, y1, x2, y2);
document.getElementById("q").innerHTML = document.getElementById("q").innerHTML + s.toString();
</script>

<p id="c">Cubic: S = </p>

<svg  height="500" width="500">
<rect width="500" height="500" style="fill:none; stroke-width:2; stroke:black" />
<path id="cubic" fill="lightgray" stroke="red" stroke-width="1" />
<circle id="center1" r="5" fill="black" />
</svg>

<script>
var xc=0.1, yc=0.2, x0=0.9, y0=0.1, x1=0.9, y1=0.9, x2=0.5, y2=0.5, x3=0.1, y3=0.9
var cubic = document.getElementById("cubic");
cubic.setAttribute("d", "M "+xc*500+" "+yc*500+" L "+x0*500+" "+y0*500+" C "+x1*500+" "+y1*500+" "+x2*500+" "+y2*500+" "+x3*500+" "+y3*500+" L "+xc*500+" "+yc*500);
var center1 = document.getElementById("center1");
center1.setAttribute("cx", xc*500);
center1.setAttribute("cy", yc*500);

function squareAreaCubic(xc, yc, x0, y0, x1, y1, x2, y2, x3, y3)
    {
    var s;
    s = 3/4*( (x0-xc)*(y1-y0) + (x3-xc)*(y3-y2) ) +
    1/4*(x3-x0)*(y1+y2-y0-y3) +
    1/8*( (x0+x3-2*xc)*(3*y2-3*y1+y0-y3) + (x1+x2-x0-x3)*(y1-y0+y3-y2) ) +
    3/40*( (2*x1-x0-x2)*(y1-y0) + (2*x2-x1-x3)*(y3-y2) ) +
    1/20*( (2*x1-x0-x2)*(y3-y2) + (2*x2-x1-x3)*(y1-y0) + (x1+x2-x0-x3)*(3*y2-3*y1+y0-y3) ) +
    1/40*(x1+x2-x0-x3)*(3*y2-3*y1+y0-y3) -
    3/4*( (y0-yc)*(x1-x0) + (y3-yc)*(x3-x2) ) -
    1/4*(y3-y0)*(x1+x2-x0-x3) -
    1/8*( (y0+y3-2*yc)*(3*x2-3*x1+x0-x3) + (y1+y2-y0-y3)*(x1-x0+x3-x2) ) -
    3/40*( (2*y1-y0-y2)*(x1-x0) + (2*y2-y1-y3)*(x3-x2) ) -
    1/20*( (2*y1-y0-y2)*(x3-x2) + (2*y2-y1-y3)*(x1-x0) + (y1+y2-y0-y3)*(3*x2-3*x1+x0-x3) ) -
    1/40*(y1+y2-y0-y3)*(3*x2-3*x1+x0-x3) ;
    return s;
    }

var s = squareAreaCubic(xc, yc, x0, y0, x1, y1, x2, y2, x3, y3);
document.getElementById("c").innerHTML = document.getElementById("c").innerHTML + s.toString();
</script>

</body>
</html>