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

1923 lines
50 KiB
Vue
Raw Normal View History

2025-06-09 10:25:54 +08:00
<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>
2025-06-18 11:05:23 +08:00
<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>
2025-06-09 10:25:54 +08:00
<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,
2025-06-18 11:05:23 +08:00
createLiquifyStateCommand,
serializeFabricObject,
2025-06-09 10:25:54 +08:00
} from "../commands/LiquifyCommands";
import { OperationType } from "../utils/layerHelper";
2025-06-18 11:05:23 +08:00
import { LiquifyRealTimeUpdater } from "../managers/liquify/LiquifyRealTimeUpdater";
import { LiquifyStateManager } from "../managers/liquify/LiquifyStateManager";
2025-06-09 10:25:54 +08:00
// 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可以显示调试信息
2025-06-09 10:25:54 +08:00
const currImage = ref("");
// 当前状态
const isEditing = ref(false);
const isDrawing = ref(false); // 是否正在绘制
const targetObject = ref(null);
const targetLayerId = ref(null);
2025-06-18 11:05:23 +08:00
const targetObjectId = ref(null); // 新增目标对象的唯一ID
2025-06-09 10:25:54 +08:00
const originalImageData = ref(null);
// 渲染模式信息
const renderMode = ref("未知");
const webglAvailable = ref(false);
const operationCount = ref(0);
const avgOperationTime = ref(0);
2025-06-18 11:05:23 +08:00
// 新推拉算法相关状态
const liquifyAlgorithmType = ref("增强版推拉算法");
const isDragging = ref(false);
const dragDistance = ref(0);
2025-06-09 10:25:54 +08:00
// 鼠标位置追踪
const lastX = ref(0);
const lastY = ref(0);
2025-06-18 11:05:23 +08:00
// 持续按压相关状态
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坐标
2025-06-09 10:25:54 +08:00
// 当前模式
const currentMode = ref("push"); // 默认为推动模式
// 参数设置
2025-06-18 11:05:23 +08:00
const size = ref(100); // 工具大小 (5-200)
2025-06-09 10:25:54 +08:00
const pressure = ref(50); // 压力大小 (0-100)
const distortion = ref(0); // 失真程度 (0-100)
const power = ref(50); // 动力/强度 (0-100)
// 批量操作缓存
const compositeCommand = ref(null);
2025-06-18 11:05:23 +08:00
// 液化操作的初始状态记录
const initialObjectState = ref(null);
const initialImageData = ref(null); // 新增:初始图像数据
const currentLiquifyCommand = ref(null);
// 实时更新器
const realtimeUpdater = ref(null);
// 状态管理器
const stateManager = ref(null);
2025-06-09 10:25:54 +08:00
// 可用液化模式
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: "↩" },
2025-06-09 10:25:54 +08:00
]);
// 事件监听器引用
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;
2025-06-18 11:05:23 +08:00
// 设置实时更新器的目标对象
if (realtimeUpdater.value) {
realtimeUpdater.value.setTargetObject(targetObj);
}
2025-06-09 10:25:54 +08:00
// 获取图层信息
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);
2025-06-18 11:05:23 +08:00
// 创建实时更新器
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
);
}
2025-06-09 10:25:54 +08:00
// 监听画布事件
setupCanvasListeners();
});
// 生命周期 - beforeUnmount
onUnmounted(() => {
document.removeEventListener("showLiquifyPanel", showPanel);
removeCanvasListeners();
2025-06-18 11:05:23 +08:00
// 清理状态管理器
if (stateManager.value) {
stateManager.value.dispose();
stateManager.value = null;
}
// 清理实时更新器
if (realtimeUpdater.value) {
realtimeUpdater.value.dispose();
realtimeUpdater.value = null;
}
2025-06-09 10:25:54 +08:00
});
// 方法定义
/**
* 显示液化面板
*/
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();
}
2025-06-18 11:05:23 +08:00
// 使用新的设置目标对象方法
setTargetObject(targetObj);
2025-06-09 10:25:54 +08:00
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,
2025-06-18 11:05:23 +08:00
targetObject: getCurrentTargetObject(),
2025-06-09 10:25:54 +08:00
targetLayerId: targetLayerId.value,
originalData: originalImageData.value,
layerManager: props.layerManager,
});
}
visible.value = true;
isEditing.value = true;
// 初始化液化管理器并准备液化环境
2025-06-18 11:05:23 +08:00
if (props.liquifyManager) {
const currentTarget = getCurrentTargetObject();
if (currentTarget) {
// 确保液化管理器正确初始化
props.liquifyManager.initialize({
canvas: props.canvas,
layerManager: props.layerManager,
});
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 如果没有原始图像数据,则需要先准备液化环境
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();
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
})
.catch((err) => {
console.error("准备液化环境失败:", err);
});
} else {
// 已有原始图像数据,直接准备环境
props.liquifyManager.prepareForLiquify(currentTarget).then((result) => {
2025-06-09 10:25:54 +08:00
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();
}
});
2025-06-18 11:05:23 +08:00
}
2025-06-09 10:25:54 +08:00
}
}
}
2025-06-18 11:05:23 +08:00
/**
* 根据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);
}
2025-06-09 10:25:54 +08:00
// 更新所有参数到液化管理器
function updateAllParams() {
2025-06-18 11:05:23 +08:00
if (!props.liquifyManager) {
console.warn("❌ 液化管理器未初始化,无法更新参数");
return;
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
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} 已更新并应用到液化管理器`);
}
2025-06-09 10:25:54 +08:00
}
/**
* 设置画布事件监听
*/
function setupCanvasListeners() {
if (!props.canvas) return;
_handleMouseDown = handleMouseDown;
_handleMouseMove = handleMouseMove;
_handleMouseUp = handleMouseUp;
2025-06-18 11:05:23 +08:00
// 鼠标事件
2025-06-09 10:25:54 +08:00
props.canvas.on("mouse:down", _handleMouseDown);
props.canvas.on("mouse:move", _handleMouseMove);
props.canvas.on("mouse:up", _handleMouseUp);
2025-06-18 11:05:23 +08:00
// 触摸事件支持iPad兼容性
props.canvas.on("touch:start", _handleMouseDown);
props.canvas.on("touch:move", _handleMouseMove);
props.canvas.on("touch:end", _handleMouseUp);
2025-06-09 10:25:54 +08:00
}
/**
* 移除画布事件监听
*/
function removeCanvasListeners() {
if (!props.canvas) return;
2025-06-18 11:05:23 +08:00
// 移除鼠标事件
2025-06-09 10:25:54 +08:00
props.canvas.off("mouse:down", _handleMouseDown);
props.canvas.off("mouse:move", _handleMouseMove);
props.canvas.off("mouse:up", _handleMouseUp);
2025-06-18 11:05:23 +08:00
// 移除触摸事件
props.canvas.off("touch:start", _handleMouseDown);
props.canvas.off("touch:move", _handleMouseMove);
props.canvas.off("touch:end", _handleMouseUp);
2025-06-09 10:25:54 +08:00
_handleMouseDown = null;
_handleMouseMove = null;
_handleMouseUp = null;
}
/**
* 鼠标按下事件处理
*/
function handleMouseDown(event) {
if (!isEditing.value || !visible.value || !props.liquifyManager) return;
isDrawing.value = true;
2025-06-18 11:05:23 +08:00
isDragging.value = true; // 更新拖拽状态
// 使用状态管理器开始操作
if (stateManager.value) {
stateManager.value.startOperation();
stateManager.value.startDrag();
}
2025-06-09 10:25:54 +08:00
const pointer = props.canvas.getPointer(event.e);
// 记录起始点
lastX.value = pointer.x;
lastY.value = pointer.y;
2025-06-18 11:05:23 +08:00
// === 修复:记录初始图像数据 ===
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})`
);
}
}
2025-06-09 10:25:54 +08:00
// 应用液化效果
applyLiquifyAtPoint(pointer.x, pointer.y);
2025-06-18 11:05:23 +08:00
// 开始持续按压
isPressing.value = true;
pressStartTime.value = Date.now();
pressX.value = pointer.x;
pressY.value = pointer.y;
startPressTimer();
2025-06-09 10:25:54 +08:00
}
/**
* 鼠标移动事件处理
*/
async function handleMouseMove(event) {
if (!isEditing.value || !visible.value || !isDrawing.value) return;
const pointer = props.canvas.getPointer(event.e);
2025-06-18 11:05:23 +08:00
// 更新拖拽距离
if (isDragging.value) {
const dx = pointer.x - lastX.value;
const dy = pointer.y - lastY.value;
dragDistance.value = Math.sqrt(dx * dx + dy * dy);
}
2025-06-09 10:25:54 +08:00
// 应用液化效果
applyLiquifyAtPoint(pointer.x, pointer.y);
// 更新坐标
lastX.value = pointer.x;
lastY.value = pointer.y;
2025-06-18 11:05:23 +08:00
// 更新按压点
if (isPressing.value) {
pressX.value = pointer.x;
pressY.value = pointer.y;
}
2025-06-09 10:25:54 +08:00
}
/**
* 鼠标抬起事件处理
*/
2025-06-18 11:05:23 +08:00
async function handleMouseUp() {
2025-06-09 10:25:54 +08:00
if (!isEditing.value || !visible.value) return;
2025-06-18 11:05:23 +08:00
// 设置绘制状态为false
const wasDrawing = isDrawing.value;
2025-06-09 10:25:54 +08:00
isDrawing.value = false;
2025-06-18 11:05:23 +08:00
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();
2025-06-09 10:25:54 +08:00
console.log("鼠标抬起,结束液化变形操作");
}
/**
* 在特定点应用液化效果
*/
async function applyLiquifyAtPoint(x, y) {
2025-06-18 11:05:23 +08:00
// 使用getCurrentTargetObject获取最新的目标对象
const currentTarget = getCurrentTargetObject();
if (!props.liquifyManager || !currentTarget) {
2025-06-09 10:25:54 +08:00
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,
};
2025-06-18 11:05:23 +08:00
// 创建液化变形命令 - 使用最新的目标对象
2025-06-09 10:25:54 +08:00
const deformCommand = createLiquifyDeformCommand({
canvas: props.canvas,
layerManager: props.layerManager,
liquifyManager: props.liquifyManager,
mode: currentMode.value,
params: params,
2025-06-18 11:05:23 +08:00
targetObject: currentTarget,
2025-06-09 10:25:54 +08:00
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
);
2025-06-18 11:05:23 +08:00
// 应用液化效果(实际执行变形)- 使用最新的目标对象
2025-06-09 10:25:54 +08:00
const resultData = await props.liquifyManager.applyLiquify(
2025-06-18 11:05:23 +08:00
currentTarget,
2025-06-09 10:25:54 +08:00
currentMode.value,
params,
imageCoords.x, // 使用转换后的图像坐标
imageCoords.y
);
2025-06-18 11:05:23 +08:00
// 关键修复:使用实时更新器处理变形结果
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;
}
}
2025-06-09 10:25:54 +08:00
}
// 计算操作耗时
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;
}
2025-06-18 11:05:23 +08:00
// 将性能指标传递给状态管理器
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,
});
}
2025-06-09 10:25:54 +08:00
// 检查渲染模式
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);
}
}
/**
2025-06-18 11:05:23 +08:00
* 高效地更新画布上的图像
* 避免频繁创建新的fabric对象而是直接更新现有对象的图像数据
2025-06-09 10:25:54 +08:00
* @private
*/
2025-06-18 11:05:23 +08:00
async function _updateImageOnCanvas(imageData) {
if (!imageData) {
return;
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 获取最新的目标对象
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);
}
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
/**
* 完整替换目标对象为新的图像对象
* 在拖拽结束后调用确保对象状态的完整性
* @private
*/
async function _replaceTargetWithNewImage(dataURL) {
// 获取最新的目标对象
const currentTarget = getCurrentTargetObject();
if (!currentTarget) {
console.warn("无法替换对象:未找到目标对象");
return;
}
2025-06-09 10:25:54 +08:00
return new Promise((resolve) => {
fabric.Image.fromURL(dataURL, (img) => {
2025-06-18 11:05:23 +08:00
// 保留原对象的ID信息
const originalId = targetObjectId.value;
// 保留原对象的所有变换属性和状态
2025-06-09 10:25:54 +08:00
img.set({
2025-06-18 11:05:23 +08:00
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,
2025-06-09 10:25:54 +08:00
layerId: targetLayerId.value,
selected: false,
evented: false,
});
// 替换Canvas上的对象
const allObjects = props.canvas?.getObjects?.();
2025-06-18 11:05:23 +08:00
const index = allObjects.indexOf(currentTarget);
2025-06-09 10:25:54 +08:00
if (index !== -1) {
2025-06-18 11:05:23 +08:00
props.canvas.remove(currentTarget);
2025-06-09 10:25:54 +08:00
props.canvas.insertAt(img, index);
2025-06-18 11:05:23 +08:00
// 更新缓存的引用 - 关键修复
2025-06-09 10:25:54 +08:00
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;
}
}
}
2025-06-18 11:05:23 +08:00
console.log("成功替换目标对象新对象ID:", img.id);
2025-06-09 10:25:54 +08:00
}
2025-06-18 11:05:23 +08:00
2025-06-09 10:25:54 +08:00
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;
2025-06-18 11:05:23 +08:00
// 获取对象的原始尺寸(注意:这里需要考虑对象可能被缩放过)
2025-06-09 10:25:54 +08:00
const objWidth = obj.width;
const objHeight = obj.height;
2025-06-18 11:05:23 +08:00
// 修复关键问题:直接使用对象坐标系到图像坐标系的映射
// fabric对象的坐标原点在中心需要转换到左上角原点
// 同时考虑对象本身可能有内在的缩放
let imageX = (localPoint.x + objWidth / 2) * (imageWidth / objWidth);
let imageY = (localPoint.y + objHeight / 2) * (imageHeight / objHeight);
2025-06-09 10:25:54 +08:00
// 处理图像翻转的情况
if (obj.flipX) {
imageX = imageWidth - imageX;
}
2025-06-18 11:05:23 +08:00
if (obj.flipY) {
imageY = imageHeight - imageY;
}
2025-06-09 10:25:54 +08:00
console.log(
2025-06-18 11:05:23 +08:00
`坐标转换详细信息:
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}`
2025-06-09 10:25:54 +08:00
);
// 检查坐标是否在图像范围内
if (
imageX < 0 ||
imageX >= imageWidth ||
imageY < 0 ||
imageY >= imageHeight
) {
2025-06-18 11:05:23 +08:00
console.warn(
`坐标超出图像范围: (${imageX}, ${imageY}), 图像尺寸: ${imageWidth}x${imageHeight}`
);
2025-06-09 10:25:54 +08:00
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();
}
}
2025-06-18 11:05:23 +08:00
/**
* 重置参数到默认值
*/
function resetParams() {
size.value = 100;
pressure.value = 50;
distortion.value = 0;
power.value = 50;
console.log("重置液化参数到默认值");
}
2025-06-09 10:25:54 +08:00
/**
* 重置为原始状态
*/
function resetToOriginal() {
2025-06-18 11:05:23 +08:00
// 获取最新的目标对象
const currentTarget = getCurrentTargetObject();
if (!props.liquifyManager || !originalImageData.value || !currentTarget) {
console.warn("无法重置:缺少必要的组件或数据");
2025-06-09 10:25:54 +08:00
return;
2025-06-18 11:05:23 +08:00
}
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 创建重置命令 - 使用最新的目标对象
2025-06-09 10:25:54 +08:00
const resetCommand = createLiquifyResetCommand({
canvas: props.canvas,
layerManager: props.layerManager,
2025-06-18 11:05:23 +08:00
targetObject: currentTarget,
2025-06-09 10:25:54 +08:00
targetLayerId: targetLayerId.value,
originalImageData: originalImageData.value,
});
// 执行重置
props.commandManager.execute(resetCommand);
2025-06-18 11:05:23 +08:00
// 重新初始化液化环境 - 使用最新的目标对象
props.liquifyManager.prepareForLiquify(currentTarget);
2025-06-09 10:25:54 +08:00
2025-06-18 11:05:23 +08:00
// 创建新的合成命令 - 使用最新的目标对象
2025-06-09 10:25:54 +08:00
compositeCommand.value = new CompositeLiquifyCommand({
canvas: props.canvas,
2025-06-18 11:05:23 +08:00
targetObject: currentTarget,
2025-06-09 10:25:54 +08:00
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;
2025-06-18 11:05:23 +08:00
targetObjectId.value = null; // 清空对象ID
originalImageData.value = null; // 清空原始图像数据
2025-06-09 10:25:54 +08:00
compositeCommand.value = null;
2025-06-18 11:05:23 +08:00
// 清理液化状态相关数据
initialImageData.value = null; // 清空初始图像数据
initialObjectState.value = null; // 清空初始对象状态
currentLiquifyCommand.value = null; // 清空当前命令
2025-06-09 10:25:54 +08:00
// 重置性能统计
operationCount.value = 0;
avgOperationTime.value = 0;
// 释放液化管理器资源
if (props.liquifyManager) {
props.liquifyManager.dispose();
}
2025-06-18 11:05:23 +08:00
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;
}
2025-06-09 10:25:54 +08:00
}
</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;
2025-06-09 10:25:54 +08:00
}
.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;
2025-06-18 11:05:23 +08:00
right: 12px;
2025-06-09 10:25:54 +08:00
bottom: 0;
2025-06-18 11:05:23 +08:00
width: 32px;
2025-06-09 10:25:54 +08:00
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;
2025-06-09 10:25:54 +08:00
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;
2025-06-09 10:25:54 +08:00
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>