记录一下canvas图片的拖动、缩放、旋转

10,873 阅读10分钟

我们都知道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)
}

image.png canvas的坐标原点默认是在左上角,就是从0,0位置开始画图,那么我们如何实现拖拽呢?思路是这样的:

  1. 当鼠标在canvas上按下的时候,这时候就会出现一个鼠标按下的坐标点,判断这个坐标点在不在图片上;
  2. 如果是在图片上,当鼠标按下开始移动时,先清除画布,然后按照移动的点重新画图;
  3. 如果不在图片上,则不做任何操作。 首先怎么确定鼠标按下的坐标点就是在图片上呢?我们看下面这张图 image.png         上面这张图片的宽和高分别是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;
  }
}

现在图片就可以随意移动了,效果如下:

Video_2021-12-31_133559.gif

旋转

旋转的思路跟拖拽的思路差不多,主要就是旋转角度的计算,思路是:鼠标按下旋转icon按钮,记录现在的转状态,坐标原点不动,把屏幕坐标旋转的角度转变成canvas坐标角度,用ctx.rotate来调整canva旋转的角度,最后画图
角度计算的方法,涉及到高中数学知识,直接在网上找了解答思路放在下面:

20202747-c8efbb3df5054e4fb74c675031d6e62e.png
如上图,鼠标事件中获取到的点(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')。

解:
image.png 如上图,从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'M
Math.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)
}

image.png 现在图标是这一个样子,可以根据旋转图标的样式改变,改变图标是否按下的方法,在这里是圆形的,所以方法如下:

  //判断鼠标是否在可旋转图标上按下
  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;
    }
  }

那么现在就达到了我们旋转的效果,效果图如下:

Video_2021-12-31_160801.gif

缩放

这里缩放我写的是用滚轮来进行缩放,缩放也比较简单\

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>