@versa_ai/vmml-editor 1.0.15 → 1.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versa_ai/vmml-editor",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "module": "dist/index.mjs",
5
5
  "main": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -8,9 +8,11 @@ import { usePeekControl } from "../utils/usePeekControl";
8
8
  import { toSvg } from 'dom-to-image'
9
9
 
10
10
  const EditorCanvas = forwardRef(
11
- ({ previewState, showCanvas, canvasSize, enterPreview, intoTextEdit, frame, vmml, dragState, initFcObjs, onVideoChange, isBatchModify, hideConfig }: any, ref: any) => {
11
+ ({ previewState, showCanvas, canvasSize, enterPreview, intoTextEdit, frame, vmml, dragState, initFcObjs, editClips = [], onVideoChange, isBatchModify, hideConfig }: any, ref: any) => {
12
12
  const [fc, setFc] = useState<any>(null);
13
13
  const [history, setHistory] = useState<any>(null);
14
+ const [canvasReady, setCanvasReady] = useState(false);
15
+ const editRenderTime = useRef(0);
14
16
  const waitFcTasks = useRef<any>([]);
15
17
  const vmmlConverterRef = useRef<any>(null);
16
18
  const heightScaleRef = useRef<number>(1);
@@ -31,7 +33,8 @@ const EditorCanvas = forwardRef(
31
33
  setFc(canvas);
32
34
  initCanvasEvent(canvas);
33
35
  usePeekControl(canvas, hideConfig);
34
- };
36
+ setCanvasReady(true);
37
+ };
35
38
 
36
39
  const createFcObjs = (canvas: any) => {
37
40
  const ns = Math.floor((frame / 30) * 1000000);
@@ -216,10 +219,35 @@ const EditorCanvas = forwardRef(
216
219
  }
217
220
  };
218
221
 
219
- // 创建可编辑的videoClip
220
- const createImageFromClip = (clip: any, fc2: any) => {
222
+ const createEditObjes = async (canvas: any, time: number) => {
223
+ const promises = editClips.map((clip: any) => {
224
+ if (clip.videoClip) {
225
+ return createImageFromClip(clip);
226
+ }
227
+ if (clip.textClip && !clip.audioClip) {
228
+ return createTextFromClip(clip);
229
+ }
230
+ });
231
+ const res = await Promise.allSettled(promises);
232
+ const objects: any = [];
233
+ res.forEach((item: any) => {
234
+ if (item.status === 'fulfilled' && item.value) {
235
+ objects.push(item.value);
236
+ }
237
+ });
238
+ if (editRenderTime.current === time) {
239
+ canvas.add(...objects).renderAll();
240
+ }
241
+ };
242
+
243
+ // 创建可编辑的videoClip
244
+ const createImageFromClip = (clip: any, fc2?: any) => {
245
+ return new Promise((resolve) => {
221
246
  const canvas = fc || fc2;
222
- if (canvas && canvasSize.width) {
247
+ if (!canvas || !canvasSize.width) {
248
+ resolve(null);
249
+ return;
250
+ }
223
251
  const url = /video/g.test(clip.videoClip.mimeType) ? "" : clip.videoClip.sourceUrl;
224
252
  fabric.Image.fromURL(url, (img: any) => {
225
253
  const { dimension, posParam } = clip.videoClip;
@@ -253,27 +281,24 @@ const EditorCanvas = forwardRef(
253
281
  },
254
282
  visible: frame >= inFrame && frame < inFrame + durationFrame
255
283
  });
256
- canvas.add(img);
257
- img.on('modified', () => {
258
- const fObj = convertToJSON(img);
259
- fObj.src = "";
260
- vmmlConverterRef.current.updateClip(fObj);
261
- });
284
+ img.on('modified', () => {
285
+ const fObj = convertToJSON(img);
286
+ fObj.src = "";
287
+ vmmlConverterRef.current.updateClip(fObj);
262
288
  });
263
- } else {
264
- waitFcTasks.current.push(createImageFromClip.bind(this, clip));
265
- }
266
- }
289
+ resolve(img);
290
+ });
291
+ });
292
+ }
267
293
 
268
- const handleRedo = () => {
269
- history.redo();
270
- };
271
- const handleUndo = () => {
272
- history.undo();
273
- };
294
+ const handleRedo = () => {
295
+ history.redo();
296
+ };
297
+ const handleUndo = () => {
298
+ history.undo();
299
+ };
274
300
 
275
301
  const onBatchModify = (fObj: any, canvas: any) => {
276
- console.log('onBatchModify>>>>>>>>>>>>>>>>>>>>>>>>')
277
302
  if (!canvas) return;
278
303
  const textObjects = canvas.getObjects().filter((item: any) => item?.clipData?.type === "文字");
279
304
  const { left, top, scaleX, scaleY, angle } = fObj;
@@ -291,72 +316,81 @@ const EditorCanvas = forwardRef(
291
316
  const updatedFObj = convertToJSON(textObj);
292
317
  vmmlConverterRef.current.updateClip(updatedFObj);
293
318
  });
294
-
295
319
  canvas.renderAll();
320
+ const event = new CustomEvent('editor-vmml-batch-change', {
321
+ detail: {
322
+ vmml: vmmlConverterRef.current?.vmml ?? null
323
+ }
324
+ });
325
+ window.dispatchEvent(event);
296
326
  }
297
327
 
298
- // 创建可编辑的textclip
299
- const createTextFromClip = async (clip: any, fc2: any) => {
328
+ // 创建可编辑的textclip
329
+ const createTextFromClip = async (clip: any, fc2?: any) => {
330
+ return new Promise(async (resolve) => {
300
331
  const canvas = fc || fc2;
301
- if (canvas) {
302
- const { width, height } = vmml.template.dimension;
303
- const fontSize = getFontSize(width, height);
304
- const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType } = clip.textClip;
305
- const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
306
- const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
307
- const left = canvasSize.width * posParam.centerX;
308
- const top = canvasSize.height * posParam.centerY;
309
- const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
310
- const isAiError = textContent === '请输入文案' && textColor === '#00000000';
311
- const textFill = argbToRgba(isAiError ? '#ffffffff' : (textColor || '#ffffffff'));
312
- const textBasicInfo = {
313
- isBack: backgroundColor ? true : false,
314
- colorValue: textFill,
315
- colorName: 'custom',
316
- textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
317
- }
318
- const textImgData = await createTextImg({ textContent, bgColor, textColor: textFill, fontAssetUrl, textBasicInfo });
319
- const fontJSON = localStorage.getItem("VMML_PLAYER_FONTSMAP");
320
- let fontMap: any = {};
321
- try {
322
- fontMap = fontJSON ? JSON.parse(fontJSON) : {};
323
- } catch {
324
- fontMap = {};
325
- }
326
- const fontFamily = fontMap[fontAssetUrl] || '';
327
- fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
328
- imgData.set({
329
- left,
330
- top,
331
- width: textImgData.width,
332
- height: textImgData.height,
333
- scaleX,
334
- scaleY,
335
- angle: posParam.rotationZ,
336
- originX: 'center',
337
- originY: 'center',
338
- clipData: {
339
- id: uuidv4(),
340
- inPoint: clip.inPoint,
341
- inFrame: getFrames(clip.inPoint, 30),
342
- type: "文字",
343
- textColor: textFill,
344
- text: textContent,
345
- bgColor,
346
- originClip: clip,
347
- fontAssetUrl,
348
- fontFamily,
349
- textBasicInfo,
350
- isAiError,
351
- duration: clip.duration
352
- },
353
- })
354
- imgData.on("selected", (options: any) => {
355
- options.target.isSelected = -1;
356
- });
357
- imgData.on("moving", (options: any) => {
358
- options.transform.target.isSelected = 0;
359
- });
332
+ if (!canvas) {
333
+ resolve(null);
334
+ return;
335
+ }
336
+ const { width, height } = vmml.template.dimension;
337
+ const fontSize = getFontSize(width, height);
338
+ const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType } = clip.textClip;
339
+ const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
340
+ const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
341
+ const left = canvasSize.width * posParam.centerX;
342
+ const top = canvasSize.height * posParam.centerY;
343
+ const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
344
+ const isAiError = textContent === '请输入文案' && textColor === '#00000000';
345
+ const textFill = argbToRgba(isAiError ? '#ffffffff' : (textColor || '#ffffffff'));
346
+ const textBasicInfo = {
347
+ isBack: backgroundColor ? true : false,
348
+ colorValue: textFill,
349
+ colorName: 'custom',
350
+ textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
351
+ }
352
+ const textImgData = await createTextImg({ textContent, bgColor, textColor: textFill, fontAssetUrl, textBasicInfo });
353
+ const fontJSON = localStorage.getItem("VMML_PLAYER_FONTSMAP");
354
+ let fontMap: any = {};
355
+ try {
356
+ fontMap = fontJSON ? JSON.parse(fontJSON) : {};
357
+ } catch {
358
+ fontMap = {};
359
+ }
360
+ const fontFamily = fontMap[fontAssetUrl] || '';
361
+ fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
362
+ imgData.set({
363
+ left,
364
+ top,
365
+ width: textImgData.width,
366
+ height: textImgData.height,
367
+ scaleX,
368
+ scaleY,
369
+ angle: posParam.rotationZ,
370
+ originX: 'center',
371
+ originY: 'center',
372
+ clipData: {
373
+ id: clip.id,
374
+ inPoint: clip.inPoint,
375
+ inFrame: getFrames(clip.inPoint, 30),
376
+ type: "文字",
377
+ textColor: textFill,
378
+ text: textContent,
379
+ bgColor,
380
+ originClip: clip,
381
+ fontAssetUrl,
382
+ fontFamily,
383
+ textBasicInfo,
384
+ isAiError,
385
+ duration: clip.duration
386
+ },
387
+ })
388
+ imgData.on("selected", (options: any) => {
389
+ options.target.isSelected = -1;
390
+ });
391
+ imgData.on("moving", (options: any) => {
392
+ options.transform.target.isSelected = 0;
393
+ });
360
394
  imgData.on('modified', () => {
361
395
  const fObj = convertToJSON(imgData);
362
396
  if (fObj.clipData.isAiError) {
@@ -368,12 +402,10 @@ const EditorCanvas = forwardRef(
368
402
  vmmlConverterRef.current.updateClip(fObj);
369
403
  }
370
404
  });
371
- canvas.add(imgData).renderAll();
372
- })
373
- } else {
374
- waitFcTasks.current.push(createTextFromClip.bind(this, clip));
375
- }
376
- }
405
+ resolve(imgData);
406
+ });
407
+ });
408
+ }
377
409
 
378
410
  // 生成简短的字体名称
379
411
  const getFontFamilyName = (url: string) => {
@@ -529,7 +561,6 @@ const EditorCanvas = forwardRef(
529
561
  return fc.getObjects();
530
562
  }
531
563
  }
532
-
533
564
  const styles: any = useMemo(() => {
534
565
  return {
535
566
  position: "absolute",
@@ -565,11 +596,19 @@ const EditorCanvas = forwardRef(
565
596
  }
566
597
  }, [fc, dragState])
567
598
 
568
- useEffect(() => {
569
- if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current) {
570
- vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
571
- }
572
- }, [canvasSize, vmml]);
599
+ useEffect(() => {
600
+ if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current) {
601
+ vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
602
+ }
603
+ }, [canvasSize, vmml]);
604
+
605
+ // 监听 editClips 变化,自动重新渲染(参考 timeline)
606
+ useEffect(() => {
607
+ if (editClips.length && canvasReady && fc) {
608
+ editRenderTime.current = Date.now();
609
+ createEditObjes(fc, editRenderTime.current);
610
+ }
611
+ }, [editClips, canvasReady]);
573
612
 
574
613
  useEffect(() => {
575
614
  if (fc) {
@@ -582,6 +621,10 @@ const EditorCanvas = forwardRef(
582
621
  }
583
622
  }, [fc]);
584
623
 
624
+ const getCanvasCtx = () => {
625
+ return fc
626
+ }
627
+
585
628
  useImperativeHandle(ref, () => ({
586
629
  createImage,
587
630
  createText,
@@ -595,8 +638,9 @@ const EditorCanvas = forwardRef(
595
638
  checkObjectInPoint,
596
639
  createImageFromClip,
597
640
  createTextFromClip,
598
- changeObjectVisible
599
- }));
641
+ changeObjectVisible,
642
+ getCanvasCtx
643
+ }), [fc]);
600
644
 
601
645
  const getActions = () => {
602
646
  if (history) {
package/src/index.tsx CHANGED
@@ -16,7 +16,7 @@ import { emotionIcon, wordIcon } from "./utils/const";
16
16
  const historyClass = new HistoryClass();
17
17
  const EditorFn = <Schema extends AnyZodObject, Props>(
18
18
  {
19
- vmml,
19
+ vmml: propVmml,
20
20
  pauseFrame = 0,
21
21
  maxText = 10,
22
22
  maxVideo = 5,
@@ -33,7 +33,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
33
33
  }: any,
34
34
  ref: any,
35
35
  ) => {
36
- vmml = convertVmmlTextScaleByForbidden(vmml);
36
+ const [vmmlState, setVmmlState] = useState<any>(() => convertVmmlTextScaleByForbidden(propVmml));
37
37
  const textMenuRef = useRef<any>(null);
38
38
  const vmmlPlayerRef = useRef();
39
39
  const canvasRef = useRef();
@@ -45,7 +45,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
45
45
  const [isPlaying, setIsPlaying] = useState<boolean>(false);
46
46
  const [showCanvas, setShowCanvas] = useState(false);
47
47
  const [filterIds, setFilterIds] = useState<string[]>([]);
48
- const [durationInFrames, setDurationInFrames] = useState(getFrames(vmml?.template?.duration || 1, fps));
48
+ const [durationInFrames, setDurationInFrames] = useState(getFrames(vmmlState?.template?.duration || 1, fps));
49
49
  const [previewState, setPreviewState] = useState<boolean>(true); // true预览态 false编辑态
50
50
  const [menuState, setMenuState] = useState<string>(""); // text 文字菜单 video 表情包菜单 空不显示
51
51
  const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0, top: 0 }); // 画布尺寸
@@ -58,6 +58,8 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
58
58
  const [dragState, setDragState] = useState(0);
59
59
  const vmmlConverterRef = useRef<any>(null);
60
60
  const [initFcObjs, setInitFcObjs] = useState([]);
61
+ const [editClips, setEditClips] = useState<any[]>([]); // 可编辑的 clips
62
+ const [refreshEdit, setRefreshEdit] = useState(0); // 触发画布刷新
61
63
  const vmmlFlag = useRef(false);
62
64
  const needPlay = useRef(true);
63
65
 
@@ -65,9 +67,9 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
65
67
  const { current }: any = vmmlPlayerRef;
66
68
  if (!current) return;
67
69
  if (!once.current) {
68
- current.setVmml(vmml, pauseFrame);
70
+ current.setVmml(vmmlState, pauseFrame);
69
71
  } else {
70
- current.setVmml(vmml, frame);
72
+ current.setVmml(vmmlState, frame);
71
73
  }
72
74
  };
73
75
 
@@ -279,18 +281,9 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
279
281
 
280
282
  // 初始化可编辑的clip
281
283
  const initCanEditClips = (tracks: any = []) => {
282
- if (editableArray.length) {
284
+ if (editableArray.length && tracks.length) {
283
285
  const list = findEditClips(tracks);
284
- if (list.length) {
285
- const { current }: any = canvasRef;
286
- list.forEach((clip: any) => {
287
- if (clip.videoClip) {
288
- current.createImageFromClip(clip);
289
- } else {
290
- if (!clip.audioClip) current.createTextFromClip(clip);
291
- }
292
- })
293
- }
286
+ setEditClips(list); // 直接更新 state,让 Canvas 组件自动处理
294
287
  }
295
288
  };
296
289
 
@@ -324,7 +317,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
324
317
  // 获取更新后的vmml
325
318
  const getVmml = () => {
326
319
  try {
327
- const tracks = vmml.template.tracks.filter((item: any) => item.editorType === "文字");
320
+ const tracks = vmmlState.template.tracks.filter((item: any) => item.editorType === "文字");
328
321
  tracks.forEach((track: any) => {
329
322
  track.clips.forEach((clip: any) => {
330
323
  clip.fObj.src = '';
@@ -333,7 +326,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
333
326
  } catch {
334
327
  console.log("出错了")
335
328
  }
336
- return vmml
329
+ return vmmlState
337
330
  };
338
331
 
339
332
  const getPlayer = () => {
@@ -354,19 +347,35 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
354
347
  }, [previewState]);
355
348
 
356
349
  useEffect(() => {
357
- if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current && vmml) {
358
- vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
350
+ if (canvasSize.width && canvasSize.height && vmmlState) {
351
+ if (!vmmlConverterRef.current) {
352
+ vmmlConverterRef.current = new VmmlConverter({ vmml: vmmlState, canvasSize });
353
+ } else {
354
+ vmmlConverterRef.current.vmml = vmmlState;
355
+ }
356
+ }
357
+ }, [canvasSize, vmmlState]);
358
+
359
+ useEffect(() => {
360
+ if (editableArray.length && vmmlState?.template) {
361
+ initCanEditClips(vmmlState.template.tracks);
359
362
  }
360
- if (canvasSize.width) {
361
- initCanEditClips(vmml.template.tracks);
363
+ }, [editableArray, refreshEdit]);
364
+
365
+ useEffect(() => {
366
+ if (propVmml) {
367
+ const convertedVmml = convertVmmlTextScaleByForbidden(propVmml);
368
+ setVmmlState(convertedVmml);
369
+ setDurationInFrames(getFrames(propVmml?.template?.duration || 1, fps));
370
+ setRefreshEdit(Date.now());
362
371
  }
363
- }, [canvasSize, vmml]);
372
+ }, [propVmml]);
364
373
 
365
374
  useEffect(() => {
366
- if (vmml) {
367
- initFcObjects(vmml.template.tracks);
375
+ if (vmmlState) {
376
+ initFcObjects(vmmlState.template.tracks);
368
377
  }
369
- }, [vmml]);
378
+ }, [vmmlState]);
370
379
 
371
380
  useEffect(() => {
372
381
  if (player) {
@@ -404,7 +413,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
404
413
  if (dragState === 2) {
405
414
  needPlay.current = false;
406
415
  const { current }: any = vmmlPlayerRef;
407
- current.setVmml(vmml, frame, false);
416
+ current.setVmml(vmmlState, frame, false);
408
417
  setShowCanvas(false);
409
418
  }
410
419
  if (dragState === 3 || dragState === 4) {
@@ -412,6 +421,24 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
412
421
  setPreviewState(false);
413
422
  }
414
423
  }, [dragState]);
424
+
425
+ const updateVmml = (v: any) => {
426
+ const { current: playerCurrent }: any = vmmlPlayerRef;
427
+ const { current: canvasCurrent }: any = canvasRef;
428
+ if (!playerCurrent) return;
429
+ canvasCurrent?.getCanvasCtx()?.clear?.()
430
+
431
+ const convertedVmml = convertVmmlTextScaleByForbidden(v);
432
+ setVmmlState(convertedVmml);
433
+ setDurationInFrames(getFrames(v?.template?.duration || 1, fps));
434
+ playerCurrent.setVmml(convertedVmml, pauseFrame);
435
+
436
+ setRefreshEdit(Date.now());
437
+
438
+ if (canvasCurrent) {
439
+ canvasCurrent.checkObjectInPoint(pauseFrame);
440
+ }
441
+ }
415
442
 
416
443
  useImperativeHandle(ref,
417
444
  () => ({
@@ -420,9 +447,10 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
420
447
  getVmml,
421
448
  getPlayer,
422
449
  texteditClose,
423
- textFinish
450
+ textFinish,
451
+ updateVmml
424
452
  }),
425
- [vmml, player]
453
+ [vmmlState, player]
426
454
  )
427
455
  const texteditClose = ()=>{
428
456
  if (textMenuRef) {
@@ -455,7 +483,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
455
483
  <div className="vessel" onClick={onClickMain}>
456
484
  <VmmlPlayer
457
485
  ref={vmmlPlayerRef}
458
- vmml={vmml}
486
+ vmml={vmmlState}
459
487
  existenceBorderRadio
460
488
  moveToBeginningWhenEnded
461
489
  muted={true}
@@ -467,22 +495,23 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
467
495
  filterIds={filterIds}
468
496
  />
469
497
  </div>
470
- <EditorCanvas
471
- ref={canvasRef}
472
- previewState={previewState}
473
- showCanvas={showCanvas}
474
- canvasSize={canvasSize}
475
- enterPreview={enterPreview}
476
- intoTextEdit={intoTextEdit}
477
- frame={frame}
478
- vmml={vmml}
479
- dragState={dragState}
480
- initFcObjs={initFcObjs}
481
- onVideoChange={onVideoChange}
482
- isBatchModify={isBatchModify}
483
- hideConfig={hideConfig}
484
- // textInfoReset={textInfoReset}
485
- />
498
+ <EditorCanvas
499
+ ref={canvasRef}
500
+ previewState={previewState}
501
+ showCanvas={showCanvas}
502
+ canvasSize={canvasSize}
503
+ enterPreview={enterPreview}
504
+ intoTextEdit={intoTextEdit}
505
+ frame={frame}
506
+ vmml={vmmlState}
507
+ dragState={dragState}
508
+ initFcObjs={initFcObjs}
509
+ editClips={editClips}
510
+ onVideoChange={onVideoChange}
511
+ isBatchModify={isBatchModify}
512
+ hideConfig={hideConfig}
513
+ // textInfoReset={textInfoReset}
514
+ />
486
515
  <div className="controls-box">
487
516
  <Controls
488
517
  player={player}