隨著HTML5的火熱,越來越多的人投入到HTML5開發中了,canvas作為HTML5中比較重要的一個元素,在很多官網的主頁面中被使用到。今天我們一起來看看如何使用canvas畫出一個夢幻的星空背景,還會有流星運動。
本文的代碼已經放到Github上了,感興趣的可以自取,Github地址如下。
https://github.com/zhouxiongking/article-pages/blob/master/articles/starry/starry.html
HTML5
首先我們來看看通過canvas實現的星空效果圖,如下所示。
效果圖
接下來我們看看這個效果是如何通過代碼一步步實現的。
首先來看看頁面上的HTML代碼,只有一個Div元素。
HTML代碼
首先我們需要定義一些常量,比如畫布的寬和高,星星數量,流星個數。在這個星空中流星其實是星星的一個,只是添加了動態效果。
頁面初始化
然后是設定一個定時器,通過一段隨機時間生成一個流星的索引號。
流星索引號
緊接著來看看生成一個星星的方法,該方法返回一個星星的各項參數,包括x,y軸坐標,透明度,x,y軸偏移量。
生成星星的參數
然后是最重要的render方法,通過該方法可以將星星渲染至畫布上,我們將這個方法拆開看,首先是對流星的繪制,流星索引號通過上面metor方法獲得。
畫流星
然后是對于星星各項參數的處理,比如有的星星生成的點坐標超出了屏幕寬高,有的透明度是負數,都要將其處理成正常參數。
各項參數判斷
最后是在畫布中進行繪制。
畫布繪制
至此,這個畫面效果的講解完畢,如果代碼正確的話,就可以看到文中出現的效果圖。
今天這篇文章主要是借助HTML5中的canvas畫出了一個夢幻星空的效果,你學會了嗎?
天躺在床上刷抖音的時候,看見了一個馬克筆隨便畫星空的視頻,很有意思。
先看效果:
開始需求分析:
1、漸變色的背景
2、畫一顆樹和一些草
3、水面的倒影
4、隨便畫點星星
5、畫一顆流星
1、漸變色的背景
先確定200*500的區域,使用css3的線性漸變屬性,依次深藍、淺藍、紫色、粉色、黃色畫出一個漸變色的背景。
為了使背景更真實,使用同樣的顏色順序,在不同的角度,加上一些模糊和透明。再畫一遍重疊起來。
再重疊一層黑色,使畫布更暗一些。
.bg-color {
background-image: linear-gradient(170deg, #000093 13%, #9f35ff, #ff8000 70%, #f9f900 );
}
.bg-color2 {
background-image: linear-gradient( 180deg, #000093 13%, #9f35ff, #ff8000 80%, #f9f900 );
opacity: 0.3;
filter: blur(6px);
}
.bg-color3 {
background: rgba(0,0,0,.2);
}
2、畫一棵樹和草
使用html來畫一棵樹的話,需要很多個節點,性能和效果都很差。這里使用canvas來畫樹。
畫樹的教程,公眾號出過好幾次了,這里就不在重寫了。
基本原理就是,從一個點向一個方向畫一條直線。從終點開始,重新這個流程。期間可以修改一個角度畫出一分支。
草就更加簡單。隨便在底部畫一些雜亂的直線。
// 畫一棵樹
function drawTree(x, y, deg, step, type) {
var deg1=step % 2==0 ? 0.1 : -0.1;
var x1=x + Math.cos(deg + deg1) * (step + 5) * 0.9;
var y1=y + Math.sin(deg + deg1) * (step - 1) * 0.9;
ctx.beginPath();
ctx.lineWidth=step / 3;
ctx.moveTo(x, y);
ctx.lineTo(x1, y1);
ctx.stroke();
if (step > 12) {
ctx.arc(x, y, step / 6, 0, Math.PI * 2);
ctx.fill();
}
if (step < 3 && Math.random() > 0.7) {
var r=2 + Math.random() * 2;
ctx.arc(x1 + Math.random() * 3, y1 + Math.random() * 3, r, r, Math.PI + r);
ctx.fill();
}
step--;
if (step > 0) {
drawTree(x1, y1, deg, step, type);
if (step % 2==1 && step < 17)
drawTree(x1, y1, deg + 0.2, Math.round(step / 1.13));
if (step % 2==0 && step < 17)
drawTree(x1, y1, deg - 0.2, Math.round(step / 1.13) );
}
}
3、水面的倒影
最簡單的做法,就是使用canvas.toDataUrl 拿到canvas的圖片數據。在底部放一個反轉的圖片就可以。
我這里希望水面的倒影可以動起來。
新建一個canvas,使用ctx.getImageData拿到我們畫好的樹的像素點數據。
使用正弦給像素的x軸做一些偏移,得到一個新的數據。put到倒影的canvas上。
在使用requestAnimationFrame,做出一個流暢的左右擺動的倒影動畫。
最后,在原數據基礎上,增加一些雜色,使得倒影有一些黑白的橫線,模擬水波的高亮。
var startWave=0 // 水波起始位置
// 倒影增加水波紋效果
function wave(star){
var newImgData=ctxShadow.createImageData(200,150)
var pos=0
var source=0
startWave +=0.2
start=startWave
for(var y=0 ; y < CANVAS_HEIGHT ; y ++) {
start +=0.5
for(var x=0 ; x < CANVAS_WIDTH ; x ++) {
pos=(y * CANVAS_WIDTH + x) * 4
source=(y * CANVAS_WIDTH + x + Math.round(Math.sin(start)* 1.5)) * 4
newImgData.data[pos + 0]=imgData.data[source + 0];
newImgData.data[pos + 1]=imgData.data[source + 1];
newImgData.data[pos + 2]=imgData.data[source + 2];
newImgData.data[pos + 3]=imgData.data[source + 3];
}
}
ctxShadow.putImageData(newImgData,0,0)
requestAnimationFrame(wave)
}
4、畫星空
這個簡單,就不再寫代碼了,就隨意寫一些白色的div,隨機插入背景上。
其實到這一步,已經基本上完成了。
5、加一些流星
要畫流星,需要畫出一個漸漸變淡變窄的白線。
這里偷了個懶,在視覺效果上,一個漸漸變淡的白線,人眼看到,就感覺漸漸變窄。
這里使用白色加透明漸變,畫出一個流星的輪廓。加入從右到左動畫效果。
再加入一個外包的div,做一下旋轉和縮放。
效果完成!!!!
具體效果,建議查看原文。
代碼倉庫地址:
https://github.com/shb190802/html5
演示地址:
http://suohb.com/demo/win/starrySky.html
不是還蠻酷的呢?利用周末時間我們來學習并實現一下,本文我們就來一點一點分析怎么實現它!
分析
首先我們看看這個效果具體有哪些要點。首先,這么炫酷的效果肯定是要用到 Canvas 了,每個星星可以看作為一個粒子,因此,整個效果實際上就是粒子系統了。此外,我們可以發現每個粒子之間是相互連接的,只不過離的近的粒子之間的連線較粗且透明度較低,而離的遠的則相反。
開始 Coding
HTML 部分
這部分我就簡單放了一個 標簽,設置樣式使其填充全屏。
<canvas height="620" width="1360" id="canvas" style="position: absolute; height: 100%;"/>
然后為了讓所有元素沒有間距和內部,我還加了一條全局樣式:
* {
margin: 0;
padding: 0;
}
JavaScript 部分
下面我們來寫核心的代碼。首先我們要得到那個 canvas 并得到繪制上下文:
var canvasEl=document.getElementById('canvas');
var ctx=canvasEl.getContext('2d');
var mousePos=[0, 0];
緊接著我們聲明兩個變量,分別用于存儲“星星”和邊:
var nodes=[];
var edges=[];
下一步,我們做些準備工作,就是讓畫布在窗口大小發生變化時重新繪制,并且調整自身分辨率:
window.onresize=function () {
canvasEl.width=document.body.clientWidth;
canvasEl.height=canvasEl.clientHeight;
if (nodes.length==0) {
constructNodes();
}
render();
};
window.onresize(); // trigger the event manually.
我們在第一次修改大小后構建了所有節點,這里就要用到下一個函數(constructNodes)了
這個函數中我們隨機創建幾個點,我們用字典對象的方式存儲這些點的各個信息:
function constructNodes() {
for (var i=0; i < 100; i++) {
var node={
drivenByMouse: i==0,
x: Math.random() * canvasEl.width,
y: Math.random() * canvasEl.height,
vx: Math.random() * 1 - 0.5,
vy: Math.random() * 1 - 0.5,
radius: Math.random() > 0.9 ? 3 + Math.random() * 3 : 1 + Math.random() * 3
};
nodes.push(node);
}
nodes.forEach(function (e) {
nodes.forEach(function (e2) {
if (e==e2) {
return;
}
var edge={
from: e,
to: e2
}
addEdge(edge);
});
});
}
為了實現后面一個更炫酷的效果,我給第一個點加了一個 drivenByMouse 屬性,這個點的位置不會被粒子系統管理,也不會繪制出來,但是它會與其他點連線,這樣就實現了鼠標跟隨的效果了。
這里稍微解釋一下 radius 屬性的取值,我希望讓絕大部分點都是小半徑的,而極少數的點半徑比較大,所以我這里用了一點小 tricky,就是用概率控制點的半徑取值,不斷調整這個概率閾值就能獲取期待的半徑隨機分布。
點都構建完畢了,就要構建點與點之間的連線了,我們用到雙重遍歷,把兩個點捆綁成一組,放到 edges 數組中。注意這里我用了另外一個函數來完成這件事,而沒有直接用 edges.push() ,為什么?
假設我們之前連接了 A、B兩點,也就是外側循環是A,內側循環是B,那么在下一次循環中,外側為B,內側為A,是不是也會創建一條邊呢?而實際上,這兩個邊除了方向不一樣以外是完全一樣的,這完全沒有必要而且占用資源。因此我們在 addEdge 函數中進行一個判斷:
function addEdge(edge) {
var ignore=false;
edges.forEach(function (e) {
if (e.from==edge.from & e.to==edge.to) {
ignore=true;
}
if (e.to==edge.from & e.from==edge.to) {
ignore=true;
}
});
if (!ignore) {
edges.push(edge);
}
}
至此,我們的準備工作就完畢了,下面我們要讓點動起來:
function step() {
nodes.forEach(function (e) {
if (e.drivenByMouse) {
return;
}
e.x +=e.vx;
e.y +=e.vy;
function clamp(min, max, value) {
if (value > max) {
return max;
} else if (value < min) {
return min;
} else {
return value;
}
}
if (e.x <=0 || e.x >=canvasEl.width) {
e.vx *=-1;
e.x=clamp(0, canvasEl.width, e.x)
}
if (e.y <=0 || e.y >=canvasEl.height) {
e.vy *=-1;
e.y=clamp(0, canvasEl.height, e.y)
}
});
adjustNodeDrivenByMouse();
render();
window.requestAnimationFrame(step);
}
function adjustNodeDrivenByMouse() {
nodes[0].x +=(mousePos[0] - nodes[0].x) / easingFactor;
nodes[0].y +=(mousePos[1] - nodes[0].y) / easingFactor;
}
看到這么一大段代碼不要害怕,其實做的事情很簡單。這是粒子系統的核心,就是遍歷粒子,并且更新其狀態。更新的公式就是
v=v + a
s=s + v
a是加速度,v是速度,s是位移。由于我們這里不涉及加速度,所以就不寫了。然后我們需要作一個邊緣的碰撞檢測,不然我們的“星星”都無拘無束地一點點飛~走~了~。邊緣碰撞后的處理方式就是讓速度矢量反轉,這樣粒子就會“掉頭”回來。
還記得我們需要做的鼠標跟隨嗎?也在這處理,我們讓第一個點的位置一點一點移動到鼠標的位置,下面這個公式很有意思,可以輕松實現緩動:
x=x + (t - x) / factor
其中 factor 是緩動因子,t 是最終位置,x 是當前位置。至于這個公式的解釋還有個交互大神 Bret Victor 在他的演講中提到過,視頻做的非常好,有條(ti)件(zi)大家一定要看看: Bret Victor – Stop Drawing Dead Fish
好了,回到主題。我們在上面的函數中處理完了一幀中的數據,我們要讓整個粒子系統連續地運轉起來就需要一個timer了,但是十分不提倡大家使用 setInterval,而是盡可能使用 requestAnimationFrame,它能保證你的幀率鎖定在
剩下的就是繪制啦:
function render() {
ctx.fillStyle=backgroundColor;
ctx.fillRect(0, 0, canvasEl.width, canvasEl.height);
edges.forEach(function (e) {
var l=lengthOfEdge(e);
var threshold=canvasEl.width / 8;
if (l > threshold) {
return;
}
ctx.strokeStyle=edgeColor;
ctx.lineWidth=(1.0 - l / threshold) * 2.5;
ctx.globalAlpha=1.0 - l / threshold;
ctx.beginPath();
ctx.moveTo(e.from.x, e.from.y);
ctx.lineTo(e.to.x, e.to.y);
ctx.stroke();
});
ctx.globalAlpha=1.0;
nodes.forEach(function (e) {
if (e.drivenByMouse) {
return;
}
ctx.fillStyle=nodeColor;
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, 2 * Math.PI);
ctx.fill();
});
}
常規的 Canvas 繪圖操作,注意 beginPath 一定要調用,不然你的線就全部穿在一起了… 需要說明的是,在繪制邊的時候,我們先要計算兩點距離,然后根據一個閾值來判斷是否要繪制這條邊,這樣我們才能實現距離遠的點之間連線不可見的效果。
到這里,我們的整個效果就完成了。如果不明白大家也可以去GitHub項目: CyandevToys / ParticleWeb去看完整的源碼。Have fun!!
源自:http://www.jianshu.com/p/f5c0f9c4bc39
聲明:文章著作權歸作者所有,如有侵權,請聯系小編刪除。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。