65.9K
CodeProject 正在变化。 阅读更多。
Home

Pfz.AnimationManagement.js

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (20投票s)

2014年1月14日

CPOL

10分钟阅读

viewsIcon

24276

downloadIcon

544

用于 JavaScript 中交互式动画的流畅库。

查看运行中的示例

Cyberminds 57 示例。

碰撞检测 + 爆炸 示例。

背景

前段时间,我写了一篇题为《流畅和命令式动画》的文章,其中介绍了一个能够将命令式和声明式动画混合用于 .NET 的动画库。现在是时候为 JavaScript 提供该库的一个版本了。

对于认识我的人来说,考虑到我有多么热爱 C#,我写一篇关于 JavaScript 的文章可能看起来很奇怪。事实上,如果微软决定更多地支持 Silverlight,我可能会继续使用 Silverlight,但考虑到 Silverlight 已停产,甚至我的妻子也无法在她的电脑上看到动画网页,所以我决定使用跨平台的东西,那就是 JavaScript

为什么需要一个动画库?

在开始为 JavaScript 撰写文章之前,我搜索了一下,看看是否已经有为 JavaScript 制作的动画库。

在某些地方,我发现我们不需要动画库,因为 CSS 已经能够创建动画。在其他地方,我发现甚至 jQuery 也允许编写动画。

嗯,CSS 动画的问题在于它们只能是声明式的。无法将命令式代码作为动画的一部分,而尝试这样做的变通方法会使代码过于复杂。此外,许多特效仍然是“供应商特定的”,需要多种不同的写法才能支持不同的浏览器,否则效果会完全失败。我甚至发现了一个完全用 CSS 编写的小游戏,但它使用了太多“技巧”来避免 JavaScript 代码,以至于它不再适合设计师……但也并非程序员的代码。所以,在我看来,如果我们想要交互式动画,CSS 并不理想。

至于 jQuery,也许是我误解了,但它似乎只具有基本效果,因此远非编写游戏的理想选择。我将在这里介绍的库已经过游戏编写测试,即使游戏现在还不完整,嗯……大部分代码只是 C# 版本的简单移植,这也意味着如果你想学习一个单一的库来创建动画,你将能够重用 C# 和 JavaScript 中的大部分概念(甚至方法名称,除了大小写)。

此外,在 JavaScript 中制作动画的优势在于,与用 CSS 编写的动画相比,效果不太可能需要供应商特定的参数。

它是如何工作的?

概念上,一切都始于一个具有 reset()update() 函数的接口。

update() 函数将接收自上次调用以来经过的时间(以毫秒为单位)。

reset() 函数,嗯,它负责将实际动画重置为其原始值。

要有效地运行动画,只需调用 AnimationManager.add() 函数并传入动画对象即可。

即使这个原则看起来过于简单,它也极其强大,因为它可以以许多不同的方式进行装饰,并且它允许用这段代码创建交互式动画

AnimationBuilder.
beginParallel().
  beginLoop().
    beginPrematureEndCondition(function () { return _horizontalMovement <= 0; }).
      rangeBySpeed(function () { return _left; }, 700-64, 120, function (value) { _left = value; playerCharacter.style.left = value; }).
    endPrematureEndCondition().
  endLoop().
  beginLoop().
    beginPrematureEndCondition(function () { return _horizontalMovement >= 0; }).
      rangeBySpeed(function () { return _left; }, 0, 120, function (value) { _left = value; playerCharacter.style.left = value; }).
    endPrematureEndCondition().
  endLoop().
  beginLoop().
    beginPrematureEndCondition(function () { return _verticalMovement <= 0; }).
      rangeBySpeed(function () { return _top; }, 500-64, 120, function (value) { _top = value; playerCharacter.style.top = value; }).
    endPrematureEndCondition().
  endLoop().
  beginLoop().
    beginPrematureEndCondition(function () { return _verticalMovement >= 0; }).
      rangeBySpeed(function () { return _top; }, 0, 120, function (value) { _top = value; playerCharacter.style.top = value; }).
    endPrematureEndCondition().
  endLoop().
endParallel();

你可以在此处看到它运行。你可以使用光标键移动飞船。

事实上,我说它是一个“接口”是因为每个实现都可以用这些函数做任何它想做的事情。因此,为了使这个示例工作,正在发生的事情是

  • RangeBySpeedAnimation 接收 initialValue、finalValue、值在一秒内变化的量以及一个更新 UI 的函数。通过一个简单的公式,如果你请求一个动画在一秒内移动 100 像素,如果该函数在 530 毫秒后被调用,它将增加值 53。这将自然地补偿可能发生的减速,这比在每次“滴答”时使用固定的“position += something”要好得多;
  • LoopAnimation 是一个“装饰器”动画,它将持续将调用重定向到其内部动画,但当内部动画结束时,它会重置它;
  • PrematureEndCondition 是另一个“装饰器”。但在其情况下,它只会在结束条件不为真时调用内部动画。一旦条件为真,它就会停止其内部动画;
  • ParallelAnimationSequentialAnimation 是将所有部分“粘合”在一起的动画。顾名思义,一个并行运行动画,另一个按顺序运行。Sequence 动画最大的特点是它不关心一个动画需要多长时间。下一个动画只会在前一个动画结束时开始,无论是它之后一秒,还是三小时之后;
  • 正如你可能想象的,整个“表达式”都使用了这些动画,但它是用 Fluent API 编写的(这就是为什么名称与表达式中输入的不完全匹配的原因);
  • 还有其他装饰器可以用来加速或减速内部动画(这样你就可以要求一个已经写好的动画运行得慢一些,而无需重写所有时间),并且作为一个简单的接口,你可以通过简单地创建带有 update 和 reset 函数的对象来创建自己的动画/装饰器。

声明式 API

声明式 API 仅用于使事物看起来更具声明性,甚至将函数呈现在正确的位置。

例如,你可以在任何时刻创建一个 WaitAnimation,但如果将其放在 ParallelAnimation 中将毫无用处。现在尝试将其放在 SequentialAnimation 中,它就会有意义。

在 Fluent API 中,wait() 函数只存在于 SequentialAnimation(使用 beginSequence() 创建)中,而不存在于 ParallelAnimation 中。

声明式 API 的函数

声明式 API 的函数是

beginSequence()
endSequence()
创建序列动画,这是一种能够按顺序播放其他动画(内部动画)的动画。
beginParallel()
endParallel()
创建并行动画,这是一种同时播放所有内部动画的动画。
add(animationOrFunction) 将已创建的动画对象或动画函数添加到实际动画组或装饰器中。
如果将函数作为动画给出,则该函数将被调用,就像它是一个“更新”一样,并带有自上次调用以来经过的时间。该函数可以直接处理数据(如果应再次调用则返回 true,如果已结束则返回 false),或者它可以返回另一个要运行的动画,这样你就可以检查一些条件并有效地返回另一个特定的动画来运行。
range(initialValue, finalValue, duration, updateFunction) 创建一个动画,使其在特定持续时间(以毫秒为单位)内从初始值到最终值。每次动画“嘀嗒”时,它都会调用 updateFunction,并根据 initialValue、finalValue 和 duration 给出计算值。
重要的是要注意 initialValue、finalValue 或 duration 可以是实际返回这些值的函数。如果声明式表达式创建时值不存在,这将很有用。

此动画显示两个范围以不同的结束值但相同的持续时间并行运行。
rangeBySpeed(initialValue, finalValue, speed, updateFunction) 这与上一个类似,但不是为动画提供特定时间,而是提供一个“速度”,即值每秒变化的量。
当动画是交互式时,使用速度而不是固定时间是更可取的。此外,与 range() 函数一样,参数 initialValue、finalValue 和 speed 可以是调用时返回这些值的函数。

此动画显示两个 rangeBySpeed 动画以不同的结束值但相同的速度并行运行。
beginRunCondition(condition)
endRunCondition()
设置一个条件来运行单个内部动画(注意,内部动画可以是并行或序列动画,因此你可以间接播放许多动画)。动画开始播放后,条件是否改变并不重要,它将继续播放。
beginPrematureEndCondition(condition)
endPrematureEndCondition()
此函数设置一个条件以提前结束内部动画。这对于使玩家角色在松开按键时停止移动非常有用。
beginLoop()
endLoop()
此装饰器将在其内部动画一结束就重新启动它。
beginTimeMultiplier(factor)
endTimeMultiplier()
将给定内部动画时间间隔的值乘以给定因子,该因子可以是直接值,也可以是生成该值的函数。这有效地使动画运行得更快或更慢。
beginPauseCondition(condition)
endPauseCondition()
每次“嘀嗒”时都会检查条件。如果条件为真,则不执行内部动画。这有效地在条件为真时暂停内部动画。
beginSegmentedTime(interval, segmentCompleted)
endSegmentedTime()
无论是否出现整个一秒的减速,分段动画都会使用给定间隔作为其最大间隔来更新其内部动画,如果需要,可以有效地多次播放内部动画。这很有用,例如,如果你想“每帧”应用碰撞检测,但使用声明式(基于时间)动画。
可选的 segmentCompleted 函数在每个分段完成时被调用(因此如果时间流逝小于分段大小则不调用),这为你提供了应用碰撞检测的正确时机,例如。
wait(duration) 等待直到给定时间(以毫秒为单位)过去。持续时间可以是一个函数,该函数在每次调用时可能返回不同的等待时间。此函数仅在序列内部可用。
waitCondition(condition) 等待直到条件函数返回 true。与 wait() 函数一样,它仅在序列内部可用。

创建自己的动画片段

如前所述,这个库仅基于 update()reset() 函数。通过创建一个包含这两个函数的新对象,其中 update 接收自上次调用以来经过的毫秒数,你可以创建自己的动画片段。NumericRangeAnimation 是最基本的示例,但你可以创建一个动画来动画颜色或使用非线性结果,例如。

但我想解释 NumericRangeAnimation,这样你就可以有一个起点

function NumericRangeAnimation(initialValue, finalValue, duration, updateFunction) {
    this._beforeStart = true;
    this.initialValue = initialValue;
    this.finalValue = finalValue;
    this.duration = duration;
    this.updateFunction = updateFunction;
};
// This constructor will simply store all the given parameters and set a value
// telling that it was not started yet.

NumericRangeAnimation.prototype.reset = function () {
    this._beforeStart = true;
}
// Well, the reset only says that it was not started yet, so the
// update will start it.

NumericRangeAnimation.prototype.update = function (elapsed) {
    // as you can see, if the animation was not started yet, it will
    // be started now.
    if (this._beforeStart) {
        this._beforeStart = false;
        this._total = 0;

        // the AnimationManager._getValue is to be considered a kind of
        // "internal method". It is capable of evaluating a function to 
        // get its result or it simply returns the value directly.
        // evaluating the function only when the animation is starting
        // is very important for this animation to work properly, as
        // we don't want a random function (for example) to be evaluated
        // at every frame.
        this._initialValue = AnimationManager._getValue(this.initialValue);
        this._finalValue = AnimationManager._getValue(this.finalValue);
        this._duration = AnimationManager._getValue(this.duration);
    }

    // here, if the total time is greator or equal the
    // duration, we update the animation with the latest value
    // and return false, telling that the animation ended.
    this._total += elapsed;
    if (this._total >= this._duration) {
        this.updateFunction(this._finalValue);
        return false;
    }

    // here we do the actual range calculation, call the update function
    // so the UI element can be updated and we return true, telling
    // that the animation is not finished yet.
    var remaining = this._duration - this._total;
    var value = ((this._finalValue * this._total) + (this._initialValue * remaining)) / this._duration;
    this.updateFunction(value);
    return true;
};

我希望你通过我在这里的评论理解这个动画片段。但这是非流畅 API。为了使这样的动画可用于流畅 API,我们必须在流畅 API 中“注册”一个新函数,这将影响许多 begin/end 调用的结果。为此,我们使用 AnimationBuilder.registerAnimationBuilderModifier 函数,该函数接收一个函数来更改所有现有动画构建器类型的原型。

作为一个例子,如果 range() 函数没有注册,我们可以用这个来注册它

AnimationBuilder.registerAnimationBuilderModifier(function (prototype) {
  prototype.range = function (initialValue, finalValue, duration, updateFunction) {
    return this.add(new NumericRangeAnimation(initialValue, finalValue, duration, updateFunction));
  };
});

这样,range 动画就可以在 AnimationBuilder 已经存在的片段中使用了。

JavaScript 和 C# 库

我在 C# 中构建了更好的示例,所以,如果你愿意,你可以查看 C# 示例,看看这个库有什么潜力。

C# 代码实际上有更多的动画片段,并且它可以在序列中的两个片段之间纠正经过的时间(如果一个动画将在接下来的 10 毫秒内结束,而下一个时间间隔是 15 毫秒,则 5 毫秒会进入下一个动画)。嗯,我简化了 javascript 版本,所以它不处理这个问题。

但我仍然认为这个库非常强大,特别是它允许创建没有特定持续时间的交互式动画。如果你查看 C# 库,需要记住一些重要的事情是

  • JavaScript 版本默认使用毫秒作为计时单位,而 C# 版本使用秒或实际的 TimeSpan
  • C# 中的名称与 JavaScript 中的名称使用不同的首字母大写规则;
  • 实际上,JavaScript 版本支持更多的函数参数。在 C# 中,只有一些重载支持委托(是的,从这个意义上说,JavaScript 版本更好);
  • 由于 C# 是一种带有泛型的强类型语言,C# 的 Range 实际上可以处理整数、双精度浮点数、颜色以及任何类型的范围,只要你注册它。JavaScript 版本需要为每种必须支持的不同类型提供一个新函数(如 pointRange)。
© . All rights reserved.