canvas 已經出來很久了,相信你們多少都有接觸。html
若是你是前端頁面開發/移動開發,那麼你確定會有作過圖片上傳處理,圖片優化,以及圖片合成,這些都是能夠用 canvas 實現的。前端
若是你是作前端遊戲開發的,可能會很是熟悉,或者說對幾何和各類圖形變化很是瞭解。node
這裏我介紹的是簡單的、基本的,可是很是徹底的一個 2d 的 canvas 案例。npm
基本上了解了這些,全部的 canvas 中的 2d 變化基本均可以會了。canvas
先來一個截圖看看效果:api
如上面所看,能夠總結出幾個功能點:session
一、添加多張圖片或者文字到 canvas 。( 這裏沒有添加文字,咱們能夠先把文字利用canvas轉爲圖片,而後添加 canvas 上 )app
二、圖片的縮放,根據選擇不一樣的點實現不一樣縮放函數
三、圖片移動,改變圖片在 canvas 的中心位置測試
四、圖片旋轉,根據旋轉點在移動的角度進行旋轉
五、圖片選擇,兩種方式:一種根據圖片的位置,肯定當前選擇的圖形,第二種是點擊列表選擇
六、數據的保存,提供了保存按鈕,保存圖形的位置和大小以及旋轉角度
七、初始化數據,經過以前保存的數據,從新繪製。
代碼案例介紹:
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 包沒有測試,本地的能夠實現各類方法了。若有問題能夠留言。。
在開發中咱們須要不少關於平面幾何的知識來處理咱們的操做,例如:
一、肯定某個點是否在矩形內 : 用於肯定點擊時候選中的圖形
二、計算向量的角度 : 用於處理旋轉
三、計算某個向量在另外一個向量上面的距離 : 用於旋轉以後,的移動距離計算
四、某點繞道某點旋轉必定角度的點 : 用於肯定旋轉後的點的位置
是否是腦子裏浮現了不少高中初中的數學幾何公式。
若是沒有,百度下吧,都是不少有意思的公式,讓本身重溫下高中數學,回憶一下高中。
證實一下本身學太高中數學。
代碼設計/簡要開發介紹:
如何開始這個功能的開發呢?
一、首先建立一個 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(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() { 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() { 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 數據。用於存儲數據和保存
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 實例進行繪製。
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 方法重繪製。
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 : 判斷時候在對應的操做點上,並返回對應的操做點名稱
http://www.javashuo.com/article/p-zhpspoqe-bk.html