canvas 實踐
粒子效果
首先確定開發的步驟
準備基礎的 html 跟 css 當背景初始化 canvas準備一個粒子類 Particle編寫粒子連線的函數 drawLine編寫動畫函數 animate添加鼠標和觸摸移動事件、resize事件離屏渲染優化、手機端的模糊處理準備基礎的 html 跟 css 當背景
來這個網址隨便找個你喜歡的漸變色
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="viewport"content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"/><meta name="apple-mobile-web-app-status-bar-style" content="black"/><meta name="format-detection" content="email=no"/><meta name="apple-mobile-web-app-capable" content="yes"/><meta name="format-detection" content="telephone=no"/><meta name="renderer" content="webkit"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Amaze UI"/><meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/><meta http-equiv="Pragma" content="no-cache"/><meta http-equiv="Expires" content="0"/><title>canvas-粒子效果
</title>
</head>
<body><style>html,body {margin:0;overflow:hidden;width:100%;height:100%;background: #B993D6; background: -webkit-linear-gradient(to left, #8CA6DB, #B993D6); background: linear-gradient(to left, #8CA6DB, #B993D6); }</style><script src="hidpi-canvas.min.js"></script><script src="canvas-particle.js"></script>
</body>
</html>
復制代碼這樣之后你就得到了一個純凈的背景
初始化 canvas
首先準備一個可以將 context 變成鏈式調用的方法
function Canvas2DContext(canvas) {
if (
typeof canvas ===
"string") {canvas =
document.getElementById(canvas)}
if (!(
this instanceof Canvas2DContext)) {
return new Canvas2DContext(canvas)}
this.context =
this.ctx = canvas.getContext(
"2d")
if (!Canvas2DContext.prototype.arc) {Canvas2DContext.setup.call(
this,
this.ctx)}
}
Canvas2DContext.setup =
function() {
var methods = [
"arc",
"arcTo",
"beginPath",
"bezierCurveTo",
"clearRect",
"clip",
"closePath",
"drawImage",
"fill",
"fillRect",
"fillText",
"lineTo",
"moveTo",
"quadraticCurveTo",
"rect",
"restore",
"rotate",
"save",
"scale",
"setTransform",
"stroke",
"strokeRect",
"strokeText",
"transform",
"translate"]
var getterMethods = [
"createPattern",
"drawFocusRing",
"isPointInPath",
"measureText",
"createImageData",
"createLinearGradient",
"createRadialGradient",
"getImageData",
"putImageData"]
var props = [
"canvas",
"fillStyle",
"font",
"globalAlpha",
"globalCompositeOperation",
"lineCap",
"lineJoin",
"lineWidth",
"miterLimit",
"shadowOffsetX",
"shadowOffsetY",
"shadowBlur",
"shadowColor",
"strokeStyle",
"textAlign",
"textBaseline"]
for (
let m
of methods) {
let method = mCanvas2DContext.prototype[method] =
function() {
this.ctx[method].apply(
this.ctx,
arguments)
return this}}
for (
let m
of getterMethods) {
let method = mCanvas2DContext.prototype[method] =
function() {
return this.ctx[method].apply(
this.ctx,
arguments)}}
for (
let p
of props) {
let prop = pCanvas2DContext.prototype[prop] =
function(value) {
if (value ===
undefined){
return this.ctx[prop]}
this.ctx[prop] = value
return this}}
}
復制代碼接下來寫一個 ParticleCanvas 函數
const ParticleCanvas =
window.ParticleCanvas =
function(){
const canvas
return canvas
}
const canvas = ParticleCanvas()
console.log(canvas)
復制代碼ParticleCanvas 方法可能會接受很多參數
- 首先第一個參數必然是 id 啦,不然你怎么獲取到 canvas。
- 還有寬高參數,我們把 canvas 處理一下寬高。
- 可以使用 ES6 的函數默認參數跟解構賦值的方法。
- 準備一個 init 方法初始化畫布
const ParticleCanvas =
window.ParticleCanvas =
function({id = "p-canvas",width = 0,height = 0
}){
const canvas =
document.getElementById(id) ||
document.createElement(
"canvas")
if(canvas.id !== id){ (canvas.id = id) &&
document.body.appendChild(canvas)}
const context = Canvas2DContext(canvas)width = width ||
document.documentElement.clientWidthheight = height ||
document.documentElement.clientHeight
const init =
() => {canvas.width = widthcanvas.height = height}init()
return canvas
}
const canvas = ParticleCanvas({})
console.log(canvas)
復制代碼寫完之后就變成這樣了
準備一個粒子類 Particle
接下來我們磨刀霍霍向粒子了,通過觀察動畫效果我們可以知道,首先這個核心就是粒子,且每次出現的隨機的粒子,所以解決了粒子就可以解決了這個效果的 50% 啊 。那我們就開始來寫這個類
我們先來思考一下,這個粒子類,目前最需要哪些參數初始化它
- 第一個當然是,繪制上下文 context
- 然后,這個粒子實際上其實就是畫個圓,畫圓需要什么參數?
- arc(x, y, radius, startAngle, endAngle, anticlockwise)
- 前三個怎么都要傳進來吧,不然你怎么保證每個粒子實例 大小 和 位置 不一樣呢
- 頭腦風暴結束后我們目前確定了四個參數 context x y r
- 所謂 萬丈高樓平地起 要畫一百個粒子,首先先畫第一個粒子
class Particle {
constructor({context, x, y, r}){context.beginPath().fillStyle(
"#fff").arc(x, y, r,
0,
Math.PI *
2).fill().closePath()}
}
const init =
() => {canvas.width = widthcanvas.height = height
const particle =
new Particle({context,
x:
100,
y:
100,
r:
10})
}
init()
復制代碼好的,你成功邁出了第一步
我們接下來思考 現在我們的需求是畫 N 個隨機位置隨機大小的粒子,那要怎么做呢
- 首先,我們可以通過一個循環去繪制一堆粒子
- 只要傳值是隨機的,那,不就是,隨機的粒子嗎!
- 隨機的 x y 應該在屏幕內,而大小應該在一個數值以內
- 說寫就寫,用 Math.random 不就解決需求了嗎
const init =
() => {canvas.width = widthcanvas.height = height
for (
let i =
0; i <
50; i++) {
new Particle({context,
x:
Math.random() * width,
y:
Math.random() * height,
r:
Math.round(
Math.random() * (
10 -
5) +
10)})}
}
init()
復制代碼好的,隨機粒子也被我們擼出來了
接下來還有個問題,這樣直接寫雖然可以解決需求,但是其實不易于擴展。
- 每次我們調用 Particle 類的構造函數的時候,我們就去繪制,這就顯得有些奇怪。
- 我們需要另外準備一個類的內部方法,讓它去負責繪制,而構造函數存儲這些參數值,各司其職
- 然后就是我們初始化的粒子,我們需要拿一個數組來裝住這些粒子,方便我們的后續操作
- 然后機智的你又發現了,我們為什么不傳個顏色,透明度進去讓它更隨機一點
- 我們確定了要傳入 parColor ,那我們分析一波這個參數,你有可能想傳入的是一個十六進制的顏色碼,也可能傳一個 rgb 或者 rgba 形式的,我們配合透明度再來做處理,那就需要另外一個轉換的函數,讓它統一轉換一下。
- 既然你都能傳顏色值了,那支持多種顏色不也是手到擒來的事情,不就是傳個數組進去么?
- 確定完需求就開寫。
const color2Rgb =
(str, op) => {
const reg =
/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/let sColor = str.toLowerCase()op = op || (
Math.floor(
Math.random() *
10) +
4) /
10 /
2let opStr =
`,${op})`if (
this[str]) {
return this[str] + opStr}
if (sColor && reg.test(sColor)) {
if (sColor.length ===
4) {
let sColorNew =
"#"for (
let i =
1; i <
4; i +=
1) {sColorNew += sColor.slice(i, i +
1).concat(sColor.slice(i, i +
1))}sColor = sColorNew}
let sColorChange = []
for (
let i =
1; i <
7; i +=
2) {sColorChange.push(
parseInt(
"0x" + sColor.slice(i, i +
2)))}
let result =
`rgba(${sColorChange.join(",")}`this[str] = result
return result + opStr}
return sColor
}
const getArrRandomItem =
(arr) => arr[
Math.round(
Math.random() * (arr.length -
1 -
0) +
0)]
const ParticleCanvas =
window.ParticleCanvas =
function({id = "p-canvas",width = 0,height = 0,parColor = ["#fff","#000"],parOpacity,maxParR = 10, //粒子最大的尺寸minParR = 5, //粒子最小的尺寸
}){...let particles = []
class Particle {
constructor({context, x, y, r, parColor, parOpacity}){
this.context = context
this.x = x
this.y = y
this.r = r
this.color = color2Rgb(
typeof parColor ===
"string" ? parColor : getArrRandomItem(parColor), parOpacity)
this.draw()}draw(){
this.context.beginPath().fillStyle(
this.color).arc(
this.x,
this.y,
this.r,
0,
Math.PI *
2).fill().closePath()}}
const init =
() => {canvas.width = widthcanvas.height = height
for (
let i =
0; i <
50; i++) {particles.push(
new Particle({context,
x:
Math.random() * width,
y:
Math.random() * height,
r:
Math.round(
Math.random() * (maxParR - minParR) + minParR),parColor,parOpacity}))}}init()
return canvas
}
復制代碼接下來你的頁面就會長成這樣子啦,基礎的粒子類已經寫好了,接下來我們先把連線函數編寫一下
drawLine
兩個點要如何連成線?我們查一下就知道,要通過調用 moveTo(x, y) 和 lineTo(x,y)
- 觀察效果,思考一下連線的條件,我們發現在一定的距離兩個粒子會連成線
- 首先線的參數就跟粒子的是差不多的,需要線寬 lineWidth, 顏色 lineColor, 透明度 lineOpacity
- 那其實是不是再通過雙層循環來調用 drawLine 就可以讓他們彼此連線
- drawLine 其實就需要傳入另一個粒子進去,開搞
const ParticleCanvas =
window.ParticleCanvas =
function({id = "p-canvas",width = 0,height = 0,parColor = ["#fff","#000"],parOpacity,maxParR = 10, //粒子最大的尺寸minParR = 5, //粒子最小的尺寸lineColor = "#fff",lineOpacity,lineWidth = 1
}){...class Particle {
constructor({context, x, y, r, parColor, parOpacity, lineWidth, lineColor, lineOpacity}){
this.context = context
this.x = x
this.y = y
this.r = r
this.color = color2Rgb(
typeof parColor ===
"string" ? parColor : getArrRandomItem(parColor), parOpacity)
this.lineColor = color2Rgb(
typeof lineColor ===
"string" ? lineColor : getArrRandomItem(lineColor), lineOpacity)
if(lineColor !=
"#fff"){
this.color =
this.lineColor}
else{
this.lineColor =
this.color}
this.lineWidth = lineWidth
this.draw()}draw(){...}drawLine(_round) {
let dx =
this.x - _round.x,dy =
this.y - _round.y
if (
Math.sqrt(dx * dx + dy * dy) <
150) {
let x =
this.x,y =
this.y,lx = _round.x,ly = _round.y
this.context.beginPath().moveTo(x, y).lineTo(lx, ly).closePath().lineWidth(
this.lineWidth).strokeStyle(
this.lineColor).stroke()}}}
const init =
() => {canvas.width = widthcanvas.height = height
for (
let i =
0; i <
50; i++) {particles.push(
new Particle({context,
x:
Math.random() * width,
y:
Math.random() * height,
r:
Math.round(
Math.random() * (maxParR - minParR) + minParR),parColor,parOpacity,lineWidth, lineColor, lineOpacity}))}
for (
let i =
0; i < particles.length; i++) {
for (
let j = i +
1; j < particles.length; j++) {particles[i].drawLine(particles[j])}}}...
}
復制代碼現在我們就得到一個連線的粒子了,接下來我們就要讓我們的頁面動起來了
animate
首先我們要認識到,canvas是通過我們編寫的那些繪制函數繪制上去的,那么,我們如果使用一個定時器,定時的去繪制,不就是動畫的基本原理了么
- 首先我們要寫一個 animate 函數,把我們的邏輯寫進去,然后讓定時器 requestAnimationFrame 去執行它
requestAnimationFrame是瀏覽器用于定時循環操作的一個接口,類似于setTimeout,主要用途是按幀對網頁進行重繪。
設置這個API的目的是為了讓各種網頁動畫效果(DOM動畫、Canvas動畫、SVG動畫、WebGL動畫)能夠有一個統一的刷新機制,從而節省系統資源,提高系統性能,改善視覺效果。代碼中使用這個API,就是告訴瀏覽器希望執行一個動畫,讓瀏覽器在下一個動畫幀安排一次網頁重繪。
- 看不明白的話,那你就把他當成一個不用你去設置時間的 setInterval
- 那我們要通過動畫去執行繪制,粒子要動起來,我們必須要再粒子類上再擴展一個方法 move ,既然要移動了,那上下移動的偏移量必不可少 moveX 和 moveY
- 邏輯分析完畢,開炮
const ParticleCanvas =
window.ParticleCanvas =
function({id = "p-canvas",width = 0,height = 0,parColor = ["#fff","#000"],parOpacity,maxParR = 10, //粒子最大的尺寸minParR = 5, //粒子最小的尺寸lineColor = "#fff",lineOpacity,lineWidth = 1,moveX = 0,moveY = 0,
}){...class Particle {
constructor({context, x, y, r, parColor, parOpacity, lineWidth, lineColor, lineOpacity, moveX, moveY}){
this.context = context
this.x = x
this.y = y
this.r = r
this.color = color2Rgb(
typeof parColor ===
"string" ? parColor : getArrRandomItem(parColor), parOpacity)
this.lineColor = color2Rgb(
typeof lineColor ===
"string" ? lineColor : getArrRandomItem(lineColor), lineOpacity)
this.lineWidth = lineWidth
this.moveX =
Math.random() + moveX
this.moveY =
Math.random() + moveY
this.draw()}draw(){
this.context.beginPath().fillStyle(
this.color).arc(
this.x,
this.y,
this.r,
0,
Math.PI *
2).fill().closePath()}drawLine(_round) {
let dx =
this.x - _round.x,dy =
this.y - _round.y
if (
Math.sqrt(dx * dx + dy * dy) <
150) {
let x =
this.x,y =
this.y,lx = _round.x,ly = _round.y
if(
this.userCache){x =
this.x +
this.r /
this._ratioy =
this.y +
this.r /
this._ratiolx = _round.x + _round.r /
this._ratioly = _round.y + _round.r /
this._ratio}
this.context.beginPath().moveTo(x, y).lineTo(lx, ly).closePath().lineWidth(
this.lineWidth).strokeStyle(
this.lineColor).stroke()}}move() {
this.moveX =
this.x +
this.r *
2 < width &&
this.x >
0 ?
this.moveX : -
this.moveX
this.moveY =
this.y +
this.r *
2 < height &&
this.y >
0 ?
this.moveY : -
this.moveY
this.x +=
this.moveX
this.y +=
this.moveY
this.draw()}}
const animate =
() => {context.clearRect(
0,
0, width, height)
for (
let i =
0; i < particles.length; i++) {particles[i].move()
for (
let j = i +
1; j < particles.length; j++) {particles[i].drawLine(particles[j])}}requestAnimationFrame(animate)}
const init =
() => {canvas.width = widthcanvas.height = height
for (
let i =
0; i <
50; i++) {particles.push(
new Particle({context,
x:
Math.random() * width,
y:
Math.random() * height,
r:
Math.round(
Math.random() * (maxParR - minParR) + minParR),parColor,parOpacity,lineWidth, lineColor, lineOpacity,moveX,moveY,}))}animate()}init()
return canvas
}
復制代碼如果沒有意外,你的頁面應該動起來啦,是不是感覺很簡單呢
添加鼠標和觸摸移動事件
接下來我們要來添加鼠標和觸摸移動的效果了
- 首先鼠標移動會有一個粒子跟隨,我們單獨初始化一個孤單的粒子出來 currentParticle,這個粒子跟上來自己動的妖艷賤貨不一樣的點在于,currentParticle 的位置,我們需要通過監聽事件返回的鼠標位置賦值給它,是的,這個需要你讓他動。
- 既然是個獨特的粒子,那么樣式也要支持自定義啦 isMove(是否開啟跟隨) targetColor targetPpacity targetR 看你也知道是什么意思啦, 不解釋了。
- resize 事件是監聽瀏覽器窗口尺寸變化,這樣子在用戶變化尺寸的時候,我們的背景就不會變得不和諧
- 實現的思路主要是通過監聽 resize 事件,重新調用一波 init 方法,來重新渲染畫布,由于 resize 這個在事件在變化的時候回調非常的頻繁,頻繁的計算會影響性能,嚴重可能會卡死,所以我們通過防抖 debounce 或者節流 throttle 的方式來限制其調用。
- 了解完思路,那就繼續寫啦
const toFixed =
(a, n) => parseFloat(a.toFixed(n ||
1))
const throttle =
function (func,wait,options) {
var context,args,timeout
var previous =
0options = options || {}
var later =
function(){previous = options.leading ===
false ?
0 :
new Date().getTime()timeout =
nullfunc.apply(context, args)}
var throttled =
function(){
var now = +
new Date()
if (!previous && options.leading ===
false) {previous = now}
var remaining = wait - (now - previous)context =
thisargs =
argumentsif(remaining > wait || remaining <=
0){
if (timeout) {clearTimeout(timeout)timeout =
null}previous = nowfunc.apply(context, args)}
else if(!timeout && options.trailing !==
false){timeout = setTimeout(later, remaining)}}throttled.cancel =
function() {clearTimeout(timeout)previous =
0timeout =
null}
return throttled
}
const debounce =
function(func,wait,immediate){
var timeout,result
var debounced =
function() {
var context =
thisvar args =
argumentsif(timeout){clearTimeout(timeout)}
if(immediate){
var callNow = !timeout
console.log(callNow)timeout = setTimeout(
function(){timeout =
null}, wait)
if (callNow) {result = func.apply(context, args)}}
else{timeout = setTimeout(
function(){func.apply(context,args)}, wait)}
return result}debounced.cancel =
function(){
if(timeout) {clearTimeout(timeout)}timeout =
null}
return debounced
}
const ParticleCanvas =
window.ParticleCanvas =
function({id = "p-canvas",width = 0,height = 0,parColor = ["#fff"],parOpacity,maxParR = 10, //粒子最大的尺寸minParR = 5, //粒子最小的尺寸lineColor = "#fff",lineOpacity,lineWidth = 1,moveX = 0,moveY = 0,isMove = true,targetColor = ["#000"],targetPpacity = 0.6,targetR = 10,
}){
let currentParticle,isWResize = width,isHResize = height,myReq =
nullclass Particle {...}
const animate =
() => {context.clearRect(
0,
0, width, height)
for (
let i =
0; i < particles.length; i++) {particles[i].move()
for (
let j = i +
1; j < particles.length; j++) {particles[i].drawLine(particles[j])}}
if (isMove && currentParticle.x) {
for (
let i =
0; i < particles.length; i++) {currentParticle.drawLine(particles[i])}currentParticle.draw()}myReq = requestAnimationFrame(animate)}
const init =
() => {canvas.width = widthcanvas.height = height
if (isMove && !currentParticle) {currentParticle =
new Particle({
x:
0,
y:
0,
r: targetR,
parColor: targetColor,
parOpacity: targetPpacity,lineColor,lineOpacity, lineWidth,context})
const moveEvent =
(e = window.event) => {currentParticle.x = e.clientX || e.touches[
0].clientXcurrentParticle.y = e.clientY || e.touches[
0].clientY}
const outEvent =
() => {currentParticle.x = currentParticle.y =
null}
const eventObject = {
"pc": {
move:
"mousemove",
out:
"mouseout"},
"phone": {
move:
"touchmove",
out:
"touchend"}}
const event = eventObject[
/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent) ?
"phone" :
"pc"]canvas.removeEventListener(event.move,moveEvent)canvas.removeEventListener(event.out, outEvent)canvas.addEventListener(event.move,moveEvent)canvas.addEventListener(event.out, outEvent)}
for (
let i =
0; i <
50; i++) {particles.push(
new Particle({context,
x:
Math.random() * width,
y:
Math.random() * height,
r:
Math.round(
Math.random() * (maxParR - minParR) + minParR),parColor,parOpacity,lineWidth, lineColor, lineOpacity,moveX,moveY,}))}animate()
if(!isWResize || !isHResize){
window.addEventListener(
"resize",debounce(resize,
100))}}
const resize =
() => {
if(
this.timeout){clearTimeout(
this.timeout)}
if(myReq){
window.cancelAnimationFrame(myReq)}particles = []width = isWResize ? width :
document.documentElement.clientWidthheight = isHResize ? height :
document.documentElement.clientHeight
this.timeout = setTimeout(init,
20)}init()
return canvas
}
復制代碼寫到這里,這個東西差不多啦,接下來就是優化的問題了
離屏渲染優化和手機端的模糊處理
離屏渲染
其實是指用離屏canvas上預渲染相似的圖形或重復的對象,簡單點說就是,你現在其他canvas對象上畫好,然后再通過 drawImage() 放進去目標畫布里面
- 我們需要提供一個方法,用于離屏渲染粒子,用于生成一個看不見的 canvas 然后在上面畫畫畫
- 最好能夠提供一下緩存用過的 canvas 用于節省空間性能,提高復用率
- 畫的時候要注意,提供一個倍數,然后再縮小,看上去就比較清晰
- 這里的注意點是,理解這種渲染方式,以及倍數之間的關系
const getCachePoint =
(r,color,cacheRatio) => {
let key = r +
"cache" + color
if(
this[key]){
return this[key]}
const _ratio =
2 * cacheRatio,width = r * _ratio,cacheCanvas =
document.createElement(
"canvas"),cacheContext = Canvas2DContext(cacheCanvas)cacheCanvas.width = cacheCanvas.height = widthcacheContext.save().fillStyle(color).arc(r * cacheRatio, r * cacheRatio, r,
0,
360).closePath().fill().restore()
this[key] = cacheCanvas
return cacheCanvas
}
const ParticleCanvas =
window.ParticleCanvas =
function({...useCache = true //新增一個useCache表示是否開啟離屏渲染
}){...class Particle {
constructor({context, x, y, r, parColor, parOpacity, lineWidth, lineColor, lineOpacity, moveX, moveY, useCache}){...this.ratio =
3this.useCache = useCache}draw(){
if(
this.useCache){
this.context.drawImage(getCachePoint(
this.r,
this.color,
this.ratio),
this.x -
this.r *
this.ratio,
this.y -
this.r *
this.ratio)}
else{
this.context.beginPath().fillStyle(
this.color).arc(toFixed(
this.x), toFixed(
this.y), toFixed(
this.r),
0,
Math.PI *
2).fill().closePath()}}...}...
const init =
() => {...if (isMove && !currentParticle) {currentParticle =
new Particle({...useCache}) ...}
for (
let i =
0; i <
50; i++) {particles.push(
new Particle({...useCache}))}...}...
}
復制代碼高清屏的模糊處理
因為 canvas 繪制的圖像并不是矢量圖,而是跟圖片一樣的位圖,所以在高 dpi 的屏幕上看的時候,就會顯得比較模糊,比如 蘋果的 Retina 屏幕,它會用兩個或者三個像素來合成一個像素,相當于圖被放大了兩倍或者三倍,所以自然就模糊了
我們可以通過引入 hidpi-canvas.min.js 來處理在手機端高清屏繪制變得模糊的問題
這個插件的原理是通過這個方法來獲取 dpi
getPixelRatio =
(context) => {
var backingStore = context.backingStorePixelRatio ||context.webkitBackingStorePixelRatio ||context.mozBackingStorePixelRatio ||context.msBackingStorePixelRatio ||context.oBackingStorePixelRatio ||context.backingStorePixelRatio ||
1return (
window.devicePixelRatio ||
1) / backingStore
}
復制代碼然后通過放大畫布,再通過CSS的寬高縮小畫布
const setRetina =
(canvas,context,width,height) => {
var ratio = getPixelRatio(context)ratio =
2if(context._retinaRatio && context._retinaRatio !== ratio){
window.location.reload()}canvas.style.width = width * ratio +
"px"canvas.style.height = height * ratio +
"px"context.setTransform(ratio,
0,
0, ratio,
0,
0)canvas.width = width * ratiocanvas.height = height * ratiocontext._retinaRatio = ratio
return ratio
}
復制代碼這個方法通過處理是可以兼容好手機模糊的問題,但是在屏幕比較好的電腦屏幕感覺還是有點模糊,所以我就改造了一下...
- 如果是手機端,放大三倍,電腦端則放大兩倍,再縮小到指定大小
- 需要注意的是,drawImage 的倍數關系
- 如果有更好更優雅的辦法,希望能交流一下
const PIXEL_RATIO =
/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent) ?
3 :
2
;(
function(prototype) {
var forEach =
function(obj, func) {
for (
var p
in obj) {
if (obj.hasOwnProperty(p)) {func(obj[p], p)}}},ratioArgs = {
"fillRect":
"all",
"clearRect":
"all",
"strokeRect":
"all",
"moveTo":
"all",
"lineTo":
"all",
"arc": [
0,
1,
2],
"arcTo":
"all",
"bezierCurveTo":
"all",
"isPointinPath":
"all",
"isPointinStroke":
"all",
"quadraticCurveTo":
"all",
"rect":
"all",
"translate":
"all",
"createRadialGradient":
"all",
"createLinearGradient":
"all"}forEach(ratioArgs,
function(value, key) {prototype[key] = (
function(_super) {
return function() {
var i, len,args =
Array.prototype.slice.call(
arguments)
if (value ===
"all") {args = args.map(
function(a) {
return a * PIXEL_RATIO})}
else if (
Array.isArray(value)) {
for (i =
0, len = value.length; i < len; i++) {args[value[i]] *= PIXEL_RATIO}}
return _super.apply(
this, args)}})(prototype[key])})prototype.stroke = (
function(_super) {
return function() {
this.lineWidth *= PIXEL_RATIO_super.apply(
this,
arguments)
this.lineWidth /= PIXEL_RATIO}})(prototype.stroke)prototype.fillText = (
function(_super) {
return function() {
var args =
Array.prototype.slice.call(
arguments)args[
1] *= PIXEL_RATIO args[
2] *= PIXEL_RATIO
this.font =
this.font.replace(
/(\d+)(px|em|rem|pt)/g,
function(w, m, u) {
return m * PIXEL_RATIO + u})_super.apply(
this, args)
this.font =
this.font.replace(
/(\d+)(px|em|rem|pt)/g,
function(w, m, u) {
return m / PIXEL_RATIO + u})}})(prototype.fillText)prototype.strokeText = (
function(_super) {
return function() {
var args =
Array.prototype.slice.call(
arguments)args[
1] *= PIXEL_RATIO args[
2] *= PIXEL_RATIO
this.font =
this.font.replace(
/(\d+)(px|em|rem|pt)/g,
function(w, m, u) {
return m * PIXEL_RATIO + u})_super.apply(
this, args)
this.font =
this.font.replace(
/(\d+)(px|em|rem|pt)/g,
function(w, m, u) {
return m / PIXEL_RATIO + u})}})(prototype.strokeText)
})(CanvasRenderingContext2D.prototype)
const setRetina =
(canvas,context,width,height) => {
var ratio = PIXEL_RATIOcanvas.style.width = width +
"px"canvas.style.height = height +
"px"context.setTransform(ratio,
0,
0, ratio,
0,
0)canvas.width = width * ratiocanvas.height = height * ratiocontext._retinaRatio = ratio
return ratio
}
function Canvas2DContext(canvas) {
if (
typeof canvas ===
"string") {canvas =
document.getElementById(canvas)}
if (!(
this instanceof Canvas2DContext)) {
return new Canvas2DContext(canvas)}
this.context =
this.ctx = canvas.getContext(
"2d")
if (!Canvas2DContext.prototype.arc) {Canvas2DContext.setup.call(
this,
this.ctx)}
}
Canvas2DContext.setup =
function() {
var methods = [
"arc",
"arcTo",
"beginPath",
"bezierCurveTo",
"clearRect",
"clip",
"closePath",
"drawImage",
"fill",
"fillRect",
"fillText",
"lineTo",
"moveTo",
"quadraticCurveTo",
"rect",
"restore",
"rotate",
"save",
"scale",
"setTransform",
"stroke",
"strokeRect",
"strokeText",
"transform",
"translate"]
var getterMethods = [
"createPattern",
"drawFocusRing",
"isPointInPath",
"measureText",
"createImageData",
"createLinearGradient",
"createRadialGradient",
"getImageData",
"putImageData"]
var props = [
"canvas",
"fillStyle",
"font",
"globalAlpha",
"globalCompositeOperation",
"lineCap",
"lineJoin",
"lineWidth",
"miterLimit",
"shadowOffsetX",
"shadowOffsetY",
"shadowBlur",
"shadowColor",
"strokeStyle",
"textAlign",
"textBaseline"]
for (
let m
of methods) {
let method = mCanvas2DContext.prototype[method] =
function() {
this.ctx[method].apply(
this.ctx,
arguments)
return this}}
for (
let m
of getterMethods) {
let method = mCanvas2DContext.prototype[method] =
function() {
return this.ctx[method].apply(
this.ctx,
arguments)}}
for (
let p
of props) {
let prop = pCanvas2DContext.prototype[prop] =
function(value) {
if (value ===
undefined){
return this.ctx[prop]}
this.ctx[prop] = value
return this}}
}
const color2Rgb =
(str, op) => {
const reg =
/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/let sColor = str.toLowerCase()op = op || (
Math.floor(
Math.random() *
10) +
4) /
10 /
2let opStr =
`,${op})`if (
this[str]) {
return this[str] + opStr}
if (sColor && reg.test(sColor)) {
if (sColor.length ===
4) {
let sColorNew =
"#"for (
let i =
1; i <
4; i +=
1) {sColorNew += sColor.slice(i, i +
1).concat(sColor.slice(i, i +
1))}sColor = sColorNew}
let sColorChange = []
for (
let i =
1; i <
7; i +=
2) {sColorChange.push(
parseInt(
"0x" + sColor.slice(i, i +
2)))}
let result =
`rgba(${sColorChange.join(",")}`this[str] = result
return result + opStr}
return sColor
}
const getArrRandomItem =
(arr) => arr[
Math.round(
Math.random() * (arr.length -
1 -
0) +
0)]
const toFixed =
(a, n) => parseFloat(a.toFixed(n ||
1))
const throttle =
function (func,wait,options) {
var context,args,timeout
var previous =
0options = options || {}
var later =
function(){previous = options.leading ===
false ?
0 :
new Date().getTime()timeout =
nullfunc.apply(context, args)}
var throttled =
function(){
var now = +
new Date()
if (!previous && options.leading ===
false) {previous = now}
var remaining = wait - (now - previous)context =
thisargs =
argumentsif(remaining > wait || remaining <=
0){
if (timeout) {clearTimeout(timeout)timeout =
null}previous = nowfunc.apply(context, args)}
else if(!timeout && options.trailing !==
false){timeout = setTimeout(later, remaining)}}throttled.cancel =
function() {clearTimeout(timeout)previous =
0timeout =
null}
return throttled
}
const debounce =
function(func,wait,immediate){
var timeout,result
var debounced =
function() {
var context =
thisvar args =
argumentsif(timeout){clearTimeout(timeout)}
if(immediate){
var callNow = !timeout
console.log(callNow)timeout = setTimeout(
function(){timeout =
null}, wait)
if (callNow) {result = func.apply(context, args)}}
else{timeout = setTimeout(
function(){func.apply(context,args)}, wait)}
return result}debounced.cancel =
function(){
if(timeout) {clearTimeout(timeout)}timeout =
null}
return debounced
}
const getCachePoint =
(r,color,cacheRatio) => {
let key = r +
"cache" + color
if(
this[key]){
return this[key]}
const _ratio =
2 * cacheRatio,width = r * _ratio,cR = toFixed(r * cacheRatio),cacheCanvas =
document.createElement(
"canvas"),cacheContext = Canvas2DContext(cacheCanvas)setRetina(cacheCanvas,cacheContext,width,width)cacheContext.save().fillStyle(color).arc(cR, cR, cR,
0,
360).closePath().fill().restore()
this[key] = cacheCanvas
return cacheCanvas
}
const ParticleCanvas =
window.ParticleCanvas =
function({id = "p-canvas",num = 30,width = 0,height = 0,parColor = ["#fff"],parOpacity,maxParR = 4, //粒子最大的尺寸minParR = 8, //粒子最小的尺寸lineColor = ["#fff"],lineOpacity = 0.3,lineWidth = 1,moveX = 0,moveY = 0,isMove = true,targetColor = ["#fff"],targetPpacity = 0.6,targetR = 6,useCache = false
}){
const canvas =
document.getElementById(id) ||
document.createElement(
"canvas")
if(canvas.id !== id){ (canvas.id = id) &&
document.body.appendChild(canvas)}
const context = Canvas2DContext(canvas)
let currentParticle,isWResize = width,isHResize = height,myReq =
nulllet particles = []width = width ||
document.documentElement.clientWidthheight = height ||
document.documentElement.clientHeight
class Particle {
constructor({context, x, y, r, parColor, parOpacity, lineWidth, lineColor, lineOpacity, moveX, moveY, useCache}){
this.context = context
this.x = x
this.y = y
this.r = toFixed(r)
this.ratio =
3this.color = color2Rgb(
typeof parColor ===
"string" ? parColor : getArrRandomItem(parColor), parOpacity)
this.lineColor = color2Rgb(
typeof lineColor ===
"string" ? lineColor : getArrRandomItem(lineColor), lineOpacity)
if(lineColor ===
"#fff"){
this.color =
this.lineColor}
else{
this.lineColor =
this.color}
this.lineWidth = lineWidth
this.x = x >
this.r ? x -
this.r : x
this.y = y >
this.r ? y -
this.r : y
this.moveX =
Math.random() + moveX
this.moveY =
Math.random() + moveY
this.useCache = useCache
this.draw()}draw(){
if(
this.x >=
0 &&
this.y >=
0){
if(
this.useCache){
this.context.drawImage(getCachePoint(
this.r,
this.color,
this.ratio), toFixed(
this.x -
this.r) *
this.context._retinaRatio, toFixed(
this.y -
this.r) *
this.context._retinaRatio,
this.r *
2 *
this.context._retinaRatio,
this.r *
2 *
this.context._retinaRatio)}
else{
this.context.beginPath().fillStyle(
this.color).arc(toFixed(
this.x), toFixed(
this.y), toFixed(
this.r),
0,
Math.PI *
2).fill().closePath()}}}drawLine(_round) {
let dx =
this.x - _round.x,dy =
this.y - _round.y
if (
Math.sqrt(dx * dx + dy * dy) <
150) {
let x =
this.x,y =
this.y,lx = _round.x,ly = _round.y
if(
this.userCache){x =
this.x +
this.r /
this._ratioy =
this.y +
this.r /
this._ratiolx = _round.x + _round.r /
this._ratioly = _round.y + _round.r /
this._ratio}
if(x >=
0 && y >=
0 && lx >=
0 && ly >=
0){
this.context.beginPath().moveTo(toFixed(x), toFixed(y)).lineTo(toFixed(lx), toFixed(ly)).closePath().lineWidth(
this.lineWidth).strokeStyle(
this.lineColor).stroke()}}}move() {
this.moveX =
this.x +
this.r *
2 < width &&
this.x >
0 ?
this.moveX : -
this.moveX
this.moveY =
this.y +
this.r *
2 < height &&
this.y >
0 ?
this.moveY : -
this.moveY
this.x +=
this.moveX
this.y +=
this.moveY
this.draw()}}
const animate =
() => {context.clearRect(
0,
0, width, height)
for (
let i =
0; i < particles.length; i++) {particles[i].move()
for (
let j = i +
1; j < particles.length; j++) {particles[i].drawLine(particles[j])}}
if (isMove && currentParticle.x) {
for (
let i =
0; i < particles.length; i++) {currentParticle.drawLine(particles[i])}currentParticle.draw()}myReq = requestAnimationFrame(animate)}
const init =
() => {setRetina(canvas, context, width, height)
if (isMove && !currentParticle) {currentParticle =
new Particle({
x:
0,
y:
0,
r: targetR,
parColor: targetColor,
parOpacity: targetPpacity,lineColor,lineOpacity, lineWidth,context,useCache})
const moveEvent =
(e = window.event) => {currentParticle.x = e.clientX || e.touches[
0].clientXcurrentParticle.y = e.clientY || e.touches[
0].clientY}
const outEvent =
() => {currentParticle.x = currentParticle.y =
null}
const eventObject = {
"pc": {
move:
"mousemove",
out:
"mouseout"},
"phone": {
move:
"touchmove",
out:
"touchend"}}
const event = eventObject[
/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent) ?
"phone" :
"pc"]canvas.removeEventListener(event.move,moveEvent)canvas.removeEventListener(event.out, outEvent)canvas.addEventListener(event.move,moveEvent)canvas.addEventListener(event.out, outEvent)}
for (
let i =
0; i < num; i++) {particles.push(
new Particle({context,
x:
Math.random() * width,
y:
Math.random() * height,
r:
Math.round(
Math.random() * (maxParR - minParR) + minParR),parColor,parOpacity,lineWidth, lineColor, lineOpacity,moveX,moveY,useCache}))}animate()
if(!isWResize || !isHResize){
window.addEventListener(
"resize",debounce(resize,
100))}}
const resize =
() => {
if(
this.timeout){clearTimeout(
this.timeout)}
if(myReq){
window.cancelAnimationFrame(myReq)}particles = []width = isWResize ? width :
document.documentElement.clientWidthheight = isHResize ? height :
document.documentElement.clientHeight
this.timeout = setTimeout(init,
20)}init()
return canvas
}
const canvas = ParticleCanvas({})
console.log(canvas)
復制代碼寫到這里基本也就寫完了...
溜了溜了
轉載于:https://juejin.im/post/5bf506576fb9a049a62c329a
總結
以上是生活随笔為你收集整理的canvas 粒子效果 - 手残实践纪录的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。