我正在参加 码上掘金体验活动,详情:show出你的创意代码块
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
前言
这次整了整canvas的烂大街效果之一:下雨,于是迫不及待拿来分享(shui)给大家,有需要的小伙伴也可以动手跟着摸摸看,虽然烂大街,逼格还是有的(狗头)
效果
实现
原理就是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.moveTo、ctx.lineTo、ctx.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();
其实就是获取屏幕尺寸,然后传参构造一个实例,当屏幕变化就更新尺寸;也可以在这里调整一下速度和数量
结束
本来想在页面内加一个手动输入参数的入口,但是感觉有点破坏,遂罢,想调节速度和雨量的改代码吧
完整代码:
有问题和意见的可以在评论区指出感谢