位图示例:带动画效果的旋转的月亮

Flash Player 9 和更高版本,Adobe AIR 1.0 和更高版本

旋转的月球动画示例说明了位图对象和位图图像数据(BitmapData 对象)的使用方法。该示例使用月球表面的平面图像作为原始图像数据来创建一个旋转的月球动画。将对以下方法进行说明:

  • 加载外部图像并访问其原始图像数据

  • 通过重复复制源图像不同部分的像素创建动画

  • 通过设置像素值创建位图图像

若要获取此范例的应用程序文件,请参阅 www.adobe.com/go/learn_programmingAS3samples_flash_cn。可以在 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)来实际启动加载过程。第二行设置在完成图像加载时将触发的事件侦听器。请注意:不是对 Loader 实例本身调用 addEventListener() 方法;而是对 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 属性,该属性将引用触发事件的对象(本例中为 LoaderInfo 实例,如前所述,将对该实例调用 addEventListener() 方法。)而 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 一样,sphere 对象也是在 imageLoadComplete() 方法中创建并使用其初始图像数据填充,使用的代码如下:

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 复制图像数据。

  • 第二个参数(新的 Rectangle 实例)指定图像快照应该从 textureMap 的哪部分拍摄;在本例中,快照是从 textureMap 左上角开始的一个矩形(由前两个 Rectangle() 参数 0, 0 指明),矩形快照的宽度和高度与 spherewidthheight 属性一致。

  • 第三个参数(新的 Point 实例)的 x 和 y 值都为 0,它定义了像素数据的目标位置 — 本例中为 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() 方法。最后,计时器实际上是通过调用其 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 用于确定 textureMap 中的位置(从该位置将像素复制到 sphere),因此该代码会产生在 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 对象。将在通过设置像素值创建位图图像中介绍创建该图像的说明。其他参数说明过滤后的图像中应该应用滤镜的位置、使用哪些颜色通道来控制置换效果以及将在何种范围内对置换产生影响。一旦创建置换图滤镜,它将应用于仍位于 imageLoadComplete() 方法中的 sphere

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. 使用标准三角公式(勾股定理),通过 pctXpctY 计算圆心和当前点之间的直线距离。该值存储在名为 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)有一个值。为清楚起见,此处显示了绿色和蓝色值(变量 greenblue),不过可以省略。因为该方法的目的是创建一个包含红色渐变的圆,所以不需要绿色和蓝色。

    三个颜色值都确定之后,将使用标准移位算法组合成一个整数颜色值,如以下代码所示:

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

    最后,计算出颜色值后,使用 result BitmapData 对象的 setPixel() 方法将该颜色值实际赋给当前像素,如下所示:

    result.setPixel(i, j, rgb);