勝手に添削:数学的な曲線を描画する (2)
勝手に添削:数学的な曲線を描画する の完結篇。
今回は図形クラスに手を入れていく。
図形ごとに異なるクラスを定義しているんだけど、描画のルーチンは同じものだ。異なるのは座標計算の部分だけ。
ならば、座標データを外からパラメータとして渡すようにしてやろう。こんな具合に。
private var shapes:Array = [
{
text : "円",
rad : 180,
fx : function(r:Number, t:Number, x:Number):Number{
return r * Math.cos(t) + x
},
fy : function(r:Number, t:Number, y:Number):Number{
return r * Math.sin(t) + y
}
},
{
text : "三葉線",
rad : 180,
fx : function(r:Number, t:Number, x:Number):Number{
return r * Math.sin(3 * t) * Math.cos(t) + x
},
fy : function(r:Number, t:Number, y:Number):Number{
return r * Math.sin(3 * t) * Math.sin(t) + y
}
},
// (以下略)
あとは、このオブジェクトを食う描画クラスを作る。
Tweener みたいな使い方ができるように、static なメソッドに変えた。
import flash.display.Sprite;
import flash.events.Event;
class PolarDrawer{
// 描画メソッド (Sprite, 円の半径, 中心x, 中心y, Object)
static public function draw(m:Sprite, cx:Number, cy:Number, shape:Object):void {
var rad:Number = shape.rad * Math.random();
var angle:Number = 0;
m.graphics.lineStyle(1, 0xff0000);
m.graphics.moveTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy));
m.addEventListener(Event.ENTER_FRAME, function(e:Event):void{
angle += 0.05
m.graphics.lineTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy))
if(angle >= 2*Math.PI){
m.removeEventListener("enterFrame", arguments.callee);
}
});
}
}
shapes[i] を PolarDrawer.draw() を呼び出せば描画できる。クリック時イベントで次のように呼び出している。
private function clickHandler(e:Event):void{
var s:Sprite = new Sprite();
addChild(s);
PolarDrawer.draw(s, mouseX, mouseY, shapes[curveMode]);
}
はい、だいぶシンプルになりました。行数はたった105行! (ソースは最後に掲載)
シンプルは重要
デザイン用のコードなんだから勢いでもいいじゃないか、と考える人もいるだろうけど、個人的にはデザイナの人にこそ綺麗なコードを書いてもらいたいと思ってる。
というのも、修正前のコードは7つのクラスに描画ルーチンが散らばっていた。例えば、次のような実験をしようにも、めんどくさくて青ざめてしまう。
- 色を少しずつ変えて線を引きたい
- 線の太さを変えたい
- 線を引く開始角度をランダムにしたい
今回の改造で描画処理が1箇所に集約されたので、これらの実験が簡単に実現できるようになった。気軽に試して芸の肥やし(?)にできるというわけだ。
ただ、最初からきれいに書こうとしてたら、何もかけなくなってしまう。自分も最初は勢いで書き始める方だ。
コードを整形するのは、同じコードを3箇所以上に書きたくなったときにしている。同じものが散らばっているとメンテナンスが大変だし、後でソースを読むときに違う部分を探し出すのが面倒だからだ。
心構えをマスターしよう
プログラム言語が分かってきたら、OOP の概念とかデザインパターンに行ってしまうよりも前に、ソースをシンプルに書く方法を身につけていくのがよいと思う。
そんな「心構え」を教えてくれるのがこの本。
リファクタリング―プログラムの体質改善テクニック (Object Technology Series)
- 作者: マーチン ファウラー
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2000-05
- メディア: 単行本
- Amazon のレビューを見る
既存のコードをきれいに、シンプルにしていくためのレシピが多数紹介されている。今回や前回のエントリで行った改造も、全部この本に載ってるレシピで説明できる。
挙動を変えずに、ソースを変更していくこと(=リファクタリング)を学ぶにはうってつけ。去年読んだんだけど、もうちょっと早くこの本に出会ってればなぁ…と後悔した。自信をもってお薦めできる数少ない良本!
本で紹介されているソースコードは Java だけど、最初の導入でちょっと長めなソースがでてくるぐらいで、ほとんどが概念的なコード。ActionScript しか知らない人でも違和感なく読めると思う。ActionScript も Java みたいなもんだし。
ソースコード
きもい書き方で行数を短くしてたりするけど、そこはご愛嬌で。
package{
import flash.display.Sprite;
import flash.events.*;
import flash.text.TextField;
public class Test extends Sprite{
private var dtext:TextField;
private var curveMode:int = 0;
private var vertex:int = 5; // for hypocycloid
private var shapes:Array = [
{
text : "円",
rad : 180,
fx : function(r:Number, t:Number, x:Number):Number{return r * Math.cos(t) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(t) + y}
},
{
text : "三葉線",
rad : 180,
fx : function(r:Number, t:Number, x:Number):Number{return r * Math.sin(3 * t) * Math.cos(t) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(3 * t) * Math.sin(t) + y}
},
{
text : "螺旋",
rad : 15,
fx : function(r:Number, t:Number, x:Number):Number{return r * t * Math.cos(t) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * t * Math.sin(t) + y}
},
{
text : "アステロイド曲線",
rad : 150,
fx : function(r:Number, t:Number, x:Number):Number{return r * Math.pow(Math.cos(t), 3) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * Math.pow(Math.sin(t), 3) + y}
},
{
text : "内サイクロイド",
rad : 30,
fx : function(r:Number, t:Number, x:Number):Number{return r * (vertex * Math.cos(t) + Math.cos(vertex * t)) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * (vertex * Math.sin(t) - Math.sin(vertex * t)) + y}
},
{
text : "リサジュー曲線",
rad : 180,
fx : function(r:Number, t:Number, x:Number):Number{return r * Math.sin(2 * t) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(3 * t) + y}
},
{
text : "四葉線",
rad : 150,
fx : function(r:Number, t:Number, x:Number):Number{return r * Math.sin(2 * t) * Math.cos(t) + x},
fy : function(r:Number, t:Number, y:Number):Number{return r * Math.sin(2 * t) * Math.sin(t) + y}
}
];
public function Test(){
addChild(dtext = new TextField());
dtext.text = shapes[curveMode].text;
stage.addEventListener(MouseEvent.CLICK,clickHandler);
stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownHandler);
}
private function clickHandler(e:Event):void{
PolarDrawer.draw(Sprite(addChild(new Sprite())), mouseX, mouseY, shapes[curveMode]);
}
private function erase():void{
for(var i:int = numChildren - 1; i >= 0; i--)
if(getChildAt(i) is Sprite)
removeChildAt(i);
}
private function keyDownHandler(e:KeyboardEvent):void{
if(49 <= e.keyCode && e.keyCode < 49 + shapes.length){
curveMode = e.keyCode - 49;
dtext.text = shapes[curveMode].text;
}
else if(e.keyCode == 49 + shapes.length)
erase();
}
}
}
import flash.display.Sprite;
import flash.events.Event;
class PolarDrawer{
// 描画メソッド (Sprite, 円の半径, 中心x, 中心y, Object)
static public function draw(m:Sprite, cx:Number, cy:Number, shape:Object):void {
var rad:Number = shape.rad * Math.random();
var angle:Number = 0;
m.graphics.lineStyle(1, 0xff0000);
m.graphics.moveTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy));
m.addEventListener(Event.ENTER_FRAME, function(e:Event):void{
angle += 0.05
m.graphics.lineTo(shape.fx(rad, angle, cx), shape.fy(rad, angle, cy))
if(angle >= 2*Math.PI)
m.removeEventListener("enterFrame", arguments.callee);
});
}
}