Canvas 实现照片涂鸦
canvas 是 HTML5 中的一个元素,它能用来绘制各种图形、文字,目前前端的很多图表库也会用到 canvas 来绘制。canvas 除了绘制图形外,也能用来处理照片,我用过的一个图片压缩库也是用 canvas 实现的。
我之前写过 使用 canvas 截取摄像头画面来实现拍照 和 使用 canvas 裁剪图片 ,这里就继续来写照片涂鸦。
我要实现的功能包括:
- 打开和读取本地图片
- 用鼠标在图片上涂鸦
- 笔画粗细和颜色可以自定义
- 可以把涂鸦后的图片导出为 PNG
访问 canvas照片涂鸦demo 可以在线测试和查看源码。
下面是最终实现的效果,我只是实现了功能,没有加样式:
颜色调节使用的是 input
的颜色选择器。
HTML 元素
下面是会用到的 HTML 元素:
<div role="toolbar" id="toolbar">
<input type="file" id="file-input">
<button type="button" id="open-img-file">打开图片</button>
<label for="color-select">画笔颜色</label>
<input type="color" id="color-select">
<label for="thickness">笔画粗细</label>
<input type="number" id="thickness" placeholder="笔画的粗细单位为像素" value="5">
<button type="button" id="export-btn">导出图片</button>
</div>
<hr>
<canvas id="canvas"></canvas>
下面是用到的 HTML 元素说明:
- id 为
file-input
的input
是文件表单,用来选择本地图片 - id 为
open-img-file
的按钮用来打开文件表单,文件表单为了美观,我把display
设置为了none
- id 为
color-select
的input
是颜色选择器,用来设置画笔的颜色 - id 为
thickness
的input
用来设置笔画粗细 - id 为
export-btn
的按钮用来导出图片
下面在 JavaScript 中选择这些元素:
const openImgFile = document.querySelector('#open-img-file'); // 打开图片按钮
const fileInput = document.querySelector('#file-input'); // 文件选择表单
const colorSelect = document.querySelector('#color-select'); // 颜色选择器
const thickness = document.querySelector('#thickness'); // 笔画粗细的输入表单
const exportBtn = document.querySelector('#export-btn'); // 导出图片按钮
const canvas = document.querySelector('#canvas'); // canvas 元素
const ctx = canvas.getContext('2d');
let press = false; // 用来记录鼠标按下状态
ctx
存储的 canvas.getContext('2d')
就是 canvas 的 2D 上下文,canvas.getContext('2d')
的属性和方法后面会直接通过 ctx
调用。
press
用来记录鼠标按下的状态,true
就是鼠标按下,false
就是放开。
在 canvas 中显示本地图片
下面先实现打开本地图片和在 canvas 中显示:
// 打开图片按钮点击
openImgFile.addEventListener('click', () => {
// 点击文件表单
fileInput.click();
});
// 文件表单改变
fileInput.addEventListener('change', ev => {
// 如果没有选择文件就直接返回
if (ev.target.value === '') return false;
// 检测是否是 jpg 或 png 的图片,如果不是就返回
if (ev.target.files[0].type !== 'image/jpeg' && ev.target.files[0].type !== 'image/png') {
alert('目前只支持 jpg 和 png 格式的图片!');
return false;
}
// 创建一个 img 元素
const img = new Image();
// 创建一个对象 URL,把对象 URL 传给 img
img.src = URL.createObjectURL(ev.target.files[0]);
// img 图片加载完成
img.onload = () => {
// 把 canvas 的宽高设置为图片的真实宽高
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
// 在 canvas 中绘制图片
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, img.naturalWidth, img.naturalHeight);
}
});
点击 打开图片
按钮后,调用文件表单的 click
方法就能触发文件表单的点击事件,文件选择对话框就会弹出。
文件选择完成后,我创建了一个 img
元素,把图片文件转为对象 URL 传给 img
,这个 img
主要是用来获取图片的真实尺寸,可以不用插入到页面。
通过 img 的 naturalWidth
和 naturalHeight
获取真实尺寸后,把 canvas 的宽高设置为图片的真实尺寸。我这里设置的是图片真实尺寸,没有做过缩放之类的处理,如果图片的尺寸超出屏幕分辨率就会出现滚动条。
使用 drawImage
可以从 img
截取图像到 vanvas 绘制,下面是 drawImage
的参数说明:
image
: 截取的图像资源sX
: 截取图像的左侧起始位置sY
: 截取图像的顶部起始位置sW
: 截取图像的宽度sH
: 截取图像的高度dX
:canvas
绘制图像的左侧起始位置dY
:canvas
绘制图像的顶部起始位置dW
:canvas
绘制图像的宽度dH
:canvas
绘制图像的高度
成功显示图片后,下面就可以开始涂鸦了。
鼠标涂鸦
涂鸦包括鼠标按下、鼠标移动、鼠标松开三个部分:
- 鼠标按下时,调整画笔的基本参数,然后建立一个点作为起点
- 鼠标移动时,根据鼠标位置,使用点来标记出一条路线,然后使用线连接起来
- 鼠标松开时,停止绘制
调用 canvas 上下文的 fillRect
和 arc
都能实现画点,但是鼠标在移动时,不能不间断的触发移动事件,如果只是画点的话,笔画就是一个个的点,不能连接起来。我这里会用到 moveTo
、lineTo
,stroke
来配合实现画线。
下面是鼠标涂鸦的代码:
// 鼠标按下
canvas.addEventListener('mousedown', ev => {
// 把鼠标按下状态设置为 true
press = true;
// 开始一个新的路径
ctx.beginPath();
// 标记起点
ctx.moveTo(ev.offsetX, ev.offsetY);
// 设置线的宽度
ctx.lineWidth = Number(thickness.value);
// 设置颜色,颜色从颜色选择器表单获取
ctx.strokeStyle = colorSelect.value;
});
// 鼠标移动
canvas.addEventListener('mousemove', ev => {
// 如果鼠标没有按下就直接返回
if (!press) return false;
// 直线连接位置
ctx.lineTo(ev.offsetX, ev.offsetY);
ctx.stroke();
});
// 鼠标松开
canvas.addEventListener('mouseup', () => {
// 把鼠标按下的状态设置为 false
press = false;
// 停止当前路劲
ctx.closePath();
});
下面是详细的步骤说明:
鼠标按下
我在上面定义了一个 press
变量用来存储鼠标状态,鼠标按下时 press
就设置为 true
。
调用 beginPath
方法可以开始一个新的路劲,beginPath
可以让每条笔画都是一个新的路劲。如果不调用 beginPath
,中途如果改变了笔画颜色,之前画的线也会受到影响。
调用 moveTo
来标记点,参数 x
是水平位置 y
是垂直位置,moveTo
只是标记,不会绘制。
通过 lineWidth
属性可以设置线的宽度,我的宽度是从 input
获取的,通过 strokeStyle
属性可以设置画笔颜色和样式,我的颜色也是从 input
的颜色选择器获取的,颜色选择器获取的颜色就是一个 #FFFFFF
的十六进制颜色。
鼠标移动
鼠标移动时,首先要检测 press
鼠标状态是否是按下,如果没有按下就直接返回。
lineTo
方法可以用线把上一个点和指定位置连接起来,参数 x
是水平位置 y
是垂直位置,我的 x
和 y
都是当前的鼠标位置。lineTo
只是把点连接起来,不会绘制出内容。
上面已经使用 moveTo
和 lineTo
创建出了一条路劲,调用 stroke
就能绘制出内容。
鼠标松开
首先把 press
鼠标状态设置为 false
,然后调用 closePath
停止当前路劲。
上面就实现了在照片上涂鸦。
我这里只是编写了鼠标事件,没有编写 touch
触摸事件,在触屏上是无法使用的。要支持触屏可以加入 touchstart
手指接触到屏幕、touchmove
手指在屏幕上移动、touchend
手指离开屏幕三个事件,方法和鼠标事件是差不多的,触摸事件获取位置的时候会包含多个点,如果不需要多指操作的画,获取第 0 个点就可以。
导出为图片
代码:
// 导出图片按钮点击
exportBtn.addEventListener('click', () => {
// 把 canvas 内容转换为 DataURL 数据
const imgData = canvas.toDataURL('image/png');
const linkEl = document.createElement('a');
linkEl.href = imgData;
linkEl.download = 'image.png';
linkEl.click();
});
下面是导出步骤说明:
- 调用
canvas
的toDataURL
方法把画布转换为dataURL
- 创建一个链接,把链接的
href
设置为dataURL
- 链接的
download
属性可以设置导出的文件名 - 调用链接的
click
方法来触发点击
canvas
的 toDataURL
的第一个参数是图片类型,例如 image/jpeg
或 image/png
,第二个参数是图片质量,取值从 0 到 1,默认为 0.92。
类似文章:
版权声明:本文为原创文章,版权归 Mr. Ma's Blog 所有,转载请联系博主获得授权。
本文地址:https://www.misterma.com/archives/918/
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。