Canvas实现一个上传图片截图功能

1. 前言

在我们平时开发图片上传时,有时需要实现图片的裁剪功能。通常我们使用Vue-cropper来实现图片的裁剪功能。

如何实现呢?本文介绍下图片裁剪的功能的实现过程。

2. 实现思路

  1. 使用 input 元素让用户选择图片文件,然后使用 FileReader API 读取图片文件的内容,并将其设置为 img 元素的 src 属性。
  2. 创建一个 canvas 元素并获取其 2D 渲染上下文,然后使用 drawImage 方法将图片绘制到 canvas 上。
  3. 监听鼠标的 mousedownmousemovemouseup 事件,用于创建和调整裁剪框。当用户点击鼠标并移动时,记录鼠标的位置并在 canvas 上绘制一个矩形作为裁剪框。
  4. 当用户释放鼠标按钮时,使用 getImageData 方法获取裁剪框内的像素数据,然后创建一个新的 canvas 并使用 putImageData 方法将这些像素数据放入新的 canvas 中。这样就得到了裁剪后的图片。
  5. 最后,使用 toDataURL 方法将裁剪后的图片转换为 Base64 格式的字符串,或者使用 toBlob 方法将其转换为 Blob 对象,然后可以将其下载到用户的计算机上,或者上传到服务器。

3. 实现步骤

使用原生js进行实现,使用两个canvas,一个是原图展示,一个是截图后的。

HTML部分:

html 复制代码
<!-- 文件上传 -->
<input type="file" id="inputFile" accept="image/*" />
<!-- 展示读取的图片的canvas -->
<div class="clipContainer">
    <canvas id="clip"></canvas>
</div>
<!-- 截图后的图片的canvas -->
<div class="drawContainer">
    <canvas id="draw"></canvas>
</div>

JS公共部分:

js 复制代码
const inputNode = document.querySelector("#inputFile");
const clipCanvas = document.querySelector("#clip");
const drawCanvas = document.querySelector("#draw");
const clipContainer = document.querySelector(
    ".clipContainer"
);
const drawContainer = document.querySelector(
    ".drawContainer"
);

const clipCanvasCtx = clipCanvas.getContext("2d");
const drawCanvasCtx = drawCanvas.getContext("2d");

const imageNode = document.createElement('img');
const drawImageNode = document.createElement('img');//用于最终展示图片
let resultFile = null;//最终生成的File对象图片文件

3.1 选择文件进行预览

使用FileReader读取文件的url和宽高,进行cavas绘制。

js 复制代码
// 1. 选择图片 读取图片
inputNode.addEventListener(
    "change",
    function (e) {
        const file = e.target.files[0];
        const reader = new FileReader();
        reader.onload = function (e) {
            imageNode.src = e.target.result;
            imageNode.onload = function () {
                // 获取真实的图片宽高  canvas绘制图片
                generateCanvas(clipContainer, clipCanvas, this.width, this.height);
                clipCanvasCtx.drawImage(imageNode, 0, 0, this.width, this.height);

            };
        };
        file && reader.readAsDataURL(file);
    },
    false
);

// 2. canvas绘制图片
function generateCanvas(canvasContainer, canvas, width, height) {
    // 2.1 外部容器的宽高
    canvasContainer.style.width = width + 'px';
    canvasContainer.style.height = height + 'px';
    // 2.2 canvas的宽高
    canvas.width = width;
    canvas.height = height;
}

结果如下:

3.2 监听mousedown事件

监听mousedown 记录起点坐标,使用startPositon进行记录起始位置。

js 复制代码
// 记录点击开始结束位置
let startPosition = []

clipCanvas.addEventListener('mousedown', function (e) {
    // 3.1 记录开始截图的位置
    startPosition = [e.offsetX, e.offsetY]
    // 3.2 开始监听 mousemove 和 mouseup 事件
    clipCanvas.addEventListener('mousemove', mousemoveHandler, false)
    clipCanvas.addEventListener('mouseup', mouseupHandler, false)
})

3.3 监听mousemove事件

记录鼠标移动坐标,计算出截图大小,绘制截图蒙层区域

js 复制代码
// 记录截图的偏移x,y和截图的宽高w,h
let screenShotData = [] 

function mousemoveHandler(e) {
    // 1. 计算裁剪区域的宽度和高度
    const { offsetX, offsetY } = e
    const [startX, startY] = startPosition
    // 偏移位置-起始位置 = 真实宽高
    const [rectWidth, rectHeight] = [offsetX - startX, offsetY - startY]
    // 2. 保存裁剪区域的相关信息
    screenShotData = [startX, startY, rectWidth, rectHeight]
    // 3. 重新绘制
    fill(clipCanvasCtx, clipCanvas, screenShotData)
}

function fill(ctx, canvas, sizeData) {

    const { width, height } = canvas
    const [x, y, w, h] = sizeData
    // 1. 再次绘制前,清理canvas1画布上的内容
    ctx.clearRect(0, 0, width, height)

    // 2. 画布上绘制蒙层
    ctx.fillStyle = `rgba(0,0,0,0.6)`
    ctx.fillRect(0, 0, width, height)

    // 3. 绘制裁剪区域
    // 在源图像外绘制新图像,只有源图像外的目标图像部分会被显示,源图像是透明的
    ctx.globalCompositeOperation = 'destination-out'
    ctx.fillStyle = '#2c2c2c'
    ctx.fillRect(...sizeData)

    // 设置在现有画布上绘制新的图形
    ctx.globalCompositeOperation = 'destination-over'
    // 剪切图像,并在画布上定位被剪切的部分
    ctx.drawImage(imageNode, 0, 0, width, height, 0, 0, width, height);
}

结果如下:

3.4 监听mouseup事件

注销事件监听,将裁剪区域放入另一个canvas中进行处理,并读取File对象

js 复制代码
function mouseupHandler(e) {
    // 1. 注销监听 mousedown 和 mousemove 事件
    clipCanvas.removeEventListener('mousemove', mousemoveHandler, false)
    clipCanvas.removeEventListener('mouseup', mouseupHandler, false)

    // 2. 开始绘制截图区域图片
    const data = clipCanvasCtx.getImageData(...screenShotData)

    // 3. 使用新画布
    const [x, y, w, h] = screenShotData
    generateCanvas(drawCanvas, drawContainer, w, h)

    // 每次绘制前,都先进行清除操作
    drawCanvasCtx.clearRect(...screenShotData);
    // 将 clipCanvas 裁剪区域的数据放入 drawCanvas 中
    drawCanvasCtx.putImageData(data, 0, 0);
    drawCanvas.style.display = 'none'

    // 4. 展示图片
    const imageUrl = drawCanvas.toDataURL('image/png')
    drawImageNode.src = imageUrl;
    document.body.appendChild(drawImageNode)

    // 5. 生成File对象
    drawCanvas.toBlob(blob => {
        resultFile = new File([blob], 'test.png', { type: 'image/png' })
    }, 'image.png')
}

4. 总结

最后总结一下如图:

通过本次实践,了解截图的运行过程。希望能对大家有所帮助,如有错误,请指正O^O!


参考文章:

使用 Canvas 实现基本的截图功能

W3C Canvas

MDN drawImage

相关推荐
盏茶作酒2911 分钟前
打造自己的组件库(一)宏函数解析
前端·vue.js
山有木兮木有枝_31 分钟前
JavaScript 设计模式--单例模式
前端·javascript·代码规范
一大树1 小时前
Vue3 开发必备:20 个实用技巧
前端·vue.js
颜渊呐1 小时前
uniapp中APPwebview与网页的双向通信
前端·uni-app
10年前端老司机1 小时前
React 受控组件和非受控组件区别和使用场景
前端·javascript·react.js
夏晚星1 小时前
vue实现微信聊天emoji表情
前端·javascript
停止重构1 小时前
【方案】前端UI布局的绝技,响应式布局,多端适配
前端·网页布局·响应式布局·grid布局·网页适配多端
極光未晚1 小时前
TypeScript在前端项目中的那些事儿:不止于类型的守护者
前端·javascript·typescript
ze_juejin1 小时前
Vue3 + Vite + Ant Design Vue + Axios + Pinia 脚手架搭建
前端·vue.js
lichenyang4531 小时前
React项目(移动app)
前端