跳到主要内容

Canvas 绘图——使用 JavaScript 在 HTML5 Canvas 上自由绘制和涂画

构建一个 Canvas 绘图工具,让用户直接在 HTML5 Canvas 上绘制自由笔画。这是 Canvas 应用最常见的功能之一,包括白板、标注工具、设计编辑器和签名板。

使用 Konva 进行 Canvas 绘图有两种常见方法:

  1. 基于 Konva 的矢量图形——每条笔画都是一个 Konva.Line 对象,以后可以选择、移动和删除(简单,适合大多数场景)
  2. 手动 2D Canvas 绘图——直接绘制到 Canvas 像素缓冲区,在包含大量笔画时获得最高性能(高级)

使用 Konva 节点自由绘制

第一种方法可能也是最简单的方法:

  1. 创建新的 Konva.Line,并在 mousedown/touchstart 时开始绘制
  2. mousemove/touchmove 时向线条添加新点

此方法适用于许多应用。也可以在某处以矢量形式轻松存储绘图 state,例如存储在 React store 中或以 JSON 格式保存到数据库。

import Konva from 'konva';

// create tool select
const select = document.createElement('select');
select.innerHTML = `
  <option value="brush">Brush</option>
  <option value="eraser">Eraser</option>
`;
document.body.appendChild(select);

const width = window.innerWidth;
const height = window.innerHeight - 25;

// first we need Konva core things: stage and layer
const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

const layer = new Konva.Layer();
stage.add(layer);

let isPaint = false;
let mode = 'brush';
let lastLine;

stage.on('mousedown touchstart', function (e) {
  isPaint = true;
  const pos = stage.getPointerPosition();
  lastLine = new Konva.Line({
    stroke: '#df4b26',
    strokeWidth: 5,
    globalCompositeOperation:
      mode === 'brush' ? 'source-over' : 'destination-out',
    // round cap for smoother lines
    lineCap: 'round',
    lineJoin: 'round',
    // add point twice, so we have some drawings even on a simple click
    points: [pos.x, pos.y, pos.x, pos.y],
  });
  layer.add(lastLine);
});

stage.on('mouseup touchend', function () {
  isPaint = false;
});

// and core function - drawing
stage.on('mousemove touchmove', function (e) {
  if (!isPaint) {
    return;
  }

  // prevent scrolling on touch devices
  e.evt.preventDefault();

  const pos = stage.getPointerPosition();
  const newPoints = lastLine.points().concat([pos.x, pos.y]);
  lastLine.points(newPoints);
});

select.addEventListener('change', function () {
  mode = select.value;
});

手动自由绘制

如果要直接使用底层 2D Canvas API,第一种方法存在限制。如果需要对 Canvas 进行高级访问,最好使用原生上下文访问

我们将创建一个特殊的离屏 Canvas,并在其中添加所有绘图。 通过原生 Canvas 访问,可以使用底层 2D 上下文函数。 要在舞台上显示此 Canvas,将使用 Konva.Image

import Konva from 'konva';

// create tool select
const select = document.createElement('select');
select.innerHTML = `
  <option value="brush">Brush</option>
  <option value="eraser">Eraser</option>
`;
document.body.appendChild(select);

const width = window.innerWidth;
const height = window.innerHeight - 25;

// first we need Konva core things: stage and layer
const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

const layer = new Konva.Layer();
stage.add(layer);

// then we are going to draw into special canvas element
const canvas = document.createElement('canvas');
canvas.width = stage.width();
canvas.height = stage.height();

// created canvas we can add to layer as "Konva.Image" element
const image = new Konva.Image({
  image: canvas,
  x: 0,
  y: 0,
});
layer.add(image);

// Good. Now we need to get access to context element
const context = canvas.getContext('2d');
context.strokeStyle = '#df4b26';
context.lineJoin = 'round';
context.lineWidth = 5;

let isPaint = false;
let lastPointerPosition;
let mode = 'brush';

// now we need to bind some events
// we need to start drawing on mousedown
// and stop drawing on mouseup
image.on('mousedown touchstart', function () {
  isPaint = true;
  lastPointerPosition = stage.getPointerPosition();
});

stage.on('mouseup touchend', function () {
  isPaint = false;
});

// and core function - drawing
stage.on('mousemove touchmove', function () {
  if (!isPaint) {
    return;
  }

  if (mode === 'brush') {
    context.globalCompositeOperation = 'source-over';
  }
  if (mode === 'eraser') {
    context.globalCompositeOperation = 'destination-out';
  }
  context.beginPath();

  const localPos = {
    x: lastPointerPosition.x - image.x(),
    y: lastPointerPosition.y - image.y(),
  };
  context.moveTo(localPos.x, localPos.y);
  const pos = stage.getPointerPosition();
  const newLocalPos = {
    x: pos.x - image.x(),
    y: pos.y - image.y(),
  };
  context.lineTo(newLocalPos.x, newLocalPos.y);
  context.closePath();
  context.stroke();

  lastPointerPosition = pos;
  // redraw manually
  layer.batchDraw();
});

select.addEventListener('change', function () {
  mode = select.value;
});