欢迎光临散文网 会员登陆 & 注册

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

2021-08-22 22:34 作者:Cubx  | 我要投稿


/*表达式文末自取*/

关于弹性运动网上都有表达式可以直接拿来用的

本文就不讲如何用表达式实现弹性运动了/*主要是我也不会,也许以后会*/

//其实文章标题应该叫做 表达式控制正弦函数图像的各种变化

分析

小球的弹性运动直接上表达式:/*反正我也是抄的*/

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函数图像

p=1    对比    p=2

原理就是控制图像上相邻两点水平间距:

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就能用了


【AE表达式】createPath创建可控制弹性绳 //用于MG弹跳动画的评论 (共 条)

分享到微博请遵守国家法律