画布 2D 网页应用/过渡
本章扩展了有关 页面 的章节,通过添加页面之间动画过渡效果(简称为“过渡”)来进行扩展。为此,它还依赖于在有关 动画 的章节中介绍的动画系统。
实现被封装在一个具有多个参数的函数中。因此,一方面,只需更改参数而无需查看函数的实现,就可以实现多种过渡;另一方面,可能难以理解所有参数的含义以及如何使用它们来实现某些效果。因此,下面列出了 24 种流行过渡的实现。此外,还介绍了一些关于如何在网页应用中设计和选择过渡的指南。
本章的示例(可在 在线 获取;也可作为 可下载版本 获取)为三个页面之间的过渡添加了四个动画过渡效果。以下部分将讨论如何使用这些函数创建这些过渡以及这些函数是如何实现的。有关其他部分,请参阅有关 页面、动画 和 响应式按钮 的章节。
<!DOCTYPE HTML>
<html>
<head>
<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;
// initialize and start cui2d
cuiInit(firstPage);
}
// first page
var firstPage = new cuiPage(400, 300, firstPageProcess);
var button0 = new cuiButton();
var imageNormalButton = new Image();
var imageFocusedButton = new Image();
var imagePressedButton = new Image();
function firstPageProcess(event) {
if (button0.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
cuiPlayTransition(this, secondPage, true, false, 0, 0, 0.33, // page turn
-1.2, 0.1, 0.2, 1.1, 5, 1.0,
0, 0, 1, 1, 0, 0.8);
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("First page using landcape format.", 200, 150);
cuiContext.fillStyle = "#E0E0E0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false; // event has not been processed
}
// second page
var secondPage = new cuiPage(400, 400, secondPageProcess);
var button1 = new cuiButton();
var button2 = new cuiButton();
function secondPageProcess(event) {
if (button1.process(event, 20, 50, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button1.isClicked()) {
cuiPlayTransition(this, firstPage, false, false, 0, 0, 0.33, // page turn
0, 0, 1, 1, 0, 0.8,
-1.2, 0.1, 0.2, 1.1, 5, 1.0);
}
return true;
}
if (button2.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button2.isClicked()) {
var startPoint = {x: 300 + 40, y: 50 + 25}; // start point for maximization
this.transformPageToTransitionCoordinates(startPoint);
cuiPlayTransition(this, thirdPage, false, false, 1, 0, 0.25, // maxmize
0.0, 0.0, 1.0, 1.0, 0, 0.8,
startPoint.x, startPoint.y, 0.1, 0.1, -5, 1.0);
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Second page using square format.", 200, 200);
cuiContext.fillStyle = "#FFF0E0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
// third page
var thirdPage = new cuiPage(400, 533, thirdPageProcess);
var button3 = new cuiButton();
function thirdPageProcess(event) {
if (button3.process(event, 20, 50, 120, 50, "previous",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button3.isClicked()) {
var targetPoint = {x: 300 + 40, y: 50 + 25}; // target point for minimization
secondPage.transformPageToTransitionCoordinates(targetPoint);
cuiPlayTransition(this, secondPage, true, false, 0, 1, 0.3, // minimize
targetPoint.x, targetPoint.y, 0.1, 0.1, 5, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 0.8);
}
return true;
}
if (null == event) {
// draw background
cuiContext.fillText("Third page using portrait format.", 200, 266);
cuiContext.fillStyle = "#FFE0F0";
cuiContext.fillRect(0, 0, this.width, this.height);
}
return false;
}
</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>
为了启动两个页面之间的过渡,必须调用函数 cuiPlayTransition()
。在示例中,第一个页面和第二个页面之间的动画过渡以这种方式开始
...
if (button0.process(event, 300, 50, 80, 50, "next",
imageNormalButton, imageFocusedButton, imagePressedButton)) {
if (button0.isClicked()) {
cuiPlayTransition(this, secondPage, true, false, 0, 0, 0.33, // page turn
-1.2, 0.1, 0.2, 1.1, 5, 1.0,
0, 0, 1, 1, 0, 0.8);
}
return true;
}
...
过渡的外观完全由 cuiPlayTransition()
的 19 个参数控制,这些参数将在下一节中讨论。
函数 cuiPlayTransition(previousPage, nextPage, isPreviousOverNext, isFrontMaskAnimated, animationInitialSpeed, animationFinalSpeed, animationLength, previousFinalPositionX, previousFinalPositionY, previousFinalScaleX, previousFinalScaleY, previousFinalRotation, previousFinalOpacity, nextInitialPositionX, nextInitialPositionY, nextInitialScaleX, nextInitialScaleY, nextInitialRotation, nextInitialOpacity)
配置过渡的方方面面,包括过渡的时间和外观。参数为
previousPage
:前一个页面的cuiPage
对象;过渡是从前一个页面到下一个页面nextPage
:下一个页面的cuiPage
对象isPreviousOverNext
:previousPage
是渲染在nextPage
上方还是反之;必须为整个过渡选择一种可能性isFrontMaskAnimated
:是否对前面页面的不透明度蒙版进行动画处理,而不是对页面本身进行动画处理(如果isPreviousOverNext
为true
,则previousPage
是前面的页面,否则是nextPage
);不透明度蒙版的动画处理主要对擦除过渡很有用animationInitialSpeed
:过渡开始时动画值的更改的初始速度;0.0 表示缓慢开始,1.0 表示线性插值,较大的值表示更快的开始;由于三次 Hermite 曲线用于插值,因此大于约 3 的值会导致过冲animationFinalSpeed
:过渡结束时动画值的更改的最终速度(见animationInitialSpeed
)animationLength
:过渡的持续时间(以秒为单位)(通常,这应该不超过 0.25;在某些情况下 0.33 可能没问题;更长的持续时间可能会让移动设备上的专家用户感到厌烦)- 指定过渡结束时
previousPage
最终外观的参数(初始外观始终相同)previousFinalPositionX
和previousFinalPositionY
:previousPage
中心点的最终位置,其中屏幕的左/上边缘由 -1 指定,右/下边缘由 +1 指定,因此屏幕的中心位于 0。(初始位置始终为 (0,0);即previousPage
最初居中。)previousFinalScaleX
和previousFinalScaleY
:previousPage
的宽度和高度的最终缩放比例。(初始缩放比例始终为 1;即最初没有缩放。)previousFinalRotation
:previousPage
的最终顺时针旋转角度(以度为单位)。(初始旋转角度始终为 0;即没有旋转。)previousFinalOpacity
:previousPage
的最终不透明度。(初始不透明度始终为 1;即页面完全不透明。)
- 指定过渡开始时
nextPage
初始外观的参数(最终外观始终相同)nextInitialPositionX
和nextInitialPositionY
:nextPage
中心点的初始位置,其中屏幕的左/上边缘由 -1 指定,右/下边缘由 +1 指定,因此屏幕的中心位于 0。(最终位置始终为 (0,0);即nextPage
最终居中。)nextInitialScaleX
和nextInitialScaleY
:nextPage
的宽度和高度的初始缩放比例。(最终缩放比例始终为 1;即最初没有缩放。)nextInitialRotation
:nextPage
的初始顺时针旋转角度(以度为单位)。(最终旋转角度始终为 0;即没有旋转。)nextInitialOpacity
:nextPage
的初始不透明度。(最终不透明度始终为 1;即页面完全不透明。)
由于很难找到合适的数值,因此以下列表包含对 cuiPlayTransition()
的调用示例,用于实现流行的过渡效果。每个效果都有一个反向效果,应该用于以相反顺序在相同页面之间进行过渡
// push to left (reverse of push to right)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
-2.0, 0.0, 1.0, 1.0, 0, 1.0,
+2.0, 0.0, 1.0, 1.0, 0, 1.0);
// push to right (reverse of push to left)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
+2.0, 0.0, 1.0, 1.0, 0, 1.0,
-2.0, 0.0, 1.0, 1.0, 0, 1.0);
// push down (reverse of push to up)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
0.0, -2.0, 1.0, 1.0, 0, 1.0,
0.0, +2.0, 1.0, 1.0, 0, 1.0);
// push up (reverse of push down)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
0.0, +2.0, 1.0, 1.0, 0, 1.0,
0.0, -2.0, 1.0, 1.0, 0, 1.0);
// cover from top to bottom (reverse of uncover from bottom to top)
cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
0.0, +0.0, 1.0, 1.0, 0, 1.0,
0.0, -2.0, 1.0, 1.0, 0, 1.0);
// uncover from bottom to top (reverse of cover from top to bottom)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
0.0, -2.0, 1.0, 1.0, 0, 1.0,
0.0, +0.0, 1.0, 1.0, 0, 1.0);
// cover from bottom to top (reverse of uncover from top to bottom)
cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
0.0, -0.0, 1.0, 1.0, 0, 1.0,
0.0, +2.0, 1.0, 1.0, 0, 1.0);
// uncover from top to bottom (reverse of cover from bottom to top)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
0.0, +2.0, 1.0, 1.0, 0, 1.0,
0.0, -0.0, 1.0, 1.0, 0, 1.0);
// cover from left to right (reverse of uncover from right to left)
cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
+0.0, 0.0, 1.0, 1.0, 0, 1.0,
-2.0, 0.0, 1.0, 1.0, 0, 1.0);
// uncover from right to left (reverse of cover from left to right)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
-2.0, 0.0, 1.0, 1.0, 0, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// cover from right to left (reverse of uncover from left to right)
cuiPlayTransition(firstPage, secondPage, false, false, 2, 0, 0.25,
+0.0, 0.0, 1.0, 1.0, 0, 1.0,
+2.0, 0.0, 1.0, 1.0, 0, 1.0);
// uncover from left to right (reverse of cover from right to left)
cuiPlayTransition(firstPage, secondPage, true, false, 2, 0, 0.25,
+2.0, 0.0, 1.0, 1.0, 0, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// page turn uncovering from right to left (reverse of page turn covering from left to right)
cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
-1.2, 0.0, 0.2, 1.1, 5, 1.0,
0, 0, 1, 1, 0, 0.8);
// page turn covering from left to right (reverse of page turn uncovering from left to right)
cuiPlayTransition(firstPage, secondPage, false, false, 0, 0, 0.33,
0, 0, 1, 1, 0, 0.8,
-1.2, 0.0, 0.2, 1.1, -5, 1.0);
// maximize (reverse of minimize)
var startPoint = {x: 300 + 40, y: 50 + 25}; // start point for maximization
firstPage.transformPageToTransitionCoordinates(startPoint);
cuiPlayTransition(firstPage, secondPage, false, false, 1, 0, 0.25,
0.0, 0.0, 1.0, 1.0, 0, 0.8,
startPoint.x, startPoint.y, 0.1, 0.1, -5, 1.0);
// minimize (reverse of maximize)
var targetPoint = {x: 300 + 40, y: 50 + 25}; // target point for minimization
secondPage.transformPageToTransitionCoordinates(targetPoint);
cuiPlayTransition(firstPage, secondPage, true, false, 0, 1, 0.3,
targetPoint.x, targetPoint.y, 0.1, 0.1, 5, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 0.8);
// dissolve (reverse of itself)
cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
0.0, 0.0, 1.0, 1.0, 0, 0.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// fade through black (reverse of itself)
cuiPlayTransition(firstPage, secondPage, true, false, 1, 1, 0.33,
0.0, 0.0, 1.0, 1.0, 0, -1.0,
0.0, 0.0, 1.0, 1.0, 0, -1.0);
// materialize from air (reverse of dissolve into air)
cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
0.0, 0.0, 1.0, 1.0, 0, 0.0,
0.0, 0.0, 2.0, 2.0, 0, 1.0);
// dissolve into air (reverse of materialize from air)
cuiPlayTransition(firstPage, secondPage, true, false, 0, 0, 0.33,
0.0, 0.0, 2.0, 2.0, 0, 0.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// wipe from left to right (reverse of wipe from right to left)
cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
2.0, 0.0, 1.0, 1.0, 0, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// wipe from right to left (reverse of wipe from left to right)
cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
-2.0, 0.0, 1.0, 1.0, 0, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// wipe from top to bottom (reverse of wipe from bottom to top)
cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
0.0, 2.0, 1.0, 1.0, 0, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
// wipe from bottom to top (reverse of wipe from top to bottom)
cuiPlayTransition(firstPage, secondPage, true, true, 1, 1, 0.25,
0.0, -2.0, 1.0, 1.0, 0, 1.0,
0.0, 0.0, 1.0, 1.0, 0, 1.0);
最大化和最小化过渡应该使用 cuiPage
的 transformPageToTransitionCoordinates()
方法,从用于定位按钮等的(页面)坐标计算出 cuiPlayTransition()
的合适坐标。
你可能对许多过渡及其名称从幻灯片演示软件中了解到。实际上,包含的过渡倾向于这种软件提供的更微妙的过渡。另一方面,移动设备上的大多数应用程序几乎完全依赖于这种过渡。下一节关于选择和设计过渡指南中将讨论其中的一些原因。
通过阻止、使人迷失方向和/或分散用户的注意力,很容易用过渡惹恼用户。确保你不这样做。至少,用户应该更喜欢你的过渡,而不是从一个页面立即切换到另一个页面。(这并不像听起来那么容易。)以下是一个检查列表,以避免令人讨厌的过渡
- 使它们快速:不要阻止用户在下一个页面上执行任何操作(如果过渡持续时间超过约 1/4 秒,就必须有一个充分的理由;如果过渡持续时间超过约 1/3 秒,就必须有一个非常充分的理由(例如,你确信用户想要这样做);确保在最小的适用显示器上测试你的过渡,因为尺寸越小,物理速度就越低;因此,过渡看起来会更慢)
- 使它们一致:不要使用户迷失方向
- 相同类型的过渡之间存在不一致(例如,列表中下一个(或上一个)页面的过渡、层次结构中较低(或较高)级别的页面的过渡、到(或从)对话框的过渡等)
- 从页面 A 到页面 B 的过渡与从页面 B 返回页面 A 的过渡之间存在不一致(它们应该在视觉上反转,除非过渡本身就是反转过渡,例如溶解)
- 过渡与激活它的用户操作的位置之间存在不一致(例如,如果屏幕右侧的图形元素上的点击或触摸激活了过渡,则过渡的主要方向应该是从右到左,无论使用哪种类型的过渡)
- 过渡与激活它的手势之间存在不一致(例如,一个方向上的轻扫应该导致同一个方向上的过渡,没有缓入,但有缓出)
- 使它们微妙:不要分散用户对页面内容的注意力(页面内容应该比任何过渡都更重要);请记住,用户会一遍又一遍地看到相同的过渡(至少如果你一致地使用它们);因此,即使过渡在最初的 40 次观看时很有趣,它们也可能很快变得过时。
这仅仅是为了避免阻止、使人迷失方向和/或分散用户注意力的令人讨厌的过渡。此外,良好的过渡应该是有意义的,并且可以传达信息
- 4. 使用过渡尽可能多地传达信息
- 传达页面对之间的关系(例如,在适当的方向使用推入过渡,以传达两个页面是列表中的相邻页面)
- 传达一个或两个页面的类型(例如,为通知或对话框保留从上到下的封面)
- 传达页面组织的结构(例如,使用向上/向下推入在层次结构的同一级别上移动,使用向左/向右推入在层次结构中向上或向下移动)
- 传达页面的功能(例如,使用电影中的过渡(特别是溶解和擦除)来讲述故事;或使用最大化来提供有关特定位置的详细信息)
- 传达激活过渡的用户操作的类型和位置(例如,使用与激活它的轻扫手势类似的方向的推入过渡)
与往常一样,对于设计指南:如果你有充分的理由,可以打破它们。
本节讨论在 cui2d.js
中实现的两个函数 cuiPlayTransition()
和 cuiDrawTransition()
。cuiDrawTransition()
由全局变量 cuiPageForTransitions
中特殊页面的处理函数调用。cuiPlayTransition()
函数执行四个主要任务。
- 如有必要,它会为上一页和下一页创建画布。
- 然后,它将上一页的快照存储在一个画布中,将下一页的快照存储在另一个画布中。
- 此外,它还会为动画过渡设置全局变量
cuiAnimationForTransitions
的属性。 - 最后,它启动动画,指定在动画结束之前忽略事件,并请求重新绘制画布。
要理解代码,您应该知道 2 个关键帧中的 12 个动画值是:previousPositionX
、previousPositionY
、previousScaleX
、previousScaleY
、previousRotation
、previousOpacity
、nextPositionX
、nextPositionY
、nextScaleX
、nextScaleY
、nextRotation
、nextOpacity
。第一个关键帧中上一页的值和第 0 个关键帧中下一页的值由用户指定,而其余的值是默认值,它们指定上一页在第 0 个关键帧中开始时没有任何变化,下一页在第 1 个关键帧中结束时没有任何变化。代码如下:
/**
* Play a transition between two pages.
* @param {cuiPage} previousPage - The initial page for the transition.
* @param {cuiPage} nextPage - The final page for the transition.
* @param {boolean} isPreviousOverNext - Whether to draw previousPage over nextPage.
* @param {boolean} isFrontMaskAnimated - Whether to animate only an opacity mask of the page in front.
* @param {number} animationInitialSpeed - 0 for zero initial speed, 1 for linear interpolation, other values scale the speed.
* @param {number} animationFinalSpeed - 0 for zero final speed, 1 for linear interpolation, other values scale the speed.
* @param {number} animationLength - Length of the transition in seconds.
* @param {number} previousFinalPositionX - Final x position of the previous page (-1/+1: centered on left/right edge).
* @param {number} previousFinalPositionY - Final y position of the previous page (-1/+1: centered on top/bottom edge).
* @param {number} previousFinalScaleX - Final x scale of the previous page (1: no scaling).
* @param {number} previousFinalScaleY - Final y scale of the previous page (1: no scaling).
* @param {number} previousFinalRotation - Final rotation in degrees of the previous page (0: no rotation).
* @param {number} previousFinalOpacity - Final opacity of the previous page (0: transparent, 1: opaque).
* @param {number} nextInitialPositionX - Initial x position of the next page (-1/+1: centered on left/right edge).
* @param {number} nextInitialPositionY - Initial y position of the next page (-1/+1: centered on top/bottom edge).
* @param {number} nextInitialScaleX - Initial x scale of the next page (1: no scaling).
* @param {number} nextInitialScaleY - Initial y scale of the next page (1: no scaling).
* @param {number} nextInitialRotation - Initial rotation in degrees of the next page (0: no rotation).
* @param {number} nextInitialOpacity - Initial opacity of the next page (0: transparent, 1: opaque).
*/
function cuiPlayTransition(
previousPage, nextPage, isPreviousOverNext, isFrontMaskAnimated,
animationInitialSpeed, animationFinalSpeed, animationLength,
previousFinalPositionX, previousFinalPositionY,
previousFinalScaleX, previousFinalScaleY,
previousFinalRotation, previousFinalOpacity,
nextInitialPositionX, nextInitialPositionY,
nextInitialScaleX, nextInitialScaleY,
nextInitialRotation, nextInitialOpacity)
{
// if necessary, create previousCanvas and nextCanvas
if (null == cuiAnimationForTransitions.previousCanvas) {
cuiAnimationForTransitions.previousCanvas = document.createElement("canvas");
}
if (null == cuiAnimationForTransitions.nextCanvas) {
cuiAnimationForTransitions.nextCanvas = document.createElement("canvas");
}
// draw previousCanvas and nextCanvas
// save current animations state and make sure the render loop doesn't render now
var tempCanvas = cuiCanvas;
var tempAnimationsArePlaying = cuiAnimationsArePlaying;
var tempAnimationsEnd = cuiAnimationsEnd;
cuiAnimationsArePlaying = false;
cuiAnimationsEnd = 0;
// draw previous page into previousCanvas
var previousCanvas = cuiAnimationForTransitions.previousCanvas;
cuiCurrentPage = previousPage;
cuiCanvas = previousCanvas;
cuiContext = previousCanvas.getContext("2d");
cuiProcess(null);
// draw next page into nextCanvas
var nextCanvas = cuiAnimationForTransitions.nextCanvas;
cuiCurrentPage = nextPage;
cuiCanvas = nextCanvas;
cuiContext = nextCanvas.getContext("2d");
cuiProcess(null);
// restore cui state
cuiCanvas = tempCanvas;
cuiContext = cuiCanvas.getContext("2d");
cuiCurrentPage = cuiPageForTransitions;
cuiAnimationsArePlaying = tempAnimationsArePlaying; // restore animations state
cuiAnimationsEnd = tempAnimationsEnd; // restore animations state
// set cuiAnimationForTransitions
var transitionKeyframes = [
{time : 0.00, out : -animationInitialSpeed,
values : [
0.0, 0.0, 1.0, 1.0, 0.0, 1.0, // previous page initial values
nextInitialPositionX, nextInitialPositionY,
nextInitialScaleX, nextInitialScaleY,
nextInitialRotation, nextInitialOpacity
]},
{time : 1.00, in : -animationFinalSpeed,
values : [
previousFinalPositionX, previousFinalPositionY,
previousFinalScaleX, previousFinalScaleY,
previousFinalRotation, previousFinalOpacity,
0.0, 0.0, 1.0, 1.0, 0.0, 1.0 // next page final values
]}
];
cuiAnimationForTransitions.nextPage = nextPage;
cuiAnimationForTransitions.isPreviousOverNext = isPreviousOverNext;
cuiAnimationForTransitions.isFrontMaskAnimated = isFrontMaskAnimated;
cuiAnimationForTransitions.keyframes = transitionKeyframes;
cuiAnimationForTransitions.stretch = animationLength;
cuiAnimationForTransitions.play();
cuiIgnoringEventsEnd = cuiAnimationForTransitions.end;
cuiRepaint();
}
动画的实际渲染由 cuiDrawTransition
执行。首先,它使用 animateValues
计算动画值并提取 12 个动画参数。根据 isPreviousOverNext
,它要么将上一页的画布渲染到下一页的画布上,要么反之亦然。(因为我们使用复合操作 "destination-over"
,所以前页必须先渲染。)如果 isFrontMaskAnimated
为假,则直接渲染前页(使用 drawImage
),否则渲染一个动画遮罩(使用 fillRect
),并且仅在遮罩区域内渲染静态前页,方法是使用复合操作 "source-in"
。(动画遮罩对于擦除过渡很有用。)画布和遮罩的动画主要通过不透明度的动画值和使用动画值作为参数的几何变换来实现。最后,cuiDrawTransition
检查过渡是否完成,如果是,则将 cuiCurrentPage
设置为 nextPage
并请求重新绘制。
/** Draw a frame of the current transition; called by cuiProcess(). */
function cuiDrawTransition() {
var previousCanvas = cuiAnimationForTransitions.previousCanvas;
var nextCanvas = cuiAnimationForTransitions.nextCanvas;
var width = cuiCanvas.width;
var height = cuiCanvas.height;
var values = cuiAnimationForTransitions.animateValues();
var previousPositionX = values[0];
var previousPositionY = values[1];
var previousScaleX = values[2];
var previousScaleY = values[3];
var previousRotation = values[4];
var previousOpacity = values[5];
var nextPositionX = values[6];
var nextPositionY = values[7];
var nextScaleX = values[8];
var nextScaleY = values[9];
var nextRotation = values[10];
var nextOpacity = values[11];
if (cuiAnimationForTransitions.isPreviousOverNext) {
// first draw previous page then next page
if (!cuiAnimationForTransitions.isFrontMaskAnimated) {
// draw without mask
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, previousOpacity));
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate((0.5 * previousPositionX + 0.5) * width,
(0.5 * previousPositionY + 0.5) * height);
// translate center as specified
cuiContext.rotate(previousRotation * Math.PI / 180.0);
// rotate around center
cuiContext.scale(previousScaleX, previousScaleY);
// scale image as specified
cuiContext.translate(-0.5 * width, -0.5 * height);
// translate center to origin
cuiContext.drawImage(previousCanvas, 0, 0, width, height);
// draw full size
} else { // draw with transform mask for wipe transitions
cuiContext.globalCompositeOperation = "copy";
cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, previousOpacity));
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate((0.5 * previousPositionX + 0.5) * width,
(0.5 * previousPositionY + 0.5) * height);
// translate center as specified
cuiContext.rotate(previousRotation * Math.PI / 180.0);
// rotate around center
cuiContext.scale(previousScaleX, previousScaleY);
// scale image as specified
cuiContext.translate(-0.5 * width, -0.5 * height);
// translate center to origin
cuiContext.fillStyle = "#000000";
cuiContext.fillRect(0, 0, width, height);
// draw black full-size mask with specified opacity
cuiContext.globalCompositeOperation = "source-in";
cuiContext.globalAlpha = 1.0;
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.drawImage(previousCanvas, 0, 0, width, height);
// draw canvas without trafo
}
// now draw next page under previous page
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, nextOpacity));
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate((0.5 * nextPositionX + 0.5) * width,
(0.5 * nextPositionY + 0.5) * height);
// translate center as specified
cuiContext.rotate(nextRotation * Math.PI / 180.0);
// rotate around center
cuiContext.scale(nextScaleX, nextScaleY); // scale image as specified
cuiContext.translate(-0.5 * width, -0.5 * height);
// translate center to origin
cuiContext.drawImage(nextCanvas, 0, 0, width, height);
// draw full size
} else {
// first draw next page then previous page
if (!cuiAnimationForTransitions.isFrontMaskAnimated) {
// draw without mask
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, nextOpacity));
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate((0.5 * nextPositionX + 0.5) * width,
(0.5 * nextPositionY + 0.5) * height);
// translate center as specified
cuiContext.rotate(nextRotation * Math.PI / 180.0);
// rotate around center
cuiContext.scale(nextScaleX, nextScaleY);
// scale image as specified
cuiContext.translate(-0.5 * width, -0.5 * height);
// translate center to origin
cuiContext.drawImage(nextCanvas, 0, 0, width, height);
// draw full size
} else {
// draw with transform mask for wipe transitions
cuiContext.globalCompositeOperation = "copy";
cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, nextOpacity));
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate((0.5 * nextPositionX + 0.5) * width,
(0.5 * nextPositionY + 0.5) * height);
// translate center as specified
cuiContext.rotate(nextRotation * Math.PI / 180.0);
// rotate around center
cuiContext.scale(nextScaleX, nextScaleY);
// scale image as specified
cuiContext.translate(-0.5 * width, -0.5 * height);
// translate center to origin
cuiContext.fillStyle = "#000000";
cuiContext.fillRect(0, 0, width, height);
// draw black full-size mask with specified opacity
cuiContext.globalCompositeOperation = "source-in";
cuiContext.globalAlpha = 1.0;
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.drawImage(nextCanvas, 0, 0, width, height);
// draw canvas without trafo
}
// now draw previous page under next page
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = Math.max(0.0, Math.min(1.0, previousOpacity));
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.translate((0.5 * previousPositionX + 0.5) * width,
(0.5 * previousPositionY + 0.5) * height);
// translate center as specified
cuiContext.rotate(previousRotation * Math.PI / 180.0);
// rotate around center
cuiContext.scale(previousScaleX, previousScaleY);
// scale image as specified
cuiContext.translate(-0.5 * width, -0.5 * height);
// translate center to origin
cuiContext.drawImage(previousCanvas, 0, 0, width, height);
// draw full size
}
// draw opaque background to avoid any semitransparent colors in the canvas
cuiContext.globalCompositeOperation = "destination-over";
cuiContext.globalAlpha = 1.0;
cuiContext.setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
cuiContext.fillStyle = "#000000";
cuiContext.fillRect(0, 0, width, height);
if (!cuiAnimationForTransitions.isPlaying()) {
// transition has finished
cuiCurrentPage = cuiAnimationForTransitions.nextPage;
cuiRepaint();
}
}
如果您查看代码,您会注意到代码中有许多重复,事实上,可以显著缩短代码;但是,为了便于阅读,代码以这种长形式保留下来。