Canvas 小地图——大型 Konva 舞台预览
需要生成画布的小预览吗?
生成小预览的方法有很多种。Konva 并没有提供任何自动执行此操作的方法。
但我们可以使用 Konva 的方法手动生成预览区域。
我们将展示两种选项——克隆和使用图像。在大型应用程序中,从应用的状态生成预览更为合适。
从主舞台克隆节点
我们可以简单地克隆舞台或图层,并从主画布区域的状态更新其内部节点。 同时,简化预览上的形状也是有意义的。比如隐藏文本,去掉轮廓和阴影等。
说明:尝试拖动圆形并双击以添加新圆形。预览会在拖动过程中或添加形状后更新。
- 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}
onDblTap={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"
@dbltap="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();
}, [shapes, updatePreview]);
const handleDragEnd = (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
ref={stageRef}
width={window.innerWidth}
height={window.innerHeight}
onDblClick={handleDblClick}
onDblTap={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"
@dbltap="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 { nextTick, 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 = async (e, id) => {
const { x, y } = e.target.position();
shapes.value = shapes.value.map(shape =>
shape.id === id ? { ...shape, x, y } : shape
);
await nextTick();
updatePreview();
};
const handleDblClick = async (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);
await nextTick();
updatePreview();
};
onMounted(() => {
updatePreview();
});
</script>