點陣圖範例:動畫轉動的月球

Flash Player 9 以及更新的版本,Adobe AIR 1.0 以及更新的版本

轉動的月球動畫範例示範了使用 Bitmap 物件和點陣圖影像資料 (BitmapData 物件) 的技術。該範例會使用月球表面的平面化影像做為原始影像資料,建立轉動、球面的月球動畫。當中將示範下列技術:

  • 載入外部影像並存取其原始影像資料

  • 從來源影像的不同部分重複複製像素以建立動畫

  • 設定像素值來建立點陣圖影像

若要取得此樣本的應用程式檔案,請參閱 www.adobe.com/go/learn_programmingAS3samples_flash_tw 。您可以在 Samples/SpinningMoon 資料夾中找到轉動的月球動畫之應用程式檔案。此應用程式是由下列檔案組成:

檔案

說明

SpinningMoon.mxml

SpinningMoon.fla

主應用程式檔案,在 Flex 中為 MXML,在 Flash 中為 FLA。

com/example/programmingas3/moon/MoonSphere.as

此類別會執行月球的載入、顯示和製成動畫等功能。

moonMap.png

內含月球表面相片的影像檔,該檔案會載入並用於建立旋轉的月球動畫。

將外部影像載入為點陣圖資料

這個樣本執行的第一項主要工作是載入一個外部影像檔,即月球表面的相片。載入作業會在 MoonSphere 類別內以兩種方法處理:在 MoonSphere() 建構函式中初始化載入程序,以及在外部影像完全載入時呼叫 imageLoadComplete() 方法。

載入外部影像的方法與載入外部 SWF 的方法類似;兩者皆使用 flash.display.Loader 類別的實體執行載入作業。 MoonSphere() 方法內開始載入影像的實際程式碼如下:

var imageLoader:Loader = new Loader(); 
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadComplete); 
imageLoader.load(new URLRequest("moonMap.png"));

在第一行中,宣告名為 imageLoader 的 Loader 實體。在第三行中,藉由呼叫 Loader 物件的 load() 方法,傳遞一個 URLRequest 實體以代表要載入之影像的 URL,實際啟動載入程序。在第二行中,設定將在影像完全載入時觸發的事件偵聽程式。請注意, addEventListener() 方法不是對 Loader 實體本身呼叫,而是對 Loader 物件的 contentLoaderInfo 屬性呼叫。Loader 實體本身不會傳送與載入之內容相關的事件。不過,其 contentLoaderInfo 屬性內含 LoaderInfo 物件的參考,而該物件與正在載入至 Loader 物件的內容 (此例中為外部影像) 的內容相關。LoaderInfo 物件提供了數個與載入外部內容的進度和完成相關的事件,包括 complete 事件 ( Event.COMPLETE ),該事件將會在影像完全載入時觸發對 imageLoadComplete() 方法的呼叫。

雖然啟動外部影像載入是程序中的重要部分,不過瞭解載入完成時應執行什麼動作也同樣重要。如上面的程式碼中所示,影像載入完成時,便會呼叫 imageLoadComplete() 函數。該函數會對載入的影像資料執行幾項作業,我們將於陸續說明。不過,若要使用影像資料,就必須存取該資料。當使用 Loader 物件載入外部影像時,載入的影像會成為 Bitmap 實體,此實體會附加為 Loader 物件的子顯示物件。在這個情況下,Loader 實體可以讓做為事件物件一部分的事件偵聽程式方法使用,將其當做參數以傳遞至方法。 imageLoadComplete() 方法的前幾行程式碼如下:

private function imageLoadComplete(event:Event):void 
{ 
    textureMap = event.target.content.bitmapData; 
    ... 
}

請注意,事件物件的參數名為 event ,它是 Event 類別的實體。Event 類別的每個實體都有一個 target 屬性,參照到觸發事件的物件 (此例中,即為 addEventListener() 方法所呼叫的 LoaderInfo 實體,如先前的內容所提及)。接著,LoaderInfo 物件內會有一個 content 屬性 (載入程序完成後),內含載入之點陣圖影像的 Bitmap 實體。如果您想要直接在螢幕上顯示影像,則可以將此 Bitmap 實體 ( event.target.content ) 附加到顯示物件容器 (您也可以將 Loader 物件附加到顯示物件容器)。不過,在此樣本中,載入的內容會當做原始影像資料的來源使用,而不會顯示在螢幕上。因此, imageLoadComplete() 方法的第一行會讀取所載入 Bitmap 實體的 bitmapData 屬性 ( event.target.content.bitmapData ),並將其儲存到名為 textureMap 的實體變數中,在建立旋轉的月球動畫時,該變數會做為影像資料的來源。本項於下列說明。

複製像素以建立動畫

動畫的基本定義是隨著時間而變更影像所產生的動態效果或變化。在此樣本中,目標是要建立依垂直軸旋轉的球面月球效果。不過,為了要瞭解動畫,您可以忽略樣本內球面扭曲的部分。考量實際載入以及做為月球影像資料來源的影像:

如您所見,影像並不是一或多個球體,而是月球表面的矩形相片。由於相片正好是在月球的赤道上拍攝,因此靠近影像頂端和底端的影像部分便會遭到延伸及扭曲。為了移除影像的扭曲效果,並使其顯示為球面,我們將會使用一個置換對應濾鏡,此部分將於稍後說明。不過,由於來源影像為矩形,因此若要建立球體旋轉的效果,程式碼只需要水平滑動月球表面相片即可。

請注意,影像實際包含了兩張緊鄰著彼此的月球表面相片之副本。這個影像為來源影像,影像資料會為了建立動畫的外觀而重複複製此來源影像。兩個影像副本若緊鄰著彼此,便可以更輕鬆地建立一個連續、不中斷的捲動效果。讓我們來逐步進行動作的處理程序,以瞭解其運作方式。

這個程序實際上包含了兩個不同的 ActionScript 物件。首先,其中有一個載入的來源影像,在程式碼內是以名為 textureMap 的 BitmapData 實體來表示。如同先前的內容所提及, textureMap 會在載入外部影像後,立即填入影像資料,當中使用的程式碼如下:

textureMap = event.target.content.bitmapData;

textureMap 的內容是矩形的月球影像。此外,為了建立旋轉的動畫,程式碼中會使用名為 sphere 的 Bitmap 實體,該實體是實際在螢幕上顯示月球影像的顯示物件。和 textureMap 一樣,在 imageLoadComplete() 方法內會建立 sphere 物件,並填入其初始的影像資料,其中使用了下列程式碼:

sphere = new Bitmap(); 
sphere.bitmapData = new BitmapData(textureMap.width / 2, textureMap.height); 
sphere.bitmapData.copyPixels(textureMap, 
                         new Rectangle(0, 0, sphere.width, sphere.height), 
                         new Point(0, 0));

如程式碼中所示, sphere 已初始化。其 bitmapData 屬性 (由 sphere 顯示的原始影像資料) 會建立與 textureMap 具有相同的高度和其一半的寬度。換句話說, sphere 的內容將會是一個月球相片的大小 (因為 textureMap 影像內含兩個相連的月球相片)。接著, bitmapData 屬性會使用其 copyPixels() 方法填入影像資料。 copyPixels() 方法呼叫內的參數會指定以下幾項作業:

  • 第一個參數指定影像資料是從 textureMap 複製而來。

  • 第二個參數 (即 new Rectangle 實體) 會指定要從 textureMap 的哪個部分拍攝影像快照。在此例中,該快照為起始自 textureMap 左上角的矩形 (由前兩個 Rectangle() 參數 0, 0 表示),而此矩形快照的寬度和高度都符合 sphere width height 屬性。

  • 第三個參數 (即 x 和 y 值都為 0 的新 Point 實體) 會定義像素資料的目標。在此例中,即為 sphere.bitmapData 的左上角 (0, 0)。

以視覺化的方式呈現時,程式碼會從下列影像以外框框住的部分複製 textureMap 像素,並將它們貼到 sphere 中。換句話說, sphere 的 BitmapData 內容即是此處強調的 textureMap 部分:

不過仍請記住,這只是 sphere 的初始狀態,也就是複製到 sphere 的第一個影像。

載入來源影像及建立 sphere 之後,由 imageLoadComplete() 方法執行的最後一項工作即是設定動畫。動畫由名為 rotationTimer 的 Timer 實體所驅動,這個實體是使用下列程式碼建立及啟動:

var rotationTimer:Timer = new Timer(15); 
rotationTimer.addEventListener(TimerEvent.TIMER, rotateMoon); 
rotationTimer.start();

程式碼會先建立名為 rotationTimer 的 Timer 實體,而傳遞給 Timer() 建構函式的參數表示 rotationTimer 應該每 15 毫秒就觸發其 timer 事件。接下來,則會呼叫 addEventListener() 方法,指定在 timer 事件 ( TimerEvent.TIMER ) 發生時呼叫 rotateMoon() 方法。最後,timer 實際上是由呼叫其 start() 方法所啟動的。

受到 rotationTimer 方法的定義方式之影響,因此大約每 15 毫秒 Flash Player 就會呼叫 MoonSphere 類別內的 rotateMoon() 方法,此處即是月球的動畫產生的地點。 rotateMoon() 方法的原始碼如下:

private function rotateMoon(event:TimerEvent):void 
{ 
    sourceX += 1; 
    if (sourceX > textureMap.width / 2) 
    { 
        sourceX = 0; 
    } 
     
    sphere.Data.copyPixels(textureMap, 
                                    new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                    new Point(0, 0)); 
     
    event.updateAfterEvent(); 
}

程式碼會執行下列三項作業:

  1. 變數 sourceX 的值 (最初設定為 0) 會遞增 1。

    sourceX += 1;

    如您所見, sourceX 會用來決定像素將被複製到 sphere 上的 textureMap 內的哪個位置,因此這個程式碼具有在 textureMap 上將矩形向右移動一個像素的效果。回到視覺化呈現的部分,在幾個動畫循環後,來源矩形將會向右移動數個像素,就像這樣:

    在更多個循環後,矩形將會移動得更遠:

    在那些複製之像素中,這個逐步、平緩的位置移動即是動畫的關鍵。藉由緩慢及連續地將來源位置向右移動,顯示在螢幕上的影像會以 sphere 顯示為連續向左滑動的情形。這就是為什麼來源影像 ( textureMap ) 需要使用兩個月球表面相片副本的原因。由於矩形會連續向右移動,因此大部分的時間它不會覆蓋在單一月球相片上,而是與兩張月球相片重疊。

  2. 隨著來源矩形緩慢地向右移動,將出現一個問題。最後,矩形將會到達 textureMap 的右邊緣,並且將會耗盡可複製到 sphere 上的月球相片像素:

    接下來的程式碼將可解決此問題:

    if (sourceX >= textureMap.width / 2) 
    { 
        sourceX = 0; 
    }

    程式碼會檢查 sourceX (矩形的左邊緣) 是否已到達 textureMap 的中間。如果是的話,便會將 sourceX 重設回 0,使其移回 textureMap 的左邊緣,並再次啟動循環:

  3. 有了適當計算的 sourceX 值,建立動畫的最後一個步驟便是實際將新來源矩形像素複製到 sphere 上。執行此動作的程式碼非常類似於最初填入的 sphere (先前的內容中已提及);唯一的差別是,在此例中,在 new Rectangle() 建構函式呼叫內,矩形的左邊緣會放置在 sourceX 上:

    sphere.bitmapData.copyPixels(textureMap, 
                                new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                new Point(0, 0));

請記得,每隔 15 毫秒就會重複呼叫此程式碼。隨著來源矩形的位置連續地移動,並將像素複製到 sphere 上,畫面上會顯示由 sphere 代表的月球相片影像連續滑入之效果。換句話說,月球看起來就像在連續旋轉。

建立球面外觀

當然,月球是一個球體而不是一個矩形。因此,樣本需要使用矩形的月球表面相片,將其製作成連續的動畫,並將其轉換為球體。這包含兩個不同的步驟:使用遮罩隱藏除了月球表面相片之圓形區域以外的所有內容,使用置換對應濾鏡扭曲月球相片外觀,使其看起來有立體感。

首先,使用一個圓形的遮罩來隱藏 MoonSphere 物件的所有內容,除了由濾鏡所建立的球體之外。下列程式碼會將遮罩建立為一個 Shape 實體,並將其套用為 MoonSphere 實體的遮罩:

moonMask = new Shape(); 
moonMask.graphics.beginFill(0); 
moonMask.graphics.drawCircle(0, 0, radius); 
this.addChild(moonMask); 
this.mask = moonMask;

請注意,由於 MoonSphere 是顯示物件 (以 Sprite 類別為基礎),因此可以使用所繼承的 mask 屬性將遮罩直接套用到 MoonSphere 實體上。

只使用圓形遮罩來隱藏相片的某些部分並不足以建立逼真的旋轉球體效果。由於月球表面的相片拍攝方式,其尺寸並未對稱;越接近影像頂端或底端的部分會越扭曲,相對於赤道部分會更具拉伸的感覺。為了要扭曲月球相片的外觀,使其看起來具有立體感,我們將會使用置換對應濾鏡。

置換對應濾鏡是一種用於扭曲影像的濾鏡類型。在此例中,將會「扭曲」月球相片,藉由水平地擠壓影像的頂端和底端,同時保持中間部分不變動,使其看起來更為逼真。假設濾鏡在相片的方形部分上運作,則會擠壓頂端和底端而非中間部分,這會把方形轉換為圓形。將此扭曲的影像製作成動畫的副作用之一是,影像的中間部分實際移動的距離似乎比接近頂端和底端的部分更遠,如此將會產生圓形為立體 (球體) 的效果。

下列程式碼會用來建立置換對應濾鏡,其名為 displaceFilter

var displaceFilter:DisplacementMapFilter; 
displaceFilter = new DisplacementMapFilter(fisheyeLens, 
                                new Point(radius, 0),  
                                BitmapDataChannel.RED, 
                                BitmapDataChannel.GREEN, 
                                radius, 0);

第一個參數 fisheyeLens 稱為對應影像;在此例中是以程式設計方式建立的 BitmapData 物件。該影像的建立方式將於 設定像素值來建立點陣圖影像 中說明。其他參數描述在已套用濾鏡影像中濾鏡的套用位置、將使用哪些顏色色版來控制置換效果,以及它們影響置換的程度。一旦建立了置換對應濾鏡,便會將其套用至 sphere (仍舊在 imageLoadComplete() 方法內):

sphere.filters = [displaceFilter];

最終的影像 (即套用遮罩及置換對應濾鏡的影像) 看起來就像這樣:

隨著每個旋轉月球動畫的循環,來源影像資料的新快照會覆寫 sphere 的 BitmapData 內容。不過,並不需要每一次都重新套用濾鏡。這是因為濾鏡會套用至 Bitmap 實體上 (即顯示物件),而不是點陣圖資料上 (即原始像素資訊)。請記住,Bitmap 實體不是實際的點陣圖資料;而是一個在螢幕上顯示點陣圖資料的顯示物件。舉例來說,Bitmap 實體就像是幻燈片放映機,可在螢幕上顯示相片幻燈片,而 BitmapData 物件則像是實際的相片幻燈片,可以透過幻燈片放映檔呈現。濾鏡可以直接套用至 BitmapData 物件,可與直接在相片幻燈片上繪圖以修改影像的動作進行做比較。濾鏡也可以套用到任何顯示物件上,包括 Bitmap 實體;這種情形類似於將濾鏡放在幻燈片放映機的鏡頭前方,以扭曲顯示在螢幕上的輸出 (不會改變原始的幻燈片)。由於原始點陣圖資料可透過 Bitmap 實體的 bitmapData 屬性存取,濾鏡便會直接套用至原始點陣圖資料上。不過在此例中,將濾鏡套用至 Bitmap 顯示物件比套用至點陣圖資料更為合理。

如需有關在 ActionScript 中使用置換對應的詳細資訊,請參閱 以濾鏡處理顯示物件

設定像素值來建立點陣圖影像

置換對應濾鏡的重要觀點是,濾鏡實際上包含了兩個影像。其中一個影像 (即來源影像) 是實際由濾鏡修改的影像。在此樣本中,其來源影像是名為 sphere 的 Bitmap 實體。由濾鏡使用的其它影像稱為對應影像。對應影像實際上並未顯示在螢幕上。每個像素的顏色都會用來當做是對置換函數的輸入。也就是說,對應影像內的某些 x, y 座標會決定要在來源影像內的該 x,y 座標上套用多少置換量 (在適當位置的實體移動)。

因此,若要使用置換對應濾鏡來建立球體效果,樣本需要適當的對應影像:一個具有灰色背景和一個以單一顏色 (紅色) 漸層填滿的圓形,水平地由暗顯示到亮,如此處所示:

由於此樣本內只使用了一個對應影像和濾鏡,因此對應影像只會在 imageLoadComplete() 方法中建立一次 (換句話說,即是在外部影像完成載入時)。在樣本中,會呼叫 MoonSphere 類別的 createFisheyeMap() 方法,建立名為 fisheyeLens 的對應影像:

var fisheyeLens:BitmapData = createFisheyeMap(radius);

createFisheyeMap() 方法內部,對應影像會使用 BitmapData 類別的 setPixel() 方法一次繪製一個像素。 createFisheyeMap() 方法的完整程式碼列示如下,隨後會逐步討論其運作的方式:

private function createFisheyeMap(radius:int):BitmapData 
{ 
    var diameter:int = 2 * radius; 
     
    var result:BitmapData = new BitmapData(diameter, 
                                        diameter, 
                                        false, 
                                        0x808080); 
     
    // Loop through the pixels in the image one by one 
    for (var i:int = 0; i < diameter; i++) 
    { 
        for (var j:int = 0; j < diameter; j++) 
        { 
            // Calculate the x and y distances of this pixel from 
            // the center of the circle (as a percentage of the radius). 
            var pctX:Number = (i - radius) / radius; 
            var pctY:Number = (j - radius) / radius; 
             
            // Calculate the linear distance of this pixel from 
            // the center of the circle (as a percentage of the radius). 
            var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY); 
             
            // If the current pixel is inside the circle, 
            // set its color. 
            if (pctDistance < 1) 
            { 
                // Calculate the appropriate color depending on the 
                // distance of this pixel from the center of the circle. 
                var red:int; 
                var green:int; 
                var blue:int; 
                var rgb:uint; 
                red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
                green = 0; 
                blue = 0; 
                rgb = (red << 16 | green << 8 | blue); 
                // Set the pixel to the calculated color. 
                result.setPixel(i, j, rgb); 
            } 
        } 
    } 
    return result; 
}

首先,當在接收所呼叫方法時,參數 radius 會指定要建立圓形影像的半徑。接著,程式碼便會在將要繪製的圓形上建立 BitmapData 物件。該物件 (名為 result ) 最終會當做方法的傳回值傳回。如下列程式碼片段所示,這個 result BitmapData 實體是以圓形直徑大小的寬度和高度所建立,不具透明度 (第三個參數值為 false ),並已預先填入顏色 0x808080 (中級的灰色):

var result:BitmapData = new BitmapData(diameter, 
                                    diameter, 
                                    false, 
                                    0x808080);

接下來,程式碼會使用兩個迴圈來重複影像的每個像素。外部迴圈會從左至右通過影像的每一欄 (使用變數 i 代表目前操作的像素水平位置),而內部迴圈則從上到下通過目前欄的每一個像素 (使用變數 j 表示目前像素的垂直位置)。迴圈的程式碼 (包含省略的內部迴圈內容) 如下:

for (var i:int = 0; i < diameter; i++) 
{ 
    for (var j:int = 0; j < diameter; j++) 
    { 
        ... 
    } 
}

隨著迴圈一個接一個對像素執行循環,使得每一個像素都會計算出一個值 (對應影像內像素的顏色值)。此程序包含四個步驟:

  1. 程式碼會沿著 x 軸計算目前像素與圓形中心點的距離 ( i - radius )。該值可除以半徑值,使其以半徑的百分比表示,而不是以絕對距離表示 ( (i - radius) / radius )。該百分比值會儲存在名為 pctX 的變數內,而 y 軸的相等值會經過計算並儲存在變數 pctY 中,如同此程式碼所示:

    var pctX:Number = (i - radius) / radius; 
    var pctY:Number = (j - radius) / radius;
  2. 使用了標準的三角函數公式 (畢氏定理),圓形中心和點目前所在位置間的線性距離皆是從 pctX pctY 計算而來。該值會儲存在名為 pctDistance 的變數內,如此處所示:

    var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
  3. 接下來,程式碼會檢查距離百分比是否小於 1 (代表半徑的 100%,或換句話說,如果考慮的像素位於圓形半徑內)。如果像素落在圓形的內部,則會為其指定已計算的顏色值 (因篇幅有限故省略,但已在步驟 4 中說明);如果像素未落在圓形的內部,則該像素不會有任何變化,因此其顏色會保留為預設的中級灰色:

    if (pctDistance < 1) 
    { 
        ... 
    }
  4. 對於落於圓形內部的像素而言,其顏色值已計算出。最終的顏色將是紅色的陰影部分,範圍從圓形左邊緣的黑色 (0% 的紅色) 到圓形右邊緣的亮紅色 (100%)。顏色值最初會以三個部分進行計算 (紅色、綠色和藍色),如此處所示:

    red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
    green = 0; 
    blue = 0;

    請注意,只有顏色的紅色部分 (即變數 red ) 才會真正擁有值。為了在此處說明而顯示的綠色和藍色值 (即變數 green blue ) 可被省略。由於此方法的用途是要建立一個內含紅色漸層的圓形,因此不需要使用綠色或藍色值。

    一旦確定了三種個別的顏色值,便會使用標準的位元轉換演算法將其結合為單一的整數顏色值,如此程式碼中顯示:

    rgb = (red << 16 | green << 8 | blue);

    最後,便會得到已計算的顏色值 (該值實際上是由目前的像素使用 result BitmapData 物件的 setPixel() 方法所指定),如此處所示:

    result.setPixel(i, j, rgb);