diff --git a/docs/.vuepress/config/sidebar.js b/docs/.vuepress/config/sidebar.js index 4dc7b2d..ba4dc95 100644 --- a/docs/.vuepress/config/sidebar.js +++ b/docs/.vuepress/config/sidebar.js @@ -41,7 +41,14 @@ module.exports = [ "2025/02/SegmentCircle.md", "2025/02/Ellipse.md", "2025/02/Fibonacci.md", - "2025/03/BezierLine01.md" + { + text: '匀速贝塞尔曲线运动的实现', + collapsible: true, + children: [ + { text: "Part1", link: "2025/03/BezierLine01.md" }, + { text: "Part2", link: "2025/03/BezierLine02.md" } + ] + } ] }, { diff --git a/docs/.vuepress/public/html/bezier02.html b/docs/.vuepress/public/html/bezier02.html new file mode 100644 index 0000000..7a18f30 --- /dev/null +++ b/docs/.vuepress/public/html/bezier02.html @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/.vuepress/public/images/2025/03/bezier01.png b/docs/.vuepress/public/images/2025/03/bezier01.png new file mode 100644 index 0000000..7e598e2 Binary files /dev/null and b/docs/.vuepress/public/images/2025/03/bezier01.png differ diff --git a/docs/.vuepress/public/images/2025/03/bezier02.png b/docs/.vuepress/public/images/2025/03/bezier02.png new file mode 100644 index 0000000..5c4ab77 Binary files /dev/null and b/docs/.vuepress/public/images/2025/03/bezier02.png differ diff --git a/docs/.vuepress/public/images/2025/03/bezier_3_big.gif b/docs/.vuepress/public/images/2025/03/bezier_3_big.gif new file mode 100644 index 0000000..62d982c Binary files /dev/null and b/docs/.vuepress/public/images/2025/03/bezier_3_big.gif differ diff --git a/docs/.vuepress/public/js/bezier_cubic.js b/docs/.vuepress/public/js/bezier_cubic.js new file mode 100644 index 0000000..f9d5716 --- /dev/null +++ b/docs/.vuepress/public/js/bezier_cubic.js @@ -0,0 +1,116 @@ +class CubicBezierLine { + constructor(_p0, _p1, _p2, _p3, _step, _uniformSpeedMode) { + this.step=_step; + this.points=[]; + this.uniformSpeedMode=_uniformSpeedMode; + this.updatePoints(_p0, _p1, _p2, _p3); + } + + position(t) { + let it = 1-t; + let x = it*it*it*this.p0.x + 3*it*it*t*this.p1.x + + 3*it*t*t*this.p2.x + t*t*t*this.p3.x; + let y =it*it*it*this.p0.y + 3*it*it*t*this.p1.y + + 3*it*t*t*this.p2.y + t*t*t*this.p3.y; + return {x:x, y:y} + } + + speed(t) { + let it = 1-t; + let sx = -3 * this.p0.x*it*it + 3*this.p1.x*it*it - + 6*this.p1.x*it*t + 6*this.p2.x*it*t - 3*this.p2.x*t*t + 3*this.p3.x*t*t; + let sy = -3 * this.p0.y*it*it + 3*this.p1.y*it*it - + 6*this.p1.y*it*t + 6*this.p2.y*it*t - 3*this.p2.y*t*t + 3*this.p3.y*t*t; + return Math.sqrt(sx*sx+sy*sy); + } + + length(t){ + //使用simpson算法的分割数 + const TOTAL_SIMPSON_STEP = 1000; + //分割份数 + let stepCounts = Math.floor(TOTAL_SIMPSON_STEP*t); + if(stepCounts & 1) stepCounts++; //偶数 + if(stepCounts==0) return 0.0; + + let halfCounts = stepCounts/2; + let sum1=0.0, sum2=0.0; + let dStep = t/stepCounts; + + for(var i=0; i=1.0) return this.length_table[this.length_table.length-1]; + + let step=t*this.step; + let low_bounder = Math.floor(step); + let lerp = step-low_bounder; + + return this.length_table[low_bounder]*(1-lerp)+this.length_table[low_bounder+1]*lerp; + } + + //根据t推导出匀速运动自变量t'的方程(使用牛顿切线法) + invertLength(t, target_length) { + let t1 = t, t2; + do { + t2 = t1 - (this.length_from_table(t1)-target_length)/this.speed(t1); + if(abs(t1-t2)<0.00001) break; + t1=t2; + }while(true); + return t2; + } + + updatePoints(_p0, _p1, _p2, _p3) { + this.p0=_p0; + this.p1=_p1; + this.p2=_p2; + this.p3=_p3; + + let ax = this.p0.x - 2 * this.p1.x + this.p2.x; + let ay = this.p0.y - 2 * this.p1.y + this.p2.y; + let bx = 2 * this.p1.x - 2 * this.p0.x; + let by = 2 * this.p1.y - 2 * this.p0.y; + + this.A = 4 * (ax * ax + ay *ay); + this.B = 4 * (ax * bx + ay *by); + this.C = bx * bx + by * by; + + this.points.length = 0; + let totalLength = this.length(1); + + //构建一个length table + this.length_table=[]; + for (let index = 0; index <= this.step; index++) { + let t = index / this.step; + this.length_table.push(this.length(t)); + } + + for (let index = 1; index < this.step; index++) { + let t = index / this.step; + let target_length=totalLength*t; + + if(this.uniformSpeedMode) { + t = this.invertLength(t, target_length); + } + + this.points.push(this.position(t)); + } + } + + draw() { + this.points.forEach(pt => { + fill('green'); + ellipse(pt.x, pt.y, 5, 5); + }); + } +} diff --git a/docs/blog/2025/03/BezierLine02.md b/docs/blog/2025/03/BezierLine02.md new file mode 100644 index 0000000..5012d7f --- /dev/null +++ b/docs/blog/2025/03/BezierLine02.md @@ -0,0 +1,23 @@ +--- +title: "匀速贝塞尔曲线运动的实现(二)" +tags: 程序 算法 +--- +# 匀速贝塞尔曲线运动的实现(二) + +实际工程应用中最为常见的是三次贝塞尔曲线,也就是下面这种用4个控制点生成的曲线 + +![](/images/2025/03/bezier_3_big.gif) + +三次贝塞尔曲线的一个方便之处在于可以将相邻的两个控制点之间的连线视作控制点的“切线”,进而便于使用者编辑,因此在很多软件中的贝塞尔曲线编辑器都是使用的三次贝塞尔曲线。比如下面这段曲线,其实就是由几段三次贝塞尔曲线组成的。 + +![](/images/2025/03/bezier01.png) + +三次贝塞尔曲线的公式为: +$$ +\boldsymbol{B}(t)=(1-t)^3 \boldsymbol{P_0}+3t(1-t)^2 \boldsymbol{P_1}+3(1-t)t^2 \boldsymbol{P_2}+t^3\boldsymbol{P_3}, t\in[0, 1] +$$ +虽然仍然可以按照相同的思路去实现匀速运动,但是由于三次贝塞尔曲线的长度计算已经非常复杂,根本无法通过对速度进行积分得到解析解,更别说通过反函数去求解匀速需要的自变量了。 因此在实际计算中,一般也只能通过提前建立一个曲线的长度查询表辅助运算。 这个长度查询表可以利用一般的数值积分的方式建立,比如[辛普森积分法](https://en.wikipedia.org/wiki/Simpson%27s_rule) +下面是一个JavaScript实现的互动范例,代码可以直接查看这个页面的JS源码 + + + diff --git a/docs/blog/index.md b/docs/blog/index.md index 8ee5e5f..1a5ccfc 100644 --- a/docs/blog/index.md +++ b/docs/blog/index.md @@ -14,4 +14,5 @@ * [如何计算线段和圆的交点](/blog/2025/02/SegmentCircle.md) * [一道数学趣题](/blog/2025/02/Ellipse.md) * [斐波那契数列和1/89](/blog/2025/02/Fibonacci.md) - * [匀速贝塞尔曲线运动实现](/blog/2025/03/BezierLine01.md) + * 匀速贝塞尔曲线运动实现 + * [Part1](/blog/2025/03/BezierLine01.md), [Part2](/blog/2025/03/BezierLine02.md) diff --git a/docs/index.md b/docs/index.md index 348d782..fed3256 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,8 @@ * [如何计算线段和圆的交点](/blog/2025/02/SegmentCircle.md) * [一道数学趣题](/blog/2025/02/Ellipse.md) * [斐波那契数列和1/89](/blog/2025/02/Fibonacci.md) - * [匀速贝塞尔曲线运动实现](/blog/2025/03/BezierLine01.md) + * 匀速贝塞尔曲线运动实现 + * [Part1](/blog/2025/03/BezierLine01.md), [Part2](/blog/2025/03/BezierLine02.md) ## 开源项目 * [Turbolink](https://github.com/thejinchao/turbolink)