Canvas 小地图(Minimap)——预览大型 Konva 舞台
需要生成 Canvas 的小型预览吗?
生成小型预览的方法很多。Konva 不提供自动执行此操作的方法。
但可以使用 Konva 方法手动生成预览区域。
下面介绍两种方式:克隆和使用图像。在大型应用中,最好根据应用 state 生成预 览。
从主舞台克隆节点
可以克隆舞台或图层,并根据主 Canvas 区域的 state 更新其内部节点。 在预览中简化图形也很有意义,例如隐藏文本、移除描边和阴影等。
操作说明:尝试拖动圆形,并双击以添加新圆形。拖动时或添加新图形后,预览会更新。
- Vanilla
- React
- Vue
import Konva from 'konva';
// Create preview container
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);
// generate random shapes
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,
// each shape MUST have unique name
// so we can easily update the preview clone by name
name: 'shape-' + i,
});
layer.add(shape);
}
// create smaller preview stage
const previewStage = new Konva.Stage({
container: 'preview',
width: window.innerWidth / 4,
height: window.innerHeight / 4,
scaleX: 1 / 4,
scaleY: 1 / 4,
});
// clone original layer, and disable all events on it
let previewLayer = layer.clone({ listening: false });
previewStage.add(previewLayer);
function updatePreview() {
// we just need to update ALL nodes in the preview
layer.children.forEach((shape) => {
// find cloned node
const clone = previewLayer.findOne('.' + shape.name());
// update its position from the original
clone.position(shape.position());
});
}
stage.on('dragmove', updatePreview);
// add new shapes on double click or double tap
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);
// remove all layer
previewLayer.destroy();
// generate new one
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';
// Create preview container
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);
// generate random shapes
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;
// use pixelRatio to generate smaller preview
const url = stage.toDataURL({ pixelRatio: scale });
preview.src = url;
}
// update preview only on dragend for performance
stage.on('dragend', updatePreview);
// add new shapes on double click or double tap
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();
});
// show initial preview
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>