2040 lines
54 KiB
Vue
2040 lines
54 KiB
Vue
<template>
|
||
<transition name="fade">
|
||
<div v-if="visible" class="liquify-panel" :class="{active:!closePanel}">
|
||
<div class="btn" @click="setClosePanel"><i class="fi fi-br-angle-left"></i></div>
|
||
<div class="liquify-panel-header">
|
||
<div class="header-title">{{ $t('liquifyPanel.LiquefactionTool') }}</div>
|
||
<!-- <div class="header-actions">
|
||
<button class="header-btn cancel-btn" @click="cancel">取消</button>
|
||
<button class="header-btn confirm-btn" @click="confirm">完成</button>
|
||
</div> -->
|
||
</div>
|
||
|
||
<div class="liquify-panel-content">
|
||
<!-- 液化模式选择器 -->
|
||
<div class="liquify-modes">
|
||
<div
|
||
v-for="mode in availableModes"
|
||
:key="mode.id"
|
||
class="mode-item"
|
||
:class="{ active: currentMode === mode.id }"
|
||
@click="selectMode(mode.id)"
|
||
>
|
||
<div class="mode-icon" :class="`mode-${mode.id}`">
|
||
<!-- <component :is="mode.icon" v-if="mode.icon" />
|
||
<span v-else>{{ mode.iconText }}</span> -->
|
||
</div>
|
||
<div class="mode-name">{{ mode.name }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分割线 -->
|
||
<div class="liquify-divider"></div>
|
||
|
||
<!-- 参数调整区域 -->
|
||
<div class="liquify-params">
|
||
<div class="param-item">
|
||
<div class="param-label">{{ $t('liquifyPanel.size') }}</div>
|
||
<div class="param-control">
|
||
<input
|
||
type="range"
|
||
v-model.number="size"
|
||
min="5"
|
||
max="200"
|
||
@input="updateParam('size', size)"
|
||
class="slider-control"
|
||
/>
|
||
<div class="param-value">{{ size }}%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="param-item">
|
||
<div class="param-label">{{ $t('liquifyPanel.pressure') }}</div>
|
||
<div class="param-control">
|
||
<input
|
||
type="range"
|
||
v-model.number="pressure"
|
||
min="0"
|
||
max="100"
|
||
@input="updateParam('pressure', pressure / 100)"
|
||
class="slider-control"
|
||
/>
|
||
<div class="param-value">{{ pressure }}%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="param-item" v-if="showDistortion">
|
||
<div class="param-label">{{ $t('liquifyPanel.distortion') }}</div>
|
||
<div class="param-control">
|
||
<input
|
||
type="range"
|
||
v-model.number="distortion"
|
||
min="0"
|
||
max="100"
|
||
@input="updateParam('distortion', distortion / 100)"
|
||
class="slider-control"
|
||
/>
|
||
<div class="param-value">{{ distortion }}%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="param-item">
|
||
<div class="param-label">{{ $t('liquifyPanel.power') }}</div>
|
||
<div class="param-control">
|
||
<input
|
||
type="range"
|
||
v-model.number="power"
|
||
min="0"
|
||
max="100"
|
||
@input="updateParam('power', power / 100)"
|
||
class="slider-control"
|
||
/>
|
||
<div class="param-value">{{ power }}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 调试信息 (开发模式) -->
|
||
<div v-if="debug" class="debug-info">
|
||
<div class="debug-header">调试信息</div>
|
||
<div class="debug-item">
|
||
渲染模式: <span>{{ renderMode }}</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
WebGL可用: <span>{{ webglAvailable ? "是" : "否" }}</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
操作次数: <span>{{ operationCount }}</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
平均耗时: <span>{{ avgOperationTime.toFixed(2) }}ms</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
推拉算法: <span>{{ liquifyAlgorithmType }}</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
拖拽状态: <span>{{ isDragging ? "拖拽中" : "未拖拽" }}</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
拖拽距离: <span>{{ dragDistance.toFixed(1) }}px</span>
|
||
</div>
|
||
<div class="debug-item">
|
||
当前液化图: <span><img :src="currImage" class="currImage" /> </span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||
import {
|
||
CompositeLiquifyCommand,
|
||
createLiquifyResetCommand,
|
||
createLiquifyDeformCommand,
|
||
createLiquifyStateCommand,
|
||
serializeFabricObject,
|
||
} from "../commands/LiquifyCommands";
|
||
import { OperationType } from "../utils/layerHelper";
|
||
import { LiquifyRealTimeUpdater } from "../managers/liquify/LiquifyRealTimeUpdater";
|
||
import { LiquifyStateManager } from "../managers/liquify/LiquifyStateManager";
|
||
import { Modal } from "ant-design-vue";
|
||
import { useI18n } from "vue-i18n";
|
||
const {t} = useI18n()
|
||
|
||
|
||
// Props定义
|
||
const props = defineProps({
|
||
canvas: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
commandManager: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
liquifyManager: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
layerManager: {
|
||
type: Object,
|
||
required: true,
|
||
},
|
||
// 添加当前工具的prop
|
||
activeTool: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
});
|
||
|
||
// 响应式数据
|
||
const visible = ref(false);
|
||
const debug = ref(false); // 设为true可以显示调试信息
|
||
const currImage = ref("");
|
||
|
||
// 当前状态
|
||
const isEditing = ref(false);
|
||
const isDrawing = ref(false); // 是否正在绘制
|
||
const targetObject = ref(null);
|
||
const targetLayerId = ref(null);
|
||
const targetObjectId = ref(null); // 新增:目标对象的唯一ID
|
||
const originalImageData = ref(null);
|
||
|
||
// 渲染模式信息
|
||
const renderMode = ref("未知");
|
||
const webglAvailable = ref(false);
|
||
const operationCount = ref(0);
|
||
const avgOperationTime = ref(0);
|
||
|
||
// 新推拉算法相关状态
|
||
const liquifyAlgorithmType = ref("增强版推拉算法");
|
||
const isDragging = ref(false);
|
||
const dragDistance = ref(0);
|
||
|
||
// 鼠标位置追踪
|
||
const lastX = ref(0);
|
||
const lastY = ref(0);
|
||
|
||
// 持续按压相关状态
|
||
const isPressing = ref(false); // 是否正在按压
|
||
const pressStartTime = ref(0); // 按压开始时间
|
||
const pressDuration = ref(0); // 按压持续时间
|
||
const pressTimer = ref(null); // 持续按压定时器
|
||
const pressInterval = ref(50); // 持续执行间隔(毫秒)
|
||
const pressX = ref(0); // 按压点X坐标
|
||
const pressY = ref(0); // 按压点Y坐标
|
||
|
||
// 当前模式
|
||
const currentMode = ref("push"); // 默认为推动模式
|
||
|
||
// 参数设置
|
||
const size = ref(100); // 工具大小 (5-200)
|
||
const pressure = ref(50); // 压力大小 (0-100)
|
||
const distortion = ref(0); // 失真程度 (0-100)
|
||
const power = ref(50); // 动力/强度 (0-100)
|
||
|
||
// 批量操作缓存
|
||
const compositeCommand = ref(null);
|
||
|
||
// 液化操作的初始状态记录
|
||
const initialObjectState = ref(null);
|
||
const initialImageData = ref(null); // 新增:初始图像数据
|
||
const currentLiquifyCommand = ref(null);
|
||
|
||
// 实时更新器
|
||
const realtimeUpdater = ref(null);
|
||
|
||
// 状态管理器
|
||
const stateManager = ref(null);
|
||
|
||
// 可用液化模式
|
||
const availableModes = ref([
|
||
{ id: "push", name: t('liquifyPanel.push'), iconText: "↔" },
|
||
{ id: "clockwise", name: t('liquifyPanel.clockwise'), iconText: "↻" },
|
||
{ id: "counterclockwise", name: t('liquifyPanel.counterclockwise'), iconText: "↺" },
|
||
// { id: "pinch", name: "捏合", iconText: "⤢" },
|
||
// { id: "expand", name: "展开", iconText: "⤡" },
|
||
// { id: "crystal", name: "水晶", iconText: "✧" },
|
||
// { id: "edge", name: "边缘", iconText: "◈" },
|
||
// { id: "reconstruct", name: "重建", iconText: "↩" },
|
||
]);
|
||
|
||
// 事件监听器引用
|
||
let _handleMouseDown = null;
|
||
let _handleMouseMove = null;
|
||
let _handleMouseUp = null;
|
||
|
||
// 计算属性
|
||
const showDistortion = computed(() => {
|
||
// 只在特定模式下显示失真控制
|
||
return ["crystal", "edge"].includes(currentMode.value);
|
||
});
|
||
|
||
//打开隐藏操作面板
|
||
const closePanel = ref(false)
|
||
const setClosePanel = ()=>{
|
||
closePanel.value = !closePanel.value
|
||
}
|
||
|
||
// 监听当前工具变化 - 参考 SelectionPanel 的实现方式
|
||
watch(
|
||
() => props.activeTool,
|
||
(newTool, oldTool) => {
|
||
console.log("LiquifyPanel.vue 工具切换:", oldTool, "->", newTool);
|
||
// 当工具切换到液化工具时显示面板
|
||
if (newTool === OperationType.LIQUIFY) {
|
||
console.log("切换到液化工具,准备显示面板");
|
||
// 如果面板未显示且有合适的目标对象,则显示面板
|
||
if (!visible.value) {
|
||
visible.value = true;
|
||
closePanel.value = true
|
||
// 检查是否有可液化的对象
|
||
checkAndShowPanel();
|
||
}
|
||
} else {
|
||
visible.value = false; // 切换到其他工具时隐藏面板
|
||
// 切换到其他工具,隐藏液化面板
|
||
if (visible.value) {
|
||
console.log("切换到其他工具,隐藏液化面板");
|
||
close();
|
||
}
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
/**
|
||
* 检查并显示面板
|
||
*/
|
||
function checkAndShowPanel() {
|
||
// 检查当前是否有可液化的图层或对象
|
||
if (props.canvas && props.activeTool === OperationType.LIQUIFY) {
|
||
const activeLayer = props.layerManager.getActiveLayer();
|
||
const activeObject = props.canvas
|
||
.getObjects()
|
||
.find((fItem) => fItem.layerId === activeLayer.id);
|
||
if (activeObject) {
|
||
// 有合适的对象,准备液化环境
|
||
isEditing.value = true;
|
||
// 准备液化环境
|
||
prepareForLiquify(activeObject);
|
||
} else {
|
||
// 没有合适的对象
|
||
targetObject.value = null;
|
||
console.log("未选择有效的图像对象");
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 准备液化环境
|
||
*/
|
||
async function prepareForLiquify(targetObj) {
|
||
if (!props.liquifyManager || !targetObj) return;
|
||
|
||
targetObject.value = targetObj;
|
||
|
||
// 设置实时更新器的目标对象
|
||
if (realtimeUpdater.value) {
|
||
realtimeUpdater.value.setTargetObject(targetObj);
|
||
}
|
||
|
||
// 获取图层信息
|
||
const layerId =
|
||
targetObj.layerId ||
|
||
canvasManager?.layerManager?.activeLayerId?.value ||
|
||
canvasManager.activeLayerId?.value ||
|
||
props.canvas.activeLayerId?.value;
|
||
targetLayerId.value = layerId;
|
||
|
||
try {
|
||
// 初始化液化管理器
|
||
await props.liquifyManager.initialize({
|
||
canvas: props.canvas,
|
||
layerManager: props.layerManager,
|
||
});
|
||
|
||
// 准备液化环境
|
||
const result = await props.liquifyManager.prepareForLiquify(targetObj);
|
||
|
||
if (result && result.originalImageData) {
|
||
originalImageData.value = result.originalImageData;
|
||
|
||
// 创建合成命令
|
||
compositeCommand.value = new CompositeLiquifyCommand({
|
||
canvas: props.canvas,
|
||
targetObject: targetObject.value,
|
||
targetLayerId: targetLayerId.value,
|
||
originalData: originalImageData.value,
|
||
liquifyManager: props.liquifyManager,
|
||
layerManager: props.layerManager,
|
||
});
|
||
|
||
// 获取渲染模式信息
|
||
renderMode.value = result.renderMode || "未知";
|
||
const status =
|
||
props.liquifyManager.getStatus?.() ||
|
||
props.liquifyManager.enhancedManager?.getStatus?.() ||
|
||
{};
|
||
webglAvailable.value = status.isWebGLAvailable || false;
|
||
|
||
// 设置当前模式和参数
|
||
if (props.liquifyManager.setMode) {
|
||
props.liquifyManager.setMode(currentMode.value);
|
||
}
|
||
updateAllParams();
|
||
|
||
console.log("液化环境准备完成");
|
||
}
|
||
} catch (error) {
|
||
console.error("准备液化环境失败:", error);
|
||
}
|
||
}
|
||
|
||
// 生命周期 - mounted
|
||
onMounted(() => {
|
||
// 生命周期 - created的逻辑
|
||
// 监听显示事件
|
||
document.addEventListener("showLiquifyPanel", showPanel);
|
||
|
||
// 创建实时更新器
|
||
if (props.canvas) {
|
||
realtimeUpdater.value = new LiquifyRealTimeUpdater(props.canvas, {
|
||
throttleTime: 16, // 60fps
|
||
useDirectUpdate: true,
|
||
imageQuality: 1.0, // 最高质量
|
||
skipRenderDuringDrag: false, // 初始不跳过渲染
|
||
currImage,
|
||
});
|
||
|
||
// 创建状态管理器
|
||
stateManager.value = new LiquifyStateManager(
|
||
props.canvas,
|
||
realtimeUpdater.value
|
||
);
|
||
}
|
||
|
||
// 监听画布事件
|
||
setupCanvasListeners();
|
||
});
|
||
|
||
// 生命周期 - beforeUnmount
|
||
onUnmounted(() => {
|
||
document.removeEventListener("showLiquifyPanel", showPanel);
|
||
removeCanvasListeners();
|
||
|
||
// 清理状态管理器
|
||
if (stateManager.value) {
|
||
stateManager.value.dispose();
|
||
stateManager.value = null;
|
||
}
|
||
|
||
// 清理实时更新器
|
||
if (realtimeUpdater.value) {
|
||
realtimeUpdater.value.dispose();
|
||
realtimeUpdater.value = null;
|
||
}
|
||
});
|
||
|
||
// 方法定义
|
||
/**
|
||
* 显示液化面板
|
||
*/
|
||
function showPanel(event) {
|
||
console.log("接收到showLiquifyPanel事件", event.detail);
|
||
console.log(props);
|
||
// 检查当前工具是否是液化工具 - 只在必要时进行检查
|
||
if (props.activeTool !== OperationType.LIQUIFY) {
|
||
console.log(
|
||
"当前工具不是液化工具,不显示面板。当前工具:",
|
||
props.activeTool,
|
||
"期望工具:",
|
||
OperationType.LIQUIFY
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 获取从事件传入的详情数据
|
||
const detail = event.detail || {};
|
||
|
||
// 检查是否包含必要的液化信息
|
||
if (!detail.canLiquify && !detail.targetObject) {
|
||
if (detail.layerStatus && detail.layerStatus.message) {
|
||
console.log("液化操作提示:", detail.layerStatus.message);
|
||
Modal.error({
|
||
title: "错误提示",
|
||
content: detail.layerStatus.message,
|
||
okText: "确定",
|
||
centered: true,
|
||
});
|
||
} else {
|
||
Modal.error({
|
||
title: "错误提示",
|
||
content: "未选择有效图像或图层不适合液化操作",
|
||
okText: "确定",
|
||
centered: true,
|
||
});
|
||
console.log("未选择有效图像或图层不适合液化操作");
|
||
}
|
||
visible.value = true; // 仍然显示面板以便用户看到提示
|
||
closePanel.value = true
|
||
return;
|
||
}
|
||
|
||
// 从事件中获取目标对象和图层信息
|
||
const targetObj = detail.targetObject;
|
||
const targetLayerIdValue = detail.activeLayerId || detail.targetLayerId;
|
||
const originalImageDataValue = detail.originalImageData;
|
||
|
||
// 确保有可用的目标对象
|
||
if (!targetObj) {
|
||
console.log("未选择有效的图像对象");
|
||
visible.value = true; // 仍然显示面板以便显示提示
|
||
closePanel.value = true
|
||
return;
|
||
}
|
||
|
||
console.log(
|
||
"显示液化面板,目标对象:",
|
||
targetObj.type,
|
||
"图层ID:",
|
||
targetLayerIdValue
|
||
);
|
||
|
||
// 更新面板状态,但保持当前模式不变
|
||
// 只有在首次显示面板或面板已关闭时才重置模式
|
||
if (!visible.value) {
|
||
currentMode.value = "push"; // 默认模式
|
||
resetParams();
|
||
}
|
||
|
||
// 使用新的设置目标对象方法
|
||
setTargetObject(targetObj);
|
||
targetLayerId.value = targetLayerIdValue;
|
||
|
||
// 处理originalImageData
|
||
// 如果提供了新的图像数据,使用新的;否则保留现有的(如果有)
|
||
if (originalImageDataValue) {
|
||
originalImageData.value = originalImageDataValue;
|
||
console.log(
|
||
"收到原始图像数据,尺寸:",
|
||
originalImageDataValue.width,
|
||
"x",
|
||
originalImageDataValue.height
|
||
);
|
||
}
|
||
|
||
// 创建或更新合成命令
|
||
if (!compositeCommand.value || !isEditing.value) {
|
||
compositeCommand.value = new CompositeLiquifyCommand({
|
||
canvas: props.canvas,
|
||
targetObject: getCurrentTargetObject(),
|
||
targetLayerId: targetLayerId.value,
|
||
originalData: originalImageData.value,
|
||
layerManager: props.layerManager,
|
||
liquifyManager: props.liquifyManager,
|
||
});
|
||
}
|
||
|
||
visible.value = true;
|
||
// closePanel.value = true
|
||
isEditing.value = true;
|
||
|
||
// 初始化液化管理器并准备液化环境
|
||
if (props.liquifyManager) {
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (currentTarget) {
|
||
// 确保液化管理器正确初始化
|
||
props.liquifyManager.initialize({
|
||
canvas: props.canvas,
|
||
layerManager: props.layerManager,
|
||
});
|
||
|
||
// 如果没有原始图像数据,则需要先准备液化环境
|
||
if (!originalImageData.value) {
|
||
// 准备液化环境并获取原始图像数据
|
||
props.liquifyManager
|
||
.prepareForLiquify(currentTarget)
|
||
.then((result) => {
|
||
if (result && result.originalImageData) {
|
||
originalImageData.value = result.originalImageData;
|
||
console.log(
|
||
"准备液化环境,获取原始图像数据成功:",
|
||
originalImageData.value.width,
|
||
"x",
|
||
originalImageData.value.height
|
||
);
|
||
|
||
// 获取渲染模式信息
|
||
renderMode.value = result.renderMode || "未知";
|
||
const status = props.liquifyManager.getStatus
|
||
? props.liquifyManager.getStatus()
|
||
: props.liquifyManager.enhancedManager
|
||
? props.liquifyManager.enhancedManager.getStatus()
|
||
: {};
|
||
|
||
webglAvailable.value = status.isWebGLAvailable || false;
|
||
|
||
// 更新合成命令的原始数据
|
||
if (compositeCommand.value) {
|
||
compositeCommand.value.originalData = originalImageData.value;
|
||
}
|
||
|
||
// 立即设置当前模式
|
||
if (currentMode.value && props.liquifyManager.setMode) {
|
||
props.liquifyManager.setMode(currentMode.value);
|
||
}
|
||
|
||
// 设置当前参数
|
||
updateAllParams();
|
||
}
|
||
})
|
||
.catch((err) => {
|
||
console.error("准备液化环境失败:", err);
|
||
});
|
||
} else {
|
||
// 已有原始图像数据,直接准备环境
|
||
props.liquifyManager.prepareForLiquify(currentTarget).then((result) => {
|
||
if (result) {
|
||
// 获取渲染模式信息
|
||
renderMode.value = result.renderMode || "未知";
|
||
|
||
const status = props.liquifyManager.getStatus
|
||
? props.liquifyManager.getStatus()
|
||
: props.liquifyManager.enhancedManager
|
||
? props.liquifyManager.enhancedManager.getStatus()
|
||
: {};
|
||
|
||
webglAvailable.value = status.isWebGLAvailable || false;
|
||
|
||
// 立即设置当前模式
|
||
if (currentMode.value && props.liquifyManager.setMode) {
|
||
props.liquifyManager.setMode(currentMode.value);
|
||
}
|
||
|
||
// 设置当前参数
|
||
updateAllParams();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据ID从画布中获取最新的目标对象
|
||
* @returns {Object|null} 目标对象或null
|
||
*/
|
||
function getCurrentTargetObject() {
|
||
if (!targetObjectId.value || !props.canvas) {
|
||
return null;
|
||
}
|
||
|
||
// 从画布中查找具有指定ID的对象
|
||
const objects = props.canvas.getObjects();
|
||
const foundObject = objects.find(
|
||
(obj) =>
|
||
obj.id === targetObjectId.value ||
|
||
obj.objectId === targetObjectId.value ||
|
||
obj.uid === targetObjectId.value
|
||
);
|
||
|
||
if (foundObject) {
|
||
// 更新缓存的引用
|
||
targetObject.value = foundObject;
|
||
return foundObject;
|
||
}
|
||
|
||
console.warn("未找到目标对象,ID:", targetObjectId.value);
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 设置目标对象并记录其ID
|
||
* @param {Object} obj 目标对象
|
||
*/
|
||
function setTargetObject(obj) {
|
||
if (!obj) {
|
||
targetObject.value = null;
|
||
targetObjectId.value = null;
|
||
return;
|
||
}
|
||
|
||
// 确保对象有唯一ID
|
||
if (!obj.id && !obj.objectId && !obj.uid) {
|
||
obj.id =
|
||
"liquify_target_" +
|
||
Date.now() +
|
||
"_" +
|
||
Math.random().toString(36).substr(2, 9);
|
||
}
|
||
|
||
targetObject.value = obj;
|
||
targetObjectId.value = obj.id || obj.objectId || obj.uid;
|
||
|
||
console.log("设置目标对象,ID:", targetObjectId.value);
|
||
}
|
||
|
||
// 更新所有参数到液化管理器
|
||
function updateAllParams() {
|
||
if (!props.liquifyManager) {
|
||
console.warn("❌ 液化管理器未初始化,无法更新参数");
|
||
return;
|
||
}
|
||
|
||
console.log("🔧 更新所有液化参数");
|
||
|
||
// 批量设置所有参数
|
||
const params = {
|
||
size: size.value,
|
||
pressure: pressure.value / 100,
|
||
distortion: distortion.value / 100,
|
||
power: power.value / 100,
|
||
};
|
||
|
||
console.log("📋 当前参数值:", params);
|
||
|
||
// 使用批量设置方法
|
||
if (typeof props.liquifyManager.setParams === "function") {
|
||
props.liquifyManager.setParams(params);
|
||
} else {
|
||
// 逐个设置参数(兼容性处理)
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
updateParam(key, value);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新单个参数
|
||
*/
|
||
function updateParam(paramName, value) {
|
||
if (!props.liquifyManager) {
|
||
console.warn("❌ 液化管理器未初始化,无法更新参数:", paramName);
|
||
return;
|
||
}
|
||
|
||
console.log(`🔧 更新液化参数 ${paramName}:`, value);
|
||
|
||
// 使用批量设置方法(如果可用)
|
||
if (typeof props.liquifyManager.setParams === "function") {
|
||
const params = {
|
||
size: size.value,
|
||
pressure: pressure.value / 100,
|
||
distortion: distortion.value / 100,
|
||
power: power.value / 100,
|
||
};
|
||
props.liquifyManager.setParams(params);
|
||
} else {
|
||
// 兼容性:单个参数设置
|
||
if (typeof props.liquifyManager.setParam === "function") {
|
||
props.liquifyManager.setParam(paramName, value);
|
||
} else if (
|
||
typeof props.liquifyManager[
|
||
`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`
|
||
] === "function"
|
||
) {
|
||
props.liquifyManager[
|
||
`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`
|
||
](value);
|
||
} else {
|
||
console.warn(`❌ 液化管理器不支持设置参数: ${paramName}`);
|
||
}
|
||
}
|
||
|
||
// 如果正在编辑且有目标对象,立即应用参数变化
|
||
if (isEditing.value && getCurrentTargetObject()) {
|
||
console.log(`✅ 参数 ${paramName} 已更新并应用到液化管理器`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置画布事件监听
|
||
*/
|
||
function setupCanvasListeners() {
|
||
if (!props.canvas) return;
|
||
|
||
_handleMouseDown = handleMouseDown;
|
||
_handleMouseMove = handleMouseMove;
|
||
_handleMouseUp = handleMouseUp;
|
||
|
||
// 鼠标事件
|
||
props.canvas.on("mouse:down", _handleMouseDown);
|
||
props.canvas.on("mouse:move", _handleMouseMove);
|
||
props.canvas.on("mouse:up", _handleMouseUp);
|
||
|
||
// 触摸事件支持(iPad兼容性)
|
||
props.canvas.on("touch:start", _handleMouseDown);
|
||
props.canvas.on("touch:move", _handleMouseMove);
|
||
props.canvas.on("touch:end", _handleMouseUp);
|
||
}
|
||
|
||
/**
|
||
* 移除画布事件监听
|
||
*/
|
||
function removeCanvasListeners() {
|
||
if (!props.canvas) return;
|
||
|
||
// 移除鼠标事件
|
||
props.canvas.off("mouse:down", _handleMouseDown);
|
||
props.canvas.off("mouse:move", _handleMouseMove);
|
||
props.canvas.off("mouse:up", _handleMouseUp);
|
||
|
||
// 移除触摸事件
|
||
props.canvas.off("touch:start", _handleMouseDown);
|
||
props.canvas.off("touch:move", _handleMouseMove);
|
||
props.canvas.off("touch:end", _handleMouseUp);
|
||
|
||
_handleMouseDown = null;
|
||
_handleMouseMove = null;
|
||
_handleMouseUp = null;
|
||
}
|
||
|
||
/**
|
||
* 获取当前图像的实际状态数据
|
||
* @param {Object} targetObject Fabric图像对象
|
||
* @returns {Promise<ImageData|null>} 当前图像数据或null
|
||
*/
|
||
async function getCurrentImageData(targetObject) {
|
||
if (!targetObject || !targetObject._element) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
// 创建临时canvas来获取当前图像状态
|
||
const tempCanvas = document.createElement("canvas");
|
||
const element = targetObject._element;
|
||
|
||
// 设置canvas尺寸为原始图像尺寸
|
||
if (originalImageData.value) {
|
||
tempCanvas.width = originalImageData.value.width;
|
||
tempCanvas.height = originalImageData.value.height;
|
||
} else {
|
||
tempCanvas.width = element.naturalWidth || element.width;
|
||
tempCanvas.height = element.naturalHeight || element.height;
|
||
}
|
||
const tempCtx = tempCanvas.getContext("2d");
|
||
|
||
// 绘制当前图像到临时canvas
|
||
tempCtx.drawImage(element, 0, 0, tempCanvas.width, tempCanvas.height);
|
||
|
||
// 获取ImageData
|
||
const imageData = tempCtx.getImageData(
|
||
0,
|
||
0,
|
||
tempCanvas.width,
|
||
tempCanvas.height
|
||
);
|
||
|
||
console.log(
|
||
"✅ 成功获取当前图像状态,尺寸:",
|
||
imageData.width,
|
||
"x",
|
||
imageData.height
|
||
);
|
||
return imageData;
|
||
} catch (error) {
|
||
console.warn("获取当前图像数据失败:", error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 鼠标按下事件处理
|
||
*/
|
||
async function handleMouseDown(event) {
|
||
if (!isEditing.value || !visible.value || !props.liquifyManager) return;
|
||
|
||
isDrawing.value = true;
|
||
isDragging.value = true; // 更新拖拽状态
|
||
|
||
// 使用状态管理器开始操作
|
||
if (stateManager.value) {
|
||
stateManager.value.startOperation();
|
||
stateManager.value.startDrag();
|
||
}
|
||
|
||
const pointer = props.canvas.getPointer(event.e);
|
||
|
||
// 记录起始点
|
||
lastX.value = pointer.x;
|
||
lastY.value = pointer.y;
|
||
|
||
// === 修复:记录当前图像状态(而不是原始状态)===
|
||
try {
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (currentTarget) {
|
||
console.log("🎯 记录液化操作当前状态,对象ID:", targetObjectId.value);
|
||
|
||
// 获取当前图像的实际状态(而不是原始状态)
|
||
const currentImageData = await getCurrentImageData(currentTarget);
|
||
if (currentImageData) {
|
||
// 记录当前图像数据(深拷贝)
|
||
initialImageData.value = new ImageData(
|
||
new Uint8ClampedArray(currentImageData.data),
|
||
currentImageData.width,
|
||
currentImageData.height
|
||
);
|
||
|
||
console.log(
|
||
"✅ 当前图像状态已记录,尺寸:",
|
||
initialImageData.value.width,
|
||
"x",
|
||
initialImageData.value.height
|
||
);
|
||
} else {
|
||
// 如果无法获取当前状态,使用原始状态作为备用
|
||
if (originalImageData.value) {
|
||
const originalData = originalImageData.value;
|
||
initialImageData.value = new ImageData(
|
||
new Uint8ClampedArray(originalData.data),
|
||
originalData.width,
|
||
originalData.height
|
||
);
|
||
console.log("⚠️ 使用原始状态作为备用初始状态");
|
||
}
|
||
}
|
||
|
||
// 备用:也保存序列化状态
|
||
initialObjectState.value = serializeFabricObject(currentTarget);
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ 记录初始状态失败:", error);
|
||
initialObjectState.value = null;
|
||
initialImageData.value = null;
|
||
}
|
||
|
||
// 开始液化操作 - 使用新的推拉算法
|
||
if (props.liquifyManager.startLiquifyOperation) {
|
||
// 将Fabric画布坐标转换为图像内部坐标
|
||
const imageCoords = _convertFabricCoordsToImageCoords(pointer.x, pointer.y);
|
||
if (imageCoords) {
|
||
props.liquifyManager.startLiquifyOperation(imageCoords.x, imageCoords.y);
|
||
console.log(
|
||
`开始液化操作,图像坐标: (${imageCoords.x}, ${imageCoords.y})`
|
||
);
|
||
|
||
// 应用液化效果
|
||
applyLiquifyAtPoint(pointer.x, pointer.y);
|
||
|
||
// 开始持续按压
|
||
isPressing.value = true;
|
||
pressStartTime.value = Date.now();
|
||
pressX.value = pointer.x;
|
||
pressY.value = pointer.y;
|
||
startPressTimer();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 鼠标移动事件处理
|
||
*/
|
||
async function handleMouseMove(event) {
|
||
if (!isEditing.value || !visible.value || !isDrawing.value) return;
|
||
|
||
const pointer = props.canvas.getPointer(event.e);
|
||
|
||
// 更新拖拽距离
|
||
if (isDragging.value) {
|
||
const dx = pointer.x - lastX.value;
|
||
const dy = pointer.y - lastY.value;
|
||
dragDistance.value = Math.sqrt(dx * dx + dy * dy);
|
||
}
|
||
|
||
// 应用液化效果
|
||
applyLiquifyAtPoint(pointer.x, pointer.y);
|
||
|
||
// 更新坐标
|
||
lastX.value = pointer.x;
|
||
lastY.value = pointer.y;
|
||
|
||
// 更新按压点
|
||
if (isPressing.value) {
|
||
pressX.value = pointer.x;
|
||
pressY.value = pointer.y;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 鼠标抬起事件处理
|
||
*/
|
||
async function handleMouseUp() {
|
||
if (!isEditing.value || !visible.value) return;
|
||
|
||
// 设置绘制状态为false
|
||
const wasDrawing = isDrawing.value;
|
||
isDrawing.value = false;
|
||
isDragging.value = false; // 重置拖拽状态
|
||
dragDistance.value = 0; // 重置拖拽距离
|
||
|
||
// 结束液化操作 - 使用新的推拉算法
|
||
if (wasDrawing && props.liquifyManager.endLiquifyOperation) {
|
||
props.liquifyManager.endLiquifyOperation();
|
||
console.log("结束液化操作");
|
||
}
|
||
|
||
// 使用状态管理器结束操作
|
||
if (stateManager.value) {
|
||
try {
|
||
await stateManager.value.endDrag();
|
||
stateManager.value.endOperation();
|
||
} catch (error) {
|
||
console.error("结束拖拽操作时出错:", error);
|
||
}
|
||
}
|
||
|
||
// 如果刚才在绘制,同步目标对象引用
|
||
if (wasDrawing && realtimeUpdater.value) {
|
||
console.log("拖拽结束,高质量渲染已恢复");
|
||
|
||
// 同步目标对象引用
|
||
const updatedObject = realtimeUpdater.value.getTargetObject();
|
||
if (updatedObject && updatedObject !== targetObject.value) {
|
||
targetObject.value = updatedObject;
|
||
}
|
||
}
|
||
|
||
// === 修复:创建和执行液化状态命令 ===
|
||
if (wasDrawing && initialImageData.value) {
|
||
try {
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (currentTarget) {
|
||
console.log("🎯 创建液化状态命令,记录最终状态");
|
||
|
||
// 获取最终的图像数据
|
||
let finalImageData = null;
|
||
|
||
// 尝试从实时更新器获取当前图像数据
|
||
if (realtimeUpdater.value) {
|
||
try {
|
||
const currentImageElement =
|
||
realtimeUpdater.value.getTargetObject()?._element;
|
||
if (currentImageElement) {
|
||
// 从当前图像元素创建ImageData
|
||
const tempCanvas = document.createElement("canvas");
|
||
tempCanvas.width = initialImageData.value.width;
|
||
tempCanvas.height = initialImageData.value.height;
|
||
const tempCtx = tempCanvas.getContext("2d");
|
||
tempCtx.drawImage(
|
||
currentImageElement,
|
||
0,
|
||
0,
|
||
tempCanvas.width,
|
||
tempCanvas.height
|
||
);
|
||
finalImageData = tempCtx.getImageData(
|
||
0,
|
||
0,
|
||
tempCanvas.width,
|
||
tempCanvas.height
|
||
);
|
||
|
||
console.log(
|
||
"✅ 从实时更新器获取最终图像数据成功,尺寸:",
|
||
finalImageData.width,
|
||
"x",
|
||
finalImageData.height
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.warn("从实时更新器获取图像数据失败:", error);
|
||
}
|
||
}
|
||
|
||
// 如果无法从实时更新器获取,则从当前目标对象获取
|
||
if (!finalImageData && currentTarget._element) {
|
||
try {
|
||
const tempCanvas = document.createElement("canvas");
|
||
tempCanvas.width = initialImageData.value.width;
|
||
tempCanvas.height = initialImageData.value.height;
|
||
const tempCtx = tempCanvas.getContext("2d");
|
||
tempCtx.drawImage(
|
||
currentTarget._element,
|
||
0,
|
||
0,
|
||
tempCanvas.width,
|
||
tempCanvas.height
|
||
);
|
||
finalImageData = tempCtx.getImageData(
|
||
0,
|
||
0,
|
||
tempCanvas.width,
|
||
tempCanvas.height
|
||
);
|
||
|
||
console.log(
|
||
"✅ 从目标对象获取最终图像数据成功,尺寸:",
|
||
finalImageData.width,
|
||
"x",
|
||
finalImageData.height
|
||
);
|
||
} catch (error) {
|
||
console.warn("从目标对象获取图像数据失败:", error);
|
||
}
|
||
}
|
||
|
||
// 如果成功获取到最终图像数据,创建液化状态命令
|
||
if (finalImageData) {
|
||
currentLiquifyCommand.value = createLiquifyStateCommand({
|
||
canvas: props.canvas,
|
||
layerManager: props.layerManager,
|
||
liquifyManager: props.liquifyManager,
|
||
targetObject: currentTarget,
|
||
targetLayerId: targetLayerId.value,
|
||
targetObjectId: targetObjectId.value,
|
||
initialImageData: initialImageData.value,
|
||
finalImageData: finalImageData,
|
||
realtimeUpdater: realtimeUpdater.value,
|
||
name: `液化操作 - ${currentMode.value}`,
|
||
description: `应用${currentMode.value}模式的液化变形操作`,
|
||
});
|
||
|
||
// 执行命令(将其添加到命令历史中)
|
||
if (props.commandManager && currentLiquifyCommand.value) {
|
||
await props.commandManager.execute(currentLiquifyCommand.value);
|
||
console.log("✅ 液化状态命令已执行并添加到命令历史");
|
||
}
|
||
} else {
|
||
console.warn("⚠️ 无法获取最终图像数据,跳过创建状态命令");
|
||
}
|
||
|
||
// 清理初始状态记录
|
||
initialImageData.value = null;
|
||
initialObjectState.value = null;
|
||
} else {
|
||
console.warn("⚠️ 未找到当前目标对象,无法创建状态命令");
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ 创建液化状态命令失败:", error);
|
||
// 清理状态
|
||
initialImageData.value = null;
|
||
initialObjectState.value = null;
|
||
currentLiquifyCommand.value = null;
|
||
}
|
||
}
|
||
|
||
// 停止按压
|
||
isPressing.value = false;
|
||
stopPressTimer();
|
||
|
||
console.log("鼠标抬起,结束液化变形操作");
|
||
}
|
||
|
||
/**
|
||
* 在特定点应用液化效果
|
||
*/
|
||
async function applyLiquifyAtPoint(x, y) {
|
||
// 使用getCurrentTargetObject获取最新的目标对象
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (!props.liquifyManager || !currentTarget) {
|
||
console.error("无法应用液化效果: 缺少liquifyManager或targetObject");
|
||
return;
|
||
}
|
||
|
||
if (!originalImageData.value) {
|
||
console.error("无法应用液化效果: 缺少原始图像数据");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 将Fabric画布坐标转换为图像内部坐标
|
||
const imageCoords = _convertFabricCoordsToImageCoords(x, y);
|
||
|
||
if (!imageCoords) {
|
||
console.warn("坐标转换失败,点击位置不在目标图像范围内");
|
||
return;
|
||
}
|
||
|
||
console.log(
|
||
"原始坐标:",
|
||
x,
|
||
y,
|
||
"转换后图像坐标:",
|
||
imageCoords.x,
|
||
imageCoords.y
|
||
);
|
||
|
||
// 获取当前参数
|
||
const params = {
|
||
size: size.value,
|
||
pressure: pressure.value / 100,
|
||
distortion: distortion.value / 100,
|
||
power: power.value / 100,
|
||
};
|
||
|
||
// 创建液化变形命令 - 使用最新的目标对象
|
||
const deformCommand = createLiquifyDeformCommand({
|
||
canvas: props.canvas,
|
||
layerManager: props.layerManager,
|
||
liquifyManager: props.liquifyManager,
|
||
mode: currentMode.value,
|
||
params: params,
|
||
targetObject: currentTarget,
|
||
targetLayerId: targetLayerId.value,
|
||
x: imageCoords.x, // 使用转换后的图像坐标
|
||
y: imageCoords.y,
|
||
originalData: originalImageData.value,
|
||
});
|
||
|
||
// 添加到合成命令
|
||
if (compositeCommand.value) {
|
||
compositeCommand.value.addCommand(deformCommand);
|
||
}
|
||
|
||
// 开始计时
|
||
const startTime = performance.now();
|
||
|
||
console.log(
|
||
"应用液化变形,模式:",
|
||
currentMode.value,
|
||
"在图像坐标:",
|
||
imageCoords.x,
|
||
imageCoords.y
|
||
);
|
||
|
||
// 应用液化效果(实际执行变形)- 使用最新的目标对象
|
||
const resultData = await props.liquifyManager.applyLiquify(
|
||
currentTarget,
|
||
currentMode.value,
|
||
params,
|
||
imageCoords.x, // 使用转换后的图像坐标
|
||
imageCoords.y
|
||
);
|
||
// 关键修复:使用实时更新器处理变形结果
|
||
if (resultData && realtimeUpdater.value) {
|
||
// 设置目标对象(如果还未设置)- 使用最新的目标对象
|
||
if (realtimeUpdater.value.getTargetObject() !== currentTarget) {
|
||
realtimeUpdater.value.setTargetObject(currentTarget);
|
||
}
|
||
|
||
// 使用实时更新器更新图像
|
||
await realtimeUpdater.value.updateImage(resultData, isDrawing.value);
|
||
currImage.value = realtimeUpdater.value.getImageData(resultData);
|
||
// 如果实时更新器更新了对象,需要同步targetObject引用
|
||
const updatedObject = realtimeUpdater.value.getTargetObject();
|
||
if (updatedObject && updatedObject !== currentTarget) {
|
||
// 更新缓存的引用
|
||
targetObject.value = updatedObject;
|
||
// 如果对象有新的ID,也要更新ID
|
||
if (updatedObject.id || updatedObject.objectId || updatedObject.uid) {
|
||
targetObjectId.value =
|
||
updatedObject.id || updatedObject.objectId || updatedObject.uid;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算操作耗时
|
||
const endTime = performance.now();
|
||
operationCount.value++;
|
||
const operationTime = endTime - startTime;
|
||
|
||
// 更新平均耗时 (指数移动平均)
|
||
if (avgOperationTime.value === 0) {
|
||
avgOperationTime.value = operationTime;
|
||
} else {
|
||
avgOperationTime.value =
|
||
0.7 * avgOperationTime.value + 0.3 * operationTime;
|
||
}
|
||
|
||
// 将性能指标传递给状态管理器
|
||
if (stateManager.value) {
|
||
stateManager.value.recordOperationMetrics({
|
||
operationTime,
|
||
operationType: "liquify",
|
||
mode: currentMode.value,
|
||
coordinates: { x: imageCoords.x, y: imageCoords.y },
|
||
imageSize: {
|
||
width: originalImageData.value?.width || 0,
|
||
height: originalImageData.value?.height || 0,
|
||
},
|
||
renderMode: renderMode.value,
|
||
isRealTime: isDrawing.value,
|
||
});
|
||
}
|
||
|
||
// 检查渲染模式
|
||
if (props.liquifyManager.getStatus) {
|
||
const status = props.liquifyManager.getStatus();
|
||
renderMode.value = status.renderMode || "未知";
|
||
webglAvailable.value = status.isWebGLAvailable || false;
|
||
} else if (
|
||
props.liquifyManager.enhancedManager &&
|
||
props.liquifyManager.enhancedManager.getStatus
|
||
) {
|
||
const status = props.liquifyManager.enhancedManager.getStatus();
|
||
renderMode.value = status.renderMode || "未知";
|
||
webglAvailable.value = status.isWebGLAvailable || false;
|
||
}
|
||
} catch (error) {
|
||
console.error("应用液化效果失败:", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 高效地更新画布上的图像
|
||
* 避免频繁创建新的fabric对象,而是直接更新现有对象的图像数据
|
||
* @private
|
||
*/
|
||
async function _updateImageOnCanvas(imageData) {
|
||
if (!imageData) {
|
||
return;
|
||
}
|
||
|
||
// 获取最新的目标对象
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (!currentTarget) {
|
||
console.warn("无法更新画布图像:未找到目标对象");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建临时canvas来渲染图像数据
|
||
const tempCanvas = document.createElement("canvas");
|
||
tempCanvas.width = imageData.width;
|
||
tempCanvas.height = imageData.height;
|
||
const tempCtx = tempCanvas.getContext("2d");
|
||
tempCtx.putImageData(imageData, 0, 0);
|
||
|
||
// 获取数据URL
|
||
const dataURL = tempCanvas.toDataURL("image/png");
|
||
|
||
// 更新当前显示的图像
|
||
currImage.value = dataURL;
|
||
|
||
// 为了性能考虑,只在拖拽过程中简单更新元素的src
|
||
// 而不创建新的fabric对象
|
||
if (isDrawing.value) {
|
||
// 拖拽过程中使用快速更新
|
||
if (currentTarget._element) {
|
||
currentTarget._element.src = dataURL;
|
||
// 标记对象需要重新渲染
|
||
currentTarget.dirty = true;
|
||
props.canvas.renderAll();
|
||
}
|
||
} else {
|
||
// 拖拽结束后进行完整的对象替换
|
||
// await _replaceTargetObjectWithNewImage(dataURL);
|
||
}
|
||
} catch (error) {
|
||
console.error("更新画布图像时出错:", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 完整替换目标对象为新的图像对象
|
||
* 在拖拽结束后调用,确保对象状态的完整性
|
||
* @private
|
||
*/
|
||
async function _replaceTargetWithNewImage(dataURL) {
|
||
// 获取最新的目标对象
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (!currentTarget) {
|
||
console.warn("无法替换对象:未找到目标对象");
|
||
return;
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
fabric.Image.fromURL(dataURL, (img) => {
|
||
// 保留原对象的ID信息
|
||
const originalId = targetObjectId.value;
|
||
|
||
// 保留原对象的所有变换属性和状态
|
||
img.set({
|
||
left: currentTarget.left,
|
||
top: currentTarget.top,
|
||
scaleX: currentTarget.scaleX,
|
||
scaleY: currentTarget.scaleY,
|
||
angle: currentTarget.angle,
|
||
flipX: currentTarget.flipX,
|
||
flipY: currentTarget.flipY,
|
||
opacity: currentTarget.opacity,
|
||
originX: currentTarget.originX,
|
||
originY: currentTarget.originY,
|
||
id: originalId, // 使用记录的ID
|
||
name: currentTarget.name,
|
||
layerId: targetLayerId.value,
|
||
selected: false,
|
||
evented: false,
|
||
});
|
||
|
||
// 替换Canvas上的对象
|
||
const allObjects = props.canvas?.getObjects?.();
|
||
const index = allObjects.indexOf(currentTarget);
|
||
if (index !== -1) {
|
||
props.canvas.remove(currentTarget);
|
||
props.canvas.insertAt(img, index);
|
||
|
||
// 更新缓存的引用 - 关键修复
|
||
targetObject.value = img;
|
||
|
||
// 更新图层引用
|
||
const layer = props.layerManager.getLayerById(targetLayerId.value);
|
||
if (layer) {
|
||
if (layer.type === "background" || layer.isBackground) {
|
||
layer.fabricObject = img;
|
||
} else if (layer.fabricObjects) {
|
||
const objIndex = layer.fabricObjects.findIndex(
|
||
(obj) => obj.id === img.id
|
||
);
|
||
if (objIndex !== -1) {
|
||
layer.fabricObjects[objIndex] = img;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("成功替换目标对象,新对象ID:", img.id);
|
||
}
|
||
|
||
props.canvas.renderAll();
|
||
resolve(img);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 将Fabric画布坐标转换为图像内部坐标
|
||
* @param {number} fabricX Fabric画布X坐标
|
||
* @param {number} fabricY Fabric画布Y坐标
|
||
* @returns {Object|null} 图像内部坐标 {x, y} 或 null(如果不在图像范围内)
|
||
* @private
|
||
*/
|
||
function _convertFabricCoordsToImageCoords(fabricX, fabricY) {
|
||
if (!targetObject.value || !originalImageData.value) {
|
||
return null;
|
||
}
|
||
|
||
const obj = targetObject.value;
|
||
|
||
// 创建变换矩阵(考虑对象的位置、缩放、旋转等)
|
||
const transform = obj.calcTransformMatrix();
|
||
|
||
// 将变换矩阵转换为fabric矩阵格式
|
||
const matrix = fabric.util.invertTransform(transform);
|
||
|
||
// 应用逆变换,将画布坐标转换为对象本地坐标
|
||
const localPoint = fabric.util.transformPoint(
|
||
new fabric.Point(fabricX, fabricY),
|
||
matrix
|
||
);
|
||
|
||
// 获取图像的原始尺寸(未缩放前)
|
||
const imageWidth = originalImageData.value.width;
|
||
const imageHeight = originalImageData.value.height;
|
||
|
||
// 获取对象的原始尺寸(注意:这里需要考虑对象可能被缩放过)
|
||
const objWidth = obj.width;
|
||
const objHeight = obj.height;
|
||
|
||
// 修复关键问题:直接使用对象坐标系到图像坐标系的映射
|
||
// fabric对象的坐标原点在中心,需要转换到左上角原点
|
||
// 同时考虑对象本身可能有内在的缩放
|
||
let imageX = (localPoint.x + objWidth / 2) * (imageWidth / objWidth);
|
||
let imageY = (localPoint.y + objHeight / 2) * (imageHeight / objHeight);
|
||
|
||
// 处理图像翻转的情况
|
||
if (obj.flipX) {
|
||
imageX = imageWidth - imageX;
|
||
}
|
||
if (obj.flipY) {
|
||
imageY = imageHeight - imageY;
|
||
}
|
||
|
||
console.log(
|
||
`坐标转换详细信息:
|
||
Fabric坐标: (${fabricX}, ${fabricY})
|
||
本地坐标: (${localPoint.x.toFixed(2)}, ${localPoint.y.toFixed(2)})
|
||
对象尺寸: ${objWidth}x${objHeight}
|
||
图像尺寸: ${imageWidth}x${imageHeight}
|
||
对象缩放: (${obj.scaleX.toFixed(3)}, ${obj.scaleY.toFixed(3)})
|
||
最终图像坐标: (${imageX.toFixed(2)}, ${imageY.toFixed(2)})
|
||
翻转状态: flipX=${obj.flipX}, flipY=${obj.flipY}`
|
||
);
|
||
|
||
// 检查坐标是否在图像范围内
|
||
if (
|
||
imageX < 0 ||
|
||
imageX >= imageWidth ||
|
||
imageY < 0 ||
|
||
imageY >= imageHeight
|
||
) {
|
||
console.warn(
|
||
`坐标超出图像范围: (${imageX}, ${imageY}), 图像尺寸: ${imageWidth}x${imageHeight}`
|
||
);
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
x: Math.round(imageX),
|
||
y: Math.round(imageY),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 选择液化模式
|
||
*/
|
||
function selectMode(modeId) {
|
||
console.log("选择液化模式:", modeId);
|
||
currentMode.value = modeId;
|
||
|
||
// 为水晶和边缘模式设置默认失真值
|
||
if (modeId === "crystal" || modeId === "edge") {
|
||
if (distortion.value === 0) {
|
||
distortion.value = 30; // 设置默认失真值为30%
|
||
console.log("为", modeId, "模式设置默认失真值:", distortion.value);
|
||
}
|
||
}
|
||
|
||
if (props.liquifyManager) {
|
||
props.liquifyManager.setMode(modeId);
|
||
console.log("液化管理器模式已设置为:", modeId);
|
||
|
||
// 立即更新所有参数,确保失真参数正确传递
|
||
updateAllParams();
|
||
}
|
||
|
||
// 如果是重建模式,重置为原始状态
|
||
if (modeId === "reconstruct") {
|
||
resetToOriginal();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重置参数到默认值
|
||
*/
|
||
function resetParams() {
|
||
size.value = 100;
|
||
pressure.value = 50;
|
||
distortion.value = 0;
|
||
power.value = 50;
|
||
console.log("重置液化参数到默认值");
|
||
}
|
||
|
||
/**
|
||
* 重置为原始状态
|
||
*/
|
||
function resetToOriginal() {
|
||
// 获取最新的目标对象
|
||
const currentTarget = getCurrentTargetObject();
|
||
if (!props.liquifyManager || !originalImageData.value || !currentTarget) {
|
||
console.warn("无法重置:缺少必要的组件或数据");
|
||
return;
|
||
}
|
||
|
||
// 创建重置命令 - 使用最新的目标对象
|
||
const resetCommand = createLiquifyResetCommand({
|
||
canvas: props.canvas,
|
||
layerManager: props.layerManager,
|
||
targetObject: currentTarget,
|
||
targetLayerId: targetLayerId.value,
|
||
originalImageData: originalImageData.value,
|
||
});
|
||
|
||
// 执行重置
|
||
props.commandManager.execute(resetCommand);
|
||
|
||
// 重新初始化液化环境 - 使用最新的目标对象
|
||
props.liquifyManager.prepareForLiquify(currentTarget);
|
||
|
||
// 创建新的合成命令 - 使用最新的目标对象
|
||
compositeCommand.value = new CompositeLiquifyCommand({
|
||
canvas: props.canvas,
|
||
targetObject: currentTarget,
|
||
targetLayerId: targetLayerId.value,
|
||
originalData: originalImageData.value,
|
||
layerManager: props.layerManager,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 取消液化操作
|
||
*/
|
||
function cancel() {
|
||
// 还原为原始状态
|
||
resetToOriginal();
|
||
|
||
// 关闭面板
|
||
close();
|
||
}
|
||
|
||
/**
|
||
* 确认液化操作
|
||
*/
|
||
async function confirm() {
|
||
try {
|
||
// 如果有合成命令,执行它
|
||
if (
|
||
compositeCommand.value &&
|
||
compositeCommand.value.commands &&
|
||
compositeCommand.value.commands.length > 0
|
||
) {
|
||
// 提交命令到命令管理器
|
||
await props.commandManager.execute(compositeCommand.value);
|
||
}
|
||
|
||
// 关闭面板
|
||
close();
|
||
} catch (error) {
|
||
console.error("确认液化操作失败:", error);
|
||
alert("保存液化效果失败: " + error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭面板
|
||
*/
|
||
function close() {
|
||
visible.value = false;
|
||
isEditing.value = false;
|
||
targetObject.value = null;
|
||
targetLayerId.value = null;
|
||
targetObjectId.value = null; // 清空对象ID
|
||
originalImageData.value = null; // 清空原始图像数据
|
||
compositeCommand.value = null;
|
||
|
||
// 清理液化状态相关数据
|
||
initialImageData.value = null; // 清空初始图像数据
|
||
initialObjectState.value = null; // 清空初始对象状态
|
||
currentLiquifyCommand.value = null; // 清空当前命令
|
||
|
||
// 重置性能统计
|
||
operationCount.value = 0;
|
||
avgOperationTime.value = 0;
|
||
|
||
// 释放液化管理器资源
|
||
if (props.liquifyManager) {
|
||
props.liquifyManager.dispose();
|
||
}
|
||
|
||
console.log("液化面板已关闭,所有引用已清理");
|
||
}
|
||
|
||
/**
|
||
* 开始持续按压定时器
|
||
*/
|
||
function startPressTimer() {
|
||
if (pressTimer.value) return;
|
||
|
||
pressTimer.value = setInterval(() => {
|
||
// 计算按压持续时间
|
||
pressDuration.value = Date.now() - pressStartTime.value;
|
||
|
||
// 每隔一段时间执行一次液化操作
|
||
applyLiquifyAtPoint(pressX.value, pressY.value);
|
||
}, pressInterval.value);
|
||
}
|
||
|
||
/**
|
||
* 停止持续按压定时器
|
||
*/
|
||
function stopPressTimer() {
|
||
if (pressTimer.value) {
|
||
clearInterval(pressTimer.value);
|
||
pressTimer.value = null;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.liquify-panel {
|
||
position: absolute;
|
||
bottom: 22px;
|
||
left: 20px;
|
||
right: 20px;
|
||
max-width: min(90vw, 640px);
|
||
margin: 0 auto;
|
||
background-color: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(15px);
|
||
-webkit-backdrop-filter: blur(15px);
|
||
border-radius: 8px;
|
||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||
z-index: 1000;
|
||
color: #333;
|
||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||
padding-bottom: 12px;
|
||
&.active{
|
||
transform: translateY(100%);
|
||
> .btn{
|
||
> i{
|
||
transform: rotate(90deg);
|
||
}
|
||
}
|
||
}
|
||
> .btn{
|
||
width: 100%;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
|
||
> i{
|
||
font-size: 1.4rem;
|
||
display: block;
|
||
transform: rotate(270deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s, transform 0.3s;
|
||
}
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
}
|
||
|
||
.currImage {
|
||
position: fixed;
|
||
right: 12px;
|
||
bottom: 0;
|
||
width: 32px;
|
||
height: auto;
|
||
min-height: 32px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
margin-left: 4px;
|
||
background-color: red;
|
||
display: block;
|
||
}
|
||
|
||
/* 平板和手机适配 */
|
||
@media screen and (max-width: 768px) {
|
||
.liquify-panel {
|
||
bottom: 15px;
|
||
left: 15px;
|
||
right: 15px;
|
||
max-width: calc(100vw - 30px);
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 480px) {
|
||
.liquify-panel {
|
||
bottom: 10px;
|
||
left: 10px;
|
||
right: 10px;
|
||
max-width: calc(100vw - 20px);
|
||
}
|
||
}
|
||
|
||
/* .liquify-panel.is-active {
|
||
transform: translateY(0);
|
||
} */
|
||
|
||
.liquify-panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 6px 10px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||
background-color: rgba(255, 255, 255, 0.8);
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.header-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #333;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
padding: 3px 6px;
|
||
border-radius: 3px;
|
||
transition: background-color 0.2s ease;
|
||
min-width: 32px;
|
||
}
|
||
|
||
.header-btn:hover {
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.cancel-btn {
|
||
color: #666;
|
||
}
|
||
|
||
.confirm-btn {
|
||
color: #4285f4;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.liquify-panel-content {
|
||
padding: 8px 10px 10px;
|
||
}
|
||
|
||
.liquify-modes {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(42px, 1fr));
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* 平板适配:最多6列 */
|
||
@media screen and (max-width: 768px) {
|
||
.liquify-modes {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 3px;
|
||
}
|
||
|
||
.liquify-panel-header {
|
||
padding: 5px 8px;
|
||
border-radius: 6px 6px 0 0;
|
||
}
|
||
|
||
.liquify-panel-content {
|
||
padding: 6px 8px 8px;
|
||
}
|
||
}
|
||
|
||
/* 手机适配:最多4列 */
|
||
@media screen and (max-width: 480px) {
|
||
.liquify-modes {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 2px;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.header-btn {
|
||
font-size: 11px;
|
||
padding: 2px 4px;
|
||
min-width: 28px;
|
||
}
|
||
}
|
||
|
||
.mode-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 4px 2px;
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
min-height: 48px;
|
||
}
|
||
|
||
.mode-item:hover {
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.mode-item.active {
|
||
background-color: rgba(66, 133, 244, 0.1);
|
||
}
|
||
|
||
.mode-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
margin-bottom: 2px;
|
||
color: #555;
|
||
}
|
||
|
||
.mode-item.active .mode-icon {
|
||
color: #4285f4;
|
||
}
|
||
|
||
.mode-name {
|
||
font-size: 14px;
|
||
color: #333;
|
||
text-align: center;
|
||
line-height: 1.1;
|
||
word-wrap: break-word;
|
||
max-width: 100%;
|
||
}
|
||
|
||
/* 平板适配 */
|
||
@media screen and (max-width: 768px) {
|
||
.mode-item {
|
||
padding: 3px 1px;
|
||
min-height: 44px;
|
||
}
|
||
|
||
.mode-icon {
|
||
width: 22px;
|
||
height: 22px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.mode-name {
|
||
font-size: 8px;
|
||
}
|
||
}
|
||
|
||
/* 手机适配 */
|
||
@media screen and (max-width: 480px) {
|
||
.mode-item {
|
||
padding: 2px 1px;
|
||
min-height: 40px;
|
||
}
|
||
|
||
.mode-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.mode-name {
|
||
font-size: 7px;
|
||
}
|
||
}
|
||
|
||
.liquify-divider {
|
||
height: 1px;
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
margin: 6px 0 8px;
|
||
}
|
||
|
||
.liquify-params {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 12px;
|
||
align-items: start;
|
||
}
|
||
|
||
.param-item {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.param-label {
|
||
font-size: 12px;
|
||
margin-top: 12px;
|
||
margin-bottom: 12px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.param-control {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.slider-control {
|
||
flex: 1;
|
||
height: 3px;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
border-radius: 2px;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
min-width: 60px;
|
||
}
|
||
|
||
.slider-control::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
background: #4285f4;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.param-value {
|
||
font-size: 10px;
|
||
width: 32px;
|
||
text-align: right;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 平板适配 */
|
||
@media screen and (max-width: 768px) {
|
||
.liquify-params {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.liquify-divider {
|
||
margin: 5px 0 6px;
|
||
}
|
||
|
||
.param-label {
|
||
font-size: 10px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.param-control {
|
||
gap: 6px;
|
||
}
|
||
|
||
.param-value {
|
||
font-size: 9px;
|
||
width: 28px;
|
||
}
|
||
|
||
.slider-control {
|
||
height: 2px;
|
||
min-width: 50px;
|
||
}
|
||
|
||
.slider-control::-webkit-slider-thumb {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
}
|
||
|
||
/* 手机适配 */
|
||
@media screen and (max-width: 480px) {
|
||
.liquify-params {
|
||
grid-template-columns: 1fr;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
/* 不同模式的图标样式 */
|
||
.mode-push::before {
|
||
content: "↔";
|
||
}
|
||
|
||
.mode-clockwise::before {
|
||
content: "↻";
|
||
}
|
||
|
||
.mode-counterclockwise::before {
|
||
content: "↺";
|
||
}
|
||
|
||
.mode-pinch::before {
|
||
content: "⤢";
|
||
}
|
||
|
||
.mode-expand::before {
|
||
content: "⤡";
|
||
}
|
||
|
||
.mode-crystal::before {
|
||
content: "✧";
|
||
}
|
||
|
||
.mode-edge::before {
|
||
content: "◈";
|
||
}
|
||
|
||
.mode-reconstruct::before {
|
||
content: "↩";
|
||
}
|
||
|
||
/* 调试信息区域 */
|
||
.debug-info {
|
||
margin-top: 12px;
|
||
padding: 8px;
|
||
background-color: rgba(0, 0, 0, 0.03);
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
color: #666;
|
||
}
|
||
|
||
.debug-header {
|
||
font-weight: 500;
|
||
margin-bottom: 4px;
|
||
font-size: 10px;
|
||
color: #444;
|
||
}
|
||
|
||
.debug-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.debug-item span {
|
||
color: #4285f4;
|
||
font-weight: 500;
|
||
}
|
||
</style>
|