且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

 Making Things Move 第四章

更新时间:2021-08-20 17:44:16

前一章面所看到的绘图示例中,只使用了一些非常简单的绘图指令,前面我们也几次提到了这个神秘的"drawing API",但没有加以详细的解释。本章我们将学习使用 ActionScript 创建视觉元素,其中包括 ActionScript 的颜色,绘图 API , ColorTransform 类,滤镜(filter)和 BitmapData(位图) 类。在本章的很多地方都会用到颜色,那么就先来学习第一课吧。

Flash 中的颜色

在 Flash 中,颜色就是一串特殊的数字,一个颜色值可以是0到16,777,215中的任意数值,这就是24位(bit)色彩。也许大家会问,为什么有16,777,216(256*256*256)种颜色值,因为 Flash 使用 RGB 颜色值,意味着每个颜色都可以由红(red),绿(green),蓝(blue)三种颜色构成。这三种合成色的每一种都是一个从0到255中的数,所以,对于每个红黄蓝都有256种可能的深度,结果会有约1,678万种颜色。

这个系统之所以叫做24位颜色是因为要使用8位(0或1)才能表示256个数值。8位乘以3(红,黄,蓝)意味着需要24位才能表示1678万种颜色值。我们马上还要学到32位色系统,它有额外的8位数值表示透明度(alpha)。

很难想像一个值为11,273,634的颜色是什么样的。因此,开发人员通常采用另一种数值表示系统:十六进制。如果大家在 HTML 中使用过颜色,那么这对于你来说并不会陌生,但不管怎样还是让我们来学习一下这些基础知识吧。

使用十六进制表示颜色值

十六进制(Hexadecimal,简写 hex),以16为基础,每位数都是0到15中的任意一个数,而十进制则是以10为基础,每位数都是0到9中的任意一个数。由于没有可以表示10到15的数,所以要借用字母表的前六个字母,A到F,来表示它们。这样,每个16进制数都可以是0到F中的一个(在 Flash 中,十六进制数不区分大小写,使用A到F或a到f均可)。在 HTML 中使用16进制数,要加上 # 作为前缀加以标识。与其它语言一样,在 ActionScript 中,使用0x作为前缀。比如,十六进制的 0xA 与十进制的10相等,0xF等于15,0x10等于16。在十进制中,每一位都是它右边一位数的十倍,如243表示为2的100倍,4的10倍,3的1倍。在十六进制中,每一位都是它右边一位数的十六倍,如0x2B3表示为2的256倍,B(或11)的16倍,3的1倍。
对于24位来说,就等于0xFFFFFF,此外,这6个十六进制数可以分为三部分。第一部分代表红色,第二部分代表绿色,最后两位表示蓝色,被象征性地记为0xRRGGBB。
记住每一个合成色都可以为0至255(十六进制表示:0x00到0xFF)中的值。因此,红色可以表示为 0xFF0000,表示纯红色,因为它的绿色为0,蓝色为0。同样,0x0000FF表示纯蓝色。

拿11,273,634为例,将它转换为十六进制(稍后为大家介绍一种简单的方法),结果为 0xAC05A2,可以把它分解为 red(红色) = AC,green(绿色) = 05,blue(蓝色) = A2。可以看出red(红色)和blue(蓝色)的值比较高,而绿色几乎没有,我们就可以猜到这个颜色大概为紫色,这是在十进制数中看不出来的。请注意,在 ActionScript 中,使用哪种进制表示都可以,在一个函数中使用颜色值既可使用十进制又可使用十六进制。对于 Flash 来说,11,273,634 和 0xAC05A2 是一个数,只是对于可怜的人类来说后面一种表示法更易读懂。
那么如何在两种进制之间进行转换呢,将十六进制转换为十进制非常容易。只要输出这个十六进制数就可以了,trace 函数会自动将它转换为十进制。
trace(0xAC05A2);
将十进制转换为十六进制要用到 toString(16)函数,如:
trace((11273634).toString(16));
输出结果为ac05a2,如果要使用这个数,不要忘记加上0x。

透明度和32位色

前面提到过,除了24位色以外,还有32位色,多出8位用于表示透明度。就像角度制与弧度制一样(第三章内容),AS 3 在24和32位色的使用上有些混杂。AS 3 的绘图 API 很大程度上是基于 Flash MX(Flash 6) 建立的,总之,绘图 API 函数使用一个特殊的参数来指定透明度,所以还要延用24位色。另外, BitmapData 类,是从 Flash  8 才加入的,并且使用的是32位色彩。如果大家对某个函数使用哪种色彩体系有疑问的话,请查看 ActionScript 参考手册。

我们可以使用十六进制以 0xRRGGBB 这样的格式来表示一个色彩值。同样,32位的颜色也是如此,以 0xAARRGGBB 这样的格式来表示,其中 AA 表示透明度。因此,0xFFFFFFFF 就表示不透明的白色,0x00FFFFFF 表示完全透明的白色,而 0x80FFFFFF 表示近似 50%透明度的白色。

获取颜色值

假如有这样一个数 0xFF55F3,要从中提取 red,green,blue 的值。下面请看公式,首先是24位色彩:
red = color24 >> 16;
green = color24 >> 8 & 0xFF;
blue = color24 & 0xFF;

一句句来看。首先,大家也许会猜到 >> 是按位右移运算符,用于将二进制位向右移动。如果这些位向右移动得过多,那么这些数字就会消失,就没有数了。
下面从 red 开始:
111111110101010111110011
将颜色值向右移动16位,结果如下:
11111111,或是0xFF(255)
对于 green,向右移动8位,结果如下:
1111111101010101

这里已经得出了 blue 的值,但是 red 值还留在一旁。这里就是要使用与(And)操作符的地方,与(OR)操作符相同,都是对两组数值的比较,可以这样解释“两个数相比较,如果两个都是1那么结果就为1,如果其中有一个为0,那么结果就为0”。我们把它与0xFF进行比较:
1111111101010101
0000000011111111

因为所有的 red 位的数字都与0相比较,所以它们的结果均为0,只有当两个数都为1时结果才为 1,所以结果如下:
0000000001010101

对于 blue 则不需要执行右移操作,只需要让它和 0xFF 执行与(AND)操作即可。对于32位色彩,方法也是相同的,只不过需要一点小小的改动:
alpha = color32 >> 24;
red = color32 >> 16 & 0xFF;
green = color32 >> 8 & 0xFF;
blue = color32 & 0xFF;

这里,获取 alpha 的值需要向右移动24位。现在我们已经学到了很多 Flash 的色彩知识,下面就要开始进行应用了。

绘图 API

先说一下 API 是什么,它是应用程序接口(application programming interface)的缩写。总的来说,API 是指在程序中使用的一些属性和方法来访问某些相关的行为和属性。绘图 API 允许我们使用 ActionScript 绘制直线,曲线,填充色,渐变填充的一些属性和方法。在这个 API 中有些让人惊讶的方法,我们还要学习很多这方面的知识和灵活的技巧。
直至 Flash MX,已经拥有了如下这些绘图方法:

  •  clear()
  • lineStyle(width, color, alpha)
  • moveTo(x, y)
  • lineTo(x, y)
  • curveTo(x1, y1, x2, y2)
  • beginFill(color, alpha)
  • endFill()

beginGradientFill(fillType, colors, alphas, ratios, matrix)

在 Flash 8 中,又为 lineStle 和 beginGradientFill 增加了几种新的方法,同时也加入了 beginBitmapFill 和 lineGradientStyle 方法。在 AS 3中,也增加了几种非常有用的方法:

  • drawCircle(x, y, radius)
  • drawEllipse(x, y, width, height)
  • drawRect(x, y, width, height)
  • drawRoundRect(x, y, width, height, ellipseWidth, ellipseHeight)

先来预览一下这些方法,稍后再对每种方法进行详细的介绍。

绘图对象

在 Flash 早期版本中,绘图 API 方法是影片剪辑(MovieClip)类中的方法,可以在影片剪辑实例中直接调用,代码如下:

myMovieClip.lineTo(100, 100);

影片剪辑和 Sprite 都可以访问绘图 API,只是实现起来有些不同。目前,Sprite 影片和影片剪辑都有一个名为 graphics 的属性,用于访问绘图 API 的方法。为了直接访问绘图方法,我们可以这样写:

mySprite.graphics.lineTo(100, 100);

下面在示例中看看这些方法的基本使用。

新的数值类型:int 和 uint

在以前的 ActionScript 版本中,只有一种数值类型 Number,它可以表示正整数,负整数或是浮点数(或0)。我们已经习惯了这种***的用法,但是现在多增加的两种数值类型可以让我们的代码更加清晰。

第一个新增加的数值类型是 int(整型),这个类型可以为正整数或负整数或零。如果我们把一个浮点数值声明为 int 类型的话,小数部分会自动被去掉。比如,把 1.9 声明为 int,结果为 1。因此,当我们确定只使用整数时,就把变量声明为 int ,在循环中用于计数的变量一般应该是 int 。下面这段代码中,i变量永远不会得到浮点数值,这时使用int类型就有了意义。

for(var i:int = 0; i < 100; i++) {
// do something in here!
}

第二个新的类型是 uint(无符号整型),“无符号”意思是没有正负(+-)号,永远为正数。32位颜色值在 AS 3 中总是以 uint 类型存储,这是因为无符号整型比(有符号)整型能够保留更多的数值。 Int 和 uint 都可以存储32位数,这个数值大于40亿,但是 int 有一个特殊位用于存储符号(+-),所以只有31位数(大于20亿),这样就可以标记正数或负数了。所以,使用int 类型声明一个正的32位色彩值就显得太大了!如果用了又会怎样?让我们来试试:

var color1:int = 0xffffffff;
trace(color1);
var color2:uint = 0xffffffff;
trace(color2);

0xFFFFFFFF 的值相当于十进制的 4,294,967,295,因为这个值对于 int 来说太大了,所以结果被“反转”了过来变成了 -1!当然这不是我们所期望的结果。如果使用 uint 类型的话,就没问题了。因此,由于色彩值永远都是正数,并有可能超出 int 的值域范围,所以要使用 uint 来存储它们。

色彩合成

如何将红、绿、蓝三种颜色值组成一个有效的颜色值,这是个普遍的问题。假设有三个变量 red,green,blue,每个变量里面保存一个0到255之间的数。下面是这个公式:
color24 = red<<16 | green<<8 | blue;
加入透明度后,建立一个32位色彩值,公式如下:
color32 = alpha << 24 | red << 16 | green << 8 | blue;

这里用到了两个位操作符,大家以前可能没有接触过。位操作是对二进制(0或1)进行的操作,对于24位色来说,如果把颜色值的每一位都列出来,就会得到一串由24个0或1组成的字串。把十六进制 0xRRGGBB 分解成二进制后是这样的:RRRRRRRRGGGGGGGGBBBBBBBB,我们看到有8位red,8位green,8位blue,也就是说8位二进制数等于256。

在色彩合成公式中,第一个位操作符是 << ,是一个按位左移操作符,该操作是将二进制数值向左侧移动。比如,红色值(red)为0xFF或255,可以由二进制表示为:
11111111
将它向左移动16位,结果是:
111111110000000000000000

在24位色彩中,它表示红色,转换为二进制后为 0xFF0000,是纯红色。
下面,假设有一个绿色值(green)为 0x55(十进制85),二进制表示为:
01010101
将它向左移动8位后,结果为:
000000000101010100000000

这样一来,这8位数完全移动到了绿色值的范围。

最后,假设一个蓝色值为0xF3(十进制243),二进制表示为:11110011。因为它们都处在蓝色(blue)的范围,所以不需要再去移动它。这样我们总共就拥有了三组数:
111111110000000000000000
000000000101010100000000
000000000000000011110011

可以简单地将它们加起来,成为一个24位数,但是,还有一种更好更快的方法:使用或(OR)运算,符号是 | 。它会将两组数的每个二进制位进行比较,如果两个之中有一个数为1,那么结果就为1,如果两个数都为0,那么结果就为0。可以使用或(OR)运算将 red,green,blue 的值相加起来,也可以这么说“如果这个数或这个数或这个数中有一个数等于1,那么结果就为1”。最终结果为:
111111110101010111110011
将这个数转换为十六进制就等于 0xFF55F3 。当然,我们无法看到这些二进制位,也不会与这些0或1打交道,只需要学会这种写法:
var color24:Number = 0xFF << 16 | 0x55 << 8 | 0xF3;
十进制写法是:
var color24:Number = 255 << 16 | 85 << 8 | 243;
Flash 并不关心人们使用的是十进制数还是十六进制数。

同样,还可以将 red,green,blue 的值全部转换为十六进制的字符串,然后将它们连接成一条很长的字符串,最后再把它们转换为十六进制数。但是,如果这样做的话会很麻烦,而且使用字符串操作会非常慢。相反,使用二进制操作是 ActionScript 中最快的运算,因为它们属于低级运算。

对于32位数,其实道理也是一样的,加入8位 alpha(透明度)通道并将其向左移24位。例如,有一组32位数为0xFFFF55F3,将 alpha 值向左移动24位,结果如下:
11111111111111110101010111110011
前8位数表示透明度,后面的 red,green,blue 值与前面的一样。

创建多条曲线

下面我们将目光转向创建多条曲线,而不仅是一条曲线,创建一条平滑的向各个方向弯曲的线。首先,来看一个错误的做法,是我原先尝试过的一种方法。从随便一个点位出发,经过第一个点到第二个点再到第三个点,经过第四个到达第五个,经过第六个到达第七个等等绘制一条曲线。这里是代码(文档类 MultiCurve1.as):

package {
 import flash.display.Sprite;
 public class MultiCurves1 extends Sprite {
  private var numPoints:uint = 9;
  public function MultiCurves1() {
   init();
  }
  private function init():void {
   // first set up an array of random points
   var points:Array = new Array();
   for (var i:int = 0; i < numPoints; i++) {
    points[i] = new Object();
    points[i].x = Math.random() * stage.stageHeight;
    points[i].y = Math.random() * stage.stageHeight;
   }
   graphics.lineStyle(1);
   // now move to the first point
   graphics.moveTo(points[0].x, points[0].y);
   // and loop through each next successive pair
   for (i = 1; i < numPoints; i += 2) {
    graphics.curveTo(points[i].x, points[i].y,

    points[i + 1].x, points[i + 1].y);
   }
  }
 }
}

第一次循环在 init 方法中,建立一个数组存储九个点。每个点都是一个 object 拥有 x,y 属性,它们的值都是舞台尺寸的随机数。当然,在一个真正的程序中,点位也许不是随机的,只是用这种方法进行快速设置。

随后设置线条样式,将笔移动到第一个点位。下一个循环从1开始每次递增2,所以线条是经过第一点到达第二点,然后从第三点到第四点,再从第五点到第六点,最后从第七点到第八点。至此,循环停止,因为第八点是最后一个点。大家也许注意到了,这里至少要有三个点,而且点的数量必需为奇数个。

程序看起来还不错,测试一下试试。如图4-1所示,看起来不是非常平滑,有棱有角的,这是因为曲线之间没有进行协调,它们之间共用了一个点。

 Making Things Move 第四章

图4-1 多条曲线,错误的方法。我们可以清楚地看到曲线的结束和开始的位置。

我们也许不得不去加入更多的点才能使解决这个问题。这里有个策略:在每两对点之间,加入一个新点(中间点)放在这两点的正中间。然后使用这些中间点作为起点和终点,而把最初的那些点(原始点)作为控制点。

图4-2 说明了解决办法。在图中,白点为原始点,黑点为中间点。这里使用了三条 curveTo 方法,图中的点使用了不同的颜色,这样就能分辨出起点与终点了。(图4-2 是 multicurvedemo.fla 文件的一张截图,可以在 www.friendsofted.com 的 books 页面下载)

 Making Things Move 第四章
图4-2 带有中间点的多线条

注意,图4-2中第一个中间点和最后一个中间点都没有被使用,第一个和最后一个原始点留作曲线的两个端点,只需在第二个点和倒数第二个点之间进行连接。这里是前一个例子的升级版,文档类 MultiCurve2.as:

package {
 import flash.display.Sprite;
 public class MultiCurves2 extends Sprite {
  private var numPoints:uint = 9;
  public function MultiCurves2() {
   init();
  }
  private function init():void {
   // first set up an array of random points
   var points:Array = new Array();
   for (var i:int = 0; i < numPoints; i++) {
    points[i] = new Object();
    points[i].x = Math.random() * stage.stageHeight;
    points[i].y = Math.random() * stage.stageHeight;
   }
   graphics.lineStyle(1);
   // now move to the first point
   graphics.moveTo(points[0].x, points[0].y);
   // curve through the rest, stopping at each midpoint
   for (i = 1; i < numPoints - 2; i ++) {
    var xc:Number = (points[i].x + points[i + 1].x) / 2;
    var yc:Number = (points[i].y + points[i + 1].y) / 2;
    graphics.curveTo(points[i].x, points[i].y, xc, yc);
   }
   // curve through the last two points
   graphics.curveTo(points[i].x, points[i].y, points[i+1].x,
   points[i+1].y);
  }
 }
}

请注意,在新代码中, for 循环从1开始到 points.length -2 结束,也就避开了第一个点和最后一个点。程序要做的是,创建新的 x,y 点,这个点是数组中后面两个点位的平均值。然后从数组下一个点位开始画一条曲线到新的平均点(中间点)。当循环结束时, i 变量指向倒数第二个元素,因此,可以穿过这里向最后一个点画条曲线。

这时,就得到一个非常平滑的图形,见图4-3。注意,这时原始点的数量不再受奇数个的限制。

再加一点小小的变化,使用同样的技术创建一条封闭的曲线。首先,计算一个初始的中间点,并移动到这里。然后,进行循环,获得每一个中间点,最后,将最后一条曲线画回初始中间点。图4-4 为显示结果

 Making Things Move 第四章
图4-3 多条平滑曲线

package {
 import flash.display.Sprite;
 public class MultiCurves3 extends Sprite {
  private var numPoints:uint = 9;
  public function MultiCurves3() {
   init();
  }
  private function init():void {
   var points:Array = new Array();
   for (var i:int = 0; i < numPoints; i++) {
    points[i] = new Object();
    points[i].x = Math.random() * stage.stageHeight;
    points[i].y = Math.random() * stage.stageHeight;
   }
   // find the first midpoint and move to it
   var xc1:Number = (points[0].x + points[numPoints - 1].x) / 2;
   var yc1:Number = (points[0].y + points[numPoints - 1].y) / 2;
   graphics.lineStyle(1);
   graphics.moveTo(xc1, yc1);
   // curve through the rest, stopping at midpoints
   for (i = 0; i < numPoints - 1; i ++) {
    var xc:Number = (points[i].x + points[i + 1].x) / 2;
    var yc:Number = (points[i].y + points[i + 1].y) / 2;
    graphics.curveTo(points[i].x, points[i].y, xc, yc);
   }
   // curve through the last point, back to the first midpoint
   graphics.curveTo(points[i].x, points[i].y, xc1, yc1);
  }
 }
}

 Making Things Move 第四章
图4-4 多条封闭曲线

使用 beginFill 和 endFill 创建图形
beginFill(color, alpha) 方法非常简单,没有太多可说的。有一点值得注意,同 lineStyle 一样, alpha 的取值范围也变为了 0.0 到 1.0,而不是 0 到 100,这项也是可选的,默认为1.0。无论何时执行该帧的绘图代码 Flash 都会开始进行计算,无论何时遇到 endFill 指令 Flash 都会停止计算。总结一下,过程如下:

  • moveTo
  • lineStyle (如果有参数可以填入)
  • beginFill
  • 在一系列的 lineTo 和 curveTo 方法后,要在最初的点位结束
  • endFill

事实上,使用前三个方法的顺序不会影响到绘图。我们不是必需要指定线条样式,请记住如果不指定线条样式就会得到一条看不见的线条,非常适合绘制填充色,当然两者同时绘制也不错。如果所绘制的线条没有回到最初开始的点位,一但调用了 endFill, Flash 将会自动绘制一条封闭线,是为了能封闭这个图形。调用 endFill 后,无论线条样式如何,都会自动将最后一条线绘制完成。当然,我们自己将线条封闭是个很好的习惯,这样一来,既确保了最后的能够正确绘制,又可以让看代码的人知道我们究竟想画的是什么图形。

下面来试一下绘制填充色,可以使用前面的封闭曲线示例(MultiCurve3.as)来完成,这里已生成了一个封闭的图形。只要将 beginFill 语句放在第一条 curveTo 前面的任何地方——如 beginFill(0xff00ff);,这样就创建了亮紫色的填充——最后使用 endFill() 结束。

使用 clear 删除绘制

clear 是所有方法中最简单的,它可以用来删除在影片中所绘制的直线,曲线或填充色。请注意,这个命令中对其它 graphics 绘制的图像不起作用。换句话讲,如果在编辑环境下绘制了一个图形,再对其使用 clear() 命令,结果是无效的。

在绘图中,使用 clear 方法会有些意想不到的效果。在绘图 API 中,如果绘制的影片剪辑越多,运行速度就越慢。对于有很多绘制图形的影片来说,速度不会立刻慢下了,而是随着每个图形所占用的绘制时间会越来越长,从而逐渐地变慢。就算新的图形完全覆盖住了所有旧图形,旧图形的矢量信息也仍然存在并且每次都会被重绘,只有使用 clear 函数才可以完全删除之前旧图形的矢量信息。

使用 lineStyle 设置线条样式

使用 lineStyle(width,color,alpha)方法,作用是为以后使用的绘图线条设置线条样式,该命令对于前面使用的绘图线条不会产生影响。实际上,除了清除或覆盖之外,没有方法可以影响或改变已经绘制的线条或填充。

前面列出的这些参数将来会经常使用,还有一些额外的可选参数如像素提示(pixel),缩放模式(scale mode),端点(caps),拐角类型(joints)和切断尖角(mitres)。如果大家需要更多的设置,也许会用到它们,但是大多数情况下,只会用到下面这些参数。对于它们无需做太多解释,只是来复习一下:

  • width:线条的宽度以像素为单位。只能使用0或正整数。虽然可以使用十进制浮点数,但会被取整为最接近的正整数。如果输入的是0或负数, Flash 将绘制1像素宽的线。这与在 Flash IDE 中在属性面板中选择“细线”的功能相同。
  • color:线条的颜色。使用十进制或十六进制的24位色彩值表示。
  • alpha:线条的透明度。使用0.0到1.0数字之间的数表示透明度的比例。值为1.0表示完全不透明,值为0.0表示完全透明或不可见。注意,这与 AS 2 中使用 0到100 表示法是不同的。

由于这些参数是可选的,可以只使用 lineStyle(1) 来设置一条1像素宽的黑色线条。其实第一个参数也是可选的,如果不填 widh 参数,只使用 lineStyle() 的话,那么线条就被清除,只获得了一条不可见的线,相当于使用绘图指令时没有设置线条样式(lineStyle)。另一个容易出错的地方是,在使用 clear 方法时,不仅清除了当前绘制的图形而且也清除了当前使用的线条样式。如果在影片绘图时设置了一个普通的线条样式,而后又将线条清除,那么在绘制其它图形之前还需要重新设置线条样式。否则的话,接下来绘制的线条就是不可见的,调用 clear 方法同时还会将绘图指针位置归为0,0。

使用 lineTo 和 moveTo 绘制直线

在一种绘图语言中会有多种方法用来绘制直线。一种是使用画线指令,需要有一个起点和一个终点,并在这两点之间画一条直线。另一种是使用 lineTo 指令,只需要一个终点。那么 ActionScript 是怎样工作的呢,如果向某一点画线,哪里才是起点呢?如果之前没有进行过画线,那么起点就是0,0点,可以这样写:lineTo(100, 100);

结果将会看到一条从左上角(0,0)画到100,100像素位置的线(假设已经设置了线条样式)。在绘制完最少一条线后,这条线的终点位置就会成为下一条线的起点位置。不过,我们还可以使用 moveTo 方法为下一条线指定一个新的起点位置。

可以把绘图 API 想像成一个拿着笔和纸的机器人,开始的时候,笔处在0,0点。当我们告诉它向某点画一条线时,它就将笔在纸上划过,并向这个位置移动。 moveTo 方法就像在说“ok,现在抬起笔,放到下一个点上。”虽然仅使用 moveTo 指令不会产生一个新的图形,但是它会影响下一次绘图时的位置。通常使用 moveTo 作为第一条绘图指令,用于将绘图 API 的“笔”移动到起点位置。现在大家已经拥有了足够的知识可以来实践一下了,让我们创建一个简单的绘图应用程序,这个程序是完全依赖绘图 API 完成的。这里是文档类:

package {
 import flash.display.Sprite;
 import flash.events.MouseEvent;
 public class DrawingApp extends Sprite {
  public function DrawingApp() {
   init();
  }
  private function init():void {
   graphics.lineStyle(1);
   stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
   stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
  }
  private function onMouseDown(event:MouseEvent):void {
   graphics.moveTo(mouseX, mouseY);
   stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
  }
  private function onMouseUp(event:MouseEvent):void {
   stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
  }
  private function onMouseMove(event:MouseEvent):void {
   graphics.lineTo(mouseX, mouseY);
  }
 }
}

首先,导入 MouseEvent 类,因为这里的每件事都要用到鼠标事件。在 init 方法中,线条样式设置为 1 像素黑色线,并增加 mouseDown 和 mouseUp 作为事件侦听器。

然后是 onMouseDown 方法,每当用户按下鼠标是都会调用它,这意味着用户要开始在当前鼠标位置画线了。这个函数通过使用 moveTo 方法将那支虚拟的笔放置在当前鼠标的位置,以鼠标坐标为参数,随后为 mouseMove 添加了一个侦听器。
每当用户移动鼠标时,都会调用 onMouseMove 方法,向当前鼠标的位置画一条线。
最后是 onMouseUp 方法,用于删除 mouseMove 侦听器使其不再进行画线。
好的,现在已经制作好了一个短小精悍的绘图程序。我们不需要再费太大的力气就可以为这个程序加入一些简单的控制,让它具有完整绘图程序的功能。只需要为线条颜色(color)和线条宽度(width)创建一些变量,再创建一些按钮什么的用来改变它们,并重新调用 lineStyle 方法使用这些新的值。对了,还可以再放一个按钮用于调用 clear 方法。把这个留做是一个练习,希望大家有兴趣的话,能够自行完成。

使用 curveTo 绘制曲线

下一个绘图函数,curveTo(x1, y1, x2, y2),起点和 lineTo 一样,同样是以上一次画线的终点做为本次画线的起点,也可以使用 moveTo 命令指定画笔的起点,如果是第一次画线默认的起点为0,0。

可以看到, curveTo 函数中包括两个点。第一个是控制点影响曲线的形状,另一个是曲线的终点。这里使用的是名为二次方贝塞尔曲线的标准公式,该公式可以计算出两点间的曲线,这条曲线向着控制点弯曲。请注意,这条曲线不会与控制点接触,很像是曲线被它吸引过去的。

下面来看动作脚本,文档类 DrawingCurves.as:

package {
 import flash.display.Sprite;
 import flash.events.MouseEvent;
 public class DrawingCurves extends Sprite {
  private var x0:Number = 100;
  private var y0:Number = 200;
  private var x1:Number;
  private var y1:Number;
  private var x2:Number = 300;
  private var y2:Number = 200;
  public function DrawingCurves() {
   init();
  }
  private function init():void {
   stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
  }
  private function onMouseMove(event:MouseEvent):void {
   x1 = mouseX;
   y1 = mouseY;
   graphics.clear();
   graphics.lineStyle(1);
   graphics.moveTo(x0, y0);
   graphics.curveTo(x1, y1, x2, y2);
  }
 }
}

测试这个文件,把鼠标来回移动。这里使用了两个给定的点作为起点和终点,使用鼠标位置作为控制点。请注意,曲线不会真正到达控制点位置,而只到达与控制点一半的位置。

过控制点的曲线

现在,如果想让曲线真正地穿过控制点,那么这就是我们工具箱中的另一个工具。使用下面这个公式计算出控制点的实际位置,这样就可以让曲线穿过指定的点了。同样,以 x0,y0 为起点,以 x2,y2 为终点,x1,y1为控制点,把将要穿过的点叫 xt,yt (目标点)。换言之,如果让曲线穿过 xt,yt点,那么 x1,y1 又需要如何使用呢?公式如下:

x1 = xt * 2 – (x0 + x2) / 2;
y1 = yt * 2 – (y0 + y2) / 2;

只需要把目标点乘以2,然后减去起点与终点的平均值。大家可以画张图来究竟一下它的原理,要么就直接学会使用它。

把公式放在代码中,鼠标坐标用使用 xt,yt,我们只需要改变前一个文档类中的两行,将下面两行:

x1 = mouseX;
y1 = mouseY;

替换为

x1 = mouseX * 2 - (x0 + x2) / 2;
y1 = mouseY * 2 - (y0 + y2) / 2;

或者直接看 CurveThroughPoint.as,现成的文件。

位图(Bitmaps)

与滤镜相同,可以用整本书来介绍 Bitmap 和 BitmapData 类,看起来也不错,但是这并不是本书的目的。我们将通过一些简单的例子,用来指出 AS 2 与 AS 3 中位图处理的变化。

在 AS 2 中,通过调用 BitmapData()函数,新建一个 BitmapData 对象使用如下参数:

new BitmapData (width:Number,
height:Number,
transparent:Boolean,
fillColor:Number)

你也许猜到了, BitmapData 类同样也是嵌入在一个包中,完整的使用名称如下 flash.display.BitmapData。所以需要导入包,对于 width 和 height 参数则非常显而易见, transparent 参数表示创建的图像是否包涵一个 alpha 通道,选择 true 或 false ,fillColor 是创建图像的初始颜色,如果 transparent 为 true 的话,那么位图就用 32 位色表示,0xAARRGGBB,如果为 false 的话,就可以使用 24 位安全色表示。

在创建 BitmapData 对象时,也许很想能看到它的样子。在 AS 2 中,使用 attachBitmap 命令在影片剪辑中添加一个位图。大家也许会想,现在是否可以使用 addChild 在显示对象中添加一个位图,但事实上并没有这么简单。问题在于 addChild 只对继承自 DisplayObject 类的对象起作用,如 Sprite 影片,影片剪辑和文本框。然而,如果我们研究一下类的结构,就会发现 BitmapData ,没有继承自 DisplayObject,所有不能直接添加对象。这就是为什么要有 Bitmap 类的原因, Bitmap 类几乎始终都有一个函数作为 BitmapData 实例的容器,可以这样创建:

var myBitmapData:BitmapData = new BitmapData(100, 100, false, 0xff0000);
var myBitmap:Bitmap = new Bitmap(myBitmapData);

现在就可以将对象加入到显示列表了:

addChild(myBitmap);

使其可见后,Bitmap 实例也可以改变位置,进行缩放,增加滤镜等等。
测试这个例子,只需要在第二章给出的类框架的 init 方法加入这三行就可以了,不要忘记导入 flash.display.Bitmap 和 flash.display.BitmapData,运行后就会看到一个红色的正方形。乍看上去,与使用绘图 API 所画的图形没什么不同,但是要知道这并不是矢量图绘制法:填充一个红色的正方形。这是张位图图像,在位图中每一个像素都要分别指定而且是可变的。事实上,每一个像素值都可以使用 getPixel,getPixel32 和 setPixel,setPixel32 进行读取和设置。两个版本的不同之处在于 getPixel 和 setPixel 使用24位色彩值忽略了 alpha 通道,而  “32”版的则使用32位色彩值其中包括了透明度信息。让我们来做个例子,制作一个简单的喷漆工具,就像所有位图喷漆程序一样。
这里是文档类,SprayPaint.as:

package {
 import flash.display.Sprite;
 import flash.display.Bitmap;
 import flash.display.BitmapData;
 import flash.events.MouseEvent;
 import flash.events.Event;
 import flash.filters.BlurFilter;
 public class SprayPaint extends Sprite {
  private var canvas:BitmapData;
  private var color:uint;
  private var size:Number = 50;
  private var density:Number = 50;
  public function SprayPaint() {
   init();
  }
  private function init():void {
   canvas = new BitmapData(stage.stageWidth,
   stage.stageHeight,
   true, 0x00000000);
   var bmp:Bitmap = new Bitmap(canvas);
   addChild(bmp);
   stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
   stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
  }
  private function onMouseDown(event:MouseEvent):void {
   color = Math.random() * 0xffffff + 0xff000000;
   addEventListener(Event.ENTER_FRAME, onEnterFrame);
  }
  private function onMouseUp(event:MouseEvent):void {
   removeEventListener(Event.ENTER_FRAME, onEnterFrame);
  }
  private function onEnterFrame(event:Event):void {
   for (var i:int = 0; i < density; i++) {
    var angle:Number = Math.random() * Math.PI * 2;
    var radius:Number = Math.random() * size;
    var xpos:Number = mouseX + Math.cos(angle) * radius;
    var ypos:Number = mouseY + Math.sin(angle) * radius;
    canvas.setPixel32(xpos, ypos, color);
   }
  }
 }
}

这也许是目前为止最复杂的代码,但除了 BitmapData 的内容外,其它的知识前面都讲过,只不过又使用了一遍而已。一步步来看,首先,创建了一些类变量,包括 canvas 变量,用于存放 BitmapData 的实例。创建的实例尺寸等于舞台的尺寸,并使用透明的背景色。然后使用 canvas 创建一个位图,并加入到显示列表。
鼠标事件处理函数中选择了一个随机的颜色,并且带有添加和删除 enterFrame 事件处理函数的功能。我们来回忆一下三角学,首先,从 0 到 Math.PI * 2 中计算出一个随机的角度,不要忘记使用弧度制表示,相当于随机的360度。然后,计算出一个随机的半径后,再使用三角函数将半径和角度转换为 x,y 值。最后使用 setPixel32 以鼠标位置加上随机的 x,y 值的像素点设置为喷漆色,每一次开始喷漆时随机决定颜色。在这个例子中有一个 for 循环,每一帧都会进行循环,每次循环多少次由 density 的值决定。 color 的值为24位的色彩值,然后加上 0xFF000000,为的是设置 alpha 通道为完全不透明,如果没有加上这个值,那么所有的颜色就都为透明的。如果用 0xFFFFFFFF 乘以 Math.random(),那么颜色的透明度是随机的,也许是你想要的,但不是我想要的。通过改变 density 和 size 的值再测试一下,看看会有些什么不同的效果。大家也许已经想到如何让用户来控制改变这些参数了。

刚刚看到这个程序时,你也许会想,“真是小题大作,完全可以用绘图 API 或通过加载小影片剪辑并改变颜色来实现”。是的,完成可以这么做,但是如果使用绘图 API 绘出成千上万的独立图像后,会发习画得越多,速度越慢。画过几百个图形后,慢下来的速度会变得非常明显,这个程序也就费掉了,使用加载影片剪辑的方式也是如此。但是,使用位图就完全不同了,我们可以使用这个程序喷上一天,都不影响程序的速度或效率。
如果想看到更酷的效果,就把下面一行代码加在位图对象 bmp 的后面:

bmp.filters = [new BlurFilter(2, 2, 3)];

在位图中使用模糊滤镜比在矢量图中使用效果更加明显。当然,设置像素是 BitmapData 对象能做的最简单的操作之一。除了获取和设置像素,BitmapData 对象还有其它二十多种方法,这些方法可用来复制像素,设置阈值,分解,合并,滚动,等等。我个人最喜欢的一个是 perlinenoise 方法,该函数允许我们创建一个随机的有组织的图案。对于制造烟,云和水波纹效果都非常有用。有兴趣的话大家可以试验一下。

读取和嵌入资源

最后一个重点话题是获取外部资源的概念,如在影片中加载位图或外部 SWF 文件。有两种方法,一种是在动画播放时将资源读入,这就是我们所熟知的读取(loading)。另一种方法是在 SWF 编译时嵌入(embed)资源。

读取资源

创建一个 Loader 对象来读取一个资源,这是flash.display.Loader 类的一个实例。 loader 是个显示对象,意味着可以使用 addChild() 方法将它加入到显示列表中,就像 sprite 和 bitmap 一样。然后告诉这个 loader 去读取一个外部 SWF 或外部位图,如 JPEG,PNG,等等。

在 AS 2 中,当处理外部文件路径或 URL 时,只需要使用一个简单的字符串表示路径。 在 AS 3 中,则需要创建一个 flash.net.URLLoader 实例,传入表示路径的字符串,并且还需要一个额外的步骤,虽然有些烦人,但是我们还是要习惯这种用法。
这里是一个在运行时读取外部资源的例子(文档类 LoadAsset.as):

package {
 import flash.display.Sprite;
 import flash.display.Loader;
 import flash.net.URLRequest;
 public class LoadAsset extends Sprite {
  public function LoadAsset() {
   init();
  }
  private function init():void {
   var loader:Loader = new Loader();
   addChild(loader);
   loader.load(new URLRequest("picture.jpg"));
  }
 }
}

嵌入资源

虽然在有些情况下,在运行时读取资源很合适,但是在有些情况下有一些外部图形只想加载到 SWF 自里面。这时,如果使用 Flash IDE,可以简单地导入这个对象到库中并设置为“为 ActionScript 导出”。但在使用 Flex Builder 2 或 Flex 2 SDK 命令编译器时,没有库,那么如何在 SWF 中加载外部资源呢?

答案是使用[Embed]元数据(metadata)标签嵌入资源,元数据标签是指加到 ActionScript 文件中的非正式 ActionScript 语句。另外,它们指示编译器在编译过程中去做某种事情,[Embed]标签告诉编译器在最终的 SWF 文件中加载一个特殊的外部资源,资源可以是位图或外部 SWF 文件。告诉编译器要嵌入的资源所在的 source 路径的属性,如下:

[Embed(source="picture.jpg")]

在元数据语句的后面,直接声明一个 Class 类型的变量,如下:

[Embed(source="picture.jpg")]
private var Image:Class;

现在可以使用这个变量创建一个新的资源实例,如下:

var img:Bitmap = new Image();

注意创建的这个对象是 Bitmap 类型的。如果嵌入一个外部 SWF 文件,创建的这个对象应该是 Sprite 类型的,如下:

[Embed(source="animation.swf")]
private var Anim:Class;
var anim:Sprite = new Anim();

这里是一个在 SWF 中嵌入外部 JPEG 的例子:

package {
 import flash.display.Sprite;
 import flash.display.Bitmap;
 public class EmbedAsset extends Sprite {
  [Embed(source="picture.jpg")];
  private var Image:Class;
  public function EmbedAsset() {
   init();
  }
  private function init():void {
   var img:Bitmap = new Image();
   addChild(img);
  }
 }
}

如果我们使用 Flash IDE ,只要将对象导入到库中并“为 ActionScript 导出”给出一个类名就可以了。不需要使用 [Embed] 元数据标签及类变量,事实上,Flash IDE 编译器甚至不支持 [Embed] 元数据标签。这里只作一个简单的介绍,因为在本书后面的内容中不会用到这个技术,但是很显然这是个非常有用的方法。

本章重要公式

在本章中我们又收集了很多有价值的工具,大多都与颜色有关。

转换为十进制
trace(hexValue);

十进制转换为十六进制
trace(decimalValue.toString(16));

颜色合成
color24 = red << 16 | green << 8 | blue;
color32 = alpha << 24 | red << 16 | green << 8 | blue;

颜色提取
red = color24 >> 16;
green = color24 >> 8 & 0xFF;
blue = color24 & 0xFF;
alpha = color32 >> 24;
red = color32 >> 16 & 0xFF;
green = color32 >> 8 & 0xFF;
blue = color232 & 0xFF;

过控制点的曲线
// xt, yt is the point you want to draw through
// x0, y0 and x2, y2 are the end points of the curve
x1 = xt * 2 – (x0 + x2) / 2;
y1 = yt * 2 – (y0 + y2) / 2;
moveTo(x0, y0);
curveTo(x1, y1, x2, y2);

滤镜(Filter)

滤镜是一些位图的效果,可以应用于任何显示对象。在 Flash IDE 中可以通过滤镜面板或使用时间轴的 ActionScript 来使用滤镜,由于这本书是关于 ActionScript 的,所以只能简单地讨论一下应用滤镜的方法。在 AS 3 中包括以下几种滤镜:

  • Drop shadow(投影滤镜)
  • Blur(模糊滤镜)
  • Glow(发光滤镜)
  • Bevel(斜角滤镜)
  • Gradient bevel(渐变斜角滤镜)
  • Gradient glow(渐变发光滤镜)
  • Color matrix(颜色矩阵滤镜)
  • Convolution(卷积滤镜)
  • Displacement map(置换图滤镜)

虽然不能一一介绍每种滤镜的使用细节,但大家可以通过帮助文档来学习。在书中还会有很多滤镜使用的例子,所以在这里只介绍一下滤镜使用的总体方法和两个具体实例。

创建滤镜

通过使用 new 关键字及滤镜名来创建滤镜,并给出所需的参数。例如,创建一个 blur filter(模糊滤镜),最简单的一种滤镜,写法入下:

var blur:BlurFilter = new BlurFilter(5, 5, 3);

参数分别为 blurX,blurY,quality。这个例子会将对象在x和y轴上模糊5个像素,模糊的品质为中等。

另一点需要知道的是滤镜在其名为 flash.filters 的包中。所以要在文件的开始处将它们导入进来:

import flash.filters.BlurFilter;

如果希望导入包中所有的滤镜,可以使用简写:

import flash.filters.*;

现在,我们可以直接创建任何类型的滤镜了,但是一般来说,除非要使用这个包中的大部滤镜,否则***避免使用通配符(*),而是明确地导入所需要的类。这样做只是为了能够清楚,哪些是真正想要导入的而哪些不是。好了,现在已经创建了一个模糊滤镜,但怎么才能使它去模糊一个对象呢?

任何一个显示对象都有一个名为 filters 的属性,这是一个包括了所有滤镜的数组,因为如果一个对象要应用多个滤镜,那么只需要再将模糊滤镜放到数组中即可。乐观地看,应用滤镜应该可以像使用基本数组操作那样简单,push,就像这样 mySprite.filters.push(blur);,但是很遗憾,没有这么简单。在整个数组赋值为 filters 之前,Flash 不关心 filters 数组的变化。

如果已知对象没有应用任何的滤镜,或想要重写它们,只需要新建一个数组,将我们的滤镜粘在上面,再将这个新数组赋给 filters 属性就可以了。先来试一下,下面一个文档类 Filters.as,创建了一个 sprite 影片并且在里面绘制了一个黄色的正方形,然后,创建一个滤镜,加入数组中,最后将数组赋给 sprite 的 filters 属性:

package {
 import flash.display.Sprite;
 import flash.filters.BlurFilter;
 public class Filters extends Sprite {
  public function Filters() {
   init();
  }
  private function init():void {
   var sprite:Sprite = new Sprite();
   sprite.graphics.lineStyle(2);
   sprite.graphics.beginFill(0xffff00);
   sprite.graphics.drawRect(100, 100, 100, 100);
   sprite.graphics.endFill();
   addChild(sprite);
   var blur:BlurFilter = new BlurFilter(5, 5, 3);
   var filters:Array = new Array();
   filters.push(blur);
   sprite.filters = filters;
  }
 }
}

瞧!出现了一个模糊的黄色方块儿。重要的部分用黑体着重,我们可以简写一点:

var blur:BlurFilter = new BlurFilter(5, 5, 3);
var filters:Array = [blur];
sprite.filters = filters;

或再短一点:

sprite.filters = [new BlurFilter(5, 5, 3)];

在创建数组的同时,将滤镜放进去,并应用 filters 属性,这样一来,Flash 会很高兴。

但是如果已经有了滤镜并希望继续使用,这时,但又不确定是否有滤镜存在,那该怎么办呢?在 Flash 8 中,这是件很麻烦的事,因为一个显示对象的 filters 属性如果没有应用滤镜,那么它将是未定义(undefined)的。但在 AS 3 中, filters 数组总是保持为一个空数组,只需要给数组赋值,将滤镜 push 进去,并将其赋给对象的 filters 属性即可,方法如下:

var filters:Array = sprite.filters;
filters.push(new BlurFilter(5, 5, 3));
sprite.filters = filters;

如果使用这种方法,那么无论是否有滤镜存在都没有问题,滤镜只是被加入到数组列表中而已。因为 filters 属性是一套成熟的数组,所以可以使用不同的数组操作方法。比如,使用 concat 方法:

sprite.filters = sprite.filters.concat(new BlurFilter(5, 5, 3));

我不认为这是个“正确”的做法,大家只要知道将一个包涵有滤镜的数组赋给 filters 属性就足够了。

动态滤镜

现在我们已经基本上知道了如何在 ActionScript 中使用滤镜了。接下来,用已经学过的知识,制作一个动态滤镜。这个效果,使用文档类 AnimatedFilters.as:

package {
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.filters.DropShadowFilter;
 public class AnimatedFilters extends Sprite {
  private var filter:DropShadowFilter;
  private var sprite:Sprite;
  public function AnimatedFilters() {
   init();
  }
  private function init():void {
   sprite = new Sprite();
   sprite.graphics.lineStyle(2);
   sprite.graphics.beginFill(0xffff00);
   sprite.graphics.drawRect(-50, -50, 100, 100);
   sprite.graphics.endFill();
   sprite.x = 200;
   sprite.y = 200;
   addChild(sprite);
   filter = new DropShadowFilter(0, 0, 0, 1, 20, 20, .3);
   addEventListener(Event.ENTER_FRAME, onEnterFrame);
  }
  private function onEnterFrame(event:Event):void {
   var dx:Number = mouseX - sprite.x;
   var dy:Number = mouseY - sprite.y;
   filter.distance = -Math.sqrt(dx * dx + dy * dy) / 10;
   filter.angle = Math.atan2(dy, dx) * 180 / Math.PI;
   sprite.filters = [filter];
  }
 }
}

首先在 sprite 中画一个正方形,正方形在 sprite 的居中位置,然后将 sprite 移动到舞台中间,用一些默认属性创建投影滤镜(DropShadowFilter)。
添加一个 enterFrame 事件的侦听器及处理函数:onEnterFrame 方法,用于计算角度(angle)及使用三角函数计算 sprite 影片与鼠标的距离(distance)。使用 angle 和 distance 设置投影滤镜的 angle 和 distance 属性,最后将这个滤镜再应用到 sprite 上。请注意,我们不需要每次都创建一个新的滤镜,可以继续使用同一个滤镜,只需要改变它的属性即可。然而,只是改变这些属性也不能更新 sprite 影片的显示。因此,还需要再将变化过的滤镜效果赋值给 filters 属性。