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

相关推荐
万少38 分钟前
HarmonyOS官方模板集成创新活动-流蓝卡片
前端·harmonyos
-To be number.wan3 小时前
C++ 赋值运算符重载:深拷贝 vs 浅拷贝的生死线!
前端·c++
噢,我明白了3 小时前
JavaScript 中处理时间格式的核心方式
前端·javascript
纸上的彩虹4 小时前
半年一百个页面,重构系统也重构了我对前端工作的理解
前端·程序员·架构
be or not to be5 小时前
深入理解 CSS 浮动布局(float)
前端·css
LYFlied5 小时前
【每日算法】LeetCode 1143. 最长公共子序列
前端·算法·leetcode·职场和发展·动态规划
老华带你飞5 小时前
农产品销售管理|基于java + vue农产品销售管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小徐_23335 小时前
2025 前端开源三年,npm 发包卡我半天
前端·npm·github
GIS之路6 小时前
GIS 数据转换:使用 GDAL 将 Shp 转换为 GeoJSON 数据
前端
JIngJaneIL6 小时前
基于springboot + vue房屋租赁管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端