canvas图形编辑器
??原文地址:http://jeffzhong.space/2017/11/02/drawboard/
??使用canvas進行開發(fā)項目,我們離不開各種線段,曲線,圖形,但每次都必須用代碼一步一步去實現,顯得非常麻煩。有沒有一種類似于PS,CAD之類的可視化工具,繪制出基本的圖形,然后輸出代碼。之后我們就可以在這個生成的圖形場景的基礎上去實現功能,那將是多么的美妙的事啊。話不多說,我們來實現一個圖形編輯器吧?。
主要實現如下的功能:
實際效果: drawboard(推薦在chrome或safari下運行)
功能點包括:
使用方式:
該項目用到的知識點包括:
工具欄
??首先我們實現如圖所示的工具欄,也就是基本的html/css,使用了flex布局,同時使用了html5的color, range, number標簽,其它都是普通的html和css代碼。主要注意的地方就是如下用純css實現選擇效果
??其中多邊形邊數選擇范圍控制為:3-20,當然我們也可以擴大為無限大的邊數,但實際應用到的情況比較少。多角星情況類型,范圍控制為3~20。
??然后對線條粗細,描邊顏色,填充顏色顯示信息,也就是onchang事件觸發(fā)時獲取value值,再顯示出來。顯示鼠標當前的位置功能也非常簡單,在此也略過不表。
圖形基類
??開始實現畫板的功能,第一步,實現圖形基類,這個是最重要的部分。因為不管是線條,多邊形都會繼承該類。
??注意:isPointInPath非常有用,就是這個api實現鼠標是否選中的功能了,它的原理就是調用上下文context繪制路徑,然后向isPointInPath傳遞位置(x,y)信息,該api會返回這個點是否在繪制路徑上,相當于繪制的是隱形的路徑進行判斷點是否在該路徑或圖形內部,這也是我要把繪制路徑和渲染的功能分離開的原因。
??具體的功能還是直接看代碼吧
class Graph{//初始化圖形需要用到的屬性,位置,頂點列表,邊的寬度,描邊顏色,填充顏色,是否填充;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;}//實現繪制時的拖拽initUpdate(start,end){this.points[1]=end;this.x=(start.x end.x)/2;this.y=(start.y end.y)/2;}//實現修改模式下的拖拽頂點和控制點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();}//判斷鼠標是否選中對應的圖形,選中哪個頂點,選中哪個控制點,中心點;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}//繪制控制點drawController(ctx){this.drawPoints(ctx);this.drawCenter(ctx);}//繪制頂點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();}//繪制中心點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();}//繪制整個圖形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到中心點,方便使用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');}}直線
??實現直線功能相當簡單,繼承基類,只需要重寫draw和createCode方法,拖拽和變換等功能都已經在基類實現了。
??還有就是虛線功能了,其實就是先繪制一段直線,然后空出一段空間,接著再繪制一段直線,如此類推。小伙伴可以思考一下怎么實現,這個和直線所涉及的知識點相同,代碼就略過了。
貝塞爾曲線
??接著就是貝塞爾曲線的繪制了,首先繼承直線類,曲線比直線不同的是除了起始點和結束點,它還多出了控制點,2次貝塞爾曲線有一個控制點,3次貝塞爾曲線則有兩個控制點。所以對應初始化拖拽,頂點繪制的方法必須重寫,以下是3次貝塞爾曲線的代碼。
至于貝塞爾2次曲線功能類似,同時也更加簡單,代碼也略過。
多邊形
??實現任意條邊的多邊形,大家思考一下都會知道如何實現,平均角度=360度/邊數,不是嗎?
??在知道中點和第一個頂點的情況下,第n個頂點與中點的角度 = n*平均角度;然后記錄下每個頂點的位置,然后依次繪制每個頂點的連線即可。這里用到了二維旋轉的公式,也就是繞圖形的中點,旋轉一定的角度。
既然我們已經記錄了每個頂點的位置,當拖動對應的頂點后修改該頂點位置,重新繪制,就可以伸縮成任意的圖案。
??難點是拖拽控制線,實現旋轉多邊形角度,和擴大縮小多邊形。等比例擴大縮小每個頂點與中點的距離即可實現等比例縮放多邊形,記錄第一個頂點與中點的角度變化即可實現旋轉功能,這里用到反正切Math.atan2(y,x)求角度;具體實現看如下代碼。
/*** 多邊形*/class Polygon extends Graph{constructor(pos){super(pos);this.cPoints=[];}get name(){return this.sides '邊形';}//生成頂點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)});}}//生成控制點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){//拖拽控制點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 {//拖拽頂點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);}}多角星
??仔細思考一下,多角星其實就是2*n邊形,不過它是凹多邊形而已,于是我們在之前凸多邊形基礎上去實現。相比于多邊形,我們還要在此基礎上增加第二控制點,實現凹點與凸點的比值變化,通俗點就是多角星的胖瘦度。
三角形,矩形
??這兩個圖形就是特別的多邊形而已,功能非常簡單,而且只需要繼承圖形基類Graph
圓形,橢圓
??繪制圓形比較簡單,只需要知道中點和半徑,即可繪制,代碼在此省略。
??橢圓的繪制才是比較麻煩的,canvas并沒有提供相關的api,我這里參考了網上的例子,是使用4條三次貝塞爾曲線首尾相接來實現的,橢圓有兩個控制點,分別可以拖拽實現橢圓的壓扁程度。這里只展示部分的代碼,其他和多邊形類似:
事件部分
??繪圖的主體部分已經完成,接下來就是定義相關的事件了,首先mousedown的時候記錄下第一個坐標mouseStart,這個點是繪制直線和曲線的起始點,同時也是多邊形和多角星的中點;
??然后再定義mousemove事件,記錄下第二個坐標mouseEnd,這個是繪制直線和曲線的結束點,同時也是多邊形和多角星的第一個頂點;
??當然這中間還要區(qū)分繪制模式和修改模式,繪制模式下,根據類型從對象工廠獲取對應的對象,然后設置對象的屬性,完成初始化之后就把圖形對象放入圖形列表shapes中。列表中的圖形對象就可以作為后續(xù)修改模式進行應用動畫。
??如果是修改模式的話,首先是遍歷shapes中所有的圖形對象,并依次調用isInPath方法,看看當前的鼠標位置是否在該圖形上,并判斷是在中點或圖形內部,還是某個頂點上。而具體的判斷邏輯已經控制反轉在圖形對象內部,外部并不需要知道其實現原理。如果鼠標落在了某個圖形對象上,則在鼠標移動時實時更新該圖形對應的位置,頂點,控制點,并同步動畫渲染該圖形。
??刪除功能的實現,就是按下delete鍵時,遍歷shapes中所有的圖形對象,并依次調用isInPath方法,鼠標如果在該對象上面,直接在shapes數組上splice(i,1),然后重寫渲染就ok。
??生成代碼功能一樣,遍歷shapes,依次調用createCode方法獲取該圖形生成的代碼字符串,然后將所有值合并賦予textarea的value。
??這里要理解的是,只要啟動了對應的模式,改變了圖形的某部分,背景和對應所有的圖形都要重新繪制一遍,當然這也是canvas這種比較底層的繪圖api實現動畫的方式了。
// 生成對應圖形的對象工廠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 {//選中控制點后拖拽修改圖形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);// 鼠標移動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();}}// 鼠標結束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); }}//網格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);}});}最后
??功能全部完成,當然里面有很多的細節(jié),可以查看源代碼,這里有待進一步完善的是修改功能,比如調整邊框寬度,改變邊框顏色和填充顏色。 還有就是本人是在mac平臺的chrome下玩canvas,因此不保證其他對es6,canvas的支持度差的瀏覽器會出現的問題。
更多專業(yè)前端知識,請上 【猿2048】www.mk2048.com
總結
以上是生活随笔為你收集整理的canvas图形编辑器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: canvas图表(4) - 散点图
- 下一篇: 富文本编辑器、日期选择器、软件天堂、防止