深度画布
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="show" class="ai-selectbox-panel">
|
||||
<div>
|
||||
<span class="icon"><svg-icon name="dc-add" size="16" /></span>
|
||||
<span class="label">Add</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="icon"><svg-icon name="dc-remove" size="16" /></span>
|
||||
<span class="label">Remove</span>
|
||||
</div>
|
||||
<button>创建</button>
|
||||
</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 showTools = [OperationType.SELECTBOX]
|
||||
const show = computed(() => showTools.includes(props.currentTool))
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ai-selectbox-panel {
|
||||
position: absolute;
|
||||
top: 8.8rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: 4.1rem;
|
||||
padding: 0 1.7rem;
|
||||
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: 1rem;
|
||||
> div {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.6rem;
|
||||
padding: 0 0.7rem;
|
||||
border-radius: 0.3rem;
|
||||
gap: 1rem;
|
||||
> .label {
|
||||
font-size: 1.4rem;
|
||||
color: #000;
|
||||
}
|
||||
&.active,
|
||||
&:hover {
|
||||
background: rgba(235, 235, 235, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,8 +6,10 @@
|
||||
<template v-if="isReady">
|
||||
<layer-panel />
|
||||
<details-panel />
|
||||
<depth-header-tools @export="exportCanvas" @workbench="(v) => emit('workbench', v)" />
|
||||
<brush-control-panel :currentTool="toolManager.currentTool.value" />
|
||||
<ai-selectbox-panel :currentTool="toolManager.currentTool.value" />
|
||||
<depth-header-tools @export="exportCanvas" @workbench="(v) => emit('workbench', v)" />
|
||||
|
||||
<zoom
|
||||
:zoom="canvasManager.currentZoom.value / 100"
|
||||
is-home
|
||||
@@ -30,6 +32,7 @@
|
||||
import depthHeaderTools from './components/depth-header-tools.vue'
|
||||
import zoom from '../components/zoom.vue'
|
||||
import brushControlPanel from './components/brush-control-panel.vue'
|
||||
import aiSelectboxPanel from './components/ai-selectbox-panel.vue'
|
||||
|
||||
// 管理器
|
||||
import { StateManager } from './manager/StateManager'
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
id: config.value.canvasId || null
|
||||
}
|
||||
const sData = await saveDepthCanvas(data)
|
||||
console.log(sData)
|
||||
|
||||
const canvasId = sData.id
|
||||
// base64 转 file 上传转换为 url
|
||||
const file = base64Tofile(options.url, 'canvas.png')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { fabric } from 'fabric-with-all'
|
||||
import { createId } from '../../tools/tools'
|
||||
|
||||
/** 智能框选工具管理器 */
|
||||
export class AISelectboxToolManager {
|
||||
// 管理器
|
||||
@@ -14,6 +16,7 @@ export class AISelectboxToolManager {
|
||||
this.canvasManager = options.canvasManager
|
||||
this.stateManager = options.stateManager
|
||||
this.layerManager = options.layerManager
|
||||
|
||||
}
|
||||
mouseDownEvent(e) {
|
||||
this.isDragging = true
|
||||
@@ -39,11 +42,11 @@ export class AISelectboxToolManager {
|
||||
var height = e.absolutePointer.y - this.startY
|
||||
var left = this.startX
|
||||
var top = this.startY
|
||||
if(width < 0) {
|
||||
if (width < 0) {
|
||||
left += width
|
||||
width = -width
|
||||
}
|
||||
if(height < 0) {
|
||||
if (height < 0) {
|
||||
top += height
|
||||
height = -height
|
||||
}
|
||||
@@ -60,6 +63,249 @@ export class AISelectboxToolManager {
|
||||
this.canvasManager.canvas.remove(this.demoObject)
|
||||
this.canvasManager.canvas.renderAll()
|
||||
|
||||
this.createSelectbox()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
loadImageToObject(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fabric.Image.fromURL(url, (img) => {
|
||||
resolve(img);
|
||||
}, { crossOrigin: "anonymous" });// 防止污染
|
||||
});
|
||||
}
|
||||
rgba = { r: 0, g: 255, b: 0, a: 200 };
|
||||
selectionStyle = {
|
||||
stroke: "rgba(255, 77, 71, 1)",
|
||||
strokeWidth: 1.5,
|
||||
strokeDashArray: [4, 4],
|
||||
fill: "rgba(255, 186, 186, 0.5)",
|
||||
strokeUniform: true, // 保持描边宽度不随缩放改变
|
||||
// strokeLineCap: "round",// 折线端点样式
|
||||
// strokeLineJoin: "bevel", // 折线连接样式
|
||||
// selectable: false,
|
||||
// evented: false,
|
||||
excludeFromExport: true,
|
||||
hoverCursor: "default",
|
||||
moveCursor: "default",
|
||||
};
|
||||
async createSelectbox() {
|
||||
const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
|
||||
const image = await this.loadImageToObject(url)
|
||||
const canvas = getObjectAlphaToCanvas(image, null, 0, this.rgba);
|
||||
const fobject = this.canvasManager.canvas.clipPath
|
||||
// const top = fobject.top - fobject.height * scaleY / 2;
|
||||
// const left = fobject.left - fobject.width * scaleX / 2;
|
||||
const scaleY = fobject.scaleY
|
||||
const scaleX = fobject.scaleX
|
||||
const top = fobject.top
|
||||
const left = fobject.left
|
||||
const arr = traceImageContour(canvas);
|
||||
let minX = fobject.width;
|
||||
let minY = fobject.height;
|
||||
const str = arr.map((v) => {
|
||||
if (v.x < minX) minX = v.x;
|
||||
if (v.y < minY) minY = v.y;
|
||||
return `${v.x} ${v.y}`
|
||||
}).join(" L ");
|
||||
const path = new fabric.Path(`M ${str} z`);
|
||||
path.set({
|
||||
left: left + minX * scaleX,
|
||||
top: top + minY * scaleY,
|
||||
scaleX: scaleX,
|
||||
scaleY: scaleY,
|
||||
...this.selectionStyle,
|
||||
});
|
||||
const rect1 = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#f00',
|
||||
info: {
|
||||
id: createId("rect"),
|
||||
name: '矩形图层',
|
||||
}
|
||||
})
|
||||
const rect2 = new fabric.Rect({
|
||||
left: 200,
|
||||
top: 200,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#ff0',
|
||||
info: {
|
||||
id: createId("rect"),
|
||||
name: '矩形图层',
|
||||
}
|
||||
})
|
||||
this.layerManager.createGroupLayer({
|
||||
child: [rect1, rect2],
|
||||
})
|
||||
// this.canvasManager.canvas.add(path)
|
||||
// this.canvasManager.canvas.renderAll()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -29,6 +29,17 @@ fabric.Object.prototype.toObject = function () {
|
||||
return object
|
||||
}
|
||||
|
||||
fabric.Image.fromURL = (function (originalFromURL) {
|
||||
return function (url, callback, imgOptions) {
|
||||
// 为所有图片请求添加 crossOrigin
|
||||
const options = {
|
||||
crossOrigin: 'anonymous', // 关键设置
|
||||
...imgOptions
|
||||
};
|
||||
return originalFromURL.call(this, url, callback, options);
|
||||
};
|
||||
})(fabric.Image.fromURL);
|
||||
|
||||
interface CanvasInitOptions {
|
||||
canvasRef: any
|
||||
canvasViewWidth?: number
|
||||
|
||||
@@ -151,6 +151,37 @@ export class LayerManager {
|
||||
if (isActive) this.setActiveID(emptyObject.info.id, false)
|
||||
return emptyObject
|
||||
}
|
||||
/** 创建组图层 */
|
||||
createGroupLayer(options?: any, isRecord = true, isActive = false) {
|
||||
const child = options?.child || []
|
||||
delete options.child
|
||||
const groupObject = new fabric.Group(child, {
|
||||
subTargetCheck: true, // 关键:检测子对象
|
||||
interactive: true, // 启用交互
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
|
||||
// // 子对象样式
|
||||
// cornerColor: 'blue',
|
||||
// cornerSize: 8,
|
||||
// borderColor: 'green',
|
||||
|
||||
// // 允许子对象独立变换
|
||||
// lockScalingX: false,
|
||||
// lockScalingY: false,
|
||||
// lockRotation: false,
|
||||
info: {
|
||||
id: createId("group"),
|
||||
name: '组图层',
|
||||
...(options?.info || {}),
|
||||
}
|
||||
})
|
||||
// this.setLayerPosition(groupObject)
|
||||
this.canvasManager.add(groupObject, isRecord)
|
||||
if (isActive) this.setActiveID(groupObject.info.id, false)
|
||||
return groupObject
|
||||
}
|
||||
|
||||
/** 创建文本图层 */
|
||||
async createTextLayer(text: string, options?: any) {
|
||||
const textObject = new fabric.IText(text, {
|
||||
|
||||
Reference in New Issue
Block a user