自由绘画 Konva 演示
有很多方法可以在 Konva 中实现自由绘画工具。
我看到两种最常见和简单的方法:
- 基于 Konva 的矢量图形(简单)
- 手动绘制到 2D 画布(高级)
使用 Konva 节点进行自由绘画
所以第一种方法也是可能是最简单的方法是:
- 在
mousedown
/touchstart
时开始一个新的Konva.Line
- 在
mousemove
/touchmove
时向该线添加新的点
这种方式在许多应用中运行良好。要将绘图的状态存储在某处(如 React store 或 JSON 保存到数据库)也很简单。
- Vanilla
- React
- Vue
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; });
import React from 'react'; import { Stage, Layer, Line } from 'react-konva'; const App = () => { const [tool, setTool] = React.useState('brush'); const [lines, setLines] = React.useState([]); const isDrawing = React.useRef(false); const handleMouseDown = (e) => { isDrawing.current = true; const pos = e.target.getStage().getPointerPosition(); setLines([...lines, { tool, points: [pos.x, pos.y] }]); }; const handleMouseMove = (e) => { // 没有绘图 - 跳过 if (!isDrawing.current) { return; } const stage = e.target.getStage(); const point = stage.getPointerPosition(); // 绘制线条 let lastLine = lines[lines.length - 1]; // 添加点 lastLine.points = lastLine.points.concat([point.x, point.y]); // 替换最后一项 lines.splice(lines.length - 1, 1, lastLine); setLines(lines.concat()); }; const handleMouseUp = () => { isDrawing.current = false; }; return ( <> <select value={tool} onChange={(e) => { setTool(e.target.value); }} > <option value="brush">画笔</option> <option value="eraser">橡皮擦</option> </select> <Stage width={window.innerWidth} height={window.innerHeight - 25} onMouseDown={handleMouseDown} onMousemove={handleMouseMove} onMouseup={handleMouseUp} onTouchStart={handleMouseDown} onTouchMove={handleMouseMove} onTouchEnd={handleMouseUp} > <Layer> {lines.map((line, i) => ( <Line key={i} points={line.points} stroke="#df4b26" strokeWidth={5} tension={0.5} lineCap="round" lineJoin="round" globalCompositeOperation={ line.tool === 'eraser' ? 'destination-out' : 'source-over' } /> ))} </Layer> </Stage> </> ); }; export default App;
<template> <div> <select v-model="tool"> <option value="brush">画笔</option> <option value="eraser">橡皮擦</option> </select> <v-stage :config="stageConfig" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp" @touchstart="handleMouseDown" @touchmove="handleMouseMove" @touchend="handleMouseUp" > <v-layer> <v-line v-for="(line, i) in lines" :key="i" :config="{ points: line.points, stroke: '#df4b26', strokeWidth: 5, tension: 0.5, lineCap: 'round', lineJoin: 'round', globalCompositeOperation: line.tool === 'eraser' ? 'destination-out' : 'source-over' }" /> </v-layer> </v-stage> </div> </template> <script setup> import { ref } from 'vue'; const tool = ref('brush'); const lines = ref([]); const isDrawing = ref(false); const stageConfig = { width: window.innerWidth, height: window.innerHeight - 25 }; const handleMouseDown = (e) => { isDrawing.value = true; const pos = e.target.getStage().getPointerPosition(); lines.value.push({ tool: tool.value, points: [pos.x, pos.y] }); }; const handleMouseMove = (e) => { if (!isDrawing.value) { return; } const stage = e.target.getStage(); const point = stage.getPointerPosition(); let lastLine = lines.value[lines.value.length - 1]; lastLine.points = lastLine.points.concat([point.x, point.y]); lines.value.splice(lines.value.length - 1, 1, { ...lastLine }); }; const handleMouseUp = () => { isDrawing.value = false; }; </script>
手动自由绘画
如果我们想直接使用一些低级别的 2D 画布 API,第一个方法有局限性。如果需要对画布进行高级访问,最好使用 原生上下文访问
我们将创建一个特殊的离屏画布,用于添加所有绘图。
使用对画布的原生访问,我们可以使用低级别的 2D 上下文函数。
要在舞台上显示画布,我们将使用 Konva.Image
。
- Vanilla
- React
- Vue
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; });
import React from 'react'; import { Stage, Layer, Image } from 'react-konva'; const App = () => { const [tool, setTool] = React.useState('brush'); const isDrawing = React.useRef(false); const imageRef = React.useRef(null); const lastPos = React.useRef(null); const { canvas, context } = React.useMemo(() => { const canvas = document.createElement('canvas'); canvas.width = window.innerWidth; canvas.height = window.innerHeight - 25; const context = canvas.getContext('2d'); context.strokeStyle = '#df4b26'; context.lineJoin = 'round'; context.lineWidth = 5; return { canvas, context }; }, []); const handleMouseDown = (e) => { isDrawing.current = true; lastPos.current = e.target.getStage().getPointerPosition(); }; const handleMouseUp = () => { isDrawing.current = false; }; const handleMouseMove = (e) => { if (!isDrawing.current) { return; } const image = imageRef.current; const stage = e.target.getStage(); context.globalCompositeOperation = tool === 'eraser' ? 'destination-out' : 'source-over'; context.beginPath(); const localPos = { x: lastPos.current.x - image.x(), y: lastPos.current.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(); lastPos.current = pos; image.getLayer().batchDraw(); }; return ( <> <select value={tool} onChange={(e) => { setTool(e.target.value); }} > <option value="brush">画笔</option> <option value="eraser">橡皮擦</option> </select> <Stage width={window.innerWidth} height={window.innerHeight - 25} onMouseDown={handleMouseDown} onMousemove={handleMouseMove} onMouseup={handleMouseUp} onTouchStart={handleMouseDown} onTouchMove={handleMouseMove} onTouchEnd={handleMouseUp} > <Layer> <Image ref={imageRef} image={canvas} x={0} y={0} /> </Layer> </Stage> </> ); }; export default App;
<template> <div> <select v-model="tool"> <option value="brush">画笔</option> <option value="eraser">橡皮擦</option> </select> <v-stage :config="stageConfig" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp" @touchstart="handleMouseDown" @touchmove="handleMouseMove" @touchend="handleMouseUp" > <v-layer ref="layerRef"> <v-image ref="imageRef" :config="imageConfig" /> </v-layer> </v-stage> </div> </template> <script setup> import { ref, onMounted } from 'vue'; const tool = ref('brush'); const isDrawing = ref(false); const lastPos = ref(null); const imageRef = ref(null); const layerRef = ref(null); const stageConfig = { width: window.innerWidth, height: window.innerHeight - 25 }; // 创建画布元素 const canvas = document.createElement('canvas'); canvas.width = window.innerWidth; canvas.height = window.innerHeight - 25; // 获取上下文 const context = canvas.getContext('2d'); context.strokeStyle = '#df4b26'; context.lineJoin = 'round'; context.lineWidth = 5; const imageConfig = { image: canvas, x: 0, y: 0 }; const handleMouseDown = (e) => { isDrawing.value = true; lastPos.value = e.target.getStage().getPointerPosition(); }; const handleMouseUp = () => { isDrawing.value = false; }; const handleMouseMove = (e) => { if (!isDrawing.value) { return; } const ctx = context; const image = imageRef.value.getNode(); const stage = e.target.getStage(); ctx.globalCompositeOperation = tool.value === 'eraser' ? 'destination-out' : 'source-over'; ctx.beginPath(); const localPos = { x: lastPos.value.x - image.x(), y: lastPos.value.y - image.y(), }; ctx.moveTo(localPos.x, localPos.y); const pos = stage.getPointerPosition(); const newLocalPos = { x: pos.x - image.x(), y: pos.y - image.y(), }; ctx.lineTo(newLocalPos.x, newLocalPos.y); ctx.closePath(); ctx.stroke(); lastPos.value = pos; layerRef.value.getNode().batchDraw(); }; </script>