【AE表达式】createPath创建可控制弹性绳 //用于MG弹跳动画


/*表达式文末自取*/
关于弹性运动网上都有表达式可以直接拿来用的
本文就不讲如何用表达式实现弹性运动了/*主要是我也不会,也许以后会*/
//其实文章标题应该叫做 表达式控制正弦函数图像的各种变化

分析
小球的弹性运动直接上表达式:/*反正我也是抄的*/
a=2;//振荡频率
b=2;//衰减率
n=0;
if (numKeys>0){
n=nearestKey(time).index;
if (key(n).time>time) n--;}
if (n>0){
t=time-key(n).time;
amp=velocityAtTime(key(n).time-.001);
w=a*Math.PI*2;
value+amp*(Math.sin(t*w)/Math.exp(b*t)/w);}
else{value}
//添加关键帧动画后才有效果
绳子部分使用createPath创建正弦函数路径,并使绳子末端跟随小球运动,顶端不动
/*如果你不会用createPath,我这有个好康的cv6759435
后面我也会提一下,就只提一下,不能提多了,毕竟文章中心是控制正弦曲线
总之这个表达式非常好玩*/

目标
1、 可改变弹性绳的圈数和宽度/*频率和振幅*/

2、 通过移动两个控制点操控弹性绳位置与水平长度
同时绳子自身长度不变//近似不变

3、 曲线折线的切换

4、 弹性绳的相位变化


思路
控制振幅和频率:建立y=lon*sin(fx) 的函数,lon:振幅,f:频率
控制位置:
为函数图像上所有点加上首点/*第一控制点*/的坐标
读取两控制点的间距控制图像水平距离//以便图像上最后一个点与末点/*第二控制点*/重合
通过两点坐标差的商求出图像整体需要旋转的角度
//顺手做个动画以便理解

保持绳长固定:
通过弧微分公式//你确实可以这样做,但之后你会为了求个弧长自学椭圆积分、阶乘Γ函数、高斯常数…先不说浪费时间,做个动画也不至于这么卷
曲线长难求,可以用直线代替
让x只取 π/2 倍数,就可以得到这些位置的点

//createPath的画图逻辑就是描点连线,所以目前只是折线
再用勾股定理表示一段直线的平方:lon^2+(T/4)^2 确保该式为定值//T为图像周期水平长度
曲线/折线转换:无疑就是是否有切线的区别,使用if判断语句
相位变化:自变量x后面加参数c以控制相位

实现
在一切的开始,请务必用表达式把图层的PSR属性锁好,因为鼠标很容易误操作改变图层形态,导致函数图像位置错误

//所有表达式写在一个形状图层中,这样可以方便复制到别的合成里

初始设置
/* createPath(a,b1,b2,c)共四个参数,使用于路径中
a为二维数组(必要),b1出点数组,b2入点数组,c为true/false表示是否闭合路径
a = [ x , y , z …] x y z等 是点的位置/*位置是含有两个元素的数组*/,从左向右读取连线 */
创建形状图层,效果面板里添加并重命名一些表达式控制//也可以先把名字命名为参数

钢笔工具在视图窗中画个点,内容中就会出现形状 1
然后按住Alt点击路径前面的圈,打开表达式编辑框

先定义几个变量:
f=effect("频率")("滑块");//最重要,不能<=0
p=effect("点密度")("滑块");//表示单位长度内点的数量有点鸡肋
a=effect("宽度")("滑块");//用于控制振幅lon
c=effect("相位")("滑块");
x1=effect("首点")("点")[0];y1=effect("首点")("点")[1];//用于控制位置和方向
x2=effect("末点")("点")[0];y2=effect("末点")("点")[1];
pots=[];ins=[];outs=[];//要传给createPath的二维数组
lon=100;hoz=50;m=1;n=10;//先定义几个待会要用的参数

建立函数
创建一个正弦函数:
function fx(t){
x = t;
y = lon * Math.sin( f * t );//函数lon*sinfx
return [ x , y ]+[ x1 , y1 ];//所有点受首点控制
}
使用时只需要输 fx/*方法名,随便取*/(0),就返回x=0时点的位置
但一个一个输x值太麻烦,就加一个for循环自动取点:
for (i=0;i<=n;i=i+1){//n表示点数,之后由别的参数进行控制
w = Math.PI/2/f; //只取x = π/2 倍数的点,除以f是因为图像上两点间距与f成反比
pos = fx(i*w);
pots.push(pos); //每次循环向数组pots 添加一个点的位置
}
最后写:
if (effect("切线")("复选框")==0){
ins = [] ; outs = [] ;
}//如果复选框没勾选的话,就让ins和outs成为空数组,就没有切线
createPath( pots , ins , outs , 0 )
于是你得到了一条只有聪明人才看得见的sinx函数曲线

曲线水平方向太小,是因为点的x值 = i/f*w ,这三个参数都是个位数
所以画出来的图像上相邻两点间距很短//大概就几个像素

放大水平长度
解决方法就是让函数输出的x值乘一个参数hoz:
function fx(t){
x = t;
y = lon * Math.sin( f * t );//函数lon*sinfx
return [hoz * x , y ] +[ x1 , y1 ];//hoz 控制水平长度
}

位置控制
现在开始进入正题
要让图像水平长度跟随首点和末点的距离变化,就让这两个量进行联系
让参数 l 表示两点间距:
l=Math.sqrt(Math.pow((x2-x1), 2)+Math.pow((y2-y1), 2));
要使图像最后一个点与末点重合,l 就应为图像周期长度的倍数
根据函数求得周期长度为 hoz*2π/f
/*代码函数本身的周期长度并不是这个,由于代码函数输出的点为[hoz * x , y ],做出图像的函数实则变了y = lon * sin( f * x / hoz ),该函数才是真正作图的函数,知道这个对写导函数有大用*/

我们还希望频率f 为1时,首末点间只有一个周期长度图像,为2时有两个
则可以让l = hoz*2π,只需要在function前面输:
hoz=l/2/Math.PI;
hoz解决了,但还有点数n的问题,没有足够的n就不能生成足够长的线
n应与l和f成正比,但n与f的关系更明显
f为1时,首末点间只有一个图像周期的所有点,点数为5,n=4
//因为在for循环取值时i取0、1、2、3、4刚好5个点
f为2时,首末点间有两个图像周期的所有点,点数为5+4=9,n=8
很容易看出关系,把之前的n改为:
n = 4 * f ;

方向控制
但现在还不能让图像跟着首末点跑,它还不能旋转
进入内容-形状 1-变换-旋转表达式编辑界面
/*选中图层按快捷键R的是图层旋转属性,它应该被表达式锁住的
这个是形状 1的旋转属性*/

直接输:
x1=effect("首点")("点")[0];y1=effect("首点")("点")[1];
x2=effect("末点")("点")[0];y2=effect("末点")("点")[1];
dy=y2-y1;dx=x2-x1;//xy坐标差求角度
radiansToDegrees(Math.atan2(dy, dx))
//原理上文的动图解释得很直观了

锚点跟随首点
但移动控制点时就发现图像并不以控制点为中心旋转
/*这不废话?图像肯定绕锚点旋转啊*/
改变锚点的参数只会移动图像,改变位置的参数则锚点和图像一起动
于是在锚点和位置属性里同时输入:
x1=effect("首点")("点")[0];y1=effect("首点")("点")[1];
[x1,y1]//首点控制路径
让锚点跟随首点的同时不改变位置,现在图像可以被完全操控了
//如果还不行就重新检查图层的位置和锚点属性是否都为[ 0 , 0 ]

绳长固定
根据思路用直线代替弧长
让图像中一段直线长度的平方:lon^2+(T/4)^2成为定值
于是假设这个定值为a*k/*k为常数*/图像周期长度T/4为l/(4*f)
则lon^2+( l/(4*f))^2=(a*k)^2 ,于是输入:
lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));
//经过调参决定把k设为100,
//AE中Y轴正方向是向下的,所以lon改为负值

生成切线
目前为止我们只画了个折线,形成曲线得添加切线
切线就是滑杆或手柄//会用钢笔工具的你一定知道手柄是什么
手柄有出点和出点,这里只需要确定出点的位置然后传给数组ins
//“出点的位置”指出点相对与父点的位置,并非其世界坐标

父点就是在图像上的点,我们要基于这些点确定出点的位置
要确定出点位置就应先知道切线斜率
切线斜率用导数公式求,函数代码下方输入导函数:
function dy(t){
k=lon*f*Math.cos(f*t)/hoz;//求斜率
x=-Math.cos(Math.atan(k)); //因为是出点,所以为负
y=-Math.sin(Math.atan(k));
return [x,y]; //输出出点的位置
}
/*根据作图的函数y=lon*sin(f*x/hoz)求出导函数lon*f*cosfx /hoz
注意作图函数自变量为x/hoz,而导函数自变量为x,不理解就别理解吧*/
同理,用for循环把所有出点位置传给数组ins:
for (i=0;i<=n;i=i+1){
w=Math.PI/2/f;
pos=fx(i*w);inp=dy(i*w);//给函数和导函数的x值一样
pots.push(pos);ins.push(inp);outs.push(-inp);//入点位置就是出点位置的反方向
}
现在打开效果控件里的切线开关,就可以得到聪明人也看不见的切线了

切线长度
还是同理,切线长度只有一个像素左右大,于是改导函数:
function dy(t){
k=lon*f*Math.cos(f*t)/hoz;//求斜率
x=-Math.cos(Math.atan(k));
y=-Math.sin(Math.atan(k));
return [m*x,m*y]; //m控制切线长度
}
m应随着l的增长而增长,随着f的增加而减短
所以输入:
m = o * l / f;
//o为常数,建议为参数o创建一个滑块控制,调节图像与由描点连线形成的图像进行契合,找出最佳契合常数,大概是0.1左右吧
你可能会觉得m应该也与振幅控制系数a有关,但如果不管a的影响的话,契合误差还是比较小的
/*图中灰色为由切线作出的图像,浅绿色是描点连线的图像
贝塞尔曲线不可能完全契合sinx函数图像*/

相位
注意函数和导函数都要加相位控制参数c:
function fx(t){
x=t;
y=lon*Math.sin(f*t+c/*相位*/);
return [hoz*x+x1,y+y1]
}
function dy(t){
k=lon*f*Math.cos(f*t+c/*相位*/)/hoz;
x=-Math.cos(Math.atan(k));
y=-Math.sin(Math.atan(k));
return [m*x,m*y];
}

建立点密度/*可略过*/
点密度p就是单位长度内点的数量,在一些极端情况/*比如振幅极大时*/可以通过增加点密度让曲线生成的图像更像sinx函数图像

原理就是控制图像上相邻两点水平间距:
for (i=0;i<=n;i=i+1){
w=Math.PI/2/f/p;//使p增大时两点间距减小
pos=fx(i*w);inp=dy(i*w);
pots.push(pos);ins.push(inp);outs.push(-inp);
}
图像上两点间距减小,那点数n也就要增加:
n=f*p*4;
点数n多了,切线长度m就要减小:
m=o*l/f/p;//o为由你调出的常数
/*点密度p可能有点鸡肋,但还是建议加上它,说不定就用上了*/

参数控制
做到这里基本结束了,接下来优化代码,处理一些报错的情况

心血来潮把点密度p调得过大导致点数过多,使循环次数过多导致电脑爆炸
同理f也是,使用clamp限制其数值:
p=clamp(p,20,0.1);
f=Math.round(clamp(f,20,1));
//f取整使首末点始终位于图像中间,f必须>0
心血来潮让首末点间距过长则发生报错,因为首末点间距过长时
振幅公式lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));
中的a*100-Math.pow(l/4/f, 2)成为负数,则Math.sqrt()不能计算
用if判断语句解决:
if (a*100 < Math.pow(l/4/f, 2)){
lon=0;
}
else{
lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));
}
使当l过长时振幅lon为0,形成一条直线

总结
后来觉得可以改进绳长固定的代码,于是用微分的方法把弧长确定到了个位
但算出了弧长后不知道怎么根据弧长控制振幅lon,就傻了

发现自己花了一堆时间整出个没用的sin弧长计算代码
//本来连图都画出来了的……

前天看了大佬的三篇文章cv6759435,从而了解createPath的使用方法
于是前天晚上就有一个这样的想法,当天半夜写出了个大概,昨天进行代码各种功能的完善,下午加晚上写文章和做gif动图,今天用一堆时间整弧长
/*总之createPath这个表达式非常好玩*/

感觉这些表达式可以用在很多mg动画的方面,不仅仅是弹跳动画
之后会稍微研究一下下面的两种弹性表达式

代码/*与上文有一点出入*/
地面反弹
e=effect("弹力系数")("滑块");
g=10*effect("重力 *10")("滑块");
nMax=effect("最大反弹次数")("滑块");
n=10;
if(numKeys>0){
n=nearestKey(time).index;
if(key(n).time>time)n--;}
if(n>0){
t=time-key(n).time;
v=-velocityAtTime(key(n).time-.001)*e;
vl=length(v);
if(value instanceof Array){
vu=(vl>0)? normalize(v):[0,0,0];}
else{vu=(v<0)?-1:1;}
tCur=0;
segDur=2*vl/g;
tNext=segDur;
nb=1;//Number of bounces
while(tNext < t && nb <= nMax){
vl*=e;
segDur*=e;
tCur=tNext;
tNext+=segDur;
nb++}
if(nb<=nMax){
delta=t-tCur;
value+vu*delta*(vl-g*delta/2);}
else{value}
}
else{value}
弹性振荡
a=effect("振荡频率")("滑块");
b=effect("衰减率")("滑块");
n=0;
if (numKeys>0){
n=nearestKey(time).index;
if (key(n).time>time) n--;}
if (n>0){
t=time-key(n).time;
amp=velocityAtTime(key(n).time-.001);
w=a*Math.PI*2;
value+amp*(Math.sin(t*w)/Math.exp(b*t)/w);}
else{value}
/*两个弹性表达式都是网上找到的其中一种,都需要添加关键帧动画*/
弹性绳路径
f=effect("频率")("滑块");
p=effect("点密度")("滑块");
a=effect("宽度")("滑块");c=effect("相位")("滑块");
x1=effect("首点")("点")[0];y1=effect("首点")("点")[1];
x2=effect("末点")("点")[0];y2=effect("末点")("点")[1];
pots=[];ins=[];outs=[];
/*设置参数阈值,避免报错*/
p=clamp(p,20,0.1);f=Math.round(clamp(f,20,1));
/*参数控制*/
n=f*p*4;//点数自动改变
l=Math.sqrt(Math.pow((x2-x1), 2)+Math.pow((y2-y1), 2));//首末点距离
hoz=l/2/Math.PI;//水平距离由l控制
if (a*100 < Math.pow(l/4/f, 2)){lon=0;}//防止因l过长的报错
else{lon=-Math.sqrt(a*100-Math.pow(l/4/f, 2));}//振幅控制,使线段总长一致
m=-0.1001*l/f/p;//切线长度调节
/*函数部分*/
function fx(t){
x=t;
y=lon*Math.sin(f*t+c/*相位*/);//函数lon*sinfx
return [hoz*x+x1,y+y1]//曲线从首点出发,用hoz放大水平长度
}
function dy(t){
k=lon*f*Math.cos(f*t+c)/hoz;//导数lon*f*cosfx,得出切线斜率
x=Math.cos(Math.atan(k));
y=Math.sin(Math.atan(k));//返回入点位置
return [m*x,m*y];//m调节切线长度
}
/*通过循环获取点*/
for (i=0;i<=n;i=i+1){
w=Math.PI/2/p/f;//两点间距
pos=fx(i*w);inp=dy(i*w);
pots.push(pos);ins.push(inp);outs.push(-inp);
}
/*切线控制*/
if (effect("切线")("复选框")==0){
ins=[];outs=[];
}
/*创建路径*/
createPath(pots,ins,outs,0)
//关于形状属性的表达式详见方向控制

2022.6.15 写了脚本,粘贴到记事本里保存,后缀改成.jsx就能用了