Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions plugins/Image-Toolbox/core/src/CanvasManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,19 +450,105 @@ class CanvasManager {
if (newWidth <= 0 || newHeight <= 0) return;

if (this.canvas.width !== newWidth || this.canvas.height !== newHeight) {
this.canvas.setWidth(newWidth);
this.canvas.setHeight(newHeight);
this.canvas.calcOffset();

// 如果已加载图片,重新适配
// 调整画布尺寸时保留编辑内容的相对布局,避免覆盖层与原图错位
if (this.originalImage) {
this.fitToCanvas();
this._resizePreservingLayout(newWidth, newHeight);
} else {
this.canvas.setWidth(newWidth);
this.canvas.setHeight(newHeight);
this.canvas.calcOffset();
}

eventBus.emit('canvas:resized', { width: newWidth, height: newHeight });
}
}

/**
* 调整画布尺寸时保留编辑内容的相对布局
* 计算原图在新画布尺寸下的 fit 变换,将变换差值同步应用到所有覆盖层和裁剪路径,
* 使覆盖层(文字/图形/画笔/马赛克等)与原图的相对位置保持不变
*/
_resizePreservingLayout(newWidth, newHeight) {
const img = this.originalImage;
const padding = 40;

// 记录原图当前的变换(可能是 fit 状态,也可能是用户手动调整后的状态)
const oldLeft = img.left;
const oldTop = img.top;
const oldScaleX = img.scaleX;
const oldScaleY = img.scaleY;

// 计算新画布下的 fit 变换
const availableW = newWidth - padding * 2;
const availableH = newHeight - padding * 2;
const newScale = Math.min(availableW / img.width, availableH / img.height, 1);
const newLeft = (newWidth - img.width * newScale) / 2;
const newTop = (newHeight - img.height * newScale) / 2;

// 计算缩放比例(避免除零)
const ratioX = oldScaleX ? newScale / oldScaleX : 1;
const ratioY = oldScaleY ? newScale / oldScaleY : 1;

// 更新画布尺寸
this.canvas.setWidth(newWidth);
this.canvas.setHeight(newHeight);
this.canvas.calcOffset();

// 将差值应用到所有覆盖层(非原图、非临时对象)
const overlays = this.canvas.getObjects().filter(obj =>
obj !== img &&
!obj.excludeFromHistory &&
!obj.excludeFromLayer
);
overlays.forEach(obj => {
const relX = obj.left - oldLeft;
const relY = obj.top - oldTop;
obj.set({
left: newLeft + relX * ratioX,
top: newTop + relY * ratioY,
scaleX: obj.scaleX * ratioX,
scaleY: obj.scaleY * ratioY,
});
obj.setCoords();
});

// 同步调整画布级裁剪路径,保留裁剪效果与原图的相对位置
this._transformClipPath(this.canvas.clipPath, oldLeft, oldTop, newLeft, newTop, ratioX, ratioY);

// 应用新的 fit 变换到原图
img.set({
scaleX: newScale,
scaleY: newScale,
left: newLeft,
top: newTop,
});
img.setCoords();

this.canvas.renderAll();

// 刷新动态马赛克(基于新的原图位置重新计算)
this.refreshDynamicMosaics({ render: true });
}

/**
* 递归调整 clipPath 的位置和缩放,使其跟随原图变换
*/
_transformClipPath(clipPath, oldLeft, oldTop, newLeft, newTop, ratioX, ratioY) {
if (!clipPath) return;
const relX = (clipPath.left || 0) - oldLeft;
const relY = (clipPath.top || 0) - oldTop;
clipPath.set({
left: newLeft + relX * ratioX,
top: newTop + relY * ratioY,
scaleX: (clipPath.scaleX == null ? 1 : clipPath.scaleX) * ratioX,
scaleY: (clipPath.scaleY == null ? 1 : clipPath.scaleY) * ratioY,
});
clipPath.setCoords();
if (clipPath.clipPath) {
this._transformClipPath(clipPath.clipPath, oldLeft, oldTop, newLeft, newTop, ratioX, ratioY);
}
}

_bindEvents() {
if (!this.canvas) return;

Expand Down
7 changes: 6 additions & 1 deletion plugins/Image-Toolbox/core/src/LayerManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,16 @@ class LayerManager {
}

/**
* 根据 Fabric 对象获取图层元数据
* 根据 Fabric 对象获取图层元数据(公开接口)
*
* 仅在已同步的 _layers 列表中查找。对于尚未被 syncLayers() 收录的对象
* (如工具激活期间新增的临时对象),返回 null,由调用方决定回退策略。
*
* @param {fabric.Object} obj
* @returns {object|null}
*/
getLayerByObject(obj) {
if (!obj) return null;
return this._findMeta(obj);
}

Expand Down
23 changes: 23 additions & 0 deletions plugins/Image-Toolbox/core/src/app/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import AccountPage, {
EDITOR_BARS_LAYOUTS,
EDITOR_SIDE_PANEL_POSITION_KEY,
EDITOR_SIDE_PANEL_POSITIONS,
TOOLBAR_COLLAPSED_KEY,
TOOLBAR_COLLAPSED,
} from '../ui/AccountPage.js';
import { initTheme } from '../utils/theme.js';

Expand Down Expand Up @@ -64,6 +66,7 @@ class App {
initTheme();
this._applyEditorBarsLayout(this._getEditorBarsLayout());
this._applyEditorSidePanelPosition(this._getEditorSidePanelPosition());
this._applyToolbarCollapsed(this._getToolbarCollapsed());

// 1. 初始化画布管理器
this.canvasManager = new CanvasManager('fabric-canvas');
Expand Down Expand Up @@ -279,6 +282,10 @@ class App {
this._applyEditorSidePanelPosition(position);
});

eventBus.on('toolbar:collapsedChanged', (value) => {
this._applyToolbarCollapsed(value);
});

// ═══ 快捷键 ═══
document.addEventListener('keydown', (e) => {
// Ctrl+Z 撤销
Expand Down Expand Up @@ -528,6 +535,22 @@ class App {
normalized === EDITOR_SIDE_PANEL_POSITIONS.LEFT
);
}

_getToolbarCollapsed() {
const saved = localStorage.getItem(TOOLBAR_COLLAPSED_KEY);
return Object.values(TOOLBAR_COLLAPSED).includes(saved) ? saved : TOOLBAR_COLLAPSED.COLLAPSED;
}

_applyToolbarCollapsed(value) {
const normalized = Object.values(TOOLBAR_COLLAPSED).includes(value)
? value
: TOOLBAR_COLLAPSED.COLLAPSED;

document.getElementById('app')?.classList.toggle(
'app--toolbar-expanded',
normalized === TOOLBAR_COLLAPSED.EXPANDED
);
}
}

export default App;
Loading
Loading