用Micromedia Flash编写微型教学软件

www.minglang.org 吴建国

Macromedia Flash是制作平面矢量动画的最流行的工具,目前主要的应用领域仍然是网页中的动画制作。作为一款优秀的制作矢量动画的工具,在数学和自然科学课程的教学软件制作领域,Flash应该也会获得越来越广、越来越深的应用。作者做这样的判断是基于下述几点考虑:Flash提供的矢量绘图工具非常适合于科学和几何上的应用;Falsh 的播放文件数据量小,便于网上传送;目前最常用的网页浏览器中都已内嵌了Flash播放器;Flash入门比较容易;Flash 5的编程能力已经较为强大,例如可以控制立体声在不同声道的分配,可以从数据库获取数据,Flash MX的编程功能又有所增强;Macromedia公司为保持这个产品领先的竞争力,会不断地发展它的编程能力和其它能力;就国内的情况而言,短短三年的时间内,中学教师中用Flash制作课件的作者增加了不少,水平也有了明显的提高。本文作者也做了一些探索,这里介绍《矢量分解》和《光速的测量》两个微型教学软件的制作过程。文末附有源文件的网址。

《矢量分解》制作要点

打开《矢量分解》播放文件后,屏幕上呈现一个如图所示的平行四边形。原矢量的大小和方向,可以通过拖动位于原矢量末端的小图标来改变;分矢量的方向可以通过拖动位于分矢量上的小图标来改变。教师可以利用这个软件讲授力的分解、位移的分解、速度的分解、加速度的分解所遵循的定则,研究和例示涉及矢量分解的各种各样的问题。下面介绍用Flash 5制作《矢量分解》的过程,尤其是ActionScript代码的编写。(使用Flash 6或Flash MX,做法可以完全相同。)

一.创建符号

新建一个电影剪辑(Movie Clip),名称为“vector”,画一条线段,长度为100像素,水平,左端位于原点,右端配一个箭头,这个电影剪辑用以表示矢量。

新建一个电影剪辑,名称为“line”,画一条细线或虚线,长度为100像素,水平,左端位于原点,这个电影剪辑用以表示平行于分矢量的辅助线。

新建一个名称为“noAnswer”的“电影剪辑”:文字为“无解”。新建一个名称为“infinity”的电影剪辑,文字为“有无数的解”。

新建一个按扭(Button),其图形几何中心位于原点。新建一个名称为“aControl”的电影剪辑,其中引入上述按扭(将来在按扭上附上适当的ActionScript代码:指向按扭按住左键(press)之时,该电影剪辑开始移动,松开左键(release)之时,该电影剪辑停止移动)。再新建三个这样的内含按扭的电影剪辑,名称分别为“bControl”,“cControl”,“mainControl”。这四个可以跟随鼠标移动的电影剪辑用以确定第一分矢量的方向,第二分矢量的方向,原矢量末端的位置,平行四边形的位置。上述按扭,其第一帧上的图形(比如圆),尺寸应该很小,甚至为零,其第二帧至第四帧上的图形尺寸应该大一些。

新建一个名称为“empty”的电影剪辑,内部为空白。新建一个名称为“obsever”的电影剪辑,两帧(第二帧为普通帧),引入“empty”(将来在这个“empty”实例上捆绑比较长的一段ActionScript代码)。

新建一个名称为“main”的电影剪辑,内部暂时为空白。

二.组装

打开场景1(scene1),引入“main”,实例名与符号名相同。

打开符号库。

打开“main”。向“main”内引入一些符号:三个“vector”,实例名为“a”,“b”,“c”,它们的“中心”(左端)均位于原点,“vector”的这些实例用以表示第一个分矢量,第二个分矢量,原矢量;两个“line”,实例名为“u”,“v”,用以表示分矢量“a”,“b”的两条对边;“aControl”,“bControl”,“cControl”,“mainControl”各一个,实例名与符号名相同;“noAnswer”,“infinity”各一个,实例名与符号名相同;一个“obsever”。

三.编写ActionScript代码

“mainControl”内部的按扭实例,捆绑以下ActionScript代码:

on (press) {

_root.main.startDrag(true);

}

on (release) {

stopDrag ();

}

这段代码的功能是,鼠标指针指向“mainControl”按住左键,可以拖动“main”,松开左键,停止拖动。圆括号内的参数true表示拖动的对象“main”的中心位于鼠标指针末端。

“aControl”内部的按扭实例,捆绑以下ActionScript代码:

on (press) {

_root.main.aControl.startDrag(true);

}

on (release) {

stopDrag ();

}

这段代码的功能是,鼠标指向“aControl”时,按住左键,可以拖动“aControl”,松开左键,停止拖动。“bControl”和“cControl”内部的按扭实例捆绑类似的ActionScript代码。

“obsever”在这个微型软件中的作用,相当于乐队的指挥在整个乐队的表演中的作用。它内部的ActionScript代码负责指定有向线段c的末端随“cControl”的移动而移动,“a”的方向随“aControl”的移动而改变,“b”的方向随“bControl”的移动而改变,等等。

“obsever”内部捆绑在“empty”上的ActionScript代码,必须反复执行,为此把代码的主体部分放在以下代码的花括号内。

onClipEvent (enterFrame) { }

“empty”这个电影剪辑每次进入下一帧时,都会执行这个花括号内的代码。

代码的主体部分如下,双斜杠后面的文字是注释。

cx = _root.main.cControl._x;

cy = _root.main.cControl._y;

cr = Math.sqrt(cx*cx+cy*cy);

//变量cx和cy纪录“cControl”的位置信息,变量cr纪录“cControl”离原点的距离。

cRad = Math.atan2(cy, cx);

cDegree = cRad*180/Math.PI;

//cRad纪录连结“cControl”与原点的线段的角度(跟x轴正方向的夹角),以弧度为单位;cDegree也是纪录连结“cControl”与原点的线段的角度,以度为单位。

_root.main.c._xscale = cr;

//设置原矢量“c”的长度等于cr。

_root.main.c._rotation = cDegree;

//设置原矢量“c”的角度等于cDegree。

aRad = Math.atan2(_root.main.aControl._y, _root.main.aControl._x);

aDegree = aRad*180/Math.PI;

//aRad纪录连结“aControl”与原点的线段的角度。第一分矢量“a”的角度应该等于aDegree。

bRad = Math.atan2(_root.main.bControl._y, _root.main.bControl._x);

bDegree = bRad*180/Math.PI;

acDegree = Math.abs(aDegree-cDegree);

//acDegree纪录“a”,“c”之间的夹角,取绝对值。

bcDegree = Math.abs(bDegree-cDegree);

abDegree = Math.abs(aDegree-bDegree);

acDegreeMin = Math.min(acDegree, 360-acDegree);

//acDegreeMin纪录“a”,“c”之间的夹角,取[0,180]范围内的数值。

bcDegreeMin = Math.min(bcDegree, 360-bcDegree);

abDegreeMin = Math.min(abDegree, 360-abDegree);

//以下把“noAnswer”等电影剪辑的“可见性”设置为“false”(不可见)。

_root.noAnswer._visible = false;

_root.infinity._visible = false;

_root.main.a._visible = false;

_root.main.b._visible = false;

_root.main.u._visible = false;

_root.main.v._visible = false;

//以下挑出两种无解情况,让读者看得见“noAnswer”。

if (acDegreeMin+bcDegreeMin>abDegreeMin+0.1) {

_root.noAnswer._visible = true;}

else if (Math.abs(aDegree-bDegree) == 180 && Math.abs(aDegree-cDegree)*Math.abs(bDegree-cDegree) != 0) {

_root.noAnswer._visible = true;}

//以下挑出两种有无数解的情况,让读者看得见“infinity”。

else if (Math.abs(aDegree-bDegree) == 180 && Math.abs(aDegree-cDegree)*Math.abs(bDegree-cDegree) == 0) {

_root.infinity._visible = true;}

else if (Math.abs(aDegree-cDegree) == 0 && Math.abs(bDegree-cDegree) == 0) {

_root.infinity._visible = true;}

//以下比较长的一段代码,针对普通情况。

else {

_root.main.a._visible = true;

_root.main.b._visible = true;

_root.main.u._visible = true;

_root.main.v._visible = true;

//设置“a”,“b”,“u”,“v”可见。

ar = cr*Math.sin(bRad-cRad)/Math.sin(Math.PI-bRad+aRad);

//利用正弦定理计算第一分矢量a的长度应取的数值ar。

_root.main.a._rotation = aDegree;

//让“a”的角度等于aDegree。

_root.main.a._xscale = ar;

//让“a”的长度等于ar。

_root.main.a._yscale = 100;

//让“a”的粗细保持原来的数值。

br = cr*Math.sin(aRad-cRad)/Math.sin(Math.PI-aRad+bRad);

_root.main.b._rotation = bDegree;

_root.main.b._xscale = br;

_root.main.b._yscale = 100;

_root.main.u._rotation = aDegree;

_root.main.u._xscale = ar;

_root.main.v._rotation = bDegree;

_root.main.v._xscale = br;

//下面的ax,ay纪录分矢量“a”的末端的位置信息,它们确定“v”的位置;bx,by纪录分矢量“b”的末端的位置信息,它们确定“u”的位置。

ax = ar*Math.cos(aRad);

ay = ar*Math.sin(aRad);

bx = br*Math.cos(bRad);

by = br*Math.sin(bRad);

_root.main.u._x = bx;

_root.main.u._y = by;

_root.main.v._x = ax;

_root.main.v._y = ay;}

//以下针对两个矢量接近重合的情况.让一个矢量的粗细减为原来的50%。

if (acDegreeMin<3) {

_root.main.a._yscale = 50;}

if (bcDegreeMin<3) {

_root.main.b._yscale = 50;}

四.润饰

可以通过有关面板确定各有向线段的颜色,用不同颜色的有向线段表示不同的矢量。可以通过有关面板确定“main”中各实例的参数,使得初始画面符合作者的期望;要达到这个目标,也可以在场景1第一帧放置适当的ActionScript代码,把“obsever”中引用的变量的初值做适当的设置。可以做一个电影剪辑,画上辐射状的一系列线段,作为调整和观察角度的基准,这个电影剪辑的可见性可以让读者选择。可以在“main”中放置几个“输入文本框”(input textbox),以便读者指定数据以及向读者显示数据。像前面叙述的那样用一个包含线段和箭头的电影剪辑表示一个矢量是方便的,但如果箭头画得长一些,当放大率比较大时,箭头会过分大,比较难看,如果箭头画得短一些,当放大率比较小时,箭头会过分小。为了解决这个问题,可以用一个包含线段的电影剪辑和一个包含箭头的电影剪辑共同表示一个矢量,两个电影剪辑的放大倍数(_xscale或_yscale)可以独立控制。

《光速的测量》制作要点

《光速的测量》帮助文件开始部分,对软件的功能做了一个说明:

在美国物理学家迈克耳逊1926年所做的这个著名的实验中,八面镜与远处凹面镜之间的距离为22英里(35.4千米)。这个距离超过八面镜尺寸的50000倍,在动画中无法按比例来表现。八面镜以读者指定的转速旋转;不过这里显示的是“慢镜头”:这里1秒钟大约表示真实时间(1/22000)秒。这里显示的光的运动当然也是“慢镜头”,而且是更慢的“慢镜头”。八面镜静止不转时,从望远镜能看到回来的光;读者可以亲手“试验”,看看把八面镜的转速增加到多大时,从望远镜能间断地看到回来的光。

下面介绍《光速的测量》制作过程,特别是ActionScript的编写。

一.制作符号

新建一个图片(graphic),命名为“mirror”,其中画边长为20像素的八面镜的截面图。

新建一个图片,命名为“concaveAndPlane”,其中画左右彼此相对的凹面镜和平面镜,两者相距55像素。凹面镜高38像素,平面镜高34像素。

新建一个图片,命名为“telescope”,其中画一只竖直的望远镜。

新建一个图片,命名为“photon”,在原点附近画一条长度为8像素的水平短线,表示光粒子(或者一份光波)。

新建一个电影剪辑(movie clip),命名为“lampHouse”,其中作一个简单动画(比如半径变化着的圆),表示一个正在发光的光源。

新建一个电影剪辑,命名为“mirrorRotation”。首帧为关键帧,引入一个“mirror”,放在(0,0)位置,其实例名也定为“mirror”。第2帧为普通帧。在“mirror”上捆绑一段代码,指定八面镜的角度(_rotation)按读者指定的转动频率而变化。

新建一个电影剪辑,命名为“lightEarly”。1-5帧,表现光粒子从光源竖直向下运动到八面镜。6-40帧,表现光粒子从八面镜向左边(水平或倾斜)运动。第6帧为关键帧,引入一个“photon”,放在(0,0)位置,其实例名也定为“photon”。7-40帧为普通帧。在“photon”上捆绑一段代码,指定光粒子根据反射时八面镜的角度,沿适当方向运动。

新建一个电影剪辑,命名为“lightLater”,1-6帧,表现光粒子从平面镜上端水平向左运动到凹面镜,从凹面镜运动到平面镜,从平面镜运动到凹面镜。7-35帧,表现光粒子从凹面镜水平向右运动到八面镜。36-42帧,表现光粒子从凹面镜向下方(竖直或倾斜)运动。第36帧为关键帧,引入一个“photon”,放在(0,0)位置,其实例名也定为“photon”。37-42帧为普通帧。在“photon”上捆绑一段代码,指定光粒子根据反射时八面镜的角度,沿适当方向运动。

新建一个电影剪辑,命名为“light”,三层。第一层首帧为关键帧,引入一个“lightEarly”;2-40帧为普通帧。第二层第34帧为关键帧,引入“lightLater”;35-75帧为普通帧。在第三层第34帧放置一段代码,设置“如光粒子第一次反射后几乎水平向左运动,则从此帧开始“lightEarly”不可见;否则,从此帧开始 “lightLater”不可见。

新建一个电影剪辑,命名为“lights”。首帧为关键帧,第2帧为普通帧。在首帧放置一段代码:指定复制一个“light”。运行时每次执行该帧,将新复制一个“light”。如此表现光源不断发光。

新建一个电影剪辑,命名为“main”,三层,内容暂为空白。

二.组装

打开场景1,引入电影剪辑“main”(待以后调整位置)。实例名为“main”。放置一个输入文本框(input textbox),其变量名为w。这个文本框用以让用户输入一个数值,作为八面镜的转动频率。

打开电影剪辑“mian”,从符号库引入若干符号。第一层右上方放置“lampHouse”。其下方放置“mirrorRotation”,实例名为“mirrorRotation”。“mirrorRotation”下方放置望远镜“telescope”。“mirrorRotation”上的A点应位于“lampHouse”正下方100像素处。望远镜上端应位于A点正下方144像素处。左边放置“concaveAndPlane”,其凹面镜上端位于A点左边605像素处。第二层右上方放置 “light”,其实例名也定为“light”。 “light”的中心点跟“lampHouse”的中心点重合。第三层放置“lights”,位置任意。

三.编写ActionScript代码

双斜杠后面的文字是注释。

1.捆绑在电影剪辑实例_root.main.mirrorRotation.mirror上的代码。

onClipEvent (load) {q1 = 0;}

//装载(load)的时候,本地变量q1取0.以后可以看出,q1是八面镜相对初始位置旋转的角度。

//每次进入(enter)新的一帧(frame),都执行下面的代码。

onClipEvent (enterFrame) {

w = _root.w;

if (isNaN(w) == true) {w = 0;}

//本地变量w获取场景1文本框内的内容(转动频率)。如果w不是数值,w取0。

q1 = q1-1/12*w*360/22225;

//w为正值时,八面镜逆时针旋转,八面镜的角度减小(跟解析几何中的规定不同),q1减小。其中(1/12)是推进一帧所用的时间。22225这个除数是额外加上去的,从而读者看到的是慢镜头。

this._rotation = q1;

//八面镜的角度取q1。

q = q1-45*Math.round(q1/45);}

//Math.round()根据四舍五入法则取整数。q的取值范围是[-22.5,22.5].八面镜当时接受入射光的那个面相对当初接受入射光的那个面当初的位置转过的角度等于这个数值。后面会用到这个数值。

2.捆绑在电影剪辑实例_root.main.light.lightEarly上的代码。

onClipEvent (load) {

q = _root.main.mirrorRotate.mirror.q;

//本地变量q等于前一段代码中的q。

x1 = 20*Math.cos((180+2*q)*Math.PI/180);

y1 = 20*Math.sin((180+2*q)*Math.PI/180);

//Math.PI是圆周率常数。(180+2*q)是反射后光粒子前进的方向。设每推进一帧光粒子前进20像素。x1和y1是每推进一帧光粒子横坐标和纵坐标的变化量。

this._rotation = 180+2*q;}

onClipEvent (enterFrame) {

this._x = this._x+x1;

this._y = this._y+y1;}

3.捆绑在电影剪辑实例_root.main.light.lightLater上的代码。

onClipEvent(load){

q=_root.main.mirrorRotate.mirror.q

x1 = 20*Math.cos((90+2*q)*Math.PI/180);

y1 = 20*Math.sin((90+2*q)*Math.PI/180);

this._rotation=90+2*q}

onClipEvent (enterFrame) {

this._x = this._x+x1

this._y = this._y+y1}

4.电影剪辑light第三层第34帧代码.

w = _root.w;

if (isNaN(w) == true) {w = 0;}

d = Math.abs(w)/1800+0.15;

//光粒子反射时的q取0附近区域(-d,d)内的数值时,光能够到达凹面镜。初步确定d的值。由于动画设计上的局限,需要有点不合逻辑的这样一个式子。

d = Math.min(d, 0.5);

//进一步确定d的数值,d不至于超过0.5.

q = this.lightEarly.photon.q;

//q取光粒子向下运动射到八面镜时八面镜的q。

if (Math.abs(q)>=d) {

this.lightLater._visible = false;}

//q取值超出(-d,d)时,不显示lightLater。

if (Math.abs(q+0.89)<0.89+d) {

this.lightEarly._visible = false;}

//从左边过来的光粒子射到平面镜后,不再显示。

5.电影剪辑lights第1帧代码。

n = n+1;

duplicateMovieClip ("_root.main.light", "l"+n, n);

//第一次运行这一帧时,把_root.main.light复制一份,命名为l1;第二次运行这一帧时,把_root.main.light复制一份,命名为l2;等等。

if (n>38) { n = 0;}

//如此,第39,40,41…个复制品,将覆盖第1,2,3…个复制品,以免陈旧的复制品无益地占用内存。

把八面镜的转速调到约529转/秒的时候,射向望远镜的光实际上是一闪一闪的,但闪烁得太快了,人眼看起来,光是稳定的。在软件中八面镜的转速调到约1058转/秒的时候,同样可以从望远镜看到光;但当时的实验中是不会发生这样的事情的,530转/秒可能已经接近当时技术上的极限了。

2003年1月