借助 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 的动画配置还是有些区别,大家需要多多注意呀


目录
相关文章
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
52 4
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
143 3
|
1天前
|
Kubernetes 安全 Devops
有效抵御网络应用及API威胁,聊聊F5 BIG-IP Next Web应用防火墙
有效抵御网络应用及API威胁,聊聊F5 BIG-IP Next Web应用防火墙
18 10
有效抵御网络应用及API威胁,聊聊F5 BIG-IP Next Web应用防火墙
|
1月前
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
44 1
|
1月前
|
JSON API 数据格式
如何使用Python和Flask构建一个简单的RESTful API。Flask是一个轻量级的Web框架
本文介绍了如何使用Python和Flask构建一个简单的RESTful API。Flask是一个轻量级的Web框架,适合小型项目和微服务。文章从环境准备、创建基本Flask应用、定义资源和路由、请求和响应处理、错误处理等方面进行了详细说明,并提供了示例代码。通过这些步骤,读者可以快速上手构建自己的RESTful API。
38 2
|
2月前
|
监控 负载均衡 API
Web、RESTful API 在微服务中有哪些作用?
在微服务架构中,Web 和 RESTful API 扮演着至关重要的角色。它们帮助实现服务之间的通信、数据交换和系统的可扩展性。
51 2
|
2月前
|
人工智能 搜索推荐 API
用于企业AI搜索的Bocha Web Search API,给LLM提供联网搜索能力和长文本上下文
博查Web Search API是由博查提供的企业级互联网网页搜索API接口,允许开发者通过编程访问博查搜索引擎的搜索结果和相关信息,实现在应用程序或网站中集成搜索功能。该API支持近亿级网页内容搜索,适用于各类AI应用、RAG应用和AI Agent智能体的开发,解决数据安全、价格高昂和内容合规等问题。通过注册博查开发者账户、获取API KEY并调用API,开发者可以轻松集成搜索功能。
|
2月前
|
前端开发 JavaScript API
惊呆了!学会AJAX与Fetch API,你的Python Web项目瞬间高大上!
在Web开发领域,AJAX与Fetch API是提升交互体验的关键技术。AJAX(Asynchronous JavaScript and XML)作为异步通信的先驱,通过XMLHttpRequest对象实现了局部页面更新,提升了应用流畅度。Fetch API则以更现代、简洁的方式处理HTTP请求,基于Promises提供了丰富的功能。当与Python Web框架(如Django、Flask)结合时,这两者能显著增强应用的响应速度和用户体验,使项目更加高效、高大上。
51 2
|
2月前
|
前端开发 API 开发者
从零到精通,AJAX与Fetch API让你的Python Web前后端交互无所不能!
从零到精通,AJAX与Fetch API让你的Python Web前后端交互无所不能!
45 3
|
2月前
|
移动开发 前端开发 JavaScript
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
前端开发实战:利用Web Speech API之speechSynthesis实现文字转语音功能
210 0