林大大又来更新啦~

注意:最近蛮多童鞋想要了解关于截图拖动及导出的全部源码,我这里原生写了个demo,参考仓库地址如下! 

图片截取拖拽导出实战demo

好了废话不多说,开始正文,这次主要做的是关于canvas图像绘制的部分,具体功能如下

0、图像的缩放

1、图像的拖动

2、图像的截图及导出 

主页面结构

    <div
      ref="box-main"
      class="box-main"
    >
      <div
        id="preview-box"
        class="preview-box"
        @mousewheel="(e) => scaleDom(e, 'wheel')"
      >
        <img
          class="preview-img"
          :src="previewImg.src"
        >
      </div>
    </div>

 本文参考所需的主要参数及参数赋值

    // 当前底图
    previewImg: {
        target: null, // 图像对象
        src: '' // 图像地址
    },
    // canvas绘制参数
    pageImage: {
        imgX: 0, // canvas图像距离左上角 x轴距离
        imgY: 0, // canvas图像距离左上角 y轴距离
        imgScale: 0, // canvas实际默认为1
        minImgScale: 0, // canvas实际默认为1最小为0.2
        maxImgScale: 0, // canvas实际默认为1最大为2
        unit: 0, // 实际和展示canvas 中间的转换比例单位
        scale: 100, // 展示canvas比例
        origin: {} // 图片原始宽高
      }


    /**
     * 初始化图片url
     */
    initImage() {
      this.previewImg.target = document.getElementById('preview-box')
      this.previewImg.src = 'xxx'
      this.calcImage()
    },
    
    /**
     * 计算图片原始比例
     */
    calcImage() {
      const image = new Image()
      image.onload = () => {
        const parent = this.$refs['box-main']
        if (this.pageImage.scale === 100) { // 标准原图模式
          this.pageImage.imgScale = Number(((parent.offsetHeight - 4) / image.height).toFixed(6))
          this.pageImage.unit = 100 / Number(this.pageImage.imgScale.toFixed(6)) // 转换比例生成 保存4位小数 更精确
          this.pageImage.maxImgScale = Number((this.pageImage.imgScale * 2).toFixed(6)) // 放大最大2倍数生成
          this.pageImage.minImgScale = Number((this.pageImage.imgScale * 0.2).toFixed(6)) // 缩小最小0.2倍数生成
        } else { // 放大缩小模式
          const panel = this.pageImage.scale / 100
          this.pageImage.imgScale = Number(((parent.offsetHeight - 4) / image.height).toFixed(6)) * panel
        }
        this.pageImage.origin = {
          w: image.width,
          h: image.height
        }
      }
      image.src = this.previewImg.src
    }

0 、图像的缩放

包含按钮点击放大缩小,和滑轮滚动底图的放大缩小

<div>
    <i class="el-icon_narrow" @click="emitControl('scaleControl', -1)" />
    <span>{{ scale + "%" }}</span>
    <i class="el-icon_amplification" @click="emitControl('scaleControl', 1)" />
    <i class="el-icon_adapt" @click="emitControl('scaleControl', 0)" />
</div>

 0.1、按钮点击控制当前缩放比例

    /**
     * 当前缩放比例控制
     * @param {*} val
     */
    scaleControl(val) {
      if (val) {
        this.scaleDom(val, 'click')
      } else {
        this.pageImage.scale = 100
        this.pageImage.imgScale = 100 / this.pageImage.unit
        this.previewImg.target.style.width = this.pageImage.origin.w * this.pageImage.imgScale + 'px'
        this.previewImg.target.style.height = this.pageImage.origin.h * this.pageImage.imgScale + 'px'
      }
    }

0.2、鼠标滑轮放大缩小

    /**
     * 鼠标滑轮事件
     * @param {*} e
     */
    scaleDom(e, type = 'wheel') {
      const unit = 100 / this.pageImage.unit // 基准
      let scaleReal = this.pageImage.imgScale
      const size = type === 'wheel' ? e.wheelDelta / 1200 : parseFloat(e / 10)
      scaleReal += size * unit;
      if (scaleReal >= unit * 0.18 && scaleReal <= unit * 2.02) { // 不能直接取 0.2, 因为浏览器不同,可能每次size都不能刚好为 0.1
        this.pageImage.imgScale = Number(scaleReal.toFixed(6))
        this.pageImage.scale = Number((Number(scaleReal.toFixed(6)) * this.pageImage.unit).toFixed(0))
        this.previewImg.target.style.width = this.pageImage.origin.w * this.pageImage.imgScale + 'px'
        this.previewImg.target.style.height = this.pageImage.origin.h * this.pageImage.imgScale + 'px'
      }
    }

1、图像的拖动

    this.$nextTick(() => {
        const dom = this.previewImg.target
        if (type) {
          this.previewImg.target.style.cursor = 'move'
          dom.onmousedown = (e) => {
            // 鼠标按下,计算当前元素距离可视区的距离
            const disX = e.clientX - dom.offsetLeft;
            const disY = e.clientY - dom.offsetTop;
            dom.onmousemove = (e) => {
              // 计算移动的距离
              const l = e.clientX - disX // 阻止越界
              const t = e.clientY - disY
              dom.style.left = l + 'px'
              dom.style.top = t + 'px'
            }
            dom.onmouseup = (e) => {
              dom.onmousemove = null;
              dom.onmouseup = null;
            }
            return false
          }
        } else {
          dom.onmousedown = null
          dom.onmousemove = null
          dom.onmouseup = null
        }
      })

2、图像的截图及导出

在讲图像截图之前,我们先来大概了解下canvas:

2.1、canvas略解

<canvas ref="canvas" id="canvas" :width="width" :height="height" />

this.canvas = document.getElementById('canvas') // 画布对象
this.context = this.canvas.getContext('2d') // 画布显示二维图片

// 注意设置百分比的话,父盒子要设置实际宽高
width: 200px 或者 100%
height: 200px 或者 100%

 宽高是必须加上的,当然你也可以选择动态宽高,可以设置宽高百分比喔,但是我建议还是尽量设置实际像素~(因为当你涂鸦canvas时,不设置实际像素距离的话,canvas就会出现偏离,线宽也会是百分比,所以强烈建议实际像素)

canvas用于在页面上绘制图像(可以是自己的图片,也可以绘制自定义矢量图形:矩形等),其实我们不用担心大量的重绘canvas,现在浏览器完全扛得住这压力,非常流畅丝滑

下面我来大概讲解下绘制图像,绘制自定义矢量图形其他博客上有很多,我这里就先省略了

2.2、canvas绘制图像

举例

 首先整个灰色部分都是canvas(上面操作栏不是canvas喔),我们可以看到我们所使用的图是位于左上角的,canvas绘制是以左上角(0,0)为原始点的

2.2.1、图像绘制(drawImage用法)

可以说drawImage方法是绘制图像的独有方法,只有调用了这个方法才能进行绘制,那在绘制之前应该注意些什么呢?

1.context.clearRect()

首先要将canvas里进行清除(例如你之前已经绘制过了,想要重新绘制,则应该先清除)

this.context.clearRect(0, 0, width, height);

 这里的第一二参数分别为你要清除的区域起始点的x,y轴坐标,第三四参数分别为你要清除的区域的宽高,我们就只需要写入 0, 0, width, height 来清除整个canvas区域

2.image.onload

在绘制前我们得对要绘制的图片有了解,比如图片宽高,是否跨域(这是个重点,后面我讲一下,很常遇到),并且drawImage必须要在image.onload里面调用才行,因为onload是异步,你将drawImage放在外面会有偶发性的bug(图像一会能绘制,一会不能绘制),这是全局重点

注意:onload只在 Image类被实例化后赋值src才会触发,只在赋值src才会触发,才会触发!!

  3.图像绘制 drawImage(核心)

  下面我对drawImage参数的理解

下面s开头的,都是对图片本身的裁剪参数,d开头的,都是对canvas放置地方的参数

this.context.drawImage(image, dx, dy);
this.context.drawImage(image, dx, dy, dwidth, dheight);
this.context.drawImage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);

当然 drawImage 第一参数也可以是canvas画布噢,常见的使用就是图片或者画布

 我们一般都想对图片进行完全展示,那么sx, sy 就设置为 0,0 ,swidth,sheight就将之前我们计算出的图片实际宽高给设置进来。想展示在canvas左上角,那么dx,dy就为0,0,最后两个参数比较重要,是关于缩放比例的(计算缩放比例在上面calcImage函数内)

我们想要全部展示,那么最后两个参数就设置为:

this.img.width * this.pageImage.imgScale, this.img.height * this.pageImage.imgScale

 实例结果如下

    /**
     * 绘制图像
     */
    drawImage() {
      this.clearImage() // 绘制前先清除
      this.context.drawImage(
        this.img, // 规定要使用的图像、画布或视频。
        0, 0, // 开始剪切的 x 坐标位置。
        this.img.width, this.img.height, // 被剪切图像的高度。
        this.pageImage.imgX, this.pageImage.imgY, // 在画布上放置图像的 x 、y坐标位置。
        this.img.width * this.pageImage.imgScale, this.img.height * this.pageImage.imgScale                 // 要使用的图像的宽度、高度
      )
    }

2.3、图像截图及导出

ok相信大家都对canvas有了一定了解,现在来讲截图了,其实核心逻辑就是 一个矩形框在底图上的左上角位置及矩形框宽高,但是我们想要图像缩放不影响最后的截图结果的话,就一定要计算在原始比例下的矩形框参数噢~

 本篇重点:图像的截图所使用的矩形框是怎么画出来的呢?想使用的童鞋参考我另一篇博客~ 

【JS】原生js实现矩形框的绘制/拖动/缩放

ok矩形框出来了,那里面的参数如何解析呢?将里面的x,y,w,h全部除以this.pageImage.imgScale,这样才能得到原始比例下的真实坐标及宽高,然后就是绘制截图了

    drawImage() {
      this.context.drawImage(
        this.img, // 规定要使用的图像、画布或视频。
        0, 0, // 开始剪切的 x 坐标位置。
        this.img.width, this.img.height, // 被剪切图像的高度。
        this.pageImage.imgX, this.pageImage.imgY, // 在画布上放置图像的 x 、y坐标位置。
        this.img.width * this.pageImage.imgScale, this.img.height * this.pageImage.imgScale                 // 要使用的图像的宽度、高度
      )
    }

 然后就是截图图片导出,blob2file方法在下面 疑难问题解答的第二点中

this.canvas.toBlob((blob) => { resolve(this.blob2file(blob)) }, 'image/png', 1)

3、疑难问题解答

 1、图片跨域如何解决?

这种一般都是在图片服务器配置跨域参数解决

header("Access-Control-Allow-Origin: *"); // 任意域名
header("Access-Control-Allow-Origin: xxx"); // 指定域名

如果还是不行,则按照下面方式解决

const image = new Image()
image.crossOrigin = 'Anonymous'
image.src = xxx

 2、canvas.toBlob转image

this.canvas.toBlob((blob) => { resolve(this.blob2file(blob)) }, 'image/png', 1)
    /**
     * 随机id
     */
    uuid() {
      let d = new Date().getTime();
      const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
      });
      return uuid
    },
    /**
     * canvas转base64
     * @param {*} blob
     * @param {*} type
     * @param {*} name
     */
    blob2file(blob, type = 'png', name = '') {
      const fileName = name || this.uuid() + '.' + type
      const file = new File([blob], fileName, { type: blob.type, lastModified: Date.now() })
      return file
    }

3、getImageData想转image?

我不建议这么转,虽然getImageData获取到所有像素点,并且可以修改,但是貌似不咋好写后续(实际就是我没弄出来,你们可以试试),我还是建议toBlob

4、toDataUrl好还是toBlob好?

我建议使用toBlob,这个网上有很多说法了,我就不一一解释了

5、获取图像数据并绘制使用getImageData配合putImageData?

如果你只是想要将涂鸦后的canvas转图片并且只展示的话,就可以getImageData配合putImageData,如果你想通过接口传给后端,那我建议你是用toBlob

注意getImageData配合putImageData使用需要新建一个匿名canvas噢!

举例:我有一个canvas可以随时进行涂鸦,但是有俩按钮可以放大缩小,此时如何保持之前的canvas涂鸦记录不消失呢?

知识点:canvas在宽高改变时,自身内容是一定会被销毁的

所以在点击放大缩小前,先将canvas数据获取到,赋值给一个变量,然后创建匿名canvas,将canvasData 通过putImageData绘制上去,然后将匿名canvas返回回来,然后将当前canvas数据清除掉,再通过drawImage绘制canvas,就可以了噢

    // 匿名canvas保存已经存在的canvas图形对象
    createCanvas(canvasData) {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      canvas.width = this.canvas.width
      canvas.height = this.canvas.height
      ctx.putImageData(canvasData, 0, 0)
      return canvas
    },
    // 放大缩小canvas的保存绘制
    resize(width, height){
      const canvasData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); // 提取画布数据
      const annoyCanvas = this.createCanvas(canvasData)
      this.canvas.width = width
      this.canvas.height = height
      this.$nextTick(() => {
        // 清除画布
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
        //  改变完宽高后,重绘画布
        this.ctx.drawImage(annoyCanvas, 0, 0, annoyCanvas.width, annoyCanvas.height, 0, 0, width, height);
      })
    }

6、canvas图片污染?

Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

一般为上述描述,出现这种情况,你就得新开辟一个canvas,然后进行绘制,不在之前已经绘制过的canvas再次操作

document.createElement('canvas')

7、在绘制或编辑矩形框时出现黑色拒绝符号/卡顿?

加上下面这几行阻止浏览器默认事件即可以解决卡顿导致的事件丢失问题,提升效率

    document.onmousemove = (e) => {
          e.preventDefault();
          if (e.stopPropagation) {
            e.stopPropagation();
          } else {
            e.cancelBubble = true;
          }
    }

8、为什么图片没有跨域,但是图片的onload事件就是不触发?

有细心的童鞋会发现,对同一张图片进行多次赋相同的值,除了第一次onload生效外,后续的onload事件不会触发,这个时候与跨域无关,因为跨域会报错。我觉得可能是与浏览器的缓存机制有关,所以解决方式是后面加上时间戳,每次都去请求新的图片地址。

        const image = new Image()
        image.crossOrigin = 'Anonymous'
        image.src = `${this.xxx}?t=${Date.now()}`
        image.onload = () => {}

9、不想通过canvas实现拖动,想通过dom事件实现拖动?

v-drag 为自定义拖拽指令,用于一般dom在父节点范围内拖动,想 了解的在我另一篇博客查看

vue学习(6)自定义指令详解及常见自定义指令

<div
    v-drag
>
    <img src="xxx" />
</div>

10、 文章开头的矩形框截图 是可以直接使用的吗?

当然,全原生js实现,可以直接用,但是当你的底图放大缩小是通过transform: scale 来实现的,那就不可以噢~,这是为啥呢? 因为scale放大缩小后,他原始的宽高并没有改变,所以当放大缩小后,我们截图dom框所需要的相关距离就不是当前页面的真实距离,所以一定切记噢! 图片底图通过scale来操作的就不能使用我的矩形框方法噢!

11、drawImage出现 Failed to execute 'drawImage' on 'CanvasRenderingContext2D'报错

报错原因是 drawImage的第一参数不能为base64数据,如果你想用图片的话,请先new Image,将base64赋值给这张图片,然后再进行绘制噢~不过切记drawImage要写在实例后的image的onload事件后噢

12、canvas涂鸦时候出现绘制偏离是怎么回事?

答案就是因为canvas设置了百分比宽高,canvas里面也会继承此百分比,所以导致画笔和绘制相关都会出错,所以我强烈建议大家对canvas设置实际宽高像素!

---有问题的话持续更新,欢迎提问---

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐