引言

对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的更多相关文章

  1. canvas 图片拖拽旋转之二——canvas状态保存(save和restore)

    引言 在上一篇日志“canvas 图片拖拽旋转之一”中,对坐标转换有了比较深入的了解,但是仅仅利用坐标转换实现的拖拽旋转,会改变canvas坐标系的状态,从而影响画布上其他元素的绘制.因此,这个时候需 ...

  2. 自制一个H5图片拖拽、裁剪插件(原生JS)

    前言 如今的H5运营活动中,有很多都是让用户拍照或者上传图片,然后对照片加滤镜.加贴纸.评颜值之类的.尤其是一些拍照软件公司的运营活动几乎全部都是这样的. 博主也做过不少,为了省事就封装了一个简单的图 ...

  3. HTML5新特性之Canvas+drag(拖拽图像实现图像反转)

    1.什么是canvas 在网页上使用canvas元素时,会创建一块矩形区域,默认矩形区域宽度300px,高度150px.. 页面中加入canvas元素后,可以通过javascript自由控制.可以在其 ...

  4. vue在移动端使用alloyfinger手势库操作图片拖拽、缩放

    最近开发一个活动需要在手机上给上传的头像加上边框.装饰,需要拖拽.手势缩放边框下的头像图片,因为是vue项目,开始尝试了vue-drag-resize这个组件,对图片拖拽支持很完美,但是无法手势缩放, ...

  5. HTML5图片拖拽预览原理及实现

    一.前言 这两天恰好有一位同事问我怎样做一个图片预览功能.作为现代人的我们首先想到的当然是HTML5啦,其实HTML5做图片预览已经是一个老生常谈的问题了.我在这里就简单说说其中相关的一些东西,当然会 ...

  6. HTML5多图片拖拽上传带进度条

    前言 昨天利用css2的clip属性实现了网页进度条觉得还不错,但是很多情况下,我们在那些时候用进度条呢,一般网页加载的时候如果有需要可以用,那么问题就来了,怎么才算整个加载完毕呢,是页面主要模块加载 ...

  7. Android 仿微信朋友圈发表图片拖拽和删除功能

    朋友圈实现原理 我们使用 Android Device Monitor 来分析朋友圈发布图片的界面实现原理.如果需要分析其他应用的界面实现也是采用这种方法哦. 打开 Android Device Mo ...

  8. Vue富文本编辑器(图片拖拽缩放)

    富文本编辑器(图片拖拽缩放) 需求: 根据业务要求,需要能够上传图片,且上传的图片能在移动端中占满屏幕宽度,故需要能等比缩放上传的图片,还需要能拖拽.缩放.改变图片大小.尝试多个第三方富文本编辑器,很 ...

  9. CSS 奇思妙想 | 使用 resize 实现强大的图片拖拽切换预览功能

    本文将介绍一个非常有意思的功能,使用纯 CSS 利用 resize 实现强大的图片切换预览功能.类似于这样: 思路 首先,要实现这样一个效果如果不要求可以拖拽,其实有非常多的办法. 将两张图片叠加在一 ...

随机推荐

  1. sql rowversion

    RowsVersion就是timestamp   丢失更新的解决方法      丢失更新概念:当用户同时修改一行数据,他们先读取数据,放在前端进行修改,当修改后,再提交数据,这样最后提交的数据会覆盖先 ...

  2. awk使用shell变量

    awk使用shell变量  (可以计算浮点数) 其实在awk里,是不能直接使用shell变量的 方法是:awk -v 选项让awk 里使用shell变量 TIME=60 awk -v time=&qu ...

  3. USACO翻译:USACO 2014 DEC Silver三题

    USACO 2014 DEC SILVER 一.题目概览 中文题目名称 回程 马拉松 奶牛慢跑 英文题目名称 piggyback marathon cowjog 可执行文件名 piggyback ma ...

  4. web api Route属性定义

    ASP.NET Web API路由,简单来说,就是把客户端请求映射到对应的Action上的过程.在"ASP.NET Web API实践系列03,路由模版, 路由惯例, 路由设置"一 ...

  5. 让代码重构渐行渐远系列(3)——string.Equals取代直接比较与非比较

    重构背景及原因 最近由于项目组的人员在不断扩充,导致项目中代码风格各异,大有百花齐放甚至怒放之势.考虑到团队的生存与发展,经过众人多次舌战之后,最终决定项目组根据业务分成几个小分队,以加强团队管理与提 ...

  6. easyUI datagrid 根据查询条件 选中对应数据的行

    开始 输入了 土豆,南瓜,再次是小青菜,每次输入点击搜索的时候(模糊查询),选中的当前数据对应的行 在做之前,在网上查询了许多资料,也在技术群里问过许多次,弄了好久终于好了. 第一次写博客真不知道写啥 ...

  7. perl 删除过期文件

    #!/usr/bin/perl `find /bak/ >list.txt`; open LIST,"/root/list.txt"; while (<LIST> ...

  8. 安装mysql

    查看已安装的mysql,并删除它们 rpm -qa|grep -i mysql rpm -e --nodeps filename 如果重装mysql,查找安装mysql产生的文件,并删除它们 find ...

  9. ASP.NET MVC 5 入门指南汇总

    经过前一段时间的翻译和编辑,我们陆续发出12篇ASP.NET MVC 5的入门文章.其中大部分翻译自ASP.NET MVC 5 官方教程,由于本系列文章言简意赅,篇幅适中,从一个web网站示例开始讲解 ...

  10. 使用VS2013分析DMP文件

    当一个发布的.NET应用程序出现app crash,无法通过日志分析异常原因时,就需要通过分析DMP文件了,传统方式是通过WinDbg来分析DMP文件,但是WinDbg用起来不是很方便,其实VS就是一 ...