引言
對canvas中繪制的圖片進行旋轉操作,需要使用ctx.translate變換坐標系,將圖片旋轉的基點設為坐標系的原點,然后ctx.rotate旋轉。
這個時候,因為canvas坐標系發生了旋轉,而視覺感受上的坐標以及鼠標事件中的坐標都是旋轉之前的屏幕坐標系。再根據鼠標的移動去控制canvas中的圖片時,就會出現問題。
用A坐標系中的偏移來控制B坐標系中的圖形,就需要進行一個坐標轉換,即通過一種轉換關系,將A坐標系中的點在B坐標系中表示出來,然后根據B坐標系中的偏移來控制B坐標系中的圖形。
下面按照先易后難的順序,把基本的坐標轉換解釋一下。
[備注]
這篇文章只是記錄分享下解決問題的過程,找我要demo的,或者問我什么東西怎么做的,就不要加我了。你可以加一個canvas相關的交流群,或者如果需要用到KineticJS/FabricJS的話,可以加群251572039。
一、拖拽中的坐標轉換
因為沒有旋轉,所以不需要考慮角度變化,屏幕坐標系的偏移=canvas坐標系的偏移。
實現思路:繪制圖片之前,將canvas坐標系的原點移動到圖片的中心點位置。移動的時候,根據鼠標move后在屏幕坐標系的偏移得出圖片中心點需要的偏移量,算出新的圖片中心點的坐標,再根據新的圖片中心點在屏幕坐標系的坐標計算其在canvas坐標系的坐標值P,然后將canvas坐標系的原點ctx.translate到P。
demo中有詳細的注釋 鏈接http://youryida.duapp.com/demo_canvas/coor_convert_move.html

1 <!doctype html> 2 <html> 3 <head> 4 <title> </title> 5 <meta http-equiv="X-UA-Compatible" content="IE=9"> 6 <meta charset="utf-8" /> 7 <meta http-equiv="pragma" content="no-cache"> 8 <meta http-equiv="cache-control" content="no-cache"> 9 <meta http-equiv="expires" content="0"> 10 <style> 11 #canvas{border:1px solid #ccc;} 12 </style> 13 </head> 14 <body> 15 <canvas id="canvas" width="500" height="300"></canvas> 16 <pre> 17 功能:拖拽 18 思路:始終保持圖片中心點在canvas坐標系的原點處,圖片的每一次重繪都基於canvas坐標系的原點來繪制,即drawImage(img,-imgW/2,-imgH/2)。 19 移動的時候繪制方法不變,變換的是canvas坐標系。 20 關鍵:理解屏幕坐標系和canvas坐標系的關系。將鼠標事件的屏幕坐標,轉換為canvas坐標系中的坐標。 21 </pre> 22 <script> 23 var cvs =document.getElementById("canvas"); 24 var ctx =cvs.getContext("2d"); 25 var cvsH=cvs.height; 26 var cvsW=cvs.width; 27 var beginX,beginY; 28 var LT={x:30,y:30};//圖片左上角的點 29 var isDown=false; 30 var imgH,imgW; 31 var moveAble=false; 32 var img = new Image(); 33 img.src ="img/niuniu.jpg"; 34 img.onload=function (){ 35 imgH=img.height; 36 imgW=img.width; 37 PO={x:LT.x+imgW/2,y:LT.y+imgH/2}; 38 ctx.translate(PO.x,PO.y); 39 onDraw(); 40 } 41 function onDraw(){ 42 ctx.clearRect(-cvsW,-cvsH,2*cvsW,2*cvsH); 43 ctx.drawImage(img,-imgW/2,-imgH/2); 44 } 45 46 cvs.addEventListener("mousedown", startMove, false); 47 cvs.addEventListener("mousemove", moving, false); 48 cvs.addEventListener("mouseup", endMove, false); 49 cvs.addEventListener("mouseout",endMove, false); 50 51 function imgIsDown(x,y){ 52 return (-imgW/2<=x && x<=imgW/2 && -imgH/2<y && y<=imgH/2); 53 } 54 55 function startMove(){ 56 event.preventDefault(); 57 isDown=true; 58 var loc=getEvtLoc();//獲取鼠標事件在屏幕坐標系的位置(原點在canvas左上角) 59 var x=loc.x,y=loc.y; 60 var cLoc=convertCoor(loc); 61 var Xc=cLoc.x,Yc=cLoc.y; 62 beginX=x,beginY=y; 63 moveAble=imgIsDown(Xc,Yc); 64 if (moveAble) cvs.style.cursor="move"; 65 66 } 67 function moving(){ 68 event.preventDefault(); 69 if(isDown==false) return; 70 var loc=getEvtLoc(); 71 72 if(moveAble){ 73 var x=loc.x,y=loc.y; 74 var dx=x-beginX,dy=y-beginY; 75 var mPO={x:PO.x+dx,y:PO.y+dy};//因為鼠標移動dx dy,所以PO在屏幕坐標系的坐標也 移動dx dy 76 var cPO=convertCoor(mPO);//屏幕坐標系移動后的PO轉換成canvas坐標系的坐標 77 ctx.translate(cPO.x,cPO.y);//canvas坐標系原點移動到新的圖片中心點 78 onDraw(); 79 80 PO.x=PO.x+dx;//記錄下屏幕坐標系上PO的坐標變化 81 PO.y=PO.y+dy; 82 beginX=x,beginY=y; //記錄移動后鼠標在屏幕坐標系的新位置 83 } 84 } 85 function endMove(){ 86 event.preventDefault(); 87 isDown=false; 88 moveAble=false; 89 cvs.style.cursor="auto"; 90 } 91 function getEvtLoc(){//獲取相對canvas標簽左上角的鼠標事件坐標 92 return {x:event.offsetX,y:event.offsetY} 93 } 94 95 function convertCoor(P) {//坐標變換 屏幕坐標系的點 轉換為canvas新坐標系的點 96 var x=P.x-PO.x;//在屏幕坐標系中,鼠標位置和新坐標系原點PO的偏移 97 var y=P.y-PO.y; 98 return {x:x,y:y}; 99 } 100 </script> 101 </body> 102 </html>
二、拖拽+旋轉 中的坐標轉換
實現思路:還是上面的思路,要把屏幕坐標系的點都轉換成canvas坐標系的點。關於旋轉,圖片中心點不動,即canvas坐標系原點不動,鼠標摁住旋鈕(假設旋鈕在圖片中心上方)后,圖片跟隨鼠標進行旋轉,需要計算鼠標點在canvas坐標系中的坐標值,並且計算出該點相對canvas坐標系y軸反方向的夾角θ,然后旋轉canvas坐標系ctx.rotate(θ);
帶有旋轉的坐標轉換詳解:
如左圖,鼠標事件中獲取到的點(M) 坐標都是基於屏幕的坐標系,即XOY坐標系。
設canvas中經過一些旋轉操作之后的canvas坐標系為X'O'Y'。
因為繪圖代碼是依據canvas中的坐標系進行繪制,所以就需要將屏幕坐標系中點的坐標值轉換成canvas坐標系中點的坐標值。
該坐標轉換抽象為一道高中幾何題就是:
平面內一個直角坐標系XOY,經過平移、順時針旋轉θ角度后形成新的直角坐標系X'O'Y',已知O'在XOY坐標系中的坐標為(Xo,Yo),點M在XOY坐標系中的坐標為(Xm,Ym),求M在X'O'Y'坐標系中的坐標(x',y')。
解:
如左圖,從M點對兩坐標系的xy軸做垂線並連接O'M,
Δx=Xm-Xo;
Δy=Ym-Yo;
O'M = Math.sqrt(Δx*Δx+Δy*Δy);//勾股定理
Math.atan2(Δy,Δx)=α+β;//M點與X軸的夾角 三角函數對邊/臨邊
β=Math.atan2(Δy,Δx)-θ;//因為θ=α
x'=O'M*Math.cos(β);
y'=O'M*Math.sin(β); //可得M在X'O'Y'坐標系中的坐標(x',y')
over;
demo中有詳細的注釋 鏈接http://youryida.duapp.com/demo_canvas/coor_convert_move_rotate.html

1 <!doctype html> 2 <html> 3 <head> 4 <title> </title> 5 <meta http-equiv="X-UA-Compatible" content="IE=9"> 6 <meta charset="utf-8" /> 7 <meta http-equiv="pragma" content="no-cache"> 8 <meta http-equiv="cache-control" content="no-cache"> 9 <meta http-equiv="expires" content="0"> 10 <style> 11 #canvas{border:1px solid #ccc;} 12 </style> 13 14 </head> 15 <body> 16 <canvas id="canvas" width="500" height="300"></canvas> 17 <pre> 18 功能:拖拽+旋轉 19 思路:始終保持圖片中心點在canvas坐標系的原點處,圖片的每一次重繪都基於canvas坐標系的原點來繪制,即drawImage(img,-imgW/2,-imgH/2)。 20 移動、旋轉的時候繪制方法不變,變換的是canvas坐標系。 21 關鍵:理解屏幕坐標系和canvas坐標系的關系。將鼠標事件的屏幕坐標,轉換為canvas坐標系中的坐標。 22 計算旋轉時每一次mousemove,在旋轉前的canvas坐標系中move的角度。 23 </pre> 24 <script> 25 var cvs =document.getElementById("canvas"); 26 var ctx =cvs.getContext("2d"); 27 var cvsH=cvs.height; 28 var cvsW=cvs.width; 29 var beginX,beginY; 30 var LT={x:30,y:30};//圖片左上角的點 31 var Selected_Round_R=12; 32 var isDown=false; 33 var imgH,imgW; 34 var moveAble=false,rotateAble=false; 35 var img = new Image(); 36 var rotate_radian=0;//canvas坐標系x軸與屏幕坐標系X軸夾角弧度 37 img.src ="img/niuniu.jpg"; 38 img.onload=function (){ 39 imgH=img.height; 40 imgW=img.width; 41 PO={x:LT.x+imgW/2,y:LT.y+imgH/2}; 42 ctx.translate(PO.x,PO.y);//載入時將canvas坐標系原點移到圖片中心點上 43 onDraw(); 44 45 } 46 function onDraw(){ 47 ctx.clearRect(-cvsW,-cvsH,2*cvsW,2*cvsH); 48 ctx.drawImage(img,-imgW/2,-imgH/2); 49 //旋轉控制旋鈕 50 ctx.beginPath(); 51 ctx.arc(0,-imgH/2-Selected_Round_R,Selected_Round_R,0,Math.PI*2,false); 52 ctx.closePath(); 53 ctx.lineWidth=2; 54 ctx.strokeStyle="#0000ff"; 55 ctx.stroke(); 56 } 57 cvs.addEventListener("mousedown", startMove, false); 58 cvs.addEventListener("mousemove", moving, false); 59 cvs.addEventListener("mouseup", endMove, false); 60 cvs.addEventListener("mouseout",endMove, false); 61 62 function imgIsDown(x,y){ 63 return (-imgW/2<=x && x<=imgW/2 && -imgH/2<y && y<=imgH/2); 64 } 65 function RTIsDown(x,y){ 66 var round_center={x:0,y:-imgH/2-Selected_Round_R}; 67 var bool=getPointDistance({x:x,y:y},round_center)<=Selected_Round_R; 68 return bool; 69 } 70 function startMove(){ 71 event.preventDefault(); 72 isDown=true; 73 var loc=getEvtLoc();//獲取鼠標事件在屏幕坐標系的位置(原點在canvas標簽左上角) 74 var x=loc.x,y=loc.y; 75 beginX=x,beginY=y; 76 var cLoc=convertCoor(loc); 77 var Xc=cLoc.x,Yc=cLoc.y; 78 moveAble=imgIsDown(Xc,Yc); 79 rotateAble=RTIsDown(Xc,Yc); 80 if (moveAble) cvs.style.cursor="move"; 81 if (rotateAble) cvs.style.cursor="crosshair"; 82 } 83 function moving(){ 84 event.preventDefault(); 85 if(isDown==false) return; 86 var loc=getEvtLoc(); 87 if(moveAble){ 88 var x=loc.x,y=loc.y; 89 var dx=x-beginX,dy=y-beginY; 90 var mPO={x:PO.x+dx,y:PO.y+dy};//因為鼠標移動dx dy,所以PO在屏幕坐標系的坐標也 移動dx dy 91 var cPO=convertCoor(mPO);//屏幕坐標系移動后的PO轉換成canvas坐標系的坐標 92 ctx.translate(cPO.x,cPO.y);//canvas坐標系原點移動到新的圖片中心點 93 onDraw(); 94 95 PO.x=PO.x+dx;//記錄下屏幕坐標系上PO的坐標變化 96 PO.y=PO.y+dy; 97 beginX=x,beginY=y;//記錄移動后鼠標在屏幕坐標系的新位置 98 }else if(rotateAble){ 99 var cLoc=convertCoor(loc); 100 var Xc=cLoc.x,Yc=cLoc.y; 101 var newR = Math.atan2(Xc,-Yc);//在旋轉前的canvas坐標系中 move的角度(因為旋鈕在上方,所以跟,應該計算 在旋轉前canvas坐標系中,鼠標位置和原點連線 與 y軸反方向的夾角) 102 ctx.rotate(newR); 103 rotate_radian+=newR; 104 onDraw(); 105 } 106 } 107 function endMove(){ 108 event.preventDefault(); 109 isDown=false; 110 moveAble=rotateAble=false; 111 cvs.style.cursor="auto"; 112 } 113 114 function getEvtLoc(){//獲取相對canvas標簽左上角的鼠標事件坐標 115 return {x:event.offsetX,y:event.offsetY} 116 } 117 118 function convertCoor(P) {//坐標變換 屏幕坐標系的點 轉換為canvas坐標系的點 119 var x=P.x-PO.x;//在屏幕坐標系中,P點相對canvas坐標系原點PO的偏移 120 var y=P.y-PO.y; 121 122 if(rotate_radian!=0){ 123 var len = Math.sqrt(x*x + y*y); 124 var oldR=Math.atan2(y,x);//屏幕坐標系中 PO與P點連線 與屏幕坐標系X軸的夾角弧度 125 var newR =oldR-rotate_radian;//canvas坐標系中PO與P點連線 與canvas坐標系x軸的夾角弧度 126 x = len*Math.cos(newR); 127 y = len*Math.sin(newR); 128 } 129 130 return {x:x,y:y}; 131 } 132 //獲取兩點距離 133 function getPointDistance(a,b){ 134 var x1=a.x,y1=a.y,x2=b.x,y2=b.y; 135 var dd= Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)); 136 return dd; 137 } 138 </script> 139 </body> 140 </html>
三、總結
在canvas上繪制的元素比較多的時候,不適合用這種辦法進行拖拽旋轉,因為時刻變換的坐標系會影響到canvas上的其他元素,增加其他元素繪制的復雜性。
有時間再研究save和restore在以上需求中的應用。