diff --git a/docs/chapters/aligning/aligning.js b/docs/chapters/aligning/aligning.js index 4cc0d4e4..6beca728 100644 --- a/docs/chapters/aligning/aligning.js +++ b/docs/chapters/aligning/aligning.js @@ -7,7 +7,6 @@ setup() { } draw() { - resetTransform(); clear(); curve.drawSkeleton(); curve.drawCurve(); diff --git a/docs/chapters/canonical/canonical.js b/docs/chapters/canonical/canonical.js index de0f3f51..378d3757 100644 --- a/docs/chapters/canonical/canonical.js +++ b/docs/chapters/canonical/canonical.js @@ -4,7 +4,6 @@ setup() { } draw() { - resetTransform(); clear(); if (this.canonicalMap) { diff --git a/docs/chapters/canonical/interactive.js b/docs/chapters/canonical/interactive.js index 1ca34741..195bbf69 100644 --- a/docs/chapters/canonical/interactive.js +++ b/docs/chapters/canonical/interactive.js @@ -15,7 +15,6 @@ setup() { } draw() { - resetTransform(); clear(); const w = this.width/2, diff --git a/docs/chapters/components/components.js b/docs/chapters/components/components.js index 38fb5e48..fefeeb5c 100644 --- a/docs/chapters/components/components.js +++ b/docs/chapters/components/components.js @@ -12,7 +12,6 @@ setup() { } draw() { - resetTransform(); clear(); const dim = this.height; curve.drawSkeleton(); diff --git a/docs/chapters/control/lerp.js b/docs/chapters/control/lerp.js index 64b69f91..b78d1ae2 100644 --- a/docs/chapters/control/lerp.js +++ b/docs/chapters/control/lerp.js @@ -44,7 +44,6 @@ binomial(n,k) { } draw() { - resetTransform(); clear(); setFill(`black`); setStroke(`black`); diff --git a/docs/chapters/curveintersection/curve-curve.js b/docs/chapters/curveintersection/curve-curve.js index 42f3f73f..ae7eb601 100644 --- a/docs/chapters/curveintersection/curve-curve.js +++ b/docs/chapters/curveintersection/curve-curve.js @@ -37,7 +37,6 @@ setupEventListening() { } draw() { - resetTransform(); clear(); // panel 1: base curves diff --git a/docs/chapters/extremities/extremities.js b/docs/chapters/extremities/extremities.js index ab683fe1..697945e3 100644 --- a/docs/chapters/extremities/extremities.js +++ b/docs/chapters/extremities/extremities.js @@ -12,7 +12,6 @@ setup() { } draw() { - resetTransform(); clear(); const dim = this.height; const degree = curve.points.length - 1; diff --git a/docs/chapters/moulding/content.en-GB.md b/docs/chapters/moulding/content.en-GB.md index 3d1f0df8..4ac43422 100644 --- a/docs/chapters/moulding/content.en-GB.md +++ b/docs/chapters/moulding/content.en-GB.md @@ -2,7 +2,7 @@ Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points. - + **Click-dragging the curve itself** shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, the original point C for that t-value, as well as the new point B' based on the mouse cursor. Since we know the t-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance: diff --git a/docs/chapters/moulding/mould-quadratic.js b/docs/chapters/moulding/mould-quadratic.js new file mode 100644 index 00000000..6e79417e --- /dev/null +++ b/docs/chapters/moulding/mould-quadratic.js @@ -0,0 +1,110 @@ +let curve; + +setup() { + setPanelCount(3); + curve = Bezier.defaultQuadratic(this); + this.position = {x:0,y:0}; + setMovable(curve.points, [this.position]); +} + +draw() { + clear(); + + curve.drawSkeleton(); + curve.drawCurve(); + curve.drawPoints(); + + if (this.position) { + setColor(`blue`); + let p = this.position.projection; + if (!this.mark) { + p = this.position.projection = curve.project( + this.position.x, + this.position.y + ) + this.position.x = p.x; + this.position.y = p.y; + } + circle(p.x, p.y, 3); + } + + nextPanel(); + setStroke(`black`); + line(0,0,0,this.height); + + curve.drawSkeleton(`lightblue`); + curve.drawCurve(`lightblue`); + curve.points.forEach(p => circle(p.x, p.y, 2)); + + if (this.mark) { + let B = this.mark.B; + setFill(`black`); + text(`t = ${this.mark.t.toFixed(2)}`, B.x + 5, B.y + 10); + + let {A, C, S, E} = curve.getABC(this.mark.t, B); + setColor(`lightblue`); + line(S.x, S.y, E.x, E.y); + line(A.x, A.y, C.x, C.y); + circle(A.x, A.y, 3); + circle(B.x, B.y, 3); + circle(C.x, C.y, 3); + + if (this.currentPoint) { + let {A,B,C,S,E} = curve.getABC(this.mark.t, this.position); + setColor(`purple`); + line(A.x, A.y, C.x, C.y); + line(S.x, S.y, A.x, A.y); + line(E.x, E.y, A.x, A.y); + circle(A.x, A.y, 3); + circle(B.x, B.y, 3); + circle(C.x, C.y, 3); + + this.moulded = new Bezier(this, [S,A,E]); + } + } + + nextPanel(); + setStroke(`black`); + line(0,0,0,this.height); + + if (this.moulded) { + this.moulded.drawSkeleton(`lightblue`); + this.moulded.drawCurve(`black`); + this.moulded.points.forEach(p => circle(p.x, p.y, 2)); + } else { + curve.drawSkeleton(`lightblue`); + curve.drawCurve(`black`); + curve.points.forEach(p => circle(p.x, p.y, 2)); + } +} + +onMouseDown() { + if (this.currentPoint !== this.position) { + this.mark = false; + this.position.projection = false; + } + else if (this.position.projection) { + this.mark = { + B: this.position.projection, + t: this.position.projection.t + }; + } + redraw(); +} + +onMouseMove() { + if (!this.currentPoint && !this.mark) { + this.position.x = this.cursor.x; + this.position.y = this.cursor.y; + } + redraw(); +} + +onMouseUp() { + this.mark = false; + if (this.moulded) { + curve = this.moulded; + resetMovable(curve.points, [this.position]); + } + redraw(); +} diff --git a/docs/chapters/pointvectors3d/frenet.js b/docs/chapters/pointvectors3d/frenet.js index 15469102..5ec8d3f7 100644 --- a/docs/chapters/pointvectors3d/frenet.js +++ b/docs/chapters/pointvectors3d/frenet.js @@ -29,7 +29,6 @@ setup() { } draw() { - resetTransform(); clear(); translate(this.width/2 - 60, this.height/2 + 75); const curve = this.curve; diff --git a/docs/chapters/pointvectors3d/rotation-minimizing.js b/docs/chapters/pointvectors3d/rotation-minimizing.js index c2c977cf..5d59b7d9 100644 --- a/docs/chapters/pointvectors3d/rotation-minimizing.js +++ b/docs/chapters/pointvectors3d/rotation-minimizing.js @@ -29,7 +29,6 @@ setup() { } draw() { - resetTransform(); clear(); translate(this.width/2 - 60, this.height/2 + 75); const curve = this.curve; diff --git a/docs/chapters/projections/content.en-GB.md b/docs/chapters/projections/content.en-GB.md index fe68471e..73f42928 100644 --- a/docs/chapters/projections/content.en-GB.md +++ b/docs/chapters/projections/content.en-GB.md @@ -1,16 +1,27 @@ # Projecting a point onto a Bézier curve -Say we have a Bézier curve and some point, not on the curve, of which we want to know which `t` value on the curve gives us an on-curve point closest to our off-curve point. Or: say we want to find the projection of a random point onto a curve. How do we do that? +Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve? -If the Bézier curve is of low enough order, we might be able to [work out the maths for how to do this](https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html), and get a perfect `t` value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal `t` value using a [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm). First, we do a coarse distance-check based on `t` values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run this algorithm: +If the Bézier curve is of low enough order, we might be able to [work out the maths for how to do this](https://web.archive.org/web/20140713004709/http://jazzros.blogspot.com/2011/03/projecting-point-on-bezier-curve.html), and get a perfect `t` value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal `t` value using a [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm). First, we do a coarse distance-check based on `t` values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast: -1. with the `t` value we found, start with some small interval around `t` (1/length_of_LUT on either side is a reasonable start), -2. if the distance to `t ± interval/2` is larger than the distance to `t`, try again with the interval reduced to half its original length. -3. if the distance to `t ± interval/2` is smaller than the distance to `t`, replace `t` with the smaller-distance value. -4. after reducing the interval, or changing `t`, go back to step 1. +``` +p = some point to project onto the curve +d = some initially huge value +i = 0 +for (coordinate, index) in LUT: + if distance(coordinate, p) < d: + d = distance(coordinate, p) + i = index +``` -We keep repeating this process until the interval is small enough to claim the difference in precision found is irrelevant for the purpose we're trying to find `t` for. In this case, I'm arbitrarily fixing it at 0.0001. +After this runs, we know that `LUT[i]` is the coordinate on the curve _in our LUT_ that is closest to the point we want to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that LUT[i] is a better guess than both LUT[i-1] and LUT[i+1], but there might be an even better projection _somewhere else_ between those two values, so that's what we're going to be testing for, using a variation of the binary search. -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. +1. we start with our point `p`, and the `t` values `t1=LUT[i-1].t` and `t2=LUT[i+1].t`, which span an interval `v = t2-t1`. +2. we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and start/end points +3. we then check which of these five points is the closest to our original point `p`, and then repeat step 1 with the points before and after the closest point we just found. - +This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes so small as to lead to distances that are, for all intents and purposes, the same for all points. + +So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bezier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds. + + diff --git a/docs/chapters/projections/handler.js b/docs/chapters/projections/handler.js deleted file mode 100644 index b59acb7f..00000000 --- a/docs/chapters/projections/handler.js +++ /dev/null @@ -1,55 +0,0 @@ -module.exports = { - setup: function(api) { - api.setSize(320,320); - var curve = new api.Bezier([ - {x:248,y:188}, - {x:218,y:294}, - {x:45,y:290}, - {x:12,y:236}, - {x:14,y:82}, - {x:186,y:177}, - {x:221,y:90}, - {x:18,y:156}, - {x:34,y:57}, - {x:198,y:18} - ]); - api.setCurve(curve); - api._lut = curve.getLUT(); - }, - - findClosest: function(LUT, p, dist) { - var i, - end = LUT.length, - d, - dd = dist(LUT[0],p), - f = 0; - for(i=1; i \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg b/docs/images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg index 02e71f37..a89f561a 100644 --- a/docs/images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg +++ b/docs/images/chapters/moulding/7bba0a4fd605e023cda922de125b3e32.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/moulding/7f080cfc5764282db126164d6705d83d.png b/docs/images/chapters/moulding/7f080cfc5764282db126164d6705d83d.png new file mode 100644 index 00000000..9be106b1 Binary files /dev/null and b/docs/images/chapters/moulding/7f080cfc5764282db126164d6705d83d.png differ diff --git a/docs/images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg b/docs/images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg index 73973d9b..1d1c2eb2 100644 --- a/docs/images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg +++ b/docs/images/chapters/moulding/94f61d17f896aebddcf5a7c676aee7d1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png b/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png new file mode 100644 index 00000000..8ed9c7d4 Binary files /dev/null and b/docs/images/chapters/pointvectors3d/9983328e50c2156c124bb1ea4ad5c05b.png differ diff --git a/docs/images/chapters/pointvectors3d/ca775688322e0da5c5ecc5a15e2e57de.png b/docs/images/chapters/pointvectors3d/ca775688322e0da5c5ecc5a15e2e57de.png deleted file mode 100644 index 49c4b2fa..00000000 Binary files a/docs/images/chapters/pointvectors3d/ca775688322e0da5c5ecc5a15e2e57de.png and /dev/null differ diff --git a/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png b/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png new file mode 100644 index 00000000..8ed9c7d4 Binary files /dev/null and b/docs/images/chapters/pointvectors3d/cbadec403b99dec015ab084ee10e1671.png differ diff --git a/docs/images/chapters/pointvectors3d/f989c02a1c37d8837e1e50e993415a73.png b/docs/images/chapters/pointvectors3d/f989c02a1c37d8837e1e50e993415a73.png deleted file mode 100644 index 49c4b2fa..00000000 Binary files a/docs/images/chapters/pointvectors3d/f989c02a1c37d8837e1e50e993415a73.png and /dev/null differ diff --git a/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png b/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png new file mode 100644 index 00000000..68d6ee86 Binary files /dev/null and b/docs/images/chapters/projections/c40ab9e3f3d1f53872dff30a7bcdb003.png differ diff --git a/docs/images/chapters/splitting/43f3024045439e3bcc95434fa39f9d03.png b/docs/images/chapters/splitting/43f3024045439e3bcc95434fa39f9d03.png deleted file mode 100644 index 95cf74a6..00000000 Binary files a/docs/images/chapters/splitting/43f3024045439e3bcc95434fa39f9d03.png and /dev/null differ diff --git a/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png b/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png new file mode 100644 index 00000000..5c45044c Binary files /dev/null and b/docs/images/chapters/splitting/bef2f09698c0d3d2b7c4c031be17ff69.png differ diff --git a/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png b/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png new file mode 100644 index 00000000..b310ab7b Binary files /dev/null and b/docs/images/chapters/tracing/133bf9d02801a3149c9ddb8b313e6797.png differ diff --git a/docs/images/chapters/tracing/25e9697557129c651e9c7cc4e4878b16.png b/docs/images/chapters/tracing/25e9697557129c651e9c7cc4e4878b16.png deleted file mode 100644 index e1a6cb39..00000000 Binary files a/docs/images/chapters/tracing/25e9697557129c651e9c7cc4e4878b16.png and /dev/null differ diff --git a/docs/images/chapters/tracing/4f2cd306ec6fa0340ac7f410744b3118.png b/docs/images/chapters/tracing/4f2cd306ec6fa0340ac7f410744b3118.png deleted file mode 100644 index 57642dbf..00000000 Binary files a/docs/images/chapters/tracing/4f2cd306ec6fa0340ac7f410744b3118.png and /dev/null differ diff --git a/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png b/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png new file mode 100644 index 00000000..53b536b0 Binary files /dev/null and b/docs/images/chapters/tracing/52f815cefe99dabc47ca83d0b97b61fc.png differ diff --git a/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png b/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png new file mode 100644 index 00000000..6494d1ac Binary files /dev/null and b/docs/images/chapters/whatis/9b3633889c38325c24c19ce18ab94ad6.png differ diff --git a/docs/images/chapters/whatis/d3d0579ce3534dcee1fda11bdf609c22.png b/docs/images/chapters/whatis/d3d0579ce3534dcee1fda11bdf609c22.png deleted file mode 100644 index cb486f7e..00000000 Binary files a/docs/images/chapters/whatis/d3d0579ce3534dcee1fda11bdf609c22.png and /dev/null differ diff --git a/docs/images/chapters/yforx/a6f705eb306c43e5709970b2ccad9d20.png b/docs/images/chapters/yforx/a6f705eb306c43e5709970b2ccad9d20.png deleted file mode 100644 index 4e202b28..00000000 Binary files a/docs/images/chapters/yforx/a6f705eb306c43e5709970b2ccad9d20.png and /dev/null differ diff --git a/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png b/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png new file mode 100644 index 00000000..e1237568 Binary files /dev/null and b/docs/images/chapters/yforx/dd28d64458d22f4fe89c98568258efcb.png differ diff --git a/docs/images/chapters/yforx/e1549cca3a5203c4e4d7fa22948cb3f8.png b/docs/images/chapters/yforx/e1549cca3a5203c4e4d7fa22948cb3f8.png deleted file mode 100644 index 4d855c06..00000000 Binary files a/docs/images/chapters/yforx/e1549cca3a5203c4e4d7fa22948cb3f8.png and /dev/null differ diff --git a/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png b/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png new file mode 100644 index 00000000..8b7276d3 Binary files /dev/null and b/docs/images/chapters/yforx/efcfe9b48ca4e65eef3d4bf3e4c97bc3.png differ diff --git a/docs/index.html b/docs/index.html index 6560270d..89470acf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -95,6 +95,7 @@
  • Intersections
  • Curve/curve intersection
  • The projection identity
  • +
  • Projecting a point onto a Bézier curve
  • Manipulating a curve
  • Creating a curve from three points
  • Curve fitting
  • @@ -102,7 +103,6 @@
  • Creating a Catmull-Rom curve from three points
  • Forming poly-Bézier curves
  • Boolean shape operations
  • -
  • Projecting a point onto a Bézier curve
  • Curve offsetting
  • Graduated curve offsetting
  • Circles and quadratic Bézier curves
  • @@ -219,7 +219,7 @@

    So let's look at that in action: the following graphic is interactive in that you can use your up and down arrow keys to increase or decrease the interpolation ratio, to see what happens. We start with three points, which gives us two lines. Linear interpolation over those lines gives us two points, between which we can again perform linear interpolation, yielding a single point. And that point —and all points we can form in this way for all ratios taken together— form our Bézier curve:

    - + Scripts are disabled. Showing fallback image. @@ -328,7 +328,7 @@ function Bezier(3,t):
    - + Scripts are disabled. Showing fallback image. @@ -336,7 +336,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -344,7 +344,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -604,7 +604,7 @@ function RationalBezier(3,t,w[],r[]):

    Using de Casteljau's algorithm, we can also find all the points we need to split up a Bézier curve into two, smaller curves, which taken together form the original curve. When we construct de Casteljau's skeleton for some value t, the procedure gives us all the points we need to split a curve at that t value: one curve is defined by all the inside skeleton points found prior to our on-curve point, with the other curve being defined by all the inside skeleton points after our on-curve point.

    - + Scripts are disabled. Showing fallback image. @@ -836,7 +836,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:

    - + Scripts are disabled. Showing fallback image. @@ -910,7 +910,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:

    - + Scripts are disabled. Showing fallback image. @@ -928,13 +928,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.

    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1070,14 +1070,14 @@ function getCubicRoots(pa, pb, pc, pd) {

    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. For the quadratic curve, that means just the first derivative, in red:

    - + Scripts are disabled. Showing fallback image.

    And for cubic curves, that means first and second derivatives, in red and purple respectively:

    - + Scripts are disabled. Showing fallback image. @@ -1123,12 +1123,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1198,7 +1198,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    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:

    - + Scripts are disabled. Showing fallback image. @@ -1264,7 +1264,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    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:

    - + Scripts are disabled. Showing fallback image. @@ -1275,7 +1275,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

    - + Scripts are disabled. Showing fallback image. @@ -1302,7 +1302,7 @@ y = curve.get(t).y

    So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

    - + Scripts are disabled. Showing fallback image. @@ -1449,7 +1449,7 @@ y = curve.get(t).y

    The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.

    - + Scripts are disabled. Showing fallback image. @@ -1457,7 +1457,7 @@ y = curve.get(t).y

    So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

    - + Scripts are disabled. Showing fallback image. @@ -1540,7 +1540,7 @@ lli = function(line1, line2):

    (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

    - + Scripts are disabled. Showing fallback image. @@ -1600,22 +1600,52 @@ lli = function(line1, line2):

    So: if we have a curve's start and end point, then for any t value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.

    + +
    +

    Projecting a point onto a Bézier curve

    +

    Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    +

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:

    +
    p = some point to project onto the curve
    +d = some initially huge value
    +i = 0
    +for (coordinate, index) in LUT:
    +  if distance(coordinate, p) < d:
    +    d = distance(coordinate, p)
    +    i = index
    +

    After this runs, we know that LUT[i] is the coordinate on the curve in our LUT that is closest to the point we want to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that LUT[i] is a better guess than both LUT[i-1] and LUT[i+1], but there might be an even better projection somewhere else between those two values, so that's what we're going to be testing for, using a variation of the binary search.

    +
      +
    1. we start with our point p, and the t values t1=LUT[i-1].t and t2=LUT[i+1].t, which span an interval v = t2-t1.
    2. +
    3. we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and start/end points
    4. +
    5. we then check which of these five points is the closest to our original point p, and then repeat step 1 with the points before and after the closest point we just found.
    6. +
    +

    This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes so small as to lead to distances that are, for all intents and purposes, the same for all points.

    +

    So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bezier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.

    + + + + Scripts are disabled. Showing fallback image. + +

    Manipulating a curve

    Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.

    - + + + + Scripts are disabled. Showing fallback image. +

    Click-dragging the curve itself shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, the original point C for that t-value, as well as the new point B' based on the mouse cursor. Since we know the t-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:

    - +

    For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point. For cubic curves, we need to do a little more work:

    To help understand what's going on, the cubic graphic shows the full de Casteljau construction "hull" when repositioning point B. We compute A' in exactly the same way as before, but we also record the final strut line that forms B in the original curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points should be. Remember that B' lies on line e1--e2 at a distance t, because that's how Bézier curves work. In the same manner, we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing the new control points is fairly easy.

    First, we construct the one-level-of-de-Casteljau-up points:

    - +

    And then we can compute the new control points:

    - +

    And that's cubic curve manipulation.

    @@ -1885,21 +1915,6 @@ lli = function(line1, line2): - -
    -

    Projecting a point onto a Bézier curve

    -

    Say we have a Bézier curve and some point, not on the curve, of which we want to know which t value on the curve gives us an on-curve point closest to our off-curve point. Or: say we want to find the projection of a random point onto a curve. How do we do that?

    -

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run this algorithm:

    -
      -
    1. with the t value we found, start with some small interval around t (1/length_of_LUT on either side is a reasonable start),
    2. -
    3. if the distance to t ± interval/2 is larger than the distance to t, try again with the interval reduced to half its original length.
    4. -
    5. if the distance to t ± interval/2 is smaller than the distance to t, replace t with the smaller-distance value.
    6. -
    7. after reducing the interval, or changing t, go back to step 1.
    8. -
    -

    We keep repeating this process until the interval is small enough to claim the difference in precision found is irrelevant for the purpose we're trying to find t for. In this case, I'm arbitrarily fixing it at 0.0001.

    -

    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.

    - -

    Curve offsetting

    diff --git a/docs/ja-JP/index.html b/docs/ja-JP/index.html index 40c7519c..a50667df 100644 --- a/docs/ja-JP/index.html +++ b/docs/ja-JP/index.html @@ -95,6 +95,7 @@
  • Intersections
  • Curve/curve intersection
  • The projection identity
  • +
  • Projecting a point onto a Bézier curve
  • Manipulating a curve
  • Creating a curve from three points
  • Curve fitting
  • @@ -102,7 +103,6 @@
  • Creating a Catmull-Rom curve from three points
  • Forming poly-Bézier curves
  • Boolean shape operations
  • -
  • Projecting a point onto a Bézier curve
  • Curve offsetting
  • Graduated curve offsetting
  • Circles and quadratic Bézier curves
  • @@ -222,7 +222,7 @@

    では、実際に見てみましょう。下の図はインタラクティブになっています。上下キーで補間の比率が増減しますので、どうなるか確かめてみましょう。最初に3点があり、それを結んで2本の直線が引かれています。この直線の上でそれぞれ線形補間を行うと、2つの点が得られます。この2点の間でさらに線形補間を行うと、1つの点を得ることができます。そして、あらゆる比率に対して同様に点を求め、それをすべて集めると、このようにベジエ曲線ができるのです。

    - + Scripts are disabled. Showing fallback image. @@ -330,7 +330,7 @@ function Bezier(3,t):
    - + Scripts are disabled. Showing fallback image. @@ -338,7 +338,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -346,7 +346,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -601,7 +601,7 @@ function RationalBezier(3,t,w[],r[]):

    ベジエ曲線を分割して、繫ぎ合わせたときに元に戻るような小さい2曲線にしたい場合にも、ド・カステリョのアルゴリズムを使えば、これに必要な点をすべて求めることができます。ある値tに対してド・カステリョの骨格を組み立てると、そのtで曲線を分割する際に必要になる点がすべて得られます。骨格内部の点のうち、曲線上の点から見て手前側にある点によって一方の曲線が定義され、向こう側にある点によってもう一方の曲線が定義されます。

    - + Scripts are disabled. Showing fallback image. @@ -833,7 +833,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:

    - + Scripts are disabled. Showing fallback image. @@ -907,7 +907,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:

    - + Scripts are disabled. Showing fallback image. @@ -925,13 +925,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.

    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1067,14 +1067,14 @@ function getCubicRoots(pa, pb, pc, pd) {

    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. For the quadratic curve, that means just the first derivative, in red:

    - + Scripts are disabled. Showing fallback image.

    And for cubic curves, that means first and second derivatives, in red and purple respectively:

    - + Scripts are disabled. Showing fallback image. @@ -1120,12 +1120,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1195,7 +1195,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    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:

    - + Scripts are disabled. Showing fallback image. @@ -1261,7 +1261,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    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:

    - + Scripts are disabled. Showing fallback image. @@ -1272,7 +1272,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

    - + Scripts are disabled. Showing fallback image. @@ -1299,7 +1299,7 @@ y = curve.get(t).y

    So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

    - + Scripts are disabled. Showing fallback image. @@ -1446,7 +1446,7 @@ y = curve.get(t).y

    The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.

    - + Scripts are disabled. Showing fallback image. @@ -1454,7 +1454,7 @@ y = curve.get(t).y

    So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

    - + Scripts are disabled. Showing fallback image. @@ -1537,7 +1537,7 @@ lli = function(line1, line2):

    (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

    - + Scripts are disabled. Showing fallback image. @@ -1597,22 +1597,52 @@ lli = function(line1, line2):

    So: if we have a curve's start and end point, then for any t value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.

    +
    +
    +

    Projecting a point onto a Bézier curve

    +

    Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    +

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:

    +
    p = some point to project onto the curve
    +d = some initially huge value
    +i = 0
    +for (coordinate, index) in LUT:
    +  if distance(coordinate, p) < d:
    +    d = distance(coordinate, p)
    +    i = index
    +

    After this runs, we know that LUT[i] is the coordinate on the curve in our LUT that is closest to the point we want to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that LUT[i] is a better guess than both LUT[i-1] and LUT[i+1], but there might be an even better projection somewhere else between those two values, so that's what we're going to be testing for, using a variation of the binary search.

    +
      +
    1. we start with our point p, and the t values t1=LUT[i-1].t and t2=LUT[i+1].t, which span an interval v = t2-t1.
    2. +
    3. we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and start/end points
    4. +
    5. we then check which of these five points is the closest to our original point p, and then repeat step 1 with the points before and after the closest point we just found.
    6. +
    +

    This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes so small as to lead to distances that are, for all intents and purposes, the same for all points.

    +

    So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bezier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.

    + + + + Scripts are disabled. Showing fallback image. + +

    Manipulating a curve

    Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.

    - + + + + Scripts are disabled. Showing fallback image. +

    Click-dragging the curve itself shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, the original point C for that t-value, as well as the new point B' based on the mouse cursor. Since we know the t-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:

    - +

    For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point. For cubic curves, we need to do a little more work:

    To help understand what's going on, the cubic graphic shows the full de Casteljau construction "hull" when repositioning point B. We compute A' in exactly the same way as before, but we also record the final strut line that forms B in the original curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points should be. Remember that B' lies on line e1--e2 at a distance t, because that's how Bézier curves work. In the same manner, we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing the new control points is fairly easy.

    First, we construct the one-level-of-de-Casteljau-up points:

    - +

    And then we can compute the new control points:

    - +

    And that's cubic curve manipulation.

    @@ -1882,21 +1912,6 @@ lli = function(line1, line2): - -
    -

    Projecting a point onto a Bézier curve

    -

    Say we have a Bézier curve and some point, not on the curve, of which we want to know which t value on the curve gives us an on-curve point closest to our off-curve point. Or: say we want to find the projection of a random point onto a curve. How do we do that?

    -

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run this algorithm:

    -
      -
    1. with the t value we found, start with some small interval around t (1/length_of_LUT on either side is a reasonable start),
    2. -
    3. if the distance to t ± interval/2 is larger than the distance to t, try again with the interval reduced to half its original length.
    4. -
    5. if the distance to t ± interval/2 is smaller than the distance to t, replace t with the smaller-distance value.
    6. -
    7. after reducing the interval, or changing t, go back to step 1.
    8. -
    -

    We keep repeating this process until the interval is small enough to claim the difference in precision found is irrelevant for the purpose we're trying to find t for. In this case, I'm arbitrarily fixing it at 0.0001.

    -

    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.

    - -

    Curve offsetting

    diff --git a/docs/js/custom-element/api/graphics-api.js b/docs/js/custom-element/api/graphics-api.js index 9f615b13..bfa9a4f8 100644 --- a/docs/js/custom-element/api/graphics-api.js +++ b/docs/js/custom-element/api/graphics-api.js @@ -436,12 +436,13 @@ class GraphicsAPI extends BaseAPI { /** * Reset the canvas bitmap to a uniform color. */ - clear(color = `white`) { + clear(color = `white`, preserveTransforms = false) { this.ctx.cacheStyle(); this.resetTransform(); this.ctx.fillStyle = color; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restoreStyle(); + if (!preserveTransforms) this.resetTransform(); } /** diff --git a/docs/js/custom-element/api/types/bezier.js b/docs/js/custom-element/api/types/bezier.js index 0a4b018e..ff3165c0 100644 --- a/docs/js/custom-element/api/types/bezier.js +++ b/docs/js/custom-element/api/types/bezier.js @@ -34,8 +34,11 @@ class Bezier extends Original { this.ctx = apiInstance.ctx; } - getPointNear(point, d = 5) { - const { x, y } = point; + project(x, y) { + return super.project({ x, y }); + } + + getPointNear(x, y, d = 5) { const p = this.points; for (let i = 0, e = p.length; i < e; i++) { let dx = Math.abs(p[i].x - x); @@ -46,46 +49,6 @@ class Bezier extends Original { } } - getProjectionPoint(point) { - const { x, y } = point; - // project this point onto the curve and return _that_ point - const n = this.lut.length - 1, - p = this.points; - - let d, - closest, - smallestDistance = Number.MAX_SAFE_INTEGER; - - // coarse check - this.lut.forEach((p, i) => { - d = p.dist(x, y); - if (d < smallestDistance) { - smallestDistance = d; - p.t = i / n; - closest = p; - } - }); - - // fine check - for (let o = -0.1, t, np, st = closest.t; o <= 0.1; o += 0.005) { - t = st + o; - if (t < 0) continue; - if (t > 1) continue; - np = new Point( - compute(t, p[0].x, p[1].x, p[2].x, p[3].x), - compute(t, p[0].y, p[1].y, p[2].y, p[3].y) - ); - d = np.dist(x, y); - if (d < smallestDistance) { - smallestDistance = d; - closest = np; - closest.t = t; - } - } - - return closest; - } - drawCurve(color = `#333`) { const ctx = this.ctx; ctx.cacheStyle(); diff --git a/docs/js/custom-element/lib/bezierjs/bezier.js b/docs/js/custom-element/lib/bezierjs/bezier.js index fd3a96c4..6ae40ce5 100644 --- a/docs/js/custom-element/lib/bezierjs/bezier.js +++ b/docs/js/custom-element/lib/bezierjs/bezier.js @@ -32,7 +32,7 @@ function getABC(n, S, B, E, t) { x: B.x + (B.x - C.x) / s, y: B.y + (B.y - C.y) / s, }; - return { A: A, B: B, C: C }; + return { A, B, C }; } /** @@ -238,6 +238,15 @@ class Bezier { return utils.length(this.derivative.bind(this)); } + getABC(t, B) { + let S = this.points[0]; + let E = this.points[this.order]; + let ret = getABC(this.order, S, B || this.get(t), E, t); + ret.S = S; + ret.E = E; + return ret; + } + getLUT(steps) { this.verify(); steps = steps || 100; @@ -248,8 +257,11 @@ class Bezier { // We want a range from 0 to 1 inclusive, so // we decrement and then use <= rather than <: steps--; - for (let t = 0; t <= steps; t++) { - this._lut.push(this.compute(t / steps)); + for (let i = 0, p, t; i < steps; i++) { + t = i / (steps - 1); + p = this.compute(t); + p.t = t; + this._lut.push(p); } return this._lut; } diff --git a/docs/js/custom-element/lib/bezierjs/utils.js b/docs/js/custom-element/lib/bezierjs/utils.js index 6cbbb35a..b4d5d7c5 100644 --- a/docs/js/custom-element/lib/bezierjs/utils.js +++ b/docs/js/custom-element/lib/bezierjs/utils.js @@ -90,12 +90,14 @@ const utils = { compute: function (t, points, _3d) { // shortcuts if (t === 0) { + points[0].t = 0; return points[0]; } const order = points.length - 1; if (t === 1) { + points[order].t = 1; return points[order]; } @@ -104,6 +106,7 @@ const utils = { // constant? if (order === 0) { + points[0].t = t; return points[0]; } @@ -112,6 +115,7 @@ const utils = { const ret = { x: mt * p[0].x + t * p[1].x, y: mt * p[0].y + t * p[1].y, + t: t, }; if (_3d) { ret.z = mt * p[0].z + t * p[1].z; @@ -141,6 +145,7 @@ const utils = { const ret = { x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y, + t: t, }; if (_3d) { ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z; @@ -162,6 +167,7 @@ const utils = { } dCpts.splice(dCpts.length - 1, 1); } + dCpts[0].t = t; return dCpts[0]; }, @@ -186,6 +192,7 @@ const utils = { x: (f1 * p[0].x + f2 * p[1].x) / d, y: (f1 * p[0].y + f2 * p[1].y) / d, z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d, + t: t, }; } @@ -200,6 +207,7 @@ const utils = { x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d, y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d, z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d, + t: t, }; } @@ -217,6 +225,7 @@ const utils = { z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d, + t: t, }; } }, diff --git a/docs/zh-CN/index.html b/docs/zh-CN/index.html index 4197f42d..b17ada6e 100644 --- a/docs/zh-CN/index.html +++ b/docs/zh-CN/index.html @@ -95,6 +95,7 @@
  • Intersections
  • Curve/curve intersection
  • The projection identity
  • +
  • Projecting a point onto a Bézier curve
  • Manipulating a curve
  • Creating a curve from three points
  • Curve fitting
  • @@ -102,7 +103,6 @@
  • Creating a Catmull-Rom curve from three points
  • Forming poly-Bézier curves
  • Boolean shape operations
  • -
  • Projecting a point onto a Bézier curve
  • Curve offsetting
  • Graduated curve offsetting
  • Circles and quadratic Bézier curves
  • @@ -217,7 +217,7 @@ : - + Scripts are disabled. Showing fallback image. @@ -324,7 +324,7 @@ function Bezier(3,t):
    - + Scripts are disabled. Showing fallback image. @@ -332,7 +332,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -340,7 +340,7 @@ function Bezier(3,t): - + Scripts are disabled. Showing fallback image. @@ -595,7 +595,7 @@ function RationalBezier(3,t,w[],r[]):

    使用 de Casteljau 算法我们也可以将一条贝塞尔曲线分割成两条更小的曲线,二者拼接起来即可形成原来的曲线。当采用某个 t 值构造 de Casteljau 算法时,该过程会给到我们在 t 点分割曲线的所有点: 一条曲线包含该曲线上点之前的所有点,另一条曲线包含该曲线上点之后的所有点。

    - + Scripts are disabled. Showing fallback image. @@ -827,7 +827,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    And then we're done, we found "the" normal vector for a 3D curve. Let's see what that looks like for a sample curve, shall we? You can move your cursor across the graphic from left to right, to show the normal at a point with a t value that is based on your cursor position: all the way on the left is 0, all the way on the right = 1, midway is t=0.5, etc:

    - + Scripts are disabled. Showing fallback image. @@ -901,7 +901,7 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    Speaking of better looking, what does this actually look like? Let's revisit that earlier curve, but this time use rotation minimising frames rather than Frenet frames:

    - + Scripts are disabled. Showing fallback image. @@ -919,13 +919,13 @@ treated as a sequence of three (elementary) shear operations. When we combine th

    If you move points in a curve sideways, you should only see the middle graph change; likewise, moving points vertically should only show a change in the right graph.

    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image. @@ -1061,14 +1061,14 @@ function getCubicRoots(pa, pb, pc, pd) {

    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. For the quadratic curve, that means just the first derivative, in red:

    - + Scripts are disabled. Showing fallback image.

    And for cubic curves, that means first and second derivatives, in red and purple respectively:

    - + Scripts are disabled. Showing fallback image. @@ -1114,12 +1114,12 @@ function getCubicRoots(pa, pb, pc, pd) {
    - + Scripts are disabled. Showing fallback image. - + Scripts are disabled. Showing fallback image.
    @@ -1189,7 +1189,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    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:

    - + Scripts are disabled. Showing fallback image. @@ -1255,7 +1255,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    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:

    - + Scripts are disabled. Showing fallback image. @@ -1266,7 +1266,7 @@ function getCubicRoots(pa, pb, pc, pd) {

    We'll be tackling this problem in two stages: the first, which is the hard part, is figuring out which "t" value belongs to any given "x" value. For instance, have a look at the following graphic. On the left we have a Bézier curve that looks for all intents and purposes like it fits our criteria: every "x" has one and only one associated "y" value. On the right we see the function for just the "x" values: that's a cubic curve, but not a really crazy cubic curve. If you move the graphic's slider, you will see a red line drawn that corresponds to the x coordinate: this is a vertical line in the left graphic, and a horizontal line on the right.

    - + Scripts are disabled. Showing fallback image. @@ -1293,7 +1293,7 @@ y = curve.get(t).y

    So the procedure is fairly straight forward: pick an x, find the associted t value, evaluate our curve for that t value, which gives us the curve's {x,y} coordinate, which means we know y for this x. Move the slider for the following graphic to see this in action:

    - + Scripts are disabled. Showing fallback image. @@ -1440,7 +1440,7 @@ y = curve.get(t).y

    The following graphic shows a particularly illustrative curve, and it's distance-for-t plot. For linear traversal, this line needs to be straight, running from (0,0) to (length,1). That is, it's safe to say, not what we'll see: we'll see something very wobbly, instead. To make matters even worse, the distance-for-t function is also of a much higher order than our curve is: while the curve we're using for this exercise is a cubic curve, which can switch concave/convex form twice at best, the distance function is our old friend the arc length function, which can have more inflection points.

    - + Scripts are disabled. Showing fallback image. @@ -1448,7 +1448,7 @@ y = curve.get(t).y

    So let's do exactly that: the following graph is similar to the previous one, showing how we would have to "chop up" our distance-for-t curve in order to get regularly spaced points on the curve. It also shows what using those t values on the real curve looks like, by coloring each section of curve between two distance markers differently:

    - + Scripts are disabled. Showing fallback image. @@ -1531,7 +1531,7 @@ lli = function(line1, line2):

    (can you find the configuration that yields the maximum number of intersections between two cubic curves? Nine intersections!)

    - + Scripts are disabled. Showing fallback image. @@ -1591,22 +1591,52 @@ lli = function(line1, line2):

    So: if we have a curve's start and end point, then for any t value, we implicitly know all the ABC values, which gives us the necessary information to reconstruct a curve's "de Casteljau skeleton". Which means that we can now do several things: we can "fit" curves using only three points, which means we can also "mould" curves by moving an on-curve point but leaving its start and end point, and then reconstructing the curve based on where we moved the on-curve point to. These are very useful things, and we'll look at both in the next sections.

    +
    +
    +

    Projecting a point onto a Bézier curve

    +

    Before we can move on to actual curve moulding, it'll be good if know how to actually be able to find "some point on the curve" that we're trying to click on. After all, if all we have is our Bézier coordinates, that is not in itself enough to figure out which point on the curve our cursor will be closest to. So, how do we project points onto a curve?

    +

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast:

    +
    p = some point to project onto the curve
    +d = some initially huge value
    +i = 0
    +for (coordinate, index) in LUT:
    +  if distance(coordinate, p) < d:
    +    d = distance(coordinate, p)
    +    i = index
    +

    After this runs, we know that LUT[i] is the coordinate on the curve in our LUT that is closest to the point we want to project, so that's a pretty good initial guess as to what the best projection onto our curve is. To refine it, we note that LUT[i] is a better guess than both LUT[i-1] and LUT[i+1], but there might be an even better projection somewhere else between those two values, so that's what we're going to be testing for, using a variation of the binary search.

    +
      +
    1. we start with our point p, and the t values t1=LUT[i-1].t and t2=LUT[i+1].t, which span an interval v = t2-t1.
    2. +
    3. we test this interval in five spots: the start, middle, and end (which we already have), and the two points in between the middle and start/end points
    4. +
    5. we then check which of these five points is the closest to our original point p, and then repeat step 1 with the points before and after the closest point we just found.
    6. +
    +

    This makes the interval we check smaller and smaller at each iteration, and we can keep running the three steps until the interval becomes so small as to lead to distances that are, for all intents and purposes, the same for all points.

    +

    So, let's see that in action: in this case, I'm going to arbitrarily say that if we're going to run the loop until the interval is smaller than 0.001, and show you what that means for projecting your mouse cursor or finger tip onto a rather complex Bezier curve (which, of course, you can reshape as you like). Also shown are the original three points that our coarse check finds.

    + + + + Scripts are disabled. Showing fallback image. + +

    Manipulating a curve

    Armed with knowledge of the "ABC" relation, we can now update a curve interactively, by letting people click anywhere on the curve, find the t-value matching that coordinate, and then letting them drag that point around. With every drag update we'll have a new point "B", which we can combine with the fixed point "C" to find our new point A. Once we have those, we can reconstruct the de Casteljau skeleton and thus construct a new curve with the same start/end points as the original curve, passing through the user-selected point B, with correct new control points.

    - + + + + Scripts are disabled. Showing fallback image. +

    Click-dragging the curve itself shows what we're using to compute the new coordinates: while dragging you will see the original point B and its corresponding t-value, the original point C for that t-value, as well as the new point B' based on the mouse cursor. Since we know the t-value for this configuration, we can compute the ABC ratio for this configuration, and we know that our new point A' should like at a distance:

    - +

    For quadratic curves, this means we're done, since the new point A' is equivalent to the new quadratic control point. For cubic curves, we need to do a little more work:

    To help understand what's going on, the cubic graphic shows the full de Casteljau construction "hull" when repositioning point B. We compute A' in exactly the same way as before, but we also record the final strut line that forms B in the original curve. Given A', B', and the endpoints e1 and e2 of the strut line relative to B', we can now compute where the new control points should be. Remember that B' lies on line e1--e2 at a distance t, because that's how Bézier curves work. In the same manner, we know the distance A--e1 is only line-interval [0,t] of the full segment, and A--e2 is only line-interval [t,1], so constructing the new control points is fairly easy.

    First, we construct the one-level-of-de-Casteljau-up points:

    - +

    And then we can compute the new control points:

    - +

    And that's cubic curve manipulation.

    @@ -1876,21 +1906,6 @@ lli = function(line1, line2): - -
    -

    Projecting a point onto a Bézier curve

    -

    Say we have a Bézier curve and some point, not on the curve, of which we want to know which t value on the curve gives us an on-curve point closest to our off-curve point. Or: say we want to find the projection of a random point onto a curve. How do we do that?

    -

    If the Bézier curve is of low enough order, we might be able to work out the maths for how to do this, and get a perfect t value back, but in general this is an incredibly hard problem and the easiest solution is, really, a numerical approach again. We'll be finding our ideal t value using a binary search. First, we do a coarse distance-check based on t values associated with the curve's "to draw" coordinates (using a lookup table, or LUT). This is pretty fast. Then we run this algorithm:

    -
      -
    1. with the t value we found, start with some small interval around t (1/length_of_LUT on either side is a reasonable start),
    2. -
    3. if the distance to t ± interval/2 is larger than the distance to t, try again with the interval reduced to half its original length.
    4. -
    5. if the distance to t ± interval/2 is smaller than the distance to t, replace t with the smaller-distance value.
    6. -
    7. after reducing the interval, or changing t, go back to step 1.
    8. -
    -

    We keep repeating this process until the interval is small enough to claim the difference in precision found is irrelevant for the purpose we're trying to find t for. In this case, I'm arbitrarily fixing it at 0.0001.

    -

    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.

    - -

    Curve offsetting