WeChat Mini Program | canvas adds rain and snow effects to your weather forecast

In Canvas development, the particle system is often mentioned. The particle system can be used to simulate abstract visual effects such as fire, fog, clouds, and snow. It is a great practice for adding diversified effects to the page newly added by HTML5.

It just so happened that a weather WeChat applet I made recently came to an end, thinking about what appears to be missing on the homepage where information such as temperature is displayed. I took a look at the weather forecast that comes with Android, and suddenly realized. So combined with the canvas that I studied some time ago, I added rain and snow particles to it.


"Drawing" API in Mini Program

Although the drawing API of the applet is also called canvas, it is essentially different from the Canvas of HTML5. The reason is that the drawing of the applet (Canvas) is a Native UI component implemented by the client, not a normal H5 component, so The usage is slightly different from that of ordinary H5 components.

The implementation of IOS and Android is not the same. It is said that it is based on Ejecta .

It is mainly reflected in the following ways:

  1. Different context acquisition methods

Drawing of canvasContext applet API is acquired by way of <canvas>a canvas-id to obtain, namely:

<canvas canvas-id="test"></canvas>
//获取canvas
let ctx = wx.createCanvasContext('test')

There is one point here: it is not the same as "getting elements"!

  1. API is written differently

The usage of the drawing API of the former applet is different from most HTML5 Canvas attribute writing. It has its own applet writing, for example:

const ctx = wx.createCanvasContext('myCanvas')
ctx.setFillStyle('red')
ctx.fillRect(10, 10, 150, 75)
ctx.draw()

However, it is worth mentioning that above the 1.9.0 basic library, things like fillStyle and lineWidth can be written directly in the same way as H5, and there is no need to use setXxxx.

  1. Drawing want to show results, you need ctx.draw()to use

Small drawing program, then draw on the context, and does not immediately drawn to the canvas, but by performing ctx.draw()the way, in the previously described drawing context (path, deformation, pattern) drawn on the canvas. But the ctx.draw()method is relatively consumption performance and is not recommended multiple calls in a draw cycle!


Particle system design

In this small program, the author uses the particle system to make rain and snow effects, using es6-Class notation: the particle system is composed of base classes and subclasses. Particle is the base class, subclass defines uniform way, such as run(), stop(), clear()and the like. The base class is responsible for the maintenance of the entire particle system animation cycle and flow, and the subclass is responsible for the specific particle effects implemented. For example, the effect of rain and snow is implemented by the subclass, while the switch and public processing flow of rain and snow are controlled by the base class. of.

The base class consists of the following methods:

  • _init(): The method to be executed first at instantiation; empty, which is implemented by subclasses
  • _draw(): The method used to draw pictures in each animation cycle; empty, which is implemented by subclasses
  • run: set the timer, execute _draw() regularly to realize the animation cycle
  • stop: stop the animation
  • clear: stop the animation and clear the drawing board

The general process is as follows:

  • Call _init in the constructor to randomly generate a single particle and put it into an array object;
  • When the instance run is executed, the timer is set, and the timer callback calls _draw to draw the particles and set the properties of the individual particles in the next step;
  • And _init and _draw are implemented by subclasses according to the effect

According to the above explanation, the whole process is very clear:

//同级的effect.js文件
// 两个状态
const STATUS_STOP = 'stop'
const STATUS_RUNNING = 'running'
//“基”类-这里就直接当“下雨”类了
class Particle {
    
    
  constructor(ctx, width, height, opts) {
    
    
    this._timer = null
    this._options = opts || {
    
    }
    // canvas 上下文
    this.ctx = ctx
    this.status = STATUS_STOP
    this.w = width
    this.h = height

    this._init()
  }
  _init() {
    
    }
  _draw() {
    
    }
  _update(){
    
    }
  run() {
    
    
    if (this.status !== STATUS_RUNNING) {
    
    
      // 更改状态
      this.status = STATUS_RUNNING
      // 绘制循环
      this._timer = setInterval(() => {
    
    
        this._draw()
      }, 30)
    }
    return this
  }
  stop() {
    
    
    // 清理定时器,状态修改
    this.status = STATUS_STOP
    clearInterval(this._timer)
    return this
  }
  clear(){
    
    
    this.stop()
    this.ctx.clearRect(0, 0, this.w, this.h)
    this.ctx.draw()
    return this
  }
}
exports.Particle=Particle

According to the above prompt, in _init()the, according to the number of particles to be generated randomly generated each particle circulation amount, into this.particlesan array:

// _init
let h = this.h
let w = this.w
// 数量,根据不同雨大小,数量可调
let amount = this._options.amount || 100
// 速度参数,调节下落速度
let speedFactor = this._options.speedFactor || 0.03
let speed = speedFactor * h
let ps = (this.particles = [])
for (let i = 0; i < amount; i++) {
    
    
  let p = {
    
    
    x: Math.random() * w,
    y: Math.random() * h,
    l: 2 * Math.random(),
    xs: -1,
    ys: 10 * Math.random() + speed,
    color: 'rgba(0, 0, 0, 0.15)'
  }
  ps.push(p)
}

among them:

  • x and y represent the position of a single particle, that is, the position where the raindrop starts drawing
  • xs and ys represent the acceleration in the x and y directions, that is, the falling speed and angle of raindrops
  • l represents the length of the raindrop

And _draw()the method, the canvas is first emptied and then traverse this.particlesthe array and remove the individual raindrops draw, a separate call to achieve the final _update()position of the individual raindrops recalculated:

// _draw
let ps = this.particles
let ctx = this.ctx
// 清空画布
ctx.clearRect(0, 0, this.w, this.h)
// 遍历绘制雨滴
for (let i = 0; i < ps.length; i++) {
    
    
  let s = ps[i]
  ctx.beginPath()
  ctx.moveTo(s.x, s.y)
  // 画线绘制雨点效果
  ctx.lineTo(s.x + s.l * s.xs, s.y + s.l * s.ys)
  ctx.setStrokeStyle(s.color)
  ctx.stroke()
}
ctx.draw()
return this._update()

And _update(), we have to do is determine "the next time a raindrop every position," and "exceeds the" "range" Canvas:

// _update
let {
    
    w, h} = this // 获取画布大小
for (let ps = this.particles, i = 0; i < ps.length; i++) {
    
    
  // 开始下一个周期的位置计算
  let s = ps[i]
  s.x += s.xs
  s.y += s.ys
  // 超出范围,重新回收,重复利用
  if (s.x > w || s.y > h) {
    
    
    s.x = Math.random() * w
    s.y = -10
  }
}

rain-canvas

At a glance, we will find that, except for the different names of the calls, it seems to be no different from the canvas API 1 in native js .

The above is to control the "rain". In fact, the only difference between the snowing example class and the rain is the "particle shape":

class Snow extends Particle {
    
    
  _init() {
    
    
    let {
    
    w, h} = this
    let colors = this._options._colors || ['#ccc', '#eee', '#fff', '#ddd']
    // 雪的大小用数量来计算
    let amount = this._options.amount || 100

    let speedFactor = this._options.speedFactor || 0.03
    // 速度
    let speed = speedFactor * h * 0.15

    let radius = this._options.radius || 2
    let ps = (this.particles = [])

    for (let i = 0; i < amount; i++) {
    
    
      let x = Math.random() * w
      let y = Math.random() * h
      ps.push({
    
    
        x,
        y,
        // 原始 x 坐标,后面计算随机雪摆动是以此为基础
        ox: x,
        // 向下运动动能变量
        ys: Math.random() + speed,
        // 雪的半径大小
        r: Math.floor(Math.random() * (radius + 0.5) + 0.5),
        // 颜色随机取
        color: colors[Math.floor(Math.random() * colors.length)],
        rs: Math.random() * 80
      })
    }
  }
  _draw() {
    
    
    let ps = this.particles
    let ctx = this.ctx
    ctx.clearRect(0, 0, this.w, this.h)
    for (let i = 0; i < ps.length; i++) {
    
    
      let {
    
    x, y, r, color} = ps[i]
      ctx.beginPath()
      // 绘制下雪的效果
      ctx.arc(x, y, r, 0, Math.PI * 2, false)
      ctx.setFillStyle(color)
      ctx.fill()
      ctx.closePath()
    }

    ctx.draw()
    this._update()
  }
  _update() {
    
    
    let {
    
    w, h} = this
    let v = this._options.speedFactor / 10
    for (let ps = this.particles, i = 0; i < ps.length; i++) {
    
    
      let p = ps[i]
      let {
    
    ox, ys} = p
      p.rs += v
      // 这里使用了 cos,做成随机左右摆动的效果
      p.x = ox + Math.cos(p.rs) * w / 2
      p.y += ys
      // console.log(ys)
      // 重复利用
      if (p.x > w || p.y > h) {
    
    
        p.x = Math.random() * w
        p.y = -10
      }
    }
  }
}

Use particle system

First, in the WXML code, add a Canvas component with an id of effect to the real-time weather module:

<canvas canvas-id="effect" id="effect"></canvas>

Then introduce the above js file:

import Particle from './effect'
let Rain=Particle.Particle

Important: In the WeChat applet, the unit of length and width in the drawing API (Canvas) is px, and our page layout uses rpx. Although we have used rpx to set the Canvas size in CSS, it is due to internal units. When instantiating the Rain/Snow particle system, the width and height parameters passed in should be the actual px size.

rpx to px is converted according to different device screen sizes. Although the cut image can be paged according to the standard iPhone 6 visual draft such as 1rpx=2px, and WeChat seems to help us to do the compatible processing, but when it comes to actual px calculation, we still can’t simply use 1rpx=2px to solve it. We need to follow The actual rpx is converted to the ratio of px. How to get the actual ratio of rpx and px? We know that small micro-channel program specifies the default screen width 750rpx, according to this design, we can wx.getSystemInfoget to the information, find the size of the phone screen width windowWidthratio can be calculated corresponding code is as follows:

// 在 onload 内
wx.getSystemInfo({
    
    
  success: (res) => {
    
    
    let width = res.windowWidth
    this.setData({
    
    
      width,
      scale: width / 375
    })
  }
})

Thus, the above width is the actual width of the screen px, px actual height of each element by 元素 rpx 高度 / 2 * scaleobtained.

Finally, in the page code, the code in actual use is as follows:

const ctx = wx.createCanvasContext('effect')
let {
    
    width, scale} = this.data
// 768 为 CSS 中设置的 rpx 值
let height = 768 / 2 * scale
let rain = new Rain(ctx, width, height, {
    
    
  amount: 100,
  speedFactor: 0.03
})
// 跑起来
rain.run()

Call clear to remove the effect when switching cities or detecting no rain or snow:

rain.clear()

  1. Interested friends can refer to this article: HTML5 canvas basics and "business card generation" application ↩︎

Guess you like

Origin blog.csdn.net/qq_43624878/article/details/110059776