feat: 裁剪组裁剪跟随选择组移动
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user