Qomolangma OpenProject v1.0

类别    :Rich Web Client

关键词  :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,

          DOM,DTHML,CSS,JavaScript,JScript


================================================================================

一、框架库:时间线与时间处理器

~~~~~~~~~~~~~~~~~~

几乎所有的动画特效都与时间线有关系。在一般的应用软件里,会提供一个固定间隔的时间

线,设计人员则在时间线上描述指定的时间里会发生的事件。这些事件被连续起来,就成为

了动画;而一组时间线合并起来,就成了动画场景。

Qomo里实现时间线的初衷,只是为了在绘制界面组件的效果时,提供一些用时间控制的效果。

例如窗体关闭/隐藏时的卷入效果(以及在打开时的展开);又例如一个Outlook风格的纵向Bar

在点击一个按钮时的展开。

在Windows32和vista风格的界面设计中,这些组件都具有一些特殊的动画效果。例如窗体弹

出或控件的淡入淡出。这些动态效果其实都是在时间线的基础上来实现的。

但这些仍然只是单一组件,或一类组件的单一特效。这种情况下,只能算是“动态效果”。

复杂的设计是“一组时间线+一组元素”,这种情况通常表现为动画场景。

动画场景有些时候并不用“时间线”来控制,而是有“帧”来控制。多画面的帧切换也就构

成了动画。所以,在每一个帧事件中的处理,通常就是“效果渲染”。当然,实际来做的时

候会更加复杂,涉及到非常多的演染技术。但如今动画制作领域的关键技术,就是这里提到

的时间线与帧。

Qomo框架库同时实现时间线与帧,但对二者不提供有偏好的推荐。Qomo中,二者的区别表现

在:时间线并不精确,而且并不按标准时间间隔提供;帧是纯序列化的演染空间和数据供应,

但与时间并不精确的重叠。

这一切根源在于IE等浏览器中并没有足够精度的时钟或日期对象。所以如果你真的打算用JS

或Qomo来做过于复杂的、涉及时间线或帧序列的动画场景,那可能效果并不会如你想象的平

滑。

二、时间处理体系的设计

~~~~~~~~~~~~~~~~~~

Qomo把一个动画效果理解为“时间变化”和“数据变化”两个部分。Qomo认为,无论时间变

化还是数据变化,都可以导致动画效果的产生。

例如在窗口弹出效果中,一方面可以是“每单位时间窗口变大的比例(data)成曲线”,另一

方面也可以是“窗口每变大单位比例所用的时间(time)成曲线”,两者之任一,都可以产生

动态效果,但代码却不一样:

------------------

function resize_win(v) {

  win.width = win.width * v;

  win.height = win.height * v;

}

// 第一种

var data = 1;

setInterval("resize_win(data=data*1.2))", 10);

// 第二种

var time = 1000;

setTimeout(function() {

  resize_win(1.2);

  setTimeout(arguments.callee, time=time*0.8);

}, time=time*0.8);

------------------

Qomo也认可时间与数据同时发生的变化,也就是“非固定间隔的时间线”下的数据变化。尽

管在实用中这种变化很难处理,但可以产生独特的效果。

Qomo把具备理解这种逻辑的能力对象称为“时间处理器(TimeMachine)”。这个类接口描述为:

------------------

ITimeMachine = function() {

  this.start = Abstract;    // function(time, data) {}

  this.OnTimer = Abstract;  // function(step, data) {}

  this.stop = Abstract;

}

------------------

其中,ITimeMachine.start()方法的入口有time与data。它们不是单纯的值,而一个TSteper

类型的对象。这种对象用于产生每个单位间隔可释出的值(新的数据,或者新的时间间隔)。

三者的关系主要建立在OnTimer事件的step参数上。因为TSteper类的事件OnStep的类型声明为

------------------

TOnStep = function(nStep, nLast) {}

------------------

其中nStep表明第几步,nLast表明上一步产生的数据,作为此次产生数据使用的参考。

因此,整个Qomo的时间处理体系表达的逻辑就是:

 - TimeMachine.OnTime,time.OnStep和data.OnStep在相同的step值时产生一组数据;

 - 在相同的step下,OnTime针对于data.OnStep产生的数据data所进行的处理;

 - TimeMachine通过time.OnStep来得到下一次发生处理的延时。

 所以,Qomo在时间处理体系上的类继承图设计如下:

---------------


(images/timer_architectur.jpg)

---------------

其中TTimer是对window.setInterval和window.setTimeout的一个封装;TTimeline派生自

TTimeMachine,用于简化“固定间隔的时间线”的处理。TYuiSteper继承自TSteper,它的

部分代码来自于Yahoo UI开源项目。其中有一个名为TStepTrigger类,它的作用是在每次

step时计算数据/时间值的增量,是一个工具类,也来自于对Yahoo UI中相同功能的封装。

三、测试代码及分析(1)

~~~~~~~~~~~~~~~~~~

Qomo最初打算按Yahoo UI中的动画效果演示来做一个DEMO,因此这个DEMO最初的效果,只

是在屏幕上的一个点,从位置A飞行到位置B。

飞行过程有一点要求:

  - 飞行中速度是可控的,便如渐快/渐慢,或快-慢-快这样的变化;

我们说过,速度可以表现为单位时间内的数据(飞行长度)变化,也可以表现为单位数据所

耗的时间不同。在示例中,我们使用前者,也就是“单位时间”。那么显然,我们可以使

用一个TTimeline组件来控制整个过程。

所以基本的代码框架就是:

------------------

<body>

<div id=dot style="font-size:0; width:10px; height:10px; background:red; position:absolute"></div>

</body>

<script>

// 0. 初始数据

var el = document.getElementById('dot');

var x0 = el.offsetLeft, x1 = 400;

// 1. 构造一个时钟及其处理程序

var doFly = function(step, data) {

  var sty = this.get('TimerData').style;

  sty.left = data;

}

var T2 = TTimeline.Create(doFly);

T2.set('TimerData', el);

// 2. 构建一个数据发生器, 用于向时钟提供数据

provide = TYuiSteper.Create();

provide.set('From', x0);

provide.set('To', x1);

// 缺省值

// provide.set('Frames', 100);

// provide.set('Easing', 'easeOut');

// 4. 启动时钟

T2.start(provide, 1);

</script>

------------------

这个示例的完整代码参见(DOCUMENTs/TestCase/T_TimeLine2.html)。


运行效果参见(DOCUMENTs/TestCase/images/T_TimeLine2.jpg)。

我们看到,数据发生器提供的一组参数的含义,就是“用100次的周期,产生x0~x1之间的平

滑变化的数据,采用的数据变化的算法为easeOut”。

这个数据发生器被作为Timeline的参数传入:

------------------

T2.start(provide, 1);

------------------

表明数据使用provide提供的值,而时间采用1ms为间隔的时间线。

那么,在“100次的周期”中,时间处理器(T2)的变化是什么呢?这在T2被创建的时候就声明

过了:

------------------

var T2 = TTimeline.Create(doFly);

T2.set('TimerData', el);

------------------

创建时的这行代码与下面的代码是相同的:

------------------

var T2 = new Timeline(doFly);

var T2 = TTimeline.Create();

T2.OnTimer.add(doFly);

------------------

而后我们初始化了TimerData属性的值,它表明这个时间关注的数据对象是el。

在每次时间处理器被激活时,我们看到的doFly操作是这样:

------------------

var doFly = function(step, data) {

  var sty = this.get('TimerData').style;

  sty.left = data;

}

------------------

我们从TimerData属性中取出el元素,并修改el.style.left,就完成了飞行动画。

我们在T2.start()之前,可以调整一些数据发生器的参数,就可以改变这个飞行动画的效果。

这些参数包括:

------------------

this.set('Easing', 'easeOut'); // 数据产生的方法

this.set('Frames', 100); // 帧数,控制step的总数

this.set('Fps', 200);   // 帧速率, qomo beta2中未实现.

------------------

其中Easing的取值参考StepTrigger.js中TStepTrigger类的方法,目前包括:

------------------

easeNone

easeIn

easeOut

easeBoth

backIn

backOut

backBoth

------------------

四、测试代码及分析(2)

~~~~~~~~~~~~~~~~~~

接下来,对飞行过程又增加了一点要求:

  - 飞行的路线是可控的,而不是单纯的A-B的直线。

路线可控是通过“控制点”来实现的。贝赛尔曲线的特点是“在两点之间增加一个控制点,

即可以形成贝赛尔曲线”。用来做“飞行路线”,那么即是:无论在两点之间增加多少个

控制点,则通过贝赛尔曲线的连结,最终可以从A点飞行到B点。

这与上面的示例还有一点明显不一致的地方:飞行的坐标是x,y同时发生变化,而非单一

地在x方向上平移。所以这个需求其实包含了两个技术要点:

  - 处理器(TTimeline)与提供者(TYuiSteper)都需要能处理两个以上的数据

  - 提供者能够有更复杂的运算能力

但是,需要留意的是,这个需求的基本逻辑并没有变化:要求一个文档对象(element)能

做飞行的动态效果。因此我们在上面的代码框架上做一些修改:

------------------

// 0. 初始数据

var el = document.getElementById('dot');

var fromPoint = [el.offsetLeft, el.offsetTop];

// 1. 构造一个时钟及其处理程序

var doFly = function(step, data) {

  var sty = this.get('TimerData').style;

  sty.left = data[0];

  sty.right = data[1];

}

var T2 = TTimeline.Create(doFly);

T2.set('TimerData', el);

// 2. 构建一个数据发生器, 用于向时钟提供数据

provide = TYuiSteper.Create();

provide.set('Points', [

 fromPoint,    // from: x0, y0

 [400, 400]    // to:   x1, y1

]);

// 5. 启动时钟

T2.start(provide, 1);

------------------

这个示例的完整代码参见(DOCUMENTs/TestCase/T_TimeLine3.html)。

我们看到,基本上我们只重新约定了doFly与provide交互的数据格式(从原来的单一值,

变成数组表示的坐标点),然后我们就完成了主要代码。

但是我们前面说过,路线可控是通过贝赛尔曲线来实现的。当贝赛尔曲线只有起始点与

结束点,而没有中间控制点时,其实将绘制为一条直线。也就是说,上例的实飞行路线

的效果是从fromPoint到[400, 400]的一条直线。

但我们只需要调整控制点,即可以完成“可控的飞行路线”。例如:

------------------

provide.set('Points', [

 fromPoint,    // from: x0, y0

 [200, 180],

 [500, 600],

 [200, 400],

 [1024, 200],

 [100,320],

 [400, 400]    // to:   x1, y1

]);

------------------

我们注意到一点事实:我们并没有修改任何屏幕表现的算法,也没有修改代码框架,就

实现了UI上的表达效果的可控。这其实是“数据提供”与“数据表现”分离所带来的效

果。在现代的UI设计上,数据表现与数据提供,以及业务逻辑三者的分离,是一个非常

关键的话题。

五、测试代码及分析(3)

~


例中都用了AOP。

两个代码中只有极少的不同,主要差异还是在于data提供的格式不一致。我们以T_Time-

Line3.html为例:

------------------

// 3. 使用切面来观察绘制过程

var asp_OnTimer = new ObjectAspect(T2, 'OnTimer', 'Event', fromPoint); //push a meta_data

asp_OnTimer.OnAfter.add(function(o, n, p, a, v) {

  var data = a[1], pt = this.get('MetaData')[0];

  drawLine(pt[0], pt[1], data[0], data[1], 'red', 1, 0);

  this.set('MetaData', [data]);

  $debug(a[0], ':', data);

});

------------------

运行效果参见(DOCUMENTs/TestCase/images/T_TimeLine3_1.jpg)。


这里用到了一个以前在讲AOP时未详述的meta_data。所谓meta_data,是与一个切面相关

的数据,它应当在切面进入时通过aspect自身可访问到。它是切面方法执行过程中的参

考数据。

这个aspect需要一个起点,来做轨迹绘制的第一个坐标。这个起点就是fromPoint。它在切

面创建时被传入。所以"MetaData"属性的值。——MetaData是一个数组,所以fromPoint实

际上是该属性值的第一个元素。

所以我们看到了切面的OnAfter中添加了一个事件处理函数。其中:

------------------

var data = a[1], pt = this.get('MetaData')[0];

// ...

this.set('MetaData', [data]);

------------------

data是当前的“被观察系统”正在处理的数据,而pt则是上一次的数据,它被不断更新着。

该事件的入口参数a,是被观察者T2.OnTimer调用时的参数。我们知道这个事件的声明是:

------------------

T2.OnTimer = function(step, data) { }

------------------

所以第一个参数就是data,这就是data=a[1]的由来。

最后,由于我们知道data与切面的元数据(MetaData[0])都是表示点的数据。所以下面这

行代码就是画线了:

------------------

drawLine(pt[0], pt[1], data[0], data[1], ...);

------------------

我们回顾一下前面的内容,由于OnTimer的调用与数据提供者有关。也就是说,provide提

供多少数据,则界面上显示多少数据。——而这个“多少数据”是由TSteper中的step值来

控制的。也就是OnTimer中的第一个参数,或说是OnStep中的第一个参数。因此,在切面中,

我们可以通过下面的代码显示出数据的变化:

------------------

$debug(a[0], ':', data);

------------------

在这个示例中,如果我们可以改变provide的一些属性,那么就可以在界面上看到这些变化

了。例如缺省情况下,Frames值为100帧,所以界面上显示了100条数据。但如果改成10帧,

那么显示数据也减少了,而曲线也就不足够平滑了:

------------------

// 2. 构建一个数据发生器, 用于向时钟提供数据

provide = TYuiSteper.Create();

provide.set('Frames', 10);

------------------

运行效果参见(DOCUMENTs/TestCase/images/T_TimeLine3_2.jpg)。


而如果你修改时间线的间隔,也会发现显示效果不够平滑了。例如:

------------------

// 5. 启动时钟

T2.start(provide, 200);

------------------

六、测试代码及分析(4)

~~~~~~~~~~~~~~~~~~

这一小节的分析,我们只是简单地说一下这个测试代码的界面控制。

如果一个时间线被设计得过长,例如1000 * 1ms,那么我们就可能需要一个随时可以中断

的时间线。也就是说,时间不但能被start,也能被stop。因此,TTimer这个基类就设计了

一些基本的控制逻辑。包括:

------------------

TTimer.start()

  TTimer.OnStart

  TTimer.OnTimer

TTimer.stop()

  TTimer.OnStop

------------------

因此,为了测试这些逻辑,示例中也包括下面的一些代码:

------------------

// 4. 测试基类中的控制方法

T2.OnStart.add(function() {

  $debug(' -- timer start --');

});

T2.OnStop.add(function() {

  $debug(' -- timer stop --');

});

document.onclick = function() {

  T2.stop();

}

------------------


七、其它

~~~~~~~~~~~~~~~~~~

本文档中,我们讲述了Qomo的时间框架,也结合实例讲述了AOP的用法。

  1. 关于在界面上的图形绘制

  -----------

  大家在本文档中看到的图形都比较精细,但运行代码包中示例时看到的线条却比较粗糙。这是

因为文档中使用的drawLine()来自于一个更精细的图形代码包。我将在后续的版本发布中,公开

这个图形库。——目前在Qomo中有一个VML的图形库,还有在TestCase中用的drawLine.js。但这

些都不是Qomo最终的图形框架。

更新的图形框架在设计和实现中,并没有完成,故暂不公开。

  2. 关于兼容性问题

  -----------

  目前大家看到的示例代码在FireFox上不能运行。其实只有一个原因,就是FF里没有insertAdj-

acentHTML方法,因此也就不能用drawLine.js中的代码在界面上画图,以及用$debug来输出文字。

因此去掉相关的代码后,就可以运行示例了。

  在开发中的版本,已经解决过insertAdjacentHTML等问题,因此是可以run在firefox上的。但

这些代码放在一个还没有被pub出来的DOM兼容层上,因此也暂时不公开。

Qomo试图在DOM上很好的兼容Firefox等浏览器,但目前看起仍有较大的困难。:(

  3. 关于TTimer类

  -----------

  TTimer类是对window.setTimeout和window.setInterval的封装。它明显的特点是规范了这两个

方法的使用流程。——在Qomo中,二者的使用将没有明显的差异。

  由于TTimer.OnTimer事实上已经支持使用TSteper,因此TTimer能使用Steper.js中的各种类。这

个示例在T_Timeline.html中。

  TTimer类另一个最大的特点,是便得OnTimer中能够使用类方法,并正确地传入对象的this引用。

——而setTimeout与setInterval只能传入函数引用,不能传入方法。因此this引用总是指向window。

-----------

setTimeout(function() {

  alert(this === window);

}, 1000);

-----------

  4. Qomo时间框架上的一些TODO

  -----------

  事实上目前的Qomo时间框架并不完整。例如还没有处理Fps(帧速率),以及修正由IE的setTimeout()

精度带来的时间线不规则。——这其实需要在运算中做时间补偿。

  Qomo应该还有一个能表现“时间与数据同时变化”的效果的示例。

这些代码将在beta 3发布时统一提供。