关于挤压动画的一种尝试
前言
前不久在codepen看到一个点击按钮出现挤压动画的demo
,看起来很流畅,也比较简洁;
然后一看源码,使用的是GSAP
这个动画库加上svg
路径结合的,看起来SVG
的路径有点复杂。然后心里想着能不能用更简单的代码或者思路来还原这种效果,看了一些资料后,心里大概出现了几种思路:
- 方法1:尝试利用
clip-path
+animation
来实现 - 方法2:尝试利用
clip-path
+SVG clipPath animation
- 方法3:尝试利用
transform
的matrix()
进行矩阵变换 +animation
尝试
方法一:clip-path + animation
clip-path
属性用于设置裁剪区域,使得元素只有裁剪区域的部分才会显示,最关键的是clip-path
支持动画!但是经过一番尝试,clip-path
目前支持的裁剪形状并不能满足挤压动画的需求,即凹曲线;目前clip-path
支持的形状有:
inset()
:矩形;circle()
:圆形;ellipse()
:椭圆;polygon()
:多边形;url()
:引用SVG
形状;- 几何框盒;
事实上,clip-path
有个很强大的形状来源,即path()
方法,该方法可以使用SVG Path
语法来构建形状,但是该方法目前很多浏览器并不支持在clip-path
属性中使用,所以就比较遗憾了;
方法二:clip-path + SVG clipPath aimation
没错,由于clip-path
中可以使用url()
方法来引用SVG
图形,因此我们也可以借助SVG
这条思路来实现挤压所需要的形状,毕竟SVG path
语法是十分的强大,还支持贝塞尔曲线,几乎任何形状都可以绘制出来;
而具体到SVG
中就是使用clipPath
元素来声明一个裁剪区域,然后使用url(#name)
来引用即可;这看名字就知道clip-path
属性就是借鉴的SVG clipPath
了,基本用法如下:
1 | <svg id="mask" width="0" height="0"> |
1 | .demo { |
在clipPath
元素内定义的形状就是裁剪区域,除了可以使用path
,还可以使用SVG
内其他用来定义形状的元素,如:<rect>
,<circle>
等;不仅如此,还可以使用SVG animation
语法,对形状进行动画处理;但是经实践,path
的d
属性开启动画后,被引用时并没有预想中那样有插值关键帧过渡的效果,而是直接跳到最后一帧,也就是说clipPath
内的动画对于path
没有效果:
1 | <svg id="mask" width="0" height="0"> |
1 | .demo { |
像上面这种利用animate
改变<path>
元素的d
属性,在clipPath
并没有看到效果;不知道是不是用法不对,反正利用SVG animation
来改变贝塞尔曲线实现挤压动画的尝试失败了……
方法三:matrix()/matrix3d()
突然想起transform
属性中可以使用matrix()/matrix3d()
这种方法,也就是说还有矩阵变换这条路可以走;于是乎去网上找了下有没有类似挤压动画这种的扭曲变换,没想到还真找到一个比较相似的,叫做“柱面投影变换”;原理很简单,就是通过把矩形区域投影到一个圆柱体外侧面或内侧面上,从而得到一个挤压或拉伸的图形:
如果投影平面是圆柱体的外侧,那么就能得到跟挤压效果类似的凹曲线:
然而,很明显这种变换是非线性变换,而matrix()/matrix3d()
只能接受常数作为矩阵元素,也就没办法实现非线性变换了!
改变思路:思考原理
上面尝试的三种方法都失败了,可能是把问题想的太简单了,想通过已有的属性直接插值形成动画,而不想增加任何额外的计算;事实上,由于clip-path
属性中有个polygan()
方法可以绘制任意形状的多边形,而且支持动画(也就是可以关键帧自动插值),然而在图形学中,所有的曲线本质上就是通过对曲线的插值绘制出线段得到的;也就是说我们可以通过插值得到一个近似挤压动画需要的多边形形状,只要能够找出描述那个挤压曲线的公式即可,实践证明这是可行的,而且最终代码还不怎么复杂且可控;
挤压曲线的插值点坐标求解
如图所示,以矩形区域左下角为原点,假设挤压曲线为一段圆弧,挤压曲线距离原底边最高处的高度(波峰高度)为,圆弧所处圆的半径为,再设圆弧对应的弦长度的一半为,于是就能得到:
根据及圆心坐标就可以得到圆的轨迹方程:
根据圆的轨迹方程又可以得到挤压曲线(圆弧)部分的求解:
由于a
和c
是已知的,然后就能得到;因此,当确定后,就能得到对应的值了;所以挤压曲线上每个点的坐标都可以求出,也就能够进行插值化处理了!
插值化处理
在底边上等间距选取个点,根据这些点的坐标和已确定的波峰高度就能够得到对应位置的挤压曲线上的坐标点位置(其实主要是值,以水平方向挤压曲线为例),然后按顺序连接这些插值得到的挤压曲线上的点,就可以得到近似挤压曲线的线段,这些线段闭合后就符合挤压动画所需的挤压效果了;
可以设计函数来根据参数(如插值点个数,波峰高度等)自动生成符合polygan()
方法接受的路径格式;如下所示:
1 | /** |
其他注意事项
动态修改挤压效果
如果想要动态修改动画效果,即修改@keyframes
里面的内容;有一种思路就是利用原生的CSS
变量,用CSS
变量来存储关键帧中clip-path
属性的值,然后利用:root
(即根文档节点)元素的style
来设置变量值,如:
1 | const root = document.documentElement // 获取根文档节点 |
然后在关键帧动画相应的位置引用变量即可,这样动态修改变量值后,对应的动画效果也会改变;如:
1 | @keyframes test { |
如何在每次点击的时候触发动画
简单粗暴的通过点击事件添加动画,动画完成后移除动画这种方式我没试过是否可行;我使用的是另一种思路:将动画播放次数设置为无限次数,但是默认的animation-play-state
为paused
(即暂停状态),点击后将动画的播放状态设置为running
(即播放状态),每次动画结束后自动切换为暂停状态。
顺便说一下,监听动画每一次结束的时机可以使用animationiteration
这个事件(该事件本质是在每次动画开始前触发,但不包括第一次,因此可用来当作动画每次播放结束的触发点);
1 | demo.addEventListener('click', () => { |
后话
我承认这种方法有点“硬核”,包含一些数学公式的推导,但实际上用到的知识只是高中数学里面的,过程并不复杂,只不过很久没用有点生疏了;而且第一次推导的时候还弄错了,有点尴尬,不过推导成功还是挺舒服的,最后得到的代码也并不复杂,最重要的是理解了本质问题,又加以应用,还是收获很大的;
上面就是推导过程的草稿,好久没写过数学推导了,还是挺有意思的;最后写了一个交互的demo
,效果看起来还比较满意,可能动画参数还需要打磨一下;
这个交互demo
还可以随时调整一些挤压动画的参数,然后查看改变后的效果;demo
地址为:A squish animation demo
扩展资料:关于柱面投影变换的思路
- 28. 图像扭曲 - 知乎
- 2D射影儿何和变换——柱面投影,图像拼接柱面投影_yang6464158的专栏-CSDN博客
- 3d - Warp Image to Appear in Cylindrical Projection - Stack Overflow:有详细的原理推导和代码示例
- css3动画的更深层次的探究(矩阵变换) - 简书