贝塞尔曲线的身影几乎在所有绘图软件中都有出现,下面的代码演示了如何用AS3.0画一段简单的贝塞尔曲线(没有使用Document文档类,想测试的朋友,直接把下面的代码复制贴到第一帧即可)
import fl.controls.Label; var x1:uint=80; var y1:uint=200; var x2:uint=450; var y2:uint=200; var lbl1:Label = new Label(); var lbl2:Label = new Label(); var lbl3:Label = new Label(); lbl1.text="x1,y1"; lbl2.text="x2,y2"; lbl3.text="x3,y3"; addChild(lbl1); addChild(lbl2); addChild(lbl3); lbl1.move(x1-30,y1+5); lbl2.move(x2,y2+5); stage.addEventListener(MouseEvent.MOUSE_MOVE,MouseMoveHandler); function MouseMoveHandler(e:MouseEvent):void { drawCurve(mouseX,mouseY); } function drawCurve(uX:uint,uY:uint):void { graphics.clear();//清除上次画的内容 graphics.lineStyle(1,0xff0000,1);//设置线条为红色 graphics.drawCircle(x1,y1,5);//在x1,y1(左端点)处画一个圈做标记 graphics.drawCircle(x2,y2,5);//在x2,y2(右端点)处理一个圈做标记 var x3:uint=uX; var y3:uint=uY; lbl3.move(x3+15,y3); graphics.drawCircle(x3,y3,5);//在目标点,画一个圈 graphics.lineStyle(1,0,0.1);//设置线条为黑色,且透明度为0.1 graphics.moveTo(x1,y1); graphics.lineTo(x3,y3);//画一根从左端点到目标点的线 graphics.moveTo(x2,y2); graphics.lineTo(x3,y3);//画一根从右端点到目标点的线 graphics.moveTo(x1,y1); graphics.lineTo(x2,y2);//画一根从左端点到右端点的线 graphics.lineStyle(1,0xff0000,1);//设置线条为红色 graphics.moveTo(x1,y1); graphics.curveTo(x3,y3,x2,y2);//画一根从左端点出发,经过目标点,终点为右端点的贝赛尔曲线 } drawCurve(260,50);//刚显示时,先用该初始位置画线
一段曲线通常包含三个点:起点(x1,y1),控制点(x3,y3),终点(x2,y2);也许大家也看出来了:该曲线最终并不经过鼠标所在的点(x3,y3),在y轴方向上,曲线最大高度只有鼠标相对高度的一半,如果想真正的经过鼠标点,还要做一下修正(即要把控制点人为抬高或降低一些):
修正公式为:新坐标 = 目标点坐标 * 2 - (起点坐标+终点坐标)/2
即把刚才代码的第56行:
graphics.curveTo(x3,y3,x2,y2);
改为:
graphics.curveTo(2*x3-(x1+x2)/2,2*y3-(y1+y2)/2,x2,y2);
下面再来看下更多点的情况,假如随便给定一些点,要求根据这些点,画一段“平滑”的曲线,最容易想到的思路就是:先从第1个点,画到第3点(第2点为控制点),画出第一段,然后再以第3个点为开始,画到第5点(第4点为控制点)...类推直到全部画完
var numPoints:uint = 5; const X0 = 50; const Y0 = 50; const X_STEP = 70; const Y_STEP = 30; //先对点初始化 var points:Array = new Array(); for (var i:int = 0; i < numPoints; i++) { points[i] = new Object(); points[i].x = X0 + i * X_STEP; var tY:int = (i%2)==0? Y_STEP : (-1*Y_STEP); //trace(tY); points[i].y = Y0 + tY; graphics.lineStyle(1,0xff0000,1); graphics.drawCircle(points[i].x,points[i].y,(i+2)*2);//为了更直观,把这几个点都圈标出来 } graphics.lineStyle(1); //先将画笔移到第一个点 graphics.moveTo(points[0].x, points[0].y); //由于划一条曲线起码要有三个点(起点,终点,控制点),所以循环变量每次+2 for (i = 1; i < numPoints; i += 2) { graphics.curveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y); }
但是这样有一个问题,各段曲线之间的连接并不平滑,因为这完全只是把各段曲线简单的强型拼在一起而已,为了实现平滑,我们还得动点脑筋
var numPoints:uint = 7; const X0 = 50; const Y0 = 100; const X_STEP = 70; const Y_STEP = 90; //先对点初始化 var points:Array = new Array(); for (var i:int = 0; i < numPoints; i++) { points[i] = new Object(); points[i].x = X0 + i * X_STEP; var tY:int = (i%2)==0? Y_STEP : (-1*Y_STEP); //trace(tY); points[i].y = Y0 + tY; graphics.lineStyle(2,0xff0000,1); graphics.drawCircle(points[i].x,points[i].y,3);//为了更直观,把这几个点都圈标出来 } //为了看得更清楚,把新加的点,用蓝色标出来 for (i = 1; i < numPoints - 2; i ++) { var _xc:Number = (points[i].x + points[i + 1].x) / 2; var _yc:Number = (points[i].y + points[i + 1].y) / 2; graphics.lineStyle(3,0x0000ff,1); graphics.drawCircle(_xc,_yc,3); } graphics.lineStyle(1); //先把画笔移到第一点 graphics.moveTo(points[0].x, points[0].y); //去掉首尾二点后,根据剩下的点和新加的点画曲线 for (i = 1; i < numPoints - 2; i ++) { var xc:Number = (points[i].x + points[i + 1].x) / 2; var yc:Number = (points[i].y + points[i + 1].y) / 2; graphics.curveTo(points[i].x, points[i].y, xc, yc); } //处理最后一点 graphics.curveTo(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
ok,这样就平滑了,来看下原理:
先把第一点跟最后一点忽略掉(why? 因为刚才的问题仅出在各段曲线的连接点上,而第一段的开头与最后一段曲线的结尾本身没有不平滑的问题,所以我们修正时,不需要管二端),然后在各点之间插入一个新的辅助点(即上图中蓝色的点),点的位置其实可以随便定义,本例中正好取了中间位置,然后把这些新加的蓝色点以原来的头尾二点整体看作起始点与结束点,其它的点看做控制点(即去掉头尾后的红点),这样就行了 :)
类似的,我们还可以再增加三个辅助点,以达到闭合封闭曲线的效果:
var numPoints:uint = 7; const X0 = 50; const Y0 = 100; const X_STEP = 70; const Y_STEP = 90; //先对点初始化 var points:Array = new Array(); for (var i:int = 0; i < numPoints; i++) { points[i] = new Object(); points[i].x = X0 + i * X_STEP; var tY:int = (i%2)==0? Y_STEP : (-1*Y_STEP); //trace(tY); points[i].y = Y0 + tY; graphics.lineStyle(2,0xff0000,1); graphics.drawCircle(points[i].x,points[i].y,3);//为了更直观,把这几个点都圈标出来 } var _X_BEGIN = (points[0].x + points[1].x) / 2; var _Y_BEGIN = (points[0].y + points[1].y) / 2; graphics.lineStyle(3,0x00ff00,1); graphics.drawCircle(_X_BEGIN,_Y_BEGIN,3); //为了看得更清楚,把新加的点,用蓝色标出来 for (i = 1; i < numPoints - 2; i ++) { var _xc:Number = (points[i].x + points[i + 1].x) / 2; var _yc:Number = (points[i].y + points[i + 1].y) / 2; graphics.lineStyle(3,0x0000ff,1); graphics.drawCircle(_xc,_yc,3); } graphics.lineStyle(1); //先把画笔移到第一个辅助点 graphics.moveTo(_X_BEGIN, _Y_BEGIN); //去掉首尾二点后,根据剩下的点和新加的点画曲线 for (i = 1; i < numPoints - 2; i ++) { var xc:Number = (points[i].x + points[i + 1].x) / 2; var yc:Number = (points[i].y + points[i + 1].y) / 2; graphics.curveTo(points[i].x, points[i].y, xc, yc); } var _len:uint = points.length; //倒数第二个绿点 var _X_END_1 = (points[_len-2].x + points[_len-1].x)/2; var _Y_END_1 = (points[_len-2].y + points[_len-1].y)/2; //最后一个绿点 var _X_END_2 = (points[_len-1].x + points[0].x)/2; var _Y_END_2 = (points[_len-1].y + points[0].y)/2; //最后一个蓝点为起点,到_X_END_1,_Y_END_1,倒数第二个红点为控制点 graphics.curveTo(points[i].x, points[i].y, _X_END_1,_Y_END_1); graphics.curveTo(points[_len-1].x, points[_len-1].y, _X_END_2,_Y_END_2); graphics.curveTo(points[0].x, points[0].y, _X_BEGIN,_Y_BEGIN); graphics.lineStyle(3,0x00ff00,1); graphics.drawCircle(_X_END_1,_Y_END_1,3); graphics.drawCircle(_X_END_2,_Y_END_2,3);
最后把上面这段代码抽象封装一下:
package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.events.Event; import flash.display.Graphics; import flash.ui.MouseCursor; import flash.ui.Mouse; public class ShowCurve extends Sprite { private var isStop:Boolean; public function ShowCurve():void { init(); } private function init() { stage.addEventListener(MouseEvent.MOUSE_DOWN,MouseDownHandler); addEventListener(Event.ENTER_FRAME,EnterFrameHandler); isStop = false; Mouse.cursor = MouseCursor.BUTTON; } private function MouseDownHandler(e:MouseEvent) { if (!isStop){ removeEventListener(Event.ENTER_FRAME,EnterFrameHandler); isStop = true; } else{ addEventListener(Event.ENTER_FRAME,EnterFrameHandler); isStop = false; } } private function EnterFrameHandler(e:Event):void { graphics.clear(); var numPoints:uint=9; //先对点初始化 var points:Array = new Array(); for (var i:int = 0; i < numPoints; i++) { points[i] = new Object(); points[i].x=stage.stageWidth*Math.random(); points[i].y=stage.stageHeight*Math.random(); graphics.lineStyle(2,0xff0000,1); graphics.drawCircle(points[i].x,points[i].y,1);//为了更直观,把这几个点都圈标出来 } var _X_BEGIN = (points[0].x + points[1].x) / 2; var _Y_BEGIN = (points[0].y + points[1].y) / 2; graphics.lineStyle(1,0x00ff00,1); graphics.drawCircle(_X_BEGIN,_Y_BEGIN,1); //为了看得更清楚,把新加的点,用蓝色标出来 for (i = 1; i < numPoints - 2; i ++) { var _xc:Number = (points[i].x + points[i + 1].x) / 2; var _yc:Number = (points[i].y + points[i + 1].y) / 2; graphics.lineStyle(3,0x0000ff,1); graphics.drawCircle(_xc,_yc,1); } graphics.lineStyle(1); //先把画笔移到第一个辅助点 graphics.moveTo(_X_BEGIN, _Y_BEGIN); //去掉首尾二点后,根据剩下的点和新加的点画曲线 for (i = 1; i < numPoints - 2; i ++) { var xc:Number = (points[i].x + points[i + 1].x) / 2; var yc:Number = (points[i].y + points[i + 1].y) / 2; graphics.curveTo(points[i].x, points[i].y, xc, yc); } var _len:uint=points.length; //倒数第二个绿点 var _X_END_1 = (points[_len-2].x + points[_len-1].x)/2; var _Y_END_1 = (points[_len-2].y + points[_len-1].y)/2; //最后一个绿点 var _X_END_2 = (points[_len-1].x + points[0].x)/2; var _Y_END_2 = (points[_len-1].y + points[0].y)/2; //最后一个蓝点为起点,到_X_END_1,_Y_END_1,倒数第二个红点为控制点 graphics.curveTo(points[i].x, points[i].y, _X_END_1,_Y_END_1); graphics.curveTo(points[_len-1].x, points[_len-1].y, _X_END_2,_Y_END_2); graphics.curveTo(points[0].x, points[0].y, _X_BEGIN,_Y_BEGIN); graphics.lineStyle(1,0x00ff00,1); graphics.drawCircle(_X_END_1,_Y_END_1,1); graphics.drawCircle(_X_END_2,_Y_END_2,1); } } }
注:本文中所演示的只是二次曲线,对于三次曲线或高阶曲线,就更复杂了。详情见维基百科: http://zh.wikipedia.org/wiki/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A