如何使用 Konva 在画布上预览大舞台?
需要生成画布的小预览吗?
生成小预览的方法有很多种。Konva
并没有提供任何自动执行此操作的方法。
但我们可以使用 Konva
的方法手动生成预览区域。
我们将展示两种选项 - 克隆和使用图像。在大型应用程序中,从应用的状态生成预览更为合适。
从主舞台克隆节点
我们可以简单地克隆舞台或图层,并从主画布区域的状态更新其内部节点。 同时,简化预览上的形状也是有意义的 。比如隐藏文本,去掉轮廓和阴影等。
说明:尝试拖动圆形,双击以添加新的圆形。预览将在您完成拖动(dragend)或添加新形状后更新。
- Vanilla
- React
- Vue
import Konva from 'konva'; // 创建预览容器 const preview = document.createElement('div'); preview.id = 'preview'; preview.style.position = 'absolute'; preview.style.top = '2px'; preview.style.right = '2px'; preview.style.border = '1px solid grey'; preview.style.backgroundColor = 'lightgrey'; document.body.appendChild(preview); const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // 生成随机形状 for (let i = 0; i < 10; i++) { const shape = new Konva.Circle({ x: Math.random() * stage.width(), y: Math.random() * stage.height(), radius: Math.random() * 30 + 5, fill: Konva.Util.getRandomColor(), draggable: true, // 每个形状必须具有唯一名称 // 这样我们可以通过名称轻松更新预览克隆 name: 'shape-' + i, }); layer.add(shape); } // 创建较小的预览舞台 const previewStage = new Konva.Stage({ container: 'preview', width: window.innerWidth / 4, height: window.innerHeight / 4, scaleX: 1 / 4, scaleY: 1 / 4, }); // 克隆原始图层,并禁用其所有事件 let previewLayer = layer.clone({ listening: false }); previewStage.add(previewLayer); function updatePreview() { // 我们只需要更新预览中的所有节点 layer.children.forEach((shape) => { // 找到克隆节点 const clone = previewLayer.findOne('.' + shape.name()); // 更新其位置 clone.position(shape.position()); }); } stage.on('dragmove', updatePreview); // 双击或双击以添加新形状 stage.on('dblclick dbltap', () => { const shape = new Konva.Circle({ x: stage.getPointerPosition().x, y: stage.getPointerPosition().y, radius: Math.random() * 30 + 5, fill: Konva.Util.getRandomColor(), draggable: true, name: 'shape-' + layer.children.length, }); layer.add(shape); // 移除所有图层 previewLayer.destroy(); // 生成新的 previewLayer = layer.clone({ listening: false }); previewStage.add(previewLayer); });
import React from 'react'; import { Stage, Layer, Circle } from 'react-konva'; const getRandomColor = () => { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; }; const App = () => { const [shapes, setShapes] = React.useState(() => Array.from({ length: 10 }, (_, i) => ({ id: i, x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: Math.random() * 30 + 5, fill: getRandomColor(), })) ); const handleDragMove = (e, id) => { const { x, y } = e.target.position(); setShapes(shapes.map(shape => shape.id === id ? { ...shape, x, y } : shape )); }; const handleDblClick = (e) => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); const newShape = { id: shapes.length, x: pos.x, y: pos.y, radius: Math.random() * 30 + 5, fill: getRandomColor(), }; setShapes([...shapes, newShape]); }; return ( <div style={{ position: 'relative' }}> <Stage width={window.innerWidth} height={window.innerHeight} onDblClick={handleDblClick} onTap={handleDblClick} > <Layer> {shapes.map(shape => ( <Circle key={shape.id} {...shape} draggable onDragMove={(e) => handleDragMove(e, shape.id)} /> ))} </Layer> </Stage> <div style={{ position: 'absolute', top: '2px', right: '2px', border: '1px solid grey', backgroundColor: 'lightgrey', }} > <Stage width={window.innerWidth / 4} height={window.innerHeight / 4} scaleX={1/4} scaleY={1/4} > <Layer> {shapes.map(shape => ( <Circle key={shape.id} {...shape} listening={false} /> ))} </Layer> </Stage> </div> </div> ); }; export default App;
<template> <div style="position: relative"> <v-stage :config="stageConfig" @dblclick="handleDblClick" @tap="handleDblClick" > <v-layer> <v-circle v-for="shape in shapes" :key="shape.id" :config="{ ...shape, draggable: true }" @dragmove="(e) => handleDragMove(e, shape.id)" /> </v-layer> </v-stage> <div style="position: absolute; top: 2px; right: 2px; border: 1px solid grey; background-color: lightgrey" > <v-stage :config="previewConfig"> <v-layer> <v-circle v-for="shape in shapes" :key="shape.id" :config="{ ...shape, listening: false }" /> </v-layer> </v-stage> </div> </div> </template> <script setup> import { ref } from 'vue'; const getRandomColor = () => { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; }; const stageConfig = { width: window.innerWidth, height: window.innerHeight, }; const previewConfig = { width: window.innerWidth / 4, height: window.innerHeight / 4, scaleX: 1/4, scaleY: 1/4, }; const shapes = ref( Array.from({ length: 10 }, (_, i) => ({ id: i, x: Math.random() * stageConfig.width, y: Math.random() * stageConfig.height, radius: Math.random() * 30 + 5, fill: getRandomColor(), })) ); const handleDragMove = (e, id) => { const { x, y } = e.target.position(); shapes.value = shapes.value.map(shape => shape.id === id ? { ...shape, x, y } : shape ); }; const handleDblClick = (e) => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); const newShape = { id: shapes.value.length, x: pos.x, y: pos.y, radius: Math.random() * 30 + 5, fill: getRandomColor(), }; shapes.value.push(newShape); }; </script>
使用图像预览
或者我们可以将舞台导出为图像并将其用作预览。
出于性能原因,我们并不会在每次 dragmove
事件时更新预览。
- Vanilla
- React
- Vue
import Konva from 'konva'; // 创建预览容器 const preview = document.createElement('img'); preview.id = 'preview'; preview.style.position = 'absolute'; preview.style.top = '2px'; preview.style.right = '2px'; preview.style.border = '1px solid grey'; preview.style.backgroundColor = 'lightgrey'; document.body.appendChild(preview); const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); const layer = new Konva.Layer(); stage.add(layer); // 生成随机形状 for (let i = 0; i < 10; i++) { const shape = new Konva.Circle({ x: Math.random() * stage.width(), y: Math.random() * stage.height(), radius: Math.random() * 30 + 5, fill: Konva.Util.getRandomColor(), draggable: true, name: 'shape-' + i, }); layer.add(shape); } function updatePreview() { const scale = 1 / 4; // 使用像素比率生成较小的预览 const url = stage.toDataURL({ pixelRatio: scale }); preview.src = url; } // 仅在拖动结束时更新预览以提高性能 stage.on('dragend', updatePreview); // 双击或双击以添加新形状 stage.on('dblclick dbltap', () => { const shape = new Konva.Circle({ x: stage.getPointerPosition().x, y: stage.getPointerPosition().y, radius: Math.random() * 30 + 5, fill: Konva.Util.getRandomColor(), draggable: true, name: 'shape-' + layer.children.length, }); layer.add(shape); updatePreview(); }); // 显示初始预览 updatePreview();
import React from 'react'; import { Stage, Layer, Circle } from 'react-konva'; const getRandomColor = () => { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; }; const App = () => { const [shapes, setShapes] = React.useState(() => Array.from({ length: 10 }, (_, i) => ({ id: i, x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: Math.random() * 30 + 5, fill: getRandomColor(), })) ); const [previewUrl, setPreviewUrl] = React.useState(''); const stageRef = React.useRef(null); const updatePreview = React.useCallback(() => { if (!stageRef.current) return; const scale = 1 / 4; const url = stageRef.current.toDataURL({ pixelRatio: scale }); setPreviewUrl(url); }, []); React.useEffect(() => { updatePreview(); }, [updatePreview]); const handleDragEnd = (e, id) => { const { x, y } = e.target.position(); setShapes(shapes.map(shape => shape.id === id ? { ...shape, x, y } : shape )); updatePreview(); }; const handleDblClick = (e) => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); const newShape = { id: shapes.length, x: pos.x, y: pos.y, radius: Math.random() * 30 + 5, fill: getRandomColor(), }; setShapes([...shapes, newShape]); updatePreview(); }; return ( <div style={{ position: 'relative' }}> <Stage ref={stageRef} width={window.innerWidth} height={window.innerHeight} onDblClick={handleDblClick} onTap={handleDblClick} > <Layer> {shapes.map(shape => ( <Circle key={shape.id} {...shape} draggable onDragEnd={(e) => handleDragEnd(e, shape.id)} /> ))} </Layer> </Stage> <img src={previewUrl} alt="preview" style={{ position: 'absolute', top: '2px', right: '2px', border: '1px solid grey', backgroundColor: 'lightgrey', }} /> </div> ); }; export default App;
<template> <div style="position: relative"> <v-stage ref="stageRef" :config="stageConfig" @dblclick="handleDblClick" @tap="handleDblClick" > <v-layer> <v-circle v-for="shape in shapes" :key="shape.id" :config="{ ...shape, draggable: true }" @dragend="(e) => handleDragEnd(e, shape.id)" /> </v-layer> </v-stage> <img :src="previewUrl" alt="preview" style="position: absolute; top: 2px; right: 2px; border: 1px solid grey; background-color: lightgrey" /> </div> </template> <script setup> import { ref, onMounted } from 'vue'; const getRandomColor = () => { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; }; const stageConfig = { width: window.innerWidth, height: window.innerHeight, }; const shapes = ref( Array.from({ length: 10 }, (_, i) => ({ id: i, x: Math.random() * stageConfig.width, y: Math.random() * stageConfig.height, radius: Math.random() * 30 + 5, fill: getRandomColor(), })) ); const previewUrl = ref(''); const stageRef = ref(null); const updatePreview = () => { if (!stageRef.value) return; const scale = 1 / 4; const url = stageRef.value.getNode().toDataURL({ pixelRatio: scale }); previewUrl.value = url; }; const handleDragEnd = (e, id) => { const { x, y } = e.target.position(); shapes.value = shapes.value.map(shape => shape.id === id ? { ...shape, x, y } : shape ); updatePreview(); }; const handleDblClick = (e) => { const stage = e.target.getStage(); const pos = stage.getPointerPosition(); const newShape = { id: shapes.value.length, x: pos.x, y: pos.y, radius: Math.random() * 30 + 5, fill: getRandomColor(), }; shapes.value.push(newShape); updatePreview(); }; onMounted(() => { updatePreview(); }); </script>