canvas图形编辑器
??原文地址:http://jeffzhong.space/2017/11/02/drawboard/
??使用canvas進(jìn)行開發(fā)項(xiàng)目,我們離不開各種線段,曲線,圖形,但每次都必須用代碼一步一步去實(shí)現(xiàn),顯得非常麻煩。有沒有一種類似于PS,CAD之類的可視化工具,繪制出基本的圖形,然后輸出代碼。之后我們就可以在這個(gè)生成的圖形場景的基礎(chǔ)上去實(shí)現(xiàn)功能,那將是多么的美妙的事啊。話不多說,我們來實(shí)現(xiàn)一個(gè)圖形編輯器吧?。
主要實(shí)現(xiàn)如下的功能:
實(shí)際效果: drawboard(推薦在chrome或safari下運(yùn)行)
功能點(diǎn)包括:
使用方式:
該項(xiàng)目用到的知識點(diǎn)包括:
工具欄
??首先我們實(shí)現(xiàn)如圖所示的工具欄,也就是基本的html/css,使用了flex布局,同時(shí)使用了html5的color, range, number標(biāo)簽,其它都是普通的html和css代碼。主要注意的地方就是如下用純css實(shí)現(xiàn)選擇效果
??其中多邊形邊數(shù)選擇范圍控制為:3-20,當(dāng)然我們也可以擴(kuò)大為無限大的邊數(shù),但實(shí)際應(yīng)用到的情況比較少。多角星情況類型,范圍控制為3~20。
??然后對線條粗細(xì),描邊顏色,填充顏色顯示信息,也就是onchang事件觸發(fā)時(shí)獲取value值,再顯示出來。顯示鼠標(biāo)當(dāng)前的位置功能也非常簡單,在此也略過不表。
圖形基類
??開始實(shí)現(xiàn)畫板的功能,第一步,實(shí)現(xiàn)圖形基類,這個(gè)是最重要的部分。因?yàn)椴还苁蔷€條,多邊形都會繼承該類。
??注意:isPointInPath非常有用,就是這個(gè)api實(shí)現(xiàn)鼠標(biāo)是否選中的功能了,它的原理就是調(diào)用上下文context繪制路徑,然后向isPointInPath傳遞位置(x,y)信息,該api會返回這個(gè)點(diǎn)是否在繪制路徑上,相當(dāng)于繪制的是隱形的路徑進(jìn)行判斷點(diǎn)是否在該路徑或圖形內(nèi)部,這也是我要把繪制路徑和渲染的功能分離開的原因。
??具體的功能還是直接看代碼吧
class Graph{//初始化圖形需要用到的屬性,位置,頂點(diǎn)列表,邊的寬度,描邊顏色,填充顏色,是否填充;constructor(pos){this.x=pos.x;this.y=pos.y;this.points=[];this.sides=5;this.stars=5;this.lineWidth=1;this.strokeStyle='#f00';this.fillStyle='#f00';this.isFill=false;}//實(shí)現(xiàn)繪制時(shí)的拖拽initUpdate(start,end){this.points[1]=end;this.x=(start.x end.x)/2;this.y=(start.y end.y)/2;}//實(shí)現(xiàn)修改模式下的拖拽頂點(diǎn)和控制點(diǎn)update(i,pos){if(i==9999){var that=this,x1=pos.x-this.x,y1=pos.y-this.y;this.points.forEach((p,i)=>{that.points[i]={x:p.x x1, y:p.y y1 };});this.x=Math.round(pos.x);this.y=Math.round(pos.y);} else {this.points[i]=pos;var x=0,y=0;this.points.forEach(p=>{x =p.x;y =p.y;});this.x=Math.round(x/this.points.length);this.y=Math.round(y/this.points.length);}}//繪制路徑createPath(ctx){ctx.beginPath();this.points.forEach((p,i)=>{ctx[i==0?'moveTo':'lineTo'](p.x,p.y);});ctx.closePath();}//判斷鼠標(biāo)是否選中對應(yīng)的圖形,選中哪個(gè)頂點(diǎn),選中哪個(gè)控制點(diǎn),中心點(diǎn);isInPath(ctx,pos){for(var i=0,point,len=this.points.length;i<len;i ){point=this.points[i];ctx.beginPath();ctx.arc(point.x,point.y,5,0,Math.PI*2,false);if(ctx.isPointInPath(pos.x,pos.y)){return i;}}this.createPath(ctx);if(ctx.isPointInPath(pos.x,pos.y)){return 9999;}return -1}//繪制控制點(diǎn)drawController(ctx){this.drawPoints(ctx);this.drawCenter(ctx);}//繪制頂點(diǎn)drawPoints(){ctx.save();ctx.lineWidth=2;ctx.strokeStyle='#999';this.points.forEach(p=>{ctx.beginPath();ctx.arc(p.x,p.y,5,0,Math.PI*2,false);ctx.stroke();});ctx.restore();}//繪制中心點(diǎn)drawCenter(ctx){ctx.save();ctx.lineWidth=1;ctx.strokeStyle='hsla(60,100%,45%,1)';ctx.fillStyle='hsla(60,100%,50%,1)';ctx.beginPath();ctx.arc(this.x,this.y,5,0,Math.PI*2,false);ctx.stroke();ctx.fill();ctx.restore();}//繪制整個(gè)圖形draw(ctx){ctx.save();ctx.lineWidth=this.lineWidth;ctx.strokeStyle=this.strokeStyle;ctx.fillStyle=this.fillStyle;this.createPath(ctx);ctx.stroke();if(this.isFill){ ctx.fill(); }ctx.restore();}//生成代碼createCode(){var codes=['// ' this.name];codes.push('ctx.save();');codes.push('ctx.lineWidth=' this.lineWidth);codes.push('ctx.strokeStyle=\'' this.strokeStyle '\';');if(this.isFill){codes.push('ctx.fillStyle=\'' this.fillStyle '\';');}codes.push('ctx.beginPath();');codes.push('ctx.translate(' this.x ',' this.y ');')//translate到中心點(diǎn),方便使用this.points.forEach((p,i)=>{if(i==0){codes.push('ctx.moveTo(' (p.x-this.x) ',' (p.y-this.y) ');');// codes.push('ctx.moveTo(' (p.x) ',' (p.y) ');');} else {codes.push('ctx.lineTo(' (p.x-this.x) ',' (p.y-this.y) ');');// codes.push('ctx.lineTo(' (p.x) ',' (p.y) ');');}});codes.push('ctx.closePath();');codes.push('ctx.stroke();');if(this.isFill){codes.push('ctx.fill();');}codes.push('ctx.restore();');return codes.join('\n');}}直線
??實(shí)現(xiàn)直線功能相當(dāng)簡單,繼承基類,只需要重寫draw和createCode方法,拖拽和變換等功能都已經(jīng)在基類實(shí)現(xiàn)了。
??還有就是虛線功能了,其實(shí)就是先繪制一段直線,然后空出一段空間,接著再繪制一段直線,如此類推。小伙伴可以思考一下怎么實(shí)現(xiàn),這個(gè)和直線所涉及的知識點(diǎn)相同,代碼就略過了。
貝塞爾曲線
??接著就是貝塞爾曲線的繪制了,首先繼承直線類,曲線比直線不同的是除了起始點(diǎn)和結(jié)束點(diǎn),它還多出了控制點(diǎn),2次貝塞爾曲線有一個(gè)控制點(diǎn),3次貝塞爾曲線則有兩個(gè)控制點(diǎn)。所以對應(yīng)初始化拖拽,頂點(diǎn)繪制的方法必須重寫,以下是3次貝塞爾曲線的代碼。
至于貝塞爾2次曲線功能類似,同時(shí)也更加簡單,代碼也略過。
多邊形
??實(shí)現(xiàn)任意條邊的多邊形,大家思考一下都會知道如何實(shí)現(xiàn),平均角度=360度/邊數(shù),不是嗎?
??在知道中點(diǎn)和第一個(gè)頂點(diǎn)的情況下,第n個(gè)頂點(diǎn)與中點(diǎn)的角度 = n*平均角度;然后記錄下每個(gè)頂點(diǎn)的位置,然后依次繪制每個(gè)頂點(diǎn)的連線即可。這里用到了二維旋轉(zhuǎn)的公式,也就是繞圖形的中點(diǎn),旋轉(zhuǎn)一定的角度。
既然我們已經(jīng)記錄了每個(gè)頂點(diǎn)的位置,當(dāng)拖動對應(yīng)的頂點(diǎn)后修改該頂點(diǎn)位置,重新繪制,就可以伸縮成任意的圖案。
??難點(diǎn)是拖拽控制線,實(shí)現(xiàn)旋轉(zhuǎn)多邊形角度,和擴(kuò)大縮小多邊形。等比例擴(kuò)大縮小每個(gè)頂點(diǎn)與中點(diǎn)的距離即可實(shí)現(xiàn)等比例縮放多邊形,記錄第一個(gè)頂點(diǎn)與中點(diǎn)的角度變化即可實(shí)現(xiàn)旋轉(zhuǎn)功能,這里用到反正切Math.atan2(y,x)求角度;具體實(shí)現(xiàn)看如下代碼。
/*** 多邊形*/class Polygon extends Graph{constructor(pos){super(pos);this.cPoints=[];}get name(){return this.sides '邊形';}//生成頂點(diǎn)createPoints(start,end){var x1 = end.x - start.x,y1 = end.y - start.y,angle=0;this.points=[];for(var i=0;i<this.sides;i ){angle=2*Math.PI/this.sides*i;var sin=Math.sin(angle),cos=Math.cos(angle),newX = x1*cos - y1*sin,newY = y1*cos x1*sin;this.points.push({x:Math.round(start.x newX),y:Math.round(start.y newY)});}}//生成控制點(diǎn)createControlPoint(start,end,len){var x1 = end.x - start.x,y1 = end.y - start.y,angle=Math.atan2(y1,x1),c=Math.round(Math.sqrt(x1*x1 y1*y1)),l=c (!len?0:c/len),x2 =l * Math.cos(angle) start.x, y2 =l * Math.sin(angle) start.y;return {x:x2,y:y2};}initUpdate(start,end){this.createPoints(start,end);this.cPoints[0]=this.createControlPoint(start,end,3);}//拖拽功能update(i,pos){if(i==10000){//拖拽控制點(diǎn)var point=this.createControlPoint({x:this.x,y:this.y},pos,-4);this.cPoints[0]=pos;this.createPoints({x:this.x,y:this.y},point);} else if(i==9999){ //移動位置var that=this,x1=pos.x-this.x,y1=pos.y-this.y;this.points.forEach((p,i)=>{that.points[i]={x:p.x x1, y:p.y y1 };});this.cPoints.forEach((p,i)=>{that.cPoints[i]={x:p.x x1,y:p.y y1};});this.x=Math.round(pos.x);this.y=Math.round(pos.y);} else {//拖拽頂點(diǎn)this.points[i]=pos;var x=0,y=0;this.points.forEach(p=>{x =p.x;y =p.y;});this.x=Math.round(x/this.points.length);this.y=Math.round(y/this.points.length);}}createCPath(ctx){this.cPoints.forEach(p=>{ctx.beginPath();ctx.arc(p.x,p.y,6,0,Math.PI*2,false);});}isInPath(ctx,pos){var index=super.isInPath(ctx,pos);if(index>-1) return index;this.createCPath(ctx);for(var i=0,len=this.cPoints.length;i<len;i ){var p=this.cPoints[i];ctx.beginPath();ctx.arc(p.x,p.y,6,0,Math.PI*2,false);if(ctx.isPointInPath(pos.x,pos.y)){return 10000 i;break;}}return -1}drawCPoints(ctx){ctx.save();ctx.lineWidth=1;ctx.strokeStyle='hsla(0,0%,50%,1)';ctx.fillStyle='hsla(0,100%,60%,1)';this.cPoints.forEach(p=>{ctx.beginPath();ctx.moveTo(this.x,this.y);ctx.lineTo(p.x,p.y);ctx.stroke();ctx.beginPath();ctx.arc(p.x,p.y,6,0,Math.PI*2,false);ctx.stroke();ctx.fill();});ctx.restore();}drawController(ctx){this.drawPoints(ctx);this.drawCPoints(ctx);this.drawCenter(ctx);}}多角星
??仔細(xì)思考一下,多角星其實(shí)就是2*n邊形,不過它是凹多邊形而已,于是我們在之前凸多邊形基礎(chǔ)上去實(shí)現(xiàn)。相比于多邊形,我們還要在此基礎(chǔ)上增加第二控制點(diǎn),實(shí)現(xiàn)凹點(diǎn)與凸點(diǎn)的比值變化,通俗點(diǎn)就是多角星的胖瘦度。
三角形,矩形
??這兩個(gè)圖形就是特別的多邊形而已,功能非常簡單,而且只需要繼承圖形基類Graph
圓形,橢圓
??繪制圓形比較簡單,只需要知道中點(diǎn)和半徑,即可繪制,代碼在此省略。
??橢圓的繪制才是比較麻煩的,canvas并沒有提供相關(guān)的api,我這里參考了網(wǎng)上的例子,是使用4條三次貝塞爾曲線首尾相接來實(shí)現(xiàn)的,橢圓有兩個(gè)控制點(diǎn),分別可以拖拽實(shí)現(xiàn)橢圓的壓扁程度。這里只展示部分的代碼,其他和多邊形類似:
事件部分
??繪圖的主體部分已經(jīng)完成,接下來就是定義相關(guān)的事件了,首先mousedown的時(shí)候記錄下第一個(gè)坐標(biāo)mouseStart,這個(gè)點(diǎn)是繪制直線和曲線的起始點(diǎn),同時(shí)也是多邊形和多角星的中點(diǎn);
??然后再定義mousemove事件,記錄下第二個(gè)坐標(biāo)mouseEnd,這個(gè)是繪制直線和曲線的結(jié)束點(diǎn),同時(shí)也是多邊形和多角星的第一個(gè)頂點(diǎn);
??當(dāng)然這中間還要區(qū)分繪制模式和修改模式,繪制模式下,根據(jù)類型從對象工廠獲取對應(yīng)的對象,然后設(shè)置對象的屬性,完成初始化之后就把圖形對象放入圖形列表shapes中。列表中的圖形對象就可以作為后續(xù)修改模式進(jìn)行應(yīng)用動畫。
??如果是修改模式的話,首先是遍歷shapes中所有的圖形對象,并依次調(diào)用isInPath方法,看看當(dāng)前的鼠標(biāo)位置是否在該圖形上,并判斷是在中點(diǎn)或圖形內(nèi)部,還是某個(gè)頂點(diǎn)上。而具體的判斷邏輯已經(jīng)控制反轉(zhuǎn)在圖形對象內(nèi)部,外部并不需要知道其實(shí)現(xiàn)原理。如果鼠標(biāo)落在了某個(gè)圖形對象上,則在鼠標(biāo)移動時(shí)實(shí)時(shí)更新該圖形對應(yīng)的位置,頂點(diǎn),控制點(diǎn),并同步動畫渲染該圖形。
??刪除功能的實(shí)現(xiàn),就是按下delete鍵時(shí),遍歷shapes中所有的圖形對象,并依次調(diào)用isInPath方法,鼠標(biāo)如果在該對象上面,直接在shapes數(shù)組上splice(i,1),然后重寫渲染就ok。
??生成代碼功能一樣,遍歷shapes,依次調(diào)用createCode方法獲取該圖形生成的代碼字符串,然后將所有值合并賦予textarea的value。
??這里要理解的是,只要啟動了對應(yīng)的模式,改變了圖形的某部分,背景和對應(yīng)所有的圖形都要重新繪制一遍,當(dāng)然這也是canvas這種比較底層的繪圖api實(shí)現(xiàn)動畫的方式了。
// 生成對應(yīng)圖形的對象工廠function factory(type,pos){switch(type){case 'line': return new Line(pos);case 'dash': return new Dash(pos);case 'quadratic': return new Quadratic(pos);case 'bezier': return new Bezier(pos);case 'triangle': return new Triangle(pos);case 'rect': return new Rect(pos);case 'round': return new Round(pos);case 'polygon': return new Polygon(pos);case 'star': return new Star(pos);case 'ellipse': return new Ellipse(pos);default:return new Line(pos);}}canvas.addEventListener('mousedown',function(e){mouseStart=WindowToCanvas(canvas,e.clientX,e.clientY);env=getEnv();activeShape=null;//新建圖形if(drawing){activeShape = factory(env.type,mouseStart);activeShape.lineWidth = env.lineWidth;activeShape.strokeStyle = env.strokeStyle;activeShape.fillStyle = env.fillStyle;activeShape.isFill = env.isFill;activeShape.sides = env.sides;activeShape.stars = env.stars;shapes.push(activeShape);index=-1;drawGraph();} else {//選中控制點(diǎn)后拖拽修改圖形for(var i=0,len=shapes.length;i<len;i ){if((index=shapes[i].isInPath(ctx,mouseStart))>-1){canvas.style.cursor='crosshair';activeShape=shapes[i];break;}}}// saveImageData();canvas.addEventListener('mousemove',mouseMove,false);canvas.addEventListener('mouseup',mouseUp,false);},false);// 鼠標(biāo)移動function mouseMove(e){mouseEnd=WindowToCanvas(canvas,e.clientX,e.clientY);if(activeShape){if(index>-1){activeShape.update(index,mouseEnd);} else {activeShape.initUpdate(mouseStart,mouseEnd);}drawBG();if(env.guid){drawGuidewires(mouseEnd.x,mouseEnd.y); }drawGraph();}}// 鼠標(biāo)結(jié)束function mouseUp(e){canvas.style.cursor='pointer';if(activeShape){drawBG();drawGraph();resetDrawType();}canvas.removeEventListener('mousemove',mouseMove,false);canvas.removeEventListener('mouseup',mouseUp,false);}// 刪除圖形document.body.onkeydown=function(e){if(e.keyCode==8){for(var i=0,len=shapes.length;i<len;i ){if(shapes[i].isInPath(ctx,currPos)>-1){shapes.splice(i--,1);drawBG();drawGraph();break;}}}};//繪制背景function drawBG(){ctx.clearRect(0,0,W,H);if(getEnv().grid){DrawGrid(ctx,'lightGray',10,10); }}//網(wǎng)格function drawGuidewires(x,y){ctx.save();ctx.strokeStyle='rgba(0,0,230,0.4)';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(x 0.5,0);ctx.lineTo(x 0.5,ctx.canvas.height);ctx.stroke();ctx.beginPath();ctx.moveTo(0,y 0.5);ctx.lineTo(ctx.canvas.width,y 0.5);ctx.stroke();ctx.restore();}//繪制圖形列表function drawGraph(){var showControl=getEnv().control;shapes.forEach(shape=>{shape.draw(ctx);if(showControl){shape.drawController(ctx);}});}最后
??功能全部完成,當(dāng)然里面有很多的細(xì)節(jié),可以查看源代碼,這里有待進(jìn)一步完善的是修改功能,比如調(diào)整邊框?qū)挾?#xff0c;改變邊框顏色和填充顏色。 還有就是本人是在mac平臺的chrome下玩canvas,因此不保證其他對es6,canvas的支持度差的瀏覽器會出現(xiàn)的問題。
更多專業(yè)前端知識,請上 【猿2048】www.mk2048.com
總結(jié)
以上是生活随笔為你收集整理的canvas图形编辑器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: canvas图表(4) - 散点图
- 下一篇: 富文本编辑器、日期选择器、软件天堂、防止