fix: 修复多个已知问题
This commit is contained in:
@@ -257,6 +257,12 @@ function setBrushOpacity(opacity) {
|
||||
// 如果工具管理器存在,立即应用此更改
|
||||
if (toolManager) {
|
||||
toolManager.updateBrushOpacity(opacity);
|
||||
|
||||
// 同时更新颜色以确保透明度生效
|
||||
const currentColor = BrushStore.state.color;
|
||||
if (currentColor) {
|
||||
toolManager.updateBrushColor(currentColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -345,7 +345,224 @@
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- 阴影设置 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ $t("阴影设置") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="property-list">
|
||||
<!-- 阴影开关 -->
|
||||
<div class="property-item">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ $t("启用阴影") }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="brushStore.state.shadowEnabled"
|
||||
@change="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowEnabled',
|
||||
e.target.checked
|
||||
)
|
||||
"
|
||||
id="shadow-enabled"
|
||||
/>
|
||||
<label for="shadow-enabled"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影控制组 - 仅在启用阴影时显示 -->
|
||||
<template v-if="brushStore.state.shadowEnabled">
|
||||
<!-- 阴影颜色 -->
|
||||
<div class="property-item">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ $t("阴影颜色") }}</span>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: brushStore.state.shadowColor }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="brushStore.state.shadowColor"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowColor',
|
||||
e.target.value
|
||||
)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影宽度 -->
|
||||
<div class="property-item">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ $t("阴影宽度") }}</span>
|
||||
<span class="slider-value"
|
||||
>{{ brushStore.state.shadowWidth }}px</span
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="brushStore.state.shadowWidth"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowWidth',
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<div class="property-presets">
|
||||
<button
|
||||
v-for="preset in [0, 5, 10, 15, 20]"
|
||||
:key="preset"
|
||||
@click="handleShadowPropertyChange('shadowWidth', preset)"
|
||||
:class="{
|
||||
active:
|
||||
Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
|
||||
}"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影X偏移 -->
|
||||
<div class="property-item">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ $t("阴影X偏移") }}</span>
|
||||
<span class="slider-value"
|
||||
>{{ brushStore.state.shadowOffsetX }}px</span
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="brushStore.state.shadowOffsetX"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowOffsetX',
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="-50"
|
||||
:max="50"
|
||||
:step="1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<div class="property-presets">
|
||||
<button
|
||||
v-for="preset in [-10, -5, 0, 5, 10]"
|
||||
:key="preset"
|
||||
@click="
|
||||
handleShadowPropertyChange('shadowOffsetX', preset)
|
||||
"
|
||||
:class="{
|
||||
active:
|
||||
Math.abs(brushStore.state.shadowOffsetX - preset) <
|
||||
0.1,
|
||||
}"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影Y偏移 -->
|
||||
<div class="property-item">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ $t("阴影Y偏移") }}</span>
|
||||
<span class="slider-value"
|
||||
>{{ brushStore.state.shadowOffsetY }}px</span
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="brushStore.state.shadowOffsetY"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowOffsetY',
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="-50"
|
||||
:max="50"
|
||||
:step="1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<div class="property-presets">
|
||||
<button
|
||||
v-for="preset in [-10, -5, 0, 5, 10]"
|
||||
:key="preset"
|
||||
@click="
|
||||
handleShadowPropertyChange('shadowOffsetY', preset)
|
||||
"
|
||||
:class="{
|
||||
active:
|
||||
Math.abs(brushStore.state.shadowOffsetY - preset) <
|
||||
0.1,
|
||||
}"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影预览 -->
|
||||
<div class="property-item">
|
||||
<div class="shadow-preview-container">
|
||||
<div class="shadow-preview-title">{{ $t("阴影预览") }}</div>
|
||||
<div class="shadow-preview-box">
|
||||
<div
|
||||
class="shadow-preview-element"
|
||||
:style="{
|
||||
backgroundColor: brushStore.state.color,
|
||||
width: `${Math.max(
|
||||
20,
|
||||
Math.min(60, brushStore.state.size)
|
||||
)}px`,
|
||||
height: `${Math.max(
|
||||
20,
|
||||
Math.min(60, brushStore.state.size)
|
||||
)}px`,
|
||||
boxShadow: brushStore.state.shadowEnabled
|
||||
? `${brushStore.state.shadowOffsetX}px ${brushStore.state.shadowOffsetY}px ${brushStore.state.shadowWidth}px ${brushStore.state.shadowColor}`
|
||||
: 'none',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
@@ -625,6 +842,14 @@ const debouncedPropertyCommand = debounce((propId, value) => {
|
||||
commandManager.execute(command, { nonUndoable: true });
|
||||
}, 200);
|
||||
|
||||
// 处理阴影属性变化的防抖函数
|
||||
const debouncedShadowCommand = debounce((propId, value) => {
|
||||
// 通知工具管理器更新阴影
|
||||
if (toolManager?.brushManager) {
|
||||
toolManager.brushManager.updateShadow();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 处理属性变化
|
||||
function handlePropertyChange(propId, value) {
|
||||
// 直接更新UI,通过防抖函数延迟创建命令
|
||||
@@ -632,6 +857,25 @@ function handlePropertyChange(propId, value) {
|
||||
debouncedPropertyCommand(propId, value);
|
||||
}
|
||||
|
||||
// 处理阴影属性变化
|
||||
function handleShadowPropertyChange(propId, value) {
|
||||
// 更新BrushStore中的阴影属性
|
||||
if (propId === "shadowEnabled") {
|
||||
BrushStore.setShadowEnabled(value);
|
||||
} else if (propId === "shadowColor") {
|
||||
BrushStore.setShadowColor(value);
|
||||
} else if (propId === "shadowWidth") {
|
||||
BrushStore.setShadowWidth(value);
|
||||
} else if (propId === "shadowOffsetX") {
|
||||
BrushStore.setShadowOffsetX(value);
|
||||
} else if (propId === "shadowOffsetY") {
|
||||
BrushStore.setShadowOffsetY(value);
|
||||
}
|
||||
|
||||
// 通知笔刷管理器更新阴影设置
|
||||
debouncedShadowCommand(propId, value);
|
||||
}
|
||||
|
||||
// 使用命令设置笔刷类型
|
||||
function setBrushTypeWithCommand(type) {
|
||||
const command = new BrushTypeCommand({
|
||||
@@ -1761,4 +2005,65 @@ const brushStore = BrushStore;
|
||||
background-color: rgba(244, 67, 54, 1);
|
||||
transform: scale(1.2) !important;
|
||||
}
|
||||
|
||||
/* 阴影设置样式 */
|
||||
.shadow-preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shadow-preview-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shadow-preview-box {
|
||||
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
|
||||
10px 10px;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shadow-preview-element {
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.brush-panel {
|
||||
width: 95%;
|
||||
right: 2.5%;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.shadow-preview-box {
|
||||
min-height: 100px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.property-presets {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.brush-type-grid,
|
||||
.presets-container,
|
||||
.texture-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -332,6 +332,7 @@ async function prepareForLiquify(targetObj) {
|
||||
targetObject: targetObject.value,
|
||||
targetLayerId: targetLayerId.value,
|
||||
originalData: originalImageData.value,
|
||||
liquifyManager: props.liquifyManager,
|
||||
layerManager: props.layerManager,
|
||||
});
|
||||
|
||||
@@ -483,6 +484,7 @@ function showPanel(event) {
|
||||
targetLayerId: targetLayerId.value,
|
||||
originalData: originalImageData.value,
|
||||
layerManager: props.layerManager,
|
||||
liquifyManager: props.liquifyManager,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -739,10 +741,59 @@ function removeCanvasListeners() {
|
||||
_handleMouseUp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前图像的实际状态数据
|
||||
* @param {Object} targetObject Fabric图像对象
|
||||
* @returns {Promise<ImageData|null>} 当前图像数据或null
|
||||
*/
|
||||
async function getCurrentImageData(targetObject) {
|
||||
if (!targetObject || !targetObject._element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建临时canvas来获取当前图像状态
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const element = targetObject._element;
|
||||
|
||||
// 设置canvas尺寸为原始图像尺寸
|
||||
if (originalImageData.value) {
|
||||
tempCanvas.width = originalImageData.value.width;
|
||||
tempCanvas.height = originalImageData.value.height;
|
||||
} else {
|
||||
tempCanvas.width = element.naturalWidth || element.width;
|
||||
tempCanvas.height = element.naturalHeight || element.height;
|
||||
}
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 绘制当前图像到临时canvas
|
||||
tempCtx.drawImage(element, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
|
||||
// 获取ImageData
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
"✅ 成功获取当前图像状态,尺寸:",
|
||||
imageData.width,
|
||||
"x",
|
||||
imageData.height
|
||||
);
|
||||
return imageData;
|
||||
} catch (error) {
|
||||
console.warn("获取当前图像数据失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标按下事件处理
|
||||
*/
|
||||
function handleMouseDown(event) {
|
||||
async function handleMouseDown(event) {
|
||||
if (!isEditing.value || !visible.value || !props.liquifyManager) return;
|
||||
|
||||
isDrawing.value = true;
|
||||
@@ -760,29 +811,43 @@ function handleMouseDown(event) {
|
||||
lastX.value = pointer.x;
|
||||
lastY.value = pointer.y;
|
||||
|
||||
// === 修复:记录初始图像数据 ===
|
||||
// === 修复:记录当前图像状态(而不是原始状态)===
|
||||
try {
|
||||
const currentTarget = getCurrentTargetObject();
|
||||
if (currentTarget && originalImageData.value) {
|
||||
console.log("🎯 记录液化操作初始状态,对象ID:", targetObjectId.value);
|
||||
if (currentTarget) {
|
||||
console.log("🎯 记录液化操作当前状态,对象ID:", targetObjectId.value);
|
||||
|
||||
// 记录初始图像数据(深拷贝)
|
||||
const originalData = originalImageData.value;
|
||||
initialImageData.value = new ImageData(
|
||||
new Uint8ClampedArray(originalData.data),
|
||||
originalData.width,
|
||||
originalData.height
|
||||
);
|
||||
// 获取当前图像的实际状态(而不是原始状态)
|
||||
const currentImageData = await getCurrentImageData(currentTarget);
|
||||
if (currentImageData) {
|
||||
// 记录当前图像数据(深拷贝)
|
||||
initialImageData.value = new ImageData(
|
||||
new Uint8ClampedArray(currentImageData.data),
|
||||
currentImageData.width,
|
||||
currentImageData.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
"✅ 当前图像状态已记录,尺寸:",
|
||||
initialImageData.value.width,
|
||||
"x",
|
||||
initialImageData.value.height
|
||||
);
|
||||
} else {
|
||||
// 如果无法获取当前状态,使用原始状态作为备用
|
||||
if (originalImageData.value) {
|
||||
const originalData = originalImageData.value;
|
||||
initialImageData.value = new ImageData(
|
||||
new Uint8ClampedArray(originalData.data),
|
||||
originalData.width,
|
||||
originalData.height
|
||||
);
|
||||
console.log("⚠️ 使用原始状态作为备用初始状态");
|
||||
}
|
||||
}
|
||||
|
||||
// 备用:也保存序列化状态
|
||||
initialObjectState.value = serializeFabricObject(currentTarget);
|
||||
|
||||
console.log(
|
||||
"✅ 初始图像数据已记录,尺寸:",
|
||||
initialImageData.value.width,
|
||||
"x",
|
||||
initialImageData.value.height
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 记录初始状态失败:", error);
|
||||
@@ -965,6 +1030,7 @@ async function handleMouseUp() {
|
||||
currentLiquifyCommand.value = createLiquifyStateCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
liquifyManager: props.liquifyManager,
|
||||
targetObject: currentTarget,
|
||||
targetLayerId: targetLayerId.value,
|
||||
targetObjectId: targetObjectId.value,
|
||||
@@ -1646,7 +1712,7 @@ function stopPressTimer() {
|
||||
/* 平板适配:最多6列 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.liquify-modes {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
@@ -1663,7 +1729,7 @@ function stopPressTimer() {
|
||||
/* 手机适配:最多4列 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.liquify-modes {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- 顶部选区类型工具栏 -->
|
||||
<div class="toolbar-section">
|
||||
<div class="toolbar-header">
|
||||
<div class="header-title">选区工具</div>
|
||||
<div class="header-title">{{ t("选区工具") }}</div>
|
||||
<!-- 移除关闭按钮,完全通过工具切换控制显示隐藏 -->
|
||||
</div>
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
CreateSelectionCommand,
|
||||
InvertSelectionCommand,
|
||||
@@ -232,8 +233,8 @@ const hasSelection = ref(false);
|
||||
const showFeatherDialog = ref(false);
|
||||
const showColorPicker = ref(false);
|
||||
|
||||
// 国际化函数 (简单实现,可根据需要替换为实际的国际化方案)
|
||||
const $t = (key) => key;
|
||||
// 国际化
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
// 为选区管理器添加监听,以便在选区变化时更新状态
|
||||
@@ -647,7 +648,7 @@ function confirmColorPicker() {
|
||||
|
||||
.tool-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user