1、介紹
canvas 已經出來好久了,相信大家多少都有接觸。
如果你是前端頁面開發/移動開發,那么你肯定會有做過圖片上傳處理,圖片優化,以及圖片合成,這些都是可以用 canvas 實現的。
如果你是做前端游戲開發的,可能會非常熟悉,或者說對幾何和各種圖形變化非常了解。
這里我介紹的是簡單的、基本的,但是非常完全的一個 2d 的 canvas 案例。
基本上了解了這些,所有的 canvas 中的 2d 變化基本都可以會了。
先來一個截圖看看效果:

如上面所看,可以總結出幾個功能點:
1、添加多張圖片或者文字到 canvas 。( 這里沒有添加文字,我們可以先把文字利用canvas轉為圖片,然后添加 canvas 上 )
2、圖片的縮放,根據選擇不同的點實現不同縮放
3、圖片移動,改變圖片在 canvas 的中心位置
4、圖片旋轉,根據旋轉點在移動的角度進行旋轉
5、圖片選擇,兩種方式:一種根據圖片的位置,確定當前選擇的圖形,第二種是點擊列表選擇
6、數據的保存,提供了保存按鈕,保存圖形的位置和大小以及旋轉角度
7、初始化數據,通過之前保存的數據,重新繪制。
代碼案例介紹:
html 代碼:
<canvas height="960" width="960" style="width: 100%;" id="test"></canvas> <div id="list"></div> <button id="save">保存</button>
js代碼是模塊形式開發的,並且傳到 npm 上面,可以自行下載並且有源碼:
yarn add xl_canvas
代碼調用和實現:
import Canvas from 'xl_cnavas';
const dataCa = sessionStorage.getItem('test_tst_111');
const canvas = new Canvas({
canvas: 'test',
target: 'test',
list: 'list',
height: 960,
width: 960,
data: dataCa?JSON.parse(dataCa):[],
});
document.getElementById('save').addEventListener('click', () => {
sessionStorage.setItem('test_tst_111',
JSON.stringify(canvas.save()));
});
// canvas.addPhoto('https://cdn.eoniq.co/spree/images/283205/desktop/CI-26-LS_b6bb28a3914ae9caa651abbddb548054.jpg?1533196945');
// canvas.addPhoto('http://www.runoob.com/wp-content/uploads/2013/11/img_the_scream.jpg');
npm 包沒有測試,本地的可以實現各種方法了。如有問題可以留言。。
2、項目開發
知識梳理:
在開發中我們需要很多關於平面幾何的知識來處理我們的操作,例如:
1、確定某個點是否在矩形內 : 用於確定點擊時候選中的圖形
2、計算向量的角度 : 用於處理旋轉
3、計算某個向量在另一個向量上面的距離 : 用於旋轉之后,的移動距離計算
4、某點繞道某點旋轉一定角度的點 : 用於確定旋轉后的點的位置
是不是腦子里浮現了很多高中初中的數學幾何公式。
如果沒有,百度下吧,都是很多有意思的公式,讓自己重溫下高中數學,回憶一下高中。
證明一下自己學過高中數學。
代碼設計/簡要開發介紹:
以下如果需要查看,最好下載源碼對照的查看
如何開始這個功能的開發呢?
1、首先創建一個 Canvas 類
constructor(options) {
this.options = options;
const {
canvas,
height,
width,
target,
before,
after,
data = [],
list = null,
} = this.options;
this.canvas = null; // 畫布
this.height = height; // 畫布的寬高
this.width = width;
this.target = target;
this.before = before;
this.after = after;
this.data = data;
this.layers = []; // 畫布的層
if (typeof canvas === 'string') {
this.canvas = document.getElementById(canvas);
} else {
this.canvas = canvas;
}
if (typeof target === 'string') {
this.target = document.getElementById(target);
} else {
this.target = target;
}
if (typeof list === 'string') {
this.list = document.getElementById(list);
} else {
this.list = list;
}
this.canvas.width = width;
this.canvas.height = height;
this.context = this.canvas.getContext('2d'); // 畫布對象
this.loaded = 0;
this.border = new Border(this);
this.current = null;
this.init();
this.initEvent();
}
這是 canvas 類的構造函數,這里接受有參數:
canvas : 傳入 canvas 對象或者當前 html 的元素的 id,以供整個功能的開發。
height / width : 寬和高,整個繪制過程中,寬和高都是這個為基准
target : 這個是用來接受事件的元素。這個應該和 canvas 對象的元素寬高相等
before / after :當初始化數據到時候,會知道初始化數據之前操作和初始化之后操作
data : 繪制的數據
重要的屬性:
layers :添加到畫布的圖形,類似圖層。
context : canvas 的上下文,用來繪制的 api 集合
border : 繪制的骨架,當選中某一個圖形的時候,會出現外層的骨架。( 這個單獨創建一個類 )
current :當前的圖形,也可以理解為當前的圖層。
主要方法介紹(介紹幾個重要的):
addPhoto 方法:
// 添加圖片
addPhoto(image) {
if (typeof image === 'string') {
this.loaded += 1;
const lyr = new Photo(image, this, () => {
setTimeout(() => {
this.loaded -= 1;
if (this.loaded < 1) {
this.draw();
}
}, 100);
});
this.layers.push(lyr);
this.addItem(image, lyr.id);
} else {
const lyr = new Photo(image, this);
this.layers.push(lyr);
this.addItem(image, lyr.id);
this.draw();
}
}
這里是添加 Photo 的方法,其中 photo 是用 Photo 類創建實例的。
可以先看一下下面介紹的 Photo 類,可以更好了解開發過程。
draw方法(用來觸發繪制):
draw() {
this.clear();
this.layers.forEach((item) => {
if (typeof item === 'function') {
item.apply(null, this.context, this.canvas);
} else {
item.draw();
}
});
if (this.current) {
this.border.refresh(this.current.rect);
}
}
上面代碼是來繪制 layers 的圖層到 canvas 上。
這里會判斷 layers 中是否是圖層,如果是圖層才會繪制圖層
如果不是,就會直接執行方法,該方法傳入的當前的 canvas 這個實例。
也可以繪圖案到 canvas 上,這樣就可以實現層級關系。
上面做了一個判斷,就是是否繪制 border ,在有選中的情況下會繪制 骨架的
即 調用 border 的 refresh 方法。在這里可以先去看看 Border 類。( 下面有介紹 )
initEvent 方法(用於綁定方法):
initEvent() {
this.target.addEventListener('mousedown', (e) => {
let p_x = e.pageX;
let p_y = e.pageY;
const position = getDocPosition(this.target);
const scale = this.width / this.target.offsetWidth;
const point = [
(p_x - position.x) * scale,
(p_y - position.y) * scale,
];
const status = this.selectPhoto(point);
if (status) {
const move = (event) => {
const m_x = event.pageX;
const m_y = event.pageY;
const vector = [(m_x - p_x) * scale, (m_y - p_y) * scale];
if (status === 1) {
this.current.rect.translate(vector);
} else if (status === 'r_point') {
const e_point = [(m_x - position.x) * scale, (m_y - position.y) * scale];
const angle = Canvas.getAngle(
this.current.rect.center,
this.border.r_point,
e_point,
);
if (!isNaN(angle)) {
this.current.rect.rotate(angle);
} else {
return;
}
} else {
this.current.rect.zoom(status, vector);
}
this.draw();
p_x = m_x;
p_y = m_y;
};
this.target.addEventListener('mousemove', move);
this.target.addEventListener('mouseup', () => {
this.target.removeEventListener('mousemove', move);
});
}
});
this.list.addEventListener('click', (e) => {
if (e.target && e.target.nodeName.toUpperCase() === 'IMG') {
const id = parseInt(e.target.getAttribute('data-id'));
this.layers.forEach((item, index) => {
if (item.id === id) {
this.chooseItem(index);
}
});
}
});
}
這個是給 target 對象綁定事件,通過對事件的不同處理來就觸發不同的方法。
都是直接改變當前的 current 上面的 rect 數據,然后重新繪制。
圖形的選取 : selectPhoto 方法調用,當選中的時候就會設置當前的 current 的圖層
圖形的移動 : move 方法調用,移動圖層
圖形的縮放 : zoom 方法調用,接受不同的縮放形式
圖形的旋轉 : rotate 方法調用,接受角度進行旋轉
其他的方法:

addItem : 向 list 元素對象中添加元素
selectPhoto :判斷當前的位置確定選中的 Photo
chooseItem :用於 list 元素中的選取
clear : 清楚 canvas 畫布
save : 返回 rect 數據。用於存儲數據和保存
2、Photo 類
constructor(image, canvas, load) {
this.canvas = canvas;
this.img = image;
this.load = load;
this.id = new Date().getTime();
this.isLoad = false;
if (image.rect) {
this.options = image;
this.img = this.options.img;
this.id = this.options.id;
}
this.pre();
}
還是看構造函數,介紹屬性和方法:
canvas : 就是相當於繼承來的,或者是說 canvas 要全局使用
image :可能是對象,也可以能是 資源地址,但是大多數應該是資源地址
id : photo 的 id,用於查找和選擇等
rect :這個是重要的,photo 的數據,如:坐標/寬高/角度等
稍后介紹 rect 類,先介紹下 photo 的方法:
用於創建 rect 的init方法:
init() {
if (this.load) this.load();
if (this.options) {
const {
width, height, center, angle,
} = this.options.rect;
this.rect = new Rect(width,
height, [center[0], center[1]], angle);
return;
}
this.rect = new Rect(this.image.width,
this.image.height, [this.canvas.width / 2, this.canvas.height / 2], 0);
}
每次 new Photo 都會創建了一個 ract 實例,作為它的數據存儲 this.rect 。
每次創建一個 Photo 的時候並且加入到 canvas 的 layers 中的時候並沒有開始繪圖
繪圖需要調用 Photo 的 draw 方法來觸發,如下:
draw() {
const { image, canvas, rect } = this;
const { context } = canvas;
const points = rect.point;
const [c_x, c_y] = rect.center;
context.save();
context.translate(c_x, c_y);
context.rotate(rect.angle);
context.drawImage(image, 0, 0, image.width, image.height,
points[0][0] - c_x,
points[0][1] - c_y,
rect.width,
rect.height);
context.restore();
}
在 canvas 實例調用 draw 方法時候,會一次繪制 layers 中的所有 photo 實例進行繪制。
3、rect 類
constructor(width, height, center, angle) {
this.height = height;
this.width = width;
this.center = center;
this.angle = angle;
this.point=[]
this.getPoint();
}
這里是通過傳入 width / height /center / angle 來確定和初始化 photo 在 canvas 上的輸出。
height / width : 這是圖形的寬高
center : 圖形的中間位置
angle :很顯熱,是圖形旋轉的角度
point : 四個頂點的位置
一個圖形,有了這個寫數據,基本上能在 canvas 確定位置、大小以及各種形變。
rect 實例的方法:

代碼有點多,就簡要介紹吧!
我們的操作實際上都是操作 rect 的數據。
一些判斷也是於 rect 數據做對比,或者計算 rect 對象里面的數據。
rotate : 旋轉后 rect 的頂點位置的計算
translate : 移動后中點位置計算和頂點位置計算
zoom : 縮放后頂點和中點的位置計算
isPointInRect : 是否在 Rect 的四個頂點里面
Rect 的類基本介紹完畢了。每次改變后調用 canvas 的 draw 方法重繪制。
4、Border 類
查看這個類最好先瀏覽下 rect 類 和 Photo 類
constructor(canvas) {
this.canvas = canvas;
}
這里創建只是獲取到了全局的 canvas 實例。用於后面調用
refresh 方法:
refresh(rect) {
this.rect = rect;
this.point = this.rect.point;
// 中點
this.c_point = [];
this.point.reduce((a, b) => {
this.c_point.push([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
return b;
}, this.point[3]);
// 旋轉點
this.r_point = [(this.point[0][0] + this.point[1][0]) / 2,
this.point[0][1] - 35];
this.draw();
}
這里是接受 rect 的數據,
然后通過 rect 數據,得到頂點 / 各個線上的中點 / 旋轉點
調用 refresh 之后就會執行 draw 方法:
draw() {
const {
point,
center,
angle,
width,
height,
} = this.rect;
const { context } = this.canvas;
const [c_x, c_y] = center;
const points = point;
context.save();
context.translate(c_x, c_y);
context.rotate(angle);
context.beginPath();
context.lineWidth = '2';
context.strokeStyle = '#73BFF9';
context.rect(points[0][0] - c_x,
points[0][1] - c_y,
width,
height);
const pointList = points.concat(this.c_point);
pointList.push(this.r_point);
pointList.forEach((item) => {
const [x, y] = item;
context.fillStyle = '#73BFF9';
context.fillRect(x - 6 - c_x, y - 6 - c_y, 12, 12);
});
context.moveTo((points[0][0] + points[1][0]) / 2 - c_x,
points[0][1] - c_y);
context.lineTo(this.r_point[0] - c_x, this.r_point[1] - c_y);
context.stroke();
context.closePath();
context.restore();
}
可以看到這里是繪制,並且繪制都是依賴 rect 的數據。
所以我們並不需要處理旋轉 / 移動 / 縮放等操作,因為每次修改后 rect 數據就會變。

isPointInSkeletion : 判斷時候在對應的操作點上,並返回對應的操作點名稱
介紹完畢,簡要的介紹開發的設計和流程。如需諒解,請看看源碼。。
https://www.cnblogs.com/jiebba/p/9667600.html
我的博客 : XiaoLong's Blog
博客園小結巴巴: https://www.cnblogs.com/jiebba
