@vvfx/sdk 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +231 -0
- package/dist/exporter/config.js +18 -0
- package/dist/exporter/const.js +71 -0
- package/dist/exporter/export-media.js +878 -0
- package/dist/exporter/exporter.js +121 -0
- package/dist/exporter/index.js +1 -0
- package/dist/exporter/types.js +1 -0
- package/dist/exporter/utils.js +190 -0
- package/dist/exporter/wav-audio.js +50 -0
- package/dist/gesture-handler/gizmo/adsorption-gizmo.js +227 -0
- package/dist/gesture-handler/gizmo/control-gizmo.js +149 -0
- package/dist/gesture-handler/gizmo/gizmo.js +17 -0
- package/dist/gesture-handler/gizmo/icon-gizmo.js +189 -0
- package/dist/gesture-handler/gizmo/index.js +6 -0
- package/dist/gesture-handler/gizmo/item-create-gizmo.js +256 -0
- package/dist/gesture-handler/gizmo/loading-gizmo.js +320 -0
- package/dist/gesture-handler/gizmo/mask-gizmo.js +648 -0
- package/dist/gesture-handler/gizmo/picture-cut-gizmo.js +550 -0
- package/dist/gesture-handler/gizmo/picture-expand-gizmo.js +491 -0
- package/dist/gesture-handler/gizmo/preference-gizmo.js +148 -0
- package/dist/gesture-handler/gizmo/selector-gizmo.d.ts +1 -1
- package/dist/gesture-handler/gizmo/selector-gizmo.js +468 -0
- package/dist/gesture-handler/gizmo/sprite-text-edit-gizmo.js +247 -0
- package/dist/gesture-handler/gizmo/text-gizmo.js +1033 -0
- package/dist/gesture-handler/gizmo/transform-gizmo.js +1199 -0
- package/dist/gesture-handler/gizmo/type.js +18 -0
- package/dist/gesture-handler/index.js +3 -0
- package/dist/gesture-handler/src/gesture-handler.js +770 -0
- package/dist/gesture-handler/src/icons.js +24 -0
- package/dist/gesture-handler/utils.js +40 -0
- package/dist/html-overlay/anchor-navigation-runtime.js +27 -0
- package/dist/html-overlay/auto-height-runtime.js +132 -0
- package/dist/html-overlay/document-runtime.js +71 -0
- package/dist/html-overlay/document-transform.js +96 -0
- package/dist/html-overlay/dom-order.js +8 -0
- package/dist/html-overlay/host-message-runtime.js +24 -0
- package/dist/html-overlay/html-capture-runtime.js +282 -0
- package/dist/html-overlay/html-viewport-style.js +10 -0
- package/dist/html-overlay/index.js +1 -0
- package/dist/html-overlay/manager.js +1663 -0
- package/dist/html-overlay/mime-utils.js +21 -0
- package/dist/html-overlay/overlay-transform.js +57 -0
- package/dist/html-overlay/path-utils.js +35 -0
- package/dist/html-overlay/shell-runtime.js +12 -0
- package/dist/index.cjs +1 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -75317
- package/dist/index.mjs +75311 -0
- package/dist/index.mjs.map +1 -0
- package/dist/layer-order.js +8 -0
- package/dist/math/box2.js +396 -0
- package/dist/math/circle.js +131 -0
- package/dist/math/euler.js +35 -0
- package/dist/math/index.js +13 -0
- package/dist/math/line2.js +153 -0
- package/dist/math/line3.js +36 -0
- package/dist/math/matrix4.js +207 -0
- package/dist/math/plane.js +52 -0
- package/dist/math/quaternion.js +3 -0
- package/dist/math/ray-caster.js +25 -0
- package/dist/math/ray.js +47 -0
- package/dist/math/type.js +1 -0
- package/dist/math/utils.js +77 -0
- package/dist/math/vector2.js +70 -0
- package/dist/math/vector3.js +60 -0
- package/dist/screen-shot/index.js +1 -0
- package/dist/screen-shot/screen-shot.js +113 -0
- package/dist/sdk-item/base-item.js +160 -0
- package/dist/sdk-item/card-item.js +131 -0
- package/dist/sdk-item/effects-item.js +144 -0
- package/dist/sdk-item/frame-item.js +217 -0
- package/dist/sdk-item/generator-item.js +188 -0
- package/dist/sdk-item/group-item.js +116 -0
- package/dist/sdk-item/index.js +39 -0
- package/dist/sdk-item/sprite-item.js +147 -0
- package/dist/sdk-item/text-item.js +223 -0
- package/dist/sdk-item/types.d.ts +1 -11
- package/dist/sdk-item/types.js +15 -0
- package/dist/sdk-item/video-item.js +174 -0
- package/dist/sdk.js +1903 -0
- package/dist/service/UndoRedo.js +79 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/player.d.ts +1 -1
- package/dist/shared/player.js +9 -0
- package/dist/shared/spec.d.ts +1 -0
- package/dist/shared/spec.js +1 -0
- package/dist/size-adapte/index.js +1 -0
- package/dist/size-adapte/size-adapt.js +395 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.js +17 -0
- package/dist/utils/background-manager.js +189 -0
- package/dist/utils/common-utils.js +183 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/interaction-utils.js +205 -0
- package/dist/utils/json-data-utils.js +1529 -0
- package/dist/utils/layout-utils.js +1217 -0
- package/dist/utils/page-data-utils.js +5397 -0
- package/dist/utils/player-data-utils.js +48 -0
- package/dist/utils/types.js +8 -0
- package/dist/wireframe/common/box.js +18 -0
- package/dist/wireframe/common/dashed-line.js +21 -0
- package/dist/wireframe/common/line.js +17 -0
- package/dist/wireframe/common/pixi-ext.js +1 -0
- package/dist/wireframe/common/pixi.js +31 -0
- package/dist/wireframe/index.js +4 -0
- package/package.json +10 -3
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { Box2, Vector2 } from '../../math';
|
|
2
|
+
import { SDK } from '../../sdk';
|
|
3
|
+
import { GizmoUndoRedo } from '../../service/UndoRedo';
|
|
4
|
+
import { assertExist } from '../../utils';
|
|
5
|
+
import { BLEND_MODES, Container, Graphics, LINE_CAP, LINE_JOIN, RenderTexture, Sprite, Texture } from '../../wireframe';
|
|
6
|
+
import { Gizmo } from './gizmo';
|
|
7
|
+
export class MaskGizmo extends Gizmo {
|
|
8
|
+
constructor(utils, app, eventEmitter) {
|
|
9
|
+
super();
|
|
10
|
+
this._pageDataUtils = utils;
|
|
11
|
+
this._app = app;
|
|
12
|
+
this._eventEmitter = eventEmitter;
|
|
13
|
+
}
|
|
14
|
+
_pageDataUtils;
|
|
15
|
+
_eventEmitter;
|
|
16
|
+
_app;
|
|
17
|
+
/**
|
|
18
|
+
* 用于持久化蒙版状态的 RenderTexture
|
|
19
|
+
*/
|
|
20
|
+
maskRenderTexture = null;
|
|
21
|
+
/**
|
|
22
|
+
* 用于显示 RenderTexture 的 Sprite
|
|
23
|
+
*/
|
|
24
|
+
maskSprite = null;
|
|
25
|
+
_config = {};
|
|
26
|
+
boxGraphics = new Graphics();
|
|
27
|
+
type = 'mask';
|
|
28
|
+
mode = 'paint';
|
|
29
|
+
/**
|
|
30
|
+
* 蒙版工具独立的撤销重做实例
|
|
31
|
+
*/
|
|
32
|
+
_undoRedo = new GizmoUndoRedo();
|
|
33
|
+
/**
|
|
34
|
+
* 存储绘画开始前的数据,用于 undoRedo
|
|
35
|
+
*/
|
|
36
|
+
_oldLines;
|
|
37
|
+
/**
|
|
38
|
+
* 框选擦除进行中标记(按下到松开之间),用于实时选框描边的显示控制
|
|
39
|
+
*/
|
|
40
|
+
_isBoxErasing = false;
|
|
41
|
+
/**
|
|
42
|
+
* 鼠标状态绘制结果
|
|
43
|
+
*/
|
|
44
|
+
cursorResult = {
|
|
45
|
+
type: 'normal',
|
|
46
|
+
angle: 0,
|
|
47
|
+
};
|
|
48
|
+
result = {
|
|
49
|
+
status: 'init',
|
|
50
|
+
lines: [],
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* 获取蒙版工具的撤销重做实例
|
|
54
|
+
*/
|
|
55
|
+
get undoRedo() {
|
|
56
|
+
return this._undoRedo;
|
|
57
|
+
}
|
|
58
|
+
get interactive() {
|
|
59
|
+
return SDK.config.gestureHandlerConfig.maskGizmoEnabled;
|
|
60
|
+
}
|
|
61
|
+
set interactive(bool) {
|
|
62
|
+
if (bool === SDK.config.gestureHandlerConfig.maskGizmoEnabled) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!bool) {
|
|
66
|
+
this.result.status = 'init';
|
|
67
|
+
}
|
|
68
|
+
SDK.config.gestureHandlerConfig.maskGizmoEnabled = bool;
|
|
69
|
+
}
|
|
70
|
+
get config() {
|
|
71
|
+
return {
|
|
72
|
+
...SDK.config.gestureHandlerConfig.maskGizmoConfig,
|
|
73
|
+
...this._config,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
set config(config) {
|
|
77
|
+
this._config = {
|
|
78
|
+
...this._config,
|
|
79
|
+
...config,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 设置模式
|
|
84
|
+
* @param mode 模式类型:'paint' 笔刷涂抹、'erase' 笔刷逐笔擦除、'box-erase' 框选擦除、'box-paint' 框选涂抹
|
|
85
|
+
*/
|
|
86
|
+
setMode(mode) {
|
|
87
|
+
this.mode = mode;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @description 判断点是否在包围盒内
|
|
91
|
+
*/
|
|
92
|
+
_isPointInBox(p, box) {
|
|
93
|
+
return p.x >= box.min.x && p.x <= box.max.x
|
|
94
|
+
&& p.y >= box.min.y && p.y <= box.max.y;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* @description 将屏幕坐标转换为包围盒内的归一化坐标 [0,1](钳制在范围内)
|
|
98
|
+
*/
|
|
99
|
+
_toNormalized(p, box) {
|
|
100
|
+
const x = Math.max(0, Math.min(1, (p.x - box.min.x) / (box.max.x - box.min.x)));
|
|
101
|
+
const y = Math.max(0, Math.min(1, (p.y - box.min.y) / (box.max.y - box.min.y)));
|
|
102
|
+
return new Vector2(x, y);
|
|
103
|
+
}
|
|
104
|
+
initialize() { }
|
|
105
|
+
preparationAction(event) {
|
|
106
|
+
if (!this.interactive) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
this.result.point = new Vector2(event.offsetX, event.offsetY);
|
|
110
|
+
return this.type;
|
|
111
|
+
}
|
|
112
|
+
preAction(event) {
|
|
113
|
+
if (!this.interactive) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
// 存储绘画前的数据用于 undoRedo
|
|
117
|
+
this._oldLines = {
|
|
118
|
+
lines: JSON.parse(JSON.stringify(this.result.lines)),
|
|
119
|
+
};
|
|
120
|
+
// 更新鼠标位置
|
|
121
|
+
this.result.point = new Vector2(event.offsetX, event.offsetY);
|
|
122
|
+
// 框选模式(框选擦除 / 框选涂抹):仅在元素包围盒内按下才触发
|
|
123
|
+
if (this.mode === 'box-erase' || this.mode === 'box-paint') {
|
|
124
|
+
const box = this.result.box;
|
|
125
|
+
if (!box || !this._isPointInBox(this.result.point, box)) {
|
|
126
|
+
// 包围盒外按下:不触发框选,交由其它 gizmo
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const start = this._toNormalized(this.result.point, box);
|
|
130
|
+
this.result.lines.push({
|
|
131
|
+
type: this.mode === 'box-paint' ? 'paint' : 'erase',
|
|
132
|
+
shape: 'rect',
|
|
133
|
+
brushSize: 0,
|
|
134
|
+
points: [start, start.clone()],
|
|
135
|
+
});
|
|
136
|
+
this._isBoxErasing = true;
|
|
137
|
+
return this.type;
|
|
138
|
+
}
|
|
139
|
+
// 开启新画笔
|
|
140
|
+
this.result.lines.push({
|
|
141
|
+
type: this.mode,
|
|
142
|
+
brushSize: this.config.brushSize,
|
|
143
|
+
points: [],
|
|
144
|
+
});
|
|
145
|
+
return this.type;
|
|
146
|
+
}
|
|
147
|
+
action(event) {
|
|
148
|
+
if (!this.interactive
|
|
149
|
+
|| this.result.status === 'init') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// 更新鼠标位置
|
|
153
|
+
const box = this.result.box;
|
|
154
|
+
const point = new Vector2(event.offsetX, event.offsetY);
|
|
155
|
+
this.result.point = point;
|
|
156
|
+
const lastLine = this.result.lines[this.result.lines.length - 1];
|
|
157
|
+
// 框选擦除:更新框选终点(钳制到包围盒内)
|
|
158
|
+
if (lastLine?.shape === 'rect') {
|
|
159
|
+
lastLine.points[1] = this._toNormalized(point, box);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// 记录鼠标划线位置(笔刷模式)
|
|
163
|
+
const np = new Vector2((event.offsetX - box.min.x) / (box.max.x - box.min.x), (event.offsetY - box.min.y) / (box.max.y - box.min.y));
|
|
164
|
+
lastLine?.points.push(np);
|
|
165
|
+
}
|
|
166
|
+
endAction() {
|
|
167
|
+
if (!this.interactive) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const lastLine = this.result.lines[this.result.lines.length - 1];
|
|
171
|
+
// 框选擦除:起点==终点视为空框选,移除;否则提交 undoRedo
|
|
172
|
+
if (lastLine?.shape === 'rect') {
|
|
173
|
+
this._isBoxErasing = false;
|
|
174
|
+
const [a, b] = lastLine.points;
|
|
175
|
+
const isEmpty = !!a && !!b && a.x === b.x && a.y === b.y;
|
|
176
|
+
if (this._oldLines && !isEmpty) {
|
|
177
|
+
const operation = {
|
|
178
|
+
type: 'update',
|
|
179
|
+
oldData: this._oldLines,
|
|
180
|
+
newData: {
|
|
181
|
+
lines: JSON.parse(JSON.stringify(this.result.lines)),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
this._undoRedo.push(operation);
|
|
185
|
+
this._eventEmitter.emit('maskGizmoUndoRedoChange', operation);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// 空框选移除
|
|
189
|
+
this.result.lines.pop();
|
|
190
|
+
}
|
|
191
|
+
this._oldLines = undefined;
|
|
192
|
+
return this.type;
|
|
193
|
+
}
|
|
194
|
+
// 记录操作到 undoRedo(仅当画线有实际点时才记录)
|
|
195
|
+
if (this._oldLines && lastLine && lastLine.points.length > 0) {
|
|
196
|
+
const operation = {
|
|
197
|
+
type: 'update',
|
|
198
|
+
oldData: this._oldLines,
|
|
199
|
+
newData: {
|
|
200
|
+
lines: JSON.parse(JSON.stringify(this.result.lines)),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
this._undoRedo.push(operation);
|
|
204
|
+
this._eventEmitter.emit('maskGizmoUndoRedoChange', operation);
|
|
205
|
+
}
|
|
206
|
+
else if (lastLine?.points.length === 0) {
|
|
207
|
+
// 如果没有实际绘制点,移除空的线条
|
|
208
|
+
this.result.lines.pop();
|
|
209
|
+
}
|
|
210
|
+
this._oldLines = undefined;
|
|
211
|
+
return this.type;
|
|
212
|
+
}
|
|
213
|
+
interruption() {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
actionKey() { }
|
|
217
|
+
endActionKey() { }
|
|
218
|
+
cancelPreparation() { }
|
|
219
|
+
refreshResults() {
|
|
220
|
+
if (this.interactive) {
|
|
221
|
+
const pageData = this._pageDataUtils.getPageData();
|
|
222
|
+
assertExist(pageData, 'You must call SDK#run() first');
|
|
223
|
+
if (pageData.activeData.selectedItems.length !== 1) {
|
|
224
|
+
console.warn('Mask Gizmo can edit one picture at a time.');
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
if (this.result.status === 'init') {
|
|
228
|
+
const itemViewBox = this._pageDataUtils.getViewBoxById(pageData.activeData.selectedItems[0]);
|
|
229
|
+
this.result = {
|
|
230
|
+
status: 'done',
|
|
231
|
+
box: itemViewBox.clone(),
|
|
232
|
+
lines: [],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const box = this.result.box;
|
|
237
|
+
const currentViewBox = this._pageDataUtils.getViewBoxById(pageData.activeData.selectedItems[0]);
|
|
238
|
+
const shift = currentViewBox.getCenter().subtract(box.getCenter());
|
|
239
|
+
const scaleValue = currentViewBox.getSize().x / box.getSize().x;
|
|
240
|
+
box.translate(shift);
|
|
241
|
+
box.scale(scaleValue);
|
|
242
|
+
}
|
|
243
|
+
this.refreshCursorResult();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.result = { status: 'init', lines: [] };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
getRenderObjects() {
|
|
251
|
+
if (!this.interactive) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
return this.refreshRenderObjects();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 清理 RenderTexture
|
|
260
|
+
*/
|
|
261
|
+
clearRenderObjects() {
|
|
262
|
+
this.boxGraphics.clear();
|
|
263
|
+
if (this.maskRenderTexture) {
|
|
264
|
+
this.maskRenderTexture.destroy(true);
|
|
265
|
+
this.maskRenderTexture = null;
|
|
266
|
+
}
|
|
267
|
+
if (this.maskSprite) {
|
|
268
|
+
this.maskSprite.destroy();
|
|
269
|
+
this.maskSprite = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
refreshRenderObjects() {
|
|
273
|
+
this.clearRenderObjects();
|
|
274
|
+
if (!this.interactive || this.result.status === 'init') {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
const { maskImage, brushColor, brushAlpha, boxLineWidth, boxLineColor, boxLineAlpha, } = this.config;
|
|
278
|
+
const box = this.result.box;
|
|
279
|
+
const { min, max } = box;
|
|
280
|
+
// 创建新的 RenderTexture,尺寸为包围盒的大小
|
|
281
|
+
const width = Math.ceil(max.x - min.x);
|
|
282
|
+
const height = Math.ceil(max.y - min.y);
|
|
283
|
+
this.maskRenderTexture = RenderTexture.create({
|
|
284
|
+
width,
|
|
285
|
+
height,
|
|
286
|
+
});
|
|
287
|
+
const viewportParam = this._pageDataUtils.interactionUtils.viewportParam;
|
|
288
|
+
// 创建 Sprite 用于显示画笔
|
|
289
|
+
this.maskSprite = new Sprite(this.maskRenderTexture);
|
|
290
|
+
this.maskSprite.width = width;
|
|
291
|
+
this.maskSprite.height = height;
|
|
292
|
+
this.maskSprite.position.set(min.x, min.y);
|
|
293
|
+
this.maskSprite.alpha = brushAlpha;
|
|
294
|
+
// 加载主体遮罩
|
|
295
|
+
if (maskImage) {
|
|
296
|
+
const container = new Container();
|
|
297
|
+
const mainPartTexture = Texture.from(maskImage);
|
|
298
|
+
const mainPartSprite = new Sprite(mainPartTexture);
|
|
299
|
+
// 设置 mask sprite 的尺寸与包围盒一致
|
|
300
|
+
mainPartSprite.width = width;
|
|
301
|
+
mainPartSprite.height = height;
|
|
302
|
+
// 创建要被遮罩的内容
|
|
303
|
+
const mainPartGraphics = new Graphics();
|
|
304
|
+
mainPartGraphics.beginFill(brushColor);
|
|
305
|
+
mainPartGraphics.drawRect(0, 0, width, height);
|
|
306
|
+
mainPartGraphics.endFill();
|
|
307
|
+
// 应用遮罩
|
|
308
|
+
mainPartGraphics.mask = mainPartSprite;
|
|
309
|
+
container.addChild(mainPartGraphics);
|
|
310
|
+
container.addChild(mainPartSprite);
|
|
311
|
+
this._app.renderer.render(container, {
|
|
312
|
+
renderTexture: this.maskRenderTexture,
|
|
313
|
+
clear: false,
|
|
314
|
+
});
|
|
315
|
+
container.destroy();
|
|
316
|
+
mainPartTexture.destroy();
|
|
317
|
+
mainPartSprite.destroy();
|
|
318
|
+
}
|
|
319
|
+
// 绘制外框
|
|
320
|
+
const topBox = new Box2(new Vector2(min.x, min.y), new Vector2(max.x, min.y));
|
|
321
|
+
const leftBox = new Box2(new Vector2(min.x, min.y), new Vector2(min.x, max.y));
|
|
322
|
+
const rightBox = new Box2(new Vector2(max.x, min.y), new Vector2(max.x, min.y));
|
|
323
|
+
const bottomBox = new Box2(new Vector2(min.x, max.y), new Vector2(max.x, max.y));
|
|
324
|
+
this.boxGraphics.fillBox(topBox);
|
|
325
|
+
this.boxGraphics.fillBox(leftBox);
|
|
326
|
+
this.boxGraphics.fillBox(rightBox);
|
|
327
|
+
this.boxGraphics.fillBox(bottomBox);
|
|
328
|
+
// 绘制元素选包围盒
|
|
329
|
+
this.boxGraphics.lineStyle(boxLineWidth, boxLineColor, boxLineAlpha);
|
|
330
|
+
this.boxGraphics.drawBox(box);
|
|
331
|
+
this.boxGraphics.endFill();
|
|
332
|
+
// 框选擦除进行中:叠加当前拖动矩形的可视描边
|
|
333
|
+
const lastLine = this.result.lines[this.result.lines.length - 1];
|
|
334
|
+
if (this._isBoxErasing && lastLine?.shape === 'rect' && lastLine.points.length >= 2) {
|
|
335
|
+
const [a, b] = lastLine.points;
|
|
336
|
+
if (a && b) {
|
|
337
|
+
const selBox = new Box2(new Vector2(box.min.x + Math.min(a.x, b.x) * (box.max.x - box.min.x), box.min.y + Math.min(a.y, b.y) * (box.max.y - box.min.y)), new Vector2(box.min.x + Math.max(a.x, b.x) * (box.max.x - box.min.x), box.min.y + Math.max(a.y, b.y) * (box.max.y - box.min.y)));
|
|
338
|
+
this.boxGraphics.lineStyle(boxLineWidth, boxLineColor, boxLineAlpha);
|
|
339
|
+
this.boxGraphics.drawBox(selBox);
|
|
340
|
+
this.boxGraphics.endFill();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// 1. 画线形(只用于遮罩,不直接显示
|
|
344
|
+
for (const line of this.result.lines) {
|
|
345
|
+
if (!line) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const lineGraphics = new Graphics();
|
|
349
|
+
// 矩形框选:填充矩形,paint 用 NORMAL 叠加,erase 用 DST_OUT 抠除
|
|
350
|
+
if (line.shape === 'rect') {
|
|
351
|
+
const a = line.points[0];
|
|
352
|
+
const b = line.points[1];
|
|
353
|
+
if (a && b) {
|
|
354
|
+
const w = box.max.x - box.min.x;
|
|
355
|
+
const h = box.max.y - box.min.y;
|
|
356
|
+
const x0 = Math.min(a.x, b.x) * w;
|
|
357
|
+
const y0 = Math.min(a.y, b.y) * h;
|
|
358
|
+
const x1 = Math.max(a.x, b.x) * w;
|
|
359
|
+
const y1 = Math.max(a.y, b.y) * h;
|
|
360
|
+
// 太小(纯点击)不渲染,避免 1px 擦除/涂抹点
|
|
361
|
+
if (x1 - x0 > 0.5 || y1 - y0 > 0.5) {
|
|
362
|
+
lineGraphics.beginFill(brushColor);
|
|
363
|
+
lineGraphics.drawRect(x0, y0, x1 - x0, y1 - y0);
|
|
364
|
+
lineGraphics.endFill();
|
|
365
|
+
lineGraphics.blendMode = line.type === 'paint' ? BLEND_MODES.NORMAL : BLEND_MODES.DST_OUT;
|
|
366
|
+
this._app.renderer.render(lineGraphics, {
|
|
367
|
+
renderTexture: this.maskRenderTexture,
|
|
368
|
+
clear: false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
lineGraphics.destroy();
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
lineGraphics.blendMode = line.type === 'paint' ? BLEND_MODES.NORMAL : BLEND_MODES.DST_OUT;
|
|
376
|
+
lineGraphics.lineStyle({
|
|
377
|
+
width: line.brushSize * viewportParam.scale,
|
|
378
|
+
color: brushColor,
|
|
379
|
+
cap: LINE_CAP.ROUND,
|
|
380
|
+
join: LINE_JOIN.ROUND,
|
|
381
|
+
native: false,
|
|
382
|
+
alignment: 0.5,
|
|
383
|
+
});
|
|
384
|
+
for (let j = 0; j < line.points.length; j++) {
|
|
385
|
+
const point = line.points[j];
|
|
386
|
+
if (!point) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const x = point.x * (box.max.x - box.min.x);
|
|
390
|
+
const y = point.y * (box.max.y - box.min.y);
|
|
391
|
+
if (j === 0) {
|
|
392
|
+
lineGraphics.moveTo(x, y);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
lineGraphics.lineTo(x, y);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
lineGraphics.endFill();
|
|
399
|
+
this._app.renderer.render(lineGraphics, {
|
|
400
|
+
renderTexture: this.maskRenderTexture,
|
|
401
|
+
clear: false,
|
|
402
|
+
});
|
|
403
|
+
lineGraphics.destroy();
|
|
404
|
+
}
|
|
405
|
+
return [this.boxGraphics, this.maskSprite];
|
|
406
|
+
}
|
|
407
|
+
/** 刷新指针结果 */
|
|
408
|
+
refreshCursorResult() {
|
|
409
|
+
const { point, box } = this.result;
|
|
410
|
+
this.cursorResult = {
|
|
411
|
+
type: 'normal',
|
|
412
|
+
angle: 0,
|
|
413
|
+
};
|
|
414
|
+
if (!point || !box) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const inside = point.x >= box.min.x && point.x <= box.max.x
|
|
418
|
+
&& point.y >= box.min.y && point.y <= box.max.y;
|
|
419
|
+
if (!inside) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// 框选模式(框选擦除 / 框选涂抹):crosshair
|
|
423
|
+
if (this.mode === 'box-erase' || this.mode === 'box-paint') {
|
|
424
|
+
this.cursorResult = {
|
|
425
|
+
type: 'box-select',
|
|
426
|
+
angle: 0,
|
|
427
|
+
};
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const viewportParam = this._pageDataUtils.interactionUtils.viewportParam;
|
|
431
|
+
this.cursorResult = {
|
|
432
|
+
type: 'circle',
|
|
433
|
+
angle: 0,
|
|
434
|
+
radius: this.config.brushSize * viewportParam.scale / 2,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
close() {
|
|
438
|
+
this._config.maskImage = undefined;
|
|
439
|
+
this.result.lines = [];
|
|
440
|
+
this._undoRedo.clear();
|
|
441
|
+
}
|
|
442
|
+
clearMask() {
|
|
443
|
+
// 记录清空操作到 undoRedo
|
|
444
|
+
if (this.result.lines.length > 0) {
|
|
445
|
+
const operation = {
|
|
446
|
+
type: 'update',
|
|
447
|
+
oldData: {
|
|
448
|
+
lines: JSON.parse(JSON.stringify(this.result.lines)),
|
|
449
|
+
},
|
|
450
|
+
newData: {
|
|
451
|
+
lines: [],
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
this._undoRedo.push(operation);
|
|
455
|
+
this._eventEmitter.emit('maskGizmoUndoRedoChange', operation);
|
|
456
|
+
}
|
|
457
|
+
this.result.lines = [];
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 撤销操作
|
|
461
|
+
* @returns 是否成功撤销
|
|
462
|
+
*/
|
|
463
|
+
undo() {
|
|
464
|
+
const operation = this._undoRedo.undo();
|
|
465
|
+
if (operation) {
|
|
466
|
+
this.result.lines = JSON.parse(JSON.stringify(operation.oldData.lines));
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* 重做操作
|
|
473
|
+
* @returns 是否成功重做
|
|
474
|
+
*/
|
|
475
|
+
redo() {
|
|
476
|
+
const operation = this._undoRedo.redo();
|
|
477
|
+
if (operation) {
|
|
478
|
+
this.result.lines = JSON.parse(JSON.stringify(operation.newData.lines));
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* 检查是否可以撤销
|
|
485
|
+
*/
|
|
486
|
+
get canUndo() {
|
|
487
|
+
return this._undoRedo.canUndo;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 检查是否可以重做
|
|
491
|
+
*/
|
|
492
|
+
get canRedo() {
|
|
493
|
+
return this._undoRedo.canRedo;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 清空 undoRedo 历史
|
|
497
|
+
*/
|
|
498
|
+
clearUndoRedo() {
|
|
499
|
+
this._undoRedo.clear();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* 加载蒙版图片
|
|
503
|
+
* @param url 图片地址
|
|
504
|
+
* @returns Promise<HTMLImageElement>
|
|
505
|
+
*/
|
|
506
|
+
loadMaskImage(url) {
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const img = new Image();
|
|
509
|
+
img.crossOrigin = 'anonymous';
|
|
510
|
+
img.onload = () => {
|
|
511
|
+
resolve(img);
|
|
512
|
+
};
|
|
513
|
+
img.onerror = (err) => {
|
|
514
|
+
reject(new Error('Failed to load mask image.'));
|
|
515
|
+
};
|
|
516
|
+
img.src = url;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 获取蒙版 base64(异步版本,确保 maskImage 正确加载)
|
|
521
|
+
* @returns Promise<蒙版图像的 base64 字符串 | null>
|
|
522
|
+
* @description maskImage 是一张黑白蒙版图片,白色表示主体部分
|
|
523
|
+
* 输出蒙版:主体色为 maskColor,背景色为白色
|
|
524
|
+
*/
|
|
525
|
+
getMask() {
|
|
526
|
+
const box = this.result.box;
|
|
527
|
+
if (!box) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
const width = Math.ceil(box.max.x - box.min.x);
|
|
531
|
+
const height = Math.ceil(box.max.y - box.min.y);
|
|
532
|
+
// 创建 Canvas 用于绘制蒙版
|
|
533
|
+
const canvas = document.createElement('canvas');
|
|
534
|
+
canvas.width = width;
|
|
535
|
+
canvas.height = height;
|
|
536
|
+
const ctx = canvas.getContext('2d');
|
|
537
|
+
if (!ctx) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
// 获取蒙版配置
|
|
541
|
+
const { maskImage, maskColor, maskBackgroundColor } = this.config;
|
|
542
|
+
// 若无划线且无蒙版图片,则认为是无蒙版
|
|
543
|
+
if (this.result.lines.length === 0 && !maskImage) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
// 将数字颜色转换为十六进制字符串
|
|
547
|
+
const maskHexColor = '#' + maskColor.toString(16).padStart(6, '0');
|
|
548
|
+
const maskBackgroundHexColor = '#' + maskBackgroundColor.toString(16).padStart(6, '0');
|
|
549
|
+
// 填充白色背景(非主体部分)
|
|
550
|
+
ctx.fillStyle = maskBackgroundHexColor;
|
|
551
|
+
ctx.fillRect(0, 0, width, height);
|
|
552
|
+
// 如果配置了蒙版图片,先加载并绘制蒙版图片
|
|
553
|
+
if (maskImage) {
|
|
554
|
+
try {
|
|
555
|
+
ctx.drawImage(maskImage, 0, 0, width, height);
|
|
556
|
+
// 通过像素操作:将 maskImage 白色区域变为 maskColor,黑色区域变为白色背景
|
|
557
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
558
|
+
const data = imageData.data;
|
|
559
|
+
// 解析 maskColor
|
|
560
|
+
const mcR = (maskColor >> 16) & 0xFF;
|
|
561
|
+
const mcG = (maskColor >> 8) & 0xFF;
|
|
562
|
+
const mcB = maskColor & 0xFF;
|
|
563
|
+
// 解析 maskBackgroundColor
|
|
564
|
+
const mbcR = (maskBackgroundColor >> 16) & 0xFF;
|
|
565
|
+
const mbcG = (maskBackgroundColor >> 8) & 0xFF;
|
|
566
|
+
const mbcB = maskBackgroundColor & 0xFF;
|
|
567
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
568
|
+
const r = data[i];
|
|
569
|
+
const g = data[i + 1];
|
|
570
|
+
const b = data[i + 2];
|
|
571
|
+
// 计算亮度,白色亮度高(主体),黑色亮度低(背景)
|
|
572
|
+
const brightness = (r + g + b) / 3;
|
|
573
|
+
if (brightness > 128) {
|
|
574
|
+
// 白色区域(主体)-> maskColor
|
|
575
|
+
data[i] = mcR;
|
|
576
|
+
data[i + 1] = mcG;
|
|
577
|
+
data[i + 2] = mcB;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
// 黑色区域(背景)-> 白色
|
|
581
|
+
data[i] = mbcR;
|
|
582
|
+
data[i + 1] = mbcG;
|
|
583
|
+
data[i + 2] = mbcB;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
ctx.putImageData(imageData, 0, 0);
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
console.warn('Failed to load mask image:', err);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const viewportParam = this._pageDataUtils.interactionUtils.viewportParam;
|
|
593
|
+
const scale = viewportParam.scale;
|
|
594
|
+
// 绘制用户画笔线条
|
|
595
|
+
// paint: 添加主体色(maskColor)
|
|
596
|
+
// erase: 露出白色背景
|
|
597
|
+
for (const line of this.result.lines) {
|
|
598
|
+
if (!line || line.points.length === 0) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
// 矩形框选:填充矩形
|
|
602
|
+
if (line.shape === 'rect') {
|
|
603
|
+
const [a, b] = line.points;
|
|
604
|
+
if (!a || !b) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const x0 = Math.min(a.x, b.x) * width;
|
|
608
|
+
const y0 = Math.min(a.y, b.y) * height;
|
|
609
|
+
const x1 = Math.max(a.x, b.x) * width;
|
|
610
|
+
const y1 = Math.max(a.y, b.y) * height;
|
|
611
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
612
|
+
ctx.fillStyle = line.type === 'paint' ? maskHexColor : maskBackgroundHexColor;
|
|
613
|
+
ctx.fillRect(x0, y0, Math.max(1, x1 - x0), Math.max(1, y1 - y0));
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
ctx.lineWidth = line.brushSize * scale;
|
|
617
|
+
ctx.lineCap = 'round';
|
|
618
|
+
ctx.lineJoin = 'round';
|
|
619
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
620
|
+
if (line.type === 'paint') {
|
|
621
|
+
// 涂抹:添加主体色
|
|
622
|
+
ctx.strokeStyle = maskHexColor;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// 擦除:露出白色背景
|
|
626
|
+
ctx.strokeStyle = maskBackgroundHexColor;
|
|
627
|
+
}
|
|
628
|
+
ctx.beginPath();
|
|
629
|
+
for (let j = 0; j < line.points.length; j++) {
|
|
630
|
+
const point = line.points[j];
|
|
631
|
+
if (!point) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const x = point.x * width;
|
|
635
|
+
const y = point.y * height;
|
|
636
|
+
if (j === 0) {
|
|
637
|
+
ctx.moveTo(x, y);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
ctx.lineTo(x, y);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
ctx.stroke();
|
|
644
|
+
}
|
|
645
|
+
// 生成 base64
|
|
646
|
+
return canvas.toDataURL('image/png');
|
|
647
|
+
}
|
|
648
|
+
}
|