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

1. 前言

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

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

2. 实现思路

  1. 使用 input 元素让用户选择图片文件,然后使用 FileReader API 读取图片文件的内容,并将其设置为 img 元素的 src 属性。
  2. 创建一个 canvas 元素并获取其 2D 渲染上下文,然后使用 drawImage 方法将图片绘制到 canvas 上。
  3. 监听鼠标的 mousedown、mousemove 和 mouseup 事件,用于创建和调整裁剪框。当用户点击鼠标并移动时,记录鼠标的位置并在 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

相关推荐
原则猫7 小时前
UDP 到底怎么实现可靠的
前端
Java后端的Ai之路7 小时前
14、ReactJS - 路由(Router)
前端·react.js·状态模式
裕晟资质规划9 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全
ShineWinsu9 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
月夏9 小时前
Vue3 转 React:18 项对照表 + 5 个必踩的坑(附 7 天上手路线)
前端
广州华水科技9 小时前
单北斗GNSS位移监测系统的应用与维护解析
前端
梦曦i9 小时前
uni-storage 0.1.0 发布:TTL过期管理+多实例隔离
前端·storage·uni-app x
猫头_10 小时前
AI 流式传输工程指南:模型吐出半截 JSON,前端如何边收边渲染?
前端·javascript·llm
console.log('npc')11 小时前
第 1 章 项目概述与环境搭建
前端·人工智能·大模型
定栓11 小时前
Webpack迁到Vite第七节(二):全家桶移除 — Babel 全家桶深度分析
前端·webpack·node.js