前言
最近写了一个视频聊天室的项目,其中包括实时通讯的功能,能够发送文本消息、图片、视频等等所有文件,我希望在用户选择图片之后能够像其他聊天软件一样能够对图片做简单的处理,比如裁剪、涂鸦、马赛克等功能。
虽说现如今每个人的设备都具备拥有这样能力的应用,但是当你使用一款应用时还要借助其他应用的功能略显有些麻烦,如果你是使用Snipaste这样的截屏工具,确实这款应用很方便,但是你如果是想裁剪一个图片使用的是截图的话则会降低图片分辨率,所以给项目新增此功能。
效果图放开头
需求概述
此需求包含画笔、记号笔、折线、矩形、马赛克、裁剪、编辑历史记录(撤销和重做)、更改颜色和线宽功能,以及取消编辑和保存。
实现
此需求使用Canvas API实现,介绍下主要功能都需要使用哪些接口和方法;以下只分析编辑功能,即画笔、记号笔、折线、矩形、马赛克、裁剪功能的实现并给出代码案例。
CanvasRenderingContext2D
CanvasRenderingContext2D
接口是 Canvas API 的一部分,可为<canvas>
元素的绘图表面提供 2D 渲染上下文。它用于绘制形状,文本,图像和其他对象。
js
const canvas = document.createElement('canvas')
const ctx = canvas.getContext("2d")
如何绘制一条线段?
CanvasRenderingContext2D.lineTo()
是 Canvas 2D API 使用直线连接子路径的终点到 x,y 坐标的方法(并不会真正地绘制)。CanvasRenderingContext2D.stroke()
是 Canvas 2D API 使用非零环绕规则,根据当前的画线样式,绘制当前或已经存在的路径的方法。
通俗的说就是使用lineTo
来在画布上打点,使用stroke
将这些点连起来。通过以下代码可以得到一条从左上角到右下角的黑色直线。
js
function getRectInfo(pointList) {
const [minX, minY, maxX, maxY] = pointList.reduce((a, b) => {
return [
Math.min(a[0], b[0]),
Math.min(a[1], b[1]),
Math.max(a[2], b[0]),
Math.max(a[3], b[1])
]
}, [Infinity, Infinity, 0, 0])
return [maxX - minX, maxY - minY]
}
function drawLine(pointList) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext("2d")
const [width, height] = getRectInfo(pointList)
canvas.width = width
canvas.height = height
ctx.beginPath()
pointList.forEach((point) => ctx.lineTo(point[0], point[1]))
ctx.stroke()
return canvas
}
const pointList = [[0, 0], [1, 1], [2, 2], ..., [100, 100]] // 伪代码
const line = drawLine(pointList)
document.querySelector('body').append(line)
以上是画笔、记号笔、折线的实现方案,三者不同的是:
- 画笔在鼠标移动时会收集所有经过的点位;
- 记号笔和折线都是只收集第一个点 和最后一个点 ,(标点下down -> 移动move -> 抬起up这一过程中的首尾两点);
- 记号笔的颜色是半透明的。
如何绘制一个矩形?
CanvasRenderingContext2D.strokeRect()
是 Canvas 2D API 在 canvas 中,使用当前的绘画样式,描绘一个起点在 (x, y) 、宽度为 w 、高度为 h 的矩形的方法。
只要确认矩形左上角位置坐标以及宽度高度就可以绘制出一个矩形。通过以下代码可以得到一个黑色线段绘制的矩形。
js
function getRectInfo(pointList) {
const [minX, minY, maxX, maxY] = pointList.reduce((a, b) => {
return [
Math.min(a[0], b[0]),
Math.min(a[1], b[1]),
Math.max(a[2], b[0]),
Math.max(a[3], b[1])
]
}, [Infinity, Infinity, 0, 0])
return [maxX - minX, maxY - minY]
}
function drawRect(pointList) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext("2d")
const [width, height] = getRectInfo(pointList)
canvas.width = width
canvas.height = height
ctx.beginPath()
ctx.strokeRect(0, 0, width, height)
return canvas
}
const pointList = [[0, 0], [100, 100]]
const rect = drawRect(pointList)
document.querySelector('body').append(rect)
以上是矩形的实现方案
- 它和前面的记号笔和折线一样,只收集第一个点 和最后一个点。
如何绘制一个马赛克?
CanvasRenderingContext2D.fillRect()
是 Canvas 2D API 绘制填充矩形的方法。当前渲染上下文中的fillStyle
属性决定了对这个矩形对的填充样式。
CanvasRenderingContext2D.drawImage()
方法是Canvas 2D API 提供了多种在画布(Canvas)上绘制图像的方式。CanvasRenderingContext2D.getImageData()
返回一个ImageData
对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为*(sx, sy)、宽为 sw、高为sh。*
绘制马赛克的原理就是在图片的每个固定块内填充单一颜色,颜色可取这一块内任意点位的颜色,调用fillRect
来填充这块区域。通过以下代码可以得到一个对img
进行马赛克处理后的canvas
js
function createMosaic(ctx, imgData) {
const { data, width, height } = imgData
const block = 4 * 5
for (let i = 0; i < width; i += block) {
for (let j = 0; j < height; j += block){
const index = (i + j * width) * 4
const [r, g, b, a] = [data[index], data[index + 1], data[index + 2], data[index + 3]]
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`
ctx.fillRect(i, j, block, block)
}
}
}
function drawMosaic(img) {
const canvas = document.createElement('canvas')
const { width, height } = img
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height)
const imgData = ctx.getImageData(0, 0, width, height)
createMosaic(ctx, imgData)
return canvas
}
const img = document.querySelector('img')
const canvas = drawMosaic(img)
document.querySelector('body').append(canvas)
改造drawMosaic
方法,实现局部马赛克。
js
function getRectInfo(pointList) {
const [minX, minY, maxX, maxY] = pointList.reduce((a, b) => {
return [
Math.min(a[0], b[0]),
Math.min(a[1], b[1]),
Math.max(a[2], b[0]),
Math.max(a[3], b[1])
]
}, [Infinity, Infinity, 0, 0])
return [minX, minY, maxX - minX, maxY - minY]
}
function createMosaic(ctx, imgData, x, y) {
const { data, width, height } = imgData
const block = 4 * 5
for (let i = 0; i < width; i += block) {
for (let j = 0; j < height; j += block){
const index = (i + j * width) * 4
const [r, g, b, a] = [data[index], data[index + 1], data[index + 2], data[index + 3]]
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`
ctx.fillRect(i + x, j + y, block, block)
}
}
}
function drawMosaic(img, pointList) {
const canvas = document.createElement('canvas')
const [left, top, width, height] = getRectInfo(pointList)
const { width:imgWidth, height: imgHeight } = img
canvas.width = imgWidth
canvas.height = imgHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, imgWidth, imgHeight)
const imgData = ctx.getImageData(left, top, width, height)
createMosaic(ctx, imgData, left, top)
return canvas
}
const img = document.querySelector('img')
const pointList = [[500, 500], [1000, 1000]]
const canvas = drawMosaic(img, pointList)
document.querySelector('body').append(canvas)
马赛克使用以上方法实现
- 绘制马赛克除了和绘制矩形的绘制内容不同之外,鼠标事件以及收集的点都和绘制矩形无异。
如何图片裁剪?
CanvasRenderingContext2D.drawImage()
方法是Canvas 2D API 提供了多种在画布(Canvas)上绘制图像的方式。
js
drawImage(image, dx, dy);
drawImage(image, dx, dy, dWidth, dHeight);
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
只需使用drawImage
方法将需要的图片区域绘制到canvas
画布上,再将canvas
转为图片即可完成裁剪。通过以下代码可以得到一个对图片裁剪后的canvas
js
export function cropPicture(img, x, y, width, height) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = width
canvas.height = height
ctx.drawImage(img, x, y, width, height, 0, 0, width, height)
return canvas
}
图片裁剪使用以上方法实现
- 选择裁剪区域在这里就不做解读了,代码量庞大,全是业务逻辑
缩放问题
以上案例为了便于理解并未将存在缩放的情况考虑进去,在这里解释一下为什么会存在缩放问题?缩放会带来什么异常?如何消除缩放带来的异常?
- 为什么存在缩放问题?
图片的实际尺寸比设备屏幕的尺寸要大,开发者会给图片容器限制大小,例如宽高最大100%,保持原比例,这时的图片width
和heigth
就与原尺寸进行了一定比例的缩放。 - 缩放会带来什么异常?
举个栗子,图片裁剪时如果使用缩放后的width
和heigth
,那么裁剪后图片的分辨率也将是缩放后的,如果这张图很大,将会出现裁剪后分辨率大幅降低的情况,原本放大图片清晰可见,现在放大后模糊无比。 - 如何消除缩放带来的异常?
获取图片原尺寸,在裁剪时还原width
和heigth
后重新计算区域及偏移量后再进行裁剪。以下方法可获取图片原尺寸。
js
function getOriginalImageRect(file) {
return new Promise((resolve, reject) => {
const image = new Image()
image.src = URL.createObjectURL(file)
image.style.position = 'absolute'
image.style.left = '-10000px'
image.style.top = '-10000px'
image.onload = () => {
resolve(image.getBoundingClientRect())
image.remove()
};
image.onerror = () => {
reject()
image.remove()
}
document.body.appendChild(image)
})
}
最后
以上主要介绍了各个功能的核心点,但是只有这些是没办法完成这个组件的,还需要绑定鼠标事件(mousedown、mousemove、mouseup)来获取在哪部分区域进行绘制,由这些事件采集需要绘制的点位,收集需要裁剪的区域数据;本文就不介绍这部分的实现了,代码比较多,整个组件总共1000+行代码,有兴趣可以去看看源码,也可以在线体验一下。
求求各位观众老爷给个赞吧😭!
扩展阅读
参考
- MDN
- CanvasRenderingContext2D: developer.mozilla.org/zh-CN/docs/...
未经作者授权 禁止转载