mirror of
https://github.com/Pomax/BezierInfo-2.git
synced 2025-08-18 06:21:26 +02:00
experimental tangents and normals
This commit is contained in:
@@ -12,7 +12,7 @@ So: if we have have three points, we have three (different) chords, and conseque
|
||||
|
||||
The following graphic shows this procedure with a different colour for each chord and its associated perpendicular through the center. You can move the points around as much as you like, those lines will always meet!
|
||||
|
||||
<Graphic preset="simple" title="Finding a circle through three points" setup={this.setupCircle} draw={this.drawCircle} />
|
||||
<Graphic title="Finding a circle through three points" setup={this.setupCircle} draw={this.drawCircle} />
|
||||
|
||||
So, with the procedure on how to find a circle through three points, finding the arc through those points is straight-forward: pick one of the three points as start point, pick another as an end point, and the arc has to necessarily go from the start point, over the remaining point, to the end point.
|
||||
|
||||
@@ -40,10 +40,10 @@ The result of this is shown in the next graphic: we start at a guaranteed failur
|
||||
|
||||
The following graphic shows the result of this approach, with a default error threshold of 0.5, meaning that if an arc is off by a <em>combined</em> half pixel over both verification points, then we treat the arc as bad. This is an extremely simple error policy, but already works really well. Note that the graphic is still interactive, and you can use your up and down arrow keys keys to increase or decrease the error threshold, to see what the effect of a smaller or larger error threshold is.
|
||||
|
||||
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawSingleArc} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
With that in place, all that's left now is to "restart" the procedure by treating the found arc's end point as the new to-be-determined arc's starting point, and using points further down the curve. We keep trying this until the found end point is for <em>t=1</em>, at which point we are done. Again, the following graphic allows for up and down arrow key input to increase or decrease the error threshold, so you can see how picking a different threshold changes the number of arcs that are necessary to reasonably approximate a curve:
|
||||
|
||||
<Graphic preset="simple" title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawArcs} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic title="Arc approximation of a Bézier curve" setup={this.setupCubic} draw={this.drawArcs} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
So... what is this good for? Obviously, If you're working with technologies that can't do curves, but can do lines and circles, then the answer is pretty straight-forward, but what else? There are some reasons why you might need this technique: using circular arcs means you can determine whether a coordinate lies "on" your curve really easily: simply compute the distance to each circular arc center, and if any of those are close to the arc radii, at an angle betwee the arc start and end: bingo, this point can be treated as lying "on the curve". Another benefit is that this approximation is "linear": you can almost trivially travel along the arcs at fixed speed. You can also trivially compute the arc length of the approximated curve (it's a bit like curve flattening). The only thing to bear in mind is that this is a lossy equivalence: things that you compute based on the approximation are guaranteed "off" by some small value, and depending on how much precision you need, arc approximation is either going to be super useful, or completely useless. It's up to you to decide which, based on your application!
|
||||
|
@@ -98,4 +98,4 @@ We can program that pretty easily, provided we have that *f(t)* available, which
|
||||
|
||||
If we use the Legendre-Gauss values for our *C* values (thickness for each strip) and *t* values (location of each strip), we can determine the approximate length of a Bézier curve by computing the Legendre-Gauss sum. The following graphic shows a cubic curve, with its computed lengths; Go ahead and change the curve, to see how its length changes. One thing worth trying is to see if you can make a straight line, and see if the length matches what you'd expect. What if you form a line with the control points on the outside, and the start/end points on the inside?
|
||||
|
||||
<Graphic preset="simple" title="Arc length for a Bézier curve" setup={this.setupCurve} draw={this.drawCurve}/>
|
||||
<Graphic title="Arc length for a Bézier curve" setup={this.setupCurve} draw={this.drawCurve}/>
|
||||
|
@@ -10,7 +10,7 @@ If we have the extremities, and the start/end points, a simple for loop that tes
|
||||
|
||||
Applying this approach to our previous root finding, we get the following bounding boxes (with all curve extremity points shown on the curve):
|
||||
|
||||
<Graphic preset="simple" title="Quadratic Bézier bounding box" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="simple" title="Cubic Bézier bounding box" setup={this.setupCubic} draw={this.draw} />
|
||||
<Graphic title="Quadratic Bézier bounding box" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic title="Cubic Bézier bounding box" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
We can construct even nicer boxes by aligning them along our curve, rather than along the x- and y-axis, but in order to do so we first need to look at how aligning works.
|
||||
|
@@ -6,7 +6,7 @@ As it so happens, the answer is yes and the solution we're going to look at was
|
||||
|
||||
The first observation that makes things work is that if we have a cubic curve with four points, we can apply a linear transformation to these points such that three of the points end up on (0,0), (0,1) and (1,1), with the last point then being "somewhere". After applying that transformation, the location of that last point can then tell us what kind of curve we're dealing with. Specifically, we see the following breakdown:
|
||||
|
||||
<Graphic static={true} preset="simple" title="The canonical curve map" setup={this.setup} draw={this.drawBase} />
|
||||
<Graphic static={true} title="The canonical curve map" setup={this.setup} draw={this.drawBase} />
|
||||
|
||||
This is a fairly funky image, so let's see how it breaks down. We see the three fixed points at (0,0), (0,1) and (1,1), and then the fourth point is somewhere. Depending on where it is, our curve will have certain features. Namely, if the fourth point is...
|
||||
|
||||
@@ -280,4 +280,4 @@ Now, I know, you're thinking "but Mathematica is super expensive!" and that's tr
|
||||
|
||||
So, let's write up a sketch that'll show us the canonical form for any curve drawn in blue, overlaid on our canonical map, so that we can immediately tell which features our curve must have, based on where the fourth coordinate is located on the map:
|
||||
|
||||
<Graphic preset="simple" title="A cubic curve mapped to canonical form" setup={this.setup} draw={this.draw} />
|
||||
<Graphic title="A cubic curve mapped to canonical form" setup={this.setup} draw={this.draw} />
|
||||
|
@@ -177,4 +177,4 @@ Which, in decimal values, rounded to six significant digits, is:
|
||||
|
||||
Of course, this is for a circle with radius 1, so if you have a different radius circle, simply multiply the coordinate by the radius you need. And then finally, forming a full curve is now a simple a matter of mirroring these coordinates about the origin:
|
||||
|
||||
<Graphic preset="simple" title="Cubic Bézier circle approximation" draw={this.drawCircle} static={true}/>
|
||||
<Graphic title="Cubic Bézier circle approximation" draw={this.drawCircle} static={true}/>
|
||||
|
@@ -8,5 +8,5 @@ Let's look at how a parametric Bézier curve "splits up" into two normal functio
|
||||
|
||||
If you move points in a curve sideways, you should only see the middle graph change; likely, moving points vertically should only show a change in the right graph.
|
||||
|
||||
<Graphic preset="simple" title="Quadratic Bézier curve components" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic preset="simple" title="Cubic Bézier curve components" setup={this.setupCubic} draw={this.draw}/>
|
||||
<Graphic title="Quadratic Bézier curve components" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic title="Cubic Bézier curve components" setup={this.setupCubic} draw={this.draw}/>
|
||||
|
@@ -5,9 +5,9 @@ Bézier curves are (like all "splines") interpolation functions, meaning they ta
|
||||
The following graphs show the interpolation functions for quadratic and cubic curves, with "S" being the strength of a point's contribution to the total sum of the Bézier function. Click or click-drag to see the interpolation percentages for each curve-defining point at a specific <i>t</i> value.
|
||||
|
||||
<div className="figure">
|
||||
<Graphic inline={true} preset="simple" title="Quadratic interpolations" draw={this.drawQuadraticLerp}/>
|
||||
<Graphic inline={true} preset="simple" title="Cubic interpolations" draw={this.drawCubicLerp}/>
|
||||
<Graphic inline={true} preset="simple" title="15th order interpolations" draw={this.draw15thLerp}/>
|
||||
<Graphic inline={true} title="Quadratic interpolations" draw={this.drawQuadraticLerp}/>
|
||||
<Graphic inline={true} title="Cubic interpolations" draw={this.drawCubicLerp}/>
|
||||
<Graphic inline={true} title="15th order interpolations" draw={this.draw15thLerp}/>
|
||||
</div>
|
||||
|
||||
Also shown is the interpolation function for a 15<sup>th</sup> order Bézier function. As you can see, the start and end point contribute considerably more to the curve's shape than any other point in the control point set.
|
||||
@@ -34,7 +34,7 @@ That looks complicated, but as it so happens, the "weights" are actually just th
|
||||
|
||||
Which gives us the curve we saw at the top of the article:
|
||||
|
||||
<Graphic preset="simple" title="Our cubic Bézier curve" setup={this.drawCubic} draw={this.drawCurve}/>
|
||||
<Graphic title="Our cubic Bézier curve" setup={this.drawCubic} draw={this.drawCurve}/>
|
||||
|
||||
What else can we do with Bézier curves? Quite a lot, actually. The rest of this article covers a multitude of possible operations and algorithms that we can apply, and the tasks they achieve.
|
||||
|
||||
|
@@ -5,9 +5,9 @@
|
||||
下のグラフは、2次ベジエ曲線や3次ベジエ曲線の補間関数を表しています。ここでSは、ベジエ関数全体に対しての、その点の寄与の大きさを示します。ある<i>t</i>において、ベジエ曲線を定義する各点の補間率がどのようになっているのか、クリックやドラッグをして確かめてみてください。
|
||||
|
||||
<div className="figure">
|
||||
<Graphic inline={true} preset="simple" title="2次の補間" draw={this.drawQuadraticLerp}/>
|
||||
<Graphic inline={true} preset="simple" title="3次の補間" draw={this.drawCubicLerp}/>
|
||||
<Graphic inline={true} preset="simple" title="15次の補間" draw={this.draw15thLerp}/>
|
||||
<Graphic inline={true} title="2次の補間" draw={this.drawQuadraticLerp}/>
|
||||
<Graphic inline={true} title="3次の補間" draw={this.drawCubicLerp}/>
|
||||
<Graphic inline={true} title="15次の補間" draw={this.draw15thLerp}/>
|
||||
</div>
|
||||
|
||||
あわせて、15次ベジエ関数における補間関数も示しています。始点と終点は他の制御点と比較して、曲線の形に対してかなり大きな影響を与えていることがわかります。
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
この式からは、記事の冒頭に出てきた曲線が得られます。
|
||||
|
||||
<Graphic preset="simple" title="あの3次ベジエ曲線" setup={this.drawCubic} draw={this.drawCurve}/>
|
||||
<Graphic title="あの3次ベジエ曲線" setup={this.drawCubic} draw={this.drawCurve}/>
|
||||
|
||||
ベジエ曲線で、他にはどんなことができるでしょうか?実は、非常にたくさんのことが可能です。この記事の残りの部分では、実現可能な各種操作や適用可能なアルゴリズム、そしてこれによって達成できるタスクについて扱います。
|
||||
|
||||
|
@@ -5,9 +5,9 @@
|
||||
下面的图形显示了二次曲线和三次曲线的差值方程,“S”代表了点对贝塞尔方程总和的贡献。点击或拖动点来看看在特定的<i>t</i>值时,每个曲线定义的点的插值百分比。
|
||||
|
||||
<div className="figure">
|
||||
<Graphic inline={true} preset="simple" title="二次插值" draw={this.drawQuadraticLerp}/>
|
||||
<Graphic inline={true} preset="simple" title="三次插值" draw={this.drawCubicLerp}/>
|
||||
<Graphic inline={true} preset="simple" title="15次插值" draw={this.draw15thLerp}/>
|
||||
<Graphic inline={true} title="二次插值" draw={this.drawQuadraticLerp}/>
|
||||
<Graphic inline={true} title="三次插值" draw={this.drawCubicLerp}/>
|
||||
<Graphic inline={true} title="15次插值" draw={this.draw15thLerp}/>
|
||||
</div>
|
||||
|
||||
上面有一张是15<sup>th</sup>阶的插值方程。如你所见,在所有控制点中,起点和终点对曲线形状的贡献比其他点更大些。
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
这就是我们在文章开头看到的曲线:
|
||||
|
||||
<Graphic preset="simple" title="我们的三次贝塞尔曲线" setup={this.drawCubic} draw={this.drawCurve}/>
|
||||
<Graphic title="我们的三次贝塞尔曲线" setup={this.drawCubic} draw={this.drawCurve}/>
|
||||
|
||||
我们还能对贝塞尔曲线做些什么?实际上还有很多。文章接下来涉及到我们可能运用到的一系列操作和算法,以及它们可以完成的任务。
|
||||
|
||||
|
@@ -51,4 +51,4 @@ function drawCurve(points[], t):
|
||||
|
||||
下の図にマウスを乗せると、この様子を実際に見ることができます。ド・カステリョのアルゴリズムによって曲線上の点を明示的に計算していますが、マウスを動かすと求める点が変わります。マウスカーソルを左から右へ(もちろん、右から左へでも)動かせば、このアルゴリズムによって曲線が生成される様子がわかります。
|
||||
|
||||
<Graphic preset="simple" title="ド・カステリョのアルゴリズムで曲線をたどる" setup={this.setup} draw={this.draw}/>
|
||||
<Graphic title="ド・カステリョのアルゴリズムで曲線をたどる" setup={this.setup} draw={this.draw}/>
|
||||
|
@@ -22,7 +22,7 @@ In the case of Bézier curves, extending the interval simply makes our curve "ke
|
||||
|
||||
The following two graphics show you Bézier curves rendered "the usual way", as well as the curves they "lie on" if we were to extend the `t` values much further. As you can see, there's a lot more "shape" hidden in the rest of the curve, and we can model those parts by moving the curve points around.
|
||||
|
||||
<Graphic preset="simple" title="Quadratic infinite interval Bézier curve" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="simple" title="Cubic infinite interval Bézier curve" setup={this.setupCubic} draw={this.draw} />
|
||||
<Graphic title="Quadratic infinite interval Bézier curve" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic title="Cubic infinite interval Bézier curve" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
In fact, there are curves used in graphics design and computer modelling that do the opposite of Bézier curves, where rather than fixing the interval, and giving you free coordinates, they fix the coordinates, but give you freedom over the interval. A great example of this is the ["Spiro" curve](http://levien.com/phd/phd.html), which is a curve based on part of a [Cornu Spiral, also known as Euler's Spiral](https://en.wikipedia.org/wiki/Euler_spiral). It's a very aesthetically pleasing curve and you'll find it in quite a few graphics packages like [FontForge](https://fontforge.github.io) and [Inkscape](https://inkscape.org), having even been used in font design (such as for the Inconsolata font).
|
||||
|
@@ -22,7 +22,7 @@
|
||||
|
||||
下の2つの図は「いつもの方法」で描いたベジエ曲線ですが、これと一緒に、`t`の値をずっと先まで広げた場合の「延びた」ベジエ曲線も表示しています。見てわかるように、曲線の残りの部分には多くの「かたち」が隠れています。そして曲線の点を動かせば、その部分の形状も変わります。
|
||||
|
||||
<Graphic preset="simple" title="無限区間の2次ベジエ曲線" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="simple" title="無限区間の3次ベジエ曲線" setup={this.setupCubic} draw={this.draw} />
|
||||
<Graphic title="無限区間の2次ベジエ曲線" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic title="無限区間の3次ベジエ曲線" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
実際に、グラフィックデザインやコンピュータモデリングで使われている曲線の中には、座標が固定されていて、区間は自由に動かせるような曲線があります。これは、区間が固定されていて、座標を自由に動かすことのできるベジエ曲線とは反対になっています。すばらしい例が[「Spiro」曲線](http://levien.com/phd/phd.html)で、これは[オイラー螺旋とも呼ばれるクロソイド曲線](https://ja.wikipedia.org/wiki/クロソイド曲線)の一部分に基づいた曲線です。非常に美しく心地よい曲線で、[FontForge](https://fontforge.github.io)や[Inkscape](https://inkscape.org/ja/)など多くのグラフィックアプリに実装されており、フォントデザインにも利用されています(Inconsolataフォントなど)。
|
||||
|
@@ -22,7 +22,7 @@
|
||||
|
||||
下面两个图形给你展示了以“普通方式”来渲染的贝塞尔曲线,以及如果我们扩大`t`值时它们所“位于”的曲线。如你所见,曲线的剩余部分隐藏了很多“形状”,我们可以通过移动曲线的点来建模这部分。
|
||||
|
||||
<Graphic preset="simple" title="二次无限区间贝塞尔曲线" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic preset="simple" title="三次无限区间贝塞尔曲线" setup={this.setupCubic} draw={this.draw} />
|
||||
<Graphic title="二次无限区间贝塞尔曲线" setup={this.setupQuadratic} draw={this.draw} />
|
||||
<Graphic title="三次无限区间贝塞尔曲线" setup={this.setupCubic} draw={this.draw} />
|
||||
|
||||
实际上,图形设计和计算机建模中还用了一些和贝塞尔曲线相反的曲线,这些曲线没有固定区间和自由的坐标,相反,它们固定座标但给你自由的区间。["Spiro"曲线](http://levien.com/phd/phd.html)就是一个很好的例子,它的构造是基于[羊角螺线,也就是欧拉螺线](https://zh.wikipedia.org/wiki/%E7%BE%8A%E8%A7%92%E8%9E%BA%E7%BA%BF)的一部分。这是在美学上很令人满意的曲线,你可以在一些图形包中看到它,比如[FontForge](https://fontforge.github.io)和[Inkscape](https://inkscape.org),它也被用在一些字体设计中(比如Inconsolata字体)。
|
||||
|
@@ -169,5 +169,5 @@ As it turns out, Newton-Raphson is so blindingly fast, so we could get away with
|
||||
|
||||
So now that we know how to do root finding, we can determine the first and second derivative roots for our Bézier curves, and show those roots overlaid on the previous graphics:
|
||||
|
||||
<Graphic preset="simple" title="Quadratic Bézier curve extremities" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic preset="simple" title="Cubic Bézier curve extremities" setup={this.setupCubic} draw={this.draw}/>
|
||||
<Graphic title="Quadratic Bézier curve extremities" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic title="Cubic Bézier curve extremities" setup={this.setupCubic} draw={this.draw}/>
|
||||
|
@@ -12,5 +12,5 @@ Like normal offsetting we cut up our curve in sub-curves, and then check at whic
|
||||
|
||||
At each of the relevant points (start, end, and the projections of the control points onto the curve) we know the curve's normal, so offsetting is simply a matter of taking our original point, and moving it along the normal vector by the offset distance for each point. Doing so will give us the following result (these have with a starting width of 0, and an end width of 40 pixels, but can be controlled with your up and down arrow keys):
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown}/>
|
||||
|
@@ -27,6 +27,7 @@ module.exports = {
|
||||
// information that can be obtained through analysis
|
||||
derivatives: require("./derivatives"),
|
||||
pointvectors: require("./pointvectors"),
|
||||
pointvectors3d: require("./pointvectors3d"),
|
||||
components: require("./components"),
|
||||
extremities: require("./extremities"),
|
||||
boundingbox: require("./boundingbox"),
|
||||
|
@@ -10,7 +10,7 @@ if we have two line segments with two coordinates each, segments A-B and C-D, we
|
||||
|
||||
The following graphic implements this intersection detection, showing a red point for an intersection on the lines our segments lie on (thus being a virtual intersection point), and a green point for an intersection that lies on both segments (being a real intersection point).
|
||||
|
||||
<Graphic preset="simple" title="Line/line intersections" setup={this.setupLines} draw={this.drawLineIntersection} />
|
||||
<Graphic title="Line/line intersections" setup={this.setupLines} draw={this.drawLineIntersection} />
|
||||
|
||||
<div className="howtocode">
|
||||
|
||||
@@ -44,7 +44,7 @@ lli = function(line1, line2):
|
||||
|
||||
Curve/line intersection is more work, but we've already seen the techniques we need to use in order to perform it: first we translate/rotate both the line and curve together, in such a way that the line coincides with the x-axis. This will position the curve in a way that makes it cross the line at points where its y-function is zero. By doing this, the problem of finding intersections between a curve and a line has now become the problem of performing root finding on our translated/rotated curve, as we already covered in the section on finding extremities.
|
||||
|
||||
<Graphic preset="simple" title="Quadratic curve/line intersections" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic preset="simple" title="Cubic curve/line intersections" setup={this.setupCubic} draw={this.draw}/>
|
||||
<Graphic title="Quadratic curve/line intersections" setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic title="Cubic curve/line intersections" setup={this.setupCubic} draw={this.draw}/>
|
||||
|
||||
Curve/curve intersection, however, is more complicated. Since we have no straight line to align to, we can't simply align one of the curves and be left with a simple procedure. Instead, we'll need to apply two techniques we've not covered yet: de Casteljau's algorithm, and curve splitting.
|
||||
|
@@ -59,7 +59,7 @@ A good way to do this reduction is to first find the curve's extreme points, as
|
||||
|
||||
The following graphics show off curve offsetting, and you can use your up and down arrow keys to control the distance at which the curve gets offset. The curve first gets reduced to safe segments, each of which is then offset at the desired distance. Especially for simple curves, particularly easily set up for quadratic curves, no reduction is necessary, but the more twisty the curve gets, the more the curve needs to be reduced in order to get segments that can safely be scaled.
|
||||
|
||||
<Graphic preset="simple" title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic preset="simple" title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic title="Offsetting a quadratic Bézier curve" setup={this.setupQuadratic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic title="Offsetting a cubic Bézier curve" setup={this.setupCubic} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
You may notice that this may still lead to small 'jumps' in the sub-curves when moving the curve around. This is caused by the fact that we're still performing a naive form of offsetting, moving the control points the same distance as the start and end points. If the curve is large enough, this may still lead to incorrect offsets.
|
||||
|
@@ -73,6 +73,6 @@ treated as a sequence of three (elementary) shear operations. When we combine th
|
||||
The following two graphics show the tangent and normal along a quadratic and cubic curve, with the direction vector coloured blue, and the normal vector coloured red (the markers are spaced out evenly as *t*-intervals, not spaced equidistant).
|
||||
|
||||
<div className="figure">
|
||||
<Graphic preset="simple" title="Quadratic Bézier tangents and normals" inline={true} setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic preset="simple" title="Cubic Bézier tangents and normals" inline={true} setup={this.setupCubic} draw={this.draw}/>
|
||||
<Graphic title="Quadratic Bézier tangents and normals" inline={true} setup={this.setupQuadratic} draw={this.draw}/>
|
||||
<Graphic title="Cubic Bézier tangents and normals" inline={true} setup={this.setupCubic} draw={this.draw}/>
|
||||
</div>
|
||||
|
68
components/sections/pointvectors3d/content.en-GB.md
Normal file
68
components/sections/pointvectors3d/content.en-GB.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Getting 3D normals
|
||||
|
||||
Before we move on to the next section we need to spend a little bit of time on the difference between 2D and 3D, because while for many things this difference is irrelevant and the procedures are identical (for instance, getting the 3D tangent is just doing what we do for 2D, but for x, y, and z, instead of just for x and y), when it comes to normals things are a little more complex, and thus more work. Mind you, it's not "super hard", but there are more steps involved and we should have a look at those.
|
||||
|
||||
Getting normals in 3D is in principle the same as in 2D: we need to take the normalised tangent vector, and then rotate it by a quarter turn. However, this is where things get that little more complex: we can turn in quite a few directions, so we need to restrict the rotation to the plane that the tangent lies on. That might sound strange: tangents are themselves lines and lines simultaneously lie on an infinite number of planes, so what's up with that?
|
||||
|
||||
Well, we know more about the tangent: we also know its rate of change. Think of the Bezier curve as the path of a car. The curve itself tells us the "place in space" at any given time, and the first derivative at any point tells us the "speed of the car at that point". However, we know more: the second derivative tells us the "accelleration of the car at that point", and if we add the accelleration to the velocity, we know where the car will be "if the curve stopped changing": as long as the curve we're dealing with is not degenerate (that is to say: it isn't actually a pure 2D curve that we simply rotated in 3D) then at any point in time we know two vectors in the same plane, with a third vector in that same plane, and a fourth vector perpendicular that we don't know yet:
|
||||
|
||||
- **t**, the (normalized) vector for the direction of travel at some point B(t),
|
||||
- **a**, the difference vector between "the tangent here" to what "the tangent at the next point" would be,
|
||||
- **t'** = **t** + **a**, that (normalized) "tangent at the next point", superimposed on the current point,
|
||||
- **r**, a (normalized) vector aligned with the axis over which we can rotate **t** to overlap **t'**, and
|
||||
- **n**, the normal at B(t).
|
||||
|
||||
<Graphic title="Known and unknown vectors" setup={this.setupVectors} draw={this.drawVectors}/>
|
||||
|
||||
All these vectors have the same origin (except for **a** but we only use that to find **t'**): our on-curve point. And that means we can quite easily compute the axis over which we need to rotate any of these vectors to overlap another. Since we know **t** and **t'**, we can compute that axis with some linear algebra, and then we're almost done, because as in the 2D case getting the normal is a question of rotating the (normalized) tangent by a quarter turn over our axis of rotation.
|
||||
|
||||
First up: we need to actually *find* that axis of rotation. As it turns out, this is quite easy: we just compute the [cross product](https://en.wikipedia.org/wiki/Cross_product#Mnemonic) of our two known vectors, and that will give us **r**:
|
||||
|
||||
\[
|
||||
r = t' \times t = \begin{bmatrix}
|
||||
t'_y \cdot t_z - t'_z \cdot t_y\\
|
||||
t'_z \cdot t_x - t'_x \cdot t_z\\
|
||||
t'_x \cdot t_y - t'_y \cdot t_x
|
||||
\end{bmatrix}
|
||||
\]
|
||||
|
||||
(Note that the order of operations matters for cross products: we compute **t'**×**t**, because if we compute **t**×**t'** we'll be computing the samerotation axis but represented by a vector in the opposite direction, so our final normal will actually be rotated a quarter turn "the wrong way". While correcting that is super easy, literally just taking our final normal and multiplying by -1, why correct after the fact what we can get it right from the start?)
|
||||
|
||||
Now we have everything we need: in order to turn our normalised tangent vectors into normal vectors, all we have to do is rotate them about the axes we just found by a quarter turn. If we turn them one way, we get normals, if we turn them the other, we get backfacing normals.
|
||||
|
||||
[Rotating about an axis is perhaps laborious, but not difficult](https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle), and much like in the 2D case, quarter turns in 3D greatly simplify the maths. To rotate a point a quarter turn over our rotation axis **r**, the rotation matrix is:
|
||||
|
||||
\[
|
||||
R = \begin{bmatrix}
|
||||
r^2_x & r_x \cdot r_y - r_z & r_x \cdot r_z + r_y \\
|
||||
r_x \cdot r_y + r_z & r^2_y & r_y \cdot r_z - r_x \\
|
||||
r_x \cdot r_z - r_y & r_y \cdot r_z + r_x & r^2_z
|
||||
\end{bmatrix}
|
||||
\]
|
||||
|
||||
So that's still easy: just tell the computer to evaluate those nine values, and all that's left is a matrix multiplication to get our 3D normal:
|
||||
|
||||
\[
|
||||
n = R \cdot t
|
||||
\]
|
||||
|
||||
Which means computing:
|
||||
|
||||
\[
|
||||
n =
|
||||
\begin{bmatrix}
|
||||
n_x \\
|
||||
n_y \\
|
||||
n_z
|
||||
\end{bmatrix}
|
||||
=
|
||||
\begin{bmatrix}
|
||||
t_x \cdot R_{1,1} + t_y \cdot R_{1,2} + t_z \cdot R_{1,3} \\
|
||||
t_x \cdot R_{2,1} + t_y \cdot R_{2,2} + t_z \cdot R_{2,3} \\
|
||||
t_x \cdot R_{3,1} + t_y \cdot R_{3,2} + t_z \cdot R_{3,3}
|
||||
\end{bmatrix}
|
||||
\]
|
||||
|
||||
And we have the normal vector(s) we need. Perfect! And if we need backfacing normals, we can either effect those "from the start" by evaluating the cross product as **t**×**t'** as mentioned above, or we can multiply the normal vector we get here by -1.
|
||||
|
||||
<Graphic title="3D curve normals" setup={this.setupNormals} draw={this.drawNormals}/>
|
173
components/sections/pointvectors3d/handler.js
Normal file
173
components/sections/pointvectors3d/handler.js
Normal file
@@ -0,0 +1,173 @@
|
||||
var vectorOffset;
|
||||
var normalsOffset;
|
||||
|
||||
module.exports = {
|
||||
setupVectors: function(api) {
|
||||
var curve = api.getDefault3DCubic();
|
||||
vectorOffset = {
|
||||
x: 2 * api.getPanelWidth() / 5,
|
||||
y: 4 * api.getPanelHeight() / 5
|
||||
};
|
||||
api.setCurve(curve);
|
||||
api.setSize(1.25 * api.getPanelWidth(),api.getPanelHeight());
|
||||
},
|
||||
|
||||
drawCube: function(api) {
|
||||
var prj = p => api.project(p, vectorOffset);
|
||||
|
||||
var cube = [
|
||||
{x:0, y:0, z:0},
|
||||
{x:200,y:0, z:0},
|
||||
{x:200,y:200,z:0},
|
||||
{x:0, y:200,z:0},
|
||||
{x:0, y:0, z:200},
|
||||
{x:200,y:0, z:200},
|
||||
{x:200,y:200,z:200},
|
||||
{x:0, y:200,z:200}
|
||||
].map(p => prj(p));
|
||||
|
||||
// "most of the cube"
|
||||
api.setColor("grey");
|
||||
api.drawLine(cube[1], cube[2]);
|
||||
api.drawLine(cube[2], cube[3]);
|
||||
api.drawLine(cube[1], cube[5]);
|
||||
api.drawLine(cube[2], cube[6]);
|
||||
api.drawLine(cube[3], cube[7]);
|
||||
api.drawLine(cube[4], cube[5]);
|
||||
api.drawLine(cube[5], cube[6]);
|
||||
api.drawLine(cube[6], cube[7]);
|
||||
api.drawLine(cube[7], cube[4]);
|
||||
|
||||
// x axis
|
||||
api.setColor("blue");
|
||||
api.drawLine(cube[0], cube[1]);
|
||||
|
||||
// y axis
|
||||
api.setColor("red");
|
||||
api.drawLine(cube[3], cube[0]);
|
||||
|
||||
// z axis
|
||||
api.setColor("green");
|
||||
api.drawLine(cube[0], cube[4]);
|
||||
},
|
||||
|
||||
drawCurveProjection(api, curvepoints) {
|
||||
var prj = p => api.project(p, vectorOffset),
|
||||
curve2d = curvepoints.map(p => prj(p)),
|
||||
points;
|
||||
|
||||
// projections
|
||||
api.setColor("#E0E0E0");
|
||||
api.drawCurve({ points: curvepoints.map(p => api.projectXY(p, vectorOffset)) });
|
||||
api.drawCurve({ points: curvepoints.map(p => api.projectYZ(p, vectorOffset)) });
|
||||
api.drawCurve({ points: curvepoints.map(p => api.projectXZ(p, vectorOffset)) });
|
||||
|
||||
// control lines
|
||||
api.setColor("#333");
|
||||
api.drawLine(curve2d[0], curve2d[1]);
|
||||
api.drawCircle(curve2d[1], 3);
|
||||
api.drawCircle(curve2d[2], 3);
|
||||
api.drawLine(curve2d[2], curve2d[3]);
|
||||
|
||||
// main curve
|
||||
api.setColor("black");
|
||||
api.drawCircle(curve2d[0], 3);
|
||||
api.drawCircle(curve2d[3], 3);
|
||||
|
||||
var curve = new api.Bezier(curve2d);
|
||||
api.drawCurve({ points: curve2d });
|
||||
},
|
||||
|
||||
drawVectors: function(api) {
|
||||
api.reset();
|
||||
var prj = p => api.project(p, vectorOffset);
|
||||
var t = api.hover.x? api.hover.x / api.getPanelWidth() : 0.35;
|
||||
|
||||
this.drawCube(api);
|
||||
|
||||
var curvepoints = [
|
||||
{x:120,y:0,z:0},
|
||||
{x:120,y:220,z:0},
|
||||
{x:30,y:0,z:30},
|
||||
{x:0,y:0,z:200}
|
||||
];
|
||||
|
||||
this.drawCurveProjection(api, curvepoints);
|
||||
|
||||
var curve = new api.Bezier(curvepoints);
|
||||
var d1curve = new api.Bezier(curve.dpoints[0]);
|
||||
var d2curve = new api.Bezier(curve.dpoints[1]);
|
||||
|
||||
// let's mark t
|
||||
var mt = curve.get(t);
|
||||
api.drawCircle(prj(mt), 3);
|
||||
|
||||
// and let's show the tangent at that point
|
||||
var dt = d1curve.get(t);
|
||||
var pt1 = { x: mt.x + dt.x, y: mt.y + dt.y, z: mt.z + dt.z };
|
||||
|
||||
// and then let's work in the change in tangent
|
||||
var roc = d2curve.get(t);
|
||||
var f = 10;
|
||||
var d = Math.sqrt(roc.x*roc.x + roc.y*roc.y + roc.z*roc.z);
|
||||
roc = { x: f * roc.x/d, y: f * roc.y/d, z: f * roc.z/d };
|
||||
var pt2 = { x: mt.x + dt.x + roc.x, y: mt.y + dt.y + roc.y, z: mt.z + dt.z + roc.z };
|
||||
|
||||
api.drawLine(prj(mt), prj(pt1));
|
||||
api.drawLine(prj(mt), prj(pt2));
|
||||
|
||||
// normalize t (=dt) and t' (=ddt) and compute the crossproduct:
|
||||
roc = d2curve.get(t);
|
||||
var ddt = { x: dt.x + roc.x, y: dt.y + roc.y, z: dt.z + roc.z };
|
||||
d = Math.sqrt(dt.x*dt.x + dt.y*dt.y + dt.z*dt.z);
|
||||
dt = { x: dt.x/d, y: dt.y/d, z: dt.z/d };
|
||||
d = Math.sqrt(ddt.x*ddt.x + ddt.y*ddt.y + ddt.z*ddt.z);
|
||||
ddt = { x: ddt.x/d, y: ddt.y/d, z: ddt.z/d };
|
||||
var r = {
|
||||
x: ddt.y * dt.z - ddt.z * dt.y,
|
||||
y: ddt.z * dt.x - ddt.x * dt.z,
|
||||
z: ddt.x * dt.y - ddt.y * dt.x
|
||||
};
|
||||
f = 20;
|
||||
var mc = {
|
||||
x: mt.x + f * r.x,
|
||||
y: mt.y + f * r.y,
|
||||
z: mt.z + f * r.z
|
||||
};
|
||||
// let's see the cross product
|
||||
api.setColor("darkgreen");
|
||||
api.drawLine(prj(mt), prj(mc));
|
||||
|
||||
// and finally, compute the normal
|
||||
var R = [
|
||||
r.x*r.x, r.x*r.y -r.z, r.x*r.z + r.y,
|
||||
r.x*r.y + r.z, r.y*r.y, r.y*r.z - r.x,
|
||||
r.x*r.z - r.y, r.y*r.z + r.x, r.z*r.z
|
||||
];
|
||||
var n = {
|
||||
x: dt.x * R[0] + dt.y * R[1] + dt.z * R[2],
|
||||
y: dt.x * R[3] + dt.y * R[4] + dt.z * R[5],
|
||||
z: dt.x * R[6] + dt.y * R[6] + dt.z * R[7]
|
||||
};
|
||||
var mr = {
|
||||
x: mt.x + f * n.x,
|
||||
y: mt.y + f * n.y,
|
||||
z: mt.z + f * n.z
|
||||
};
|
||||
api.setColor("red");
|
||||
api.drawLine(prj(mt), prj(mr));
|
||||
},
|
||||
|
||||
setupNormals: function(api) {
|
||||
var curve = api.getDefault3DCubic();
|
||||
normalsOffset = {
|
||||
x: api.getPanelWidth() / 2,
|
||||
y: api.getPanelHeight() / 2
|
||||
};
|
||||
api.setCurve(curve);
|
||||
},
|
||||
|
||||
drawNormals: function(api, curve) {
|
||||
api.reset();
|
||||
}
|
||||
};
|
@@ -13,4 +13,4 @@ We keep repeating this process until the interval is small enough to claim the d
|
||||
|
||||
The following graphic demonstrates the result of this procedure.Simply move the cursor around, and if it does not lie on top of the curve, you will see a line that projects the cursor onto the curve based on an iteratively found "ideal" `t` value.
|
||||
|
||||
<Graphic preset="simple" title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
<Graphic title="Projecting a point onto a Bézier curve" setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}/>
|
||||
|
@@ -20,6 +20,6 @@ However, this rule also has as direct consequence that you **cannot** generally
|
||||
|
||||
We can apply this to a (semi) random curve, as is done in the following graphic. Select the sketch and press your up and down arrow keys to elevate or lower the curve order.
|
||||
|
||||
<Graphic preset="simple" title={"A " + this.getOrder() + " order Bézier curve"} setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
<Graphic title={"A " + this.getOrder() + " order Bézier curve"} setup={this.setup} draw={this.draw} onKeyDown={this.props.onKeyDown} />
|
||||
|
||||
There is a good, if mathematical, explanation on the matrices necessary for optimal reduction over on [Sirver's Castle](http://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves), which given time will find its way in a more direct description into this article.
|
||||
|
@@ -44,7 +44,7 @@ These operations are expensive, and implementing your own code for this is gener
|
||||
|
||||
The following graphic shows Paper.js doing its thing for two shapes: one static, and one that is linked to your mouse pointer. If you move the mouse around, you'll see how the shape intersections are resolved. The base shapes are outlined in blue, and the boolean result is coloured red.
|
||||
|
||||
<Graphic preset="simple" title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}><br/>{
|
||||
<Graphic title="Boolean shape operations with Paper.js" paperjs={true} setup={this.setup} draw={this.draw} onMouseMove={this.onMouseMove}><br/>{
|
||||
this.modes.map(mode => {
|
||||
var className = (this.state.mode === mode) ? "selected" : null;
|
||||
return <button className={className} key={mode} onClick={() => this.setMode(mode)}>{mode}</button>;
|
||||
|
Reference in New Issue
Block a user