借助 Web Animations API 实现一个鼠标跟随偏移动画

简介: 借助 Web Animations API 实现一个鼠标跟随偏移动画

前言


今天笔者在逛 CodePen 的时候,突然发现了一个用 JS 实现的动画效果,仔细一看又发现了一个以前没有见过的东西 ———— Web Animations API


翻了翻 MDN 文档才发现这部分功能已经出来5年多了!这怎么能忍,于是赶紧 fork 学习了一波。


Web Animations API


MDN 定义是:允许同步和定时更改网页的呈现,即 DOM 元素的动画。它通过组合两个模型来实现:时序模型和动画模型。


目前该部分功能包含以下几个 类(class) 和一些 元素的扩展方法/属性


类 Classes:


  • Animation提供播放控制、动画节点或源的时间轴。控制方法有四个:finish 终止、pause 暂停、play 开始/恢复动画执行、reverse 反转动画,再加上一个清除动画的方法 cancel;另外还有两个监听事件用来配置对应的回调:oncancel 动画被取消、onfinish 动画执行结束,这两个属性直接读取时是获取对应的事件回调函数,进行赋值操作时才是设置对应的事件回调,并且都支持 Promise 方式使用


  • KeyframeEffect:用来创建动画的关键帧,然后提供给 Animation 的动画使用


  • AnimationTimeLine:动画执行的时间轴,提供一个 currentTime 属性,但本身并不能使用


  • DocumentTimeLine:用来定义一个动画的执行时间线,可以在实例化多个 Animation 时共用同一个 timeline 实例来控制一组动画


方法 Functions:


  • document.getAnimations:返回文档流中所有的 Animation 实例数组


  • element.getAnimations:返回该动画对应的 Animation 实例数组


  • element.animate:为一个元素创建一个 Animation 实例 的便捷方法,每次调用都会返回一个新的 Animation 实例。


属性 Properties:


  • document.timeline:一个只读属性,用来获取当前文档的时间轴,在网页加载时创建


当然,一般来说我们 常常使用的也只有 Animation 和 KeyframeEffect,以及 element.animate


1. Class KeyframeEffect


因为在创建一个动画之前,肯定要先定义一个动画的关键帧,所以我们从 KeyframeEffect 关键帧定义 开始。


构造函数参数和说明:


该类可以通过下面三种方式进行实例化:


new KeyframeEffect(target, keyframes, options)
new KeyframeEffect(target, keyframes)
new KeyframeEffect(sourceKeyFrames)


其中:


  • target 为需要执行动画的元素,可以为 null


  • keyframes 则是一个动画帧定义的 对象数组,也可以是 null


  • options 可以是一个 number,也可以是一个配置对象


  • 是数字时表示动画的 执行总时间


  • 是对象时可以配置 delay 延迟、duration 动画时间、easing 动画运动曲线 等


  • sourceKeyFrames 是一个通过 KeyframeEffect 实例化后的动画定义,这么使用会 复制 传入的动画定义来创建一个新的动画帧定义实例


一般情况下,都是通过 第一种方式并且指定 target 为 null 来定义动画,可以增加动画的复用性。


使用:


假设我们现在要定义一个元素向下移动的动画(transform)


const downKeyframes = new KeyframeEffect(
    null, 
    [
      { transform: 'translateY(0%)' }, 
      { transform: 'translateY(100%)' }
    ], 
    { duration: 3000 }
  );


这个实例的代表的动画效果就是:在 3s 内元素会通过 transformY 向下偏移整个元素的高度。


2. Class Animation


上面介绍过,这个类就是创建一个用来控制动画以及这种动画状态监听/读取的实例对象


构造函数参数和说明:


该类的实例化方式只有一种:


const animation = new Animation(effect, timeline);


其中:


  • effect:就是上面通过 KeyframeEffect 实例化的动画帧定义


  • timeline:指定与动画相关联的时间轴,当前阶段还没有相关功能实现,默认是 document.timeline;使用时可以省略


使用:


现在我们就使用上面定义的那个下移动画(当然,此时上面的动画帧定义实例需要绑定动画的目标元素):


const el = document.getElementById("active")
downKeyframes.target = el;
const downAnimation = new Animation(downKeyframes, document.timeline);


效果如下:


image.png


Markup:

<div id="app">
  <button id="button1">开始/暂停</button>
  <button id="button2">逆转</button>
  <div id="active"></div>
</div>


Style:

#active {
  width: 200px;
  height: 200px;
  margin: 20px auto;
  background-color: aqua;
}


Script:

const el = document.getElementById("active")
const btn1 = document.getElementById("button1")
const btn2 = document.getElementById("button2")
const downKeyframes = new KeyframeEffect(
  null, 
  [
    { transform: 'translateY(0%)' }, 
    { transform: 'translateY(100%)' }
  ], 
  { duration: 3000 }
);
downKeyframes.target = el;
const downAnimation = new Animation(downKeyframes, document.timeline);
btn1.addEventListener('click', () => {
  if (downAnimation.playState === 'running') {
    downAnimation.pause()
  } else {
    downAnimation.play()
  }
})
btn2.addEventListener('click', () => {
  downAnimation.reverse()
})


然后,我们还可以 通过调用该实例的相关方法来控制这个动画的执行


const btn1 = document.getElementById("button1")
const btn2 = document.getElementById("button2")
btn1.addEventListener('click', () => {
  if (downAnimation.playState === 'running') {
    downAnimation.pause()
  } else {
    downAnimation.play()
  }
})
btn2.addEventListener('click', () => {
  downAnimation.reverse()
})


这里介绍一下 playState 属性:用来获取该动画的 执行状态,是一个 枚举值。具体值有以下几个:


  1. idle:此时动画的事件还无法解析,并且队列里也没有处于等待执行的动画任务


  1. pending:还处于等待过程中,需要等待其他任务执行完毕


  1. running:正处于动画过程中


  1. paused:动画被暂停


  1. finished:动画已经执行结束


3. element.animate()


当然,上面这种方式对实际使用来说还是有点繁琐,所以又有一种比较快捷的方式来创建一个 Animation 实例,也就是上面提到的 element.animate()。并且,该方式创建的动画 将直接作用于元素并开始执行动画过程


用法和参数说明:


在使用时,和一般的函数使用一样:


const animation = element.animate(keyframes, options);


其中的参数:


  • keyframes:与 KeyframeEffect 中的 keyframes 参数类似,都是用来定义动画执行过程中的关键帧,只是这里是一个 对象 形式


  • options:与 KeyframeEffect 中的 options 参数一致,可以是数字或者对象;但是这里 多了一个 id 配置,用来作为该动画的唯一标识


当然,MDN 中的文档还表示未来可能会增加composite、spacing 等多个配置项,不过目前还不是所有浏览器都支持


使用:


此时假设我们还要实现上面的那个下移效果的话,就可以这么写:


const el = document.getElementById("active")
const animation = el.animate(
  { transform: 'translateY(100%)' },
  3000
)


页面加载完成时该动画就会直接执行


假设我们要加上相关的一些控制的话,也可以和控制 Animation 实例一样:


const btn1 = document.getElementById("button1")
const btn2 = document.getElementById("button2")
btn1.addEventListener('click', () => {
  if (animation.playState === 'running') {
    animation.pause()
  } else {
    animation.play()
  }
})
btn2.addEventListener('click', () => {
  animation.reverse()
})


效果如下:


image.png


Markup:

<div id="app">
  <button id="button1">开始/暂停</button>
  <button id="button2">逆转</button>
  <div id="active"></div>
</div>


Style:

#active {
  width: 200px;
  height: 200px;
  margin: 20px auto;
  background-color: aqua;
}


Script:

const el = document.getElementById("active")
const btn1 = document.getElementById("button1")
const btn2 = document.getElementById("button2")
const animation = el.animate(
  { transform: 'translateY(100%)' },
  3000
)
btn1.addEventListener('click', () => {
  if (animation.playState === 'running') {
    animation.pause()
  } else {
    animation.play()
  }
})
btn2.addEventListener('click', () => {
  animation.reverse()
})


借助 animate() 实现鼠标跟随


首先上效果:


image.png


Markup:

<div class="animation-box" id="box">
  <div id="gallery">
    <div class="tile">
      <img
          src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/515258c213064463b1fac843363ece92~tplv-k3u1fbpfcp-no-mark:480:400:0:0.awebp?"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/03/15/FpS314YGk0tJ6CKFGF1CO49ql1Wn.jpg!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/03/16/lpj-asxbmJDP6ig9DAnRWRQ12qwl.png!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/03/16/Fl7X9CJjkJkexYEz8hjweZB1nc5v.png!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/05/09/FtniwlmXIdCRMrFBfEMemJUwsmpc.jpg!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/03/15/lpzDtULqBYXYQYn9KRLvancGAyOn.png!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/03/15/FkSpDHY7P9ygn-rn9vfY6A7nXw7W.jpg!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/05/30/Fpht4CzextqfHA048FGU8m_t4NP5.jpeg!280x280.jpg"
        />
    </div>
    <div class="tile">
      <img
          src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54c5bda89428417f8009279464776b1f~tplv-k3u1fbpfcp-watermark.image?"
        />
    </div>
  </div>
</div>


Style:

body {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.animation-box {
  background-color: rgb(10, 10, 10);
  height: 100%;
  margin: 0;
  overflow: hidden;
  position: relative;
}
#gallery {
  height: 120vmax;
  width: 120vmax;
  position: absolute;
}
.tile {
  border-radius: 1vmax;
  position: absolute;
  transition: transform 800ms ease;
}
.tile:hover {
  transform: scale(1.1);
}
.tile:hover > img {
  opacity: 1;
  transform: scale(1.01);
}
.tile > img {
  height: 100%;
  width: 100%;
  object-fit: cover;
  border-radius: inherit;
  opacity: 0;
  transition: opacity 800ms ease, transform 800ms ease;
}
.tile:nth-child(1) {
  background-color: rgb(255, 238, 88);
  height: 14%;
  width: 20%;
  left: 5%;
  top: 5%;
}
.tile:nth-child(2) {
  background-color: rgb(66, 165, 245);
  height: 24%;
  width: 14%;
  left: 42%;
  top: 12%;
}
.tile:nth-child(3) {
  background-color: rgb(239, 83, 80);
  height: 18%;
  width: 16%;
  left: 12%;
  top: 34%;
}
.tile:nth-child(4) {
  background-color: rgb(102, 187, 106);
  height: 14%;
  width: 12%;
  left: 45%;
  top: 48%;
}
.tile:nth-child(5) {
  background-color: rgb(171, 71, 188);
  height: 16%;
  width: 32%;
  left: 8%;
  top: 70%;
}
.tile:nth-child(6) {
  background-color: rgb(255, 167, 38);
  height: 24%;
  width: 24%;
  left: 68%;
  top: 8%;
}
.tile:nth-child(7) {
  background-color: rgb(63, 81, 181);
  height: 16%;
  width: 20%;
  left: 50%;
  top: 74%;
}
.tile:nth-child(8) {
  background-color: rgb(141, 110, 99);
  height: 24%;
  width: 18%;
  left: 72%;
  top: 42%;
}
.tile:nth-child(9) {
  background-color: rgb(250, 250, 250);
  height: 10%;
  width: 8%;
  left: 84%;
  top: 84%;
}


Script:

const box = document.getElementById('box')
const gallery = document.getElementById("gallery");
function animation(e) {
  const mouseX = e.clientX,
    mouseY = e.clientY;
  const xDecimal = mouseX / box.clientWidth,
    yDecimal = mouseY / box.clientHeight;
  const maxX = gallery.offsetWidth - box.clientWidth,
    maxY = gallery.offsetHeight - box.clientHeight;
  const panX = maxX * xDecimal * -1,
    panY = maxY * yDecimal * -1;
  const animation = gallery.animate(
    {
      transform: `translate(${panX}px, ${panY}px)`
    },
    {
      duration: 4000,
      fill: "forwards",
      easing: "ease"
    }
  );
  animation.onfinish = () => animation.cancel()
}
window.addEventListener("mousemove", animation);


1. 布局


整个界面包含 一个外层的限制元素、一个尺寸大于外层元素的内层元素、以及一系列图片显示的元素


<div class="animation-box" id="box">
  <div id="gallery">
    <div class="tile">
      <img
          src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/515258c213064463b1fac843363ece92~tplv-k3u1fbpfcp-no-mark:480:400:0:0.awebp?"
        />
    </div>
    <div class="tile">
      <img
          src="https://img01.yzcdn.cn/upload_files/2022/03/15/FpS314YGk0tJ6CKFGF1CO49ql1Wn.jpg!280x280.jpg"
        />
    </div>
    ...
  </div>
</div>


2. 样式


为了增加一个交互效果,给图片添加了一个 带颜色的遮罩层,并设置透明,在鼠标 Hover 时在显示图片并稍微放大。


body {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.animation-box {
  background-color: rgb(10, 10, 10);
  height: 100%;
  margin: 0;
  overflow: hidden;
  position: relative;
}
#gallery {
  height: 120vmax;
  width: 120vmax;
  position: absolute;
}
.tile {
  border-radius: 1vmax;
  position: absolute;
  transition: transform 800ms ease;
}
.tile:hover {
  transform: scale(1.1);
}
.tile:hover > img {
  opacity: 1;
  transform: scale(1.01);
}
.tile > img {
  height: 100%;
  width: 100%;
  object-fit: cover;
  border-radius: inherit;
  opacity: 0;
  transition: opacity 800ms ease, transform 800ms ease;
}
.tile:nth-child(1) {
  background-color: rgb(255, 238, 88);
  height: 14%;
  width: 20%;
  left: 5%;
  top: 5%;
}
.tile:nth-child(2) {
  background-color: rgb(66, 165, 245);
  height: 24%;
  width: 14%;
  left: 42%;
  top: 12%;
}
// ...


Animate 动画


这里需要实现的其实就是 计算出鼠标当前在窗口的位置,然后按比例换算成内部区域的偏移量,最后通过 animate 方法定义一个动画将内层元素移动到相应的位置


const box = document.getElementById('box')
const gallery = document.getElementById("gallery");
function animation(e) {
  const mouseX = e.clientX,
    mouseY = e.clientY;
  const xDecimal = mouseX / box.clientWidth,
    yDecimal = mouseY / box.clientHeight;
  const maxX = gallery.offsetWidth - box.clientWidth,
    maxY = gallery.offsetHeight - box.clientHeight;
  const panX = maxX * xDecimal * -1,
    panY = maxY * yDecimal * -1;
  const animation = gallery.animate(
    {
      transform: `translate(${panX}px, ${panY}px)`
    },
    {
      duration: 4000,
      fill: "forwards",
      easing: "ease"
    }
  );
  animation.onfinish = () => animation.cancel()
}
window.addEventListener("mousemove", animation);


值得注意的是,通过 element.animate 创建的 Animation 实例每次都是新的,并且 不会自动清除,所以建议通过设置 onfinished 事件回调来清除掉原来的动画实例。


最后


本文也只是大致说明了一下这几个 API 的功能和使用方式,但是具体还有哪些坑或者彩蛋还需要大家去实际体验一下才知道。


总的来说,这个功能的出现对于我们前端来说 可以更加方便且准确的实现和控制页面的动画内容,但是部分属性与 CSS 的动画配置还是有些区别,大家需要多多注意呀


目录
相关文章
|
6天前
|
IDE Java API
使用Java Web技术构建RESTful API的实践指南
使用Java Web技术构建RESTful API的实践指南
|
2月前
|
前端开发 API Docker
web前端开发项目走proxy代理后端接口,构建发布到生产等环境后,如何修改api接口
web前端开发项目走proxy代理后端接口,构建发布到生产等环境后,如何修改api接口
26 0
|
2月前
|
JavaScript 前端开发 API
8个鲜为人知但很实用的Web API(下)
8个鲜为人知但很实用的Web API(下)
|
1天前
|
前端开发 JavaScript API
前端秘法番外篇----学完Web API,前端才能算真正的入门
前端秘法番外篇----学完Web API,前端才能算真正的入门
|
3天前
|
API 网络架构
解释 RESTful API,以及如何使用它构建 web 应用程序。
解释 RESTful API,以及如何使用它构建 web 应用程序。
10 0
|
5天前
|
存储 前端开发 搜索推荐
前端开发中值得关注的三个Web API
【2月更文挑战第4天】Web API是前端开发中非常重要的一部分,它们为开发者提供了众多的功能和特性,帮助我们构建更加高效、优美的Web应用。本文将介绍三个值得关注的Web API,包括Web Storage、Geolocation和Web Notifications,希望能够对前端开发者有所帮助。
|
22天前
|
缓存 安全 API
深入理解Web开发中的RESTful API设计
在当今快速演进的技术世界中,RESTful API已成为构建现代Web应用不可或缺的一部分。它不仅促进了前后端的分离发展,还为不同平台间的数据交换提供了一种高效、标准化的方式。本文旨在深入探讨RESTful API的设计原则和最佳实践,通过具体示例说明如何设计易于维护、可扩展和安全的API。我们将从REST的基本概念出发,逐步深入到资源命名、HTTP方法的恰当使用、状态码的选择、以及安全性考虑等方面,为读者提供一个全面而深入的视角,帮助大家更好地理解和运用RESTful API。
|
26天前
|
小程序 IDE Java
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
26 0
|
27天前
|
小程序 IDE Java
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
社区每周丨订单中心模板更新及基础API增加音频与动画(5.15-5.19)
23 0
|
2月前
|
JSON API 数据格式
RESTful API,以及如何使用它构建 web 应用程序。
RESTful API,以及如何使用它构建 web 应用程序。
24 0

相关产品

  • 云迁移中心