本文首發於個人我的博客:cherryblog.site/
github項目地址:github.com/sunshine940…
項目演示地址:sunshine940326.github.io/canvasStar/css
以前看到了一個很好看的canvas效果,而後拿來作個人博客背景,很多童鞋留言說求教程,而且反應說太耗內存,因而前一段我就重寫了一遍,而且使用離屏渲染進行優化,效果仍是挺顯著的。可是由於畢竟是canvas,須要一直進行重繪,因此仍是比較耗內存的,可是比優化以前已經好不少了。而且最近準備本身寫插件,因而就拿這個練手了,html
github地址:github.com/sunshine940… git
代碼還有不少的不足,求大神 review (づ。◕‿‿◕。)づ~github
canvas
是 HTML5 新定義的標籤,經過使用腳本(一般是 JavaScript)繪製圖形。<canvas>
標籤只是圖形容器,至關於一個畫布,canvas
元素自己是沒有繪圖能力的。全部的繪製工做必須在 JavaScript 內部完成,至關於使用畫筆在畫布上畫畫。canvas
默認狀況下,<canvas>
沒有邊框和內容。默認是一個 300*150 的畫布,因此咱們建立了 <canvas>
以後要對其設置寬高。api
咱們能夠經過html屬性‘width’,‘height’來設置canvas的寬高,不能夠經過 css 屬性來設置寬高。由於經過 css 屬性設置的寬高會使 canvas 內的圖像按照 300*150 時的比例放大或縮小緩存
context
是一個封裝了不少繪圖功能的對象,咱們在頁面中建立一個 canvas
標籤以後,首先要使用 getContext()
獲取 canvas 的上下文環境,目前 getContext()
的參數只有 2d
,暫時還不支持 3d
bash
getContext("2d")
對象是內建的 HTML5 對象,擁有多種繪製路徑、矩形、圓形、字符以及添加圖像的方法。網絡
canvas 建立圖形有兩種方式dom
fill()
方法填充當前的圖像(路徑)。默認顏色是黑色。在填充前要先使用 fillStyle
設置填充的顏色或者漸變,而且若是路徑未關閉,那麼 fill()
方法會從路徑結束點到開始點之間添加一條線,以關閉該路徑(正如 closePath()
同樣),而後填充該路徑。
stroke()
方法會實際地繪製出經過 moveTo()
和 lineTo()
方法定義的路徑。默認顏色是黑色。在進行圖形繪製前,要設置好繪圖的樣式
fillStyle()//填充的樣式
strokeStyle()//邊框樣式
context.lineWidth()//圖形邊框寬度複製代碼
用 canvas 繪製一個矩形很簡單
fillRect(x,y,width,height) // 實心矩形
strokeRect(x,y,width,height) // 空心矩形複製代碼
//html代碼
<canvas id="canvas"></canvas>
//script代碼
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 100, 100);
context.strokeRect(120, 0, 100, 100);複製代碼
顯示以下:
咱們能夠看出,在沒有設置顏色的狀況下,默認是黑色的。
咱們還能夠經過設置 fillStyle
或者 fillStyle
改變其填充顏色。
context.fillStyle = "pink";
context.strokeStyle = "darkred";
context.fillRect(0, 0, 100, 100);
context.strokeRect(120, 0, 100, 100);複製代碼
效果以下
clearRect(x,y,width,height)複製代碼
var canvas = document.getElementById('canvas');
var context = canvas.getContext("2d");
context.fillRect(0, 0, 100, 100);
context.strokeRect(120, 0, 100, 100);
context.fillStyle = "pink";
context.strokeStyle = "darkred";
context.fillRect(0, 120, 100, 100);
context.strokeRect(120, 120, 100, 100);
context.clearRect( 50,50,120,120)複製代碼
效果以下:context.arc(x, y, radius, starAngle,endAngle, anticlockwise)
context.beginPath();
context.arc(300, 350, 100, 0, Math.PI * 2, true);
//不關閉路徑路徑會一直保留下去
context.closePath();
context.fillStyle = 'rgba(0,255,0,0.25)';
context.fill();複製代碼
效果以下:若是不填充顏色,實心圓就是圓弧
context.beginPath();
context.arc(600, 350, 100, 0, Math.PI , true);
context.strokeStyle = 'pink';
context.closePath();
context.stroke();
context.beginPath();
context.arc(300, 350, 100, 0, Math.PI , true);
context.strokeStyle = 'red';
//沒有closePath
context.stroke();複製代碼
效果如圖:
因此說,若是第一個圓弧沒有 closePath()
而且第二個圓弧沒有 beginPath()
的話就是這樣的效果:
moveTo(x,y)
:把路徑移動到畫布中的指定點,不建立線條lineTo(x,y)
:添加一個新點,而後在畫布中建立從該點到最後指定點的線條context.strokeStyle = 'pink';
context.moveTo(0, 0);
context.lineTo(100, 100);
context.stroke();*/複製代碼
效果以下:context.strokeStyle = 'pink';
context.lineTo(100, 100);
context.lineTo(200, 200);
context.stroke();*/複製代碼
效果以下:
// 繪製片斷
context.strokeStyle = 'pink';
context.lineTo(200, 200);
context.lineTo(200, 100);
context.lineTo(100,50);
context.stroke();複製代碼
效果以下:
var n = 0;
var dx = 150;
var dy = 150;
var s = 100;
context.beginPath();
context.fillStyle = 'pink';
context.strokeStyle = 'rgb(0,0,100)';
var x = Math.sin(0);
var y = Math.cos(0);
var dig = Math.PI / 15 * 5;
for (var i = 0; i < 6; i++) {
var x = Math.sin(i * dig);
var y = Math.cos(i * dig);
context.lineTo(dx + x * s, dy + y * s);
console.log( x ,y )
}
context.closePath();
context.fill();
context.stroke();複製代碼
var n = 0;
var dx = 150;
var dy = 150;
var s = 100;
context.beginPath();
context.fillStyle = 'pink';
context.strokeStyle = 'rgb(0,0,100)';
var x = Math.sin(0);
var y = Math.cos(0);
var dig = Math.PI / 15 * 7;
for (var i = 0; i < 30; i++) {
var x = Math.sin(i * dig);
var y = Math.cos(i * dig);
context.lineTo(dx + x * s, dy + y * s);
console.log( x ,y )
}
context.closePath();
context.fill();
context.stroke();複製代碼
效果以下:
var lg= context.createLinearGradient(xStart,yStart,xEnd,yEnd)
lg.addColorStop(offset,color)
例如:
var g1 = context.createLinearGradient(0, 0, 0, 300);
g1.addColorStop(0, '#E55D87');
g1.addColorStop(1, '#5FC3E4');
context.fillStyle = g1;
context.fillRect(0, 0, 400, 300);複製代碼
效果以下:
var rg=context.createRadialGradient(xStart,yStart,radiusStart,xEnd,yEnd,radiusEnd)
rg.addColorStop(offset,color)
// 同心圓徑向漸變
var g1 = context.createRadialGradient(200, 150, 0, 200, 150, 200);
g1.addColorStop(0.1, '#F09819');
g1.addColorStop(1, '#EDDE5D');
context.fillStyle = g1;
context.beginPath();
context.arc(200, 150, 100, 0, Math.PI * 2, true);
context.closePath();
context.fill();複製代碼
//不一樣圓心的徑向漸變模型
var g1 = context.createRadialGradient(100, 150, 10, 300, 150, 80);
g1.addColorStop(0.1, '#F09819');
g1.addColorStop(0.8, 'red');
g1.addColorStop(1, '#EDDE5D');
context.fillStyle = g1;
context.fillRect(0, 0, 300, 500);複製代碼
效果圖:
scale(x,y)
rotate(angle)
translate(x,y)
平移,縮放,旋轉前後順序不一樣,座標軸的變化圖,圖片來源於網絡:
globalCompositeOperation=type
設置或返回新圖像如何繪製到已有的圖像上。最後的效果取決於 type 的值
type:
shadowOffsetX:設置或返回陰影距形狀的水平距離(默認值爲 0)
shadowOffsetY:設置或返回陰影距形狀的垂直距離(默認值爲 0)
shadowColor:設置或返回用於陰影的顏色
shadowBlur:設置或返回用於陰影的模糊級別(值越大越模糊)複製代碼
例如:
context.fillStyle = 'white';
context.beginPath();
context.arc(100,100,10,0,2 * Math.PI);
context.shadowColor = 'white';
context.shadowBlur = 10;
context.fill();
context.closePath();複製代碼
咱們看到的效果就是咱們在開頭提起的例子中的 star 粒子的效果,由於其有白色陰影的效果,因此看起來像是發光同樣,效果以下圖:
drawImage()
向畫布上繪製圖像、畫布或視頻
context.drawImage(img,x,y);
context.drawImage(img,x,y,width,height);
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
img
:規定要使用的圖像、畫布或視頻。sx
:可選。開始剪切的 x 座標位置。sy
:可選。開始剪切的 y 座標位置。swidth
:可選。被剪切圖像的寬度。sheight
:可選。被剪切圖像的高度。x
:在畫布上放置圖像的 x 座標位置。y
:在畫布上放置圖像的 y 座標位置。width
:可選。要使用的圖像的寬度。(伸展或縮小圖像)height
:可選。要使用的圖像的高度。(伸展或縮小圖像)createPattern(image,type)
type:
clip()
從原始畫布剪切任意形狀和尺寸的區域,須要先建立裁剪區域,再繪製圖像;一旦剪切了某個區域,則全部以後的繪圖都會被限制在被剪切的區域內(不能訪問畫布上的其餘區域)。您也能夠在使用 clip() 方法前經過使用 save() 方法對當前畫布區域進行保存,並在之後的任意時間對其進行恢復(經過 restore() 方法)。
例如:
// 設置剪切區域(粉色矩形)
context.rect(0,0,500,400);
context.fillStyle = "pink";
context.fill();
context.clip();
// 在剪切區域中繪製圖形(白色矩形)
context.fillStyle = "white";
context.fillRect(10,10,100,100);
// 以後繪製的圖形只能顯示在剪切區域以內(紅色矩形)
context.fillStyle = "red";
context.fillRect(100,100,600,600)複製代碼
效果以下:能夠看到咱們設置的紅色矩形是一個 600600 的矩形,可是顯然是沒有顯示完的,*一旦剪切了某個區域,則全部以後的繪圖都會被限制在被剪切的區域內(不能訪問畫布上的其餘區域)。
context.save();
// 設置剪切區域
context.rect(0,0,500,400);
context.fillStyle = "pink";
context.fill();
context.clip();
// 在剪切區域中繪製圖形
context.fillStyle = "white";
context.fillRect(10,10,100,100);
context.restore();
// 以後繪製的圖形只能顯示在剪切區域以內
context.fillStyle = "red";
context.fillRect(100,100,600,600)複製代碼
這樣就能夠正常顯示了:
fillText(text,x,y):繪製實心文字
strokeText():繪製文字描邊(空心)
textAlign:設置或返回文本內容的當前對齊方式
textBaseline:設置或返回在繪製文本時使用的當前文本基線
font:設置或返回文本內容的當前字體屬性複製代碼
例如:
context.font="40px Arial";
context.fillText("Hello world",200,200);
context.strokeText("Hello world",200,300)複製代碼
效果以下:
好的開始是成功的一半
簡單介紹了下 canvas 的經常使用 api,你們發現是否是也沒有那麼難呢~( ̄▽ ̄)~*,那麼讓咱們回到標題,一塊兒來看一下這個少女心滿滿的例子是怎樣實現的~
canvas 其實寫一個炫酷的特效在技術上並不難,難的是你的創意,由於 canvas 實現粒子的效果仍是比較驚豔的,但其實代碼都是比較簡單的,無非就是隨機的建立圖形或者路徑,固然圖形也是閉合的路徑。在加上必定的位移就能夠了。可是你要設計出一個好的特效是很是不容易的。
因此咱們就先來分析一下這個效果由那幾部分構成,將其拆分開來。
特效pc端演示地址:sunshine940326.github.io/canvasStar/ (固然,能夠直接查看個人博客,背景暫時就是這個,不知道何時會變,捂臉ing:cherryblog.site/)
咱們能夠將其一直位移向上的粒子稱爲 star,咱們觀察 star 的特色:
因此咱們就能夠總結出 star 的特色就是總數固定,建立時座標和半徑還有透明度隨機,勻速上升。是否是很簡單了呢~[]~( ̄▽ ̄)~*
再讓咱們來看一下隨着鼠標移入產生的粒子,咱們稱爲 dot,同理,咱們觀察獲得 dot 的特色
這樣,咱們就完成了一半了呢~將事件屢清楚以後咱們就能夠開始着手擼代碼了!
其實須要的 HTML 代碼和 CSS 代碼很簡答的,HTML 只須要一行就能夠了呢,設置一個漸變的背景蒙層和一個 canvas
標籤。
<div class="filter"></div>
<canvas id="canvas"></canvas>複製代碼
CSS 以下:
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: black;
background: linear-gradient(to bottom, #dcdcdc 0%, palevioletred 100%);
}
#main-canvas {
width: 100%;
height: 100%;
}
.filter {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background: #fe5757;
animation: colorChange 30s ease-in-out infinite;
animation-fill-mode: both;
mix-blend-mode: overlay;
}
@keyframes colorChange {
0%, 100% {
opacity: 0;
}
50% {
opacity: .7;
}
}複製代碼
是的,我使用的是一個漸變的背景,不只是從上到下的漸變,而且顏色也是會漸變的,效果以下:
/*
* @var star_r:star半徑係數,係數越大,半徑越大
* @var star_alpha:生成star的透明度,star_alpha越大,透明度越低
* @var initStarsPopulation:初始化stars的個數
* @var move_distance:star位移的距離,數值越大,位移越大
* @var dot_r : dot半徑係數,係數越大,半徑越大
* @var dot_speeds : dots運動的速度
* @var dot_alpha : dots的透明度
* @var aReduction:dot消失條件,透明度小於aReduction時消失
* @var dotsMinDist:dot最小距離
* @var maxDistFromCursor:dot最大距離
* */
var config = {
star_r : 3,
star_alpha : 5,
initStarsPopulation : 150,
move_distance : 0.25,
dot_r : 5,
dot_speeds : 0.5,
dot_alpha : 0.5,
dot_aReduction : 0.01,
dotsMinDist : 5,
maxDistFromCursor : 50,
};
var stars = [],
dots = [],
canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
WIDTH,
HEIGHT,
mouseMoving = false,
mouseMoveChecker,
mouseX,
mouseY;複製代碼
/* 設置單個 star
* @param id:id
* @param x:x座標
* @param y:y座標
* @param useCache:是否使用緩存
* */
function Star(id, x, y) {
this.id = id;
this.x = x;
this.y = y;
this.cacheCanvas = document.createElement("canvas");
this.cacheCtx = this.cacheCanvas.getContext("2d");
this.r = Math.floor(Math.random() * star_r) + 1;
this.cacheCtx.width = 6 * this.r;
this.cacheCtx.height = 6 * this.r;
var alpha = ( Math.floor(Math.random() * 10) + 1) / star_alpha;
this.color = "rgba(255,255,255," + alpha + ")";
if (useCache) {
this.cache()
}
}複製代碼
這裏我使用的是原型的方式,將 draw
、cache
、move
和 die
方法都設置在 Star
的原型上,這樣在使用 new
建立對象的時候,每個 star 均可以繼承這些方法。
Star.prototype = {
draw : function () {
if (!this.useCacha) {
ctx.save();
ctx.fillStyle = this.color;
ctx.shadowBlur = this.r * 2;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
ctx.restore();
} else {
ctx.drawImage(this.cacheCanvas, this.x - this.r, this.y - this.r);
}
},
cache : function () {
this.cacheCtx.save();
this.cacheCtx.fillStyle = this.color;
this.cacheCtx.shadowColor = "white";
this.cacheCtx.shadowBlur = this.r * 2;
this.cacheCtx.beginPath();
this.cacheCtx.arc(this.r * 3, this.r * 3, this.r, 0, 2 * Math.PI);
this.cacheCtx.closePath();
this.cacheCtx.fill();
this.cacheCtx.restore();
},
move : function () {
this.y -= move_distance;
if (this.y <= -10) {
this.y += HEIGHT + 10;
}
this.draw();
},
die : function () {
stars[this.id] = null;
delete stars[this.id]
}
};複製代碼
function Dot(id, x, y, useCache) {
this.id = id;
this.x = x;
this.y = y;
this.r = Math.floor(Math.random() * dot_r)+1;
this.speed = dot_speeds;
this.a = dot_alpha;
this.aReduction = dot_aReduction;
this.useCache = useCache;
this.dotCanvas = document.createElement("canvas");
this.dotCtx = this.dotCanvas.getContext("2d");
this.dotCtx.width = 6 * this.r;
this.dotCtx.height = 6 * this.r;
this.dotCtx.a = 0.5;
this.color = "rgba(255,255,255," + this.a +")";
this.dotCtx.color = "rgba(255,255,255," + this.dotCtx.a + ")";
this.linkColor = "rgba(255,255,255," + this.a/4 + ")";
this.dir = Math.floor(Math.random()*140)+200;
if( useCache){
this.cache()
}
}複製代碼
Dot.prototype = {
draw : function () {
if( !this.useCache){
ctx.save();
ctx.fillStyle = this.color;
ctx.shadowColor = "white";
ctx.shadowBlur = this.r * 2;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
ctx.restore();
}else{
ctx.drawImage(this.dotCanvas, this.x - this.r * 3, this.y - this.r *3);
}
},
cache : function () {
this.dotCtx.save();
this.dotCtx.a -= this.aReduction;
this.dotCtx.color = "rgba(255,255,255," + this.dotCtx.a + ")";
this.dotCtx.fillStyle = this.dotCtx.color;
this.dotCtx.shadowColor = "white";
this.dotCtx.shadowBlur = this.r * 2;
this.dotCtx.beginPath();
this.dotCtx.arc(this.r * 3, this.r * 3, this.r, 0, 2 * Math.PI, false);
this.dotCtx.closePath();
this.dotCtx.fill();
this.dotCtx.restore();
},
link : function () {
if (this.id == 0) return;
var previousDot1 = getPreviousDot(this.id, 1);
var previousDot2 = getPreviousDot(this.id, 2);
var previousDot3 = getPreviousDot(this.id, 3);
var previousDot4 = getPreviousDot(this.id, 4);
if (!previousDot1) return;
ctx.strokeStyle = this.linkColor;
ctx.moveTo(previousDot1.x, previousDot1.y);
ctx.beginPath();
ctx.lineTo(this.x, this.y);
if (previousDot2 != false) ctx.lineTo(previousDot2.x, previousDot2.y);
if (previousDot3 != false) ctx.lineTo(previousDot3.x, previousDot3.y);
if (previousDot4 != false) ctx.lineTo(previousDot4.x, previousDot4.y);
ctx.stroke();
ctx.closePath();
},
move : function () {
this.a -= this.aReduction;
if(this.a <= 0 ){
this.die();
return
}
this.dotCtx.a -= this.aReduction;
this.dotCtx.color = "rgba(255,255,255," + this.dotCtx.a + ")";
this.color = "rgba(255,255,255," + this.a + ")";
this.linkColor = "rgba(255,255,255," + this.a/4 + ")";
this.x = this.x + Math.cos(degToRad(this.dir)) * this.speed;
this.y = this.y + Math.sin(degToRad(this.dir)) * this.speed;
this.draw();
this.link();
},
die : function () {
dots[this.id] = null;
delete dots[this.id];
}
};複製代碼
此外,咱們還須要設置一些其餘的函數和對鼠標移入事件的監聽,這裏就再也不贅述了,感興趣的同窗能夠直接到 github 下載源碼。
我所使用的離屏優化是基於此文,原文寫的很好,你們感興趣的話能夠去看一下:www.cnblogs.com/axes/p/3567…
由於這個效果以前我也在博客用當作背景過,很多同窗都反應很卡,因此我就找了下優化的教程作了下優化,我發現對性能影響最大的可能就是 canvas 的離屏渲染優化了,這也是 canvas 的最多見優化之一。
名字聽起來很複雜,什麼離屏渲染,其實就是設置緩存,繪製圖像的時候在屏幕以外的地方繪製好,而後再直接拿過來用,這不就是緩存的概念嗎?!︿( ̄︶ ̄)︿.
創建兩個 canvas 標籤,大小一致,一個正常顯示,一個隱藏(緩存用的,不插入dom中),先將結果draw緩存用的canvas上下文中,由於遊離canvas不會形成ui的渲染,因此它不會展示出來,再把緩存的內容整個裁剪再 draw 到正常顯示用的 canvas 上,這樣能優化很多。
其實已經體如今上述的代碼中的,好比,建立 star 的代碼中:
/* 設置單個star
* @param id:id
* @param x:x座標
* @param y:y座標
* @param useCache:是否使用緩存
* */
function Star(id, x, y, useCache) {
this.id = id;
this.x = x;
this.y = y;
this.useCacha = useCache;
this.cacheCanvas = document.createElement("canvas");
this.cacheCtx = this.cacheCanvas.getContext("2d");
this.r = Math.floor(Math.random() * star_r) + 1;
this.cacheCtx.width = 6 * this.r;
this.cacheCtx.height = 6 * this.r;
var alpha = ( Math.floor(Math.random() * 10) + 1) / star_alpha;
this.color = "rgba(255,255,255," + alpha + ")";
if (useCache) {
this.cache()
}
}複製代碼
細心的同窗可能就會發現
this.cacheCanvas = document.createElement("canvas");
this.cacheCtx = this.cacheCanvas.getContext("2d");複製代碼
這段代碼就是又建立了一個 canvas 標籤,而後再 star 的原型中有一個 cache 方法,這個 cache 方法就是在剛剛建立的 canvas 中繪製 star,而不是直接在原來的 canvas 畫布中繪製的。
cache : function () {
this.cacheCtx.save();
this.cacheCtx.fillStyle = this.color;
this.cacheCtx.shadowColor = "white";
this.cacheCtx.shadowBlur = this.r * 2;
this.cacheCtx.beginPath();
this.cacheCtx.arc(this.r * 3, this.r * 3, this.r, 0, 2 * Math.PI);
this.cacheCtx.closePath();
this.cacheCtx.fill();
this.cacheCtx.restore();
},複製代碼
以後咱們須要將咱們繪製的離屏 canvas 使用 drawImage
方法插入到咱們最早開始建立的 canvas 畫布中。
這裏要注意的是,建立的離屏 canvas 的大小,由於太大的話一樣會浪費性能,因此咱們能夠建立和咱們每個 star 粒子相同的 canvas ,可是這個例子中不適用,要將離屏的 canvas 設置的稍微大一些,由於咱們還須要設置發光的效果(也就是設置陰影)。
發福利的時間到了~╰( ̄▽ ̄)╭,不少小夥伴對 canvas 不是很感興趣,可是想直接使用這個效果,因而我就將其封裝起來,你只須要引入這個 JS,在 HTML 中添加一個 id 爲 canvas 的標籤,而後設置相應的 CSS 就能夠~
github 下載地址:github.com/sunshine940…
在 README 中有使用方法~由於是第一次本身封裝函數,本身一我的在不停的摸索中前進,因此還有不少的不足,但願有大神能夠指點一二~