Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
X1627315083@163.com
2026-03-11 16:57:52 +08:00
47 changed files with 12691 additions and 232 deletions

View File

@@ -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>

View File

@@ -15,6 +15,9 @@
<span class="icon"><svg-icon name="export" size="12" /></span>
<span class="text">Export</span>
</button>
<button class="export" @click="emit('import')">
<span class="text">import</span>
</button>
<button class="workbench">
<span class="icon"><svg-icon name="dc-workbench" size="20" /></span>
<span class="text">Workbench</span>
@@ -30,6 +33,7 @@
step: { default: 0.1, type: Number }
})
const emit = defineEmits(['export', 'import'])
const importLocalImage = inject('importLocalImage') as () => void
const stateManager = inject('stateManager') as any
const toolManager = inject('toolManager') as any
const tool = computed(() => toolManager.currentTool.value)
@@ -42,7 +46,13 @@
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
{ name: OperationType.DRAW, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
{ name: OperationType.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
{ name: OperationType.IMAGE, icon: 'dc-image', iconSize: 17, disabled: ref(false) },
{
name: OperationType.IMAGE,
icon: 'dc-image',
iconSize: 17,
disabled: ref(false),
on: () => importLocalImage()
},
{ name: OperationType.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
{ type: 'line' },

View File

@@ -3,7 +3,7 @@
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
<div class="thumb"></div>
<div class="name">
<div @dblclick="onClickEditName" v-if="!editName">
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
{{ layer.info.name || '未命名图层' }}
</div>
<input
@@ -16,10 +16,10 @@
/>
</div>
<div class="icons">
<span @click="onClickShowHide"
<span @click.stop="onClickShowHide"
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
/></span>
<span @click="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
<span><svg-icon name="dc-down_arrow" size="11" /></span>
</div>
</div>
@@ -27,7 +27,10 @@
<script setup lang="ts">
import { ref, inject, nextTick } from 'vue'
import { OperationType } from '../../tools/layerHelper'
const layerManager = inject('layerManager') as any
const toolManager = inject('toolManager') as any
const editName = ref(false)
const props = defineProps({
layer: {
@@ -58,6 +61,7 @@
}
const onClickLayer = () => {
layerManager.setActiveID(props.layer.info.id)
toolManager.setTool(OperationType.SELECT)
}
</script>
@@ -109,13 +113,11 @@
> .name {
flex: 1;
font-size: 1.4rem;
overflow: hidden;
> div {
cursor: pointer;
width: 100%;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -6,7 +6,7 @@
<span class="title">Layer</span>
</div>
<div class="right">
<span class="icon"><svg-icon name="add" size="14" /></span>
<span class="icon" @click="addLayer"><svg-icon name="add" size="14" /></span>
</div>
</div>
<div class="content">
@@ -60,9 +60,11 @@
const handleDragEnd = (event) => {
draging.value = false
const { from, to, oldIndex, newIndex, data } = event
console.log('oldIndex', data)
layerManager.dragSort(data.info.id, newIndex)
}
const addLayer = () => {
layerManager.createEmptyLayer()
}
</script>
<style lang="less" scoped>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -3,35 +3,40 @@
<div class="canvas-container" ref="canvasContainerRef">
<canvas ref="canvasRef"></canvas>
</div>
<layer-panel />
<details-panel />
<header-tools @export="exportCanvas" />
<zoom
:zoom="canvasManager.currentZoom.value / 100"
:step="0.1"
is-home
@home="() => canvasManager.resetZoom()"
/>
<template v-if="isReady">
<layer-panel />
<details-panel />
<depth-header-tools @export="exportCanvas" @import="importCanvas" />
<brush-control-panel :currentTool="toolManager.currentTool.value" />
<zoom
:zoom="canvasManager.currentZoom.value / 100"
:step="0.1"
is-home
@home="() => canvasManager.resetZoom()"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { fabric } from 'fabric-with-all'
import { computed, ref, markRaw, onMounted, nextTick, provide, onBeforeMount } from 'vue'
import { NODE_TYPE, NODE_COMPONENT } from './tools/index.d'
import { OperationType } from './tools/layerHelper'
// 组件
import layerPanel from './components/layer-panel/index.vue'
import detailsPanel from './components/details-panel/index.vue'
import headerTools from './components/header-tools.vue'
import depthHeaderTools from './components/depth-header-tools.vue'
import zoom from '../components/zoom.vue'
import brushControlPanel from './components/brush-control-panel.vue'
// 管理器
import { StateManager } from './manager/StateManager'
import { LayerManager } from './manager/LayerManager'
import { EventManager } from './manager/EventManager'
import { CanvasManager } from './manager/CanvasManager'
import { ToolManager } from './manager/ToolManager'
// 准备就绪
const isReady = ref(false)
const canvasContainerRef = ref(null)
const canvasRef = ref(null)
@@ -56,11 +61,6 @@
stateManager.setManager({ layerManager })
provide('layerManager', layerManager)
// 事件管理器
const eventManager = new EventManager({ stateManager })
stateManager.setManager({ eventManager })
provide('eventManager', eventManager)
// 工具管理器
const toolManager = new ToolManager({ stateManager, canvasManager })
stateManager.setManager({ toolManager })
@@ -75,6 +75,10 @@
canvasWidth: 750,
canvasHeight: 600
})
stateManager?.onMounted?.()
canvasManager?.onMounted?.()
layerManager?.onMounted?.()
toolManager?.onMounted?.()
const trailingTimeout = ref(null)
observer.value = new ResizeObserver((entries) => {
@@ -84,10 +88,10 @@
}, 100)
})
observer.value.observe(canvasContainerRef.value)
isReady.value = true // 准备就绪
})
onBeforeMount(() => {
// eventManager.removeEvents() // 移除事件
})
onBeforeMount(() => {})
async function handleWindowResize() {
console.log('==========画布窗口大小变化==========')
canvasManager.setCanvasViewSize({
@@ -96,8 +100,42 @@
})
canvasManager.resetZoom()
}
/** 导入本地图片 */
const importLocalImage = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.click()
input.addEventListener('change', (e: any) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
toolManager.setTool(OperationType.SELECT)
const url = reader.result as string
layerManager.createImageLayer(url, {
info: { name: file.name }
})
}
})
}
provide('importLocalImage', importLocalImage)
const exportCanvas = () => {
console.log(canvasManager.getBitObjects())
const json = canvasManager.getCanvasJSON()
localStorage.setItem('canvasJSON', json)
}
const importCanvas = () => {
const json = localStorage.getItem('canvasJSON')
if (!json) return
canvasManager.loadJSON(json, (success) => {
if (success) {
console.log('导入成功')
} else {
console.log('导入失败')
}
})
}
</script>
<style lang="less">

View File

@@ -115,7 +115,7 @@ export class AnimationManager {
const adjustY = (1 - currentScaleFactor) * point.y;
vpt[4] = currentVpt[4] * scale + adjustX;
vpt[5] = currentVpt[5] * scale + adjustY;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
},
onComplete: () => {
@@ -167,7 +167,7 @@ export class AnimationManager {
const adjustY = (1 - currentScaleFactor) * point.y;
vpt[4] = currentVpt[4] * scale + adjustX;
vpt[5] = currentVpt[5] * scale + adjustY;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
},
onComplete: () => {
@@ -252,7 +252,7 @@ export class AnimationManager {
const vpt = this.canvas.viewportTransform;
vpt[4] = -x;
vpt[5] = -y;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
}
@@ -337,7 +337,7 @@ export class AnimationManager {
vpt[3] = viewTransform.zoom;
vpt[4] = viewTransform.panX;
vpt[5] = viewTransform.panY;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
},
onComplete: () => {
@@ -814,6 +814,7 @@ export class AnimationManager {
// 更新缩放值显示
this.currentZoom.value = Math.round(transform.zoom * 100);
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
},
onComplete: () => {

View 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;
}
}

View File

@@ -58,55 +58,20 @@ export class CanvasManager {
const canvasX = this.canvasViewWidth / 2 - this.canvasWidth / 2
const canvasY = this.canvasViewHeight / 2 - this.canvasHeight / 2
this.canvas.viewportTransform = [1, 0, 0, 1, canvasX, canvasY]
// 创建矩形
const rect = new fabric.Rect({
left: 20,
top: 20,
width: 100,
height: 100,
fill: '#f00',
info: {
id: 'rect1',
name: 'rect1',
}
const rect = this.layerManager.createRectLayer({
left: 400,
top: 100,
})
this.canvas.add(rect)
//创建圆形
const circle = new fabric.Circle({
const circle = this.layerManager.createCircleLayer({
left: 200,
top: 200,
radius: 50,
fill: '#0f0',
info: {
id: 'circle',
name: 'circle',
}
})
this.canvas.add(circle)
// 文字
const text = new fabric.Text('Hello World', {
left: 300,
top: 300,
fontSize: 24,
fill: '#000',
info: {
id: 'text1',
name: 'text',
}
})
this.canvas.add(text)
// 文字
const text2 = new fabric.Text('Hello World', {
left: 300,
top: 300,
fontSize: 24,
fill: '#000',
info: {
id: 'text2',
name: 'tex2t',
}
})
this.canvas.add(text2)
const text = this.layerManager.createTextLayer('Hello World');
this.animationManager = new AnimationManager(this.canvas, {
currentZoom: this.currentZoom,
canvasManager: this,
@@ -117,18 +82,38 @@ export class CanvasManager {
this.setupCanvasEvents()
this.stateManager.toolManager.setTool(OperationType.SELECT)
this.layerManager.updateLayers()
this.layerManager.setActiveID(text2.info.id)
this.layerManager.setActiveID(text.info.id)
this.setupBrushEvents()
}
/** 画布添加对象 */
add(obj: any, isUpdate = true) {
this.canvas.add(obj)
if (isUpdate) {
this.layerManager.updateLayers()
this.renderAll()
}
}
/** 设置画布事件 */
setupCanvasEvents() {
// 创建画布事件管理器
this.eventManager = new CanvasEventManager(this.canvas, {
canvasManager: this,
animationManager: this.animationManager,
toolManager: this.stateManager.toolManager,
layerManager: this.stateManager.layerManager,
});
// 设置动画交互效果
this.animationManager.setupInteractionAnimations();
}
/** 设置激活对象 */
setActiveObjectByID(id: string) {
const obj = this.getObjectById(id)
if (obj) this.canvas.setActiveObject(obj)
this.renderAll()
}
resetZoom() {
this.animationManager.resetZoom()
}
@@ -163,5 +148,51 @@ export class CanvasManager {
})
}
/** 画笔事件 */
setupBrushEvents() {
this.canvas.onBrushImageConverted = async (fabricImage) => {
const currentTool = this.stateManager.toolManager.currentTool.value;
if (currentTool === OperationType.DRAW) {
this.handleDrawImage(fabricImage)
}
return true
};
}
/** 处理绘制图像 */
handleDrawImage(fabricImage: fabric.Object) {
const activeID = this.stateManager.layerManager.activeID.value
const activeLayer = this.getObjectById(activeID)
if (activeLayer) {
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
} else {
const emptyLayer = this.layerManager.createEmptyLayer();
this.layerManager.setActiveID(emptyLayer.info.id, false)
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
}
return true
}
/** 导出画布为JSON */
getCanvasJSON() {
const keys = ["top", "left", "width", "height", "scaleX", "scaleY", "info",]
const json = this.canvas.toJSON(keys)
console.log(json, this.getObjects())
return JSON.stringify(json)
}
/** 加载画布JSON */
loadJSON(json: string, callback?: (success: boolean) => void) {
let jsonObj = null;
try {
jsonObj = JSON.parse(json)
} catch (error) {
console.error('JSON解析错误:', error)
}
if (!jsonObj) return callback?.(false);
this.canvas.loadFromJSON(jsonObj, () => {
this.layerManager.updateLayers()
this.renderAll()
callback?.(true)
})
}
}

View File

@@ -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;
}
}

View File

@@ -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))
}
}

View File

@@ -1,4 +1,7 @@
import { ref } from 'vue'
import { fabric } from 'fabric-with-all'
import { createId } from '../../tools/tools'
import { exportObjectsToImage } from '../tools/exportMethod'
export class LayerManager {
stateManager: any
@@ -11,7 +14,12 @@ export class LayerManager {
this.layers = ref([])
this.activeID = ref("")
}
setActiveID(id: string) { this.activeID.value = id }
setActiveID(id: string, isActive = true) {
this.activeID.value = id
if (isActive) {
this.canvasManager.setActiveObjectByID(id)
}
}
getLayerByID(id) {
return this.layers.value.find((item: any) => item.info.id === id)
}
@@ -31,8 +39,11 @@ export class LayerManager {
this.canvasManager.renderAll()
}
}
deleteLayerByID(id) {
deleteLayerByID(id, isActive = true) {
this.canvasManager.deleteObjectById(id)
if (id === this.activeID.value && isActive) {
this.setActiveID(this.layers.value[0]?.info?.id || "")
}
}
// 拖拽排序
dragSort(id, newIndex) {
@@ -41,6 +52,156 @@ export class LayerManager {
}
// 更新图层列表
updateLayers() {
this.layers.value = this.canvasManager.getObjects().reverse()
this.layers.value = this.canvasManager.getObjects().filter((v: any) => !!v?.info?.id).reverse()
}
}
/** 设置图层位置-不设置默认居中 */
setLayerPosition(layer, options?: any) {
const width = this.canvasManager.canvasWidth
const height = this.canvasManager.canvasHeight
if (options && options.top !== undefined) {
layer.set({ top: options.top })
} else {
layer.set({ top: height / 2 - layer.height * layer.scaleY / 2 })
}
if (options && options.left !== undefined) {
layer.set({ left: options.left })
} else {
layer.set({ left: width / 2 - layer.width * layer.scaleX / 2 })
}
}
/** 创建空图层 */
createEmptyLayer() {
const emptyLayer = new fabric.Rect({
width: 0,
height: 0,
fill: 'transparent',
info: {
id: createId("image"),
name: '空图层',
}
})
this.setLayerPosition(emptyLayer)
this.canvasManager.add(emptyLayer)
return emptyLayer
}
/** 创建文本图层 */
createTextLayer(text: string, options?: any) {
const textLayer = new fabric.IText(text, {
fontSize: 24,
fill: '#000',
...(options || {}),
info: {
id: createId("text"),
name: '文本图层',
...(options?.info || {}),
}
})
this.setLayerPosition(textLayer, options)
this.canvasManager.add(textLayer)
return textLayer
}
/** 创建矩形图层 */
createRectLayer(options?: any) {
const rectLayer = new fabric.Rect({
width: 100,
height: 100,
fill: '#000',
...(options || {}),
info: {
id: createId("rect"),
name: '矩形图层',
...(options?.info || {}),
}
})
this.setLayerPosition(rectLayer, options)
this.canvasManager.add(rectLayer)
return rectLayer
}
/** 创建圆形图层 */
createCircleLayer(options?: any) {
const circleLayer = new fabric.Circle({
radius: 50,
fill: '#000',
...(options || {}),
info: {
id: createId("circle"),
name: '圆形图层',
...(options?.info || {}),
}
})
this.setLayerPosition(circleLayer, options)
this.canvasManager.add(circleLayer)
return circleLayer
}
/** 创建图片图层 */
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any) {
const canvasWidth = this.canvasManager.canvasWidth
const canvasHeight = this.canvasManager.canvasHeight
const imageLayer = await new Promise((resolve) => {
const url = typeof imgOrUrl === 'string' ? imgOrUrl : imgOrUrl.src
fabric.Image.fromURL(url, (img) => {
const width = img.width
const height = img.height
const scaleX = width > canvasWidth ? canvasWidth * 0.8 / width : 1
const scaleY = height > canvasHeight ? canvasHeight * 0.8 / height : 1
const scale = Math.min(scaleX, scaleY)
img.set({
scaleX: scale,
scaleY: scale,
...(options || {}),
info: {
id: createId("image"),
name: "图片图层",
...(options?.info || {}),
}
})
resolve(img)
})
}) as fabric.Object
this.setLayerPosition(imageLayer, options)
this.canvasManager.add(imageLayer)
this.setActiveID(imageLayer.info.id)
return imageLayer
}
/** 合并图层 */
async imageMergeToLayer(targetLayer: fabric.Object, fabricImage: fabric.Object) {
const info = await exportObjectsToImage([targetLayer, fabricImage], true)
const mergedImage = await new Promise((resolve) => {
fabric.Image.fromURL(info.url, (img) => {
img.set({
left: info.left,
top: info.top,
info: {
id: createId("image"),
name: targetLayer?.info?.name || "合并图层",
}
})
resolve(img)
})
})
// console.log(mergedImage)
const index = this.canvasManager.getObjects().indexOf(targetLayer);
this.deleteLayerByID(targetLayer.info.id, false)
this.setActiveID(mergedImage.info.id, false)
this.canvasManager.add(mergedImage, false);
this.canvasManager.canvas.moveTo(mergedImage, index);
this.canvasManager.renderAll()
this.updateLayers()
return true;
}
/** 设置激活对象可擦除 */
setActiveObjectErasable() {
const objects = this.canvasManager.getObjects()
objects.forEach((item: any) => {
item.set({
erasable: item.info.id === this.activeID.value
})
})
}
}

View File

@@ -1,5 +1,4 @@
import { ref, computed } from "vue";
import { NODE_TYPE } from '../tools/index.d'
import { ElMessageBox } from 'element-plus'
import i18n from '@/lang'
const t = i18n.global.t
@@ -16,24 +15,22 @@ export class StateManager {
layerManager: any
eventManager: any
toolManager: any
brushManager: any
// 设置管理器
setManager(options) {
options.eventManager && (this.eventManager = options.eventManager)
options.canvasManager && (this.canvasManager = options.canvasManager)
options.layerManager && (this.layerManager = options.layerManager)
options.toolManager && (this.toolManager = options.toolManager)
options.brushManager && (this.brushManager = options.brushManager)
}
constructor(options) {
this.mxHistory = ref(50)
this.historyList = ref([])
this.historyIndex = ref(0)
this.activeNodeID = ref("")
}
/** 设置激活节点 */
setActiveNodeID(id: string) { this.activeNodeID.value = id }

View File

@@ -1,14 +1,21 @@
import { ref } from 'vue'
import { OperationType } from '../tools/layerHelper'
import { BrushManager } from "./brushes/brushManager";
import { BrushIndicator } from "./BrushIndicator";
import i18n from "@/lang";
const t = i18n.global.t
export class ToolManager {
stateManager: any
canvasManager: any
currentTool: any
brushManager: any
tools: any[]
brushIndicator: any
constructor(options) {
this.stateManager = options.stateManager;
this.canvasManager = options.canvasManager;
this.currentTool = ref(null)
this.tools = [
/** 选择工具 */
{
@@ -27,11 +34,15 @@ export class ToolManager {
{
name: OperationType.DRAW,
cursor: "crosshair",
setup: this.setupBrushTool.bind(this),
isDrawingMode: true,
},
/** 橡皮擦工具 */
{
name: OperationType.ERASER,
cursor: "crosshair",
setup: this.setupEraserTool.bind(this),
isDrawingMode: true,
},
/** 智能选框工具 */
{
@@ -45,15 +56,37 @@ export class ToolManager {
},
]
}
onMounted() {
this.brushIndicator = new BrushIndicator(this.canvasManager.canvas, {
strokeColor: "rgba(0, 0, 0, 0.6)",
strokeWidth: 1,
fillColor: "rgba(0, 0, 0, 0.1)",
});
this.brushManager = new BrushManager({
canvas: this.canvasManager.canvas,
layerManager: this.canvasManager.layerManager, // 传入图层管理器引用
brushIndicator: this.brushIndicator,
t,
});
this.stateManager.setManager({
brushManager: this.brushManager,
})
}
setTool(value: string) {
const tool = this.tools.find((t) => t.name === value)
if (!tool) return console.warn(`工具${tool}不存在`)
this.currentTool.value = tool.name
this.canvasManager.canvas.defaultCursor = tool.cursor
this.setCanvasEvented(!!tool.selection)
this.canvasManager.canvas.isDragging = !!tool.isDragging
this.canvasManager.canvas.isDrawingMode = !!tool.isDrawingMode;// 绘制模式
if (!tool.isDrawingMode) this._disableBrushIndicator()// 非绘制模式,禁用笔刷指示器
if (tool.setup) tool.setup()
setTimeout(() => {
this.canvasManager.renderAll()
});
}
// 切换工具时,设置画布事件
setCanvasEvented(value: boolean) {
@@ -66,4 +99,76 @@ export class ToolManager {
/** 移动工具 */
setupMoveTool() {
}
/** 画笔工具 */
setupBrushTool() {
if (!this.canvasManager.canvas) return;
// 确保有笔刷管理器
if (this.brushManager) {
// 检查画笔是否正在更新中
if (this.brushManager.isUpdatingBrush) {
console.warn("画笔正在更新中,请稍候...");
return;
}
const brushStore = this.brushManager?.brushStore
if (brushStore) {
// 同步基本属性
this.brushManager.setBrushSize(brushStore.state.size);
this.brushManager.setBrushColor(brushStore.state.color);
this.brushManager.setBrushOpacity(brushStore.state.opacity);
// 同步笔刷类型 - 修复方法名使用正确的setBrushType方法
this.brushManager.setBrushType("pencil");
}
// 更新应用到画布
this.brushManager.updateBrush();
}
// 启用笔刷指示器并同步颜色
this._enableBrushIndicator();
}
/**
* 设置橡皮擦工具
*/
setupEraserTool() {
if (!this.canvasManager.canvas) return;
// 确保有笔刷管理器
if (this.brushManager) {
this.brushManager.createEraser();
}
this.stateManager.layerManager.setActiveObjectErasable()
// 启用笔刷指示器
this._enableBrushIndicator();
}
/**
* 启用笔刷指示器
* @param {String} color 笔刷颜色(可选)
* @private
*/
_enableBrushIndicator(color?: string) {
if (!this.brushIndicator) return;
// 获取当前笔刷大小
const brushSize = this.brushManager?.getBrushSize() || 5;
// 获取当前笔刷颜色
const brushColor = color || this.brushManager?.getBrushColor() || "#000000";
// 启用指示器
this.brushIndicator.enable(brushSize);
this.brushIndicator.updateSize(brushSize);
// 更新指示器颜色
this.brushIndicator.updateColor(brushColor);
}
/** 禁用笔刷指示器 */
_disableBrushIndicator() {
if (!this.brushIndicator) return;
this.brushIndicator.disable();
}
}

View 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;
}
}

View File

@@ -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;

View 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;
}
}

View File

@@ -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. **性能测试**: 在大量对象的场景下测试性能表现
通过这些优化,你的项目可以获得更好的性能和更简洁的代码结构。

View File

@@ -0,0 +1,280 @@
<!-- https://github.com/tennisonchan/fabric-brush?tab=readme-ov-file -->
<!-- eraser_brushhttps://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('艺术笔刷');
```

View File

@@ -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;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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=";
}
}

View File

@@ -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+";
}
}

View File

@@ -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=";
}
}

View File

@@ -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+";
}
}

View File

@@ -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+";
}
}

View File

@@ -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
}
}

View File

@@ -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==";
}
}

View File

@@ -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+";
}
}

View File

@@ -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+";
}
}

View File

@@ -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=";
}
}

View File

@@ -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=";
}
}

View File

@@ -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);
}
}

View File

@@ -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+";
}
}

View File

@@ -43,7 +43,7 @@ export class CanvasEventManager {
}
// 共享事件
// this.setupSelectionEvents();
this.setupSelectionEvents();
// this.setupObjectEvents();
// this.setupDoubleClickEvents();
@@ -228,7 +228,7 @@ export class CanvasEventManager {
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
@@ -429,7 +429,7 @@ export class CanvasEventManager {
this.touchState.lastTouchTime = now;
}
this.canvas.setViewportTransform(vpt);
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
@@ -616,7 +616,8 @@ export class CanvasEventManager {
if (this.toolManager) {
// this.toolManager.restoreSelectionState(); // 恢复选择状态
}
const vpt = this.canvas.viewportTransform;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
}
@@ -878,7 +879,7 @@ export class CanvasEventManager {
updateSelectedLayer(opt) {
const selected = opt.selected[0];
if (selected) {
this.layerManager.activeLayerId.value = selected.layerId;
this.layerManager.setActiveID(selected?.info?.id, false)
}
}
@@ -1068,7 +1069,7 @@ export class CanvasEventManager {
const vpt = this.canvas.viewportTransform;
vpt[4] += e.pageX - this.canvas.lastPosX;
vpt[5] += e.pageY - this.canvas.lastPosY;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;

View 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,
}
}

View 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
}

View File

@@ -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,
}