引言

在上一篇日志“canvas 图片拖拽旋转之一”中,对坐标转换有了比较深入的了解,但是仅仅利用坐标转换实现的拖拽旋转,会改变canvas坐标系的状态,从而影响画布上其他元素的绘制。因此,这个时候需要用到一对canvas方法,在变换坐标系前保存canvas状态,在变换并绘制完成之后,恢复canvas状态,即save()和restore()。

[备注]

这篇文章只是记录分享下解决问题的过程,找我要demo的,或者问我什么东西怎么做的,就不要加我了。你可以加一个canvas相关的交流群,或者如果需要用到KineticJS/FabricJS的话,可以加群251572039。

 一、理解save和restore的操作对象

对于save和restore方法,有一个误解就是,认为每一步都save之后restore就等同于ctrl+z。其实save保存的只是CanvasRenderingContext2D 对象的状态以及对象的所有属性,并不包括这个对象上绘制的图形。引用一段w3school上的解释:

save() 和 restore() 方法允许你保存和恢复一个 CanvasRenderingContext2D 对象的状态。save() 把当前状态推入到栈中,而 restore() 从栈的顶端弹出最近保存的状态,并且根据这些存储的值来设置当前绘图状态。

CanvasRenderingContext2D 对象的所有属性(除了画布的属性是一个常量)都是保存的状态的一部分。变换矩阵和剪切区域也是这个状态的一部分,但是当前路径和当前点并不是。

也就是说,save()可以保存canvas的状态(比如坐标系的状态)以及fillStyle、strokeStyle、lineWidth 等等属性。

基于这一点,我们就可以在变换坐标系之前save(),变换并绘制完成后restor(),这样就可以保证图形发生了旋转偏移而canvas坐标系仍然是屏幕坐标系的状态(类似于拿了一把标尺画完图之后又把标尺放回了原位)。关键代码如下:

ctx.save();
ctx.translate(PO.x,PO.y);//坐标原点移动到图片中心点
ctx.rotate(now_rotate_radian);//旋转画布 在屏幕坐标系基础上旋转 now_rotate_radian
ctx.drawImage(img,-imgW/2,-imgH/2);
drawRotateCtrl();
ctx.restore();//还原画布坐标系

二、实现思路

因为涉及到旋转,所以不管是进行拖拽还是旋转,每次绘制都是先将canvas坐标系原点translate到图片中心点,然后rotate旋转的角度。

根据坐标变换原则,在旋转时计算角度变化(鼠标相对于图片中心点与屏幕坐标系y轴负方向的夹角),拖拽时记录图片中心点偏移。

三、代码实现

  demo链接:http://youryida.duapp.com/demo_canvas/img_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>
<br/>
<pre>
一、因为涉及到旋转,所以不管是进行拖拽还是旋转,每次绘制都是先将canvas坐标系原点移到图片中心点。旋转时记录角度变化,拖拽时记录图片中心点偏移。
二、为了不影响坐标系变换后其他图形的绘制,使用ctx.save()保存旋转前的坐标系,绘制完成后ctx.restore()恢复。
QQ:1140215489
</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 PO;
var Selected_Round_R=12;
var now_rotate_radian=0;//记录图片的旋转角度 初始为0
var isDown=false;
var moveAble=false,rotateAble=false;
var imgH,imgW;
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};
onDraw();
}
function imgIsDown(x,y){
var P={x:x,y:y};
var A={x:PO.x-imgW/2,y:PO.y-imgH/2};
var B={x:PO.x+imgW/2,y:PO.y-imgH/2};
var C={x:PO.x+imgW/2,y:PO.y+imgH/2};
var D={x:PO.x-imgW/2,y:PO.y+imgH/2};
A=getRotatedPoint(A,PO,now_rotate_radian);
B=getRotatedPoint(B,PO,now_rotate_radian);
C=getRotatedPoint(C,PO,now_rotate_radian);
D=getRotatedPoint(D,PO,now_rotate_radian);
var APB=getRadian(A,P,B);
var BPC=getRadian(B,P,C);
var CPD=getRadian(C,P,D);
var DPA=getRadian(D,P,A);
var sum=(APB+BPC+CPD+DPA).toFixed(5);//某点与矩形四顶点夹角之和=360那么判断点在矩形内
return (sum==(2*Math.PI).toFixed(5));
}
function RTIsDown(x,y){
var RT={x:PO.x,y:PO.y-imgH/2-Selected_Round_R};
var rotate_top=getRotatedPoint(RT,PO,now_rotate_radian);
var bool=(x-rotate_top.x)*(x-rotate_top.x)+(y-rotate_top.y)*(y-rotate_top.y)<=Selected_Round_R*Selected_Round_R;
return bool;
}
function onDraw(){
ctx.clearRect(0,0,cvsW,cvsH);
ctx.save();
ctx.translate(PO.x,PO.y);//坐标原点移动到图片中心点
ctx.rotate(now_rotate_radian);//旋转画布 在屏幕坐标系基础上旋转 now_rotate_radian
ctx.drawImage(img,-imgW/2,-imgH/2);
drawRotateCtrl();
ctx.restore();//还原画布坐标系
}
function drawRotateCtrl(){
var rotate_top={x:0,y:-imgH/2-Selected_Round_R};
ctx.beginPath();
ctx.arc(rotate_top.x,rotate_top.y,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 startMove(){
preventDefault();
isDown=true;
var loc=getEvtLoc();
var x=loc.x,y=loc.y;
beginX=x,beginY=y;
moveAble=imgIsDown(x,y);
rotateAble=RTIsDown(x,y);
if (moveAble) cvs.style.cursor="move";
if (rotateAble) cvs.style.cursor="crosshair";
}
function moving(){
preventDefault();
if(isDown==false) return;
var loc=getEvtLoc();
var x=loc.x,y=loc.y;
if(moveAble){
var dx=x-beginX,dy=y-beginY;
PO.x=PO.x+dx;
PO.y=PO.y+dy;
onDraw();
}
if(rotateAble){
var A={x:beginX-PO.x,y:beginY-PO.y};//转换为以PO为原点的屏幕坐标系 计算鼠标move前后两点与y轴反方向的角度
var B={x:x-PO.x,y:y-PO.y};
var a=Math.atan2(A.x,-A.y);
var b=Math.atan2(B.x,-B.y);
var θ=b-a;
now_rotate_radian+=θ;
onDraw();
}
beginX=x,beginY=y;
}
function endMove(){
isDown=false;
moveAble=rotateAble=false;
cvs.style.cursor="auto";
}
function getEvtLoc(){
return {x:event.offsetX,y:event.offsetY}
}
//取消浏览器默认事件
function preventDefault(){
if(event.preventDefault){
event.preventDefault();
}else{
event.returnValue = false;//注意加window
}
}
//获取三点角度
function getRadian(A,O,B) {
var Xo=O.x,Yo=O.y;
var Xa=A.x,Ya=A.y;
var Xb=B.x,Yb=B.y;
var oa = Math.sqrt((Xo - Xa) * (Xo - Xa) + (Yo - Ya)* (Yo - Ya));
var ob = Math.sqrt((Xo - Xb) * (Xo - Xb) + (Yo - Yb)* (Yo - Yb));
var ab = Math.sqrt((Xa - Xb) * (Xa - Xb) + (Ya - Yb)* (Ya - Yb));
var aob = Math.acos((oa * oa + ob * ob - ab * ab) / (2 * oa * ob)); // 弧度
return aob;//
}
//获取绕点旋转角度后的新点坐标
function getRotatedPoint(A,O,α){
var dx =A.x-O.x;
var dy =A.y-O.y;
var x=Math.cos(α)*dx-Math.sin(α)*dy+O.x;
var y=Math.cos(α)*dy+Math.sin(α)*dx+O.y;
return {x:x,y:y};
} </script>
</body>
</html>

最新文章

  1. sql union和union all的用法及效率
  2. 备忘:spring jdbc事务代码 mybatis, nhibernate
  3. Linux定时任务Crontab详解
  4. PHP: 深入pack/unpack
  5. Unity3d 保存和使用地形高度
  6. Y2K Accounting Bug(贪心)
  7. SELECT INTO和INSERT INTO SELECT
  8. 记录:asp.net mvc 中 使用 jquery 实现html5 实现placeholder 密码框 提示兼容password IE6
  9. Android基础之Activity launchMode详解
  10. oc-27-@property的参数
  11. 终端I/O之波特率函数
  12. 如何在Win10中启用和关闭管理员账户?
  13. UVA 11235 Frequent values(RMQ)
  14. SQL2008将服务器的数据库表数据插入到本地数据库
  15. 在vue中使用css modules替代scroped
  16. 使用ubuntu做为dotnet core开发环境
  17. npm vue ivew vue-cli3
  18. vue-resource post请求后台接口报400(跨域问题解决方法)
  19. Kubernetes 持续集成 SpringCloud
  20. js基础概念-操作符

热门文章

  1. ArcGIS AddIN开发:如何调用ArcMap中的选择工作空间的窗体
  2. [收藏]C++简单五子棋
  3. java面向对象_static关键字
  4. 构建 Android 应用程序一定要绕过的 30 个坑
  5. webserver几个例子
  6. 【实战Java高并发程序设计 2】无锁的对象引用:AtomicReference
  7. Call for Papers IEEE/ACM International Conference on Advances in Social Network Analysis and Mining (ASONAM)
  8. AMD and CMD are dead之js模块化黑魔法
  9. 从零3D基础入门XNA 4.0(1)——3D开发基础
  10. 初试ASP.NET Web API/MVC API(附Demo)