鼠标漫游就是通过移动光标和滚轮,完成画布缩放、移动的交互过程。
svg 绘图使用原点在左上角的坐标系统,一个单位代表一像素。这里的像素不能简单理解为屏幕像素,是一个用户单位。svg 的 width 和 height 属性决定图像在用户系统的占位。viewBox 属性则决定占位视口映射到 svg 图像范围的映射。
调整 viewBox 即可完成图像内容的缩放和移动:
鼠标漫游
SvgRoam
(() => {
const svg = document.querySelector('svg');
if (!svg) {
return;
}
const width = Number(svg.getAttribute('width'));
const height = Number(svg.getAttribute('height'));
const diagonal = Math.sqrt(width * width + height * height);
const [x, y, viewWidth, viewHeight] = svg.getAttribute('viewBox')
.split(' ')
.map(item => Number(item));
let currentDiagonal = Math.sqrt(viewWidth * viewWidth + viewHeight * viewHeight);
let draggingContext: {
point: [number, number];
viewBox: string;
}|null = null;
svg.addEventListener('wheel', e => {
e.preventDefault();
if (e.deltaY > 0) {
if (currentDiagonal / diagonal >= 5) {
return;
}
currentDiagonal = currentDiagonal + 0.01 * diagonal;
}
else if (e.deltaY < 0) {
if (currentDiagonal / diagonal <= .1) {
return;
}
currentDiagonal = currentDiagonal - 0.01 * diagonal;
}
const {offsetX, offsetY} = e;
const [strX, strY, strW, strH] = svg.getAttribute('viewBox')?.split(' ') ?? [];
const w = currentDiagonal * width / diagonal;
const h = currentDiagonal * height / diagonal;
const x = Number(strX) + offsetX * (Number(strW) - w) / width;
const y = Number(strY) + offsetY * (Number(strH) - h) / height;
svg.setAttribute('viewBox', `${x} ${y} ${w} ${h}`);
if (draggingContext) {
draggingContext = {
point: [e.offsetX, e.offsetY],
viewBox: `${x} ${y} ${w} ${h}`,
};
}
});
// 拖动
svg.addEventListener('mousedown', e => {
draggingContext = {
point: [e.offsetX, e.offsetY],
viewBox: svg.getAttribute('viewBox') as string,
};
});
window.addEventListener('mouseup', () => {
draggingContext = null;
});
svg.addEventListener('mousemove', e => {
if (!draggingContext) {
return;
}
const offset = [e.offsetX - draggingContext.point[0], e.offsetY - draggingContext.point[1]];
const [strX, strY, strW, strH] = draggingContext.viewBox.split(' ');
const realOffet = [
offset[0] * Number(strW) / width,
offset[1] * Number(strH) / height,
];
const x = Number(strX) - realOffet[0];
const y = Number(strY) - realOffet[1];
svg.setAttribute('viewBox', `${x} ${y} ${strW} ${strH}`);
});
})();
interface ViewInfo {
scale: number;
x: number;
y: number;
}
abstract class Roam {
constructor(el: HTMLElement|SVGSVGElement) {
this.el = el;
this.scaleHandler();
this.dragHandler();
}
abstract init(): void;
el: HTMLElement|SVGSVGElement;
draggingContext: ({point: [number, number]}&ViewInfo)|null = null;
currentScale = 1;
client2offset(clientX: number, clientY: number) {
const { x, y } = this.el.getBoundingClientRect();
return {
offsetX: clientX - x,
offsetY: clientY - y,
};
}
scaleHandler(step = 0.01, minSide = .1, maxSide = 5) {
this.el.addEventListener('wheel', e => {
e.preventDefault();
const event = e as WheelEvent;
if (event.deltaY < 0) {
if (this.currentScale >= maxSide) {
return;
}
this.currentScale = this.currentScale + step;
}
else if (event.deltaY > 0) {
if (this.currentScale <= minSide) {
return;
}
this.currentScale = this.currentScale - step;
}
const { offsetX, offsetY } = this.client2offset(event.clientX, event.clientY);
const { x: oldX, y: oldY, scale: oldScale } = this.getViewInfo();
const x = oldX + offsetX * (oldScale - this.currentScale);
const y = oldY + offsetY * (oldScale - this.currentScale);
const viewInfo = {
x,
y,
scale: this.currentScale,
};
this.setViewInfo(viewInfo);
if (this.draggingContext) {
this.draggingContext = {
point: [offsetX, offsetY],
...viewInfo,
};
}
});
}
dragHandler() {
this.el.addEventListener('mousedown', e => {
// 防止子元素冒泡导致的 offsetX 参照对象变化
const { clientX, clientY } = e as WheelEvent;
const {offsetX, offsetY} = this.client2offset(clientX, clientY);
this.draggingContext = {
point: [offsetX, offsetY],
...this.getViewInfo(),
};
});
window.addEventListener('mouseup', () => {
this.draggingContext = null;
});
this.el.addEventListener('mousemove', e => {
if (!this.draggingContext) {
return;
}
const { clientX, clientY } = e as WheelEvent;
const {offsetX, offsetY} = this.client2offset(clientX, clientY);
const offset = [offsetX - this.draggingContext.point[0], offsetY - this.draggingContext.point[1]];
const { x: oldX, y: oldY, scale: oldScale } = this.draggingContext;
const realOffet = [
offset[0] * Number(oldScale),
offset[1] * Number(oldScale),
];
this.setViewInfo({
x: oldX - realOffet[0],
y: oldY - realOffet[1],
scale: this.currentScale,
});
});
}
abstract getViewInfo(): ViewInfo;
abstract setViewInfo(params: ViewInfo): void;
}
class SvgRoam extends Roam {
constructor(el: SVGSVGElement) {
super(el);
this.init();
}
realWidth = 0;
realHeight = 0;
init() {
this.realWidth = Number(this.el.getAttribute('width'));
this.realHeight = Number(this.el.getAttribute('height'));
this.currentScale = this.getViewInfo().scale;
}
getViewInfo() {
const [strX, strY, strW] = this.el.getAttribute('viewBox')?.split(' ') ?? [];
return {
x: Number(strX),
y: Number(strY),
scale: Number(strW) / this.realWidth,
};
}
setViewInfo({ x, y, scale }: { x: number; y: number; scale: number; }) {
this.el.setAttribute('viewBox', `${x} ${y} ${scale * this.realWidth} ${scale * this.realHeight}`);
}
}
const svg = document.querySelector('svg');
if (svg) {
new SvgRoam(svg);
}
npm install -g typescript
按住shift键,然后鼠标右键,选择在此处打开Powershell 窗口 输入 tsc xxx.ts
将typescript 转为 javascript