增加一篇博客
This commit is contained in:
@@ -3,9 +3,11 @@ import { defaultTheme } from '@vuepress/theme-default'
|
|||||||
import { markdownMathPlugin } from '@vuepress/plugin-markdown-math'
|
import { markdownMathPlugin } from '@vuepress/plugin-markdown-math'
|
||||||
import { markdownImagePlugin } from '@vuepress/plugin-markdown-image'
|
import { markdownImagePlugin } from '@vuepress/plugin-markdown-image'
|
||||||
import { defineUserConfig } from 'vuepress'
|
import { defineUserConfig } from 'vuepress'
|
||||||
|
import { getDirname, path } from 'vuepress/utils'
|
||||||
|
|
||||||
const navbar_def = require('./config/nav.js');
|
const navbar_def = require('./config/nav.js');
|
||||||
const sidebar_def = require('./config/sidebar.js');
|
const sidebar_def = require('./config/sidebar.js');
|
||||||
|
const __dirname = getDirname(import.meta.url)
|
||||||
|
|
||||||
export default defineUserConfig({
|
export default defineUserConfig({
|
||||||
bundler: viteBundler({
|
bundler: viteBundler({
|
||||||
@@ -37,7 +39,7 @@ export default defineUserConfig({
|
|||||||
markdownMathPlugin({
|
markdownMathPlugin({
|
||||||
// options
|
// options
|
||||||
type: 'mathjax',
|
type: 'mathjax',
|
||||||
output: 'chtml'
|
output: 'svg'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
markdownImagePlugin({
|
markdownImagePlugin({
|
||||||
@@ -52,6 +54,10 @@ export default defineUserConfig({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
markdown: {
|
markdown: {
|
||||||
lineNumbers: false
|
lineNumbers: false,
|
||||||
|
importCode: {
|
||||||
|
handleImportPath: (str) =>
|
||||||
|
str.replace(/^@public/, path.resolve(__dirname, 'public/')),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.js"></script>
|
||||||
|
<script src="/js/bezier_app.js"></script>
|
||||||
|
<script src="/js/bezier_base.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0px; padding:0px; overflow: hidden">
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var lineSegments;
|
||||||
|
var quadBezierLine;
|
||||||
|
var P0={x:10.0, y:10.0}, P1={x:260.0, y:235.0}, P2={x:300.0, y:80.0};
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
let uniformSpeed = searchParams.get('uniformSpeed')!=0;
|
||||||
|
|
||||||
|
canvas = createCanvas(windowWidth, windowHeight);
|
||||||
|
|
||||||
|
lineSegments = new LineSegments();
|
||||||
|
lineSegments.addPoint(P0.x, P0.y);
|
||||||
|
lineSegments.addPoint(P1.x, P1.y);
|
||||||
|
lineSegments.addPoint(P2.x, P2.y);
|
||||||
|
|
||||||
|
quadBezierLine = new QuadBezierLine(P0, P1, P2, 20, uniformSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
clear();
|
||||||
|
background('white');
|
||||||
|
noStroke();
|
||||||
|
|
||||||
|
lineSegments.draw();
|
||||||
|
quadBezierLine.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
function mousePressed(){
|
||||||
|
lineSegments.handleMousePressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseDragged(){
|
||||||
|
lineSegments.handleMouseDragged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseReleased(){
|
||||||
|
lineSegments.handleMouseReleased();
|
||||||
|
quadBezierLine.updatePoints(lineSegments.points[0], lineSegments.points[1], lineSegments.points[2]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<this.points.length-1; index++) {
|
||||||
|
line(this.points[index].x, this.points[index].y, this.points[index+1].x, this.points[index+1].y);
|
||||||
|
}
|
||||||
|
this.points.forEach(pt => 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; });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
$$
|
||||||
|
用一个动画来演示,可以更加清楚的表明这条曲线的构建过程
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如果$t$变量本身是线性变化的话,这条贝塞尔曲线的生成过程是并不是匀速的,通常都是两头快中间慢。
|
||||||
|
|
||||||
|
<iframe width="100%" height="270" frameborder=0 src="/html/bezier.html?uniformSpeed=0"></iframe>
|
||||||
|
|
||||||
|
可以看出中间的点较为密集,而两边则较为稀疏。
|
||||||
|
如何想要得到匀速的贝塞尔曲线运动呢?比如我们在某款游戏中设计了一条贝塞尔曲线的路径,如何实现玩家匀速在这条路径上运动呢?
|
||||||
|
首先需要求得$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$来开始求解,以下是修正后的匀速贝塞尔曲线
|
||||||
|
|
||||||
|
<iframe width="100%" height="270" frameborder=0 src="/html/bezier.html?uniformSpeed=1"></iframe>
|
||||||
|
|
||||||
|
上面是使用javascript实现的互动曲线,核心代码如下
|
||||||
|
|
||||||
|
@[code js :no-line-numbers](@public/js/bezier_app.js)
|
||||||
|
|
||||||
|
## End
|
||||||
+2
-1
@@ -13,4 +13,5 @@
|
|||||||
* [一个简单的DH密钥协商算法的实现](/blog/2025/02/DH.md), [Github](https://github.com/thejinchao/dhexchange)
|
* [一个简单的DH密钥协商算法的实现](/blog/2025/02/DH.md), [Github](https://github.com/thejinchao/dhexchange)
|
||||||
* [如何计算线段和圆的交点](/blog/2025/02/SegmentCircle.md)
|
* [如何计算线段和圆的交点](/blog/2025/02/SegmentCircle.md)
|
||||||
* [一道数学趣题](/blog/2025/02/Ellipse.md)
|
* [一道数学趣题](/blog/2025/02/Ellipse.md)
|
||||||
* [斐波那契数列和1/89](/blog/2025/02/Fibonacci.md)
|
* [斐波那契数列和1/89](/blog/2025/02/Fibonacci.md)
|
||||||
|
* [匀速贝塞尔曲线运动实现](/blog/2025/03/BezierLine.md)
|
||||||
|
|||||||
Reference in New Issue
Block a user