[AS3] Illustratorのような3次ベジェ曲線を書く

2010/04/14

こんにちは。きんくまです。
今回は、3次ベジェ曲線についてです。

このブログでも今までも何度かベジェ曲線について調べたりしてました。

>> 線をアニメーションするやつを作ってみたけど色々と課題が…
>> 線のアニメーションできた!ただし節はあいかわらず…

でまあ、nutsu先生のを参考にいろいろとやってたりしました。

今回はとある案件で必要になりそうなので、もう少しつっこんでやってみようという感じです。

↓ コントロールポイントとハンドルをドラッグできます。

AS3だと2次ベジェの描画APIしかないんで、イラレみたいな3次ベジェは自分で実装します。
といっても、自分じゃわからないので、下記サイトにお世話になりました。

>> ベジエ曲線の仕組み (3) – 3次ベジエ曲線(てっく煮ブログ)
>>滑らかな3次ベジェでゆらゆら曲線を描く(FICC LABS)

それで、3次ベジェを2次ベジェで近似したりもできたりするみたいですが、今回は直線で近似しました。
ポイントはBezierSegmentというクラスです。

BezierSegment.getValue(t)
というやつで、0 <= t <= 1 の範囲でtをつっこむと、そのときのx,y座標がもとめられちゃう便利メソッドです。 今回のソースで、工夫したところは、セグメントをアンカー1、ハンドル2のセットにして、 一方のハンドルをドラッグしたらうまくもう片方のハンドルが自動で線対称になるところです。 ソースです。メイン

package  
{
	import fl.motion.BezierSegment;
	import flash.display.DisplayObject;
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.geom.Point;
	/**
	 * Cubic Bezier Curve like Illustrator
	 * Illustratorのような3次ベジェ曲線
	 * 
	 * @see ベジエ曲線の仕組み (3) – 3次ベジエ曲線(てっく煮)
	 * http://d.hatena.ne.jp/nitoyon/20070920/bezier_3
	 * 
	 * @see 滑らかな3次ベジェでゆらゆら曲線を描く(FICC LABS)
	 * http://www.ficc.jp/labs/archives/akira/3d_beizer/
	 * 
	 * @author KinkumaDesign
	 */
	[SWF(width=”450″, height=”450″, frameRate=”30″, backgroundColor=”0xffffff”)]
	public class LineSprite extends Sprite
	{
		private var segments:Array;
		private var numSegments:int = 5;
		
		public function LineSprite() 
		{
			segments = [];
			addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		private function init(e:Event):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			addEventListener(Event.ENTER_FRAME, update);
			
			var segment:Segment;
			var tx:Number, ty:Number;
			var hw:Number = stage.stageWidth / 2;
			var hh:Number = stage.stageHeight / 2;
			var tRad:Number = Math.PI * 2 / numSegments;
			for (var i:int = 0; i < numSegments; i++) {				
				tx = hw + Math.cos(tRad * i) * (hw - 50);
				ty = hh + Math.sin(tRad * i) * (hh - 50);
				segment = new Segment(new Point(tx, ty), Math.random() * 2 * Math.PI, Math.random() * 100 + 10);
				addChild(segment);
				segments.push(segment);
			}
		}
		
		public function update(e: Event):void
		{
			var i:int;
			var p0:Point, p1:Point, p2:Point, p3:Point;
			var seg0:Segment;
			var seg1:Segment;
			var cnt:int = numSegments - 1;
			while (cnt > -1) {
				seg0 = segments[cnt];
				seg0.update();
				cnt–;
			}
			
			var g:Graphics = this.graphics;
			g.clear();
			
			g.lineStyle(1, 0x666666);
			for (i = 0; i < numSegments - 1; i++) {
				seg0 = segments[i];
				seg1 = segments[i + 1];
				p0 = seg0.anchorPos;
				p1 = seg0.handle1Pos;
				p2 = seg1.handle0Pos;
				p3 = seg1.anchorPos;
				drawBezier(p0, p1, p2, p3, g);
			}
		}
		
		private function drawBezier(p0:Point, p1:Point, p2:Point, p3:Point, g:Graphics):void
		{
			var bezier:BezierSegment = new BezierSegment(p0, p1, p2, p3);
			var numSplit:int = 30;
			var i:int;
			var t:Number = 1 / numSplit;
			var drawPt:Point;
			g.moveTo(p0.x, p0.y);
			for (i = 1; i <= numSplit; i++) {
				drawPt = bezier.getValue(t * i);
				g.lineTo(drawPt.x, drawPt.y);
			}
		}
	}

}

Segment
ハンドルの長さや角度を外から設定してあげることもできます。

package  
{
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	/**
	 * ...
	 * @author KinkumaDesign
	 */
	public class Segment extends Sprite
	{
		private var _handleAngle:Number; //傾き
		private var _handleHalfLen:Number;
		private var _handle0:ControlPoint;
		private var _handle1:ControlPoint;
		private var _anchor:ControlPoint;
		
		public function Segment(anchorPoint:Point, handleAngle:Number, handleHalfLength:Number) 
		{
			_anchor = new ControlPoint(0x4F80FF, 2, true);
			addChild(_anchor);
			_anchor.x = anchorPoint.x;
			_anchor.y = anchorPoint.y;
			
			_handle0 = new ControlPoint(0x4F80FF, 2);
			addChild(_handle0);
			
			_handle1 = new ControlPoint(0x4F80FF, 2);
			addChild(_handle1);
			
			this.handleAngle = handleAngle;
			this.handleHalfLength = handleHalfLength;
		}
		
		public function set handleHalfLength(value:Number):void
		{
			_handleHalfLen = value;
		}
		
		public function set handleAngle(value:Number):void
		{
			_handleAngle = value;
		}
		
		public function get handle0Pos():Point
		{
			return new Point(_handle0.x, _handle0.y);
		}
		
		public function get handle1Pos():Point
		{
			return new Point(_handle1.x, _handle1.y);
		}
		
		public function get anchorPos():Point
		{
			return new Point(_anchor.x, _anchor.y);
		}
		
		public function set anchorPos(pos:Point):void
		{
			_anchor.x = pos.x;
			_anchor.y = pos.y;
		}
		
		public function update():void
		{
			if (_handle0.isDragging || _handle1.isDragging) {
				updateHandlePositionByDrag();
			}else {
				updateHandlePosition();
			}
			draw();
		}
		
		private function updateHandlePosition():void
		{
			var dx:Number = Math.cos(_handleAngle) * _handleHalfLen;
			var dy:Number = Math.sin(_handleAngle) * _handleHalfLen;
			_handle0.x = -dx + _anchor.x;
			_handle0.y = -dy + _anchor.y;
			_handle1.x = dx + _anchor.x;
			_handle1.y = dy + _anchor.y;
		}
		
		private function updateHandlePositionByDrag():void
		{
			var baseHandle:ControlPoint;
			var moveHandle:ControlPoint;
			if (_handle0.isDragging) {
				baseHandle = _handle0;
				moveHandle = _handle1;
			}else if(_handle1.isDragging){
				baseHandle = _handle1;
				moveHandle = _handle0;
			}
			var dx:Number = _anchor.x - baseHandle.x;
			var dy:Number = _anchor.y - baseHandle.y;
			_handleAngle = Math.atan2(dy, dx);
			if (baseHandle == _handle1) {
				_handleAngle += Math.PI;
			}
			_handleHalfLen = Math.sqrt(dx * dx + dy * dy);
			moveHandle.x = dx + _anchor.x;
			moveHandle.y = dy + _anchor.y;
		}
		
		public function draw():void
		{
			var g:Graphics = this.graphics;
			g.clear();
			g.lineStyle(1, 0x4F80FF);
			g.moveTo(_handle0.x, _handle0.y);
			g.lineTo(_handle1.x, _handle1.y);
		}
	}

}

ControlPoint

package  
{
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	/**
	 * ...
	 * @author KinkumaDesign
	 */
	public class ControlPoint extends Sprite
	{
		private var _isDragging:Boolean = false;
		
		public function ControlPoint(color:int = 0xff0000, size:int = 3, isAnchor:Boolean = false) 
		{
			var g:Graphics = this.graphics;
			if (isAnchor) {
				g.lineStyle(1, color);
			}else {
				g.beginFill(color, 1);
			}
			g.drawCircle(0, 0, size);
			g.endFill();
			g.lineStyle(0,0,0);
			g.beginFill(0, 0);
			g.drawCircle(0, 0, 5);
			g.endFill();
			addEventListener(MouseEvent.MOUSE_DOWN, downHD);
			buttonMode = true;
		}
		
		private function downHD(e:MouseEvent):void 
		{
			stage.addEventListener(MouseEvent.MOUSE_UP, upHD);
			startDrag(false);
			_isDragging = true;
		}
		
		private function upHD(e:MouseEvent):void 
		{
			stage.removeEventListener(MouseEvent.MOUSE_UP, upHD);
			stopDrag();
			_isDragging = false;
		}
		
		public function get isDragging():Boolean
		{
			return _isDragging;
		}
	}

}

wonderflにもアップしようとしたら、fl.motion.BezierSegmentがないっていわれて
コンパイルできんかった、、。
>> Cubic Bezier Curve like Illustrator | Illustratorのような3次ベジェ曲線 (wonderfl)


自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る