我们都知道canvas是一个位图,你可以在里面渲染图片、文字等等,但是canvas没有属性来操作渲染的东西,例如用canvas画一张图片,想要操作图片就没办法直接通过属性来操作图片,因为拿到的只能是canvas对象,那么我们怎么操作canvas内的图片呢?
DOM
方面就不说了,直接贴代码
<canvas width="800" height="800" style="border: 1px solid #000;" id="canvas"></canvas>
拖动
首先我们画一个图
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = './03131604882900.jpg';
img.onload = () => {
ctx.drawImage(img, 0, 0)
}
canvas的坐标原点默认是在左上角,就是从0,0
位置开始画图,那么我们如何实现拖拽呢?思路是这样的:
- 当鼠标在canvas上按下的时候,这时候就会出现一个鼠标按下的坐标点,判断这个坐标点在不在图片上;
- 如果是在图片上,当鼠标按下开始移动时,先清除画布,然后按照移动的点重新画图;
- 如果不在图片上,则不做任何操作。
首先怎么确定鼠标按下的坐标点就是在图片上呢?我们看下面这张图
上面这张图片的宽和高分别是
200*200
,坐标也是从0开始,那么图片占据多大的位置,从画图的起始点加上图片的宽就是图片横向占据的位置,从起始点加上图片的高就是图片占据纵向占据的位置,x轴从坐标0开始加上图片宽度200,y轴从0开始加上图片宽度200就是图片的所在位置,这些参数我们画图的时候都是知道的,图片的最小值是0,0
,最大值是200,200
,用我们通俗的话来说想知道一个坐标点在不在图片上,大于图片的最小值并且小于图片的最大值,那么就说明这个坐标在图片上。并且是x轴和y轴要同时满足,代码实现是:图片x轴最小值 < x && x < 图片x轴最大值 && 图片y轴最小值 < y && y < 图片y轴最大值
只要是满足这个公式,就能确定鼠标点击的点在不在图片上
下面我们换种方式来画图,通过改变坐标原点的方式,让坐标原点始终在图片的中心点,
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var imgH, imgW;
//这里用改变坐标原点的方式来画图,让坐标原点始终在图片的中心
var PO = { x: 0, y: 0 };
var img = new Image();
img.src = './03131604882900.jpg';
img.onload = () => {
imgH = img.height;
imgW = img.width;
//记录一下canvas原点 为图片的中心点
PO = { x: imgW / 2, y: imgH / 2};
//改变画布的中心点
ctx.translate(PO.x, PO.y);
onDraw();
}
onDraw = () => {
//先清除画布,清除两倍的画布,因为要改变坐标原点,只有这样才能不管原点在哪里都能完全清除画布
ctx.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
//画图片,因为原点在图片的中心点,所以每次画图只需要从图片的负一半坐标开始画,就能看到我们想要的效果
ctx.drawImage(img, -imgW / 2, -imgH / 2)
}
通过这种方式画的图,不管怎么拖拽,我们只要改变坐标原点就行,不用考虑图片应该在哪里,因为原点始终在图片的中心点。
这种方法就有一个问题,屏幕坐标和canvas坐标现在不是同步的,因为canvas原点是随时需要变化的,而屏幕坐标是没有变化的,所以我们需要一个方法来把屏幕坐标转化为canvas坐标。
方法如下:
//判断鼠标是否在图片上按下
imgIsDown = (x, y) => {
//找到图片的最小值和最大值,因为画图是从-imgW / 2开始的,那么这就是图片占据的位置的最小值,最大值是imgW / 2,y轴同理
return -imgW / 2 < x && x < imgW / 2 && -imgH / 2 < y && y < imgH / 2;
}
//window屏幕坐标转化为canvas坐标
convertCoordinate = (x, y) => {
//在屏幕坐标系中,相对canvas坐标系原点PO的偏移,所以要减去canvas坐标原点
x = x - PO.x;
y = y - PO.y;
return { x: x, y: y }
}
然后开始写代码,实现思路是:鼠标按下,保存现在开始的坐标,当屏幕坐标出现偏移,然后把屏幕坐标的偏移量转化为canvas坐标的偏移量,计算出现在canvas原点坐标,用ctx.translate方法改变原点,最后画图,保存现在改变的值,为下一次移动做准备
//全局定义变量,来保存开始的屏幕的坐标
var beginX,beginY;
var canMove = false;
canvas.onmousedown = (e) => {
//e.offsetX是鼠标点击到canvas边的位置
beginX = e.offsetX;
beginY = e.offsetY;
//把点击的win坐标转为canvas坐标
var Cp = convertCoordinate(beginX, beginY)
//判断在canvas坐标点上是否在图片上
canMove = imgIsDown(Cp.x, Cp.y)
if (canMove) {
canvas.onmousemove = (e) => {
var x = e.offsetX;
var y = e.offsetY;
//算出来移动的像素(每次都是减去上次的值)
var Mx = x - beginX;
var My = y - beginY;
//Mx和My是win上面移动的像素,还需要转为canvas坐标,加上坐标是因为要从坐标原点开始算
var CPO = convertCoordinate(Mx + PO.x, My + PO.y);
//改变canvas原点坐标
ctx.translate(CPO.x, CPO.y)
onDraw()
//画完以后要保存起来最终画到哪里了
PO.x += Mx; //canvas坐标原点
PO.y += My;
beginX = x; //保存起来这次图画到了哪里
beginY = y;
}
}
document.onmouseup = () => {
canvas.onmousemove = null;
document.onmouseup = null;
canMove = false;
}
}
现在图片就可以随意移动了,效果如下:
旋转
旋转的思路跟拖拽的思路差不多,主要就是旋转角度的计算,思路是:鼠标按下旋转icon按钮,记录现在的转状态,坐标原点不动,把屏幕坐标旋转的角度转变成canvas坐标角度,用ctx.rotate来调整canva旋转的角度,最后画图
角度计算的方法,涉及到高中数学知识,直接在网上找了解答思路放在下面:
如上图,鼠标事件中获取到的点(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'MMath.cos(β);
y'=O'MMath.sin(β); //可得M在X'O'Y'坐标系中的坐标(x',y')
这样就可以计算到旋转以后的canvas的坐标,我们把convertCoordinate
方法修改一下
//初始化旋转角度是0,没有旋转。
var routate = 0;
//window屏幕坐标转化为canvas坐标
convertCoordinate = (x, y) => {
//在屏幕坐标系中,相对canvas坐标系原点PO的偏移,所以要减去canvas坐标原点
x = x - PO.x;
y = y - PO.y;
//如果没有旋转,那么只计算偏移量就行,不用考虑角度
if (rotate != 0) {
//Math.sqrt是两点之间的距离图中OM的距离,简化版本,正确用法应该是Math.sqrt((x-0)*(x-0) + (y-0)*(y-0))
var len = Math.sqrt(x * x + y * y);
//屏幕坐标系中 PO与按下点连线 与屏幕坐标系X轴的夹角弧度
var oldR = Math.atan2(y, x);
//canvas坐标系中PO与按下点连线 与canvas坐标系x轴的夹角弧度
var newR = oldR - rotate;
//最终算出来canvas坐标系上的M点
x = len * Math.cos(newR);
y = len * Math.sin(newR);
}
return { x: x, y: y }
}
还需要画出来一个可以旋转的icon,修改一下代码
var imgIcon = new Image();
//可旋转图标是一个圆形,所以记录一下半径,等会判断是否在图标上好判断
var iconR = 0;
imgIcon.src = './xuanzhuan.png';
img.onload = () => {
imgH = img.height;
imgW = img.width;
//记录一下canvas原点 为图片的中心点,因为旋转图标在外面,初始化改变一下位置,离边远一点
PO = { x: imgW / 2 + 50, y: imgH / 2 + 50 };
//改变画布的中心点
ctx.translate(PO.x, PO.y);
imgIcon.onload = () => {
iconR = imgIcon.width / 2
onDraw();
}
}
onDraw = () => {
//先清除画布,清除两倍的画布,因为要改变坐标原点,只有这样才能不管原点在哪里都能完全清除画布
ctx.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
//画图片,因为原点在图片的中心点,所以每次画图只需要从图片的负一半坐标开始画,就能看到我们想要的效果
ctx.drawImage(img, -imgW / 2, -imgH / 2);
//可旋转图标
ctx.drawImage(imgIcon, imgW / 2, -imgH / 2 - iconR)
}
现在图标是这一个样子,可以根据旋转图标的样式改变,改变图标是否按下的方法,在这里是圆形的,所以方法如下:
//判断鼠标是否在可旋转图标上按下
iconIsDown = (x, y) => {
//原理,只要点击的点,与图标圆心的距离,不超过图标的半径,那么就说明点击的坐标在图标上
//先根据图标的位置,计算出圆心点的位置
var RX = imgW / 2 + iconR;
var RY = -imgH / 2;
//Math.sqrt可以获取到canvas两个点之间的距离,A点{x1,y1} B点{x2,y2}
return Math.sqrt((RX - x) * (RX - x) + (RY - y) * (RY - y)) <= iconR
}
可以找到可旋转图标的位置以后,就要修改canvas.onmousedown方法了,增加旋转的功能
canvas.onmousedown = (e) => {
//e.offsetX是鼠标点击到canvas边的位置
beginX = e.offsetX;
beginY = e.offsetY;
//把点击的win坐标转为canvas坐标
var Cp = convertCoordinate(beginX, beginY)
//判断在canvas坐标点上是否在图片上
canMove = imgIsDown(Cp.x, Cp.y)
canRotate = iconIsDown(Cp.x, Cp.y)
canvas.onmousemove = (e) => {
var x = e.offsetX;
var y = e.offsetY;
//拖拽
if (canMove) {
//算出来移动的像素(每次都是减去上次的值)
var Mx = x - beginX;
var My = y - beginY;
//Mx和My是win上面移动的像素,还需要转为canvas坐标,加上坐标是因为要从坐标原点开始算
var CPO = convertCoordinate(Mx + PO.x, My + PO.y);
//改变canvas原点坐标
ctx.translate(CPO.x, CPO.y)
onDraw()
//画完以后要保存起来最终画到哪里了
PO.x += Mx; //canvas坐标原点
PO.y += My;
beginX = x; //保存起来这次图画到了哪里
beginY = y;
}
//旋转
if (canRotate) {
//还是先算出来canvas坐标
var CP = convertCoordinate(x, y);
var Cx = CP.x, Cy = CP.y;
//根据坐标算出来旋转的角度,这里减去一个50°是因为可旋转图标在图标的右上角,往上是逆时针,所以要减去他原有的角度,根据可旋转图标的位置来更改这个初始角度
var newR = Math.atan2(Cx, -Cy) - 50 * Math.PI / 180;
//旋转canvas画布
ctx.rotate(newR);
//记录一下现在的角度
rotate += newR;
onDraw()
}
}
document.onmouseup = () => {
canvas.onmousemove = null;
document.onmouseup = null;
canMove = false;
canRotate = false;
}
}
那么现在就达到了我们旋转的效果,效果图如下:
缩放
这里缩放我写的是用滚轮来进行缩放,缩放也比较简单\
var scale = 1;
//保存一下图片的初始值,因为imgW,imgH这两个,我们上面用来控制宽度位置,所以这两个需要频繁改动,保存两个不动的值
var initImgW, initImgH;
var canScale = false;
img.onload = () => {
//修改这里,保存一下图片的初始值
initImgW = imgH = img.height;
initImgH = imgW = img.width;
//记录一下canvas原点 为图片的中心点,因为旋转图标在外面,初始化改变一下位置,离边远一点
PO = { x: imgW / 2 + 50, y: imgH / 2 + 50 };
//改变画布的中心点,
ctx.translate(PO.x, PO.y);
imgIcon.onload = () => {
iconR = imgIcon.width / 2
onDraw();
}
}
onDraw = () => {
//先清除画布,清除两倍的画布,因为要改变坐标原点,只有这样才能不管原点在哪里都能完全清除画布
ctx.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
//画图片,因为原点在图片的中心点,所以每次画图只需要从图片的负一半坐标开始画,就能看到我们想要的效果
ctx.drawImage(img, -imgW / 2, -imgH / 2, imgW, imgH); //这里再加上两个参数,这两个参数是告诉canvas需要画多宽多高
//可旋转图标
ctx.drawImage(imgIcon, imgW / 2, -imgH / 2 - iconR)
}
//滚轮操作
canvas.onmousewheel = (e) => {
var x = e.offsetX;
var y = e.offsetY;
var Cp = convertCoordinate(x, y)
//同样需要判断如果鼠标在图片上,才允许缩放
canScale = imgIsDown(Cp.x, Cp.y)
if (canScale) {
//e.wheelDelta如果大于0,证明鼠标是向上滚动,反之向下
if (e.wheelDelta > 0) {
//放大的倍数可以根据实际情况定义,可以丝滑一点,也可以控制最大放大倍数和最小倍数
scale += 0.04
}
if (e.wheelDelta < 0) {
scale -= 0.04
}
//不管放大还是缩下,都是用初始的宽高,来放大或者缩小
imgW = scale * initImgW;
imgH = scale * initImgH;
onDraw()
}
}
总结
频繁改动坐标原点,只适合元素少,或者都需要跟着变化的,如果元素比较多,而又只需要移动其中一部分的话,改动起来就比较复杂,感兴趣的同学可以用canvas的save和restore方法来解决这个问题。
demo源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas图片的拖动、缩放、旋转</title>
</head>
<body>
<canvas width="800" height="800" style="border: 1px solid #000;" id="canvas"></canvas>
</body>
<script>
/**
* @param ctx 画布
* @param img 图片
* @param imgIcon 可旋转标志
* @param imgH 图片的高
* @param imgW 图片的宽
* @param initImgW 图片的初始宽
* @param initImgH 图片的初始高
* @param iconR 可旋转标志的半径
* @param beginX 最终渲染x轴坐标
* @param beginY 最终渲染y轴坐标
* @param PO 原点坐标
* @param scale 缩放比例
* @param rotate 旋转角度
*/
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var imgH, imgW, beginX, beginY;
var initImgW, initImgH;
var canMove = canRotate = canScale = false;
var rotate = 0;
var iconR = 0;
var scale = 1;
//这里用改变坐标原点的方式来画图,让坐标原点始终在图片的中心
var PO = { x: 0, y: 0 };
var img = new Image();
var imgIcon = new Image();
img.src = './03131604882900.jpg';
imgIcon.src = './xuanzhuan.png';
img.onload = () => {
initImgW = imgH = img.height;
initImgH = imgW = img.width;
//记录一下canvas原点 为图片的中心点,因为旋转图标在外面,初始化改变一下位置,离边远一点
PO = { x: imgW / 2 + 50, y: imgH / 2 + 50 };
//改变画布的中心点,
ctx.translate(PO.x, PO.y);
imgIcon.onload = () => {
iconR = imgIcon.width / 2
onDraw();
}
}
onDraw = () => {
//先清除画布,清除两倍的画布,因为要改变坐标原点,只有这样才能不管原点在哪里都能完全清除画布
ctx.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
//画图片,因为原点在图片的中心点,所以每次画图只需要从图片的负一半坐标开始画,就能看到我们想要的效果
ctx.drawImage(img, -imgW / 2, -imgH / 2, imgW, imgH);
//可旋转图标
ctx.drawImage(imgIcon, imgW / 2, -imgH / 2 - iconR)
}
//判断鼠标是否在图片上按下
imgIsDown = (x, y) => {
//找到图片的最小值和最大值,因为画图是从-imgW / 2开始的,那么这就是图片占据的位置的最小值,最大值是imgW / 2,y轴同理
return -imgW / 2 < x && x < imgW / 2 && -imgH / 2 < y && y < imgH / 2;
}
//判断鼠标是否在可旋转图标上按下
iconIsDown = (x, y) => {
//原理,只要点击的点,与图标圆心的距离,不超过图标的半径,那么就说明点击的坐标在图标上
//先根据图标的位置,计算出圆心点的位置
var RX = imgW / 2 + iconR;
var RY = -imgH / 2;
//Math.sqrt可以获取到canvas两个点之间的距离,A点{x1,y1} B点{x2,y2}
return Math.sqrt((RX - x) * (RX - x) + (RY - y) * (RY - y)) <= iconR
}
//window屏幕坐标转化为canvas坐标
convertCoordinate = (x, y) => {
//在屏幕坐标系中,相对canvas坐标系原点PO的偏移,所以要减去canvas坐标原点
x = x - PO.x;
y = y - PO.y;
//如果没有旋转,那么只计算偏移量就行,不用考虑角度
if (rotate != 0) {
//Math.sqrt是两点之间的距离图中OM的距离,简化版本,正确用法应该是Math.sqrt((x-0)*(x-0) + (y-0)*(y-0))
var len = Math.sqrt(x * x + y * y);
//屏幕坐标系中 PO与按下点连线 与屏幕坐标系X轴的夹角弧度
var oldR = Math.atan2(y, x);
//canvas坐标系中PO与按下点连线 与canvas坐标系x轴的夹角弧度
var newR = oldR - rotate;
//最终算出来canvas坐标系上的M点
x = len * Math.cos(newR);
y = len * Math.sin(newR);
}
return { x: x, y: y }
}
canvas.onmousedown = (e) => {
//e.offsetX是鼠标点击到canvas边的位置
beginX = e.offsetX;
beginY = e.offsetY;
//把点击的win坐标转为canvas坐标
var Cp = convertCoordinate(beginX, beginY)
//判断在canvas坐标点上是否在图片上
canMove = imgIsDown(Cp.x, Cp.y)
canRotate = iconIsDown(Cp.x, Cp.y)
canvas.onmousemove = (e) => {
var x = e.offsetX;
var y = e.offsetY;
//拖拽
if (canMove) {
//算出来移动的像素(每次都是减去上次的值)
var Mx = x - beginX;
var My = y - beginY;
//Mx和My是win上面移动的像素,还需要转为canvas坐标,加上坐标是因为要从坐标原点开始算
var CPO = convertCoordinate(Mx + PO.x, My + PO.y);
//改变canvas原点坐标
ctx.translate(CPO.x, CPO.y)
onDraw()
//画完以后要保存起来最终画到哪里了
PO.x += Mx; //canvas坐标原点
PO.y += My;
beginX = x; //保存起来这次图画到了哪里
beginY = y;
}
//旋转
if (canRotate) {
//还是先算出来canvas坐标
var CP = convertCoordinate(x, y);
var Cx = CP.x, Cy = CP.y;
//根据坐标算出来旋转的角度,这里减去一个50°是因为可旋转图标在图标的右上角,往上是逆时针,所以要减去他原有的角度,根据可旋转图标的位置来更改这个初始角度
var newR = Math.atan2(Cx, -Cy) - 50 * Math.PI / 180;
//旋转canvas画布
ctx.rotate(newR);
//记录一下现在的角度
rotate += newR;
onDraw()
}
}
document.onmouseup = () => {
canvas.onmousemove = null;
document.onmouseup = null;
canMove = false;
canRotate = false;
}
}
canvas.onmousewheel = (e) => {
var x = e.offsetX;
var y = e.offsetY;
var Cp = convertCoordinate(x, y)
//同样需要判断如果鼠标在图片上,才允许缩放
canScale = imgIsDown(Cp.x, Cp.y)
if (canScale) {
//e.wheelDelta如果大于0,证明鼠标是向上滚动,反之向下
if (e.wheelDelta > 0) {
//放大的倍数可以根据实际情况定义,可以丝滑一点
scale += 0.04
}
if (e.wheelDelta < 0) {
scale -= 0.04
}
//不管放大还是缩下,都是用初始的宽高,来放大或者缩小
imgW = scale * initImgW;
imgH = scale * initImgH;
onDraw()
}
}
</script>
</html>