Files
aida_front/src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue

1923 lines
50 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<transition name="fade">
<div v-if="visible" class="liquify-panel">
<div class="liquify-panel-header">
<div class="header-title">液化工具</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">尺寸</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">压力</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">失真</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">动力</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";
// 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: "推", iconText: "↔" },
{ id: "clockwise", name: "顺时针转动", iconText: "↻" },
{ id: "counterclockwise", name: "逆时针转动", 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);
});
// 监听当前工具变化 - 参考 SelectionPanel 的实现方式
watch(
() => props.activeTool,
(newTool, oldTool) => {
console.log("LiquifyPanel.vue 工具切换:", oldTool, "->", newTool);
// 当工具切换到液化工具时显示面板
if (newTool === OperationType.LIQUIFY) {
console.log("切换到液化工具,准备显示面板");
// 如果面板未显示且有合适的目标对象,则显示面板
if (!visible.value) {
visible.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 || 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,
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);
} else {
console.log("未选择有效图像或图层不适合液化操作");
}
visible.value = true; // 仍然显示面板以便用户看到提示
return;
}
// 从事件中获取目标对象和图层信息
const targetObj = detail.targetObject;
const targetLayerIdValue = detail.activeLayerId || detail.targetLayerId;
const originalImageDataValue = detail.originalImageData;
// 确保有可用的目标对象
if (!targetObj) {
console.log("未选择有效的图像对象");
visible.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,
});
}
visible.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;
}
/**
* 鼠标按下事件处理
*/
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 && originalImageData.value) {
console.log("🎯 记录液化操作初始状态对象ID:", targetObjectId.value);
// 记录初始图像数据(深拷贝)
const originalData = originalImageData.value;
initialImageData.value = new ImageData(
new Uint8ClampedArray(originalData.data),
originalData.width,
originalData.height
);
// 备用:也保存序列化状态
initialObjectState.value = serializeFabricObject(currentTarget);
console.log(
"✅ 初始图像数据已记录,尺寸:",
initialImageData.value.width,
"x",
initialImageData.value.height
);
}
} 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,
targetObject: currentTarget,
targetLayerId: targetLayerId.value,
targetObjectId: targetObjectId.value,
initialImageData: initialImageData.value,
finalImageData: finalImageData,
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;
}
.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(6, 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(4, 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>