canvas为你的天气预报添加雨雪效果 | 微信小程序

关注 前端瓶子君,回复“交流”

加入我们一起学习,天天进步

来源:行舟客

https://yunxiaomeng.blog.csdn.net/article/details/110059776

在 Canvas 开发中,经常会提到粒子系统,使用粒子系统可以模拟出火、雾、云、雪等抽象视觉效果。它是HTML5新增的为页面添加多样化效果的绝佳实践。

正巧最近做的一个天气类微信小程序紧接尾声,寻思着首页展示温度等信息的地方似乎少了点什么。到Android自带的天气预报上看了下,恍然大悟。于是结合前一段时间钻研的canvas为其添加了雨雪粒子效果。


小程序中的“绘图”API

小程序的绘图 API 虽然也叫canvas,但跟 HTML5 的 Canvas 本质上有很大区别的,其原因是小程序的绘图(Canvas)是客户端实现的 Native UI 组件,而不是普通的 H5 组件,所以在使用上跟普通的 H5 组件用法略有不同。

IOS和Android实现方式还不一样,据说它是基于 Ejecta 实现的。

其主要体现在以下方式上:

  1. 上下文获取方式不同

小程序绘图 API 的 canvasContext 获取方式是通过 <canvas>canvas-id 来获取的,即:

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

这里有一点:它并不同于“获取元素”!

  1. API 写法不同

曾经的小程序的绘图 API 在用法上区别于绝大部分的 HTML5 Canvas 属性写法,它有自己的小程序写法,例如:

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

不过值得一提的是,在 1.9.0 基础库以上,类似 fillStyle、lineWidth 这类的,可以直接跟 H5 的写法一样,不需要使用 setXxxx 的方式了。

  1. 想要显示绘制效果,需要 ctx.draw() 使用

在小程序的绘图使用中,对 context 进行绘制之后,并不会立即绘制到画布上,而是通过执行 ctx.draw() 的方式,将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中。但 ctx.draw() 方法比较消耗性能,因此不建议在一个绘制周期内多次调用!


粒子系统的设计

在这个小程序中,笔者使用粒子系统做了雨雪效果,采用es6-Class写法:粒子系统由基类和子类组成。Particle 是基类,定义了子类统一的方法,如 run()stop()clear() 等。基类负责整个粒子系统动画周期和流程的维护,子类负责具体实现的粒子效果,比如下雨下雪的效果是子类实现的,而下雨下雪的开关和公共处理流程是基类控制的。

基类由如下几个方法组成:

  • _init():实例化时第一执行的方法;空,由子类具体实现

  • _draw():每个动效周期内画图用的方法;空,由子类具体实现

  • run:设置定时器,定时执行 _draw(),实现动画周期

  • stop:停止动画

  • clear:停止动画,并且清空画板

其大致流程如下:

  • 在构造器内调用 _init,随机生成单个粒子,放进数组对象;

  • 在执行实例 run 的时候,设置定时器,定时器回调调用 _draw 绘制粒子,设置单个粒子下一步的属性;

  • 而 _init 和 _draw 是子类具体根据效果实现的

根据如上解释,整个流程就很明了了:

//同级的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

根据上面的提示,在 _init() 中,根据需要生成的粒子个数 amount 循环随机生成每个粒子,放入 this.particles 数组:

// _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)
}

其中:

  • x、y 代表单个粒子的位置,即雨滴开始绘图的位置

  • xs、ys 分别代表 x、y 方向上的加速度,即雨滴的下落速度和角度

  • l 代表雨滴的长度

_draw() 的方法,是先将画布清空,然后遍历 this.particles 数组取出单个雨滴并进行绘制,最后调用一个单独实现的 _update() 重新计算单个雨滴的位置:

// _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()

_update() 中,我们要做的就是判断“下一时刻每一个雨点的位置”以及“是否超出了“画布”的范围”:

// _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

我们大致一看会发现,除了调用的名称有的不一样之外,似乎和原生js中的canvas API1没什么区别。

上面是控制“下雨”的,其实下雪的例子类和下雨的唯一区别就是“粒子的形状”了:

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
      }
    }
  }
}

使用粒子系统

首先,在 WXML 代码中,给实时天气模块增加 id 为 effect 的 Canvas 组件:

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

而后引入上面的js文件:

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

重点: 在微信小程序内,绘图 API(Canvas)内的长宽单位为 px,而我们页面布局用的是 rpx,虽然我们在 CSS 内已经使用 rpx 设置了 Canvas 的大小,但是由于内部单位的缘故,在实例化 Rain/Snow 粒子系统的时候,传入的 width 和 height 参数应该是实际的 px 大小。

rpx 转 px 是根据不同的设备屏幕尺寸转换的。虽然切图可以按照 1rpx=2px 这样标准的 iPhone 6 视觉稿做页面,而且微信似乎帮我们做了兼容处理,但是涉及实际 px 计算时,仍不能简单采用 1rpx=2px 的方式来解决,需要我们按照实际的 rpx 对应 px 的比例进行转换。如何获取 rpx 和 px 的实际比例呢?我们知道微信小程序中默认规定了屏幕宽度为 750rpx,根据这个设计,我们可以通过 wx.getSystemInfo 获取到的信息,找到手机屏幕的宽度大小 windowWidth 即可算出对应的比例,代码如下:

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

这样,上面的 width就是屏幕的实际 px 宽度,而每个元素的实际 px 高度则由 元素 rpx 高度 / 2 * scale 得到。

最后,我们在页面代码中,实际使用时的代码是下面这样的:

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()

在切换城市或者检测到没有雨雪天气时调用clear去除效果:

rain.clear()

  1. 感兴趣的朋友可以参见这篇文章:HTML5 canvas基础与「生成名片」应用程序 :https://yunxiaomeng.blog.csdn.net/article/details/107123585

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值