@versa_ai/vmml-editor 1.0.2

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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +335 -0
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +1 -0
  4. package/biome.json +7 -0
  5. package/dist/index.d.mts +5 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +2675 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +2673 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/package.json +48 -0
  12. package/postcss.config.js +3 -0
  13. package/src/assets/css/closeLayer.scss +50 -0
  14. package/src/assets/css/colorSelector.scss +59 -0
  15. package/src/assets/css/editorTextMenu.less +130 -0
  16. package/src/assets/css/editorTextMenu.scss +149 -0
  17. package/src/assets/css/index.scss +252 -0
  18. package/src/assets/css/loading.scss +31 -0
  19. package/src/assets/css/maxTextLayer.scss +31 -0
  20. package/src/assets/img/icon_Brush.png +0 -0
  21. package/src/assets/img/icon_Change.png +0 -0
  22. package/src/assets/img/icon_Cut.png +0 -0
  23. package/src/assets/img/icon_Face.png +0 -0
  24. package/src/assets/img/icon_Graffiti.png +0 -0
  25. package/src/assets/img/icon_Mute.png +0 -0
  26. package/src/assets/img/icon_Refresh.png +0 -0
  27. package/src/assets/img/icon_Text1.png +0 -0
  28. package/src/assets/img/icon_Text2.png +0 -0
  29. package/src/assets/img/icon_Volume.png +0 -0
  30. package/src/assets/img/icon_Word.png +0 -0
  31. package/src/components/CloseLayer.tsx +25 -0
  32. package/src/components/ColorSelector.tsx +90 -0
  33. package/src/components/Controls.tsx +32 -0
  34. package/src/components/EditorCanvas.tsx +566 -0
  35. package/src/components/Loading.tsx +16 -0
  36. package/src/components/MaxTextLayer.tsx +27 -0
  37. package/src/components/SeekBar.tsx +126 -0
  38. package/src/components/TextMenu.tsx +332 -0
  39. package/src/components/VideoMenu.tsx +49 -0
  40. package/src/index.tsx +551 -0
  41. package/src/utils/HistoryClass.ts +131 -0
  42. package/src/utils/VmmlConverter.ts +339 -0
  43. package/src/utils/const.ts +10 -0
  44. package/src/utils/keyBoardUtils.ts +199 -0
  45. package/src/utils/usePeekControl.ts +242 -0
  46. package/tsconfig.json +5 -0
  47. package/tsup.config.ts +14 -0
package/src/index.tsx ADDED
@@ -0,0 +1,551 @@
1
+ import { VmmlPlayer } from "@versa_ai/vmml-player";
2
+ import { type MutableRefObject, forwardRef, useEffect, useRef, useState, useImperativeHandle, useMemo } from "react";
3
+ import type { AnyZodObject } from "zod";
4
+ import { getFrames, takeScreenshot, convertVmmlTextScaleByForbidden } from "@versa_ai/vmml-utils";
5
+ import "./assets/css/index.scss";
6
+ import Controls from "./components/Controls";
7
+ import EditorCanvas from "./components/EditorCanvas";
8
+ import Loaidng from "./components/Loading";
9
+ import MaxTextLayer from "./components/MaxTextLayer";
10
+ import TextMenu from "./components/TextMenu";
11
+ import VideoMenu from "./components/VideoMenu";
12
+ import HistoryClass from "./utils/HistoryClass";
13
+ import VmmlConverter from "./utils/VmmlConverter";
14
+ import { emotionIcon, wordIcon } from "./utils/const";
15
+
16
+ const historyClass = new HistoryClass();
17
+ const EditorFn = <Schema extends AnyZodObject, Props>(
18
+ {
19
+ vmml,
20
+ pauseFrame = 0,
21
+ maxText = 10,
22
+ maxVideo = 5,
23
+ videoMenus = [],
24
+ onMenuChange,
25
+ showTextButtons = false,
26
+ editableArray = [],
27
+ templateId,
28
+ strategyId,
29
+ pauseWhenBuffering = false,
30
+ }: any,
31
+ ref: any,
32
+ ) => {
33
+ vmml = convertVmmlTextScaleByForbidden(vmml);
34
+ const textMenuRef = useRef<any>(null);
35
+ const vmmlPlayerRef = useRef();
36
+ const canvasRef = useRef();
37
+ const fps = 30;
38
+ const once = useRef(false);
39
+ const [player, setPlayer] = useState<any>();
40
+ const [loading, setLoading] = useState<boolean>(true);
41
+ const [frame, setFrame] = useState<number>(pauseFrame);
42
+ const [isPlaying, setIsPlaying] = useState<boolean>(false);
43
+ const [showCanvas, setShowCanvas] = useState(false);
44
+ const [durationInFrames, setDurationInFrames] = useState(getFrames(vmml?.template?.duration || 1, fps));
45
+ const [previewState, setPreviewState] = useState<boolean>(true); // true预览态 false编辑态
46
+ const [menuState, setMenuState] = useState<string>(""); // text 文字菜单 video 表情包菜单 空不显示
47
+ const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0, top: 0 }); // 画布尺寸
48
+ const [textInfo, setTextInfo] = useState({});
49
+ const [isShowMaxTextLayer, setIsShowMaxTextLayer] = useState(false);
50
+ const [buffering, setBuffering] = useState(false);
51
+ const videoList = useRef<any>([]);
52
+ const [signList, setSignList] = useState<any>([]);
53
+ const [tips, setTips] = useState("");
54
+ const [dragState, setDragState] = useState(0);
55
+ const vmmlConverterRef = useRef<any>(null);
56
+ const [initFcObjs, setInitFcObjs] = useState([]);
57
+ const vmmlFlag = useRef(false);
58
+ const needPlay = useRef(true);
59
+
60
+ const setVmml = () => {
61
+ const { current }: any = vmmlPlayerRef;
62
+ if (!current) return;
63
+ if (!once.current) {
64
+ current.setVmml(vmml, pauseFrame);
65
+ } else {
66
+ current.setVmml(vmml, Math.max(0, frame - 3));
67
+ }
68
+ };
69
+
70
+ const hideLoading = () => {
71
+ const { current }: any = vmmlPlayerRef;
72
+ vmmlFlag.current = false;
73
+ if (current && current.playerRef) {
74
+ setPlayer(current.playerRef);
75
+ current.unmute();
76
+ if (!once.current) {
77
+ once.current = true;
78
+ } else {
79
+ if (needPlay.current) {
80
+ current.play();
81
+ } else {
82
+ needPlay.current = true;
83
+ }
84
+ }
85
+ }
86
+ setShowCanvas(false);
87
+ setLoading(false);
88
+ };
89
+
90
+ const onClickMain = () => {
91
+ if (buffering) return
92
+ const { current }: any = vmmlPlayerRef;
93
+ if (current) {
94
+ if (isPlaying) {
95
+ intoEdit();
96
+ } else {
97
+ current.unmute();
98
+ current.play();
99
+ }
100
+ }
101
+ };
102
+
103
+ // 回到预览态
104
+ const enterPreview = (canvas: any, hiddenMenu: boolean = false) => {
105
+ if (hiddenMenu) {
106
+ setMenuState("");
107
+ } else {
108
+ setMenuState("");
109
+ setPreviewState(true);
110
+ historyClass.onCanvasRoute(canvas);
111
+ }
112
+ };
113
+
114
+ const onControlsClick = () => {
115
+ if (previewState) {
116
+ const { current }: any = vmmlPlayerRef;
117
+ current.unmute();
118
+ current.play();
119
+ } else {
120
+ setMenuState("");
121
+ setPreviewState(true);
122
+ }
123
+ }
124
+
125
+ // 点击底部菜单
126
+ const onClickMenu = (type: string) => {
127
+ if (type === "text" && checkTextNum()) return;
128
+ if (type === "video" && checkVideoNum()) return;
129
+ if (previewState) {
130
+ intoEdit();
131
+ }
132
+ setTextInfo({});
133
+ setMenuState(type);
134
+ };
135
+
136
+ const checkTextNum = () => {
137
+ const { current }: any = canvasRef;
138
+ if (current) {
139
+ const textNums = current.getfObjectNums("文字");
140
+ if (textNums >= maxText) {
141
+ setTips(`最多添加${maxText}个文字`);
142
+ setIsShowMaxTextLayer(true);
143
+ return true;
144
+ }
145
+ }
146
+ return false;
147
+ };
148
+ // 检查表情包数量
149
+ const checkVideoNum = () => {
150
+ const { current }: any = canvasRef;
151
+ if (current) {
152
+ const videoNum = current.getfObjectNums("表情包");
153
+ if (videoNum >= maxVideo) {
154
+ setTips(`最多添加${maxVideo}个表情包`);
155
+ setIsShowMaxTextLayer(true);
156
+ return true;
157
+ }
158
+ }
159
+ return false;
160
+ };
161
+
162
+ // 获取画板素材实例
163
+ const getfcObject = () => {
164
+ const { current }: any = canvasRef;
165
+ if (current) {
166
+ return current.getfcObject();
167
+ }
168
+ return []
169
+ }
170
+
171
+ // 暂停进入编辑
172
+ const intoEdit = (frame?: number) => {
173
+ const { current }: any = vmmlPlayerRef;
174
+ if (current) {
175
+ frame && current.seekTo(frame);
176
+ current.pause();
177
+ if (canvasRef.current) {
178
+ const { current: canvasC }: any = canvasRef;
179
+ canvasC.checkObjectInPoint(frame);
180
+ }
181
+ getAllVideoFrames();
182
+ setShowCanvas(true);
183
+ setPreviewState(false);
184
+ }
185
+ };
186
+
187
+ // 获取表情包当前帧截图
188
+ const getAllVideoFrames = () => {
189
+ const { current }: any = canvasRef;
190
+ const doms = document.querySelectorAll("[data-clipid][data-cliptype=video][data-editorType],[data-editclip='true'][data-cliptype=video]");
191
+ for (const item of Array.from(doms)) {
192
+ const id = item.getAttribute("data-clipid");
193
+ const video = item.querySelector("video");
194
+ const base64 = takeScreenshot(video);
195
+ current.updateImage(id, base64);
196
+ }
197
+ switchClipDisplay("none");
198
+ }
199
+
200
+ // 隐藏新增的clip
201
+ const switchClipDisplay = (display: string) => {
202
+ const clips = document.querySelectorAll("[data-editorType],[data-editclip='true']");
203
+ clips.forEach((clip: any) => {
204
+ clip.style.display = display;
205
+ })
206
+ };
207
+
208
+ // 新增表情包
209
+ const createImage = async (file: any, emojiId: string) => {
210
+ const { current }: any = canvasRef;
211
+ if (current && !checkVideoNum()) {
212
+ const fObj = await current.createImage(file, emojiId);
213
+ vmmlConverterRef.current.addVideoClip(fObj);
214
+ setMenuState("");
215
+ setPreviewState(true);
216
+ }
217
+ };
218
+
219
+ const createText = async (data: any) => {
220
+ const { current }: any = canvasRef;
221
+ if (current) {
222
+ const server = data.id ? 'updateText' : 'createText';
223
+ await current[server](data);
224
+ setMenuState("");
225
+ }
226
+ };
227
+
228
+ const onVideoChange = (clipData: any) => {
229
+ const index = videoList.current.findIndex((item: any) => item.id === clipData.id);
230
+ if (index > -1) {
231
+ videoList.current.splice(index, 1);
232
+ } else {
233
+ videoList.current.push(clipData);
234
+ }
235
+ setSignList([...videoList.current]);
236
+ }
237
+
238
+ const handleUndo = () => {
239
+ const { current }: any = canvasRef;
240
+ if (current) {
241
+ current.handleUndo();
242
+ }
243
+ };
244
+ const handleRedo = () => {
245
+ const { current }: any = canvasRef;
246
+ if (current) {
247
+ current.handleRedo();
248
+ }
249
+ };
250
+
251
+ const getActions = () => {
252
+ const { current }: any = canvasRef;
253
+ return current.getActions().length
254
+ }
255
+ const textClose = (id: string) => {
256
+ const { current }: any = canvasRef;
257
+ id && current.changeObjectVisible(id);
258
+ setMenuState("");
259
+ };
260
+
261
+ const intoTextEdit = (textInfo: any) => {
262
+ setMenuState("text");
263
+ setTextInfo(textInfo);
264
+ };
265
+
266
+ const onFrameUpdate = (e: any) => {
267
+ setFrame(e.detail.frame)
268
+ };
269
+ const onPlay = () => {
270
+ setIsPlaying(true);
271
+ };
272
+ const onPause = () => {
273
+ setIsPlaying(false);
274
+ };
275
+ const onWaiting = () => {
276
+ setShowCanvas(false);
277
+ setPreviewState(true);
278
+ setBuffering(true);
279
+ }
280
+
281
+ const onResume = () => {
282
+ setBuffering(false);
283
+ }
284
+
285
+ // 初始化可编辑的clip
286
+ const initCanEditClips = (tracks: any = []) => {
287
+ if (editableArray.length) {
288
+ const list = findEditClips(tracks);
289
+ if (list.length) {
290
+ const { current }: any = canvasRef;
291
+ list.forEach((clip: any) => {
292
+ if (clip.videoClip) {
293
+ current.createImageFromClip(clip);
294
+ } else {
295
+ current.createTextFromClip(clip);
296
+ }
297
+ })
298
+ }
299
+ }
300
+ };
301
+
302
+ const findEditClips = (tracks: any = []) => {
303
+ const list: any = [];
304
+ tracks.forEach((track: any) => {
305
+ track.clips.forEach((clip: any) => {
306
+ if (editableArray.includes(clip.id)) {
307
+ clip.edit = true;
308
+ list.push(clip);
309
+ }
310
+ })
311
+ });
312
+ return list
313
+ }
314
+
315
+ // 返序列化fc对象
316
+ const initFcObjects = (tracks: any = []) => {
317
+ const fcTracks = tracks.filter((item: any) => item.editorType);
318
+ if (fcTracks.length) {
319
+ const clips = fcTracks.map((item: any) => item.clips).flat().filter((item: any) => Reflect.has(item, "fObj"));
320
+ const fObjs = clips.map((item: any) => item.fObj);
321
+ setInitFcObjs(fObjs);
322
+ // 表情包标记
323
+ fObjs.forEach((item: any) => {
324
+ onVideoChange(item.clipData);
325
+ });
326
+ }
327
+ }
328
+
329
+ // 获取更新后的vmml
330
+ const getVmml = () => {
331
+ try {
332
+ const tracks = vmml.template.tracks.filter((item: any) => item.editorType === "文字");
333
+ tracks.forEach((track: any) => {
334
+ track.clips.forEach((clip: any) => {
335
+ clip.fObj.src = '';
336
+ })
337
+ })
338
+ } catch {
339
+ console.log("出错了")
340
+ }
341
+ return vmml
342
+ };
343
+
344
+ const getPlayer = () => {
345
+ return player
346
+ }
347
+
348
+ useEffect(() => {
349
+ if (onMenuChange && typeof onMenuChange === "function") {
350
+ onMenuChange(menuState);
351
+ }
352
+ }, [menuState]);
353
+
354
+ useEffect(() => {
355
+ if (previewState && !vmmlFlag.current) {
356
+ vmmlFlag.current = true;
357
+ setVmml();
358
+ }
359
+ }, [previewState]);
360
+
361
+ useEffect(() => {
362
+ if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current && vmml) {
363
+ vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
364
+ }
365
+ if (canvasSize.width) {
366
+ initCanEditClips(vmml.template.tracks);
367
+ }
368
+ }, [canvasSize, vmml]);
369
+
370
+ useEffect(() => {
371
+ if (vmml) {
372
+ initFcObjects(vmml.template.tracks);
373
+ }
374
+ }, [vmml]);
375
+
376
+ useEffect(() => {
377
+ if (!showCanvas) {
378
+ switchClipDisplay("block");
379
+ }
380
+ }, [showCanvas])
381
+
382
+ useEffect(() => {
383
+ if (player) {
384
+ const parent = player.getContainerNode();
385
+ if (!canvasSize.width) {
386
+ const playerElement = parent?.children[0];
387
+ const { width, height } = playerElement?.getBoundingClientRect() || {};
388
+ setCanvasSize({ width, height, top: playerElement.offsetTop });
389
+ }
390
+ player.addEventListener("play", onPlay);
391
+ player.addEventListener("pause", onPause);
392
+ player.addEventListener("frameupdate", onFrameUpdate);
393
+ player.addEventListener("waiting", onWaiting);
394
+ player.addEventListener("resume", onResume);
395
+ return () => {
396
+ player.removeEventListener("play", onPlay);
397
+ player.removeEventListener("pause", onPause);
398
+ player.removeEventListener("frameupdate", onFrameUpdate);
399
+ player.removeEventListener("waiting", onWaiting);
400
+ player.removeEventListener("resume", onResume);
401
+ }
402
+ }
403
+ }, [player]);
404
+
405
+
406
+ useEffect(() => {
407
+ if (dragState === 2) {
408
+ needPlay.current = false;
409
+ const { current }: any = vmmlPlayerRef;
410
+ current.setVmml(vmml, frame, false);
411
+ setShowCanvas(false);
412
+ }
413
+ if (dragState === 3 || dragState === 4) {
414
+ setShowCanvas(true);
415
+ setPreviewState(false);
416
+ }
417
+ }, [dragState]);
418
+
419
+ useImperativeHandle(ref,
420
+ () => ({
421
+ getActions,
422
+ getfcObject,
423
+ getVmml,
424
+ getPlayer,
425
+ texteditClose,
426
+ textFinish
427
+ }),
428
+ [vmml, player]
429
+ )
430
+ const texteditClose = ()=>{
431
+ if (textMenuRef) {
432
+ textMenuRef.current.close();
433
+ }
434
+ }
435
+ const textFinish = () =>{
436
+ if (textMenuRef) {
437
+ textMenuRef.current.handleSubmit();
438
+ }
439
+ }
440
+
441
+ const menuStyles: any = useMemo(() => {
442
+ if (loading) {
443
+ return {
444
+ opacity: 0.4,
445
+ pointerEvents: 'none'
446
+ }
447
+ }
448
+ return {}
449
+ }, [loading])
450
+
451
+ return (
452
+ <>
453
+ <div className="editor">
454
+ <div className="editor-vessel" onClick={() => {
455
+ setMenuState("")
456
+ }}>
457
+ <section className={`main ${menuState === 'text' ? 'text-style' : ''}`}>
458
+ <div className="vessel" onClick={onClickMain}>
459
+ <VmmlPlayer
460
+ ref={vmmlPlayerRef}
461
+ vmml={vmml}
462
+ existenceBorderRadio
463
+ moveToBeginningWhenEnded
464
+ isEditorState
465
+ muted={true}
466
+ fps={fps}
467
+ hideLoading={hideLoading}
468
+ editableArray={editableArray}
469
+ premountFor={40}
470
+ pauseWhenBuffering={pauseWhenBuffering}
471
+ />
472
+ </div>
473
+ <EditorCanvas
474
+ ref={canvasRef}
475
+ previewState={previewState}
476
+ showCanvas={showCanvas}
477
+ canvasSize={canvasSize}
478
+ enterPreview={enterPreview}
479
+ intoTextEdit={intoTextEdit}
480
+ frame={frame}
481
+ vmml={vmml}
482
+ dragState={dragState}
483
+ initFcObjs={initFcObjs}
484
+ onVideoChange={onVideoChange}
485
+ // textInfoReset={textInfoReset}
486
+ />
487
+ <div className="controls-box">
488
+ <Controls
489
+ player={player}
490
+ vmmlRef={vmmlPlayerRef}
491
+ fps={fps}
492
+ durationInFrames={durationInFrames}
493
+ intoEdit={intoEdit}
494
+ frame={frame}
495
+ isPlaying={isPlaying}
496
+ setDragState={setDragState}
497
+ onControlsClick={onControlsClick}
498
+ signList={signList}
499
+ />
500
+ </div>
501
+ <Loaidng show={loading} />
502
+ </section>
503
+ </div>
504
+ <div className="padding-box"></div>
505
+ <section style={menuStyles} className={`footer ${menuState === 'text' ? 'text-style' : ''}`}>
506
+ {
507
+ maxText > 0 && (
508
+ <div
509
+ onClick={() => onClickMenu("text")}
510
+ data-aspm-click="c375146.d480849"
511
+ data-aspm-expo
512
+ data-aspm-param={`Template_Id=${templateId}^Strategy_Id=${strategyId}`}
513
+ >
514
+ <img src={wordIcon} alt="word" />
515
+ <p>文字</p>
516
+ </div>
517
+ )
518
+ }
519
+ {
520
+ maxVideo > 0 && (
521
+ <div
522
+ onClick={() => onClickMenu("video")}
523
+ data-aspm-click="c375146.d480850"
524
+ data-aspm-expo
525
+ data-aspm-param={`Template_Id=${templateId}^Strategy_Id=${strategyId}`}
526
+ >
527
+ <img src={emotionIcon} alt="emotion" />
528
+ <p>表情包</p>
529
+ </div>
530
+ )
531
+ }
532
+ </section>
533
+ {
534
+ maxVideo > 0 && (
535
+ <VideoMenu menuState={menuState} createImage={createImage} videoMenus={videoMenus} />
536
+ )
537
+ }
538
+ {menuState === "text" && (
539
+ <TextMenu ref={textMenuRef} createText={createText} textClose={textClose} textInfo={textInfo} showTextButtons={showTextButtons} />
540
+ )}
541
+ </div>
542
+ <MaxTextLayer textLayerHide={() => setIsShowMaxTextLayer(false)} show={isShowMaxTextLayer} text={tips} />
543
+ </>
544
+ );
545
+ };
546
+
547
+ const forward = forwardRef as <T, P = { [propName: string]: any }>(
548
+ render: (props: P, PlayerRef: React.MutableRefObject<T>) => React.ReactElement | null,
549
+ ) => (props: P & React.RefAttributes<T>) => React.ReactElement | null;
550
+
551
+ export const Editor = forward(EditorFn);
@@ -0,0 +1,131 @@
1
+ import type { fabric } from "fabric";
2
+
3
+ //重写Canvas类的方法
4
+ export default class HistoryClass {
5
+ historyUndo: any[];
6
+ historyRedo: any[];
7
+ cacheAction: any[];
8
+ canvas: fabric.Canvas | any;
9
+ historyProcessing: boolean;
10
+ editorInstance: any[];
11
+ actionTypeList: any[];
12
+ constructor(canvas?: fabric.Canvas) {
13
+ console.log("history init");
14
+ this.canvas = canvas;
15
+ this.historyUndo = [];
16
+ this.historyRedo = [];
17
+ this.cacheAction = [];
18
+ this.editorInstance = [];
19
+ this.actionTypeList = [];
20
+ this.historyProcessing = false;
21
+ this.initHistory();
22
+ }
23
+
24
+ initHistory() {
25
+ this.historyUndo = [];
26
+ this.historyRedo = [];
27
+ this.cacheAction = [];
28
+ this.canvas && this.canvas.on(this._historyEvents());
29
+ }
30
+ _historyNext() {
31
+ const keys = [
32
+ "id",
33
+ "gradientAngle",
34
+ "selectable",
35
+ "hasControls",
36
+ "linkData",
37
+ "editable",
38
+ "extensionType",
39
+ "extension",
40
+ "clipData",
41
+ ];
42
+ const getJson = this.canvas && this.canvas.toJSON(keys);
43
+ return getJson;
44
+ }
45
+
46
+ _historyEvents() {
47
+ return {
48
+ "object:added": (e: any) => this._historySaveAction(e, "object:added"),
49
+ "object:removed": (e: any) => this._historySaveAction(e, "object:removed"),
50
+ "object:modified": (e: any) => this._historySaveAction(e, "object:modified"),
51
+ "object:skewing": (e: any) => this._historySaveAction(e, "object:skewing"),
52
+ "object:muteChange": (e: any) => this._historySaveAction(e, "object:muteChange"),
53
+ };
54
+ }
55
+ _historySaveAction(e: any, type: string) {
56
+ if (this.historyProcessing) return;
57
+ this.actionTypeList.length < 1 && this.actionTypeList.push({ type });
58
+ if (!e || (e.target && !e.target.excludeFromExport)) {
59
+ const json = this._historyNext();
60
+ this.cacheAction.push(json);
61
+ if (this.cacheAction.length > 1) {
62
+ const cacheItem = this.cacheAction.shift();
63
+ //this.historyUndo.push(cacheItem);
64
+ }
65
+ this.historyRedo = [];
66
+ // console.log("_historySaveAction", this.historyUndo);
67
+ this.canvas.fire("history:appnd", { json: json });
68
+ }
69
+ }
70
+
71
+ undo(callback?: any) {
72
+ // undo进程会渲染新的对象的状态
73
+ // 因此,object:added和object:modified事件将再次触发
74
+ // 设置一个flag用来忽略这些事件
75
+ this.historyProcessing = true;
76
+ const history = this.historyUndo.pop();
77
+ if (history) {
78
+ // this.historyRedo.push(this._historyNext());
79
+ this._loadHistory(history, "history:undo", callback);
80
+ } else {
81
+ this.historyProcessing = false;
82
+ }
83
+ }
84
+ redo(callback?: any) {
85
+ // undo进程会渲染新的对象的状态
86
+ // 因此,object:added和object:modified事件将再次触发
87
+ // 设置一个flag用来忽略这些事件
88
+ this.historyProcessing = true;
89
+ const history = this.historyRedo.pop();
90
+ if (history) {
91
+ //每一个redo action实际上是一个新的action到undo中
92
+ this.historyUndo.push(this._historyNext());
93
+ this._loadHistory(history, "history:redo", callback);
94
+ } else {
95
+ this.historyProcessing = false;
96
+ }
97
+ }
98
+ _loadHistory(history: string | Record<string, any>, event: string, callback: () => void) {
99
+ this.canvas &&
100
+ this.canvas.loadFromJSON(history, () => {
101
+ this.canvas.renderAll();
102
+ this.canvas.fire(event);
103
+ this.historyProcessing = false;
104
+
105
+ if (callback && typeof callback === "function") callback();
106
+ });
107
+ }
108
+ _historyDispose() {
109
+ this.canvas.off(this._historyEvents());
110
+ }
111
+ clearHistory() {
112
+ this.historyUndo = [];
113
+ this.historyRedo = [];
114
+ this.cacheAction = [];
115
+ this.canvas.fire("history:clear");
116
+ }
117
+ canUndo() {
118
+ return this.historyUndo.length > 0;
119
+ }
120
+
121
+ canRedo() {
122
+ return this.historyRedo.length > 0;
123
+ }
124
+ //离开画布保存上一次编辑的状态
125
+ onCanvasRoute(canvas: fabric.Canvas) {
126
+ this.editorInstance.push(canvas);
127
+ }
128
+ getActionType() {
129
+ return this.actionTypeList;
130
+ }
131
+ }