跳到主要内容

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;
});