canvas简单的图像处理

学习canvas入门,实现一个简单的图像平移缩放切换层级,通过这个例子,能够明白基础的canvas编辑器类的操作,其中图片换成多边形是一样的道理,对于如果要绘制文本,需要测量文本的宽度,这里只写这个简单的例子

代码地址

加载图片

const loadImg = (src: string) => {
  return new Promise((resolve) => {
    const image = new Image()
    image.src = src
    // imge.crossOrigin = 'anonymous'
    image.addEventListener('load', () => {
      resolve(image)
    })
  })
}

如果需要最终通过toDataURL将canvas导出图片,则需要设置imge.crossOrigin = 'anonymous'解决跨域问题

路径拾取

所有的操作基于路径拾取,路径拾取基于以下几种方法

  • isPointInPath
    官方api,给定的点的坐标是否位于路径之内的话(包括路径的边)
  • 数学方法
    • 对于矩形,可以判断点的坐标x,y是否位于左上角点和其对角点的坐标之间
    • 对于圆,判断点和圆心的距离是否小于半径
    • 对于复杂的多边形,利用三角剖分将多边形划分成多个三角形,根据点到三角形各点与对应的边的叉积方向是否一致可以判断,所以只要点在其中一个三角形内,就在复杂多边形内


  private findIsInImgPath(imageItem: IImageItem, point: IPosition) {
    const { ctx } = this
    const { info } = imageItem
    const p1 = { x: info.x, y: info.y }
    const p2 = { x: info.x + info.width, y: info.y }
    const p3 = { x: info.x + info.width, y: info.y + info.height }
    const p4 = { x: info.x, y: info.y + info.height }
    ctx.save()
    ctx.beginPath()
    ctx.moveTo(p1.x, p1.y)
    ctx.lineTo(p2.x, p2.y)
    ctx.lineTo(p3.x, p3.y)
    ctx.lineTo(p4.x, p4.y)
    ctx.closePath()
    ctx.restore()
    return ctx.isPointInPath(point.x, point.y)
  }


  private findIsInCornerPath(imageItem: IImageItem, point: { x: number; y: number }) {
    const cornerList = getCornerPath(imageItem.info, this.cornerSize)
    const { ctx, cornerSize } = this
    for (const item of cornerList) {
      const p1 = { x: item.x, y: item.y }
      const p2 = { x: item.x + cornerSize, y: item.y }
      const p3 = { x: item.x + cornerSize, y: item.y + cornerSize }
      const p4 = { x: item.x, y: item.y + cornerSize }
      ctx.save()
      ctx.beginPath()
      ctx.moveTo(p1.x, p1.y)
      ctx.lineTo(p2.x, p2.y)
      ctx.lineTo(p3.x, p3.y)
      ctx.lineTo(p4.x, p4.y)
      ctx.closePath()
      ctx.restore()
      if (ctx.isPointInPath(point.x, point.y)) {
        return {
          position: item.position
        }
      }
    }
    return false
  }

这里直接使用api

移动

对于移动,只需要知道从上一点到下一点移动的向量,然后左上角的点的向量加上移动的向量,并重新绘图即可

    // move
    const isInImg = this.findIsInImgPath(this.selectImg, { x: e.offsetX, y: e.offsetY })
    if (isInImg) {
      this.canvas.style.cursor = 'move'
      if (this.canEdit) {
        const offsetObj = { x: e.pageX - this.mousedownPoint.x, y: e.pageY - this.mousedownPoint.y }
        // eslint-disable-next-line operator-assignment

        this.selectImg.info.x = this.selectImg.info.x + offsetObj.x
        // eslint-disable-next-line operator-assignment
        this.selectImg.info.y = this.selectImg.info.y + offsetObj.y
        this.refresh()
        this.mousedownPoint = { x: e.pageX, y: e.pageY }
      }
      return
    }

计算之后,需要重置上一点的坐标为最新鼠标的点位坐标

缩放

此处指的是不改变原图片的尺寸比例进行缩放

  • 路径拾取找到在四个缩放的角上
  handleCanvasMousedown = (e: MouseEvent) => {
    this.mousedownPoint = {
      x: e.pageX,
      y: e.pageY
    }
    if (this.selectImg) {
      const inCornerObj = this.findIsInCornerPath(this.selectImg, { x: e.offsetX, y: e.offsetY })
      if (inCornerObj) {
        this.canEdit = true
        this.eventingObj = {
          position: inCornerObj.position
        }
        return
      }
    }


    const isInImg = this.mapWalker({ x: e.offsetX, y: e.offsetY })
    if (isInImg) {
      this.canEdit = true
    }
    this.refresh()
  }

这里需要变量控制表示在缩放,否在放的时候鼠标到了图片外,就丢失操作状态了,因为鼠标已经不在角上了

  • 找到缩放轴
    左上到右下,左下到右上,两条缩放轴,根据此时处在哪个点,确定在哪条轴
  const { position } = this.eventingObj
  const { ltToRb, lbToRt } = this.selectImg.axiosMap
  const axiosVec = ['LT', 'RB'].includes(position) ? ltToRb : lbToRt
  • 确定缩放量
    此时缩放轴是序列化长度为1的向量,根据移动的向量和缩放轴的点积,计算出沿着轴的缩放长度,由于

投影向量长度 / 对角线向量的长度 === 投影向量在x轴的投影 / 对角线向量在x轴的投影(对角线向量的x坐标)
对角线向量为序列化的标准向量 长度为1,所以
投影向量在x轴的投影 = 投影向量长度 * 对角线向量在x轴的投影(对角线向量的x坐标)

由此可以计算出实际沿着缩放轴缩放的向量的坐标

const getScaleMap = (point1: IPosition, point2: IPosition) => {
  const projectionLen = point1.x * point2.x + point1.y * point2.y
  if (point1.x === 0 && point2.x === 0) {
    return {
      x: 0,
      y: 0
    }
  }
  return {
    /**
     * 投影向量长度 / 对角线向量的长度 === 投影向量在x轴的投影 / 对角线向量在x轴的投影(对角线向量的x坐标)
     * 对角线向量为序列化的标准向量 长度为1,所以
     * 投影向量在x轴的投影 = 投影向量长度 * 对角线向量在x轴的投影(对角线向量的x坐标)
     */
    x: projectionLen * point2.x,
    // y轴同理
    y: projectionLen * point2.y
  }

}
  • 执行缩放
    此处取的是两条轴 ltToRb:左上到右下 lbToRt:左下到右上
    坐标都是沿着轴变化的,所以坐标的变动都是+,正负号由投影的正负性决定
    宽高沿着轴的变化是绝对的,所以需要沿着轴方向y是负的需要加负号
 /**
       * 此处取的是两条轴 ltToRb:左上到右下  lbToRt:左下到右上
       * 坐标都是沿着轴变化的,所以坐标的变动都是+,正负号由投影的正负性决定
       * 宽高沿着轴的变化是绝对的,所以需要沿着轴方向y是负的需要加负号
       */
      if (position === 'LT') {
        // eslint-disable-next-line operator-assignment

        this.selectImg.info = {
          ...this.selectImg.info,
          x: x + scaleVec.x,
          y: y + scaleVec.y,
          width: width - scaleVec.x,
          height: height - scaleVec.y
        }
      } else if (position === 'RB') {
        // eslint-disable-next-line operator-assignment
        this.selectImg.info = {
          ...this.selectImg.info,
          width: width + scaleVec.x,
          height: height + scaleVec.y
        }
      } else if (position === 'LB') {
        // eslint-disable-next-line operator-assignment
        this.selectImg.info = {
          ...this.selectImg.info,
          x: x + scaleVec.x,
          width: width - scaleVec.x,
          height: height - -scaleVec.y
        }
      } else if (position === 'RT') {
        // eslint-disable-next-line operator-assignment
        this.selectImg.info = {
          ...this.selectImg.info,
          y: y + scaleVec.y,
          width: width + scaleVec.x,
          height: height + -scaleVec.y
        }
      }

      this.refresh()
      this.mousedownPoint = { x: e.pageX, y: e.pageY }

改变层级

改变层级只需要改变图片信息对象在数组中的位置,重新绘图即可



const swapArrayElements = (array: any, index1: number, index2: number) => {
  const newArray = [...array]
  ;[newArray[index1], newArray[index2]] = [newArray[index2], newArray[index1]]
  return newArray
}

  handleKeydown = (event: KeyboardEvent) => {
    if (!this.selectImg) {
      return
    }
    const index = this.imageList.findIndex((v) => v.id === this.selectImg?.id)
    if (event.code === 'ArrowUp') {
      if (index === this.imageList.length - 1) {
        return
      }
      this.imageList = swapArrayElements(this.imageList, index, index + 1)
    } else if (event.code === 'ArrowDown') {
      if (index === 0) {
        return
      }
      this.imageList = swapArrayElements(this.imageList, index - 1, index)
    }
    this.refresh()
  }

总结

简单的canvas绘图在于处理其中的数据,利用数据去绘图

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYgKynQo' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片