Skip to main content

自由绘画 Konva 演示

有很多方法可以在 Konva 中实现自由绘画工具。

我看到两种最常见和简单的方法:

  1. 基于 Konva 的矢量图形(简单)
  2. 手动绘制到 2D 画布(高级)

使用 Konva 节点进行自由绘画

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

  1. mousedown/touchstart 时开始一个新的 Konva.Line
  2. mousemove/touchmove 时向该线添加新的点

这种方式在许多应用中运行良好。要将绘图的状态存储在某处(如 React store 或 JSON 保存到数据库)也很简单。

import Konva from 'konva';

// 创建工具选择
const select = document.createElement('select');
select.innerHTML = `
  <option value="brush">画笔</option>
  <option value="eraser">橡皮擦</option>
`;
document.body.appendChild(select);

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

// 首先我们需要 Konva 的核心组件:舞台和图层
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',
    // 为了更平滑的线条,使用圆形端点
    lineCap: 'round',
    lineJoin: 'round',
    // 添加点两次,以便我们即使在简单点击时也有一些绘制
    points: [pos.x, pos.y, pos.x, pos.y],
  });
  layer.add(lastLine);
});

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

// 核心功能 - 绘画
stage.on('mousemove touchmove', function (e) {
  if (!isPaint) {
    return;
  }

  // 防止触摸设备上的滚动
  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 画布 API,第一个方法有局限性。如果需要对画布进行高级访问,最好使用 原生上下文访问

我们将创建一个特殊的离屏画布,用于添加所有绘图。 使用对画布的原生访问,我们可以使用低级别的 2D 上下文函数。 要在舞台上显示画布,我们将使用 Konva.Image

import Konva from 'konva';

// 创建工具选择
const select = document.createElement('select');
select.innerHTML = `
  <option value="brush">画笔</option>
  <option value="eraser">橡皮擦</option>
`;
document.body.appendChild(select);

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

// 首先我们需要 Konva 的核心组件:舞台和图层
const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

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

// 然后我们将绘制到特殊的画布元素中
const canvas = document.createElement('canvas');
canvas.width = stage.width();
canvas.height = stage.height();

// 创建的画布我们可以作为 "Konva.Image" 元素添加到图层
const image = new Konva.Image({
  image: canvas,
  x: 0,
  y: 0,
});
layer.add(image);

// 很好。现在我们需要访问上下文元素
const context = canvas.getContext('2d');
context.strokeStyle = '#df4b26';
context.lineJoin = 'round';
context.lineWidth = 5;

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

// 现在我们需要绑定一些事件
// 我们需要在鼠标按下时开始绘制
// 在鼠标抬起时停止绘制
image.on('mousedown touchstart', function () {
  isPaint = true;
  lastPointerPosition = stage.getPointerPosition();
});

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

// 核心功能 - 绘画
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;
  // 手动重绘
  layer.batchDraw();
});

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