前言
女友常逛的設計網站這兩天頁面上多了下雪的效果,因而問我個人網站能下雪嗎,做爲一個程序員我通常會說實現不了,可是做爲男友,不能說不行。css
雪
雪咱們可使用span
標籤和css的徑向漸變簡單意思一下:html
.snow { display: block; width: 100px; height: 100px; background-image: radial-gradient(#fff 0%, rgba(255, 255, 255, 0) 60%); border-radius: 50%; }
效果以下:css3
不少雪
一片雪是不夠的,成千上萬才浪漫,世界上沒有兩片相同的雪花,因此每片雪都有本身的大小位置速度等屬性,爲此先建立一個雪花類:git
class Snow { constructor (opt = {}) { // 元素 this.el = null // 直徑 this.width = 0 // 最大直徑 this.maxWidth = opt.maxWidth || 80 // 最小直徑 this.minWidth = opt.minWidth || 2 // 透明度 this.opacity = 0 // 水平位置 this.x = 0 // 重置位置 this.y = 0 // 速度 this.speed = 0 // 最大速度 this.maxSpeed = opt.maxSpeed || 4 // 最小速度 this.minSpeed = opt.minSpeed || 1 // 瀏覽器窗口尺寸 this.windowWidth = window.innerWidth this.windowHeight = window.innerHeight this.init() } // 初始化各類屬性 init () { this.width = Math.floor(Math.random() * this.maxWidth + this.minWidth) this.opacity = Math.random() this.x = Math.floor(Math.random() * (this.windowWidth - this.width)) this.y = Math.floor(Math.random() * (this.windowHeight - this.width)) this.speed = Math.random() * this.maxSpeed + this.minSpeed } // 設置樣式 setStyle () { this.el.style.cssText = ` position: fixed; left: 0; top: 0; display: block; width: ${this.width}px; height: ${this.width}px; opacity: ${this.opacity}; background-image: radial-gradient(#fff 0%, rgba(255, 255, 255, 0) 60%); border-radius: 50%; z-index: 9999999999999; pointer-events: none; transform: translate(${this.x}px, ${this.y}px); ` } // 渲染 render () { this.el = document.createElement('div') this.setStyle() document.body.appendChild(this.el) } }
init
方法用來生成隨機的初始大小、位置、速度等屬性,在瀏覽器窗口內new
100片試試:程序員
let snowList = [] for (let i = 0; i < 100; i++) { let snow = new Snow() snow.render() snowList.push(snow) }
效果以下:github
動起來
雪動起來才能叫下雪,動起來很簡單,不斷改變x
和y
座標就能夠了,給snow
類加個運動的方法:web
class snow { move () { this.x += this.speed this.y += this.speed this.el.style.left = this.x + 'px' this.el.style.top = this.y + 'px' } }
接下來使用requestAnimationFrame
不斷刷新:canvas
moveSnow () { window.requestAnimationFrame(() => { snowList.forEach((item) => { item.move() }) moveSnow() }) }
效果以下,由於速度是正數,因此總體是往右斜的:api
能夠看到動起來了,可是出屏幕就不見了,因此雪是會消失的對嗎?要讓雪不停很簡單,檢測雪的位置,若是超出屏幕了就讓它回到頂部,修改一下move
方法:瀏覽器
move () { this.x += this.speed this.y += this.speed // 徹底離開窗口就調一下初始化方法,另外還須要修改一下init方法,由於從新出現咱們是但願它的y座標爲0或者小於0,這樣就不會又憑空出現的感受,而是從天上下來的 if (this.x < -this.width || this.x > this.windowWidth || this.y > this.windowHeight) { this.init(true) this.setStyle() } this.el.style.left = this.x + 'px' this.el.style.top = this.y + 'px' }
init (reset) { // ... this.width = Math.floor(Math.random() * this.maxWidth + this.minWidth) this.y = reset ? -this.width : Math.floor(Math.random() * this.windowHeight) // ... }
這樣就能源源不斷的下雪了:
優化
1.水平速度
水平和垂直方向的速度是同樣的,可是看起來有點太斜了,因此調整一下,把水平速度和垂直速度區分開來:
class Snow { constructor (opt = {}) { // ... // 水平速度 this.sx = 0 // 垂直速度 this.sy = 0 // ... } init (reset) { // ... this.sy = Math.random() * this.maxSpeed + this.minSpeed this.sx = this.sy * Math.random() } move () { this.x += this.sx this.y += this.sy // ... } }
2.左下角沒有雪
由於總體向右傾斜,因此左下角大機率沒有雪,這能夠經過讓雪隨機出如今左側來解決:
init (reset) { // ... this.x = Math.floor(Math.random() * (this.windowWidth - this.width)) this.y = Math.floor(Math.random() * (this.windowHeight - this.width)) if (reset && Math.random() > 0.8) {// 讓一小部分的雪初始化在左側 this.x = -this.width } else if (reset) { this.y = -this.width } // ... }
3.眼前的雪
隨機性的選擇一點雪給它較大的體積、透明度和速度,而後再使用css3
的3D
透視效果,把它的z
軸數值調大一點,這樣的感受就好像是在眼前劃過的同樣:
<body style="perspective: 500;-webkit-perspective: 500"></body>
class Snow { constructor (opt = {}) { // ... // z軸數值 this.z = 0 // 快速劃過的最大速度 this.quickMaxSpeed = opt.quickMaxSpeed || 10 // 快速劃過的最小速度 this.quickMinSpeed = opt.quickMinSpeed || 8 // 快速劃過的寬度 this.quickWidth = opt.quickWidth || 80 // 快速劃過的透明度 this.quickOpacity = opt.quickOpacity || 0.2 // ... } init (reset) { let isQuick = Math.random() > 0.8 this.width = isQuick ? this.quickWidth : Math.floor(Math.random() * this.maxWidth + this.minWidth) this.z = isQuick ? Math.random() * 300 + 200 : 0 this.opacity = isQuick ? this.quickOpacity : Math.random() // ... this.sy = isQuick ? Math.random() * this.quickMaxSpeed + this.quickMinSpeed : Math.random() * this.maxSpeed + this.minSpeed // ... } move () { // ... this.el.style.transform = `translate3d(${this.x}px, ${this.y}px, ${this.z}px)` } }
4.鵝毛大雪
雪花嘛,輕如鵝毛,鵝毛是怎麼飄的?是否是左右擺動的飄?那咱們也能夠選擇一部分的雪花讓它跟鵝毛同樣飄,左右搖擺很簡單,速度一會加一會減就能夠了:
class Snow { constructor (opt = {}) { // ... // 是否左右搖擺 this.isSwing = false // 左右搖擺的步長 this.stepSx = 0.03 // ... } // 隨機初始化屬性 init (reset) { // ... this.isSwing = Math.random() > 0.8 // ... } move () { if (this.isSwing) { if (this.sx >= 1 || this.sx <= -1) { this.stepSx = -this.stepSx } this.sx += this.stepSx } // ... } }
除了上述這種方法,左右搖擺還有一種方式,就是使用正弦或餘弦函數,由於它們的曲線翻轉90度就是左右搖擺:
咱們使用正弦函數,公式爲:y=sin(x)
,x
的值是弧度表示,只要一直增長就能夠了,y
的值用來修改雪花的水平方向的速度變化步長:
class Snow { constructor (opt = {}) { // ... // 是否左右搖擺 this.isSwing = false // 左右搖擺的正弦函數x變量 this.swingRadian = 0 // 左右搖擺的正弦x步長 this.swingStep = 0.01 // ... } init (reset) { // ... this.swingStep = 0.01 * Math.random() } move () { if (this.isSwing) { this.swingRadian += this.swingStep this.x += this.sx * Math.sin(this.swingRadian * Math.PI) * 0.2 } else { this.x += this.sx } // ... } }
由於正弦函數y
的值是從1變化到-1,擺動幅度太了,因此乘了個小數0.2
縮小一點,想要幅度小一點,還有一個方法是不要使用整個正弦曲線,能夠從中截取一個適合的區間大小,好比就讓x
的值在0.9π
到1.1π
以前變化:
class Snow { constructor (opt = {}) { // ... // 是否左右搖擺 this.isSwing = false // 左右搖擺的正弦函數x變量 this.swingRadian = 1// 須要改爲一箇中間值 // 左右搖擺的正弦x步長 this.swingStep = 0.01 // ... } init (reset) { // ... this.swingStep = 0.01 * Math.random() this.swingRadian = Math.random() * (1.1 - 0.9) + 0.9// 也讓它隨機一下 } move () { if (this.isSwing) { if (this.swingRadian > 1.1 || this.swingRadian < 0.9) { this.swingStep = -this.swingStep } this.swingRadian += this.swingStep this.x += this.sx * Math.sin(this.swingRadian * Math.PI) } else { this.x += this.sx } // ... } }
5.下的慢一點
既然給水平加了曲線,垂直方向上是否是也能夠改爲非勻速呢?固然能夠,區別是速度得一直是正的,否則就要出現反天然現象了,改變速度曲線一樣可使用正餘弦,上面咱們使用了0.9π
到1.1π
之間的正弦曲線,根據上圖能夠發現對應的餘弦曲線都是負的,趨勢是先慢後快,因此能夠利用這一段來改變垂直方向的速度:
move () { if (this.isSwing) { if (this.swingRadian > 1.1 || this.swingRadian < 0.9) { this.swingStep = -this.swingStep } this.swingRadian += this.swingStep this.x += this.sx * Math.sin(this.swingRadian * Math.PI) this.y -= this.sy * Math.cos(this.swingRadian * Math.PI)// 由於速度都是負的,因此改爲- } else { this.x += this.sx this.y += this.sy } // ... }
6.在最上面
爲了防止爲頁面上本來層級更高的元素遮擋,給雪花的樣式加一個很大的層級:
render () { this.el = document.createElement('div') this.el.style.cssText = ` // ... z-index: 9999999999999; ` document.body.appendChild(this.el) }
7.看不見我
修改了層級,因此雪花會在頁面的最上層,那麼可能會擋住其餘元素的鼠標事件,須要禁止它響應鼠標事件:
render () { this.el = document.createElement('div') this.el.style.cssText = ` // ... pointer-events: none; ` document.body.appendChild(this.el) }
8.更好一點
使用性能更好的transform
屬性來作動畫:
render () { this.el = document.createElement('div') this.el.style.cssText = ` left: 0; top: 0; transform: translate(${this.x}px, ${this.y}px); ` document.body.appendChild(this.el) }
move () { // ... // this.el.style.left = this.x + 'px' // this.el.style.top = this.y + 'px' this.el.style.transform = `translate(${this.x}px, ${this.y}px)` }
固然,最好的方式是用canvas
來畫。
最終效果:
下雨&雨夾雪
下完雪,接下來順便下個雨,雨和雪差很少,都是從天上掉下來,可是雨的速度更快,一般也不會左右搖擺什麼的,方向也基本是一致的,先來修改一下樣式:
setStyle () { this.el.style.cssText = ` // ... width: 1px; // ... ` }
很簡單,只要把寬度寫死爲1就好了:
接下來把搖擺去掉:
move () { this.x += this.sx this.y += this.sy // ... }
效果以下:
能夠發現雨是豎着在水平移動,顯然是不行的,須要讓它傾斜必定的角度,和運動方向保持一致,這個也很簡單,算一下斜率,水平速度除以垂直速度:
move () { // ... this.el.style.transform = `translate(${this.x}px, ${this.y}px) ${this.getRotate(this.sy, this.sx)}` } getRotate(sy, sx) { return `rotate(${sx === 0 ? 0 : (90 + Math.atan(sy / sx) * (180 / Math.PI))}deg)` }
由於tan(θ)=sy/sx
,θ=Math.atan(sy / sx)
,由於雨的線段默認是從上到下垂直的,θ
是表明和水平方向上的夾角,因此須要先旋轉90度,再旋轉夾角的度數,最後弧度轉角度的公式爲:角度=弧度*(180/π)。
雨和雪都實現了,讓它們一塊兒出來,就是雨夾雪了:
根據天氣下雪
把上面的代碼放到網站上就有下雪的效果了,另外也可使用天氣廠商的api,根據實時天氣來下雪或者下雨,再實現一下太陽、烏雲等效果,一個沉浸式天氣就完成了,有興趣的可自行實踐。