網(wǎng)頁使用怎么做太原seo推廣
Canvas繪圖
Canvas的意義
隨著前端的不斷發(fā)展,頁面特效越來越炫酷,W3C組織也不斷退出新的CSS特性:例如各種漸變,瀑布流布局,各種陰影,但是隨著需求越來越花哨,W3C表示:我去你媽的,你自己畫去吧。
于是瀏覽器就暴露出了Canvas API讓用戶自己實(shí)現(xiàn)各種炫酷的效果。
學(xué)習(xí)過瀏覽器的渲染過程,我們可以知道其實(shí)瀏覽器的窗口本身就是一個(gè)畫布,他根據(jù)DOM和CSSOM不斷得生成繪制指令來重繪頁面。
Canvas其實(shí)就是瀏覽器將繪制指令封裝成API給用戶進(jìn)行調(diào)用,這也是為什么Canvas的性能要比直接操作DOM的性能更高的原因。
這就是Canvas存在的意義,可以自定義炫酷的效果,可以有比DOM操作更好的性能。
圖形繪制API
坐標(biāo)系
講繪圖之前先講解一下坐標(biāo)系。
Canvas的坐標(biāo)系與瀏覽器的坐標(biāo)系相同,都是以左上角為原點(diǎn),向右為x軸,向下為y軸。
上下文
想用Canvas進(jìn)行繪制,首先需要拿到Canvas的上下文,它就相當(dāng)于一個(gè)畫筆,可以發(fā)出各種繪制指令。
<canvas id="canvas" />const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext('2d');
這個(gè) ctx
就是canvas的上下文,這個(gè)上下文共有4種類型:
- 2d: 繪制2d圖形
- bitmaprenderer: 繪制位圖
- webgl: 繪制3d圖形,只在實(shí)現(xiàn)WebGL1的瀏覽器種可用
- webgl2: 繪制3d圖形,只在實(shí)現(xiàn)WebGL2的瀏覽器種可用
這里只學(xué)習(xí)最基本的2d圖形繪制。
繪制指令
canvas支持4種圖形的繪制
- 直線和矩形
- 曲線和橢圓
- 文本
- 圖片
不管繪制哪種圖形,都是按照下面的步驟發(fā)出繪圖指令,和AI還有PS的邏輯很相似:
- 開啟路徑
- 設(shè)置著色(描邊和填充)
- 設(shè)置路徑
- 閉合路徑
- 繪制
ctx.beginPath(); // 開啟路徑
ctx.strokeStyle = '#aaaaaa'; // 設(shè)置描邊顏色
ctx.fillStyle = '#111111'; // 設(shè)置填充顏色// ... 若干繪圖指令ctx.closePath(); // 閉合路徑,可選的,也可以不閉合,繪制一條開放的路徑,如果使用填充指令來繪制的話,路徑會(huì)自動(dòng)閉合。ctx.fill(); // 填充
ctx.stroke(); // 描邊
直線繪制
繪制一個(gè)三角形演示一下
ctx.moveTo(100, 100); // 移動(dòng)畫筆到100, 100的位置
ctx.lineTo(100, 200); // 從當(dāng)前位置向100, 200畫一條路徑
ctx.lineTo(200, 200);
ctx.closePath(); // 閉合路徑形成完整的三角形
ctx.stroke(); // 根據(jù)路徑描邊
矩形繪制
矩形的繪制有三種API
- 繪制矩形的路徑
- 繪制帶描邊的矩形
- 繪制帶填充的矩形
三種API不做演示,剩余的圓形,曲線等API也不做演示,可自行查閱文檔。
Canvas 教程 - Web API 接口參考 | MDN (mozilla.org)
案例
利用圖形API可以制作一個(gè)粒子連線效果。
首先定義一個(gè)類,用于表示粒子點(diǎn):
class Point {constructor() {this.r = 8;this.x = getRandomInt(canvas.width - this.r, this.r);this.y = getRandomInt(canvas.height - this.r, this.r);}draw() {ctx.beginPath();ctx.fillStyle = 'rgba(11, 11, 11, 255)';ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);ctx.closePath();ctx.fill();}}
然后再定義一個(gè)類,用于表示粒子圖
class Graph {constructor(count = 30, maxDis = 200) {this.maxDis = maxDis;this.points = new Array(30).fill(null).map(item => new Point());}draw() {for(let i = 0; i < this.points.length; i++) {const p = this.points[i];p.draw();for(let j = i+1; j < this.points.length; j++) {const p2 = this.points[j];const d = Math.sqrt((p.x - p2.x) ** 2 + (p.y - p2.y) ** 2);ctx.beginPath();ctx.moveTo(p.x, p.y);ctx.lineTo(p2.x, p2.y);ctx.closePath();ctx.strokeStyle = `rgba(11, 11, 11, ${255 * (this.maxDis - d) / this.maxDis})`;ctx.stroke();}}}}function getRandomInt(max, min = 0) {return Math.floor( Math.random() * (max - min + 1) ) + min;}
加上最后的代碼,就可以實(shí)現(xiàn)一副一直抽搐的粒子圖。
function main() {ctx.fillStyle = '#fff';ctx.fillRect(0, 0, canvas.width, canvas.height);new Graph().draw();requestAnimationFrame(main);}main();
清晰度問題
隨著頁面的放大,Canvas繪制的圖形會(huì)變得模糊。
要解決這個(gè)問題,首先要知道,其實(shí)Canvas繪制的結(jié)果,就是一張圖片。
圖像有兩種尺寸,一種是自然尺寸,一種是樣式尺寸。
自然尺寸就是圖像原本的大小,樣式尺寸是通過css或者JS設(shè)置的尺寸。
圖片隨著放大會(huì)變得模糊,是因?yàn)樗臉邮匠叽绱笥谒淖匀怀叽?#xff0c;使得原來的一個(gè)像素點(diǎn)需要兩個(gè)或者更多像素點(diǎn)來顯示,但是圖片本身并沒有包含這么多信息,因此就會(huì)變得模糊。
但是Canvas的尺寸是可以自己設(shè)置的,繪制的圖形大小也是可以自己設(shè)置的,也就是自然尺寸是可以更改的。所以利用這個(gè)特性,我們只要滿足 自然尺寸 = 樣式尺寸 * 縮放倍率
,就可以做到Canvas不會(huì)模糊。
使用下面的代碼可以在頁面上繪制一個(gè)圓
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');function init() {ctx.width = 200;ctx.height = 200;
}init();ctx.beginPath();
const r = 80;
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, Math.PI * 2);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 10; // 設(shè)置canvas線寬
ctx.stroke();
通過這一段代碼可以繪制初始化一個(gè)200 * 200的canvas頁面,并在canvas的中央繪制了一個(gè)半徑為80的圓。
現(xiàn)在的代碼,當(dāng)瀏覽器的縮放倍率變化時(shí),圓會(huì)變得模糊。
我們可以修改代碼,使得 自然尺寸 = 樣式尺寸 * 縮放倍率
這個(gè)等式永遠(yuǎn)成立,只要這個(gè)等式成立,圖像一定是清晰的。
縮放倍率變化后,可以修改Canvas的自然尺寸使得圖像依然清晰。
// 修改原始尺寸
function init() {// devicePixelRatio 可以獲取當(dāng)前瀏覽器的放大倍率canvas.width = 200 * devicePixelRatio; // 樣式尺寸 * 縮放倍率 = 原始尺寸canvas.height = 200 * devicePixelRatio;
}
如果想讓canvas上繪制的內(nèi)容也跟著放大和縮小,只需要讓繪制指令中的數(shù)值也跟著放大和縮小。
ctx.beginPath();
const r = 80 * devicePixelRatio;
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, Math.PI * 2);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 10 * devicePixelRatio; // 設(shè)置canvas線寬
ctx.stroke();
動(dòng)畫
使用Canvas來做動(dòng)畫,原理很簡單,就是每間隔一段時(shí)間重新繪制Canvas。
因?yàn)镴S的定時(shí)器不準(zhǔn)確,計(jì)時(shí)間隔小的時(shí)候可能會(huì)出現(xiàn)掉幀的現(xiàn)象,所以使用 requestAnimationFrame()
更合適。
有點(diǎn)類似遞歸,這樣寫就可以讓瀏覽器每次刷新時(shí)都重新繪制Canvas。
function draw() {// 若干繪制指令reuqestAnimationFrame(draw);
}
文字繪制–代碼雨效果
canvas繪制文字的方式很簡單。只有下面的幾個(gè)API
// 設(shè)置字體和對(duì)齊方式
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.font = fontSize + 'px Verdana';// 填充或者描邊文字
ctx.fillText(text, x, y);
ctx.strokeText(text, x, y);
有了這幾個(gè)API,我們可以制作一個(gè)代碼雨效果,下面是實(shí)現(xiàn)的代碼。
// 文字繪制函數(shù)function draw() {// 逐行繪制文字,繪制每行文字之前先繪制一層淺淺的遮罩來降低原有文字的不透明度// 每次繪制文字之前ctx.fillStyle = '#ffffff20';ctx.fillRect(0, 0, canvas.width, canvas.height);ctx.fillStyle = 'green';for(let i = 0; i < col; i++) {// 隨機(jī)獲取一個(gè)字符,并繪制,每列繪制一個(gè)// rows數(shù)組存儲(chǔ)的是要繪制的文字的y坐標(biāo),有多少列就有多少項(xiàng)ctx.fillText(getRandomChar(), i * fontSize, rows[i]);// 當(dāng)文字的y坐標(biāo)同時(shí)滿足兩項(xiàng)時(shí)才將文字繪制的y坐標(biāo)清0if(rows[i] > canvas.height && Math.random() > 0.99) {rows[i] = 0;} else {rows[i] += fontSize;}}}// 使用定時(shí)器頻繁繪制,這里不使用requestAnimationFrame的原因是幀率太高。setInterval(() => {draw()}, 50);draw();// 輔助函數(shù),獲取一個(gè)隨機(jī)字符function getRandomChar() {const chars = '0123456789qwertyuiopasdfghjklzxcvbnm'.split('');const index = Math.floor( Math.random() * chars.length );return chars[index];}
圖片繪制–魔棒效果
圖像相關(guān)的CanvasAPI如下:
ctx.drawImage(img, x, y); // 在canvas中繪制圖像
ctx.getImageData(x, y, width, height); // 獲取Canvas的像素信息
ctx.putImageData(imageData, x, y); // 根據(jù)像素信息繪制Canvas
imageData.data.set(greenColor, i); // 設(shè)置Canvas的像素信息
canvas不僅可以將圖片繪制出來,甚至還可以拿到像素點(diǎn)的信息。
function init() {const img = new Image();img.src = '';// 加載完成時(shí)間,當(dāng)圖像加載完成后執(zhí)行img.onload((e) => {// 設(shè)置canvas尺寸與圖片一致canvas.width = img.width;canvas.height = img.height;ctx.drawImage(img, 0, 0); // 在Canvas中繪制圖片})
}canvas.addEventListener('click', (e) => {// 獲取用戶點(diǎn)擊的位置在Canvas中的坐標(biāo);const x = e.offsetX;const y = e.offsetY;// 取出Canvas的像素信息,這是一個(gè)大數(shù)組,每四項(xiàng)為一組,分別代表一個(gè)像素點(diǎn)的RGBA值const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
})
如果我們想做到點(diǎn)擊后修改圖片中的顏色,那我們需要先定義一些輔助函數(shù),下面的兩個(gè)輔助函數(shù)能幫助我們更方便的查找Canvas中的顏色。
// 坐標(biāo)轉(zhuǎn)下標(biāo)
function point2Index() {return (y * canvas.width + x) * 4;
}// 根據(jù)坐標(biāo)獲取像素點(diǎn)信息
function getColor(x, y, imageData) {const i = point2Index(x, y);return [imageData[i],imageData[i+1],imageData[i+2],imageData[i+3]]
}
如果我希望我點(diǎn)擊的位置變?yōu)榫G色,那我們可以修改對(duì)應(yīng)位置的RGBA值,然后將新的像素信息交給Canvas去繪制。
canvas.addEventListener('click', (e) => {// 獲取用戶點(diǎn)擊的位置在Canvas中的坐標(biāo);const x = e.offsetX;const y = e.offsetY;// 取出Canvas的像素信息,這是一個(gè)大數(shù)組,每四項(xiàng)為一組,分別代表一個(gè)像素點(diǎn)的RGBA值const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);const clickColor = getColor(x, y, imageData);const greenColor = [0, 255, 0, 255]function _changeColor(x, y) {const i = point2Index(x, y);// 設(shè)置ImageData中的值imageData.data.set(greenColor, i);}_changeColor(x, y);// 上面修改的只是內(nèi)存中的像素點(diǎn)信息,通過putImageData將數(shù)組應(yīng)用到Canvas中。ctx.putImageData(imageData, 0, 0);
})// 因?yàn)樾枰啻潍@取Canvas中的像素信息,瀏覽器發(fā)出了警告,建議我們加上一個(gè)配置
const ctx = canvas.getContext('2d', {willReadFrequently: true // 告訴瀏覽器將會(huì)頻繁的讀取像素信息,需要做相應(yīng)優(yōu)化
})
需求還可以進(jìn)一步提升,例如我需要像PS的魔棒一樣,點(diǎn)擊后,相鄰相似的顏色都會(huì)改變。
我們只需要改寫一下 _changeColor()
函數(shù),遞歸得調(diào)用它即可。
function _changeColor() {// 如果超出邊界,停止遞歸if(x<0 || x>canvas.width || y<0 || y>canvas.height) {return;}const color = getColor(x, y, imageData);// 如果顏色差值較大,則停止遞歸if(diffColor(color, clickColor) > 100) {return;}// 已經(jīng)被改為綠色,不再更改if(diffColor(color, greenColor) === 0) {return;}const i = point2Index(x, y);// 設(shè)置ImageData中的值imageData.data.set(greenColor, i);// 遞歸調(diào)用,如果圖片比較大,遞歸可能會(huì)棧溢出,可以改用循環(huán)來寫。_changeColor(x + 1, y);_changeColor(x - 1, y);_changeColor(x, y + 1);_changeColor(x, y - 1);
}// 輔助函數(shù),返回兩個(gè)函數(shù)的顏色差值
function diffColor(color1, color2) {return Math.abs(color1[0] - color2[0]) + Math.abs(color1[1] - color2[1]) + Math.abs(color1[2] - color2[2]) + Math.abs(color1[3] - color2[3])
}
繪制和拖拽
有了前面的Canvas基礎(chǔ),可以來做兩個(gè)綜合案例。
下面制作一個(gè)使用畫筆圖板和一個(gè)圖形畫板,Canvas可以做的效果很多,主要是制作的思路。