feat: 裁剪组裁剪跟随选择组移动

This commit is contained in:
bighuixiang
2025-07-14 01:00:23 +08:00
parent 96e13cb22a
commit 24e9ba8ae5
80 changed files with 2052 additions and 4292 deletions

View File

@@ -12,25 +12,16 @@
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
:class="['brush-type-item', { active: brushStore.state.type === brush.id }]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<div class="brush-preview" :style="getBrushPreviewStyle(brush)"></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<template v-for="(properties, category) in propertiesByCategory" :key="category">
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
@@ -43,11 +34,7 @@
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<div v-for="prop in properties" :key="prop.id" class="property-item">
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
@@ -61,13 +48,7 @@
<input
type="range"
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
"
@input="(e) => handlePropertyChange(prop.id, parseFloat(e.target.value))"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
@@ -93,25 +74,19 @@
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
<div class="color-preview" :style="{ backgroundColor: prop.value }"></div>
</div>
<div class="color-row">
<input
type="color"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@input="(e) => handlePropertyChange(prop.id, e.target.value)"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state
.recentColors"
v-for="(color, index) in brushStore.state.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@@ -130,9 +105,7 @@
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
@change="(e) => handlePropertyChange(prop.id, e.target.checked)"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
@@ -146,9 +119,7 @@
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@change="(e) => handlePropertyChange(prop.id, e.target.value)"
class="property-select"
>
<option
@@ -225,10 +196,7 @@
<!-- 上传的纹理缓存区域 -->
<!-- 自定义纹理上传按钮 -->
<div
class="texture-item upload-item"
@click="triggerTextureUpload"
>
<div class="texture-item upload-item" @click="triggerTextureUpload">
<div class="upload-icon">
<span>+</span>
</div>
@@ -275,9 +243,7 @@
<input
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@input="(e) => handlePropertyChange(prop.id, e.target.value)"
class="property-input"
/>
</div>
@@ -360,13 +326,7 @@
<input
type="checkbox"
:checked="brushStore.state.shadowEnabled"
@change="
(e) =>
handleShadowPropertyChange(
'shadowEnabled',
e.target.checked
)
"
@change="(e) => handleShadowPropertyChange('shadowEnabled', e.target.checked)"
id="shadow-enabled"
/>
<label for="shadow-enabled"></label>
@@ -390,13 +350,7 @@
<input
type="color"
:value="brushStore.state.shadowColor"
@input="
(e) =>
handleShadowPropertyChange(
'shadowColor',
e.target.value
)
"
@input="(e) => handleShadowPropertyChange('shadowColor', e.target.value)"
class="color-picker"
/>
</div>
@@ -408,20 +362,14 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影宽度") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowWidth }}px</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)
)
(e) => handleShadowPropertyChange('shadowWidth', parseFloat(e.target.value))
"
:min="0"
:max="50"
@@ -435,8 +383,7 @@
:key="preset"
@click="handleShadowPropertyChange('shadowWidth', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
active: Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
}"
>
{{ preset }}
@@ -450,9 +397,7 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影X偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetX }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowOffsetX }}px</span>
</div>
<div class="slider-container">
<input
@@ -460,10 +405,7 @@
:value="brushStore.state.shadowOffsetX"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetX',
parseFloat(e.target.value)
)
handleShadowPropertyChange('shadowOffsetX', parseFloat(e.target.value))
"
:min="-50"
:max="50"
@@ -475,13 +417,9 @@
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetX', preset)
"
@click="handleShadowPropertyChange('shadowOffsetX', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetX - preset) <
0.1,
active: Math.abs(brushStore.state.shadowOffsetX - preset) < 0.1,
}"
>
{{ preset }}
@@ -495,9 +433,7 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影Y偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetY }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowOffsetY }}px</span>
</div>
<div class="slider-container">
<input
@@ -505,10 +441,7 @@
:value="brushStore.state.shadowOffsetY"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetY',
parseFloat(e.target.value)
)
handleShadowPropertyChange('shadowOffsetY', parseFloat(e.target.value))
"
:min="-50"
:max="50"
@@ -520,13 +453,9 @@
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetY', preset)
"
@click="handleShadowPropertyChange('shadowOffsetY', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetY - preset) <
0.1,
active: Math.abs(brushStore.state.shadowOffsetY - preset) < 0.1,
}"
>
{{ preset }}
@@ -544,14 +473,8 @@
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`,
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',
@@ -567,11 +490,7 @@
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<button class="save-preset-btn" @click="saveCurrentAsPreset" title="保存当前设置为预设">
<i class="save-icon">+</i>
</button>
</div>
@@ -694,9 +613,7 @@ const filteredTextures = computed(() => {
if (selectedCategory.value === "全部") {
return textureLibrary;
}
return textureLibrary.filter(
(texture) => texture.category === selectedCategory.value
);
return textureLibrary.filter((texture) => texture.category === selectedCategory.value);
});
// 从材质库选择材质
@@ -909,8 +826,7 @@ function getBrushPreviewStyle(brush) {
case "spray":
return {
...baseStyle,
backgroundImage:
"radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundImage: "radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundSize: "4px 4px",
backgroundPosition: "center",
opacity: 0.8,
@@ -931,8 +847,7 @@ function getBrushPreviewStyle(brush) {
case "rainbow":
return {
...baseStyle,
background:
"linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
background: "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
height: "3px",
};
case "texture":
@@ -957,10 +872,7 @@ function applyPresetWithCommand(presetIndex) {
// 保存当前设置为预设
function saveCurrentAsPreset() {
// 简单实现,可以后续优化为弹窗输入名称
const name = prompt(
"请输入预设名称:",
`预设 ${BrushStore.state.presets.length + 1}`
);
const name = prompt("请输入预设名称:", `预设 ${BrushStore.state.presets.length + 1}`);
if (name) {
const presetIndex = BrushStore.saveCurrentAsPreset(name);
// 应用新创建的预设(可选)
@@ -1740,7 +1652,9 @@ const brushStore = BrushStore;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
animation: modalSlideUp 0.3s ease;
}
@@ -1930,12 +1844,7 @@ const brushStore = BrushStore;
content: "";
flex: 1;
height: 1px;
background: linear-gradient(
to right,
transparent,
rgba(0, 0, 0, 0.08),
transparent
);
background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.08), transparent);
}
.uploaded-textures-divider span {
@@ -2022,8 +1931,7 @@ const brushStore = BrushStore;
}
.shadow-preview-box {
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
border-radius: 8px;
padding: 30px;
display: flex;