深度画布功能
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="icon"><svg-icon name="export" size="12" /></span>
|
||||||
<span class="text">Export</span>
|
<span class="text">Export</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="export" @click="emit('import')">
|
||||||
|
<span class="text">import</span>
|
||||||
|
</button>
|
||||||
<button class="workbench">
|
<button class="workbench">
|
||||||
<span class="icon"><svg-icon name="dc-workbench" size="20" /></span>
|
<span class="icon"><svg-icon name="dc-workbench" size="20" /></span>
|
||||||
<span class="text">Workbench</span>
|
<span class="text">Workbench</span>
|
||||||
@@ -30,6 +33,7 @@
|
|||||||
step: { default: 0.1, type: Number }
|
step: { default: 0.1, type: Number }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['export', 'import'])
|
const emit = defineEmits(['export', 'import'])
|
||||||
|
const importLocalImage = inject('importLocalImage') as () => void
|
||||||
const stateManager = inject('stateManager') as any
|
const stateManager = inject('stateManager') as any
|
||||||
const toolManager = inject('toolManager') as any
|
const toolManager = inject('toolManager') as any
|
||||||
const tool = computed(() => toolManager.currentTool.value)
|
const tool = computed(() => toolManager.currentTool.value)
|
||||||
@@ -42,7 +46,13 @@
|
|||||||
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
||||||
{ name: OperationType.DRAW, icon: 'dc-brush', 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.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.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
|
||||||
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
||||||
{ type: 'line' },
|
{ type: 'line' },
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
||||||
<div class="thumb"></div>
|
<div class="thumb"></div>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<div @dblclick="onClickEditName" v-if="!editName">
|
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
|
||||||
{{ layer.info.name || '未命名图层' }}
|
{{ layer.info.name || '未命名图层' }}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="icons">
|
<div class="icons">
|
||||||
<span @click="onClickShowHide"
|
<span @click.stop="onClickShowHide"
|
||||||
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
||||||
/></span>
|
/></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>
|
<span><svg-icon name="dc-down_arrow" size="11" /></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject, nextTick } from 'vue'
|
import { ref, inject, nextTick } from 'vue'
|
||||||
|
import { OperationType } from '../../tools/layerHelper'
|
||||||
|
|
||||||
const layerManager = inject('layerManager') as any
|
const layerManager = inject('layerManager') as any
|
||||||
|
const toolManager = inject('toolManager') as any
|
||||||
const editName = ref(false)
|
const editName = ref(false)
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
layer: {
|
layer: {
|
||||||
@@ -58,6 +61,7 @@
|
|||||||
}
|
}
|
||||||
const onClickLayer = () => {
|
const onClickLayer = () => {
|
||||||
layerManager.setActiveID(props.layer.info.id)
|
layerManager.setActiveID(props.layer.info.id)
|
||||||
|
toolManager.setTool(OperationType.SELECT)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -109,13 +113,11 @@
|
|||||||
> .name {
|
> .name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
overflow: hidden;
|
||||||
> div {
|
> div {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
display: flex;
|
line-height: 3rem;
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<span class="title">Layer</span>
|
<span class="title">Layer</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<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>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -60,9 +60,11 @@
|
|||||||
const handleDragEnd = (event) => {
|
const handleDragEnd = (event) => {
|
||||||
draging.value = false
|
draging.value = false
|
||||||
const { from, to, oldIndex, newIndex, data } = event
|
const { from, to, oldIndex, newIndex, data } = event
|
||||||
console.log('oldIndex', data)
|
|
||||||
layerManager.dragSort(data.info.id, newIndex)
|
layerManager.dragSort(data.info.id, newIndex)
|
||||||
}
|
}
|
||||||
|
const addLayer = () => {
|
||||||
|
layerManager.createEmptyLayer()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<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">
|
<div class="canvas-container" ref="canvasContainerRef">
|
||||||
<canvas ref="canvasRef"></canvas>
|
<canvas ref="canvasRef"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<layer-panel />
|
<template v-if="isReady">
|
||||||
<details-panel />
|
<layer-panel />
|
||||||
<header-tools @export="exportCanvas" />
|
<details-panel />
|
||||||
<zoom
|
<depth-header-tools @export="exportCanvas" @import="importCanvas" />
|
||||||
:zoom="canvasManager.currentZoom.value / 100"
|
<brush-control-panel :currentTool="toolManager.currentTool.value" />
|
||||||
:step="0.1"
|
<zoom
|
||||||
is-home
|
:zoom="canvasManager.currentZoom.value / 100"
|
||||||
@home="() => canvasManager.resetZoom()"
|
:step="0.1"
|
||||||
/>
|
is-home
|
||||||
|
@home="() => canvasManager.resetZoom()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { fabric } from 'fabric-with-all'
|
import { fabric } from 'fabric-with-all'
|
||||||
import { computed, ref, markRaw, onMounted, nextTick, provide, onBeforeMount } from 'vue'
|
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 layerPanel from './components/layer-panel/index.vue'
|
||||||
import detailsPanel from './components/details-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 zoom from '../components/zoom.vue'
|
||||||
|
import brushControlPanel from './components/brush-control-panel.vue'
|
||||||
|
|
||||||
// 管理器
|
// 管理器
|
||||||
import { StateManager } from './manager/StateManager'
|
import { StateManager } from './manager/StateManager'
|
||||||
import { LayerManager } from './manager/LayerManager'
|
import { LayerManager } from './manager/LayerManager'
|
||||||
import { EventManager } from './manager/EventManager'
|
|
||||||
import { CanvasManager } from './manager/CanvasManager'
|
import { CanvasManager } from './manager/CanvasManager'
|
||||||
import { ToolManager } from './manager/ToolManager'
|
import { ToolManager } from './manager/ToolManager'
|
||||||
|
|
||||||
|
// 准备就绪
|
||||||
|
const isReady = ref(false)
|
||||||
const canvasContainerRef = ref(null)
|
const canvasContainerRef = ref(null)
|
||||||
const canvasRef = ref(null)
|
const canvasRef = ref(null)
|
||||||
|
|
||||||
@@ -56,11 +61,6 @@
|
|||||||
stateManager.setManager({ layerManager })
|
stateManager.setManager({ layerManager })
|
||||||
provide('layerManager', layerManager)
|
provide('layerManager', layerManager)
|
||||||
|
|
||||||
// 事件管理器
|
|
||||||
const eventManager = new EventManager({ stateManager })
|
|
||||||
stateManager.setManager({ eventManager })
|
|
||||||
provide('eventManager', eventManager)
|
|
||||||
|
|
||||||
// 工具管理器
|
// 工具管理器
|
||||||
const toolManager = new ToolManager({ stateManager, canvasManager })
|
const toolManager = new ToolManager({ stateManager, canvasManager })
|
||||||
stateManager.setManager({ toolManager })
|
stateManager.setManager({ toolManager })
|
||||||
@@ -75,6 +75,10 @@
|
|||||||
canvasWidth: 750,
|
canvasWidth: 750,
|
||||||
canvasHeight: 600
|
canvasHeight: 600
|
||||||
})
|
})
|
||||||
|
stateManager?.onMounted?.()
|
||||||
|
canvasManager?.onMounted?.()
|
||||||
|
layerManager?.onMounted?.()
|
||||||
|
toolManager?.onMounted?.()
|
||||||
|
|
||||||
const trailingTimeout = ref(null)
|
const trailingTimeout = ref(null)
|
||||||
observer.value = new ResizeObserver((entries) => {
|
observer.value = new ResizeObserver((entries) => {
|
||||||
@@ -84,10 +88,10 @@
|
|||||||
}, 100)
|
}, 100)
|
||||||
})
|
})
|
||||||
observer.value.observe(canvasContainerRef.value)
|
observer.value.observe(canvasContainerRef.value)
|
||||||
|
|
||||||
|
isReady.value = true // 准备就绪
|
||||||
})
|
})
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {})
|
||||||
// eventManager.removeEvents() // 移除事件
|
|
||||||
})
|
|
||||||
async function handleWindowResize() {
|
async function handleWindowResize() {
|
||||||
console.log('==========画布窗口大小变化==========')
|
console.log('==========画布窗口大小变化==========')
|
||||||
canvasManager.setCanvasViewSize({
|
canvasManager.setCanvasViewSize({
|
||||||
@@ -96,8 +100,42 @@
|
|||||||
})
|
})
|
||||||
canvasManager.resetZoom()
|
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 = () => {
|
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>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export class AnimationManager {
|
|||||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||||
vpt[4] = currentVpt[4] * scale + adjustX;
|
vpt[4] = currentVpt[4] * scale + adjustX;
|
||||||
vpt[5] = currentVpt[5] * scale + adjustY;
|
vpt[5] = currentVpt[5] * scale + adjustY;
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
@@ -167,7 +167,7 @@ export class AnimationManager {
|
|||||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||||
vpt[4] = currentVpt[4] * scale + adjustX;
|
vpt[4] = currentVpt[4] * scale + adjustX;
|
||||||
vpt[5] = currentVpt[5] * scale + adjustY;
|
vpt[5] = currentVpt[5] * scale + adjustY;
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
@@ -252,7 +252,7 @@ export class AnimationManager {
|
|||||||
const vpt = this.canvas.viewportTransform;
|
const vpt = this.canvas.viewportTransform;
|
||||||
vpt[4] = -x;
|
vpt[4] = -x;
|
||||||
vpt[5] = -y;
|
vpt[5] = -y;
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ export class AnimationManager {
|
|||||||
vpt[3] = viewTransform.zoom;
|
vpt[3] = viewTransform.zoom;
|
||||||
vpt[4] = viewTransform.panX;
|
vpt[4] = viewTransform.panX;
|
||||||
vpt[5] = viewTransform.panY;
|
vpt[5] = viewTransform.panY;
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
@@ -814,6 +814,7 @@ export class AnimationManager {
|
|||||||
|
|
||||||
// 更新缩放值显示
|
// 更新缩放值显示
|
||||||
this.currentZoom.value = Math.round(transform.zoom * 100);
|
this.currentZoom.value = Math.round(transform.zoom * 100);
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
},
|
},
|
||||||
onComplete: () => {
|
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 canvasX = this.canvasViewWidth / 2 - this.canvasWidth / 2
|
||||||
const canvasY = this.canvasViewHeight / 2 - this.canvasHeight / 2
|
const canvasY = this.canvasViewHeight / 2 - this.canvasHeight / 2
|
||||||
this.canvas.viewportTransform = [1, 0, 0, 1, canvasX, canvasY]
|
this.canvas.viewportTransform = [1, 0, 0, 1, canvasX, canvasY]
|
||||||
|
|
||||||
// 创建矩形
|
// 创建矩形
|
||||||
const rect = new fabric.Rect({
|
const rect = this.layerManager.createRectLayer({
|
||||||
left: 20,
|
left: 400,
|
||||||
top: 20,
|
top: 100,
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
fill: '#f00',
|
|
||||||
info: {
|
|
||||||
id: 'rect1',
|
|
||||||
name: 'rect1',
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.canvas.add(rect)
|
|
||||||
//创建圆形
|
//创建圆形
|
||||||
const circle = new fabric.Circle({
|
const circle = this.layerManager.createCircleLayer({
|
||||||
left: 200,
|
left: 200,
|
||||||
top: 200,
|
top: 200,
|
||||||
radius: 50,
|
|
||||||
fill: '#0f0',
|
|
||||||
info: {
|
|
||||||
id: 'circle',
|
|
||||||
name: 'circle',
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
this.canvas.add(circle)
|
|
||||||
// 文字
|
// 文字
|
||||||
const text = new fabric.Text('Hello World', {
|
const text = this.layerManager.createTextLayer('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)
|
|
||||||
this.animationManager = new AnimationManager(this.canvas, {
|
this.animationManager = new AnimationManager(this.canvas, {
|
||||||
currentZoom: this.currentZoom,
|
currentZoom: this.currentZoom,
|
||||||
canvasManager: this,
|
canvasManager: this,
|
||||||
@@ -117,18 +82,38 @@ export class CanvasManager {
|
|||||||
this.setupCanvasEvents()
|
this.setupCanvasEvents()
|
||||||
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
||||||
this.layerManager.updateLayers()
|
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() {
|
setupCanvasEvents() {
|
||||||
// 创建画布事件管理器
|
// 创建画布事件管理器
|
||||||
this.eventManager = new CanvasEventManager(this.canvas, {
|
this.eventManager = new CanvasEventManager(this.canvas, {
|
||||||
canvasManager: this,
|
canvasManager: this,
|
||||||
animationManager: this.animationManager,
|
animationManager: this.animationManager,
|
||||||
toolManager: this.stateManager.toolManager,
|
toolManager: this.stateManager.toolManager,
|
||||||
|
layerManager: this.stateManager.layerManager,
|
||||||
});
|
});
|
||||||
// 设置动画交互效果
|
// 设置动画交互效果
|
||||||
this.animationManager.setupInteractionAnimations();
|
this.animationManager.setupInteractionAnimations();
|
||||||
}
|
}
|
||||||
|
/** 设置激活对象 */
|
||||||
|
setActiveObjectByID(id: string) {
|
||||||
|
const obj = this.getObjectById(id)
|
||||||
|
if (obj) this.canvas.setActiveObject(obj)
|
||||||
|
this.renderAll()
|
||||||
|
}
|
||||||
resetZoom() {
|
resetZoom() {
|
||||||
this.animationManager.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 { ref } from 'vue'
|
||||||
|
import { fabric } from 'fabric-with-all'
|
||||||
|
import { createId } from '../../tools/tools'
|
||||||
|
import { exportObjectsToImage } from '../tools/exportMethod'
|
||||||
|
|
||||||
export class LayerManager {
|
export class LayerManager {
|
||||||
stateManager: any
|
stateManager: any
|
||||||
@@ -11,7 +14,12 @@ export class LayerManager {
|
|||||||
this.layers = ref([])
|
this.layers = ref([])
|
||||||
this.activeID = 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) {
|
getLayerByID(id) {
|
||||||
return this.layers.value.find((item: any) => item.info.id === id)
|
return this.layers.value.find((item: any) => item.info.id === id)
|
||||||
}
|
}
|
||||||
@@ -31,8 +39,11 @@ export class LayerManager {
|
|||||||
this.canvasManager.renderAll()
|
this.canvasManager.renderAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleteLayerByID(id) {
|
deleteLayerByID(id, isActive = true) {
|
||||||
this.canvasManager.deleteObjectById(id)
|
this.canvasManager.deleteObjectById(id)
|
||||||
|
if (id === this.activeID.value && isActive) {
|
||||||
|
this.setActiveID(this.layers.value[0]?.info?.id || "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 拖拽排序
|
// 拖拽排序
|
||||||
dragSort(id, newIndex) {
|
dragSort(id, newIndex) {
|
||||||
@@ -41,6 +52,156 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
// 更新图层列表
|
// 更新图层列表
|
||||||
updateLayers() {
|
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 { ref, computed } from "vue";
|
||||||
import { NODE_TYPE } from '../tools/index.d'
|
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import i18n from '@/lang'
|
import i18n from '@/lang'
|
||||||
const t = i18n.global.t
|
const t = i18n.global.t
|
||||||
@@ -16,24 +15,22 @@ export class StateManager {
|
|||||||
layerManager: any
|
layerManager: any
|
||||||
eventManager: any
|
eventManager: any
|
||||||
toolManager: any
|
toolManager: any
|
||||||
|
brushManager: any
|
||||||
// 设置管理器
|
// 设置管理器
|
||||||
setManager(options) {
|
setManager(options) {
|
||||||
options.eventManager && (this.eventManager = options.eventManager)
|
options.eventManager && (this.eventManager = options.eventManager)
|
||||||
options.canvasManager && (this.canvasManager = options.canvasManager)
|
options.canvasManager && (this.canvasManager = options.canvasManager)
|
||||||
options.layerManager && (this.layerManager = options.layerManager)
|
options.layerManager && (this.layerManager = options.layerManager)
|
||||||
options.toolManager && (this.toolManager = options.toolManager)
|
options.toolManager && (this.toolManager = options.toolManager)
|
||||||
|
options.brushManager && (this.brushManager = options.brushManager)
|
||||||
}
|
}
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.mxHistory = ref(50)
|
this.mxHistory = ref(50)
|
||||||
this.historyList = ref([])
|
this.historyList = ref([])
|
||||||
this.historyIndex = ref(0)
|
this.historyIndex = ref(0)
|
||||||
|
|
||||||
this.activeNodeID = ref("")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
/** 设置激活节点 */
|
|
||||||
setActiveNodeID(id: string) { this.activeNodeID.value = id }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { OperationType } from '../tools/layerHelper'
|
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 {
|
export class ToolManager {
|
||||||
stateManager: any
|
stateManager: any
|
||||||
canvasManager: any
|
canvasManager: any
|
||||||
currentTool: any
|
currentTool: any
|
||||||
|
brushManager: any
|
||||||
tools: any[]
|
tools: any[]
|
||||||
|
brushIndicator: any
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.stateManager = options.stateManager;
|
this.stateManager = options.stateManager;
|
||||||
this.canvasManager = options.canvasManager;
|
this.canvasManager = options.canvasManager;
|
||||||
this.currentTool = ref(null)
|
this.currentTool = ref(null)
|
||||||
|
|
||||||
this.tools = [
|
this.tools = [
|
||||||
/** 选择工具 */
|
/** 选择工具 */
|
||||||
{
|
{
|
||||||
@@ -27,11 +34,15 @@ export class ToolManager {
|
|||||||
{
|
{
|
||||||
name: OperationType.DRAW,
|
name: OperationType.DRAW,
|
||||||
cursor: "crosshair",
|
cursor: "crosshair",
|
||||||
|
setup: this.setupBrushTool.bind(this),
|
||||||
|
isDrawingMode: true,
|
||||||
},
|
},
|
||||||
/** 橡皮擦工具 */
|
/** 橡皮擦工具 */
|
||||||
{
|
{
|
||||||
name: OperationType.ERASER,
|
name: OperationType.ERASER,
|
||||||
cursor: "crosshair",
|
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) {
|
setTool(value: string) {
|
||||||
const tool = this.tools.find((t) => t.name === value)
|
const tool = this.tools.find((t) => t.name === value)
|
||||||
if (!tool) return console.warn(`工具${tool}不存在`)
|
if (!tool) return console.warn(`工具${tool}不存在`)
|
||||||
this.currentTool.value = tool.name
|
this.currentTool.value = tool.name
|
||||||
this.canvasManager.canvas.defaultCursor = tool.cursor
|
this.canvasManager.canvas.defaultCursor = tool.cursor
|
||||||
this.setCanvasEvented(!!tool.selection)
|
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()
|
if (tool.setup) tool.setup()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.canvasManager.renderAll()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 切换工具时,设置画布事件
|
// 切换工具时,设置画布事件
|
||||||
setCanvasEvented(value: boolean) {
|
setCanvasEvented(value: boolean) {
|
||||||
@@ -66,4 +99,76 @@ export class ToolManager {
|
|||||||
/** 移动工具 */
|
/** 移动工具 */
|
||||||
setupMoveTool() {
|
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.setupObjectEvents();
|
||||||
// this.setupDoubleClickEvents();
|
// this.setupDoubleClickEvents();
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ export class CanvasEventManager {
|
|||||||
if (this.lastMousePositions.length > this.positionHistoryLimit) {
|
if (this.lastMousePositions.length > this.positionHistoryLimit) {
|
||||||
this.lastMousePositions.shift();
|
this.lastMousePositions.shift();
|
||||||
}
|
}
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
this.canvas.lastPosX = opt.e.clientX;
|
this.canvas.lastPosX = opt.e.clientX;
|
||||||
this.canvas.lastPosY = opt.e.clientY;
|
this.canvas.lastPosY = opt.e.clientY;
|
||||||
@@ -429,7 +429,7 @@ export class CanvasEventManager {
|
|||||||
|
|
||||||
this.touchState.lastTouchTime = now;
|
this.touchState.lastTouchTime = now;
|
||||||
}
|
}
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
|
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
|
||||||
this.canvas.lastPosX = currentX;
|
this.canvas.lastPosX = currentX;
|
||||||
this.canvas.lastPosY = currentY;
|
this.canvas.lastPosY = currentY;
|
||||||
@@ -616,7 +616,8 @@ export class CanvasEventManager {
|
|||||||
if (this.toolManager) {
|
if (this.toolManager) {
|
||||||
// this.toolManager.restoreSelectionState(); // 恢复选择状态
|
// this.toolManager.restoreSelectionState(); // 恢复选择状态
|
||||||
}
|
}
|
||||||
|
const vpt = this.canvas.viewportTransform;
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +879,7 @@ export class CanvasEventManager {
|
|||||||
updateSelectedLayer(opt) {
|
updateSelectedLayer(opt) {
|
||||||
const selected = opt.selected[0];
|
const selected = opt.selected[0];
|
||||||
if (selected) {
|
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;
|
const vpt = this.canvas.viewportTransform;
|
||||||
vpt[4] += e.pageX - this.canvas.lastPosX;
|
vpt[4] += e.pageX - this.canvas.lastPosX;
|
||||||
vpt[5] += e.pageY - this.canvas.lastPosY;
|
vpt[5] += e.pageY - this.canvas.lastPosY;
|
||||||
|
this.canvas.setViewportTransform(vpt);
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
this.canvas.lastPosX = e.pageX;
|
this.canvas.lastPosX = e.pageX;
|
||||||
this.canvas.lastPosY = e.pageY;
|
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,
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
const showMenu = ref(false)
|
const showMenu = ref(false)
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
url: props.data?.url || '',
|
url: props.data?.url || '',
|
||||||
scale: props.data?.scale || { x: 1, y: 1 }
|
scale: props.data?.scale || { x: 1, y: 1 },
|
||||||
})
|
})
|
||||||
watch(
|
watch(
|
||||||
() => props.data.url,
|
() => props.data.url,
|
||||||
|
|||||||
Reference in New Issue
Block a user