引言
对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在以上需求中的应用。