Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="brush-control-panel">
|
||||
<div class="input-box">
|
||||
<div class="label">Size</div>
|
||||
<depth-slider
|
||||
:is-input="false"
|
||||
v-model="brushSize"
|
||||
:tip-formatter="(v) => v + 'px'"
|
||||
:min="5"
|
||||
:max="100"
|
||||
@input="onInputSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-box" v-if="currentTool === OperationType.DRAW">
|
||||
<div class="label">Opacity</div>
|
||||
<depth-slider
|
||||
:is-input="false"
|
||||
v-model="brushOpacity"
|
||||
:tip-formatter="(v) => (v * 100).toFixed(0) + '%'"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@input="onInputOpacity"
|
||||
/>
|
||||
</div>
|
||||
<div class="color-box" v-if="currentTool === OperationType.DRAW">
|
||||
<input type="color" v-model="brushColor" @input="onInputColor" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, computed, watch } from 'vue'
|
||||
import depthSlider from './tools/depth-slider.vue'
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
const props = defineProps({
|
||||
currentTool: { required: true, type: [String, null] }
|
||||
})
|
||||
const stateManager = inject('stateManager') as any
|
||||
const toolManager = inject('toolManager') as any
|
||||
const brushState = computed(() => stateManager.brushManager.brushStore.state)
|
||||
const showTools = [OperationType.DRAW, OperationType.ERASER]
|
||||
const show = computed(() => showTools.includes(props.currentTool))
|
||||
watch(brushState, (value) => {
|
||||
if (value) updateBrushState()
|
||||
})
|
||||
const brushSize = ref(40)
|
||||
const brushOpacity = ref(100)
|
||||
const brushColor = ref('#000000')
|
||||
const onInputSize = (value: number) => {
|
||||
stateManager.brushManager.setBrushSize(value)
|
||||
}
|
||||
const onInputOpacity = (value: number) => {
|
||||
stateManager.brushManager.setBrushOpacity(value)
|
||||
}
|
||||
const onInputColor = () => {
|
||||
stateManager.brushManager.setBrushColor(brushColor.value)
|
||||
}
|
||||
const updateBrushState = () => {
|
||||
brushSize.value = brushState.value.size
|
||||
brushOpacity.value = brushState.value.opacity
|
||||
brushColor.value = brushState.value.color
|
||||
}
|
||||
updateBrushState()
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.brush-control-panel {
|
||||
position: absolute;
|
||||
top: 8.8rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: 4.1rem;
|
||||
padding: 0 2rem;
|
||||
border: 0.2rem solid #ebebeb;
|
||||
background: #ffffff;
|
||||
border-radius: 1.1rem;
|
||||
box-shadow: 0 1.66rem 2.33rem 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
gap: 1.5rem;
|
||||
> .input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
> .label {
|
||||
font-size: 1.4rem;
|
||||
color: #000;
|
||||
}
|
||||
> .depth-slider {
|
||||
width: 8.4rem;
|
||||
}
|
||||
}
|
||||
> .color-box {
|
||||
> input {
|
||||
width: 4.7rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,9 @@
|
||||
<span class="icon"><svg-icon name="export" size="12" /></span>
|
||||
<span class="text">Export</span>
|
||||
</button>
|
||||
<button class="export" @click="emit('import')">
|
||||
<span class="text">import</span>
|
||||
</button>
|
||||
<button class="workbench">
|
||||
<span class="icon"><svg-icon name="dc-workbench" size="20" /></span>
|
||||
<span class="text">Workbench</span>
|
||||
@@ -30,6 +33,7 @@
|
||||
step: { default: 0.1, type: Number }
|
||||
})
|
||||
const emit = defineEmits(['export', 'import'])
|
||||
const importLocalImage = inject('importLocalImage') as () => void
|
||||
const stateManager = inject('stateManager') as any
|
||||
const toolManager = inject('toolManager') as any
|
||||
const tool = computed(() => toolManager.currentTool.value)
|
||||
@@ -42,7 +46,13 @@
|
||||
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
||||
{ name: OperationType.DRAW, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
|
||||
{ name: OperationType.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
|
||||
{ name: OperationType.IMAGE, icon: 'dc-image', iconSize: 17, disabled: ref(false) },
|
||||
{
|
||||
name: OperationType.IMAGE,
|
||||
icon: 'dc-image',
|
||||
iconSize: 17,
|
||||
disabled: ref(false),
|
||||
on: () => importLocalImage()
|
||||
},
|
||||
{ name: OperationType.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
|
||||
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
||||
{ type: 'line' },
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
||||
<div class="thumb"></div>
|
||||
<div class="name">
|
||||
<div @dblclick="onClickEditName" v-if="!editName">
|
||||
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
|
||||
{{ layer.info.name || '未命名图层' }}
|
||||
</div>
|
||||
<input
|
||||
@@ -16,10 +16,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="icons">
|
||||
<span @click="onClickShowHide"
|
||||
<span @click.stop="onClickShowHide"
|
||||
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
||||
/></span>
|
||||
<span @click="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
|
||||
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
|
||||
<span><svg-icon name="dc-down_arrow" size="11" /></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +27,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, nextTick } from 'vue'
|
||||
import { OperationType } from '../../tools/layerHelper'
|
||||
|
||||
const layerManager = inject('layerManager') as any
|
||||
const toolManager = inject('toolManager') as any
|
||||
const editName = ref(false)
|
||||
const props = defineProps({
|
||||
layer: {
|
||||
@@ -58,6 +61,7 @@
|
||||
}
|
||||
const onClickLayer = () => {
|
||||
layerManager.setActiveID(props.layer.info.id)
|
||||
toolManager.setTool(OperationType.SELECT)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -109,13 +113,11 @@
|
||||
> .name {
|
||||
flex: 1;
|
||||
font-size: 1.4rem;
|
||||
|
||||
overflow: hidden;
|
||||
> div {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<span class="title">Layer</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="icon"><svg-icon name="add" size="14" /></span>
|
||||
<span class="icon" @click="addLayer"><svg-icon name="add" size="14" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -60,9 +60,11 @@
|
||||
const handleDragEnd = (event) => {
|
||||
draging.value = false
|
||||
const { from, to, oldIndex, newIndex, data } = event
|
||||
console.log('oldIndex', data)
|
||||
layerManager.dragSort(data.info.id, newIndex)
|
||||
}
|
||||
const addLayer = () => {
|
||||
layerManager.createEmptyLayer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="depth-input">
|
||||
<span class="decorate"></span>
|
||||
<span v-show="icon" class="icon">
|
||||
<svg-icon :name="icon" :size="iconSize" size-unit="px" />
|
||||
</span>
|
||||
<span v-show="before" class="before">{{ before }}</span>
|
||||
<input v-bind="attrs" :value="modelValue" @input="onInput" @copy.stop @keydown.stop />
|
||||
<span v-show="after" class="after">{{ after }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
modelValue: { type: [String, Number] },
|
||||
icon: { default: '', type: String },
|
||||
iconSize: { default: '10', type: [Number, String] },
|
||||
before: { default: '', type: String },
|
||||
after: { default: '', type: String }
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
const emit = defineEmits(['update:modelValue', 'input'])
|
||||
const onInput = (e) => {
|
||||
var value = e.target.value
|
||||
if (attrs.type === 'number') value = Number(value)
|
||||
emit('update:modelValue', value)
|
||||
emit('input', value)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.depth-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(230, 230, 231, 1);
|
||||
border-radius: 1.7px;
|
||||
height: 17px;
|
||||
padding: 0 4px 0 2px;
|
||||
> .decorate {
|
||||
width: 2px;
|
||||
background-color: rgba(230, 230, 231, 1);
|
||||
border-radius: 3px;
|
||||
height: 85%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
> .iconfont {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> .before {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> .after {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-left: 1px;
|
||||
}
|
||||
> input {
|
||||
font-size: 12px;
|
||||
width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="depth-offset-tool">
|
||||
<div class="input" v-show="showInput">
|
||||
<depth-input v-model="left" type="number" before="X" after="%" :min="-100" :max="100" />
|
||||
<depth-input v-model="top" type="number" before="Y" after="%" :min="-100" :max="100" />
|
||||
</div>
|
||||
<div
|
||||
class="dish"
|
||||
@mousedown="mousedown"
|
||||
@touchstart="mousedown"
|
||||
ref="dishRef"
|
||||
v-show="showDish"
|
||||
>
|
||||
<img src="/src/assets/images/icon/xyz.png" />
|
||||
<span class="ball" :style="ballStyle"></span>
|
||||
<span class="tip x">X: {{ left }}%</span>
|
||||
<span class="tip y">Y: {{ top }}%</span>
|
||||
<span class="line x"></span>
|
||||
<span class="line y"></span>
|
||||
<span class="line z" :style="lineZStyle"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import depthInput from './depth-input.vue'
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object as () => { x: number; y: number } },
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showDish: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'input'])
|
||||
// 工具的实际坐标 -100 ~ 100
|
||||
const top = ref(Math.round(props.modelValue.y))
|
||||
const left = ref(Math.round(props.modelValue.x))
|
||||
|
||||
// 原点的坐标 0 ~ 100
|
||||
const ballStyle = computed(() => ({
|
||||
top: 50 + top.value / 2 + '%',
|
||||
left: 50 + left.value / 2 + '%'
|
||||
}))
|
||||
watch(
|
||||
() => props.modelValue.x,
|
||||
(v) => (left.value = v)
|
||||
)
|
||||
watch(
|
||||
() => props.modelValue.y,
|
||||
(v) => (top.value = v)
|
||||
)
|
||||
const dishRef = ref<HTMLDivElement>()
|
||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return
|
||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return
|
||||
const rect = dishRef.value.getBoundingClientRect()
|
||||
const X = e.clientX || (e as TouchEvent).touches[0].clientX
|
||||
const Y = e.clientY || (e as TouchEvent).touches[0].clientY
|
||||
var x = ((X - rect.left) / rect.width) * 100
|
||||
var y = ((Y - rect.top) / rect.height) * 100
|
||||
if (x < 0) x = 0
|
||||
if (x > 100) x = 100
|
||||
if (y < 0) y = 0
|
||||
if (y > 100) y = 100
|
||||
left.value = Math.round((x - 50) * 2)
|
||||
top.value = Math.round((y - 50) * 2)
|
||||
onInput()
|
||||
}
|
||||
mousemove(e)
|
||||
const mouseup = () => {
|
||||
onChange()
|
||||
document.removeEventListener('mousemove', mousemove)
|
||||
document.removeEventListener('touchmove', mousemove)
|
||||
document.removeEventListener('mouseup', mouseup)
|
||||
document.removeEventListener('touchend', mouseup)
|
||||
}
|
||||
document.addEventListener('mousemove', mousemove)
|
||||
document.addEventListener('touchmove', mousemove)
|
||||
document.addEventListener('mouseup', mouseup)
|
||||
document.addEventListener('touchend', mouseup)
|
||||
}
|
||||
const onInput = () => {
|
||||
const value = {
|
||||
x: left.value,
|
||||
y: top.value
|
||||
}
|
||||
emit('update:modelValue', value)
|
||||
emit('input', value)
|
||||
}
|
||||
var changeTime: any = null
|
||||
const onChange = () => {
|
||||
clearTimeout(changeTime)
|
||||
changeTime = setTimeout(() => {
|
||||
const value = {
|
||||
x: left.value,
|
||||
y: top.value
|
||||
}
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}, 500)
|
||||
}
|
||||
const lineZStyle = computed(() => ({
|
||||
'--rotateZ': calculateAngle(0, 0, left.value, top.value) + 'deg',
|
||||
width: calculateDistance(0, 0, left.value, top.value) / 2 + '%'
|
||||
}))
|
||||
// 计算角度
|
||||
function calculateAngle(x1: number, y1: number, x2: number, y2: number) {
|
||||
const deltaX = x2 - x1
|
||||
const deltaY = y1 - y2
|
||||
let angle = Math.atan2(deltaX, deltaY) * (180 / Math.PI) - 90
|
||||
return angle
|
||||
}
|
||||
// 计算距离
|
||||
function calculateDistance(x1: number, y1: number, x2: number, y2: number) {
|
||||
const deltaX = x2 - x1
|
||||
const deltaY = y2 - y1
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||
return distance
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.depth-offset-tool {
|
||||
position: relative;
|
||||
> .input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> * {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .dish {
|
||||
width: 115px;
|
||||
height: 115px;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 3.4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: #f6f6f6;
|
||||
margin-top: 20px;
|
||||
> * {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
bottom: 3.5px;
|
||||
right: 3.5px;
|
||||
}
|
||||
> .ball {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8.5px;
|
||||
height: 8.5px;
|
||||
border: 1px solid #fff;
|
||||
background-color: #333;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0.68px 0.17px 0px rgba(0, 0, 0, 0.26);
|
||||
}
|
||||
> .tip {
|
||||
font-size: 8.5px;
|
||||
color: #000;
|
||||
line-height: 24px;
|
||||
&.x {
|
||||
top: 50%;
|
||||
right: 0%;
|
||||
transform: translate(100%, -50%);
|
||||
padding-left: 6px;
|
||||
}
|
||||
&.y {
|
||||
top: 0%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
}
|
||||
> .line {
|
||||
border-color: #d9d9d9;
|
||||
border-style: dashed;
|
||||
border-width: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
&.x {
|
||||
width: 100%;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
&.y {
|
||||
height: 100%;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
&.z {
|
||||
width: 50%;
|
||||
border-top-width: 1px;
|
||||
border-color: #454754;
|
||||
transform: translate(0%, -50%) rotateZ(var(--rotateZ));
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="depth-select">
|
||||
<el-select :model-value="modelValue" @change="onChange" v-bind="attrs">
|
||||
<el-option v-for="v in list" :key="v.value" :label="v.label" :value="v.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
modelValue: { required: true },
|
||||
list: { default: () => [], type: [Array, Object] }
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
const onChange = (value) => {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.depth-select {
|
||||
&:deep(.el-select) {
|
||||
--el-select-input-font-size: 12px;
|
||||
.el-select__wrapper {
|
||||
font-size: 12px;
|
||||
min-height: 0;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.el-select__selected-item,
|
||||
.el-select__input-wrapper,
|
||||
.el-select__placeholder {
|
||||
line-height: normal;
|
||||
}
|
||||
.el-select__input {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-popper {
|
||||
.el-select-dropdown {
|
||||
li {
|
||||
padding-left: 8px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="depth-slider" :disabled="disabled">
|
||||
<div
|
||||
class="input-range"
|
||||
:style="{
|
||||
'--progress': (value - props.min) / (props.max - props.min)
|
||||
}"
|
||||
>
|
||||
<span class="tip">{{ props.tipFormatter(value) }}</span>
|
||||
<input
|
||||
type="range"
|
||||
v-model="value"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
/>
|
||||
</div>
|
||||
<div class="input" v-show="isInput">
|
||||
<depth-input
|
||||
type="number"
|
||||
v-model="value"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from 'vue'
|
||||
import depthInput from './depth-input.vue'
|
||||
const props = defineProps({
|
||||
modelValue: { type: Number },
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
tipFormatter: {
|
||||
type: Function,
|
||||
default: (v) => v
|
||||
},
|
||||
isInput: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'input'])
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
value.value = v
|
||||
}
|
||||
)
|
||||
const value = ref(props.modelValue)
|
||||
const onInput = () => {
|
||||
if (props.disabled) return
|
||||
const v = Number(value.value)
|
||||
emit('update:modelValue', v)
|
||||
emit('input', v)
|
||||
}
|
||||
const onChange = () => {
|
||||
if (props.disabled) return
|
||||
const v = Number(value.value)
|
||||
emit('update:modelValue', v)
|
||||
emit('change', v)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.depth-slider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--input-thumb-size: 1.4rem;
|
||||
--backcolor1: var(--depth-slider-thumb-color1, #000);
|
||||
--backcolor2: var(--depth-slider-thumb-color2, #d3d3d3);
|
||||
&:hover {
|
||||
> .input-range > .tip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
> .input-range {
|
||||
position: relative;
|
||||
flex: 2;
|
||||
display: flex;
|
||||
> input {
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 0.47rem;
|
||||
border-radius: 0.47rem;
|
||||
outline: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--backcolor1) 0%,
|
||||
var(--backcolor1) calc(var(--progress) * 100%),
|
||||
var(--backcolor2) calc(var(--progress) * 100%),
|
||||
var(--backcolor2) 100%
|
||||
);
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: var(--input-thumb-size);
|
||||
height: var(--input-thumb-size);
|
||||
border-radius: 50%;
|
||||
background: var(--backcolor1); /* 蓝色滑块 */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #666;
|
||||
top: calc(var(--input-thumb-size) / -2 - 0.5rem);
|
||||
left: calc(
|
||||
(100% - var(--input-thumb-size)) * var(--progress) + var(--input-thumb-size) / 2
|
||||
);
|
||||
transform: translate(-50%, -100%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1.4rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 97%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .input {
|
||||
flex: 1;
|
||||
margin-left: 0.6rem;
|
||||
> input {
|
||||
border-radius: 0.3rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,35 +3,40 @@
|
||||
<div class="canvas-container" ref="canvasContainerRef">
|
||||
<canvas ref="canvasRef"></canvas>
|
||||
</div>
|
||||
<layer-panel />
|
||||
<details-panel />
|
||||
<header-tools @export="exportCanvas" />
|
||||
<zoom
|
||||
:zoom="canvasManager.currentZoom.value / 100"
|
||||
:step="0.1"
|
||||
is-home
|
||||
@home="() => canvasManager.resetZoom()"
|
||||
/>
|
||||
<template v-if="isReady">
|
||||
<layer-panel />
|
||||
<details-panel />
|
||||
<depth-header-tools @export="exportCanvas" @import="importCanvas" />
|
||||
<brush-control-panel :currentTool="toolManager.currentTool.value" />
|
||||
<zoom
|
||||
:zoom="canvasManager.currentZoom.value / 100"
|
||||
:step="0.1"
|
||||
is-home
|
||||
@home="() => canvasManager.resetZoom()"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fabric } from 'fabric-with-all'
|
||||
import { computed, ref, markRaw, onMounted, nextTick, provide, onBeforeMount } from 'vue'
|
||||
import { NODE_TYPE, NODE_COMPONENT } from './tools/index.d'
|
||||
import { OperationType } from './tools/layerHelper'
|
||||
// 组件
|
||||
import layerPanel from './components/layer-panel/index.vue'
|
||||
import detailsPanel from './components/details-panel/index.vue'
|
||||
import headerTools from './components/header-tools.vue'
|
||||
import depthHeaderTools from './components/depth-header-tools.vue'
|
||||
import zoom from '../components/zoom.vue'
|
||||
import brushControlPanel from './components/brush-control-panel.vue'
|
||||
|
||||
// 管理器
|
||||
import { StateManager } from './manager/StateManager'
|
||||
import { LayerManager } from './manager/LayerManager'
|
||||
import { EventManager } from './manager/EventManager'
|
||||
import { CanvasManager } from './manager/CanvasManager'
|
||||
import { ToolManager } from './manager/ToolManager'
|
||||
|
||||
// 准备就绪
|
||||
const isReady = ref(false)
|
||||
const canvasContainerRef = ref(null)
|
||||
const canvasRef = ref(null)
|
||||
|
||||
@@ -56,11 +61,6 @@
|
||||
stateManager.setManager({ layerManager })
|
||||
provide('layerManager', layerManager)
|
||||
|
||||
// 事件管理器
|
||||
const eventManager = new EventManager({ stateManager })
|
||||
stateManager.setManager({ eventManager })
|
||||
provide('eventManager', eventManager)
|
||||
|
||||
// 工具管理器
|
||||
const toolManager = new ToolManager({ stateManager, canvasManager })
|
||||
stateManager.setManager({ toolManager })
|
||||
@@ -75,6 +75,10 @@
|
||||
canvasWidth: 750,
|
||||
canvasHeight: 600
|
||||
})
|
||||
stateManager?.onMounted?.()
|
||||
canvasManager?.onMounted?.()
|
||||
layerManager?.onMounted?.()
|
||||
toolManager?.onMounted?.()
|
||||
|
||||
const trailingTimeout = ref(null)
|
||||
observer.value = new ResizeObserver((entries) => {
|
||||
@@ -84,10 +88,10 @@
|
||||
}, 100)
|
||||
})
|
||||
observer.value.observe(canvasContainerRef.value)
|
||||
|
||||
isReady.value = true // 准备就绪
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
// eventManager.removeEvents() // 移除事件
|
||||
})
|
||||
onBeforeMount(() => {})
|
||||
async function handleWindowResize() {
|
||||
console.log('==========画布窗口大小变化==========')
|
||||
canvasManager.setCanvasViewSize({
|
||||
@@ -96,8 +100,42 @@
|
||||
})
|
||||
canvasManager.resetZoom()
|
||||
}
|
||||
/** 导入本地图片 */
|
||||
const importLocalImage = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.click()
|
||||
input.addEventListener('change', (e: any) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
toolManager.setTool(OperationType.SELECT)
|
||||
const url = reader.result as string
|
||||
layerManager.createImageLayer(url, {
|
||||
info: { name: file.name }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
provide('importLocalImage', importLocalImage)
|
||||
|
||||
const exportCanvas = () => {
|
||||
console.log(canvasManager.getBitObjects())
|
||||
const json = canvasManager.getCanvasJSON()
|
||||
localStorage.setItem('canvasJSON', json)
|
||||
}
|
||||
const importCanvas = () => {
|
||||
const json = localStorage.getItem('canvasJSON')
|
||||
if (!json) return
|
||||
canvasManager.loadJSON(json, (success) => {
|
||||
if (success) {
|
||||
console.log('导入成功')
|
||||
} else {
|
||||
console.log('导入失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
|
||||
@@ -115,7 +115,7 @@ export class AnimationManager {
|
||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||
vpt[4] = currentVpt[4] * scale + adjustX;
|
||||
vpt[5] = currentVpt[5] * scale + adjustY;
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
@@ -167,7 +167,7 @@ export class AnimationManager {
|
||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||
vpt[4] = currentVpt[4] * scale + adjustX;
|
||||
vpt[5] = currentVpt[5] * scale + adjustY;
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
@@ -252,7 +252,7 @@ export class AnimationManager {
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[4] = -x;
|
||||
vpt[5] = -y;
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ export class AnimationManager {
|
||||
vpt[3] = viewTransform.zoom;
|
||||
vpt[4] = viewTransform.panX;
|
||||
vpt[5] = viewTransform.panY;
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
@@ -814,6 +814,7 @@ export class AnimationManager {
|
||||
|
||||
// 更新缩放值显示
|
||||
this.currentZoom.value = Math.round(transform.zoom * 100);
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
|
||||
520
src/components/Canvas/DepthCanvas/manager/BrushIndicator.js
Normal file
520
src/components/Canvas/DepthCanvas/manager/BrushIndicator.js
Normal file
@@ -0,0 +1,520 @@
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { OperationType } from "../tools/layerHelper";
|
||||
|
||||
/**
|
||||
* 笔刷指示器
|
||||
* 在画笔模式下显示当前笔刷大小的圆圈指示器
|
||||
* 使用独立的 fabric.StaticCanvas,完全不干扰主画布的绘制流程
|
||||
*/
|
||||
export class BrushIndicator {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric.js画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.options = {
|
||||
strokeColor: options.strokeColor || "rgba(0, 0, 0, 0.5)",
|
||||
strokeWidth: options.strokeWidth || 1,
|
||||
fillColor: options.fillColor || "rgba(0, 0, 0, 0.1)",
|
||||
...options,
|
||||
};
|
||||
|
||||
// 创建独立的静态画布
|
||||
this.indicatorCanvas = null;
|
||||
this.staticCanvas = null;
|
||||
this.indicatorCircle = null;
|
||||
|
||||
// 事件处理器
|
||||
this._mouseEnterHandler = null;
|
||||
this._mouseLeaveHandler = null;
|
||||
this._mouseMoveHandler = null;
|
||||
this._zoomHandler = null;
|
||||
this._viewportHandler = null;
|
||||
|
||||
// 当前状态
|
||||
this.isVisible = false;
|
||||
this.currentSize = 10;
|
||||
this.isEnabled = false;
|
||||
this.currentPosition = { x: 0, y: 0 };
|
||||
|
||||
// 缓存画布状态,用于优化性能
|
||||
this._lastCanvasState = {
|
||||
width: null,
|
||||
height: null,
|
||||
zoom: null,
|
||||
viewportTransform: null,
|
||||
};
|
||||
this._createStaticCanvas();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建独立的 fabric.StaticCanvas 画布层
|
||||
* @private
|
||||
*/
|
||||
_createStaticCanvas() {
|
||||
if (!this.canvas.wrapperEl) return;
|
||||
|
||||
// 创建独立的 canvas 元素
|
||||
this.indicatorCanvas = document.createElement("canvas");
|
||||
this.indicatorCanvas.className = "brush-indicator-canvas";
|
||||
|
||||
// 设置样式,使其覆盖在主画布上
|
||||
Object.assign(this.indicatorCanvas.style, {
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
pointerEvents: "none", // 不阻挡鼠标事件
|
||||
zIndex: "10", // 确保在最上层
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
// 将指示器画布添加到 fabric.js 画布容器中
|
||||
this.canvas.wrapperEl.appendChild(this.indicatorCanvas);
|
||||
|
||||
// 创建 fabric.StaticCanvas 实例
|
||||
this.staticCanvas = new fabric.StaticCanvas(this.indicatorCanvas, {
|
||||
enableRetinaScaling: this.canvas.enableRetinaScaling,
|
||||
allowTouchScrolling: false,
|
||||
selection: false,
|
||||
skipTargetFind: true,
|
||||
preserveObjectStacking: true,
|
||||
});
|
||||
|
||||
// 同步画布尺寸和变换
|
||||
this._syncCanvasProperties();
|
||||
|
||||
// 监听主画布变化
|
||||
this._observeCanvasChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步画布属性(尺寸、缩放、视口变换等)
|
||||
* @private
|
||||
*/
|
||||
_syncCanvasProperties() {
|
||||
if (!this.staticCanvas || !this.canvas) return;
|
||||
|
||||
// 检查是否为笔刷或橡皮擦模式,非相关模式直接返回
|
||||
const isBrushMode =
|
||||
this.canvas.isDrawingMode && this.canvas.freeDrawingBrush;
|
||||
const isEraserMode =
|
||||
this.canvas.isDrawingMode &&
|
||||
this.canvas.freeDrawingBrush &&
|
||||
this.canvas.freeDrawingBrush.type === "eraser";
|
||||
|
||||
const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式
|
||||
if ([isBrushMode, isEraserMode, isLiquifyMode].every(v => !v)) return;
|
||||
|
||||
let hasChanges = false;
|
||||
|
||||
// 检查画布尺寸是否变化
|
||||
const currentWidth = this.canvas.width;
|
||||
const currentHeight = this.canvas.height;
|
||||
if (
|
||||
currentWidth !== this._lastCanvasState.width ||
|
||||
currentHeight !== this._lastCanvasState.height
|
||||
) {
|
||||
this.staticCanvas.setWidth(currentWidth);
|
||||
this.staticCanvas.setHeight(currentHeight);
|
||||
this._lastCanvasState.width = currentWidth;
|
||||
this._lastCanvasState.height = currentHeight;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// 检查缩放比例是否变化
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
if (Math.abs(currentZoom - (this._lastCanvasState.zoom || 0)) > 0.001) {
|
||||
this.staticCanvas.setZoom(currentZoom);
|
||||
this._lastCanvasState.zoom = currentZoom;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// 检查视口变换是否变化
|
||||
const currentVpt = this.canvas.viewportTransform;
|
||||
if (
|
||||
currentVpt &&
|
||||
!this._areViewportTransformsEqual(
|
||||
currentVpt,
|
||||
this._lastCanvasState.viewportTransform
|
||||
)
|
||||
) {
|
||||
this.staticCanvas.setViewportTransform([...currentVpt]);
|
||||
this._lastCanvasState.viewportTransform = [...currentVpt];
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// 只有在有变化时才重新渲染
|
||||
if (hasChanges) {
|
||||
this.staticCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个视口变换矩阵是否相等
|
||||
* @private
|
||||
* @param {Array} vpt1 视口变换矩阵1
|
||||
* @param {Array} vpt2 视口变换矩阵2
|
||||
* @returns {Boolean} 是否相等
|
||||
*/
|
||||
_areViewportTransformsEqual(vpt1, vpt2) {
|
||||
if (!vpt1 || !vpt2) return false;
|
||||
if (vpt1.length !== vpt2.length) return false;
|
||||
|
||||
const tolerance = 0.001;
|
||||
for (let i = 0; i < vpt1.length; i++) {
|
||||
if (Math.abs(vpt1[i] - vpt2[i]) > tolerance) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听主画布变化
|
||||
* @private
|
||||
*/
|
||||
_observeCanvasChanges() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 监听缩放变化
|
||||
this._zoomHandler = () => {
|
||||
this._syncCanvasProperties();
|
||||
};
|
||||
|
||||
// 监听视口变化
|
||||
this._viewportHandler = () => {
|
||||
this._syncCanvasProperties();
|
||||
};
|
||||
|
||||
// 监听画布尺寸变化
|
||||
this._resizeHandler = () => {
|
||||
this._syncCanvasProperties();
|
||||
};
|
||||
|
||||
// 绑定事件
|
||||
this.canvas.on("after:render", this._zoomHandler);
|
||||
this.canvas.on("canvas:zoomed", this._zoomHandler);
|
||||
this.canvas.on("viewport:changed", this._viewportHandler);
|
||||
this.canvas.on("canvas:resized", this._resizeHandler);
|
||||
|
||||
// 使用 ResizeObserver 监听 DOM 尺寸变化
|
||||
if (window.ResizeObserver && this.canvas.upperCanvasEl) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this._syncCanvasProperties();
|
||||
});
|
||||
this.resizeObserver.observe(this.canvas.upperCanvasEl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用笔刷指示器
|
||||
* @param {Number} brushSize 笔刷大小
|
||||
*/
|
||||
enable(brushSize) {
|
||||
if (this.isEnabled) return;
|
||||
|
||||
this.isEnabled = true;
|
||||
this.currentSize = brushSize;
|
||||
|
||||
// 绑定事件
|
||||
this._bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用笔刷指示器
|
||||
*/
|
||||
disable() {
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
this.isEnabled = false;
|
||||
|
||||
// 隐藏指示器
|
||||
this.hide();
|
||||
|
||||
// 重置颜色配置为默认值
|
||||
this.options.strokeColor = "";
|
||||
this.options.fillColor = "";
|
||||
|
||||
// 解绑事件
|
||||
this._unbindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷大小
|
||||
* @param {Number} size 新的笔刷大小
|
||||
*/
|
||||
updateSize(size) {
|
||||
this.currentSize = size;
|
||||
|
||||
// 如果指示器正在显示,更新其大小
|
||||
if (this.isVisible && this.indicatorCircle) {
|
||||
this.indicatorCircle.set({
|
||||
radius: size / 2,
|
||||
});
|
||||
this.staticCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指示器颜色
|
||||
* @param {String} color 新的颜色值
|
||||
*/
|
||||
updateColor(color) {
|
||||
// 更新配置选项中的颜色
|
||||
if (color) {
|
||||
this.options.strokeColor = color;
|
||||
}
|
||||
|
||||
// 如果指示器正在显示,更新其颜色
|
||||
if (this.isVisible && this.indicatorCircle) {
|
||||
this.indicatorCircle.set({
|
||||
stroke: this.options.strokeColor,
|
||||
fill: this.options.fillColor,
|
||||
});
|
||||
this.staticCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指示器
|
||||
* @param {Object} pointer 鼠标位置
|
||||
*/
|
||||
show(pointer) {
|
||||
if (!this.isEnabled || this.isVisible) return;
|
||||
|
||||
this.isVisible = true;
|
||||
|
||||
// 创建指示器圆圈
|
||||
this._createIndicatorCircle();
|
||||
|
||||
// 更新位置
|
||||
this.updatePosition(pointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏指示器
|
||||
*/
|
||||
hide() {
|
||||
if (!this.isVisible) return;
|
||||
|
||||
this.isVisible = false;
|
||||
|
||||
// 移除指示器圆圈
|
||||
if (this.indicatorCircle && this.staticCanvas) {
|
||||
this.staticCanvas.remove(this.indicatorCircle);
|
||||
this.indicatorCircle = null;
|
||||
this.staticCanvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指示器位置
|
||||
* @param {Object} pointer 鼠标位置
|
||||
*/
|
||||
updatePosition(pointer) {
|
||||
if (!this.isVisible || !this.indicatorCircle) return;
|
||||
|
||||
let canvasPointer;
|
||||
// 如果是原生事件(如 e.e),优先用 clientX/clientY 转换
|
||||
if (
|
||||
pointer &&
|
||||
pointer.clientX !== undefined &&
|
||||
pointer.clientY !== undefined
|
||||
) {
|
||||
// 获取主画布的包裹元素位置
|
||||
const rect = this.canvas.upperCanvasEl.getBoundingClientRect();
|
||||
const x = pointer.clientX - rect.left;
|
||||
const y = pointer.clientY - rect.top;
|
||||
// 逆变换到画布坐标
|
||||
canvasPointer = fabric.util.transformPoint(
|
||||
new fabric.Point(x, y),
|
||||
fabric.util.invertTransform(this.canvas.viewportTransform)
|
||||
);
|
||||
} else {
|
||||
// 兼容 fabric 的 getPointer
|
||||
canvasPointer = this.canvas.getPointer(pointer);
|
||||
}
|
||||
|
||||
// 更新当前位置
|
||||
this.currentPosition = {
|
||||
x: canvasPointer.x,
|
||||
y: canvasPointer.y,
|
||||
};
|
||||
|
||||
// 更新指示器位置
|
||||
this.indicatorCircle.set({
|
||||
left: canvasPointer.x,
|
||||
top: canvasPointer.y,
|
||||
});
|
||||
|
||||
// 优化渲染
|
||||
this.staticCanvas.requestRenderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指示器圆圈对象
|
||||
* @private
|
||||
*/
|
||||
_createIndicatorCircle() {
|
||||
if (this.indicatorCircle || !this.staticCanvas) return;
|
||||
|
||||
// 创建圆圈对象
|
||||
this.indicatorCircle = new fabric.Circle({
|
||||
radius: this.currentSize / 2,
|
||||
fill: this.options.fillColor,
|
||||
stroke: this.options.strokeColor,
|
||||
strokeWidth: this.options.strokeWidth,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
selectable: false,
|
||||
evented: false,
|
||||
excludeFromExport: true,
|
||||
isTemp: true,
|
||||
pointer: true,
|
||||
});
|
||||
|
||||
// 添加到静态画布
|
||||
this.staticCanvas.add(this.indicatorCircle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件处理器
|
||||
* @private
|
||||
*/
|
||||
_bindEvents() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 鼠标进入画布
|
||||
this._mouseEnterHandler = (e) => {
|
||||
if (this._shouldShowIndicator()) {
|
||||
this.show(e.e);
|
||||
const currentVpt = this.canvas.viewportTransform;
|
||||
this.staticCanvas.setViewportTransform([...currentVpt]);
|
||||
this.staticCanvas.renderAll();
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标离开画布
|
||||
this._mouseLeaveHandler = () => {
|
||||
this.hide();
|
||||
};
|
||||
|
||||
// 鼠标移动
|
||||
this._mouseMoveHandler = (e) => {
|
||||
if (this._shouldShowIndicator()) {
|
||||
if (!this.isVisible) {
|
||||
// this.show(e.e);
|
||||
this._mouseEnterHandler && this._mouseEnterHandler(e)
|
||||
} else {
|
||||
// requestIdleCallback(() => {
|
||||
// this.updatePosition(e.e);
|
||||
// });
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.updatePosition(e.e);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定事件
|
||||
this.canvas.on("mouse:over", this._mouseEnterHandler);
|
||||
this.canvas.on("mouse:out", this._mouseLeaveHandler);
|
||||
this.canvas.on("mouse:move", this._mouseMoveHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑事件处理器
|
||||
* @private
|
||||
*/
|
||||
_unbindEvents() {
|
||||
if (!this.canvas) return;
|
||||
// 解绑鼠标事件
|
||||
if (this._mouseEnterHandler) {
|
||||
this.canvas.off("mouse:over", this._mouseEnterHandler);
|
||||
this._mouseEnterHandler = null;
|
||||
}
|
||||
|
||||
if (this._mouseLeaveHandler) {
|
||||
this.canvas.off("mouse:out", this._mouseLeaveHandler);
|
||||
this._mouseLeaveHandler = null;
|
||||
}
|
||||
|
||||
if (this._mouseMoveHandler) {
|
||||
this.canvas.off("mouse:move", this._mouseMoveHandler);
|
||||
this._mouseMoveHandler = null;
|
||||
}
|
||||
|
||||
// 解绑画布变化事件
|
||||
if (this._zoomHandler) {
|
||||
this.canvas.off("after:render", this._zoomHandler);
|
||||
this.canvas.off("canvas:zoomed", this._zoomHandler);
|
||||
this._zoomHandler = null;
|
||||
}
|
||||
|
||||
if (this._viewportHandler) {
|
||||
this.canvas.off("viewport:changed", this._viewportHandler);
|
||||
this._viewportHandler = null;
|
||||
}
|
||||
|
||||
if (this._resizeHandler) {
|
||||
this.canvas.off("canvas:resized", this._resizeHandler);
|
||||
this._resizeHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该显示指示器
|
||||
* @private
|
||||
* @returns {Boolean} 是否显示
|
||||
*/
|
||||
_shouldShowIndicator() {
|
||||
const isDrawingMode = this.canvas.isDrawingMode;// 检查画布是否在绘图模式
|
||||
const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式
|
||||
// console.log(`笔刷指示器\n绘图模式:${isDrawingMode}\n液化模式:${isLiquifyMode}`)
|
||||
// 检查画布是否在绘图模式OR液化模式
|
||||
if ([isDrawingMode, isLiquifyMode].every(v => !v)) return false;
|
||||
|
||||
// 检查是否有笔刷
|
||||
if (!this.canvas.freeDrawingBrush) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁指示器
|
||||
*/
|
||||
dispose() {
|
||||
this.disable();
|
||||
|
||||
// 解绑画布变化事件
|
||||
this._unbindEvents();
|
||||
|
||||
// 停止监听尺寸变化
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
|
||||
// 销毁静态画布
|
||||
if (this.staticCanvas) {
|
||||
this.staticCanvas.dispose();
|
||||
this.staticCanvas = null;
|
||||
}
|
||||
|
||||
// 移除指示器画布
|
||||
if (this.indicatorCanvas && this.indicatorCanvas.parentNode) {
|
||||
this.indicatorCanvas.parentNode.removeChild(this.indicatorCanvas);
|
||||
}
|
||||
|
||||
this.canvas = null;
|
||||
this.indicatorCanvas = null;
|
||||
this.indicatorCircle = null;
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
@@ -58,55 +58,20 @@ export class CanvasManager {
|
||||
const canvasX = this.canvasViewWidth / 2 - this.canvasWidth / 2
|
||||
const canvasY = this.canvasViewHeight / 2 - this.canvasHeight / 2
|
||||
this.canvas.viewportTransform = [1, 0, 0, 1, canvasX, canvasY]
|
||||
|
||||
// 创建矩形
|
||||
const rect = new fabric.Rect({
|
||||
left: 20,
|
||||
top: 20,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#f00',
|
||||
info: {
|
||||
id: 'rect1',
|
||||
name: 'rect1',
|
||||
}
|
||||
const rect = this.layerManager.createRectLayer({
|
||||
left: 400,
|
||||
top: 100,
|
||||
})
|
||||
this.canvas.add(rect)
|
||||
//创建圆形
|
||||
const circle = new fabric.Circle({
|
||||
const circle = this.layerManager.createCircleLayer({
|
||||
left: 200,
|
||||
top: 200,
|
||||
radius: 50,
|
||||
fill: '#0f0',
|
||||
info: {
|
||||
id: 'circle',
|
||||
name: 'circle',
|
||||
}
|
||||
})
|
||||
this.canvas.add(circle)
|
||||
// 文字
|
||||
const text = new fabric.Text('Hello World', {
|
||||
left: 300,
|
||||
top: 300,
|
||||
fontSize: 24,
|
||||
fill: '#000',
|
||||
info: {
|
||||
id: 'text1',
|
||||
name: 'text',
|
||||
}
|
||||
})
|
||||
this.canvas.add(text)
|
||||
// 文字
|
||||
const text2 = new fabric.Text('Hello World', {
|
||||
left: 300,
|
||||
top: 300,
|
||||
fontSize: 24,
|
||||
fill: '#000',
|
||||
info: {
|
||||
id: 'text2',
|
||||
name: 'tex2t',
|
||||
}
|
||||
})
|
||||
this.canvas.add(text2)
|
||||
const text = this.layerManager.createTextLayer('Hello World');
|
||||
|
||||
this.animationManager = new AnimationManager(this.canvas, {
|
||||
currentZoom: this.currentZoom,
|
||||
canvasManager: this,
|
||||
@@ -117,18 +82,38 @@ export class CanvasManager {
|
||||
this.setupCanvasEvents()
|
||||
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
||||
this.layerManager.updateLayers()
|
||||
this.layerManager.setActiveID(text2.info.id)
|
||||
this.layerManager.setActiveID(text.info.id)
|
||||
|
||||
|
||||
this.setupBrushEvents()
|
||||
}
|
||||
/** 画布添加对象 */
|
||||
add(obj: any, isUpdate = true) {
|
||||
this.canvas.add(obj)
|
||||
if (isUpdate) {
|
||||
this.layerManager.updateLayers()
|
||||
this.renderAll()
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置画布事件 */
|
||||
setupCanvasEvents() {
|
||||
// 创建画布事件管理器
|
||||
this.eventManager = new CanvasEventManager(this.canvas, {
|
||||
canvasManager: this,
|
||||
animationManager: this.animationManager,
|
||||
toolManager: this.stateManager.toolManager,
|
||||
layerManager: this.stateManager.layerManager,
|
||||
});
|
||||
// 设置动画交互效果
|
||||
this.animationManager.setupInteractionAnimations();
|
||||
}
|
||||
/** 设置激活对象 */
|
||||
setActiveObjectByID(id: string) {
|
||||
const obj = this.getObjectById(id)
|
||||
if (obj) this.canvas.setActiveObject(obj)
|
||||
this.renderAll()
|
||||
}
|
||||
resetZoom() {
|
||||
this.animationManager.resetZoom()
|
||||
}
|
||||
@@ -163,5 +148,51 @@ export class CanvasManager {
|
||||
})
|
||||
}
|
||||
|
||||
/** 画笔事件 */
|
||||
setupBrushEvents() {
|
||||
this.canvas.onBrushImageConverted = async (fabricImage) => {
|
||||
const currentTool = this.stateManager.toolManager.currentTool.value;
|
||||
if (currentTool === OperationType.DRAW) {
|
||||
this.handleDrawImage(fabricImage)
|
||||
}
|
||||
return true
|
||||
};
|
||||
}
|
||||
/** 处理绘制图像 */
|
||||
handleDrawImage(fabricImage: fabric.Object) {
|
||||
const activeID = this.stateManager.layerManager.activeID.value
|
||||
const activeLayer = this.getObjectById(activeID)
|
||||
if (activeLayer) {
|
||||
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
|
||||
} else {
|
||||
const emptyLayer = this.layerManager.createEmptyLayer();
|
||||
this.layerManager.setActiveID(emptyLayer.info.id, false)
|
||||
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 导出画布为JSON */
|
||||
getCanvasJSON() {
|
||||
const keys = ["top", "left", "width", "height", "scaleX", "scaleY", "info",]
|
||||
const json = this.canvas.toJSON(keys)
|
||||
console.log(json, this.getObjects())
|
||||
return JSON.stringify(json)
|
||||
}
|
||||
/** 加载画布JSON */
|
||||
loadJSON(json: string, callback?: (success: boolean) => void) {
|
||||
let jsonObj = null;
|
||||
try {
|
||||
jsonObj = JSON.parse(json)
|
||||
} catch (error) {
|
||||
console.error('JSON解析错误:', error)
|
||||
}
|
||||
if (!jsonObj) return callback?.(false);
|
||||
this.canvas.loadFromJSON(jsonObj, () => {
|
||||
this.layerManager.updateLayers()
|
||||
this.renderAll()
|
||||
callback?.(true)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// import { EraserCommand } from "../commands/EraserCommand";
|
||||
class EraserCommand { }
|
||||
|
||||
/**
|
||||
* 橡皮擦状态管理器
|
||||
* 用于管理橡皮擦操作的状态快照
|
||||
*/
|
||||
export class EraserStateManager {
|
||||
constructor(canvas, layerManager) {
|
||||
this.canvas = canvas;
|
||||
this.layerManager = layerManager;
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
|
||||
setLayerManager(layerManager) {
|
||||
this.layerManager = layerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始橡皮擦操作 - 捕获初始状态
|
||||
*/
|
||||
startErasing() {
|
||||
console.log("橡皮擦操作开始 - 捕获状态快照");
|
||||
this.currentSnapshot = this._captureCanvasSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束橡皮擦操作 - 创建命令
|
||||
* @param {Array} affectedObjects 受影响的对象
|
||||
* @returns {EraserCommand|null} 创建的橡皮擦命令
|
||||
*/
|
||||
endErasing(affectedObjects = []) {
|
||||
if (!this.currentSnapshot) {
|
||||
console.warn("没有初始状态快照,无法创建橡皮擦命令");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!affectedObjects || affectedObjects.length === 0) {
|
||||
console.log("没有对象被擦除,不创建命令");
|
||||
this.currentSnapshot = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`橡皮擦操作结束 - 影响了 ${affectedObjects.length} 个对象`);
|
||||
|
||||
// 捕获擦除后的状态
|
||||
const afterSnapshot = this._captureCanvasSnapshot();
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const command = new EraserCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
affectedObjects: affectedObjects,
|
||||
beforeSnapshot: this.currentSnapshot,
|
||||
afterSnapshot: afterSnapshot,
|
||||
});
|
||||
|
||||
// 重置状态
|
||||
this.currentSnapshot = null;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获画布状态快照
|
||||
* @returns {Object} 画布状态快照
|
||||
* @private
|
||||
*/
|
||||
_captureCanvasSnapshot() {
|
||||
try {
|
||||
return this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("捕获画布状态快照失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { OperationType } from "../tools/layerHelper"
|
||||
export class EventManager {
|
||||
stateManager: any
|
||||
vueFlow: any
|
||||
zoom: any
|
||||
constructor(options) {
|
||||
this.stateManager = options.stateManager;
|
||||
this.vueFlow = options.vueFlow
|
||||
this.zoom = this.stateManager.zoom
|
||||
this.registerEvents()
|
||||
}
|
||||
/** 处理视口变化 */
|
||||
handleViewportChange(e: any) {
|
||||
const { zoom } = e
|
||||
this.zoom.value = zoom
|
||||
}
|
||||
/** 处理节点拖动停止 */
|
||||
handleNodeDragStop(e: any) {
|
||||
const { node } = e
|
||||
const { id, position } = node
|
||||
this.stateManager.nodes.value.forEach((item) => {
|
||||
if (item.id === id) {
|
||||
item.position.x = position.x
|
||||
item.position.y = position.y
|
||||
}
|
||||
})
|
||||
this.stateManager.recordState()
|
||||
}
|
||||
/** 处理点击 */
|
||||
handleClick(event: any) {
|
||||
this.stateManager.setActiveNodeID("")
|
||||
const tool = this.stateManager.tool.value
|
||||
if (tool === OperationType.TEXT) {
|
||||
const { x, y, zoom } = this.vueFlow.value.viewport
|
||||
const position = {
|
||||
x: (event.offsetX - x) / zoom,
|
||||
y: (event.offsetY - y) / zoom
|
||||
}
|
||||
this.stateManager.nodeManager.createTextNode({ position })
|
||||
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理复制 */
|
||||
handleCopy(event: any, activeNodeID: string) {
|
||||
event.preventDefault()
|
||||
if (!activeNodeID) return console.warn('没有选中节点')
|
||||
this.stateManager.nodeManager.copyNodeById(activeNodeID)
|
||||
}
|
||||
/** 处理删除 */
|
||||
handleDelete(event: any, activeNodeID: string) {
|
||||
event.preventDefault()
|
||||
if (!activeNodeID) return console.warn('没有选中节点')
|
||||
this.stateManager.deleteNode(activeNodeID, { isElMessageBox: true })
|
||||
}
|
||||
/** 处理键盘事件 */
|
||||
handleKeyDown(event: any) {
|
||||
const activeNodeID = this.stateManager.activeNodeID.value;
|
||||
// const shiftKey
|
||||
const ctrl = event.ctrlKey ? 'ctrl-' : "";
|
||||
const shift = event.shiftKey ? 'shift-' : "";
|
||||
const key = event.key;
|
||||
const reg = new RegExp(`^${ctrl}${shift}${key}$`, 'i')
|
||||
const list = [
|
||||
{ key: "ctrl-c", handler: () => this.handleCopy(event, activeNodeID) },
|
||||
{ key: "delete", handler: () => this.handleDelete(event, activeNodeID) },
|
||||
{ key: "ctrl-z", handler: () => this.stateManager.undoState() },
|
||||
{ key: "ctrl-shift-z", handler: () => this.stateManager.redoState() },
|
||||
]
|
||||
list.forEach((v: any) => {
|
||||
if (reg.test(v.key)) v.handler(event)
|
||||
})
|
||||
}
|
||||
/** 注册事件 */
|
||||
registerEvents() {
|
||||
// document.addEventListener('copy', this.handleCopy.bind(this))
|
||||
document.addEventListener('keydown', this.handleKeyDown.bind(this))
|
||||
}
|
||||
/** 删除事件 */
|
||||
removeEvents() {
|
||||
// document.removeEventListener('copy', this.handleCopy.bind(this))
|
||||
document.removeEventListener('keydown', this.handleKeyDown.bind(this))
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { fabric } from 'fabric-with-all'
|
||||
import { createId } from '../../tools/tools'
|
||||
import { exportObjectsToImage } from '../tools/exportMethod'
|
||||
|
||||
export class LayerManager {
|
||||
stateManager: any
|
||||
@@ -11,7 +14,12 @@ export class LayerManager {
|
||||
this.layers = ref([])
|
||||
this.activeID = ref("")
|
||||
}
|
||||
setActiveID(id: string) { this.activeID.value = id }
|
||||
setActiveID(id: string, isActive = true) {
|
||||
this.activeID.value = id
|
||||
if (isActive) {
|
||||
this.canvasManager.setActiveObjectByID(id)
|
||||
}
|
||||
}
|
||||
getLayerByID(id) {
|
||||
return this.layers.value.find((item: any) => item.info.id === id)
|
||||
}
|
||||
@@ -31,8 +39,11 @@ export class LayerManager {
|
||||
this.canvasManager.renderAll()
|
||||
}
|
||||
}
|
||||
deleteLayerByID(id) {
|
||||
deleteLayerByID(id, isActive = true) {
|
||||
this.canvasManager.deleteObjectById(id)
|
||||
if (id === this.activeID.value && isActive) {
|
||||
this.setActiveID(this.layers.value[0]?.info?.id || "")
|
||||
}
|
||||
}
|
||||
// 拖拽排序
|
||||
dragSort(id, newIndex) {
|
||||
@@ -41,6 +52,156 @@ export class LayerManager {
|
||||
}
|
||||
// 更新图层列表
|
||||
updateLayers() {
|
||||
this.layers.value = this.canvasManager.getObjects().reverse()
|
||||
this.layers.value = this.canvasManager.getObjects().filter((v: any) => !!v?.info?.id).reverse()
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置图层位置-不设置默认居中 */
|
||||
setLayerPosition(layer, options?: any) {
|
||||
const width = this.canvasManager.canvasWidth
|
||||
const height = this.canvasManager.canvasHeight
|
||||
|
||||
if (options && options.top !== undefined) {
|
||||
layer.set({ top: options.top })
|
||||
} else {
|
||||
layer.set({ top: height / 2 - layer.height * layer.scaleY / 2 })
|
||||
}
|
||||
if (options && options.left !== undefined) {
|
||||
layer.set({ left: options.left })
|
||||
} else {
|
||||
layer.set({ left: width / 2 - layer.width * layer.scaleX / 2 })
|
||||
}
|
||||
}
|
||||
/** 创建空图层 */
|
||||
createEmptyLayer() {
|
||||
const emptyLayer = new fabric.Rect({
|
||||
width: 0,
|
||||
height: 0,
|
||||
fill: 'transparent',
|
||||
info: {
|
||||
id: createId("image"),
|
||||
name: '空图层',
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(emptyLayer)
|
||||
this.canvasManager.add(emptyLayer)
|
||||
return emptyLayer
|
||||
}
|
||||
/** 创建文本图层 */
|
||||
createTextLayer(text: string, options?: any) {
|
||||
const textLayer = new fabric.IText(text, {
|
||||
fontSize: 24,
|
||||
fill: '#000',
|
||||
...(options || {}),
|
||||
info: {
|
||||
id: createId("text"),
|
||||
name: '文本图层',
|
||||
...(options?.info || {}),
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(textLayer, options)
|
||||
this.canvasManager.add(textLayer)
|
||||
return textLayer
|
||||
}
|
||||
/** 创建矩形图层 */
|
||||
createRectLayer(options?: any) {
|
||||
const rectLayer = new fabric.Rect({
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#000',
|
||||
...(options || {}),
|
||||
info: {
|
||||
id: createId("rect"),
|
||||
name: '矩形图层',
|
||||
...(options?.info || {}),
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(rectLayer, options)
|
||||
this.canvasManager.add(rectLayer)
|
||||
return rectLayer
|
||||
}
|
||||
/** 创建圆形图层 */
|
||||
createCircleLayer(options?: any) {
|
||||
const circleLayer = new fabric.Circle({
|
||||
radius: 50,
|
||||
fill: '#000',
|
||||
...(options || {}),
|
||||
info: {
|
||||
id: createId("circle"),
|
||||
name: '圆形图层',
|
||||
...(options?.info || {}),
|
||||
}
|
||||
})
|
||||
this.setLayerPosition(circleLayer, options)
|
||||
this.canvasManager.add(circleLayer)
|
||||
return circleLayer
|
||||
}
|
||||
/** 创建图片图层 */
|
||||
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any) {
|
||||
const canvasWidth = this.canvasManager.canvasWidth
|
||||
const canvasHeight = this.canvasManager.canvasHeight
|
||||
|
||||
const imageLayer = await new Promise((resolve) => {
|
||||
const url = typeof imgOrUrl === 'string' ? imgOrUrl : imgOrUrl.src
|
||||
fabric.Image.fromURL(url, (img) => {
|
||||
const width = img.width
|
||||
const height = img.height
|
||||
const scaleX = width > canvasWidth ? canvasWidth * 0.8 / width : 1
|
||||
const scaleY = height > canvasHeight ? canvasHeight * 0.8 / height : 1
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
img.set({
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
...(options || {}),
|
||||
info: {
|
||||
id: createId("image"),
|
||||
name: "图片图层",
|
||||
...(options?.info || {}),
|
||||
}
|
||||
})
|
||||
resolve(img)
|
||||
})
|
||||
}) as fabric.Object
|
||||
this.setLayerPosition(imageLayer, options)
|
||||
this.canvasManager.add(imageLayer)
|
||||
this.setActiveID(imageLayer.info.id)
|
||||
return imageLayer
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 合并图层 */
|
||||
async imageMergeToLayer(targetLayer: fabric.Object, fabricImage: fabric.Object) {
|
||||
const info = await exportObjectsToImage([targetLayer, fabricImage], true)
|
||||
const mergedImage = await new Promise((resolve) => {
|
||||
fabric.Image.fromURL(info.url, (img) => {
|
||||
img.set({
|
||||
left: info.left,
|
||||
top: info.top,
|
||||
info: {
|
||||
id: createId("image"),
|
||||
name: targetLayer?.info?.name || "合并图层",
|
||||
}
|
||||
})
|
||||
resolve(img)
|
||||
})
|
||||
})
|
||||
// console.log(mergedImage)
|
||||
const index = this.canvasManager.getObjects().indexOf(targetLayer);
|
||||
this.deleteLayerByID(targetLayer.info.id, false)
|
||||
this.setActiveID(mergedImage.info.id, false)
|
||||
this.canvasManager.add(mergedImage, false);
|
||||
this.canvasManager.canvas.moveTo(mergedImage, index);
|
||||
this.canvasManager.renderAll()
|
||||
this.updateLayers()
|
||||
return true;
|
||||
}
|
||||
/** 设置激活对象可擦除 */
|
||||
setActiveObjectErasable() {
|
||||
const objects = this.canvasManager.getObjects()
|
||||
objects.forEach((item: any) => {
|
||||
item.set({
|
||||
erasable: item.info.id === this.activeID.value
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ref, computed } from "vue";
|
||||
import { NODE_TYPE } from '../tools/index.d'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import i18n from '@/lang'
|
||||
const t = i18n.global.t
|
||||
@@ -16,24 +15,22 @@ export class StateManager {
|
||||
layerManager: any
|
||||
eventManager: any
|
||||
toolManager: any
|
||||
brushManager: any
|
||||
// 设置管理器
|
||||
setManager(options) {
|
||||
options.eventManager && (this.eventManager = options.eventManager)
|
||||
options.canvasManager && (this.canvasManager = options.canvasManager)
|
||||
options.layerManager && (this.layerManager = options.layerManager)
|
||||
options.toolManager && (this.toolManager = options.toolManager)
|
||||
options.brushManager && (this.brushManager = options.brushManager)
|
||||
}
|
||||
constructor(options) {
|
||||
this.mxHistory = ref(50)
|
||||
this.historyList = ref([])
|
||||
this.historyIndex = ref(0)
|
||||
|
||||
this.activeNodeID = ref("")
|
||||
|
||||
}
|
||||
/** 设置激活节点 */
|
||||
setActiveNodeID(id: string) { this.activeNodeID.value = id }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { ref } from 'vue'
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
import { BrushManager } from "./brushes/brushManager";
|
||||
import { BrushIndicator } from "./BrushIndicator";
|
||||
import i18n from "@/lang";
|
||||
const t = i18n.global.t
|
||||
export class ToolManager {
|
||||
stateManager: any
|
||||
canvasManager: any
|
||||
currentTool: any
|
||||
brushManager: any
|
||||
tools: any[]
|
||||
brushIndicator: any
|
||||
constructor(options) {
|
||||
this.stateManager = options.stateManager;
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.currentTool = ref(null)
|
||||
|
||||
this.tools = [
|
||||
/** 选择工具 */
|
||||
{
|
||||
@@ -27,11 +34,15 @@ export class ToolManager {
|
||||
{
|
||||
name: OperationType.DRAW,
|
||||
cursor: "crosshair",
|
||||
setup: this.setupBrushTool.bind(this),
|
||||
isDrawingMode: true,
|
||||
},
|
||||
/** 橡皮擦工具 */
|
||||
{
|
||||
name: OperationType.ERASER,
|
||||
cursor: "crosshair",
|
||||
setup: this.setupEraserTool.bind(this),
|
||||
isDrawingMode: true,
|
||||
},
|
||||
/** 智能选框工具 */
|
||||
{
|
||||
@@ -45,15 +56,37 @@ export class ToolManager {
|
||||
},
|
||||
]
|
||||
}
|
||||
onMounted() {
|
||||
this.brushIndicator = new BrushIndicator(this.canvasManager.canvas, {
|
||||
strokeColor: "rgba(0, 0, 0, 0.6)",
|
||||
strokeWidth: 1,
|
||||
fillColor: "rgba(0, 0, 0, 0.1)",
|
||||
});
|
||||
this.brushManager = new BrushManager({
|
||||
canvas: this.canvasManager.canvas,
|
||||
layerManager: this.canvasManager.layerManager, // 传入图层管理器引用
|
||||
brushIndicator: this.brushIndicator,
|
||||
t,
|
||||
});
|
||||
this.stateManager.setManager({
|
||||
brushManager: this.brushManager,
|
||||
})
|
||||
}
|
||||
setTool(value: string) {
|
||||
const tool = this.tools.find((t) => t.name === value)
|
||||
if (!tool) return console.warn(`工具${tool}不存在`)
|
||||
this.currentTool.value = tool.name
|
||||
this.canvasManager.canvas.defaultCursor = tool.cursor
|
||||
this.setCanvasEvented(!!tool.selection)
|
||||
this.canvasManager.canvas.isDragging = !!tool.isDragging
|
||||
this.canvasManager.canvas.isDrawingMode = !!tool.isDrawingMode;// 绘制模式
|
||||
if (!tool.isDrawingMode) this._disableBrushIndicator()// 非绘制模式,禁用笔刷指示器
|
||||
|
||||
|
||||
if (tool.setup) tool.setup()
|
||||
|
||||
setTimeout(() => {
|
||||
this.canvasManager.renderAll()
|
||||
});
|
||||
}
|
||||
// 切换工具时,设置画布事件
|
||||
setCanvasEvented(value: boolean) {
|
||||
@@ -66,4 +99,76 @@ export class ToolManager {
|
||||
/** 移动工具 */
|
||||
setupMoveTool() {
|
||||
}
|
||||
/** 画笔工具 */
|
||||
setupBrushTool() {
|
||||
if (!this.canvasManager.canvas) return;
|
||||
|
||||
// 确保有笔刷管理器
|
||||
if (this.brushManager) {
|
||||
// 检查画笔是否正在更新中
|
||||
if (this.brushManager.isUpdatingBrush) {
|
||||
console.warn("画笔正在更新中,请稍候...");
|
||||
return;
|
||||
}
|
||||
const brushStore = this.brushManager?.brushStore
|
||||
if (brushStore) {
|
||||
// 同步基本属性
|
||||
this.brushManager.setBrushSize(brushStore.state.size);
|
||||
this.brushManager.setBrushColor(brushStore.state.color);
|
||||
this.brushManager.setBrushOpacity(brushStore.state.opacity);
|
||||
|
||||
// 同步笔刷类型 - 修复方法名,使用正确的setBrushType方法
|
||||
this.brushManager.setBrushType("pencil");
|
||||
}
|
||||
|
||||
// 更新应用到画布
|
||||
this.brushManager.updateBrush();
|
||||
}
|
||||
|
||||
// 启用笔刷指示器并同步颜色
|
||||
this._enableBrushIndicator();
|
||||
}
|
||||
/**
|
||||
* 设置橡皮擦工具
|
||||
*/
|
||||
setupEraserTool() {
|
||||
if (!this.canvasManager.canvas) return;
|
||||
|
||||
// 确保有笔刷管理器
|
||||
if (this.brushManager) {
|
||||
this.brushManager.createEraser();
|
||||
}
|
||||
|
||||
this.stateManager.layerManager.setActiveObjectErasable()
|
||||
// 启用笔刷指示器
|
||||
this._enableBrushIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用笔刷指示器
|
||||
* @param {String} color 笔刷颜色(可选)
|
||||
* @private
|
||||
*/
|
||||
_enableBrushIndicator(color?: string) {
|
||||
if (!this.brushIndicator) return;
|
||||
|
||||
// 获取当前笔刷大小
|
||||
const brushSize = this.brushManager?.getBrushSize() || 5;
|
||||
// 获取当前笔刷颜色
|
||||
const brushColor = color || this.brushManager?.getBrushColor() || "#000000";
|
||||
|
||||
// 启用指示器
|
||||
this.brushIndicator.enable(brushSize);
|
||||
this.brushIndicator.updateSize(brushSize);
|
||||
// 更新指示器颜色
|
||||
this.brushIndicator.updateColor(brushColor);
|
||||
}
|
||||
|
||||
/** 禁用笔刷指示器 */
|
||||
_disableBrushIndicator() {
|
||||
if (!this.brushIndicator) return;
|
||||
|
||||
this.brushIndicator.disable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
359
src/components/Canvas/DepthCanvas/manager/brushes/BaseBrush.js
Normal file
359
src/components/Canvas/DepthCanvas/manager/brushes/BaseBrush.js
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 笔刷基类
|
||||
* 所有笔刷类型应继承此基类并实现必要的方法
|
||||
*/
|
||||
export class BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 笔刷配置选项
|
||||
* @param {Object} t 翻译函数
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.options = options;
|
||||
this.t = options.t;
|
||||
// 基本属性
|
||||
this.id = options.id || this.constructor.name;
|
||||
this.name = options.name || "未命名笔刷";
|
||||
this.description = options.description || "";
|
||||
this.icon = options.icon || null;
|
||||
this.category = options.category || "默认";
|
||||
|
||||
// 笔刷实例
|
||||
this.brush = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例(必须由子类实现)
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
throw new Error("必须由子类实现create方法");
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷(必须由子类实现)
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined && options.opacity !== undefined) {
|
||||
// 使用RGBA颜色而不是设置globalAlpha
|
||||
brush.color = this._createRGBAColor(options.color, options.opacity);
|
||||
brush.opacity = 1; // 保持fabric层面的opacity为1
|
||||
} else if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
} else if (options.opacity !== undefined) {
|
||||
// 如果只设置了透明度,基于当前颜色创建RGBA
|
||||
const currentColor = brush.color || this.options.color || "#000000";
|
||||
brush.color = this._createRGBAColor(currentColor, options.opacity);
|
||||
brush.opacity = 1;
|
||||
}
|
||||
|
||||
// 配置阴影
|
||||
this.configureShadow(brush, options);
|
||||
|
||||
// 确保不使用globalAlpha,避免圆圈绘制问题
|
||||
if (brush.canvas && brush.canvas.contextTop) {
|
||||
brush.canvas.contextTop.globalAlpha = 1;
|
||||
brush.canvas.contextTop.lineCap = "round";
|
||||
brush.canvas.contextTop.lineJoin = "round";
|
||||
brush.canvas.contextTop.globalCompositeOperation = "source-over";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷阴影
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configureShadow(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 简化的阴影配置获取方法
|
||||
let shadowConfig = null;
|
||||
|
||||
// 尝试从全局获取BrushStore(在Vue组件中已经导入)
|
||||
if (typeof window !== "undefined" && window.BrushStore) {
|
||||
shadowConfig = window.BrushStore.getShadowConfig();
|
||||
} else {
|
||||
// 如果没有全局BrushStore,尝试从选项中获取
|
||||
if (options.shadowEnabled) {
|
||||
shadowConfig = {
|
||||
color: options.shadowColor || "#000000",
|
||||
blur: options.shadowWidth || 0,
|
||||
offsetX: options.shadowOffsetX || 0,
|
||||
offsetY: options.shadowOffsetY || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (shadowConfig) {
|
||||
// 创建fabric.Shadow实例
|
||||
if (typeof fabric !== "undefined" && fabric.Shadow) {
|
||||
brush.shadow = new fabric.Shadow(shadowConfig);
|
||||
}
|
||||
} else {
|
||||
// 清除阴影
|
||||
brush.shadow = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷阴影设置
|
||||
*/
|
||||
updateShadow() {
|
||||
if (this.brush) {
|
||||
this.configureShadow(this.brush);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建RGBA颜色字符串
|
||||
* @private
|
||||
* @param {String} color 十六进制颜色或已有颜色
|
||||
* @param {Number} opacity 透明度 (0-1)
|
||||
* @returns {String} RGBA颜色字符串
|
||||
*/
|
||||
_createRGBAColor(color, opacity) {
|
||||
// 如果已经是rgba颜色,先提取RGB部分
|
||||
if (color.startsWith("rgba")) {
|
||||
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
|
||||
if (rgbaMatch) {
|
||||
const [, r, g, b] = rgbaMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是rgb颜色,提取RGB部分
|
||||
if (color.startsWith("rgb")) {
|
||||
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理十六进制颜色
|
||||
if (color.startsWith("#")) {
|
||||
const hex = color.replace("#", "");
|
||||
let r, g, b;
|
||||
|
||||
if (hex.length === 3) {
|
||||
r = parseInt(hex[0] + hex[0], 16);
|
||||
g = parseInt(hex[1] + hex[1], 16);
|
||||
b = parseInt(hex[2] + hex[2], 16);
|
||||
} else if (hex.length === 6) {
|
||||
r = parseInt(hex.substring(0, 2), 16);
|
||||
g = parseInt(hex.substring(2, 4), 16);
|
||||
b = parseInt(hex.substring(4, 6), 16);
|
||||
} else {
|
||||
// 无效的十六进制颜色,使用默认
|
||||
r = g = b = 0;
|
||||
}
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
// 如果是其他格式的颜色,尝试转换(例如颜色名称)
|
||||
// 这里简化处理,实际项目中可以使用更复杂的颜色解析
|
||||
return color; // fallback到原颜色
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷的元数据
|
||||
* @returns {Object} 笔刷元数据
|
||||
*/
|
||||
getMetadata() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
icon: this.icon,
|
||||
category: this.category,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷预览
|
||||
* @returns {String|null} 预览图URL或null
|
||||
*/
|
||||
getPreview() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* 这个方法返回一个对象数组,每个对象描述一个可配置属性
|
||||
* 每个属性对象包含:
|
||||
* - id: 属性标识符
|
||||
* - name: 属性显示名称
|
||||
* - type: 属性类型(例如:'slider', 'color', 'checkbox', 'select')
|
||||
* - defaultValue: 默认值
|
||||
* - min/max/step: 对于slider类型的限制值
|
||||
* - options: 对于select类型的选项
|
||||
* - description: 属性描述
|
||||
* - category: 属性分类
|
||||
* - order: 显示顺序(越小越靠前)
|
||||
* - visibleWhen: 函数或对象,定义何时显示该属性
|
||||
* - dynamicOptions: 函数,返回动态的选项列表
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 返回基础属性,所有笔刷都有这些属性
|
||||
return [
|
||||
{
|
||||
id: "size",
|
||||
name: this.t('Canvas.BrushSize'),
|
||||
type: "slider",
|
||||
defaultValue: 5,
|
||||
min: 0.5,
|
||||
max: 100,
|
||||
step: 0.5,
|
||||
description: this.t('Canvas.BrushDeSize'),
|
||||
category: this.t('Canvas.basic'),
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
id: "color",
|
||||
name: this.t('Canvas.BrushColor'),
|
||||
type: "color",
|
||||
defaultValue: "#000000",
|
||||
description: this.t('Canvas.BrushDeColor'),
|
||||
category: this.t('Canvas.basic'),
|
||||
order: 20,
|
||||
},
|
||||
{
|
||||
id: "opacity",
|
||||
name: this.t('Canvas.BrushOpacity'),
|
||||
type: "slider",
|
||||
defaultValue: 1,
|
||||
min: 0.05,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
description: this.t('Canvas.BrushdeOpacity'),
|
||||
category: this.t('Canvas.basic'),
|
||||
order: 30,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并特有属性与基本属性
|
||||
* 子类应该调用此方法来合并自身特有属性与基类提供的基本属性
|
||||
* @param {Array} specificProperties 特有属性数组
|
||||
* @returns {Array} 合并后的属性数组
|
||||
*/
|
||||
mergeWithBaseProperties(specificProperties) {
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 过滤掉同名属性(子类优先)
|
||||
const basePropsFiltered = baseProperties.filter(
|
||||
(baseProp) => !specificProperties.some((specificProp) => specificProp.id === baseProp.id)
|
||||
);
|
||||
|
||||
return [...basePropsFiltered, ...specificProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
if (propId === "size") {
|
||||
if (this.brush) {
|
||||
this.brush.width = value;
|
||||
this.configure(this.brush, { width: value });
|
||||
}
|
||||
return true;
|
||||
} else if (propId === "color") {
|
||||
if (this.brush) {
|
||||
this.brush.color = value;
|
||||
this.configure(this.brush, { color: value });
|
||||
}
|
||||
return true;
|
||||
} else if (propId === "opacity") {
|
||||
if (this.brush) {
|
||||
this.brush.opacity = value;
|
||||
this.configure(this.brush, { opacity: value });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否可见
|
||||
* @param {Object} property 属性对象
|
||||
* @param {Object} currentValues 当前所有属性的值
|
||||
* @returns {Boolean} 是否可见
|
||||
*/
|
||||
isPropertyVisible(property, currentValues) {
|
||||
// 如果没有visibleWhen条件,则始终显示
|
||||
if (!property.visibleWhen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果visibleWhen是函数,则调用函数判断
|
||||
if (typeof property.visibleWhen === "function") {
|
||||
return property.visibleWhen(currentValues);
|
||||
}
|
||||
|
||||
// 如果visibleWhen是对象,检查条件是否满足
|
||||
if (typeof property.visibleWhen === "object") {
|
||||
for (const [key, value] of Object.entries(property.visibleWhen)) {
|
||||
if (currentValues[key] !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态选项
|
||||
* @param {Object} property 属性对象
|
||||
* @param {Object} currentValues 当前所有属性的值
|
||||
* @returns {Array} 选项数组
|
||||
*/
|
||||
getDynamicOptions(property, currentValues) {
|
||||
if (property.dynamicOptions && typeof property.dynamicOptions === "function") {
|
||||
return property.dynamicOptions(currentValues);
|
||||
}
|
||||
return property.options || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生命周期方法:笔刷被选中
|
||||
*/
|
||||
onSelected() {
|
||||
// 可由子类覆盖
|
||||
}
|
||||
|
||||
/**
|
||||
* 生命周期方法:笔刷被取消选中
|
||||
*/
|
||||
onDeselected() {
|
||||
// 可由子类覆盖
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁笔刷实例并清理资源
|
||||
*/
|
||||
destroy() {
|
||||
this.brush = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 笔刷注册表
|
||||
* 用于注册、获取和管理所有笔刷
|
||||
*/
|
||||
export class BrushRegistry {
|
||||
constructor() {
|
||||
// 存储所有注册的笔刷类
|
||||
this.brushes = new Map();
|
||||
|
||||
// 按类别组织的笔刷
|
||||
this.brushesByCategory = new Map();
|
||||
|
||||
// 事件监听器
|
||||
this.listeners = {
|
||||
register: [],
|
||||
unregister: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个笔刷
|
||||
* @param {String} id 笔刷唯一标识
|
||||
* @param {Class} brushClass 笔刷类(需要继承BaseBrush)
|
||||
* @param {Object} metadata 笔刷元数据(可选)
|
||||
* @returns {Boolean} 是否注册成功
|
||||
*/
|
||||
register(id, brushClass, metadata = {}) {
|
||||
if (this.brushes.has(id)) {
|
||||
console.warn(`笔刷 ${id} 已存在,请使用其他ID`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 存储笔刷信息
|
||||
const brushInfo = {
|
||||
id,
|
||||
class: brushClass,
|
||||
metadata: {
|
||||
...metadata,
|
||||
id,
|
||||
},
|
||||
};
|
||||
|
||||
this.brushes.set(id, brushInfo);
|
||||
|
||||
// 添加到分类
|
||||
const category = metadata.category || "默认";
|
||||
if (!this.brushesByCategory.has(category)) {
|
||||
this.brushesByCategory.set(category, []);
|
||||
}
|
||||
this.brushesByCategory.get(category).push(brushInfo);
|
||||
|
||||
// 触发事件
|
||||
this._triggerEvent("register", brushInfo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册笔刷
|
||||
* @param {String} id 笔刷ID
|
||||
* @returns {Boolean} 是否成功
|
||||
*/
|
||||
unregister(id) {
|
||||
if (!this.brushes.has(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const brushInfo = this.brushes.get(id);
|
||||
this.brushes.delete(id);
|
||||
|
||||
// 从分类中移除
|
||||
const category = brushInfo.metadata.category || "默认";
|
||||
if (this.brushesByCategory.has(category)) {
|
||||
const brushes = this.brushesByCategory.get(category);
|
||||
const index = brushes.findIndex((b) => b.id === id);
|
||||
if (index !== -1) {
|
||||
brushes.splice(index, 1);
|
||||
}
|
||||
|
||||
// 如果分类为空,删除该分类
|
||||
if (brushes.length === 0) {
|
||||
this.brushesByCategory.delete(category);
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
this._triggerEvent("unregister", brushInfo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷信息
|
||||
* @param {String} id 笔刷ID
|
||||
* @returns {Object|null} 笔刷信息或null
|
||||
*/
|
||||
getBrush(id) {
|
||||
return this.brushes.get(id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有笔刷
|
||||
* @returns {Array} 笔刷信息数组
|
||||
*/
|
||||
getAllBrushes() {
|
||||
return Array.from(this.brushes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分类的笔刷
|
||||
* @param {String} category 分类名称
|
||||
* @returns {Array} 笔刷信息数组
|
||||
*/
|
||||
getBrushesByCategory(category) {
|
||||
return this.brushesByCategory.get(category) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分类
|
||||
* @returns {Array} 分类名称数组
|
||||
*/
|
||||
getCategories() {
|
||||
return Array.from(this.brushesByCategory.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个笔刷实例
|
||||
* @param {String} id 笔刷ID
|
||||
* @param {Object} canvas 画布实例
|
||||
* @param {Object} options 配置选项
|
||||
* @returns {Object|null} 笔刷实例或null
|
||||
*/
|
||||
createBrushInstance(id, canvas, options = {}) {
|
||||
const brushInfo = this.getBrush(id);
|
||||
if (!brushInfo) {
|
||||
console.error(`笔刷 ${id} 不存在`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建笔刷实例
|
||||
return new brushInfo.class(canvas, {
|
||||
...options,
|
||||
id: brushInfo.id,
|
||||
...brushInfo.metadata,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`创建笔刷 ${id} 失败:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
* @param {String} event 事件名称 ('register'|'unregister')
|
||||
* @param {Function} callback 回调函数
|
||||
*/
|
||||
addEventListener(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
* @param {String} event 事件名称
|
||||
* @param {Function} callback 回调函数
|
||||
*/
|
||||
removeEventListener(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
const index = this.listeners[event].indexOf(callback);
|
||||
if (index !== -1) {
|
||||
this.listeners[event].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param {String} event 事件名称
|
||||
* @param {*} data 事件数据
|
||||
* @private
|
||||
*/
|
||||
_triggerEvent(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((callback) => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`执行 ${event} 事件监听器出错:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const brushRegistry = new BrushRegistry();
|
||||
|
||||
// 默认导出单例
|
||||
export default brushRegistry;
|
||||
694
src/components/Canvas/DepthCanvas/manager/brushes/BrushStore.js
Normal file
694
src/components/Canvas/DepthCanvas/manager/brushes/BrushStore.js
Normal file
@@ -0,0 +1,694 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
// import texturePresetManager from "../managers/brushes/TexturePresetManager";
|
||||
class texturePresetManager { }
|
||||
|
||||
export class BrushState {
|
||||
constructor(options) {
|
||||
this.state = reactive({
|
||||
// 笔刷基础属性
|
||||
size: 5, // 笔刷大小
|
||||
color: "#000000", // 笔刷颜色
|
||||
opacity: 1, // 笔刷透明度
|
||||
type: "pencil", // 当前笔刷类型
|
||||
|
||||
// 阴影相关属性
|
||||
shadowEnabled: false, // 是否启用阴影
|
||||
shadowColor: "#000000", // 阴影颜色(默认为笔刷颜色)
|
||||
shadowWidth: 0, // 阴影宽度
|
||||
shadowOffsetX: 0, // 阴影X偏移
|
||||
shadowOffsetY: 0, // 阴影Y偏移
|
||||
|
||||
// 笔刷材质相关
|
||||
textureScale: 1, // 材质缩放
|
||||
textureEnabled: false, // 是否启用材质
|
||||
texturePath: "", // 材质图片路径
|
||||
textureOpacity: 1, // 材质透明度
|
||||
textureRepeat: "repeat", // 材质重复模式
|
||||
textureAngle: 0, // 材质旋转角度
|
||||
selectedTextureId: null, // 当前选中的材质ID
|
||||
|
||||
// 可用笔刷类型列表 (由BrushManager初始化)
|
||||
availableBrushes: [],
|
||||
|
||||
// 自定义笔刷列表
|
||||
customBrushes: [],
|
||||
|
||||
// 笔刷预设
|
||||
presets: [
|
||||
// { name: "细线", size: 2, opacity: 1, color: "#000000", type: "pencil" },
|
||||
// { name: "中粗", size: 5, opacity: 1, color: "#000000", type: "pencil" },
|
||||
// { name: "粗线", size: 10, opacity: 1, color: "#000000", type: "pencil" },
|
||||
// { name: "水彩", size: 15, opacity: 0.7, color: "#3366ff", type: "marker" },
|
||||
// { name: "喷枪", size: 20, opacity: 0.5, color: "#ff6633", type: "spray" },
|
||||
],
|
||||
|
||||
// 材质预设
|
||||
texturePresets: [
|
||||
{
|
||||
name: "默认纹理",
|
||||
textureId: "preset_texture_0",
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
repeat: "repeat",
|
||||
angle: 0,
|
||||
},
|
||||
{
|
||||
name: "细纹理",
|
||||
textureId: "preset_texture_1",
|
||||
scale: 0.5,
|
||||
opacity: 0.8,
|
||||
repeat: "repeat",
|
||||
angle: 0,
|
||||
},
|
||||
{
|
||||
name: "粗纹理",
|
||||
textureId: "preset_texture_2",
|
||||
scale: 2,
|
||||
opacity: 1,
|
||||
repeat: "repeat",
|
||||
angle: 45,
|
||||
},
|
||||
{
|
||||
name: "水彩纹理",
|
||||
textureId: "preset_texture_5",
|
||||
scale: 1.5,
|
||||
opacity: 0.6,
|
||||
repeat: "no-repeat",
|
||||
angle: 0,
|
||||
}
|
||||
],
|
||||
|
||||
// 上传的纹理缓存列表
|
||||
uploadedTextures: [],
|
||||
|
||||
// 最近使用的颜色
|
||||
recentColors: ["#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff"],
|
||||
|
||||
// 最近使用的材质
|
||||
recentTextures: [],
|
||||
|
||||
// 当前笔刷可配置属性(由当前选中笔刷动态设置)
|
||||
currentBrushProperties: [],
|
||||
|
||||
// 当前笔刷实例的引用
|
||||
currentBrushInstance: null,
|
||||
|
||||
// 笔刷属性值的映射,存储可由UI修改的属性值
|
||||
propertyValues: {},
|
||||
});
|
||||
}
|
||||
setBrushSize(size) {
|
||||
this.state.size = Math.max(0.5, Math.min(100, size));
|
||||
}
|
||||
|
||||
setBrushColor(color) {
|
||||
this.state.color = color;
|
||||
// 添加到最近使用的颜色
|
||||
if (!this.state.recentColors.includes(color)) {
|
||||
this.state.recentColors.unshift(color);
|
||||
if (this.state.recentColors.length > 10) {
|
||||
this.state.recentColors.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setBrushOpacity(opacity) {
|
||||
this.state.opacity = Math.max(0.05, Math.min(1, opacity));
|
||||
}
|
||||
|
||||
setBrushType(type) {
|
||||
if (this.state.availableBrushes.some((brush) => brush.id === type)) {
|
||||
this.state.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
setTextureScale(scale) {
|
||||
this.state.textureScale = Math.max(0.1, Math.min(10, scale));
|
||||
}
|
||||
|
||||
setTextureEnabled(enabled) {
|
||||
this.state.textureEnabled = enabled;
|
||||
}
|
||||
|
||||
setTexturePath(path) {
|
||||
this.state.texturePath = path;
|
||||
}
|
||||
|
||||
// 阴影相关方法
|
||||
setShadowEnabled(enabled) {
|
||||
this.state.shadowEnabled = enabled;
|
||||
}
|
||||
|
||||
setShadowColor(color) {
|
||||
this.state.shadowColor = color;
|
||||
}
|
||||
|
||||
setShadowWidth(width) {
|
||||
this.state.shadowWidth = Math.max(0, Math.min(50, width));
|
||||
}
|
||||
|
||||
setShadowOffsetX(offsetX) {
|
||||
this.state.shadowOffsetX = Math.max(-50, Math.min(50, offsetX));
|
||||
}
|
||||
|
||||
setShadowOffsetY(offsetY) {
|
||||
this.state.shadowOffsetY = Math.max(-50, Math.min(50, offsetY));
|
||||
}
|
||||
|
||||
setAvailableBrushes(brushes) {
|
||||
this.state.availableBrushes = brushes;
|
||||
}
|
||||
|
||||
addCustomBrush(brush) {
|
||||
if (!brush.id) {
|
||||
brush.id = `custom_${Date.now()}`;
|
||||
}
|
||||
|
||||
this.state.customBrushes.push(brush);
|
||||
return brush.id;
|
||||
}
|
||||
|
||||
removeCustomBrush(brushId) {
|
||||
const index = this.state.customBrushes.findIndex((b) => b.id === brushId);
|
||||
if (index !== -1) {
|
||||
this.state.customBrushes.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 应用预设
|
||||
applyPreset(presetIndex) {
|
||||
const preset = this.state.presets[presetIndex];
|
||||
if (preset) {
|
||||
this.state.size = preset.size;
|
||||
this.state.opacity = preset.opacity;
|
||||
this.state.color = preset.color;
|
||||
this.state.type = preset.type;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将当前设置保存为新预设
|
||||
saveCurrentAsPreset(name) {
|
||||
const newPreset = {
|
||||
name: name || `预设 ${this.state.presets.length + 1}`,
|
||||
size: this.state.size,
|
||||
opacity: this.state.opacity,
|
||||
color: this.state.color,
|
||||
type: this.state.type,
|
||||
textureEnabled: this.state.textureEnabled,
|
||||
textureScale: this.state.textureScale,
|
||||
texturePath: this.state.texturePath,
|
||||
};
|
||||
|
||||
this.state.presets.push(newPreset);
|
||||
return this.state.presets.length - 1; // 返回新预设的索引
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前笔刷实例
|
||||
* @param {Object} brushInstance BaseBrush实例
|
||||
*/
|
||||
setCurrentBrushInstance(brushInstance) {
|
||||
this.state.currentBrushInstance = brushInstance;
|
||||
|
||||
// 获取并设置当前笔刷的可配置属性
|
||||
if (brushInstance && brushInstance.getConfigurableProperties) {
|
||||
const properties = brushInstance.getConfigurableProperties();
|
||||
this.state.currentBrushProperties = properties;
|
||||
|
||||
// 初始化属性值
|
||||
properties.forEach((prop) => {
|
||||
// 如果是基础属性,使用已有值
|
||||
if (prop.id === "size") {
|
||||
this.state.propertyValues[prop.id] = this.state.size;
|
||||
} else if (prop.id === "color") {
|
||||
this.state.propertyValues[prop.id] = this.state.color;
|
||||
} else if (prop.id === "opacity") {
|
||||
this.state.propertyValues[prop.id] = this.state.opacity;
|
||||
} else {
|
||||
// 对于特殊属性,使用默认值
|
||||
this.state.propertyValues[prop.id] = prop.defaultValue;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果没有实例或方法,清空属性列表
|
||||
this.state.currentBrushProperties = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性值
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
*/
|
||||
updatePropertyValue(propId, value) {
|
||||
// 更新Store中的值
|
||||
this.state.propertyValues[propId] = value;
|
||||
|
||||
// 同步更新基础属性
|
||||
if (propId === "size") {
|
||||
this.state.size = value;
|
||||
} else if (propId === "color") {
|
||||
this.state.color = value;
|
||||
} else if (propId === "opacity") {
|
||||
this.state.opacity = value;
|
||||
}
|
||||
|
||||
// 如果有当前笔刷实例且有更新方法,则调用
|
||||
if (this.state.currentBrushInstance && this.state.currentBrushInstance.updateProperty) {
|
||||
this.state.currentBrushInstance.updateProperty(propId, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性值
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} defaultValue 默认值
|
||||
* @returns {any} 属性值
|
||||
*/
|
||||
getPropertyValue(propId, defaultValue) {
|
||||
// 检查属性值是否存在
|
||||
if (Object.prototype.hasOwnProperty.call(this.state.propertyValues, propId)) {
|
||||
return this.state.propertyValues[propId];
|
||||
}
|
||||
|
||||
// 对于基础属性,返回store中的值
|
||||
if (propId === "size") {
|
||||
return this.state.size;
|
||||
} else if (propId === "color") {
|
||||
return this.state.color;
|
||||
} else if (propId === "opacity") {
|
||||
return this.state.opacity;
|
||||
}
|
||||
|
||||
// 否则返回默认值
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类获取当前笔刷可配置属性
|
||||
* @returns {Object} 按分类分组的属性对象
|
||||
*/
|
||||
getPropertiesByCategory() {
|
||||
const result = {};
|
||||
|
||||
this.state.currentBrushProperties.forEach((prop) => {
|
||||
const category = prop.category || "默认";
|
||||
if (!result[category]) {
|
||||
result[category] = [];
|
||||
}
|
||||
result[category].push({
|
||||
...prop,
|
||||
value: this.getPropertyValue(prop.id, prop.defaultValue),
|
||||
});
|
||||
});
|
||||
|
||||
// 按order排序每个分类中的属性
|
||||
Object.keys(result).forEach((category) => {
|
||||
result[category].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 材质相关方法
|
||||
*/
|
||||
setTextureOpacity(opacity) {
|
||||
this.state.textureOpacity = Math.max(0, Math.min(1, opacity));
|
||||
}
|
||||
|
||||
setTextureRepeat(repeat) {
|
||||
const validModes = ["repeat", "repeat-x", "repeat-y", "no-repeat"];
|
||||
if (validModes.includes(repeat)) {
|
||||
this.state.textureRepeat = repeat;
|
||||
}
|
||||
}
|
||||
|
||||
setTextureAngle(angle) {
|
||||
this.state.textureAngle = angle % 360;
|
||||
}
|
||||
|
||||
setSelectedTextureId(textureId) {
|
||||
this.state.selectedTextureId = textureId;
|
||||
|
||||
// 添加到最近使用的材质
|
||||
if (textureId && !this.state.recentTextures.includes(textureId)) {
|
||||
this.state.recentTextures.unshift(textureId);
|
||||
if (this.state.recentTextures.length > 8) {
|
||||
this.state.recentTextures.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用材质预设
|
||||
* @param {Number} presetIndex 预设索引
|
||||
* @returns {Boolean} 是否应用成功
|
||||
*/
|
||||
applyTexturePreset(presetIndex) {
|
||||
const preset = this.state.texturePresets[presetIndex];
|
||||
if (preset) {
|
||||
this.state.selectedTextureId = preset.textureId;
|
||||
this.state.textureScale = preset.scale;
|
||||
this.state.textureOpacity = preset.opacity;
|
||||
this.state.textureRepeat = preset.repeat;
|
||||
this.state.textureAngle = preset.angle;
|
||||
|
||||
// 添加到最近使用
|
||||
this.setSelectedTextureId(preset.textureId);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前材质设置保存为新预设
|
||||
* @param {String} name 预设名称
|
||||
* @returns {Number} 新预设的索引
|
||||
*/
|
||||
saveCurrentTextureAsPreset(name) {
|
||||
const newPreset = {
|
||||
name: name || `材质预设 ${this.state.texturePresets.length + 1}`,
|
||||
textureId: this.state.selectedTextureId,
|
||||
scale: this.state.textureScale,
|
||||
opacity: this.state.textureOpacity,
|
||||
repeat: this.state.textureRepeat,
|
||||
angle: this.state.textureAngle,
|
||||
};
|
||||
|
||||
this.state.texturePresets.push(newPreset);
|
||||
return this.state.texturePresets.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除材质预设
|
||||
* @param {Number} presetIndex 预设索引
|
||||
* @returns {Boolean} 是否删除成功
|
||||
*/
|
||||
removeTexturePreset(presetIndex) {
|
||||
if (presetIndex >= 0 && presetIndex < this.state.texturePresets.length) {
|
||||
this.state.texturePresets.splice(presetIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用材质(预设+自定义)
|
||||
* @returns {Array} 材质列表
|
||||
*/
|
||||
getAllTextures() {
|
||||
return texturePresetManager.getAllTextures();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取材质信息
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Object|null} 材质对象
|
||||
*/
|
||||
getTextureById(textureId) {
|
||||
return texturePresetManager.getTextureById(textureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类获取材质
|
||||
* @param {String} category 分类名称
|
||||
* @returns {Array} 材质列表
|
||||
*/
|
||||
getTexturesByCategory(category) {
|
||||
return texturePresetManager.getTexturesByCategory(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材质分类列表
|
||||
* @returns {Array} 分类名称数组
|
||||
*/
|
||||
getTextureCategories() {
|
||||
return texturePresetManager.getCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索材质
|
||||
* @param {String} keyword 搜索关键词
|
||||
* @returns {Array} 匹配的材质列表
|
||||
*/
|
||||
searchTextures(keyword) {
|
||||
return texturePresetManager.searchTextures(keyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义材质
|
||||
* @param {Object} textureData 材质数据
|
||||
* @returns {String} 材质ID
|
||||
*/
|
||||
addCustomTexture(textureData) {
|
||||
const textureId = texturePresetManager.addCustomTexture(textureData);
|
||||
|
||||
// 保存到本地存储
|
||||
texturePresetManager.saveCustomTexturesToStorage();
|
||||
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义材质
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Boolean} 是否删除成功
|
||||
*/
|
||||
removeCustomTexture(textureId) {
|
||||
const success = texturePresetManager.removeCustomTexture(textureId);
|
||||
|
||||
if (success) {
|
||||
// 如果删除的是当前选中的材质,清空选择
|
||||
if (this.state.selectedTextureId === textureId) {
|
||||
this.state.selectedTextureId = null;
|
||||
}
|
||||
|
||||
// 从最近使用中移除
|
||||
const recentIndex = this.state.recentTextures.indexOf(textureId);
|
||||
if (recentIndex !== -1) {
|
||||
this.state.recentTextures.splice(recentIndex, 1);
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
texturePresetManager.saveCustomTexturesToStorage();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件上传自定义材质
|
||||
* @param {File} file 图片文件
|
||||
* @param {String} name 材质名称(可选)
|
||||
* @returns {Promise<String>} 材质ID
|
||||
*/
|
||||
uploadCustomTexture(file, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 验证文件
|
||||
if (!texturePresetManager.validateTextureFile(file)) {
|
||||
reject(new Error("无效的材质文件"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const textureData = {
|
||||
name: name || file.name.replace(/\.[^/.]+$/, ""),
|
||||
path: e.target.result,
|
||||
preview: e.target.result,
|
||||
description: `用户上传的材质: ${file.name}`,
|
||||
};
|
||||
|
||||
const textureId = this.addCustomTexture(textureData);
|
||||
resolve(textureId);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error("文件读取失败"));
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出材质预设配置
|
||||
* @returns {String} JSON格式的配置
|
||||
*/
|
||||
exportTexturePresets() {
|
||||
const config = {
|
||||
texturePresets: this.state.texturePresets,
|
||||
customTextures: texturePresetManager.exportCustomTextures(),
|
||||
};
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入材质预设配置
|
||||
* @param {String} configJson JSON格式的配置
|
||||
* @returns {Boolean} 是否导入成功
|
||||
*/
|
||||
importTexturePresets(configJson) {
|
||||
try {
|
||||
const config = JSON.parse(configJson);
|
||||
|
||||
// 导入材质预设
|
||||
if (config.texturePresets && Array.isArray(config.texturePresets)) {
|
||||
this.state.texturePresets = [...this.state.texturePresets, ...config.texturePresets];
|
||||
}
|
||||
|
||||
// 导入自定义材质
|
||||
if (config.customTextures) {
|
||||
texturePresetManager.importCustomTextures(config.customTextures);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("导入材质预设失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化材质预设管理器
|
||||
*/
|
||||
initializeTexturePresets() {
|
||||
// 从本地存储加载自定义材质
|
||||
texturePresetManager.loadCustomTexturesFromStorage();
|
||||
|
||||
// 确保预设材质引用的是有效的材质ID
|
||||
this.state.texturePresets.forEach((preset, index) => {
|
||||
const texture = texturePresetManager.getTextureById(preset.textureId);
|
||||
if (!texture) {
|
||||
console.warn(`材质预设 "${preset.name}" 引用的材质 ${preset.textureId} 不存在`);
|
||||
// 可以选择使用默认材质替换或删除该预设
|
||||
if (texturePresetManager.getAllTextures().length > 0) {
|
||||
preset.textureId = texturePresetManager.getAllTextures()[0].id;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空上传的纹理缓存
|
||||
*/
|
||||
clearUploadedTextures() {
|
||||
this.state.uploadedTextures = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加纹理到缓存
|
||||
* @param {String} textureId 材质ID
|
||||
*/
|
||||
cacheUploadedTexture(textureId) {
|
||||
const texture = texturePresetManager.getTextureById(textureId);
|
||||
if (texture && !this.state.uploadedTextures.includes(textureId)) {
|
||||
this.state.uploadedTextures.push(textureId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中移除纹理
|
||||
* @param {String} textureId 材质ID
|
||||
*/
|
||||
removeCachedTexture(textureId) {
|
||||
const index = this.state.uploadedTextures.indexOf(textureId);
|
||||
if (index !== -1) {
|
||||
this.state.uploadedTextures.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
getRGBAColor() {
|
||||
// 解析十六进制颜色并添加透明度
|
||||
const hex = this.state.color.replace("#", "");
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${this.state.opacity})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前笔刷的实际绘制颜色(包含透明度)
|
||||
* @returns {String} RGBA格式的颜色字符串
|
||||
*/
|
||||
getCurrentBrushColor() {
|
||||
return this.getRGBAColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从RGBA颜色字符串中提取RGB值
|
||||
* @param {String} rgbaColor RGBA颜色字符串
|
||||
* @returns {Object} {r, g, b, a} 颜色值对象
|
||||
*/
|
||||
parseRGBAColor(rgbaColor) {
|
||||
const rgbaMatch = rgbaColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
||||
if (rgbaMatch) {
|
||||
return {
|
||||
r: parseInt(rgbaMatch[1]),
|
||||
g: parseInt(rgbaMatch[2]),
|
||||
b: parseInt(rgbaMatch[3]),
|
||||
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将RGB值转换为十六进制颜色
|
||||
* @param {Number} r 红色值 (0-255)
|
||||
* @param {Number} g 绿色值 (0-255)
|
||||
* @param {Number} b 蓝色值 (0-255)
|
||||
* @returns {String} 十六进制颜色字符串
|
||||
*/
|
||||
rgbToHex(r, g, b) {
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前阴影配置对象
|
||||
* @returns {Object|null} fabric.Shadow配置对象或null
|
||||
*/
|
||||
getShadowConfig() {
|
||||
if (!this.state.shadowEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
color: this.state.shadowColor,
|
||||
blur: this.state.shadowWidth,
|
||||
offsetX: this.state.shadowOffsetX,
|
||||
offsetY: this.state.shadowOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建fabric.Shadow实例
|
||||
* @returns {fabric.Shadow|null} fabric.Shadow实例或null
|
||||
*/
|
||||
createFabricShadow() {
|
||||
const config = this.getShadowConfig();
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保fabric已加载
|
||||
if (typeof fabric !== "undefined" && fabric.Shadow) {
|
||||
return new fabric.Shadow(config);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
# fabric-with-erasing 库的 erasable 属性功能使用指南
|
||||
|
||||
## 库功能概述
|
||||
|
||||
`fabric-with-erasing` 库提供了强大的基于属性的擦除控制功能,无需手动实现复杂的图层检查逻辑。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. erasable 属性的三种模式
|
||||
|
||||
- **`true`** (默认): 对象可以被擦除
|
||||
- **`false`**: 对象不能被擦除
|
||||
- **`'deep'`**: 对于组合对象,可以对内部可擦除的子对象进行细粒度控制
|
||||
|
||||
### 2. 选择性擦除机制
|
||||
|
||||
库内置了选择性擦除机制:
|
||||
- 橡皮擦会自动检测对象的 `erasable` 属性
|
||||
- 只有 `erasable !== false` 的对象才会被擦除
|
||||
- 支持复杂的嵌套对象结构
|
||||
|
||||
### 3. 反向擦除功能
|
||||
|
||||
- 设置 `brush.inverted = true` 可以实现"撤销擦除"效果
|
||||
- 恢复已被擦除的内容
|
||||
|
||||
## 项目中的优化实现
|
||||
|
||||
### LayerManager 优化
|
||||
|
||||
```javascript
|
||||
// 基于图层状态自动设置 erasable 属性
|
||||
obj.erasable = isInActiveLayer && layer.visible && !layer.locked && !layer.isBackground;
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- 只有活动图层、可见、非锁定、非背景的对象才可擦除
|
||||
- 自动处理复杂的权限逻辑
|
||||
- 性能优秀,无需手动遍历检查
|
||||
|
||||
### BrushManager 简化
|
||||
|
||||
```javascript
|
||||
// 直接使用库的 EraserBrush
|
||||
this.brush = new fabric.EraserBrush(this.canvas);
|
||||
this.brush.inverted = this.options.inverted || false;
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- 移除了复杂的手动图层检查逻辑
|
||||
- 直接利用库的内置功能
|
||||
- 支持反向擦除(恢复功能)
|
||||
- 代码更简洁、更可靠
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```javascript
|
||||
// 设置对象不可擦除
|
||||
fabricObject.erasable = false;
|
||||
|
||||
// 设置对象可擦除
|
||||
fabricObject.erasable = true;
|
||||
|
||||
// 组合对象的深度擦除控制
|
||||
group.erasable = 'deep';
|
||||
```
|
||||
|
||||
### 高级用法
|
||||
|
||||
```javascript
|
||||
// 启用反向擦除模式
|
||||
eraserBrush.inverted = true;
|
||||
|
||||
// 监听擦除事件
|
||||
canvas.on('erasing:start', () => {
|
||||
console.log('开始擦除');
|
||||
});
|
||||
|
||||
canvas.on('erasing:end', (e) => {
|
||||
console.log('擦除完成', e.targets);
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优势
|
||||
|
||||
1. **内置优化**: 库已经进行了性能优化,避免重复计算
|
||||
2. **事件驱动**: 基于事件的架构,响应更快
|
||||
3. **选择性渲染**: 只重新渲染需要更新的部分
|
||||
4. **内存效率**: 合理的对象管理和清理机制
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
- 完全兼容标准的 fabric.js API
|
||||
- 新增的 `erasable` 属性不会影响现有功能
|
||||
- 可以逐步迁移现有代码
|
||||
|
||||
## 建议
|
||||
|
||||
1. **简化现有实现**: 移除手动的图层检查逻辑,直接使用 `erasable` 属性
|
||||
2. **利用内置事件**: 使用库提供的擦除事件进行状态管理
|
||||
3. **测试反向擦除**: 尝试使用 `inverted` 属性实现撤销功能
|
||||
4. **性能测试**: 在大量对象的场景下测试性能表现
|
||||
|
||||
通过这些优化,你的项目可以获得更好的性能和更简洁的代码结构。
|
||||
280
src/components/Canvas/DepthCanvas/manager/brushes/README.md
Normal file
280
src/components/Canvas/DepthCanvas/manager/brushes/README.md
Normal file
@@ -0,0 +1,280 @@
|
||||
<!-- https://github.com/tennisonchan/fabric-brush?tab=readme-ov-file -->
|
||||
<!-- eraser_brush:https://unpkg.com/fabric@5.5.2/src/mixins/eraser_brush.mixin.js -->
|
||||
# 笔刷系统使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
这是一个基于插件架构的笔刷系统,允许轻松扩展和添加新的笔刷类型。整个系统由以下几个关键部分组成:
|
||||
|
||||
1. `BaseBrush` - 所有笔刷的基类
|
||||
2. `BrushRegistry` - 笔刷注册表,用于管理所有笔刷
|
||||
3. `BrushManager` - 笔刷管理器,处理笔刷的实例化和切换
|
||||
4. `BrushStore` - 笔刷状态存储
|
||||
|
||||
## 如何添加新笔刷
|
||||
|
||||
添加新笔刷只需简单几步:
|
||||
|
||||
### 1. 创建新的笔刷类
|
||||
|
||||
最简单的方法是继承 `BaseBrush` 类。在 `types` 目录下创建你的笔刷文件:
|
||||
|
||||
```javascript
|
||||
import { BaseBrush } from '../BaseBrush';
|
||||
|
||||
/**
|
||||
* 我的自定义笔刷
|
||||
*/
|
||||
export class MyCustomBrush extends BaseBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: 'my-custom-brush',
|
||||
name: '我的笔刷',
|
||||
description: '这是我自定义的笔刷',
|
||||
category: '自定义笔刷',
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
// 创建笔刷实例
|
||||
create() {
|
||||
// 创建底层fabric.js笔刷
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
// 配置笔刷
|
||||
configure(brush, options = {}) {
|
||||
// 设置基本属性
|
||||
if (options.color) brush.color = options.color;
|
||||
if (options.width !== undefined) brush.width = options.width;
|
||||
if (options.opacity !== undefined) brush.opacity = options.opacity;
|
||||
|
||||
// 设置自定义属性
|
||||
brush.strokeLineCap = 'round';
|
||||
brush.strokeLineJoin = 'round';
|
||||
// ...更多自定义设置
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册笔刷
|
||||
|
||||
有两种方式注册笔刷:
|
||||
|
||||
#### 方式一:使用BrushRegistry直接注册
|
||||
|
||||
```javascript
|
||||
import { brushRegistry } from '../BrushRegistry';
|
||||
import { MyCustomBrush } from './MyCustomBrush';
|
||||
|
||||
// 注册笔刷
|
||||
brushRegistry.register('my-custom-brush', MyCustomBrush, {
|
||||
name: '我的笔刷',
|
||||
description: '这是我自定义的笔刷',
|
||||
category: '自定义笔刷'
|
||||
});
|
||||
```
|
||||
|
||||
#### 方式二:通过BrushManager注册
|
||||
|
||||
```javascript
|
||||
import { BrushManager } from '../brushManager';
|
||||
import { MyCustomBrush } from './MyCustomBrush';
|
||||
|
||||
// 获取BrushManager实例
|
||||
const brushManager = new BrushManager({ canvas });
|
||||
|
||||
// 注册笔刷
|
||||
brushManager.registerBrush('my-custom-brush', MyCustomBrush, {
|
||||
name: '我的笔刷',
|
||||
description: '这是我自定义的笔刷',
|
||||
category: '自定义笔刷'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 使用笔刷
|
||||
|
||||
注册笔刷后,可以在应用中使用它:
|
||||
|
||||
```javascript
|
||||
// 切换到你的自定义笔刷
|
||||
brushManager.setBrushType('my-custom-brush');
|
||||
```
|
||||
|
||||
## 笔刷生命周期
|
||||
|
||||
每个笔刷有以下生命周期方法:
|
||||
|
||||
1. `constructor` - 创建笔刷类实例
|
||||
2. `create` - 创建底层fabric.js笔刷实例
|
||||
3. `configure` - 配置笔刷属性
|
||||
4. `onSelected` - 笔刷被选中时调用
|
||||
5. `onDeselected` - 笔刷被取消选中时调用
|
||||
6. `destroy` - 销毁笔刷实例释放资源
|
||||
|
||||
## 示例:创建具有独特行为的笔刷
|
||||
|
||||
这个例子创建了一个"脉冲笔刷",线条宽度会自动脉动变化:
|
||||
|
||||
```javascript
|
||||
import { BaseBrush } from '../BaseBrush';
|
||||
|
||||
export class PulseBrush extends BaseBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: 'pulse',
|
||||
name: '脉动笔刷',
|
||||
description: '线条宽度会自动脉动变化',
|
||||
category: '特效笔刷',
|
||||
...options
|
||||
});
|
||||
|
||||
this.originalWidth = options.width || 5;
|
||||
this.pulseRate = options.pulseRate || 0.1;
|
||||
this.pulseAmount = options.pulseAmount || 3;
|
||||
this.pulseTimer = null;
|
||||
}
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
// 覆盖鼠标按下方法,开始脉冲效果
|
||||
const originalMouseDown = this.brush.onMouseDown;
|
||||
this.brush.onMouseDown = (pointer, options) => {
|
||||
this.startPulse();
|
||||
return originalMouseDown.call(this.brush, pointer, options);
|
||||
};
|
||||
|
||||
// 覆盖鼠标松开方法,停止脉冲效果
|
||||
const originalMouseUp = this.brush.onMouseUp;
|
||||
this.brush.onMouseUp = (options) => {
|
||||
this.stopPulse();
|
||||
return originalMouseUp.call(this.brush, options);
|
||||
};
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
configure(brush, options = {}) {
|
||||
if (options.width !== undefined) {
|
||||
this.originalWidth = options.width;
|
||||
brush.width = this.originalWidth;
|
||||
}
|
||||
|
||||
if (options.color) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
if (options.pulseRate !== undefined) {
|
||||
this.pulseRate = options.pulseRate;
|
||||
}
|
||||
|
||||
if (options.pulseAmount !== undefined) {
|
||||
this.pulseAmount = options.pulseAmount;
|
||||
}
|
||||
}
|
||||
|
||||
startPulse() {
|
||||
this.stopPulse();
|
||||
|
||||
let phase = 0;
|
||||
this.pulseTimer = setInterval(() => {
|
||||
phase += this.pulseRate;
|
||||
const pulseFactor = Math.sin(phase) * this.pulseAmount;
|
||||
|
||||
if (this.brush) {
|
||||
this.brush.width = Math.max(1, this.originalWidth + pulseFactor);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
stopPulse() {
|
||||
if (this.pulseTimer) {
|
||||
clearInterval(this.pulseTimer);
|
||||
this.pulseTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onDeselected() {
|
||||
this.stopPulse();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopPulse();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用预设笔刷类型
|
||||
|
||||
系统内置了几种笔刷类型,可以直接使用:
|
||||
|
||||
- `pencil` - 基础铅笔笔刷
|
||||
- `spray` - 喷枪笔刷
|
||||
- `marker` - 马克笔笔刷
|
||||
- `eraser` - 橡皮擦笔刷
|
||||
- `texture` - 材质笔刷
|
||||
- `watercolor` - 水彩笔刷
|
||||
- `chalk` - 粉笔笔刷
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 1. 使用分类组织笔刷
|
||||
|
||||
注册笔刷时可以指定类别,便于在UI中分组展示:
|
||||
|
||||
```javascript
|
||||
brushRegistry.register('my-brush', MyBrushClass, {
|
||||
category: '艺术笔刷'
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 自定义笔刷参数
|
||||
|
||||
可以为笔刷添加特殊参数,例如:
|
||||
|
||||
```javascript
|
||||
// 笔刷类中
|
||||
setGlowIntensity(intensity) {
|
||||
this.glowIntensity = intensity;
|
||||
// 更新笔刷效果
|
||||
}
|
||||
|
||||
// 使用时
|
||||
const neonBrush = brushManager.setBrushType('neon');
|
||||
if (neonBrush && typeof neonBrush.setGlowIntensity === 'function') {
|
||||
neonBrush.setGlowIntensity(15);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何创建复杂的自定义笔刷效果?
|
||||
|
||||
对于复杂的效果,可以:
|
||||
|
||||
1. 覆盖fabric.js笔刷的关键方法,如`onMouseMove`
|
||||
2. 使用自定义渲染器处理绘制
|
||||
3. 结合Canvas API创建特殊效果
|
||||
|
||||
### 如何获取所有注册的笔刷?
|
||||
|
||||
```javascript
|
||||
// 获取所有笔刷
|
||||
const allBrushes = brushRegistry.getAllBrushes();
|
||||
|
||||
// 获取所有分类
|
||||
const categories = brushRegistry.getCategories();
|
||||
|
||||
// 获取指定分类的笔刷
|
||||
const artisticBrushes = brushRegistry.getBrushesByCategory('艺术笔刷');
|
||||
```
|
||||
@@ -0,0 +1,617 @@
|
||||
/**
|
||||
* 材质预设管理器
|
||||
* 负责管理所有材质预设,包括内置预设和用户自定义预设
|
||||
*/
|
||||
export class TexturePresetManager {
|
||||
constructor() {
|
||||
// 内置材质预设
|
||||
this.builtInTextures = [];
|
||||
|
||||
// 用户自定义材质
|
||||
this.customTextures = [];
|
||||
|
||||
// 材质分类
|
||||
this.categories = new Map();
|
||||
|
||||
// 材质缓存
|
||||
this.textureCache = new Map();
|
||||
|
||||
// 事件监听器
|
||||
this.listeners = {
|
||||
textureAdded: [],
|
||||
textureRemoved: [],
|
||||
textureUpdated: [],
|
||||
};
|
||||
|
||||
// 初始化内置材质
|
||||
this._initBuiltInTextures();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化内置材质预设
|
||||
* @private
|
||||
*/
|
||||
_initBuiltInTextures() {
|
||||
// 基于项目中的texture文件夹内容创建预设
|
||||
const textureList = [
|
||||
// 基础纹理
|
||||
{
|
||||
id: "texture0",
|
||||
name: "纸质纹理",
|
||||
category: "基础纹理",
|
||||
path: "/src/assets/texture/texture0.webp",
|
||||
},
|
||||
{
|
||||
id: "texture1",
|
||||
name: "粗糙表面",
|
||||
category: "基础纹理",
|
||||
path: "/src/assets/texture/texture1.webp",
|
||||
},
|
||||
{
|
||||
id: "texture2",
|
||||
name: "细腻纹理",
|
||||
category: "基础纹理",
|
||||
path: "/src/assets/texture/texture2.webp",
|
||||
},
|
||||
{
|
||||
id: "texture3",
|
||||
name: "颗粒质感",
|
||||
category: "基础纹理",
|
||||
path: "/src/assets/texture/texture3.webp",
|
||||
},
|
||||
{
|
||||
id: "texture4",
|
||||
name: "布料纹理",
|
||||
category: "基础纹理",
|
||||
path: "/src/assets/texture/texture4.webp",
|
||||
},
|
||||
{
|
||||
id: "texture5",
|
||||
name: "木质纹理",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture5.webp",
|
||||
},
|
||||
{
|
||||
id: "texture6",
|
||||
name: "石材纹理",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture6.webp",
|
||||
},
|
||||
{
|
||||
id: "texture7",
|
||||
name: "金属质感",
|
||||
category: "金属纹理",
|
||||
path: "/src/assets/texture/texture7.webp",
|
||||
},
|
||||
{
|
||||
id: "texture8",
|
||||
name: "皮革纹理",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture8.webp",
|
||||
},
|
||||
{
|
||||
id: "texture9",
|
||||
name: "水彩纸质",
|
||||
category: "艺术纹理",
|
||||
path: "/src/assets/texture/texture9.webp",
|
||||
},
|
||||
{
|
||||
id: "texture10",
|
||||
name: "画布纹理",
|
||||
category: "艺术纹理",
|
||||
path: "/src/assets/texture/texture10.webp",
|
||||
},
|
||||
{
|
||||
id: "texture11",
|
||||
name: "沙砾质感",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture11.webp",
|
||||
},
|
||||
{
|
||||
id: "texture12",
|
||||
name: "水波纹理",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture12.webp",
|
||||
},
|
||||
{
|
||||
id: "texture13",
|
||||
name: "云朵纹理",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture13.webp",
|
||||
},
|
||||
{
|
||||
id: "texture14",
|
||||
name: "火焰纹理",
|
||||
category: "特效纹理",
|
||||
path: "/src/assets/texture/texture14.webp",
|
||||
},
|
||||
{
|
||||
id: "texture15",
|
||||
name: "烟雾效果",
|
||||
category: "特效纹理",
|
||||
path: "/src/assets/texture/texture15.webp",
|
||||
},
|
||||
{
|
||||
id: "texture16",
|
||||
name: "星空纹理",
|
||||
category: "特效纹理",
|
||||
path: "/src/assets/texture/texture16.webp",
|
||||
},
|
||||
{
|
||||
id: "texture17",
|
||||
name: "大理石纹",
|
||||
category: "石材纹理",
|
||||
path: "/src/assets/texture/texture17.webp",
|
||||
},
|
||||
{
|
||||
id: "texture18",
|
||||
name: "花岗岩纹",
|
||||
category: "石材纹理",
|
||||
path: "/src/assets/texture/texture18.webp",
|
||||
},
|
||||
{
|
||||
id: "texture19",
|
||||
name: "竹纹理",
|
||||
category: "自然纹理",
|
||||
path: "/src/assets/texture/texture19.webp",
|
||||
},
|
||||
{
|
||||
id: "texture20",
|
||||
name: "抽象图案",
|
||||
category: "艺术纹理",
|
||||
path: "/src/assets/texture/texture20.webp",
|
||||
},
|
||||
];
|
||||
|
||||
// 添加内置材质
|
||||
textureList.forEach((texture) => {
|
||||
this.builtInTextures.push({
|
||||
id: texture.id,
|
||||
name: texture.name,
|
||||
category: texture.category,
|
||||
path: texture.path,
|
||||
type: "builtin",
|
||||
preview: texture.path, // 使用原图作为预览
|
||||
description: `内置${texture.category} - ${texture.name}`,
|
||||
tags: [texture.category.replace("纹理", ""), "内置"],
|
||||
created: new Date().toISOString(),
|
||||
// 默认属性
|
||||
defaultSettings: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
repeat: "repeat",
|
||||
angle: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// 添加到分类
|
||||
if (!this.categories.has(texture.category)) {
|
||||
this.categories.set(texture.category, []);
|
||||
}
|
||||
this.categories.get(texture.category).push(texture.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有材质(内置 + 自定义)
|
||||
* @returns {Array} 材质数组
|
||||
*/
|
||||
getAllTextures() {
|
||||
return [...this.builtInTextures, ...this.customTextures];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取材质
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Object|null} 材质对象
|
||||
*/
|
||||
getTextureById(textureId) {
|
||||
return this.getAllTextures().find((texture) => texture.id === textureId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类获取材质
|
||||
* @param {String} category 分类名称
|
||||
* @returns {Array} 材质数组
|
||||
*/
|
||||
getTexturesByCategory(category) {
|
||||
return this.getAllTextures().filter((texture) => texture.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分类
|
||||
* @returns {Array} 分类名称数组
|
||||
*/
|
||||
getCategories() {
|
||||
const categories = new Set();
|
||||
this.getAllTextures().forEach((texture) => {
|
||||
categories.add(texture.category);
|
||||
});
|
||||
return Array.from(categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义材质
|
||||
* @param {Object} textureData 材质数据
|
||||
* @returns {String} 材质ID
|
||||
*/
|
||||
addCustomTexture(textureData) {
|
||||
const textureId =
|
||||
textureData.id || `custom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const texture = {
|
||||
id: textureId,
|
||||
name: textureData.name || "自定义材质",
|
||||
category: textureData.category || "自定义材质",
|
||||
path: textureData.path || textureData.dataUrl,
|
||||
type: "custom",
|
||||
preview: textureData.preview || textureData.path || textureData.dataUrl,
|
||||
description: textureData.description || "用户自定义材质",
|
||||
tags: textureData.tags || ["自定义"],
|
||||
created: new Date().toISOString(),
|
||||
defaultSettings: {
|
||||
scale: textureData.scale || 1,
|
||||
opacity: textureData.opacity || 1,
|
||||
repeat: textureData.repeat || "repeat",
|
||||
angle: textureData.angle || 0,
|
||||
...textureData.defaultSettings,
|
||||
},
|
||||
// 保存原始文件信息
|
||||
file: textureData.file || null,
|
||||
dataUrl: textureData.dataUrl || null,
|
||||
};
|
||||
|
||||
this.customTextures.push(texture);
|
||||
|
||||
// 添加到分类
|
||||
if (!this.categories.has(texture.category)) {
|
||||
this.categories.set(texture.category, []);
|
||||
}
|
||||
this.categories.get(texture.category).push(textureId);
|
||||
|
||||
// 触发事件
|
||||
this._triggerEvent("textureAdded", texture);
|
||||
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义材质
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Boolean} 是否删除成功
|
||||
*/
|
||||
removeCustomTexture(textureId) {
|
||||
const index = this.customTextures.findIndex((texture) => texture.id === textureId);
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const texture = this.customTextures[index];
|
||||
|
||||
// 只能删除自定义材质
|
||||
if (texture.type !== "custom") {
|
||||
console.warn("不能删除内置材质");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.customTextures.splice(index, 1);
|
||||
|
||||
// 从分类中移除
|
||||
if (this.categories.has(texture.category)) {
|
||||
const categoryTextures = this.categories.get(texture.category);
|
||||
const categoryIndex = categoryTextures.indexOf(textureId);
|
||||
if (categoryIndex !== -1) {
|
||||
categoryTextures.splice(categoryIndex, 1);
|
||||
}
|
||||
|
||||
// 如果分类为空且不是内置分类,删除分类
|
||||
if (categoryTextures.length === 0 && texture.category === "自定义材质") {
|
||||
this.categories.delete(texture.category);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
this.textureCache.delete(textureId);
|
||||
|
||||
// 触发事件
|
||||
this._triggerEvent("textureRemoved", texture);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新材质信息
|
||||
* @param {String} textureId 材质ID
|
||||
* @param {Object} updates 更新数据
|
||||
* @returns {Boolean} 是否更新成功
|
||||
*/
|
||||
updateTexture(textureId, updates) {
|
||||
const texture = this.getTextureById(textureId);
|
||||
if (!texture || texture.type === "builtin") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新材质属性
|
||||
Object.assign(texture, updates);
|
||||
|
||||
// 触发事件
|
||||
this._triggerEvent("textureUpdated", texture);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材质预览URL
|
||||
* @param {Object} texture 材质对象
|
||||
* @returns {String} 预览URL
|
||||
*/
|
||||
getTexturePreviewUrl(texture) {
|
||||
if (!texture) return null;
|
||||
|
||||
// 如果有预览图,使用预览图
|
||||
if (texture.preview) {
|
||||
return texture.preview;
|
||||
}
|
||||
|
||||
// 否则使用原图
|
||||
return texture.path || texture.dataUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载材质图像
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Promise<HTMLImageElement>} 图像对象
|
||||
*/
|
||||
loadTextureImage(textureId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查缓存
|
||||
if (this.textureCache.has(textureId)) {
|
||||
resolve(this.textureCache.get(textureId));
|
||||
return;
|
||||
}
|
||||
|
||||
const texture = this.getTextureById(textureId);
|
||||
if (!texture) {
|
||||
reject(new Error(`材质 ${textureId} 不存在`));
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
|
||||
img.onload = () => {
|
||||
// 缓存图像
|
||||
this.textureCache.set(textureId, img);
|
||||
resolve(img);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error(`材质 ${textureId} 加载失败`));
|
||||
};
|
||||
|
||||
img.src = texture.path || texture.dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索材质
|
||||
* @param {String} query 搜索关键词
|
||||
* @returns {Array} 匹配的材质数组
|
||||
*/
|
||||
searchTextures(query) {
|
||||
if (!query) return this.getAllTextures();
|
||||
|
||||
const searchTerm = query.toLowerCase();
|
||||
return this.getAllTextures().filter((texture) => {
|
||||
return (
|
||||
texture.name.toLowerCase().includes(searchTerm) ||
|
||||
texture.category.toLowerCase().includes(searchTerm) ||
|
||||
texture.description.toLowerCase().includes(searchTerm) ||
|
||||
texture.tags.some((tag) => tag.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存自定义材质到本地存储
|
||||
*/
|
||||
saveCustomTexturesToStorage() {
|
||||
try {
|
||||
const customTexturesData = this.customTextures.map((texture) => ({
|
||||
...texture,
|
||||
// 不保存file对象到localStorage
|
||||
file: null,
|
||||
}));
|
||||
|
||||
localStorage.setItem("canvasEditor_customTextures", JSON.stringify(customTexturesData));
|
||||
} catch (error) {
|
||||
console.error("保存自定义材质失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储加载自定义材质
|
||||
*/
|
||||
loadCustomTexturesFromStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem("canvasEditor_customTextures");
|
||||
if (stored) {
|
||||
const customTexturesData = JSON.parse(stored);
|
||||
this.customTextures = customTexturesData;
|
||||
|
||||
// 重建分类索引
|
||||
this.customTextures.forEach((texture) => {
|
||||
if (!this.categories.has(texture.category)) {
|
||||
this.categories.set(texture.category, []);
|
||||
}
|
||||
if (!this.categories.get(texture.category).includes(texture.id)) {
|
||||
this.categories.get(texture.category).push(texture.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载自定义材质失败:", error);
|
||||
this.customTextures = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建材质预设
|
||||
* @param {String} name 预设名称
|
||||
* @param {Object} settings 材质设置
|
||||
* @returns {String} 预设ID
|
||||
*/
|
||||
createTexturePreset(name, settings) {
|
||||
const presetId = `preset_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const preset = {
|
||||
id: presetId,
|
||||
name: name,
|
||||
type: "preset",
|
||||
category: "材质预设",
|
||||
created: new Date().toISOString(),
|
||||
settings: {
|
||||
textureId: settings.textureId,
|
||||
scale: settings.scale || 1,
|
||||
opacity: settings.opacity || 1,
|
||||
repeat: settings.repeat || "repeat",
|
||||
angle: settings.angle || 0,
|
||||
brushSize: settings.brushSize || 5,
|
||||
brushOpacity: settings.brushOpacity || 1,
|
||||
brushColor: settings.brushColor || "#000000",
|
||||
},
|
||||
};
|
||||
|
||||
this.customTextures.push(preset);
|
||||
this._triggerEvent("textureAdded", preset);
|
||||
|
||||
return presetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用材质预设
|
||||
* @param {String} presetId 预设ID
|
||||
* @returns {Object|null} 预设设置
|
||||
*/
|
||||
applyTexturePreset(presetId) {
|
||||
const preset = this.getTextureById(presetId);
|
||||
if (!preset || preset.type !== "preset") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preset.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
* @param {String} event 事件名称
|
||||
* @param {Function} callback 回调函数
|
||||
*/
|
||||
addEventListener(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
* @param {String} event 事件名称
|
||||
* @param {Function} callback 回调函数
|
||||
*/
|
||||
removeEventListener(event, callback) {
|
||||
if (this.listeners[event]) {
|
||||
const index = this.listeners[event].indexOf(callback);
|
||||
if (index !== -1) {
|
||||
this.listeners[event].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param {String} event 事件名称
|
||||
* @param {*} data 事件数据
|
||||
* @private
|
||||
*/
|
||||
_triggerEvent(event, data) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((callback) => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`执行 ${event} 事件监听器出错:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.textureCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
builtInCount: this.builtInTextures.length,
|
||||
customCount: this.customTextures.length,
|
||||
totalCount: this.getAllTextures().length,
|
||||
categoriesCount: this.getCategories().length,
|
||||
cacheSize: this.textureCache.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证纹理文件
|
||||
* @param {File} file 要验证的文件
|
||||
* @returns {Boolean} 是否为有效的纹理文件
|
||||
*/
|
||||
validateTextureFile(file) {
|
||||
if (!file) {
|
||||
console.warn("文件不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith("image/")) {
|
||||
console.warn("文件类型无效,必须是图片文件");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为 10MB)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
console.warn("文件大小超过限制(10MB)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查支持的图片格式
|
||||
const supportedTypes = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
];
|
||||
|
||||
if (!supportedTypes.includes(file.type)) {
|
||||
console.warn("不支持的图片格式");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const texturePresetManager = new TexturePresetManager();
|
||||
|
||||
// 导出单例
|
||||
export default texturePresetManager;
|
||||
1000
src/components/Canvas/DepthCanvas/manager/brushes/brushManager.js
Normal file
1000
src/components/Canvas/DepthCanvas/manager/brushes/brushManager.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1596
src/components/Canvas/DepthCanvas/manager/brushes/fabric.brushes.js
Normal file
1596
src/components/Canvas/DepthCanvas/manager/brushes/fabric.brushes.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,245 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 蜡笔笔刷
|
||||
* 模拟蜡笔效果,具有颗粒感和纹理
|
||||
*/
|
||||
export class CrayonBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "crayon",
|
||||
name: options.t("Canvas.Crayon"),
|
||||
description: "模拟蜡笔效果,具有颗粒感和纹理",
|
||||
category: options.t("Canvas.SpecialEffectsBrush"),
|
||||
icon: "crayon",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 蜡笔笔刷特有属性
|
||||
this._baseWidth = options._baseWidth || 15;
|
||||
this._size = options._size || 0;
|
||||
this._sep = options._sep || options._sep === 0 ? options._sep : 3;
|
||||
this._inkAmount = options._inkAmount || 10;
|
||||
this.randomness = options.randomness || 0.5; // 随机性
|
||||
this.texture = options.texture || "default"; // 纹理类型
|
||||
this.t = options.t
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生蜡笔笔刷
|
||||
this.brush = new fabric.CrayonBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
// 更新笔刷相关属性
|
||||
this._baseWidth = options.width / 2;
|
||||
this._size = options.width / 2 + this._baseWidth;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 蜡笔笔刷特有属性
|
||||
if (options._baseWidth !== undefined) {
|
||||
brush._baseWidth = options._baseWidth;
|
||||
this._baseWidth = options._baseWidth;
|
||||
this._size = this.width / 2 + this._baseWidth;
|
||||
}
|
||||
|
||||
if (options._sep !== undefined) {
|
||||
brush._sep = options._sep;
|
||||
this._sep = options._sep;
|
||||
}
|
||||
|
||||
if (options._inkAmount !== undefined) {
|
||||
brush._inkAmount = options._inkAmount;
|
||||
this._inkAmount = options._inkAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颗粒分离度
|
||||
* @param {Number} sep 分离度值
|
||||
*/
|
||||
setSeparation(sep) {
|
||||
this._sep = Math.max(0.5, Math.min(10, sep));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._sep = this._sep;
|
||||
}
|
||||
|
||||
return this._sep;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置墨量
|
||||
* @param {Number} amount 墨量值
|
||||
*/
|
||||
setInkAmount(amount) {
|
||||
this._inkAmount = Math.max(1, Math.min(50, amount));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._inkAmount = this._inkAmount;
|
||||
}
|
||||
|
||||
return this._inkAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置随机性
|
||||
* @param {Number} value 随机性值(0-1)
|
||||
*/
|
||||
setRandomness(value) {
|
||||
this.randomness = Math.max(0, Math.min(1, value));
|
||||
return this.randomness;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理类型
|
||||
* @param {String} type 纹理类型
|
||||
*/
|
||||
setTexture(type) {
|
||||
this.texture = type;
|
||||
// 实际应用可能需要更多的实现逻辑
|
||||
return this.texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义蜡笔笔刷特有属性
|
||||
const crayonProperties = [
|
||||
{
|
||||
id: "separation",
|
||||
name: this.t('Canvas.ParticleSeparationDegree'),
|
||||
type: "slider",
|
||||
defaultValue: this._sep,
|
||||
min: 0.5,
|
||||
max: 10,
|
||||
step: 0.5,
|
||||
description: this.t('Canvas.ParticleSeparationDegreeDescription'),
|
||||
category: this.t('Canvas.PencilSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "inkAmount",
|
||||
name: this.t('Canvas.TheAmountOfInk'),
|
||||
type: "slider",
|
||||
defaultValue: this._inkAmount,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: this.t('Canvas.TheAmountOfInkDescription'),
|
||||
category: this.t('Canvas.PencilSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "randomness",
|
||||
name: this.t('Canvas.randomness'),
|
||||
type: "slider",
|
||||
defaultValue: this.randomness,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.randomnessDescription'),
|
||||
category: this.t('Canvas.PencilSettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "texture",
|
||||
name: this.t('Canvas.TextureType'),
|
||||
type: "select",
|
||||
defaultValue: this.texture,
|
||||
options: [
|
||||
{ value: "default", label: this.t('Canvas.Default') },
|
||||
{ value: "rough", label: this.t('Canvas.Rough') },
|
||||
{ value: "smooth", label: this.t('Canvas.Smooth') },
|
||||
],
|
||||
description: this.t('Canvas.TextureTypeDescription'),
|
||||
category: this.t('Canvas.PencilSettings'),
|
||||
order: 130,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...crayonProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理蜡笔笔刷特有属性
|
||||
if (propId === "separation") {
|
||||
this.setSeparation(value);
|
||||
return true;
|
||||
} else if (propId === "inkAmount") {
|
||||
this.setInkAmount(value);
|
||||
return true;
|
||||
} else if (propId === "randomness") {
|
||||
this.setRandomness(value);
|
||||
return true;
|
||||
} else if (propId === "texture") {
|
||||
this.setTexture(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIzMCIgeT0iMzAiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=";
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 钢笔笔刷
|
||||
* 模拟钢笔效果,具有变化的透明度
|
||||
*/
|
||||
export class CustomPenBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "pen",
|
||||
name: options.t('Canvas.Pen'),
|
||||
description: "模拟钢笔效果,具有变化的透明度",
|
||||
category: options.t('Canvas.BasicBrushes'),
|
||||
icon: "pen",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 钢笔笔刷特有属性
|
||||
this._baseWidth = options._baseWidth || 15;
|
||||
this._lineWidth = options._lineWidth || 2;
|
||||
this.inkOpacityMin = options.inkOpacityMin || 0.2;
|
||||
this.inkOpacityMax = options.inkOpacityMax || 0.6;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生钢笔笔刷
|
||||
this.brush = new fabric.PenBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
// 更新笔刷相关属性
|
||||
this._baseWidth = options.width / 2;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 钢笔笔刷特有属性
|
||||
if (options._baseWidth !== undefined) {
|
||||
brush._baseWidth = options._baseWidth;
|
||||
this._baseWidth = options._baseWidth;
|
||||
}
|
||||
|
||||
if (options._lineWidth !== undefined) {
|
||||
brush._lineWidth = options._lineWidth;
|
||||
this._lineWidth = options._lineWidth;
|
||||
}
|
||||
|
||||
// 确保线条连接设置正确
|
||||
brush.canvas.contextTop.lineJoin = "round";
|
||||
brush.canvas.contextTop.lineCap = "round";
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最小墨水透明度
|
||||
* @param {Number} opacity 透明度值(0-1)
|
||||
*/
|
||||
setInkOpacityMin(opacity) {
|
||||
this.inkOpacityMin = Math.max(0.1, Math.min(0.5, opacity));
|
||||
return this.inkOpacityMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大墨水透明度
|
||||
* @param {Number} opacity 透明度值(0-1)
|
||||
*/
|
||||
setInkOpacityMax(opacity) {
|
||||
this.inkOpacityMax = Math.max(0.3, Math.min(1, opacity));
|
||||
return this.inkOpacityMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义钢笔笔刷特有属性
|
||||
const penProperties = [
|
||||
{
|
||||
id: "lineWidth",
|
||||
name: this.t('Canvas.PenWidth'),
|
||||
type: "slider",
|
||||
defaultValue: this._lineWidth,
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 0.5,
|
||||
description: this.t('Canvas.PenWidthDescription'),
|
||||
category: this.t('Canvas.PenSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "inkOpacityMin",
|
||||
name: this.t('Canvas.PenMinimumInkTransparency'),
|
||||
type: "slider",
|
||||
defaultValue: this.inkOpacityMin,
|
||||
min: 0.1,
|
||||
max: 0.5,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.PenMinimumInkTransparencyDescription'),
|
||||
category: this.t('Canvas.PenSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "inkOpacityMax",
|
||||
name: this.t('Canvas.PenMaximumInkTransparency'),
|
||||
type: "slider",
|
||||
defaultValue: this.inkOpacityMax,
|
||||
min: 0.3,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.PenMaximumInkTransparencyDescription'),
|
||||
category: this.t('Canvas.PenSettings'),
|
||||
order: 120,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...penProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理钢笔笔刷特有属性
|
||||
if (propId === "lineWidth") {
|
||||
this._lineWidth = value;
|
||||
if (this.brush) {
|
||||
this.brush._lineWidth = value;
|
||||
}
|
||||
return true;
|
||||
} else if (propId === "inkOpacityMin") {
|
||||
this.setInkOpacityMin(value);
|
||||
return true;
|
||||
} else if (propId === "inkOpacityMax") {
|
||||
this.setInkOpacityMax(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjAgMjBMODAgODBNMjAgODBMODAgMjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 毛发笔刷
|
||||
* 创建类似于毛发或草的效果
|
||||
*/
|
||||
export class FurBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "fur",
|
||||
name: options.t('Canvas.Longfur'),
|
||||
description: "创建类似于毛发或草的纹理效果",
|
||||
category: options.t('Canvas.SpecialEffectsBrush'),
|
||||
icon: "fur",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 毛发笔刷特有属性
|
||||
this.furLength = options.furLength || 10;
|
||||
this.furDensity = options.furDensity || 0.7;
|
||||
this.furRandomness = options.furRandomness || 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生毛发笔刷
|
||||
this.brush = new fabric.FurBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 这里可以添加对毛发笔刷特有属性的配置
|
||||
// 由于fabric.FurBrush的原始实现可能没有直接暴露这些属性,
|
||||
// 我们可能需要在onMouseMove等事件中动态调整行为
|
||||
|
||||
// 存储特有属性,供后续使用
|
||||
if (options.furLength !== undefined) {
|
||||
this.furLength = options.furLength;
|
||||
}
|
||||
|
||||
if (options.furDensity !== undefined) {
|
||||
this.furDensity = options.furDensity;
|
||||
}
|
||||
|
||||
if (options.furRandomness !== undefined) {
|
||||
this.furRandomness = options.furRandomness;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发长度
|
||||
* @param {Number} length 长度值
|
||||
*/
|
||||
setFurLength(length) {
|
||||
this.furLength = Math.max(1, Math.min(50, length));
|
||||
return this.furLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发密度
|
||||
* @param {Number} density 密度值(0-1)
|
||||
*/
|
||||
setFurDensity(density) {
|
||||
this.furDensity = Math.max(0.1, Math.min(1, density));
|
||||
return this.furDensity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发随机性
|
||||
* @param {Number} randomness 随机性值(0-1)
|
||||
*/
|
||||
setFurRandomness(randomness) {
|
||||
this.furRandomness = Math.max(0, Math.min(1, randomness));
|
||||
return this.furRandomness;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义毛发笔刷特有属性
|
||||
const furProperties = [
|
||||
{
|
||||
id: "furLength",
|
||||
name: this.t('Canvas.FurLength'),
|
||||
type: "slider",
|
||||
defaultValue: this.furLength,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: this.t('Canvas.FurLengthDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "furDensity",
|
||||
name: this.t('Canvas.FurDensity'),
|
||||
type: "slider",
|
||||
defaultValue: this.furDensity,
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.FurDensityDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "furRandomness",
|
||||
name: this.t('Canvas.FurRandomness'),
|
||||
type: "slider",
|
||||
defaultValue: this.furRandomness,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.FurRandomnessDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 120,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...furProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理毛发笔刷特有属性
|
||||
if (propId === "furLength") {
|
||||
this.setFurLength(value);
|
||||
return true;
|
||||
} else if (propId === "furDensity") {
|
||||
this.setFurDensity(value);
|
||||
return true;
|
||||
} else if (propId === "furRandomness") {
|
||||
this.setFurRandomness(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMTAgODBMNTAgMjBNMjAgODBMNjAgMjBNMzAgODBMNzAgMjBNNDAgODBMODAgMjBNNTAgODBMOTAgMjAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIi8+PC9zdmc+";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 水墨笔刷
|
||||
* 模拟中国传统水墨画效果
|
||||
*/
|
||||
export class InkBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "ink",
|
||||
name: options.t('Canvas.Ink'),
|
||||
description: "模拟中国传统水墨画效果,墨色深浅不一",
|
||||
category: options.t('Canvas.SpecialEffectsBrush'),
|
||||
icon: "ink",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 水墨笔刷特有属性
|
||||
this._baseWidth = options._baseWidth || 15;
|
||||
this._inkAmount = options._inkAmount || 7;
|
||||
this._range = options._range || 10;
|
||||
this.splashEnabled = options.splashEnabled !== undefined ? options.splashEnabled : true;
|
||||
this.splashSize = options.splashSize || 5;
|
||||
this.splashDistance = options.splashDistance || 30;
|
||||
this.t = options.t
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生水墨笔刷
|
||||
this.brush = new fabric.InkBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
this._baseWidth = options.width / 2;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 水墨笔刷特有属性
|
||||
if (options._inkAmount !== undefined) {
|
||||
brush._inkAmount = options._inkAmount;
|
||||
this._inkAmount = options._inkAmount;
|
||||
}
|
||||
|
||||
if (options._range !== undefined) {
|
||||
brush._range = options._range;
|
||||
this._range = options._range;
|
||||
}
|
||||
|
||||
// 更新溅墨相关配置(这需要修改原始InkBrush的drawSplash方法)
|
||||
if (this.splashEnabled !== undefined) {
|
||||
// 由于原始InkBrush没有直接暴露这个配置,我们可能需要覆盖方法
|
||||
// 这里仅保存配置,实际逻辑需要在brush创建后处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置墨量
|
||||
* @param {Number} amount 墨量值
|
||||
*/
|
||||
setInkAmount(amount) {
|
||||
this._inkAmount = Math.max(1, Math.min(20, amount));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._inkAmount = this._inkAmount;
|
||||
}
|
||||
|
||||
return this._inkAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置笔触范围
|
||||
* @param {Number} range 范围值
|
||||
*/
|
||||
setRange(range) {
|
||||
this._range = Math.max(5, Math.min(50, range));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._range = this._range;
|
||||
}
|
||||
|
||||
return this._range;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用溅墨效果
|
||||
* @param {Boolean} enabled 是否启用
|
||||
*/
|
||||
setSplashEnabled(enabled) {
|
||||
this.splashEnabled = enabled;
|
||||
|
||||
// 实际应用需要更多的逻辑来支持这个功能
|
||||
// 由于需要修改fabric.InkBrush的内部行为
|
||||
|
||||
return this.splashEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置溅墨大小
|
||||
* @param {Number} size 大小值
|
||||
*/
|
||||
setSplashSize(size) {
|
||||
this.splashSize = Math.max(1, Math.min(20, size));
|
||||
return this.splashSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置溅墨距离
|
||||
* @param {Number} distance 距离值
|
||||
*/
|
||||
setSplashDistance(distance) {
|
||||
this.splashDistance = Math.max(10, Math.min(100, distance));
|
||||
return this.splashDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义水墨笔刷特有属性
|
||||
const inkProperties = [
|
||||
{
|
||||
id: "inkAmount",
|
||||
name: this.t('Canvas.InkAmount'),
|
||||
type: "slider",
|
||||
defaultValue: this._inkAmount,
|
||||
min: 1,
|
||||
max: 20,
|
||||
step: 1,
|
||||
description: this.t('Canvas.InkAmountDescription'),
|
||||
category: this.t('Canvas.InkSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "range",
|
||||
name: this.t('Canvas.InkRange'),
|
||||
type: "slider",
|
||||
defaultValue: this._range,
|
||||
min: 5,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: this.t('Canvas.InkRangeDescription'),
|
||||
category: this.t('Canvas.InkSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "splashEnabled",
|
||||
name: this.t('Canvas.InkSplashEnabled'),
|
||||
type: "checkbox",
|
||||
defaultValue: this.splashEnabled,
|
||||
description: this.t('Canvas.InkSplashEnabledDescription'),
|
||||
category: this.t('Canvas.InkSettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "splashSize",
|
||||
name: this.t('Canvas.InkSize'),
|
||||
type: "slider",
|
||||
defaultValue: this.splashSize,
|
||||
min: 1,
|
||||
max: 20,
|
||||
step: 1,
|
||||
description: this.t('Canvas.InkSizeDescription'),
|
||||
category: this.t('Canvas.InkSettings'),
|
||||
order: 130,
|
||||
visibleWhen: { splashEnabled: true },
|
||||
},
|
||||
{
|
||||
id: "splashDistance",
|
||||
name: this.t('Canvas.InkRandomness'),
|
||||
type: "slider",
|
||||
defaultValue: this.splashDistance,
|
||||
min: 10,
|
||||
max: 100,
|
||||
step: 5,
|
||||
description: this.t('Canvas.InkRandomnessDescription'),
|
||||
category: this.t('Canvas.InkSettings'),
|
||||
order: 140,
|
||||
visibleWhen: { splashEnabled: true },
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...inkProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理水墨笔刷特有属性
|
||||
if (propId === "inkAmount") {
|
||||
this.setInkAmount(value);
|
||||
return true;
|
||||
} else if (propId === "range") {
|
||||
this.setRange(value);
|
||||
return true;
|
||||
} else if (propId === "splashEnabled") {
|
||||
this.setSplashEnabled(value);
|
||||
return true;
|
||||
} else if (propId === "splashSize") {
|
||||
this.setSplashSize(value);
|
||||
return true;
|
||||
} else if (propId === "splashDistance") {
|
||||
this.setSplashDistance(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjAgODBDNDAgNjAgNjAgNDAgODAgMjAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSI1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiLz48Y2lyY2xlIGN4PSI3MCIgY3k9IjMwIiByPSI1IiBmaWxsPSIjMDAwIi8+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMyIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjMwIiBjeT0iNzAiIHI9IjYiIGZpbGw9IiMwMDAiLz48L3N2Zz4=";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 长毛发笔刷
|
||||
* 创建类似于长毛、毛皮、草或头发的效果
|
||||
*/
|
||||
export class LongfurBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "longfur",
|
||||
name: options.t('Canvas.Longfur'),
|
||||
description: "创建流动的长毛发效果,适合绘制动物毛皮、草或头发",
|
||||
category: options.t('Canvas.SpecialEffectsBrush'),
|
||||
icon: "longfur",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 长毛发笔刷特有属性
|
||||
this.furLength = options.furLength || 20;
|
||||
this.furDensity = options.furDensity || 0.7;
|
||||
this.furFlowFactor = options.furFlowFactor || 0.5;
|
||||
this.furCurvature = options.furCurvature || 0.3;
|
||||
this.randomizeDirection =
|
||||
options.randomizeDirection !== undefined ? options.randomizeDirection : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生长毛发笔刷
|
||||
this.brush = new fabric.LongfurBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 长毛发笔刷特有属性
|
||||
if (options.furLength !== undefined) {
|
||||
this.furLength = options.furLength;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.furLength !== undefined) {
|
||||
brush.furLength = this.furLength;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.furDensity !== undefined) {
|
||||
this.furDensity = options.furDensity;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.furDensity !== undefined) {
|
||||
brush.furDensity = this.furDensity;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.furFlowFactor !== undefined) {
|
||||
this.furFlowFactor = options.furFlowFactor;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.furFlowFactor !== undefined) {
|
||||
brush.furFlowFactor = this.furFlowFactor;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.furCurvature !== undefined) {
|
||||
this.furCurvature = options.furCurvature;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.furCurvature !== undefined) {
|
||||
brush.furCurvature = this.furCurvature;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.randomizeDirection !== undefined) {
|
||||
this.randomizeDirection = options.randomizeDirection;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.randomizeDirection !== undefined) {
|
||||
brush.randomizeDirection = this.randomizeDirection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发长度
|
||||
* @param {Number} length 长度值
|
||||
*/
|
||||
setFurLength(length) {
|
||||
this.furLength = Math.max(5, Math.min(100, length));
|
||||
|
||||
if (this.brush && this.brush.furLength !== undefined) {
|
||||
this.brush.furLength = this.furLength;
|
||||
}
|
||||
|
||||
return this.furLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发密度
|
||||
* @param {Number} density 密度值(0-1)
|
||||
*/
|
||||
setFurDensity(density) {
|
||||
this.furDensity = Math.max(0.1, Math.min(1, density));
|
||||
|
||||
if (this.brush && this.brush.furDensity !== undefined) {
|
||||
this.brush.furDensity = this.furDensity;
|
||||
}
|
||||
|
||||
return this.furDensity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发流动系数
|
||||
* @param {Number} factor 流动系数(0-1)
|
||||
*/
|
||||
setFurFlowFactor(factor) {
|
||||
this.furFlowFactor = Math.max(0, Math.min(1, factor));
|
||||
|
||||
if (this.brush && this.brush.furFlowFactor !== undefined) {
|
||||
this.brush.furFlowFactor = this.furFlowFactor;
|
||||
}
|
||||
|
||||
return this.furFlowFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置毛发弯曲度
|
||||
* @param {Number} curvature 弯曲度(0-1)
|
||||
*/
|
||||
setFurCurvature(curvature) {
|
||||
this.furCurvature = Math.max(0, Math.min(1, curvature));
|
||||
|
||||
if (this.brush && this.brush.furCurvature !== undefined) {
|
||||
this.brush.furCurvature = this.furCurvature;
|
||||
}
|
||||
|
||||
return this.furCurvature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否随机化方向
|
||||
* @param {Boolean} randomize 是否随机化
|
||||
*/
|
||||
setRandomizeDirection(randomize) {
|
||||
this.randomizeDirection = randomize;
|
||||
|
||||
if (this.brush && this.brush.randomizeDirection !== undefined) {
|
||||
this.brush.randomizeDirection = this.randomizeDirection;
|
||||
}
|
||||
|
||||
return this.randomizeDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义长毛发笔刷特有属性
|
||||
const longfurProperties = [
|
||||
{
|
||||
id: "furLength",
|
||||
name: this.t('Canvas.FurLength'),
|
||||
type: "slider",
|
||||
defaultValue: this.furLength,
|
||||
min: 5,
|
||||
max: 100,
|
||||
step: 1,
|
||||
description: this.t('Canvas.FurLengthDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "furDensity",
|
||||
name: this.t('Canvas.FurDensity'),
|
||||
type: "slider",
|
||||
defaultValue: this.furDensity,
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.FurDensityDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "furFlowFactor",
|
||||
name: this.t('Canvas.FlowCoefficient'),
|
||||
type: "slider",
|
||||
defaultValue: this.furFlowFactor,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.FlowCoefficientDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "furCurvature",
|
||||
name: this.t('Canvas.furCurvature'),
|
||||
type: "slider",
|
||||
defaultValue: this.furCurvature,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.furCurvatureDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
id: "randomizeDirection",
|
||||
name: this.t('Canvas.randomizeDirection'),
|
||||
type: "checkbox",
|
||||
defaultValue: this.randomizeDirection,
|
||||
description: this.t('Canvas.randomizeDirectionDescription'),
|
||||
category: this.t('Canvas.FurSettings'),
|
||||
order: 140,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...longfurProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理长毛发笔刷特有属性
|
||||
if (propId === "furLength") {
|
||||
this.setFurLength(value);
|
||||
return true;
|
||||
} else if (propId === "furDensity") {
|
||||
this.setFurDensity(value);
|
||||
return true;
|
||||
} else if (propId === "furFlowFactor") {
|
||||
this.setFurFlowFactor(value);
|
||||
return true;
|
||||
} else if (propId === "furCurvature") {
|
||||
this.setFurCurvature(value);
|
||||
return true;
|
||||
} else if (propId === "randomizeDirection") {
|
||||
this.setRandomizeDirection(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjUgNTBDMjUgNTAgNTAgMTAgNTAgNTBDNTAgNTAgNTAgOTAgNzUgNTAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+PGxpbmUgeDE9IjMwIiB5MT0iNDUiIHgyPSIzMCIgeTI9IjEwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMSIvPjxsaW5lIHgxPSI0MCIgeTE9IjQwIiB4Mj0iNDAiIHkyPSI1IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMSIvPjxsaW5lIHgxPSI1MCIgeTE9IjQwIiB4Mj0iNTAiIHkyPSIxMCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEiLz48bGluZSB4MT0iNjAiIHkxPSI0MCIgeDI9IjYwIiB5Mj0iNSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEiLz48bGluZSB4MT0iNzAiIHkxPSI0NSIgeDI9IjcwIiB5Mj0iMTAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PC9zdmc+";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 马克笔笔刷
|
||||
* 模拟马克笔效果,具有半透明平头笔触
|
||||
*/
|
||||
export class MarkerBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "marker",
|
||||
name: options.t("Canva.Marker"),
|
||||
description: "模拟马克笔效果,具有半透明平头笔触",
|
||||
category: options.t("Canvas.BasicBrushes"),
|
||||
icon: "marker",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 马克笔特有属性
|
||||
this._baseWidth = options._baseWidth || 15;
|
||||
this._lineWidth = options._lineWidth || 2;
|
||||
this.capStyle = options.capStyle || "round"; // "round" 或 "square"
|
||||
this.blendMode = options.blendMode || "multiply"; // 混合模式
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生马克笔笔刷
|
||||
this.brush = new fabric.MarkerBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
// 更新笔刷相关属性
|
||||
this._baseWidth = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
// 马克笔的透明度默认不要太高
|
||||
brush.opacity = Math.min(0.8, options.opacity || 0.6);
|
||||
}
|
||||
|
||||
// 马克笔笔刷特有属性
|
||||
if (options._baseWidth !== undefined) {
|
||||
brush._baseWidth = options._baseWidth;
|
||||
this._baseWidth = options._baseWidth;
|
||||
}
|
||||
|
||||
if (options._lineWidth !== undefined) {
|
||||
brush._lineWidth = options._lineWidth;
|
||||
this._lineWidth = options._lineWidth;
|
||||
}
|
||||
|
||||
// 笔触样式设置
|
||||
brush.canvas.contextTop.lineJoin = "round";
|
||||
brush.canvas.contextTop.lineCap = this.capStyle || "round";
|
||||
|
||||
// 马克笔的混合模式设置
|
||||
if (this.blendMode === "multiply") {
|
||||
brush.canvas.contextTop.globalCompositeOperation = "multiply";
|
||||
} else {
|
||||
brush.canvas.contextTop.globalCompositeOperation = "source-over";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置笔触线宽
|
||||
* @param {Number} width 线宽值
|
||||
*/
|
||||
setLineWidth(width) {
|
||||
this._lineWidth = Math.max(1, Math.min(10, width));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._lineWidth = this._lineWidth;
|
||||
}
|
||||
|
||||
return this._lineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置笔头样式
|
||||
* @param {String} style 笔头样式 ('round' 或 'square')
|
||||
*/
|
||||
setCapStyle(style) {
|
||||
if (style === "round" || style === "square") {
|
||||
this.capStyle = style;
|
||||
|
||||
if (this.brush && this.brush.canvas) {
|
||||
this.brush.canvas.contextTop.lineCap = style;
|
||||
}
|
||||
}
|
||||
|
||||
return this.capStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置混合模式
|
||||
* @param {String} mode 混合模式 ('multiply' 或 'normal')
|
||||
*/
|
||||
setBlendMode(mode) {
|
||||
this.blendMode = mode;
|
||||
|
||||
if (this.brush && this.brush.canvas) {
|
||||
this.brush.canvas.contextTop.globalCompositeOperation =
|
||||
mode === "multiply" ? "multiply" : "source-over";
|
||||
}
|
||||
|
||||
return this.blendMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义马克笔笔刷特有属性
|
||||
const markerProperties = [
|
||||
{
|
||||
id: "lineWidth",
|
||||
name: this.t('Canvas.MarkerWidth'),
|
||||
type: "slider",
|
||||
defaultValue: this._lineWidth,
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 0.5,
|
||||
description: this.t('Canvas.MarkerWidthDescription'),
|
||||
category: this.t('Canvas.MarkerSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "capStyle",
|
||||
name: this.t('Canvas.MarkerCapStyle'),
|
||||
type: "select",
|
||||
defaultValue: this.capStyle,
|
||||
options: [
|
||||
{ value: "round", label: this.t('Canvas.MarkerCapStyleRound') },
|
||||
{ value: "square", label: this.t('Canvas.MarkerCapStyleSquare') },
|
||||
],
|
||||
description: this.t('Canvas.MarkerCapStyleDescription'),
|
||||
category: this.t('Canvas.MarkerSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "blendMode",
|
||||
name: this.t('Canvas.MarkerMixedModel'),
|
||||
type: "select",
|
||||
defaultValue: this.blendMode,
|
||||
options: [
|
||||
{ value: "multiply", label: this.t('Canvas.MarkerMixedModelMultiply') },
|
||||
{ value: "normal", label: this.t('Canvas.MarkerMixedModelNormal') },
|
||||
],
|
||||
description: this.t('Canvas.MarkerMixedModelDescription'),
|
||||
category: this.t('Canvas.MarkerSettings'),
|
||||
order: 120,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...markerProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理马克笔笔刷特有属性
|
||||
if (propId === "lineWidth") {
|
||||
this.setLineWidth(value);
|
||||
return true;
|
||||
} else if (propId === "capStyle") {
|
||||
this.setCapStyle(value);
|
||||
return true;
|
||||
} else if (propId === "blendMode") {
|
||||
this.setBlendMode(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMTAgNTBIOTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyMCIgc3Ryb2tlLW9wYWNpdHk9IjAuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 铅笔笔刷
|
||||
* fabric原生铅笔笔刷的包装类
|
||||
*/
|
||||
export class PencilBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
// console.log(options,'=-===========================')
|
||||
super(canvas, {
|
||||
id: "pencil",
|
||||
name: "铅笔",
|
||||
description: "基础铅笔工具,适合精细线条绘制",
|
||||
category: "基础笔刷",
|
||||
icon: "pencil",
|
||||
t: options.t,
|
||||
...options,
|
||||
});
|
||||
|
||||
// 铅笔笔刷特有属性
|
||||
this.decimate = options.decimate || 0.4;
|
||||
this.strokeLineCap = options.strokeLineCap || "round";
|
||||
this.strokeLineJoin = options.strokeLineJoin || "round";
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric.PencilBrush实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生铅笔笔刷
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
|
||||
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
|
||||
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(this.brush);
|
||||
const self = this; // 保存外部this引用
|
||||
|
||||
this.brush._finalizeAndAddPath = function () {
|
||||
console.log("PencilBrush: _finalizeAndAddPath called");
|
||||
const ctx = this.canvas.contextTop;
|
||||
ctx.closePath();
|
||||
|
||||
// 应用点简化
|
||||
if (this.decimate) {
|
||||
this._points = this.decimatePoints(this._points, this.decimate);
|
||||
}
|
||||
|
||||
console.log("PencilBrush: points count =", this._points ? this._points.length : 0);
|
||||
|
||||
// 检查是否有有效的路径数据
|
||||
if (!this._points || this._points.length < 2) {
|
||||
// 如果点数不足,直接请求重新渲染
|
||||
console.log("PencilBrush: Not enough points, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const pathData = this.convertPointsToSVGPath(this._points);
|
||||
|
||||
const isEmpty = self._isEmptySVGPath(pathData);
|
||||
console.log("PencilBrush: isEmpty =", isEmpty);
|
||||
|
||||
if (isEmpty) {
|
||||
// 如果路径为空,直接请求重新渲染
|
||||
console.log("PencilBrush: Path is empty, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先触发事件,模拟原生行为
|
||||
const path = this.createPath(pathData);
|
||||
this.canvas.fire("before:path:created", { path: path });
|
||||
|
||||
console.log("PencilBrush: Calling convertToImg");
|
||||
|
||||
// 调用 convertToImg 方法将绘制内容转换为图片
|
||||
if (typeof this.convertToImg === "function") {
|
||||
// 确保在转换前设置正确的透明度
|
||||
const currentAlpha = ctx.globalAlpha;
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
|
||||
this.convertToImg();
|
||||
console.log("PencilBrush: convertToImg called successfully");
|
||||
|
||||
// 恢复透明度
|
||||
ctx.globalAlpha = currentAlpha;
|
||||
} else {
|
||||
console.warn("convertToImg method not found, falling back to original behavior");
|
||||
// 如果没有convertToImg方法,回退到原始行为
|
||||
this.canvas.add(path);
|
||||
this.canvas.fire("path:created", { path: path });
|
||||
this.canvas.clearContext(this.canvas.contextTop);
|
||||
}
|
||||
|
||||
// 重置阴影
|
||||
this._resetShadow();
|
||||
};
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SVG 路径是否为空
|
||||
* @private
|
||||
* @param {Array} pathData SVG 路径数据
|
||||
* @returns {Boolean} 是否为空路径
|
||||
*/
|
||||
_isEmptySVGPath(pathData) {
|
||||
if (!pathData || pathData.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查路径是否只包含移动命令或者是一个点
|
||||
let hasDrawing = false;
|
||||
let moveCount = 0;
|
||||
|
||||
for (let i = 0; i < pathData.length; i++) {
|
||||
const command = pathData[i];
|
||||
if (command[0] === "M") {
|
||||
moveCount++;
|
||||
} else if (command[0] === "L" || command[0] === "Q" || command[0] === "C") {
|
||||
hasDrawing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有移动命令且超过1个,或者没有绘制命令,则认为是空路径
|
||||
return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric.PencilBrush实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined && options.opacity !== undefined) {
|
||||
// 使用RGBA颜色而不是设置globalAlpha
|
||||
brush.color = this._createRGBAColor(options.color, options.opacity);
|
||||
} else if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
} else if (options.opacity !== undefined) {
|
||||
// 如果只设置了透明度,基于当前颜色创建RGBA
|
||||
const currentColor = brush.color || "#000000";
|
||||
brush.color = this._createRGBAColor(currentColor, options.opacity);
|
||||
}
|
||||
|
||||
// 确保不使用globalAlpha,避免圆圈绘制问题
|
||||
if (brush.canvas && brush.canvas.contextTop) {
|
||||
brush.canvas.contextTop.globalAlpha = 1;
|
||||
brush.canvas.contextTop.lineCap = "round";
|
||||
brush.canvas.contextTop.lineJoin = "round";
|
||||
}
|
||||
|
||||
// 特殊属性配置
|
||||
options.decimate = 0;
|
||||
if (options.decimate !== undefined) {
|
||||
brush.decimate = options.decimate;
|
||||
this.decimate = options.decimate;
|
||||
}
|
||||
options.strokeLineCap = "round";
|
||||
if (options.strokeLineCap !== undefined) {
|
||||
brush.strokeLineCap = options.strokeLineCap;
|
||||
this.strokeLineCap = options.strokeLineCap;
|
||||
}
|
||||
options.strokeLineJoin = "round";
|
||||
if (options.strokeLineJoin !== undefined) {
|
||||
brush.strokeLineJoin = options.strokeLineJoin;
|
||||
this.strokeLineJoin = options.strokeLineJoin;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建RGBA颜色字符串
|
||||
* @private
|
||||
* @param {String} color 十六进制颜色或已有颜色
|
||||
* @param {Number} opacity 透明度 (0-1)
|
||||
* @returns {String} RGBA颜色字符串
|
||||
*/
|
||||
_createRGBAColor(color, opacity) {
|
||||
// 如果已经是rgba颜色,先提取RGB部分
|
||||
if (color.startsWith("rgba")) {
|
||||
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
|
||||
if (rgbaMatch) {
|
||||
const [, r, g, b] = rgbaMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是rgb颜色,提取RGB部分
|
||||
if (color.startsWith("rgb")) {
|
||||
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理十六进制颜色
|
||||
if (color.startsWith("#")) {
|
||||
const hex = color.replace("#", "");
|
||||
let r, g, b;
|
||||
|
||||
if (hex.length === 3) {
|
||||
r = parseInt(hex[0] + hex[0], 16);
|
||||
g = parseInt(hex[1] + hex[1], 16);
|
||||
b = parseInt(hex[2] + hex[2], 16);
|
||||
} else if (hex.length === 6) {
|
||||
r = parseInt(hex.substring(0, 2), 16);
|
||||
g = parseInt(hex.substring(2, 4), 16);
|
||||
b = parseInt(hex.substring(4, 6), 16);
|
||||
} else {
|
||||
// 无效的十六进制颜色,使用默认
|
||||
r = g = b = 0;
|
||||
}
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
// 如果是其他格式的颜色,尝试转换(例如颜色名称)
|
||||
// 这里简化处理,实际项目中可以使用更复杂的颜色解析
|
||||
return color; // fallback到原颜色
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
// 定义铅笔笔刷特有属性
|
||||
const pencilProperties = [
|
||||
// {
|
||||
// id: "decimate",
|
||||
// name: "精细度",
|
||||
// type: "slider",
|
||||
// defaultValue: this.decimate,
|
||||
// min: 0,
|
||||
// max: 1,
|
||||
// step: 0.1,
|
||||
// description: "控制笔触路径的简化程度,值越小路径越精细",
|
||||
// category: "铅笔设置",
|
||||
// order: 100,
|
||||
// },
|
||||
// {
|
||||
// id: "strokeLineCap",
|
||||
// name: "线条端点",
|
||||
// type: "select",
|
||||
// defaultValue: this.strokeLineCap,
|
||||
// options: [
|
||||
// { value: "round", label: "圆形" },
|
||||
// { value: "butt", label: "平直" },
|
||||
// { value: "square", label: "方形" },
|
||||
// ],
|
||||
// description: "线条端点的形状",
|
||||
// category: "铅笔设置",
|
||||
// order: 110,
|
||||
// },
|
||||
// {
|
||||
// id: "strokeLineJoin",
|
||||
// name: "线条连接",
|
||||
// type: "select",
|
||||
// defaultValue: this.strokeLineJoin,
|
||||
// options: [
|
||||
// { value: "round", label: "圆角" },
|
||||
// { value: "bevel", label: "斜角" },
|
||||
// { value: "miter", label: "尖角" },
|
||||
// ],
|
||||
// description: "线条拐角的连接方式",
|
||||
// category: "铅笔设置",
|
||||
// order: 120,
|
||||
// },
|
||||
// {
|
||||
// id: "smoothingEnabled",
|
||||
// name: "启用平滑",
|
||||
// type: "checkbox",
|
||||
// defaultValue: false,
|
||||
// description: "是否对线条进行平滑处理",
|
||||
// category: "铅笔设置",
|
||||
// order: 130,
|
||||
// },
|
||||
// {
|
||||
// id: "smoothingFactor",
|
||||
// name: "平滑程度",
|
||||
// type: "slider",
|
||||
// defaultValue: 0.5,
|
||||
// min: 0,
|
||||
// max: 1,
|
||||
// step: 0.05,
|
||||
// description: "线条平滑的强度",
|
||||
// category: "铅笔设置",
|
||||
// order: 140,
|
||||
// // 只有当smoothingEnabled为true时才显示
|
||||
// visibleWhen: { smoothingEnabled: true },
|
||||
// },
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...pencilProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理铅笔特有属性
|
||||
if (propId === "decimate") {
|
||||
this.decimate = value;
|
||||
if (this.brush) {
|
||||
this.brush.decimate = value;
|
||||
return true;
|
||||
}
|
||||
} else if (propId === "strokeLineCap") {
|
||||
this.strokeLineCap = value;
|
||||
if (this.brush) {
|
||||
this.brush.strokeLineCap = value;
|
||||
return true;
|
||||
}
|
||||
} else if (propId === "strokeLineJoin") {
|
||||
this.strokeLineJoin = value;
|
||||
if (this.brush) {
|
||||
this.brush.strokeLineJoin = value;
|
||||
return true;
|
||||
}
|
||||
} else if (propId === "smoothingEnabled") {
|
||||
this.smoothingEnabled = value;
|
||||
// 实现平滑逻辑...
|
||||
return true;
|
||||
} else if (propId === "smoothingFactor") {
|
||||
this.smoothingFactor = value;
|
||||
// 实现平滑度调整...
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
// 实际项目中可以返回一个实际的预览图URL
|
||||
return "data:image/svg+xml;base64,..."; // 示例SVG
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 丝带笔刷
|
||||
* 创建流畅的飘带状线条
|
||||
*/
|
||||
export class RibbonBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "ribbon",
|
||||
name: options.t('Canvas.Ribbon'),
|
||||
description: "创建流畅的飘带状线条,具有动态宽度变化和曲线美感",
|
||||
category: options.t('Canvas.SpecialEffectsBrush'),
|
||||
icon: "ribbon",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 丝带笔刷特有属性
|
||||
this.ribbonWidth = options.ribbonWidth || 20;
|
||||
this.widthVariation = options.widthVariation || 0.5;
|
||||
this.ribbonSmoothness = options.ribbonSmoothness || 0.7;
|
||||
this.gradient = options.gradient !== undefined ? options.gradient : true;
|
||||
this.gradientColors = options.gradientColors || ["#000000", "#555555"];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生丝带笔刷
|
||||
this.brush = new fabric.RibbonBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
// 基于主宽度更新丝带宽度
|
||||
this.ribbonWidth = options.width * 2;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
|
||||
// 如果启用渐变,更新渐变的第一个颜色
|
||||
if (this.gradient && this.gradientColors.length > 0) {
|
||||
this.gradientColors[0] = options.color;
|
||||
this.updateGradient();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
// 如果启用渐变,更新渐变的第一个颜色
|
||||
if (this.gradient && this.gradientColors.length > 0) {
|
||||
this.gradientColors[0] = this._createRGBAColor(brush.color, options.opacity);
|
||||
this.updateGradient();
|
||||
}
|
||||
brush.canvas.freeDrawingBrush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 丝带笔刷特有属性
|
||||
if (options.ribbonWidth !== undefined) {
|
||||
this.ribbonWidth = options.ribbonWidth;
|
||||
|
||||
// 如果原生笔刷支持此属性
|
||||
if (brush.ribbonWidth !== undefined) {
|
||||
brush.ribbonWidth = this.ribbonWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.widthVariation !== undefined) {
|
||||
this.widthVariation = options.widthVariation;
|
||||
|
||||
// 如果原生笔刷支持此属性
|
||||
if (brush.widthVariation !== undefined) {
|
||||
brush.widthVariation = this.widthVariation;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ribbonSmoothness !== undefined) {
|
||||
this.ribbonSmoothness = options.ribbonSmoothness;
|
||||
|
||||
// 如果原生笔刷支持此属性
|
||||
if (brush.ribbonSmoothness !== undefined) {
|
||||
brush.ribbonSmoothness = this.ribbonSmoothness;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.gradient !== undefined) {
|
||||
this.gradient = options.gradient;
|
||||
this.updateGradient();
|
||||
}
|
||||
|
||||
if (options.gradientColors !== undefined) {
|
||||
this.gradientColors = options.gradientColors;
|
||||
this.updateGradient();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新渐变设置
|
||||
* @private
|
||||
*/
|
||||
updateGradient() {
|
||||
if (!this.brush || !this.canvas) return;
|
||||
|
||||
if (this.gradient && this.gradientColors.length >= 2) {
|
||||
// 创建渐变对象
|
||||
const ctx = this.canvas.contextTop;
|
||||
const gradient = ctx.createLinearGradient(0, 0, this.ribbonWidth, 0);
|
||||
|
||||
// 添加渐变色
|
||||
const colorCount = this.gradientColors.length;
|
||||
this.gradientColors.forEach((color, index) => {
|
||||
gradient.addColorStop(index / (colorCount - 1), color);
|
||||
});
|
||||
|
||||
// 如果原生笔刷支持渐变
|
||||
if (typeof this.brush.setGradient === "function") {
|
||||
this.brush.setGradient(gradient);
|
||||
} else if (this.brush.gradient !== undefined) {
|
||||
this.brush.gradient = gradient;
|
||||
}
|
||||
|
||||
// 如果原生笔刷支持渐变标志
|
||||
if (this.brush.useGradient !== undefined) {
|
||||
this.brush.useGradient = true;
|
||||
}
|
||||
} else if (this.brush.useGradient !== undefined) {
|
||||
// 禁用渐变
|
||||
this.brush.useGradient = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置丝带宽度
|
||||
* @param {Number} width 宽度值
|
||||
*/
|
||||
setRibbonWidth(width) {
|
||||
this.ribbonWidth = Math.max(5, Math.min(100, width));
|
||||
|
||||
if (this.brush && this.brush.ribbonWidth !== undefined) {
|
||||
this.brush.ribbonWidth = this.ribbonWidth;
|
||||
}
|
||||
|
||||
// 更新渐变(因为宽度变了)
|
||||
if (this.gradient) {
|
||||
this.updateGradient();
|
||||
}
|
||||
|
||||
return this.ribbonWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置宽度变化率
|
||||
* @param {Number} variation 变化率(0-1)
|
||||
*/
|
||||
setWidthVariation(variation) {
|
||||
this.widthVariation = Math.max(0, Math.min(1, variation));
|
||||
|
||||
if (this.brush && this.brush.widthVariation !== undefined) {
|
||||
this.brush.widthVariation = this.widthVariation;
|
||||
}
|
||||
|
||||
return this.widthVariation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置丝带平滑度
|
||||
* @param {Number} smoothness 平滑度值(0-1)
|
||||
*/
|
||||
setRibbonSmoothness(smoothness) {
|
||||
this.ribbonSmoothness = Math.max(0, Math.min(1, smoothness));
|
||||
|
||||
if (this.brush && this.brush.ribbonSmoothness !== undefined) {
|
||||
this.brush.ribbonSmoothness = this.ribbonSmoothness;
|
||||
}
|
||||
|
||||
return this.ribbonSmoothness;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用渐变效果
|
||||
* @param {Boolean} enabled 是否启用
|
||||
*/
|
||||
setGradient(enabled) {
|
||||
this.gradient = enabled;
|
||||
this.updateGradient();
|
||||
return this.gradient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置渐变颜色
|
||||
* @param {Array} colors 颜色数组
|
||||
*/
|
||||
setGradientColors(colors) {
|
||||
if (Array.isArray(colors) && colors.length >= 2) {
|
||||
this.gradientColors = colors;
|
||||
this.updateGradient();
|
||||
}
|
||||
return this.gradientColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义丝带笔刷特有属性
|
||||
const ribbonProperties = [
|
||||
{
|
||||
id: "ribbonWidth",
|
||||
name: this.t('Canvas.RibbonWidth'),
|
||||
type: "slider",
|
||||
defaultValue: this.ribbonWidth,
|
||||
min: 5,
|
||||
max: 100,
|
||||
step: 5,
|
||||
description: this.t('Canvas.RibbonWidthDescription'),
|
||||
category: this.t('Canvas.RibbonSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "widthVariation",
|
||||
name: this.t('Canvas.RibbonVariation'),
|
||||
type: "slider",
|
||||
defaultValue: this.widthVariation,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.RibbonVariationDescription'),
|
||||
category: this.t('Canvas.RibbonSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "ribbonSmoothness",
|
||||
name: this.t('Canvas.RibbonSmoothness'),
|
||||
type: "slider",
|
||||
defaultValue: this.ribbonSmoothness,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.RibbonSmoothnessDescription'),
|
||||
category: this.t('Canvas.RibbonSettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "gradient",
|
||||
name: this.t('Canvas.RibbonGradient'),
|
||||
type: "checkbox",
|
||||
defaultValue: this.gradient,
|
||||
description: this.t('Canvas.RibbonGradientDescription'),
|
||||
category: this.t('Canvas.RibbonSettings'),
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
id: "gradientColor1",
|
||||
name: this.t('Canvas.RibbonGradientColor1'),
|
||||
type: "color",
|
||||
defaultValue: this.gradientColors[0] || "#000000",
|
||||
description: this.t('Canvas.RibbonGradientColor1Description'),
|
||||
category: this.t('Canvas.RibbonSettings'),
|
||||
order: 140,
|
||||
visibleWhen: { gradient: true },
|
||||
},
|
||||
{
|
||||
id: "gradientColor2",
|
||||
name: this.t('Canvas.RibbonGradientColor2'),
|
||||
type: "color",
|
||||
defaultValue: this.gradientColors[1] || "#555555",
|
||||
description: this.t('Canvas.RibbonGradientColor2Description'),
|
||||
category: this.t('Canvas.RibbonSettings'),
|
||||
order: 150,
|
||||
visibleWhen: { gradient: true },
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...ribbonProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理丝带笔刷特有属性
|
||||
if (propId === "ribbonWidth") {
|
||||
this.setRibbonWidth(value);
|
||||
return true;
|
||||
} else if (propId === "widthVariation") {
|
||||
this.setWidthVariation(value);
|
||||
return true;
|
||||
} else if (propId === "ribbonSmoothness") {
|
||||
this.setRibbonSmoothness(value);
|
||||
return true;
|
||||
} else if (propId === "gradient") {
|
||||
this.setGradient(value);
|
||||
return true;
|
||||
} else if (propId === "gradientColor1") {
|
||||
const colors = [...this.gradientColors];
|
||||
colors[0] = value;
|
||||
this.setGradientColors(colors);
|
||||
return true;
|
||||
} else if (propId === "gradientColor2") {
|
||||
const colors = [...this.gradientColors];
|
||||
colors[1] = value;
|
||||
this.setGradientColors(colors);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDAwIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNTU1Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIwIDUwQzMwIDMwIDUwIDMwIDYwIDUwQzcwIDcwIDgwIDcwIDkwIDUwIiBzdHJva2U9InVybCgjZ3JhZCkiIHN0cm9rZS13aWR0aD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 阴影笔刷
|
||||
* 创建带有阴影效果的绘制,有深浅变化
|
||||
*/
|
||||
export class ShadedBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "shaded",
|
||||
name: options.t('Canvas.Shaded'),
|
||||
description: "创建带有阴影效果的绘制,适合素描和明暗表现",
|
||||
category: options.t('Canvas.PaintingBrush'),
|
||||
icon: "shaded",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 阴影笔刷特有属性
|
||||
this.shadowColor = options.shadowColor || "#000000";
|
||||
this.shadowBlur = options.shadowBlur || 5;
|
||||
this.shadowOffsetX = options.shadowOffsetX || 2;
|
||||
this.shadowOffsetY = options.shadowOffsetY || 2;
|
||||
this.blendMode = options.blendMode || "multiply";
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生阴影笔刷
|
||||
this.brush = new fabric.ShadedBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 阴影笔刷特有属性
|
||||
if (options.shadowColor !== undefined) {
|
||||
this.shadowColor = options.shadowColor;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.shadow) {
|
||||
brush.shadow.color = this.shadowColor;
|
||||
} else {
|
||||
brush.shadow = new fabric.Shadow({
|
||||
color: this.shadowColor,
|
||||
blur: this.shadowBlur,
|
||||
offsetX: this.shadowOffsetX,
|
||||
offsetY: this.shadowOffsetY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.shadowBlur !== undefined) {
|
||||
this.shadowBlur = options.shadowBlur;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.shadow) {
|
||||
brush.shadow.blur = this.shadowBlur;
|
||||
} else {
|
||||
brush.shadow = new fabric.Shadow({
|
||||
color: this.shadowColor,
|
||||
blur: this.shadowBlur,
|
||||
offsetX: this.shadowOffsetX,
|
||||
offsetY: this.shadowOffsetY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.shadowOffsetX !== undefined) {
|
||||
this.shadowOffsetX = options.shadowOffsetX;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.shadow) {
|
||||
brush.shadow.offsetX = this.shadowOffsetX;
|
||||
} else {
|
||||
brush.shadow = new fabric.Shadow({
|
||||
color: this.shadowColor,
|
||||
blur: this.shadowBlur,
|
||||
offsetX: this.shadowOffsetX,
|
||||
offsetY: this.shadowOffsetY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.shadowOffsetY !== undefined) {
|
||||
this.shadowOffsetY = options.shadowOffsetY;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.shadow) {
|
||||
brush.shadow.offsetY = this.shadowOffsetY;
|
||||
} else {
|
||||
brush.shadow = new fabric.Shadow({
|
||||
color: this.shadowColor,
|
||||
blur: this.shadowBlur,
|
||||
offsetX: this.shadowOffsetX,
|
||||
offsetY: this.shadowOffsetY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.blendMode !== undefined) {
|
||||
this.blendMode = options.blendMode;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.globalCompositeOperation !== undefined) {
|
||||
brush.globalCompositeOperation = this.blendMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阴影颜色
|
||||
* @param {String} color 颜色值
|
||||
*/
|
||||
setShadowColor(color) {
|
||||
this.shadowColor = color;
|
||||
|
||||
if (this.brush && this.brush.shadow) {
|
||||
this.brush.shadow.color = this.shadowColor;
|
||||
}
|
||||
|
||||
return this.shadowColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阴影模糊值
|
||||
* @param {Number} blur 模糊值
|
||||
*/
|
||||
setShadowBlur(blur) {
|
||||
this.shadowBlur = Math.max(0, Math.min(50, blur));
|
||||
|
||||
if (this.brush && this.brush.shadow) {
|
||||
this.brush.shadow.blur = this.shadowBlur;
|
||||
}
|
||||
|
||||
return this.shadowBlur;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阴影X偏移
|
||||
* @param {Number} offset X偏移值
|
||||
*/
|
||||
setShadowOffsetX(offset) {
|
||||
this.shadowOffsetX = Math.max(-20, Math.min(20, offset));
|
||||
|
||||
if (this.brush && this.brush.shadow) {
|
||||
this.brush.shadow.offsetX = this.shadowOffsetX;
|
||||
}
|
||||
|
||||
return this.shadowOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置阴影Y偏移
|
||||
* @param {Number} offset Y偏移值
|
||||
*/
|
||||
setShadowOffsetY(offset) {
|
||||
this.shadowOffsetY = Math.max(-20, Math.min(20, offset));
|
||||
|
||||
if (this.brush && this.brush.shadow) {
|
||||
this.brush.shadow.offsetY = this.shadowOffsetY;
|
||||
}
|
||||
|
||||
return this.shadowOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置混合模式
|
||||
* @param {String} mode 混合模式
|
||||
*/
|
||||
setBlendMode(mode) {
|
||||
const validModes = [
|
||||
"normal",
|
||||
"multiply",
|
||||
"screen",
|
||||
"overlay",
|
||||
"darken",
|
||||
"lighten",
|
||||
"color-dodge",
|
||||
"color-burn",
|
||||
"hard-light",
|
||||
"soft-light",
|
||||
"difference",
|
||||
"exclusion",
|
||||
"hue",
|
||||
"saturation",
|
||||
"color",
|
||||
"luminosity",
|
||||
];
|
||||
|
||||
if (validModes.includes(mode)) {
|
||||
this.blendMode = mode;
|
||||
|
||||
if (this.brush && this.brush.globalCompositeOperation !== undefined) {
|
||||
this.brush.globalCompositeOperation = this.blendMode;
|
||||
}
|
||||
}
|
||||
|
||||
return this.blendMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义阴影笔刷特有属性
|
||||
const shadedProperties = [
|
||||
{
|
||||
id: "shadowColor",
|
||||
name: this.t('Canvas.ShadedColor'),
|
||||
type: "color",
|
||||
defaultValue: this.shadowColor,
|
||||
description: this.t('Canvas.ShadedColorDescription'),
|
||||
category: this.t('Canvas.ShadedSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "shadowBlur",
|
||||
name: this.t('Canvas.ShadedBlur'),
|
||||
type: "slider",
|
||||
defaultValue: this.shadowBlur,
|
||||
min: 0,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: this.t('Canvas.ShadedBlurDescription'),
|
||||
category: this.t('Canvas.ShadedSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "shadowOffsetX",
|
||||
name: this.t('Canvas.ShadedOffsetX'),
|
||||
type: "slider",
|
||||
defaultValue: this.shadowOffsetX,
|
||||
min: -20,
|
||||
max: 20,
|
||||
step: 1,
|
||||
description: this.t('Canvas.ShadedOffsetXDescription'),
|
||||
category: this.t('Canvas.ShadedSettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "shadowOffsetY",
|
||||
name: this.t('Canvas.ShadedOffsetY'),
|
||||
type: "slider",
|
||||
defaultValue: this.shadowOffsetY,
|
||||
min: -20,
|
||||
max: 20,
|
||||
step: 1,
|
||||
description: this.t('Canvas.ShadedOffsetYDescription'),
|
||||
category: this.t('Canvas.ShadedSettings'),
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
id: "blendMode",
|
||||
name: this.t('Canvas.ShadedMixedModel'),
|
||||
type: "select",
|
||||
defaultValue: this.blendMode,
|
||||
options: [
|
||||
{ value: "normal", label: this.t('Canvas.ShadedMixedModelNormal') },
|
||||
{ value: "multiply", label: this.t('Canvas.ShadedMixedModelMultiply') },
|
||||
{ value: "screen", label: this.t('Canvas.ShadedMixedModelScreen') },
|
||||
{ value: "overlay", label: this.t('Canvas.ShadedMixedModelOverlay') },
|
||||
{ value: "darken", label: this.t('Canvas.ShadedMixedModelDarken') },
|
||||
{ value: "lighten", label: this.t('Canvas.ShadedMixedModelLighten') },
|
||||
{ value: "color-dodge", label: this.t('Canvas.ShadedMixedModelColorDodge') },
|
||||
{ value: "color-burn", label: this.t('Canvas.ShadedMixedModelColorBurn') },
|
||||
{ value: "hard-light", label: this.t('Canvas.ShadedMixedModelHardLight') },
|
||||
{ value: "soft-light", label: this.t('Canvas.ShadedMixedModelSoftLight') },
|
||||
{ value: "difference", label: this.t('Canvas.ShadedMixedModelDifference') },
|
||||
{ value: "exclusion", label: this.t('Canvas.ShadedMixedModelExclusion') },
|
||||
],
|
||||
description: this.t('Canvas.ShadedMixedModelDescription'),
|
||||
category: this.t('Canvas.ShadedSettings'),
|
||||
order: 140,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...shadedProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理阴影笔刷特有属性
|
||||
if (propId === "shadowColor") {
|
||||
this.setShadowColor(value);
|
||||
return true;
|
||||
} else if (propId === "shadowBlur") {
|
||||
this.setShadowBlur(value);
|
||||
return true;
|
||||
} else if (propId === "shadowOffsetX") {
|
||||
this.setShadowOffsetX(value);
|
||||
return true;
|
||||
} else if (propId === "shadowOffsetY") {
|
||||
this.setShadowOffsetY(value);
|
||||
return true;
|
||||
} else if (propId === "blendMode") {
|
||||
this.setBlendMode(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI0MCIgY3k9IjQwIiByPSIyMCIgZmlsbD0iIzY2NiIvPjxjaXJjbGUgY3g9IjQ1IiBjeT0iNDUiIHI9IjIwIiBmaWxsPSIjMDAwIi8+PHBhdGggZD0iTTIwIDgwQzMwIDYwIDUwIDcwIDcwIDUwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTIzIDgzQzMzIDYzIDUzIDczIDczIDUzIiBzdHJva2U9IiM2NjYiIHN0cm9rZS13aWR0aD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 素描笔刷
|
||||
* 创建手绘素描效果,有不规则的线条和纹理
|
||||
*/
|
||||
export class SketchyBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "sketchy",
|
||||
name: "素描",
|
||||
description: "创建手绘素描效果,有不规则的线条和纹理",
|
||||
category: "绘画笔刷",
|
||||
icon: "sketchy",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 素描笔刷特有属性
|
||||
this.roughness = options.roughness || 0.7;
|
||||
this.bowing = options.bowing || 0.5;
|
||||
this.stroke = options.stroke !== undefined ? options.stroke : true;
|
||||
this.hachureAngle = options.hachureAngle || 60;
|
||||
this.dashOffset = options.dashOffset || 0;
|
||||
this.dashArray = options.dashArray || [6, 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生素描笔刷
|
||||
this.brush = new fabric.SketchyBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 素描笔刷特有属性
|
||||
if (options.roughness !== undefined) {
|
||||
this.roughness = options.roughness;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.roughness !== undefined) {
|
||||
brush.roughness = this.roughness;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bowing !== undefined) {
|
||||
this.bowing = options.bowing;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.bowing !== undefined) {
|
||||
brush.bowing = this.bowing;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.stroke !== undefined) {
|
||||
this.stroke = options.stroke;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.stroke !== undefined) {
|
||||
brush.stroke = this.stroke;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.hachureAngle !== undefined) {
|
||||
this.hachureAngle = options.hachureAngle;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.hachureAngle !== undefined) {
|
||||
brush.hachureAngle = this.hachureAngle;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dashOffset !== undefined) {
|
||||
this.dashOffset = options.dashOffset;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.dashOffset !== undefined) {
|
||||
brush.dashOffset = this.dashOffset;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dashArray !== undefined) {
|
||||
this.dashArray = options.dashArray;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.dashArray !== undefined) {
|
||||
brush.dashArray = this.dashArray;
|
||||
}
|
||||
}
|
||||
|
||||
// 为笔刷设置手绘效果
|
||||
const originalOnMouseMove = brush.onMouseMove;
|
||||
brush.onMouseMove = function (pointer, options) {
|
||||
// 添加微小随机偏移,模拟手绘效果
|
||||
const jitter = (this.width / 4) * this.roughness;
|
||||
pointer.x += (Math.random() - 0.5) * jitter;
|
||||
pointer.y += (Math.random() - 0.5) * jitter;
|
||||
|
||||
// 调用原始方法
|
||||
if (originalOnMouseMove) {
|
||||
originalOnMouseMove.call(this, pointer, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置粗糙度
|
||||
* @param {Number} value 粗糙度值(0-1)
|
||||
*/
|
||||
setRoughness(value) {
|
||||
this.roughness = Math.max(0, Math.min(1, value));
|
||||
|
||||
if (this.brush && this.brush.roughness !== undefined) {
|
||||
this.brush.roughness = this.roughness;
|
||||
}
|
||||
|
||||
return this.roughness;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置弯曲度
|
||||
* @param {Number} value 弯曲度值(0-1)
|
||||
*/
|
||||
setBowing(value) {
|
||||
this.bowing = Math.max(0, Math.min(1, value));
|
||||
|
||||
if (this.brush && this.brush.bowing !== undefined) {
|
||||
this.brush.bowing = this.bowing;
|
||||
}
|
||||
|
||||
return this.bowing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否描边
|
||||
* @param {Boolean} value 是否描边
|
||||
*/
|
||||
setStroke(value) {
|
||||
this.stroke = value;
|
||||
|
||||
if (this.brush && this.brush.stroke !== undefined) {
|
||||
this.brush.stroke = this.stroke;
|
||||
}
|
||||
|
||||
return this.stroke;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置素描线条角度
|
||||
* @param {Number} value 角度值(0-180)
|
||||
*/
|
||||
setHachureAngle(value) {
|
||||
this.hachureAngle = Math.max(0, Math.min(180, value));
|
||||
|
||||
if (this.brush && this.brush.hachureAngle !== undefined) {
|
||||
this.brush.hachureAngle = this.hachureAngle;
|
||||
}
|
||||
|
||||
return this.hachureAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置虚线偏移量
|
||||
* @param {Number} value 偏移量
|
||||
*/
|
||||
setDashOffset(value) {
|
||||
this.dashOffset = value;
|
||||
|
||||
if (this.brush && this.brush.dashOffset !== undefined) {
|
||||
this.brush.dashOffset = this.dashOffset;
|
||||
}
|
||||
|
||||
return this.dashOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置虚线数组
|
||||
* @param {Array} value 虚线数组[线长, 间隔]
|
||||
*/
|
||||
setDashArray(value) {
|
||||
if (Array.isArray(value) && value.length >= 2) {
|
||||
this.dashArray = value;
|
||||
|
||||
if (this.brush && this.brush.dashArray !== undefined) {
|
||||
this.brush.dashArray = this.dashArray;
|
||||
}
|
||||
}
|
||||
|
||||
return this.dashArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义素描笔刷特有属性
|
||||
const sketchyProperties = [
|
||||
{
|
||||
id: "roughness",
|
||||
name: "粗糙度",
|
||||
type: "slider",
|
||||
defaultValue: this.roughness,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: "控制素描线条的粗糙程度",
|
||||
category: "素描设置",
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "bowing",
|
||||
name: "弯曲度",
|
||||
type: "slider",
|
||||
defaultValue: this.bowing,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: "控制素描线条的弯曲程度",
|
||||
category: "素描设置",
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "stroke",
|
||||
name: "描边",
|
||||
type: "checkbox",
|
||||
defaultValue: this.stroke,
|
||||
description: "是否使用描边",
|
||||
category: "素描设置",
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "hachureAngle",
|
||||
name: "线条角度",
|
||||
type: "slider",
|
||||
defaultValue: this.hachureAngle,
|
||||
min: 0,
|
||||
max: 180,
|
||||
step: 5,
|
||||
description: "控制素描线条的角度",
|
||||
category: "素描设置",
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
id: "dashOffset",
|
||||
name: "虚线偏移",
|
||||
type: "slider",
|
||||
defaultValue: this.dashOffset,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
description: "控制虚线的偏移量",
|
||||
category: "素描设置",
|
||||
order: 140,
|
||||
},
|
||||
{
|
||||
id: "dashArray",
|
||||
name: "虚线模式",
|
||||
type: "select",
|
||||
defaultValue: JSON.stringify(this.dashArray),
|
||||
options: [
|
||||
{ value: JSON.stringify([0]), label: "实线" },
|
||||
{ value: JSON.stringify([6, 2]), label: "短虚线" },
|
||||
{ value: JSON.stringify([10, 5]), label: "长虚线" },
|
||||
{ value: JSON.stringify([2, 2]), label: "点线" },
|
||||
{ value: JSON.stringify([10, 5, 2, 5]), label: "点划线" },
|
||||
],
|
||||
description: "设置虚线的模式",
|
||||
category: "素描设置",
|
||||
order: 150,
|
||||
parseValue: (value) => JSON.parse(value),
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...sketchyProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理素描笔刷特有属性
|
||||
if (propId === "roughness") {
|
||||
this.setRoughness(value);
|
||||
return true;
|
||||
} else if (propId === "bowing") {
|
||||
this.setBowing(value);
|
||||
return true;
|
||||
} else if (propId === "stroke") {
|
||||
this.setStroke(value);
|
||||
return true;
|
||||
} else if (propId === "hachureAngle") {
|
||||
this.setHachureAngle(value);
|
||||
return true;
|
||||
} else if (propId === "dashOffset") {
|
||||
this.setDashOffset(value);
|
||||
return true;
|
||||
} else if (propId === "dashArray") {
|
||||
let parsedValue = value;
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error("Invalid dashArray value:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.setDashArray(parsedValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjMgMzBDMjUgMjggNTIgMzggNzUgMzciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjIgNDBDMjIgMzggNTkgNDYgNzYgNDMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjAgNTBDMjIgNDggNTYgNTYgNzYgNTIiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjQgNjBDMjMgNTggNDYgNjQgNzUgNjQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyLjgiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yNiA3MkMyNyA2OSA0OSA3NCA3NSA3MiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIuNCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 喷枪笔刷
|
||||
* 模拟喷枪效果,具有颗粒感和纹理
|
||||
*/
|
||||
export class SprayBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "crayon",
|
||||
name: options.t('Canvas.Spray'),
|
||||
description: "模拟喷枪效果,具有颗粒感和纹理",
|
||||
category: options.t('Canvas.SpecialEffectsBrush'),
|
||||
icon: "crayon",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 喷枪笔刷特有属性
|
||||
this._baseWidth = options._baseWidth || 15;
|
||||
this._size = options._size || 0;
|
||||
this._sep = options._sep || options._sep === 0 ? options._sep : 1;
|
||||
this._inkAmount = options._inkAmount || 10;
|
||||
this.randomness = options.randomness || 0.8; // 随机性
|
||||
this.texture = options.texture || "default"; // 纹理类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生喷枪笔刷
|
||||
this.brush = new fabric.CrayonBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
options._sep = options._sep || this._sep;
|
||||
options._inkAmount = options._inkAmount || this._inkAmount;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
// 更新笔刷相关属性
|
||||
this._baseWidth = options.width / 2;
|
||||
this._size = options.width / 2 + this._baseWidth;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 喷枪笔刷特有属性
|
||||
if (options._baseWidth !== undefined) {
|
||||
brush._baseWidth = options._baseWidth;
|
||||
this._baseWidth = options._baseWidth;
|
||||
this._size = this.width / 2 + this._baseWidth;
|
||||
}
|
||||
if (options._sep !== undefined) {
|
||||
brush._sep = options._sep;
|
||||
this._sep = options._sep;
|
||||
}
|
||||
|
||||
if (options._inkAmount !== undefined) {
|
||||
brush._inkAmount = options._inkAmount;
|
||||
this._inkAmount = options._inkAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颗粒分离度
|
||||
* @param {Number} sep 分离度值
|
||||
*/
|
||||
setSeparation(sep) {
|
||||
this._sep = Math.max(0.5, Math.min(10, sep));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._sep = this._sep;
|
||||
}
|
||||
|
||||
return this._sep;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置墨量
|
||||
* @param {Number} amount 墨量值
|
||||
*/
|
||||
setInkAmount(amount) {
|
||||
this._inkAmount = Math.max(1, Math.min(50, amount));
|
||||
|
||||
if (this.brush) {
|
||||
this.brush._inkAmount = this._inkAmount;
|
||||
}
|
||||
|
||||
return this._inkAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置随机性
|
||||
* @param {Number} value 随机性值(0-1)
|
||||
*/
|
||||
setRandomness(value) {
|
||||
this.randomness = Math.max(0, Math.min(1, value));
|
||||
return this.randomness;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理类型
|
||||
* @param {String} type 纹理类型
|
||||
*/
|
||||
setTexture(type) {
|
||||
this.texture = type;
|
||||
// 实际应用可能需要更多的实现逻辑
|
||||
return this.texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义喷枪笔刷特有属性
|
||||
const crayonProperties = [
|
||||
{
|
||||
id: "separation",
|
||||
name: this.t('Canvas.ParticleSeparationDegree'),
|
||||
type: "slider",
|
||||
defaultValue: this._sep,
|
||||
min: 0.5,
|
||||
max: 10,
|
||||
step: 0.5,
|
||||
description: this.t('Canvas.ParticleSeparationDegree'),
|
||||
category: this.t('Canvas.SpraySettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "inkAmount",
|
||||
name: this.t('Canvas.TheAmountOfInk'),
|
||||
type: "slider",
|
||||
defaultValue: this._inkAmount,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: this.t('Canvas.TheAmountOfInkDescription'),
|
||||
category: this.t('Canvas.SpraySettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "randomness",
|
||||
name: this.t('Canvas.randomness'),
|
||||
type: "slider",
|
||||
defaultValue: this.randomness,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.randomnessDescription'),
|
||||
category: this.t('Canvas.SpraySettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "texture",
|
||||
name: this.t('Canvas.TextureType'),
|
||||
type: "select",
|
||||
defaultValue: this.texture,
|
||||
options: [
|
||||
{ value: "default", label: this.t('Canvas.Default') },
|
||||
{ value: "rough", label: this.t('Canvas.Rough') },
|
||||
{ value: "smooth", label: this.t('Canvas.Smooth') },
|
||||
],
|
||||
description: this.t('Canvas.TextureTypeDescription'),
|
||||
category: this.t('Canvas.SpraySettings'),
|
||||
order: 130,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...crayonProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理喷枪笔刷特有属性
|
||||
if (propId === "separation") {
|
||||
this.setSeparation(value);
|
||||
return true;
|
||||
} else if (propId === "inkAmount") {
|
||||
this.setInkAmount(value);
|
||||
return true;
|
||||
} else if (propId === "randomness") {
|
||||
this.setRandomness(value);
|
||||
return true;
|
||||
} else if (propId === "texture") {
|
||||
this.setTexture(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIzMCIgeT0iMzAiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 喷漆笔刷
|
||||
* 创建喷漆效果,点状分散的绘制风格
|
||||
*/
|
||||
export class SpraypaintBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "spraypaint",
|
||||
name: "喷漆笔刷",
|
||||
description: "创建喷漆效果,点状分散的绘制风格",
|
||||
category: "绘画笔刷",
|
||||
icon: "spraypaint",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 喷漆笔刷特有属性
|
||||
this.density = options.density || 20;
|
||||
this.sprayRadius = options.sprayRadius || 10;
|
||||
this.randomOpacity = options.randomOpacity !== undefined ? options.randomOpacity : true;
|
||||
this.dotSize = options.dotSize || 1;
|
||||
this.dotShape = options.dotShape || "circle";
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生喷漆笔刷
|
||||
this.brush = new fabric.SprayBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 设置基本属性
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 喷漆笔刷特有属性
|
||||
if (options.density !== undefined) {
|
||||
this.density = options.density;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.density !== undefined) {
|
||||
brush.density = this.density;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.sprayRadius !== undefined) {
|
||||
this.sprayRadius = options.sprayRadius;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.sprayWidth !== undefined) {
|
||||
brush.sprayWidth = this.sprayRadius;
|
||||
} else if (brush.width !== undefined) {
|
||||
brush.width = this.sprayRadius;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.randomOpacity !== undefined) {
|
||||
this.randomOpacity = options.randomOpacity;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.randomOpacity !== undefined) {
|
||||
brush.randomOpacity = this.randomOpacity;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dotSize !== undefined) {
|
||||
this.dotSize = options.dotSize;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.dotWidth !== undefined) {
|
||||
brush.dotWidth = this.dotSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dotShape !== undefined) {
|
||||
this.dotShape = options.dotShape;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.dotShape !== undefined) {
|
||||
brush.dotShape = this.dotShape;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置喷漆密度
|
||||
* @param {Number} value 密度值
|
||||
*/
|
||||
setDensity(value) {
|
||||
this.density = Math.max(1, Math.min(100, value));
|
||||
|
||||
if (this.brush && this.brush.density !== undefined) {
|
||||
this.brush.density = this.density;
|
||||
}
|
||||
|
||||
return this.density;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置喷漆半径
|
||||
* @param {Number} value 半径值
|
||||
*/
|
||||
setSprayRadius(value) {
|
||||
this.sprayRadius = Math.max(1, value);
|
||||
|
||||
if (this.brush) {
|
||||
if (this.brush.sprayWidth !== undefined) {
|
||||
this.brush.sprayWidth = this.sprayRadius;
|
||||
} else if (this.brush.width !== undefined) {
|
||||
this.brush.width = this.sprayRadius;
|
||||
}
|
||||
}
|
||||
|
||||
return this.sprayRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否随机透明度
|
||||
* @param {Boolean} value 是否随机透明度
|
||||
*/
|
||||
setRandomOpacity(value) {
|
||||
this.randomOpacity = value;
|
||||
|
||||
if (this.brush && this.brush.randomOpacity !== undefined) {
|
||||
this.brush.randomOpacity = this.randomOpacity;
|
||||
}
|
||||
|
||||
return this.randomOpacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置点大小
|
||||
* @param {Number} value 点大小
|
||||
*/
|
||||
setDotSize(value) {
|
||||
this.dotSize = Math.max(0.1, value);
|
||||
|
||||
if (this.brush && this.brush.dotWidth !== undefined) {
|
||||
this.brush.dotWidth = this.dotSize;
|
||||
}
|
||||
|
||||
return this.dotSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置点形状
|
||||
* @param {String} value 点形状,如 'circle', 'square', 'diamond'
|
||||
*/
|
||||
setDotShape(value) {
|
||||
const validShapes = ["circle", "square", "diamond", "random"];
|
||||
|
||||
if (validShapes.includes(value)) {
|
||||
this.dotShape = value;
|
||||
|
||||
if (this.brush && this.brush.dotShape !== undefined) {
|
||||
this.brush.dotShape = this.dotShape;
|
||||
}
|
||||
}
|
||||
|
||||
return this.dotShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义喷漆笔刷特有属性
|
||||
const spraypaintProperties = [
|
||||
{
|
||||
id: "density",
|
||||
name: "喷漆密度",
|
||||
type: "slider",
|
||||
defaultValue: this.density,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
description: "控制喷漆点的密度",
|
||||
category: "喷漆设置",
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "sprayRadius",
|
||||
name: "喷漆半径",
|
||||
type: "slider",
|
||||
defaultValue: this.sprayRadius,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: "控制喷漆的覆盖半径",
|
||||
category: "喷漆设置",
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "randomOpacity",
|
||||
name: "随机透明度",
|
||||
type: "checkbox",
|
||||
defaultValue: this.randomOpacity,
|
||||
description: "使喷漆点有随机透明度",
|
||||
category: "喷漆设置",
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "dotSize",
|
||||
name: "点大小",
|
||||
type: "slider",
|
||||
defaultValue: this.dotSize,
|
||||
min: 0.1,
|
||||
max: 10,
|
||||
step: 0.1,
|
||||
description: "控制喷漆点的大小",
|
||||
category: "喷漆设置",
|
||||
order: 130,
|
||||
},
|
||||
{
|
||||
id: "dotShape",
|
||||
name: "点形状",
|
||||
type: "select",
|
||||
defaultValue: this.dotShape,
|
||||
options: [
|
||||
{ value: "circle", label: "圆形" },
|
||||
{ value: "square", label: "方形" },
|
||||
{ value: "diamond", label: "菱形" },
|
||||
{ value: "random", label: "随机" },
|
||||
],
|
||||
description: "设置喷漆点的形状",
|
||||
category: "喷漆设置",
|
||||
order: 140,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...spraypaintProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理喷漆笔刷特有属性
|
||||
if (propId === "density") {
|
||||
this.setDensity(value);
|
||||
return true;
|
||||
} else if (propId === "sprayRadius") {
|
||||
this.setSprayRadius(value);
|
||||
return true;
|
||||
} else if (propId === "randomOpacity") {
|
||||
this.setRandomOpacity(value);
|
||||
return true;
|
||||
} else if (propId === "dotSize") {
|
||||
this.setDotSize(value);
|
||||
return true;
|
||||
} else if (propId === "dotShape") {
|
||||
this.setDotShape(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1NSIgY3k9IjUwIiByPSIyMCIgZmlsbD0icmdiYSgwLDAsMCwwLjEpIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMC41Ii8+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU1IiBjeT0iNTUiIHI9IjAuOCIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjYwIiBjeT0iNDUiIHI9IjEuMiIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjQ1IiBjeT0iNTUiIHI9IjAuNyIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjQ3IiBjeT0iNDgiIHI9IjAuOSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU4IiBjeT0iNTMiIHI9IjEuMSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjYyIiBjeT0iNTYiIHI9IjAuNiIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjUyIiBjeT0iNTgiIHI9IjAuOCIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU0IiBjeT0iNDMiIHI9IjEiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0OSIgY3k9IjQzIiByPSIwLjYiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0MyIgY3k9IjQ3IiByPSIwLjciIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2NSIgY3k9IjQ4IiByPSIwLjkiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2MiIgY3k9IjQxIiByPSIwLjUiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0OSIgY3k9IjYxIiByPSIwLjgiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2NiIgY3k9IjUyIiByPSIwLjciIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0MSIgY3k9IjUxIiByPSIwLjYiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2OCIgY3k9IjU3IiByPSIwLjQiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0NSIgY3k9IjQwIiByPSIwLjUiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI1NyIgY3k9IjYxIiByPSIwLjciIGZpbGw9IiMwMDAiLz48L3N2Zz4=";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,945 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import texturePresetManager from "../TexturePresetManager";
|
||||
import i18n from "@/lang/index.ts";
|
||||
const {t} = i18n.global;
|
||||
|
||||
/**
|
||||
* 纹理笔刷
|
||||
* 使用图像纹理进行绘制的笔刷
|
||||
*/
|
||||
export class TextureBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "texture",
|
||||
name: "纹理笔刷",
|
||||
description: "使用图像纹理进行绘制的笔刷",
|
||||
category: "特效笔刷",
|
||||
icon: "texture",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 纹理笔刷特有属性
|
||||
this.textureSource = options.textureSource || null;
|
||||
this.textureRepeat = options.textureRepeat || "repeat";
|
||||
this.textureScale = options.textureScale || 1;
|
||||
this.textureAngle = options.textureAngle || 0;
|
||||
this.opacity = options.opacity !== undefined ? options.opacity : 1;
|
||||
|
||||
// 预设材质相关
|
||||
this.selectedTextureId = options.selectedTextureId || null;
|
||||
this.texturePresets = [];
|
||||
|
||||
// 加载预设材质
|
||||
this._loadTexturePresets();
|
||||
|
||||
// 当前选中的材质索引
|
||||
this.currentTextureIndex = options.currentTextureIndex || 0;
|
||||
|
||||
// 从预设管理器加载自定义材质
|
||||
texturePresetManager.loadCustomTexturesFromStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载材质预设
|
||||
* @private
|
||||
*/
|
||||
_loadTexturePresets() {
|
||||
// 从预设管理器获取所有材质
|
||||
this.texturePresets = texturePresetManager.getAllTextures();
|
||||
|
||||
// 如果没有选中的材质ID,使用第一个预设材质
|
||||
if (!this.selectedTextureId && this.texturePresets.length > 0) {
|
||||
this.selectedTextureId = this.texturePresets[0].id;
|
||||
this.currentTextureIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生纹理笔刷
|
||||
this.brush = new fabric.PatternBrush(this.canvas);
|
||||
|
||||
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
|
||||
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(this.brush);
|
||||
const self = this; // 保存外部this引用
|
||||
|
||||
this.brush._finalizeAndAddPath = function () {
|
||||
console.log("TextureBrush: _finalizeAndAddPath called");
|
||||
const ctx = this.canvas.contextTop;
|
||||
ctx.closePath();
|
||||
|
||||
// 应用点简化(如果PatternBrush支持)
|
||||
if (this.decimate && this._points) {
|
||||
this._points = this.decimatePoints(this._points, this.decimate);
|
||||
}
|
||||
|
||||
console.log("TextureBrush: points count =", this._points ? this._points.length : 0);
|
||||
|
||||
// 检查是否有有效的路径数据
|
||||
if (!this._points || this._points.length < 2) {
|
||||
// 如果点数不足,直接请求重新渲染
|
||||
console.log("TextureBrush: Not enough points, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const pathData = this.convertPointsToSVGPath(this._points);
|
||||
|
||||
const isEmpty = self._isEmptySVGPath(pathData);
|
||||
console.log("TextureBrush: isEmpty =", isEmpty);
|
||||
|
||||
if (isEmpty) {
|
||||
// 如果路径为空,直接请求重新渲染
|
||||
console.log("TextureBrush: Path is empty, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先触发事件,模拟原生行为
|
||||
const path = this.createPath(pathData);
|
||||
this.canvas.fire("before:path:created", { path: path });
|
||||
|
||||
console.log("TextureBrush: Calling convertToImg");
|
||||
|
||||
// 调用 convertToImg 方法将绘制内容转换为图片
|
||||
if (typeof this.convertToImg === "function") {
|
||||
this.convertToImg();
|
||||
console.log("TextureBrush: convertToImg called successfully");
|
||||
} else {
|
||||
console.warn("convertToImg method not found, falling back to original behavior");
|
||||
// 如果没有convertToImg方法,回退到原始行为
|
||||
this.canvas.add(path);
|
||||
this.canvas.fire("path:created", { path: path });
|
||||
this.canvas.clearContext(this.canvas.contextTop);
|
||||
}
|
||||
|
||||
// 重置阴影
|
||||
this._resetShadow();
|
||||
};
|
||||
|
||||
// 配置笔刷基本属性
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
// 如果有选中的材质,则设置纹理
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 设置基本属性
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 纹理笔刷特有属性
|
||||
if (options.textureRepeat !== undefined) {
|
||||
this.textureRepeat = options.textureRepeat;
|
||||
// 需要重新应用纹理以应用重复模式
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.textureScale !== undefined) {
|
||||
this.textureScale = options.textureScale;
|
||||
// 需要重新应用纹理以应用缩放
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.textureAngle !== undefined) {
|
||||
this.textureAngle = options.textureAngle;
|
||||
// 需要重新应用纹理以应用旋转角度
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
this.opacity = options.opacity;
|
||||
// 需要重新应用纹理以应用透明度
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据材质ID设置纹理
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
setTextureById(textureId) {
|
||||
const texture = texturePresetManager.getTextureById(textureId);
|
||||
if (!texture) {
|
||||
return Promise.reject(new Error(`材质 ${textureId} 不存在`));
|
||||
}
|
||||
|
||||
this.selectedTextureId = textureId;
|
||||
|
||||
// 更新当前材质索引
|
||||
const allTextures = texturePresetManager.getAllTextures();
|
||||
this.currentTextureIndex = allTextures.findIndex((t) => t.id === textureId);
|
||||
|
||||
return this.setTexture(texture.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理
|
||||
* @param {String|Object} source 纹理源(URL或Image对象)
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
setTexture(source) {
|
||||
this.textureSource = source;
|
||||
|
||||
if (!this.brush) {
|
||||
return Promise.reject(new Error("笔刷实例不存在"));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof source === "string") {
|
||||
// 如果是URL,加载图像
|
||||
fabric.util.loadImage(source, (img) => {
|
||||
if (!img) {
|
||||
reject(new Error("纹理加载失败"));
|
||||
return;
|
||||
}
|
||||
this._applyTextureToPatternBrush(img);
|
||||
resolve(img);
|
||||
});
|
||||
} else if (source instanceof Image || source instanceof HTMLCanvasElement) {
|
||||
// 如果已经是Image或Canvas对象,直接使用
|
||||
this._applyTextureToPatternBrush(source);
|
||||
resolve(source);
|
||||
} else {
|
||||
reject(new Error("无效的纹理源"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将纹理应用到PatternBrush
|
||||
* @param {Object} img 图像对象
|
||||
* @private
|
||||
*/
|
||||
_applyTextureToPatternBrush(img) {
|
||||
if (!this.brush || !img) return;
|
||||
|
||||
// 创建Canvas来处理纹理
|
||||
const canvasTexture = document.createElement("canvas");
|
||||
const ctx = canvasTexture.getContext("2d");
|
||||
|
||||
// 根据缩放设置Canvas大小
|
||||
const width = img.width * this.textureScale;
|
||||
const height = img.height * this.textureScale;
|
||||
canvasTexture.width = width;
|
||||
canvasTexture.height = height;
|
||||
|
||||
// 应用纹理透明度设置 - 仅控制纹理的透明度
|
||||
if (this.opacity < 1) {
|
||||
ctx.globalAlpha = this.opacity;
|
||||
}
|
||||
|
||||
// 绘制前应用旋转
|
||||
if (this.textureAngle !== 0) {
|
||||
ctx.save();
|
||||
ctx.translate(width / 2, height / 2);
|
||||
ctx.rotate((this.textureAngle * Math.PI) / 180);
|
||||
ctx.translate(-width / 2, -height / 2);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
}
|
||||
|
||||
// 缓存处理后的纹理图像
|
||||
this._processedTextureCanvas = canvasTexture;
|
||||
|
||||
// 使用fabric.js 5.x正确的API方式设置PatternBrush
|
||||
// 直接设置source属性,这是PatternBrush的标准用法
|
||||
this.brush.source = canvasTexture;
|
||||
|
||||
this.brush.canvas.opacity = this.opacity; // 确保画布不透明
|
||||
|
||||
// 设置笔刷颜色为完全透明的RGBA,避免重复绘制时的叠加效果
|
||||
// 笔刷的透明度通过RGBA颜色控制,而不是通过globalAlpha
|
||||
const brushOpacity = this.brush.opacity || 1;
|
||||
this.brush.color = `rgba(0, 0, 0, ${brushOpacity})`;
|
||||
|
||||
// 确保画布上下文使用完整的透明度,避免圆圈重叠问题
|
||||
// if (this.canvas && this.canvas.contextTop) {
|
||||
// this.canvas.contextTop.globalAlpha = 1;
|
||||
// }
|
||||
|
||||
// 通知画布重绘
|
||||
if (this.canvas && this.canvas.requestRenderAll) {
|
||||
this.canvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理重复模式
|
||||
* @param {String} mode 重复模式:'repeat', 'repeat-x', 'repeat-y', 'no-repeat'
|
||||
* @returns {String} 设置后的重复模式
|
||||
*/
|
||||
setTextureRepeat(mode) {
|
||||
const validModes = ["repeat", "repeat-x", "repeat-y", "no-repeat"];
|
||||
if (validModes.includes(mode)) {
|
||||
this.textureRepeat = mode;
|
||||
|
||||
// 重新应用纹理以更新重复模式
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
}
|
||||
|
||||
return this.textureRepeat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理缩放比例
|
||||
* @param {Number} scale 缩放比例
|
||||
* @returns {Number} 设置后的缩放比例
|
||||
*/
|
||||
setTextureScale(scale) {
|
||||
this.textureScale = Math.max(0.1, scale);
|
||||
|
||||
// 重新应用纹理以更新缩放
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
|
||||
return this.textureScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理旋转角度
|
||||
* @param {Number} angle 旋转角度(度)
|
||||
* @returns {Number} 设置后的旋转角度
|
||||
*/
|
||||
setTextureAngle(angle) {
|
||||
this.textureAngle = angle % 360;
|
||||
|
||||
// 重新应用纹理以更新旋转角度
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
|
||||
return this.textureAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理透明度
|
||||
* @param {Number} opacity 透明度
|
||||
* @returns {Number} 设置后的透明度
|
||||
*/
|
||||
setopacity(opacity) {
|
||||
this.opacity = Math.min(1, Math.max(0, opacity));
|
||||
|
||||
// 重新应用纹理以更新透明度
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
} else if (this.textureSource) {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
|
||||
return this.opacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到下一个预设材质
|
||||
* @returns {Promise} 切换完成的Promise
|
||||
*/
|
||||
nextTexture() {
|
||||
const textures = texturePresetManager.getAllTextures();
|
||||
if (textures.length === 0) return Promise.resolve();
|
||||
|
||||
this.currentTextureIndex = (this.currentTextureIndex + 1) % textures.length;
|
||||
const nextTexture = textures[this.currentTextureIndex];
|
||||
|
||||
return this.setTextureById(nextTexture.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一个预设材质
|
||||
* @returns {Promise} 切换完成的Promise
|
||||
*/
|
||||
previousTexture() {
|
||||
const textures = texturePresetManager.getAllTextures();
|
||||
if (textures.length === 0) return Promise.resolve();
|
||||
|
||||
this.currentTextureIndex =
|
||||
this.currentTextureIndex === 0 ? textures.length - 1 : this.currentTextureIndex - 1;
|
||||
const prevTexture = textures[this.currentTextureIndex];
|
||||
|
||||
return this.setTextureById(prevTexture.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用索引切换纹理
|
||||
* @param {Number} index 纹理索引
|
||||
*/
|
||||
switchTexture(index) {
|
||||
const textures = texturePresetManager.getAllTextures();
|
||||
if (index >= 0 && index < textures.length) {
|
||||
this.currentTextureIndex = index;
|
||||
const texture = textures[index];
|
||||
return this.setTextureById(texture.id);
|
||||
}
|
||||
return Promise.reject(new Error("无效的纹理索引"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的材质信息
|
||||
* @returns {Object|null} 材质信息
|
||||
*/
|
||||
getCurrentTexture() {
|
||||
if (this.selectedTextureId) {
|
||||
return texturePresetManager.getTextureById(this.selectedTextureId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 获取所有可用材质
|
||||
const allTextures = texturePresetManager.getAllTextures();
|
||||
const textureOptions = allTextures.map((texture, index) => ({
|
||||
value: texture.id,
|
||||
label: texture.name,
|
||||
preview: texturePresetManager.getTexturePreviewUrl(texture),
|
||||
category: texture.category,
|
||||
}));
|
||||
|
||||
// 定义纹理笔刷特有属性
|
||||
const textureProperties = [
|
||||
{
|
||||
id: "textureSelector",
|
||||
name: t('Canvas.TextureSelector'),
|
||||
type: "texture-grid",
|
||||
defaultValue: this.selectedTextureId,
|
||||
options: textureOptions,
|
||||
description: t('Canvas.selectTexture'),
|
||||
category: t('Canvas.TextureSettings'),
|
||||
order: 100,
|
||||
hidden: allTextures.length === 0,
|
||||
},
|
||||
// {
|
||||
// id: "textureRepeat",
|
||||
// name: "纹理重复模式",
|
||||
// type: "select",
|
||||
// defaultValue: this.textureRepeat,
|
||||
// options: [
|
||||
// { value: "repeat", label: "双向重复" },
|
||||
// { value: "repeat-x", label: "水平重复" },
|
||||
// { value: "repeat-y", label: "垂直重复" },
|
||||
// { value: "no-repeat", label: "不重复" },
|
||||
// ],
|
||||
// description: "设置纹理的重复模式",
|
||||
// category: "纹理设置",
|
||||
// order: 110,
|
||||
// },
|
||||
// {
|
||||
// id: "textureScale",
|
||||
// name: "纹理缩放",
|
||||
// type: "slider",
|
||||
// defaultValue: this.textureScale,
|
||||
// min: 0.1,
|
||||
// max: 5,
|
||||
// step: 0.1,
|
||||
// description: "调整纹理的缩放比例",
|
||||
// category: "纹理设置",
|
||||
// order: 120,
|
||||
// },
|
||||
// {
|
||||
// id: "textureAngle",
|
||||
// name: "纹理旋转",
|
||||
// type: "slider",
|
||||
// defaultValue: this.textureAngle,
|
||||
// min: 0,
|
||||
// max: 360,
|
||||
// step: 5,
|
||||
// description: "调整纹理的旋转角度",
|
||||
// category: "纹理设置",
|
||||
// order: 130,
|
||||
// },
|
||||
// {
|
||||
// id: "opacity",
|
||||
// name: "纹理透明度",
|
||||
// type: "slider",
|
||||
// defaultValue: this.opacity,
|
||||
// min: 0,
|
||||
// max: 1,
|
||||
// step: 0.05,
|
||||
// description: "调整纹理的透明度",
|
||||
// category: "纹理设置",
|
||||
// order: 140,
|
||||
// },
|
||||
// {
|
||||
// id: "uploadTexture",
|
||||
// name: "上传纹理",
|
||||
// type: "file",
|
||||
// action: "uploadTexture",
|
||||
// description: "上传自定义纹理",
|
||||
// category: "纹理设置",
|
||||
// order: 150,
|
||||
// },
|
||||
// {
|
||||
// id: "texturePreview",
|
||||
// name: "纹理预览",
|
||||
// type: "preview",
|
||||
// description: "当前纹理预览",
|
||||
// category: "纹理设置",
|
||||
// order: 160,
|
||||
// getValue: () => {
|
||||
// const currentTexture = this.getCurrentTexture();
|
||||
// return currentTexture
|
||||
// ? texturePresetManager.getTexturePreviewUrl(currentTexture)
|
||||
// : null;
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...textureProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理纹理笔刷特有属性
|
||||
if (propId === "textureSelector") {
|
||||
this.setTextureById(value);
|
||||
return true;
|
||||
} else if (propId === "textureRepeat") {
|
||||
this.setTextureRepeat(value);
|
||||
return true;
|
||||
} else if (propId === "textureScale") {
|
||||
this.setTextureScale(value);
|
||||
return true;
|
||||
} else if (propId === "textureAngle") {
|
||||
this.setTextureAngle(value);
|
||||
return true;
|
||||
} else if (propId === "opacity") {
|
||||
this.setopacity(value);
|
||||
return true;
|
||||
} else if (propId === "uploadTexture") {
|
||||
// 触发上传纹理事件
|
||||
// 这里通常由外部处理,返回true表示属性被处理
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义材质
|
||||
* @param {Object} textureData 材质数据
|
||||
* @returns {String} 材质ID
|
||||
*/
|
||||
addCustomTexture(textureData) {
|
||||
const textureId = texturePresetManager.addCustomTexture(textureData);
|
||||
|
||||
// 重新加载材质预设
|
||||
this._loadTexturePresets();
|
||||
|
||||
// 保存到本地存储
|
||||
texturePresetManager.saveCustomTexturesToStorage();
|
||||
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义材质
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Boolean} 是否删除成功
|
||||
*/
|
||||
removeCustomTexture(textureId) {
|
||||
const success = texturePresetManager.removeCustomTexture(textureId);
|
||||
|
||||
if (success) {
|
||||
// 如果删除的是当前选中的材质,切换到第一个可用材质
|
||||
if (this.selectedTextureId === textureId) {
|
||||
const allTextures = texturePresetManager.getAllTextures();
|
||||
if (allTextures.length > 0) {
|
||||
this.setTextureById(allTextures[0].id);
|
||||
} else {
|
||||
this.selectedTextureId = null;
|
||||
this.currentTextureIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载材质预设
|
||||
this._loadTexturePresets();
|
||||
|
||||
// 保存到本地存储
|
||||
texturePresetManager.saveCustomTexturesToStorage();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
const currentTexture = this.getCurrentTexture();
|
||||
if (currentTexture) {
|
||||
return texturePresetManager.getTexturePreviewUrl(currentTexture);
|
||||
}
|
||||
|
||||
// 返回默认纹理预览
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48cGF0dGVybiBpZD0icGF0dGVybiIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIj48cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiBmaWxsPSIjZGRkIi8+PHJlY3QgeD0iNSIgeT0iNSIgd2lkdGg9IjUiIGhlaWdodD0iNSIgZmlsbD0iI2RkZCIvPjwvcGF0dGVybj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9InVybCgjcGF0dGVybikiLz48L3N2Zz4=";
|
||||
}
|
||||
|
||||
/**
|
||||
* 笔刷被选中时调用
|
||||
* @override
|
||||
*/
|
||||
onSelected() {
|
||||
// 重新加载材质预设(可能有新的自定义材质)
|
||||
this._loadTexturePresets();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁笔刷实例并清理资源
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.textureSource = null;
|
||||
this.selectedTextureId = null;
|
||||
this.texturePresets = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置材质属性
|
||||
* @param {String} property 属性名称
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否设置成功
|
||||
*/
|
||||
setTextureProperty(property, value) {
|
||||
switch (property) {
|
||||
case "scale":
|
||||
return this.setTextureScale(value);
|
||||
case "opacity":
|
||||
return this.setopacity(value);
|
||||
case "repeat":
|
||||
return this.setTextureRepeat(value);
|
||||
case "angle":
|
||||
return this.setTextureAngle(value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材质属性
|
||||
* @param {String} property 属性名称
|
||||
* @returns {any} 属性值
|
||||
*/
|
||||
getTextureProperty(property) {
|
||||
switch (property) {
|
||||
case "scale":
|
||||
return this.textureScale;
|
||||
case "opacity":
|
||||
return this.opacity;
|
||||
case "repeat":
|
||||
return this.textureRepeat;
|
||||
case "angle":
|
||||
return this.textureAngle;
|
||||
case "textureId":
|
||||
return this.selectedTextureId;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用材质预设
|
||||
* @param {String|Object} preset 预设ID或预设对象
|
||||
* @returns {Boolean} 是否应用成功
|
||||
*/
|
||||
applyTexturePreset(preset) {
|
||||
let presetData = null;
|
||||
|
||||
if (typeof preset === "string") {
|
||||
// 如果是预设ID,从预设管理器获取
|
||||
presetData = texturePresetManager.applyTexturePreset(preset);
|
||||
} else if (typeof preset === "object") {
|
||||
// 如果是预设对象,直接使用
|
||||
presetData = preset;
|
||||
}
|
||||
|
||||
if (!presetData) {
|
||||
console.warn("无效的材质预设:", preset);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 应用预设设置
|
||||
if (presetData.textureId) {
|
||||
this.setTextureById(presetData.textureId);
|
||||
}
|
||||
|
||||
if (presetData.scale !== undefined) {
|
||||
this.setTextureScale(presetData.scale);
|
||||
}
|
||||
|
||||
if (presetData.opacity !== undefined) {
|
||||
this.setopacity(presetData.opacity);
|
||||
}
|
||||
|
||||
if (presetData.repeat !== undefined) {
|
||||
this.setTextureRepeat(presetData.repeat);
|
||||
}
|
||||
|
||||
if (presetData.angle !== undefined) {
|
||||
this.setTextureAngle(presetData.angle);
|
||||
}
|
||||
|
||||
// 如果预设包含笔刷属性,也一并应用
|
||||
if (presetData.brushSize !== undefined && this.brush) {
|
||||
this.brush.width = presetData.brushSize;
|
||||
}
|
||||
|
||||
if (presetData.brushOpacity !== undefined && this.brush) {
|
||||
this.brush.opacity = presetData.brushOpacity;
|
||||
}
|
||||
|
||||
if (presetData.brushColor !== undefined && this.brush) {
|
||||
this.brush.color = presetData.brushColor;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前材质状态
|
||||
* @returns {Object} 当前材质状态
|
||||
*/
|
||||
getCurrentTextureState() {
|
||||
return {
|
||||
textureId: this.selectedTextureId,
|
||||
scale: this.textureScale,
|
||||
opacity: this.opacity,
|
||||
repeat: this.textureRepeat,
|
||||
angle: this.textureAngle,
|
||||
// 包含笔刷状态
|
||||
brushSize: this.brush ? this.brush.width : this.options.width,
|
||||
brushOpacity: this.brush ? this.brush.opacity : this.options.opacity,
|
||||
brushColor: this.brush ? this.brush.color : this.options.color,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复材质状态
|
||||
* @param {Object} state 要恢复的状态
|
||||
* @returns {Boolean} 是否恢复成功
|
||||
*/
|
||||
restoreTextureState(state) {
|
||||
if (!state) return false;
|
||||
|
||||
try {
|
||||
// 恢复材质属性
|
||||
if (state.textureId) {
|
||||
this.setTextureById(state.textureId);
|
||||
}
|
||||
|
||||
if (state.scale !== undefined) {
|
||||
this.setTextureScale(state.scale);
|
||||
}
|
||||
|
||||
if (state.opacity !== undefined) {
|
||||
this.setopacity(state.opacity);
|
||||
}
|
||||
|
||||
if (state.repeat !== undefined) {
|
||||
this.setTextureRepeat(state.repeat);
|
||||
}
|
||||
|
||||
if (state.angle !== undefined) {
|
||||
this.setTextureAngle(state.angle);
|
||||
}
|
||||
|
||||
// 恢复笔刷属性
|
||||
if (this.brush) {
|
||||
if (state.brushSize !== undefined) {
|
||||
this.brush.width = state.brushSize;
|
||||
}
|
||||
|
||||
if (state.brushOpacity !== undefined) {
|
||||
this.brush.opacity = state.brushOpacity;
|
||||
}
|
||||
|
||||
if (state.brushColor !== undefined) {
|
||||
this.brush.color = state.brushColor;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("恢复材质状态失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建材质预设
|
||||
* @param {String} name 预设名称
|
||||
* @returns {String} 预设ID
|
||||
*/
|
||||
createTexturePreset(name) {
|
||||
const currentState = this.getCurrentTextureState();
|
||||
return texturePresetManager.createTexturePreset(name, currentState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的材质分类
|
||||
* @returns {Array} 分类数组
|
||||
*/
|
||||
getTextureCategories() {
|
||||
return texturePresetManager.getCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类获取材质
|
||||
* @param {String} category 分类名称
|
||||
* @returns {Array} 材质数组
|
||||
*/
|
||||
getTexturesByCategory(category) {
|
||||
return texturePresetManager.getTexturesByCategory(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索材质
|
||||
* @param {String} query 搜索关键词
|
||||
* @returns {Array} 匹配的材质数组
|
||||
*/
|
||||
searchTextures(query) {
|
||||
return texturePresetManager.searchTextures(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载材质图像
|
||||
* @param {String} textureId 材质ID
|
||||
* @returns {Promise<HTMLImageElement>} 图像对象
|
||||
*/
|
||||
preloadTexture(textureId) {
|
||||
return texturePresetManager.loadTextureImage(textureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量预加载材质
|
||||
* @param {Array} textureIds 材质ID数组
|
||||
* @returns {Promise<Array>} 加载结果数组
|
||||
*/
|
||||
preloadTextures(textureIds) {
|
||||
const loadPromises = textureIds.map((id) =>
|
||||
this.preloadTexture(id).catch((error) => ({ id, error }))
|
||||
);
|
||||
return Promise.all(loadPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取材质统计信息
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getTextureStats() {
|
||||
return texturePresetManager.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SVG 路径是否为空
|
||||
* @private
|
||||
* @param {Array} pathData SVG 路径数据
|
||||
* @returns {Boolean} 是否为空路径
|
||||
*/
|
||||
_isEmptySVGPath(pathData) {
|
||||
if (!pathData || pathData.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查路径是否只包含移动命令或者是一个点
|
||||
let hasDrawing = false;
|
||||
let moveCount = 0;
|
||||
|
||||
for (let i = 0; i < pathData.length; i++) {
|
||||
const command = pathData[i];
|
||||
if (command[0] === "M") {
|
||||
moveCount++;
|
||||
} else if (command[0] === "L" || command[0] === "Q" || command[0] === "C") {
|
||||
hasDrawing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有移动命令且超过1个,或者没有绘制命令,则认为是空路径
|
||||
return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
|
||||
/**
|
||||
* 书法笔刷
|
||||
* 模拟中国传统书法效果,具有笔锋和墨色变化
|
||||
*/
|
||||
export class WritingBrush extends BaseBrush {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "writing",
|
||||
name: options.t('Canvas.Writing'),
|
||||
description: "模拟中国传统书法毛笔效果,具有笔锋和墨色变化",
|
||||
category: options.t('Canvas.SpecialEffectsBrush'),
|
||||
icon: "writing",
|
||||
...options,
|
||||
});
|
||||
|
||||
// 书法笔刷特有属性
|
||||
this.brushPressure = options.brushPressure || 0.7;
|
||||
this.inkAmount = options.inkAmount || 20;
|
||||
this.brushTaperFactor = options.brushTaperFactor || 0.6;
|
||||
this.enableInkDripping =
|
||||
options.enableInkDripping !== undefined ? options.enableInkDripping : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔刷实例
|
||||
* @returns {Object} fabric笔刷实例
|
||||
*/
|
||||
create() {
|
||||
if (!this.canvas) {
|
||||
throw new Error("画布实例不存在");
|
||||
}
|
||||
|
||||
// 创建fabric原生书法笔刷
|
||||
this.brush = new fabric.WritingBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 书法笔刷特有属性
|
||||
if (options.brushPressure !== undefined) {
|
||||
this.brushPressure = options.brushPressure;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.brushPressure !== undefined) {
|
||||
brush.brushPressure = this.brushPressure;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.inkAmount !== undefined) {
|
||||
this.inkAmount = options.inkAmount;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.inkAmount !== undefined) {
|
||||
brush.inkAmount = this.inkAmount;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.brushTaperFactor !== undefined) {
|
||||
this.brushTaperFactor = options.brushTaperFactor;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.brushTaperFactor !== undefined) {
|
||||
brush.brushTaperFactor = this.brushTaperFactor;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.enableInkDripping !== undefined) {
|
||||
this.enableInkDripping = options.enableInkDripping;
|
||||
|
||||
// 如果原生笔刷支持此属性,则设置
|
||||
if (brush.enableInkDripping !== undefined) {
|
||||
brush.enableInkDripping = this.enableInkDripping;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置笔压感应
|
||||
* @param {Number} pressure 笔压值(0-1)
|
||||
*/
|
||||
setBrushPressure(pressure) {
|
||||
this.brushPressure = Math.max(0.1, Math.min(1, pressure));
|
||||
|
||||
if (this.brush && this.brush.brushPressure !== undefined) {
|
||||
this.brush.brushPressure = this.brushPressure;
|
||||
}
|
||||
|
||||
return this.brushPressure;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置墨量
|
||||
* @param {Number} amount 墨量值
|
||||
*/
|
||||
setInkAmount(amount) {
|
||||
this.inkAmount = Math.max(1, Math.min(50, amount));
|
||||
|
||||
if (this.brush && this.brush.inkAmount !== undefined) {
|
||||
this.brush.inkAmount = this.inkAmount;
|
||||
}
|
||||
|
||||
return this.inkAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置笔锋系数
|
||||
* @param {Number} factor 笔锋系数(0-1)
|
||||
*/
|
||||
setBrushTaperFactor(factor) {
|
||||
this.brushTaperFactor = Math.max(0, Math.min(1, factor));
|
||||
|
||||
if (this.brush && this.brush.brushTaperFactor !== undefined) {
|
||||
this.brush.brushTaperFactor = this.brushTaperFactor;
|
||||
}
|
||||
|
||||
return this.brushTaperFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用墨滴效果
|
||||
* @param {Boolean} enabled 是否启用
|
||||
*/
|
||||
setInkDripping(enabled) {
|
||||
this.enableInkDripping = enabled;
|
||||
|
||||
if (this.brush && this.brush.enableInkDripping !== undefined) {
|
||||
this.brush.enableInkDripping = this.enableInkDripping;
|
||||
}
|
||||
|
||||
return this.enableInkDripping;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
* @override
|
||||
*/
|
||||
getConfigurableProperties() {
|
||||
// 获取基础属性
|
||||
const baseProperties = super.getConfigurableProperties();
|
||||
|
||||
// 定义书法笔刷特有属性
|
||||
const writingProperties = [
|
||||
{
|
||||
id: "brushPressure",
|
||||
name: this.t('Canvas.WritingBrushPressure'),
|
||||
type: "slider",
|
||||
defaultValue: this.brushPressure,
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.WritingBrushPressureDescription'),
|
||||
category: this.t('Canvas.WritingSettings'),
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
id: "inkAmount",
|
||||
name: this.t('Canvas.WritingAmount'),
|
||||
type: "slider",
|
||||
defaultValue: this.inkAmount,
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
description: this.t('Canvas.WritingAmountDescription'),
|
||||
category: this.t('Canvas.WritingSettings'),
|
||||
order: 110,
|
||||
},
|
||||
{
|
||||
id: "brushTaperFactor",
|
||||
name: this.t('Canvas.WritingPenTipCoefficient'),
|
||||
type: "slider",
|
||||
defaultValue: this.brushTaperFactor,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
description: this.t('Canvas.WritingPenTipCoefficientDescription'),
|
||||
category: this.t('Canvas.WritingSettings'),
|
||||
order: 120,
|
||||
},
|
||||
{
|
||||
id: "enableInkDripping",
|
||||
name: this.t('Canvas.WritingDropEffect'),
|
||||
type: "checkbox",
|
||||
defaultValue: this.enableInkDripping,
|
||||
description: this.t('Canvas.WritingDropEffectDescription'),
|
||||
category: this.t('Canvas.WritingSettings'),
|
||||
order: 130,
|
||||
},
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
return [...baseProperties, ...writingProperties];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷属性
|
||||
* @param {String} propId 属性ID
|
||||
* @param {any} value 属性值
|
||||
* @returns {Boolean} 是否更新成功
|
||||
* @override
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 先检查基类能否处理此属性
|
||||
if (super.updateProperty(propId, value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理书法笔刷特有属性
|
||||
if (propId === "brushPressure") {
|
||||
this.setBrushPressure(value);
|
||||
return true;
|
||||
} else if (propId === "inkAmount") {
|
||||
this.setInkAmount(value);
|
||||
return true;
|
||||
} else if (propId === "brushTaperFactor") {
|
||||
this.setBrushTaperFactor(value);
|
||||
return true;
|
||||
} else if (propId === "enableInkDripping") {
|
||||
this.setInkDripping(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览图
|
||||
* @returns {String} 预览图URL
|
||||
*/
|
||||
getPreview() {
|
||||
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMzAgMzBDNTAgMzAgNjAgNzAgODAgNzAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTc1IDYwQzc4IDcwIDg1IDY1IDkwIDcwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+PC9zdmc+";
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export class CanvasEventManager {
|
||||
}
|
||||
|
||||
// 共享事件
|
||||
// this.setupSelectionEvents();
|
||||
this.setupSelectionEvents();
|
||||
// this.setupObjectEvents();
|
||||
// this.setupDoubleClickEvents();
|
||||
|
||||
@@ -228,7 +228,7 @@ export class CanvasEventManager {
|
||||
if (this.lastMousePositions.length > this.positionHistoryLimit) {
|
||||
this.lastMousePositions.shift();
|
||||
}
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
this.canvas.lastPosX = opt.e.clientX;
|
||||
this.canvas.lastPosY = opt.e.clientY;
|
||||
@@ -429,7 +429,7 @@ export class CanvasEventManager {
|
||||
|
||||
this.touchState.lastTouchTime = now;
|
||||
}
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
|
||||
this.canvas.lastPosX = currentX;
|
||||
this.canvas.lastPosY = currentY;
|
||||
@@ -616,7 +616,8 @@ export class CanvasEventManager {
|
||||
if (this.toolManager) {
|
||||
// this.toolManager.restoreSelectionState(); // 恢复选择状态
|
||||
}
|
||||
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
@@ -878,7 +879,7 @@ export class CanvasEventManager {
|
||||
updateSelectedLayer(opt) {
|
||||
const selected = opt.selected[0];
|
||||
if (selected) {
|
||||
this.layerManager.activeLayerId.value = selected.layerId;
|
||||
this.layerManager.setActiveID(selected?.info?.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,7 +1069,7 @@ export class CanvasEventManager {
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[4] += e.pageX - this.canvas.lastPosX;
|
||||
vpt[5] += e.pageY - this.canvas.lastPosY;
|
||||
|
||||
this.canvas.setViewportTransform(vpt);
|
||||
this.canvas.renderAll();
|
||||
this.canvas.lastPosX = e.pageX;
|
||||
this.canvas.lastPosY = e.pageY;
|
||||
|
||||
40
src/components/Canvas/DepthCanvas/tools/canvasMethod.js
Normal file
40
src/components/Canvas/DepthCanvas/tools/canvasMethod.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/** 克隆对象 */
|
||||
export async function cloneObjects(objects = []) {
|
||||
const arrs = []
|
||||
for (const obj of objects) {
|
||||
const clonedObj = await new Promise((resolve, reject) => {
|
||||
obj.clone((v) => {
|
||||
v.set({
|
||||
left: obj.left,
|
||||
top: obj.top,
|
||||
width: obj.width,
|
||||
height: obj.height,
|
||||
scaleX: obj.scaleX,
|
||||
scaleY: obj.scaleY,
|
||||
})
|
||||
resolve(v)
|
||||
})
|
||||
})
|
||||
arrs.push(clonedObj)
|
||||
}
|
||||
return arrs
|
||||
}
|
||||
|
||||
|
||||
/** 获取组合对象边界 */
|
||||
export async function getObjectsBoundingBox(objects = []) {
|
||||
const box1 = { x: Infinity, y: Infinity }
|
||||
const box2 = { x: -Infinity, y: -Infinity }
|
||||
objects.forEach(obj => {
|
||||
box1.x = Math.min(box1.x, obj.left)
|
||||
box1.y = Math.min(box1.y, obj.top)
|
||||
box2.x = Math.max(box2.x, obj.left + obj.width * obj.scaleX)
|
||||
box2.y = Math.max(box2.y, obj.top + obj.height * obj.scaleY)
|
||||
})
|
||||
return {
|
||||
left: box1.x,
|
||||
top: box1.y,
|
||||
width: box2.x - box1.x,
|
||||
height: box2.y - box1.y,
|
||||
}
|
||||
}
|
||||
26
src/components/Canvas/DepthCanvas/tools/exportMethod.js
Normal file
26
src/components/Canvas/DepthCanvas/tools/exportMethod.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createStaticCanvas } from './canvasFactory'
|
||||
import { getObjectsBoundingBox, cloneObjects } from './canvasMethod'
|
||||
/** 导出指定对象 */
|
||||
export async function exportObjectsToImage(objects = [], isDetails = false) {
|
||||
const clonedObjects = await cloneObjects(objects)
|
||||
const boundingBox = await getObjectsBoundingBox(clonedObjects)
|
||||
const staticCanvas = createStaticCanvas(document.createElement('canvas'))
|
||||
staticCanvas.setWidth(boundingBox.width)
|
||||
staticCanvas.setHeight(boundingBox.height)
|
||||
clonedObjects.forEach(obj => {
|
||||
obj.set({
|
||||
left: obj.left - boundingBox.left,
|
||||
top: obj.top - boundingBox.top,
|
||||
})
|
||||
staticCanvas.add(obj)
|
||||
})
|
||||
// 导出图片
|
||||
const dataURL = staticCanvas.toDataURL({
|
||||
type: 'image/png',
|
||||
quality: 1,
|
||||
})
|
||||
return isDetails ? {
|
||||
url: dataURL,
|
||||
...boundingBox,
|
||||
} : dataURL
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 组件
|
||||
*/
|
||||
export const NODE_COMPONENT = {
|
||||
RESULT_IMAGE: 'result-image',
|
||||
CARD: 'card',
|
||||
TEXT: 'text',
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型
|
||||
*/
|
||||
export const NODE_TYPE = {
|
||||
INPUT: 'InputNode',
|
||||
SECONDARY: 'SecondaryNode',
|
||||
OUTPUT: 'OutputNode',
|
||||
ALONE: 'AloneNode',
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点数据类型
|
||||
*/
|
||||
export const NODE_DATATYPE = {
|
||||
RESULT_IMAGE: 'result-image',
|
||||
CARDS_SELECT: 'cards-select',
|
||||
TO_REAL_STYLE: 'to-real-style',
|
||||
SURFACE_EDIT: 'surface-edit',
|
||||
SCENE_COMPOSITION: 'scene-composition',
|
||||
COLOR_PALETTE: 'color-palette',
|
||||
TO_3D_MODEL: 'to-3d-model',
|
||||
TO_3VIEW: 'to-3view',
|
||||
}
|
||||
/**
|
||||
* 节点数据层级
|
||||
*/
|
||||
export const NODE_DATATIER = {
|
||||
RESULT_IMAGE: 0,
|
||||
CARDS_SELECT: 0,
|
||||
TO_REAL_STYLE: 1,
|
||||
SURFACE_EDIT: 1,
|
||||
SCENE_COMPOSITION: 2,
|
||||
COLOR_PALETTE: 2,
|
||||
TO_3D_MODEL: 2,
|
||||
TO_3VIEW: 3,
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
import { chatUrl } from '@/api/agent'
|
||||
import type { AgentParamsType } from '@/api/agent'
|
||||
import { useUserInfoStore, useProjectStore, useAgentStore } from '@/stores'
|
||||
import MyEvent from '@/utils/myEvent'
|
||||
|
||||
const userStore = useUserInfoStore()
|
||||
const agentStore = useAgentStore()
|
||||
@@ -235,7 +236,7 @@
|
||||
// 过滤掉 id: 等字段,只取 data:
|
||||
|
||||
let isNodeIdEvent = false
|
||||
if (event.startsWith('event:')) {
|
||||
if (event.includes('nodeId')) {
|
||||
isNodeIdEvent = true
|
||||
// continue
|
||||
}
|
||||
@@ -248,23 +249,30 @@
|
||||
flag = false
|
||||
break
|
||||
}
|
||||
if (event.includes('todo') || event.includes('webAddress')) {
|
||||
break
|
||||
}
|
||||
|
||||
const dataLines = event
|
||||
.split(/\n/)
|
||||
.filter((line) => line.startsWith('data:'))
|
||||
.map((line) => line.replace(/^data:\s*/, '').trim())
|
||||
const dataLines = event
|
||||
.split(/\n/)
|
||||
.filter((line) => line.startsWith('data:'))
|
||||
.map((line) => line.replace(/^data:\s*/, '').trim())
|
||||
.filter((content) => content.startsWith('{') || content.startsWith('['))
|
||||
// console.log('dataLInes', dataLines)
|
||||
if (isNodeIdEvent) {
|
||||
params.versionID = dataLines[0]
|
||||
projectStore.setProject({ nodeId: dataLines[0] })
|
||||
}
|
||||
if (event.includes('tool')) {
|
||||
MyEvent.emit('loading-sketch')
|
||||
}
|
||||
|
||||
if (dataLines.length === 0) continue
|
||||
const jsonText = dataLines.join('\n')
|
||||
|
||||
try {
|
||||
const jsonData = JSON.parse(jsonText)
|
||||
// console.log('jsonData', jsonData)
|
||||
console.log('jsonData', jsonData)
|
||||
|
||||
// 赋值 project_id 和 version_id
|
||||
// if (jsonData.project_id) params.projectID = jsonData.project_id
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
@load="handleImageLoad(index)"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="showLoading" class="sketch-item loading-gif">
|
||||
<img src="@/assets/images/sketch-loading.gif" alt="loading" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="reportBorder">
|
||||
<div class="report">
|
||||
@@ -146,7 +149,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue'
|
||||
import Menu from './Menu.vue'
|
||||
import LoadingImg from '@/assets/images/sketch-loading.gif'
|
||||
import reportNull from '@/assets/images/reportNull.png'
|
||||
@@ -154,11 +157,12 @@
|
||||
import { deleteSketchFlowCanvas } from '@/api/flow-canvas'
|
||||
import { useProjectStore } from '@/stores'
|
||||
const projectStore = useProjectStore()
|
||||
import MyEvent from '@/utils/myEvent'
|
||||
|
||||
const emits = defineEmits(['deleteSketch'])
|
||||
|
||||
// 存储每个图片的加载状态
|
||||
const loadedStatus = reactive<Record<number, boolean>>({})
|
||||
const loadedStatus = ref<boolean>(false)
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -174,6 +178,9 @@
|
||||
// 图片加载完成时触发
|
||||
const handleImageLoad = (index: number) => {
|
||||
loadedStatus[index] = true
|
||||
if (index === props.sketchList.length - 1) {
|
||||
showLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前显示的图片源
|
||||
@@ -182,7 +189,7 @@
|
||||
return null
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return loadedStatus[index] ? item : LoadingImg
|
||||
return item
|
||||
}
|
||||
if (typeof item === 'object') {
|
||||
return Object.values(item)[0]
|
||||
@@ -215,6 +222,28 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showLoading = ref(false)
|
||||
const handleLoadingSketch = () => {
|
||||
showLoading.value = true
|
||||
}
|
||||
watch(
|
||||
() => props.sketchList,
|
||||
(val) => {
|
||||
console.log('-sketchList-', val)
|
||||
|
||||
if (val.length > 0) {
|
||||
showLoading.value = false
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
onMounted(() => {
|
||||
MyEvent.add('loading-sketch', handleLoadingSketch)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
MyEvent.remove('loading-sketch', handleLoadingSketch)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
Reference in New Issue
Block a user