缩放画布时保持标签和控制柄大小不变
当你缩放 stage 时,所有内容都会缩放——包括那些原本作为界面的部分。标签在 20% 时变得难以阅读,在 400% 时又变得巨大。拖动控制柄会不断变大,直到遮住它们原本要调整大小的图形。
解决方法是按照 stage 缩放比例的倒数缩放这些节点,使两者相互抵消。
const s = stage.scaleX();
label.scale({ x: 1 / s, y: 1 / s });
节点会保留其在场景坐标中的位置,因此它仍会随所属图形一起移动。只有它的大小不再变化。
何时重新计算
在修改缩放的同一个处理程序中执行。不存在名为 scale change 的事件——Konva 将属性事件命名为 <attr>Change,因此真正的事件是 scaleXChange:
// Correct: fires when scaleX is set.
stage.on('scaleXChange', updateLabels);
// Not a real event. This handler will never run, silently.
stage.on('scale change', updateLabels);
监听 scaleXChange 是可行的 ,但通常直接在滚轮处理程序中调用更新函数更简单,并且可以将重绘集中在一个位置。
固定在图形上的标签
滚动以缩放。标签在任何缩放级别下都保持清晰易读,同时它们所属的图形仍会正常缩放。
- Vanilla
- React
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: 500,
height: 300,
draggable: true,
});
const layer = new Konva.Layer();
stage.add(layer);
const shapes = [
{ x: 90, y: 90, radius: 45, fill: '#2f6df6', name: 'Alpha' },
{ x: 250, y: 170, radius: 35, fill: '#10784f', name: 'Beta' },
{ x: 390, y: 100, radius: 40, fill: '#d6336c', name: 'Gamma' },
];
const labels = [];
shapes.forEach((s) => {
layer.add(new Konva.Circle({ x: s.x, y: s.y, radius: s.radius, fill: s.fill }));
// The label sits in scene coordinates, so it moves with its circle.
const label = new Konva.Text({
x: s.x,
y: s.y + s.radius + 6,
text: s.name,
fontSize: 14,
fill: '#333',
});
label.offsetX(label.width() / 2);
labels.push(label);
layer.add(label);
});
// Undo the stage scale on every label.
function keepLabelsReadable() {
const s = stage.scaleX();
labels.forEach((label) => label.scale({ x: 1 / s, y: 1 / s }));
}
const hint = new Konva.Text({
x: 10,
y: 10,
text: 'scroll to zoom, drag to pan',
fontSize: 12,
fill: '#888',
});
layer.add(hint);
stage.on('wheel', (e) => {
e.evt.preventDefault();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const pointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
const direction = e.evt.deltaY > 0 ? -1 : 1;
const newScale = Math.max(0.2, Math.min(5, oldScale * (direction > 0 ? 1.08 : 1 / 1.08)));
stage.scale({ x: newScale, y: newScale });
stage.position({
x: pointer.x - pointTo.x * newScale,
y: pointer.y - pointTo.y * newScale,
});
keepLabelsReadable();
});
import React, { useState } from 'react';
import { Stage, Layer, Circle, Text } from 'react-konva';
const SHAPES = [
{ x: 90, y: 90, radius: 45, fill: '#2f6df6', name: 'Alpha' },
{ x: 250, y: 170, radius: 35, fill: '#10784f', name: 'Beta' },
{ x: 390, y: 100, radius: 40, fill: '#d6336c', name: 'Gamma' },
];
const App = () => {
const [view, setView] = useState({ scale: 1, x: 0, y: 0 });
const handleWheel = (e) => {
e.evt.preventDefault();
const stage = e.target.getStage();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const pointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
const direction = e.evt.deltaY > 0 ? -1 : 1;
const scale = Math.max(
0.2,
Math.min(5, oldScale * (direction > 0 ? 1.08 : 1 / 1.08))
);
setView({
scale,
x: pointer.x - pointTo.x * scale,
y: pointer.y - pointTo.y * scale,
});
};
const handleDragMove = (e) => {
const { x, y } = e.target.position();
setView((current) => ({ ...current, x, y }));
};
return (
<Stage
width={500}
height={300}
draggable
scaleX={view.scale}
scaleY={view.scale}
x={view.x}
y={view.y}
onWheel={handleWheel}
onDragMove={handleDragMove}
>
<Layer>
{SHAPES.map((s) => (
<Circle key={s.name} x={s.x} y={s.y} radius={s.radius} fill={s.fill} />
))}
{SHAPES.map((s) => (
<Text
key={s.name}
x={s.x}
y={s.y + s.radius + 6}
text={s.name}
width={100}
fontSize={14}
fill="#333"
offsetX={50}
align="center"
// Undo the stage scale so the label keeps its size on screen.
scaleX={1 / view.scale}
scaleY={1 / view.scale}
/>
))}
<Text x={10} y={10} text="scroll to zoom, drag to pan" fontSize={12} fill="#888"
scaleX={1 / view.scale} scaleY={1 / view.scale} />
</Layer>
</Stage>
);
};
export default App;
描边宽度是一个特殊情况
描边有自己的开关,因此你不需要为它进行反向缩放:
shape.strokeScaleEnabled(false); // the stroke keeps its pixel width when zoomed
Konva.Transformer 的锚点是普通形状,因此会随舞台一起缩放。要在缩放时保持锚点大小可用,请根据缩放比例设置 anchorSize,而不是对 transformer 进行反向缩放:
tr.anchorSize(10 / stage.scaleX());
完全忽略缩放的图层
反向缩放可以保持节点的大小不变,同时让其位置仍然跟随场景。对于 HUD——工具栏、比例尺、坐标读数——你希望大小和位置都固定在屏幕空间中。
每个图层都是舞台的子项,因此会继承舞台变换。不要只抵消缩放,而是抵消整个变换:
function pinLayerToScreen(layer) {
const s = stage.scaleX();
layer.scale({ x: 1 / s, y: 1 / s });
layer.position({ x: -stage.x() / s, y: -stage.y() / s });
}
这会将 图层坐标映射回屏幕坐标:无论舞台如何缩放或平移,该图层上位于 (10, 10) 的节点都会保持在距离左上角 10 像素的位置。请在调用反向缩放的同一位置调用它。
如果 HUD 图层纯粹用于装饰,请将其设置为 listening: false,这样它就不会吸收原本应传递给下方场景的点击。