1389 lines
34 KiB
Vue
1389 lines
34 KiB
Vue
|
|
<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>
|