canvas实战-图片部分截取

记录一下上周在工作中遇到的一个实用的canvas知识点。

周一的时候老大告诉我,系统需要这么一个功能:

用户在一张场景图中圈出自己需要的部分图片,然后再上传。

我本着能摸鱼就摸鱼的考虑,就开始反问,难道不能用户自己用截图工具截出来再上传吗?老大想了一会儿说:你做不了吗 ?这痛击灵魂的一句话,让我立马回答可以 。老大,微微一笑,好的,明天看效果。

我: 。。。。。(这该死的逞强)

先上最后实现的demo效果图和源码链接。

demo源码

具体怎么做,我大概百度了一下 。其中有个回答就是使用canvas的api 。

使用context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)将图片的部分片段绘制到canvas上,再通过canvas.toDataURL(type, encoderOptions)将绘制的片段转为base64进行显示。

实现思路:

 首先显示一张需要裁剪的图片,当点击按钮的时候,把和图片大小一致的canvas显示出来,监听鼠标事件,记录裁剪开始点和结束点的位置。再点击按钮把裁剪区域转为base64显示在另一张图片上。 思路听起来很简单,但是这其中有很多的细节之处。先看看整个页面的结构:

dom结构

<template>
    <div class="page">
        <div class="operate-btn">
            <a-button @click="handleBtnClick">{{screenshot?"就决定是你了":"开始截图"}}</a-button>
        </div>
        <div class="clip-area">
            <canvas
                    v-show="screenshot"
                    class="clip-canvas"
                    @mousedown="handleDrawStart"
                    @mouseup="handleMouseUp"></canvas>
            <img class="big-img" :style="bigImgStyle" src="https://juejin.cn/post/assets/duola.jpg" alt="小图">
            <div class="draw-area" :style="drawAreaStyle" v-show="!screenshot"></div>
        </div>
        <div class="thumb">
            <p style="text-align: center">截图预览</p>
            <img class="thumb-img" :src="https://juejin.cn/post/clipBase64" v-show="clipBase64" alt="大图">
        </div>
    </div>
</template>
复制代码

css结构

 html, body {
        overflow: hidden;
    }

    .clip-area {
        position: relative;
        overflow: hidden;
        width: 500px;
        height: 500px;
        margin: 10px auto;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    .operate-btn {
        margin-top: 10px;
        text-align: center;
    }

    .clip-canvas {
        position: absolute;
        top: 0;
        bottom: 0;
        cursor: crosshair;
    }

    .draw-area {
        border: 2px solid #ff0000;
        position: absolute;
        z-index: 10;
    }

    .big-img {
    }

    .thumb-img {
        height: 300px;
        display: block;
        margin: 10px auto;
        border: 1px solid red;
    }
复制代码

js部分大概梳理为以下三步:

  • 步骤一:将图片居中显示并按照原比例显示在固定区域里

居中显示使用flex布局,主要记录js代码。

   mounted() {
            this.getDomELe();
            // 监听图片的加载
            this.bigImgEle = document.querySelector(".big-img");
            this.bigImgEle.onload = () => {
                this.getAreaSize();
                this.getImgSize();
                this.setImgContain();
            };
        },
复制代码

首先获取可以显示图片的固定区域的宽高(通过offsetWidth属性获取)

// 获取canvas绘制的尺寸,与外层div大小一致
            getAreaSize() {
                let areaEle = document.querySelector(".clip-area");
                this.canvasElement.width = this.canvasWidth = areaEle.offsetWidth;
                this.canvasElement.height = this.canvasHeight = areaEle.offsetHeight;
                this.canvasContext.strokeStyle = "#ff0000";
                this.canvasContext.lineWidth = 1;
            },
复制代码

再获取图片的尺寸和比例

 // 获取原图展示的尺寸
            getImgSize() {
                this.imgOriginalWidth = this.bigImgEle.width;
                this.imgOriginalHeight = this.bigImgEle.height;
            },
复制代码

保持图片比例进行显示并记录下图片距离

// 将图片尽可能地填满该区域
            setImgContain() {
                // 宽高比例
                let ratio = this.imgOriginalWidth / this.imgOriginalHeight;
                // 如果图片宽小于高,则以容器高为准,按比例还原宽度
                if (this.imgOriginalWidth < this.imgOriginalHeight) {
                    this.imgDisplayHeight = this.canvasHeight;
                    this.imgDisplayWidth = ratio * this.imgDisplayHeight;
                    this.imgOffset = {
                        x: (this.canvasWidth - this.imgDisplayWidth) / 2,
                        y: 0,
                    };
                } else {
                    this.imgDisplayWidth = this.canvasWidth;
                    this.imgDisplayHeight = this.imgDisplayWidth / ratio;
                    this.imgOffset = {
                        x: 0,
                        y: (this.canvasHeight - this.imgDisplayHeight) / 2,
                    };
                }
            },
复制代码
  • 步骤二:canvas绘制矩形框

首先获取绘制开始点并监听鼠标移动

 // 记录在canvas里的开始点-清空画布-监听鼠标移动
            handleDrawStart(e) {
                this.points.start = this.getRelativePosition(e);
                document.addEventListener("mousemove", this.handleDrawMove);
            },

 getRelativePosition(e) {
                let clientRect = this.canvasElement.getBoundingClientRect();
                // 这里必须取整数
                let x = Math.round(e.clientX - clientRect.x);
                let y = Math.round(e.clientY - clientRect.y);
                return [x, y];
            },
复制代码

因为使用overflow:hidden,所以没有考虑滚动的高度,这里使用getBoundingClientRect函数来获取鼠标距离顶部的距离。但是如果非要考虑scroll的距离,需要参考jq的offset()方法。

鼠标移动过程实时绘制矩形框

// 记录移动的点位
            handleDrawMove(e) {
                this.points.end = this.getRelativePosition(e);
                this.drawRect();
            },

// 根据开始和结束点来绘制矩形
            drawRect() {
                let {
                    yStart,
                    yDistance,
                    xDistance,
                    xStart,
                } = this.formatPointPosition;
                // 清空画布
                this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
                // 开始绘制
                this.canvasContext.strokeRect(xStart, yStart, xDistance, yDistance);
            },
复制代码

因为移动过程中,坐标可能比起始坐标大或小,所以这里用formatPointPosition 这个计算属性来获取绘制矩形框的起始坐标和宽高。

formatPointPosition() {
                // 取开始点和结束点的最小值
                let [x, y] = this.points.start;
                let [x1, y1] = this.points.end;
                let xStart = Math.min(x, x1);
                let yStart = Math.min(y, y1);
                // 计算移动距离
                let xDistance = Math.abs(x - x1);
                let yDistance = Math.abs(y - y1);
                return {
                    yDistance,
                    xDistance,
                    yStart,
                    xStart,
                };
            },
复制代码

最后在鼠标抬起的时候取消mousemove事件的监听

handleMouseUp() {
                document.removeEventListener("mousemove", this.handleDrawMove);
            },
复制代码
  • 步骤三:点击按钮时将canvas绘制的区域转为base64

这里计算框选区域相对于图片的定位比较繁琐,需要用到图片显示尺寸和原始尺寸的缩放比例、图片位置与canvas的偏移量。这就是为什么在mounted时进行了这些要素的计算。

 handleBtnClick() {
                this.screenshot = !this.screenshot;
                if (!this.screenshot && this.formatPointPosition.xDistance && this.formatPointPosition.yDistance) {
                    this.exportBase64();
                }
            },

 exportBase64() {
                // 图片缩放比例
                let scale = this.imgDisplayWidth / this.imgOriginalWidth;
                // 创建一个新的canvas
                let canvasElement = document.createElement("canvas");
                let canvasContext = canvasElement.getContext("2d");
                // 计算框选区域相对于图片的定位
                let startX = (this.formatPointPosition.xStart - this.imgOffset.x) / scale;
                let startY = (this.formatPointPosition.yStart - this.imgOffset.y) / scale;
                canvasElement.width = this.formatPointPosition.xDistance / scale;
                canvasElement.height = this.formatPointPosition.yDistance / scale;
                // 框选区域绘制到canvas里
                canvasContext.drawImage(this.bigImgEle,
                    startX, startY, canvasElement.width, canvasElement.height,
                    0, 0, canvasElement.width, canvasElement.height);
                // 转为base64进行显示
                this.clipBase64 = canvasElement.toDataURL("image/jpeg");
            },
复制代码

总结

虽然剪裁图片只是一个小功能,但是涉及到的知识点还是很多,也学到了不少。现在的实现还需要完善:没有对框选图片之外的区域进行限制,图片框选时需要对图片进行放大。等有时间再一一完善吧。

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