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

1389 lines
34 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>
<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,
} from "../commands/LiquifyCommands";
import { OperationType } from "../utils/layerHelper";
// 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(true); // 设为true可以显示调试信息
const currImage = ref("");
// 当前状态
const isEditing = ref(false);
const isDrawing = ref(false); // 是否正在绘制
const targetObject = ref(null);
const targetLayerId = ref(null);
const originalImageData = ref(null);
// 渲染模式信息
const renderMode = ref("未知");
const webglAvailable = ref(false);
const operationCount = ref(0);
const avgOperationTime = ref(0);
// 鼠标位置追踪
const lastX = ref(0);
const lastY = ref(0);
// 当前模式
const currentMode = ref("push"); // 默认为推动模式
// 参数设置
const size = ref(50); // 工具大小 (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 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;
// 获取图层信息
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);
// 监听画布事件
setupCanvasListeners();
});
// 生命周期 - beforeUnmount
onUnmounted(() => {
document.removeEventListener("showLiquifyPanel", showPanel);
removeCanvasListeners();
});
// 方法定义
/**
* 显示液化面板
*/
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();
}
targetObject.value = 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: targetObject.value,
targetLayerId: targetLayerId.value,
originalData: originalImageData.value,
layerManager: props.layerManager,
});
}
visible.value = true;
isEditing.value = true;
// 初始化液化管理器并准备液化环境
if (props.liquifyManager && targetObject.value) {
// 确保液化管理器正确初始化
props.liquifyManager.initialize({
canvas: props.canvas,
layerManager: props.layerManager,
});
// 如果没有原始图像数据,则需要先准备液化环境
if (!originalImageData.value) {
// 准备液化环境并获取原始图像数据
props.liquifyManager
.prepareForLiquify(targetObject.value)
.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(targetObject.value)
.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();
}
});
}
}
}
// 更新所有参数到液化管理器
function updateAllParams() {
if (!props.liquifyManager) return;
updateParam("size", size.value);
updateParam("pressure", pressure.value / 100);
updateParam("distortion", distortion.value / 100);
updateParam("power", power.value / 100);
}
/**
* 设置画布事件监听
*/
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);
}
/**
* 移除画布事件监听
*/
function removeCanvasListeners() {
if (!props.canvas) return;
props.canvas.off("mouse:down", _handleMouseDown);
props.canvas.off("mouse:move", _handleMouseMove);
props.canvas.off("mouse:up", _handleMouseUp);
_handleMouseDown = null;
_handleMouseMove = null;
_handleMouseUp = null;
}
/**
* 鼠标按下事件处理
*/
function handleMouseDown(event) {
if (!isEditing.value || !visible.value || !props.liquifyManager) return;
isDrawing.value = true;
const pointer = props.canvas.getPointer(event.e);
// 记录起始点
lastX.value = pointer.x;
lastY.value = pointer.y;
// 应用液化效果
applyLiquifyAtPoint(pointer.x, pointer.y);
}
/**
* 鼠标移动事件处理
*/
async function handleMouseMove(event) {
if (!isEditing.value || !visible.value || !isDrawing.value) return;
const pointer = props.canvas.getPointer(event.e);
// 应用液化效果
applyLiquifyAtPoint(pointer.x, pointer.y);
// 更新坐标
lastX.value = pointer.x;
lastY.value = pointer.y;
}
/**
* 鼠标抬起事件处理
*/
function handleMouseUp() {
if (!isEditing.value || !visible.value) return;
// 设置绘制状态为false但不触发其他事件
isDrawing.value = false;
// 在这里不要触发其他可能导致重复初始化的事件
console.log("鼠标抬起,结束液化变形操作");
}
/**
* 在特定点应用液化效果
*/
async function applyLiquifyAtPoint(x, y) {
if (!props.liquifyManager || !targetObject.value) {
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: targetObject.value,
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(
targetObject.value,
currentMode.value,
params,
imageCoords.x, // 使用转换后的图像坐标
imageCoords.y
);
// 关键修复:将变形结果应用到画布上的对象
if (resultData) {
await _updateCanvasObject(resultData);
}
// 计算操作耗时
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 (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);
}
}
/**
* 将变形后的ImageData更新到Canvas对象上
* @param {ImageData} imageData 变形后的图像数据
* @private
*/
async function _updateCanvasObject(imageData) {
if (!imageData || !targetObject.value || !props.canvas) return;
// 创建临时Canvas来渲染结果数据
const tempCanvas = document.createElement("canvas");
tempCanvas.width = imageData.width;
tempCanvas.height = imageData.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.putImageData(imageData, 0, 0);
// 将Canvas转换为DataURL
const dataURL = tempCanvas.toDataURL("png", 1);
currImage.value = dataURL;
// 创建新的fabric图像对象
return new Promise((resolve) => {
fabric.Image.fromURL(dataURL, (img) => {
// 保留原对象的变换属性
img.set({
left: targetObject.value.left,
top: targetObject.value.top,
scaleX: targetObject.value.scaleX,
scaleY: targetObject.value.scaleY,
angle: targetObject.value.angle,
flipX: targetObject.value.flipX,
flipY: targetObject.value.flipY,
opacity: targetObject.value.opacity,
originX: targetObject.value.originX,
originY: targetObject.value.originY,
id: targetObject.value.id,
name: targetObject.value.name,
layerId: targetLayerId.value,
selected: false,
evented: false,
});
// 替换Canvas上的对象
const allObjects = props.canvas?.getObjects?.();
const index = allObjects.indexOf(targetObject.value);
if (index !== -1) {
props.canvas.remove(targetObject.value);
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;
}
}
}
} else {
const fabricObject = allObjects.find(
(fItem) => fItem.layerId === props.layerManager.getActiveLayerId()
);
targetObject.value = img;
props.canvas.remove(fabricObject);
props.canvas.add(img);
}
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;
// 计算从对象坐标到图像像素坐标的缩放比例
const scaleX = imageWidth / objWidth;
const scaleY = imageHeight / objHeight;
// 转换为图像像素坐标
// 注意Fabric对象坐标原点在中心需要调整到左上角
// 修复检查对象是否有翻转如果有flipX则需要翻转X坐标
let imageX = (localPoint.x + objWidth / 2) * scaleX;
const imageY = (localPoint.y + objHeight / 2) * scaleY;
// 处理图像翻转的情况
if (obj.flipX) {
imageX = imageWidth - imageX;
}
console.log(
`坐标转换: Fabric(${fabricX}, ${fabricY}) -> 本地(${localPoint.x.toFixed(
2
)}, ${localPoint.y.toFixed(2)}) -> 图像(${imageX.toFixed(
2
)}, ${imageY.toFixed(2)}), flipX=${obj.flipX}`
);
// 检查坐标是否在图像范围内
if (
imageX < 0 ||
imageX >= imageWidth ||
imageY < 0 ||
imageY >= 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 resetToOriginal() {
if (!props.liquifyManager || !originalImageData.value || !targetObject.value)
return;
// 创建重置命令
const resetCommand = createLiquifyResetCommand({
canvas: props.canvas,
layerManager: props.layerManager,
targetObject: targetObject.value,
targetLayerId: targetLayerId.value,
originalImageData: originalImageData.value,
});
// 执行重置
props.commandManager.execute(resetCommand);
// 重新初始化液化环境
props.liquifyManager.prepareForLiquify(targetObject.value);
// 创建新的合成命令
compositeCommand.value = new CompositeLiquifyCommand({
canvas: props.canvas,
targetObject: targetObject.value,
targetLayerId: targetLayerId.value,
originalData: originalImageData.value,
layerManager: props.layerManager,
});
}
/**
* 更新参数
*/
function updateParam(name, value) {
if (props.liquifyManager) {
props.liquifyManager.setParam(name, value);
}
}
/**
* 重置参数为默认值
*/
function resetParams() {
size.value = 50;
pressure.value = 50;
distortion.value = 0;
power.value = 50;
if (props.liquifyManager) {
props.liquifyManager.resetParams();
}
}
/**
* 取消液化操作
*/
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() {
debugger;
visible.value = false;
isEditing.value = false;
targetObject.value = null;
targetLayerId.value = null;
compositeCommand.value = null;
// 重置性能统计
operationCount.value = 0;
avgOperationTime.value = 0;
// 释放液化管理器资源
if (props.liquifyManager) {
props.liquifyManager.dispose();
}
}
</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);
}
.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: -319px;
bottom: 0;
width: 244px;
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: 9px;
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: 11px;
margin-bottom: 6px;
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>