canvas 图片拖拽旋转之一——坐标转换translate
引言
对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
<!doctype html>
<html>
<head>
<title> </title>
<meta http-equiv="X-UA-Compatible" content="IE=9">
<meta charset="utf-8" />
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<style>
#canvas{border:1px solid #ccc;}
</style>
</head>
<body>
<canvas id="canvas" width="500" height="300"></canvas>
<pre>
功能:拖拽
思路:始终保持图片中心点在canvas坐标系的原点处,图片的每一次重绘都基于canvas坐标系的原点来绘制,即drawImage(img,-imgW/2,-imgH/2)。
移动的时候绘制方法不变,变换的是canvas坐标系。
关键:理解屏幕坐标系和canvas坐标系的关系。将鼠标事件的屏幕坐标,转换为canvas坐标系中的坐标。
</pre>
<script>
var cvs =document.getElementById("canvas");
var ctx =cvs.getContext("2d");
var cvsH=cvs.height;
var cvsW=cvs.width;
var beginX,beginY;
var LT={x:30,y:30};//图片左上角的点
var isDown=false;
var imgH,imgW;
var moveAble=false;
var img = new Image();
img.src ="img/niuniu.jpg";
img.onload=function (){
imgH=img.height;
imgW=img.width;
PO={x:LT.x+imgW/2,y:LT.y+imgH/2};
ctx.translate(PO.x,PO.y);
onDraw();
}
function onDraw(){
ctx.clearRect(-cvsW,-cvsH,2*cvsW,2*cvsH);
ctx.drawImage(img,-imgW/2,-imgH/2);
} cvs.addEventListener("mousedown", startMove, false);
cvs.addEventListener("mousemove", moving, false);
cvs.addEventListener("mouseup", endMove, false);
cvs.addEventListener("mouseout",endMove, false); function imgIsDown(x,y){
return (-imgW/2<=x && x<=imgW/2 && -imgH/2<y && y<=imgH/2);
} function startMove(){
event.preventDefault();
isDown=true;
var loc=getEvtLoc();//获取鼠标事件在屏幕坐标系的位置(原点在canvas左上角)
var x=loc.x,y=loc.y;
var cLoc=convertCoor(loc);
var Xc=cLoc.x,Yc=cLoc.y;
beginX=x,beginY=y;
moveAble=imgIsDown(Xc,Yc);
if (moveAble) cvs.style.cursor="move"; }
function moving(){
event.preventDefault();
if(isDown==false) return;
var loc=getEvtLoc(); if(moveAble){
var x=loc.x,y=loc.y;
var dx=x-beginX,dy=y-beginY;
var mPO={x:PO.x+dx,y:PO.y+dy};//因为鼠标移动dx dy,所以PO在屏幕坐标系的坐标也 移动dx dy
var cPO=convertCoor(mPO);//屏幕坐标系移动后的PO转换成canvas坐标系的坐标
ctx.translate(cPO.x,cPO.y);//canvas坐标系原点移动到新的图片中心点
onDraw(); PO.x=PO.x+dx;//记录下屏幕坐标系上PO的坐标变化
PO.y=PO.y+dy;
beginX=x,beginY=y; //记录移动后鼠标在屏幕坐标系的新位置
}
}
function endMove(){
event.preventDefault();
isDown=false;
moveAble=false;
cvs.style.cursor="auto";
}
function getEvtLoc(){//获取相对canvas标签左上角的鼠标事件坐标
return {x:event.offsetX,y:event.offsetY}
} function convertCoor(P) {//坐标变换 屏幕坐标系的点 转换为canvas新坐标系的点
var x=P.x-PO.x;//在屏幕坐标系中,鼠标位置和新坐标系原点PO的偏移
var y=P.y-PO.y;
return {x:x,y:y};
}
</script>
</body>
</html>
图片拖拽 坐标转换demo
二、拖拽+旋转 中的坐标转换
实现思路:还是上面的思路,要把屏幕坐标系的点都转换成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
<!doctype html>
<html>
<head>
<title> </title>
<meta http-equiv="X-UA-Compatible" content="IE=9">
<meta charset="utf-8" />
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<style>
#canvas{border:1px solid #ccc;}
</style> </head>
<body>
<canvas id="canvas" width="500" height="300"></canvas>
<pre>
功能:拖拽+旋转
思路:始终保持图片中心点在canvas坐标系的原点处,图片的每一次重绘都基于canvas坐标系的原点来绘制,即drawImage(img,-imgW/2,-imgH/2)。
移动、旋转的时候绘制方法不变,变换的是canvas坐标系。
关键:理解屏幕坐标系和canvas坐标系的关系。将鼠标事件的屏幕坐标,转换为canvas坐标系中的坐标。
计算旋转时每一次mousemove,在旋转前的canvas坐标系中move的角度。
</pre>
<script>
var cvs =document.getElementById("canvas");
var ctx =cvs.getContext("2d");
var cvsH=cvs.height;
var cvsW=cvs.width;
var beginX,beginY;
var LT={x:30,y:30};//图片左上角的点
var Selected_Round_R=12;
var isDown=false;
var imgH,imgW;
var moveAble=false,rotateAble=false;
var img = new Image();
var rotate_radian=0;//canvas坐标系x轴与屏幕坐标系X轴夹角弧度
img.src ="img/niuniu.jpg";
img.onload=function (){
imgH=img.height;
imgW=img.width;
PO={x:LT.x+imgW/2,y:LT.y+imgH/2};
ctx.translate(PO.x,PO.y);//载入时将canvas坐标系原点移到图片中心点上
onDraw(); }
function onDraw(){
ctx.clearRect(-cvsW,-cvsH,2*cvsW,2*cvsH);
ctx.drawImage(img,-imgW/2,-imgH/2);
//旋转控制旋钮
ctx.beginPath();
ctx.arc(0,-imgH/2-Selected_Round_R,Selected_Round_R,0,Math.PI*2,false);
ctx.closePath();
ctx.lineWidth=2;
ctx.strokeStyle="#0000ff";
ctx.stroke();
}
cvs.addEventListener("mousedown", startMove, false);
cvs.addEventListener("mousemove", moving, false);
cvs.addEventListener("mouseup", endMove, false);
cvs.addEventListener("mouseout",endMove, false); function imgIsDown(x,y){
return (-imgW/2<=x && x<=imgW/2 && -imgH/2<y && y<=imgH/2);
}
function RTIsDown(x,y){
var round_center={x:0,y:-imgH/2-Selected_Round_R};
var bool=getPointDistance({x:x,y:y},round_center)<=Selected_Round_R;
return bool;
}
function startMove(){
event.preventDefault();
isDown=true;
var loc=getEvtLoc();//获取鼠标事件在屏幕坐标系的位置(原点在canvas标签左上角)
var x=loc.x,y=loc.y;
beginX=x,beginY=y;
var cLoc=convertCoor(loc);
var Xc=cLoc.x,Yc=cLoc.y;
moveAble=imgIsDown(Xc,Yc);
rotateAble=RTIsDown(Xc,Yc);
if (moveAble) cvs.style.cursor="move";
if (rotateAble) cvs.style.cursor="crosshair";
}
function moving(){
event.preventDefault();
if(isDown==false) return;
var loc=getEvtLoc();
if(moveAble){
var x=loc.x,y=loc.y;
var dx=x-beginX,dy=y-beginY;
var mPO={x:PO.x+dx,y:PO.y+dy};//因为鼠标移动dx dy,所以PO在屏幕坐标系的坐标也 移动dx dy
var cPO=convertCoor(mPO);//屏幕坐标系移动后的PO转换成canvas坐标系的坐标
ctx.translate(cPO.x,cPO.y);//canvas坐标系原点移动到新的图片中心点
onDraw(); PO.x=PO.x+dx;//记录下屏幕坐标系上PO的坐标变化
PO.y=PO.y+dy;
beginX=x,beginY=y;//记录移动后鼠标在屏幕坐标系的新位置
}else if(rotateAble){
var cLoc=convertCoor(loc);
var Xc=cLoc.x,Yc=cLoc.y;
var newR = Math.atan2(Xc,-Yc);//在旋转前的canvas坐标系中 move的角度(因为旋钮在上方,所以跟,应该计算 在旋转前canvas坐标系中,鼠标位置和原点连线 与 y轴反方向的夹角)
ctx.rotate(newR);
rotate_radian+=newR;
onDraw();
}
}
function endMove(){
event.preventDefault();
isDown=false;
moveAble=rotateAble=false;
cvs.style.cursor="auto";
} function getEvtLoc(){//获取相对canvas标签左上角的鼠标事件坐标
return {x:event.offsetX,y:event.offsetY}
} function convertCoor(P) {//坐标变换 屏幕坐标系的点 转换为canvas坐标系的点
var x=P.x-PO.x;//在屏幕坐标系中,P点相对canvas坐标系原点PO的偏移
var y=P.y-PO.y; if(rotate_radian!=0){
var len = Math.sqrt(x*x + y*y);
var oldR=Math.atan2(y,x);//屏幕坐标系中 PO与P点连线 与屏幕坐标系X轴的夹角弧度
var newR =oldR-rotate_radian;//canvas坐标系中PO与P点连线 与canvas坐标系x轴的夹角弧度
x = len*Math.cos(newR);
y = len*Math.sin(newR);
} return {x:x,y:y};
}
//获取两点距离
function getPointDistance(a,b){
var x1=a.x,y1=a.y,x2=b.x,y2=b.y;
var dd= Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
return dd;
}
</script>
</body>
</html>
图片拖拽+旋转 坐标转换demo
三、总结
在canvas上绘制的元素比较多的时候,不适合用这种办法进行拖拽旋转,因为时刻变换的坐标系会影响到canvas上的其他元素,增加其他元素绘制的复杂性。
有时间再研究save和restore在以上需求中的应用。
canvas 图片拖拽旋转之一——坐标转换translate的更多相关文章
- canvas 图片拖拽旋转之二——canvas状态保存(save和restore)
引言 在上一篇日志“canvas 图片拖拽旋转之一”中,对坐标转换有了比较深入的了解,但是仅仅利用坐标转换实现的拖拽旋转,会改变canvas坐标系的状态,从而影响画布上其他元素的绘制.因此,这个时候需 ...
- 自制一个H5图片拖拽、裁剪插件(原生JS)
前言 如今的H5运营活动中,有很多都是让用户拍照或者上传图片,然后对照片加滤镜.加贴纸.评颜值之类的.尤其是一些拍照软件公司的运营活动几乎全部都是这样的. 博主也做过不少,为了省事就封装了一个简单的图 ...
- HTML5新特性之Canvas+drag(拖拽图像实现图像反转)
1.什么是canvas 在网页上使用canvas元素时,会创建一块矩形区域,默认矩形区域宽度300px,高度150px.. 页面中加入canvas元素后,可以通过javascript自由控制.可以在其 ...
- vue在移动端使用alloyfinger手势库操作图片拖拽、缩放
最近开发一个活动需要在手机上给上传的头像加上边框.装饰,需要拖拽.手势缩放边框下的头像图片,因为是vue项目,开始尝试了vue-drag-resize这个组件,对图片拖拽支持很完美,但是无法手势缩放, ...
- HTML5图片拖拽预览原理及实现
一.前言 这两天恰好有一位同事问我怎样做一个图片预览功能.作为现代人的我们首先想到的当然是HTML5啦,其实HTML5做图片预览已经是一个老生常谈的问题了.我在这里就简单说说其中相关的一些东西,当然会 ...
- HTML5多图片拖拽上传带进度条
前言 昨天利用css2的clip属性实现了网页进度条觉得还不错,但是很多情况下,我们在那些时候用进度条呢,一般网页加载的时候如果有需要可以用,那么问题就来了,怎么才算整个加载完毕呢,是页面主要模块加载 ...
- Android 仿微信朋友圈发表图片拖拽和删除功能
朋友圈实现原理 我们使用 Android Device Monitor 来分析朋友圈发布图片的界面实现原理.如果需要分析其他应用的界面实现也是采用这种方法哦. 打开 Android Device Mo ...
- Vue富文本编辑器(图片拖拽缩放)
富文本编辑器(图片拖拽缩放) 需求: 根据业务要求,需要能够上传图片,且上传的图片能在移动端中占满屏幕宽度,故需要能等比缩放上传的图片,还需要能拖拽.缩放.改变图片大小.尝试多个第三方富文本编辑器,很 ...
- CSS 奇思妙想 | 使用 resize 实现强大的图片拖拽切换预览功能
本文将介绍一个非常有意思的功能,使用纯 CSS 利用 resize 实现强大的图片切换预览功能.类似于这样: 思路 首先,要实现这样一个效果如果不要求可以拖拽,其实有非常多的办法. 将两张图片叠加在一 ...
随机推荐
- lua中基类和“继承机制”
基类:基类定义了所有对于派生类来说普通的属性和方法,派生类从基类继承所需的属性和方法,且在派生类中增加新的属性和方法. 继承:继承是C++语言的一种重要机制,它允许在已定义的类的基础上产生新类. lu ...
- PHP的输出缓冲区(转)
什么是缓冲区?简单而言,缓冲区的作用就是,把输入或者输出的内容先放进内存,而不显示或者读取.至于为什么要有缓冲区,这是一个很广泛的问题,如果有兴趣,可以在网山找下资料.其实缓冲区最本质的作用就是,协调 ...
- TP5验证规则使用
定义验证器类: namespace app\index\validate; use think\Validate; class User extends Validate { protected $r ...
- css伪类制作三角箭头
<meta charset="utf-8"> <style type="text/css"> .tip{ padding: 5px 10 ...
- div+css中clear用法
一开始用clear属性,只是跟float相对使用,今天看视频的时候还是不大明白,查了下资料原来是这样的哦,看咯. clear属性值有四个clear:both|left|right|none; 作用:该 ...
- IT人生知识分享:概率与运气
前言: 最近的人生多了些体验,也读了些许书,感觉还是有些知识是可以分享的. 今天难得周六,特意开电脑了,花几个小时写写,和大伙分享分享点知识. 以下内容,更多的需要读者思考,所以结论不会写太清晰,但一 ...
- js框架模版
(function() { //注册命名空间zzw到window对象上 window['zzw'] = {} //定义一个$函数 function $() { alert("hello $& ...
- [ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)
继上一篇文章之后,本文将介绍 Controller 和 Action 的一些较高级特性,包括 Controller Factory.Action Invoker 和异步 Controller 等内容. ...
- RabbitMQ Exchange & Queue Design Trade-off
In previous post, I mentioned the discussion on StackOverflow regarding designing exchanges. Usually ...
- JavaScript学习笔记之数值
JavaScript内部,所有数字都是以64位浮点数形式储存,即使整数也是如此.(整数也是通过64浮点数的形式来存储的) 所以,1+1.0=2:且1===1.0的 浮点数不是精确的值,所以涉及小数的比 ...