在本书的制作过程中,Adobe After Effects发布了软件的更新,采用了新的表达式引擎。新的表达式引擎:JavaScript ,这是最大的更新之一。这次更新增加了我们到目前为止所学到的一切功能,不仅引擎比以往任何时候都快,而且我们现在可以使用更多的方法、关键字、运算符和许多其他现代JavaScript语言的工具。在本章中,我们将发现以下新特性不仅有助于保持我们表达方式的简单性和可读性,而且还可以让我们讨论一些我们还没有提到的非常重要的东西:优化。
在本书中,我们一直使用字符串值来理解表达式的工作原理。在这个新的引擎中,字符串值也获得了新的功能,使我们能够更容易地操作它们。例如,你还记得,当我们想在多行上写一个文本时,我们必须使用字符串值"\r",它可以产生一个文本操作来转到下一行。在新版本中,这就变得简单多了,我们所要做的就是使用背标:`。只需在回车键之间写上你的文本,你甚至不需要使用引号,里面的所有内容都会被解释为一个字符串值。
需要注意的是,你也可以用背标来代替字符串值的引号。
我最喜欢做的事情之一,是通过使用一个函数来探索表达式世界,该函数可以揭示对象中可用的值和方法。
例如,我们新建一个文本图层,并在源文本属性中写下这个表达式。
正如我们所看到的,这个函数使用for / in语句在一个对象中循环,因此可以生成源文本对象中的每个元素。我们马上就可以看到有新的术语可用:concat(), endsWith(), includes(), repeat(), startsWith() 和 trim()现在都出现在这个新引擎中。这些新术语都是JavaScript方法,可以用来操作一个字符串值。
和字符串值一样,数组值也有一些值得注意的新的方法。
常规方法:
产生数组迭代对象的方法:
使用回调函数的方法:
find(), findIndex(), forEach(), filter(), find(), reduce(), some()和every()。回调函数是一个可以在另一个函数中作为参数使用的函数。它们是循环的绝佳替代品,因为它们更易读。例如:
map()方法从回调函数中产生一个新的数组值,分别对数组值的值进行操作。
数学对象的新方法
现在是时候思考优化问题了。到目前为止,我们确实还没有机会思考我们的表达式的这方面的问题,主要是因为本书中使用的示例是很轻量的,优化它们只会让计算速度加快几毫秒,不会太明显。然而当你有一个有很多表达式的大项目,或者想做一个很高级的表达式时,有好的习惯真的可以提升计算速度。让你的表达式反应超快而不是很慢。类似于当你需要在一个项目的资源和效果中找到合适的平衡点,才能有一个合理的渲染时间。
你还记得表达式没有内存,好吧,它没有,但是有一些技巧可以让引擎尽可能地在后台工作。正如我们在var关键字上看到的,一个变量可以是全局的,也可以是局部的,这取决于你在哪里声明了它。例如,如果你在一个函数中声明一个带有var关键字的变量,它将成为一个局部变量,这意味着它将只存在于函数中。如果你在函数外用同样的var关键字声明它,这个变量将成为全局变量。在新的引擎下,我们现在可以使用let和const关键字来创建一个变量。使用let和const的一大优势是它们已经在可读性方面提供了宝贵的信息。
因此,当你在一个表达式中看到一个let或const变量时,你会收到关于该变量的使用以及是否可以重新分配的额外信息。然后 var 变量则不提供任何这类信息。let和const的第二大优点是它们声明了变量的范围,这意味着它们将只存在于声明它的表达式中,类似于在函数中声明的局部变量。让我给你看一个例子。
在合成中创建一个文本图层,并在源文本属性的表达式编辑器中这样写。
然后创建一个新的合成,并创建一个文本层,并在源文本属性中写上这个内容。
如你所见,我们重新使用了这个函数来获取一个对象的值和方法。$对象储存了你项目的表达式系统的各种信息,里面有全局对象,我们可以看到它在后台保留的变量。我们可以看到,之前用var创建的组成的Variable1在那里,但是没有其他变量。我们还可以看到,上面表达式中的变量x也在那里。我们已经证明了用let和const创建的变量不会比它们在表达式中的所在地走得更远。好了,让我们确认这一点,并重写这个实际表达式。
我们现在用let关键字声明了变量x,也把它移到了函数里面,但是没有任何变化。
让我们尝试用删除关键字从引擎中删除这些变量。
通过用let关键字声明x变量,我们能够删除它,但不能删除用var声明的Variable1。 看到这一点,关键是即使这些变量不能使用,它们仍然存在于后台。对于小的表达式来说,这并没有太大的影响,但是当你有很多复杂的表达式时,这可能是拖慢计算速度的根本,因此使用now let和const来声明一个变量,不仅对可读性有用,而且当你想有更好的性能时,对优化也很有用。
在本书中,我们已经看到了如何创建自己的对象,以及如何在表达式中使用这个对象的值(或属性/赋值)。我们还看到了对象,就像层一样,可以用thisComp对象中的layer()方法来调用。既然我们已经知道如何创建一个函数来对值进行操作,那么我们就来学习如何使用函数来创建单个或多个对象。
使用this关键字可以让我们创建一个可定制的对象。正如你所记得的,这采用了对象容器的形式,因此我们从函数中创建的对象将从函数中输入的参数中继承其值。然后通过使用new关键字,我们可以从同一个函数中创建单个或多个对象。如果我们需要在同一个AE属性中创建多个对象,这种方法就会非常有用。通过这个新的表达式引擎,你也可以使用类构造函数的方法来创建一个对象。
这个新引擎带来的对象的另一个增强是向一个对象添加值(或属性/赋值)的简单性,而不一定要使用一对nameValue:value。现在你可以直接用这种方式向一个对象添加一个值。下图左边旧引擎,右边新引擎:
随着箭头函数的实现,函数的编写方式也得到了简化。看看这个例子。
更易读了:
构建函数的另一个新功能是默认参数的存在。在旧的引擎中,我们不得不在函数中使用if语句,以防在调用函数时没有插入参数。
旧引擎:
现在我们有两种新的方式来声明一个参数的默认值。
新引擎:
如你所见,我们可以使用||逻辑运算符在函数内部声明默认参数,也可以直接在声明参数的括号内声明。
rest 和 spread 运算符......是简化表达式和避免额外步骤的另一种新方法。rest运算符在函数中特别方便。为了理解这一点,我们首先需要看到我们以前没有遇到过的东西:参数项。这个项会产生一个数组值,可以在函数内部用来存储函数的参数。
rest运算符有点类似,但不同的是,你不必定义你的函数可以接受多少个参数。
spread运算符与rest运算符相反,将数组值转化为单数值。
我们已经创建了一个可以输入多个值作为参数的函数。现在我们想知道这些数字之间的最高数值。为此我们可以使用Math.max()方法,但是它只接受数值,我们可以使用spread运算符将数组值转换为数值来解决这个问题。
反构赋值是一种简单快速的方法,可以从一个现成的对象或数组中创建变量。
首先创建一个新的文本图层,并键入TEXT,然后在源文本属性的表达式编辑器中写下以下内容。
现在看看当我们把返构赋值写成第一条语句时会发生什么。
可以看到,现在的值是从位置对象继承的,而不是从表达式所在的源文本对象继承的。
同样的方法也适用于数组值。
for / of
在这个新的引擎中,for / of语句是我们可以添加到我们的工具列表中的一个循环,它可以在一个对象中循环。它与for / in语句非常相似,然而for / of语句更有目的性地循环一个字符串值或一个数组值。
现在让我们把本章所学的新工具用起来,看看新的表达式引擎将如何显著提高表达式的性能。我们要做一个高级表达式,在旧的引擎中,这个表达式不仅难以执行,而且计算时间也会更长。
我们还要利用这个例子来使用新的路径表达式术语,这样我们就可以控制一个蒙版或一个形状的点。在这个例子中,我们将对一个蒙板进行操作,但这可以在任何类型的路径上工作,如形状层。
首先,我们要了解路径在表达式中的工作原理。在一个新的构图中,添加一个固态层。在这个固态层中,只要画出或添加任何一种蒙版,这样蒙版文件夹就会出现在层次结构中。
现在我们可以看到蒙版的路径属性了,让我们打开它的表达式编辑器,选择表达式语言菜单中的createPath()方法。
这个来自蒙版路径对象的creatPath()方法比看起来要简单。首先要记住的是一个数组值可以包含其他数组值,而我们正是需要用这个方法来填写参数的类型。让我们看看细节,看看需要哪些参数。
此时我们通过creatPath()方法生成了一个正方形,但我们要生成一个圆,其实我们可以生成几个。我们知道每个Mask Path属性只能生成一个路径,但是如果重复了带有表达式的Mask,也会重复圆的蒙版。通过添加一个随机的方法,它可以自动把重复的圆放在一个新的位置。这个createPath()方法需要很多参数,我们希望能够控制圆的位置和半径,前面我们看到一个对象构造函数可以创建一个新的对象,我们可以在其中输入参数,所以我们尝试使用一个对象构造函数来实现。
我们刚才通过使用一个对象构造函数在合成的中间创建了一个圆。在这个名为circle的对象构造函数中,我们添加了x、y和直径三个参数,可以为其添加一个函数。我们使用原型的方法添加了这个函数,原型方法是一种向对象构造函数添加值或方法的方式。我们本可以像添加其他参数一样添加它(this.create = function()),但我想展示一下这个方法,这样当你遇到它的时候就不会迷茫了。现在我们有了一个可以生成圆的对象构造函数,你可能已经猜到了,如果我们随机化x和y参数,当复制多个蒙版时,它将会分散构图上的圆圈。如果你这样做,你是完全正确的,但是因为我们要全面测试新引擎,所以我们要更进一步!让我们假设复制的圆圈会分散在不同位置,但永远不会相互重叠。
让我们一步步来看看我们要在这个表达式中加入什么。
首先,我们需要做的是通过for循环创建尽可能多的圆。这个循环将为每个圆生成新的x、y和直径,并在最后通过push()方法将来自对象构造函数的新圆添加到一个数组值的圆中。当输入参数时,该方法可以将新的值添加到一个数组中。数组圆将包含所有的圆,所以圆 1 将是 circles[0],圆 2 将是 circles[1],等等。一个数组值可以包含值,也可以包含对象。首先要注意的是变量i,它是指执行for语句的循环次数,在我们的例子中是4000次。这是一个很大的数字,但是因为我们将随机地调整圆的位置,如果我们想找到不重叠的位置值,我们需要创建足够的选项,因为我们将只保留不重叠的位置。如果我们创建了10个圆,其中8个圆与两个圆重合,那么我们最终在合成上将不会有足够的圆,因此我们需要一个大的数字。
第二件要注意的事情是,这个循环是反过来写的(不是for(i=0;i<=4000,i++)),也就是说它是向后数的。这是一个众所周知的技巧,可以让循环的速度更快。你应该明白,循环语句是表达式中最耗费热量的,我们的循环将有4000次迭代,因此如果我们想要好的性能,我们现在需要考虑优化。
现在我们需要添加一个随机方法,这样每一个新生成的圆都会有一个不同的位置。使用我们知道的方法,如wiggle()或random()方法的主要问题是,它们会在不同的属性上生成不同的值。即使你使用seedRandom()方法,我们仍然需要一个随机方法,它将在任何存在的地方创建相同的值。通过使用i变量创建一个名为random2的随机函数,对于每一次迭代,我们将有新的值,这些值将在4000次迭代中是相同的。然而这个函数产生的是0和1之间的十进制数值,如果我们想要的值能够适配在合成内,我们需要转换它们。要做到这一点,我们可以使用linear()方法来获得相同的结果,但在这种情况下,我使用了一个自定义函数,这样你就可以看到一种不同的方式将值转换为新的值范围。最后,我们创建了变量,我们将根据我们为随机性创建的函数在对象构造函数中作为参数输入。你还应该注意到,我们希望数值介于圆的直径和构图的宽度和高度之间,这意味着生成的位置将始终在合成内。
然后我们需要使用上面的函数,如果两个圆是重合的,基本上会产生一个真值,否则会产生一个假值。我们不能使用length()方法,因为它只能给我们提供两点之间的距离,而且圆也有一个半径,要加上它们之间的距离。
最后一个我们需要添加到我们的表达式中的函数就是上面这个。你可以看到,这是一个使用我们刚才创建的重叠函数的循环。每次循环产生一个新圆的位置,这个函数将在每个已经创建的圆中测试新位置。当新的位置没有与一个圆重合时,它将在数组总数中添加一个假值。如果位置与一个圆重叠,它将在数组总数中添加一个真值,然后离开循环,因为我们不需要用更多的圆来测试它,这意味着如果新的位置与已经创建的一个圆重叠,我们就不想使用这个位置。
最后我们有if语句。首先我们将最后一个变量赋值给前面函数的结果,它用现有的圆圈测试新的迭代。你可以看到这是一个数组,它包含了它与已经生成的圆圈进行的每一次重叠测试的false和true值。如果这个数组值不包含一个真值--重叠,它就会创建圆。如果它包含一个真值,它就不会做任何事情,并移动到下一个迭代。我们能够使用新的includes()方法来实现这一点,它检查在方法中作为参数输入的值是否存在于数组中,在我们的例子中,我们想知道这个数组是否包含一个真值。如果有就会产生一个真值,否则就会产生一个假值。
最后,正如我所提到的,循环语句是表达式中最耗费精力的计算,你要尽可能地避免有更多的循环。当使用这个if语句,如果生成的圆的数组索引号与层次结构中蒙版组的索引号相同,它就会离开循环。所以比如当它在Mask 2上生成圆2时,就不会计算下面的圆。这样做可以避免不必要的计算,这意味着更好的性能。
现在让我们把蒙版复制20次左右,看看结果。
成功了,而且它的反应也很灵敏。我们可以看到,如果我们一直复制更多的圆圈,最终会出现错误,这是因为如果我们想要更多的圆圈,我们必须创建更多的选项,意味着更多的迭代。要做到这一点,你只需要将循环迭代次数增加到一个更高的数字。但在我们的例子中,我们将保持在4000。对象构造函数的好处是,所有的东西都只取决于一个表达式。比方说,你想修改一些参数,你可以简单地删除所有复制的蒙版,并编辑蒙版1中的表达式,然后再次复制。例如,我们也可以用同样的随机函数随机化圆的直径。
现在,为了更进一步,我想添加最后一个条件,我想让圆圈只显示在一个路径内。添加一个新的蓝色固态层,并在其中绘制一个圆形蒙版。
我们将使用这个路径作为我们圆的形状容器。我们可以使用任何一种形状。无论你决定使用像我这样的圆圈形状还是其他形状,但请确保你在路径上添加大量的点,这将使表达式引擎更加精确。现在让我们编辑一下我们的表达式。让我们添加一个变量来存储这个路径的点的值。
现在,我们必须再添加一个函数来实现这一目标。我们需要一个函数来说明一个点(x,y)是否在路径内。
当我们把主循环生成的x和y位置作为第一个参数,把我们创建的形状变量作为第二个参数输入时,函数会告诉我们这个点是否在路径内。知道了这一点,当函数产生真时,用一个简单的if语句就可以创建一个新的圆,否则它什么也不做并开始一个新的迭代。
最后我们要限制迭代,以便只在形状所在的区域内创建值,因此它增加了寻找存在于路径内的圆的选项数量。否则循环可能会生成存在于这个区域之外的圆圈位置,但由于我们不需要它们,这些迭代就没有用了。通过在表达式中添加这个选项,它可以定位哪些是形状在x和y上的最小值和最大值--因此限制了生成位置的区域。你可以注意到,我们使用了spread算符,这确实让我们避免了额外的步骤。最后,我们可以将其落实到我们的变量上。
最终表达式应该是这样的:
如果我们复制Mask 1,大概20次左右,我们可以看到它的结果很好,而且反应还是很灵敏的。另外,如果我们修改形状的路径,我们可以看到它会自动响应,圆圈只会分布在新的形状里面。
运行这个测试后,你会惊奇地发现,通过使用新引擎和它的新工具,再加上养成良好的习惯,生成这个高级表达式的计算最多只需要11秒左右。如果我们要尝试在旧引擎中做出一个类似的表达式,在没有新术语的情况下,这个的计算将持续4分钟。这是一个显著的差异。我们可以看到,得益于这个新引擎,我们可以更进一步,在表达式中拥有更多的控制力和影响力。
全书完。