动画组件(多图预警):一套Animation API,实现React和Vue两种动画组件

多图预警 多图预警 多图预警

社会人

在网页中经常会见到效果酷炫的动态效果,如之前文章分享比较简单的动态可交互“粒子-线”效果,如下图:


网上也有比较多的教程,但是学习的最终成果,往往是各个独立的html文件,很难复用在其他项目中。


而如今,前端开发讲究模块化以及组件化,所以便想通过一定方式将其封装为模块或者组件,方便在其他项目中调用。考虑到如今比较流行React与Vue组件开发, 所以本文主要分享如何将之前的动效以模块和React及Vue组件的形式实现。

效果放前面,具体请戳源码:

‘粒子-线’组件化.gif

热力图.gif
鼠标移动--气泡.gif

使用很是简单,具体如下:

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=1920, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>React BGAnimation</title>
    <link rel='stylesheet' href='./index.css'></link>
</head>
<body>
    <div id="container" class="txt-center">
        <div class="left">
            <header>
                <h3>React组件</h3>
            </header>
            <main id="root" class="content"> </main>
        </div>
        <div class="right">
            <header>
                <h3>Vue组件</h3>
            </header>
            <main id="app" class="content">
                <app></app>
            </main>
        </div>
    </div>
    <script src='./dist/react.build.js'></script>
    <script src='./dist/vue.build.js'></script>
</body>
</html>
  • Vue使用方法
<template>
    <MouseMovingBubble :width='width' :height='height' :number='number' :style="style"></MouseMovingBubble>
</template>
<script>
export default {
  data() {
    return {
      width: 600,
      height: 500,
      number: 10,
      style: {
        border: "1px solid lightgray"
      }
    };
  }
};
</script>
  • React使用方法
import React from "react";
import ReactDOM from "react-dom";
import * as Coms from "./components/index";
const mountDom = document.getElementById("root");
ReactDOM.render(
  <Coms.MouseMovingBubble width="600" height="500" number={10} />,
  mountDom
);

动画

我们都知道,动画只不过是一个短时间内连续绘制不同图形(帧)产生的效果.通过requestAnimationFrame可以实现流畅的动画,因为浏览器内部对动画执行做了优化(与浏览器渲染同步);不建议使用setTimeInterval,因为这是个不靠谱的家伙!

那么下边介绍一下我自己理解的一个动画的基本框架:

  • 初始化(Init):完成一些数据的预处理和画布画笔的创建等
  • 步进(Step):完成每一帧动画图形变化需要做的操作(如清空画布/计算新的位置等)
  • 循环(Loop):完成动画的循环,即循环执行步进(Step)方法

是不是很简单呢?哈哈 下边动手实现以下:

所有动画父类:IAniamtion

通过分析完成一个动画效果的过程,我们可以抽象出一个基类(IAnimation),定义所有动画必须实现的方法以及对一下公共操作进行封装.

export default class IAnimation {
  constructor(props) {
    // 接收配置参数
    this.canvas = props.canvas;
    this.width = parseInt(props.width);
    this.height = parseInt(props.height);
    // 为方法绑定this
    this.init = this.init.bind(this);
    this.step = this.step.bind(this);
    this.loop = this.loop.bind(this);
  }
  init() {
    // 初始化画布和画笔
    this.ctx = this.canvas.getContext("2d");
    if (!this.ctx) throw "浏览器不支持Canvas,请使用其他浏览器试试!";
    this.canvas.width = this.width;
    this.canvas.height = this.height;
  }
  loop() {
    // 每次loop前清空之前画布的内容
    this.ctx.clearRect(0, 0, this.width, this.height);
  }
  step() {
    // 步进
  }
}

"鼠标移动--气泡"动画实现

以"鼠标气泡"为例,简单说明实现动画的过程.因为逻辑不复杂,所以实现也比较简单.

import IAnimation from "./IAnimation";
import { Extent, Particle } from "../graphic";
import { random } from "../utils";

export default class MouseMovingBubbleAnimation extends IAnimation {
  constructor(props) {
    super(props);
    // 每次鼠标移动产生的粒子数
    this.number = props.number || 0;
    this.oArray = [];
  }
  init() {
    // 调用父类(IAnimation)的init方法
    super.init();
    var self = this;
    // 监视鼠标移动事件
    this.canvas.onmousemove = function(e) {
      const mpos = { x: e.offsetX, y: e.offsetY };
      for (let i = 0; i < self.number; i++) {
        // 创建粒子
        let oPr = new Particle();
        oPr.range = new Extent([0, 0], [self.width, self.height]);
        oPr.x = mpos.x;
        oPr.y = mpos.y;
        oPr.radius = random(8, 15);
        oPr.speed = 20 / oPr.radius;
        oPr.direction = random(0, 2 * Math.PI);
        self.oArray.push(oPr);
      }
    };
    // 执行动画
    self.loop();
  }
  loop() {
    // 判断粒子数
    // 当例子说大于0的时候执行步进操作
    if (this.oArray.length > 0) {
      super.loop();
      this.step();
    }
    requestAnimationFrame(this.loop);
  }
  step() {
    // 调用父类步进(step)方法
    super.step();
    let deletAry = [];
    this.oArray.forEach(p => {
      // 粒子步进
      p.step();
      // 减小粒子半径
      p.radius -= p.oldRadius / 50;
      // 直到小于0 消失
      if (p.radius <= 0) deletAry.push(p);
      else p.draw(this.ctx);
    });

    // 移除消失的粒子
    deletAry.forEach(p => {
      let index = this.oArray.indexOf(p);
      this.oArray.splice(index, 1);
    });
  }
}

React与Vue组件封装

其实在动画逻辑实现好了的情况下,封装组件是一件异常简单的事情(暂时不考虑复杂的逻辑或者交互),只需要在组件特定的生命周期执行动画实例的相应发方法即可.这里以为需要传入canvas DOM作为动画参数,所以在组件挂载之后创建了动画实例,并执行了初始化方法.

具体以"鼠标移动--气泡"动画为例说明React和Vue封装:

  • MouseMovingBubble.jsx
import React from "react";
import { MouseMovingBubbleAnimation } from "../../../common/index";

export default class Particle extends React.Component {
  constructor(props) {
    super(props);
  }

  // 挂载后创建动画实例
  componentDidMount() {
    this.animation = new MouseMovingBubbleAnimation ({
      canvas: this.oCanvas,
      width: this.props.width,
      height: this.props.height,
      number: this.props.number
    });
    // 执行初始化方法
    this.animation.init();
  }

  render() {
    return (
      <canvas style={this.props.style} ref={canvas => (this.oCanvas = canvas)}>
        您的浏览器不支持Canvas,请使用其他浏览器试试看!
      </canvas>
    );
  }
}

// 默认配置参数
Particle.defaultProps = {
  width: 1080,
  height: 512,
  number: 10,
  style: {
    border: "1px solid lightgray"
  }
};
  • MouseMovingBubble.vue
<template>
    <canvas :width="width" :height="height">您的浏览器不支持Canvas,请使用其他浏览器试试看!</canvas>
</template>

<script>
import { MouseMovingBubbleAnimation } from "../../../common/animation/index";
export default {
  // 默认参数配置
  props: {
    width: {
      type: Number,
      default: 300
    },
    height: {
      type: Number,
      default: 300
    },
    number: {
      type: Number,
      default: 30
    }
  },
  mounted() {
    // 组件挂载后创建动画实例
    const animation = new MouseMovingBubbleAnimation ({
      width: this.width,
      height: this.height,
      number: this.number,
      canvas: this.$el
    });
    // 初始化动画
    animation.init();
  }
};
</script>

<style>
</style>

其他的效果实现,暂时不在这里抛代码了,感兴趣的可以去看源码,后面也会针对个别效果做详细介绍...


如果您感觉有所帮助,或者有问题需要交流,欢迎留言评论,非常感谢!
前端菜鸟,还请多多关照!


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342