画布增加的新功能
This commit is contained in:
@@ -54,6 +54,24 @@
|
|||||||
<div class="content unicode" style="display: block;">
|
<div class="content unicode" style="display: block;">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">混合模式</div>
|
||||||
|
<div class="code-name">&#xe7a4;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">更多</div>
|
||||||
|
<div class="code-name">&#xe60f;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">平铺</div>
|
||||||
|
<div class="code-name">&#xe8d7;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont"></span>
|
<span class="icon iconfont"></span>
|
||||||
<div class="name">裁剪</div>
|
<div class="name">裁剪</div>
|
||||||
@@ -276,9 +294,9 @@
|
|||||||
<pre><code class="language-css"
|
<pre><code class="language-css"
|
||||||
>@font-face {
|
>@font-face {
|
||||||
font-family: 'iconfont';
|
font-family: 'iconfont';
|
||||||
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
|
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
|
||||||
url('iconfont.woff?t=1762934152017') format('woff'),
|
url('iconfont.woff?t=1766460927921') format('woff'),
|
||||||
url('iconfont.ttf?t=1762934152017') format('truetype');
|
url('iconfont.ttf?t=1766460927921') format('truetype');
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||||
@@ -304,6 +322,33 @@
|
|||||||
<div class="content font-class">
|
<div class="content font-class">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont icon-hunhemoshi"></span>
|
||||||
|
<div class="name">
|
||||||
|
混合模式
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.icon-hunhemoshi
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont icon-gengduo"></span>
|
||||||
|
<div class="name">
|
||||||
|
更多
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.icon-gengduo
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont icon-repeat"></span>
|
||||||
|
<div class="name">
|
||||||
|
平铺
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.icon-repeat
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont icon-caijian"></span>
|
<span class="icon iconfont icon-caijian"></span>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -637,6 +682,30 @@
|
|||||||
<div class="content symbol">
|
<div class="content symbol">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-hunhemoshi"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">混合模式</div>
|
||||||
|
<div class="code-name">#icon-hunhemoshi</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-gengduo"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">更多</div>
|
||||||
|
<div class="code-name">#icon-gengduo</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#icon-repeat"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">平铺</div>
|
||||||
|
<div class="code-name">#icon-repeat</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
<use xlink:href="#icon-caijian"></use>
|
<use xlink:href="#icon-caijian"></use>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 4292253 */
|
font-family: "iconfont"; /* Project id 4292253 */
|
||||||
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
|
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
|
||||||
url('iconfont.woff?t=1762934152017') format('woff'),
|
url('iconfont.woff?t=1766460927921') format('woff'),
|
||||||
url('iconfont.ttf?t=1762934152017') format('truetype');
|
url('iconfont.ttf?t=1766460927921') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,18 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-hunhemoshi:before {
|
||||||
|
content: "\e7a4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gengduo:before {
|
||||||
|
content: "\e60f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-repeat:before {
|
||||||
|
content: "\e8d7";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-caijian:before {
|
.icon-caijian:before {
|
||||||
content: "\e650";
|
content: "\e650";
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,27 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "42604348",
|
||||||
|
"name": "混合模式",
|
||||||
|
"font_class": "hunhemoshi",
|
||||||
|
"unicode": "e7a4",
|
||||||
|
"unicode_decimal": 59300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "45981931",
|
||||||
|
"name": "更多",
|
||||||
|
"font_class": "gengduo",
|
||||||
|
"unicode": "e60f",
|
||||||
|
"unicode_decimal": 58895
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "17005660",
|
||||||
|
"name": "平铺",
|
||||||
|
"font_class": "repeat",
|
||||||
|
"unicode": "e8d7",
|
||||||
|
"unicode_decimal": 59607
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "22138606",
|
"icon_id": "22138606",
|
||||||
"name": "裁剪",
|
"name": "裁剪",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -69,7 +69,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
|||||||
layer.clippingMask,
|
layer.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||||
this.newFill = new fabric.Rect({
|
this.newFill = new fabric.Rect({
|
||||||
width: clippingMaskFabricObject.width,
|
width: clippingMaskFabricObject.width,
|
||||||
@@ -117,7 +117,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
|||||||
this.parent.clippingMask,
|
this.parent.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||||
this.newFill = new fabric.Rect({
|
this.newFill = new fabric.Rect({
|
||||||
width: clippingMaskFabricObject.width,
|
width: clippingMaskFabricObject.width,
|
||||||
@@ -222,7 +222,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
|||||||
this.parent?.clippingMask,
|
this.parent?.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clipPath.clipPath = null;
|
// clipPath.clipPath = null;
|
||||||
clipPath.set({ absolutePositioned: true });
|
clipPath.set({ absolutePositioned: true });
|
||||||
this.group.clipPath = clipPath;
|
this.group.clipPath = clipPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class FillLayerBackgroundCommand extends Command {
|
|||||||
layer.clippingMask,
|
layer.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
|
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
// 设置绝对定位
|
// 设置绝对定位
|
||||||
|
|||||||
301
src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
Normal file
301
src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import { Command } from "./Command";
|
||||||
|
import { findLayerRecursively } from "../utils/layerHelper";
|
||||||
|
import { fabric } from "fabric-with-all";
|
||||||
|
import {
|
||||||
|
findObjectById,
|
||||||
|
generateId,
|
||||||
|
insertObjectAtZIndex,
|
||||||
|
removeCanvasObjectByObject,
|
||||||
|
createPatternTransform,
|
||||||
|
} from "../utils/helper";
|
||||||
|
import { restoreFabricObject } from "../utils/objectHelper";
|
||||||
|
|
||||||
|
const scale = 0.3;// 默认缩放比例
|
||||||
|
|
||||||
|
export const FillSourceToBase64 = (source) => {
|
||||||
|
if (source?.toDataURL) {
|
||||||
|
return source.toDataURL?.();
|
||||||
|
} else if (source?.src) {
|
||||||
|
return source.src;
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充图案平铺命令
|
||||||
|
* 填充重复属性:repeat | repeat-x | repeat-y | no-repeat
|
||||||
|
* 默认缩放比例:0.3
|
||||||
|
* 默认偏移量:50%
|
||||||
|
*/
|
||||||
|
export class FillRepeatCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({ name: "填充图案平铺", saveState: true });
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layers = options.layers;
|
||||||
|
this.canvasManager = options.canvasManager;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.layerId = options.layerId;
|
||||||
|
this.fillRepeat = options.fillRepeat;
|
||||||
|
this.oldObjects = null;
|
||||||
|
this.oldLocked = null;
|
||||||
|
this.oldIsDisableUnlock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||||
|
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||||
|
console.warn("图层不存在或没有 fabric 对象");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||||
|
if (!object || (object.type !== "rect" && object.type !== "image")) {
|
||||||
|
console.warn("当前对象不能平铺", object.type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.oldObjects = object;
|
||||||
|
const img = await new Promise((resolve, reject) => {
|
||||||
|
if (object.type === "rect") {
|
||||||
|
let source = object.fill.source;
|
||||||
|
resolve(source);
|
||||||
|
} else if (object.type === "image") {
|
||||||
|
// resolve(object.getElement());
|
||||||
|
// fabric.Image.fromURL(
|
||||||
|
// object.src,
|
||||||
|
// v => resolve(v),
|
||||||
|
// { crossOrigin: "anonymous" }
|
||||||
|
// );
|
||||||
|
const imgElement = object.getElement();
|
||||||
|
// 创建透明 Canvas
|
||||||
|
const tcanvas = document.createElement('canvas');
|
||||||
|
tcanvas.width = imgElement.width;
|
||||||
|
tcanvas.height = imgElement.height;
|
||||||
|
const ctx = tcanvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
||||||
|
ctx.drawImage(imgElement, 0, 0);
|
||||||
|
resolve(tcanvas);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const fill_ = {
|
||||||
|
source: FillSourceToBase64(img),
|
||||||
|
gapX: 0,
|
||||||
|
gapY: 0,
|
||||||
|
};
|
||||||
|
const bgObject = this.canvasManager.getBackgroundLayerObject();
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
source: img,
|
||||||
|
repeat: this.fillRepeat,
|
||||||
|
patternTransform: object.fill?.hasOwnProperty("patternTransform") ? object.fill.patternTransform : createPatternTransform(scale, 0),
|
||||||
|
offsetX: object.fill?.hasOwnProperty("offsetX") ? object.fill.offsetX : bgObject.width / 2, // 水平偏移
|
||||||
|
offsetY: object.fill?.hasOwnProperty("offsetY") ? object.fill.offsetY : bgObject.height / 2, // 垂直偏移
|
||||||
|
});
|
||||||
|
const rect = new fabric.Rect({
|
||||||
|
id: object.id,
|
||||||
|
layerId: object.layerId,
|
||||||
|
layerName: object.layerName,
|
||||||
|
fill_,
|
||||||
|
});
|
||||||
|
layer.fabricObjects = [rect.toObject(["id", "layerId", "layerName"])];
|
||||||
|
this.oldLocked = layer.locked;
|
||||||
|
// this.oldIsDisableUnlock = layer.isDisableUnlock;
|
||||||
|
// layer.isDisableUnlock = true;
|
||||||
|
if (this.oldObjects.type === "rect") {
|
||||||
|
rect.set({
|
||||||
|
width: object.width,
|
||||||
|
height: object.height,
|
||||||
|
top: object.top,
|
||||||
|
left: object.left,
|
||||||
|
originX: object.originX,
|
||||||
|
originY: object.originY,
|
||||||
|
angle: object.angle,
|
||||||
|
scaleX: object.scaleX,
|
||||||
|
scaleY: object.scaleY,
|
||||||
|
flipX: object.flipX,
|
||||||
|
flipY: object.flipY,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
rect.set({
|
||||||
|
width: bgObject.width,
|
||||||
|
height: bgObject.height,
|
||||||
|
top: bgObject.top,
|
||||||
|
left: bgObject.left,
|
||||||
|
originX: bgObject.originX,
|
||||||
|
originY: bgObject.originY,
|
||||||
|
});
|
||||||
|
layer.locked = true;
|
||||||
|
}
|
||||||
|
rect.set("fill", pattern);
|
||||||
|
this.canvas.add(rect);
|
||||||
|
this.canvas.remove(object);
|
||||||
|
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
|
await this.layerManager?.sortLayersWithTool?.();
|
||||||
|
await this.canvasManager.thumbnailManager?.generateLayerThumbnail(
|
||||||
|
this.layerId
|
||||||
|
);
|
||||||
|
await this.layerManager.selectLayerObjects(this.layerId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async undo() {
|
||||||
|
if (!this.oldObjects) {
|
||||||
|
console.warn("没有旧对象可恢复");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.oldObjects.layerId);
|
||||||
|
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||||
|
console.warn("图层不存在或没有 fabric 对象");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||||
|
this.canvas.remove(object);
|
||||||
|
this.canvas.add(this.oldObjects);
|
||||||
|
layer.fabricObjects = [this.oldObjects.toObject(["id", "layerId", "layerName"])];
|
||||||
|
layer.locked = this.oldLocked;
|
||||||
|
// layer.isDisableUnlock = this.oldIsDisableUnlock;
|
||||||
|
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
|
await this.layerManager?.sortLayersWithTool?.();
|
||||||
|
this.canvas.renderAll();
|
||||||
|
this.canvasManager.thumbnailManager?.generateLayerThumbnail(this.layerId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充图案更改参数
|
||||||
|
*/
|
||||||
|
export class FillRepeatChangeCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({ name: "填充图案更改参数", saveState: true });
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layers = options.layers;
|
||||||
|
this.canvasManager = options.canvasManager;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.layerId = options.layerId;
|
||||||
|
this.newPattern = options.newPattern;
|
||||||
|
this.oldPattern = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||||
|
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||||
|
console.warn("图层不存在或没有 fabric 对象");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||||
|
if (!object || object.type !== "rect") {
|
||||||
|
console.warn("当前对象不是矩形", object);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.oldPattern = object.oldPattern || object.get("fill");
|
||||||
|
delete object.oldPattern;
|
||||||
|
const oldPattern = { ...this.oldPattern };
|
||||||
|
delete oldPattern.id;
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
...oldPattern,
|
||||||
|
...this.newPattern,
|
||||||
|
});
|
||||||
|
object.set("fill", pattern);
|
||||||
|
this.canvas.renderAll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async undo() {
|
||||||
|
if (!this.oldPattern) {
|
||||||
|
console.warn("没有旧图案可恢复");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||||
|
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||||
|
console.warn("图层不存在或没有 fabric 对象");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||||
|
if (!object || object.type !== "rect") {
|
||||||
|
console.warn("当前对象不是矩形", object);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
...this.oldPattern
|
||||||
|
});
|
||||||
|
object.set("fill", pattern);
|
||||||
|
this.canvas.renderAll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充图案更改间隙
|
||||||
|
*/
|
||||||
|
export class FillRepeatGapChangeCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({ name: "填充图案更改间隙", saveState: true });
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layers = options.layers;
|
||||||
|
this.canvasManager = options.canvasManager;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.layerId = options.layerId;
|
||||||
|
this.newGapX = options.newGapX;
|
||||||
|
this.newGapY = options.newGapY;
|
||||||
|
this.record = !!options.record;
|
||||||
|
this.oldGapX = null;
|
||||||
|
this.oldGapY = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(isUndo = false) {
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||||
|
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||||
|
console.warn("图层不存在或没有 fabric 对象");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||||
|
if (!object || object.type !== "rect") {
|
||||||
|
console.warn("当前对象不是矩形", object);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!object.fill_) {
|
||||||
|
object.fill_ = {
|
||||||
|
source: FillSourceToBase64(object.fill.source),
|
||||||
|
gapX: 0,
|
||||||
|
gapY: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isUndo) {
|
||||||
|
object.fill_.gapX = this.oldGapX;
|
||||||
|
object.fill_.gapY = this.oldGapY;
|
||||||
|
} else {
|
||||||
|
if (!object.oldFill_ && this.record) {
|
||||||
|
object.oldFill_ = { ...object.fill_ };
|
||||||
|
}
|
||||||
|
this.oldGapX = object.fill_.gapX;
|
||||||
|
this.oldGapY = object.fill_.gapY;
|
||||||
|
object.fill_.gapX = this.newGapX;
|
||||||
|
object.fill_.gapY = this.newGapY;
|
||||||
|
}
|
||||||
|
const image = new Image();
|
||||||
|
image.src = object.fill_.source;
|
||||||
|
await image.decode();
|
||||||
|
// 创建透明 Canvas
|
||||||
|
const tcanvas = document.createElement('canvas');
|
||||||
|
tcanvas.width = image.width + object.fill_.gapX;
|
||||||
|
tcanvas.height = image.height + object.fill_.gapY;
|
||||||
|
const ctx = tcanvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const fill = object.get("fill");
|
||||||
|
fill.source = tcanvas;
|
||||||
|
object.set("fill", new fabric.Pattern(fill));
|
||||||
|
this.canvas.renderAll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async undo() {
|
||||||
|
if (this.oldGapX === null || this.oldGapY === null) {
|
||||||
|
console.warn("没有旧间隙可恢复");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.execute(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
|
|||||||
import { ToolCommand } from "./ToolCommands";
|
import { ToolCommand } from "./ToolCommands";
|
||||||
import {
|
import {
|
||||||
findObjectById,
|
findObjectById,
|
||||||
|
findObjectByLayerId,
|
||||||
generateId,
|
generateId,
|
||||||
getObjectZIndex,
|
getObjectZIndex,
|
||||||
insertObjectAtZIndex,
|
insertObjectAtZIndex,
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
} from "../utils/helper";
|
} from "../utils/helper";
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { restoreFabricObject } from "../utils/objectHelper";
|
import { restoreFabricObject } from "../utils/objectHelper";
|
||||||
|
import EventManager from "../utils/event.js";
|
||||||
/**
|
/**
|
||||||
* 添加图层命令
|
* 添加图层命令
|
||||||
*/
|
*/
|
||||||
@@ -36,7 +37,7 @@ export class AddLayerCommand extends Command {
|
|||||||
|
|
||||||
this.insertIndex = options.insertIndex;
|
this.insertIndex = options.insertIndex;
|
||||||
this.oldActiveLayerId = null;
|
this.oldActiveLayerId = null;
|
||||||
this.beforeLayers = [...this.layers.value]; // 备份原图层列表
|
this.beforeLayers = JSON.stringify(this.layers.value); // 备份原图层列表
|
||||||
|
|
||||||
this.options = options.options || {};
|
this.options = options.options || {};
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ export class AddLayerCommand extends Command {
|
|||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
// 从图层列表删除该图层
|
// 从图层列表删除该图层
|
||||||
this.layers.value = [...this.beforeLayers];
|
this.layers.value = JSON.parse(this.beforeLayers);
|
||||||
|
|
||||||
// 恢复原活动图层
|
// 恢复原活动图层
|
||||||
this.activeLayerId.value = this.oldActiveLayerId;
|
this.activeLayerId.value = this.oldActiveLayerId;
|
||||||
@@ -251,12 +252,12 @@ export class PasteLayerCommand extends Command {
|
|||||||
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
|
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
clippingMaskFabricObject.dirty = true;
|
// clippingMaskFabricObject.dirty = true;
|
||||||
clippingMaskFabricObject.setCoords();
|
clippingMaskFabricObject.setCoords();
|
||||||
// 添加所有对象到画布
|
// 添加所有对象到画布
|
||||||
allObjects.forEach((obj) => {
|
allObjects.forEach((obj) => {
|
||||||
@@ -802,15 +803,23 @@ export class ToggleLayerVisibilityCommand extends Command {
|
|||||||
|
|
||||||
// 切换可见性
|
// 切换可见性
|
||||||
this.layer.visible = !this.layer.visible;
|
this.layer.visible = !this.layer.visible;
|
||||||
|
const ids = [this.layerId];
|
||||||
|
const childLayers = this.layer?.children || [];
|
||||||
|
childLayers.forEach((childLayer) => {
|
||||||
|
childLayer.visible = this.layer.visible;
|
||||||
|
ids.push(childLayer.id);
|
||||||
|
});
|
||||||
|
|
||||||
// 更新画布上图层对象的可见性
|
// 更新画布上图层对象的可见性
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
const layerObjects = this.canvas
|
this.canvas.getObjects().forEach((obj) => {
|
||||||
.getObjects()
|
if (ids.includes(obj.layerId)) {
|
||||||
.filter((obj) => obj.layerId === this.layerId);
|
obj.getObjects?.()?.forEach((item) => {
|
||||||
layerObjects.forEach((obj) => {
|
item.visible = this.layer.visible;
|
||||||
obj.visible = this.layer.visible;
|
});
|
||||||
});
|
obj.visible = this.layer.visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 更新画布上对象的可选择状态
|
// 更新画布上对象的可选择状态
|
||||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
@@ -868,13 +877,14 @@ export class ToggleChildLayerVisibilityCommand extends Command {
|
|||||||
|
|
||||||
// 更新画布上图层对象的可见性
|
// 更新画布上图层对象的可见性
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
const layerObjects = this.canvas
|
this.canvas.getObjects().forEach((obj) => {
|
||||||
.getObjects()
|
if (obj.layerId === this.layerId) {
|
||||||
.filter((obj) => obj.layerId === this.layerId);
|
obj.getObjects?.()?.forEach((item) => {
|
||||||
|
item.visible = this.childLayer.visible;
|
||||||
layerObjects.forEach((obj) => {
|
});
|
||||||
obj.visible = this.childLayer.visible;
|
obj.visible = this.childLayer.visible;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新画布上对象的可选择状态
|
// 更新画布上对象的可选择状态
|
||||||
@@ -1007,9 +1017,8 @@ export class LayerLockCommand extends Command {
|
|||||||
|
|
||||||
// 如果是组图层,递归更新所有子图层
|
// 如果是组图层,递归更新所有子图层
|
||||||
if (
|
if (
|
||||||
layer.type === "group" &&
|
|
||||||
layer.children &&
|
layer.children &&
|
||||||
Array.isArray(layer.children)
|
Array.isArray(layer.children) && layer.children.length > 0
|
||||||
) {
|
) {
|
||||||
layer.children.forEach((child) => {
|
layer.children.forEach((child) => {
|
||||||
this._updateLayerLockState(child, locked);
|
this._updateLayerLockState(child, locked);
|
||||||
@@ -1108,7 +1117,7 @@ export class SetLayerOpacityCommand extends Command {
|
|||||||
|
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
|
EventManager.emit("object:opacity:execute", this.layerId, this.opacity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,6 +1139,7 @@ export class SetLayerOpacityCommand extends Command {
|
|||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EventManager.emit("object:opacity:undo", this.layerId, this.opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfo() {
|
getInfo() {
|
||||||
@@ -1371,7 +1381,7 @@ export class GroupLayersCommand extends Command {
|
|||||||
// 备份原图层
|
// 备份原图层
|
||||||
this.originalLayers = [...this.layers.value];
|
this.originalLayers = [...this.layers.value];
|
||||||
// 新组ID
|
// 新组ID
|
||||||
this.groupId =
|
this.groupId = options.id ||
|
||||||
generateId("group_layer_") ||
|
generateId("group_layer_") ||
|
||||||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||||
|
|
||||||
@@ -4434,3 +4444,87 @@ export class ChildLayerLockCommand extends Command {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 设置图层混合模式
|
||||||
|
*/
|
||||||
|
export class SetLayerCompositeCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: "设置图层混合模式",
|
||||||
|
saveState: false,
|
||||||
|
});
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layers = options.layers;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.layerId = options.layerId;
|
||||||
|
this.newValue = options.newValue;
|
||||||
|
this.oldValue = options.oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(isUndo = false) {
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||||
|
const { object } = findObjectByLayerId(this.canvas, this.layerId);
|
||||||
|
if (!layer || !object) {
|
||||||
|
console.error(`图层${this.layerId}不存在`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// console.log("==========", this.newValue, this.oldValue);
|
||||||
|
const value = isUndo ? this.oldValue : this.newValue;
|
||||||
|
layer.blendMode = value;
|
||||||
|
object.set("globalCompositeOperation", value);
|
||||||
|
this.canvas.renderAll();
|
||||||
|
const event = isUndo ? "object:composite:undo" : "object:composite:execute";
|
||||||
|
EventManager.emit(event, object);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
return this.execute(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置颜色图层颜色
|
||||||
|
*/
|
||||||
|
export class SetColorLayerFillCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: "设置颜色图层颜色",
|
||||||
|
saveState: false,
|
||||||
|
});
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.object = options.object;
|
||||||
|
this.layer = this.layerManager?.getLayerById(this.object.layerId);
|
||||||
|
this.newFill = options.newFill;
|
||||||
|
this.oldFill = JSON.parse(JSON.stringify(this.object.fill));
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(isUndo = false) {
|
||||||
|
if (!this.object) {
|
||||||
|
console.error(`颜色图层不存在`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isVisible = this.layer?.visible;
|
||||||
|
if(!isVisible && this.layer) this.layer.visible = true;
|
||||||
|
const gradient = new fabric.Gradient({
|
||||||
|
type: "linear",
|
||||||
|
gradientUnits: "percentage",
|
||||||
|
...(isUndo ? this.oldFill : this.newFill),
|
||||||
|
});
|
||||||
|
this.object.setFill(gradient);
|
||||||
|
this.canvas.renderAll();
|
||||||
|
await this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
|
||||||
|
this.object.id
|
||||||
|
);
|
||||||
|
if(!isVisible && this.layer) this.layer.visible = false;
|
||||||
|
this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
this.execute(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/component/Canvas/CanvasEditor/commands/ObjectCommands.js
Normal file
50
src/component/Canvas/CanvasEditor/commands/ObjectCommands.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Command } from "./Command.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象移动命令
|
||||||
|
* 轻量级命令,只记录对象的移动属性变化(位置)
|
||||||
|
*/
|
||||||
|
export class ObjectMoveCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: options.name || "对象移动",
|
||||||
|
description: options.description || "移动对象",
|
||||||
|
saveState: false, // 自己管理状态,避免递归
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.initPos = options.initPos;
|
||||||
|
this.finalPos = options.finalPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行命令
|
||||||
|
*/
|
||||||
|
async execute() {
|
||||||
|
this.setObjectsPos(this.finalPos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 撤销命令
|
||||||
|
* 应用初始状态
|
||||||
|
*/
|
||||||
|
async undo() {
|
||||||
|
this.setObjectsPos(this.initPos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setObjectsPos(pos) {
|
||||||
|
const objects = this.canvas.getObjects();
|
||||||
|
const arr = typeof pos === "object" ? [pos] : pos;
|
||||||
|
arr.forEach((item) => {
|
||||||
|
const obj = objects.find((o) => o.id === item.id);
|
||||||
|
if(obj) {
|
||||||
|
obj.set({
|
||||||
|
left: item.left,
|
||||||
|
top: item.top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.canvas.renderAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,7 +234,7 @@ export class AddObjectToLayerCommand extends Command {
|
|||||||
parent.clippingMask,
|
parent.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||||
this.fabricObject.clipPath = clippingMaskFabricObject;
|
this.fabricObject.clipPath = clippingMaskFabricObject;
|
||||||
// 标记为脏对象
|
// 标记为脏对象
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
this.layerId
|
this.layerId
|
||||||
);
|
);
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
this.parentLayer = parent;
|
// this.parentLayer = parent;
|
||||||
|
|
||||||
// 新增:如果有父图层,则栅格化父图层及其所有子图层
|
// // 新增:如果有父图层,则栅格化父图层及其所有子图层
|
||||||
if (this.parentLayer) {
|
// if (this.parentLayer) {
|
||||||
this.layer = this.parentLayer;
|
// this.layer = this.parentLayer;
|
||||||
this.layerId = this.parentLayer.id;
|
// this.layerId = this.parentLayer.id;
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 恢复原始图层结构
|
// 恢复原始图层结构
|
||||||
this.layers.value = [...this.originalLayerStructure];
|
this.layers.value = JSON.parse(this.originalLayerStructure);
|
||||||
|
|
||||||
// 恢复原活动图层
|
// 恢复原活动图层
|
||||||
this.activeLayerId.value = this.layerId;
|
this.activeLayerId.value = this.layerId;
|
||||||
@@ -191,7 +191,7 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
_saveOriginalLayerStructure() {
|
_saveOriginalLayerStructure() {
|
||||||
// 只保存相关的图层结构,而不是整个图层数组
|
// 只保存相关的图层结构,而不是整个图层数组
|
||||||
this.originalLayerStructure = JSON.parse(JSON.stringify(this.layers.value));
|
this.originalLayerStructure = JSON.stringify(this.layers.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,12 +517,12 @@ export class ExportLayerToImageCommand extends Command {
|
|||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
clippingMaskFabricObject.dirty = true;
|
// clippingMaskFabricObject.dirty = true;
|
||||||
clippingMaskFabricObject.setCoords();
|
clippingMaskFabricObject.setCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { findObjectById } from "../utils/helper";
|
|||||||
import { findLayerRecursively } from "../utils/layerHelper";
|
import { findLayerRecursively } from "../utils/layerHelper";
|
||||||
import { restoreFabricObject } from "../utils/objectHelper";
|
import { restoreFabricObject } from "../utils/objectHelper";
|
||||||
import { Command } from "./Command";
|
import { Command } from "./Command";
|
||||||
|
import EventManager from "../utils/event.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对象变换命令
|
* 对象变换命令
|
||||||
@@ -75,7 +76,7 @@ export class TransformCommand extends Command {
|
|||||||
|
|
||||||
// 触发画布更新
|
// 触发画布更新
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
|
EventManager.emit("object:modified:execute", targetObject);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ export class TransformCommand extends Command {
|
|||||||
}, 300);
|
}, 300);
|
||||||
// 触发画布更新
|
// 触发画布更新
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
|
EventManager.emit("object:modified:undo", targetObject);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ export class TransformCommand extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (clippingMaskFabricObject) {
|
if (clippingMaskFabricObject) {
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export class UpdateGroupMaskPositionCommand extends Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 图片列表面板 -->
|
|
||||||
<div v-if="showPanel" class="crop-image-overlay" @click.self="close">
|
<div v-if="showPanel" class="crop-image-overlay" @click.self="close">
|
||||||
<div class="crop-image-modal">
|
<div class="crop-image-modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -392,7 +391,7 @@
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
/* 弹窗遮罩层 */
|
/* 弹窗遮罩层 */
|
||||||
.crop-image-overlay {
|
.crop-image-overlay {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -420,8 +419,8 @@
|
|||||||
.crop-image-modal {
|
.crop-image-modal {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
width: 80%;
|
width: 90%;
|
||||||
height: 80%;
|
height: 90%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
import { ref, nextTick, computed, inject } from "vue";
|
import { ref, nextTick, computed, inject } from "vue";
|
||||||
import { Checkbox } from "ant-design-vue";
|
import { Checkbox } from "ant-design-vue";
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { isGroupLayer } from "../../utils/layerHelper";
|
import { isGroupLayer, SpecialLayerId } from "../../utils/layerHelper";
|
||||||
|
import { fillToCssStyle, palletToFill, fillToPallet } from "../../utils/helper";
|
||||||
|
import { SetColorLayerFillCommand } from "../../commands/LayerCommands";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
// 设置组件名称,用于递归渲染
|
// 设置组件名称,用于递归渲染
|
||||||
@@ -183,6 +185,9 @@ function handleToggleVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleLock() {
|
function handleToggleLock() {
|
||||||
|
// 禁用解锁的图层不能操作
|
||||||
|
if (props.layer.isDisableUnlock) return;
|
||||||
|
|
||||||
if (props.isChild) {
|
if (props.isChild) {
|
||||||
// 子图层需要传递父图层ID - 从父级组件获取
|
// 子图层需要传递父图层ID - 从父级组件获取
|
||||||
const parentId = props.layer.parentId || findParentLayerId();
|
const parentId = props.layer.parentId || findParentLayerId();
|
||||||
@@ -348,6 +353,30 @@ function findParentLayerId() {
|
|||||||
console.warn("无法找到图层的父图层:", props.layer.id);
|
console.warn("无法找到图层的父图层:", props.layer.id);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const canvasManager = inject('canvasManager');
|
||||||
|
const layerObject = computed(() => {
|
||||||
|
const layer = props.layer;
|
||||||
|
const id = layer.fabricObject?.id || layer.fabricObjects?.[0]?.id || layer.id;
|
||||||
|
return canvasManager.getLayerObjectById(id);
|
||||||
|
});
|
||||||
|
const palletPanel = inject("palletPanel");
|
||||||
|
const clickColor = () => {
|
||||||
|
const fill = layerObject.value.fill;
|
||||||
|
if (fill) {
|
||||||
|
const obj = fillToPallet(fill);
|
||||||
|
console.log("===========:", obj);
|
||||||
|
palletPanel(obj).then((res) => {
|
||||||
|
console.log("===========:", res);
|
||||||
|
const cmd = new SetColorLayerFillCommand({
|
||||||
|
canvas: canvasManager.canvas,
|
||||||
|
layerManager: layerManager,
|
||||||
|
object: layerObject.value,
|
||||||
|
newFill: palletToFill(res),
|
||||||
|
});
|
||||||
|
layerManager.commandManager.execute(cmd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -377,8 +406,8 @@ function findParentLayerId() {
|
|||||||
@contextmenu.prevent="handleContextMenu"
|
@contextmenu.prevent="handleContextMenu"
|
||||||
>
|
>
|
||||||
<!-- 拖拽手柄 -->
|
<!-- 拖拽手柄 -->
|
||||||
<div class="layer-drag-handle" :title="$t('拖拽排序')">
|
<div class="layer-drag-handle" :title="$t('拖拽排序')" v-if="!isHidenDragHandle">
|
||||||
<SvgIcon v-if="!isHidenDragHandle" :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
|
<SvgIcon :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图层头部 -->
|
<!-- 图层头部 -->
|
||||||
@@ -417,9 +446,18 @@ function findParentLayerId() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 颜色图层按钮 -->
|
||||||
|
<div
|
||||||
|
class="layer-color-btn"
|
||||||
|
v-if="layer.id === SpecialLayerId.COLOR"
|
||||||
|
@click.stop="clickColor"
|
||||||
|
:style="{
|
||||||
|
background: fillToCssStyle(layerObject.fill),
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- 图层操作按钮 -->
|
<!-- 图层操作按钮 -->
|
||||||
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
|
<div class="layer-actions" >
|
||||||
<!-- 可见性切换 -->
|
<!-- 可见性切换 -->
|
||||||
<div
|
<div
|
||||||
class="visibility-btn"
|
class="visibility-btn"
|
||||||
@@ -434,7 +472,7 @@ function findParentLayerId() {
|
|||||||
<span
|
<span
|
||||||
v-if="layer.locked"
|
v-if="layer.locked"
|
||||||
class="status-icon locked"
|
class="status-icon locked"
|
||||||
:class="{ disabled: layer.isBackground || layer.isFixed }"
|
:class="{ disabled: layer.isBackground || layer.isFixed || layer.isDisableUnlock || layer.isFixedOther }"
|
||||||
:title="$t('锁定')"
|
:title="$t('锁定')"
|
||||||
@click.stop="handleToggleLock"
|
@click.stop="handleToggleLock"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ const fillColorRef = ref(null);
|
|||||||
// 计算属性:可排序的根级图层(排除背景层和固定层)
|
// 计算属性:可排序的根级图层(排除背景层和固定层)
|
||||||
const sortableRootLayers = computed(() => {
|
const sortableRootLayers = computed(() => {
|
||||||
if (!layers) return [];
|
if (!layers) return [];
|
||||||
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground);
|
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground && !layer.isFixedOther);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性:不可排序的固定图层(背景层和固定层)
|
// 计算属性:不可排序的固定图层(背景层和固定层)
|
||||||
const fixedLayers = computed(() => {
|
const fixedLayers = computed(() => {
|
||||||
if (!layers) return [];
|
if (!layers) return [];
|
||||||
return layers.value.filter((layer) => {
|
return layers.value.filter((layer) => {
|
||||||
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground);
|
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground || layer.isFixedOther);
|
||||||
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
|
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -576,7 +576,7 @@ function handleLayerClick(layer, event) {
|
|||||||
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
|
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
|
||||||
toggleLayerSelection(layer, event);
|
toggleLayerSelection(layer, event);
|
||||||
} else {
|
} else {
|
||||||
lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
|
if(!layer.isFixedClipMask) lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
|
||||||
// 普通点击:进入单选模式
|
// 普通点击:进入单选模式
|
||||||
// selectedLayerIds.value = [layer.id];
|
// selectedLayerIds.value = [layer.id];
|
||||||
// isMultiSelectMode.value = false;
|
// isMultiSelectMode.value = false;
|
||||||
@@ -596,7 +596,7 @@ function handleLayerClick(layer, event) {
|
|||||||
layerManager?.updateLayersObjectsInteractivity();
|
layerManager?.updateLayersObjectsInteractivity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
|
if(!layer.isFixedClipMask) lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,7 +999,7 @@ function buildChildLayerContextMenuItems(childLayer) {
|
|||||||
{
|
{
|
||||||
label: childLayer.locked ? "解锁图层" : "锁定图层",
|
label: childLayer.locked ? "解锁图层" : "锁定图层",
|
||||||
icon: childLayer.locked ? "CUnLock" : "CLock",
|
icon: childLayer.locked ? "CUnLock" : "CLock",
|
||||||
disabled: childLayer.isBackground || childLayer.isFixed,
|
disabled: childLayer.isBackground || childLayer.isFixed || childLayer.isDisableUnlock,
|
||||||
action: () => toggleChildLayerLock(childLayer.id),
|
action: () => toggleChildLayerLock(childLayer.id),
|
||||||
},
|
},
|
||||||
// 显示/隐藏
|
// 显示/隐藏
|
||||||
@@ -1633,7 +1633,6 @@ async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex
|
|||||||
@delete-child="deleteChildLayer"
|
@delete-child="deleteChildLayer"
|
||||||
@rename-child="renameChildLayer"
|
@rename-child="renameChildLayer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 固定层(背景层和固定层) -->
|
<!-- 固定层(背景层和固定层) -->
|
||||||
<div v-if="fixedLayers.length > 0" class="fixed-layers">
|
<div v-if="fixedLayers.length > 0" class="fixed-layers">
|
||||||
<!-- 遍历固定层 -->
|
<!-- 遍历固定层 -->
|
||||||
|
|||||||
@@ -340,6 +340,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layer-color-btn{
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
// 图层操作
|
// 图层操作
|
||||||
.layer-actions {
|
.layer-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ async function prepareForLiquify(targetObj) {
|
|||||||
}
|
}
|
||||||
updateAllParams();
|
updateAllParams();
|
||||||
|
|
||||||
console.log("液化环境准备完成");
|
console.log("液化环境准备完成",compositeCommand);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("准备液化环境失败:", error);
|
console.error("准备液化环境失败:", error);
|
||||||
@@ -1614,6 +1614,7 @@ function close() {
|
|||||||
*/
|
*/
|
||||||
function startPressTimer() {
|
function startPressTimer() {
|
||||||
if (pressTimer.value) return;
|
if (pressTimer.value) return;
|
||||||
|
if (currentMode.value === compositeCommand.value.liquifyManager.enhancedManager.modes.PUSH) return;
|
||||||
|
|
||||||
pressTimer.value = setInterval(() => {
|
pressTimer.value = setInterval(() => {
|
||||||
// 计算按压持续时间
|
// 计算按压持续时间
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 颜色选择器模板 -->
|
||||||
|
<div v-show="showPanel" class="pallet-overlay" @click.self="close">
|
||||||
|
<div class="pallet-modal">
|
||||||
|
<!-- <div class="modal-header">
|
||||||
|
<h3></h3>
|
||||||
|
<button class="close-btn" @click="close">×</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<pallet
|
||||||
|
v-if="showPanel"
|
||||||
|
:selectColor="selectColor"
|
||||||
|
@selectUplpadColor="selectUplpadColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="image-count" @click="close">
|
||||||
|
{{ $t("Canvas.close") }}
|
||||||
|
</div>
|
||||||
|
<div class="image-submit gallery_btn" @click="confirm">
|
||||||
|
{{ $t("Canvas.confirm") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
computed,
|
||||||
|
defineProps,
|
||||||
|
onDeactivated,
|
||||||
|
reactive,
|
||||||
|
onMounted,
|
||||||
|
defineExpose,
|
||||||
|
nextTick,
|
||||||
|
onUnmounted,
|
||||||
|
} from "vue";
|
||||||
|
import pallet from "./pallet.vue";
|
||||||
|
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps([]);
|
||||||
|
const { t } = useI18n();
|
||||||
|
var resolveFn: (value: any) => void;
|
||||||
|
const showPanel = ref(false);
|
||||||
|
const open = (obj = {}) => {
|
||||||
|
selectColor.value = JSON.parse(JSON.stringify(obj));
|
||||||
|
showPanel.value = true;
|
||||||
|
return new Promise((resolve) => (resolveFn = resolve));
|
||||||
|
};
|
||||||
|
const close = () => {
|
||||||
|
showPanel.value = false;
|
||||||
|
};
|
||||||
|
//提交选中的T图片
|
||||||
|
const confirm = () => {
|
||||||
|
close();
|
||||||
|
resolveFn && resolveFn(JSON.parse(JSON.stringify(selectColor.value)));
|
||||||
|
};
|
||||||
|
const selectColor = ref({});
|
||||||
|
const selectUplpadColor = (item: any) => {
|
||||||
|
selectColor.value = JSON.parse(JSON.stringify(item));
|
||||||
|
};
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 弹窗遮罩层 */
|
||||||
|
.pallet-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1001;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗主体 */
|
||||||
|
.pallet-modal {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 95%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
animation: modalSlideUp 0.3s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗头部 */
|
||||||
|
.modal-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #333;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗内容 */
|
||||||
|
.modal-content {
|
||||||
|
width: 35rem;
|
||||||
|
// max-width: 240px;
|
||||||
|
margin: 10px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗底部 */
|
||||||
|
.modal-footer {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
// border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
> .image-submit {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pallet" ref="palletRef">
|
||||||
|
<div class="palletColo" @click="openPallet">
|
||||||
|
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
||||||
|
{{ selectColor.hex }}
|
||||||
|
</div>
|
||||||
|
<div v-show="selectColor.gradient" class="palletBackColor" :style="{'background-image':`linear-gradient(${selectColor.gradient?.angle}deg,${setGradient(selectColor.gradient)})`}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="palletBox">
|
||||||
|
<div class="color_setting_block" @click.stop>
|
||||||
|
<Chrome class="chrome_color" v-model="color_"></Chrome>
|
||||||
|
<div class="color_setting_operateSingle">
|
||||||
|
<div class="color_setting_btn" :class="{active:!color?.gradient?.gradientShow}">{{ $t('ColorboardUpload.Single') }}</div>
|
||||||
|
<a-switch :checked="color?.gradient?.gradientShow" @click="setOperate"/>
|
||||||
|
<div class="color_setting_btn" :class="{active:color?.gradient?.gradientShow}">{{ $t('ColorboardUpload.Gradual') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="color_setting_operate" v-if="color?.gradient?.gradientShow">
|
||||||
|
<div class="color_setting_operate_item color_setting_operate_control">
|
||||||
|
<div class="operate_item_box">
|
||||||
|
<div>{{ $t('ColorboardUpload.Alignment') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="operate_item_box operate_item_angle">
|
||||||
|
<div class="operate_item_angle_box" @mousedown="mousedownGradientAngle(getMousePosition($event,false))" @touchstart="mousedownGradientAngle(getMousePosition($event,true))">
|
||||||
|
<div :style="{'transform':`rotate(${color.gradient.angle}deg)`}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="operate_item_box operate_item_delete">
|
||||||
|
<i class="fi fi-rr-trash" @click="deleteGradientItem"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color_setting_operate_item color_setting_operate_input">
|
||||||
|
<div class="color_setting_operate_bg" @click="addGradient($event)" :style="{'background-image':color?.gradient?`linear-gradient(90deg,${setGradient(color.gradient)})`:'none'}">
|
||||||
|
</div>
|
||||||
|
<div v-for="item,index in color.gradient.gradientList" :key="item" class="color_setting_operate_btn" :class="{'active':index == color.gradient.selectIndex}" :style="{'left':item.left,'background-color':`rgba(${item.rgba.r},${item.rgba.g},${item.rgba.b},${item.rgba.a})`}" @mousedown="mousedownGradient(getMousePosition($event,false),item,index,)" @touchstart="mousedownGradient(getMousePosition($event,true),item,index,)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent,computed,ref,watch,nextTick,onMounted,onUnmounted,toRefs, reactive} from 'vue'
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { message,Upload} from 'ant-design-vue';
|
||||||
|
import { Sketch, Chrome} from '@ans1998/vue3-color'
|
||||||
|
import { getMousePosition } from "@/tool/mdEvent";
|
||||||
|
import { rgbaToHex } from "@/tool/util"
|
||||||
|
import { color } from 'echarts/core';
|
||||||
|
export default defineComponent({
|
||||||
|
components:{
|
||||||
|
Chrome,
|
||||||
|
},
|
||||||
|
props:{
|
||||||
|
selectColor:{
|
||||||
|
type:Object,
|
||||||
|
default:()=>{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits:['selectUplpadColor'],
|
||||||
|
setup(props,{emit}) {
|
||||||
|
const {t} = useI18n()
|
||||||
|
const store = useStore();
|
||||||
|
const palletData = reactive({
|
||||||
|
palletShow: true,
|
||||||
|
palletList:[],
|
||||||
|
color_:{} as any,
|
||||||
|
color:{} as any,
|
||||||
|
updataSelectColorTime:null as any,
|
||||||
|
gradient:{
|
||||||
|
gradientList:[
|
||||||
|
{
|
||||||
|
rgba:{
|
||||||
|
r:117,
|
||||||
|
g:119,
|
||||||
|
b:255,
|
||||||
|
a:1,
|
||||||
|
},
|
||||||
|
left:'0%'
|
||||||
|
},{
|
||||||
|
rgba:{
|
||||||
|
r:0,
|
||||||
|
g:222,
|
||||||
|
b:152,
|
||||||
|
a:1,
|
||||||
|
},
|
||||||
|
left:'100%'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
angle:45,
|
||||||
|
selectIndex:-1,
|
||||||
|
gradientShow:false,
|
||||||
|
},
|
||||||
|
setGradient:computed(()=>{
|
||||||
|
return (gradient:any)=>{
|
||||||
|
let gradientStr = ''
|
||||||
|
if(!gradient?.gradientList)return
|
||||||
|
gradient.gradientList.sort((a:any, b:any) => {
|
||||||
|
let aArr = a.left.split('%')[0]
|
||||||
|
let bArr = b.left.split('%')[0]
|
||||||
|
return aArr - bArr;
|
||||||
|
});
|
||||||
|
gradient.gradientList.forEach((item:any,index:any)=>{
|
||||||
|
let str = ','
|
||||||
|
if(gradient.gradientList.length == index+1)str = ''
|
||||||
|
let rgba = item.rgba?item.rgba:{r:255,g:255,b:255}
|
||||||
|
gradientStr += `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a}) ${item.left}${str}`
|
||||||
|
|
||||||
|
})
|
||||||
|
return `${gradientStr}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
const getpalletListDom = reactive({
|
||||||
|
})
|
||||||
|
const palletRef = ref(null)
|
||||||
|
watch(()=>palletData.color_,(newVal:any)=>{
|
||||||
|
if(!newVal?.rgba?.r)return
|
||||||
|
if(palletData.color?.gradient?.gradientShow){
|
||||||
|
palletData.color.gradient.gradientList[palletData.color.gradient.selectIndex].rgba = {
|
||||||
|
r:newVal.rgba.r,
|
||||||
|
g:newVal.rgba.g,
|
||||||
|
b:newVal.rgba.b,
|
||||||
|
a:newVal.rgba.a,
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
palletData.color = newVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(()=>palletData.color,(newVal:any)=>{
|
||||||
|
if(JSON.stringify(props.selectColor) != JSON.stringify(newVal)){
|
||||||
|
newVal.name = ''
|
||||||
|
newVal.tcx = ''
|
||||||
|
let rgba = [newVal.rgba.r,newVal.rgba.g,newVal.rgba.b]
|
||||||
|
let hex = rgbaToHex(rgba)
|
||||||
|
newVal.hex = hex
|
||||||
|
emit('selectUplpadColor',newVal)
|
||||||
|
}
|
||||||
|
},{deep: true })
|
||||||
|
const setOperate = ()=>{
|
||||||
|
if(!palletData.color.rgba)return message.info(t('DesignDetailAlter.jsContent7'))
|
||||||
|
palletData.color.rgba = palletData.color?.rgba?.r?palletData.color.rgba:{r:0,g:0,b:0,a:1}
|
||||||
|
palletData.gradient.selectIndex = 0
|
||||||
|
palletData.gradient.gradientShow = true
|
||||||
|
if(!palletData.color.gradient){
|
||||||
|
if(palletData.color.rgba.r){
|
||||||
|
palletData.gradient.gradientList[palletData.gradient.selectIndex].rgba = {
|
||||||
|
r:palletData.color.rgba.r,
|
||||||
|
g:palletData.color.rgba.g,
|
||||||
|
b:palletData.color.rgba.b,
|
||||||
|
a:1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
palletData.color.gradient = JSON.parse(JSON.stringify(palletData.gradient))
|
||||||
|
}else{
|
||||||
|
palletData.color.rgba = palletData.color.gradient.gradientList[0].rgba
|
||||||
|
palletData.color.gradient = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deleteGradientItem = ()=>{
|
||||||
|
if(palletData.color.gradient.gradientList.length <= 2)return
|
||||||
|
palletData.color.gradient.gradientList.splice(palletData.color.gradient.selectIndex,1)
|
||||||
|
}
|
||||||
|
const addGradient = (event:any)=>{
|
||||||
|
let gradientWidth = event.target.clientWidth
|
||||||
|
let left:any = event.offsetX/gradientWidth
|
||||||
|
palletData.color.gradient.gradientList.push({
|
||||||
|
rgba:palletData.color_.rgba,
|
||||||
|
left:left.toFixed(2)*100+'%'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mousedownGradientAngle = (event:any)=>{
|
||||||
|
// isMoible() true为移动端
|
||||||
|
let domPosition = event.target.getBoundingClientRect()
|
||||||
|
let position = {
|
||||||
|
x:domPosition.x+domPosition.width/2,
|
||||||
|
y:domPosition.y+domPosition.height/2,
|
||||||
|
}
|
||||||
|
let angle
|
||||||
|
let mousedown = function(event:any){
|
||||||
|
let e = getMousePosition(event,false)
|
||||||
|
mouseDownOperation(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchstart = function(event:any){
|
||||||
|
let e = getMousePosition(event,true)
|
||||||
|
mouseDownOperation(e)
|
||||||
|
}
|
||||||
|
let mouseDownOperation = (e:any)=>{
|
||||||
|
let X = position.x
|
||||||
|
let Y = position.y
|
||||||
|
let x = (e.clientX) - X
|
||||||
|
let y = Y -( e.clientY)
|
||||||
|
angle = Math.atan2(x,y)*(180 / Math.PI)
|
||||||
|
// this.colorList[this.selectIndex].gradient = JSON.parse(JSON.stringify(this.gradient))
|
||||||
|
palletData.color.gradient.angle = angle
|
||||||
|
|
||||||
|
}
|
||||||
|
let mouseupGradientAngle = ()=>{
|
||||||
|
window.removeEventListener('touchmove',touchstart)
|
||||||
|
window.removeEventListener('touchend',mouseupGradientAngle)
|
||||||
|
|
||||||
|
window.removeEventListener('mousemove',mousedown)
|
||||||
|
window.removeEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
}
|
||||||
|
window.addEventListener('touchmove',touchstart)
|
||||||
|
window.addEventListener('touchend',mouseupGradientAngle)
|
||||||
|
|
||||||
|
window.addEventListener('mousemove',mousedown)
|
||||||
|
window.addEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousedownGradient = (event:any,item:any,index:number)=>{
|
||||||
|
palletData.color.gradient.selectIndex = index
|
||||||
|
|
||||||
|
|
||||||
|
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
||||||
|
let gradientWidth = (palletRef.value.querySelector('.color_setting_operate_bg') as any).clientWidth
|
||||||
|
let position = {
|
||||||
|
x:event.clientX,
|
||||||
|
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
||||||
|
}
|
||||||
|
let mousedown = function(event:any){
|
||||||
|
let e = getMousePosition(event,false)
|
||||||
|
mousedownGradient(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchstart = function(event:any){
|
||||||
|
let e = getMousePosition(event,true)
|
||||||
|
mousedownGradient(e)
|
||||||
|
}
|
||||||
|
let mousedownGradient = (e:any)=>{
|
||||||
|
let left = ((e.clientX) - position.x)/gradientWidth*100+Number(position.left)
|
||||||
|
left = (left<0?0:left>100?100:left)
|
||||||
|
item.left = left+'%'
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouseupGradientAngle = ()=>{
|
||||||
|
window.removeEventListener('touchmove',touchstart)
|
||||||
|
window.removeEventListener('touchend',mouseupGradientAngle)
|
||||||
|
window.removeEventListener('mousemove',mousedown)
|
||||||
|
window.removeEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
}
|
||||||
|
window.addEventListener('touchmove',touchstart)
|
||||||
|
window.addEventListener('touchend',mouseupGradientAngle)
|
||||||
|
|
||||||
|
window.addEventListener('mousemove',mousedown)
|
||||||
|
window.addEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
const selectImgItem = ()=>{
|
||||||
|
|
||||||
|
}
|
||||||
|
const openPallet = ()=>{
|
||||||
|
if(palletData.palletShow && props.selectColor?.rgba?.r){
|
||||||
|
if(props.selectColor.gradient){
|
||||||
|
palletData.color_.rgba = props.selectColor.gradient.gradientList[0].rgba
|
||||||
|
}else{
|
||||||
|
palletData.color_ = JSON.parse(JSON.stringify(props.selectColor))
|
||||||
|
palletData.gradient.gradientShow = false
|
||||||
|
}
|
||||||
|
|
||||||
|
palletData.color = JSON.parse(JSON.stringify(props.selectColor))
|
||||||
|
}else{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 点击外部区域关闭颜色选择器
|
||||||
|
const handleClickOutside = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const colorSettingBlock = palletRef.value.querySelector('.color_setting_block');
|
||||||
|
const palletColo = palletRef.value.querySelector('.palletColo');
|
||||||
|
|
||||||
|
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
||||||
|
if (palletData.palletShow && colorSettingBlock &&
|
||||||
|
!colorSettingBlock.contains(target) &&
|
||||||
|
!palletColo?.contains(target)) {
|
||||||
|
palletData.palletShow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
// 添加点击外部区域监听器
|
||||||
|
// document.addEventListener('click', handleClickOutside);
|
||||||
|
|
||||||
|
nextTick().then(()=>{
|
||||||
|
const backIcon = document.createElement('div');
|
||||||
|
backIcon.classList.add('vc-sketch-color-wrap')
|
||||||
|
let dropperDom = palletRef.value.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
||||||
|
dropperDom.appendChild(backIcon);
|
||||||
|
backIcon.addEventListener('click',async ()=>{
|
||||||
|
try {
|
||||||
|
const dropper = new EyeDropper();
|
||||||
|
const result = await dropper.open();
|
||||||
|
let hex = result.sRGBHex.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);
|
||||||
|
palletData.color = {rgba:{r:r,g:g,b:b,a:1},hex:result.sRGBHex}
|
||||||
|
// return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
// box.style.backgroundColor = label.textContent = result.sRGBHex;
|
||||||
|
} catch (e) {
|
||||||
|
message.info(t('DesignDetailAlter.jsContent1'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
openPallet();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(()=>{
|
||||||
|
// 清理事件监听器
|
||||||
|
// document.removeEventListener('click', handleClickOutside);
|
||||||
|
})
|
||||||
|
return{
|
||||||
|
...toRefs(palletData),
|
||||||
|
...toRefs(getpalletListDom),
|
||||||
|
palletRef,
|
||||||
|
openPallet,
|
||||||
|
selectImgItem,
|
||||||
|
setOperate,
|
||||||
|
deleteGradientItem,
|
||||||
|
addGradient,
|
||||||
|
mousedownGradientAngle,
|
||||||
|
mousedownGradient,
|
||||||
|
getMousePosition,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.pallet{
|
||||||
|
// position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
> .palletColo{
|
||||||
|
width: 100%;
|
||||||
|
height: 7rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: .5rem .6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
> .palletBackColor{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .palletBox{
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
> .color_setting_block{
|
||||||
|
margin: auto;
|
||||||
|
background: linear-gradient(70deg, #eee4f3, #f3f4e6);
|
||||||
|
width: 100%;
|
||||||
|
// border-radius: calc(1rem*1.2);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 2px 2px 8px rgba(0,0,0,.3);
|
||||||
|
.vc-chrome{
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
box-shadow:none;
|
||||||
|
}
|
||||||
|
:deep(.chrome_color){
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.vc-chrome-saturation-wrap{
|
||||||
|
width: 30rem;
|
||||||
|
height: 30rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.vc-saturation-pointer{
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.vc-chrome-body{
|
||||||
|
padding: 0;
|
||||||
|
width: 90%;
|
||||||
|
margin: 2 auto;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
// display: none;
|
||||||
|
.vc-chrome-fields-wrap{
|
||||||
|
margin-top: 5%;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
.vc-chrome-toggle-btn{
|
||||||
|
width: calc(3.2rem*1.2);
|
||||||
|
.vc-chrome-toggle-icon{
|
||||||
|
height: auto;
|
||||||
|
margin-right: calc(-0.4rem*1.2);
|
||||||
|
margin-top: calc(0rem*1.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
svg{
|
||||||
|
width: calc(2.4rem*1.2) !important;
|
||||||
|
height: calc(2.4rem*1.2) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-fields{
|
||||||
|
.vc-chrome-field{
|
||||||
|
padding-left: calc(.6rem*1.2);
|
||||||
|
}
|
||||||
|
.vc-input__label{
|
||||||
|
font-size: calc(1.6rem*1.2);
|
||||||
|
}
|
||||||
|
.vc-input__input{
|
||||||
|
font-size: 2rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-upload-list{
|
||||||
|
|
||||||
|
}
|
||||||
|
.vc-sketch-color-wrap{
|
||||||
|
background-image: url(@/assets/images/homePage/dropper.png);
|
||||||
|
background-size: 3rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
padding: calc(.7rem*1.2);
|
||||||
|
border: 1px solid;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2rem;
|
||||||
|
right: 0rem;
|
||||||
|
border-radius: calc(.5rem*1.2);
|
||||||
|
|
||||||
|
}
|
||||||
|
.vc-chrome-fields{
|
||||||
|
.vc-input__label{
|
||||||
|
margin-top: calc(1rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-fields:nth-child(2){
|
||||||
|
>:last-of-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-fields:nth-child(3){
|
||||||
|
>:last-of-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-controls{
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
.vc-chrome-color-wrap{
|
||||||
|
// width: 3.6rem*1.2);
|
||||||
|
margin-left: calc(2rem*1.2);
|
||||||
|
width: auto;
|
||||||
|
.vc-chrome-active-color{
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.vc-chrome-active-color,.vc-checkerboard{
|
||||||
|
width: calc(3rem*1.2);
|
||||||
|
height: calc(3rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-hue-wrap,.vc-chrome-alpha-wrap{
|
||||||
|
.vc-hue{
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.vc-alpha{
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
height: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
.vc-hue-pointer{
|
||||||
|
transform: translateX(-1.25rem);
|
||||||
|
}
|
||||||
|
.vc-hue-picker{
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(0px,-3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-alpha-wrap{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-saturation-wrap .vc-saturation-circle{
|
||||||
|
width: calc(1rem*1.2);
|
||||||
|
height: calc(1rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_block{
|
||||||
|
// margin-top: calc(1rem;
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
// font-size: calc(1.6rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 5%;
|
||||||
|
padding-bottom: 5%;
|
||||||
|
margin: calc(0.5rem*1.2) auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.color_right{
|
||||||
|
width: 13rem;
|
||||||
|
font-size: calc(1.2rem*1.2);
|
||||||
|
color: #666666;
|
||||||
|
.color_rgb_block{
|
||||||
|
display: flex;
|
||||||
|
.rgb_item{
|
||||||
|
margin-left: calc(.2rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_left{
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgb(153, 153, 153);
|
||||||
|
}
|
||||||
|
.color_right,.color_left{
|
||||||
|
>div{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.color_HEX_block,.color_rgb_block{
|
||||||
|
padding: .25rem .6rem;
|
||||||
|
box-shadow: inset 0 0 0 1px #ccc;
|
||||||
|
border-radius: .5rem;
|
||||||
|
justify-content: space-around;
|
||||||
|
text-transform:uppercase;
|
||||||
|
.color_block_bg{
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
// margin-right: .5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_block_bg{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.color_setting_operateSingle{
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
.color_setting_btn{
|
||||||
|
margin: 0 1rem;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
&.active{
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate{
|
||||||
|
*{
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.color_setting_operate_item{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
.operate_item_box{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_control{
|
||||||
|
.operate_item_delete,.operate_item_angle{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.operate_item_delete{
|
||||||
|
i{
|
||||||
|
display: flex;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.operate_item_angle{
|
||||||
|
.operate_item_angle_box{
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
border: solid 2px #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
>div{
|
||||||
|
height: 100%;
|
||||||
|
width: 1rem;
|
||||||
|
position: relative;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
>div::before{
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
top: 0.2rem;
|
||||||
|
left: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_input{
|
||||||
|
width: 80%;
|
||||||
|
// padding: 0 10%;
|
||||||
|
margin: 1.2rem 10%;
|
||||||
|
border-radius: 10%;
|
||||||
|
position: relative;
|
||||||
|
height: 2.5rem;
|
||||||
|
.color_setting_operate_bg{
|
||||||
|
border-radius: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: #fff;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_btn{
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
left: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 110%;
|
||||||
|
border: .2rem solid;
|
||||||
|
border-radius: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: content-box;
|
||||||
|
z-index: 2;
|
||||||
|
&.active{
|
||||||
|
border: .3rem solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_btn:hover{
|
||||||
|
border: .3rem solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,744 +0,0 @@
|
|||||||
<template>
|
|
||||||
<transition name="fade">
|
|
||||||
<div
|
|
||||||
class="select-menu-panel"
|
|
||||||
v-if="visible"
|
|
||||||
:class="{ active: !closePanel }"
|
|
||||||
>
|
|
||||||
<div class="btn" @click="setClosePanel">
|
|
||||||
<i class="fi fi-br-angle-left"></i>
|
|
||||||
</div>
|
|
||||||
<!-- 变换工具顶部 -->
|
|
||||||
<div class="panel-select">
|
|
||||||
<!-- <div class="panel-header">
|
|
||||||
<div class="header-title">变换工具</div>
|
|
||||||
</div> -->
|
|
||||||
<!-- 分割线 -->
|
|
||||||
<!-- <div class="panel-divider"></div> -->
|
|
||||||
<!-- 变换工具内容 -->
|
|
||||||
<div class="tool-content">
|
|
||||||
<div
|
|
||||||
class="object-item"
|
|
||||||
v-for="v in activeObjects"
|
|
||||||
:key="v.id"
|
|
||||||
>
|
|
||||||
<div class="title">{{ v.layer?.name }}</div>
|
|
||||||
<div class="list">
|
|
||||||
<div>
|
|
||||||
<span class="label">W</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="v.width"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="label">H</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="v.height"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- <div>
|
|
||||||
<span class="label">X</span>
|
|
||||||
<input type="number" :value="v.left" disabled />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="label">Y</span>
|
|
||||||
<input type="number" :value="v.top" disabled />
|
|
||||||
</div> -->
|
|
||||||
<div>
|
|
||||||
<span class="label iconfont icon-angle"></span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="Number(Number(v.angle).toFixed(3))"
|
|
||||||
@change="(e) => changeAngle(e, v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="btn" @click="clickflipHorizontal(v)">
|
|
||||||
<i class="iconfont icon-flip-horizontal"></i>
|
|
||||||
<p class="tip">
|
|
||||||
{{ t("Canvas.flipHorizontal") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="btn" @click="clickflipVertical(v)">
|
|
||||||
<i class="iconfont icon-flip-vertical"></i>
|
|
||||||
<p class="tip">
|
|
||||||
{{ t("Canvas.flipVertical") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="btn" @click="clickCropImage(v)">
|
|
||||||
<i class="iconfont icon-caijian"></i>
|
|
||||||
<p class="tip">
|
|
||||||
{{ t("Canvas.cropAndAdd") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import showViewVideo from "@/tool/mount";
|
|
||||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { ToolCommand } from "../commands/ToolCommands";
|
|
||||||
import { OperationType } from "../utils/layerHelper";
|
|
||||||
import { loadImageUrlToLayer } from "../utils/imageHelper";
|
|
||||||
import { TransformCommand } from "../commands/StateCommands";
|
|
||||||
const props = defineProps({
|
|
||||||
canvas: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
commandManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
selectManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
layerManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
toolManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
activeTool: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const visible = ref(false);
|
|
||||||
//打开隐藏操作面板
|
|
||||||
const closePanel = ref(false);
|
|
||||||
const setClosePanel = () => {
|
|
||||||
closePanel.value = !closePanel.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 国际化
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setupCanvasListeners();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
removeCanvasListeners();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听 activeTool 变化
|
|
||||||
watch(
|
|
||||||
() => props.activeTool,
|
|
||||||
(newTool) => {
|
|
||||||
if (newTool === OperationType.SELECT) {
|
|
||||||
show();
|
|
||||||
} else {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示面板
|
|
||||||
*/
|
|
||||||
function show() {
|
|
||||||
if (activeObjects.value.length === 0) return;
|
|
||||||
visible.value = true;
|
|
||||||
closePanel.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭面板
|
|
||||||
*/
|
|
||||||
function close() {
|
|
||||||
visible.value = false;
|
|
||||||
}
|
|
||||||
// 获取当前选中的对象
|
|
||||||
const activeObjects = ref([]);
|
|
||||||
const getActiveObject = (e) => {
|
|
||||||
console.log("==========切换激活对象", e);
|
|
||||||
activeObjects.value = e.selected.map((v) => v);
|
|
||||||
activeObjects.value.forEach((v) => {
|
|
||||||
v.layer = props.layerManager.getLayerById(v.layerId);
|
|
||||||
});
|
|
||||||
if (activeObjects.value.length === 0) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
show(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const lastSelectLayerId = inject("lastSelectLayerId");
|
|
||||||
const layers = inject("layers");
|
|
||||||
const transformObject = (activeObj, initialState, finalState) => {
|
|
||||||
const transformCmd = new TransformCommand({
|
|
||||||
canvas: props.canvas,
|
|
||||||
objectId: activeObj.id,
|
|
||||||
initialState,
|
|
||||||
finalState,
|
|
||||||
objectType: activeObj.type,
|
|
||||||
name: `变换 ${activeObj.type || "对象"}`,
|
|
||||||
layerManager: props.layerManager,
|
|
||||||
layers: layers,
|
|
||||||
lastSelectLayerId: lastSelectLayerId,
|
|
||||||
});
|
|
||||||
props.layerManager.commandManager.execute(transformCmd, {
|
|
||||||
name: "对象修改",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据左上角坐标计算旋转后的新坐标
|
|
||||||
* @param {number} W - 宽度
|
|
||||||
* @param {number} H - 高度
|
|
||||||
* @param {number} currentX - 当前左上角x坐标
|
|
||||||
* @param {number} currentY - 当前左上角y坐标
|
|
||||||
* @param {number} currentAngleDeg - 当前角度(度)
|
|
||||||
* @param {number} newAngleDeg - 新角度(度)
|
|
||||||
* @returns {Object} 旋转后的左上角坐标 {x, y}
|
|
||||||
*/
|
|
||||||
function calculateRotatedTopLeftDeg(
|
|
||||||
W,
|
|
||||||
H,
|
|
||||||
currentX,
|
|
||||||
currentY,
|
|
||||||
currentAngleDeg,
|
|
||||||
newAngleDeg
|
|
||||||
) {
|
|
||||||
const currentAngle = (currentAngleDeg * Math.PI) / 180;
|
|
||||||
const newAngle = (newAngleDeg * Math.PI) / 180;
|
|
||||||
// 1. 用当前角度计算中心点位置
|
|
||||||
const cosCurrent = Math.cos(currentAngle);
|
|
||||||
const sinCurrent = Math.sin(currentAngle);
|
|
||||||
const Cx = currentX + (W / 2) * cosCurrent - (H / 2) * sinCurrent;
|
|
||||||
const Cy = currentY + (W / 2) * sinCurrent + (H / 2) * cosCurrent;
|
|
||||||
|
|
||||||
// 2. 用新角度计算旋转后的左上角位置
|
|
||||||
const cosNew = Math.cos(newAngle);
|
|
||||||
const sinNew = Math.sin(newAngle);
|
|
||||||
const newX = Cx + (-W / 2) * cosNew - (-H / 2) * sinNew;
|
|
||||||
const newY = Cy + (-W / 2) * sinNew + (-H / 2) * cosNew;
|
|
||||||
|
|
||||||
return { x: newX, y: newY };
|
|
||||||
}
|
|
||||||
// 改变角度
|
|
||||||
const changeAngle = (e, obj) => {
|
|
||||||
const initialState = TransformCommand.captureTransformState(obj);
|
|
||||||
const finalState = { ...initialState };
|
|
||||||
const angle = e.target.value;
|
|
||||||
if (obj.originX === "left" && obj.originY === "top") {
|
|
||||||
const width = obj.width * obj.scaleX;
|
|
||||||
const height = obj.height * obj.scaleY;
|
|
||||||
const left = obj.left;
|
|
||||||
const top = obj.top;
|
|
||||||
const { x, y } = calculateRotatedTopLeftDeg(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
obj.angle,
|
|
||||||
angle
|
|
||||||
);
|
|
||||||
finalState.left = x;
|
|
||||||
finalState.top = y;
|
|
||||||
}
|
|
||||||
finalState.angle = angle;
|
|
||||||
transformObject(obj, initialState, finalState);
|
|
||||||
};
|
|
||||||
// 水平翻转
|
|
||||||
const clickflipHorizontal = (obj) => {
|
|
||||||
const initialState = TransformCommand.captureTransformState(obj);
|
|
||||||
const finalState = { ...initialState };
|
|
||||||
finalState.flipX = !finalState.flipX;
|
|
||||||
transformObject(obj, initialState, finalState);
|
|
||||||
};
|
|
||||||
// 垂直翻转
|
|
||||||
const clickflipVertical = (obj) => {
|
|
||||||
const initialState = TransformCommand.captureTransformState(obj);
|
|
||||||
const finalState = { ...initialState };
|
|
||||||
finalState.flipY = !finalState.flipY;
|
|
||||||
transformObject(obj, initialState, finalState);
|
|
||||||
};
|
|
||||||
// 裁剪图片
|
|
||||||
const cropImage = inject("cropImage");
|
|
||||||
const clickCropImage = async (obj) => {
|
|
||||||
const base64 = await props.layerManager.getLayerToBase64(obj.layerId);
|
|
||||||
if(base64) cropImage(base64).then((res) => {
|
|
||||||
loadImageUrlToLayer({
|
|
||||||
imageUrl: res,
|
|
||||||
layerManager: props.layerManager,
|
|
||||||
canvas: props.canvas,
|
|
||||||
toolManager: props.toolManager,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateActiveObjects = (arrs, keys) => {
|
|
||||||
arrs.forEach((v) => {
|
|
||||||
activeObjects.value.forEach((item) => {
|
|
||||||
if (item.id === v.id) {
|
|
||||||
keys.forEach((key) => (item[key] = v[key]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
activeObjects.value = [...activeObjects.value];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const objectRotatingChange = (e) => {
|
|
||||||
const arrs = [];
|
|
||||||
if (e.target._objects) {
|
|
||||||
e.target._objects.forEach((v) => arrs.push(v));
|
|
||||||
} else {
|
|
||||||
arrs.push(e.target);
|
|
||||||
}
|
|
||||||
updateActiveObjects(arrs, ["angle"]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置画布事件监听
|
|
||||||
*/
|
|
||||||
function setupCanvasListeners() {
|
|
||||||
if (!props.canvas) return;
|
|
||||||
// 鼠标事件
|
|
||||||
props.canvas.on("selection:created", getActiveObject);
|
|
||||||
props.canvas.on("selection:updated", getActiveObject);
|
|
||||||
props.canvas.on("selection:cleared", close);
|
|
||||||
props.canvas.on("object:rotating", objectRotatingChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除画布事件监听
|
|
||||||
*/
|
|
||||||
function removeCanvasListeners() {
|
|
||||||
if (!props.canvas) return;
|
|
||||||
|
|
||||||
// 移除鼠标事件
|
|
||||||
props.canvas.off("selection:created", getActiveObject);
|
|
||||||
props.canvas.off("selection:updated", getActiveObject);
|
|
||||||
props.canvas.off("selection:cleared", close);
|
|
||||||
props.canvas.off("object:rotating", objectRotatingChange);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.select-menu-panel {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 22px;
|
|
||||||
left: 20px;
|
|
||||||
right: 20px;
|
|
||||||
max-width: min(90vw, 640px);
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
z-index: 1000;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
user-select: none;
|
|
||||||
&.active {
|
|
||||||
transform: translateY(100%);
|
|
||||||
> .btn {
|
|
||||||
> i {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
transform: rotate(270deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板和手机适配 */
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.select-menu-panel {
|
|
||||||
bottom: 15px;
|
|
||||||
left: 15px;
|
|
||||||
right: 15px;
|
|
||||||
max-width: calc(100vw - 30px);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
.select-menu-panel {
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
right: 10px;
|
|
||||||
max-width: calc(100vw - 20px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-menu-panel.is-active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-select {
|
|
||||||
// padding: 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板适配 */
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.panel-header {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机适配 */
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
.panel-header {
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn span {
|
|
||||||
margin-top: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn svg {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn.active {
|
|
||||||
background-color: #007aff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-divider {
|
|
||||||
height: 1px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
margin: 0 10px 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-content {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 220px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 0 10px;
|
|
||||||
> .object-item {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 10px 0;
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
> .title {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
> .list {
|
|
||||||
display: flex;
|
|
||||||
> div {
|
|
||||||
margin-right: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #474747;
|
|
||||||
> .label {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
> input {
|
|
||||||
width: 65px;
|
|
||||||
}
|
|
||||||
.iconfont {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> div.btn {
|
|
||||||
position: relative;
|
|
||||||
min-width: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
> .tip {
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -100%);
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
color: white;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
margin-left: 0.8rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.08);
|
|
||||||
> .tip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板适配 - 每行4个按钮 */
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.tool-content {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px 6px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机适配 - 每行3个按钮 */
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
.tool-content {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 6px 4px;
|
|
||||||
padding: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-btn {
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
min-width: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
display: flex;
|
|
||||||
// flex-direction: column;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
gap: 4px;
|
|
||||||
.c-svg {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn svg {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框样式 */
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
-webkit-backdrop-filter: blur(5px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-container {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 280px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-dialog-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #666;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-control {
|
|
||||||
flex: 1;
|
|
||||||
height: 4px;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 2px;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-control::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #007aff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather-value {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn,
|
|
||||||
.confirm-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
background-color: #007aff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-picker {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s, transform 0.3s;
|
|
||||||
}
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repeat-setting">
|
||||||
|
<div class="repeat-setting-item">
|
||||||
|
<span class="label">{{ t("Canvas.angle") }}</span>
|
||||||
|
<angle-tool
|
||||||
|
:angle="angle"
|
||||||
|
@input="(e) => emit('inputFillAngle', e)"
|
||||||
|
@change="(e) => emit('changeFillAngle', e)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="repeat-setting-item">
|
||||||
|
<span class="label">{{ t("Canvas.scale") }}</span>
|
||||||
|
<slider
|
||||||
|
:min="1"
|
||||||
|
:max="500"
|
||||||
|
:step="1"
|
||||||
|
is-input
|
||||||
|
:tipFormatter="(v) => `${scale}%`"
|
||||||
|
:value="scale"
|
||||||
|
@input="inputFillScale"
|
||||||
|
@change="changeFillScale"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="repeat-setting-item">
|
||||||
|
<span class="label">Gap X</span>
|
||||||
|
<slider
|
||||||
|
:min="0"
|
||||||
|
:max="1000"
|
||||||
|
:step="1"
|
||||||
|
is-input
|
||||||
|
:tipFormatter="(v) => `${v}px`"
|
||||||
|
:value="gapX"
|
||||||
|
@input="(e) => emit('inputFill_Gap', e, gapY)"
|
||||||
|
@change="(e) => emit('changeFill_Gap', e, gapY)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="repeat-setting-item">
|
||||||
|
<span class="label">Gap Y</span>
|
||||||
|
<slider
|
||||||
|
:min="0"
|
||||||
|
:max="1000"
|
||||||
|
:step="1"
|
||||||
|
is-input
|
||||||
|
:tipFormatter="(v) => `${v}px`"
|
||||||
|
:value="gapY"
|
||||||
|
@input="(e) => emit('inputFill_Gap', gapX, e)"
|
||||||
|
@change="(e) => emit('changeFill_Gap', gapX, e)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div class="repeat-setting-item">
|
||||||
|
<span class="label">{{ t("Canvas.offset") }}</span>
|
||||||
|
<offset-tool
|
||||||
|
:top="(props.object.fill?.offsetY / props.object.height) * 100"
|
||||||
|
:left="(props.object.fill?.offsetX / props.object.width) * 100"
|
||||||
|
@input="(e) => emit('inputFillOffset', e)"
|
||||||
|
@change="(e) => emit('changeFillOffset', e)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, defineEmits, computed } from "vue";
|
||||||
|
import { getTransformScaleAngle } from "../../utils/helper";
|
||||||
|
import AngleTool from "../tools/AngleTool.vue";
|
||||||
|
import OffsetTool from "../tools/OffsetTool.vue";
|
||||||
|
import Slider from "../tools/Slider.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
object: {
|
||||||
|
required: true,
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const angle = computed(
|
||||||
|
() => getTransformScaleAngle(props.object.fill?.patternTransform).angle
|
||||||
|
);
|
||||||
|
const scale = computed(() => {
|
||||||
|
const patternTransform = props.object.fill?.patternTransform;
|
||||||
|
const scaleValue = getTransformScaleAngle(patternTransform).scale * 100;
|
||||||
|
return Number(Number(scaleValue).toFixed(2));
|
||||||
|
});
|
||||||
|
const gapX = computed(() => props.object.fill_?.gapX || 0);
|
||||||
|
const gapY = computed(() => props.object.fill_?.gapY || 0);
|
||||||
|
const emit = defineEmits([
|
||||||
|
"inputFillAngle",
|
||||||
|
"changeFillAngle",
|
||||||
|
"inputFillOffset",
|
||||||
|
"changeFillOffset",
|
||||||
|
"inputFillScale",
|
||||||
|
"changeFillScale",
|
||||||
|
"inputFill_Gap",
|
||||||
|
"changeFill_Gap",
|
||||||
|
]);
|
||||||
|
const inputFillScale = (e) => {
|
||||||
|
const scale = e / 100;
|
||||||
|
emit("inputFillScale", scale);
|
||||||
|
};
|
||||||
|
const changeFillScale = (e) => {
|
||||||
|
const scale = e / 100;
|
||||||
|
emit("changeFillScale", scale);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.repeat-setting {
|
||||||
|
user-select: none;
|
||||||
|
> .repeat-setting-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
//虚线
|
||||||
|
> .label {
|
||||||
|
min-width: 50px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
> .angle-tool {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> p {
|
||||||
|
margin: 10px 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 1px dashed #e5e5e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import i18n from "@/lang/index.ts";
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
/** 填充重复模式 */
|
||||||
|
export const getSelectOptions = () => ref([
|
||||||
|
{ value: "no-repeat", label: t("Canvas.noRepeat") },
|
||||||
|
{ value: "repeat", label: t("Canvas.repeat") },
|
||||||
|
{ value: "repeat-x", label: t("Canvas.repeatX") },
|
||||||
|
{ value: "repeat-y", label: t("Canvas.repeatY") },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 图层混合模式 */
|
||||||
|
export const getLayerCompositeOptions = () => ref([
|
||||||
|
{ value: "source-over", label: t("Canvas.CompositeNormal"), tip: t("Canvas.CompositeNormalTip") },// 正常
|
||||||
|
{ value: "darken", label: t("Canvas.CompositeDarken"), tip: t("Canvas.CompositeDarkenTip") },// 变暗
|
||||||
|
{ value: "multiply", label: t("Canvas.CompositeMultiply"), tip: t("Canvas.CompositeMultiplyTip") },// 正片叠底
|
||||||
|
{ value: "color-burn", label: t("Canvas.CompositeColorBurn"), tip: t("Canvas.CompositeColorBurnTip") },// 颜色加深
|
||||||
|
|
||||||
|
{ value: "lighten", label: t("Canvas.CompositeLighten"), tip: t("Canvas.CompositeLightenTip") },// 颜色减淡
|
||||||
|
{ value: "screen", label: t("Canvas.CompositeScreen"), tip: t("Canvas.CompositeScreenTip") },// 滤色
|
||||||
|
{ value: "color-dodge", label: t("Canvas.CompositeColorDodge"), tip: t("Canvas.CompositeColorDodgeTip") },// 颜色减淡
|
||||||
|
{ value: "lighter", label: t("Canvas.CompositeLighter"), tip: t("Canvas.CompositeLighterTip") },// 颜色减淡
|
||||||
|
|
||||||
|
{ value: "overlay", label: t("Canvas.CompositeOverlay"), tip: t("Canvas.CompositeOverlayTip") },// 叠加
|
||||||
|
{ value: "soft-light", label: t("Canvas.CompositeSoftLight"), tip: t("Canvas.CompositeSoftLightTip") },// 柔光
|
||||||
|
{ value: "hard-light", label: t("Canvas.CompositeHardLight"), tip: t("Canvas.CompositeHardLightTip") },// 强光
|
||||||
|
|
||||||
|
{ value: "difference", label: t("Canvas.CompositeDifference"), tip: t("Canvas.CompositeDifferenceTip") },// 差值
|
||||||
|
{ value: "exclusion", label: t("Canvas.CompositeExclusion"), tip: t("Canvas.CompositeExclusionTip") },// 排除
|
||||||
|
|
||||||
|
{ value: "hue", label: t("Canvas.CompositeHue"), tip: t("Canvas.CompositeHueTip") },// 色相
|
||||||
|
{ value: "saturation", label: t("Canvas.CompositeSaturation"), tip: t("Canvas.CompositeSaturationTip") },// 饱和度
|
||||||
|
{ value: "color", label: t("Canvas.CompositeColor"), tip: t("Canvas.CompositeColorTip") },// 颜色
|
||||||
|
{ value: "luminosity", label: t("Canvas.CompositeLuminosity"), tip: t("Canvas.CompositeLuminosityTip") },// 亮度
|
||||||
|
|
||||||
|
// { value: "destination-over", label: "背后", tip:"背后:新图形绘制到原内容下方" },
|
||||||
|
// { value: "source-in", label: "颜色加深", tip:"颜色加深:只显示重叠部分,其他透明" },
|
||||||
|
// { value: "destination-in", label: "颜色减淡", tip:"颜色减淡:只显示原内容与新图形重叠部分" },
|
||||||
|
// { value: "source-out", label: "排除", tip:"排除:只显示新图形中不重叠部分" },
|
||||||
|
// { value: "destination-out", label: "差值", tip:"差值:只清除原内容中与新图形重叠部分" },
|
||||||
|
// { value: "xor", label: "排除", tip:"排除:重叠部分透明" },
|
||||||
|
// { value: "copy", label: "正常", tip:"正常:完全忽略原内容,只显示新图形" },
|
||||||
|
// { value: "source-atop", label: "叠加", tip:"叠加:只在与现有内容重叠处绘制新图形" },
|
||||||
|
// { value: "destination-atop", label: "柔光", tip:"柔光:仅保留重叠部分,新图形在原内容后绘制" },
|
||||||
|
// { value: "darker", label: "变暗", tip:"变暗:重叠部分颜色减淡" },
|
||||||
|
]);
|
||||||
@@ -0,0 +1,899 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
class="select-menu-panel"
|
||||||
|
v-if="visible"
|
||||||
|
:class="{ active: !closePanel }"
|
||||||
|
>
|
||||||
|
<div class="btn" @click="setClosePanel">
|
||||||
|
<i class="fi fi-br-angle-left"></i>
|
||||||
|
</div>
|
||||||
|
<!-- 变换工具顶部 -->
|
||||||
|
<div class="panel-select">
|
||||||
|
<!-- <div class="panel-header">
|
||||||
|
<div class="header-title">变换工具</div>
|
||||||
|
</div> -->
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<!-- <div class="panel-divider"></div> -->
|
||||||
|
<!-- 变换工具内容 -->
|
||||||
|
<div class="tool-content">
|
||||||
|
<div
|
||||||
|
class="object-item"
|
||||||
|
v-for="v in activeObjects"
|
||||||
|
:key="v.id"
|
||||||
|
>
|
||||||
|
<div class="title">{{ v.layer?.name }}</div>
|
||||||
|
<div class="list">
|
||||||
|
<div
|
||||||
|
class="input"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<angle-tool
|
||||||
|
:angle="Number(Number(v.angle).toFixed(3))"
|
||||||
|
@input="(e) => inputAngle(e, v)"
|
||||||
|
@change="(e) => changeAngle(e, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<span class="label"
|
||||||
|
>{{ t("Canvas.opacity") }}:</span
|
||||||
|
>
|
||||||
|
<slider
|
||||||
|
:tipFormatter="
|
||||||
|
(v) => `${Math.round(v * 100)}%`
|
||||||
|
"
|
||||||
|
:value="v.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
@change="(e) => changeOpacity(e, v)"
|
||||||
|
@input="(e) => inputOpacity(e, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="btn"
|
||||||
|
@click="clickflipHorizontal(v)"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-flip-horizontal"></i>
|
||||||
|
<p class="tip">
|
||||||
|
{{ t("Canvas.flipHorizontal") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="btn"
|
||||||
|
@click="clickflipVertical(v)"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-flip-vertical"></i>
|
||||||
|
<p class="tip">
|
||||||
|
{{ t("Canvas.flipVertical") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
class="btn"
|
||||||
|
@click="clickCropImage(v)"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-caijian"></i>
|
||||||
|
<p class="tip">
|
||||||
|
{{ t("Canvas.cropAndAdd") }}
|
||||||
|
</p>
|
||||||
|
</div> -->
|
||||||
|
<!-- <div
|
||||||
|
class="btn"
|
||||||
|
@click="clickRasterizeLayer(v)"
|
||||||
|
v-if="v.type !== 'image'"
|
||||||
|
>
|
||||||
|
<span class="label">{{ t("Canvas.RasterizedLayer") }}</span>
|
||||||
|
</div> -->
|
||||||
|
<div class="select">
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<i class="iconfont icon-hunhemoshi"></i>
|
||||||
|
<my-select
|
||||||
|
:defaultValue="
|
||||||
|
v.layer?.blendMode ||
|
||||||
|
v.globalCompositeOperation
|
||||||
|
"
|
||||||
|
:list="layerCompositeOptions"
|
||||||
|
@change="
|
||||||
|
(n, o) => setLayerComposite(n, o, v, 1)
|
||||||
|
"
|
||||||
|
@active="
|
||||||
|
(n, o) => setLayerComposite(n, o, v, 0)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
class="btn"
|
||||||
|
@click="clickTest(v)"
|
||||||
|
>
|
||||||
|
<span class="label">测试</span>
|
||||||
|
</div> -->
|
||||||
|
<div
|
||||||
|
class="select"
|
||||||
|
v-if="v.type === 'rect' || v.type === 'image'"
|
||||||
|
>
|
||||||
|
<!-- 平铺 -->
|
||||||
|
<i class="iconfont icon-repeat"></i>
|
||||||
|
<a-select
|
||||||
|
size="small"
|
||||||
|
:defaultValue="
|
||||||
|
typeof v.fill === 'object'
|
||||||
|
? v.fill?.repeat || 'no-repeat'
|
||||||
|
: 'no-repeat'
|
||||||
|
"
|
||||||
|
:options="selectOptions"
|
||||||
|
@change="(e) => changeFillRepeat(e, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 平铺设置 -->
|
||||||
|
<a-popover
|
||||||
|
v-if="v.type === 'rect'"
|
||||||
|
trigger="click"
|
||||||
|
destroyTooltipOnHide
|
||||||
|
:title="t('Canvas.repeatSetting')"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<repeat-setting
|
||||||
|
:object="v"
|
||||||
|
@inputFillAngle="
|
||||||
|
(e) => inputFillAngle(e, v)
|
||||||
|
"
|
||||||
|
@changeFillAngle="
|
||||||
|
(e) => changeFillAngle(e, v)
|
||||||
|
"
|
||||||
|
@inputFillOffset="
|
||||||
|
(e) => inputFillOffset(e, v)
|
||||||
|
"
|
||||||
|
@changeFillOffset="
|
||||||
|
(e) => changeFillOffset(e, v)
|
||||||
|
"
|
||||||
|
@inputFillScale="
|
||||||
|
(e) => inputFillScale(e, v)
|
||||||
|
"
|
||||||
|
@changeFillScale="
|
||||||
|
(e) => changeFillScale(e, v)
|
||||||
|
"
|
||||||
|
@inputFill_Gap="
|
||||||
|
(x, y) => inputFill_Gap(x, y, v)
|
||||||
|
"
|
||||||
|
@changeFill_Gap="
|
||||||
|
(x, y) => changeFill_Gap(x, y, v)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="btn">
|
||||||
|
<i class="iconfont icon-gengduo"></i>
|
||||||
|
</div>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, onUnmounted, reactive } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
const { t } = useI18n();
|
||||||
|
import { OperationType, SpecialLayerId } from "../../utils/layerHelper";
|
||||||
|
import { loadImageUrlToLayer } from "../../utils/imageHelper";
|
||||||
|
import {
|
||||||
|
calculateRotatedTopLeftDeg,
|
||||||
|
createPatternTransform,
|
||||||
|
getTransformScaleAngle,
|
||||||
|
} from "../../utils/helper";
|
||||||
|
import { TransformCommand } from "../../commands/StateCommands";
|
||||||
|
import {
|
||||||
|
FillRepeatCommand,
|
||||||
|
FillRepeatChangeCommand,
|
||||||
|
FillRepeatGapChangeCommand,
|
||||||
|
} from "../../commands/FillRepeatCommand";
|
||||||
|
import { SetLayerCompositeCommand } from "../../commands/LayerCommands.js";
|
||||||
|
import RepeatSetting from "./RepeatSetting.vue";
|
||||||
|
import Slider from "../tools/Slider.vue";
|
||||||
|
import AngleTool from "../tools/AngleTool.vue";
|
||||||
|
import MySelect from "../tools/MySelect.vue";
|
||||||
|
import EventManager from "../../utils/event.js";
|
||||||
|
import { getSelectOptions, getLayerCompositeOptions } from "./data.js";
|
||||||
|
const selectOptions = getSelectOptions();
|
||||||
|
const layerCompositeOptions = getLayerCompositeOptions();
|
||||||
|
const props = defineProps({
|
||||||
|
canvas: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
commandManager: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
selectManager: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
layerManager: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
canvasManager: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
toolManager: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
activeTool: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 响应式数据
|
||||||
|
const visible = ref(false);
|
||||||
|
//打开隐藏操作面板
|
||||||
|
const closePanel = ref(false);
|
||||||
|
const setClosePanel = () => {
|
||||||
|
closePanel.value = !closePanel.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupCanvasListeners();
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeCanvasListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 activeTool 变化
|
||||||
|
watch(
|
||||||
|
() => props.activeTool,
|
||||||
|
(newTool) => {
|
||||||
|
if (newTool === OperationType.SELECT) {
|
||||||
|
show();
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示面板
|
||||||
|
*/
|
||||||
|
function show() {
|
||||||
|
if (activeObjects.length === 0) return;
|
||||||
|
visible.value = true;
|
||||||
|
closePanel.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭面板
|
||||||
|
*/
|
||||||
|
function close() {
|
||||||
|
visible.value = false;
|
||||||
|
}
|
||||||
|
// 获取当前选中的对象
|
||||||
|
const activeObjects = reactive([]);
|
||||||
|
const getActiveObject = (e) => {
|
||||||
|
console.log("==========切换激活对象", e, activeObjects);
|
||||||
|
activeObjects.splice(0, activeObjects.length, ...e.selected);
|
||||||
|
activeObjects.forEach((v) => {
|
||||||
|
v.layer = props.layerManager.getLayerById(v.layerId);
|
||||||
|
});
|
||||||
|
if (activeObjects.length === 0) {
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//取消当前选中
|
||||||
|
const cancelSelect = () => {
|
||||||
|
activeObjects.splice(0, activeObjects.length);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
const lastSelectLayerId = inject("lastSelectLayerId");
|
||||||
|
const layers = inject("layers");
|
||||||
|
const transformObject = (
|
||||||
|
activeObj,
|
||||||
|
initialState,
|
||||||
|
finalState,
|
||||||
|
isCommand = true
|
||||||
|
) => {
|
||||||
|
const cmd = new TransformCommand({
|
||||||
|
canvas: props.canvas,
|
||||||
|
objectId: activeObj.id,
|
||||||
|
initialState,
|
||||||
|
finalState,
|
||||||
|
objectType: activeObj.type,
|
||||||
|
name: `变换 ${activeObj.type || "对象"}`,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
layers: layers,
|
||||||
|
lastSelectLayerId: lastSelectLayerId,
|
||||||
|
});
|
||||||
|
if (isCommand) {
|
||||||
|
props.commandManager.execute(cmd);
|
||||||
|
} else {
|
||||||
|
cmd.execute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 改变不透明度
|
||||||
|
const changeOpacity = (opacity, obj) => {
|
||||||
|
props.layerManager?.setLayerOpacity(obj.layerId, opacity);
|
||||||
|
};
|
||||||
|
const inputOpacity = (opacity, obj) => {
|
||||||
|
obj.opacity = opacity;
|
||||||
|
props.canvas.renderAll();
|
||||||
|
};
|
||||||
|
// 改变角度
|
||||||
|
const inputAngle = (angle, obj) => {
|
||||||
|
const initialState = TransformCommand.captureTransformState(obj);
|
||||||
|
const finalState = computeAngleState(angle, obj, initialState);
|
||||||
|
transformObject(obj, initialState, finalState, false);
|
||||||
|
if (!obj.hasOwnProperty("oldState")) obj.oldState = initialState;
|
||||||
|
};
|
||||||
|
const changeAngle = (angle, obj) => {
|
||||||
|
var initialState;
|
||||||
|
if (obj.hasOwnProperty("oldState")) {
|
||||||
|
initialState = obj.oldState;
|
||||||
|
delete obj.oldState;
|
||||||
|
} else {
|
||||||
|
initialState = TransformCommand.captureTransformState(obj);
|
||||||
|
}
|
||||||
|
const finalState = computeAngleState(angle, obj, initialState);
|
||||||
|
transformObject(obj, initialState, finalState);
|
||||||
|
};
|
||||||
|
const computeAngleState = (angle, obj, initialState) => {
|
||||||
|
const finalState = { ...initialState };
|
||||||
|
if (obj.originX === "left" && obj.originY === "top") {
|
||||||
|
const width = obj.width * obj.scaleX;
|
||||||
|
const height = obj.height * obj.scaleY;
|
||||||
|
const left = obj.left;
|
||||||
|
const top = obj.top;
|
||||||
|
const { x, y } = calculateRotatedTopLeftDeg(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
obj.angle,
|
||||||
|
angle
|
||||||
|
);
|
||||||
|
finalState.left = x;
|
||||||
|
finalState.top = y;
|
||||||
|
}
|
||||||
|
finalState.angle = angle;
|
||||||
|
return finalState;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 水平翻转
|
||||||
|
const clickflipHorizontal = (obj) => {
|
||||||
|
const initialState = TransformCommand.captureTransformState(obj);
|
||||||
|
const finalState = { ...initialState };
|
||||||
|
finalState.flipX = !finalState.flipX;
|
||||||
|
transformObject(obj, initialState, finalState);
|
||||||
|
};
|
||||||
|
// 垂直翻转
|
||||||
|
const clickflipVertical = (obj) => {
|
||||||
|
const initialState = TransformCommand.captureTransformState(obj);
|
||||||
|
const finalState = { ...initialState };
|
||||||
|
finalState.flipY = !finalState.flipY;
|
||||||
|
transformObject(obj, initialState, finalState);
|
||||||
|
};
|
||||||
|
// 裁剪图片
|
||||||
|
const cropImage = inject("cropImage");
|
||||||
|
const clickCropImage = async (obj) => {
|
||||||
|
const base64 = await props.layerManager.getLayerToBase64(obj.layerId);
|
||||||
|
if (base64)
|
||||||
|
cropImage(base64).then((res) => {
|
||||||
|
loadImageUrlToLayer({
|
||||||
|
imageUrl: res,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
canvas: props.canvas,
|
||||||
|
toolManager: props.toolManager,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 栅格化图层
|
||||||
|
const clickRasterizeLayer = (obj) => {
|
||||||
|
props.layerManager.rasterizeLayer(obj.layerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 改变填充重复
|
||||||
|
const changeFillRepeat = async (value, obj) => {
|
||||||
|
console.log("==========改变填充重复", obj.type);
|
||||||
|
const cmd = new FillRepeatCommand({
|
||||||
|
canvas: props.canvas,
|
||||||
|
layers: layers,
|
||||||
|
canvasManager: props.canvasManager,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
layerId: obj.layerId,
|
||||||
|
fillRepeat: value,
|
||||||
|
});
|
||||||
|
props.commandManager.execute(cmd);
|
||||||
|
};
|
||||||
|
// 改变填充角度
|
||||||
|
const inputFillAngle = (angle, obj) => {
|
||||||
|
if (!obj.oldPattern) obj.oldPattern = obj.get("fill");
|
||||||
|
const fill = obj.get("fill");
|
||||||
|
const scale = getTransformScaleAngle(fill?.patternTransform).scale;
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
...fill,
|
||||||
|
patternTransform: createPatternTransform(scale, angle),
|
||||||
|
});
|
||||||
|
obj.set("fill", pattern);
|
||||||
|
props.canvas.renderAll();
|
||||||
|
};
|
||||||
|
const changeFillAngle = (angle, obj) => {
|
||||||
|
const fill = obj.get("fill");
|
||||||
|
const scale = getTransformScaleAngle(fill?.patternTransform).scale;
|
||||||
|
const pattern = {
|
||||||
|
patternTransform: createPatternTransform(scale, angle),
|
||||||
|
};
|
||||||
|
changeFill(obj, pattern);
|
||||||
|
};
|
||||||
|
// 改变填充便宜
|
||||||
|
const inputFillOffset = (value, obj) => {
|
||||||
|
if (!obj.oldPattern) obj.oldPattern = obj.get("fill");
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
...obj.get("fill"),
|
||||||
|
offsetX: (value.left / 100) * obj.width,
|
||||||
|
offsetY: (value.top / 100) * obj.height,
|
||||||
|
});
|
||||||
|
obj.set("fill", pattern);
|
||||||
|
props.canvas.renderAll();
|
||||||
|
};
|
||||||
|
const changeFillOffset = (value, obj) => {
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
offsetX: (value.left / 100) * obj.width,
|
||||||
|
offsetY: (value.top / 100) * obj.height,
|
||||||
|
});
|
||||||
|
changeFill(obj, pattern);
|
||||||
|
};
|
||||||
|
// 改变填充缩放
|
||||||
|
const inputFillScale = (scale, obj) => {
|
||||||
|
if (!obj.oldPattern) obj.oldPattern = obj.get("fill");
|
||||||
|
const fill = obj.get("fill");
|
||||||
|
const angle = getTransformScaleAngle(fill?.patternTransform).angle;
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
...fill,
|
||||||
|
patternTransform: createPatternTransform(scale, angle),
|
||||||
|
});
|
||||||
|
obj.set("fill", pattern);
|
||||||
|
props.canvas.renderAll();
|
||||||
|
};
|
||||||
|
const changeFillScale = (scale, obj) => {
|
||||||
|
const fill = obj.get("fill");
|
||||||
|
const angle = getTransformScaleAngle(fill?.patternTransform).angle;
|
||||||
|
const pattern = {
|
||||||
|
patternTransform: createPatternTransform(scale, angle),
|
||||||
|
};
|
||||||
|
changeFill(obj, pattern);
|
||||||
|
};
|
||||||
|
const changeFill = (obj, pattern) => {
|
||||||
|
const cmd = new FillRepeatChangeCommand({
|
||||||
|
canvas: props.canvas,
|
||||||
|
layers: layers,
|
||||||
|
canvasManager: props.canvasManager,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
layerId: obj.layerId,
|
||||||
|
newPattern: pattern,
|
||||||
|
});
|
||||||
|
props.commandManager.execute(cmd);
|
||||||
|
};
|
||||||
|
// 改变填充间隙
|
||||||
|
const inputFill_Gap = (gapX, gapY, obj) => {
|
||||||
|
const cmd = new FillRepeatGapChangeCommand({
|
||||||
|
canvas: props.canvas,
|
||||||
|
layers: layers,
|
||||||
|
canvasManager: props.canvasManager,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
layerId: obj.layerId,
|
||||||
|
newGapX: gapX,
|
||||||
|
newGapY: gapY,
|
||||||
|
record: true,
|
||||||
|
});
|
||||||
|
cmd.execute();
|
||||||
|
};
|
||||||
|
const changeFill_Gap = (gapX, gapY, obj) => {
|
||||||
|
if (obj.oldFill_) {
|
||||||
|
obj.fill_ = { ...obj.oldFill_ };
|
||||||
|
delete obj.oldFill_;
|
||||||
|
}
|
||||||
|
const cmd = new FillRepeatGapChangeCommand({
|
||||||
|
canvas: props.canvas,
|
||||||
|
layers: layers,
|
||||||
|
canvasManager: props.canvasManager,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
layerId: obj.layerId,
|
||||||
|
newGapX: gapX,
|
||||||
|
newGapY: gapY,
|
||||||
|
});
|
||||||
|
props.commandManager.execute(cmd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLayerComposite = (newValue, oldValue, obj, isCmd) => {
|
||||||
|
const cmd = new SetLayerCompositeCommand({
|
||||||
|
canvas: props.canvas,
|
||||||
|
layers: layers,
|
||||||
|
layerManager: props.layerManager,
|
||||||
|
layerId: obj.layerId,
|
||||||
|
newValue: newValue,
|
||||||
|
oldValue: oldValue,
|
||||||
|
});
|
||||||
|
if (isCmd) {
|
||||||
|
props.commandManager.execute(cmd);
|
||||||
|
} else {
|
||||||
|
cmd.execute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickTest = (obj) => {
|
||||||
|
console.log("==========点击测试", obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新选中对象属性
|
||||||
|
const updateActiveObjects = (arrs, keys, isNumber = true) => {
|
||||||
|
arrs.forEach((v) => {
|
||||||
|
activeObjects.forEach((item) => {
|
||||||
|
if (item.id === v.id) {
|
||||||
|
keys.forEach(
|
||||||
|
(key) => (item[key] = isNumber ? Number(v[key]) : v[key])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 旋转对象时更新角度
|
||||||
|
const objectRotatingChange = (e) => {
|
||||||
|
const arrs = [];
|
||||||
|
if (e.target._objects) {
|
||||||
|
e.target._objects.forEach((v) => arrs.push(v));
|
||||||
|
} else {
|
||||||
|
arrs.push(e.target);
|
||||||
|
}
|
||||||
|
updateActiveObjects(arrs, ["angle"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 对象属性修改后触发
|
||||||
|
const objectModifiedChange = (e) => {
|
||||||
|
console.log("==========object:modified", e.target);
|
||||||
|
};
|
||||||
|
// 不透明度撤销时触发
|
||||||
|
const objectOpacityUndo = (layerId, opacity) => {
|
||||||
|
const layerObjects = props.canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.layerId === layerId);
|
||||||
|
updateActiveObjects(layerObjects, ["opacity"]);
|
||||||
|
};
|
||||||
|
// 对象属性修改撤销时触发
|
||||||
|
const objectModifiedUndo = (object) => {
|
||||||
|
updateActiveObjects([object], ["angle"]);
|
||||||
|
};
|
||||||
|
// 组合操作撤销时触发
|
||||||
|
const objectCompositeChange = (object) => {
|
||||||
|
updateActiveObjects([object], ["globalCompositeOperation"], false);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 设置画布事件监听
|
||||||
|
*/
|
||||||
|
function setupCanvasListeners() {
|
||||||
|
if (!props.canvas) return;
|
||||||
|
// 注册事件
|
||||||
|
props.canvas.on("selection:created", getActiveObject);
|
||||||
|
props.canvas.on("selection:updated", getActiveObject);
|
||||||
|
props.canvas.on("selection:cleared", cancelSelect);
|
||||||
|
props.canvas.on("object:rotating", objectRotatingChange);
|
||||||
|
props.canvas.on("object:modified", objectModifiedChange);
|
||||||
|
EventManager.on("object:opacity:execute", objectOpacityUndo);
|
||||||
|
EventManager.on("object:opacity:undo", objectOpacityUndo);
|
||||||
|
EventManager.on("object:modified:execute", objectModifiedUndo);
|
||||||
|
EventManager.on("object:modified:undo", objectModifiedUndo);
|
||||||
|
EventManager.on("object:composite:execute", objectCompositeChange);
|
||||||
|
EventManager.on("object:composite:undo", objectCompositeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除画布事件监听
|
||||||
|
*/
|
||||||
|
function removeCanvasListeners() {
|
||||||
|
if (!props.canvas) return;
|
||||||
|
// 移除事件
|
||||||
|
props.canvas.off("selection:created", getActiveObject);
|
||||||
|
props.canvas.off("selection:updated", getActiveObject);
|
||||||
|
props.canvas.off("selection:cleared", cancelSelect);
|
||||||
|
props.canvas.off("object:rotating", objectRotatingChange);
|
||||||
|
props.canvas.off("object:modified", objectModifiedChange);
|
||||||
|
EventManager.off("object:opacity:execute", objectOpacityUndo);
|
||||||
|
EventManager.off("object:opacity:undo", objectOpacityUndo);
|
||||||
|
EventManager.off("object:modified:execute", objectModifiedUndo);
|
||||||
|
EventManager.off("object:modified:undo", objectModifiedUndo);
|
||||||
|
EventManager.off("object:composite:execute", objectCompositeChange);
|
||||||
|
EventManager.off("object:composite:undo", objectCompositeChange);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.select-menu-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 22px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
// max-width: min(90vw, 640px);
|
||||||
|
max-width: 95%;
|
||||||
|
width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
user-select: none;
|
||||||
|
&.active {
|
||||||
|
transform: translateY(100%);
|
||||||
|
> .btn {
|
||||||
|
> i {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板和手机适配 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.select-menu-panel {
|
||||||
|
bottom: 15px;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
max-width: calc(100vw - 30px);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.select-menu-panel {
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-menu-panel.is-active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-select {
|
||||||
|
// padding: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板适配 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.panel-header {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机适配 */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.panel-header {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn span {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn.active {
|
||||||
|
background-color: #007aff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
margin: 0 10px 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 20rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
> .object-item {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 1rem 0;
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
> .title {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
> .list {
|
||||||
|
display: flex;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
> .iconfont {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
> .label {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
> .angle-tool {
|
||||||
|
width: 9rem;
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
// margin-left: 0.8rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
> .tip {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> div.input {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #474747;
|
||||||
|
> .label {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
> .iconfont {
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
> .slider {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> div.select {
|
||||||
|
> .iconfont {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
> .my-select,
|
||||||
|
> .ant-select {
|
||||||
|
width: 12rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> div.btn {
|
||||||
|
min-width: 2.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> div.color {
|
||||||
|
width: 4rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background-image: linear-gradient(to bottom, #ff0000, #ffff00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板适配 - 每行4个按钮 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.tool-content {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机适配 - 每行3个按钮 */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.tool-content {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px 4px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -412,8 +412,12 @@ const handleToolClick = (tool) => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
.tools-list::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.red-green-mode {
|
.red-green-mode {
|
||||||
background-color: #fff4f4;
|
background-color: #060505;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-indicator {
|
.mode-indicator {
|
||||||
|
|||||||
@@ -270,6 +270,13 @@
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
.layer-color-btn {
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
.layer-actions {
|
.layer-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
|
|||||||
121
src/component/Canvas/CanvasEditor/components/tools/AngleTool.vue
Normal file
121
src/component/Canvas/CanvasEditor/components/tools/AngleTool.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<div class="angle-tool">
|
||||||
|
<div
|
||||||
|
ref="dishRef"
|
||||||
|
class="dish"
|
||||||
|
@mousedown.stop="mousedown"
|
||||||
|
@touchmove.stop="mousedown"
|
||||||
|
>
|
||||||
|
<div class="pointer" :style="{ transform: `rotate(${angle}deg)` }">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<input type="number" v-model="angle" @input="onInput" @change="onChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
import { calculateAngle } from "../../utils/helper";
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
angle: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["change", "input"]);
|
||||||
|
const angle = ref(props.angle);
|
||||||
|
watch(() => props.angle, (value) => {
|
||||||
|
angle.value = value;
|
||||||
|
});
|
||||||
|
const dishRef = ref<HTMLDivElement>();
|
||||||
|
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||||
|
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!dishRef.value) return;
|
||||||
|
const { left, top, width, height } =
|
||||||
|
dishRef.value.getBoundingClientRect();
|
||||||
|
const centerX = left + width / 2;
|
||||||
|
const centerY = top + height / 2;
|
||||||
|
const { clientX, clientY } = e?.touches?.[0] || e;
|
||||||
|
angle.value = calculateAngle(centerX, centerY, clientX, clientY, true);
|
||||||
|
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 = () => emit("input", angle.value);
|
||||||
|
var changeTime: any = null;
|
||||||
|
const onChange = () => {
|
||||||
|
clearTimeout(changeTime);
|
||||||
|
changeTime = setTimeout(() => emit("change", angle.value), 500);
|
||||||
|
};
|
||||||
|
// var angleTime = null;
|
||||||
|
// watch(angle, (value) => {
|
||||||
|
// emit("input", value);
|
||||||
|
// clearTimeout(angleTime);
|
||||||
|
// angleTime = setTimeout(() => emit("change", value), 50);
|
||||||
|
// });
|
||||||
|
// defineExpose({
|
||||||
|
// open,
|
||||||
|
// close,
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.angle-tool {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
> .dish {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
> .pointer {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
> span {
|
||||||
|
position: absolute;
|
||||||
|
top: 10%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
width: 35%;
|
||||||
|
height: 35%;
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .input {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
flex: 1;
|
||||||
|
// min-width: 45px;
|
||||||
|
// max-width: 80px;
|
||||||
|
// width: 50px;
|
||||||
|
> input {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
class="my-select"
|
||||||
|
:size="size"
|
||||||
|
@change="change"
|
||||||
|
:defaultValue="defaultValue"
|
||||||
|
@dropdownVisibleChange="dropdownVisibleChange"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="v in list"
|
||||||
|
:key="v.value"
|
||||||
|
:value="v.value"
|
||||||
|
:title="v.tip"
|
||||||
|
@mouseover.stop.prevent="mouseover(v)"
|
||||||
|
@mouseleave="mouseleave(v)"
|
||||||
|
>{{ v.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
defaultValue: {
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "small",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["change", "active"]);
|
||||||
|
const isChange = ref(false);
|
||||||
|
const initValue = ref(props.defaultValue);
|
||||||
|
const activeValue = ref(props.defaultValue);
|
||||||
|
const timeout = ref(null);
|
||||||
|
const mouseover = (v) => {
|
||||||
|
clearTimeout(timeout.value);
|
||||||
|
if (v.value === activeValue.value) return;
|
||||||
|
emit("active", v.value, activeValue.value);
|
||||||
|
activeValue.value = v.value;
|
||||||
|
};
|
||||||
|
const mouseleave = () => {
|
||||||
|
clearTimeout(timeout.value);
|
||||||
|
timeout.value = setTimeout(() => {
|
||||||
|
dropdownVisibleChange(false);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
const change = (v) => {
|
||||||
|
isChange.value = true;
|
||||||
|
emit("change", v, initValue.value);
|
||||||
|
};
|
||||||
|
const dropdownVisibleChange = (v) => {
|
||||||
|
if (v) {
|
||||||
|
isChange.value = false;
|
||||||
|
initValue.value = props.defaultValue;
|
||||||
|
} else if (!isChange.value) {
|
||||||
|
emit("active", initValue.value, activeValue.value);
|
||||||
|
activeValue.value = initValue.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<div class="offset-tool">
|
||||||
|
<div
|
||||||
|
class="dish"
|
||||||
|
@mousedown="mousedown"
|
||||||
|
@touchstart="mousedown"
|
||||||
|
ref="dishRef"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:style="{ top: data.top + '%', left: data.left + '%' }"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="top"
|
||||||
|
type="range"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:step="0.1"
|
||||||
|
v-model="data.top"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="left"
|
||||||
|
type="range"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:step="0.1"
|
||||||
|
v-model="data.left"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
<span class="tip"
|
||||||
|
>x:{{ tofix(data.left) }}% y:{{ tofix(data.top) }}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
top: {
|
||||||
|
type: Number,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
type: Number,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const tofix = (v: number | string) => Number(Number(v).toFixed(1));
|
||||||
|
const emit = defineEmits(["change", "input"]);
|
||||||
|
const data = reactive({
|
||||||
|
top: tofix(props.top),
|
||||||
|
left: tofix(props.left),
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.top,
|
||||||
|
(v) => (data.top = tofix(v))
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.left,
|
||||||
|
(v) => (data.left = tofix(v))
|
||||||
|
);
|
||||||
|
const dishRef = ref<HTMLDivElement>();
|
||||||
|
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!dishRef.value) return;
|
||||||
|
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!dishRef.value) return;
|
||||||
|
const { left, top, width, height } =
|
||||||
|
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 - left) / width) * 100;
|
||||||
|
var y = ((Y - top) / height) * 100;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
if (x > 100) x = 100;
|
||||||
|
if (y < 0) y = 0;
|
||||||
|
if (y > 100) y = 100;
|
||||||
|
data.left = tofix(x);
|
||||||
|
data.top = tofix(y);
|
||||||
|
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 = () => emit("input", { ...data });
|
||||||
|
var changeTime: any = null;
|
||||||
|
const onChange = () => {
|
||||||
|
clearTimeout(changeTime);
|
||||||
|
changeTime = setTimeout(() => emit("change", { ...data }), 500);
|
||||||
|
};
|
||||||
|
// var offsetTime = null;
|
||||||
|
// watch(data, (v) => {
|
||||||
|
// const obj = { ...v };
|
||||||
|
// emit("input", obj);
|
||||||
|
// clearTimeout(offsetTime);
|
||||||
|
// offsetTime = setTimeout(() => emit("change", obj), 50);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// defineExpose({
|
||||||
|
// open,
|
||||||
|
// close,
|
||||||
|
// });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.offset-tool {
|
||||||
|
width: 125px;
|
||||||
|
height: 125px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
--gap: 15px;
|
||||||
|
> .dish {
|
||||||
|
margin: var(--gap) 0 0 var(--gap);
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff;
|
||||||
|
> span {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0%;
|
||||||
|
left: 0%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
> input.left {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
> input.top {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform-origin: left bottom;
|
||||||
|
transform: rotate(90deg) translateX(-100%);
|
||||||
|
}
|
||||||
|
> input {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - var(--gap));
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||||||
|
// outline: none;
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4285f4; /* 蓝色滑块 */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
&::-webkit-slider-thumb:hover {
|
||||||
|
background: #3b77db;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
src/component/Canvas/CanvasEditor/components/tools/Slider.vue
Normal file
160
src/component/Canvas/CanvasEditor/components/tools/Slider.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<div class="slider">
|
||||||
|
<div class="input-range">
|
||||||
|
<span
|
||||||
|
class="tip"
|
||||||
|
:style="{
|
||||||
|
'--progress': (value - props.min) / (props.max - props.min),
|
||||||
|
}"
|
||||||
|
>{{ props.tipFormatter(value) }}</span
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model="value"
|
||||||
|
:min="props.min"
|
||||||
|
:max="props.max"
|
||||||
|
:step="props.step"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input" v-show="isInput">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="value"
|
||||||
|
:min="props.min"
|
||||||
|
:max="props.max"
|
||||||
|
:step="props.step"
|
||||||
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
tipFormatter: {
|
||||||
|
type: Function,
|
||||||
|
default: (v) => v,
|
||||||
|
},
|
||||||
|
isInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["change", "input"]);
|
||||||
|
const value = ref(props.value);
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(v) => (value.value = v)
|
||||||
|
);
|
||||||
|
const onInput = () => emit("input", Number(value.value));
|
||||||
|
var changeTime: any = null;
|
||||||
|
const onChange = () => {
|
||||||
|
clearTimeout(changeTime);
|
||||||
|
changeTime = setTimeout(() => emit("change", Number(value.value)), 500);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.slider {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
--input-thumb-size: 12px;
|
||||||
|
width: 150px;
|
||||||
|
// &:focus-within,
|
||||||
|
&:hover {
|
||||||
|
> .input-range > .tip {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .input-range {
|
||||||
|
position: relative;
|
||||||
|
flex: 2;
|
||||||
|
> input {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||||||
|
outline: none;
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: var(--input-thumb-size);
|
||||||
|
height: var(--input-thumb-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4285f4; /* 蓝色滑块 */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
&::-webkit-slider-thumb:hover {
|
||||||
|
background: #3b77db;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
color: #666;
|
||||||
|
top: 0;
|
||||||
|
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.4rem 0.8rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
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: 10px;
|
||||||
|
> input {
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,7 +35,8 @@ import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
|
|||||||
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
||||||
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
||||||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||||||
import SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件
|
import PalletPanel from "./components/PalletPanel/index.vue";
|
||||||
|
import SelectMenuPanel from "./components/SelectMenuPanel/index.vue"; // 引入选择工具菜单组件
|
||||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||||
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
||||||
import { ToolManager } from "./managers/ToolManager.js";
|
import { ToolManager } from "./managers/ToolManager.js";
|
||||||
@@ -64,6 +65,10 @@ const props = defineProps({
|
|||||||
type: [Object, String],
|
type: [Object, String],
|
||||||
default: "", // 默认空
|
default: "", // 默认空
|
||||||
},
|
},
|
||||||
|
otherData: {
|
||||||
|
type: [Object, null],
|
||||||
|
default: null, // 默认空对象
|
||||||
|
},
|
||||||
config: {
|
config: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => CanvasConfig, // 默认配置
|
default: () => CanvasConfig, // 默认配置
|
||||||
@@ -78,7 +83,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
clothingImageUrl: {
|
clothingImageUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "", // 衣服底图URL
|
default: "", // 衣服底图URL-线稿
|
||||||
|
},
|
||||||
|
clothingImageUrl2: {
|
||||||
|
type: String,
|
||||||
|
default: "", // 衣服底图URL-上色
|
||||||
},
|
},
|
||||||
redGreenImageUrl: {
|
redGreenImageUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -250,6 +259,7 @@ onMounted(async () => {
|
|||||||
canvasColor,
|
canvasColor,
|
||||||
enabledRedGreenMode: props.enabledRedGreenMode,
|
enabledRedGreenMode: props.enabledRedGreenMode,
|
||||||
isFixedErasable: props.isFixedErasable,
|
isFixedErasable: props.isFixedErasable,
|
||||||
|
props,
|
||||||
});
|
});
|
||||||
canvasManager.canvas.activeLayerId = activeLayerId;
|
canvasManager.canvas.activeLayerId = activeLayerId;
|
||||||
canvasManager.activeLayerId = activeLayerId;
|
canvasManager.activeLayerId = activeLayerId;
|
||||||
@@ -307,6 +317,7 @@ onMounted(async () => {
|
|||||||
canvas: canvasManager.canvas,
|
canvas: canvasManager.canvas,
|
||||||
commandManager,
|
commandManager,
|
||||||
layerManager,
|
layerManager,
|
||||||
|
canvasManager,
|
||||||
toolManager,
|
toolManager,
|
||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
pasteText: (text) => {
|
pasteText: (text) => {
|
||||||
@@ -435,6 +446,12 @@ onMounted(async () => {
|
|||||||
canvasManager.canvas.width,
|
canvasManager.canvas.width,
|
||||||
canvasManager.canvas.height
|
canvasManager.canvas.height
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(props.otherData && !props.otherData.canvasId) {
|
||||||
|
await canvasManager?.createOtherLayers(props.otherData);
|
||||||
|
await layerManager?.layerSort?.rearrangeObjects();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 设置固定图层是否可擦除
|
// // 设置固定图层是否可擦除
|
||||||
@@ -720,42 +737,7 @@ function deleteFun() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeLayer(layerId) {
|
function removeLayer(layerId) {
|
||||||
// Check if this is the last layer - prevent deletion
|
|
||||||
var isChild = false;
|
|
||||||
var parentLength = 0;
|
|
||||||
layers.value.forEach((layer) => {
|
|
||||||
if(layer.children.some(v => v.id == layerId)){
|
|
||||||
isChild = true;
|
|
||||||
parentLength = layer.children.length;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if(isChild && parentLength == 1 || layers.value.length <= 3){
|
|
||||||
console.warn(
|
|
||||||
"Cannot delete the last layer. At least one layer must remain."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
layerManager.removeLayer(layerId);
|
layerManager.removeLayer(layerId);
|
||||||
// 此处删除画布上内容导致撤回操作无效(多余)
|
|
||||||
// if (canvasManager && canvasManager.canvas) {
|
|
||||||
// const layerToRemove = layers.value.find((l) => l.id === layerId);
|
|
||||||
// if (layerToRemove) {
|
|
||||||
// const elementIds = layerToRemove?.fabricObjects?.map((e) => e.id);
|
|
||||||
// elementIds.forEach((elementId) => {
|
|
||||||
// const objectToRemove = canvasManager.canvas
|
|
||||||
// .getObjects()
|
|
||||||
// .find((obj) => obj.id === elementId);
|
|
||||||
// if (objectToRemove) {
|
|
||||||
// canvasManager.canvas.remove(objectToRemove);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// if (activeLayerId.value === layerId) {
|
|
||||||
// activeElementId.value = null;
|
|
||||||
// }
|
|
||||||
// canvasManager.canvas.renderAll();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerImageUpload() {
|
function triggerImageUpload() {
|
||||||
@@ -902,13 +884,17 @@ const changeCanvas = async (command) => {
|
|||||||
...command, // 传递完整的命令数据
|
...command, // 传递完整的命令数据
|
||||||
};
|
};
|
||||||
emit("changeCanvas", commandData);
|
emit("changeCanvas", commandData);
|
||||||
if (command.canUndo || command.canRedo) {
|
if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const imageData = await canvasManager.exportImage({
|
try {
|
||||||
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
|
const imageData = await canvasManager.exportImage({
|
||||||
isCropByBg: true,
|
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
|
||||||
});
|
isCropByBg: true,
|
||||||
emit("trigger-red-green-mouseup", imageData);
|
});
|
||||||
|
emit("trigger-red-green-mouseup", imageData);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -918,6 +904,14 @@ const cropImage = (url) => {
|
|||||||
return cropImageRef.value.open(url)
|
return cropImageRef.value.open(url)
|
||||||
};
|
};
|
||||||
provide("cropImage", cropImage); // 提供给子组件使用
|
provide("cropImage", cropImage); // 提供给子组件使用
|
||||||
|
// 颜色选择器组件
|
||||||
|
const palletPanelRef = ref(null);
|
||||||
|
const palletPanel = (url) => {
|
||||||
|
return palletPanelRef.value.open(url)
|
||||||
|
};
|
||||||
|
provide("palletPanel", palletPanel); // 提供给子组件使用
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 处理画布容器的拖放事件
|
// 处理画布容器的拖放事件
|
||||||
const isDragOver = ref(false);
|
const isDragOver = ref(false);
|
||||||
@@ -1030,6 +1024,10 @@ defineExpose({
|
|||||||
isEnhanceImg,
|
isEnhanceImg,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 导出颜色图层
|
||||||
|
exportColorLayer: () => {
|
||||||
|
return canvasManager.exportColorLayer();
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 移动图层位置
|
* 移动图层位置
|
||||||
* @param {string} layerId 图层ID
|
* @param {string} layerId 图层ID
|
||||||
@@ -1245,6 +1243,7 @@ defineExpose({
|
|||||||
:commandManager="commandManager"
|
:commandManager="commandManager"
|
||||||
:selectionManager="selectionManager"
|
:selectionManager="selectionManager"
|
||||||
:layerManager="layerManager"
|
:layerManager="layerManager"
|
||||||
|
:canvasManager="canvasManager"
|
||||||
:toolManager="toolManager"
|
:toolManager="toolManager"
|
||||||
:activeTool="activeTool"
|
:activeTool="activeTool"
|
||||||
/>
|
/>
|
||||||
@@ -1269,6 +1268,7 @@ defineExpose({
|
|||||||
?
|
?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图层面板组件 -->
|
<!-- 图层面板组件 -->
|
||||||
@@ -1298,9 +1298,11 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<!-- 裁剪图片组件 -->
|
|
||||||
<CropImage ref="cropImageRef" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 裁剪图片组件 -->
|
||||||
|
<CropImage ref="cropImageRef" />
|
||||||
|
<!-- 颜色选择器组件 -->
|
||||||
|
<PalletPanel ref="palletPanelRef" />
|
||||||
|
|
||||||
<!-- <div class="footer-actions">
|
<!-- <div class="footer-actions">
|
||||||
<button class="share-btn">Share</button>
|
<button class="share-btn">Share</button>
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import {
|
|||||||
isGroupLayer,
|
isGroupLayer,
|
||||||
OperationType,
|
OperationType,
|
||||||
OperationTypes,
|
OperationTypes,
|
||||||
|
findLayer,
|
||||||
|
createLayer,
|
||||||
|
LayerType,
|
||||||
|
SpecialLayerId,
|
||||||
} from "../utils/layerHelper";
|
} from "../utils/layerHelper";
|
||||||
|
import { ObjectMoveCommand } from "../commands/ObjectCommands";
|
||||||
import { AnimationManager } from "./animation/AnimationManager";
|
import { AnimationManager } from "./animation/AnimationManager";
|
||||||
import { createCanvas } from "../utils/canvasFactory";
|
import { createCanvas } from "../utils/canvasFactory";
|
||||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||||
@@ -21,6 +26,10 @@ import {
|
|||||||
findObjectById,
|
findObjectById,
|
||||||
generateId,
|
generateId,
|
||||||
optimizeCanvasRendering,
|
optimizeCanvasRendering,
|
||||||
|
palletToFill,
|
||||||
|
fillToCssStyle,
|
||||||
|
calculateRotatedTopLeftDeg,
|
||||||
|
createPatternTransform,
|
||||||
} from "../utils/helper";
|
} from "../utils/helper";
|
||||||
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
||||||
import { isFunction } from "lodash-es";
|
import { isFunction } from "lodash-es";
|
||||||
@@ -30,6 +39,11 @@ import {
|
|||||||
validateLayerAssociations,
|
validateLayerAssociations,
|
||||||
} from "../utils/layerUtils";
|
} from "../utils/layerUtils";
|
||||||
import { imageModeHandler } from "../utils/imageHelper";
|
import { imageModeHandler } from "../utils/imageHelper";
|
||||||
|
import { getObjectAlphaToCanvas } from "../utils/objectHelper";
|
||||||
|
import { AddLayerCommand } from "../commands/LayerCommands";
|
||||||
|
import { fa, id } from "element-plus/es/locales.mjs";
|
||||||
|
import i18n from "@/lang/index.ts";
|
||||||
|
const {t} = i18n.global;
|
||||||
|
|
||||||
export class CanvasManager {
|
export class CanvasManager {
|
||||||
constructor(canvasElement, options) {
|
constructor(canvasElement, options) {
|
||||||
@@ -50,6 +64,7 @@ export class CanvasManager {
|
|||||||
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
|
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
|
||||||
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
||||||
this.handleCanvasInit = null; // 画布初始化回调函数
|
this.handleCanvasInit = null; // 画布初始化回调函数
|
||||||
|
this.props = options.props || {};
|
||||||
// 初始化画布
|
// 初始化画布
|
||||||
this.initializeCanvas();
|
this.initializeCanvas();
|
||||||
}
|
}
|
||||||
@@ -83,10 +98,10 @@ export class CanvasManager {
|
|||||||
|
|
||||||
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
|
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
|
||||||
|
|
||||||
// // 设置画布辅助线
|
// 设置画布辅助线
|
||||||
// initAligningGuidelines(this.canvas);
|
initAligningGuidelines(this.canvas);
|
||||||
|
|
||||||
// // 设置画布中心线
|
// 设置画布中心线
|
||||||
// initCenteringGuidelines(this.canvas);
|
// initCenteringGuidelines(this.canvas);
|
||||||
|
|
||||||
// 初始化画布事件监听器
|
// 初始化画布事件监听器
|
||||||
@@ -431,7 +446,7 @@ export class CanvasManager {
|
|||||||
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
||||||
* 这样可以保持对象间的相对位置关系不变
|
* 这样可以保持对象间的相对位置关系不变
|
||||||
*/
|
*/
|
||||||
centerAllObjects() {
|
async centerAllObjects() {
|
||||||
if (!this.canvas) return;
|
if (!this.canvas) return;
|
||||||
|
|
||||||
// 获取所有可见对象(不是背景元素的对象)
|
// 获取所有可见对象(不是背景元素的对象)
|
||||||
@@ -448,8 +463,8 @@ export class CanvasManager {
|
|||||||
// 获取背景对象
|
// 获取背景对象
|
||||||
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
||||||
|
|
||||||
!this.canvas?.clipPath &&
|
// !this.canvas?.clipPath &&
|
||||||
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
// this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
this.canvas?.clipPath?.set?.({
|
this.canvas?.clipPath?.set?.({
|
||||||
left: this.width / 2,
|
left: this.width / 2,
|
||||||
@@ -496,7 +511,6 @@ export class CanvasManager {
|
|||||||
// 计算背景层的偏移量
|
// 计算背景层的偏移量
|
||||||
const deltaX = backgroundObject.left - backgroundOldLeft;
|
const deltaX = backgroundObject.left - backgroundOldLeft;
|
||||||
const deltaY = backgroundObject.top - backgroundOldTop;
|
const deltaY = backgroundObject.top - backgroundOldTop;
|
||||||
|
|
||||||
// 将相同的偏移量应用到所有其他对象上
|
// 将相同的偏移量应用到所有其他对象上
|
||||||
const otherObjects = visibleObjects.filter(
|
const otherObjects = visibleObjects.filter(
|
||||||
(obj) => obj !== backgroundObject
|
(obj) => obj !== backgroundObject
|
||||||
@@ -549,8 +563,21 @@ export class CanvasManager {
|
|||||||
this.updateMaskPosition(backgroundObject);
|
this.updateMaskPosition(backgroundObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新颜色层信息
|
||||||
|
const fixedLayerObj = this.getFixedLayerObject();
|
||||||
|
const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
|
||||||
|
if(colorObject && fixedLayerObj){
|
||||||
|
await this.setColorObjectInfo(colorObject, fixedLayerObj);
|
||||||
|
}
|
||||||
|
const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
|
||||||
|
if(groupLayer && fixedLayerObj){
|
||||||
|
const groupRect = new fabric.Rect({});
|
||||||
|
await this.setColorObjectInfo(groupRect, fixedLayerObj);
|
||||||
|
groupLayer.clippingMask = groupRect.toObject();
|
||||||
|
}
|
||||||
|
|
||||||
// 重新渲染画布
|
// 重新渲染画布
|
||||||
// this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -600,7 +627,7 @@ export class CanvasManager {
|
|||||||
* @param {Number} canvasWidth 画布宽度
|
* @param {Number} canvasWidth 画布宽度
|
||||||
* @param {Number} canvasHeight 画布高度
|
* @param {Number} canvasHeight 画布高度
|
||||||
*/
|
*/
|
||||||
centerBackgroundLayer(canvasWidth, canvasHeight) {
|
async centerBackgroundLayer(canvasWidth, canvasHeight) {
|
||||||
const backgroundLayerObject = this.getBackgroundLayer();
|
const backgroundLayerObject = this.getBackgroundLayer();
|
||||||
if (!backgroundLayerObject) return false;
|
if (!backgroundLayerObject) return false;
|
||||||
|
|
||||||
@@ -646,6 +673,11 @@ export class CanvasManager {
|
|||||||
if (this.maskLayer) {
|
if (this.maskLayer) {
|
||||||
this.canvas.remove(this.maskLayer);
|
this.canvas.remove(this.maskLayer);
|
||||||
}
|
}
|
||||||
|
this.canvas.getObjects().forEach((obj) => {
|
||||||
|
if (obj.id === "canvasMaskLayer") {
|
||||||
|
this.canvas.remove(obj);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 创建蒙层 - 使用透明矩形作为裁剪区域
|
// 创建蒙层 - 使用透明矩形作为裁剪区域
|
||||||
this.maskLayer = new fabric.Rect({
|
this.maskLayer = new fabric.Rect({
|
||||||
@@ -706,6 +738,75 @@ export class CanvasManager {
|
|||||||
|
|
||||||
return backgroundLayerByBgLayer;
|
return backgroundLayerByBgLayer;
|
||||||
}
|
}
|
||||||
|
getFixedLayerObject() {
|
||||||
|
if (!this.canvas) return null;
|
||||||
|
const fixedLayer = this.canvas.getObjects().find((obj) => {
|
||||||
|
return obj.isFixed;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fixedLayer) return fixedLayer;
|
||||||
|
|
||||||
|
// 如果没有找到固定层,则根据图层ID查找
|
||||||
|
const fixedLayerId = this.layers.value.find((layer) => {
|
||||||
|
return layer.isFixed;
|
||||||
|
})?.id;
|
||||||
|
|
||||||
|
const fixedLayerByFixedLayer = this.canvas.getObjects().find((obj) => {
|
||||||
|
return obj.isFixed || obj.id === fixedLayerId;
|
||||||
|
});
|
||||||
|
if (!fixedLayerByFixedLayer) {
|
||||||
|
console.warn(
|
||||||
|
"CanvasManager.js = >getFixedLayerObject 方法没有找到固定层"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedLayerByFixedLayer;
|
||||||
|
}
|
||||||
|
getBackgroundLayerObject() {
|
||||||
|
if (!this.canvas) return null;
|
||||||
|
const backgroundLayer = this.canvas.getObjects().find((obj) => {
|
||||||
|
return obj.isBackground;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (backgroundLayer) return backgroundLayer;
|
||||||
|
|
||||||
|
// 如果没有找到背景层,则根据图层ID查找
|
||||||
|
const backgroundLayerId = this.layers.value.find((layer) => {
|
||||||
|
return layer.isBackground;
|
||||||
|
})?.id;
|
||||||
|
|
||||||
|
const backgroundLayerByBgLayer = this.canvas.getObjects().find((obj) => {
|
||||||
|
return obj.isBackground || obj.id === backgroundLayerId;
|
||||||
|
});
|
||||||
|
if (!backgroundLayerByBgLayer) {
|
||||||
|
console.warn(
|
||||||
|
"CanvasManager.js = >getBackgroundLayerObject 方法没有找到背景层"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return backgroundLayerByBgLayer;
|
||||||
|
}
|
||||||
|
getLayerObjectById(layerId) {
|
||||||
|
if (!this.canvas) return null;
|
||||||
|
|
||||||
|
const layerObject = this.canvas.getObjects().find((obj) => {
|
||||||
|
return obj.id === layerId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (layerObject) return layerObject;
|
||||||
|
|
||||||
|
// 如果没有找到图层对象,则根据图层ID查找
|
||||||
|
const layerObjectByLayerId = this.canvas.getObjects().find((obj) => {
|
||||||
|
return obj.id === layerId;
|
||||||
|
});
|
||||||
|
if (!layerObjectByLayerId) {
|
||||||
|
console.warn(
|
||||||
|
"CanvasManager.js = >getLayerObjectById 方法没有找到图层对象"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layerObjectByLayerId;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 更新蒙层位置
|
* 更新蒙层位置
|
||||||
* @param {Object} backgroundLayerObject 背景层对象
|
* @param {Object} backgroundLayerObject 背景层对象
|
||||||
@@ -798,7 +899,7 @@ export class CanvasManager {
|
|||||||
|
|
||||||
// 如果找到了图层,则生成缩略图
|
// 如果找到了图层,则生成缩略图
|
||||||
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
|
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
|
||||||
|
this.layerManager?.sortLayers?.();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,6 +913,7 @@ export class CanvasManager {
|
|||||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||||
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
||||||
|
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
||||||
* @returns {String} 导出的图片数据URL
|
* @returns {String} 导出的图片数据URL
|
||||||
*/
|
*/
|
||||||
async exportImage(options = {}) {
|
async exportImage(options = {}) {
|
||||||
@@ -857,11 +959,55 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
return await this.exportManager.exportImage(enhancedOptions);
|
return await this.exportManager.exportImage(enhancedOptions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("CanvasManager导出图片失败:", error);
|
console.warn("CanvasManager导出图片失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出颜色图层
|
||||||
|
* @returns {Object} 导出的颜色图层数据URL
|
||||||
|
*/
|
||||||
|
async exportColorLayer() {
|
||||||
|
if (!this.exportManager) {
|
||||||
|
console.warn("导出管理器未初始化,请确保已设置图层管理器");
|
||||||
|
return Promise.reject("颜色图层不存在");
|
||||||
|
}
|
||||||
|
const object = this.getLayerObjectById(SpecialLayerId.COLOR);
|
||||||
|
if(!object){
|
||||||
|
console.warn("颜色图层不存在,请确保已添加颜色图层");
|
||||||
|
return Promise.reject("颜色图层不存在");
|
||||||
|
}
|
||||||
|
const color = fillToCssStyle(object.fill)
|
||||||
|
const canvas = new fabric.StaticCanvas();
|
||||||
|
canvas.setDimensions({
|
||||||
|
width: object.width,
|
||||||
|
height: object.height,
|
||||||
|
backgroundColor: null,
|
||||||
|
// enableRetinaScaling: true,
|
||||||
|
imageSmoothingEnabled: true,
|
||||||
|
});
|
||||||
|
const cloneObject = await new Promise((resolve, reject) => {
|
||||||
|
object.clone(resolve);
|
||||||
|
});
|
||||||
|
cloneObject.set({
|
||||||
|
left: canvas.width / 2,
|
||||||
|
top: canvas.height / 2,
|
||||||
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
|
visible: true,
|
||||||
|
clipPath: null,
|
||||||
|
});
|
||||||
|
canvas.add(cloneObject);
|
||||||
|
canvas.renderAll();
|
||||||
|
const base64 = canvas.toDataURL({
|
||||||
|
format: "png",
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
canvas.clear();
|
||||||
|
return {color, base64};
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
// 释放导出管理器资源
|
// 释放导出管理器资源
|
||||||
if (this.exportManager) {
|
if (this.exportManager) {
|
||||||
@@ -956,14 +1102,13 @@ export class CanvasManager {
|
|||||||
// };
|
// };
|
||||||
try {
|
try {
|
||||||
// 清除画布中选中状态
|
// 清除画布中选中状态
|
||||||
this.canvas.discardActiveObject();
|
// this.canvas.discardActiveObject();
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
|
|
||||||
const simplifyLayersData = simplifyLayers(
|
const simplifyLayersData = simplifyLayers(
|
||||||
JSON.parse(JSON.stringify(this.layers.value))
|
JSON.parse(JSON.stringify(this.layers.value))
|
||||||
);
|
);
|
||||||
console.log("获取画布JSON数据...", simplifyLayersData);
|
const data = JSON.stringify({
|
||||||
return JSON.stringify({
|
|
||||||
canvas: this.canvas.toJSON([
|
canvas: this.canvas.toJSON([
|
||||||
"id",
|
"id",
|
||||||
"type",
|
"type",
|
||||||
@@ -978,6 +1123,7 @@ export class CanvasManager {
|
|||||||
"eraserable",
|
"eraserable",
|
||||||
"erasable",
|
"erasable",
|
||||||
"customType",
|
"customType",
|
||||||
|
"fill_",
|
||||||
]),
|
]),
|
||||||
layers: simplifyLayersData, // 简化图层数据
|
layers: simplifyLayersData, // 简化图层数据
|
||||||
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
|
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
|
||||||
@@ -988,6 +1134,8 @@ export class CanvasManager {
|
|||||||
canvasColor: this.canvasColor.value,
|
canvasColor: this.canvasColor.value,
|
||||||
activeLayerId: this.layerManager?.activeLayerId?.value,
|
activeLayerId: this.layerManager?.activeLayerId?.value,
|
||||||
});
|
});
|
||||||
|
console.log("获取画布JSON数据...", data);
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取画布JSON失败:", error);
|
console.error("获取画布JSON失败:", error);
|
||||||
throw new Error("获取画布JSON失败");
|
throw new Error("获取画布JSON失败");
|
||||||
@@ -1070,8 +1218,10 @@ export class CanvasManager {
|
|||||||
// }
|
// }
|
||||||
try {
|
try {
|
||||||
// 重置画布数据
|
// 重置画布数据
|
||||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
await this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||||
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
await this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
||||||
|
await this.createOtherLayers(this.props.otherData);
|
||||||
|
|
||||||
// 重新构建对象关系
|
// 重新构建对象关系
|
||||||
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
|
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
|
||||||
// 验证图层关联关系 - 稳定后可以注释
|
// 验证图层关联关系 - 稳定后可以注释
|
||||||
@@ -1099,9 +1249,7 @@ export class CanvasManager {
|
|||||||
await calllBack?.();
|
await calllBack?.();
|
||||||
|
|
||||||
// 确保所有对象的交互性正确设置
|
// 确保所有对象的交互性正确设置
|
||||||
await this.layerManager?.updateLayersObjectsInteractivity?.(
|
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||||
false
|
|
||||||
);
|
|
||||||
console.log(this.layerManager.layers.value);
|
console.log(this.layerManager.layers.value);
|
||||||
|
|
||||||
// 更新所有缩略图
|
// 更新所有缩略图
|
||||||
@@ -1126,6 +1274,253 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建其他图层:印花、颜色、元素...
|
||||||
|
* @param {Object} otherData - 其他图层数据
|
||||||
|
*/
|
||||||
|
async createOtherLayers(otherData) {
|
||||||
|
if (!otherData) return console.warn("otherData 为空不需要添加");
|
||||||
|
const otherData_ = JSON.parse(JSON.stringify(otherData));
|
||||||
|
console.log("==========创建其他图层", otherData_);
|
||||||
|
// 创建颜色图层
|
||||||
|
await this.createColorLayer(otherData_.color);
|
||||||
|
|
||||||
|
if(findLayer(this.layers.value, SpecialLayerId.SPECIAL_GROUP)){
|
||||||
|
console.warn("画布中已存在印花和元素组图层");
|
||||||
|
}else{
|
||||||
|
const printTrimsLayers = [];// 印花和元素图层
|
||||||
|
const singleLayers = [];// 平铺图层
|
||||||
|
otherData_?.printObject?.prints?.forEach((print, index) => {
|
||||||
|
print.name = t("Canvas.Print") + (index + 1);
|
||||||
|
if(print.ifSingle){
|
||||||
|
printTrimsLayers.unshift({...print});
|
||||||
|
}else{
|
||||||
|
singleLayers.unshift({...print});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
otherData_?.trims?.prints?.forEach((print, index) => {
|
||||||
|
print.name = t("Canvas.Elements") + (index + 1);
|
||||||
|
printTrimsLayers.unshift({...print});
|
||||||
|
})
|
||||||
|
await this.createPrintTrimsLayers(printTrimsLayers, singleLayers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setColorObjectInfo(colorRect, fixedLayerObj){
|
||||||
|
colorRect.set({
|
||||||
|
top: fixedLayerObj.top,
|
||||||
|
left: fixedLayerObj.left,
|
||||||
|
width: fixedLayerObj.width,
|
||||||
|
height: fixedLayerObj.height,
|
||||||
|
originX: fixedLayerObj.originX,
|
||||||
|
originY: fixedLayerObj.originY,
|
||||||
|
scaleX: fixedLayerObj.scaleX,
|
||||||
|
scaleY: fixedLayerObj.scaleY,
|
||||||
|
});
|
||||||
|
var object = fixedLayerObj;
|
||||||
|
const imageUrl = this.props.clothingImageUrl2;
|
||||||
|
if(imageUrl){
|
||||||
|
object = await new Promise((resolve, reject) => {
|
||||||
|
fabric.Image.fromURL(imageUrl, (imgObject) => {
|
||||||
|
colorRect.set({
|
||||||
|
width: imgObject.width,
|
||||||
|
height: imgObject.height,
|
||||||
|
});
|
||||||
|
resolve(imgObject);
|
||||||
|
}, { crossOrigin: "anonymous" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const canvas = getObjectAlphaToCanvas(object);
|
||||||
|
const transparentMask = new fabric.Image(canvas, {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
originX: fixedLayerObj.originX,
|
||||||
|
originY: fixedLayerObj.originY,
|
||||||
|
});
|
||||||
|
colorRect.set('clipPath', transparentMask);
|
||||||
|
}
|
||||||
|
async createColorLayer(color){
|
||||||
|
if(!color) return console.warn("颜色为空不需要添加");
|
||||||
|
if(findLayer(this.layers.value, SpecialLayerId.COLOR)) return console.warn("画布中已存在颜色图层");
|
||||||
|
console.log("==========添加颜色图层", color, this.layers.value.length)
|
||||||
|
const fixedLayerObj = this.getFixedLayerObject();
|
||||||
|
// 创建颜色图层对象
|
||||||
|
const colorRect = new fabric.Rect({
|
||||||
|
id: SpecialLayerId.COLOR,
|
||||||
|
layerId: SpecialLayerId.COLOR,
|
||||||
|
layerName: t("Canvas.color"),
|
||||||
|
isVisible: true,
|
||||||
|
isLocked: true,
|
||||||
|
});
|
||||||
|
await this.setColorObjectInfo(colorRect, fixedLayerObj);
|
||||||
|
const gradientObj = palletToFill(color);
|
||||||
|
const gradient = new fabric.Gradient({
|
||||||
|
type: 'linear',
|
||||||
|
gradientUnits: 'percentage',
|
||||||
|
...gradientObj,
|
||||||
|
})
|
||||||
|
colorRect.set('fill', gradient);
|
||||||
|
this.canvas.add(colorRect);
|
||||||
|
// 创建颜色图层
|
||||||
|
const colorLayer = createLayer({
|
||||||
|
id: colorRect.layerId,
|
||||||
|
name: colorRect.layerName,
|
||||||
|
type: LayerType.SHAPE,
|
||||||
|
visible: colorRect.isVisible,
|
||||||
|
locked: colorRect.isLocked,
|
||||||
|
opacity: 1.0,
|
||||||
|
isFixedOther: true,
|
||||||
|
fabricObjects: [colorRect.toObject(["id", "layerId", "layerName"])],
|
||||||
|
})
|
||||||
|
const groupIndex = this.layers.value.findIndex(layer => layer.isFixed || layer.isBackground);
|
||||||
|
this.layers.value.splice(groupIndex, 0, colorLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建印花和元素图层
|
||||||
|
async createPrintTrimsLayers(printTrimsLayers, singleLayers){
|
||||||
|
console.log("==========添加印花和元素图层组", printTrimsLayers, singleLayers)
|
||||||
|
const fixedLayerObj = this.getFixedLayerObject();
|
||||||
|
const flWidth = fixedLayerObj.width
|
||||||
|
const flHeight = fixedLayerObj.height
|
||||||
|
const flTop = fixedLayerObj.top
|
||||||
|
const flLeft = fixedLayerObj.left
|
||||||
|
const flScaleX = fixedLayerObj.scaleX
|
||||||
|
const flScaleY = fixedLayerObj.scaleY
|
||||||
|
const children = [];
|
||||||
|
// 添加印花和元素图层
|
||||||
|
for(let index = 0; index < printTrimsLayers.length; index++){
|
||||||
|
let print = printTrimsLayers[index];
|
||||||
|
let id = generateId("layer_image_");
|
||||||
|
let name = print.name;
|
||||||
|
let image = await new Promise(resolve => {
|
||||||
|
fabric.Image.fromURL(print.path, (fabricImage)=>{
|
||||||
|
const left = flLeft - flWidth * flScaleX / 2 + (print.location?.[0] || 0) * flScaleX
|
||||||
|
const top = flTop - flHeight * flScaleY / 2 + (print.location?.[1] || 0) * flScaleY
|
||||||
|
const scaleX = flWidth * (print.scale?.[0] || 1) / fabricImage.width * flScaleX
|
||||||
|
const scaleY = flHeight * (print.scale?.[1] || 1) / fabricImage.height * flScaleY
|
||||||
|
const {x, y} = calculateRotatedTopLeftDeg(
|
||||||
|
fabricImage.width * scaleX,
|
||||||
|
fabricImage.height * scaleY,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
0,
|
||||||
|
print.angle || 0
|
||||||
|
)
|
||||||
|
const angle = print.angle || 0
|
||||||
|
fabricImage.set({
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
scaleX: scaleX,
|
||||||
|
scaleY: scaleY,
|
||||||
|
angle: angle,
|
||||||
|
id: id,
|
||||||
|
layerId: id,
|
||||||
|
layerName: name,
|
||||||
|
selectable: true,
|
||||||
|
hasControls: true,
|
||||||
|
hasBorders: true,
|
||||||
|
});
|
||||||
|
resolve(fabricImage);
|
||||||
|
}, { crossOrigin: "anonymous" });
|
||||||
|
})
|
||||||
|
this.canvas.add(image);
|
||||||
|
let layer = createLayer({
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
type: LayerType.BITMAP,
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
opacity: 1.0,
|
||||||
|
fabricObjects: [image.toObject(["id", "layerId", "layerName"])],
|
||||||
|
})
|
||||||
|
children.push(layer);
|
||||||
|
};
|
||||||
|
// 添加平铺图层
|
||||||
|
for(let index = 0; index < singleLayers.length; index++){
|
||||||
|
let print = singleLayers[index];
|
||||||
|
let id = generateId("layer_image_");
|
||||||
|
let name = print.name;
|
||||||
|
let image = await new Promise(resolve => {
|
||||||
|
fabric.Image.fromURL(print.path, (fabricImage)=>{
|
||||||
|
const imgElement = fabricImage.getElement();
|
||||||
|
const tcanvas = document.createElement('canvas');
|
||||||
|
tcanvas.width = imgElement.width;
|
||||||
|
tcanvas.height = imgElement.height;
|
||||||
|
const ctx = tcanvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
||||||
|
ctx.drawImage(imgElement, 0, 0);
|
||||||
|
resolve(tcanvas);
|
||||||
|
}, { crossOrigin: "anonymous" });
|
||||||
|
})
|
||||||
|
console.log("==========添加平铺图层", fixedLayerObj.width,image.width)
|
||||||
|
let scaleX = fixedLayerObj.width / image.width * (print.scale?.[0] || 1) / 5;
|
||||||
|
let scaleY = fixedLayerObj.height / image.height * (print.scale?.[1] || 1) / 5;
|
||||||
|
let scale = fixedLayerObj.width > fixedLayerObj.height ? scaleX : scaleY;
|
||||||
|
let left = (print.location?.[0] || 0) - image.width * scale / 2
|
||||||
|
let top = (print.location?.[1] || 0) - image.height * scale / 2
|
||||||
|
let rect = new fabric.Rect({
|
||||||
|
id: id,
|
||||||
|
layerId: id,
|
||||||
|
layerName: name,
|
||||||
|
width: fixedLayerObj.width,
|
||||||
|
height: fixedLayerObj.height,
|
||||||
|
top: fixedLayerObj.top,
|
||||||
|
left: fixedLayerObj.left,
|
||||||
|
scaleX: fixedLayerObj.scaleX,
|
||||||
|
scaleY: fixedLayerObj.scaleY,
|
||||||
|
originX: fixedLayerObj.originX,
|
||||||
|
originY: fixedLayerObj.originY,
|
||||||
|
fill: new fabric.Pattern({
|
||||||
|
source: image,
|
||||||
|
repeat: "repeat",
|
||||||
|
patternTransform: createPatternTransform(scale, print.angle || 0),
|
||||||
|
offsetX: left, // 水平偏移
|
||||||
|
offsetY: top, // 垂直偏移
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this.canvas.add(rect);
|
||||||
|
let layer = createLayer({
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
type: LayerType.BITMAP,
|
||||||
|
visible: true,
|
||||||
|
locked: true,
|
||||||
|
opacity: 1,
|
||||||
|
fabricObjects: [rect.toObject(["id", "layerId", "layerName"])],
|
||||||
|
})
|
||||||
|
children.push(layer);
|
||||||
|
};
|
||||||
|
if(children.length === 0){
|
||||||
|
let layer = createLayer({
|
||||||
|
id: generateId("layer_image_"),
|
||||||
|
name: t("Canvas.EmptyLayer"),
|
||||||
|
type: LayerType.BITMAP,
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
opacity: 1.0,
|
||||||
|
fabricObjects: [],
|
||||||
|
})
|
||||||
|
children.push(layer);
|
||||||
|
}
|
||||||
|
const groupRect = new fabric.Rect({});
|
||||||
|
await this.setColorObjectInfo(groupRect, fixedLayerObj);
|
||||||
|
// 插入组图层
|
||||||
|
const groupIndex = this.layers.value.findIndex(layer => layer.isFixedOther || layer.isFixed || layer.isBackground);
|
||||||
|
const groupLayer = createLayer({
|
||||||
|
id: SpecialLayerId.SPECIAL_GROUP,
|
||||||
|
name: t("Canvas.PrintAndElementsGroup"),
|
||||||
|
type: LayerType.GROUP,
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
opacity: 1.0,
|
||||||
|
fabricObjects: [],
|
||||||
|
children: children,
|
||||||
|
clippingMask: groupRect.toObject(),
|
||||||
|
isFixedClipMask: true,
|
||||||
|
});
|
||||||
|
this.layers.value.splice(groupIndex, 0, groupLayer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缩放红绿图模式内容以适应当前画布大小
|
* 缩放红绿图模式内容以适应当前画布大小
|
||||||
* 确保衣服底图和红绿图永远在画布内可见
|
* 确保衣服底图和红绿图永远在画布内可见
|
||||||
@@ -1249,6 +1644,7 @@ export class CanvasManager {
|
|||||||
return fixedLayer.fabricObject || null;
|
return fixedLayer.fabricObject || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有普通图层对象(包括红绿图)
|
* 获取所有普通图层对象(包括红绿图)
|
||||||
* @returns {Array} 普通图层对象数组
|
* @returns {Array} 普通图层对象数组
|
||||||
@@ -1315,4 +1711,46 @@ export class CanvasManager {
|
|||||||
|
|
||||||
return sizeMatch && positionMatch;
|
return sizeMatch && positionMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘移动激活对象
|
||||||
|
* @param {String} direction 移动方向(up, down, left, right)
|
||||||
|
* @param {<Number>} step 移动步长
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
moveActiveObject(direction, step = 1) {
|
||||||
|
const objects = [];
|
||||||
|
const activeObject = this.canvas.getActiveObject();
|
||||||
|
if(!activeObject) return;
|
||||||
|
const initPos = {
|
||||||
|
id: activeObject.id,
|
||||||
|
left: activeObject.left,
|
||||||
|
top: activeObject.top,
|
||||||
|
};
|
||||||
|
switch(direction) {
|
||||||
|
case "up":
|
||||||
|
activeObject.top -= step;
|
||||||
|
break;
|
||||||
|
case "down":
|
||||||
|
activeObject.top += step;
|
||||||
|
break;
|
||||||
|
case "left":
|
||||||
|
activeObject.left -= step;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
activeObject.left += step;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!activeObject.id) return this.canvas.renderAll();
|
||||||
|
const cmd = new ObjectMoveCommand({
|
||||||
|
canvas: this.canvas,
|
||||||
|
initPos,
|
||||||
|
finalPos: {
|
||||||
|
id: activeObject.id,
|
||||||
|
left: activeObject.left,
|
||||||
|
top: activeObject.top,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.commandManager.executeCommand(cmd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { findObjectById } from "../utils/helper";
|
import { findObjectById } from "../utils/helper";
|
||||||
import { createRasterizedImage } from "../utils/selectionToImage";
|
import { createRasterizedImage } from "../utils/selectionToImage";
|
||||||
|
import { OperationType, SpecialLayerId } from "../utils/layerHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片导出管理器
|
* 图片导出管理器
|
||||||
@@ -18,7 +19,7 @@ export class ExportManager {
|
|||||||
* @param {Object} options 导出选项
|
* @param {Object} options 导出选项
|
||||||
* @param {Boolean} options.isContainBg 是否包含背景图层
|
* @param {Boolean} options.isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
||||||
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
||||||
* @param {String} options.layerId 导出具体图层ID
|
* @param {String} options.layerId 导出具体图层ID
|
||||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||||
@@ -26,7 +27,7 @@ export class ExportManager {
|
|||||||
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
||||||
* @returns {String} 导出的图片数据URL
|
* @returns {String} 导出的图片数据URL
|
||||||
*/
|
*/
|
||||||
exportImage(options = {}) {
|
async exportImage(options = {}) {
|
||||||
const {
|
const {
|
||||||
isContainBg = false,
|
isContainBg = false,
|
||||||
isContainFixed = false,
|
isContainFixed = false,
|
||||||
@@ -35,9 +36,16 @@ export class ExportManager {
|
|||||||
layerIdArray = [],
|
layerIdArray = [],
|
||||||
expPicType = "png",
|
expPicType = "png",
|
||||||
restoreOpacityInRedGreen = true,
|
restoreOpacityInRedGreen = true,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
} = options;
|
} = options;
|
||||||
try {
|
try {
|
||||||
|
// 查找颜色图层并隐藏
|
||||||
|
const colorLayer = this.layerManager.getLayerById(SpecialLayerId.COLOR);
|
||||||
|
if (colorLayer && colorLayer.visible) {
|
||||||
|
colorLayer.visible = false;
|
||||||
|
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否为红绿图模式
|
// 检查是否为红绿图模式
|
||||||
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
||||||
// 如果指定了具体图层ID,导出指定图层
|
// 如果指定了具体图层ID,导出指定图层
|
||||||
@@ -48,7 +56,7 @@ export class ExportManager {
|
|||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +70,7 @@ export class ExportManager {
|
|||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +82,7 @@ export class ExportManager {
|
|||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("导出图片失败:", error);
|
console.error("导出图片失败:", error);
|
||||||
@@ -128,8 +136,6 @@ export class ExportManager {
|
|||||||
objectsToExport,
|
objectsToExport,
|
||||||
expPicType,
|
expPicType,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg, // 是否使用背景大小裁剪
|
|
||||||
isEnhanceImg, // 是否是增强图片
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,37 +561,22 @@ export class ExportManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
|
|
||||||
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
|
|
||||||
|
|
||||||
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
||||||
const canvasWidth = Math.round(fixedBounds.width);
|
const canvasWidth = (fixedLayerObject.width);
|
||||||
const canvasHeight = Math.round(fixedBounds.height);
|
const canvasHeight = (fixedLayerObject.height);
|
||||||
|
|
||||||
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
||||||
console.log("固定图层边界:", fixedBounds);
|
const tempFabricCanvas = new fabric.StaticCanvas()
|
||||||
|
tempFabricCanvas.setDimensions({
|
||||||
// 创建固定尺寸的临时画布
|
|
||||||
const scaleFactor = 2; // 高清导出
|
|
||||||
const tempCanvas = document.createElement("canvas");
|
|
||||||
tempCanvas.width = canvasWidth * scaleFactor;
|
|
||||||
tempCanvas.height = canvasHeight * scaleFactor;
|
|
||||||
tempCanvas.style.width = canvasWidth + "px";
|
|
||||||
tempCanvas.style.height = canvasHeight + "px";
|
|
||||||
|
|
||||||
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
|
|
||||||
width: canvasWidth,
|
width: canvasWidth,
|
||||||
height: canvasHeight,
|
height: canvasHeight,
|
||||||
backgroundColor: null,
|
backgroundColor: null,
|
||||||
enableRetinaScaling: true,
|
// enableRetinaScaling: true,
|
||||||
imageSmoothingEnabled: true,
|
imageSmoothingEnabled: true,
|
||||||
});
|
});
|
||||||
tempFabricCanvas.setZoom(1);
|
// tempFabricCanvas.setZoom(1);
|
||||||
|
console.log("==========", fixedLayerObject)
|
||||||
try {
|
try {
|
||||||
// 获取裁剪路径对象(如果存在)
|
|
||||||
const clipPathObject = await this._getClipPathObject(fixedBounds);
|
|
||||||
|
|
||||||
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
||||||
for (let i = 0; i < objectsToExport.length; i++) {
|
for (let i = 0; i < objectsToExport.length; i++) {
|
||||||
const obj = objectsToExport[i];
|
const obj = objectsToExport[i];
|
||||||
@@ -596,18 +587,16 @@ export class ExportManager {
|
|||||||
if (cloned) {
|
if (cloned) {
|
||||||
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
|
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
|
||||||
cloned.set({
|
cloned.set({
|
||||||
left: cloned.left - fixedBounds.left,
|
left: canvasWidth / 2,
|
||||||
top: cloned.top - fixedBounds.top,
|
top: canvasHeight / 2,
|
||||||
|
scaleX: cloned.scaleX / fixedLayerObject.scaleX,
|
||||||
|
scaleY: cloned.scaleY / fixedLayerObject.scaleY,
|
||||||
|
originX: "center",
|
||||||
|
originY: "center",
|
||||||
});
|
});
|
||||||
|
console.log("==========", {...cloned})
|
||||||
// 更新对象坐标
|
// 更新对象坐标
|
||||||
cloned.setCoords();
|
cloned.setCoords();
|
||||||
|
|
||||||
// 设置裁剪路径到对象
|
|
||||||
if (clipPathObject) {
|
|
||||||
cloned.clipPath = clipPathObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFabricCanvas.add(cloned);
|
tempFabricCanvas.add(cloned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,7 +605,7 @@ export class ExportManager {
|
|||||||
tempFabricCanvas.renderAll();
|
tempFabricCanvas.renderAll();
|
||||||
|
|
||||||
// 生成图片
|
// 生成图片
|
||||||
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
return this._generateHighQualityDataURL(tempFabricCanvas, expPicType);
|
||||||
} finally {
|
} finally {
|
||||||
this._cleanupTempCanvas(tempFabricCanvas);
|
this._cleanupTempCanvas(tempFabricCanvas);
|
||||||
}
|
}
|
||||||
@@ -736,7 +725,7 @@ export class ExportManager {
|
|||||||
*/
|
*/
|
||||||
_cloneObjectAsync(
|
_cloneObjectAsync(
|
||||||
obj,
|
obj,
|
||||||
propertiesToInclude = ["id", "layerId", "layerName", "name"]
|
propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"]
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "../commands/ObjectLayerCommands";
|
} from "../commands/ObjectLayerCommands";
|
||||||
import {
|
import {
|
||||||
LayerType,
|
LayerType,
|
||||||
|
SpecialLayerId,
|
||||||
BlendMode,
|
BlendMode,
|
||||||
createLayer,
|
createLayer,
|
||||||
createBackgroundLayer,
|
createBackgroundLayer,
|
||||||
@@ -343,35 +344,36 @@ export class LayerManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 批量更新对象
|
// 批量更新对象
|
||||||
objects.forEach(async (obj) => {
|
for(let obj of objects){
|
||||||
const layer = layerMap[obj.layerId];
|
let layer = layerMap[obj.layerId];
|
||||||
|
|
||||||
if (!obj.layerId) {
|
if (!obj.layerId) {
|
||||||
// 没有关联图层的对象使用默认设置
|
// 没有关联图层的对象使用默认设置
|
||||||
obj.selectable = false;
|
obj.selectable = false;
|
||||||
obj.evented = false;
|
obj.evented = false;
|
||||||
obj.erasable = false; // 未关联图层的对象不可擦除
|
obj.erasable = false; // 未关联图层的对象不可擦除
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!layer) return;
|
if (!layer) break;
|
||||||
|
|
||||||
// 设置一级图层对象的交互性
|
// 设置一级图层对象的交互性
|
||||||
await this._setObjectInteractivity(obj, layer, editorMode);
|
await this._setObjectInteractivity(obj, layer, editorMode);
|
||||||
|
|
||||||
// 设置子图层对象的交互性
|
// 设置子图层对象的交互性
|
||||||
layer?.children?.forEach(async (childLayer) => {
|
for(let childLayer of layer.children){
|
||||||
const childObj = this.canvas
|
let childObj = this.canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
.find((o) => o.layerId === childLayer.id);
|
.find((o) => o.layerId === childLayer.id);
|
||||||
if (childObj) {
|
if (childObj) {
|
||||||
await this._setObjectInteractivity(childObj, childLayer, editorMode);
|
await this._setObjectInteractivity(childObj, childLayer, editorMode);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
// 设置裁剪对象
|
// 设置裁剪对象
|
||||||
layers.forEach(async (layer) => {
|
for(let layer of layers){
|
||||||
|
if(layer.id === SpecialLayerId.COLOR) break;
|
||||||
let clippingMaskFabricObject = null;
|
let clippingMaskFabricObject = null;
|
||||||
if (layer.clippingMask) {
|
if (layer.clippingMask) {
|
||||||
// 反序列化 clippingMask
|
// 反序列化 clippingMask
|
||||||
@@ -379,7 +381,7 @@ export class LayerManager {
|
|||||||
layer.clippingMask,
|
layer.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
|
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
// 设置绝对定位
|
// 设置绝对定位
|
||||||
@@ -403,7 +405,7 @@ export class LayerManager {
|
|||||||
.find((o) => o.layerId === childLayer.id);
|
.find((o) => o.layerId === childLayer.id);
|
||||||
if (childObj) {
|
if (childObj) {
|
||||||
childObj.clipPath = clippingMaskFabricObject;
|
childObj.clipPath = clippingMaskFabricObject;
|
||||||
childObj.dirty = true; // 标记为脏对象
|
// childObj.dirty = true; // 标记为脏对象
|
||||||
childObj.setCoords();
|
childObj.setCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +501,7 @@ export class LayerManager {
|
|||||||
isOldSelectObject
|
isOldSelectObject
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -952,18 +954,28 @@ export class LayerManager {
|
|||||||
// 查找要删除的图层
|
// 查找要删除的图层
|
||||||
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
|
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
|
||||||
// 如果是背景层或固定层,不允许删除
|
// 如果是背景层或固定层,不允许删除
|
||||||
if (layer && (layer.isBackground || layer.isFixed)) {
|
if (layer && (layer.isBackground || layer.isFixed || layer.isFixedOther)) {
|
||||||
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
||||||
message.warning(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
message.warning(layer.isBackground ? this.t("Canvas.backLayerCannotDelete") : this.t("Canvas.fixedLayerCannotDelete"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 检查是否是唯一的普通图层
|
// 检查是否是唯一的普通图层
|
||||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
|
var isChild = false;
|
||||||
|
var parentLength = 0;
|
||||||
|
const normalLayers = this.layers.value.filter((layer) => {
|
||||||
|
if(layer.children.some(v => v.id == layerId)){
|
||||||
|
isChild = true;
|
||||||
|
parentLength = layer.children.length;
|
||||||
|
}
|
||||||
|
return !layer.isFixed && !layer.isFixedOther && !layer.isBackground
|
||||||
|
})
|
||||||
|
// const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
|
||||||
console.log("普通图层:", normalLayers)
|
console.log("普通图层:", normalLayers)
|
||||||
if (normalLayers.length === 1) {
|
if (isChild ? parentLength <= 1 : normalLayers.length <= 1) {
|
||||||
console.warn("不能删除唯一的普通图层");
|
console.warn("不能删除唯一的普通图层");
|
||||||
message.warning("不能删除唯一的普通图层");
|
message.warning(this.t("Canvas.cannotDeleteOnlyLayer"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// // 如果图层有子图层,提示确认
|
// // 如果图层有子图层,提示确认
|
||||||
@@ -1132,7 +1144,7 @@ export class LayerManager {
|
|||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
console.log("==========", allObjects)
|
||||||
// if (layer.fill) {
|
// if (layer.fill) {
|
||||||
// // 如果图层有填充颜色,设置所有对象的填充颜色
|
// // 如果图层有填充颜色,设置所有对象的填充颜色
|
||||||
// const { object } = findObjectById(this.canvas, layer.fill.id);
|
// const { object } = findObjectById(this.canvas, layer.fill.id);
|
||||||
@@ -1578,6 +1590,12 @@ export class LayerManager {
|
|||||||
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||||
if (b.isFixed && !a.isFixed) return -1;
|
if (b.isFixed && !a.isFixed) return -1;
|
||||||
|
|
||||||
|
// 如果a是固定图层而b不是固定图层,a应该排在后面(固定图层在普通图层下方)
|
||||||
|
if (a.isFixedOther && !b.isFixedOther) return 1;
|
||||||
|
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||||
|
if (b.isFixedOther && !a.isFixedOther) return -1;
|
||||||
|
|
||||||
|
|
||||||
// 其他情况保持原有顺序
|
// 其他情况保持原有顺序
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
@@ -1848,9 +1866,9 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是唯一的普通图层
|
// 检查是否是唯一的普通图层
|
||||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
|
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
|
||||||
console.log("普通图层:", normalLayers)
|
console.log("普通图层:", normalLayers)
|
||||||
if (normalLayers.length === 1) {
|
if (normalLayers.length <= 1) {
|
||||||
console.warn("不能剪切唯一的普通图层");
|
console.warn("不能剪切唯一的普通图层");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -3250,7 +3268,7 @@ export class LayerManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_setupGroupMaskMovementSync(activeSelection, layer) {
|
_setupGroupMaskMovementSync(activeSelection, layer) {
|
||||||
if (!activeSelection || !layer || !layer.clippingMask) {
|
if (!activeSelection || !layer || !layer.clippingMask || layer.isFixedClipMask) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3314,7 +3332,6 @@ export class LayerManager {
|
|||||||
// 计算移动距离
|
// 计算移动距离
|
||||||
const deltaX = target.left - initialLeft;
|
const deltaX = target.left - initialLeft;
|
||||||
const deltaY = target.top - initialTop;
|
const deltaY = target.top - initialTop;
|
||||||
|
|
||||||
// 创建更新遮罩位置的命令
|
// 创建更新遮罩位置的命令
|
||||||
const command = new UpdateGroupMaskPositionCommand({
|
const command = new UpdateGroupMaskPositionCommand({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ export class ThumbnailManager {
|
|||||||
// 重新创建遮罩对象
|
// 重新创建遮罩对象
|
||||||
clippingMaskFabricObject = await restoreFabricObject(layer?.clippingMask, this.canvas);
|
clippingMaskFabricObject = await restoreFabricObject(layer?.clippingMask, this.canvas);
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
clippingMaskFabricObject.dirty = true;
|
// clippingMaskFabricObject.dirty = true;
|
||||||
clippingMaskFabricObject.setCoords();
|
clippingMaskFabricObject.setCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,8 +128,13 @@ export class ThumbnailManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
||||||
let layersToRasterize = [];
|
|
||||||
|
|
||||||
|
if (!layer) {
|
||||||
|
console.warn("⚠️ 无效的图层,无法收集对象");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let layersToRasterize = [];
|
||||||
if (layer.children && layer.children.length > 0) {
|
if (layer.children && layer.children.length > 0) {
|
||||||
// 组图层:收集自身和所有子图层
|
// 组图层:收集自身和所有子图层
|
||||||
layersToRasterize = this._collectLayersToRasterize(layer);
|
layersToRasterize = this._collectLayersToRasterize(layer);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class AnimationManager {
|
|||||||
|
|
||||||
// 如果变化太小,直接应用缩放
|
// 如果变化太小,直接应用缩放
|
||||||
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
||||||
// this._applyZoom(point, targetZoom);
|
this._applyZoom(point, targetZoom);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export class AnimationManager {
|
|||||||
this._zoomAnimation = null;
|
this._zoomAnimation = null;
|
||||||
|
|
||||||
// 确保最终状态准确
|
// 确保最终状态准确
|
||||||
// this._applyZoom(point, targetZoom, true);
|
this._applyZoom(point, targetZoom, true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ export class AnimationManager {
|
|||||||
this._zoomAnimation = null;
|
this._zoomAnimation = null;
|
||||||
|
|
||||||
// 确保最终状态准确
|
// 确保最终状态准确
|
||||||
// this._applyZoom(point, targetZoom, true);
|
this._applyZoom(point, targetZoom, true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -817,7 +817,7 @@ export class AnimationManager {
|
|||||||
this._wasZooming = false;
|
this._wasZooming = false;
|
||||||
|
|
||||||
// 确保最终状态准确
|
// 确保最终状态准确
|
||||||
// this._applyZoom(point, targetZoom, true);
|
this._applyZoom(point, targetZoom, true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PerformanceManager } from "./PerformanceManager.js";
|
|||||||
*/
|
*/
|
||||||
export class CommandManager {
|
export class CommandManager {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
|
this.canvas = options.canvas;
|
||||||
this.undoStack = [];
|
this.undoStack = [];
|
||||||
this.redoStack = [];
|
this.redoStack = [];
|
||||||
this.maxHistorySize = options.maxHistorySize || 50;
|
this.maxHistorySize = options.maxHistorySize || 50;
|
||||||
@@ -205,6 +206,7 @@ export class CommandManager {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.canvas?.discardActiveObject();
|
||||||
const command = this.undoStack.pop();
|
const command = this.undoStack.pop();
|
||||||
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
|
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
|
||||||
|
|
||||||
@@ -243,6 +245,7 @@ export class CommandManager {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.canvas?.discardActiveObject();
|
||||||
const command = this.redoStack.pop();
|
const command = this.redoStack.pop();
|
||||||
console.log(`↪️ 重做命令: ${command.constructor.name}`);
|
console.log(`↪️ 重做命令: ${command.constructor.name}`);
|
||||||
|
|
||||||
|
|||||||
@@ -688,7 +688,6 @@ export class CanvasEventManager {
|
|||||||
this.layerManager.commandManager.execute(transformCmd, {
|
this.layerManager.commandManager.execute(transformCmd, {
|
||||||
name: "对象修改",
|
name: "对象修改",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清除临时状态记录
|
// 清除临时状态记录
|
||||||
delete activeObj._initialTransformState;
|
delete activeObj._initialTransformState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class KeyboardManager {
|
|||||||
* @param {Object} options.toolManager 工具管理器实例
|
* @param {Object} options.toolManager 工具管理器实例
|
||||||
* @param {Object} options.commandManager 命令管理器实例
|
* @param {Object} options.commandManager 命令管理器实例
|
||||||
* @param {Object} options.layerManager 图层管理器实例
|
* @param {Object} options.layerManager 图层管理器实例
|
||||||
|
* @param {Object} options.canvasManager 画布管理器实例
|
||||||
* @param {Function} options.pasteText 粘贴文本回调函数
|
* @param {Function} options.pasteText 粘贴文本回调函数
|
||||||
* @param {Function} options.pasteImage 粘贴图片回调函数
|
* @param {Function} options.pasteImage 粘贴图片回调函数
|
||||||
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
|
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
|
||||||
@@ -19,6 +20,7 @@ export class KeyboardManager {
|
|||||||
this.toolManager = options.toolManager;
|
this.toolManager = options.toolManager;
|
||||||
this.commandManager = options.commandManager;
|
this.commandManager = options.commandManager;
|
||||||
this.layerManager = options.layerManager;
|
this.layerManager = options.layerManager;
|
||||||
|
this.canvasManager = options.canvasManager;
|
||||||
this.container = options.container || document;
|
this.container = options.container || document;
|
||||||
this.pasteText = options.pasteText || (() => {});
|
this.pasteText = options.pasteText || (() => {});
|
||||||
this.pasteImage = options.pasteImage || (() => {});
|
this.pasteImage = options.pasteImage || (() => {});
|
||||||
@@ -125,6 +127,10 @@ export class KeyboardManager {
|
|||||||
// 删除
|
// 删除
|
||||||
delete: { action: "delete", description: "删除" },
|
delete: { action: "delete", description: "删除" },
|
||||||
backspace: { action: "delete", description: "删除" },
|
backspace: { action: "delete", description: "删除" },
|
||||||
|
up: { action: "up", description: "上" },
|
||||||
|
down: { action: "down", description: "下" },
|
||||||
|
left: { action: "left", description: "左" },
|
||||||
|
right: { action: "right", description: "右" },
|
||||||
|
|
||||||
// 选择
|
// 选择
|
||||||
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
|
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
|
||||||
@@ -488,6 +494,14 @@ export class KeyboardManager {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "up":
|
||||||
|
case "down":
|
||||||
|
case "left":
|
||||||
|
case "right":
|
||||||
|
// 方向键逻辑
|
||||||
|
this.canvasManager.moveActiveObject(action);
|
||||||
|
break;
|
||||||
|
|
||||||
case "increaseBrushSize":
|
case "increaseBrushSize":
|
||||||
// 增大画笔尺寸
|
// 增大画笔尺寸
|
||||||
if (this.toolManager && this.toolManager.brushManager) {
|
if (this.toolManager && this.toolManager.brushManager) {
|
||||||
@@ -639,7 +653,6 @@ export class KeyboardManager {
|
|||||||
if (event.altKey) shortcutKey += "alt+";
|
if (event.altKey) shortcutKey += "alt+";
|
||||||
|
|
||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
|
|
||||||
// 特殊键处理
|
// 特殊键处理
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case " ":
|
case " ":
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class LiquifyCPUManager {
|
|||||||
sharpenAmount: 0.3, // 添加锐化强度参数
|
sharpenAmount: 0.3, // 添加锐化强度参数
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
console.log("CPU版本的液化管理器config",this.config);
|
console.log("CPU版本的液化管理器config", this.config);
|
||||||
|
|
||||||
this.params = {
|
this.params = {
|
||||||
size: 60, // 增大默认尺寸
|
size: 60, // 增大默认尺寸
|
||||||
@@ -63,7 +63,8 @@ export class LiquifyCPUManager {
|
|||||||
// 新增:持续按压相关状态
|
// 新增:持续按压相关状态
|
||||||
this.pressStartTime = 0; // 按压开始时间
|
this.pressStartTime = 0; // 按压开始时间
|
||||||
this.pressDuration = 0; // 按压持续时间
|
this.pressDuration = 0; // 按压持续时间
|
||||||
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)
|
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)--废除使用固定角度
|
||||||
|
this.fixedRotationAngle = 0.32; // 固定旋转角度
|
||||||
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
|
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
|
||||||
this.lastApplyTime = 0; // 上次应用时间
|
this.lastApplyTime = 0; // 上次应用时间
|
||||||
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
|
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
|
||||||
@@ -189,7 +190,7 @@ export class LiquifyCPUManager {
|
|||||||
this.isHolding = true;
|
this.isHolding = true;
|
||||||
|
|
||||||
// 启动持续效果定时器(对于所有模式都支持持续按压)
|
// 启动持续效果定时器(对于所有模式都支持持续按压)
|
||||||
this.startContinuousEffect();
|
// this.startContinuousEffect();
|
||||||
|
|
||||||
console.log(`开始液化操作,初始点: (${x}, ${y})`);
|
console.log(`开始液化操作,初始点: (${x}, ${y})`);
|
||||||
}
|
}
|
||||||
@@ -220,7 +221,6 @@ export class LiquifyCPUManager {
|
|||||||
// 新增:启动持续效果
|
// 新增:启动持续效果
|
||||||
startContinuousEffect() {
|
startContinuousEffect() {
|
||||||
this.stopContinuousEffect(); // 先停止已有的定时器
|
this.stopContinuousEffect(); // 先停止已有的定时器
|
||||||
|
|
||||||
this.continuousTimer = setInterval(() => {
|
this.continuousTimer = setInterval(() => {
|
||||||
if (this.isHolding && this.initialized) {
|
if (this.isHolding && this.initialized) {
|
||||||
// 更新持续时间
|
// 更新持续时间
|
||||||
@@ -273,7 +273,6 @@ export class LiquifyCPUManager {
|
|||||||
*/
|
*/
|
||||||
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
|
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
|
||||||
if (!this.currentImageData) return;
|
if (!this.currentImageData) return;
|
||||||
|
|
||||||
const data = this.currentImageData.data;
|
const data = this.currentImageData.data;
|
||||||
const width = this.currentImageData.width;
|
const width = this.currentImageData.width;
|
||||||
const height = this.currentImageData.height;
|
const height = this.currentImageData.height;
|
||||||
@@ -286,6 +285,7 @@ export class LiquifyCPUManager {
|
|||||||
const rotationAngle =
|
const rotationAngle =
|
||||||
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
|
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
|
||||||
|
|
||||||
|
console.log("持续应用旋转效果");
|
||||||
// 累积旋转角度 - 关键:这确保了持续旋转效果
|
// 累积旋转角度 - 关键:这确保了持续旋转效果
|
||||||
this.accumulatedRotation += rotationAngle;
|
this.accumulatedRotation += rotationAngle;
|
||||||
|
|
||||||
@@ -309,13 +309,14 @@ export class LiquifyCPUManager {
|
|||||||
|
|
||||||
// 计算旋转后的源位置 - 关键算法
|
// 计算旋转后的源位置 - 关键算法
|
||||||
const angle = Math.atan2(dy, dx);
|
const angle = Math.atan2(dy, dx);
|
||||||
const newAngle = angle + this.accumulatedRotation * falloff;
|
// const newAngle = angle + this.accumulatedRotation * falloff;
|
||||||
|
const newAngle = angle + (isClockwise ? this.fixedRotationAngle : -this.fixedRotationAngle) * falloff;
|
||||||
|
|
||||||
const sourceX = centerX + Math.cos(newAngle) * distance;
|
const sourceX = centerX + Math.cos(newAngle) * distance;
|
||||||
const sourceY = centerY + Math.sin(newAngle) * distance;
|
const sourceY = centerY + Math.sin(newAngle) * distance;
|
||||||
|
|
||||||
// 双线性插值采样 - 确保像素连续性
|
// 双线性插值采样 - 确保像素连续性
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -376,7 +377,7 @@ export class LiquifyCPUManager {
|
|||||||
const sourceY = centerY + dy * scale;
|
const sourceY = centerY + dy * scale;
|
||||||
|
|
||||||
// 双线性插值采样
|
// 双线性插值采样
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -401,16 +402,17 @@ export class LiquifyCPUManager {
|
|||||||
*/
|
*/
|
||||||
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
|
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
|
||||||
if (!this.currentImageData) return;
|
if (!this.currentImageData) return;
|
||||||
|
|
||||||
const data = this.currentImageData.data;
|
const data = this.currentImageData.data;
|
||||||
const width = this.currentImageData.width;
|
const width = this.currentImageData.width;
|
||||||
const height = this.currentImageData.height;
|
const height = this.currentImageData.height;
|
||||||
const tempData = new Uint8ClampedArray(data);
|
const tempData = new Uint8ClampedArray(data);
|
||||||
|
|
||||||
// 计算推拉方向
|
// 计算推拉方向
|
||||||
const deltaX = this.currentMouseX - this.initialMouseX;
|
const deltaX = this.currentMouseX - this.lastMouseX;
|
||||||
const deltaY = this.currentMouseY - this.initialMouseY;
|
const deltaY = this.currentMouseY - this.lastMouseY;
|
||||||
const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
this.lastMouseX = this.currentMouseX;
|
||||||
|
this.lastMouseY = this.currentMouseY;
|
||||||
|
|
||||||
const processRadius = Math.min(radius, Math.min(width, height) / 2);
|
const processRadius = Math.min(radius, Math.min(width, height) / 2);
|
||||||
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
||||||
@@ -426,6 +428,7 @@ export class LiquifyCPUManager {
|
|||||||
|
|
||||||
for (let y = minY; y < maxY; y++) {
|
for (let y = minY; y < maxY; y++) {
|
||||||
for (let x = minX; x < maxX; x++) {
|
for (let x = minX; x < maxX; x++) {
|
||||||
|
// 此处循环4万次
|
||||||
const dx = x - centerX;
|
const dx = x - centerX;
|
||||||
const dy = y - centerY;
|
const dy = y - centerY;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
@@ -442,7 +445,7 @@ export class LiquifyCPUManager {
|
|||||||
const sourceX = x - pushX;
|
const sourceX = x - pushX;
|
||||||
const sourceY = y - pushY;
|
const sourceY = y - pushY;
|
||||||
|
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -461,9 +464,9 @@ export class LiquifyCPUManager {
|
|||||||
// 有拖拽时的推拉效果
|
// 有拖拽时的推拉效果
|
||||||
const dirX = deltaX / dragLength;
|
const dirX = deltaX / dragLength;
|
||||||
const dirY = deltaY / dragLength;
|
const dirY = deltaY / dragLength;
|
||||||
|
|
||||||
for (let y = minY; y < maxY; y++) {
|
for (let y = minY; y < maxY; y++) {
|
||||||
for (let x = minX; x < maxX; x++) {
|
for (let x = minX; x < maxX; x++) {
|
||||||
|
// 此处循环4万次
|
||||||
const dx = x - centerX;
|
const dx = x - centerX;
|
||||||
const dy = y - centerY;
|
const dy = y - centerY;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
@@ -473,13 +476,13 @@ export class LiquifyCPUManager {
|
|||||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||||
const factor = falloff * strength;
|
const factor = falloff * strength;
|
||||||
|
|
||||||
const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15);
|
const offsetX = dirX * factor * Math.min(dragLength * 2, 30);
|
||||||
const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15);
|
const offsetY = dirY * factor * Math.min(dragLength * 2, 30);
|
||||||
|
|
||||||
const sourceX = x - offsetX;
|
const sourceX = x - offsetX;
|
||||||
const sourceY = y - offsetY;
|
const sourceY = y - offsetY;
|
||||||
|
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -527,7 +530,7 @@ export class LiquifyCPUManager {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case this.modes.PUSH:
|
case this.modes.PUSH:
|
||||||
this._applyEnhancedPushDeformation(x, y, radius, strength);
|
// this._applyEnhancedPushDeformation(x, y, radius, strength);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
@@ -553,101 +556,7 @@ export class LiquifyCPUManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用液化变形 - 主要入口,集成增强算法
|
* 双线性插值函数
|
||||||
*/
|
|
||||||
// applyDeformation(x, y) {
|
|
||||||
// if (!this.initialized || !this.originalImageData) {
|
|
||||||
// console.warn("液化管理器未初始化或缺少必要数据");
|
|
||||||
// return this.currentImageData;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 更新鼠标位置
|
|
||||||
// this.currentMouseX = x;
|
|
||||||
// this.currentMouseY = y;
|
|
||||||
|
|
||||||
// // 计算拖拽参数
|
|
||||||
// const deltaX = this.currentMouseX - this.initialMouseX;
|
|
||||||
// const deltaY = this.currentMouseY - this.initialMouseY;
|
|
||||||
// this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
||||||
// this.dragAngle = Math.atan2(deltaY, deltaX);
|
|
||||||
|
|
||||||
// // 获取当前参数
|
|
||||||
// const { size, pressure, power } = this.params;
|
|
||||||
// const mode = this.currentMode;
|
|
||||||
// const radius = size;
|
|
||||||
// const strength = pressure * power;
|
|
||||||
|
|
||||||
// // 根据模式选择算法
|
|
||||||
// const pixelModes = [
|
|
||||||
// this.modes.CLOCKWISE,
|
|
||||||
// this.modes.COUNTERCLOCKWISE,
|
|
||||||
// this.modes.PINCH,
|
|
||||||
// this.modes.EXPAND,
|
|
||||||
// this.modes.PUSH,
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// if (pixelModes.includes(mode)) {
|
|
||||||
// // 使用增强的像素算法
|
|
||||||
// switch (mode) {
|
|
||||||
// case this.modes.CLOCKWISE:
|
|
||||||
// this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
|
|
||||||
// break;
|
|
||||||
// case this.modes.COUNTERCLOCKWISE:
|
|
||||||
// this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
|
|
||||||
// break;
|
|
||||||
// case this.modes.PINCH:
|
|
||||||
// this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
|
|
||||||
// break;
|
|
||||||
// case this.modes.EXPAND:
|
|
||||||
// this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
|
|
||||||
// break;
|
|
||||||
// case this.modes.PUSH:
|
|
||||||
// this._applyEnhancedPushDeformation(x, y, radius, strength);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 更新最后应用时间
|
|
||||||
// this.lastApplyTime = Date.now();
|
|
||||||
// this.isFirstApply = false;
|
|
||||||
|
|
||||||
// return this.currentImageData;
|
|
||||||
// } else {
|
|
||||||
// // 使用原有的网格算法处理其他模式
|
|
||||||
// if (!this.mesh) {
|
|
||||||
// console.warn("网格未初始化");
|
|
||||||
// return this.currentImageData;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const finalStrength = (strength * this.config.maxStrength) / 100;
|
|
||||||
|
|
||||||
// // 应用变形
|
|
||||||
// this._applyDeformation(
|
|
||||||
// x,
|
|
||||||
// y,
|
|
||||||
// radius,
|
|
||||||
// finalStrength,
|
|
||||||
// mode,
|
|
||||||
// this.params.distortion,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // 平滑处理
|
|
||||||
// if (this.config.smoothingIterations > 0) {
|
|
||||||
// this._smoothMesh();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 更新图像数据
|
|
||||||
// const result = this._applyMeshToImage();
|
|
||||||
|
|
||||||
// // 更新最后应用时间
|
|
||||||
// this.lastApplyTime = Date.now();
|
|
||||||
// this.isFirstApply = false;
|
|
||||||
|
|
||||||
// return result;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 双线性插值采样 - 用于像素级算法
|
|
||||||
* @param {Uint8ClampedArray} data 图像数据
|
* @param {Uint8ClampedArray} data 图像数据
|
||||||
* @param {number} width 图像宽度
|
* @param {number} width 图像宽度
|
||||||
* @param {number} height 图像高度
|
* @param {number} height 图像高度
|
||||||
@@ -655,19 +564,55 @@ export class LiquifyCPUManager {
|
|||||||
* @param {number} y Y坐标
|
* @param {number} y Y坐标
|
||||||
* @returns {Array|null} RGBA颜色值数组或null
|
* @returns {Array|null} RGBA颜色值数组或null
|
||||||
*/
|
*/
|
||||||
_bilinearSample(data, width, height, x, y) {
|
_bilinearInterpolate(data, width, height, x, y) {
|
||||||
return this._bicubicInterpolate(data, width, height, x, y);
|
const x1 = Math.floor(x);
|
||||||
|
const y1 = Math.floor(y);
|
||||||
|
const x2 = Math.min(width - 1, x1 + 1);
|
||||||
|
const y2 = Math.min(height - 1, y1 + 1);
|
||||||
|
|
||||||
|
const dx = x - x1;
|
||||||
|
const dy = y - y1;
|
||||||
|
const dx1 = 1 - dx;
|
||||||
|
const dy1 = 1 - dy;
|
||||||
|
const index1 = (y1 * width + x1) * 4;
|
||||||
|
const index2 = (y1 * width + x2) * 4;
|
||||||
|
const index3 = (y2 * width + x1) * 4;
|
||||||
|
const index4 = (y2 * width + x2) * 4;
|
||||||
|
const r =
|
||||||
|
data[index1] * dx1 * dy1 +
|
||||||
|
data[index2] * dx * dy1 +
|
||||||
|
data[index3] * dx1 * dy +
|
||||||
|
data[index4] * dx * dy;
|
||||||
|
const g =
|
||||||
|
data[index1 + 1] * dx1 * dy1 +
|
||||||
|
data[index2 + 1] * dx * dy1 +
|
||||||
|
data[index3 + 1] * dx1 * dy +
|
||||||
|
data[index4 + 1] * dx * dy;
|
||||||
|
const b =
|
||||||
|
data[index1 + 2] * dx1 * dy1 +
|
||||||
|
data[index2 + 2] * dx * dy1 +
|
||||||
|
data[index3 + 2] * dx1 * dy +
|
||||||
|
data[index4 + 2] * dx * dy;
|
||||||
|
const a =
|
||||||
|
data[index1 + 3] * dx1 * dy1 +
|
||||||
|
data[index2 + 3] * dx * dy1 +
|
||||||
|
data[index3 + 3] * dx1 * dy +
|
||||||
|
data[index4 + 3] * dx * dy;
|
||||||
|
return [Math.round(r), Math.round(g), Math.round(b), Math.round(a)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 双三次插值实现 - 确保正确处理Alpha通道
|
* 三次插值实现 - 确保正确处理Alpha通道
|
||||||
* @param {Uint8ClampedArray} data 图像数据
|
* @param {Uint8ClampedArray} data 图像数据
|
||||||
* @param {number} width 图像宽度
|
* @param {number} width 图像宽度
|
||||||
* @param {number} height 图像高度
|
* @param {number} height 图像高度
|
||||||
* @param {number} x X坐标
|
* @param {number} x X坐标
|
||||||
* @param {number} y Y坐标
|
* @param {number} y Y坐标
|
||||||
* @returns {Array|null} RGBA颜色值数组或null
|
* @returns {Array|null} RGBA颜色值数组或null
|
||||||
*/
|
*/
|
||||||
_bicubicInterpolate(data, width, height, x, y) {
|
_bicubicInterpolate(data, width, height, x, y) {
|
||||||
|
// return this._bilinearInterpolate(data, width, height, x, y);
|
||||||
|
|
||||||
// 获取周围16个像素点
|
// 获取周围16个像素点
|
||||||
const x1 = Math.floor(x) - 1;
|
const x1 = Math.floor(x) - 1;
|
||||||
const y1 = Math.floor(y) - 1;
|
const y1 = Math.floor(y) - 1;
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export class LiquifyWebGLManager {
|
|||||||
this.isHolding = true;
|
this.isHolding = true;
|
||||||
|
|
||||||
// 启动持续效果定时器
|
// 启动持续效果定时器
|
||||||
this.startContinuousEffect();
|
// this.startContinuousEffect();
|
||||||
|
|
||||||
console.log(`WebGL液化开始,初始点: (${x}, ${y})`);
|
console.log(`WebGL液化开始,初始点: (${x}, ${y})`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export class LayerSort {
|
|||||||
} else if (layer.isFixed && layer.fabricObject) {
|
} else if (layer.isFixed && layer.fabricObject) {
|
||||||
// 固定图层对象
|
// 固定图层对象
|
||||||
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||||
|
} else if (layer.isFixedOther && layer.fabricObject) {
|
||||||
|
// 其他固定图层对象
|
||||||
|
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||||
} else if (!layer.isBackground && !layer.isFixed) {
|
} else if (!layer.isBackground && !layer.isFixed) {
|
||||||
// 普通图层
|
// 普通图层
|
||||||
currentZIndex = this.processLayerObjects(
|
currentZIndex = this.processLayerObjects(
|
||||||
|
|||||||
37
src/component/Canvas/CanvasEditor/utils/event.js
Normal file
37
src/component/Canvas/CanvasEditor/utils/event.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
class EventManager {
|
||||||
|
constructor() {
|
||||||
|
this.eventMap = {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册事件
|
||||||
|
* @param {string} eventName - 事件名称
|
||||||
|
* @param {function} callback - 事件回调函数
|
||||||
|
*/
|
||||||
|
on(eventName, callback) {
|
||||||
|
if (!this.eventMap[eventName]) {
|
||||||
|
this.eventMap[eventName] = [];
|
||||||
|
}
|
||||||
|
this.eventMap[eventName].push(callback);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 触发事件
|
||||||
|
* @param {string} eventName - 事件名称
|
||||||
|
* @param {...any} args - 事件参数
|
||||||
|
*/
|
||||||
|
emit(eventName, ...args) {
|
||||||
|
if (this.eventMap[eventName]) {
|
||||||
|
this.eventMap[eventName].forEach(callback => callback(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 移除事件
|
||||||
|
* @param {string} eventName - 事件名称
|
||||||
|
* @param {function} callback - 事件回调函数
|
||||||
|
*/
|
||||||
|
off(eventName, callback) {
|
||||||
|
if (this.eventMap[eventName]) {
|
||||||
|
this.eventMap[eventName] = this.eventMap[eventName].filter(cb => cb !== callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new EventManager();
|
||||||
@@ -429,7 +429,8 @@ export function objectIsInCanvas(canvas, targetObj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetId = targetObj.id;
|
const targetId = targetObj.id;
|
||||||
if (!targetId) {
|
const targetLayerId = targetObj.layerId;
|
||||||
|
if (!targetId && !targetLayerId) {
|
||||||
return { flag: false, object: null, parent: null };
|
return { flag: false, object: null, parent: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +438,11 @@ export function objectIsInCanvas(canvas, targetObj) {
|
|||||||
const topLevelObjects = canvas.getObjects();
|
const topLevelObjects = canvas.getObjects();
|
||||||
|
|
||||||
// 直接在顶层查找
|
// 直接在顶层查找
|
||||||
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
|
const directMatch = topLevelObjects.find((obj) => {
|
||||||
|
const isId = !targetId ? true : obj.id === targetId;
|
||||||
|
const isLayerId = !targetLayerId ? true : obj.layerId === targetLayerId;
|
||||||
|
return isId && isLayerId;
|
||||||
|
});
|
||||||
if (directMatch) {
|
if (directMatch) {
|
||||||
return { flag: true, object: directMatch, parent: null };
|
return { flag: true, object: directMatch, parent: null };
|
||||||
}
|
}
|
||||||
@@ -500,6 +505,22 @@ export function findObjectById(canvas, objectId) {
|
|||||||
return { object: result.object, parent: result.parent };
|
return { object: result.object, parent: result.parent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过layerID查找对象(增强版)
|
||||||
|
* @param {fabric.Canvas} canvas 画布实例
|
||||||
|
* @param {string} layerId 图层ID
|
||||||
|
* @returns {Object} { object: fabric.Object|null, parent: fabric.Group|null }
|
||||||
|
*/
|
||||||
|
export function findObjectByLayerId(canvas, layerId) {
|
||||||
|
if (!canvas || !layerId) {
|
||||||
|
return { object: null, parent: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = objectIsInCanvas(canvas, { layerId: layerId });
|
||||||
|
return { object: result.object, parent: result.parent };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安全移除画布对象(包括组内对象)
|
* 安全移除画布对象(包括组内对象)
|
||||||
* @param {fabric.Canvas} canvas 画布实例
|
* @param {fabric.Canvas} canvas 画布实例
|
||||||
@@ -738,3 +759,203 @@ export function getLayerObjectsZIndex(canvas, layerId) {
|
|||||||
const allInfo = getAllObjectsZIndex(canvas);
|
const allInfo = getAllObjectsZIndex(canvas);
|
||||||
return allInfo.filter((info) => info.layerId === layerId);
|
return allInfo.filter((info) => info.layerId === layerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两点之间的角度
|
||||||
|
* @param {number} x1 第一个点的x坐标
|
||||||
|
* @param {number} y1 第一个点的y坐标
|
||||||
|
* @param {number} x2 第二个点的x坐标
|
||||||
|
* @param {number} y2 第二个点的y坐标
|
||||||
|
* @returns {number} 角度值(-90 - 270度)
|
||||||
|
*/
|
||||||
|
export function calculateAngle(x1, y1, x2, y2, int = false) {
|
||||||
|
// 计算两点之间的差值
|
||||||
|
const deltaX = x2 - x1;
|
||||||
|
const deltaY = y2 - y1;
|
||||||
|
|
||||||
|
// 使用Math.atan2计算弧度,然后转换为角度
|
||||||
|
let angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI + 90;
|
||||||
|
|
||||||
|
return int ? Math.round(angle) : angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过角度计算直线上的两点坐标返回0-1范围的坐标
|
||||||
|
* @param {number} angle 角度值(0-360度)
|
||||||
|
* @returns {{x1: number, y1: number, x2: number, y2: number}} 包含两个点坐标
|
||||||
|
*/
|
||||||
|
export function calculateLinePoints(angle) {
|
||||||
|
// 将角度转换为弧度
|
||||||
|
const radian = (angle - 90) * Math.PI / 180;
|
||||||
|
|
||||||
|
// 计算直线上的两点坐标
|
||||||
|
const x1 = 0.5 - 0.5 * Math.cos(radian);
|
||||||
|
const y1 = 0.5 - 0.5 * Math.sin(radian);
|
||||||
|
const x2 = 0.5 + 0.5 * Math.cos(radian);
|
||||||
|
const y2 = 0.5 + 0.5 * Math.sin(radian);
|
||||||
|
|
||||||
|
return {x1, y1, x2, y2};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbaToHex(rgba){
|
||||||
|
const r = rgba.r.toString(16).padStart(2, "0");
|
||||||
|
const g = rgba.g.toString(16).padStart(2, "0");
|
||||||
|
const b = rgba.b.toString(16).padStart(2, "0");
|
||||||
|
return `#${r}${g}${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillToPallet(fill) {
|
||||||
|
if(!fill.coords || !fill.colorStops) return {};
|
||||||
|
const angle = calculateAngle(fill.coords.x1, fill.coords.y1, fill.coords.x2, fill.coords.y2);
|
||||||
|
const colors = new Set();
|
||||||
|
// console.log("==========fill", fill);
|
||||||
|
const gradientList = fill.colorStops.map((stop) => {
|
||||||
|
colors.add(stop.color);
|
||||||
|
const rgbas = stop.color.replace("rgb(", "").replace("rgba(", "").replace(")", "").split(", ");
|
||||||
|
const rgba = {
|
||||||
|
r: parseInt(rgbas[0]),
|
||||||
|
g: parseInt(rgbas[1]),
|
||||||
|
b: parseInt(rgbas[2]),
|
||||||
|
a: parseFloat(rgbas[3]),
|
||||||
|
};
|
||||||
|
if(isNaN(rgba.r)) rgba.r = 0;
|
||||||
|
if(isNaN(rgba.g)) rgba.g = 0;
|
||||||
|
if(isNaN(rgba.b)) rgba.b = 0;
|
||||||
|
if(isNaN(rgba.a)) rgba.a = 1;
|
||||||
|
return {
|
||||||
|
rgba: rgba,
|
||||||
|
left: parseInt(stop.offset * 100) + "%",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const isGradient = colors.size > 1;
|
||||||
|
if(isGradient) {
|
||||||
|
return {
|
||||||
|
// hex: rgbaToHex(gradientList[0].rgba),
|
||||||
|
rgba: gradientList[0].rgba,
|
||||||
|
gradient:{ angle, selectIndex: 0, gradientShow: true, gradientList },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
hex: rgbaToHex(gradientList[0].rgba),
|
||||||
|
rgba: gradientList[0].rgba,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function palletToFill(pallet) {
|
||||||
|
const fill = {
|
||||||
|
coords: calculateLinePoints(0),
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: "rgba(0, 0, 0, 0)" },
|
||||||
|
{ offset: 1, color: "rgba(0, 0, 0, 0)" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if(pallet?.gradient){
|
||||||
|
let obj = pallet.gradient;
|
||||||
|
fill.coords = calculateLinePoints(obj.angle);
|
||||||
|
if(obj.gradientList.length >= 2){
|
||||||
|
fill.colorStops = obj.gradientList.map(item => ({
|
||||||
|
offset: parseInt(item.left) / 100,
|
||||||
|
color: `rgba(${item.rgba.r}, ${item.rgba.g}, ${item.rgba.b}, ${item.rgba.a})`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}else if(pallet?.rgba?.hasOwnProperty("r") && pallet?.rgba?.hasOwnProperty("g") && pallet?.rgba?.hasOwnProperty("b")){
|
||||||
|
let rgba = pallet.rgba;
|
||||||
|
fill.colorStops = [
|
||||||
|
{ offset: 0, color: `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` },
|
||||||
|
{ offset: 1, color: `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillToCssStyle(fill) {
|
||||||
|
if(!fill.coords || !fill.colorStops) return "";
|
||||||
|
const angle = calculateAngle(fill.coords.x1, fill.coords.y1, fill.coords.x2, fill.coords.y2);
|
||||||
|
if(fill.colorStops.every(v => v.color === fill.colorStops[0].color)){
|
||||||
|
return fill.colorStops[0].color;
|
||||||
|
}else{
|
||||||
|
var str = "linear-gradient(" + angle + "deg, ";
|
||||||
|
fill.colorStops.forEach((v) => {
|
||||||
|
str += `${v.color} ${v.offset * 100}%, `
|
||||||
|
})
|
||||||
|
return str.slice(0, -2) + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据左上角坐标计算旋转后的新坐标
|
||||||
|
* @param {number} W - 宽度
|
||||||
|
* @param {number} H - 高度
|
||||||
|
* @param {number} currentX - 当前左上角x坐标
|
||||||
|
* @param {number} currentY - 当前左上角y坐标
|
||||||
|
* @param {number} currentAngleDeg - 当前角度(度)
|
||||||
|
* @param {number} newAngleDeg - 新角度(度)
|
||||||
|
* @returns {Object} 旋转后的左上角坐标 {x, y}
|
||||||
|
*/
|
||||||
|
export function calculateRotatedTopLeftDeg(
|
||||||
|
W,
|
||||||
|
H,
|
||||||
|
currentX,
|
||||||
|
currentY,
|
||||||
|
currentAngleDeg,
|
||||||
|
newAngleDeg
|
||||||
|
) {
|
||||||
|
const currentAngle = (currentAngleDeg * Math.PI) / 180;
|
||||||
|
const newAngle = (newAngleDeg * Math.PI) / 180;
|
||||||
|
// 1. 用当前角度计算中心点位置
|
||||||
|
const cosCurrent = Math.cos(currentAngle);
|
||||||
|
const sinCurrent = Math.sin(currentAngle);
|
||||||
|
const Cx = currentX + (W / 2) * cosCurrent - (H / 2) * sinCurrent;
|
||||||
|
const Cy = currentY + (W / 2) * sinCurrent + (H / 2) * cosCurrent;
|
||||||
|
|
||||||
|
// 2. 用新角度计算旋转后的左上角位置
|
||||||
|
const cosNew = Math.cos(newAngle);
|
||||||
|
const sinNew = Math.sin(newAngle);
|
||||||
|
const newX = Cx + (-W / 2) * cosNew - (-H / 2) * sinNew;
|
||||||
|
const newY = Cy + (-W / 2) * sinNew + (-H / 2) * cosNew;
|
||||||
|
|
||||||
|
return { x: newX, y: newY };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建缩放+旋转的变换矩阵
|
||||||
|
* @param {number} scale - 缩放比例
|
||||||
|
* @param {number} angle - 旋转角度(度)
|
||||||
|
* @returns {Array} 变换矩阵 [a, b, c, d, e, f]
|
||||||
|
*/
|
||||||
|
export function createPatternTransform(scale, angle) {
|
||||||
|
// return fabric.util.composeMatrix({
|
||||||
|
// scaleX: scale,
|
||||||
|
// scaleY: scale,
|
||||||
|
// angle: angle,
|
||||||
|
// });
|
||||||
|
const angle_ = angle * Math.PI / 180;
|
||||||
|
const cos = Math.cos(angle_);
|
||||||
|
const sin = Math.sin(angle_);
|
||||||
|
|
||||||
|
// 先缩放,后旋转
|
||||||
|
return [
|
||||||
|
scale * cos, // a
|
||||||
|
scale * sin, // b
|
||||||
|
-scale * sin, // c
|
||||||
|
scale * cos, // d
|
||||||
|
0, // e (水平位移)
|
||||||
|
0 // f (垂直位移)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取变换矩阵的缩放、旋转
|
||||||
|
* @param {Array} Transform - 变换矩阵、
|
||||||
|
* @returns {Object} 缩放、旋转角度 {scale, angle}
|
||||||
|
*/
|
||||||
|
export function getTransformScaleAngle(Transform) {
|
||||||
|
const a = Transform[0];
|
||||||
|
const b = Transform[1];
|
||||||
|
const c = Transform[2];
|
||||||
|
const d = Transform[3];
|
||||||
|
const scale = Math.sqrt(a * a + b * b);
|
||||||
|
const angle = Math.round(Math.atan2(b, a) * 180 / Math.PI);
|
||||||
|
return { scale, angle };
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
function initAligningGuidelines(canvas) {
|
function initAligningGuidelines(canvas) {
|
||||||
var ctx = canvas.getSelectionContext(),
|
var ctx = canvas.getSelectionContext(),
|
||||||
aligningLineOffset = 5,
|
aligningLineOffset = 1,
|
||||||
aligningLineMargin = 4,
|
aligningLineMargin = 1,
|
||||||
aligningLineWidth = 1,
|
aligningLineWidth = 1,
|
||||||
aligningLineColor = "rgb(0,255,0)",
|
aligningLineColor = "rgb(0,255,0)",
|
||||||
viewportTransform,
|
viewportTransform,
|
||||||
@@ -14,9 +14,9 @@ function initAligningGuidelines(canvas) {
|
|||||||
|
|
||||||
function drawVerticalLine(coords) {
|
function drawVerticalLine(coords) {
|
||||||
drawLine(
|
drawLine(
|
||||||
coords.x + 0.5,
|
coords.x,
|
||||||
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
|
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
|
||||||
coords.x + 0.5,
|
coords.x,
|
||||||
coords.y2 > coords.y1 ? coords.y2 : coords.y1
|
coords.y2 > coords.y1 ? coords.y2 : coords.y1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,9 @@ function initAligningGuidelines(canvas) {
|
|||||||
function drawHorizontalLine(coords) {
|
function drawHorizontalLine(coords) {
|
||||||
drawLine(
|
drawLine(
|
||||||
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
|
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
|
||||||
coords.y + 0.5,
|
coords.y,
|
||||||
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
|
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
|
||||||
coords.y + 0.5
|
coords.y
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ export function initCenteringGuidelines(canvas) {
|
|||||||
canvasHeightCenter = canvasHeight / 2,
|
canvasHeightCenter = canvasHeight / 2,
|
||||||
canvasWidthCenterMap = {},
|
canvasWidthCenterMap = {},
|
||||||
canvasHeightCenterMap = {},
|
canvasHeightCenterMap = {},
|
||||||
centerLineMargin = 4,
|
centerLineMargin = 1,
|
||||||
centerLineColor = "rgba(255,0,241,0.5)",
|
centerLineColor = "rgba(255,0,241,0.5)",
|
||||||
centerLineWidth = 1,
|
centerLineWidth = 1,
|
||||||
ctx = canvas.getSelectionContext(),
|
ctx = canvas.getSelectionContext(),
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ export const LayerType = {
|
|||||||
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特殊图层ID
|
||||||
|
*/
|
||||||
|
export const SpecialLayerId = {
|
||||||
|
SPECIAL_GROUP: "group_special", // 特殊组
|
||||||
|
COLOR: "special_color", // 颜色图层
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 画布操作模式枚举:draw(绘画)、select(选择)、pan(拖拽)....
|
* 画布操作模式枚举:draw(绘画)、select(选择)、pan(拖拽)....
|
||||||
*/
|
*/
|
||||||
@@ -178,12 +188,17 @@ export function createLayer(options = {}) {
|
|||||||
locked: options.locked !== undefined ? options.locked : false,
|
locked: options.locked !== undefined ? options.locked : false,
|
||||||
opacity: options.opacity !== undefined ? options.opacity : 1.0,
|
opacity: options.opacity !== undefined ? options.opacity : 1.0,
|
||||||
blendMode: options.blendMode || BlendMode.NORMAL,
|
blendMode: options.blendMode || BlendMode.NORMAL,
|
||||||
|
isHidenDragHandle: options.isHidenDragHandle || false,
|
||||||
|
isDisableUnlock: options.isDisableUnlock || false,
|
||||||
|
isFixedOther: options.isFixedOther || false,
|
||||||
|
isFixedClipMask: options.isFixedClipMask || false,
|
||||||
|
|
||||||
// 确保不是背景图层
|
// 确保不是背景图层
|
||||||
isBackground: false,
|
isBackground: false,
|
||||||
|
|
||||||
// Fabric.js 对象列表
|
// Fabric.js 对象列表
|
||||||
fabricObjects: options.fabricObjects || [],
|
fabricObjects: options.fabricObjects || [],
|
||||||
|
fabricObject: options.fabricObject || null,
|
||||||
|
|
||||||
// 嵌套结构 - 适用于组图层
|
// 嵌套结构 - 适用于组图层
|
||||||
children: options.children || [],
|
children: options.children || [],
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ export function simplifyLayers(layers) {
|
|||||||
opacity: layer.opacity,
|
opacity: layer.opacity,
|
||||||
isBackground: layer.isBackground || false,
|
isBackground: layer.isBackground || false,
|
||||||
isFixed: layer.isFixed || false,
|
isFixed: layer.isFixed || false,
|
||||||
|
isFixedOther: layer.isFixedOther || false,
|
||||||
|
isFixedClipMask: layer.isFixedClipMask || false,
|
||||||
|
isHidenDragHandle: layer.isHidenDragHandle || false,
|
||||||
|
isDisableUnlock: layer.isDisableUnlock || false,
|
||||||
clippingMask:
|
clippingMask:
|
||||||
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
|
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
|
||||||
layer.clippingMask ||
|
layer.clippingMask ||
|
||||||
|
|||||||
@@ -7,55 +7,92 @@ import { fabric } from "fabric-with-all";
|
|||||||
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
|
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
|
||||||
*/
|
*/
|
||||||
export async function restoreFabricObject(serializedObject, canvas) {
|
export async function restoreFabricObject(serializedObject, canvas) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const objectType = serializedObject.type;
|
const objectType = serializedObject.type;
|
||||||
// 定义恢复后的处理函数
|
// 定义恢复后的处理函数
|
||||||
const handleRestoredObject = (fabricObject) => {
|
const handleRestoredObject = (fabricObject) => {
|
||||||
if (!fabricObject) {
|
if (!fabricObject) {
|
||||||
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
|
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 恢复自定义属性
|
||||||
|
if (serializedObject.id) fabricObject.id = serializedObject.id;
|
||||||
|
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
|
||||||
|
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
|
||||||
|
|
||||||
// 恢复自定义属性
|
// 更新坐标
|
||||||
if (serializedObject.id) fabricObject.id = serializedObject.id;
|
fabricObject.setCoords();
|
||||||
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
|
|
||||||
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
|
|
||||||
|
|
||||||
// 更新坐标
|
// 添加到画布
|
||||||
fabricObject.setCoords();
|
// canvas.add(fabricObject);
|
||||||
|
|
||||||
// 添加到画布
|
resolve(fabricObject);
|
||||||
// canvas.add(fabricObject);
|
};
|
||||||
|
|
||||||
resolve(fabricObject);
|
// 根据类型选择恢复方法
|
||||||
};
|
switch (objectType) {
|
||||||
|
case "rect":
|
||||||
// 根据类型选择恢复方法
|
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
|
||||||
switch (objectType) {
|
break;
|
||||||
case "rect":
|
case "circle":
|
||||||
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
|
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "circle":
|
case "path":
|
||||||
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
|
fabric.Path.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "path":
|
case "image":
|
||||||
fabric.Path.fromObject(serializedObject, handleRestoredObject);
|
fabric.Image.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "image":
|
case "group":
|
||||||
fabric.Image.fromObject(serializedObject, handleRestoredObject);
|
fabric.Group.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "group":
|
default:
|
||||||
fabric.Group.fromObject(serializedObject, handleRestoredObject);
|
// 使用通用方法
|
||||||
break;
|
fabric.util.enlivenObjects([serializedObject], (objects) => {
|
||||||
default:
|
if (objects && objects[0]) {
|
||||||
// 使用通用方法
|
handleRestoredObject(objects[0]);
|
||||||
fabric.util.enlivenObjects([serializedObject], (objects) => {
|
} else {
|
||||||
if (objects && objects[0]) {
|
reject(new Error("对象恢复失败"));
|
||||||
handleRestoredObject(objects[0]);
|
}
|
||||||
} else {
|
});
|
||||||
reject(new Error("对象恢复失败"));
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对象黑白通道画布
|
||||||
|
*/
|
||||||
|
export function getObjectAlphaToCanvas(object) {
|
||||||
|
const image = object.getElement();
|
||||||
|
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];
|
||||||
|
if (r || g || b || a) {
|
||||||
|
data.data[i + 0] = 255;
|
||||||
|
data.data[i + 1] = 255;
|
||||||
|
data.data[i + 2] = 255;
|
||||||
|
data.data[i + 3] = 255;
|
||||||
|
}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;
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ export const createRasterizedImage = async ({
|
|||||||
isReturenDataURL,
|
isReturenDataURL,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("创建栅格化图像失败:", error);
|
console.warn("创建栅格化图像失败:", error);
|
||||||
throw new Error(`栅格化失败: ${error.message}`);
|
throw new Error(`栅格化失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ const createClippedObjects = async ({
|
|||||||
console.log("✅ 返回裁剪后的fabric对象,已恢复到优化后的原始大小和位置");
|
console.log("✅ 返回裁剪后的fabric对象,已恢复到优化后的原始大小和位置");
|
||||||
return fabricImage;
|
return fabricImage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("创建裁剪对象失败:", error);
|
console.warn("创建裁剪对象失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1239,7 +1239,7 @@ const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
|
|||||||
|
|
||||||
return optimizedBounds;
|
return optimizedBounds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("计算优化边界框失败:", error);
|
console.warn("计算优化边界框失败:", error);
|
||||||
// 返回原始计算方式作为备选
|
// 返回原始计算方式作为备选
|
||||||
return clippingObject.getBoundingRect(true, true);
|
return clippingObject.getBoundingRect(true, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ onUnmounted(() => {
|
|||||||
:style="tool.style"
|
:style="tool.style"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<SvgIcon :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
|
<SvgIcon v-if="tool.icon" :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
|
||||||
|
<span v-else>{{ tool.label }}</span>
|
||||||
<teleport to="body" v-if="tipBody">
|
<teleport to="body" v-if="tipBody">
|
||||||
<div class="tool-tooltip" :id="tipId">{{ t(tool.title) }}</div>
|
<div class="tool-tooltip" :id="tipId">{{ t(tool.title) }}</div>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const changeFixedImage = () => {
|
|||||||
canvasEditor.value.changeFixedImage(changeImageUrl);
|
canvasEditor.value.changeFixedImage(changeImageUrl);
|
||||||
};
|
};
|
||||||
const frontBackChange = (value) =>{
|
const frontBackChange = (value) =>{
|
||||||
console.log(value)
|
console.log("==========红绿图导出url", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时绑定键盘事件
|
// 组件挂载时绑定键盘事件
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ToolButton from "@/component/Canvas/ExistsImageList/ToolButton.vue";
|
|||||||
const canvasEditor = ref();
|
const canvasEditor = ref();
|
||||||
const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample
|
const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample
|
||||||
|
|
||||||
const clothingImageUrl = "/src/assets/work/3.PNG";
|
const clothingImageUrl = "https://www.minio-api.aida.com.hk/aida-collection-element/24299/Printboard/4eba03bd-4367-4c69-b1a3-3f3177a1be1f.jpg?response-content-type=image%2Fjpeg&response-content-disposition=inline&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20251217%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251217T081126Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=b8223ba37625370c9716024536a08ce1ee5c5a7aaefc47d9baf8bd1e0e4d2d91";
|
||||||
const clothingImageUrlInit = "/src/assets/work/5.PNG";
|
const clothingImageUrlInit = "/src/assets/work/5.PNG";
|
||||||
|
|
||||||
const imageData = [
|
const imageData = [
|
||||||
@@ -84,6 +84,20 @@ const exportImage = async () => {
|
|||||||
document.body.removeChild(link); // 下载后移除链接元素
|
document.body.removeChild(link); // 下载后移除链接元素
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 导出颜色图层
|
||||||
|
const exportColorLayer = async () => {
|
||||||
|
if (canvasEditor.value) {
|
||||||
|
const colorLayer = await canvasEditor.value.exportColorLayer();
|
||||||
|
console.log("导出颜色图层:",colorLayer);
|
||||||
|
// 模拟下载图片
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = colorLayer.base64;
|
||||||
|
link.download = "canvas_image.png"; // 设置下载文件名
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click(); // 触发下载
|
||||||
|
document.body.removeChild(link); // 下载后移除链接元素
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const changeCanvas = (command) => {
|
const changeCanvas = (command) => {
|
||||||
console.log(command);
|
console.log(command);
|
||||||
@@ -106,32 +120,6 @@ const loadImageUrlToLayer = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义工具配置相关
|
|
||||||
const customToolsList = ref([
|
|
||||||
{
|
|
||||||
id: "exportPNG",
|
|
||||||
title: "导出PNG", //导出画布图片
|
|
||||||
action: exportAsPNG,
|
|
||||||
icon: { name: "CExport", size: "24" },
|
|
||||||
class: "export-btn",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "saveCanvas",
|
|
||||||
title: "保存画布",
|
|
||||||
action: saveCanvas,
|
|
||||||
icon: { name: "CBottom", size: "24" },
|
|
||||||
class: "save-btn",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: "readCanvas",
|
|
||||||
title: "读取画布",
|
|
||||||
action: canvasProject,
|
|
||||||
icon: { name: "CMiniMap", size: "24" },
|
|
||||||
class: "clear-btn",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 自定义工具方法
|
// 自定义工具方法
|
||||||
function exportAsPNG() {
|
function exportAsPNG() {
|
||||||
console.log("导出PNG");
|
console.log("导出PNG");
|
||||||
@@ -201,10 +189,89 @@ const canvasInit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const frontBackChange =(value)=>{
|
const frontBackChange =(value)=>{
|
||||||
console.log(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isShowLeft = ref(true);
|
// 自定义工具配置相关
|
||||||
|
const customToolsList = ref([
|
||||||
|
{
|
||||||
|
id: "exportPNG",
|
||||||
|
title: "导出颜色图层",
|
||||||
|
action: exportColorLayer,
|
||||||
|
label: "导颜",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exportPNG",
|
||||||
|
title: "导出PNG", //导出画布图片
|
||||||
|
action: exportAsPNG,
|
||||||
|
icon: { name: "CExport", size: "24" },
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "saveCanvas",
|
||||||
|
title: "保存画布",
|
||||||
|
action: saveCanvas,
|
||||||
|
icon: { name: "CBottom", size: "24" },
|
||||||
|
class: "save-btn",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "readCanvas",
|
||||||
|
title: "读取画布",
|
||||||
|
action: canvasProject,
|
||||||
|
icon: { name: "CMiniMap", size: "24" },
|
||||||
|
class: "clear-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "loadImageUrlToLayer",
|
||||||
|
title: "添加画布图片",
|
||||||
|
action: loadImageUrlToLayer,
|
||||||
|
label: "🎨",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "redGreenExample",
|
||||||
|
title: "红绿图模式",
|
||||||
|
action: () => switchView('redGreenExample'),
|
||||||
|
label: "红",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "canvasEditor",
|
||||||
|
title: "普通模式",
|
||||||
|
action: () => switchView('canvasEditor'),
|
||||||
|
label: "普",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "changeFixedImage",
|
||||||
|
title: "更换底图",
|
||||||
|
action: changeFixedImage,
|
||||||
|
label: "更",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exportJSON",
|
||||||
|
title: "导出JSON",
|
||||||
|
action: exportJSON,
|
||||||
|
label: "导J",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "copyJSON",
|
||||||
|
title: "复制JSON",
|
||||||
|
action: copyJSON,
|
||||||
|
label: "复J",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "getLayers",
|
||||||
|
title: "查询图层",
|
||||||
|
action: getLayers,
|
||||||
|
label: "查L",
|
||||||
|
class: "export-btn",
|
||||||
|
},
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -250,46 +317,15 @@ const isShowLeft = ref(true);
|
|||||||
<template #customToolsBottom="{ toolButtonProps }">
|
<template #customToolsBottom="{ toolButtonProps }">
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="tool-separator"></div>
|
<div class="tool-separator"></div>
|
||||||
|
<!-- 自定义工具按钮 -->
|
||||||
<!-- 自定义工具按钮 -->
|
|
||||||
<ToolButton
|
<ToolButton
|
||||||
v-for="tool in customToolsList"
|
v-for="tool in customToolsList"
|
||||||
:key="tool.id"
|
:key="tool.id"
|
||||||
:tool="tool"
|
:tool="tool"
|
||||||
:active-tool="toolButtonProps.activeTool"
|
:active-tool="toolButtonProps.activeTool"
|
||||||
@click="handleCustomToolClick"
|
@click="handleCustomToolClick"
|
||||||
|
tipBody
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 也可以直接使用普通的按钮 -->
|
|
||||||
<div class="custom-tool-btn" @click="loadImageUrlToLayer">
|
|
||||||
<span>🎨</span>
|
|
||||||
<div class="tool-tooltip">添加画布图片</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="custom-tool-btn" @click="switchView('redGreenExample')">
|
|
||||||
<span>红</span>
|
|
||||||
<div class="tool-tooltip">红绿图模式</div>
|
|
||||||
</div>
|
|
||||||
<div class="custom-tool-btn" @click="switchView('canvasEditor')">
|
|
||||||
<span>普</span>
|
|
||||||
<div class="tool-tooltip">普通模式</div>
|
|
||||||
</div>
|
|
||||||
<div class="custom-tool-btn" @click="changeFixedImage">
|
|
||||||
<span>更</span>
|
|
||||||
<div class="tool-tooltip">更换底图</div>
|
|
||||||
</div>
|
|
||||||
<div class="custom-tool-btn" @click="exportJSON">
|
|
||||||
<span>导</span>
|
|
||||||
<div class="tool-tooltip">导出JSON</div>
|
|
||||||
</div>
|
|
||||||
<div class="custom-tool-btn" @click="copyJSON">
|
|
||||||
<span>复</span>
|
|
||||||
<div class="tool-tooltip">复制JSON</div>
|
|
||||||
</div>
|
|
||||||
<div class="custom-tool-btn" @click="getLayers">
|
|
||||||
<span>查</span>
|
|
||||||
<div class="tool-tooltip">查询图层</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</CanvasEditor>
|
</CanvasEditor>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,15 +460,17 @@ body {
|
|||||||
|
|
||||||
.tool-tooltip {
|
.tool-tooltip {
|
||||||
display: none;
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
writing-mode: vertical-rl; /* 竖直排列 */
|
||||||
top: 50%;
|
text-orientation: upright; /* 保持文字正常显示 */// left: 100%;
|
||||||
transform: translateY(-50%);
|
left: 50%;
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
top: -0.8rem;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.8rem 0.4rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
margin-left: 0.8rem;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -441,12 +479,12 @@ body {
|
|||||||
.tool-tooltip:before {
|
.tool-tooltip:before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
left: 50%;
|
||||||
right: 100%;
|
bottom: 0;
|
||||||
margin-top: -0.5rem;
|
transform: translate(-50%, 100%);
|
||||||
border-width: 0.5rem;
|
border-width: 0.5rem;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: transparent rgba(0, 0, 0, 0.7) transparent transparent;
|
border-color: rgba(0, 0, 0, 0.9) transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 深色模式适配 */
|
/* 深色模式适配 */
|
||||||
|
|||||||
@@ -5,14 +5,17 @@
|
|||||||
<div class="canvasContent" ref="canvasContent">
|
<div class="canvasContent" ref="canvasContent">
|
||||||
<div class="content-bottom" ref="canvasContent">
|
<div class="content-bottom" ref="canvasContent">
|
||||||
<div class="contet">
|
<div class="contet">
|
||||||
|
<!-- :clothingImageUrl="selectDetail?.undividedLayerWithSinglePrint || selectDetail.undividedLayer || selectDetail.path" -->
|
||||||
<div class="canvas" v-if="currentView === 'canvasEditor'" @click.stop>
|
<div class="canvas" v-if="currentView === 'canvasEditor'" @click.stop>
|
||||||
<editCanvas v-if="canvasLoad" :config="canvasConfig"
|
<editCanvas v-if="canvasLoad" :config="canvasConfig"
|
||||||
@canvasInit="canvasInit"
|
@canvasInit="canvasInit"
|
||||||
@changeCanvas="changeCanvas"
|
@changeCanvas="changeCanvas"
|
||||||
is-edit
|
is-edit
|
||||||
:clothingImageUrl="selectDetail?.undividedLayerWithSinglePrint || selectDetail.undividedLayer || selectDetail.path"
|
:clothingImageUrl="selectDetail.path"
|
||||||
|
:clothingImageUrl2="selectDetail.undividedLayer"
|
||||||
showFixedLayer
|
showFixedLayer
|
||||||
:canvasJSON="canvasJSON"
|
:canvasJSON="canvasJSON"
|
||||||
|
:otherData="otherData"
|
||||||
:clothing-image-opts="{
|
:clothing-image-opts="{
|
||||||
imageMode:'contains',
|
imageMode:'contains',
|
||||||
}"
|
}"
|
||||||
@@ -108,6 +111,12 @@ export default defineComponent({
|
|||||||
canvasInstance:null as any,
|
canvasInstance:null as any,
|
||||||
canvasJSON:'',
|
canvasJSON:'',
|
||||||
hideCanvas: computed(()=>store.state.Workspace.projectPath !== route.fullPath),
|
hideCanvas: computed(()=>store.state.Workspace.projectPath !== route.fullPath),
|
||||||
|
otherData:computed(()=>({
|
||||||
|
canvasId: store.state.DesignDetail.selectDetail.canvasId,
|
||||||
|
color: store.state.DesignDetail.selectDetail.color,
|
||||||
|
printObject: store.state.DesignDetail.selectDetail.printObject,
|
||||||
|
trims: store.state.DesignDetail.selectDetail.trims,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
watch(()=>detailData.selectDetail,(newValue,oldValue)=>{
|
watch(()=>detailData.selectDetail,(newValue,oldValue)=>{
|
||||||
detailData.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
|
detailData.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
|
||||||
@@ -117,7 +126,6 @@ export default defineComponent({
|
|||||||
provide('canvasType',detailData.canvasType)
|
provide('canvasType',detailData.canvasType)
|
||||||
|
|
||||||
const editFront = (str:any)=>{//编辑前后片
|
const editFront = (str:any)=>{//编辑前后片
|
||||||
|
|
||||||
let canvasJSON = '' as any
|
let canvasJSON = '' as any
|
||||||
if(detailData.currentView === 'canvasEditor'){
|
if(detailData.currentView === 'canvasEditor'){
|
||||||
sessionStorage.setItem('sketchEdit',detailDom.editCanvas.getJSON())
|
sessionStorage.setItem('sketchEdit',detailDom.editCanvas.getJSON())
|
||||||
@@ -309,7 +317,7 @@ export default defineComponent({
|
|||||||
sessionStorage.removeItem('frontBackEdit');
|
sessionStorage.removeItem('frontBackEdit');
|
||||||
sessionStorage.removeItem('sketchEdit');
|
sessionStorage.removeItem('sketchEdit');
|
||||||
detailData.canvasLoad = false
|
detailData.canvasLoad = false
|
||||||
privewDetail()
|
// privewDetail()
|
||||||
})
|
})
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
nextTick(async ()=>{
|
nextTick(async ()=>{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pallet">
|
<div class="pallet" ref="palletRef">
|
||||||
<div class="palletColo" @click="openPallet">
|
<div class="palletColo" @click="openPallet">
|
||||||
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
||||||
{{ selectColor.hex }}
|
{{ selectColor.hex }}
|
||||||
@@ -117,6 +117,7 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
const getpalletListDom = reactive({
|
const getpalletListDom = reactive({
|
||||||
})
|
})
|
||||||
|
const palletRef = ref(null)
|
||||||
watch(()=>palletData.color_,(newVal:any)=>{
|
watch(()=>palletData.color_,(newVal:any)=>{
|
||||||
if(!newVal?.rgba?.r)return
|
if(!newVal?.rgba?.r)return
|
||||||
if(palletData.color?.gradient?.gradientShow){
|
if(palletData.color?.gradient?.gradientShow){
|
||||||
@@ -221,7 +222,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
|
|
||||||
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
||||||
let gradientWidth = (document.querySelector('.pallet .color_setting_operate_bg') as any).clientWidth
|
let gradientWidth = (palletRef.value.querySelector('.color_setting_operate_bg') as any).clientWidth
|
||||||
let position = {
|
let position = {
|
||||||
x:event.clientX,
|
x:event.clientX,
|
||||||
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
||||||
@@ -276,8 +277,8 @@ export default defineComponent({
|
|||||||
// 点击外部区域关闭颜色选择器
|
// 点击外部区域关闭颜色选择器
|
||||||
const handleClickOutside = (event: Event) => {
|
const handleClickOutside = (event: Event) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const colorSettingBlock = document.querySelector('.color_setting_block');
|
const colorSettingBlock = palletRef.value.querySelector('.color_setting_block');
|
||||||
const palletColo = document.querySelector('.palletColo');
|
const palletColo = palletRef.value.querySelector('.palletColo');
|
||||||
|
|
||||||
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
||||||
if (palletData.palletShow && colorSettingBlock &&
|
if (palletData.palletShow && colorSettingBlock &&
|
||||||
@@ -294,7 +295,7 @@ export default defineComponent({
|
|||||||
nextTick().then(()=>{
|
nextTick().then(()=>{
|
||||||
const backIcon = document.createElement('div');
|
const backIcon = document.createElement('div');
|
||||||
backIcon.classList.add('vc-sketch-color-wrap')
|
backIcon.classList.add('vc-sketch-color-wrap')
|
||||||
let dropperDom = document.getElementsByClassName("pallet")?.[0]?.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
let dropperDom = palletRef.value.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
||||||
dropperDom.appendChild(backIcon);
|
dropperDom.appendChild(backIcon);
|
||||||
backIcon.addEventListener('click',async ()=>{
|
backIcon.addEventListener('click',async ()=>{
|
||||||
try {
|
try {
|
||||||
@@ -322,7 +323,7 @@ export default defineComponent({
|
|||||||
return{
|
return{
|
||||||
...toRefs(palletData),
|
...toRefs(palletData),
|
||||||
...toRefs(getpalletListDom),
|
...toRefs(getpalletListDom),
|
||||||
|
palletRef,
|
||||||
openPallet,
|
openPallet,
|
||||||
selectImgItem,
|
selectImgItem,
|
||||||
setOperate,
|
setOperate,
|
||||||
@@ -614,6 +615,7 @@ export default defineComponent({
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
top: 0.2rem;
|
top: 0.2rem;
|
||||||
|
left: 0;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -1433,7 +1433,42 @@ export default {
|
|||||||
selectTexture: '选择要使用的纹理',
|
selectTexture: '选择要使用的纹理',
|
||||||
DeleteTexture: '删除纹理',
|
DeleteTexture: '删除纹理',
|
||||||
TextureSettings: '纹理设置',
|
TextureSettings: '纹理设置',
|
||||||
TextureSelector: '纹理选择'
|
TextureSelector: '纹理选择',
|
||||||
|
// 混合模式
|
||||||
|
CompositeNormal: '正常',
|
||||||
|
CompositeNormalTip: '正常:默认,新图形覆盖原内容',
|
||||||
|
CompositeDarken: '变暗',
|
||||||
|
CompositeDarkenTip: '变暗:取暗部颜色',
|
||||||
|
CompositeMultiply: '正片叠底',
|
||||||
|
CompositeMultiplyTip: '正片叠底:图像变暗',
|
||||||
|
CompositeColorBurn: '颜色加深',
|
||||||
|
CompositeColorBurnTip: '颜色加深:增加对比度,变暗底层颜色',
|
||||||
|
CompositeLighten: '变亮',
|
||||||
|
CompositeLightenTip: '变亮:取亮部颜色',
|
||||||
|
CompositeScreen: '滤色',
|
||||||
|
CompositeScreenTip: '滤色:图像变亮',
|
||||||
|
CompositeColorDodge: '颜色减淡',
|
||||||
|
CompositeColorDodgeTip: '颜色减淡:降低对比度,加亮底层颜色',
|
||||||
|
CompositeLighter: '颜色减淡(添加)',
|
||||||
|
CompositeLighterTip: '颜色减淡(添加):重叠部分亮度叠加',
|
||||||
|
CompositeOverlay: '叠加',
|
||||||
|
CompositeOverlayTip: '叠加:高光效果',
|
||||||
|
CompositeSoftLight: '柔光',
|
||||||
|
CompositeSoftLightTip: '柔光:混合效果',
|
||||||
|
CompositeHardLight: '强光',
|
||||||
|
CompositeHardLightTip: '强光:高光效果',
|
||||||
|
CompositeDifference: '差值',
|
||||||
|
CompositeDifferenceTip: '差值:取两图像颜色差',
|
||||||
|
CompositeExclusion: '排除',
|
||||||
|
CompositeExclusionTip: '排除:取两图像颜色差的绝对值',
|
||||||
|
CompositeHue: '色相',
|
||||||
|
CompositeHueTip: '色相:保留原图像颜色,改变新图像色相',
|
||||||
|
CompositeSaturation: '饱和度',
|
||||||
|
CompositeSaturationTip: '饱和度:保留原图像色相,改变新图像饱和度',
|
||||||
|
CompositeColor: '颜色',
|
||||||
|
CompositeColorTip: '颜色:保留原图像饱和度,改变新图像颜色',
|
||||||
|
CompositeLuminosity: '亮度',
|
||||||
|
CompositeLuminosityTip: '亮度:保留原图像颜色,改变新图像亮度',
|
||||||
},
|
},
|
||||||
speedList: {
|
speedList: {
|
||||||
High: '高级',
|
High: '高级',
|
||||||
|
|||||||
@@ -1433,7 +1433,42 @@ export default {
|
|||||||
selectTexture: 'Select the texture you want to use',
|
selectTexture: 'Select the texture you want to use',
|
||||||
DeleteTexture: 'Delete Texture',
|
DeleteTexture: 'Delete Texture',
|
||||||
TextureSettings: 'Texture Settings',
|
TextureSettings: 'Texture Settings',
|
||||||
TextureSelector: 'Texture Selector'
|
TextureSelector: 'Texture Selector',
|
||||||
|
// 混合模式
|
||||||
|
CompositeNormal: 'Normal',
|
||||||
|
CompositeNormalTip: 'Normal: Default, new graphics cover original content',
|
||||||
|
CompositeDarken: 'Darken',
|
||||||
|
CompositeDarkenTip: 'Darken: Take the darkest color',
|
||||||
|
CompositeMultiply: 'Multiply',
|
||||||
|
CompositeMultiplyTip: 'Multiply: Darken the image',
|
||||||
|
CompositeColorBurn: 'Color Burn',
|
||||||
|
CompositeColorBurnTip: 'Color Burn: Increase contrast and darken the bottom color',
|
||||||
|
CompositeLighten: 'Lighten',
|
||||||
|
CompositeLightenTip: 'Lighten: Take the brightest color',
|
||||||
|
CompositeScreen: 'Screen',
|
||||||
|
CompositeScreenTip: 'Screen: Lighten the image',
|
||||||
|
CompositeColorDodge: 'Color Dodge',
|
||||||
|
CompositeColorDodgeTip: 'Color Dodge: Reduce contrast and lighten the bottom color',
|
||||||
|
CompositeLighter: 'Color Dodge (Add)',
|
||||||
|
CompositeLighterTip: 'Color Dodge (Add): Add the brightness of the overlapping parts',
|
||||||
|
CompositeOverlay: 'Overlay',
|
||||||
|
CompositeOverlayTip: 'Overlay: Highlight effect',
|
||||||
|
CompositeSoftLight: 'Soft Light',
|
||||||
|
CompositeSoftLightTip: 'Soft Light: Blend effect',
|
||||||
|
CompositeHardLight: 'Hard Light',
|
||||||
|
CompositeHardLightTip: 'Hard Light: Highlight effect',
|
||||||
|
CompositeDifference: 'Difference',
|
||||||
|
CompositeDifferenceTip: 'Difference: Take the color difference between the two images',
|
||||||
|
CompositeExclusion: 'Exclusion',
|
||||||
|
CompositeExclusionTip: 'Exclusion: Take the absolute value of the color difference between the two images',
|
||||||
|
CompositeHue: 'Hue',
|
||||||
|
CompositeHueTip: 'Hue: Preserve the original image color and change the hue of the new image',
|
||||||
|
CompositeSaturation: 'Saturation',
|
||||||
|
CompositeSaturationTip: 'Saturation: Preserve the original image hue and change the saturation of the new image',
|
||||||
|
CompositeColor: 'Color',
|
||||||
|
CompositeColorTip: 'Color: Preserve the original image saturation and change the color of the new image',
|
||||||
|
CompositeLuminosity: 'Luminosity',
|
||||||
|
CompositeLuminosityTip: 'Luminosity: Preserve the original image color and change the luminosity of the new image',
|
||||||
},
|
},
|
||||||
speedList: {
|
speedList: {
|
||||||
High: 'High',
|
High: 'High',
|
||||||
|
|||||||
Reference in New Issue
Block a user