接入画布
This commit is contained in:
@@ -0,0 +1,850 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 小地图管理器类
|
||||
* 实现画布的小地图功能,展示当前视窗位置和内容概览
|
||||
*/
|
||||
export class MinimapManager {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {fabric.Canvas} mainCanvas 主画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(mainCanvas, options = {}) {
|
||||
this.mainCanvas = mainCanvas;
|
||||
this.minimapCanvas = null;
|
||||
this.minimapCtx = null;
|
||||
this.container = null;
|
||||
this.minimapSize = options.size || { width: 200, height: 120 };
|
||||
this.visible = options.visible !== undefined ? options.visible : true;
|
||||
this.isDragging = false;
|
||||
this.lastRenderTime = 0;
|
||||
this.renderInterval = options.renderInterval || 100; // 增加渲染间隔到100ms,降低频率
|
||||
this.initialized = false;
|
||||
this.eventHandlers = {};
|
||||
|
||||
// 内容边界,用于确定小地图显示范围
|
||||
this.contentBounds = {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
};
|
||||
|
||||
// 缓存上一次视口大小,用于减少抖动
|
||||
this.lastViewportSize = { width: 0, height: 0 };
|
||||
// 添加缓存标志,避免频繁重新计算
|
||||
this.contentBoundsDirty = true;
|
||||
|
||||
// 预先绑定方法,避免上下文丢失
|
||||
this.render = this.render.bind(this);
|
||||
this.handleMainCanvasChange = this.handleMainCanvasChange.bind(this);
|
||||
this.handleMinimapMouseDown = this.handleMinimapMouseDown.bind(this);
|
||||
this.handleMinimapMouseMove = this.handleMinimapMouseMove.bind(this);
|
||||
this.handleMinimapMouseUp = this.handleMinimapMouseUp.bind(this);
|
||||
this.calculateViewportRect = this.calculateViewportRect.bind(this);
|
||||
this.calculateContentBounds = this.calculateContentBounds.bind(this);
|
||||
this.moveViewport = this.moveViewport.bind(this);
|
||||
|
||||
// 创建canvas元素
|
||||
this._createCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建小地图的canvas元素
|
||||
* @private
|
||||
*/
|
||||
_createCanvas() {
|
||||
// 创建canvas元素
|
||||
this.minimapCanvas = document.createElement("canvas");
|
||||
this.minimapCanvas.width = this.minimapSize.width;
|
||||
this.minimapCanvas.height = this.minimapSize.height;
|
||||
this.minimapCanvas.style.width = "100%";
|
||||
this.minimapCanvas.style.height = "100%";
|
||||
this.minimapCanvas.style.display = this.visible ? "block" : "none";
|
||||
|
||||
// 获取绘图上下文
|
||||
this.minimapCtx = this.minimapCanvas.getContext("2d");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将小地图挂载到指定的DOM容器中
|
||||
* @param {HTMLElement} containerElement 容器DOM元素
|
||||
* @returns {MinimapManager} 返回实例自身,支持链式调用
|
||||
*/
|
||||
mount(containerElement) {
|
||||
if (!containerElement) {
|
||||
console.error("小地图挂载失败:未提供有效的容器元素");
|
||||
return this;
|
||||
}
|
||||
|
||||
// 保存容器引用
|
||||
this.container = containerElement;
|
||||
|
||||
// 清空容器,防止重复挂载
|
||||
while (containerElement.firstChild) {
|
||||
containerElement.removeChild(containerElement.firstChild);
|
||||
}
|
||||
|
||||
// 将canvas添加到容器
|
||||
containerElement.appendChild(this.minimapCanvas);
|
||||
|
||||
// 初始化小地图
|
||||
if (!this.initialized) {
|
||||
// 计算初始内容边界
|
||||
this.calculateContentBounds();
|
||||
|
||||
// 添加事件监听器
|
||||
this.addEventListeners();
|
||||
|
||||
// 首次渲染
|
||||
this.render();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
addEventListeners() {
|
||||
if (!this.mainCanvas || !this.minimapCanvas) return;
|
||||
|
||||
// 监听主画布变化事件
|
||||
this.mainCanvas.on("after:render", this.handleMainCanvasChange);
|
||||
// 仅在缩放时重新计算内容边界,避免频繁计算
|
||||
this.mainCanvas.on("zoom:change", () => {
|
||||
this.contentBoundsDirty = true;
|
||||
this.handleMainCanvasChange();
|
||||
});
|
||||
// 仅当对象添加、删除或修改时重新计算内容边界
|
||||
this.mainCanvas.on("object:added", () => {
|
||||
this.contentBoundsDirty = true;
|
||||
this.handleMainCanvasChange();
|
||||
});
|
||||
this.mainCanvas.on("object:removed", () => {
|
||||
this.contentBoundsDirty = true;
|
||||
this.handleMainCanvasChange();
|
||||
});
|
||||
this.mainCanvas.on("object:modified", () => {
|
||||
this.contentBoundsDirty = true;
|
||||
this.handleMainCanvasChange();
|
||||
});
|
||||
|
||||
// 移动、缩放、旋转操作时使用更强的节流,不重新计算内容边界
|
||||
this.mainCanvas.on("object:moving", this.handleMainCanvasChange);
|
||||
this.mainCanvas.on("object:scaling", this.handleMainCanvasChange);
|
||||
this.mainCanvas.on("object:rotating", this.handleMainCanvasChange);
|
||||
|
||||
// 小地图交互事件 - 鼠标
|
||||
this.eventHandlers.mousedown = this.handleMinimapMouseDown;
|
||||
this.eventHandlers.mousemove = this.handleMinimapMouseMove;
|
||||
this.eventHandlers.mouseup = this.handleMinimapMouseUp;
|
||||
// 移除mouseout事件处理,允许拖动操作持续到鼠标释放
|
||||
|
||||
this.minimapCanvas.addEventListener(
|
||||
"mousedown",
|
||||
this.eventHandlers.mousedown
|
||||
);
|
||||
document.addEventListener("mousemove", this.eventHandlers.mousemove);
|
||||
document.addEventListener("mouseup", this.eventHandlers.mouseup);
|
||||
// 移除mouseout事件监听
|
||||
|
||||
// 小地图交互事件 - 触摸
|
||||
this.eventHandlers.touchstart = (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
this.handleMinimapMouseDown({
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
preventDefault: () => {},
|
||||
});
|
||||
};
|
||||
|
||||
this.eventHandlers.touchmove = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.isDragging) {
|
||||
const touch = e.touches[0];
|
||||
this.handleMinimapMouseMove({
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
preventDefault: () => {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.eventHandlers.touchend = this.handleMinimapMouseUp;
|
||||
|
||||
this.minimapCanvas.addEventListener(
|
||||
"touchstart",
|
||||
this.eventHandlers.touchstart
|
||||
);
|
||||
document.addEventListener("touchmove", this.eventHandlers.touchmove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this.eventHandlers.touchend);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
*/
|
||||
removeEventListeners() {
|
||||
if (!this.mainCanvas || !this.minimapCanvas) return;
|
||||
|
||||
// 移除画布事件监听
|
||||
this.mainCanvas.off("after:render", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("zoom:change", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("object:added", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("object:removed", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("object:modified", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("object:moving", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("object:scaling", this.handleMainCanvasChange);
|
||||
this.mainCanvas.off("object:rotating", this.handleMainCanvasChange);
|
||||
|
||||
// 移除鼠标事件监听
|
||||
this.minimapCanvas.removeEventListener(
|
||||
"mousedown",
|
||||
this.eventHandlers.mousedown
|
||||
);
|
||||
document.removeEventListener("mousemove", this.eventHandlers.mousemove);
|
||||
document.removeEventListener("mouseup", this.eventHandlers.mouseup);
|
||||
|
||||
// 移除触摸事件监听
|
||||
this.minimapCanvas.removeEventListener(
|
||||
"touchstart",
|
||||
this.eventHandlers.touchstart
|
||||
);
|
||||
document.removeEventListener("touchmove", this.eventHandlers.touchmove);
|
||||
document.removeEventListener("touchend", this.eventHandlers.touchend);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理主画布变化事件
|
||||
* 使用节流限制渲染频率
|
||||
*/
|
||||
handleMainCanvasChange() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastRenderTime > this.renderInterval) {
|
||||
this.lastRenderTime = now;
|
||||
|
||||
// 只在内容边界标记为脏时才重新计算
|
||||
if (this.contentBoundsDirty) {
|
||||
this.calculateContentBounds();
|
||||
this.contentBoundsDirty = false;
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算画布内容的边界范围
|
||||
* 包括所有可见对象和画布本身
|
||||
*/
|
||||
calculateContentBounds() {
|
||||
if (!this.mainCanvas) return;
|
||||
|
||||
const objects = this.mainCanvas.getObjects();
|
||||
|
||||
// 初始化为画布尺寸
|
||||
let minX = 0;
|
||||
let minY = 0;
|
||||
let maxX = this.mainCanvas.getWidth();
|
||||
let maxY = this.mainCanvas.getHeight();
|
||||
|
||||
// 如果有对象,则计算所有对象的边界
|
||||
if (objects.length > 0) {
|
||||
// 重置为极值
|
||||
minX = Infinity;
|
||||
minY = Infinity;
|
||||
maxX = -Infinity;
|
||||
maxY = -Infinity;
|
||||
|
||||
// 考虑所有可见对象的边界
|
||||
objects.forEach((obj) => {
|
||||
if (!obj.visible) return;
|
||||
|
||||
const rect = obj.getBoundingRect(true, true);
|
||||
minX = Math.min(minX, rect.left);
|
||||
minY = Math.min(minY, rect.top);
|
||||
maxX = Math.max(maxX, rect.left + rect.width);
|
||||
maxY = Math.max(maxY, rect.top + rect.height);
|
||||
});
|
||||
|
||||
// 确保边界至少包含画布尺寸
|
||||
minX = Math.min(minX, 0);
|
||||
minY = Math.min(minY, 0);
|
||||
maxX = Math.max(maxX, this.mainCanvas.getWidth());
|
||||
maxY = Math.max(maxY, this.mainCanvas.getHeight());
|
||||
}
|
||||
|
||||
// 添加边距
|
||||
const padding =
|
||||
Math.max(this.mainCanvas.getWidth(), this.mainCanvas.getHeight()) * 0.1;
|
||||
this.contentBounds = {
|
||||
minX: minX - padding,
|
||||
minY: minY - padding,
|
||||
maxX: maxX + padding,
|
||||
maxY: maxY + padding,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理小地图鼠标按下事件
|
||||
*/
|
||||
handleMinimapMouseDown(e) {
|
||||
if (!this.visible || !this.minimapCanvas) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const rect = this.minimapCanvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// 检查点击是否在视口矩形内
|
||||
const vpRect = this.calculateViewportRect();
|
||||
|
||||
// 在视口矩形内点击开始拖拽,否则直接跳转到点击位置
|
||||
if (
|
||||
x >= vpRect.x &&
|
||||
x <= vpRect.x + vpRect.width &&
|
||||
y >= vpRect.y &&
|
||||
y <= vpRect.y + vpRect.height
|
||||
) {
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x, y };
|
||||
this.dragStartViewport = { ...vpRect };
|
||||
|
||||
// 缓存当前视口大小,确保拖动过程中大小不变
|
||||
this.lastViewportSize = {
|
||||
width: vpRect.width,
|
||||
height: vpRect.height,
|
||||
};
|
||||
} else {
|
||||
// 直接移动视口中心到点击位置
|
||||
this.moveViewport(x, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理小地图鼠标移动事件
|
||||
*/
|
||||
handleMinimapMouseMove(e) {
|
||||
if (!this.isDragging || !this.visible) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const rect = this.minimapCanvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const deltaX = x - this.dragStart.x;
|
||||
const deltaY = y - this.dragStart.y;
|
||||
|
||||
// 更新拖拽起始位置
|
||||
this.dragStart = { x, y };
|
||||
|
||||
// 移动画布视口
|
||||
this.moveViewport(
|
||||
this.dragStartViewport.x + deltaX,
|
||||
this.dragStartViewport.y + deltaY,
|
||||
false
|
||||
);
|
||||
|
||||
// 更新拖拽起始视口位置
|
||||
this.dragStartViewport = this.calculateViewportRect();
|
||||
|
||||
// 立即渲染小地图,提升拖动流畅度
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理小地图鼠标抬起事件
|
||||
*/
|
||||
handleMinimapMouseUp() {
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动主画布视口到指定位置
|
||||
*/
|
||||
moveViewport(x, y, isCentered) {
|
||||
if (!this.mainCanvas) return;
|
||||
|
||||
// 获取主画布的当前视图信息
|
||||
const vpt = this.mainCanvas.viewportTransform;
|
||||
const zoom = this.mainCanvas.getZoom();
|
||||
|
||||
// 计算内容边界在小地图上的比例
|
||||
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
|
||||
const contentHeight = this.contentBounds.maxY - this.contentBounds.minY;
|
||||
|
||||
const scaleX = this.minimapSize.width / contentWidth;
|
||||
const scaleY = this.minimapSize.height / contentHeight;
|
||||
|
||||
// 计算视口在小地图上的宽高
|
||||
let viewportWidth, viewportHeight;
|
||||
if (this.isDragging && this.lastViewportSize.width > 0) {
|
||||
viewportWidth = this.lastViewportSize.width;
|
||||
viewportHeight = this.lastViewportSize.height;
|
||||
} else {
|
||||
viewportWidth = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX);
|
||||
viewportHeight = Math.round(
|
||||
(this.mainCanvas.getHeight() / zoom) * scaleY
|
||||
);
|
||||
}
|
||||
|
||||
// 添加边界限制,确保视口不会超出小地图
|
||||
x = Math.max(0, Math.min(x, this.minimapSize.width - viewportWidth));
|
||||
y = Math.max(0, Math.min(y, this.minimapSize.height - viewportHeight));
|
||||
|
||||
// 将小地图坐标转换为主画布坐标
|
||||
let targetX = x / scaleX + this.contentBounds.minX;
|
||||
let targetY = y / scaleY + this.contentBounds.minY;
|
||||
|
||||
if (isCentered) {
|
||||
// 如果是直接点击,则将点击位置设为视口中心
|
||||
targetX -= this.mainCanvas.getWidth() / zoom / 2;
|
||||
targetY -= this.mainCanvas.getHeight() / zoom / 2;
|
||||
}
|
||||
|
||||
// 设置主画布的位置
|
||||
this.mainCanvas.setViewportTransform([
|
||||
vpt[0],
|
||||
vpt[1],
|
||||
vpt[2],
|
||||
vpt[3],
|
||||
-targetX * zoom,
|
||||
-targetY * zoom,
|
||||
]);
|
||||
|
||||
// 触发主画布重新渲染
|
||||
this.mainCanvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前视口在小地图中的位置和大小
|
||||
*/
|
||||
calculateViewportRect() {
|
||||
if (!this.mainCanvas) return { x: 0, y: 0, width: 0, height: 0 };
|
||||
|
||||
// 获取主画布的视图变换信息
|
||||
const vpt = this.mainCanvas.viewportTransform;
|
||||
const zoom = this.mainCanvas.getZoom();
|
||||
|
||||
// 计算内容边界在小地图上的比例
|
||||
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
|
||||
const contentHeight = this.contentBounds.maxY - this.contentBounds.minY;
|
||||
|
||||
const scaleX = this.minimapSize.width / contentWidth;
|
||||
const scaleY = this.minimapSize.height / contentHeight;
|
||||
|
||||
// 计算当前视口区域相对于内容边界的位置
|
||||
const viewLeft = -vpt[4] / zoom - this.contentBounds.minX;
|
||||
const viewTop = -vpt[5] / zoom - this.contentBounds.minY;
|
||||
|
||||
// 转换为小地图上的坐标,使用取整减少精度误差
|
||||
const x = Math.round(viewLeft * scaleX);
|
||||
const y = Math.round(viewTop * scaleY);
|
||||
|
||||
// 如果正在拖动,则使用缓存的大小避免抖动
|
||||
let width, height;
|
||||
if (this.isDragging && this.lastViewportSize.width > 0) {
|
||||
width = this.lastViewportSize.width;
|
||||
height = this.lastViewportSize.height;
|
||||
} else {
|
||||
width = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX);
|
||||
height = Math.round((this.mainCanvas.getHeight() / zoom) * scaleY);
|
||||
|
||||
// 更新缓存的视口大小
|
||||
if (!this.isDragging) {
|
||||
this.lastViewportSize = { width, height };
|
||||
}
|
||||
}
|
||||
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染小地图
|
||||
* 使用高性能的离屏渲染
|
||||
*/
|
||||
render() {
|
||||
if (!this.visible || !this.minimapCanvas || !this.mainCanvas) return;
|
||||
|
||||
try {
|
||||
// 清空小地图
|
||||
this.minimapCtx.clearRect(
|
||||
0,
|
||||
0,
|
||||
this.minimapSize.width,
|
||||
this.minimapSize.height
|
||||
);
|
||||
|
||||
// 绘制小地图背景
|
||||
this.minimapCtx.fillStyle = this.mainCanvas.backgroundColor || "#f0f0f0";
|
||||
this.minimapCtx.fillRect(
|
||||
0,
|
||||
0,
|
||||
this.minimapSize.width,
|
||||
this.minimapSize.height
|
||||
);
|
||||
|
||||
// 计算内容边界尺寸
|
||||
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
|
||||
const contentHeight = this.contentBounds.maxY - this.contentBounds.minY;
|
||||
|
||||
// 检查是否有内容需要渲染
|
||||
const objects = this.mainCanvas.getObjects();
|
||||
if (objects.length === 0) {
|
||||
// 如果没有对象,只需绘制视口框
|
||||
this.drawViewportBox();
|
||||
return;
|
||||
}
|
||||
|
||||
// 优化离屏渲染尺寸计算
|
||||
const maxSize = 1000; // 限制离屏canvas最大尺寸,提高性能
|
||||
let offscreenWidth = contentWidth;
|
||||
let offscreenHeight = contentHeight;
|
||||
let scale = 1;
|
||||
|
||||
if (contentWidth > maxSize || contentHeight > maxSize) {
|
||||
scale = Math.min(maxSize / contentWidth, maxSize / contentHeight);
|
||||
offscreenWidth *= scale;
|
||||
offscreenHeight *= scale;
|
||||
}
|
||||
|
||||
const offscreenCanvas = document.createElement("canvas");
|
||||
offscreenCanvas.width = offscreenWidth;
|
||||
offscreenCanvas.height = offscreenHeight;
|
||||
const offCtx = offscreenCanvas.getContext("2d");
|
||||
|
||||
// 创建临时fabric.Canvas用于渲染全内容
|
||||
const tempFabricCanvas = new fabric.StaticCanvas();
|
||||
tempFabricCanvas.setWidth(offscreenWidth);
|
||||
tempFabricCanvas.setHeight(offscreenHeight);
|
||||
tempFabricCanvas.backgroundColor = this.mainCanvas.backgroundColor;
|
||||
|
||||
// 复制主画布对象到临时画布
|
||||
objects.forEach((obj) => {
|
||||
if (!obj.visible) return;
|
||||
|
||||
try {
|
||||
// 使用浅克隆,避免深度克隆带来的性能开销
|
||||
const clonedObj = fabric.util.object.clone(obj);
|
||||
|
||||
// 调整对象位置和大小,使其相对于内容边界并适应缩放
|
||||
clonedObj.set({
|
||||
left: (obj.left - this.contentBounds.minX) * scale,
|
||||
top: (obj.top - this.contentBounds.minY) * scale,
|
||||
scaleX: obj.scaleX * scale,
|
||||
scaleY: obj.scaleY * scale,
|
||||
// 禁用对象的交互属性,提高性能
|
||||
selectable: false,
|
||||
evented: false,
|
||||
hasControls: false,
|
||||
hasBorders: false,
|
||||
});
|
||||
|
||||
tempFabricCanvas.add(clonedObj);
|
||||
} catch (err) {
|
||||
console.warn("无法克隆对象到小地图", err);
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染临时画布
|
||||
tempFabricCanvas.renderAll();
|
||||
|
||||
// 将临时画布内容绘制到离屏canvas
|
||||
offCtx.drawImage(tempFabricCanvas.getElement(), 0, 0);
|
||||
|
||||
// 将离屏canvas缩放绘制到小地图
|
||||
this.minimapCtx.drawImage(
|
||||
offscreenCanvas,
|
||||
0,
|
||||
0,
|
||||
offscreenWidth,
|
||||
offscreenHeight,
|
||||
0,
|
||||
0,
|
||||
this.minimapSize.width,
|
||||
this.minimapSize.height
|
||||
);
|
||||
|
||||
// 释放临时画布资源
|
||||
// tempFabricCanvas.dispose();
|
||||
|
||||
// 绘制视口框
|
||||
this.drawViewportBox();
|
||||
} catch (error) {
|
||||
console.error("小地图渲染出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制视口框,从render方法中分离出来提高代码清晰度
|
||||
*/
|
||||
drawViewportBox() {
|
||||
// 计算当前视口范围
|
||||
const vpRect = this.calculateViewportRect();
|
||||
|
||||
// 视口矩形边框
|
||||
this.minimapCtx.strokeStyle = "#ff3333";
|
||||
this.minimapCtx.lineWidth = 2;
|
||||
this.minimapCtx.strokeRect(vpRect.x, vpRect.y, vpRect.width, vpRect.height);
|
||||
|
||||
// 视口矩形半透明填充
|
||||
this.minimapCtx.fillStyle = "rgba(255, 0, 0, 0.1)";
|
||||
this.minimapCtx.fillRect(vpRect.x, vpRect.y, vpRect.width, vpRect.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置小地图可见性
|
||||
*/
|
||||
setVisibility(visible) {
|
||||
this.visible = visible;
|
||||
|
||||
// 更新canvas显示状态
|
||||
if (this.minimapCanvas) {
|
||||
this.minimapCanvas.style.display = visible ? "block" : "none";
|
||||
}
|
||||
|
||||
if (visible && this.initialized) {
|
||||
this.contentBoundsDirty = true; // 标记需要重新计算内容边界
|
||||
this.calculateContentBounds();
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新小地图
|
||||
* 重新读取大画布数据并渲染
|
||||
*/
|
||||
refresh() {
|
||||
this.contentBoundsDirty = true;
|
||||
this.calculateContentBounds();
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整小地图大小
|
||||
* @param {Object} size 小地图尺寸,{width, height}
|
||||
*/
|
||||
resize(size) {
|
||||
if (!size || !size.width || !size.height) return;
|
||||
|
||||
this.minimapSize = {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
};
|
||||
|
||||
if (this.minimapCanvas) {
|
||||
this.minimapCanvas.width = size.width;
|
||||
this.minimapCanvas.height = size.height;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源,释放内存
|
||||
*/
|
||||
dispose() {
|
||||
this.removeEventListeners();
|
||||
|
||||
// 从DOM中移除canvas
|
||||
if (
|
||||
this.container &&
|
||||
this.minimapCanvas &&
|
||||
this.minimapCanvas.parentNode === this.container
|
||||
) {
|
||||
this.container.removeChild(this.minimapCanvas);
|
||||
}
|
||||
|
||||
this.mainCanvas = null;
|
||||
this.minimapCanvas = null;
|
||||
this.minimapCtx = null;
|
||||
this.container = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新小地图
|
||||
* 使用更高效的渲染策略,减少不必要的重绘
|
||||
*/
|
||||
update() {
|
||||
if (!this.enabled || !this.minimapCanvas) return;
|
||||
|
||||
// 使用节流来控制更新频率
|
||||
if (this._updateTimeout) {
|
||||
clearTimeout(this._updateTimeout);
|
||||
}
|
||||
|
||||
this._updateTimeout = setTimeout(() => {
|
||||
this._renderMinimap();
|
||||
}, 100); // 100ms 的节流,避免频繁渲染
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染小地图
|
||||
* 优化渲染性能,只在必要时重绘
|
||||
*/
|
||||
_renderMinimap() {
|
||||
if (!this.minimapCanvas || !this.canvas) return;
|
||||
|
||||
const ctx = this.minimapCanvas.getContext("2d");
|
||||
const ratio = this.minimapCanvas.width / this.canvas.width;
|
||||
|
||||
// 清除小地图
|
||||
ctx.clearRect(0, 0, this.minimapCanvas.width, this.minimapCanvas.height);
|
||||
|
||||
// 使用缓存策略
|
||||
if (!this._minimapCache || this._shouldUpdateCache()) {
|
||||
// 创建离屏画布作为缓存
|
||||
if (!this._offscreenCanvas) {
|
||||
this._offscreenCanvas = document.createElement("canvas");
|
||||
this._offscreenCanvas.width = this.minimapCanvas.width;
|
||||
this._offscreenCanvas.height = this.minimapCanvas.height;
|
||||
}
|
||||
|
||||
const offCtx = this._offscreenCanvas.getContext("2d");
|
||||
offCtx.clearRect(
|
||||
0,
|
||||
0,
|
||||
this._offscreenCanvas.width,
|
||||
this._offscreenCanvas.height
|
||||
);
|
||||
|
||||
// 绘制图层内容到离屏画布
|
||||
this._renderLayersToMinimap(offCtx, ratio);
|
||||
|
||||
// 保存渲染时间戳
|
||||
this._lastCacheUpdate = Date.now();
|
||||
this._minimapCache = true;
|
||||
}
|
||||
|
||||
// 将缓存的内容渲染到实际小地图画布
|
||||
if (this._offscreenCanvas) {
|
||||
ctx.drawImage(this._offscreenCanvas, 0, 0);
|
||||
}
|
||||
|
||||
// 绘制可视区域指示器
|
||||
this._renderViewportIndicator(ctx, ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该更新小地图缓存
|
||||
*/
|
||||
_shouldUpdateCache() {
|
||||
// 如果没有缓存或缓存时间超过500ms,则更新
|
||||
return !this._lastCacheUpdate || Date.now() - this._lastCacheUpdate > 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染图层内容到小地图
|
||||
*/
|
||||
_renderLayersToMinimap(ctx, ratio) {
|
||||
// 获取画布上所有可见的图层
|
||||
const visibleLayers = [];
|
||||
|
||||
// 安全地访问图层数据,避免 "forEach is not a function" 错误
|
||||
if (this.canvas && this.canvas.layers) {
|
||||
// 检查 layers 是否是响应式对象 (有 value 属性)
|
||||
const layersArray =
|
||||
typeof this.canvas.layers.value !== "undefined"
|
||||
? this.canvas.layers.value
|
||||
: Array.isArray(this.canvas.layers)
|
||||
? this.canvas.layers
|
||||
: [];
|
||||
|
||||
// 过滤出可见图层
|
||||
layersArray.forEach((layer) => {
|
||||
if (layer.visible) {
|
||||
visibleLayers.push(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 按照图层顺序渲染到小地图
|
||||
for (const layer of visibleLayers) {
|
||||
let objectsToRender = [];
|
||||
|
||||
// 根据图层类型获取要渲染的对象
|
||||
if (layer.type === "background" && layer.fabricObject) {
|
||||
objectsToRender = [layer.fabricObject];
|
||||
} else if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
|
||||
objectsToRender = layer.fabricObjects;
|
||||
}
|
||||
|
||||
for (const fabricObj of objectsToRender) {
|
||||
if (!fabricObj.visible) continue;
|
||||
|
||||
// 根据对象类型渲染到小地图
|
||||
if (fabricObj.type === "image" && fabricObj._element) {
|
||||
ctx.globalAlpha = fabricObj.opacity || 1;
|
||||
const left = fabricObj.left * ratio;
|
||||
const top = fabricObj.top * ratio;
|
||||
const width = fabricObj.width * fabricObj.scaleX * ratio;
|
||||
const height = fabricObj.height * fabricObj.scaleY * ratio;
|
||||
|
||||
ctx.drawImage(fabricObj._element, left, top, width, height);
|
||||
} else if (
|
||||
fabricObj.type === "path" ||
|
||||
fabricObj.type === "rect" ||
|
||||
fabricObj.type === "circle"
|
||||
) {
|
||||
// 简单地用颜色块表示其他类型的对象
|
||||
ctx.fillStyle = fabricObj.fill || "#888";
|
||||
ctx.globalAlpha = fabricObj.opacity || 0.5;
|
||||
|
||||
const left = fabricObj.left * ratio;
|
||||
const top = fabricObj.top * ratio;
|
||||
const width =
|
||||
(fabricObj.width || 20) * (fabricObj.scaleX || 1) * ratio;
|
||||
const height =
|
||||
(fabricObj.height || 20) * (fabricObj.scaleY || 1) * ratio;
|
||||
|
||||
ctx.fillRect(left, top, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染视口指示器
|
||||
*/
|
||||
_renderViewportIndicator(ctx, ratio) {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
if (!vpt) return;
|
||||
|
||||
// 计算可视区域在小地图上的位置和大小
|
||||
const zoom = this.canvas.getZoom();
|
||||
const viewportWidth = this.canvas.width / zoom;
|
||||
const viewportHeight = this.canvas.height / zoom;
|
||||
|
||||
const x = (-vpt[4] / zoom) * ratio;
|
||||
const y = (-vpt[5] / zoom) * ratio;
|
||||
const width = viewportWidth * ratio;
|
||||
const height = viewportHeight * ratio;
|
||||
|
||||
// 绘制视口指示器
|
||||
ctx.strokeStyle = "#ff0000";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制完全更新小地图
|
||||
*/
|
||||
forceUpdate() {
|
||||
this._minimapCache = false;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
export default MinimapManager;
|
||||
Reference in New Issue
Block a user