深度画布智能选区
This commit is contained in:
@@ -1,33 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layer-item" @click="onClickLayer">
|
<div class="layer-item">
|
||||||
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
<div class="item" @click="onClickLayer">
|
||||||
<div class="thumb">
|
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
|
||||||
<img v-if="layer.thumbnail" :src="layer.thumbnail" />
|
<div class="thumb">
|
||||||
</div>
|
<img v-if="layer.thumbnail" :src="layer.thumbnail" />
|
||||||
<div class="name">
|
</div>
|
||||||
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
|
<div class="name">
|
||||||
{{ layer.info.name || '未命名图层' }}
|
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
|
||||||
|
{{ layer.info.name || '未命名图层' }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
ref="nameInputRef"
|
||||||
|
:value="layer.info.name"
|
||||||
|
@blur="onChangeName"
|
||||||
|
@keyup.enter="onChangeName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="icons">
|
||||||
|
<span @click.stop="onClickLock">
|
||||||
|
<svg-icon v-show="!layer.info.lock" name="dc-lock_0" size="15" />
|
||||||
|
<svg-icon v-show="layer.info.lock" name="dc-lock_1" size="15" color="#1890ff" />
|
||||||
|
</span>
|
||||||
|
<span @click.stop="onClickShowHide"
|
||||||
|
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
||||||
|
/></span>
|
||||||
|
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
|
||||||
|
<span
|
||||||
|
v-if="isGroup"
|
||||||
|
@click.stop="onShowGroup"
|
||||||
|
class="show-group"
|
||||||
|
:class="{ active: layer.info.showChildren }"
|
||||||
|
>
|
||||||
|
<svg-icon name="dc-down_arrow" size="11" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
v-else
|
|
||||||
type="text"
|
|
||||||
ref="nameInputRef"
|
|
||||||
:value="layer.info.name"
|
|
||||||
@blur="onChangeName"
|
|
||||||
@keyup.enter="onChangeName"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="icons">
|
|
||||||
<span @click.stop="onClickLock">
|
|
||||||
<svg-icon v-show="!layer.info.lock" name="dc-lock_0" size="15" />
|
|
||||||
<svg-icon v-show="layer.info.lock" name="dc-lock_1" size="15" color="#1890ff" />
|
|
||||||
</span>
|
|
||||||
<span @click.stop="onClickShowHide"
|
|
||||||
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
|
|
||||||
/></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>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -42,6 +52,10 @@
|
|||||||
layer: {
|
layer: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
isGroup: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const nameInputRef = ref(null)
|
const nameInputRef = ref(null)
|
||||||
@@ -72,82 +86,98 @@
|
|||||||
const info = props.layer.info
|
const info = props.layer.info
|
||||||
layerManager.setLayerLockById(info.id, !info.lock)
|
layerManager.setLayerLockById(info.id, !info.lock)
|
||||||
}
|
}
|
||||||
|
const onShowGroup = () => {
|
||||||
|
props.layer.info.showChildren = !props.layer.info.showChildren
|
||||||
|
// layerManager.setLayerGroupVisibleById(props.layer.info.id, !props.layer.visible)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.layer-item {
|
.layer-item {
|
||||||
width: 100%;
|
&:last-child > .item {
|
||||||
height: 9.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 1.4rem;
|
|
||||||
background-color: #fafafa;
|
|
||||||
gap: 1rem;
|
|
||||||
border-bottom: 0.2rem solid #ededed;
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
&:not([draging='true']):hover {
|
&:not([draging='true']) > .item:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
&.active {
|
&.active > .item {
|
||||||
background-color: #ededed !important;
|
background-color: #ededed !important;
|
||||||
}
|
}
|
||||||
&.drag {
|
&.drag {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
&.ghost,
|
&.ghost > .item,
|
||||||
&.chosen {
|
&.chosen > .item {
|
||||||
box-shadow: inset 0 0 5px #aaa;
|
box-shadow: inset 0 0 5px #aaa;
|
||||||
background-color: #ededed !important;
|
background-color: #ededed !important;
|
||||||
}
|
}
|
||||||
> .drag {
|
> .item {
|
||||||
padding: 0.3rem;
|
width: 100%;
|
||||||
cursor: move;
|
height: 9.5rem;
|
||||||
}
|
|
||||||
> .thumb {
|
|
||||||
width: 5.6rem;
|
|
||||||
height: 5.6rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 0.2rem solid #ebebeb;
|
|
||||||
background-color: #fff;
|
|
||||||
> img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .name {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
overflow: hidden;
|
|
||||||
> div {
|
|
||||||
cursor: pointer;
|
|
||||||
height: 3rem;
|
|
||||||
line-height: 3rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
> input {
|
|
||||||
width: 100%;
|
|
||||||
height: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .icons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.8rem;
|
padding: 0 1.4rem;
|
||||||
> span {
|
background-color: #fafafa;
|
||||||
cursor: pointer;
|
gap: 1rem;
|
||||||
width: 1.6rem;
|
border-bottom: 0.2rem solid #ededed;
|
||||||
height: 1.6rem;
|
|
||||||
|
> .drag {
|
||||||
|
padding: 0.3rem;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
> .thumb {
|
||||||
|
width: 5.6rem;
|
||||||
|
height: 5.6rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0.2rem solid #ebebeb;
|
||||||
|
background-color: #fff;
|
||||||
|
> img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
overflow: hidden;
|
||||||
|
> div {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 3rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
> input {
|
||||||
|
width: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
> span {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
> .show-group {
|
||||||
|
> .c-svg {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
&.active > .c-svg {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,28 +12,16 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<VueDraggable
|
<VueDraggable
|
||||||
:model-value="list"
|
:model-value="list"
|
||||||
@start="handleDragStart"
|
@start="(e) => handleDragStart(e)"
|
||||||
@end="handleDragEnd"
|
@end="(e) => handleDragEnd(e)"
|
||||||
|
@add="(e) => handleAdd(e)"
|
||||||
class="sortable-layers"
|
class="sortable-layers"
|
||||||
:data-container-type="'root'"
|
v-bind="config"
|
||||||
:data-parent-id="null"
|
|
||||||
:animation="250"
|
|
||||||
:disabled="false"
|
|
||||||
handle=".drag"
|
|
||||||
ghost-class="ghost"
|
|
||||||
chosen-class="chosen"
|
|
||||||
drag-class="drag"
|
|
||||||
:group="{
|
:group="{
|
||||||
name: 'groupName',
|
name: 'group',
|
||||||
pull: true,
|
pull: true,
|
||||||
put: true
|
put: true
|
||||||
}"
|
}"
|
||||||
:swap-threshold="0.5"
|
|
||||||
:empty-insert-threshold="5"
|
|
||||||
:force-fallback="false"
|
|
||||||
:fallback-tolerance="3"
|
|
||||||
:scroll-sensitivity="100"
|
|
||||||
:scroll-speed="10"
|
|
||||||
>
|
>
|
||||||
<layer-item
|
<layer-item
|
||||||
v-for="layer in list"
|
v-for="layer in list"
|
||||||
@@ -41,7 +29,34 @@
|
|||||||
:layer="layer"
|
:layer="layer"
|
||||||
:draging="draging"
|
:draging="draging"
|
||||||
:class="{ active: layer.info.id === layerManager.activeID.value }"
|
:class="{ active: layer.info.id === layerManager.activeID.value }"
|
||||||
/>
|
:is-group="layer.type === 'group'"
|
||||||
|
>
|
||||||
|
<VueDraggable
|
||||||
|
v-if="layer.type === 'group'"
|
||||||
|
v-show="layer.info.showChildren"
|
||||||
|
:model-value="layer.children"
|
||||||
|
@start="(e) => handleDragStart(e, layer)"
|
||||||
|
@end="(e) => handleDragEnd(e, layer)"
|
||||||
|
@add="(e) => handleAdd(e, layer)"
|
||||||
|
class="sortable-layers-child"
|
||||||
|
v-bind="config"
|
||||||
|
:group="{
|
||||||
|
name: 'child_' + layer.info.id,
|
||||||
|
pull: true,
|
||||||
|
put: true
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<layer-item
|
||||||
|
v-for="child in layer.children"
|
||||||
|
:key="child.info.id"
|
||||||
|
:layer="child"
|
||||||
|
:draging="draging"
|
||||||
|
:class="{
|
||||||
|
active: child.info.id === layerManager.activeID.value
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</VueDraggable>
|
||||||
|
</layer-item>
|
||||||
</VueDraggable>
|
</VueDraggable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,14 +68,65 @@
|
|||||||
import layerItem from './layer-item.vue'
|
import layerItem from './layer-item.vue'
|
||||||
const draging = ref(false)
|
const draging = ref(false)
|
||||||
const layerManager = inject('layerManager') as any
|
const layerManager = inject('layerManager') as any
|
||||||
|
const canvasManager = inject('canvasManager') as any
|
||||||
const list = computed(() => layerManager.layers.value)
|
const list = computed(() => layerManager.layers.value)
|
||||||
const handleDragStart = () => {
|
const config = ref({
|
||||||
draging.value = true
|
'data-container-type': 'root',
|
||||||
|
'data-parent-id': 'null',
|
||||||
|
animation: 250,
|
||||||
|
disabled: false,
|
||||||
|
handle: '.drag',
|
||||||
|
'ghost-class': 'ghost',
|
||||||
|
'chosen-class': 'chosen',
|
||||||
|
'drag-class': 'drag',
|
||||||
|
'swap-threshold': 0.5,
|
||||||
|
'empty-insert-threshold': 5,
|
||||||
|
'force-fallback': false,
|
||||||
|
'fallback-tolerance': 3,
|
||||||
|
'scroll-sensitivity': 100,
|
||||||
|
'scroll-speed': 10
|
||||||
|
})
|
||||||
|
const startParent = ref(null)
|
||||||
|
const clearData = () => {
|
||||||
|
startParent.value = null
|
||||||
}
|
}
|
||||||
const handleDragEnd = (event) => {
|
const handleDragStart = (event, parent?) => {
|
||||||
|
draging.value = true
|
||||||
|
startParent.value = parent
|
||||||
|
}
|
||||||
|
const moveElementInPlace = (arr, oldIndex, newIndex) => {
|
||||||
|
if (oldIndex === newIndex) return arr
|
||||||
|
const movedItem = arr[oldIndex]
|
||||||
|
// 移除元素
|
||||||
|
arr.splice(oldIndex, 1)
|
||||||
|
// 插入到新位置
|
||||||
|
arr.splice(newIndex, 0, movedItem)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
const handleDragEnd = (event, parent?) => {
|
||||||
draging.value = false
|
draging.value = false
|
||||||
const { from, to, oldIndex, newIndex, data } = event
|
const { from, to, oldIndex, newIndex, data } = event
|
||||||
layerManager.dragSort(data.info.id, newIndex)
|
if (from !== to) return
|
||||||
|
const arr = parent ? parent.children : layerManager.layers.value
|
||||||
|
moveElementInPlace(arr, oldIndex, newIndex)
|
||||||
|
clearData()
|
||||||
|
layerManager.sortLayers(true)
|
||||||
|
}
|
||||||
|
const handleAdd = (event, parent?) => {
|
||||||
|
const { from, to, oldIndex, newIndex, data } = event
|
||||||
|
if (data.type === 'group') return
|
||||||
|
console.log('跨级拖动', startParent.value, oldIndex, parent, newIndex)
|
||||||
|
const oldArr = startParent.value ? startParent.value.children : layerManager.layers.value
|
||||||
|
const arr = parent ? parent.children : layerManager.layers.value
|
||||||
|
const layer = oldArr.splice(oldIndex, 1)[0]
|
||||||
|
if (parent) {
|
||||||
|
layer.info.parentId = parent.info.id
|
||||||
|
} else {
|
||||||
|
delete layer.info.parentId
|
||||||
|
}
|
||||||
|
arr.splice(newIndex, 0, layer)
|
||||||
|
clearData()
|
||||||
|
layerManager.sortLayers(true)
|
||||||
}
|
}
|
||||||
const addLayer = () => {
|
const addLayer = () => {
|
||||||
layerManager.createEmptyLayer(true, true)
|
layerManager.createEmptyLayer(true, true)
|
||||||
@@ -116,4 +182,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sortable-layers-child {
|
||||||
|
border-left: 2.5rem solid #e8e8e8;
|
||||||
|
border-bottom: 0.2rem solid #ededed;
|
||||||
|
// min-height: 5rem;
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { fabric } from 'fabric-with-all'
|
import { fabric } from 'fabric-with-all'
|
||||||
import { createId } from '../../tools/tools'
|
import { OperationType } from '../tools/layerHelper'
|
||||||
|
import { getObjectAlphaToCanvas, traceImageContour } from '../tools/canvasMethod'
|
||||||
|
|
||||||
/** 智能框选工具管理器 */
|
/** 智能框选工具管理器 */
|
||||||
export class AISelectboxToolManager {
|
export class AISelectboxToolManager {
|
||||||
@@ -7,6 +8,7 @@ export class AISelectboxToolManager {
|
|||||||
canvasManager: any
|
canvasManager: any
|
||||||
stateManager: any
|
stateManager: any
|
||||||
layerManager: any
|
layerManager: any
|
||||||
|
toolManager: any
|
||||||
|
|
||||||
isDragging: boolean = false
|
isDragging: boolean = false
|
||||||
startX: number = 0
|
startX: number = 0
|
||||||
@@ -16,7 +18,7 @@ export class AISelectboxToolManager {
|
|||||||
this.canvasManager = options.canvasManager
|
this.canvasManager = options.canvasManager
|
||||||
this.stateManager = options.stateManager
|
this.stateManager = options.stateManager
|
||||||
this.layerManager = options.layerManager
|
this.layerManager = options.layerManager
|
||||||
|
this.toolManager = options.toolManager
|
||||||
}
|
}
|
||||||
mouseDownEvent(e) {
|
mouseDownEvent(e) {
|
||||||
this.isDragging = true
|
this.isDragging = true
|
||||||
@@ -81,15 +83,11 @@ export class AISelectboxToolManager {
|
|||||||
stroke: "rgba(255, 77, 71, 1)",
|
stroke: "rgba(255, 77, 71, 1)",
|
||||||
strokeWidth: 1.5,
|
strokeWidth: 1.5,
|
||||||
strokeDashArray: [4, 4],
|
strokeDashArray: [4, 4],
|
||||||
fill: "rgba(255, 186, 186, 0.5)",
|
fill: "transparent",
|
||||||
strokeUniform: true, // 保持描边宽度不随缩放改变
|
strokeUniform: true, // 保持描边宽度不随缩放改变
|
||||||
// strokeLineCap: "round",// 折线端点样式
|
selectable: false,
|
||||||
// strokeLineJoin: "bevel", // 折线连接样式
|
evented: false,
|
||||||
// selectable: false,
|
absolutePositioned: true,
|
||||||
// evented: false,
|
|
||||||
excludeFromExport: true,
|
|
||||||
hoverCursor: "default",
|
|
||||||
moveCursor: "default",
|
|
||||||
};
|
};
|
||||||
async createSelectbox() {
|
async createSelectbox() {
|
||||||
const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
|
const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
|
||||||
@@ -112,200 +110,31 @@ export class AISelectboxToolManager {
|
|||||||
}).join(" L ");
|
}).join(" L ");
|
||||||
const path = new fabric.Path(`M ${str} z`);
|
const path = new fabric.Path(`M ${str} z`);
|
||||||
path.set({
|
path.set({
|
||||||
left: left + minX * scaleX,
|
left: left + minX,
|
||||||
top: top + minY * scaleY,
|
top: top + minY,
|
||||||
scaleX: scaleX,
|
scaleX: scaleX,
|
||||||
scaleY: scaleY,
|
scaleY: scaleY,
|
||||||
...this.selectionStyle,
|
...this.selectionStyle,
|
||||||
});
|
});
|
||||||
const rect1 = new fabric.Rect({
|
const group = await this.layerManager.createGroupLayer({
|
||||||
left: 0,
|
clipPath: path,
|
||||||
top: 0,
|
}, false, false)
|
||||||
width: 100,
|
const rect = await this.layerManager.createRectLayer({
|
||||||
height: 100,
|
width: path.width,
|
||||||
fill: '#f00',
|
height: path.height,
|
||||||
info: {
|
left: left + minX,
|
||||||
id: createId("rect"),
|
top: top + minY,
|
||||||
name: '矩形图层',
|
fill: "rgba(255, 186, 186, 0.5)",
|
||||||
}
|
info: { parentId: group.info.id },
|
||||||
})
|
}, false, true)
|
||||||
const rect2 = new fabric.Rect({
|
await this.canvasManager.updateSubLayerClipPath()
|
||||||
left: 200,
|
await this.layerManager.updateLayerThumbnailsById(rect.info.id, "", false)
|
||||||
top: 200,
|
await this.layerManager.updateLayerThumbnailsById(group.info.id, rect.thumbnail)
|
||||||
width: 100,
|
this.stateManager.recordState()
|
||||||
height: 100,
|
this.toolManager.setTool(OperationType.SELECT)
|
||||||
fill: '#ff0',
|
|
||||||
info: {
|
|
||||||
id: createId("rect"),
|
|
||||||
name: '矩形图层',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.layerManager.createGroupLayer({
|
|
||||||
child: [rect1, rect2],
|
|
||||||
})
|
|
||||||
// this.canvasManager.canvas.add(path)
|
|
||||||
// this.canvasManager.canvas.renderAll()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
dispose() { }
|
dispose() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对象黑白通道画布
|
|
||||||
* @param {fabric.Object} object - 要处理的 fabric 对象
|
|
||||||
* @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明
|
|
||||||
* @param {number} diff - 差值,默认 25
|
|
||||||
* @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
|
|
||||||
* @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData
|
|
||||||
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
|
|
||||||
*/
|
|
||||||
export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
|
|
||||||
const image = object.getElement();
|
|
||||||
if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
|
|
||||||
console.warn("对象不是图片");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { width, height } = image;
|
|
||||||
if (!width || !height) {
|
|
||||||
console.warn("对象没有元素");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
ctx.drawImage(image, 0, 0, width, height);
|
|
||||||
const data = ctx.getImageData(0, 0, width, height);
|
|
||||||
for (let i = 0; i < data.data.length; i += 4) {
|
|
||||||
const r = data.data[i + 0];
|
|
||||||
const g = data.data[i + 1];
|
|
||||||
const b = data.data[i + 2];
|
|
||||||
const a = data.data[i + 3];
|
|
||||||
const revR = revData?.data[i + 0] || 0;
|
|
||||||
const revG = revData?.data[i + 1] || 0;
|
|
||||||
const revB = revData?.data[i + 2] || 0;
|
|
||||||
const revA = revData?.data[i + 3] || 0;
|
|
||||||
let isHave = false;
|
|
||||||
if (r || g || b || a) {
|
|
||||||
if (revR > diff || revG > diff || revB > diff || revA > diff) {
|
|
||||||
isHave = false;
|
|
||||||
} else {
|
|
||||||
isHave = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isMerge && (revR || revG || revB || revA)) isHave = true;
|
|
||||||
if (isHave) {
|
|
||||||
data.data[i + 0] = rgba.r;
|
|
||||||
data.data[i + 1] = rgba.g;
|
|
||||||
data.data[i + 2] = rgba.b;
|
|
||||||
data.data[i + 3] = rgba.a;
|
|
||||||
} else {
|
|
||||||
data.data[i + 0] = 0;
|
|
||||||
data.data[i + 1] = 0;
|
|
||||||
data.data[i + 2] = 0;
|
|
||||||
data.data[i + 3] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
ctx.putImageData(data, 0, 0);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片边界跟踪算法(透明底)
|
|
||||||
* @param {HTMLCanvasElement} canvas - canvas元素
|
|
||||||
* @param {Number} scale - 缩放比例
|
|
||||||
* @returns {Array} 边界点数组 [{x, y}, ...]
|
|
||||||
*/
|
|
||||||
export function traceImageContour(canvas) {
|
|
||||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
||||||
const data = imageData.data;
|
|
||||||
const width = canvas.width;
|
|
||||||
const height = canvas.height;
|
|
||||||
|
|
||||||
// 查找起始点(第一个不透明像素)
|
|
||||||
let startX = -1;
|
|
||||||
let startY = -1;
|
|
||||||
|
|
||||||
outer: for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
const index = (y * width + x) * 4;
|
|
||||||
if (data[index + 3] > 0) {
|
|
||||||
startX = x;
|
|
||||||
startY = y;
|
|
||||||
break outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startX === -1) return []; // 没有不透明像素
|
|
||||||
|
|
||||||
// Moore-Neighbor边界跟踪算法
|
|
||||||
const contour = [];
|
|
||||||
const visited = new Set();
|
|
||||||
const directions = [
|
|
||||||
[-1, 0],
|
|
||||||
[-1, -1],
|
|
||||||
[0, -1],
|
|
||||||
[1, -1],
|
|
||||||
[1, 0],
|
|
||||||
[1, 1],
|
|
||||||
[0, 1],
|
|
||||||
[-1, 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
let currentX = startX;
|
|
||||||
let currentY = startY;
|
|
||||||
let backtrackDir = 4; // 起始方向:右
|
|
||||||
|
|
||||||
do {
|
|
||||||
const pointKey = `${currentX},${currentY}`;
|
|
||||||
if (!visited.has(pointKey)) {
|
|
||||||
contour.push({ x: currentX, y: currentY });
|
|
||||||
visited.add(pointKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从右方向开始顺时针查找
|
|
||||||
let found = false;
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
const dir = (backtrackDir + i) % 8;
|
|
||||||
const dx = directions[dir][0];
|
|
||||||
const dy = directions[dir][1];
|
|
||||||
const checkX = currentX + dx;
|
|
||||||
const checkY = currentY + dy;
|
|
||||||
|
|
||||||
if (
|
|
||||||
checkX >= 0 &&
|
|
||||||
checkX < width &&
|
|
||||||
checkY >= 0 &&
|
|
||||||
checkY < height
|
|
||||||
) {
|
|
||||||
const index = (checkY * width + checkX) * 4;
|
|
||||||
if (data[index + 3] > 0) {
|
|
||||||
currentX = checkX;
|
|
||||||
currentY = checkY;
|
|
||||||
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) break;
|
|
||||||
} while (
|
|
||||||
!(currentX === startX && currentY === startY) &&
|
|
||||||
visited.size < width * height
|
|
||||||
);
|
|
||||||
|
|
||||||
return contour;
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,12 @@ import { AnimationManager } from './AnimationManager'
|
|||||||
import { detectDeviceType } from '../tools/index'
|
import { detectDeviceType } from '../tools/index'
|
||||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||||
import { OperationType } from '../tools/layerHelper'
|
import { OperationType } from '../tools/layerHelper'
|
||||||
|
import { cloneObjects } from '../tools/canvasMethod'
|
||||||
import { createId } from '../../tools/tools'
|
import { createId } from '../../tools/tools'
|
||||||
import md5 from 'md5'
|
import md5 from 'md5'
|
||||||
|
|
||||||
// 自定义画布转对象属性
|
// 自定义画布转对象属性
|
||||||
fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail"];
|
fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail", "absolutePositioned"];
|
||||||
fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject
|
fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject
|
||||||
fabric.Object.prototype.toObject = function () {
|
fabric.Object.prototype.toObject = function () {
|
||||||
const args = [...arguments]
|
const args = [...arguments]
|
||||||
@@ -129,9 +130,12 @@ export class CanvasManager {
|
|||||||
|
|
||||||
/** 测试-开始 */
|
/** 测试-开始 */
|
||||||
// this.stateManager.setIsRecord(false)
|
// this.stateManager.setIsRecord(false)
|
||||||
// const rect = await this.layerManager.createRectLayer({ left: 200 })
|
// const groupObject = await this.layerManager.createGroupLayer()
|
||||||
// await this.layerManager.createStarLayer({ left: 400 })
|
// const parentId = groupObject.info.id
|
||||||
// await this.layerManager.createArrowLayer({ left: 600 })
|
// const rect = await this.layerManager.createRectLayer({ left: 200, info: { parentId } })
|
||||||
|
// const star = await this.layerManager.createStarLayer({ left: 400, info: { parentId } })
|
||||||
|
// const arrow = await this.layerManager.createArrowLayer({ left: 600, info: { parentId } })
|
||||||
|
// await this.layerManager.createGroupLayer()
|
||||||
// this.layerManager.setActiveID(rect.info.id)
|
// this.layerManager.setActiveID(rect.info.id)
|
||||||
// this.stateManager.setIsRecord(true)
|
// this.stateManager.setIsRecord(true)
|
||||||
/** 测试-结束 */
|
/** 测试-结束 */
|
||||||
@@ -147,7 +151,7 @@ export class CanvasManager {
|
|||||||
this.canvas.add(obj)
|
this.canvas.add(obj)
|
||||||
const id = obj?.info?.id || ""
|
const id = obj?.info?.id || ""
|
||||||
if (id) {
|
if (id) {
|
||||||
this.layerManager.updateLayers()
|
await this.layerManager.updateLayers(!!obj.info.parentId)
|
||||||
this.renderAll()
|
this.renderAll()
|
||||||
await this.layerManager.updateLayerThumbnailsById(id)
|
await this.layerManager.updateLayerThumbnailsById(id)
|
||||||
}
|
}
|
||||||
@@ -162,6 +166,35 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新子图层裁剪区域 */
|
||||||
|
async updateSubLayerClipPath() {
|
||||||
|
const objects = this.getObjects().filter((v: any) => v.type !== "group" && !!v.info?.id);
|
||||||
|
for (let i = 0; i < objects.length; i++) {
|
||||||
|
let object = objects[i]
|
||||||
|
if (object.clipPath) object.set({ clipPath: null })
|
||||||
|
let group = this.getObjectById(object.info.parentId)
|
||||||
|
if (!group) continue
|
||||||
|
let path = group.clipPath
|
||||||
|
if (!path) continue
|
||||||
|
let clipPath = await cloneObjects([path]).then((v) => v[0])
|
||||||
|
clipPath.set({
|
||||||
|
absolutePositioned: true,
|
||||||
|
})
|
||||||
|
object.set({ clipPath })
|
||||||
|
}
|
||||||
|
this.renderAll()
|
||||||
|
}
|
||||||
|
/** 排序画布对象 */
|
||||||
|
async sortObjectByIds(ids: string[], isRecord?: boolean) {
|
||||||
|
ids.forEach((id, index) => {
|
||||||
|
this.canvas.moveTo(this.getObjectById(id), index)
|
||||||
|
})
|
||||||
|
await this.updateSubLayerClipPath()
|
||||||
|
this.renderAll()
|
||||||
|
if (isRecord) this.stateManager.recordState()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 设置画布事件 */
|
/** 设置画布事件 */
|
||||||
setupCanvasEvents() {
|
setupCanvasEvents() {
|
||||||
// 创建画布事件管理器
|
// 创建画布事件管理器
|
||||||
@@ -177,8 +210,18 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
/** 设置激活对象 */
|
/** 设置激活对象 */
|
||||||
setActiveObjectById(id: string) {
|
setActiveObjectById(id: string) {
|
||||||
|
this.discardActiveObject()
|
||||||
const obj = this.getObjectById(id)
|
const obj = this.getObjectById(id)
|
||||||
if (obj && obj.evented) this.canvas.setActiveObject(obj)
|
if (!obj) return
|
||||||
|
if (obj.type === "group") {
|
||||||
|
const objects = [];
|
||||||
|
this.getObjects().forEach((item: any) => {
|
||||||
|
if (item?.info?.parentId === id) objects.push(item)
|
||||||
|
})
|
||||||
|
if (objects.length > 0) this.canvas.setActiveObject(new fabric.ActiveSelection(objects, { canvas: this.canvas }));
|
||||||
|
} else {
|
||||||
|
if (obj.evented) this.canvas.setActiveObject(obj)
|
||||||
|
}
|
||||||
this.renderAll()
|
this.renderAll()
|
||||||
}
|
}
|
||||||
resetZoom(animated = true, adaptive = true) {
|
resetZoom(animated = true, adaptive = true) {
|
||||||
@@ -222,11 +265,11 @@ export class CanvasManager {
|
|||||||
renderAll() {
|
renderAll() {
|
||||||
this.canvas.renderAll()
|
this.canvas.renderAll()
|
||||||
}
|
}
|
||||||
deleteObjectById(id: string) {
|
deleteObjectById(id: string, isUpdate = true) {
|
||||||
const object = this.getObjectById(id)
|
const object = this.getObjectById(id)
|
||||||
if (object) {
|
if (object) {
|
||||||
this.canvas.remove(object)
|
this.canvas.remove(object)
|
||||||
this.layerManager.updateLayers()
|
if (isUpdate) this.layerManager.updateLayers()
|
||||||
this.renderAll()
|
this.renderAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,13 +278,6 @@ export class CanvasManager {
|
|||||||
this.canvas.discardActiveObject()
|
this.canvas.discardActiveObject()
|
||||||
this.renderAll()
|
this.renderAll()
|
||||||
}
|
}
|
||||||
// 拖拽排序
|
|
||||||
dragSort(id, newIndex) {
|
|
||||||
this.canvas.moveTo(this.getObjectById(id), newIndex)
|
|
||||||
this.layerManager.updateLayers()
|
|
||||||
this.renderAll()
|
|
||||||
this.stateManager.recordState()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 画笔事件 */
|
/** 画笔事件 */
|
||||||
setupBrushEvents() {
|
setupBrushEvents() {
|
||||||
@@ -254,13 +290,13 @@ export class CanvasManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
/** 处理绘制图像 */
|
/** 处理绘制图像 */
|
||||||
handleDrawImage(fabricImage: fabric.Object) {
|
async handleDrawImage(fabricImage: fabric.Object) {
|
||||||
const activeID = this.stateManager.layerManager.activeID.value
|
const activeID = this.stateManager.layerManager.activeID.value
|
||||||
const activeLayer = this.getObjectById(activeID)
|
const activeLayer = this.getObjectById(activeID)
|
||||||
if (activeLayer) {
|
if (activeLayer) {
|
||||||
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
|
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
|
||||||
} else {
|
} else {
|
||||||
const emptyLayer = this.layerManager.createEmptyLayer(false);
|
const emptyLayer = await this.layerManager.createEmptyLayer(false);
|
||||||
this.layerManager.setActiveID(emptyLayer.info.id, false)
|
this.layerManager.setActiveID(emptyLayer.info.id, false)
|
||||||
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
|
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,18 @@ export class LayerManager {
|
|||||||
return this.getLayerById(this.activeID.value)
|
return this.getLayerById(this.activeID.value)
|
||||||
}
|
}
|
||||||
getLayerById(id) {
|
getLayerById(id) {
|
||||||
return this.layers.value.find((item: any) => item.info.id === id)
|
function call(arr) {
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
let v = arr[i]
|
||||||
|
if (v.info.id === id) return v
|
||||||
|
if (v.children) {
|
||||||
|
let layer = call(v.children)
|
||||||
|
if (layer) return layer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return call(this.layers.value)
|
||||||
}
|
}
|
||||||
setLayerNameById(id, name: string) {
|
setLayerNameById(id, name: string) {
|
||||||
const layer = this.getLayerById(id)
|
const layer = this.getLayerById(id)
|
||||||
@@ -82,6 +93,10 @@ export class LayerManager {
|
|||||||
|
|
||||||
/** 删除指定图层 */
|
/** 删除指定图层 */
|
||||||
deleteLayerById(id, isActive = true) {
|
deleteLayerById(id, isActive = true) {
|
||||||
|
const layer = this.getLayerById(id)
|
||||||
|
if (layer.children) {
|
||||||
|
layer.children.forEach(v => this.canvasManager.deleteObjectById(v.info.id, false))
|
||||||
|
}
|
||||||
this.canvasManager.deleteObjectById(id)
|
this.canvasManager.deleteObjectById(id)
|
||||||
if (id === this.activeID.value && isActive) {
|
if (id === this.activeID.value && isActive) {
|
||||||
this.setActiveID(this.layers.value[0]?.info?.id || "")
|
this.setActiveID(this.layers.value[0]?.info?.id || "")
|
||||||
@@ -106,17 +121,38 @@ export class LayerManager {
|
|||||||
this.setActiveID(newObject.info.id)
|
this.setActiveID(newObject.info.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 拖拽排序
|
/** 根据layers排序图层 */
|
||||||
dragSort(id, newIndex) {
|
async sortLayers(isRecord?: boolean) {
|
||||||
const index = Math.abs(this.layers.value.length - newIndex - 1)
|
const ids = [];
|
||||||
this.canvasManager.dragSort(id, index)
|
call(this.layers.value)
|
||||||
|
await this.canvasManager.sortObjectByIds(ids.reverse(), isRecord)
|
||||||
|
function call(arr) {
|
||||||
|
arr.forEach(v => {
|
||||||
|
ids.push(v.info.id)
|
||||||
|
if (v.children) call(v.children)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新图层列表
|
// 更新图层列表
|
||||||
updateLayers() {
|
async updateLayers(isSort = false) {
|
||||||
this.layers.value = this.canvasManager.getObjects()
|
const objects = this.canvasManager.getObjects().map(v => v.toObject()).filter(v => !!v.info?.id).reverse()
|
||||||
.filter((v: any) => !!v?.info?.id)
|
objects.forEach(v => {
|
||||||
.reverse()
|
if (v.type === "group") {
|
||||||
.map(v => v.toObject())
|
if (!v.children) v.children = []
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentId = v.info?.parentId
|
||||||
|
if (!parentId) return
|
||||||
|
objects.forEach((obj: any) => {
|
||||||
|
if (obj.info?.id !== parentId) return
|
||||||
|
if (!obj.children) obj.children = []
|
||||||
|
obj.children.push(v)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const layers = objects.filter(v => !v.info?.parentId)
|
||||||
|
this.layers.value = layers
|
||||||
|
if (isSort) await this.sortLayers()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置图层位置-不设置默认居中 */
|
/** 设置图层位置-不设置默认居中 */
|
||||||
@@ -136,7 +172,7 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** 创建空图层 */
|
/** 创建空图层 */
|
||||||
createEmptyLayer(isRecord = true, isActive = false) {
|
async createEmptyLayer(isRecord = true, isActive = false) {
|
||||||
const emptyObject = new fabric.Rect({
|
const emptyObject = new fabric.Rect({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
@@ -147,37 +183,28 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setLayerPosition(emptyObject)
|
this.setLayerPosition(emptyObject)
|
||||||
this.canvasManager.add(emptyObject, isRecord)
|
await this.canvasManager.add(emptyObject, isRecord)
|
||||||
if (isActive) this.setActiveID(emptyObject.info.id, false)
|
if (isActive) this.setActiveID(emptyObject.info.id, false)
|
||||||
return emptyObject
|
return emptyObject
|
||||||
}
|
}
|
||||||
/** 创建组图层 */
|
/** 创建组图层 */
|
||||||
createGroupLayer(options?: any, isRecord = true, isActive = false) {
|
async createGroupLayer(options?: any, isRecord = true, isActive = false) {
|
||||||
const child = options?.child || []
|
const children = options?.children || []
|
||||||
delete options.child
|
delete options.children
|
||||||
const groupObject = new fabric.Group(child, {
|
const groupObject = new fabric.Group(children, {
|
||||||
// subTargetCheck: true, // 关键:检测子对象
|
...(options || {}),
|
||||||
// interactive: true, // 启用交互
|
hasControls: false, // 不显示控制点
|
||||||
// hasControls: true,
|
hasBorders: false, // 不显示边框
|
||||||
// hasBorders: true,
|
selectable: false, // 不可选中(可选)
|
||||||
|
|
||||||
// // 子对象样式
|
|
||||||
// cornerColor: 'blue',
|
|
||||||
// cornerSize: 8,
|
|
||||||
// borderColor: 'green',
|
|
||||||
|
|
||||||
// // 允许子对象独立变换
|
|
||||||
// lockScalingX: false,
|
|
||||||
// lockScalingY: false,
|
|
||||||
// lockRotation: false,
|
|
||||||
info: {
|
info: {
|
||||||
id: createId("group"),
|
id: createId("group"),
|
||||||
name: '组图层',
|
name: '组图层',
|
||||||
|
showChildren: true,
|
||||||
...(options?.info || {}),
|
...(options?.info || {}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// this.setLayerPosition(groupObject)
|
this.setLayerPosition(groupObject, options)
|
||||||
this.canvasManager.add(groupObject, isRecord)
|
await this.canvasManager.add(groupObject, isRecord)
|
||||||
if (isActive) this.setActiveID(groupObject.info.id, false)
|
if (isActive) this.setActiveID(groupObject.info.id, false)
|
||||||
return groupObject
|
return groupObject
|
||||||
}
|
}
|
||||||
@@ -199,7 +226,7 @@ export class LayerManager {
|
|||||||
return textObject
|
return textObject
|
||||||
}
|
}
|
||||||
/** 创建矩形图层 */
|
/** 创建矩形图层 */
|
||||||
async createRectLayer(options?: any, isActive = false) {
|
async createRectLayer(options?: any, isRecord = true, isActive = true) {
|
||||||
const rectObject = new fabric.Rect({
|
const rectObject = new fabric.Rect({
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@@ -213,12 +240,12 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setLayerPosition(rectObject, options)
|
this.setLayerPosition(rectObject, options)
|
||||||
await this.canvasManager.add(rectObject)
|
await this.canvasManager.add(rectObject, isRecord)
|
||||||
if (isActive) this.setActiveID(rectObject.info.id)
|
if (isActive) this.setActiveID(rectObject.info.id)
|
||||||
return rectObject
|
return rectObject
|
||||||
}
|
}
|
||||||
/** 创建直线图层 */
|
/** 创建直线图层 */
|
||||||
async createLineLayer(options?: any, isActive = false) {
|
async createLineLayer(options?: any, isRecord = true, isActive = true) {
|
||||||
const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0]
|
const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0]
|
||||||
delete options.x1
|
delete options.x1
|
||||||
delete options.y1
|
delete options.y1
|
||||||
@@ -235,14 +262,13 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setLayerPosition(lineObject, options)
|
this.setLayerPosition(lineObject, options)
|
||||||
await this.canvasManager.add(lineObject)
|
await this.canvasManager.add(lineObject, isRecord)
|
||||||
if (isActive) this.setActiveID(lineObject.info.id)
|
if (isActive) this.setActiveID(lineObject.info.id)
|
||||||
return lineObject
|
return lineObject
|
||||||
}
|
}
|
||||||
/** 创建椭圆图层 */
|
/** 创建椭圆图层 */
|
||||||
async createEllipseLayer(options?: any, isActive = false) {
|
async createEllipseLayer(options?: any, isRecord = true, isActive = true) {
|
||||||
const ellipseObject = new fabric.Ellipse({
|
const ellipseObject = new fabric.Ellipse({
|
||||||
radius: 50,
|
|
||||||
fill: '#000',
|
fill: '#000',
|
||||||
strokeWidth: 0,
|
strokeWidth: 0,
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
@@ -258,7 +284,7 @@ export class LayerManager {
|
|||||||
return ellipseObject
|
return ellipseObject
|
||||||
}
|
}
|
||||||
/** 创建三角形图层 */
|
/** 创建三角形图层 */
|
||||||
async createTriangleLayer(options?: any, isActive = false) {
|
async createTriangleLayer(options?: any, isRecord = true, isActive = true) {
|
||||||
const triangleObject = new fabric.Triangle({
|
const triangleObject = new fabric.Triangle({
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
@@ -272,12 +298,12 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setLayerPosition(triangleObject, options)
|
this.setLayerPosition(triangleObject, options)
|
||||||
await this.canvasManager.add(triangleObject)
|
await this.canvasManager.add(triangleObject, isRecord)
|
||||||
if (isActive) this.setActiveID(triangleObject.info.id)
|
if (isActive) this.setActiveID(triangleObject.info.id)
|
||||||
return triangleObject
|
return triangleObject
|
||||||
}
|
}
|
||||||
/** 创建五角星图层 */
|
/** 创建五角星图层 */
|
||||||
async createStarLayer(options?: any, isActive = false) {
|
async createStarLayer(options?: any, isRecord = true, isActive = true) {
|
||||||
const width = options?.width || 100
|
const width = options?.width || 100
|
||||||
const height = options?.height || 100
|
const height = options?.height || 100
|
||||||
delete options.points
|
delete options.points
|
||||||
@@ -292,12 +318,12 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.setLayerPosition(starObject, options)
|
this.setLayerPosition(starObject, options)
|
||||||
await this.canvasManager.add(starObject)
|
await this.canvasManager.add(starObject, isRecord)
|
||||||
if (isActive) this.setActiveID(starObject.info.id)
|
if (isActive) this.setActiveID(starObject.info.id)
|
||||||
return starObject
|
return starObject
|
||||||
}
|
}
|
||||||
/** 创建箭头图层 */
|
/** 创建箭头图层 */
|
||||||
async createArrowLayer(options?: any, isActive = false) {
|
async createArrowLayer(options?: any, isRecord = true, isActive = true) {
|
||||||
const width = options?.width || 100
|
const width = options?.width || 100
|
||||||
const height = options?.height || 10
|
const height = options?.height || 10
|
||||||
delete options.width
|
delete options.width
|
||||||
@@ -316,7 +342,7 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.setLayerPosition(arrowObject, options)
|
this.setLayerPosition(arrowObject, options)
|
||||||
this.canvasManager.add(arrowObject)
|
await this.canvasManager.add(arrowObject, isRecord)
|
||||||
if (isActive) this.setActiveID(arrowObject.info.id)
|
if (isActive) this.setActiveID(arrowObject.info.id)
|
||||||
return arrowObject
|
return arrowObject
|
||||||
}
|
}
|
||||||
@@ -324,7 +350,7 @@ export class LayerManager {
|
|||||||
|
|
||||||
|
|
||||||
/** 创建图片图层 */
|
/** 创建图片图层 */
|
||||||
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) {
|
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true, isActive = true) {
|
||||||
const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
|
const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
|
||||||
|
|
||||||
const imageObject = await new Promise((resolve) => {
|
const imageObject = await new Promise((resolve) => {
|
||||||
@@ -350,7 +376,7 @@ export class LayerManager {
|
|||||||
}) as fabric.Object
|
}) as fabric.Object
|
||||||
this.setLayerPosition(imageObject, options)
|
this.setLayerPosition(imageObject, options)
|
||||||
await this.canvasManager.add(imageObject, isRecord)
|
await this.canvasManager.add(imageObject, isRecord)
|
||||||
this.setActiveID(imageObject.info.id)
|
if (isActive) this.setActiveID(imageObject.info.id)
|
||||||
return imageObject
|
return imageObject
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +389,7 @@ export class LayerManager {
|
|||||||
left: info.left,
|
left: info.left,
|
||||||
top: info.top,
|
top: info.top,
|
||||||
info: {
|
info: {
|
||||||
|
...(targetLayer?.info || {}),
|
||||||
id: createId("image"),
|
id: createId("image"),
|
||||||
name: targetLayer?.info?.name || "合并图层",
|
name: targetLayer?.info?.name || "合并图层",
|
||||||
}
|
}
|
||||||
@@ -370,7 +397,6 @@ export class LayerManager {
|
|||||||
resolve(img)
|
resolve(img)
|
||||||
}, { crossOrigin: 'anonymous' })
|
}, { crossOrigin: 'anonymous' })
|
||||||
})
|
})
|
||||||
// console.log(mergedImage)
|
|
||||||
const index = this.canvasManager.getObjects().indexOf(targetLayer);
|
const index = this.canvasManager.getObjects().indexOf(targetLayer);
|
||||||
this.deleteLayerById(targetLayer.info.id, false)
|
this.deleteLayerById(targetLayer.info.id, false)
|
||||||
this.setActiveID(mergedImage.info.id, false)
|
this.setActiveID(mergedImage.info.id, false)
|
||||||
@@ -391,12 +417,12 @@ export class LayerManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/** 更新图层缩略图 */
|
/** 更新图层缩略图 */
|
||||||
async updateLayerThumbnailsById(id: string) {
|
async updateLayerThumbnailsById(id: string, thumbnail?: string, isUpdate = true) {
|
||||||
const object = this.canvasManager.getObjectById(id);
|
const object = this.canvasManager.getObjectById(id);
|
||||||
if (!object) return;
|
if (!object) return;
|
||||||
const url = await exportObjectToThumbnail(object);
|
const url = thumbnail || await exportObjectToThumbnail(object);
|
||||||
object.thumbnail = url
|
object.thumbnail = url
|
||||||
this.updateLayers()
|
if (isUpdate) this.updateLayers()
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() { }
|
dispose() { }
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export class ShapeToolManager {
|
|||||||
upRectangle(object) {
|
upRectangle(object) {
|
||||||
if (object.width === 0) object.width = 100
|
if (object.width === 0) object.width = 100
|
||||||
if (object.height === 0) object.height = 100
|
if (object.height === 0) object.height = 100
|
||||||
this.layerManager.createRectLayer(object, true)
|
this.layerManager.createRectLayer(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绘制直线 */
|
/** 绘制直线 */
|
||||||
@@ -151,7 +151,7 @@ export class ShapeToolManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
upLine(object) {
|
upLine(object) {
|
||||||
this.layerManager.createLineLayer(object, true)
|
this.layerManager.createLineLayer(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绘制椭圆 */
|
/** 绘制椭圆 */
|
||||||
@@ -170,7 +170,7 @@ export class ShapeToolManager {
|
|||||||
upEllipse(object) {
|
upEllipse(object) {
|
||||||
if (object.rx === 0) object.rx = 50
|
if (object.rx === 0) object.rx = 50
|
||||||
if (object.ry === 0) object.ry = 50
|
if (object.ry === 0) object.ry = 50
|
||||||
this.layerManager.createEllipseLayer(object, true)
|
this.layerManager.createEllipseLayer(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ export class ShapeToolManager {
|
|||||||
upTriangle(object) {
|
upTriangle(object) {
|
||||||
if (object.width === 0) object.width = 100
|
if (object.width === 0) object.width = 100
|
||||||
if (object.height === 0) object.height = 100
|
if (object.height === 0) object.height = 100
|
||||||
this.layerManager.createTriangleLayer(object, true)
|
this.layerManager.createTriangleLayer(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ export class ShapeToolManager {
|
|||||||
upStar(object) {
|
upStar(object) {
|
||||||
if (object.width === 0) object.width = 100
|
if (object.width === 0) object.width = 100
|
||||||
if (object.height === 0) object.height = 100
|
if (object.height === 0) object.height = 100
|
||||||
this.layerManager.createStarLayer(object, true)
|
this.layerManager.createStarLayer(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绘制箭头 */
|
/** 绘制箭头 */
|
||||||
@@ -249,7 +249,7 @@ export class ShapeToolManager {
|
|||||||
top: this.startY,
|
top: this.startY,
|
||||||
}, true)
|
}, true)
|
||||||
} else {
|
} else {
|
||||||
this.layerManager.createArrowLayer(object, true)
|
this.layerManager.createArrowLayer(object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export class StateManager {
|
|||||||
/** 记录状态 */
|
/** 记录状态 */
|
||||||
recordState() {
|
recordState() {
|
||||||
if (this.running.value) return
|
if (this.running.value) return
|
||||||
|
console.log("recordState")
|
||||||
this.running.value = true
|
this.running.value = true
|
||||||
if (this.historyIndex.value < this.historyList.value.length - 1) {
|
if (this.historyIndex.value < this.historyList.value.length - 1) {
|
||||||
this.historyList.value.splice(this.historyIndex.value + 1)
|
this.historyList.value.splice(this.historyIndex.value + 1)
|
||||||
|
|||||||
@@ -730,8 +730,10 @@ export class CanvasEventManager {
|
|||||||
});
|
});
|
||||||
this.canvas.on("object:modified", async (e) => {
|
this.canvas.on("object:modified", async (e) => {
|
||||||
// updateLayers(e);
|
// updateLayers(e);
|
||||||
const id = e.target?.info?.id;
|
const target = e.target;
|
||||||
|
const id = target?.info?.id;
|
||||||
if (id) await this.layerManager.updateLayerThumbnailsById(id)
|
if (id) await this.layerManager.updateLayerThumbnailsById(id)
|
||||||
|
if (target.type === "group") await this.canvasManager.updateSubLayerClipPath()
|
||||||
this.stateManager.recordState();
|
this.stateManager.recordState();
|
||||||
});
|
});
|
||||||
this.canvas.on("object:removed", (e) => {
|
this.canvas.on("object:removed", (e) => {
|
||||||
|
|||||||
@@ -93,3 +93,157 @@ export function angleBetweenPointsDegrees(x1, y1, x2, y2) {
|
|||||||
|
|
||||||
return deg;
|
return deg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对象黑白通道画布
|
||||||
|
* @param {fabric.Object} object - 要处理的 fabric 对象
|
||||||
|
* @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明
|
||||||
|
* @param {number} diff - 差值,默认 25
|
||||||
|
* @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
|
||||||
|
* @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData
|
||||||
|
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
|
||||||
|
*/
|
||||||
|
export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
|
||||||
|
const image = object.getElement();
|
||||||
|
if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
|
||||||
|
console.warn("对象不是图片");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { width, height } = image;
|
||||||
|
if (!width || !height) {
|
||||||
|
console.warn("对象没有元素");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
ctx.drawImage(image, 0, 0, width, height);
|
||||||
|
const data = ctx.getImageData(0, 0, width, height);
|
||||||
|
for (let i = 0; i < data.data.length; i += 4) {
|
||||||
|
const r = data.data[i + 0];
|
||||||
|
const g = data.data[i + 1];
|
||||||
|
const b = data.data[i + 2];
|
||||||
|
const a = data.data[i + 3];
|
||||||
|
const revR = revData?.data[i + 0] || 0;
|
||||||
|
const revG = revData?.data[i + 1] || 0;
|
||||||
|
const revB = revData?.data[i + 2] || 0;
|
||||||
|
const revA = revData?.data[i + 3] || 0;
|
||||||
|
let isHave = false;
|
||||||
|
if (r || g || b || a) {
|
||||||
|
if (revR > diff || revG > diff || revB > diff || revA > diff) {
|
||||||
|
isHave = false;
|
||||||
|
} else {
|
||||||
|
isHave = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isMerge && (revR || revG || revB || revA)) isHave = true;
|
||||||
|
if (isHave) {
|
||||||
|
data.data[i + 0] = rgba.r;
|
||||||
|
data.data[i + 1] = rgba.g;
|
||||||
|
data.data[i + 2] = rgba.b;
|
||||||
|
data.data[i + 3] = rgba.a;
|
||||||
|
} else {
|
||||||
|
data.data[i + 0] = 0;
|
||||||
|
data.data[i + 1] = 0;
|
||||||
|
data.data[i + 2] = 0;
|
||||||
|
data.data[i + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
ctx.putImageData(data, 0, 0);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片边界跟踪算法(透明底)
|
||||||
|
* @param {HTMLCanvasElement} canvas - canvas元素
|
||||||
|
* @param {Number} scale - 缩放比例
|
||||||
|
* @returns {Array} 边界点数组 [{x, y}, ...]
|
||||||
|
*/
|
||||||
|
export function traceImageContour(canvas) {
|
||||||
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
// 查找起始点(第一个不透明像素)
|
||||||
|
let startX = -1;
|
||||||
|
let startY = -1;
|
||||||
|
|
||||||
|
outer: for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const index = (y * width + x) * 4;
|
||||||
|
if (data[index + 3] > 0) {
|
||||||
|
startX = x;
|
||||||
|
startY = y;
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startX === -1) return []; // 没有不透明像素
|
||||||
|
|
||||||
|
// Moore-Neighbor边界跟踪算法
|
||||||
|
const contour = [];
|
||||||
|
const visited = new Set();
|
||||||
|
const directions = [
|
||||||
|
[-1, 0],
|
||||||
|
[-1, -1],
|
||||||
|
[0, -1],
|
||||||
|
[1, -1],
|
||||||
|
[1, 0],
|
||||||
|
[1, 1],
|
||||||
|
[0, 1],
|
||||||
|
[-1, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
let currentX = startX;
|
||||||
|
let currentY = startY;
|
||||||
|
let backtrackDir = 4; // 起始方向:右
|
||||||
|
|
||||||
|
do {
|
||||||
|
const pointKey = `${currentX},${currentY}`;
|
||||||
|
if (!visited.has(pointKey)) {
|
||||||
|
contour.push({ x: currentX, y: currentY });
|
||||||
|
visited.add(pointKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从右方向开始顺时针查找
|
||||||
|
let found = false;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const dir = (backtrackDir + i) % 8;
|
||||||
|
const dx = directions[dir][0];
|
||||||
|
const dy = directions[dir][1];
|
||||||
|
const checkX = currentX + dx;
|
||||||
|
const checkY = currentY + dy;
|
||||||
|
|
||||||
|
if (
|
||||||
|
checkX >= 0 &&
|
||||||
|
checkX < width &&
|
||||||
|
checkY >= 0 &&
|
||||||
|
checkY < height
|
||||||
|
) {
|
||||||
|
const index = (checkY * width + checkX) * 4;
|
||||||
|
if (data[index + 3] > 0) {
|
||||||
|
currentX = checkX;
|
||||||
|
currentY = checkY;
|
||||||
|
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) break;
|
||||||
|
} while (
|
||||||
|
!(currentX === startX && currentY === startY) &&
|
||||||
|
visited.size < width * height
|
||||||
|
);
|
||||||
|
|
||||||
|
return contour;
|
||||||
|
}
|
||||||
@@ -29,6 +29,12 @@ export async function exportObjectsToImage(objects = [], isDetails = false) {
|
|||||||
left: obj.left - boundingBox.left,
|
left: obj.left - boundingBox.left,
|
||||||
top: obj.top - boundingBox.top,
|
top: obj.top - boundingBox.top,
|
||||||
})
|
})
|
||||||
|
if (obj.clipPath && obj.clipPath.absolutePositioned) {
|
||||||
|
obj.clipPath.set({
|
||||||
|
left: obj.clipPath.left - boundingBox.left,
|
||||||
|
top: obj.clipPath.top - boundingBox.top,
|
||||||
|
})
|
||||||
|
}
|
||||||
staticCanvas.add(obj)
|
staticCanvas.add(obj)
|
||||||
})
|
})
|
||||||
// 导出图片
|
// 导出图片
|
||||||
|
|||||||
Reference in New Issue
Block a user