uni-image-editor 1.0.0
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/README.md +98 -0
- package/components/edit-graffiti-config.vue +151 -0
- package/components/edit-text-config.vue +112 -0
- package/components/image-clipper/image-clipper.vue +1009 -0
- package/components/image-clipper/img/photo.svg +19 -0
- package/components/image-clipper/img/rotate.svg +15 -0
- package/components/image-clipper/index.scss +184 -0
- package/components/image-clipper/utils.js +280 -0
- package/components/input-text-modal.vue +431 -0
- package/image-editor.vue +971 -0
- package/js/const.js +242 -0
- package/js/editor.js +1179 -0
- package/js/utils.js +316 -0
- package/package.json +29 -0
package/js/editor.js
ADDED
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Author: msc 862078729@qq.com
|
|
3
|
+
* @Date: 2024-04-25 17:48:23
|
|
4
|
+
* @LastEditors: msc 862078729@qq.com
|
|
5
|
+
* @LastEditTime: 2024-06-15 18:12:42
|
|
6
|
+
* @FilePath: \code\components\image-editor\image-editor.js
|
|
7
|
+
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
IMG_EDITOR_GRAFFITI_SHAPE_TYPE,
|
|
12
|
+
IMG_EDITOR_RENDER_TYPE,
|
|
13
|
+
IMG_EDITOR_EDIT_TYPE,
|
|
14
|
+
CAN_CANCEL_EDIT_TYPE,
|
|
15
|
+
} from "./const";
|
|
16
|
+
|
|
17
|
+
import { renderAwait, validImgHasEmpty, base64ToUrl, clearUrlBlobCache, pick, findLastIndex, uniqueId } from "./utils";
|
|
18
|
+
|
|
19
|
+
export default class Editor {
|
|
20
|
+
constructor(opts) {
|
|
21
|
+
// 解构赋值,从传入的opts对象中提取配置参数
|
|
22
|
+
const {
|
|
23
|
+
// 画布ID
|
|
24
|
+
canvasId,
|
|
25
|
+
// 原始图片资源
|
|
26
|
+
originImgUrl,
|
|
27
|
+
// 画布的宽度
|
|
28
|
+
width,
|
|
29
|
+
// 画布的高度
|
|
30
|
+
height,
|
|
31
|
+
// 默认画笔颜色,默认为"#000000"(黑色)
|
|
32
|
+
defaultColor = "#000000",
|
|
33
|
+
// 默认画笔粗糙度,默认为1
|
|
34
|
+
defaultRough = 1,
|
|
35
|
+
// 默认填充颜色,默认为"#000000"(黑色)
|
|
36
|
+
defaultFillColor = "#000000",
|
|
37
|
+
// 默认字体大小,默认为12
|
|
38
|
+
defaultFontSize = 12,
|
|
39
|
+
// Vue实例,用于在Vue环境下操作DOM或触发事件
|
|
40
|
+
vueInstance,
|
|
41
|
+
// 更新图片时的回调函数
|
|
42
|
+
updateImgUrlCb,
|
|
43
|
+
// 更新步骤时的回调函数
|
|
44
|
+
updateStepCb,
|
|
45
|
+
// 渲染图片完成后的回调函数
|
|
46
|
+
renderImgCb,
|
|
47
|
+
// 更新编辑记录数据的回调函数
|
|
48
|
+
updateEditRecordDataCb,
|
|
49
|
+
// 更新渲染加载状态回调函数
|
|
50
|
+
renderCanvasLoadingCb,
|
|
51
|
+
// 是否编辑后校验
|
|
52
|
+
editFinishValid = false,
|
|
53
|
+
// 转化初始化图片
|
|
54
|
+
initConvert = false,
|
|
55
|
+
// 图片最大透明占比
|
|
56
|
+
maxTransparencyRate = 5
|
|
57
|
+
} = opts;
|
|
58
|
+
|
|
59
|
+
// 初始化成员变量
|
|
60
|
+
this.canvasId = canvasId; // 画布ID
|
|
61
|
+
this.originImgData = {
|
|
62
|
+
url: originImgUrl,
|
|
63
|
+
tempFilePath: "",
|
|
64
|
+
width: width,
|
|
65
|
+
height: height,
|
|
66
|
+
};
|
|
67
|
+
this.width = width; // 画布宽度
|
|
68
|
+
this.height = height; // 画布高度
|
|
69
|
+
this.defaultColor = defaultColor; // 默认画笔颜色
|
|
70
|
+
this.defaultRough = defaultRough; // 默认画笔粗糙度
|
|
71
|
+
this.defaultFillColor = defaultFillColor; // 默认填充颜色
|
|
72
|
+
this.defaultFontSize = defaultFontSize; // 默认字体大小
|
|
73
|
+
|
|
74
|
+
// 回调函数
|
|
75
|
+
this.updateStepCb = updateStepCb; // 更新步骤时的回调函数
|
|
76
|
+
this.updateImgUrlCb = updateImgUrlCb; // 更新图片时的回调函数
|
|
77
|
+
this.renderImgCb = renderImgCb; // 渲染图片完成后的回调函数
|
|
78
|
+
this.updateEditRecordDataCb = updateEditRecordDataCb; // 更新编辑记录数据的回调函数
|
|
79
|
+
|
|
80
|
+
this.renderCanvasLoadingCb = renderCanvasLoadingCb
|
|
81
|
+
|
|
82
|
+
this.vueInstance = vueInstance; // Vue实例
|
|
83
|
+
|
|
84
|
+
// 上下文对象,用于在画布上绘制
|
|
85
|
+
this.ctx = null;
|
|
86
|
+
|
|
87
|
+
// 编辑记录数据数组
|
|
88
|
+
this.editRecordData = [];
|
|
89
|
+
|
|
90
|
+
// 当前编辑步骤,初始化为-1
|
|
91
|
+
this.curStep = -1;
|
|
92
|
+
|
|
93
|
+
// 初始化编辑器
|
|
94
|
+
this.init(initConvert);
|
|
95
|
+
|
|
96
|
+
// 编辑状态,初始化为false(非编辑状态)
|
|
97
|
+
this.editting = false;
|
|
98
|
+
|
|
99
|
+
this.renderCanvasLoading = false
|
|
100
|
+
|
|
101
|
+
this.editFinishValid = editFinishValid
|
|
102
|
+
|
|
103
|
+
this.maxTransparencyRate = maxTransparencyRate
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 初始化方法
|
|
108
|
+
async init(initConvert = false) {
|
|
109
|
+
this.renderCanvasLoading = true;
|
|
110
|
+
|
|
111
|
+
this.renderCanvasLoadingCb && this.renderCanvasLoadingCb(this.renderCanvasLoading)
|
|
112
|
+
|
|
113
|
+
await renderAwait(50)
|
|
114
|
+
// 创建画布上下文,传入画布ID和Vue实例(如果在Vue环境下)
|
|
115
|
+
this.ctx = uni.createCanvasContext(this.canvasId, this.vueInstance);
|
|
116
|
+
|
|
117
|
+
// 等待渲染完成
|
|
118
|
+
await renderAwait(200);
|
|
119
|
+
|
|
120
|
+
// 使用原始图片渲染画布
|
|
121
|
+
await this.renderByImg(this.originImgData.url, this.originImgData.width, this.originImgData.height);
|
|
122
|
+
|
|
123
|
+
// 初始化阶段转化原始拖地址为本地地址,同时生成图片base64数据用于h5生成file
|
|
124
|
+
// 初始化转化图片再某些摄像头截取的网络图片可能渲染空白,暂时废弃
|
|
125
|
+
if (initConvert) {
|
|
126
|
+
try {
|
|
127
|
+
console.log("初始化转化图片");
|
|
128
|
+
// 等待渲染完成
|
|
129
|
+
await renderAwait(100);
|
|
130
|
+
const res = await this.canvasToImgUrl();
|
|
131
|
+
this.originImgData.url = res.url;
|
|
132
|
+
this.originImgData.tempFilePath = res.tempFilePath;
|
|
133
|
+
this.updateImgUrlCb && this.updateImgUrlCb(this.originImgData.url);
|
|
134
|
+
// 等待渲染完成
|
|
135
|
+
await renderAwait(50);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.log(error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 设置画笔颜色、填充颜色和画笔粗细
|
|
142
|
+
this.ctx.strokeStyle = this.defaultColor;
|
|
143
|
+
this.ctx.setFillStyle(this.defaultFillColor);
|
|
144
|
+
this.ctx.setLineWidth(this.defaultRough);
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
this.renderCanvasLoading = false;
|
|
148
|
+
|
|
149
|
+
this.renderCanvasLoadingCb && this.renderCanvasLoadingCb(this.renderCanvasLoading)
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 恢复编辑器状态,可以清除步骤和编辑记录
|
|
154
|
+
recovery(isClearStep = false, isClearRecord = false) {
|
|
155
|
+
// 使用原始图片重新渲染画布
|
|
156
|
+
this.renderByImg(
|
|
157
|
+
this.originImgData.url,
|
|
158
|
+
this.originImgData.width,
|
|
159
|
+
this.originImgData.height
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// 如果需要清除步骤
|
|
163
|
+
if (isClearStep) {
|
|
164
|
+
// 将当前步骤设置为-1
|
|
165
|
+
this.curStep = -1;
|
|
166
|
+
|
|
167
|
+
// 调用更新步骤的回调函数
|
|
168
|
+
this.updateStepCb && this.updateStepCb(this.curStep);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 如果需要清除编辑记录
|
|
172
|
+
if (isClearRecord) {
|
|
173
|
+
clearUrlBlobCache( this.editRecordData.map(item => item.imgData.url))
|
|
174
|
+
// 清空编辑记录数据
|
|
175
|
+
this.editRecordData = [];
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// 调用更新编辑记录数据的回调函数
|
|
180
|
+
this.updateEditRecordDataCb &&
|
|
181
|
+
this.updateEditRecordDataCb(this.editRecordData);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 根据图片渲染画布
|
|
186
|
+
renderByImg(
|
|
187
|
+
url = this.originImgData.url,
|
|
188
|
+
width = this.width,
|
|
189
|
+
height = this.height
|
|
190
|
+
) {
|
|
191
|
+
// 清除画布上的所有内容
|
|
192
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
193
|
+
|
|
194
|
+
// 设置新的画布宽度和高度
|
|
195
|
+
this.width = width;
|
|
196
|
+
|
|
197
|
+
this.height = height;
|
|
198
|
+
|
|
199
|
+
// 在画布上绘制图片
|
|
200
|
+
this.ctx.drawImage(url, 0, 0, width, height);
|
|
201
|
+
|
|
202
|
+
// 如果存在渲染图片完成的回调函数,则调用它
|
|
203
|
+
this.renderImgCb && this.renderImgCb(width, height);
|
|
204
|
+
|
|
205
|
+
// 调用更新图片的回调函数
|
|
206
|
+
this.updateImgUrlCb && this.updateImgUrlCb(url);
|
|
207
|
+
|
|
208
|
+
// 将更改绘制到画布上
|
|
209
|
+
return this.draw();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 清除所有编辑和步骤,恢复到初始状态
|
|
213
|
+
clear() {
|
|
214
|
+
// 调用recovery方法,清除步骤和编辑记录
|
|
215
|
+
this.recovery(true, true);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 撤销操作,回到上一步
|
|
219
|
+
repeal() {
|
|
220
|
+
// 如果当前步骤小于0或没有编辑记录数据,则直接返回
|
|
221
|
+
if (this.curStep < 0 || !this.editRecordData.length) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 如果当前步骤为0,则清除所有编辑,恢复到原始状态,重置步骤
|
|
226
|
+
if (this.curStep === 0) {
|
|
227
|
+
this.recovery(true);
|
|
228
|
+
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 如果当前步骤大于0,则回退到上一步
|
|
233
|
+
if (this.curStep > 0) {
|
|
234
|
+
this.curStep--; // 减少步骤
|
|
235
|
+
this.updateStepCb && this.updateStepCb(this.curStep); // 更新步骤的回调函数
|
|
236
|
+
|
|
237
|
+
this.restoreRecord(); // 恢复上一步的记录(该方法未在提供的代码中定义)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 重做操作,前进到下一步
|
|
242
|
+
redo() {
|
|
243
|
+
// 如果没有编辑记录数据或已经处于最后一步,则直接返回
|
|
244
|
+
if (
|
|
245
|
+
!this.editRecordData.length ||
|
|
246
|
+
this.curStep === this.editRecordData.length - 1
|
|
247
|
+
) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.curStep++; // 增加步骤
|
|
252
|
+
this.updateStepCb && this.updateStepCb(this.curStep); // 更新步骤的回调函数
|
|
253
|
+
|
|
254
|
+
this.restoreRecord(); // 恢复下一步的记录(该方法未在提供的代码中定义)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 处理编辑开始
|
|
258
|
+
handleEditStart(data) {
|
|
259
|
+
// 标记当前处于编辑状态
|
|
260
|
+
this.editting = true;
|
|
261
|
+
|
|
262
|
+
// 从传入的data对象中解构多个属性,并为未提供的属性提供默认值
|
|
263
|
+
const {
|
|
264
|
+
type, // 编辑类型
|
|
265
|
+
x, // x坐标
|
|
266
|
+
y, // y坐标
|
|
267
|
+
color = this.defaultColor, // 颜色,默认为defaultColor
|
|
268
|
+
fillColor = this.defaultFillColor, // 填充颜色,默认为defaultFillColor
|
|
269
|
+
rough = this.defaultRough, // 粗糙度,默认为defaultRough
|
|
270
|
+
shape, // 形状
|
|
271
|
+
size = this.defaultFontSize, // 字体大小,默认为defaultFontSize
|
|
272
|
+
align, // 对齐方式
|
|
273
|
+
baseline, // 基线位置
|
|
274
|
+
lineHeight = this.defaultFontSize, // 行高,默认为defaultFontSize
|
|
275
|
+
maxWidth, // 最大宽度
|
|
276
|
+
renderType = IMG_EDITOR_RENDER_TYPE.img, // 渲染类型,默认为图片渲染
|
|
277
|
+
} = data;
|
|
278
|
+
|
|
279
|
+
// 1.生成此次编辑相关的数据
|
|
280
|
+
// 创建一个空对象用于存储编辑记录
|
|
281
|
+
let record = {};
|
|
282
|
+
|
|
283
|
+
// 根据编辑类型设置不同的编辑记录
|
|
284
|
+
switch (type) {
|
|
285
|
+
case IMG_EDITOR_EDIT_TYPE.graffiti: // 涂鸦编辑
|
|
286
|
+
// 涂鸦的起始点为一个包含x和y的对象数组
|
|
287
|
+
const points = [
|
|
288
|
+
{
|
|
289
|
+
x,
|
|
290
|
+
y,
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
// 设置涂鸦的编辑记录
|
|
294
|
+
record = {
|
|
295
|
+
type,
|
|
296
|
+
color,
|
|
297
|
+
fillColor,
|
|
298
|
+
rough,
|
|
299
|
+
shape,
|
|
300
|
+
points,
|
|
301
|
+
imgData: {
|
|
302
|
+
url: "", // 图片(可能后续添加)
|
|
303
|
+
width: "", // 图片宽度(可能后续添加)
|
|
304
|
+
height: "", // 图片高度(可能后续添加)
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
renderType,
|
|
308
|
+
};
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case IMG_EDITOR_EDIT_TYPE.text: // 文本编辑
|
|
312
|
+
// 设置文本的编辑记录
|
|
313
|
+
record = {
|
|
314
|
+
type,
|
|
315
|
+
color,
|
|
316
|
+
size,
|
|
317
|
+
lineHeight,
|
|
318
|
+
x: "", // x坐标可能后续更新
|
|
319
|
+
y: "", // y坐标可能后续更新
|
|
320
|
+
maxWidth,
|
|
321
|
+
baseline,
|
|
322
|
+
align,
|
|
323
|
+
imgData: {
|
|
324
|
+
url: "", // 图片(可能后续添加)
|
|
325
|
+
width: "", // 图片宽度(可能后续添加)
|
|
326
|
+
height: "", // 图片高度(可能后续添加)
|
|
327
|
+
},
|
|
328
|
+
renderType,
|
|
329
|
+
};
|
|
330
|
+
break;
|
|
331
|
+
|
|
332
|
+
case IMG_EDITOR_EDIT_TYPE.cut: // 剪切编辑
|
|
333
|
+
// 设置剪切的编辑记录(通常不包含其他属性)
|
|
334
|
+
record = {
|
|
335
|
+
type,
|
|
336
|
+
imgData: {
|
|
337
|
+
url: "", // 图片(可能后续添加)
|
|
338
|
+
width: "", // 图片宽度(可能后续添加)
|
|
339
|
+
height: "", // 图片高度(可能后续添加)
|
|
340
|
+
},
|
|
341
|
+
renderType,
|
|
342
|
+
};
|
|
343
|
+
break;
|
|
344
|
+
|
|
345
|
+
default:
|
|
346
|
+
// 默认情况,不设置任何记录
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
record.id = uniqueId()
|
|
351
|
+
|
|
352
|
+
// 2.删除之前的撤销记录并把新纪录添加进去
|
|
353
|
+
|
|
354
|
+
// 能取消的操作,如文本、涂鸦,开始时不删除撤销的步骤吗,确认后再删除对应的步骤
|
|
355
|
+
|
|
356
|
+
// 检查操作类型是否不是可取消的编辑类型
|
|
357
|
+
if (!CAN_CANCEL_EDIT_TYPE.includes(type)) {
|
|
358
|
+
// 如果撤销过再编辑,则删除撤销记录
|
|
359
|
+
const isRepeal = this.editRecordData.length - 1 !== this.curStep;
|
|
360
|
+
// 如果撤销过再编辑,则删除撤销记录
|
|
361
|
+
if (isRepeal) {
|
|
362
|
+
// 使用 filter 方法删除 editRecordData 中当前步骤(curStep)之后的所有记录
|
|
363
|
+
this.editRecordData = this.editRecordData.filter(
|
|
364
|
+
(_, index) => index <= this.curStep
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// 如果存在 updateEditRecordDataCb 回调函数,则调用它来更新编辑记录数据
|
|
368
|
+
this.updateEditRecordDataCb &&
|
|
369
|
+
this.updateEditRecordDataCb(this.editRecordData);
|
|
370
|
+
|
|
371
|
+
// h5环境canvas生成是base64,转化成url后,删除时清除对应的blob缓存
|
|
372
|
+
|
|
373
|
+
// #ifdef H5
|
|
374
|
+
const deleteImgs = this.editRecordData
|
|
375
|
+
.filter((_, index) => index > this.curStep && item.imgData?.url)
|
|
376
|
+
.map((item) => item.imgData.url);
|
|
377
|
+
clearUrlBlobCache(deleteImgs);
|
|
378
|
+
// #endif
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 将新的记录(record)添加到编辑记录数据的末尾
|
|
382
|
+
this.editRecordData.push(record);
|
|
383
|
+
|
|
384
|
+
// 如果存在 updateEditRecordDataCb 回调函数,则调用它来更新编辑记录数据
|
|
385
|
+
this.updateEditRecordDataCb &&
|
|
386
|
+
this.updateEditRecordDataCb(this.editRecordData);
|
|
387
|
+
|
|
388
|
+
// 增加当前步骤(curStep)的值
|
|
389
|
+
this.curStep++;
|
|
390
|
+
|
|
391
|
+
// 如果存在 updateStepCb 回调函数,则调用它来更新当前步骤
|
|
392
|
+
this.updateStepCb && this.updateStepCb(this.curStep);
|
|
393
|
+
} else {
|
|
394
|
+
// 如果是裁剪(cut)操作
|
|
395
|
+
// 因为裁剪操作存在取消的场景,所以如果确认裁剪则删除之前撤销记录
|
|
396
|
+
this.curStep++;
|
|
397
|
+
|
|
398
|
+
// 如果存在 updateStepCb 回调函数,则调用它来更新当前步骤
|
|
399
|
+
this.updateStepCb && this.updateStepCb(this.curStep);
|
|
400
|
+
|
|
401
|
+
// 在当前步骤(curStep)的位置插入新的记录(record)
|
|
402
|
+
this.editRecordData.splice(this.curStep, 0, record);
|
|
403
|
+
|
|
404
|
+
// 如果存在 updateEditRecordDataCb 回调函数,则调用它来更新编辑记录数据
|
|
405
|
+
this.updateEditRecordDataCb &&
|
|
406
|
+
this.updateEditRecordDataCb(this.editRecordData);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 处理编辑中
|
|
411
|
+
// 主要用于展示某些实时的效果,如涂鸦
|
|
412
|
+
handleEditting(data) {
|
|
413
|
+
// 如果当前不在编辑状态,则直接返回
|
|
414
|
+
if (!this.editting) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 获取当前步骤的编辑记录数据
|
|
419
|
+
const curData = this.editRecordData[this.curStep];
|
|
420
|
+
|
|
421
|
+
// 如果当前步骤的编辑记录数据类型是涂鸦(graffiti)
|
|
422
|
+
if (curData.type === IMG_EDITOR_EDIT_TYPE.graffiti) {
|
|
423
|
+
// 执行涂鸦操作,传入合并了当前步骤数据和传入数据的对象
|
|
424
|
+
this.doGraffitiOperation({ ...curData, ...data });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 更新编辑数据的方法
|
|
429
|
+
updateEditData(data, step = this.curStep) {
|
|
430
|
+
// 获取指定步骤的编辑记录数据
|
|
431
|
+
let targetData = this.editRecordData[step];
|
|
432
|
+
|
|
433
|
+
// 如果不存在指定步骤的编辑记录数据,则直接返回
|
|
434
|
+
if (!targetData) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 使用 Object.assign 方法合并传入的数据到指定步骤的编辑记录数据中
|
|
439
|
+
Object.assign(targetData, data);
|
|
440
|
+
|
|
441
|
+
// 返回更新后的编辑记录数据
|
|
442
|
+
return targetData;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 将 canvas 转换为图片 URL 的方法
|
|
446
|
+
canvasToImgUrl(
|
|
447
|
+
width = this.width, // 默认宽度为当前组件的宽度
|
|
448
|
+
height = this.height, // 默认高度为当前组件的高度
|
|
449
|
+
quality = 1, // 默认图片质量为 1(最高质量)
|
|
450
|
+
fileType = "jpg" // 默认图片格式为 jpg
|
|
451
|
+
) {
|
|
452
|
+
// 返回一个 Promise 对象,用于处理异步操作
|
|
453
|
+
return new Promise((resolve, reject) => {
|
|
454
|
+
// 调用 uni.canvasToTempFilePath 方法将 canvas 转换为临时文件路径
|
|
455
|
+
uni.canvasToTempFilePath({
|
|
456
|
+
canvasId: this.canvasId, // canvas 的 ID
|
|
457
|
+
x: 0, // 裁剪区域的左上角横坐标
|
|
458
|
+
y: 0, // 裁剪区域的左上角纵坐标
|
|
459
|
+
width: width, // 裁剪区域的宽度
|
|
460
|
+
height: height, // 裁剪区域的高度
|
|
461
|
+
quality: quality, // 图片的质量
|
|
462
|
+
fileType: fileType, // 图片的格式
|
|
463
|
+
success: async (res) => {
|
|
464
|
+
let { tempFilePath } = res; // 提取临时文件路径
|
|
465
|
+
|
|
466
|
+
// 如果是 H5 环境,则将临时文件路径转换为 base64 格式的 URL
|
|
467
|
+
|
|
468
|
+
let url = tempFilePath;
|
|
469
|
+
|
|
470
|
+
// #ifdef H5
|
|
471
|
+
url = base64ToUrl(tempFilePath);
|
|
472
|
+
// #endif
|
|
473
|
+
|
|
474
|
+
// 解析结果并返回,根据编译环境可能包含或不包含 tempFilePath
|
|
475
|
+
resolve({
|
|
476
|
+
...res, // 解析结果包含的其他属性
|
|
477
|
+
url,
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
fail: (err) => {
|
|
481
|
+
// 如果转换失败,则拒绝 Promise 并返回错误
|
|
482
|
+
reject(err);
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 删除编辑记录的方法
|
|
489
|
+
deleteEditRecord(index) {
|
|
490
|
+
// 使用数组的splice方法删除指定索引的编辑记录项
|
|
491
|
+
// 第一个参数是索引,第二个参数是要删除的项数(这里是1)
|
|
492
|
+
this.editRecordData.splice(index, 1);
|
|
493
|
+
|
|
494
|
+
// 如果定义了updateEditRecordDataCb回调函数
|
|
495
|
+
// 则调用该回调函数,并传入更新后的editRecordData数组
|
|
496
|
+
this.updateEditRecordDataCb &&
|
|
497
|
+
this.updateEditRecordDataCb(this.editRecordData);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 处理编辑失败的方法
|
|
501
|
+
handleEditFail(data) {
|
|
502
|
+
const step = this.curStep;
|
|
503
|
+
|
|
504
|
+
// 撤销此次编辑
|
|
505
|
+
this.repeal();
|
|
506
|
+
|
|
507
|
+
const deleteUrl = this.editRecordData[step].imgData.url
|
|
508
|
+
|
|
509
|
+
deleteUrl && clearUrlBlobCache([deleteUrl])
|
|
510
|
+
|
|
511
|
+
// 先删除当前步骤的编辑记录
|
|
512
|
+
this.deleteEditRecord(step);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 处理编辑成功的
|
|
516
|
+
async handleEditFinish(data) {
|
|
517
|
+
|
|
518
|
+
this.renderCanvasLoading = true;
|
|
519
|
+
|
|
520
|
+
this.renderCanvasLoadingCb && this.renderCanvasLoadingCb(this.renderCanvasLoading)
|
|
521
|
+
// 从传入的数据中解构出 img 属性
|
|
522
|
+
const { imgData } = data;
|
|
523
|
+
|
|
524
|
+
// 获取当前步骤的编辑记录数据
|
|
525
|
+
const curData = this.editRecordData[this.curStep];
|
|
526
|
+
|
|
527
|
+
// 从当前步骤的编辑记录数据中解构出 renderType, type, 和 shape 属性
|
|
528
|
+
const { renderType, type, shape } = curData;
|
|
529
|
+
|
|
530
|
+
// 设置当前不在编辑状态
|
|
531
|
+
this.editting = false;
|
|
532
|
+
|
|
533
|
+
// 1.涂鸦为直线、圆、正方形时,重新渲染之前的编辑记录和当前编辑记录
|
|
534
|
+
|
|
535
|
+
// 目前涂鸦形状不为曲线时,如直线、圆、正方形,实时交互效果为不保留之前涂鸦的轨迹,所以需要清除画布上下文
|
|
536
|
+
// 因为这些操作会清除画布所有内容,为了让视觉效果还有之前编辑的记录,把之前编辑的记录生成一张图片展示在画布的背景图
|
|
537
|
+
// 当执行这些操作时,清除画布上下文,当前画布为空,但是视觉效果还是之前的编辑记录即背景图
|
|
538
|
+
// 所以在编辑完成后需要把上一次编辑的记录或记录相关图片重新渲染后再把此次编辑的渲染上去
|
|
539
|
+
|
|
540
|
+
// 如果当前渲染类型为图片、编辑类型为涂鸦,且涂鸦形状不是曲线
|
|
541
|
+
if (
|
|
542
|
+
renderType === IMG_EDITOR_RENDER_TYPE.img &&
|
|
543
|
+
type === IMG_EDITOR_EDIT_TYPE.graffiti &&
|
|
544
|
+
shape !== IMG_EDITOR_GRAFFITI_SHAPE_TYPE.curve
|
|
545
|
+
) {
|
|
546
|
+
// 定义一个变量用于存储上一步的图片数据
|
|
547
|
+
let preImgData;
|
|
548
|
+
// 如果当前步骤是第一步,则使用原始图片作为上一步的图片数据
|
|
549
|
+
if (this.curStep === 0) {
|
|
550
|
+
preImgData = this.originImgData;
|
|
551
|
+
// 如果当前步骤不是第一步,则从上一步的编辑记录数据中获取图片数据
|
|
552
|
+
} else if (this.curStep > 0) {
|
|
553
|
+
preImgData = this.editRecordData[this.curStep - 1]?.imgData;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// 如果上一步的图片数据存在,则使用它重新渲染图片
|
|
557
|
+
preImgData && await this.renderByImg(preImgData.url, preImgData.width, preImgData.height);
|
|
558
|
+
|
|
559
|
+
// 等待一段时间后继续执行后续操作,这里可能是为了确保图片渲染完成
|
|
560
|
+
await renderAwait(200);
|
|
561
|
+
|
|
562
|
+
// 把这次编辑的记录渲染上去,保留之前渲染上下文
|
|
563
|
+
// 执行涂鸦操作,并传入当前步骤的编辑记录数据,同时设置 isReserve 为 true
|
|
564
|
+
await this.doGraffitiOperation({
|
|
565
|
+
...curData,
|
|
566
|
+
isReserve: true,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 2.渲染文本编辑到画布上
|
|
571
|
+
|
|
572
|
+
// 如果编辑类型为文本
|
|
573
|
+
if (type === IMG_EDITOR_EDIT_TYPE.text) {
|
|
574
|
+
// 从传入的数据中解构出文本相关的属性
|
|
575
|
+
const textData = pick(data, [
|
|
576
|
+
"x",
|
|
577
|
+
"y",
|
|
578
|
+
"text",
|
|
579
|
+
"color",
|
|
580
|
+
"align",
|
|
581
|
+
"baseline",
|
|
582
|
+
"size",
|
|
583
|
+
"maxWidth",
|
|
584
|
+
"lineHeight",
|
|
585
|
+
]);
|
|
586
|
+
|
|
587
|
+
// 更新当前步骤的编辑记录数据
|
|
588
|
+
this.updateEditData(textData);
|
|
589
|
+
|
|
590
|
+
// 把当前文本画到画布上
|
|
591
|
+
await this.drawText(curData);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 3.裁剪时清除的之前撤销记录
|
|
595
|
+
|
|
596
|
+
// 是否撤销过
|
|
597
|
+
const isRepeal = this.curStep !== this.editRecordData.length - 1;
|
|
598
|
+
|
|
599
|
+
// 因为裁剪操作存在取消的场景,所以如果确认裁剪则删除之前撤销记录
|
|
600
|
+
// 如果编辑类型为裁剪,且撤销过
|
|
601
|
+
if (CAN_CANCEL_EDIT_TYPE.includes(type) && isRepeal) {
|
|
602
|
+
// 过滤编辑记录数据,只保留到当前步骤为止的数据
|
|
603
|
+
this.editRecordData = this.editRecordData.filter(
|
|
604
|
+
(_, index) => index <= this.curStep
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
// 如果定义了 updateEditRecordDataCb 回调函数,则调用它并传入更新后的编辑记录数据
|
|
608
|
+
this.updateEditRecordDataCb &&
|
|
609
|
+
this.updateEditRecordDataCb(this.editRecordData);
|
|
610
|
+
|
|
611
|
+
// 如果是 H5 环境,则清理超过当前步骤的图片资源缓存
|
|
612
|
+
|
|
613
|
+
// #ifdef H5
|
|
614
|
+
const deleteImgs = this.editRecordData
|
|
615
|
+
.filter((_, index) => index > this.curStep && item.imgData?.url)
|
|
616
|
+
.map((item) => item.imgData?.url);
|
|
617
|
+
clearUrlBlobCache(deleteImgs);
|
|
618
|
+
// #endif
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 如果渲染类型不是图片类型,则直接返回
|
|
622
|
+
if (renderType !== IMG_EDITOR_RENDER_TYPE.img) {
|
|
623
|
+
|
|
624
|
+
this.renderCanvasLoading = true;
|
|
625
|
+
|
|
626
|
+
this.renderCanvasLoadingCb && this.renderCanvasLoadingCb(this.renderCanvasLoading)
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// 等待一段时间后继续执行后续操作,这里可能是为了确保图片渲染完成
|
|
631
|
+
await renderAwait(200)
|
|
632
|
+
|
|
633
|
+
// 如果传入了已经渲染好的图片地址则直接渲染并添加到记录,目前裁剪场景是传入直接渲染好的图片
|
|
634
|
+
if (imgData?.url) {
|
|
635
|
+
// 从 data 中解构出 img 相关的属性(img, imgWidth, imgHeight)
|
|
636
|
+
curData.imgData = imgData;
|
|
637
|
+
|
|
638
|
+
// 使用解构出的图片数据重新渲染图片
|
|
639
|
+
await this.renderByImg(curData.imgData.url, curData.imgData.width, curData.imgData.height);
|
|
640
|
+
} else {
|
|
641
|
+
// 如果 data 中没有 img 属性,则尝试将当前 canvas 转换为图片 URL
|
|
642
|
+
try {
|
|
643
|
+
// 调用 canvasToImgUrl 方法,并等待其异步完成
|
|
644
|
+
const res = await this.canvasToImgUrl();
|
|
645
|
+
|
|
646
|
+
// 更新当前步骤的编辑记录数据,使用生成的图片 URL 和画布尺寸
|
|
647
|
+
curData.imgData = {
|
|
648
|
+
...res,
|
|
649
|
+
width: this.width,
|
|
650
|
+
height: this.height,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// 如果定义了 updateImgUrlCb 回调函数,则调用它并传入生成的图片 URL
|
|
654
|
+
this.updateImgUrlCb && this.updateImgUrlCb(res.url);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
// 如果在转换 canvas 到图片 URL 的过程中出现错误,则调用 handleEditFail 方法处理
|
|
657
|
+
this.handleEditFail(err);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 编辑后校验当前编辑图片是否生成正常,如果不正常则撤回该条记录
|
|
662
|
+
if (this.editFinishValid) {
|
|
663
|
+
try {
|
|
664
|
+
await renderAwait(100);
|
|
665
|
+
await this.validImage();
|
|
666
|
+
console.log(`图片生成成功`);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.log(`图片生成失败:${error},返回上一次编辑状态`);
|
|
669
|
+
this.handleEditFail(error);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this.renderCanvasLoading = false;
|
|
674
|
+
|
|
675
|
+
this.renderCanvasLoadingCb && this.renderCanvasLoadingCb(this.renderCanvasLoading);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 执行涂鸦操作
|
|
679
|
+
async doGraffitiOperation(data) {
|
|
680
|
+
// 从data对象中解构出所需的属性
|
|
681
|
+
const { x, y, points, shape, renderType, isReserve } = data;
|
|
682
|
+
|
|
683
|
+
// 判断是否处于编辑状态(即x和y坐标是否已定义)
|
|
684
|
+
const isEditting = x !== undefined && y !== undefined;
|
|
685
|
+
|
|
686
|
+
// 如果处于编辑状态
|
|
687
|
+
if (isEditting) {
|
|
688
|
+
// 如果涂鸦的形状是曲线,或者当前只有一个点
|
|
689
|
+
if (shape == IMG_EDITOR_GRAFFITI_SHAPE_TYPE.curve || points.length == 1) {
|
|
690
|
+
// 将新的点(包含x和y坐标)添加到points数组中
|
|
691
|
+
points.push({
|
|
692
|
+
x,
|
|
693
|
+
y,
|
|
694
|
+
});
|
|
695
|
+
// 如果涂鸦不是曲线,并且已经有至少两个点(在此代码逻辑中,我们假设涂鸦至少需要两个点)
|
|
696
|
+
} else {
|
|
697
|
+
// 更新points数组中的第二个点(索引为1)的坐标
|
|
698
|
+
points[1] = {
|
|
699
|
+
x,
|
|
700
|
+
y,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
switch (shape) {
|
|
706
|
+
// 直线
|
|
707
|
+
case IMG_EDITOR_GRAFFITI_SHAPE_TYPE.line:
|
|
708
|
+
await this.drawLine({
|
|
709
|
+
startX: points[0].x,
|
|
710
|
+
startY: points[0].y,
|
|
711
|
+
x: points[1].x,
|
|
712
|
+
y: points[1].y,
|
|
713
|
+
...pick(data, ["rough", "color"]),
|
|
714
|
+
isReserve: !!isReserve,
|
|
715
|
+
});
|
|
716
|
+
break;
|
|
717
|
+
// 曲线
|
|
718
|
+
case IMG_EDITOR_GRAFFITI_SHAPE_TYPE.curve:
|
|
719
|
+
const len = points.length;
|
|
720
|
+
|
|
721
|
+
await this.drawCurve({
|
|
722
|
+
startX: points[len - 2].x,
|
|
723
|
+
startY: points[len - 2].y,
|
|
724
|
+
x: points[len - 1].x,
|
|
725
|
+
y: points[len - 1].y,
|
|
726
|
+
...pick(data, ["rough", "color"]),
|
|
727
|
+
isReserve: !!isReserve,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// 如果是用图片来渲染,不需要保存每个点位的数据,只保存最后两个点位的数据
|
|
731
|
+
if (renderType === IMG_EDITOR_RENDER_TYPE.img) {
|
|
732
|
+
points.shift();
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
// 空心圆
|
|
736
|
+
case IMG_EDITOR_GRAFFITI_SHAPE_TYPE.hollowCircle:
|
|
737
|
+
await this.drawHollwCircle({
|
|
738
|
+
startX: points[0].x,
|
|
739
|
+
startY: points[0].y,
|
|
740
|
+
x: points[1].x,
|
|
741
|
+
y: points[1].y,
|
|
742
|
+
...pick(data, ["fillColor", "color", "rough"]),
|
|
743
|
+
isReserve: !!isReserve,
|
|
744
|
+
});
|
|
745
|
+
break;
|
|
746
|
+
// 空心正方形
|
|
747
|
+
case IMG_EDITOR_GRAFFITI_SHAPE_TYPE.hollowRect:
|
|
748
|
+
await this.drawHollowRect({
|
|
749
|
+
startX: points[0].x,
|
|
750
|
+
startY: points[0].y,
|
|
751
|
+
x: points[1]?.x,
|
|
752
|
+
y: points[1]?.y,
|
|
753
|
+
...pick(data, ["fillColor", "color", "rough"]),
|
|
754
|
+
isReserve: !!isReserve,
|
|
755
|
+
});
|
|
756
|
+
break;
|
|
757
|
+
// 实心正方形
|
|
758
|
+
case IMG_EDITOR_GRAFFITI_SHAPE_TYPE.rect:
|
|
759
|
+
await this.drawRect({
|
|
760
|
+
startX: points[0].x,
|
|
761
|
+
startY: points[0].y,
|
|
762
|
+
x: points[1].x,
|
|
763
|
+
y: points[1].y,
|
|
764
|
+
...pick(data, ["fillColor", "color", "rough"]),
|
|
765
|
+
isReserve: !!isReserve,
|
|
766
|
+
});
|
|
767
|
+
break;
|
|
768
|
+
// 实心圆
|
|
769
|
+
case IMG_EDITOR_GRAFFITI_SHAPE_TYPE.circle:
|
|
770
|
+
await this.drawCircle({
|
|
771
|
+
startX: points[0].x,
|
|
772
|
+
startY: points[0].y,
|
|
773
|
+
x: points[1].x,
|
|
774
|
+
y: points[1].y,
|
|
775
|
+
...pick(data, ["fillColor", "color", "rough"]),
|
|
776
|
+
isReserve: !!isReserve,
|
|
777
|
+
});
|
|
778
|
+
break;
|
|
779
|
+
default:
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 判断图片是否生成正常
|
|
785
|
+
validImage() {
|
|
786
|
+
return new Promise((resolve, reject) => {
|
|
787
|
+
uni.canvasGetImageData({
|
|
788
|
+
canvasId: this.canvasId,
|
|
789
|
+
x: 0,
|
|
790
|
+
y: 0,
|
|
791
|
+
width: this.width,
|
|
792
|
+
height: this.height,
|
|
793
|
+
success: ({ data }) => {
|
|
794
|
+
const valid = validImgHasEmpty(
|
|
795
|
+
data,
|
|
796
|
+
this.width * this.height,
|
|
797
|
+
this.maxTransparencyRate
|
|
798
|
+
);
|
|
799
|
+
valid ? resolve(valid) : reject("图片空白部分过大");
|
|
800
|
+
},
|
|
801
|
+
fail: (err) => {
|
|
802
|
+
reject(err);
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// restoreRecord 方法用于恢复指定步骤的记录
|
|
809
|
+
restoreRecord(step = this.curStep) {
|
|
810
|
+
// 从当前编辑记录数据(this.editRecordData)中获取指定步骤(step)的记录,
|
|
811
|
+
// 如果没有找到,则使用空对象{}作为默认值
|
|
812
|
+
const { imgData, renderType } = this.editRecordData[step] || {};
|
|
813
|
+
|
|
814
|
+
// 过滤出所有步骤小于或等于给定步骤的记录,存入renderRecords数组
|
|
815
|
+
const renderRecords = this.editRecordData.filter(
|
|
816
|
+
(_, index) => index <= step
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
// 如果当前记录的渲染类型是图片(IMG_EDITOR_RENDER_TYPE.img),并且图片存在(img)
|
|
820
|
+
if (renderType === IMG_EDITOR_RENDER_TYPE.img && imgData?.url) {
|
|
821
|
+
// 调用renderByImg方法渲染图片
|
|
822
|
+
this.renderByImg(imgData.url, imgData.width, imgData.height);
|
|
823
|
+
}
|
|
824
|
+
// 如果当前记录的渲染类型是数据(IMG_EDITOR_RENDER_TYPE.data)
|
|
825
|
+
else if (renderType === IMG_EDITOR_RENDER_TYPE.data) {
|
|
826
|
+
// 查找renderRecords数组中最后一个类型为图片并且图片存在的记录的索引
|
|
827
|
+
// 因为使用图片渲染的包含它之前所有编辑记录的修改,所以不需要渲染它之前的步骤的修改
|
|
828
|
+
const lastImgRenderIndex = findLastIndex(
|
|
829
|
+
renderRecords,
|
|
830
|
+
(item) =>
|
|
831
|
+
item.renderType === IMG_EDITOR_RENDER_TYPE.img && item.imgData?.url
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
// 如果找到了这样的记录
|
|
835
|
+
if (lastImgRenderIndex !== -1) {
|
|
836
|
+
// 过滤出从最后一个图片记录开始的所有记录,存入renderRecords数组
|
|
837
|
+
renderRecords = renderRecords.filter(
|
|
838
|
+
(_, index) => index >= lastImgRenderIndex
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 遍历过滤后的记录,调用rerender方法重新渲染每一条记录
|
|
843
|
+
for (const curData of renderRecords) {
|
|
844
|
+
this.rerender(curData);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// 重新渲染
|
|
850
|
+
rerender(data) {
|
|
851
|
+
if (data.renderType === IMG_EDITOR_RENDER_TYPE.img && data.imgData?.url) {
|
|
852
|
+
this.renderByImg(
|
|
853
|
+
data.imgData.url,
|
|
854
|
+
data.imgData.width,
|
|
855
|
+
data.imgData.height
|
|
856
|
+
);
|
|
857
|
+
} else if (renderType === IMG_EDITOR_RENDER_TYPE.data) {
|
|
858
|
+
this.renderByData(data);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* 根据传入的数据渲染不同的内容或执行不同的操作
|
|
864
|
+
*
|
|
865
|
+
* @param {Object} data - 包含渲染或操作所需信息的对象
|
|
866
|
+
*/
|
|
867
|
+
renderByData(data) {
|
|
868
|
+
this.recovery();
|
|
869
|
+
|
|
870
|
+
// 从传入的 data 对象中解构出 type 和 shape 属性
|
|
871
|
+
const { type, shape } = data;
|
|
872
|
+
|
|
873
|
+
// 根据 type 属性的值执行不同的操作
|
|
874
|
+
switch (type) {
|
|
875
|
+
// 如果 type 是 "graffiti"(涂鸦)
|
|
876
|
+
case IMG_EDITOR_EDIT_TYPE.graffiti:
|
|
877
|
+
// 如果 shape 是 "curve"(曲线)
|
|
878
|
+
if (shape === IMG_EDITOR_GRAFFITI_SHAPE_TYPE.curve) {
|
|
879
|
+
// 调用 drawCurveOnce 方法来绘制一次曲线,并传入 data 作为参数
|
|
880
|
+
this.drawCurveOnce(data);
|
|
881
|
+
} else {
|
|
882
|
+
// 如果 shape 不是 "curve",则执行一般的涂鸦操作
|
|
883
|
+
// 调用 doGraffitiOperation 方法,并传入 data 作为参数
|
|
884
|
+
this.doGraffitiOperation(data);
|
|
885
|
+
}
|
|
886
|
+
break;
|
|
887
|
+
|
|
888
|
+
// 如果 type 是 "text"(文本)
|
|
889
|
+
case IMG_EDITOR_EDIT_TYPE.text:
|
|
890
|
+
// 调用 drawText 方法来绘制文本,并传入 data 作为参数
|
|
891
|
+
this.drawText(data);
|
|
892
|
+
break;
|
|
893
|
+
|
|
894
|
+
// 如果 type 不匹配上述任何一种情况
|
|
895
|
+
default:
|
|
896
|
+
// 不执行任何操作,直接跳出 switch 语句
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* 在绘图上下文中绘制文本
|
|
903
|
+
*
|
|
904
|
+
* @param {Object} [opts={}] - 绘制文本的配置选项
|
|
905
|
+
* @param {string} [opts.text] - 要绘制的文本
|
|
906
|
+
* @param {number} [opts.x] - 文本开始的 x 坐标
|
|
907
|
+
* @param {number} [opts.y] - 文本开始的 y 坐标
|
|
908
|
+
* @param {number} [opts.maxWidth] - 文本允许的最大宽度(用于换行)
|
|
909
|
+
* @param {string} [opts.color=this.defaultColor] - 文本颜色
|
|
910
|
+
* @param {number} [opts.size=this.defaultFontSize] - 文本字体大小
|
|
911
|
+
* @param {string} [opts.align] - 文本对齐方式
|
|
912
|
+
* @param {string} [opts.baseline="top"] - 文本基线对齐方式
|
|
913
|
+
* @param {number} [opts.lineHeight=this.defaultFontSize] - 行高
|
|
914
|
+
*/
|
|
915
|
+
drawText(opts = {}) {
|
|
916
|
+
// 解构赋值,从 opts 对象中获取配置选项,并设置默认值
|
|
917
|
+
const {
|
|
918
|
+
text,
|
|
919
|
+
x,
|
|
920
|
+
y,
|
|
921
|
+
maxWidth,
|
|
922
|
+
color = this.defaultColor,
|
|
923
|
+
size = this.defaultFontSize,
|
|
924
|
+
align,
|
|
925
|
+
baseline = "top",
|
|
926
|
+
lineHeight = this.defaultFontSize,
|
|
927
|
+
} = opts;
|
|
928
|
+
|
|
929
|
+
// 设置字体大小
|
|
930
|
+
this.ctx.setFontSize(size);
|
|
931
|
+
|
|
932
|
+
// 设置文本颜色
|
|
933
|
+
this.ctx.setFillStyle(color);
|
|
934
|
+
|
|
935
|
+
// 如果 baseline 存在,则设置文本基线对齐方式
|
|
936
|
+
baseline && this.ctx.setTextBaseline(baseline);
|
|
937
|
+
|
|
938
|
+
// 如果 align 存在,则设置文本对齐方式
|
|
939
|
+
align && this.ctx.setTextAlign(align);
|
|
940
|
+
|
|
941
|
+
// 将文本分割成字符数组
|
|
942
|
+
let chars = text.split("");
|
|
943
|
+
let currentLine = ""; // 当前行存储的字符
|
|
944
|
+
let currentLineWidth = 0; // 当前行的宽度
|
|
945
|
+
|
|
946
|
+
let _y = y; // 当前绘制位置的 y 坐标
|
|
947
|
+
|
|
948
|
+
// 遍历字符数组
|
|
949
|
+
for (let i = 0; i < chars.length; i++) {
|
|
950
|
+
// 测量单个字符的宽度
|
|
951
|
+
let charMetrics = this.ctx.measureText(chars[i]);
|
|
952
|
+
let charWidth = charMetrics.width;
|
|
953
|
+
|
|
954
|
+
// 尝试将字符添加到当前行
|
|
955
|
+
if (currentLineWidth + charWidth <= maxWidth && chars[i] !== '\n') {
|
|
956
|
+
currentLine += chars[i]; // 将字符添加到当前行
|
|
957
|
+
currentLineWidth += charWidth; // 更新当前行宽度
|
|
958
|
+
} else {
|
|
959
|
+
// 如果当前行超出最大宽度,则绘制当前行并开始新行
|
|
960
|
+
this.ctx.fillText(currentLine, x, _y); // 绘制当前行
|
|
961
|
+
|
|
962
|
+
_y += lineHeight; // 将 y 坐标移动到下一行的起始位置
|
|
963
|
+
currentLine = chars[i]; // 新行开始只有这一个字符
|
|
964
|
+
currentLineWidth = charWidth; // 更新当前行宽度
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// 填入最后一行的文字,用于未超出最大宽度的行的情况
|
|
968
|
+
if (i === chars.length - 1 && currentLine) {
|
|
969
|
+
this.ctx.fillText(currentLine, x, _y);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return this.draw(true);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// 绘制直线的方法
|
|
977
|
+
drawLine(opts) {
|
|
978
|
+
const {
|
|
979
|
+
startX, // 直线起点的 x 坐标
|
|
980
|
+
startY, // 直线起点的 y 坐标
|
|
981
|
+
x, // 直线终点的 x 坐标
|
|
982
|
+
y, // 直线终点的 y 坐标
|
|
983
|
+
isReserve = false, // 是否保留(可能是绘制到某个缓冲区或离屏渲染)
|
|
984
|
+
rough = this.defaultRough, // 线条的“粗糙度”或宽度,默认为某个默认值
|
|
985
|
+
color = this.defaultColor, // 线条颜色,默认为某个默认值
|
|
986
|
+
isCurve = false, // 是否是曲线,默认为 false
|
|
987
|
+
} = opts;
|
|
988
|
+
|
|
989
|
+
if (isCurve) {
|
|
990
|
+
// 设置画笔的端点样式为圆形
|
|
991
|
+
this.ctx.setLineCap("round");
|
|
992
|
+
|
|
993
|
+
// 设置画笔线段连接处样式为圆形
|
|
994
|
+
this.ctx.setLineJoin("round");
|
|
995
|
+
} else {
|
|
996
|
+
|
|
997
|
+
// 设置画笔的端点样式为正方形
|
|
998
|
+
this.ctx.setLineCap("square");
|
|
999
|
+
|
|
1000
|
+
// 设置画笔线段连接处样式为斜接
|
|
1001
|
+
this.ctx.setLineJoin("miter");
|
|
1002
|
+
|
|
1003
|
+
this.ctx.beginPath();
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// 设置起点
|
|
1009
|
+
this.ctx.moveTo(startX, startY);
|
|
1010
|
+
// 设置线条宽度
|
|
1011
|
+
this.ctx.setLineWidth(rough);
|
|
1012
|
+
// 设置线条颜色
|
|
1013
|
+
this.ctx.setStrokeStyle(color);
|
|
1014
|
+
|
|
1015
|
+
// 绘制直线到指定点
|
|
1016
|
+
this.ctx.lineTo(x, y);
|
|
1017
|
+
// 描边路径,即绘制线条
|
|
1018
|
+
this.ctx.stroke();
|
|
1019
|
+
|
|
1020
|
+
return this.draw(isReserve);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// 绘制曲线的方法(但这里只是简单地使用 drawLine 绘制了一系列直线段)
|
|
1024
|
+
drawCurve(opts) {
|
|
1025
|
+
return this.drawLine({ ...opts, isReserve: true, isCurve: true }); // 调用 drawLine 方法并设置 isReserve 为 true
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// 一次性绘制完整的曲线
|
|
1029
|
+
drawCurveOnce(data) {
|
|
1030
|
+
const { points } = data;
|
|
1031
|
+
|
|
1032
|
+
points.forEach((_, index) => {
|
|
1033
|
+
if (index < points.length - 1) {
|
|
1034
|
+
// 遍历除最后一个点之外的所有点
|
|
1035
|
+
this.drawCurve({
|
|
1036
|
+
startX: points[index].x,
|
|
1037
|
+
startY: points[index].y,
|
|
1038
|
+
x: points[index + 1].x,
|
|
1039
|
+
y: points[index + 1].y,
|
|
1040
|
+
...pick(data, ["rough", "color"]),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// 异步绘制的方法(使用 setTimeout 模拟异步)
|
|
1047
|
+
drawAsync(isReserve = true, cb = () => {}) {
|
|
1048
|
+
setTimeout(() => {
|
|
1049
|
+
this.ctx.draw(isReserve, cb); // 调用自定义的 draw 方法(可能是异步的)并在完成后调用回调函数 cb
|
|
1050
|
+
}, 200); // 延迟 200 毫秒执行
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// 绘制空心矩形的方法
|
|
1054
|
+
drawHollowRect(opts) {
|
|
1055
|
+
return this.drawRect({ ...opts, isHollow: true });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// 绘制矩形的方法
|
|
1059
|
+
drawRect(opts) {
|
|
1060
|
+
const {
|
|
1061
|
+
startX, // 矩形左上角的 x 坐标
|
|
1062
|
+
startY, // 矩形左上角的 y 坐标
|
|
1063
|
+
x, // 矩形右下角的 x 坐标
|
|
1064
|
+
y, // 矩形右下角的 y 坐标
|
|
1065
|
+
isHollow = false, // 是否绘制空心矩形
|
|
1066
|
+
fillColor = this.defaultFillColor, // 填充颜色
|
|
1067
|
+
color = this.defaultColor, // 线条颜色
|
|
1068
|
+
rough = this.defaultRough, // 线条宽度或“粗糙度”
|
|
1069
|
+
isReserve = false, // 是否保留
|
|
1070
|
+
} = opts;
|
|
1071
|
+
|
|
1072
|
+
// 计算矩形的宽度和高度
|
|
1073
|
+
let width = Math.abs(x - startX);
|
|
1074
|
+
let height = Math.abs(y - startY);
|
|
1075
|
+
|
|
1076
|
+
// 设置画笔的端点样式为正方形
|
|
1077
|
+
this.ctx.setLineCap("square");
|
|
1078
|
+
|
|
1079
|
+
// 设置画笔线段连接处样式为斜接
|
|
1080
|
+
this.ctx.setLineJoin("miter");
|
|
1081
|
+
|
|
1082
|
+
// 开始一个新的路径
|
|
1083
|
+
this.ctx.beginPath();
|
|
1084
|
+
// 绘制矩形
|
|
1085
|
+
this.ctx.rect(startX, startY, width, height);
|
|
1086
|
+
|
|
1087
|
+
// 如果不是空心矩形,则填充矩形
|
|
1088
|
+
if (!isHollow) {
|
|
1089
|
+
this.ctx.setFillStyle(fillColor);
|
|
1090
|
+
this.ctx.fill();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// 设置线条宽度
|
|
1094
|
+
this.ctx.setLineWidth(rough);
|
|
1095
|
+
// 设置线条颜色
|
|
1096
|
+
this.ctx.setStrokeStyle(color);
|
|
1097
|
+
// 描边矩形
|
|
1098
|
+
this.ctx.stroke();
|
|
1099
|
+
|
|
1100
|
+
return this.draw(isReserve);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// 绘制空心圆的方法
|
|
1104
|
+
drawHollwCircle(opts) {
|
|
1105
|
+
return this.drawCircle({ ...opts, isHollow: true });
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
draw(isReserve = false) {
|
|
1109
|
+
return new Promise((resolve) => {
|
|
1110
|
+
this.ctx.draw(isReserve, () => {
|
|
1111
|
+
resolve();
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// 绘制圆形的方法
|
|
1117
|
+
drawCircle(opts) {
|
|
1118
|
+
const {
|
|
1119
|
+
startX, // 圆的左上角或起始点的 x 坐标
|
|
1120
|
+
startY, // 圆的左上角或起始点的 y 坐标
|
|
1121
|
+
x, // 圆的右下角或终止点的 x 坐标
|
|
1122
|
+
y, // 圆的右下角或终止点的 y 坐标
|
|
1123
|
+
isHollow = false, // 是否绘制空心圆
|
|
1124
|
+
fillColor = this.defaultFillColor, // 填充颜色
|
|
1125
|
+
color = this.defaultColor, // 线条颜色
|
|
1126
|
+
isReserve = false, // 是否保留
|
|
1127
|
+
rough = this.defaultRough, // 线条宽度或“粗糙度”
|
|
1128
|
+
} = opts;
|
|
1129
|
+
|
|
1130
|
+
// 计算圆心和半径
|
|
1131
|
+
let pointLT = {}; // 左上角点
|
|
1132
|
+
let pointRB = {}; // 右下角点
|
|
1133
|
+
let center = {}; // 圆心
|
|
1134
|
+
|
|
1135
|
+
// 左上坐标,
|
|
1136
|
+
pointLT.X = Math.min(startX, x);
|
|
1137
|
+
pointLT.Y = Math.min(startY, y);
|
|
1138
|
+
|
|
1139
|
+
// 右下坐标
|
|
1140
|
+
pointRB.X = Math.max(startX, x);
|
|
1141
|
+
pointRB.Y = Math.max(startY, y);
|
|
1142
|
+
|
|
1143
|
+
// 圆中心点坐标
|
|
1144
|
+
center.X = (pointRB.X + pointLT.X) / 2;
|
|
1145
|
+
center.Y = (pointRB.Y + pointLT.Y) / 2;
|
|
1146
|
+
|
|
1147
|
+
let dx = pointRB.X - pointLT.X;
|
|
1148
|
+
let dy = pointRB.Y - pointLT.Y;
|
|
1149
|
+
|
|
1150
|
+
// 计算圆半径 根据勾股定理
|
|
1151
|
+
let r = Math.sqrt(dx * dx + dy * dy) / 2; // 计算半径
|
|
1152
|
+
|
|
1153
|
+
// 设置画笔的端点样式为正方形
|
|
1154
|
+
this.ctx.setLineCap("square");
|
|
1155
|
+
|
|
1156
|
+
// 设置画笔线段连接处样式为斜接
|
|
1157
|
+
this.ctx.setLineJoin("miter");
|
|
1158
|
+
// 开始一个新的路径
|
|
1159
|
+
this.ctx.beginPath();
|
|
1160
|
+
// 绘制圆形
|
|
1161
|
+
this.ctx.arc(center.X, center.Y, r, 0, 2 * Math.PI);
|
|
1162
|
+
|
|
1163
|
+
// 设置线条宽度
|
|
1164
|
+
this.ctx.setLineWidth(rough);
|
|
1165
|
+
|
|
1166
|
+
// 如果不是空心圆,则填充圆形
|
|
1167
|
+
if (!isHollow) {
|
|
1168
|
+
this.ctx.setFillStyle(fillColor);
|
|
1169
|
+
this.ctx.fill();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// 设置线条颜色
|
|
1173
|
+
this.ctx.setStrokeStyle(color);
|
|
1174
|
+
// 描边圆形
|
|
1175
|
+
this.ctx.stroke();
|
|
1176
|
+
|
|
1177
|
+
return this.draw(isReserve);
|
|
1178
|
+
}
|
|
1179
|
+
}
|