ベジェ曲線を使ったアニメーション(リプレイ機能付き)

突然ですが3次ベジェ曲線だと制御点が4つもあるので、スクリプトで制御する際にコードから描画イメージが沸きづらくないですか?
グラフィックソフトで普段からベジェ曲線に慣れている人であればそうでも無いのかもしれないですが。。。

というわけで制御に慣れるために、マウスに連動してベジェ曲線を描画するサンプルを作ってみました。

ベジェ曲線の描画は、id:nitoyonさんのところからcurveTo()を使った3次ベジェ曲線の実装を拝借しています。

今回の実装では、4つの制御点のうち始点と終点はマウス位置にしています。そうすることで鱗形の曲線が描かれます。
ほんとは4つ全部をバラバラに動かした方が制御に慣れるんでしょうが、僕のレベルでは4つを奇麗にまとめて動かす方法が思いつきませんでした。

それとベジェ曲線とは全然関係ないんですが、マウスの動きをリプレイするコードを実装してみました。
単純に、マウス位置と時間を記録してTimerを使って再生しているだけですが。

ソース一式はこちらからダウンロードできます。
コンパイルにはTweenerが必要です。

以下、コード(一部)です。

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	import caurina.transitions.*;
	import com.potix2.utils.HSV;

	[SWF(width="800", height="600", backgroundColor="#000000", frameRate="30")]
	public class BezierTest extends Sprite {
		private var NUMBER_OF_CURVES:uint = 30;
		private var myCurves:Array;
		private var currentCursor:uint = 0;
		private var prevX:Number;
		private var prevY:Number;

		private var recCurves:Array;
		private var recCurrentCursor:uint = 0;
		private var recPrevX:Number;
		private var recPrevY:Number;
		private var replayData:Array;
		private var replayIndex:uint;

		private var recWaitTimer:Timer;
		private var replayTimer:Timer;
		private var counter:Number;
		private var divisions:uint;
		private var visibleCurves:uint;
		private var colorVelocity:Number;
		private var replaying:Boolean;
		private var recording:Boolean;
		public function BezierTest() {
			recording = false;
			replaying = false;

			counter = 0;
			currentCursor = 0;
			divisions = 10;
			visibleCurves = 5;
			colorVelocity = 1.2;

			recWaitTimer = new Timer(1000, 1);
			initCurves();
			initListeners();
		}

		private function initCurves():void {
			myCurves = new Array();
			var i:uint;
			var curve:BezierCurve;
			for (i = 0; i < NUMBER_OF_CURVES; i++ ) {
				curve = new BezierCurve(new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0));
				curve.color = 0;
				curve.divisions = this.divisions;
				addChild(curve);
				myCurves.push(curve);
			}

			// for replay
			recCurves = new Array();
			for (i = 0; i < NUMBER_OF_CURVES; i++ ) {
				curve = new BezierCurve(new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0));
				curve.color = 0;
				curve.divisions = this.divisions;
				addChild(curve);
				recCurves.push(curve);
			}
		}

		private function initListeners():void {
			stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
			recWaitTimer.addEventListener(TimerEvent.TIMER, function(event:TimerEvent):void { startReplay(); });
		}

		private function drawCurves(curves:Array, x0:Number, y0:Number, x1:Number, y1:Number, startIndex:uint):void {
			var dx:Number = x1 - x0;
			var dy:Number = y1 - y0;

			for ( var i:uint = 0; i < visibleCurves; i++ ) {
				var curve:BezierCurve = BezierCurve(curves[(startIndex + i) % NUMBER_OF_CURVES]);
				curve.p0 = new Point(x1, y1);
				curve.p1 = new Point(x1 - dx * 10, y1 - dy * 10);
				curve.p2 = new Point(Math.random() * stage.stageWidth, stage.stageHeight * Math.random());
				curve.p3 = new Point(x1, y1);
				curve.hsv = new HSV((counter * colorVelocity) % 360, 0.8, 1.0);
				curve.visible = true;
				curve.draw();
				Tweener.addTween(curve, {
					time: 0.2,
					transition: "easeInSine",
					v: 0,
					onUpdate: function():void {
						this.draw();
					},
					onComplete: function():void {
						this.visible = false;
					}
				});
			}
		}

		private function onMouseMove(event:MouseEvent):void {
			recWaitTimer.reset();
			if ( !replaying && !recording ) {
				recording = true;
				replayIndex = 0;
				replayData = new Array();
			}

			counter++;
			drawCurves(this.myCurves, prevX, prevY, mouseX, mouseY, currentCursor);
			prevX = mouseX;
			prevY = mouseY;
			currentCursor = (currentCursor + visibleCurves) % NUMBER_OF_CURVES;

			if ( recording ) {
				replayData.push({
					x: mouseX,
					y: mouseY,
					time: getTimer()
				});
				recWaitTimer.start();
			}
		}

		private function startReplay():void {
			replayIndex = 0;
			recording = false;
			replaying = true;

			if ( replayTimer != null ) {
				replayTimer.removeEventListener(TimerEvent.TIMER, replayDraw);
			}
			replayTimer = new Timer(10, 1);
			replayTimer.addEventListener(TimerEvent.TIMER, replayDraw);
			replayTimer.start();
		}

		private function replayDraw(event:TimerEvent):void {
			counter++;
			var cd:Object = replayData[replayIndex];
			drawCurves(this.recCurves, recPrevX, recPrevY, cd.x, cd.y, recCurrentCursor);
			recPrevX = cd.x;
			recPrevY = cd.y;
			recCurrentCursor = (recCurrentCursor + visibleCurves) % NUMBER_OF_CURVES;
			replayIndex++;
			if ( replayIndex == replayData.length ) {
				// end
				replaying = false;
			}
			else {
				if ( replayTimer != null ) {
					replayTimer.removeEventListener(TimerEvent.TIMER, replayDraw);
				}
				replayTimer = new Timer(replayData[replayIndex].time - replayData[replayIndex - 1].time, 1);
				replayTimer.addEventListener(TimerEvent.TIMER, replayDraw);
				replayTimer.start();
			}
		}
	}

}