接入画布

This commit is contained in:
X1627315083
2025-06-09 10:25:54 +08:00
parent 87a08f5f8f
commit c266967f16
157 changed files with 43833 additions and 1571 deletions

View File

@@ -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;