Canvas 2D Web 应用程序/动画
本章介绍如何使用关键帧动画与 cui2d。具体来说,它展示了如何根据预定义的关键帧数组来更改数值变量。如果更改的变量在 process 函数中用作坐标、颜色等,则生成的图形将看起来像是动画。当然,还有其他方法可以使用画布元素来实现动画,但这里介绍的方法具有一些优点,而且非常灵活。
本章的示例(可以在线获取 在线;也可以作为 可下载版本)扩展了关于 响应式按钮 的章节的示例。因此,以下部分只讨论代码中与动画相关的部分;有关其他部分的讨论,请参阅关于 响应式按钮 的章节和前面的章节。
在示例中,使用三个按钮来启动三种不同的动画。如果再次点击,第一个按钮只是重新启动动画(即使它已经在播放)。第二个按钮如果动画已经在播放,就不会重新启动动画。最后,第三个按钮在动画播放时处于非活动状态,并且这种状态也通过半透明地绘制它来视觉地传达,从而实现了“灰显”的效果。
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no">
<script src="cui2d.js"></script>
<script>
function init() {
// get images
imageNormalButton.src = "normal.png";
imageNormalButton.onload = cuiRepaint;
imageFocusedButton.src = "selected.png";
imageFocusedButton.onload = cuiRepaint;
imagePressedButton.src = "depressed.png";
imagePressedButton.onload = cuiRepaint;
imageAlien.src = "alien_smiley.png";
imageAlien.onload = cuiRepaint;
// set defaults for all pages
cuiBackgroundFillStyle = "#000000";
cuiDefaultFont = "bold 20px Helvetica, sans-serif";
cuiDefaultFillStyle = "#FFFFFF";
// initialize cui2d and start with myPage
cuiInit(myPage);
}
// create images for the buttons
var imageNormalButton = new Image();
var imageFocusedButton = new Image();
var imagePressedButton = new Image();
var imageAlien = new Image();
// create buttons
var button0 = new cuiButton();
var button1 = new cuiButton();
var button2 = new cuiButton();
// create new arrays of keyframes (arrays of cuiKeyframe Objects)
var widthAndHeightKeys = [ // keyframes for width and height
{time : 0.00, out : -1, values : [125, 158]}, // start fast
{time : 0.25, in : 1, out : 1, values : [100, 190]}, // turn smoothly
{time : 0.80, in : 1, out : 1, values : [150, 126]}, // turn smoothly
{time : 1.50, in : 0, values : [125, 158]} // end slowly
];
var yKeys = [ // keyframes for y coordinate
{time : 0.00, out : -3, values : [200]}, // start extra fast
{time : 0.50, in : 1, out : 1, values : [ 50]}, // turn smoothly
{time : 1.00, in : -3, values : [200]} // end extra fast
];
var xAndAngleKeys = [ // keyframes for x coordinate and angle (in degrees)
new cuiKeyframe(0.00, 0, 0, [190, 0]), // start slowly
new cuiKeyframe(1.00, 1, 1, [90, -20]), // turn smoothly
new cuiKeyframe(2.00, 1, 1, [290, +20]), // turn smoothly
new cuiKeyframe(3.00, 0, 0, [190, 0]) // end slowly
];
// create new animations
var widthAndHeightAnimation = new cuiAnimation();
var yAnimation = new cuiAnimation();
var xAndAngleAnimation = new cuiAnimation();
// create a new page of size 400x300 and attach myPageProcess
var myPage = new cuiPage(400, 300, myPageProcess);
// a function to repaint the canvas and return false (if null == event)
// or to process user events (if null != event) and return true
// if the event has been processed
function myPageProcess(event) {
// draw animated image
if (null == event) {
var xAndAngle = [190, 0];
if (xAndAngleAnimation.isPlaying()) {
xAndAngle = xAndAngleAnimation.animateValues();
}
var x = xAndAngle[0];
var angle = xAndAngle[1];
var y = 200;
if (yAnimation.isPlaying()) {
y = yAnimation.animateValues()[0];
}
var widthAndHeight = [125, 158];
if (widthAndHeightAnimation.isPlaying()) {
widthAndHeight = widthAndHeightAnimation.animateValues();
}
var width = widthAndHeight[0];
var height = widthAndHeight[1];
cuiContext.save(); // save current coordinate transformation
// read the following three lines backwards, starting with the last
cuiContext.translate(x, y); // translate pivot point back to original position
cuiContext.rotate(angle * Math.PI / 180.0); // rotate around pivot point
cuiContext.translate(-x, -y); // translate pivot point to origin
cuiContext.drawImage(imageAlien, x - width/2, y - height/2, width, height);
cuiContext.restore(); // restore previous coordinate transformation
}
// draw and react to buttons
if (button0.process(event, 40, 50, 90, 50, "wobble",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
widthAndHeightAnimation.play(widthAndHeightKeys, 0.6, false);
// restart animation even if playing
}
return true;
}
if (button1.process(event, 150, 50, 80, 50, "jump",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked() && !yAnimation.isPlaying()) {
// only restart when not playing
yAnimation.play(yKeys, 1.0, false);
}
return true;
}
if (!xAndAngleAnimation.isPlaying()) { // usual button
if (button2.process(event, 250, 50, 80, 50, "rock",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
xAndAngleAnimation.play(xAndAngleKeys, 0.5, false);
}
return true;
}
}
else { // inactive button while animating
if (null == event) {
cuiContext.save(); // save current global alpha (and all context settings)
cuiContext.globalAlpha = 0.2; // use semitransparent rendering
button2.process(event, 250, 50, 80, 50, "rock",
imageNormalButton, imageNormalButton, imageNormalButton);
cuiContext.restore(); // restore previous global alpha
}
}
if (null == event) {
// draw background
cuiContext.fillStyle = "#804000";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event should be further processed
}
</script>
</head>
<body bgcolor="#000000" onload="init()"
style="-webkit-user-drag:none; -webkit-user-select:none; ">
<span style="color:white;">A canvas element cannot be displayed.</span>
</body>
</html>
示例定义了三个关键帧动画来为单个图像制作动画:widthAndHeightAnimation
动画图像的宽度和高度以创建摇摆效果;yAnimation
用于通过更改图像的 y 坐标来创建一个动画跳跃;xAndAngleAnimation
用于更改图像的 x 坐标和旋转角度以创建一个摇摆动作。
// create new animations
var widthAndHeightAnimation = new cuiAnimation();
var yAnimation = new cuiAnimation();
var xAndAngleAnimation = new cuiAnimation();
此外,还通过以下几行定义了三个关键帧数组(每个动画一个):
// create new arrays of keyframes (arrays of cuiKeyframe Objects)
var widthAndHeightKeys = [ // keyframes for width and height
{time : 0.00, out : -1, values : [125, 158]}, // start fast
{time : 0.25, in : 1, out : 1, values : [100, 190]}, // turn smoothly
{time : 0.80, in : 1, out : 1, values : [150, 126]}, // turn smoothly
{time : 1.50, in : 0, values : [125, 158]} // end slowly
];
var yKeys = [ // keyframes for y coordinate
{time : 0.00, out : -3, values : [200]}, // start extra fast
{time : 0.50, in : 1, out : 1, values : [ 50]}, // turn smoothly
{time : 1.00, in : -3, values : [200]} // end extra fast
];
var xAndAngleKeys = [ // keyframes for x coordinate and angle (in degrees)
new cuiKeyframe(0.00, 0, 0, [190, 0]), // start slowly
new cuiKeyframe(1.00, 1, 1, [90, -20]), // turn smoothly
new cuiKeyframe(2.00, 1, 1, [290, +20]), // turn smoothly
new cuiKeyframe(3.00, 0, 0, [190, 0]) // end slowly
];
每个数组都使用语法 [
第 0 个关键帧 ,
第 1 个关键帧 ,
... ]
定义。前两个数组将各个关键帧定义为具有 time
、in
、out
和 values
属性的对象。但是,第 0 个关键帧的 in
属性和最后一个关键帧的 out
属性不是必需的。time
是动画开始后关键帧的秒数。注意,关键帧必须按其时间的升序排列。in
和 out
定义了在每个关键帧之前和之后值变化的速度(即切线或斜率)。最重要的选择是
- 0 表示零速度,即缓入/缓出(也称为 ease in/out)
- 1 表示由平滑的 Catmull-Rom 样条曲线定义的速度(但只有当一个关键帧的
in
和out
相同时才平滑) - -1 表示对相邻关键帧的恒定速度(也称为线性插值,但只有当相邻关键帧的相应切线也由 -1 指定时才为线性插值)
对于更快或更慢的速度,这些数字可以进行缩放;即,0.5 的值表示 Catmull-Rom 样条曲线使用速度的一半。-3 的值表示线性插值使用速度的三倍。在关键帧之间,将应用三次埃尔米特样条曲线。(Catmull-Rom 样条曲线和线性插值都是三次埃尔米特样条曲线的特例。)
values
属性是关键帧处实际数据值的数组。它们的实际含义(是坐标、颜色分量还是大小等)取决于后来实际使用动画值的具体方式。在本示例中,yKeys
仅为图像的 y 坐标指定关键帧;因此,关键帧的 values
属性是只有一个数字的数组,例如,[200]
表示具有一个值为 200 像素的坐标的数组。xAndAngleKeys
同时为 x 坐标和旋转角度制作动画;因此,values
属性是包含两个元素的数组。例如,[90, -20]
表示包含两个元素的数组,其中第一个将解释为值为 90 像素的 x 坐标,第二个将解释为值为 -20 度的旋转角度。
第三个关键帧数组 xAndAngleKeys
使用构造函数 cuiKeyframe(time, in, out, values)
来构造具有 time
、in
、out
和 values
属性的对象。您可以选择自己喜欢的初始化关键帧的方式。
在如上一节中所述定义动画后,可以使用函数 play(keyframes, stretch, isLooping)
(在处理事件时)播放动画。
此函数使用指定的参数设置 keyframes
、stretch
和 isLooping
属性。在本示例中,keyframes
参数分别为 widthAndHeightKeys
、xAndAngleKeys
和 yKeys
,用于三个动画。第二个参数(确定 stretch
属性)是一个因子,用于使动画比关键帧定义的动画更长(因子大于 1)或更短(因子小于 1)。这在不更改所有关键帧的时间坐标的情况下更改动画的整体速度时很有用。第三个参数允许以无限循环的方式播放动画。(可以通过函数 stopLooping()
停止循环播放)。
在本示例中,第一个按钮每当点击按钮时就重新启动动画 widthAndHeightAnimation
if (button0.process(event, 40, 50, 90, 50, "wobble",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
widthAndHeightAnimation.play(widthAndHeightKeys, 0.6, false);
// restart animation even if playing
}
return true;
}
第二个按钮在重新启动动画之前检查动画是否未播放(使用 isPlaying
)
if (button1.process(event, 150, 50, 80, 50, "jump",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked() && !yAnimation.isPlaying()) {
// only restart when not playing
yAnimation.play(yKeys, 1.0, false);
}
return true;
}
因此,在动画播放时无法重新启动动画。从某种意义上说,这使得按钮在动画播放时处于非活动状态。为了传达非活动按钮,许多 GUI 会“灰显”这些按钮。第三个按钮通过使用半透明渲染并仅绘制图像 imageNormalButton
来实现类似的效果。
if (!xAndAngleAnimation.isPlaying()) { // usual button
if (button2.process(event, 250, 50, 80, 50, "rock",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
xAndAngleAnimation.play(xAndAngleKeys, 0.5, false);
}
return true;
}
}
else { // inactive button while animating
if (null == event) {
cuiContext.save(); // save current global alpha (and all context settings)
cuiContext.globalAlpha = 0.2; // use semitransparent rendering
button2.process(event, 250, 50, 80, 50, "rock",
imageNormalButton, imageNormalButton, imageNormalButton);
cuiContext.restore(); // restore previous global alpha
}
}
cuiContext.save()
保存当前全局 alpha(控制绘制命令的不透明度),cuiContext.globalAlpha = 0.2;
将全局 alpha 设置为一个相当低的值,即不透明度大幅降低,即以下图像几乎透明地渲染。在使用 process()
绘制按钮后,全局 alpha 使用 cuiContext.restore();
恢复。注意,只有当 event
为 null
时才会调用 process()
,这仅仅意味着按钮处于非活动状态,并且不会对事件做出反应。
使用函数 animateValues()
为动画的关键帧值制作动画(即为当前时间进行插值)。animateValues()
的结果是一个值数组,就像关键帧数组中指定的值一样,但包含当前时间的插值值。在本示例中,它以这种方式使用
// draw animated image
if (null == event) {
var xAndAngle = [190, 0];
if (xAndAngleAnimation.isPlaying()) {
xAndAngle = xAndAngleAnimation.animateValues();
}
var x = xAndAngle[0];
var angle = xAndAngle[1];
var y = 200;
if (yAnimation.isPlaying()) {
y = yAnimation.animateValues()[0];
}
var widthAndHeight = [125, 158];
if (widthAndHeightAnimation.isPlaying()) {
widthAndHeight = widthAndHeightAnimation.animateValues();
}
var width = widthAndHeight[0];
var height = widthAndHeight[1];
...
每次调用 animateValues()
都返回一个包含动画值的当前值的数组。从这些数组中提取各个元素并将其分配给某些变量(这里:x
、angle
、y
、width
和 height
)。然后,这些变量用于绘制动画图像。注意,通过简单地在动画的每一帧中重新绘制画布,复杂的动画比我们必须分别修改所有动画对象的动画属性要容易得多。
在本示例中,动画图像的实际绘制方式如下所示
...
cuiContext.save(); // save current coordinate transformation
// read the following three lines backwards, starting with the last
cuiContext.translate(x, y); // translate pivot point back to original position
cuiContext.rotate(angle * Math.PI / 180.0); // rotate around pivot point
cuiContext.translate(-x, -y); // translate pivot point to origin
cuiContext.drawImage(imageAlien, x - width/2, y - height/2, width, height);
cuiContext.restore(); // restore previous coordinate transformation
}
最重要的行(也是唯一真正绘制内容的行)是
cuiContext.drawImage(imageAlien, x - width/2, y - height/2, width, height);
它以 width
× height
的大小,在坐标 x
和 y
处绘制图像 imageAlien
,使其居中。(坐标 x - width/2
和 y - height/2
指定左上角。)由于 x
、y
、width
和 height
的值都随时间变化,因此这涵盖了大部分动画。其余代码仅用于处理 angle
旋转。
为此,我们应用行 cuiContext.rotate(angle * Math.PI / 180.0);
,它告诉 cuiContext
将所有后续绘制旋转 angle
。(乘以 Math.PI / 180.0
将度数转换为弧度。)但是,此旋转始终围绕坐标系的原点进行。因此,我们必须平移(即移动)旋转的枢轴点,在本示例中它位于坐标 x
和 y
处,到坐标系的原点(即坐标 x=0 和 y=0)处,以便围绕它进行旋转。这是通过行 cuiContext.translate(-x, -y);
实现的。在旋转之后,我们必须使用 cuiContext.translate(x, y);
将枢轴点移回其原始位置。如果您查看代码,您会发现实际上必须以相反的顺序指定这些平移。(如果您想按指定顺序读取变换,您必须考虑如何变换坐标系:cuiContext.translate(x, y);
将坐标系的原点移动到我们的枢轴点,然后我们围绕原点(在我们枢轴点的新位置)旋转,最后我们将原点移回其原始位置。)
由于我们不希望此旋转影响任何进一步的绘图命令,因此必须再次恢复标准变换。 最好的方法是首先使用 `cuiContext.save();` 保存上下文的当前设置(即状态)(包括变换,还包括填充颜色、字体设置等),然后在完成绘制后,可以使用 `cuiContext.restore();` 恢复这些设置。
至此,关于应用程序程序员如何使用示例动画系统的讨论已结束。 下一节将讨论如何在 cui2d.js 中实现它。
实现动画系统
[edit | edit source]首先,构造函数只是设置 `cuiKeyframe` 和 `cuiAnimation` 对象的属性
/**
* @class cuiKeyframe
* @classdesc A keyframe defines an array of numeric values at a certain time with tangents for the interpolation
* right before and right after that time. Instead of using the constructor, objects can also be initialized
* with "{time : ..., in : ..., out : ..., values : [..., ...]}"
* (See {@link cuiAnimation}.)
*
* @desc Create a new cuiKeyframe.
* @param {number} time - The time of the keyframe (in seconds relative to the start of the animation).
* @param {number} inTangent - Number specifying the tangent before the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @param {number} outTangent - Number specifying the tangent after the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @param {number[]} values - An array of numbers; all keyframes of one animation should have
* values arrays of the same size.
*/
function cuiKeyframe(time, inTangent, outTangent, values) {
/**
* The time of the keyframe (in seconds relative to the start of the animation).
* @member {number} cuiKeyframe.time
*/
this.time = time;
/**
* Number specifying the tangent before the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @member {number} cuiKeyframe.in
*/
this.in = inTangent;
/**
* Number specifying the tangent after the keyframe; -1: linear interpolation,
* 0: horizontal tangent, 1: cubic Hermite, others: scaled slope of +1/-1 cases.
* @member {number} cuiKeyframe.out
*/
this.out = outTangent;
/**
* An array of numbers; all keyframes of one animation should have
* values arrays of the same size.
* @member {number[]} cuiKeyframe.values
*/
this.values = values;
}
/**
* @class cuiAnimation
* @classdesc Animations allow to animate (i.e. interpolate) numbers specified by keyframes.
* (See {@link cuiKeyframe}.)
*
* @desc Create a new cuiAnimation.
*/
function cuiAnimation() {
this.keyframes = null;
this.stretch = 1.0;
this.start = 0;
this.end = 0;
this.isLooping = false;
}
如上所述,函数 `play()` 启动动画,`stopLooping()` 停止循环
/**
* Play an animation.
* @param {cuiKeyframe[]} keyframes - An array of keyframe objects. (Object initialization with
* something like var keys = [{time : ..., in : ..., out : ..., values : [..., ...]}, {...}, ...];
* is encouraged.) (See {@link cuiKeyframe}.)
* @param {number} stretch - A scale factor for the times in the keyframes;
* one way of usage: start designing keyframe times with stretch = 1 and
* adjust the overall timing at the end by adjusting stretch;
* another way of usage: define all times of keyframes between 0 and 1 (as in CSS transitions)
* and then set stretch to the length of the animation in seconds.
* @param {boolean} isLooping - Whether to repeat the animation endlessly.
*/
cuiAnimation.prototype.play = function(keyframes, stretch, isLooping) {
this.keyframes = keyframes;
this.stretch = stretch;
this.isLooping = isLooping;
this.start = (new Date()).getTime();
this.end = this.start +
1000.0 * this.keyframes[this.keyframes.length - 1].time * this.stretch;
if (this.end > cuiAnimationsEnd) { // new maximum end?
cuiAnimationsEnd = this.end;
}
cuiRepaint();
}
/**
* Stop looping the animation.
*/
cuiAnimation.prototype.stopLooping = function() {
this.isLooping = false;
}
基本上,`play()` 只设置 `start` 和 `end` 属性。 两者都是自 1970 年 1 月 1 日以来的毫秒数。 这些值基于当前时间(对于 `start`)以及最后一个关键帧的 `time` 属性乘以 `stretch`(并将从动画开始后的秒数转换为自 1970 年 1 月 1 日以来的毫秒数)。 此外,如果需要,可以设置全局变量 `cuiAnimationsEnd`。 `cuiAnimationsEnd` 指定所有动画结束的时间(自 1970 年 1 月 1 日以来的毫秒数)。 因此,仅当当前动画应该在所有其他动画之后停止时才需要更新它。 最后,调用 `cuiRepaint()` 以尽快请求重绘。
我们还定义了一个辅助函数 `isPlaying()`,它返回 `true` 或 `false` 来指定特定动画是否正在播放(有关如何使用它的示例,请参见上面)
/**
* Determine whether the animation is currently playing.
* @returns {boolean} True if the animation is currently playing, false otherwise.
*/
cuiAnimation.prototype.isPlaying = function() {
if (!this.isLooping) {
return ((new Date()).getTime() < this.end);
}
else {
return (this.end > 0);
}
}
为了不断重绘画布,渲染循环会检查是否有任何动画正在播放
// Render loop of cui2d, which calls cuiProcess(null) if needed.
function cuiRenderLoop() {
var now = (new Date()).getTime();
if (cuiAnimationsEnd < now ) { // all animations over?
if (cuiAnimationsArePlaying) {
cuiRepaint();
// repaint one more time since the rendering might differ
// after the animations have stopped
}
cuiAnimationsArePlaying = false;
}
else {
cuiAnimationsArePlaying = true;
}
if (cuiCanvasNeedsRepaint || cuiAnimationsArePlaying) {
cuiProcess(null);
}
window.setTimeout("cuiRenderLoop()", cuiAnimationStep); // call myself again
// using setTimeout allows to easily change cuiAnimationStep dynamically
}
它通过比较 `cuiAnimationsEnd` 与当前时间来检查是否有任何动画正在播放,并相应地更新全局变量 `cuiAnimationsArePlaying`。 请注意,当动画刚刚停止时会调用 `cuiRepaint()`。 这是必需的,因为画布可能会在动画停止后发生变化。 如果 `cuiAnimationsArePlaying` 为 `true`,则通过调用 `cuiProcess(null)` 渲染一个新帧。
除了对动画值的实际计算(将在下一节中讨论),这完成了对动画系统的描述。 请注意,该系统允许重新启动正在播放的动画,它允许任意数量的动画同时播放,它允许使用单个关键帧数组对无限数量的值进行动画处理,并且它允许使用任意三次 Hermite 样条曲线进行动画处理值,同时使其易于指定缓慢的进出运动(通过将特定关键帧的 `in` 和 `out` 属性设置为 0)、Catmull-Rom 样条曲线(通过将所有 `in` 和 `out` 属性设置为 1)和线性插值(通过将它们全部设置为 -1)。 最后一个功能是通过插值实现的,将在下一节中讨论。
动画中关键帧的值通过函数 `animateValues()` 进行动画处理(即为当前时间插值)。 如上所述,`animateValues()` 的结果是一个值数组,与关键帧数组中指定的值类似,但具有当前时间的插值值。 `animateValues()` 的实现基本上评估了每个 `values` 数组元素的 三次 Hermite 样条曲线,其斜率来自 Catmull-Rom 样条曲线 或线性插值(取决于 `in` 和 `out` 属性的符号)。 不幸的是,样条曲线的评估有些繁琐,Catmull-Rom 样条曲线与线性插值的混合并没有使其变得更容易; 因此,我们不会详细讨论这段代码。
但是,在函数开头有一个重要功能:它首先检查动画是否尚未开始或开始时间和结束时间是否没有意义(在它们都初始化为 0 之后就是这样)。 在这种情况下,将返回第 0 个关键帧的 `values` 数组。 然后它检查动画是否已经结束。 在这种情况下,将返回最后一个关键帧的 `values` 数组。 此功能使即使动画未播放也可以调用 `animateValues()`,实际上在上面描述的三个动画的示例中使用了它。
/**
* Compute an array of interpolated values based on the keyframes and the current time.
* Returns the values array of the 0th keyframe if the animation hasn't started yet
* and the values array of the last keyframe if it has finished playing.
* This makes it possible to use animateValues even after the animation has stopped.
* (See {@link cuiKeyframe}.)
* @returns {number[]} An array of interpolated values.
*/
cuiAnimation.prototype.animateValues = function() {
var now = (new Date()).getTime();
if (now < this.start || this.end <= this.start) { // animation not started?
return this.keyframes[0].values;
}
if (now > this.end) { // current loop of animation already over?
if (!this.isLooping) {
return this.keyframes[this.keyframes.length - 1].values;
}
// restart the animation
var length = 1000.0 * this.keyframes[this.keyframes.length - 1].time * this.stretch;
this.start = this.start + Math.floor((now - this.start) / length) * length;
this.end = this.start + length;
if (this.end > cuiAnimationsEnd) { // new maximum end?
cuiAnimationsEnd = this.end;
}
}
// determine index iTo of keyframe after(!) current time t
var iTo = 0;
var ut = 0.001 * (now - this.start) / this.stretch;
// unstretched time relative to animation start in seconds
while (iTo < this.keyframes.length &&
this.keyframes[iTo].time < ut) {
iTo = iTo + 1;
}
var iFrom = iTo - 1; // index of keyframe before t
if (iTo == 0) {
return this.keyframes[0].values;
}
if (iTo >= this.keyframes.length) {
return this.keyframes[this.keyframes.length - 1].values;
}
// interpolate each value
var newValues = this.keyframes[iFrom].values.slice(0);
var t0 = this.keyframes[iFrom].time;
var t1 = this.keyframes[iTo].time;
var t = (ut - t0) / (t1 - t0)
var tt = t * t;
var ttt = tt * t;
for (var iValue = 0; iValue < newValues.length; iValue++) {
// compute values for cubic Hermite spline with out/in determining
// the velocity: out/in = -1: linear, out/in = 0: slow (i.e. 0),
// out/in = 1: smooth (Catmull-Rom spline).
// The magnitude of in/out changes the velocity accordingly.
var p0, p1, m0, m1;
p0 = this.keyframes[iFrom].values[iValue];
p1 = this.keyframes[iTo].values[iValue];
// compute out slope m0 at iFrom
if (this.keyframes[iFrom].out < 0.0) { // linear
m0 = (p1 - p0) / (t1 - t0) * (-this.keyframes[iFrom].out);
}
else if (iFrom > 0) { // smooth, not in first interval
m0 = (p1 - this.keyframes[iFrom - 1].values[iValue]) /
(t1 - this.keyframes[iFrom - 1].time) *
this.keyframes[iFrom].out;
}
else { // smooth, in first interval
m0 = (p1 - p0) / (t1 - t0) * this.keyframes[iFrom].out;
}
// compute in slope m1 at iTo
if (this.keyframes[iTo].in < 0.0) { // linear
m1 = (p1 - p0) / (t1 - t0) * (-this.keyframes[iTo].in);
}
else if (iTo < this.keyframes.length - 1) { // smooth, not last interval
m1 = (this.keyframes[iTo + 1].values[iValue] - p0) /
(this.keyframes[iTo + 1].time - t0) *
this.keyframes[iTo].in;
}
else { // smooth, in last interval
m1 = (p1 - p0) / (t1 - t0) * this.keyframes[iTo].in;
}
// cubic Hermite curve interpolation
newValues[iValue] = (2.0*ttt-3.0*tt+1.0) * p0 +
(ttt-2.0*tt+t)*(t1-t0) * m0 +
(-2.0*ttt+3.0*tt) * p1 +
(ttt-tt) * (t1-t0) * m1;
}
return newValues;
}