这篇我们来把完整的代码实现一下,大家可以新开个窗口打开连载二十一,然后把数学的实现步骤跟代码的实现步骤一一对应着看。
<!DOCTYPE html>
<html>
<head>
<title>矩阵法计算二次贝塞尔曲线和直线的交点</title>
<script src="Matrix.js"></script>
<script src="MatrixUtil.js"></script>
<script src="Point.js"></script>
</head>
<body>
<canvas width="800" height="800" id="canvas"></canvas>
</body>
<script>
function toScreen(p)
{
return new Point(coordO.x + p.x * unitSize, coordO.y - p.y * unitSize);
}
var unitSize = 160;
var coordO = new Point(200, 400);
//贝塞尔曲线的3个点
var p0 = new Point(-1, 0);
var p1 = new Point(1, -1);
var p2 = new Point(2, 2);
//直线上的两个点
var lineP0 = new Point(-2, -2);
var lineP1 = new Point(4, 14 / 5);
//根据两点式求出的直线一般式系数
var lineA = (lineP1.y - lineP0.y);
var lineB = (lineP0.x - lineP1.x);
var lineC = (lineP1.x * lineP0.y - lineP1.y * lineP0.x);
//在标准抛物线上取出p0,p2,并算出这两点切线的交点为p1
var anchorCenter = new Point((p0.x + p2.x) * 0.5, (p0.y + p2.y) * 0.5);//端点连线的中点C
var popi = new Point((anchorCenter.x + p1.x) * 0.5, (anchorCenter.y + p1.y) * 0.5); //CG连线中点,也是抛物线的顶点O'
var baseX = new Point(p2.x - anchorCenter.x, p2.y - anchorCenter.y); //x方向的基向量CB
var baseY = new Point(anchorCenter.x - popi.x, anchorCenter.y - popi.y); //y方向的基向量O'C
var matrix = new Matrix();
//下面开始把对应的数值代入到矩阵中
matrix.a = baseX.x;
matrix.b = baseX.y;
matrix.c = baseY.x;
matrix.d = baseY.y;
matrix.tx = popi.x;
matrix.ty = popi.y;
//二次贝塞尔曲线在经过基向量矩阵换元后得到的一定是Y=X^2,无需再推导
//而直线的则为
//A(aX+cY+tx)+B(bX+dY+ty)+C=0
//展开可得
//(Aa+Bb)X+(Ac+Bd)Y+(A*tx+B*ty+C)=0
//基向量矩阵变换后得到的直线ABC系数
var convertedLineA = lineA * matrix.a + lineB * matrix.b;
var convertedLineB = lineA * matrix.c + lineB * matrix.d;
var convertedLineC = lineA * matrix.tx + lineB * matrix.ty + lineC;
//接着由AX+BY+C=0和Y=X^2得到
//AX+BX^2+C=0
//一元二次方程的3个系数
var squareFormulaA = convertedLineB;
var squareFormulaB = convertedLineA;
var squareFormulaC = convertedLineC;
//接着求根公式
var delta = squareFormulaB * squareFormulaB - 4 * squareFormulaA * squareFormulaC;
var intersections = [];
//delta小于0时无实数解
if(delta >= 0)
{
//在换元后的状态,y直接等于x的平方
var X1 = (- squareFormulaB + Math.sqrt(delta)) / 2 / squareFormulaA;
var Y1 = X1 * X1;
var X2 = (- squareFormulaB - Math.sqrt(delta)) / 2 / squareFormulaA;
var Y2 = X2 * X2;
//然后用基向量矩阵转换它们
intersections.push(matrix.transformPoint(new Point(X1, Y1)));
intersections.push(matrix.transformPoint(new Point(X2, Y2)));
}
var screenP0 = toScreen(p0);
var screenP1 = toScreen(p1);
var screenP2 = toScreen(p2);
var screenLineP0 = toScreen(lineP0);
var screenLineP1 = toScreen(lineP1);
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.lineWidth = 0.5;
context.strokeStyle = "#0000cc";
//绘制贝塞尔曲线
context.beginPath();
context.moveTo(screenP0.x, screenP0.y);
context.quadraticCurveTo(screenP1.x, screenP1.y, screenP2.x, screenP2.y);
context.stroke();
context.closePath();
//绘制直线
context.beginPath();
context.moveTo(screenLineP0.x, screenLineP0.y);
context.lineTo(screenLineP1.x, screenLineP1.y);
context.stroke();
context.closePath();
context.fillStyle = "#cc0000";
//绘制交点
for(var i = 0, len = intersections.length; i < len; i ++)
{
var toScreenP = toScreen(intersections[i]);
context.beginPath();
context.arc(toScreenP.x, toScreenP.y, 5, 0, Math.PI * 2, true);
context.fill();
context.closePath();
}
</script>
</body>
</html>
为了测试方便和统一直线和贝塞尔曲线的数据格式,我用端点代替了一般式,然后再自己计算出一般式的3个系数。这步的推导从略。
此外还有一个需要手算的地方是
//而直线的则为
//A(aX+cY+tx)+B(bX+dY+ty)+C=0
//展开可得
//(Aa+Bb)X+(Ac+Bd)Y+(A*tx+B*ty+C)=0
//基向量矩阵变换后得到的直线ABC系数
var convertedLineA = lineA * matrix.a + lineB * matrix.b;
var convertedLineB = lineA * matrix.c + lineB * matrix.d;
var convertedLineC = lineA * matrix.tx + lineB * matrix.ty + lineC;
不过只是直线方程的展开,没什么复杂的地方。
运行效果如下图所示,可见交点被正确算出来了。
现在我们可以试一下别的点,然后我在测试以下的一组数据时,出现了一个问题。
//贝塞尔曲线的3个点
var p0 = new Point(0, -1);
var p1 = new Point(5, 0.5);
var p2 = new Point(-1, 2);
//直线上的两个点
var lineP0 = new Point(-2, -2);
var lineP1 = new Point(4, 14 / 5);
交点只有一个,但是显示出来的却是两个。
但不难看出,这只是抛物线没有延长,点没落到贝塞尔曲线的区间而已,而不是计算的错误。所以我们只要再判断下点是否在曲线的区间内即可。
贝塞尔曲线方程是用t来做“自变量”的,其取值范围为[0, 1],所以我们需要根据xy的值算出t来。
然而这个方程还真是蛋疼,两个都是一元二次方程,解出来可能有两个解,从x解出来的t还要放进y里面进行检验,这下突然又麻烦回来,很不划算啊。
但如果脑袋也拐过弯来就可以找到更好的方法了。在基向量转换到标准方程的时候,所求得的点(X, Y)是落在标准抛物线上的。
在变形到这个位置之后,我们发现,根本不需要再求t了,而是看X是否位于[-1,1]或者Y是否位于[0, 1]即可。
所以在添加端点的时候,加入判断即可,来看看这部分调整后的代码。
if(delta >= 0)
{
//在换元后的状态,y直接等于x的平方
var X1 = (- squareFormulaB + Math.sqrt(delta)) / 2 / squareFormulaA;
var Y1 = X1 * X1;
var X2 = (- squareFormulaB - Math.sqrt(delta)) / 2 / squareFormulaA;
var Y2 = X2 * X2;
//然后用基向量矩阵转换它们
//不用判断大于等于0,这是一定的
if(Y1 <= 1)
{
intersections.push(matrix.transformPoint(new Point(X1, Y1)));
}
if(Y2 <= 1)
{
intersections.push(matrix.transformPoint(new Point(X2, Y2)));
}
}
再次运行,没落在贝塞尔曲线上的点就被过滤掉了。
真正做项目的时候,细节都有很多,比如这样的数据又出问题了。
//贝塞尔曲线的3个点
var p0 = new Point(0, -1);
var p1 = new Point(1, 2);
var p2 = new Point(2, 5);
//直线上的两个点
var lineP0 = new Point(-2, -2);
var lineP1 = new Point(4, 14 / 5);
交点没求出来,这是因为贝塞尔的3个点共线了,最后的结果为一条直线,解二次方程的时候二次项系数为0。所以这位置还要再加一个判断。
if(squareFormulaA != 0)
{
//接着求根公式
var delta = squareFormulaB * squareFormulaB - 4 * squareFormulaA * squareFormulaC;
//delta小于0时无实数解
if(delta >= 0)
{
//在换元后的状态,y直接等于x的平方
var X1 = (- squareFormulaB + Math.sqrt(delta)) / 2 / squareFormulaA;
var Y1 = X1 * X1;
var X2 = (- squareFormulaB - Math.sqrt(delta)) / 2 / squareFormulaA;
var Y2 = X2 * X2;
//然后用基向量矩阵转换它们
//不用判断大于等于0,这是一定的
if(Y1 <= 1)
{
intersections.push(matrix.transformPoint(new Point(X1, Y1)));
}
if(Y2 <= 1)
{
intersections.push(matrix.transformPoint(new Point(X2, Y2)));
}
}
}else if(squareFormulaB != 0)
{
var X = - squareFormulaC / squareFormulaB;
var Y = X * X;
intersections.push(matrix.transformPoint(new Point(X, Y)));
}
想到了二次项系数为0,自然也要想到一次项系数也为0的情况,这时候两直线平行,如果常数项都为0,则代表两直线重合。
再次运行,这种情况的交点也被算出来了。
现在我们用矩阵法实现了二次贝塞尔曲线和直线的求交,那么,为什么我会推荐用矩阵而非传统的做法?它跟常规的做法相比,优势和不足都有哪些呢?为了让大家更清楚矩阵法的优点,下一篇我会给出常规的求解方案,并进行对比分析,敬请期待!