演示地址
PC端的項目啦,須要在電腦上看哦,並且最好用Chrome打開html
這是今年三月份幫學長作的一個項目,陪我度過了兩個月的春招生活,整個項目作下來也是學到了不少東西,下面就開始個人分享啦,包括一些知識點總結和遇到的坑,dalao莫笑哈。前端
代碼是基於vue-cli碼的,因此路由、vuex這些都不用講啦,咱們把重點放在canvas上面吧。vue
這裏的拖拽是指把左邊工具欄裏的圖形圖形拖拽到右邊畫布裏,三步完成:html5
draggable="true"
;dragstart
drag
dragend
,分別對應拖拽開始、拖拽中和拖拽結束,若是你但願在這些過程加上特效,能夠試試,但更多的仍是用做響應數據,好比讓畫布知道具體是哪一個元素被拖拽進來了;dragover
drop
兩個事件,分別表示被拖拽元素在該元素範圍內移動、被拖拽元素着陸,這裏注意dragover
事件函數內需設置event.preventDefault()
防止彈出新頁面,而後咱們就能夠愉快地在drop
事件函數裏畫圖形到畫布上啦。因爲設計圖上顏色都沒有透明度,因此咱們須要手動加一個0.3的alpha,否則畫布上圖形相互層疊,會覆蓋掉層級低的圖形和背景圖。git
function hex2rgba(hex) {
// hex格式如#ffffff
let colorArr = [];
for(let i = 1; i<7; i += 2){
colorArr.push(parseInt("0x" + hex.slice(i,i+2))); // 16進制值轉10進制
}
return `rgba(${colorArr.join(",")},0.3)`;
}
複製代碼
另外若是有興趣瞭解RGBA轉RGB的小夥伴,能夠看看這篇博客RGBA轉換成RGBgithub
下面就是關於canvas的內容了,若是對它的基礎用法還不太瞭解的小夥伴,能夠看看JavaScript之Canvas畫布vuex
save
能夠保存當前canvas的狀態,包括strokeStyle
、fillStyle
、變換矩陣、剪切區域等,restore
能夠恢復到canvas狀態棧中的上一個狀態,因此咱們在這兩個函數中間作的canvas狀態改變至關於被隔離起來了,不會污染外部的canvas操做。vue-cli
這樣看來,咱們最好在每次畫圖前調用save
,畫完後調用restore
,從而保證每次繪製都有一個純粹的狀態。redux
這裏有一篇講得特別好的文章,若是嫌本直男沒講清楚的話,必定要看哦。Canvas學習:save()和restore()canvas
可能有些小夥伴會小看這個API,認爲它只能繪製圖片,實際上它還能svg、canvas繪製到畫布上,咱們先來看看如何繪製svg咯。
咱們功能界面左側工具欄裏的圖標其實都是svg,我一開始是想把他們截圖下來切成一個個背景透明的png,而後畫到canvas上,後來發現放大看的話會比較模糊,畢竟是像素圖嘛,因此新的需求來了。
我本身的代碼很差貼出來,那就看看dalao的吧,將 DOM 對象繪製到 canvas 中,他這裏是將DOM塞到svg裏再往canvas上畫的,若是你只須要畫現成的svg,則能夠不用foreignObject
包裹。
另外,若是你的svg有.svg格式圖片,能夠直接調用drawImage
去繪製。
canvas已經有畫橢圓的API了,但兼容性還不夠好,在其餘全部模擬繪製橢圓的方式裏,貝塞爾曲線能夠說是最優雅的一種了,好吧,掃盲文 => 貝塞爾曲線原理(簡單闡述)
三維貝塞爾曲線須要一個起始點、兩個中間點、一個終止點肯定,固然起始點通常默認當前點,因此bezierCurveTo
的參數就是按順序的後三個點座標了;當這四個點剛好圍成一個矩形時,就有點橢圓的模樣啦。
let a = this.width / 2;
let b = this.height / 2;
let ox = 0.5 * a,
oy = 0.6 * b;
this.ctx.beginPath();
// 從橢圓縱軸下端開始逆時針方向繪製
this.ctx.moveTo(0, b);
// 把橢圓劃成四份分開來畫
this.ctx.bezierCurveTo(ox, b, a, oy, a, 0);
this.ctx.bezierCurveTo(a, -oy, ox, -b, 0, -b);
this.ctx.bezierCurveTo(-ox, -b, -a, -oy, -a, 0);
this.ctx.bezierCurveTo(-a, oy, -ox, b, 0, b);
this.ctx.closePath();
this.ctx.fill();
複製代碼
這裏有一篇整理得比較完整的橢圓繪製方法的文章 能夠參考 HTML5 Canvas中繪製橢圓的5種方法
實線好畫,可是箭頭怎麼來作呢?Emmm,其實就是計算線段與畫布x軸的夾角,而後在線段終點畫偏移對應角度的三角形嘛
drawArrow(x1, y1, x2, y2) {
// (x1, y1)是線段起點 (x2, y2)是線段終點
// 反正切函數計算夾角
let endRadians = Math.atan((y2 - y1) / (x2 - x1));
// 三角形的底邊與線段垂直,因此還要再轉 π / 2
endRadians += ((x2 >= x1) ? 90 : -90) * Math.PI / 180;
this.ctx.save();
this.ctx.beginPath();
// 座標原點 => (x2, y2)
this.ctx.translate(x2, y2);
this.ctx.rotate(endRadians);
this.ctx.moveTo(0, 0);
this.ctx.lineTo(5, 15);
this.ctx.lineTo(-5, 15);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
}
複製代碼
setLineDash
便可指定虛線的樣式,詳見Canvas學習:繪製虛線和圓點線通常常見的波浪線都是用正弦曲線來模擬的吧,y = A * sin(ω * x + φ),指定它的A和ω就能夠肯定波浪線的振幅和頻率(或者說每一個波浪的高度和寬度)
let len = Math.sqrt(width * width + height * height);
this.ctx.save();
this.ctx.moveTo(this.start.x,this.start.y); // 起點
this.ctx.translate(this.start.x,this.start.y);
this.ctx.beginPath();
let x = 0;
let y = 0;
let amplitude = 5; // 振幅
let frequency = 5; // 頻率
while (x < len) {
y = amplitude * Math.sin(x / frequency);
this.ctx.lineTo(x, y);
x = x + 1;
}
this.ctx.stroke();
this.ctx.restore();
複製代碼
參考文章:Draw a Sine Wave in JavaScript
簡單來講,咱們畫布上的圖形都是一個類的實例,保存在一個數組中,每次有更新時都會清除畫布,再所有從新繪製一遍(後面會將優化)。這個圖形實例須要保存的屬性通常有起始和終點座標、顏色、偏移角度等,根據本身的需求設置,還至少須要一個方法去動態計算該圖形的有效範圍,以便鼠標事件找到它。
選中某圖形實例後,從圖形棧數組中刪除便可。
因爲咱們每次畫圖形的時候,都會把座標原點暫時移到圖形的中心,因此只須要rotate
一個角度再畫就能夠實現旋轉啦
Emmm,每一個圖形不太同樣,有興趣的話看看項目源碼唄
function inRange(x, y, points){
// points表示多邊形的頂點集合
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
let xi = points[i][0], yi = points[i][1];
let xj = points[j][0], yj = points[j][1];
let intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
複製代碼
相似PS的功能嘛,我這個項目沒作,可是思路不難,用past、present、future三個數組來保存圖形棧,Emm好像講起來仍是有點長,能夠參考實現撤銷歷史的思路。
圖形棧裏的實例被依次取出繪製,後畫上去的圖形會覆蓋掉以前的圖形,因此這裏涉及到一個優先級,重要的東西放在後面畫。
咱們能夠把保存圖形的數組再細分類,數組的每一個子元素都是一個Array,專門保存某一種圖形,優先級越高,對應的索引值越大,這樣咱們就能夠把重要的圖形所有放在後面畫了。
通常咱們用於雙向綁定的值都會放在vue實例的data
中,由於它默認提供了getter
和setter
;但vuex的狀態通常都須要computed
來讀取,但computed
默認是沒有setter方法的,須要手動設置,代碼以下:
computed:{
text : {
get(){
return this.$store.state.text;
},
set(value){
this.$store.commit('setText',value);
}
}
}
複製代碼
在實現保存圖片功能的時候,我但願能截取一段DOM的內容,而不只僅是canvas的內容,因此找到了這個插件html2canvas,它能夠把dom轉換成canvas,而後咱們就能canvas.toDataURL()
把它轉換成圖片了。
轉換並保存成圖片下載的代碼以下:
downImg() {
html2canvas( this.$refs.ground, {
onrendered: function(canvas) {
let url = canvas.toDataURL();
let a = document.createElement('a');
a.href = url;
a.download = new Date() + ".png";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
});
}
複製代碼
可是出現了一個bug,就是下載下來的圖片不清晰,左上角一大片空白。
因而我嘗試了網上的不少方法,都行不通,最後只能把項目從零開始慢慢加東西,最後發現是我畫虛線的時候改了CanvasRenderingContext2D的原型,我滴媽耶,作夢也沒想到會是這裏出問題,用插件有風險啊。
若是上傳到https://XXX.github.io/(GitHub的我的博客)上,則跟上傳到服務器上操做一致,但若是是傳到某個倉庫的gh-pages,那麼一堆問題都來了,解決步驟以下:
.gitignore
文件裏的/dist
刪掉,忽略了的話,還怎麼上傳打包文件到master分支呢;/config/index.js
裏build部分裏的assetsPublicPath
由'/'改爲'./',至關於說把服務器根目錄改爲了相對路徑,倉庫gh-pages的根目錄不是'/'而是'/倉庫名';static
裏的圖片,使用了絕對路徑,可能上傳後顯示不出來;git subtree push --prefix dist origin gh-pages
敲完命令,應該就能夠看到上傳成功了。上面提到,咱們的畫布每次更新時,老是要所有清除,而後從新再畫一遍,對於那些背景圖片等不變的內容來講,是否是能夠優化呢?Emmm,好尬的設問句。
咱們用多個一樣大小層疊的canvas來完成,層級低的下層canvas用來畫背景圖片等靜態圖形,層級高的上層canvas用來畫動態變化的圖形,這樣就能夠每次渲染都優化一點啦。
當咱們在畫布上拖拽圖形時,通常作法是隨着鼠標移動mousemove
,從新繪製全部圖形,但其實這個過程當中,要繪製的能夠分爲兩部分,一個是被拖拽移動的圖形,另外一個就是其餘圖形;咱們能夠分別動態建立兩個canvas,把兩部分畫在兩個離屏畫布上,mousemove
時只要調用兩次drawImage(離屏canvas)便可,這樣是否是性能又花了不少呢
代碼地址 雖然代碼質量差,我本身都不忍直視,但仍是放出來吧,萬一哪裏看不懂了還能夠翻翻源碼嘛