canvas:雨下一整晚

538 阅读2分钟

我正在参加 码上掘金体验活动,详情:show出你的创意代码块

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

前言

这次整了整canvas的烂大街效果之一:下雨,于是迫不及待拿来分享(shui)给大家,有需要的小伙伴也可以动手跟着摸摸看,虽然烂大街,逼格还是有的(狗头)

效果

rain.gif

实现

原理就是canvas的画线,在黑色的画布上画很多根白线,然后不停地计算每根线的位置,更新画布;原理其实还挺简单的,下面贴出一些核心的代码和解析吧~

建立一个RainWeather类

class RainWeather{
  constructor(ctx,width,height,opts){
    this._options = opts || {} // 外部传入配置项,雨丝数量和速度
    this.ctx=ctx // 绘图的句柄,canvas的核心
    this.w=width 
    this.h=height
    this.rains = []; // 存放雨丝
    this.timer = null; // 计时器,用于停止
    this._init();
  }
 }

主要包括背景宽高、配置项、雨丝数组的初始化,用于后续逻辑处理,后面再慢慢补充逻辑就好啦

初始化

_init(){
    let {ctx} = this
    ctx.lineWith = 2;
    ctx.lineCap = 'round' // 圆头
    let amount = this._options.amount || 100
    let rainArr = (this.rains = [])
    for(let i =0; i <amount; i++){
      rainArr.push(this._yieldRain())
    }
  }
  _yieldRain(){ // 生成雨
    const {w,h} = this
    let speed = h/this._options.speed/1000 // option.speed秒内落到地面
    return {
      x:Math.random() * w,
      y:Math.random() * h,
      l:2*Math.random(), // 雨丝的尾端
      xs:-1, // 往左下
      ys:speed * Math.random()+10, // 随机速度,最小为10
      color:'rgba(255,255,255,.4)' // 雨是有点透明的
    }
  }

两个内部方法,一个是设置线条宽度和样式,雨丝圆头更逼真一点;然后是根据外部传入的雨丝数量,生成雨丝;

这里的speed也由外部传入(硬追究的话这个外部传入的speed应该是在n*1000个更新频率内落到底部),逻辑可以自己调整,直接简单的像素加法也是可以的;

雨丝实例的属性包括:

  • 起始位置,
  • 尾端位置(偏移量),设为随机,长短不一
  • 水平速度
  • 垂直速度,设为随机,更真实一点,并设置最小值(随机可能为0)
  • 颜色,白色半透明

下吧

  _draw(){
    let rainArr = this.rains
    let ctx = this.ctx
    ctx.clearRect(0,0,this.w,this.h);  
    for(let i = 0; i < rainArr.length;i++){
      // 画雨丝
      let s =rainArr[i];
      ctx.beginPath();
      ctx.moveTo(s.x,s.y)
      ctx.lineTo(s.x+s.l * s.xs,s.y+s.l * s.ys);
      ctx.strokeStyle = s.color
      ctx.stroke()
    }
    this._update()
  }
  _update() {
    let { w, h } = this
    let rainArr = this.rains
    for(let i=0;i<rainArr.length;i++){
      let s = rainArr[i]
      s.x += s.xs
      s.y += s.ys
      if(s.x>w || s.y >h){ // 超出屏幕界限了回到天上
        s.x = Math.random() * w // 换个位置落下
        s.y = -10  // 回到天上
      }
    }
  }

这里的逻辑就是

  • draw:负责读取雨丝数组,并把每一项画到画布上
  • update:负责计算每个雨丝下一帧的位置,并更新到实例对象内

画雨丝就是遍历刚刚生成的雨丝数组,把每一个雨丝实例用画线三连:ctx.moveToctx.lineToctx.stroke()画到画布上,不了解的小伙伴可以搜索一下哈;

这里注意在画之前要清空上一帧画下的雨丝,相当于每一帧画一次,然后送去更新

更新则是根据每个雨丝的横纵速度进行计算,若超出了画布也别浪费,就回到天上,换个位置重新落下;

(话说这里合并做掉的话是不是可以省一次遍历)

运行状态控制

  run(){
      this.timer = setInterval(()=>{
        this._draw();
      },16) //1秒60帧 1000/60 = 16秒/帧
  }
  stop(){
    clearInterval(this.timer);
    this.rains = []
    this.timer = null;
  }
  resizeBg(w,h){
    this.stop();
    this.w = w;
    this.h = h;
    this._init()
  }

包含启动、停止、已经屏幕尺寸变化的重新启动逻辑

调用

  const canvas = document.querySelector('#rain');
  const ctx = canvas.getContext('2d');
  let width =  window.innerHeight;
  let height =  window.innerHeight
  canvas.width = width;
  canvas.heihgt = height;
  let rain;
  window.onresize = (()=>{
    width =  window.innerHeight;
    height =  window.innerHeight
    canvas.width = width;
    canvas.heihgt = height;
    rain && rain.resizeBg(width,height)
  })
  rain = new RainWeather(ctx,width,height,{amount:100,speed:2})
  rain.run();

其实就是获取屏幕尺寸,然后传参构造一个实例,当屏幕变化就更新尺寸;也可以在这里调整一下速度和数量

结束

本来想在页面内加一个手动输入参数的入口,但是感觉有点破坏,遂罢,想调节速度和雨量的改代码吧

完整代码:

仓库

有问题和意见的可以在评论区指出感谢