【原创】《矩阵的史诗级玩法》连载二十二:直线和二次贝塞尔曲线求交的矩阵法代码

这篇我们来把完整的代码实现一下,大家可以新开个窗口打开连载二十一,然后把数学的实现步骤跟代码的实现步骤一一对应着看。

<!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,则代表两直线重合。

再次运行,这种情况的交点也被算出来了。

现在我们用矩阵法实现了二次贝塞尔曲线和直线的求交,那么,为什么我会推荐用矩阵而非传统的做法?它跟常规的做法相比,优势和不足都有哪些呢?为了让大家更清楚矩阵法的优点,下一篇我会给出常规的求解方案,并进行对比分析,敬请期待!

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值