3.4 使用quadraticCurveTo绘制二次贝赛尔曲线
Canvas提供了一系列的方法来绘制曲线,比如quadraticCurveTo(通过起始两个点以及一个控制点来绘制,前两个参数为控制点横纵坐标,后两个参数为终点横纵坐标,使用的是数学上的二次贝赛尔方程)。下面我们来看一下常见的一些使用。
固定控制点
如下程序,我们实现了一个固定起始点,使用鼠标位置做为控制点,来绘制二次贝赛尔曲线的应用:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Canvas</title>
</head>
<body>
<canvas id="canvas" width="400" height="400">当前浏览器不支持canvas</canvas>
<script type="text/javascript" src="utils.js"></script>
<script type="text/javascript" src="arrow.js"></script>
<script type="text/javascript">
window.οnlοad=function(){
var canvas=document.getElementById("canvas");
var context=canvas.getContext("2d");
var mouse=utils.captureMousePosition(canvas);
//固定两个端点
var x0=100,y0=200,x2=300,y2=200;
canvas.addEventListener('mousemove',function(){
//context.clearRect(0,0,canvas.width,canvas.height);
//指定控制点为鼠标位置
var x1=mouse.x;
var y1=mouse.y;
//绘制曲线
context.beginPath();
context.moveTo(x0,y0);
context.quadraticCurveTo(x1,y1,x2,y2);
context.stroke();
},false);
};
</script>
</body>
</html>
如下,即为其效果图:
曲线过定点
之前我们绘制的曲线是控制点确定的曲线,如果我们已知曲线上的一个点(非起始点),则需要通过贝赛尔方程的一些特性来处理。(n次贝赛尔曲线有n+1个控制点,并且贝赛尔曲线位于这些控制点组成的多边形内部。)
已知两个端点(x0,y0)与(x2,y2)及曲线上的一个点(x1,y1).则可以得到其控制点的位置为
x = x1 * 2 - (x0 + x2) / 2; y = y1 * 2 - (y0 + y2) / 2;
(参见如下文章:
http://learn.gxtc.edu.cn/NCourse/jxcamcad/cadcam/Mains/main4-2.htm
http://www.leeyou.net/UTutorial/show.php?cid=153
http://hi.baidu.com/ggggwhw/blog/item/ffd23d35f313543c5ab5f5ed.html)。
所以,我们只需要修正上述固定控制点的代码,通过曲线上的点来求新的固定点的坐标即可:
var x1=mouse.x*2-(x0+x2)/2; var y1=mouse.y*2-(y0+y2)/2;
多点控制的平滑曲线
下面来看一个复杂一点的问题,如果我们随机有N个点,如何来使用这N个点,利用二次贝赛尔曲线来绘制一条平滑的曲线?最简单的想法就是每三个点一组来绘制一个二次贝赛尔曲线,不难得到如下代码:
window.οnlοad=function(){ var canvas=document.getElementById("canvas"); var context=canvas.getContext("2d"); var points=[]; var pointNumber=9; //随机生成点 for(var i=0;i<pointNumber;i++){ points.push({ x:Math.random()* canvas.width, y:Math.random()* canvas.height }); } //每三点来绘制一条曲线 context.beginPath(); context.moveTo(points[0].x,points[0].y); for(var i=1;i< pointNumber; i+=2){ context.quadraticCurveTo(points[i].x,points[i].y,points[i+1].x,points[i+1].y); } context.stroke(); //绘制所有给定的点 for(var i=0;i<pointNumber;i++){ context.moveTo(points[i].x,points[i].y); context.beginPath(); context.arc(points[i].x,points[i].y,2,0,Math.PI*2,true); context.closePath(); context.fill(); } };
查看结果,会发现并不是想要的效果,除了共用起始点外,所有的二次曲线都是独立分隔的,并不是一条平滑的曲线,如下图:
这是因为使用二次贝赛尔方程来绘制一个多点控制的平滑曲线,不可以每三个已知点为一组依次来绘制曲线,这样绘制出来的每一个二次曲线并不在一个相对坐标系统内。可以从第二个点开始,使用每两个点的中点来做端点,其它点均做为控制点来绘制曲线,如下图:(空心点为已知的给定点,实心点为给定点的中点)
按上面的说明,我们来更新代码:
window.οnlοad=function(){ var canvas=document.getElementById("canvas"); var context=canvas.getContext("2d"); var points=[]; var pointNumber=9; var ctrlPoint={x:0,y:0}; for(var i=0;i<pointNumber;i++){ points.push({ x:Math.random()* canvas.width, y:Math.random()* canvas.height }); } context.beginPath(); context.moveTo(points[0].x,points[0].y); //从第二个点开始,使用之后两点之间的“中点”来做为端点 for(var i=1;i< pointNumber-2; i++){ ctrlPoint.x=(points[i].x+points[i+1].x)/2; ctrlPoint.y=(points[i].y+points[i+1].y)/2; context.quadraticCurveTo(points[i].x,points[i].y,ctrlPoint.x,ctrlPoint.y); } //最后两个点需要特殊处理,无法使用此两点间的“中点” context.quadraticCurveTo(points[i].x,points[i].y,points[i+1].x,points[i+1].y); context.stroke(); for(i=0;i<pointNumber;i++){ context.moveTo(points[i].x,points[i].y); context.beginPath(); context.arc(points[i].x,points[i].y,5,0,Math.PI*2,true); context.closePath(); context.fill(); } };
其效果如下图:
封闭曲线的实现
可以看出,上述代码对于第一个点,最后一个点相关的“中点”都没有使用到。如果我们想利用到这两个点相关的“中点”来构成一个封闭的平滑曲线。那把所有的点均做为控制点来处理,使用它们的“中点”来做为每一个二次贝赛尔曲线的端点,如下代码所示:
window.οnlοad=function(){ var canvas=document.getElementById("canvas"); var context=canvas.getContext("2d"); var points=[]; var pointNumber=9; var ctrlPoint={x:0,y:0}; var ctrlPoint1={x:0,y:0}; for(var i=0;i<pointNumber;i++){ points.push({ x:Math.random()* canvas.width, y:Math.random()* canvas.height }); } //构造控制点,使用给定点的“中点”来做为曲线的端点 ctrlPoint1.x=(points[0].x+points[pointNumber-1].x)/2; ctrlPoint1.y=(points[0].y+points[pointNumber-1].y)/2; context.beginPath(); context.moveTo(ctrlPoint1.x,ctrlPoint1.y); for(var i=0;i< pointNumber-1; i++){ ctrlPoint.x=(points[i].x+points[i+1].x)/2; ctrlPoint.y=(points[i].y+points[i+1].y)/2; context.quadraticCurveTo(points[i].x,points[i].y,ctrlPoint.x,ctrlPoint.y); } //封闭曲线 context.quadraticCurveTo(points[i].x,points[i].y,ctrlPoint1.x,ctrlPoint1.y); context.stroke(); for(i=0;i<pointNumber;i++){ context.moveTo(points[i].x,points[i].y); context.beginPath(); context.arc(points[i].x,points[i].y,2,0,Math.PI*2,true); context.closePath(); context.fill(); } }
其效果图如下:
注意上面的代码中的i,我们在第一个循环外也使用了i,这是因为JavaScript的变量作用域不是块级的,而是以函数来区分的,如:
console.log(j); //undefined for(var j=0;j<10;j++){ console.log(j); //1~9 } console.log(j); //10