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

1597 lines
39 KiB
Vue
Raw Normal View History

2025-06-09 10:25:54 +08:00
<template>
<div class="brush-panel">
<div class="brush-panel-content">
<!-- 笔刷类型选择 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷类型</span>
</div>
<div class="brush-type-grid">
<div
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
>
<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"
>
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
<div class="section-actions" v-if="category === '材质'">
<button class="action-btn" @click="showLibrary = !showLibrary">
<i class="icon-library">📚</i> 材质库
</button>
</div>
</div>
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
<div class="slider-header">
<span>{{ prop.name }}</span>
<span class="slider-value">
{{ formatPropertyValue(prop) }}
</span>
</div>
<div class="slider-container">
<input
type="range"
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
class="property-slider"
/>
</div>
<!-- 预设值按钮如果定义了预设 -->
<div v-if="prop.presets" class="property-presets">
<button
v-for="preset in prop.presets"
:key="preset"
@click="handlePropertyChange(prop.id, preset)"
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
>
{{ preset }}
</button>
</div>
</div>
</template>
<!-- 颜色选择器 -->
<template v-else-if="prop.type === 'color'">
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<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)
"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@click="handlePropertyChange(prop.id, color)"
></div>
</div>
</div>
</div>
</template>
<!-- 复选框 -->
<template v-else-if="prop.type === 'checkbox'">
<div class="checkbox-property">
<span>{{ prop.name }}</span>
<div class="toggle-switch">
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
</div>
</div>
</template>
<!-- 选择器 -->
<template v-else-if="prop.type === 'select'">
<div class="select-property">
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="property-select"
>
<option
v-for="option in prop.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</template>
<!-- 文件选择器用于材质 -->
<template v-else-if="prop.type === 'file'">
<div class="file-property">
<div class="file-header">
<span>{{ prop.name }}</span>
</div>
<div class="file-preview" @click="handleFileSelect(prop.id)">
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
<div v-else class="no-file">点击上传材质图片</div>
</div>
<div class="file-actions">
<button
class="select-file-btn"
@click="handleFileSelect(prop.id)"
>
上传图片
</button>
<button
class="clear-file-btn"
@click="handlePropertyChange(prop.id, '')"
v-if="prop.value"
>
清除
</button>
</div>
</div>
</template>
<!-- 材质网格选择器 -->
<template v-else-if="prop.type === 'texture-grid'">
<div class="texture-grid-property">
<div class="texture-grid-header">
<span>{{ prop.name }}</span>
</div>
<div class="texture-grid">
<div
v-for="texture in prop.options"
:key="texture.value"
class="texture-item"
:class="{ active: prop.value === texture.value }"
@click="handleTextureSelect(texture.value)"
>
<img
:src="texture.preview || texture.value"
:alt="texture.label"
class="texture-thumbnail"
/>
<span class="texture-label">{{ texture.label }}</span>
</div>
<!-- 自定义纹理上传按钮 -->
<div class="texture-item upload-item" @click="triggerTextureUpload">
<div class="upload-icon">
<span>+</span>
</div>
<span class="texture-label">上传纹理</span>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="textureFileInput"
type="file"
accept="image/*"
@change="handleTextureUpload"
style="display: none;"
/>
</div>
</div>
</template>
<!-- 其他类型的属性 -->
<template v-else>
<div class="generic-property">
<span>{{ prop.name }}</span>
<input
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
class="property-input"
/>
</div>
</template>
<!-- 属性描述提示 -->
<div v-if="prop.description" class="property-description">
{{ prop.description }}
</div>
</div>
</div>
</div>
</template>
<!-- 材质库弹窗 -->
<div
v-if="showLibrary"
class="texture-library-overlay"
@click.self="showLibrary = false"
>
<div class="texture-library-modal">
<div class="modal-header">
<h3>材质库</h3>
<button class="close-btn" @click="showLibrary = false">
&times;
</button>
</div>
<div class="modal-content">
<div class="texture-categories">
<button
v-for="(category, index) in textureCategories"
:key="index"
:class="[
'category-btn',
{ active: selectedCategory === category },
]"
@click="selectedCategory = category"
>
{{ category }}
</button>
</div>
<div class="texture-list">
<div
v-for="(texture, index) in filteredTextures"
:key="index"
class="library-texture-item"
@click="selectLibraryTexture(texture.path)"
>
<img
:src="texture.thumbnail"
:alt="texture.name"
class="texture-thumbnail"
/>
<span class="texture-name">{{ texture.name }}</span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="upload-btn" @click="handleFileSelect('texturePath')">
上传新材质
</button>
</div>
</div>
</div>
<!-- 笔刷预设 -->
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<i class="save-icon">+</i>
</button>
</div>
<div class="presets-container">
<div
v-for="(preset, index) in brushStore.state.presets"
:key="index"
class="preset-item"
@click="applyPresetWithCommand(index)"
>
<div
class="preset-color"
:style="{
backgroundColor: preset.color,
width: preset.size + 'px',
height: preset.size + 'px',
opacity: preset.opacity,
}"
></div>
<span class="preset-name">{{ preset.name }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, inject, onUnmounted, computed } from "vue";
import { BrushStore } from "../store/BrushStore";
import {
BrushSizeCommand,
BrushColorCommand,
BrushOpacityCommand,
BrushTypeCommand,
TextureCommand,
BrushPresetCommand,
BrushPropertyCommand,
TextureUploadCommand,
} from "../commands/BrushCommands";
import { debounce } from "lodash-es";
// 从工具管理器获取可用笔刷类型
const toolManager = inject("toolManager");
// 命令管理器
const commandManager = inject("commandManager");
// 计算属性:按分类获取当前笔刷可配置属性
const propertiesByCategory = computed(() => {
return BrushStore.getPropertiesByCategory();
});
// 材质库状态
const showLibrary = ref(false);
const selectedCategory = ref("全部");
const textureFileInput = ref(null);
const textureCategories = ["全部", "纹理", "笔触", "图案", "特效"];
// 材质库中的材质实际项目中可以从API获取
const textureLibrary = [
{
name: "水彩1",
path: "/textures/watercolor1.png",
thumbnail: "/textures/watercolor1_thumb.png",
category: "笔触",
},
{
name: "水彩2",
path: "/textures/watercolor2.png",
thumbnail: "/textures/watercolor2_thumb.png",
category: "笔触",
},
{
name: "粉笔",
path: "/textures/chalk.png",
thumbnail: "/textures/chalk_thumb.png",
category: "笔触",
},
{
name: "木纹",
path: "/textures/wood.png",
thumbnail: "/textures/wood_thumb.png",
category: "纹理",
},
{
name: "皮革",
path: "/textures/leather.png",
thumbnail: "/textures/leather_thumb.png",
category: "纹理",
},
{
name: "点阵",
path: "/textures/dots.png",
thumbnail: "/textures/dots_thumb.png",
category: "图案",
},
{
name: "线条",
path: "/textures/lines.png",
thumbnail: "/textures/lines_thumb.png",
category: "图案",
},
{
name: "星星",
path: "/textures/stars.png",
thumbnail: "/textures/stars_thumb.png",
category: "特效",
},
// 可以添加更多材质...
];
// 根据选择的类别过滤材质
const filteredTextures = computed(() => {
if (selectedCategory.value === "全部") {
return textureLibrary;
}
return textureLibrary.filter(
(texture) => texture.category === selectedCategory.value
);
});
// 从材质库选择材质
function selectLibraryTexture(path) {
handlePropertyChange("texturePath", path);
showLibrary.value = false;
}
// 处理材质选择
function handleTextureSelect(textureId) {
handlePropertyChange("textureSelector", textureId);
}
// 触发纹理上传文件选择
function triggerTextureUpload() {
const fileInput = textureFileInput.value;
if (fileInput) {
fileInput.click();
}
}
// 处理纹理文件上传
async function handleTextureUpload(event) {
const file = event.target.files?.[0];
if (!file) return;
try {
// 验证文件类型
if (!file.type.startsWith('image/')) {
alert('请选择图片文件');
return;
}
// 验证文件大小 (限制为 5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
alert('文件大小不能超过 5MB');
return;
}
// 获取纹理预设管理器和笔刷管理器
const texturePresetManager = toolManager?.texturePresetManager;
const brushManager = toolManager?.brushManager;
if (!texturePresetManager || !brushManager) {
console.error('缺少必要的管理器实例');
alert('系统错误:无法上传纹理');
return;
}
// 创建并执行纹理上传命令
const command = new TextureUploadCommand({
file: file,
name: file.name.replace(/\.[^/.]+$/, ""), // 移除文件扩展名
category: "自定义材质",
texturePresetManager: texturePresetManager,
brushManager: brushManager,
});
await commandManager.execute(command);
// 清空文件输入,允许重复上传同一文件
event.target.value = '';
} catch (error) {
console.error('纹理上传失败:', error);
alert(`上传失败: ${error.message}`);
}
}
// 格式化属性值显示
function formatPropertyValue(prop) {
if (prop.type === "slider") {
// 如果是透明度,显示为百分比
if (prop.id === "opacity" || prop.id === "textureOpacity") {
return `${Math.round(prop.value * 100)}%`;
}
// 如果有单位,添加单位
if (prop.unit) {
return `${prop.value}${prop.unit}`;
}
// 根据step确定小数位数
const decimalPlaces = prop.step.toString().includes(".")
? prop.step.toString().split(".")[1].length
: 0;
return prop.value.toFixed(decimalPlaces);
}
return prop.value;
}
// 防抖处理函数,优化性能
const debouncedPropertyCommand = debounce((propId, value) => {
// 创建并执行属性更新命令
const command = new BrushPropertyCommand({
propertyId: propId,
value: value,
});
commandManager.execute(command, { nonUndoable: true });
}, 200);
// 处理属性变化
function handlePropertyChange(propId, value) {
// 直接更新UI通过防抖函数延迟创建命令
BrushStore.updatePropertyValue(propId, value);
debouncedPropertyCommand(propId, value);
}
// 使用命令设置笔刷类型
function setBrushTypeWithCommand(type) {
const command = new BrushTypeCommand({
brushType: type,
brushManager: toolManager?.brushManager,
});
commandManager.execute(command, { nonUndoable: true }); // 不需要撤销
}
// 处理文件选择
function handleFileSelect(propId) {
// 创建一个文件输入元素
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
// 更新属性值
handlePropertyChange(propId, e.target.result);
};
reader.readAsDataURL(file);
}
};
input.click();
}
// 获取笔刷预览样式
function getBrushPreviewStyle(brush) {
const baseStyle = {
backgroundColor: brush.id === "eraser" ? "#ffffff" : brushStore.state.color,
};
// 根据不同笔刷类型设置不同样式
switch (brush.id) {
case "pencil":
return {
...baseStyle,
// height: "2px",
borderRadius: "1px",
};
case "marker":
return {
...baseStyle,
// height: "4px",
opacity: 0.7,
borderRadius: "2px",
};
case "spray":
return {
...baseStyle,
backgroundImage:
"radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundSize: "4px 4px",
backgroundPosition: "center",
opacity: 0.8,
};
case "eraser":
return {
...baseStyle,
border: "1px dashed #999",
backgroundColor: "transparent",
};
case "neon":
return {
...baseStyle,
// height: "3px",
borderRadius: "1px",
boxShadow: `0 0 5px 2px ${baseStyle.backgroundColor}`,
};
case "rainbow":
return {
...baseStyle,
background:
"linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
height: "3px",
};
case "texture":
return {
...baseStyle,
backgroundImage:
"url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4IiBoZWlnaHQ9IjgiPjxyZWN0IHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNMCAwTDggOFpNOCAwTDAgOFoiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlPSIjMDAwIj48L3BhdGg+PC9zdmc+')",
backgroundSize: "8px 8px",
// height: "12px",
};
default:
return baseStyle;
}
}
// 应用预设
function applyPresetWithCommand(presetIndex) {
const command = new BrushPresetCommand({ preset: presetIndex });
commandManager.execute(command, { nonUndoable: true });
}
// 保存当前设置为预设
function saveCurrentAsPreset() {
// 简单实现,可以后续优化为弹窗输入名称
const name = prompt(
"请输入预设名称:",
`预设 ${BrushStore.state.presets.length + 1}`
);
if (name) {
const presetIndex = BrushStore.saveCurrentAsPreset(name);
// 应用新创建的预设(可选)
// applyPresetWithCommand(presetIndex);
}
}
// 初始化可用笔刷类型
onMounted(() => {
if (toolManager?.brushManager) {
const availableBrushes = toolManager.brushManager
.getBrushTypes()
?.filter((brush) => brush.id !== "eraser");
BrushStore.setAvailableBrushes(availableBrushes);
}
});
// 导出笔刷存储供模板使用
const brushStore = BrushStore;
</script>
<style scoped>
.brush-panel {
padding: 0;
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
width: 100%;
overflow-y: auto;
max-height: 85vh;
min-width: 280px;
border-left: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
position: relative;
animation: panelFadeIn 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
backdrop-filter: blur(2px); /* 添加模糊效果 */
-webkit-backdrop-filter: blur(2px);
}
/* 添加指向整个面板的倒三角 */
.brush-panel::before {
content: "";
position: absolute;
top: -10px;
left: 20px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
z-index: 1;
}
@keyframes panelFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.brush-panel-content {
display: flex;
flex-direction: column;
margin: 0;
}
.brush-section {
padding: 0;
background-color: transparent;
border: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
box-shadow: none;
transition: all 0.25s ease;
margin-bottom: 1px;
overflow: hidden;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(255, 255, 255, 0.8); /* 略微透明的白色背景 */
padding: 10px 16px;
font-weight: 600;
font-size: 15px;
color: #333; /* 更深的文字颜色 */
cursor: pointer;
user-select: none;
position: relative;
transition: background-color 0.2s ease;
}
.section-header::after {
content: "";
position: absolute;
right: 16px;
top: 50%;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #999; /* 更柔和的颜色 */
transform: translateY(-50%);
transition: transform 0.25s ease;
}
.section-header:hover {
background-color: rgba(248, 249, 250, 1);
}
.section-header:hover::after {
border-top-color: #666;
}
.section-actions {
display: flex;
gap: 8px;
margin-right: 28px; /* 为三角形留出空间 */
}
.action-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
background-color: rgba(66, 133, 244, 0.1); /* 淡蓝色背景 */
border: 1px solid rgba(66, 133, 244, 0.2);
border-radius: 4px;
color: #4285f4; /* 蓝色文字 */
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background-color: rgba(66, 133, 244, 0.15);
transform: translateY(-1px);
}
.property-list {
display: flex;
flex-direction: column;
padding: 14px 16px;
gap: 16px;
background-color: #fff; /* 纯白色背景 */
}
.property-item {
margin-bottom: 5px;
}
/* 滑块控件样式 */
.slider-property {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.slider-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.slider-header span:first-child {
font-weight: 500;
color: #333; /* 更深的文字颜色 */
font-size: 14px;
}
.slider-value {
font-size: 14px;
color: #666; /* 更深的文字颜色 */
min-width: 45px;
text-align: right;
background-color: rgba(0, 0, 0, 0.05);
padding: 3px 7px;
border-radius: 4px;
font-family: "Consolas", monospace;
}
.slider-container {
width: 100%;
padding: 0;
}
.property-slider {
width: 100%;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
height: 4px;
border-radius: 2px;
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
outline: none;
}
.property-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #4285f4; /* 蓝色滑块 */
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.property-slider::-webkit-slider-thumb:hover {
background: #3b77db;
transform: scale(1.1);
}
.property-presets {
display: flex;
justify-content: flex-start;
margin-top: 10px;
flex-wrap: wrap;
gap: 7px;
}
.property-presets button {
min-width: 32px;
height: 22px;
border: 1px solid rgba(0, 0, 0, 0.05);
background: #fff;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
padding: 0 7px;
transition: all 0.2s;
}
.property-presets button.active {
background-color: rgba(66, 133, 244, 0.1);
border-color: rgba(66, 133, 244, 0.2);
color: #4285f4;
font-weight: 500;
}
.property-presets button:hover:not(.active) {
background-color: rgba(0, 0, 0, 0.03);
transform: translateY(-1px);
}
/* 笔刷类型网格 */
.brush-type-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
padding: 14px 16px;
gap: 10px;
background-color: #fff; /* 纯白色背景 */
}
.brush-type-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px;
border-radius: 4px;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.2s;
background-color: #fff;
justify-content: center;
}
.brush-type-item:hover {
background-color: rgba(0, 0, 0, 0.02);
border-color: rgba(66, 133, 244, 0.2);
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
}
.brush-type-item.active {
background-color: rgba(66, 133, 244, 0.1);
border-color: rgba(66, 133, 244, 0.2);
}
/* 统一笔刷预览大小 */
.brush-preview {
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.02);
}
/* 保持笔刷预览内容样式一致 */
.brush-preview::before {
content: "";
width: 75%;
height: 3px;
background-color: currentColor;
}
.brush-name {
font-size: 14px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
color: #333; /* 更深的文字颜色 */
}
/* 颜色选择器样式 */
.color-property .color-header,
.file-property .file-header,
.texture-grid-property .texture-grid-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.color-property .color-header span,
.file-property .file-header span,
.texture-grid-property .texture-grid-header span {
font-weight: 500;
color: #333; /* 更深的文字颜色 */
font-size: 14px;
}
.color-row {
display: flex;
align-items: center;
gap: 12px;
}
.color-preview {
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4);
}
.color-picker {
width: 40px;
height: 40px;
border: 1px solid rgba(0, 0, 0, 0.05);
padding: 0;
background: none;
cursor: pointer;
border-radius: 4px;
overflow: hidden;
}
.recent-colors {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.color-item {
width: 22px;
height: 22px;
border-radius: 4px;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.2s;
}
.color-item:hover {
transform: scale(1.08);
border-color: rgba(66, 133, 244, 0.2);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* 复选框样式 */
.checkbox-property,
.select-property,
.generic-property {
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
}
.checkbox-property span,
.select-property span,
.generic-property span {
font-weight: 500;
color: #333; /* 更深的文字颜色 */
font-size: 14px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 22px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-switch label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
border-radius: 22px;
transition: 0.25s;
}
.toggle-switch label:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
transition: 0.25s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.toggle-switch input:checked + label {
background-color: #4285f4; /* 蓝色 */
}
.toggle-switch input:checked + label:before {
transform: translateX(22px);
}
/* 选择框样式 */
.property-select {
width: 150px;
padding: 6px 10px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.05);
background-color: #fff;
color: #333; /* 更深的文字颜色 */
font-size: 14px;
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath fill='%23666' d='M0 0h8L4 5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
-webkit-appearance: none;
appearance: none;
padding-right: 30px;
height: 34px;
}
.property-select:focus {
border-color: #4285f4;
outline: none;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.1);
}
/* 文件选择器样式 */
.file-property {
display: flex;
flex-direction: column;
gap: 10px;
}
.file-preview {
width: 100%;
height: 110px;
border: 1px dashed rgba(0, 0, 0, 0.05);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.02);
cursor: pointer;
transition: all 0.2s;
}
.file-preview:hover {
border-color: rgba(66, 133, 244, 0.2);
background-color: rgba(66, 133, 244, 0.05);
transform: translateY(-1px);
}
.file-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.no-file {
font-size: 14px;
color: #666; /* 更深的文字颜色 */
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.no-file:before {
content: "📁";
font-size: 22px;
}
.file-actions {
display: flex;
justify-content: space-between;
gap: 10px;
}
.select-file-btn,
.clear-file-btn {
padding: 7px 14px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
flex: 1;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
}
.select-file-btn {
background-color: rgba(66, 133, 244, 0.1);
border: 1px solid rgba(66, 133, 244, 0.2);
color: #4285f4;
}
.select-file-btn:hover {
background-color: rgba(66, 133, 244, 0.15);
transform: translateY(-1px);
}
.clear-file-btn {
background-color: rgba(244, 67, 54, 0.1);
border: 1px solid rgba(244, 67, 54, 0.2);
color: #f44336;
}
.clear-file-btn:hover {
background-color: rgba(244, 67, 54, 0.15);
transform: translateY(-1px);
}
.property-description {
font-size: 13px;
color: #666; /* 更深的文字颜色 */
margin-top: 5px;
line-height: 1.4;
font-style: italic;
}
/* 文本输入框样式 */
.property-input {
width: 150px;
padding: 6px 10px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.05);
font-size: 14px;
color: #333; /* 更深的文字颜色 */
height: 34px;
}
/* 预设容器样式 */
.presets-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
padding: 14px 16px;
gap: 10px;
background-color: #fff; /* 纯白色背景 */
}
.preset-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 6px;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
cursor: pointer;
background-color: white;
transition: all 0.2s;
height: 90px;
justify-content: center;
}
.preset-item:hover {
border-color: rgba(66, 133, 244, 0.2);
background-color: rgba(0, 0, 0, 0.02);
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
}
.preset-color {
border-radius: 50%;
margin-bottom: 10px;
min-width: 18px;
min-height: 18px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.preset-name {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
color: #333; /* 更深的文字颜色 */
}
.save-preset-btn {
width: 24px;
height: 24px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(66, 133, 244, 0.2);
border-radius: 4px;
background-color: rgba(66, 133, 244, 0.1);
color: #4285f4;
cursor: pointer;
padding: 0;
line-height: 0;
transition: all 0.2s;
}
.save-preset-btn:hover {
background-color: rgba(66, 133, 244, 0.15);
transform: translateY(-1px);
}
.save-icon {
font-style: normal;
}
/* 材质网格样式 */
.texture-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
gap: 10px;
margin-top: 10px;
}
.texture-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
background-color: #fff;
height: 90px;
justify-content: center;
}
.texture-item:hover {
border-color: rgba(66, 133, 244, 0.2);
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
}
.texture-item.active {
background-color: rgba(66, 133, 244, 0.1);
border-color: rgba(66, 133, 244, 0.2);
}
.texture-thumbnail {
width: 100%;
height: 45px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 8px;
}
.texture-label {
font-size: 14px;
color: #333; /* 更深的文字颜色 */
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
/* 上传纹理按钮样式 */
.texture-item.upload-item {
border: 2px dashed rgba(66, 133, 244, 0.3);
background-color: rgba(66, 133, 244, 0.05);
}
.texture-item.upload-item:hover {
border-color: rgba(66, 133, 244, 0.5);
background-color: rgba(66, 133, 244, 0.1);
}
.upload-icon {
width: 100%;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
border-radius: 4px;
background-color: rgba(66, 133, 244, 0.1);
}
.upload-icon span {
font-size: 24px;
color: #4285f4;
font-weight: bold;
}
/* 材质库样式 */
.texture-library-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.5); /* 白色半透明背景 */
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.texture-library-modal {
background-color: #fff; /* 白色背景 */
border-radius: 12px;
width: 80%;
max-width: 700px;
max-height: 80vh;
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);
animation: modalSlideUp 0.3s ease;
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 14px 18px;
background-color: rgba(255, 255, 255, 0.8); /* 略微透明的白色背景 */
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 17px;
color: #333; /* 更深的文字颜色 */
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 22px;
cursor: pointer;
color: #666; /* 更深的文字颜色 */
padding: 0;
line-height: 1;
opacity: 0.7;
transition: all 0.2s;
}
.close-btn:hover {
opacity: 1;
color: #333; /* 更深的文字颜色 */
transform: scale(1.1);
}
.modal-content {
padding: 18px;
overflow-y: auto;
max-height: calc(80vh - 110px);
display: flex;
flex-direction: column;
gap: 18px;
background-color: #fff; /* 纯白色背景 */
}
.texture-categories {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 10px;
flex-wrap: wrap;
scrollbar-width: thin;
}
.texture-categories::-webkit-scrollbar {
height: 4px;
}
.texture-categories::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
border-radius: 4px;
}
.category-btn {
padding: 6px 12px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.05);
color: #333; /* 更深的文字颜色 */
cursor: pointer;
font-size: 14px;
white-space: nowrap;
transition: all 0.2s;
height: 34px;
display: flex;
align-items: center;
}
.category-btn.active {
background-color: rgba(66, 133, 244, 0.1);
border-color: rgba(66, 133, 244, 0.2);
color: #4285f4;
}
.category-btn:hover:not(.active) {
background-color: rgba(0, 0, 0, 0.05);
transform: translateY(-1px);
}
.texture-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 12px;
}
.library-texture-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
background-color: #fff;
height: 110px;
justify-content: center;
}
.library-texture-item:hover {
border-color: rgba(66, 133, 244, 0.2);
background-color: rgba(0, 0, 0, 0.02);
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.library-texture-item img {
width: 100%;
height: 65px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 8px;
}
.texture-name {
font-size: 14px;
color: #333; /* 更深的文字颜色 */
text-align: center;
}
.modal-footer {
padding: 14px 18px;
background-color: rgba(255, 255, 255, 0.8); /* 略微透明的白色背景 */
border-top: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
justify-content: flex-end;
}
.upload-btn {
padding: 7px 16px;
background-color: #4285f4; /* 蓝色 */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 15px;
transition: all 0.2s;
height: 36px;
display: flex;
align-items: center;
}
.upload-btn:hover {
background-color: #3b77db;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 移除内部动画,只保留整体动画 */
.brush-type-grid,
.property-list,
.presets-container {
animation: none;
}
</style>