展示

想想坐在火车上回家的时候,窗外飞舞的雪花,一家人坐在一起吃年夜饭时,漫天飞舞的大雪,怎么样,雪花虽冷,随总是让我们感到一股暖意,自古也有瑞雪兆丰年一说,今天就带大家一起亲手来用代码下一场雪。

首先来看一下成品图,怎么样,还行吧,(因为gif录制原因,这是把屏幕放得比较小录制的)

当代码动起来(冬天已经过去春天马上来临)(1)

成品

实现 先来一个背景

当代码动起来(冬天已经过去春天马上来临)(2)

背景

首先来一个灰蒙蒙的背景,别问我为啥搞一个这样的背景(问就是这其实是我下一篇文章写的东西)

画一朵雪花

当代码动起来(冬天已经过去春天马上来临)(3)

雪花

我们这里没有过于追求雪花的精细程度,所以就简单弄了一个小圆点来模拟雪花,再来一个白色的阴影,给雪花一种朦胧感,是不是就有那么回事了。

生成雪花

上面的雪花我们是写死位置的,仅仅是为了先定义雪花的外形,下面我们就来模拟真实下雪的样子,随机的生成雪花,如果你看过之前的打年兽的文章,你肯定会与生成雪花用的方法了然于胸,没错,我们首先获取屏幕的宽度,然后定时的生成雪花,并给每一篇雪花一个随机的宽度,注意,这个宽度要在屏幕的宽度之内,否则我们下到屏幕外面也没啥意义啊,还浪费性能,之后我们给每一片雪花一个定时器,定时往下移动,然后就成了。

这里我们还用requestAnimationFrame,但是requestAnimationFrame没有定时功能,所以我们要记录最后一次生成雪花的时间,和当前时间对比,如果达到我们想要的间隔了,就创建下一片雪花

lastSnowTime: '', // 最后一片雪花生成时间 snowSpeed: 3, // 雪花下落的速度 lastSnowTime: '', // 最后一片雪花生成时间 snowFrequency: 4, // 雪花生成的频率 snowStart () { // 雪花生成的频率 let now = new Date().getTime() if (now - this.lastSnowTime > (1000 / this.snowFrequency)) { console.log(1); // 创建雪花 let snowItem = document.createElement('div') snowItem.className = 'snow-item' snowItem.style.top = -snowItem.offsetWidth 'px' snowItem.style.left = Math.random() * this.screenWidth 'px' this.$refs.snowWrap.appendChild(snowItem) // 雪花移动 let snowMove = () => { snowItem.style.top = snowItem.offsetTop this.snowSpeed 'px' // 如果雪花距离屏幕顶部距离大于等于屏幕高度,则移除此雪花 if (snowItem.offsetTop > this.screenHeight) { this.$refs.snowWrap.removeChild(snowItem) } else { requestAnimationFrame(snowMove) } } snowMove() this.lastSnowTime = now } this.createSnowInterval = requestAnimationFrame(this.snowStart) }

当代码动起来(冬天已经过去春天马上来临)(4)

是不是有那么点意思了

优化雪花

虽然已经达到了我们的初步目的,但是这雪总看着有那么点假,为什么?因为大自然哪有这么规律的雪,一样大小,一样速度的,所以我们得来给他加入更多的随机性。

首先就是透明度,让每一片雪花的透明度来一个随机值

snowItem.style.opacity = Math.random()

其次是大小,我们给每一片雪花一个随机的大小

snowItem.snowScale = Math.random() * 0.5 0.5 snowItem.style.width = snowItem.offsetWidth * snowItem.snowScale 'px' snowItem.style.height = snowItem.offsetHeight * snowItem.snowScale 'px'

那么问题来了,大的雪花和小的雪花下落速度一样吗?我还真没仔细观察过,不过应该是不一样的吧,大的落的快,小的落的慢?所以这里我们让雪花的下落速度跟他的大小扯上关系,大家可以看到,我们上面给雪花随机大小的时候留了一个snowScale的东西,我们暂且称呼他为缩放系数,那么我们的下落速度就要跟这个缩放系数成正比

let moveY = this.snowSpeed * snowItem.snowScale snowItem.style.top = snowItem.offsetTop moveY 'px'

这里我们将纵向偏移量记下来,后面有用,现在再来看一下效果,每片雪花都会基于我们设定的基础大小和速度再增加一定的随机变化,是不是好多了。

当代码动起来(冬天已经过去春天马上来临)(5)

这里有一个遗留的小问题啊,我们前面定义了一个snowFrequency变量,用来控制雪花的生成频率,乍一看好像没啥问题,但是如果我们在不同的设备上看就会发现,屏幕越大,雪花越稀疏,屏幕越小,雪花越密集,这肯定不对啊,所以这个频率我们要让它随着屏幕的变化而变化,并且同时还要我们可以控制。那么我们就可以设定一个变量,假如它是200,就代表1秒时间,每200像素的区域生成一片雪花,这样屏幕越大,一秒钟生成的雪花越多,屏幕越小,生成的雪花也就越少我也不知道咋称呼,咱们暂且称之为区域密度,我们拿屏幕宽度除以这个区域密度,就得出了最终的雪花生成频率。

snowFrequencyRatio: 300, // 雪花频率系数,越大雪花越少 mounted () { // 根据雪花频率系数和屏幕宽度计算雪花生成的频率 this.snowFrequency = Math.floor(this.screenWidth / this.snowFrequencyRatio) this.createCity() this.snowStart() },

来点风吧

既然是飘舞的雪花,一直垂直降落多没意思啊,不如我们来点风,让它飘起来。

思路分析,既然来电风,那肯定就是让雪花横向移动,那移动多少呢?我们最开始给雪花下落的速度给了一个定值,按照这个想法来,我们给雪花横向的偏移量也来一个定值肯定没问题,但是现在的问题是我们雪花的下降速度是和基础下降速度、自身大小都有关的,我们再设一个横向的偏移距离,再让它也跟大小有关系这就太麻烦了,况且这个值设立多少全凭我们随意,太不严谨了。

那么你有没有什么好的办法呢?哈哈哈,这里我们想象一下雪花下落的样子(不考虑雪花曲线飞舞),考虑一下横向偏移量和纵向偏移量的关系,是不是一个Rt△(部分学渣同学是不是已经忘记这是啥了,没错,这就是直角三角形)

当代码动起来(冬天已经过去春天马上来临)(6)

这里∠α是我们设置的偏移量,a就是垂直方向的位移,这两个我们都知道了,那么利用正切公式,tanα = b / a,可以很轻易的算出b的值,也就是横向偏移量的值,在js中我们可以用Math.tan这个方法实现相关的功能,Math.tan方法接收一个弧度值,角度和弧度的转换公式是

弧度角度

所以这里我们横向的偏移量就是

let moveX = Math.tan(this.snowAngle * Math.PI / 180) * moveY snowItem.style.left = snowItem.offsetLeft - moveX 'px'

这样雪花横向移动的效果就出来了(学霸同学是不是很简单,学渣的我看了好半天的正切东西才迷糊过来)

当代码动起来(冬天已经过去春天马上来临)(7)

看起来很美好,但是问题又来了,细心的同学会发现,右下角总是没有雪,那是因为现在雪的轨迹是这样的

当代码动起来(冬天已经过去春天马上来临)(8)

我们在屏幕最右边生成的雪花,落到地上就不在最右边了,而是会偏移一个b的距离,那这个问题怎么解决呢,那就是在雪花生成的时候,就把这个b的距离给算进去,就像这样

当代码动起来(冬天已经过去春天马上来临)(9)

那这个b怎么求的呢?其实跟我们求雪花每一次移动的横向距离的方法是一样的,只不过我们这里的a是屏幕的高度罢了,所以我们生成雪花时给予的随机横向距离应该是这样的

// 在给雪花随机分配横向坐标时,范围应该把雪花的偏移量也算进去,否则屏幕右下角会出现空白 let _left = (this.screenWidth Math.tan(this.snowAngle * Math.PI / 180) * this.screenHeight) * Math.random() snowItem.style.left = _left 'px'

当代码动起来(冬天已经过去春天马上来临)(10)

这样就没啥问题了,不过这样子会有一定的性能问题,就是我们会额外生成很多的雪花,就像图中那样,橙色区域的雪花虽然我们看不到,但是他们都在运动,并且消耗着性能,同时存在的雪花数量越多,性能损耗就越严重

当代码动起来(冬天已经过去春天马上来临)(11)

右边的雪花我不知道怎么优化,但是左边的,我们可以加一个判断,当雪花超出左侧屏幕时,将其移除即可。

// 如果雪花偏移角度大于0,则判断雪花是否超出左侧屏幕 if (this.snowAngle > 0) { if (snowItem.offsetLeft < (-snowItem.offsetWidth)) { this.$refs.snowWrap.removeChild(snowItem) return } }

好了,本篇文章到这里就结束了,相应的完整代码都会在我下一篇文章诞生的时候贴出来哦(希望能够顺产吧),本人文化水平较低,技术有限,大家如果有什么建议或者指正,欢迎评论区留言

,