SVGキュービックベジェ曲線を表す次のパス(たとえば)を考えると、「M300,140C300,40,500,40,500,140」であり、終点300,140から500,140(曲線の下の領域を閉じる)を結ぶ直線を想定することは可能です。そのように囲まれた面積を計算するには?
誰かがこれを達成するための式(またはJavaScript)を提案できますか?
SVGキュービックベジェ曲線を表す次のパス(たとえば)を考えると、「M300,140C300,40,500,40,500,140」であり、終点300,140から500,140(曲線の下の領域を閉じる)を結ぶ直線を想定することは可能です。そのように囲まれた面積を計算するには?
誰かがこれを達成するための式(またはJavaScript)を提案できますか?
パスを任意精度のポリゴンに変換してから、ポリゴンの面積を計算します。
インタラクティブデモ:細分化によるパスの領域 (壊れた)
上記のデモでは、パスをポリゴンに適応的に細分化し、ポリゴンの面積を計算するための関数を使用しています。
// 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 (壊れた)
上記のデモでは、パスをポリゴンで近似し、ポリゴンの面積を計算するための関数を使用しています。
// 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;
}
コメントや完全な返信をするのをためらった。しかし、「エリアベジェ曲線」をグーグルで検索すると、最初の3つのリンク(最初のリンクはこの同じ投稿です)が次のように表示されます。
http://objectmix.com/graphics/133553-area-closed-bezier-curve.html(アーカイブ済み)
これは、発散定理を使用して、閉じた形の解を提供します。このリンクがOPによって検出されなかったことに驚いています。
ウェブサイトがダウンした場合に備えてテキストをコピーし、返信の作者にKalleRutanenのクレジットを付けます。
興味深い問題。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(dot(F(g(t))、perp(g'(t)))dt、t = 0..1)=(1/2)* int(dot(g(t)、perp(g' (t)))dt、t = 0..1)
perp(x、y)=(-y、x)
ここで、intは統合、'は微分、dotは内積を表します。積分は、滑らかな曲線セグメントに対応するパーツにつなぎ合わせる必要があります。
次に例を示します。ベジェ次数3と、制御点(x0、y0)、(x1、y1)、(x2、y2)、(x3、y3)を持つ1つのそのような曲線を取ります。この曲線の積分は次のとおりです。
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
シーケンス内の各曲線についてこれを計算し、それらを合計します。合計は、曲線で囲まれた領域です(曲線がループを形成すると仮定します)。
曲線が1つのベジェ曲線のみで構成されている場合は、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
私が間違いをしなかったことを望みます。
--Kalle
Rutanen
http://kaba.hilvi.org
同じ問題が発生しましたが、JavaScriptを使用していないため、受け入れられた@Phrogzの回答を使用できません。さらに、受け入れられた回答で使用されているものは、 MozillaSVGPathElement.getPointAtLength()
に従って非推奨になっています。
(x0/y0)
点、、、および(ここで(x1/y1)
、は始点と終点)を使用してベジェ曲線を記述する場合、パラメーター化された形式を使用できます。(x2/y2)
(x3/y3)
(x0/y0)
(x3/y3)
(出典:ウィキペディア)
B(t)はベジェ曲線上の点であり、P iはベジェ曲線を定義する点です(上記を参照、P 0は開始点です...)。tは、0≤t≤1の実行変数です。
この形式を使用すると、ベジェ曲線を非常に簡単に近似できます。t = i / nポイントを使用して、必要な数のポイントを生成できます。(開始点と終了点を追加する必要があることに注意してください)。結果はポリゴンです。次に、靴紐の公式(@Phrogzが彼のソリューションで行ったように)を使用して面積を計算できます。靴紐の処方者にとって、ポイントの順序が重要であることに注意してください。パラメータとしてtを使用することにより、順序は常に正しいものになります。
ここでの質問に一致させるのは、同じく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){
if(typeof bezier_points == "undefined" || bezier_points === null){
return [];
}
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 = splitPath(path);
var points = [];
var x = 0;
var y = 0;
for(index in path_segments){
if(path_segments[index]["type"] == "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;
}
/**
* Split the given path to the segments.
*
* @param path: SVGPathElement, the path
*
* @return object, the split path `d`
*/
function splitPath(path){
let d = path.getAttribute("d");
d = d.split(/\s*,|\s+/);
let segments = [];
let segment_names = {
"M": ["x", "y"],
"m": ["dx", "dy"],
"H": ["x"],
"h": ["dx"],
"V": ["y"],
"v": ["dy"],
"L": ["x", "y"],
"l": ["dx", "dy"],
"Z": [],
"C": ["x1", "y1", "x2", "y2", "x", "y"],
"c": ["dx1", "dy1", "dx2", "dy2", "dx", "dy"],
"S": ["x2", "y2", "x", "y"],
"s": ["dx2", "dy2", "dx", "dy"],
"Q": ["x1", "y1", "x", "y"],
"q": ["dx1", "dy1", "dx", "dy"],
"T": ["x", "y"],
"t": ["dx", "dy"],
"A": ["rx", "ry", "rotation", "large-arc", "sweep", "x", "y"],
"a": ["rx", "ry", "rotation", "large-arc", "sweep", "dx", "dy"]
};
let current_segment_type;
let current_segment_value;
let current_segment_index;
for(let i = 0; i < d.length; i++){
if(typeof current_segment_value == "number" && current_segment_value < segment_names[current_segment_type].length){
let segment_values = segment_names[current_segment_type];
segments[current_segment_index][segment_values[current_segment_value]] = d[i];
current_segment_value++;
}
else if(typeof segment_names[d[i]] !== "undefined"){
current_segment_index = segments.length;
current_segment_type = d[i];
current_segment_value = 0;
segments.push({"type": current_segment_type});
}
else{
delete current_segment_type;
delete current_segment_value;
delete current_segment_index;
}
}
return segments;
}
/**
* 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", "#cccc00");
polygon.setAttribute("opacity", "0.7");
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>
まず、私はベジェ曲線にあまり詳しくありませんが、それらが連続関数であることは知っています。三次曲線がそれ自体と交差しないことを確認する場合は、それを閉じた形で(つまり、解析積分を使用して)指定された囲み領域([ab])に統合し、端によって形成される三角形の面積を差し引くことができます。直線とX軸を結合します。ベジェ曲線と交差し、直線を結ぶ端の場合は、セクションに分割して、一貫した方法で各面積を個別に計算してみてください。
私にとって適切な検索用語は、「連続関数統合」「積分」「関数下の面積」「微積分」です。
もちろん、ベジェ曲線fnから離散データを生成し、離散XYデータを取得して、積分を概算することもできます。
Phrogzが受け入れた回答の解決策が好きですが、もう少し詳しく調べて、CompoundPath
クラスとarea
プロパティを使用してPaper.jsで同じことを行う方法を見つけました。私のPaper.jsデモを参照してください。
結果(表面積= 11856)は、しきい値0を使用した場合のPhrogzのデモとまったく同じですが、処理ははるかに高速に見えます。表面積を計算するためだけにPaper.jsをロードするのはやり過ぎですが、フレームワークの実装を検討している場合、またはPaper.jsがどのように実行するかを調査したい場合は...
Tを変更してデータポイントのセットを取得し、それを方程式に入力するだけで、ガウスの魔法の靴紐の定理のアプリケーションを使用できませんか?
これが簡単なビデオデモですhttps://www.youtube.com/watch?v=0KjG8Pg6LGk&ab_channel=Mathologer
そして、これがwikihttps ://en.wikipedia.org/wiki/Shoelace_formulaです。
2D平面内を移動する点の半径ベクトルでカバーされる正方形の面積は1/2*積分[(x-xc)* dy / dt-(y-yc)* dx /dt]dtです。ここで、xcとycは原点(中心)の座標です。ベジェ曲線の場合の導出はかなり面倒ですが、可能です。以下の関数squareAreaQuadrおよびsquareAreaCubicを参照してください。私はこれらの公式をテストして再テストしましたが、間違いがないことは確かです。このシグニチャは、SVG座標平面で時計回りに回転するための正の正方形の領域を提供します。
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();
<html>
<body>
<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>
</body>
</html>