canvas 是 HTML5 中的一个元素,它能用来绘制各种图形、文字,目前前端的很多图表库也会用到 canvas 来绘制。canvas 除了绘制图形外,也能用来处理照片,我用过的一个图片压缩库也是用 canvas 实现的。

我之前写过 使用 canvas 截取摄像头画面来实现拍照使用 canvas 裁剪图片 ,这里就继续来写照片涂鸦。

我要实现的功能包括:

  • 打开和读取本地图片
  • 用鼠标在图片上涂鸦
  • 笔画粗细和颜色可以自定义
  • 可以把涂鸦后的图片导出为 PNG

访问 canvas照片涂鸦demo 可以在线测试和查看源码。

下面是最终实现的效果,我只是实现了功能,没有加样式:

canvas照片涂鸦

颜色调节使用的是 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-inputinput 是文件表单,用来选择本地图片
  • id 为 open-img-file 的按钮用来打开文件表单,文件表单为了美观,我把 display 设置为了 none
  • id 为 color-selectinput 是颜色选择器,用来设置画笔的颜色
  • id 为 thicknessinput 用来设置笔画粗细
  • 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 的 naturalWidthnaturalHeight 获取真实尺寸后,把 canvas 的宽高设置为图片的真实尺寸。我这里设置的是图片真实尺寸,没有做过缩放之类的处理,如果图片的尺寸超出屏幕分辨率就会出现滚动条。

使用 drawImage 可以从 img 截取图像到 vanvas 绘制,下面是 drawImage 的参数说明:

  • image: 截取的图像资源
  • sX: 截取图像的左侧起始位置
  • sY: 截取图像的顶部起始位置
  • sW: 截取图像的宽度
  • sH: 截取图像的高度
  • dXcanvas 绘制图像的左侧起始位置
  • dYcanvas 绘制图像的顶部起始位置
  • dWcanvas 绘制图像的宽度
  • dHcanvas 绘制图像的高度

成功显示图片后,下面就可以开始涂鸦了。

鼠标涂鸦

涂鸦包括鼠标按下、鼠标移动、鼠标松开三个部分:

  1. 鼠标按下时,调整画笔的基本参数,然后建立一个点作为起点
  2. 鼠标移动时,根据鼠标位置,使用点来标记出一条路线,然后使用线连接起来
  3. 鼠标松开时,停止绘制

调用 canvas 上下文的 fillRectarc 都能实现画点,但是鼠标在移动时,不能不间断的触发移动事件,如果只是画点的话,笔画就是一个个的点,不能连接起来。我这里会用到 moveTolineTostroke 来配合实现画线。

下面是鼠标涂鸦的代码:

// 鼠标按下
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 是垂直位置,我的 xy 都是当前的鼠标位置。lineTo 只是把点连接起来,不会绘制出内容。

上面已经使用 moveTolineTo 创建出了一条路劲,调用 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();
});

下面是导出步骤说明:

  1. 调用 canvastoDataURL 方法把画布转换为 dataURL
  2. 创建一个链接,把链接的 href 设置为 dataURL
  3. 链接的 download 属性可以设置导出的文件名
  4. 调用链接的 click 方法来触发点击

canvastoDataURL 的第一个参数是图片类型,例如 image/jpegimage/png ,第二个参数是图片质量,取值从 0 到 1,默认为 0.92。

类似文章: