目录

Popmotion —— 流动你的页面

前言

如今这几年前端一如既往的风起云涌: TypeScript 、 Flow 等静态类型的大行其道;三大框架的明争暗斗也步入平缓期;即将发力的 WebAssembly 和 PWA 等等。想到此刻,我不禁开始怀疑了前端的躁动到底解决了痛点?前端的本质又是为何?

目前很多的开源或者新技术都在解决我们程序员自己的痛点,例如:开发的效率、合作的无缝程度、脚手架的便捷性等等。那我们又要为用户解决什么痛点呢?目前市面上的产品在交互和动效方面都显得有那么点单调,永远是一样的渐隐渐现的模态弹出层,永远是跑马灯的首页广告位,未免有点乏善可陈。因此我们需要一些新鲜血液,使你的产品敢大声说:我们不一样\~

以下 demo 地址
git clone git@github.com:2b2b2b2b/popmotion-demo.git

简介

Popmotion 作为 2017 崛起的十大前端开源项目,已经席卷了 10k 的 star ,并且有着很多显著的优点。 Popmotion 官网是这样介绍 :

Popmotion is a 11.5kb (max) Swiss Army knife for animators and interaction developers.

Popmotion 的好处有啥:

  1. 无依赖、轻量,总体积只有 11.5kb ,还可以按需按模块引入
  2. 无侵入性, 可以无缝接入各种项目,无论普通的 Dom 、还是 Vue 、 React 等框架、还是 canvas 、 svg 、 Three.js ,只要接受数字作为输入的都可以简单写出动画
  3. 学习 Functional Reactive Programming (FPR) 范式
  4. 拥有处理复杂时间顺序、相互关联作用动画的能力
  5. 内置配套工具丰富函数例如:加速度、缓动函数等;也可自己定制扩展

说了那么多好处,让我们看看 Popmotion 制作的一些简单动画吧\~

方块联动翻转

惯性拖拽

和滚动结合的弹簧跳动文字

变换svg icon


从一个简单的例子入手

tween —— 仓库中的demo1

import { tween, styler ,easing} from 'popmotion';
const ball = document.querySelector('.ball'); 
const ballStyler = styler(ball); 
//styler 是 popmotion 对样式进行 get 、 set 封装的函数 
tween({ 
  from: { x: 0, scale: 1, y:0}, 
  to: { x: 300, scale: 2, y:100 }, 
  ease: easing.easeInOut, 
  flip: Infinity, 
  duration: 1000 
})
.start(ballStyler.set);

上述代码就能完成一个区间进行的动画,你可能好奇,这种动画 keyframe 或者 animation 都可以轻松完成。但是如果页面有另外个移动距离两倍的球做同样运动是不是要去写两份样式,亦或是这个球像弹簧一样左右移动逐渐衰弱直到停止,是否 css 就难以胜任或者说很麻烦了呢?但是 popmotion 就能轻易完成这些需求,因为 popmotion 采取了 FRP 的编程范式,在这里就要介绍下 popmotion 的两大核心概念 action 和 reaction 。

如果熟悉 rx.js 的同学就可以把 action 理解为 rx.js 中的 Observable , action 是一个函数可以创建一个系列的数据流。

而 reaction 可以理解为 rx.js 中的 Observer , reaction 是一组回调函数的集合,用来消费action 产生的数据流。

//一个标准的 action 与 reaction的关系 
action(({ update, complete, error }) => { 
//update , complete , error 三个函数,可以控制如何何时去输出这个流的下个值、整个流的结束与错误 
})
.start({ 
  update:()=>{},//每次action流产生新的流触发的回调 
  complete:()=>{},//整个流结束触发的回调 
  error:()=>{} 
});

用上面的入手实例来简单解释下,tween 就是一个经过封装的 action , 它输出从 from 参数到to 参数渐变的流,当然根据 duration ,和 ease 等参数可以更改流的整个过程的时间和每个数值之间的间隔和速度:

tween({ 
  from: { x: 0, scale: 1,y:0}, 
  to: { x: 300, scale: 2,y:100 }, 
}).start(console.log) 
/* 
{x: 32.47036666666665, scale: 1.1082345555555555, y: 10.823455555555551} 
{x: 102.68370000000003, scale: 1.342279, y: 34.22790000000001} 
{x: 153.04479657517106, scale: 1.5101493219172368, y: 51.01493219172368}
... 
... 
{x: 300, scale: 2, y: 100} 
*/

而 ballStyler.set 是单独只写 update 省略了 error 和 complete 的缩写,根据 tween 生产的流不停的修改球的 x、y、scale。

popmotion 中除了 tween 这个经过封装的 action 还有其余一些自带的 action ,每种 acition都是种数据流动的方式,例如: Decay (衰减) 、 Physics (物理) 、 Spring (弹簧) 等等,你还可以自己封装想要的定制化 action 。不同的 atcion ,最快的熟悉方式就是搭建一个 playground 利用 console 去查看显得最为直接。


Popmotion 的优势

不同于命令式编程的范式,在 FRP 的范式中我们只需要关注数据流的变化。如同如今流行的前端框架无论是 Vue 还是 React ,我们不在去指定何时去操纵的 dom 的 show 和 hidden ,而是提前预设了一个值,去映射 dom 的 show 和 hidden 的关系。这使得大大简化我们的代码数量和清晰程度。

不仅如此,因为 popmotion 动画都是数据流作为输入,我们可以把同一个数据流进行加工、整合从而服用到另外一个相关的动画上。就如同上述例子中提到过的,添加另外个球,移动距离等是原来的两倍,利用 popmotion 中的一些操作符对原本 tween 产生的流进行加工,便可不用写两个类或者是 keyframe ,即使操作的动画是 canvas 或者是 svg 等也可以进行轻松的复用,产生相关的动画。

并且作为一个数据流,我们可以像播放视频一般,对其进行暂停、选取进度、回放等操作,而css3 动画就很难做到这点。

举个例子:一个 css 动画是通过鼠标的 click从 0\% → 100\%,再次 click 100\% → 0\% 。如果在未变化到最终状态,比如才到 70\% 就再次点击。css动画会因为内置的序列,瞬间把状态从70\% 变化到 100\% 再逐渐动画到 0\% 。 如果快速的反复点击会造成闪动的现象。而popmotion 就可以轻松完成流进度的控制 0\% → 70\% →0\% ,让动画没有丝毫的跳动。

PS : 上述的例子可能有点抽象可以根据仓库中的 demo8 和 demo9 进行快速反复点击进行比对。

popmotion 在复杂动画的队列等问题也是得心应手,并且自带了加速度控制等一系列工具,使得你有足够的分析、拆解能力。任何动画都会触手可得。

另外在 FRP 的范式下,数据被多次消费的便捷性、可读性高、无副作用等优点,相关优秀文章很多,也不在这里一一列举 班门弄斧了。


复杂的示例

说了那么多你肯定已经跃跃欲试了吧\~我们即将完成的是这样一个动画\~

PS :具体查看仓库中的 demo9

动画整体是从三条横杠的 icon 变成 一个圆包含大叉的 icon ,让我们对动画进行下单程的拆解,回放使用 popmotion 对流的回放即可完成:

1、三条横杠的上下的两条,最终变为打叉,这个过程有个回弹的过程,有点类似于 keyframe

2、中间的横杠,弹性的从左到右消失,也是个 keyframe 类型的动画

3、中间横岗消失即将结束时候,外圈的圆开始一圈的环绕

第一步 :先完成 HTML 和 CSS 部分

利用 span 模拟三条杠,定位部分使用 margin 或者 absolute 都可以因为 pomotion 的 dom 动画基本都是基于 transform 去完成位置变化的,外层是使用 SVG 完成的 path 此处无法使用circle

因为 circle 是没用始终方向的,无法完成动画。具体 path 是使用二次贝塞尔曲线、还是三次贝塞尔、还是圆弧去画取决于自己的熟悉程度。在此就不贴代码了

第二步 :完成上下横杆的动画

在这里就要使用到 popmotion 中的 keyframe 的 action ,和 tween 不一样的是, tween 是其实直接到终点的流变化,而 keyframe 类似 css 中 的一样,是几个状态直接变化的流

import {keyframes, styler , easing} from 'popmotion';
const topLine = document.querySelector('.top') 
const bottomLine = document.querySelector('.bottom') 
//由于 popmotion 是函数式库,因此帮助我们包装了对 style 进行 get 、 set 的函数 
const topStyler = styler(topLine) const bottomStyler = styler(bottomLine) 
const topMove = keyframes({ 
  values: [ 
    { left: 0, top: 0, rotate: 0 },
    { left: 0, top: 0, rotate: 15 }, 
    { left: -5, top: 0, rotate: -60 },
    { left: -5, top: 1, rotate: -45 } 
  ], 
  duration: 600, 
  easing: easing.linear 
})
.start(v => { 
  topStyler.set({
    left: v.left, 
    top: v.top, 
    rotate: v.rotate, 
    transformOrigin: '34px 2px'
  }) 
}) 
const bottomMove = keyframes({ 
  values: [ 
    { left: 0, rotate: 0 }, 
    { left: 0, rotate: -15 },
    { left: -5, rotate: 60 }, 
    { left: -5, rotate: 45 } 
  ],
  duration: 600, easing: easing.linear 
})
.start(v => { 
  bottomStyler.set({
    left: v.left, 
    top: v.top, 
    rotate: v.rotate, 
    transformOrigin: '34px 2px'
  }) 
})

这两个 keyframe 产生的流都没有 transformOrigin 旋转基点这个属性,最后在 reaction 部分手动去补上,其实我们还可以使用操作符对流进行处理,在每个流产生的数据上添加transformOrigin

const bottomMove = keyframes({
  values: [
    { left: 0, rotate: 0 }, 
    { left: 0, rotate: -15 }, 
    { left: -5, rotate: 60 }, 
    { left: -5,rotate: 45}
  ],
  duration: 600,
  easing: easing.linear
})
.pipe(v=>{
  v.transformOrigin = '34px 2px'
  return v
})
.start(bottomStyler.set)

这两种写法孰优孰劣就看自己判断了,这样一来就完成了上下杠变成叉的动画,是不是很简单呢?

第三步:中间横杠到,圆圈围绕的动画

这步可能是这个动画中比较麻烦的部分,因为中间的杠的消失到圆圈的环绕是有关联关系,圆需要等到横杠消失前一点进行动画才能衔接的比较自然。

这里我们需要使用一个 timeline 的 action , timeline 接受一个数组,会依次按照队列完成。如果数组中有数字部分也就是前后两者之间的间隔,当然也可以是负数,代表提前开始。有了这个 action 就能轻易完成这个需求了。

import { keyframes, styler , easing , timeline , svg } from 'popmotion' 
const path = document.querySelector('path') 
//svg 类似于 styler 是 popmotion 对于 svg 修改的封装函数 
const svgStyler = svg(path) 
const ringMove = timeline([ 
  {track: 'middle', from:{ left: 0, width: 36 },to:{left: -6, width: 42},duration: 200, ease: easing.linear}, 
  {track: 'middle', to:{ left: 50, width: 0 },duration: 200, ease: easing.linear}, 
  {track: 'middle', to:{ left: 40, width: 0 },duration: 200, ease: easing.easeOut}, 
  '-250', 
  {track: 'circle', from: 0, to: 100, duration: 300, ease: easeIn}
]) 
.start(v=>{ 
  middleStyler.set({ 
    left: v.middle.left, 
    width: v.middle.width 
  }) 
  svgStyler.set('pathLength', v.circle) 
})

对于 timeline 感到费解的同学,可以在 playground 中尝试下这个 action 输出的流。

第四步:绑定鼠标事件,进行动画的反复播放

在 popmotion 对于一些事件也可定义为一个 action 的流,因此可以很简单的和一些流进行合并达到 debounce 或者 throttle 之类的效果。但是在这个例子中,和原生的事件监听并没有很大的差距。另外再一些经过封装的 action 经过订阅后会返回对于流的暂停、回复、倒叙、指定进度等功能。因此可以轻松的完成我们要的需求,只需要这样既可:

listen(icon, 'click').start(()=>{
    //对action进行暂停
    topMove.pause()
    //转换顺序
    topMove.reverse()
    //重新播放流
    topMove.resume()
    bottomMove.pause()
    bottomMove.reverse()
    bottomMove.resume()
    ringMove.pause()
    ringMove.reverse()
    ringMove.resume()
})

这样一来我们的动画就完成了\~ 再布置个作业:按钮可以反复 toggle ,如何去让这个点击加上防抖动的功能呢?并且最后的部分有点重复、怎么样能做到简化呢?


总结

不知道你是否对于 popmotion 和这种 FRP 范式的感觉有那么点熟悉的感觉了呢?如果还没有感觉,就再结合官网和我提供的例子再去感受下吧。

介于我 demo 中场景的复杂度都比较简单,我相信 popmotion 的威力远不止此\~快使用popmotion 让你的网页随着数据流流动起来吧\~

饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 xiedaimala03,官网:https://jirengu.com

本文作者:饥人谷方应杭老师