canvas花瓣飘落

351

先上效果:monkeyinwind.github.io/canvaspetal…
github:github.com/MonkeyInWin…

这个demo写了很久了,今天有时间简单写一下过程。
用了react,这个不重要,随便用什么环境都可以。
首先花瓣要有素材,随便搜了一下,切了几个出来。

image.png

1、在页面上添加一个canvas

整个页面只有一个canvas,我们需要这个canvas占满整个浏览器可视区,并且在浏览器窗口改变大小的时候依然和可视区大小相同,同时给canvas加个背景色。

image.png
这一步很简单没有什么需要说的。

2、在canvas上画一个花瓣

创建一个createPetal函数

createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    let img = new Image();
    img.src = require("./images/petal1.png");
    img.onload = () => {
      ctx.drawImage(img, 100, 100);
    }
  }

componentDidMount调用

componentDidMount() {
    this.setCanvas();
    window.onresize = this.setCanvas;
    this.createPetal();
  }

这样就在100, 100这个位置画了个花瓣

image.png

3、让这个花瓣动起来

canvas动画是高频率刷新,清空上一帧,画下一帧,看起来是动画。
了解了动画的原理,接下来就可以开始写动画,首先将坐标放到state中。

this.state = {
      cw: 0,
      ch: 0,
      x: 0,
      y: 0
    }

创建一个go函数。

go(ctx, img) {
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    this.setState({
      x: this.state.x + 1,
      y: this.state.y + 1
    });//移动花瓣坐标
    ctx.drawImage(img, this.state.x, this.state.y);
    window.requestAnimationFrame(() => {
      this.go(ctx, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

createPetal中调用。

createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    let img = new Image();
    img.src = require("./images/petal1.png");
    img.onload = () => {
      ctx.drawImage(img, this.state.x, this.state.y);
      this.go(ctx, img);
    }
  }

接下来可以看到效果。

Feb-03-2019 10-16-19.gif
录的有点卡,实际上要比这个效果好很多。。。
有没有发现问题,花瓣位置超出浏览器之后去哪了打印一下坐标。
image.png
可以看到还在继续飘,这不是想要的,所以在坐标超出浏览器之后让它回到初始位置。
go这个函数修改如下:

go(ctx, img) {
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    let x = this.state.x + 1;
    let y = this.state.y + 1;
    if (x > this.state.cw || y > this.state.ch) {
      x = 0;
      y = 0;
    }
    this.setState({
      x,
      y
    });//移动花瓣坐标
    ctx.drawImage(img, this.state.x, this.state.y);
    window.requestAnimationFrame(() => {
      this.go(ctx, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

看一下效果

Feb-03-2019 10-24-14.gif

这一步实现之后,有没有发现还有问题,要模拟自然飘落,这个花瓣不可能没有旋转,接下来再加上旋转。
这个旋转,需要的是画布旋转,旋转画好了之后再复位。
state中加上旋转角度:

this.state = {
      cw: 0,
      ch: 0,
      x: 0,
      y: 0,
      r: 0   //旋转角度
    }

go里面加上旋转,并且为了统一动作和计算方便,这里将图片位移改为画布位移,画图坐标相对画布始终在同一位置:

go(ctx, img) {
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    let x = this.state.x + 1;
    let y = this.state.y + 1;
    let r = this.state.r + 0.1;
    if (x > this.state.cw || y > this.state.ch) {
      x = 0;
      y = 0;
    }
    this.setState({
      x,
      y,
      r
    });//移动花瓣坐标
    ctx.save();//保存画布当前状态
    ctx.translate(this.state.x, this.state.y); //改为画布位移
    ctx.rotate(this.state.r);   //画布旋转
    ctx.drawImage(img, 0, 0);  //画图坐标始终在画布左上角
    ctx.restore();
    window.requestAnimationFrame(() => {
      this.go(ctx, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

Feb-03-2019 10-48-19.gif
和预想的不太一样,这是因为画布默认的旋转中心为左上角, 我们需要将旋转中心移到图片的中心。

go(ctx, img) {
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    let w = img.width;
    let h = img.height;
    let x = this.state.x + 1;
    let y = this.state.y + 1;
    let r = this.state.r + 0.1;
    if (x > this.state.cw || y > this.state.ch) {
      x = 0;
      y = 0;
    }
    this.setState({
      x,
      y,
      r
    });//移动花瓣坐标
    ctx.save();//保存画布当前状态
    ctx.translate(this.state.x + w / 2, this.state.y + h / 2); //改为画布位移
    ctx.rotate(this.state.r);
    ctx.drawImage(img, -w / 2, - h / 2);  //画图坐标始终在画布左上角
    ctx.restore();
    window.requestAnimationFrame(() => {
      this.go(ctx, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

看一下效果

Feb-03-2019 10-55-18.gif
旋转是有了, 但是好像不太对,只绕Z轴旋转,要让它变成3D旋转,这里要用到缩放scale,缩放这里不可能一直放大或者缩小,所以还要加一个变量控制。

this.state = {
      cw: 0,
      ch: 0,
      x: 0,
      y: 0,
      r: 0,
      scale: 1,
      toLarge: true
    }

接下来将go改一下,加上scale并且旋转速度调整一下:

go(ctx, img) {
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    let w = img.width;
    let h = img.height;
    let x = this.state.x + 1;
    let y = this.state.y + 1;
    let r = this.state.r + 0.05;
    let scale = this.state.scale;
    let toLarge = this.state.toLarge;

    if (scale >= 1) {
      toLarge = false;
    } else if (scale <= 0) {
      toLarge = true;
    }//这里根据scale大小设置toLarge

    if (toLarge) {
      scale += 0.01;
    } else {
      scale -= 0.01;
    }//这里根据toLarge更改scale值
    if (x > this.state.cw || y > this.state.ch) {
      x = 0;
      y = 0;
    }
    this.setState({
      x,
      y,
      r,
      scale,
      toLarge
    });//移动花瓣坐标
    ctx.save();//保存画布当前状态
    ctx.translate(this.state.x + w / 2, this.state.y + h / 2); //改为画布位移
    ctx.rotate(this.state.r);
    ctx.scale(1, this.state.scale);
    ctx.drawImage(img, -w / 2, - h / 2);  //画图坐标始终在画布左上角
    ctx.restore();
    window.requestAnimationFrame(() => {
      this.go(ctx, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

看一下效果

Feb-03-2019 11-13-14.gif
至此一个花瓣就写完了。
但是我们想要的是很多个花瓣同时飘。
这就需要一个花瓣的类。

4、创建一个花瓣的class

新建一个petal.js

export default class Petal {
  constructor(w, h) {
    this.canvasW = w;  //canvas宽
    this.canvasH = h;  //canvas高
    this.w = 0;        //花瓣宽
    this.h = 0;        //花瓣高
    this.x = 0;        //初始x坐标
    this.y = 0;        //初始y坐标
    this.r = 0;        //初始旋转角度
    this.scale = 1;    //初始缩放
    this.toLarge = false;   //默认放大为false
    this.speedX = 1;   //x方向速度
    this.speedY = 1;   //y方向速度
    this.speedScale= 0.01  //缩放速度
    this.speedR = 0.05    //旋转速度
  }
  //数据初始化,用于当花瓣超出浏览器可视区时重置位置
  init() {
    this.x = 0;
    this.y = 0;
    this.r = 0;
    this.scale = 1;
    this.speedX = 1;
    this.speedY = 1;
    this.speedScale = 0.01;
    this.speedR = 0.05;
  }
  //画布位移、画图、画布复位
  draw(ctx, img) {
    this.w = img.width;
    this.h = img.height;
    ctx.save();     //保存当前画布状态
    ctx.translate(this.x + this.w / 2,  this.y + this.h / 2);  //画布位移
    ctx.rotate(this.r);   //画布旋转
    ctx.scale(1, this.scale);  //画布缩放
    ctx.drawImage(img, -this.w / 2, -this.h / 2);   //画图
    ctx.restore();    //画布复位
  }
  //计算坐标
  move() {
    this.x += this.speedX;
    this.y += this.speedY;
    this.r += this.speedR;
    if (this.scale >= 1) {
      this.toLarge = false;
    } else if (this.scale <= 0) {
      this.toLarge = true;
    }

    if (this.toLarge) {
      this.scale += this.speedScale;
    } else {
      this.scale -= this.speedScale;
    }

    if (this.x >= this.canvasW || this.y >= this.canvasH) {
      this.init();
    }
  }
}

App.js内引入并new一个花瓣,打印一下;

import React, { Component } from 'react';
import './App.css';
import Petal from './petal';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      cw: 0,
      ch: 0,
      x: 0,
      y: 0,
      r: 0,
      scale: 1,
      toLarge: true
    }

    this.setCanvas = this.setCanvas.bind(this);
    this.componentDidMount = this.componentDidMount.bind(this);
    this.createPetal = this.createPetal.bind(this);
    this.go = this.go.bind(this);
  }

  setCanvas() {
    let W = document.documentElement.clientWidth;
    let H = document.documentElement.clientHeight;
    this.setState({
      cw: W,
      ch: H
    });
  }

  createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    let img = new Image();
    img.src = require("./images/petal1.png");
    img.onload = () => {
      //ctx.drawImage(img, this.state.x, this.state.y);
      // this.go(ctx, img);
      let petal = new Petal(this.state.cw, this.state.ch);
      console.log(petal);
    }
  }

  go(ctx, img) {
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    let w = img.width;
    let h = img.height;
    let x = this.state.x + 1;
    let y = this.state.y + 1;
    let r = this.state.r + 0.05;
    let scale = this.state.scale;
    let toLarge = this.state.toLarge;

    if (scale >= 1) {
      toLarge = false;
    } else if (scale <= 0) {
      toLarge = true;
    }

    if (toLarge) {
      scale += 0.01;
    } else {
      scale -= 0.01;
    }
    if (x > this.state.cw || y > this.state.ch) {
      x = 0;
      y = 0;
    }
    this.setState({
      x,
      y,
      r,
      scale,
      toLarge
    });//移动花瓣坐标
    ctx.save();//保存画布当前状态
    ctx.translate(this.state.x + w / 2, this.state.y + h / 2); //改为画布位移
    ctx.rotate(this.state.r);
    ctx.scale(1, this.state.scale);
    ctx.drawImage(img, -w / 2, - h / 2);  //画图坐标始终在画布左上角
    ctx.restore();
    window.requestAnimationFrame(() => {
      this.go(ctx, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

  componentDidMount() {
    this.setCanvas();
    window.onresize = this.setCanvas;
    this.createPetal();
  }

  render() {
    return (
      <div className="App">
      <canvas id="canvas" ref="canvas" width={this.state.cw} height={this.state.ch}></canvas>
      </div>
    );
  }
}

export default App;

image.png
可以看见已经创建了一个初始的花瓣,暂时还没有画图片。
接下来就是把之前的go改一下,画上花瓣并动起来。
App.js更改后如下:

import React, { Component } from 'react';
import './App.css';
import Petal from './petal';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      cw: 0,
      ch: 0
    }

    this.setCanvas = this.setCanvas.bind(this);
    this.componentDidMount = this.componentDidMount.bind(this);
    this.createPetal = this.createPetal.bind(this);
    this.go = this.go.bind(this);
  }

  setCanvas() {
    let W = document.documentElement.clientWidth;
    let H = document.documentElement.clientHeight;
    this.setState({
      cw: W,
      ch: H
    });
  }

  createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    let img = new Image();
    img.src = require("./images/petal1.png");
    img.onload = () => {
      let petal = new Petal(this.state.cw, this.state.ch);
      this.go(ctx, petal, img);
    }
  }

  go(ctx, petal, img) {
    let W = this.state.cw;
    let H = this.state.ch;
    //浏览器窗口改变大小时同步更新petal的cnavas宽高值,与花瓣坐标对比判断是否在可视区内
    petal.canvasW = W;
    petal.canvasH = H;
    ctx.clearRect(0, 0, this.state.cw, this.state.ch);//清空画布
    petal.move();
    petal.draw(ctx, img);

    window.requestAnimationFrame(() => {
      this.go(ctx, petal, img);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

  componentDidMount() {
    this.setCanvas();
    window.onresize = this.setCanvas;
    this.createPetal();
  }

  render() {
    return (
      <div className="App">
      <canvas id="canvas" ref="canvas" width={this.state.cw} height={this.state.ch}></canvas>
      </div>
    );
  }
}

export default App;

5、很多花瓣

一个花瓣已经完成了,接下来就是很多个花瓣。
这里涉及到几个点:
1、img的src不能用变量,所以要用字符串拼接变量的形式。
2、一个花瓣用了onload,很多花瓣很明显一个onload已经不能满足了,这里用promise.all
3、创建很多花瓣,并不是每次drawImage都需要clearRect,需要在第0个画之前清空canvas。
4、关于初始坐标和初始速度,很多个花瓣就需要随机坐标和随机速度,而且初始化所在的区域需要计算,否则会出现花瓣位移过程中不经过浏览器可视区或者分布不均。

img的src

state里加上花瓣数组,这里不能带后缀。

this.state = {
      cw: 0,
      ch: 0,
      n: 60,   //所要创建的花瓣数量
      imgnames: [
        "petal1",
        "petal2",
        "petal3",
        "petal4",
        "petal5",
        "petal6",
        "petal7",
        "petal8"
      ]
    }

createPetal函数改一下,创建多个img:

createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    // let img = new Image();
    // img.src = require("./images/petal1.png");
    let totalNum = this.state.imgnames.length; //图片的总数量
    for (let i = 0; i < this.state.n; i++) {
      let imgname = this.state.imgnames[i % totalNum];
      let img = new Image();
      img.src = require(`./images/${imgname}.png`);
      console.log(img)
    }

    // img.onload = () => {
      // let petal = new Petal(this.state.cw, this.state.ch);
      // this.go(ctx, petal, img);
    // }
  }

打印出60个img,src为base64;

所有图片onload

这里把单个img的load封装为promise,添加到一个数组里,然后用promise.all
新建一个imgLoad函数,返回一个load的promise;
新建一个allImgLoad函数,用于返回一个promise.all

imgLoad(imgname) {
    return new Promise((resolve, reject) => {
      try {
        let img = new Image();
        img.src = require(`./images/${imgname}.png`);
        img.onload = () => {
          resolve(img);
        }
      } catch(e) {
        reject(e);
      }
    });
  }

  allImgLoad(imgnames) {
    let p = [];
    for(let i = 0; i < imgnames.length; i++) {
      p.push(this.imgLoad(imgnames[i]));
    }
    return Promise.all(p).then(res => {
      return res;
    }).catch((e) => {
      console.log(e);
    });
  }

  async createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    // let img = new Image();
    // img.src = require("./images/petal1.png");
    let imgnames = [];
    let totalNum = this.state.imgnames.length; //图片的总数量
    for (let i = 0; i < this.state.n; i++) {
      let imgname = this.state.imgnames[i % totalNum];
      imgnames.push(imgname);
    }
    let imgs = await this.allImgLoad(imgnames);
    console.log(imgs)
  }

可以看到打印出了60个img

image.png

很多花瓣

每一帧画60个花瓣,并且在第0个画之前清空画布,如果每画一个都清空一次,会把前59个都清空,画布上只有最后一个。
Petal类里边的moveinit用异步,加个async,否则会出现有的花瓣跳帧或者init的时候花瓣突然出现在屏幕上。

export default class Petal {
  constructor(w, h) {
    this.canvasW = w;  //canvas宽
    this.canvasH = h;  //canvas高
    this.w = 0;        //花瓣宽
    this.h = 0;        //花瓣高
    this.x = 0;        //初始x坐标
    this.y = 0;        //初始y坐标
    this.r = 0;        //初始旋转角度
    this.scale = 1;    //初始缩放
    this.toLarge = false;   //默认放大为false
    this.speedX = 1;   //x方向速度
    this.speedY = 1;   //y方向速度
    this.speedScale= 0.01  //缩放速度
    this.speedR = 0.05    //旋转速度
  }
  //数据初始化,用于当花瓣超出浏览器可视区时重置位置
  async init() {
    this.x = 0;
    this.y = 0;
    this.r = 0;
    this.scale = 1;
    this.speedX = 1;
    this.speedY = 1;
    this.speedScale = 0.01;
    this.speedR = 0.05;
  }
  //画布位移、画图、画布复位
  draw(ctx, img) {
    this.w = img.width;
    this.h = img.height;
    ctx.save();     //保存当前画布状态
    ctx.translate(this.x + this.w / 2,  this.y + this.h / 2);  //画布位移
    ctx.rotate(this.r);   //画布旋转
    ctx.scale(1, this.scale);  //画布缩放
    ctx.drawImage(img, -this.w / 2, -this.h / 2);   //画图
    ctx.restore();    //画布复位
  }
  //计算坐标
  async move() {
    this.x += this.speedX;
    this.y += this.speedY;
    this.r += this.speedR;
    if (this.scale >= 1) {
      this.toLarge = false;
    } else if (this.scale <= 0) {
      this.toLarge = true;
    }

    if (this.toLarge) {
      this.scale += this.speedScale;
    } else {
      this.scale -= this.speedScale;
    }

    if (this.x >= this.canvasW || this.y >= this.canvasH) {
      await this.init();
    }
  }
}

import React, { Component } from 'react';
import './App.css';
import Petal from './petal';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      cw: 0,
      ch: 0,
      n: 60,
      imgnames: [
        "petal1",
        "petal2",
        "petal3",
        "petal4",
        "petal5",
        "petal6",
        "petal7",
        "petal8"
      ]
    }

    this.setCanvas = this.setCanvas.bind(this);
    this.componentDidMount = this.componentDidMount.bind(this);
    this.createPetal = this.createPetal.bind(this);
    this.go = this.go.bind(this);
    this.imgLoad = this.imgLoad.bind(this);
    this.allImgLoad = this.allImgLoad.bind(this);
  }

  setCanvas() {
    let W = document.documentElement.clientWidth;
    let H = document.documentElement.clientHeight;
    this.setState({
      cw: W,
      ch: H
    });
  }

  imgLoad(imgname) {
    return new Promise((resolve, reject) => {
      try {
        let img = new Image();
        img.src = require(`./images/${imgname}.png`);
        img.onload = () => {
          resolve(img);
        }
      } catch(e) {
        reject(e);
      }
    });
  }

  allImgLoad(imgnames) {
    let p = [];
    for(let i = 0; i < imgnames.length; i++) {
      p.push(this.imgLoad(imgnames[i]));
    }
    return Promise.all(p).then(res => {
      return res;
    }).catch((e) => {
      console.log(e);
    });
  }

  async createPetal() {
    let canvas = this.refs["canvas"];
    let ctx = canvas.getContext("2d");
    let imgnames = [];
    let totalNum = this.state.imgnames.length; //图片的总数量
    for (let i = 0; i < this.state.n; i++) {
      let imgname = this.state.imgnames[i % totalNum];
      imgnames.push(imgname);
    }
    let imgs = await this.allImgLoad(imgnames);
    if(!imgs) return;
    for(let i = 0; i < imgs.length; i++) {
      let petal = new Petal(canvas.width, canvas.height);
      this.go(ctx, petal, imgs[i], i);
    }
  }

  async go(ctx, petal, img, index) {
    let W = this.state.cw;
    let H = this.state.ch;
    //浏览器窗口改变大小时同步更新petal的cnavas宽高值,与花瓣坐标对比判断是否在可视区内
    petal.canvasW = W;
    petal.canvasH = H;
    if( index === 0) {
      ctx.clearRect(0, 0, W, H);//清空画布
    }
    await petal.move();
    petal.draw(ctx, img);

    window.requestAnimationFrame(() => {
      this.go(ctx, petal, img, index);
    });//重复清空画布,移动坐标重新画花瓣这个动作。
  }

  componentDidMount() {
    this.setCanvas();
    window.onresize = this.setCanvas;
    this.createPetal();
  }

  render() {
    return (
      <div className="App">
      <canvas id="canvas" ref="canvas" width={this.state.cw} height={this.state.ch}></canvas>
      </div>
    );
  }
}

export default App;

这个时候60个花瓣叠在一起,看一下效果

Feb-03-2019 14-34-22.gif

随机初始化

首先要确定一下花瓣初始化的随机区域,有以下几点要求。 1、除了打开页面或者刷新页面,可以出现在浏览器可视区,其他情况下要出现在可视区外,从可视区边缘飘进可视区。
2、花瓣移动的路径要经过可视区,并且不会出现在左下角或者右上角只有半个花瓣划过的情况,没有意义。
3、分布均匀

接下来就是具体实施,先画个图,便于理解。

image.png

把浏览器45度向左上方平移,我们需要花瓣出现在两条红线之间的区域,并且当花瓣移出浏览器可视区之后,只能出现在蓝色斜线区域。
这里花瓣首先随机出现在整个大矩形里,如果出现在想要的区域外,我们做如下处理:

image.png
这样可以保证所有花瓣都会经过浏览器可视区,左下角和右上角不会出现半个花瓣的情况,并且均匀分布整个浏览器可视区。
移动端同理这里就不画图了下面上代码:

const randNum = (min, max) => {
  return Math.random() * (max - min) + min;
}

const calculateXY = (w, h) => {
  return new Promise((resolve, reject) => {
    let x = randNum(-h + 100, w - 100);
    let y = randNum(-h + 100, h - 100);
    let b = 60;   //这里是加一个偏移量,防止移出可视区后初始化位置时突然在可视区上边缘和做边缘出现。
    if (w >= h) {
      let a = w - h;
      //坐标在canvas区域,移到左上方同canvas大小区域
      if (x > -b && y > -b) {
        x = randNum(-h + b, a - b);
        y = randNum(-h + b, -b);
      } else if (x > a - b && y < -(h - (x - a) + b)) {
        //坐标在canvas右上方三角形区域,飘落不经过canvas,移到正上方三角形区域
        y = randNum(-(h - (x - a) + b), -b);
      } else if (x < -b && y > h + x - b) {
        //坐标在canvas左下方三角形区域,飘落不经过canvas,移到正左方三角形区域
        y = randNum(0, h + x - b);
      }
    } else {
      let a = h - w;
      if (x > -b && y > -b) {
        x = randNum(-w + b, -b);
        y = randNum(-w + b, a - b);
      } else if (x > -b && y < -(w - x) + b) {
        y = randNum(-(w - x) + b, -b);
      } else if (x < -b && y > h - x - b) {
        y = randNum(a, h - x - b);
      }
    }
    resolve({x, y});
  });
}

export default class Petal {
  constructor(w, h) {
    this.canvasW = w;
    this.canvasH = h;
    this.w = 0;
    this.h = 0;
    this.y = randNum(-h + 100, h - 100); //这里两个100是防止直接出现在可视区边缘半个直接飘出去了
    this.x = randNum(-h + 100, w - 100);
    this.r = Math.random();
    this.scale = -Math.random();
    this.toLarge = false;
    this.speedX = Math.random() * 0.5 + 0.5;
    this.speedY = this.speedX;
    this.speedScale = Math.random() * 0.007;
    this.speedR = Math.random() * 0.03;
  }

  draw(ctx, img) {
    this.w = img.width;
    this.h = img.height;
    ctx.save();
    ctx.translate(this.x + this.w / 2, this.y + this.h / 2);
    ctx.rotate(this.r);
    ctx.scale(1, this.scale);
    ctx.drawImage(img, -this.w / 2, -this.h / 2);
    ctx.restore();
  }

  async init() {
    let xy = await calculateXY(this.canvasW, this.canvasH);
    this.x = xy.x;
    this.y = xy.y;
    this.r = Math.random();
    this.scale = -Math.random();
    this.speedX = Math.random() * 0.5 + 0.3;
    this.speedY = this.speedX;
    this.speedScale = Math.random() * 0.004;
    this.speedR = Math.random() * 0.03;
  }

  async move() {
    this.x += this.speedX;
    this.y += this.speedY;
    this.r += this.speedR;
    if (this.scale >= 1) {
      this.toLarge = false;
    } else if (this.scale <= 0) {
      this.toLarge = true;
    }

    if (this.toLarge) {
      this.scale += this.speedScale;
    } else {
      this.scale -= this.speedScale;
    }

    if (this.x >= this.canvasW || this.y >= this.canvasH) {
      await this.init();
    }
  }
}

到这里就完成了,看一下帧数。 打开chrome开发者模式

image.png
rendering,勾选FPS meter
image.png
可以看到在60左右,是比较理想的
image.png