diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a3fe1b3..909e293 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -3,9 +3,11 @@ import { defaultTheme } from '@vuepress/theme-default' import { markdownMathPlugin } from '@vuepress/plugin-markdown-math' import { markdownImagePlugin } from '@vuepress/plugin-markdown-image' import { defineUserConfig } from 'vuepress' +import { getDirname, path } from 'vuepress/utils' const navbar_def = require('./config/nav.js'); const sidebar_def = require('./config/sidebar.js'); +const __dirname = getDirname(import.meta.url) export default defineUserConfig({ bundler: viteBundler({ @@ -37,7 +39,7 @@ export default defineUserConfig({ markdownMathPlugin({ // options type: 'mathjax', - output: 'chtml' + output: 'svg' }), markdownImagePlugin({ @@ -52,6 +54,10 @@ export default defineUserConfig({ }), ], markdown: { - lineNumbers: false + lineNumbers: false, + importCode: { + handleImportPath: (str) => + str.replace(/^@public/, path.resolve(__dirname, 'public/')), + }, } }) diff --git a/docs/.vuepress/public/html/bezier.html b/docs/.vuepress/public/html/bezier.html new file mode 100644 index 0000000..314f0ef --- /dev/null +++ b/docs/.vuepress/public/html/bezier.html @@ -0,0 +1,51 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/.vuepress/public/images/2025/03/bezier_2_big.gif b/docs/.vuepress/public/images/2025/03/bezier_2_big.gif new file mode 100644 index 0000000..a97c77f Binary files /dev/null and b/docs/.vuepress/public/images/2025/03/bezier_2_big.gif differ diff --git a/docs/.vuepress/public/js/bezier_app.js b/docs/.vuepress/public/js/bezier_app.js new file mode 100644 index 0000000..75a3d99 --- /dev/null +++ b/docs/.vuepress/public/js/bezier_app.js @@ -0,0 +1,78 @@ +class QuadBezierLine { + constructor(_p0, _p1, _p2, _step, _uniformSpeed) { + this.step=_step; + this.points=[]; + this.uniformSpeed=_uniformSpeed; + this.updatePoints(_p0, _p1, _p2); + } + + //Speed(t_) = Sqrt[A*t*t+B*t+C] + speed(t) { + return Math.sqrt(this.A * t * t + this.B * t + this.C); + } + + //Length(t) = Integrate[Speed[t], t] + //Length(t_)=((2*Sqrt[A]*(2*A*t*Sqrt[C+t*(B+A*t)]+B*(Sqrt[C + t*(B + A*t)]-Sqrt[C])) + + // (B^2-4*A*C)(Log[B+2*Sqrt[A]*Sqrt[C]]-Log[B+2*A*t+2*Sqrt[A]*Sqrt[C+t*(B+A*t)]]))/ + // (8* A^(3/2))); + length(t) { + let temp1 = Math.sqrt(this.C + t * (this.B + this.A * t)); + let temp2 = (2 * this.A * t * temp1 + this.B * (temp1 - Math.sqrt(this.C))); + let temp3 = Math.log(this.B + 2 * Math.sqrt(this.A) * Math.sqrt(this.C)); + let temp4 = Math.log(this.B + 2 * this.A * t + 2 * Math.sqrt(this.A) * temp1); + let temp5 = 2 * Math.sqrt(this.A) * temp2; + let temp6 = (this.B * this.B - 4 * this.A * this.C) * (temp3 - temp4); + return (temp5 + temp6) / (8 * Math.pow(this.A, 1.5)); + } + + //X(n+1) = Xn - F(Xn)/F'(Xn) + invertLength(t, len) { + let t1 = t, t2; + do { + t2 = t1 - (this.length(t1) - len) / this.speed(t1); + if (Math.abs(t1 - t2) < 0.000001) + break; + t1 = t2; + } while (true); + return t2; + } + + updatePoints(_p0, _p1, _p2) { + this.p0=_p0; + this.p1=_p1; + this.p2=_p2; + + 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); + + for (let index = 1; index < this.step; index++) { + let t = index / this.step; + + if(this.uniformSpeed) { + //根据 L 函数的反函数,求得 l 对应的 t 值 + t = this.invertLength(t, totalLength*t); + } + + let x = (1-t)*(1-t)*this.p0.x+2*(1-t)*t*this.p1.x+t*t*this.p2.x; + let y = (1-t)*(1-t)*this.p0.y+2*(1-t)*t*this.p1.y+t*t*this.p2.y; + + this.points.push({x:x, y:y}); + } + } + + draw() { + this.points.forEach(pt => { + fill('green'); + ellipse(pt.x, pt.y, 5, 5); + }); + } +} diff --git a/docs/.vuepress/public/js/bezier_base.js b/docs/.vuepress/public/js/bezier_base.js new file mode 100644 index 0000000..7c283c8 --- /dev/null +++ b/docs/.vuepress/public/js/bezier_base.js @@ -0,0 +1,82 @@ + +class Point { + constructor(x, y){ + this.pos = createVector(x, y); + this.radius = 10; + this.isDragged = false; + this.isBeingDragged = false; + } + + get x(){ return this.pos.x; } + get y(){ return this.pos.y; } + set x(newVal){ this.pos.x = newVal; } + set y(newVal){ this.pos.y = newVal; } + set(x, y){ this.x = x; this.y = y; } + + containsXY(x, y){ + return dist(x, y, this.x, this.y) < this.radius; + } + + handleMousePressed(){ + this.isDragged = this.containsXY(mouseX, mouseY); + return this.isDragged; + } + + handleMouseDragged(){ + this.set(mouseX, mouseY); + } + + handleMouseReleased(){ + this.isDragged = false; + } + + draw(){ + if (this.containsXY(mouseX, mouseY)){ + fill('red'); + }else { + fill('gray'); + } + stroke('black') + ellipse(this.x, this.y, this.radius, this.radius); + } +} + +class LineSegments { + constructor() { + this.points = []; + } + + addPoint(x,y) { + this.points.push(new Point(x, y)); + } + + draw(){ + stroke('black') + if(this.points.length>1) { + for(let index=0; index pt.draw()) + } + } + + handleMousePressed(){ + const pointPressed = this.points.find(pt => pt.containsXY(mouseX, mouseY)); + if (pointPressed){ + pointPressed.isBeingDragged = true; + return true; + } + return false; + } + + handleMouseDragged(){ + const pointDragged = this.points.find(p => p.isBeingDragged); + if (pointDragged) { + pointDragged.set(mouseX, mouseY); + } + } + + handleMouseReleased(){ + this.points.forEach(p => { p.isBeingDragged = false; }); + } +} diff --git a/docs/blog/2025/03/BezierLine.md b/docs/blog/2025/03/BezierLine.md new file mode 100644 index 0000000..aeb117a --- /dev/null +++ b/docs/blog/2025/03/BezierLine.md @@ -0,0 +1,69 @@ +--- +title: "匀速贝塞尔曲线运动的实现" +tags: 程序 算法 +--- +# 匀速贝塞尔曲线运动的实现 + +二次贝塞尔曲线通常以如下方式构建,给定二维平面上的固定点$P_0$, $P_1$, $P_2$,用$B(t)$表示该条曲线  +$$ +\boldsymbol{B}(t)=(1-t)^2 \boldsymbol{P_0}+2t(1-t) \boldsymbol{P_1}+t^2 \boldsymbol{P_2} +$$ +用一个动画来演示,可以更加清楚的表明这条曲线的构建过程 + +![](/images/2025/03/bezier_2_big.gif) + +如果$t$变量本身是线性变化的话,这条贝塞尔曲线的生成过程是并不是匀速的,通常都是两头快中间慢。  + + + +可以看出中间的点较为密集,而两边则较为稀疏。 +如何想要得到匀速的贝塞尔曲线运动呢?比如我们在某款游戏中设计了一条贝塞尔曲线的路径,如何实现玩家匀速在这条路径上运动呢?​  +首先需要求得$B(t)$相对于$t$的速度公式$s(t)$ +$$ +s(t)=\sqrt{B_{x}^{'}(t)^2+B_{y}^{'}(t)^2} +$$ +为了简化公式,定义如下变量 +$$ +\begin{aligned} +\boldsymbol{a}&=\boldsymbol{P_0}-2\boldsymbol{P_1}+\boldsymbol{P_2}\\ +\boldsymbol{b}&=2\boldsymbol{P_1}-2\boldsymbol{P_0}\\ +A&=4(a_x^2+a_y^2)\\ +B&=4(a_xb_x+a_yb_y)\\ +C&=b_x^2+b_y^2 +\end{aligned} +$$ +计算出$s(t)$可以表达为 +$$ +s(t)=\sqrt{At^2+Bt+C} +$$ +根据这个公式,求得贝塞尔曲线的长度公式$L(t)$为 +$$ +\begin{aligned} +L(t)&=\int_0^t\sqrt{Ax^2+Bx+C}dx\\ +&=\frac{1}{8A^{3/2}}\biggl(2\sqrt{A}\left[2At\sqrt{At^2+Bt+C}+B\left(\sqrt{At^2+Bt+C}-\sqrt{C}\right)\right] \\ +&\quad+(B^2-4AC)\left[ln(B+2\sqrt{AC})-ln\left(B+2At+2\sqrt{A}\sqrt{At^2+Bt+C}\right)\right]\biggr) +\end{aligned} +$$ +特别当$t=1.0$时,$L(1.0)$就是这条曲线的总长度。 +设$t'$是能够使$L$实现匀速运动的自变量,那么此时曲线长度应该满足线性增长,也就是 +$$ +L(t')=L(1.0)t\tag{1} +$$ +也就是$t'=L^{-1}(L(1.0)t)$,由于$L(t)$函数非常复杂,直接求其逆函数的解析解几乎不可能,还好我们知道它的导数为$s(t)$,在实际使用中,可以使用[牛顿切线法](https://en.wikipedia.org/wiki/Newton%27s_method)获得$t'$的数值解。 +设$L_t=L(1.0)t$,视$t'$为未知数,根据公式1有以下方程 +$$ +L(t')-L_t=0 +$$ +根据牛顿切线法,求解的迭代公式为: +$$ +t'_{n+1}=t'_n-\frac{L(t'_n)-L_t}{s(t'_n)} +$$ +由于$t$和$t'$相差不大,可以设$t_0=t$来开始求解,以下是修正后的匀速贝塞尔曲线 + + + +上面是使用javascript实现的互动曲线,核心代码如下 + +@[code js :no-line-numbers](@public/js/bezier_app.js) + +## End diff --git a/docs/blog/index.md b/docs/blog/index.md index c074efd..7812d25 100644 --- a/docs/blog/index.md +++ b/docs/blog/index.md @@ -13,4 +13,5 @@ * [一个简单的DH密钥协商算法的实现](/blog/2025/02/DH.md), [Github](https://github.com/thejinchao/dhexchange) * [如何计算线段和圆的交点](/blog/2025/02/SegmentCircle.md) * [一道数学趣题](/blog/2025/02/Ellipse.md) - * [斐波那契数列和1/89](/blog/2025/02/Fibonacci.md) \ No newline at end of file + * [斐波那契数列和1/89](/blog/2025/02/Fibonacci.md) + * [匀速贝塞尔曲线运动实现](/blog/2025/03/BezierLine.md)