一、效果分解
慢动作分解一下上面的视频效果,可以看到图片入场沿着从左上角至中心点的曲线位移,加上一个运动模糊来模拟快速进入然后减速的效果,同时会有一个弹性效果。

二、沿贝塞尔曲线移动
通过分解可以看到图片进入显示区域的轨迹是一条类似如下图这样的曲线:

在数学中可以使用三次贝塞尔曲线来表达这样的曲线,三次贝塞尔曲线的公式如下:
类似的曲线还有圆弧线,但是贝塞尔曲线更灵活通用,且x坐标刚好适配在[0,1]这个区间之间。
确定曲线的端点,这里的坐标系y轴和WebGL坐标系y轴方向相反,因此记得对y做一下换算。

得到WebGL坐标系中四个控制点:p0 = vec2(0.4,0.2), p1 = vec2(0.5,0.303), p2 = vec2(0.5,0.362), p3 = vec2(0.5,0.5)。
Shader中增加Bezier曲线的公式:
float Bezier(float p0, float p1, float p2, float p3, float t) {float x0;float x1;float x2;float x3;x0 = p0 * pow((1. - t), 3.);x1 = 3. * p1 * t * pow((1. - t), 2.);x2 = 3. * p2 * pow(t, 2.) * (1. - t);x3 = p3 * pow(t, 3.);return x0 + x1 + x2 + x3;}vec2 getBezierPoint(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float progress) {return vec2(Bezier(p0.x, p1.x, p2.x, p3.x, progress),Bezier(p0.y, p1.y, p2.y, p3.y, progress));}
根据当前的动画进度拿到图片的当前的位置,计算出位移,并从图片上取到对应点:
uniform float progress;uniform sampler2D inputImageTexture;...vec4 getColor(vec2 position) {// scalarRatio为图片缩放比例position = (position - vec2(0.5, 0.5)) / scalarRatio + vec2(0.5, 0.5);return texture2D(inputImageTexture, position);}void main() {...vec2 p0 = vec2(0.45, 0.2);vec2 p1 = vec2(0.5,0.303);vec2 p2 = vec2(0.5,0.5);vec2 p3 = vec2(0.5, 0.5);vec2 currentPos = getBezierPoint(p0, p1, p2, p3, progress);vec2 distance = currentPos - vec2(0.5, 0.5);// 根据distance做图片平移操作vec2 pos = textureCoordinate.xy - distance;gl_FragColor = getColor(pos);}
这时图片可以动起来了,但是效果比较呆板,始终都是朝着一个方向。
为了模拟沿着曲线滑动的效果,我们沿着贝塞尔曲线的切线再给它加上一个旋转。贝塞尔曲线求切线方向向量的方法如下:
vec2 computeBezierDerivative(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float progress) { p0 = 3.0 * (p1-p0); p1 = 3.0 * (p2-p1); p2 = 3.0 * (p3-p2); return p0 * (1. - progress) * (1. - progress) + 2. * p1 * (1. - progress) * progress + p2 * progress * progress;}在位移的同时计算一个旋转量:
vec2 getRotate(vec2 pos,float angle){float s=sin(angle);float c=cos(angle);vec2 center=vec2(.5,.5);// 此处省略了图片适配canvas比例的操作...mat2 rotMat=mat2(c,-s,s,c);pos=pos-center;pos=rotMat * pos;pos=pos+center;return pos;}void main() {...vec2 p0 = vec2(0.4, 0.2);vec2 p3 = vec2(0.5, 0.5);vec2 p1 = vec2(0.5, 0.4);vec2 p2 = vec2(0.5, 0.45);vec2 currentPos = getBezierPoint(p0, p1, p2, p3, progress);vec2 distance = currentPos - vec2(0.5, 0.5);// 根据distance做图片平移操作vec2 pos = textureCoordinate.xy - distance;vec2 dir = computeBezierDerivative(p0, p1, p2, p3, progress);angle = asin(dir.x / dir.y);pos = getRotate(pos, -angle);gl_FragColor = getColor(pos);}
下图所示为施加后的效果,此时可以看到图片沿着贝塞尔曲线移动的效果更加自然了。
但是匀速移动往往看起来比较呆板没有动感,常常用到缓动曲线来使运动更加有节奏,查看更多常用的缓动函数可点击下方传送门【1】。
【1】https://easings.net/#
这里我使用的是弹性曲线,来实现一种duang~duang~的感觉,处理逻辑如下:
float easeOutElastic(float progress){ float c4 = (2. * 3.1415926) / 3.; return progress == 0. ? 0. : progress == 1. ? 1. : pow(2., -10. * progress) * sin((progress * 10. - 0.75) * c4) + 1.;}再用easeOutElastic函数处理一下progress:
vec2 currentPos = getBezierPoint(p0, p1, p2, p3, easeOutElastic(progress));

三、动态模糊实现
模糊算法是非常常用的图像处理算法之一了。常见的有高斯模糊、径向模糊、旋转模糊、运动模糊等,模糊可以在视觉上更好地造成快速运动的感觉。
在放大缩小效果中常常用到径向模糊,平移的时候则常用到运动模糊,旋转模糊顾名思义就常用在旋转的场景中了。

在示例斜切入画的效果里,图片主要是沿着曲线在走向下,因此我们给它加一个竖向的运动模糊。
// 动态模糊算法vec4 motionBlur(vec2 velocity, vec2 position) { int kernelSize = 20; float offset = 0.; int MAX_KERNEL_SIZE = 2048; vec4 color = getColor(position, index); if (kernelSize == 0) { return color; } velocity = velocity / filterArea.xy; offset = -offset / length(velocity) - 0.5; int k = kernelSize - 1; for(int i = 0; i < MAX_KERNEL_SIZE - 1; i++) { if (i == k) { break; } vec2 bias = velocity * (float(i) / float(k) + offset); color += getColor(position + bias); } return color / float(kernelSize);}void main() { ... float vy = 40. * progress; vec4 color = motionBlur(vec2(0., vy), pos);}大功告成,最终效果如下图所示:

四、结语
本文主要使用贝塞尔曲线位移+旋转+缓动曲线实现了一个照片的动态效果,加上动感的音乐就可以组合成时尚的卡点视频。同样的思路还可以实现更多的效果,比如我们经常在各种小视频上看到的“甩来甩去”的效果。
















