Crayon Physics

  • Crayon Physics
  • 記得兩三年前才在youtube上看到MIT的Assist Sketch Understanding System and Operation,結果現在這個設計已經商品化了,你可以參觀下面的官方網站並且下載demo來玩玩看。

    http://www.crayonphysics.com/

[Flash] Performance V.S. Reusability

  • Performance V.S. Reusability
  • 我想這是在OOP裡一個很大的tradeoff,我們必須在效能與框架設計之間取得平衡,考慮下面這一個簡單的效果:

    我們手邊已經有一個CircleMotion的Class,如下:

    /* CircleMotion.as */
    package com.xinyu.motion{
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.display.DisplayObject;

    import com.xinyu.science.Science;

    public class CircleMotion2D extends Motion{
    private var _timer:Timer;
    private var _radian:Number = 0;
    private var _radius:Number;
    private var _speed:Number;
    private var _dir:int;
    private var _xOffset:Number;
    private var _yOffset:Number;
    private var _obj:DisplayObject;

    public static const CW:int = 1;
    public static const CCW:int = -1;

    public function CircleMotion2D(obj:DisplayObject, xOffset:Number = 0 ,yOffset:Number = 0, radius:Number = 50, speed:Number = 0.1, fps:Number = 30, dir:Number = CircleMotion2D.CW, radOffset:int = 0){
    _obj = obj;
    _radius = radius;
    _speed = Math.min(1, Math.max(speed, 0.001));
    _dir = dir;
    _xOffset = xOffset;
    _yOffset = yOffset;
    _radian = Science.degToRad(radOffset%360);

    _timer = new Timer(1000/fps);
    _timer.addEventListener(TimerEvent.TIMER, onTimer);
    }

    private function onTimer(event:TimerEvent):void{
    _obj.x = _radius*Math.cos(_radian) + _xOffset - _radius;
    _obj.y = _radius*Math.sin(_radian) + _yOffset;

    _radian += _dir * _speed;

    if(_radian > 2*Math.PI){
    _radian = 0;
    }
    }

    override public function start():void{
    _timer.start();
    }

    override public function stop():void{
    _timer.stop();
    }

    override public function get running():Boolean{
    return _timer.running;
    }
    }
    }

    所以以直覺來講,RingMotion2D應該要直接沿用CircleMotion2D的定義,簡單的改變每個Object的initial radian就可以達到環繞的效果,所以RingMotion2D理論上是會這樣撰寫:

    /* RingMotion2D.as*/
    package com.xinyu.motion{
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.display.DisplayObject;

    import com.xinyu.motion.CircleMotion2D;
    import com.xinyu.science.Science;

    public class RingMotion2D extends Motion{
    private var _obj:Array;
    private var circleM2D:Array;
    public static const CW:int = 1;
    public static const CCW:int = -1;

    public function RingMotion2D(obj:Array, xOffset:Number = 0 ,yOffset:Number = 0, radius:Number = 100, speed:Number = 0.1, fps:Number = 30, dir:Number = RingMotion2D2.CW){
    _obj = obj;

    circleM2D = new Array(_obj.length);
    for(var i:uint = 0; i < _obj.length; i++){
    circleM2D[i] = new CircleMotion2D(_obj[i], xOffset, yOffset, radius, speed, fps, dir, 360 / _obj.length * i);
    }
    }

    override public function start():void{
    for(var i:uint = 0; i < _obj.length; i++){
    circleM2D[i].start();
    }
    }

    override public function stop():void{
    for(var i:uint = 0; i < _obj.length; i++){
    circleM2D[i].stop();
    }
    }

    override public function get running():Boolean{
    return circleM2D[0].running;
    }
    }
    }

    乍看之下,這樣的寫法很簡潔且沒有問題,但是實際去做測試之後問題就會浮現了,那就是效能,一旦物件增多,fps加快,這個Class所用到的timer也會是成比例成長,縱使是再快的CPU也會有些微的影響 (當然你可能會質疑說,誰沒事會在畫面上產生幾百顆球? 但別忘了這個效果可能只是你的project的功能之一,且並不是所有的瀏覽者的電腦都是那麼快的)。

    為了降低程式的效能成本,我必須重新定義一個RingMotion,如下:

    /* RingMotion2D.as */
    package com.xinyu.motion{
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.display.DisplayObject;

    import com.xinyu.science.Science;

    public class RingMotion2D extends Motion{
    private var _timer:Timer;
    private var _radian:Array;
    private var _radius:Number;
    private var _speed:Number;
    private var _dir:int;
    private var _xOffset:Number;
    private var _yOffset:Number;
    private var _obj:Array;

    public static const CW:int = 1;
    public static const CCW:int = -1;

    public function RingMotion2D(obj:Array, xOffset:Number = 0 ,yOffset:Number = 0, radius:Number = 100, speed:Number = 0.1, fps:Number = 30, dir:Number = RingMotion2D.CW){
    _obj = obj;
    _radian = new Array(_obj.length);
    for(var i:uint = 0; i<_obj.length; i++){
    _radian[i] = Science.degToRad(360/_obj.length * i);
    }

    _radius = radius;
    _speed = Math.min(1, Math.max(speed, 0.001));
    _dir = dir;
    _xOffset = xOffset;
    _yOffset = yOffset;

    _timer = new Timer(1000/fps);
    _timer.addEventListener(TimerEvent.TIMER, onTimer);
    }

    private function onTimer(event:TimerEvent):void{
    for(var i:uint = 0; i < _obj.length; i++){
    _obj[i].x = _radius * Math.cos(_radian[i]) + _xOffset - _radius;
    _obj[i].y = _radius * Math.sin(_radian[i]) + _yOffset;

    _radian[i] += _dir * _speed;
    if(_radian[i] > 2*Math.PI){
    _radian[i] = 0;
    }
    }
    }

    override public function start():void{
    _timer.start();
    }

    override public function stop():void{
    _timer.stop();
    }

    override public function get running():Boolean{
    return _timer.running;
    }
    }
    }

    無可否認這樣的寫法的確違反了OOP的精神,但是為了效能不得如此,因為這樣寫不管有多少個DisplayObject,我們都只需要一個Timer。

    看到這裡也許你會發現如果新的RingMotion2D裡只控制一個物件,那不就等於CircleMotion2D,所以理當CircleMotion2D應該要被RingMotion2D給取代掉才對? 未必,畢竟CircleMotion2D在處裡單顆物件上的效能會比RingMotion2D快一點點(也只有那麼微渺的一點點)。

[Flash] abstract class & virtual function

  • abstract class & virtual function
  • 在c++裡我們有virtual function可以用,在Java裡則可以宣告abstract class ,而在actionscript 3.0雖然有undocumented的virtual specifier可以使用,但卻無法達到pure virtual function的效果,所幸還有override可以發揮,我們一樣也可以在actionscript 3.0達到abstract class的效果,一個簡易的範例如下:

    /* Motion.as */
    package com.xinyu.motion{
    public class Motion{
    public virtual function get running():Boolean{
    return false;
    }

    public virtual function start():void{
    }

    public virtual function stop():void{
    }
    }
    }

    借用之前motion那一篇文章裡的程式碼,只是這裡我們不使用interface的方式來設定,並且將整個motion獨立出來。可以看到我在Motion裡所宣告的函式都加上virtual關鍵字,實際上有沒有加上都不會有任何差別,只是這麼做是為了一個良好的設計習慣。

    底下則是一個SpaceMotion2D繼承於Motion的撰寫方式。

    /* SpaceMotion2D */
    package com.xinyu.motion{
    import flash.display.Sprite;
    import flash.utils.Timer;
    import flash.events.TimerEvent
    import flash.geom.Rectangle;
    import flash.display.DisplayObject;

    import com.xinyu.motion.Motion;
    import com.xinyu.science.Science;

    public class SpaceMotion2D extends Motion{
    private var _timer:Timer;
    protected var _xDir:int;
    protected var _yDir:int;
    protected var _speed:Number;
    protected var _range:Rectangle;
    protected var _obj:DisplayObject;

    public function SpaceMotion2D(obj:DisplayObject, range:Rectangle, speed:Number = 0.1, fps:Number = 25){
    _obj = obj;
    _range = range;
    _speed = Math.min(20, Math.max(speed, 0.001));

    do{
    _xDir = Science.randInt(-1,1);
    }while(_xDir == 0);

    do{
    _yDir = Science.randInt(-1,1);
    }while(_yDir == 0);

    _timer = new Timer(1000/fps);
    _timer.addEventListener(TimerEvent.TIMER, onTimer);
    }

    protected function onTimer(event:TimerEvent):void{
    if(_obj.x + _obj.width/2 + _speed > _range.width){
    _xDir = -1;
    }

    if(_obj.x - _obj.width/2 - _speed < _range.x){
    _xDir = 1;
    }

    if(_obj.y + _obj.height/2 + _speed > _range.height){
    _yDir = -1;
    }

    if(_obj.y - _obj.height/2 - _speed < _range.y){
    _yDir = 1;
    }

    _obj.x += _xDir * _speed;
    _obj.y += _yDir * _speed;
    }

    override public function start():void{
    _timer.start();
    }

    override public function stop():void{
    _timer.stop();
    }

    override public function get running():Boolean{
    return _timer.running;
    }
    }
    }

    如同剛剛所說的,為了培養良好的設計模式,請在start(), stop(), running()前面都加上override以表示這些functions是從Motion繼承而來重新實做的。

    所以大致上AS3裡的abstract class就是以這種方式來實現,至於AS4裡會怎麼去定義這個性質就請拭目以待啦!

    另外如果你有興趣的話也可以將SpaceMotion2D改成3D,下面就是一個SpaceMotion3D繼承於SpaceMotion2D的寫法:

    /* SpaceMotion3D */
    package com.xinyu.motion{
    import flash.display.Sprite;
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.geom.Rectangle;
    import flash.display.DisplayObject;

    import com.xinyu.motion.SpaceMotion2D;
    import com.xinyu.science.Science;

    public class SpaceMotion3D extends SpaceMotion2D {
    private var _zDir:int;
    private var _z:Number;
    private var _depth:Number;
    private var xyScaleRatio:Number;
    private var objScaleRatio:Number;

    public function SpaceMotion3D(obj:DisplayObject, range:Rectangle, depth:Number, speed:Number = 1, fps:Number=25) {
    super(obj, range, speed, fps);

    _z = depth;
    _depth = depth;

    calXYScaleRatio();
    calObjScaleRatio();

    do {
    _zDir = Science.randInt(-1, 1);
    } while (_zDir == 0);
    }

    override protected function onTimer(event:TimerEvent):void {
    var zRatio:Number = _z / _depth;

    if (_obj.x + _obj.width / 2 + _speed > _range.width - _range.width * xyScaleRatio * zRatio ) {
    _xDir=-1;
    }
    if (_obj.x - _obj.width / 2 - _speed < _range.x + _range.width * xyScaleRatio * zRatio) {
    _xDir=1;
    }
    if (_obj.y + _obj.height / 2 + _speed > _range.height - _range.height * xyScaleRatio * zRatio ) {
    _yDir=-1;
    }
    if (_obj.y - _obj.height / 2 - _speed < _range.y + _range.height * xyScaleRatio * zRatio) {
    _yDir=1;
    }

    if (_z - _speed < 0) {
    _zDir = 1;
    }else if (_z + _speed > _depth) {
    _zDir = -1;
    }

    _obj.x += _xDir * _speed;
    _obj.y += _yDir * _speed;
    _z += _zDir * _speed;
    _obj.scaleX = 1 - objScaleRatio * zRatio;
    _obj.scaleY = 1 - objScaleRatio * zRatio;
    }

    private function calXYScaleRatio():void{
    var max: Number = Science.findMax(_range.width, _range.height);
    xyScaleRatio = _depth / ( 4*max);
    xyScaleRatio = Math.min(0.9, Math.max(xyScaleRatio, 0.1));
    }

    private function calObjScaleRatio():void{
    var min: Number = Science.findMin(_range.width, _range.height);
    objScaleRatio = min / (2 * _depth);
    objScaleRatio = Math.min(0.9, Math.max(objScaleRatio, 0.1));
    }
    }
    }

    這裡的3D效果是我簡單以視覺的判斷產生出來的,並沒有透過嚴謹的mathematical計算來達成,也就是說SpaceMotion3D.as並不是仰賴Flash CS4的z-axes的功能,想要更完善的功能請使用PaperVision3D吧!

[Flash] ActionScript 3.0 Class Access Specifiers

[Flex] Actionscript Project Migration

  • Actionscript Project Migration
  • Download: MotionUIComponent.zip

    平常在Flash底下辛苦寫的actionscript project如果要移植到Flex Project上可以透過UIComponent來承接Sprite,只是有一個限制就是如果你有使用 fl.* package在Flex裡是不支援的。這裡我直接以[Flash] Motion為範例,在這一篇裡我們寫了不小的程式碼來達到多種Motion的特效,如果要拿到Flex Project上當然是不希望要重新撰寫,以下就是一個移植的範例程式碼:

    <?xml version="1.0" encoding="utf-8"?>
    <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" applicationComplete="initApp()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#7E9ED7, #CFE6EF]" width="400" height="300" viewSourceURL="srcview/index.html">
    <mx:Script>
    <![CDATA[
    import flash.display.Sprite;

    import xinyu.geom.Ball;
    import xinyu.science.Science;
    import xinyu.motions.SpaceMotion2D;
    import xinyu.motions.CircleMotion2D;

    private var ball:Ball;

    private var ballMask:Sprite;

    private function initApp(): void {
    ball = new Ball(0xCCCCCC, 0x000055, 10, -45);
    rootStage.addChild(ball);
    ball.setMotion(new SpaceMotion2D(new Rectangle(0,0,rootStage.width, rootStage.height), 5, 30));
    ball.x = Science.randInt(0, rootStage.width);
    ball.y = Science.randInt(0, rootStage.height);
    motionPanel.title = "2D Space Motion";

    initMask();
    }

    private function initMask():void{
    ballMask = new Sprite();
    ballMask.graphics.beginFill(0x000000);
    ballMask.graphics.drawRect(0,0,rootStage.width, rootStage.height);
    ballMask.graphics.endFill();
    rootStage.addChild(ballMask);
    rootStage.mask = ballMask;
    }

    private function run():void{
    ball.motion.run();
    }

    private function stop():void{
    ball.motion.stop();
    }

    private function onMotionClicked(event:MouseEvent):void{
    var running:Boolean = ball.motion.running;

    switch(event.target.label){
    case "2D Space Motion":
    ball.setMotion(new SpaceMotion2D(new Rectangle(0,0,rootStage.width, rootStage.height), 5, 30));
    break;
    case "2D Circle Motion":
    ball.setMotion(new CircleMotion2D(ball.x, ball.y, 50, 0.1, 30, CircleMotion2D.CW));
    break;
    }

    motionPanel.title = event.target.label;

    if(running){
    run();
    }
    }
    ]]>
    </mx:Script>

    <mx:VBox verticalGap="4" left="10" top="10" right="10" bottom="10">
    <mx:Panel id="motionPanel" width="100%" height="240" layout="absolute" title="2D Space Motion" horizontalAlign="center" verticalAlign="middle" cornerRadius="5">
    <mx:UIComponent id="rootStage" width="100%" height="100%" x="0"/>
    </mx:Panel>
    <mx:ApplicationControlBar width="100%" height="100%">
    <mx:Button label="Run" click="run()" width="100%" height="100%"/>
    <mx:Button label="Stop" click="stop()" width="100%" height="100%"/>
    <mx:Button label="2D Space Motion" click="onMotionClicked(event)" width="100%" height="100%"/>
    <mx:Button label="2D Circle Motion" click="onMotionClicked(event)" width="100%" height="100%"/>
    </mx:ApplicationControlBar>
    </mx:VBox>
    </mx:Application>

    從Flex Reference裡可以得知UIComponent是FlexSprite的子類別,而FlexSprite繼承於Sprite,所以當我們要加入當初在Flash底下設計的DisplayObject時,要在Flex主場景裡新增一個UIComponent來包住Ball物件。

    Flex Reference - UIComponent

    從這樣的特性可以得知,以後如果要直接在Flex撰寫ActionScript可以直接繼承於UIComponent即可。

[Flex] Cross Domain Policy

  • Cross Domain Policy
  • 在Flex SDK裡編寫的Socket程式在連線的過程中出現下面的訊息:

    <policy-file-request/>

    這是Flash Player 安全原則設定,在做網站連線或者Socket連結時需要先取得cross domain policy才能繼續連線。

    以Socket Policy File為例,crossdomain.xml寫法如下:

    <policy-file-request/>

    <?xml version="1.0"?>
    <!DOCTYPE cross-domain-policy SYSTEM
    "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">

    <cross-domain-policy>
    <allow-access-from domain="yoursockethosts.com" to-ports="2115"/>
    </cross-domain-policy>

    下面這個則為URL Policy File的寫法:

    <?xml version="1.0"?>
    <!DOCTYPE cross-domain-policy SYSTEM
    "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">

    <cross-domain-policy>
    <site-control permitted-cross-domain-policies="master-only"/>
    <allow-access-from domain="www.yourwebpage.com"/>
    </cross-domain-policy>

    在flash裡要要載入crossdomain最簡易的方法就是使用loadPolicyFile(),如下:

    package{
    import flash.net.Socket;
    import flash.events.Event;
    import flash.events.ProgressEvent;
    import flash.text.TextField;
    import flash.display.Sprite;
    import flash.system.Security;

    public class SocketDemo extends Sprite{
    private var socket:Socket;
    private var textField:TextField;

    Security.loadPolicyFile("http://localhost/crossdomain.xml");

    public function SocketDemo(){
    socket = new Socket("localhost", 2115);
    socket.addEventListener(Event.CONNECT, onConnected);

    textField = new TextField();
    addChild(textField);
    }

    private function onConnected(event:Event):void{
    textField.text = "connected!";
    }
    }
    }

    除了透過從 flash裡面主動去載入crossdomain.xml之外,你也可以把policy file寫入到server然後由server傳到Flash Socket裡。下面的寫法是承襲[Flash] Flash Socket & C++/Java Socket
    這一篇裡的C++ Socket的部份。

    string xml = "<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"1025-9999\"/></cross-domain-policy>";
    client.send(xml);

    詳細的cross domain policy file的資料可以參考下面的網頁:

    Cross-domain policy file specification

    如果你要了解更多關於Flash Security的問題請參考下面的網頁:

    Flash Player Developer Center - Security

[Flash] Event Dispatching

  • Event Dispatching
  • Download: Reminder.zip

    在比較複雜一點的開發專案裡就比較常用到Event Dispatching,因為並不是所有的物件都可以加到Timeline上(未必繼承於Sprite),這時dispatchEvent就派上用場。

    舉個例子來說,假設我們今天要設計一個Reminder,當Reminder時間一到就顯示當初設定的訊息。比較笨的做法就是不斷的去測試Reminder的物件裡的時間是否已經Timeout,然後再顯示設定的訊息,但我想應該沒有人會這樣做。因為在Reminder裡面是使用Timer來計算時間,所以就直接使用TimerEvent.COMPLETE,當Timer結束時在主場景上顯示設定的訊息,要達到這個條件 ,Reminder必須跟主要顯示物件是繼承或者是parent/child的關係,如此才能控制parent的functions以及properties。或者你也可以在Reminder裡面新增TextField之類的物件,只是這樣的話顯示訊息的UI就被鎖死在Reminder裡,所以這種方式還是不夠好,為了讓Reminder就是專心做計時並且回傳當初設定的訊息,下面就是使用dispatchEvent來實做的方法。

    /* Reminder */
    package{
    import flash.events.TimerEvent;
    import flash.events.Event;
    import flash.utils.Timer;
    import flash.events.EventDispatcher;

    public class Reminder extends EventDispatcher{
    private var timer:Timer;
    private var msg:String;
    public function Reminder(minsec:uint, msg:String){
    this.msg = msg;
    timer = new Timer(minsec,1);
    timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
    timer.start();
    }

    public function get message():String{
    return msg;
    }

    private function onTimerComplete(event:TimerEvent):void{
    dispatchEvent(new Event("onTimeOut"));
    }
    }
    }

    這個範例裡設定的dispatchEvent名稱為onTimeOut,在ReminderDemo裡面要用到。(如果你想讓Reminder裡顯示一些資訊,可以將原本繼承於EventDispatcher改為Sprite,如此在ReminderDemo裡就可以將reminder加入為child。(因為ReminderDemo是繼承於Sprite),但是如前面所提,我們不希望Reminder本身做那麼多事情。)

    /* ReminderDemo */
    package{
    import flash.display.Sprite;
    import flash.events.Event;
    import Reminder;

    public class ReminderDemo extends Sprite{
    private var reminder:Reminder;
    public function ReminderDemo(){
    reminder = new Reminder(1000," event dispatched by remainder! ");
    reminder.addEventListener("onTimeOut", onTimeOut);
    }

    private function onTimeOut(event:Event):void{
    trace(event.target.message);
    }
    }
    }

    Reminder物件經過dispatchEvent之後就多了一個onTimeOut可以設定,我刻意在Reminder裡面將msg設定為read-only,如此我們就可以接收到當初設定的訊息(假使你要一次開啟很多的Reminder時,這樣的寫法應該會比較便捷)。

    有了Event Dispatching的功能之後,在OOP裡會更方便許多。

    P.S. 如果你熟悉flash.utils這個package,你應該會發現有一個叫做setTimeout可以用,但是在AS3裡,Adobe建議我們使用Timer來完成TimeOut的功能(我想他們可能很想把setInterval/setTimeout給拿掉。)

Orange - data analysis tool

Installation pip install orange3 Run orange python -m Orange.canvas