这篇文章里面讲的内容非常简单,又比较实用,而且很常用,它是一种可以自由设置的转场,基于表达式和形状图层,而不是效果,因此你可以用它来制作很多比较平凡又不那么平凡的转场,不管是在 AE 中还是 PR 中,甚至任何软件里面都可以使用,下面的视频仅枚举了最简单的设置。
除了实用性,本篇文章也将带你学习表达式在处理空间和时间上的一些基本用法,都是非常简单的,数学知识不超过小学二年级范围,学习时间大概半个小时。另外想起来,我前两天翻出来我那台十年前的 Thinkpad W520,因为配置很老,只能运行 AE 2018 版本,然后发现在旧版本上并不支持新的 JavaScript 引擎,但是我现在这个专栏中的所有表达式都是基于 JavaScript ES6 标准的,如果你还在用旧版本,建议升级到 2021 或最新版本。
一个 Comp 或者一个 Layer 之所以被我称为控件,就是因为它是参数控制的,通过参数控制或者一些简单的复制、删除等操作就可以自动生成出同类但不相同的效果,控件的好处就在于复用性,特别像转场这种东西,你可能需要不断的调用它,但是又不像总是重复相同的效果,所以使用控件或者可以自定义的转场效果都是不错的选择,相对于转场的效果或插件,使用控件主要是可以避开前者的一些限制,而且通过 Alpha 遮罩控制的转场也可以更灵活。如果你做出一批画质很好,尺寸很大的预设转场,使用时直接调用渲染好的视频,也可以缓解软件的运行压力。
先说一下这个控件的产生背景是一位粉丝私信我关于随机控制时间节点的问题,他的代码思路基本上没错,只是在命令上多打了一个字母,所以报错了。我从他的这个事情上获得一些灵感,就做了这个控件以备后用。这个控件的核心是被分割的画面进行透明度过渡,但是它们执行的时间是随机的,这是让转场效果看起来不那么呆板的核心。



三次转场只是分割的块不同,当你把它看作一个网格,然后只考虑存在几行几列就很容易理解了。直接看工程中的结构。

这是对应最上面那个竖条转场的,所以它有 1 行,行中存在 6 个块,按表格方式理解的话即 1 行 6 列,行列都是按照合成宽高平均分布的。我们需要实现计算的主要是行和块的大小和位置。

为了可以让每一个块都可以单独控制透明度,必须把块的路径和填充放在组中包裹起来,现在来看一下表达式所在属性的层级索引:

计算列数即查找 [4] 内容 中的数量,计算行数则查找 [6] 内容 中的数量。这部分还不知道的请继续复习 propertyGroup() 部分的知识。
getCellSize: () => {
let w = thisComp.width / thisProperty.propertyGroup(4).numProperties;
let h = thisComp.height / thisProperty.propertyGroup(6).numProperties;
return [Math.ceil(w), Math.ceil(h)]
},
块的宽度即列宽,等于合成宽度除以列数;块的高度即行高,等于合成高度除以行数。为了避免不能整除得到的画面出现缝隙,我们都用 ceil() 方法对结果进行上舍。下面再来计算一下每一个块(单元格)在行中的位置:

这里再一次用到了属性层级索引:

// 获取列的位置函数
getCellPosition: () => {
// 指定当前的位置,就是第几列
let current = thisProperty.propertyGroup(2);
// 统计出一共有多少列
let cells = current.propertyGroup(1).numProperties;
// 求出列宽,注意这里的列宽是没有进行上舍的
// 因此也不能引用矩形路径大小,不然就不能填补缝隙了
let w = thisComp.width / cells;
// 按照位置索引进行偏移计算
// 每一个偏移的倍数都比自身的索引少 1 倍列宽
let x = w * (current.propertyIndex - 1)
// 整体向左偏移
// 也可以写成下面的等效公式
// x -= h * (cells - 1) * .5;
x -= (thisComp.width - w) * .5;
return [x, 0]
},
看图分解:

在没有整体向左移动的时候,需要移动的量就是用红色矩形标记的部分,只要第一个块的位置没有变,那么跑到合成外面的部分永远都等于列数减 1 的总宽度的一半,就是被我注释掉的那行,也可以理解为用合成宽度减掉一个列宽再除以 2,在计算行定位的时候是完全相同的算法。
// 获取行的位置
getRowPosition: () => {
let current = thisProperty.propertyGroup(2);
let rows = current.propertyGroup(1).numProperties;
let h = thisComp.height / rows;
let y = h * (current.propertyIndex - 1);
y -= (thisComp.height - h) * .5;
return [0, y]
},
至此我们要简化代码的话,可以把相同的算法写成一个函数再调用:
// 获取列的位置函数
getCellPosition: () => {
let x = myClass.getOffset(thisComp.width)
return [x, 0]
},
// 获取行的位置
getRowPosition: () => {
let y = myClass.getOffset(thisComp.height)
return [0, y]
},
// 计算偏移
getOffset: (total) => {
let current = thisProperty.propertyGroup(2);
let quantity = current.propertyGroup(1).numProperties;
let ave = total / quantity;
offset = ave * (current.propertyIndex - 1);
offset -= (total - ave) * .5;
return offset
},
然后是随机时间的不透明渐变,意思就是每一个块进行渐变的开始时间,结束时间,持续时间都是随机的,但是又保证了它们在某一个相同的时间点完全不透明,这个点就是剪辑时两个画面衔接的剪辑点,我已经使用了一个 marker 作为时间标记:

// 随机时间的不透明度渐变
getRndOpacity: (start, end, range, smooth = true) => {
seedRandom(index, smooth);
value = ease(time, start - random(0, range), start + random(0, range), 0, value);
value = ease(time, end - random(0, range), end + random(0, range), value, 0);
return value
},
原理非常简单,range 用来控制随机范围,这样可以保证每个时间点偏移在可控范围内,start、end 则用来控制动画开始和结束的基础时间点。

eval(footage("myClass.jsx").sourceText)
myClass.getRndOpacity(thisComp.marker.key(1).time-framesToTime(effect("时间随机程度")(1)),
thisComp.marker.key(1).time+framesToTime(effect("时间随机程度")(1)),
framesToTime(effect("时间随机程度")(1)),
effect("平滑过渡")(1))
实际调用的时候并没有让过渡完成的部分保持一段时间,而只是选取了一个点,这样可以节约比较多的资源,使用转场的时候配合时间重映射就可以了。
myClass = {
// 获取单元格尺寸
getCellSize: () => {
let w = thisComp.width / thisProperty.propertyGroup(4).numProperties;
let h = thisComp.height / thisProperty.propertyGroup(6).numProperties;
return [Math.ceil(w), Math.ceil(h)]
},
// 获取列的位置函数
getCellPosition: () => {
let x = myClass.getOffset(thisComp.width)
return [x, 0]
},
// 获取行的位置
getRowPosition: () => {
let y = myClass.getOffset(thisComp.height)
return [0, y]
},
// 计算偏移
getOffset: (total) => {
let current = thisProperty.propertyGroup(2);
let quantity = current.propertyGroup(1).numProperties;
let ave = total / quantity;
offset = ave * (current.propertyIndex - 1);
offset -= (total - ave) * .5;
return offset
},
// 随机时间的不透明度渐变
getRndOpacity: (start, end, range, smooth = true) => {
seedRandom(index, smooth);
value = ease(time, start - random(0, range), start + random(0, range), 0, value);
value = ease(time, end - random(0, range), end + random(0, range), value, 0);
return value
},
// 生成随机颜色
getRndColor: () => {
seedRandom(index, true);
return random([0, 0, 0, 1], [1, 1, 1, 1]);
}
}
完整代码贴到一个文本文档里面,可以直接导入到 AE 中,然后使用 eval() 方法运行。