@versa_ai/vmml-editor 1.0.14 → 1.0.16

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.14",
3
+ "version": "1.0.16",
4
4
  "module": "dist/index.mjs",
5
5
  "main": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -308,7 +308,7 @@
308
308
  }
309
309
  }
310
310
  .player-controls-time {
311
- width: 6vw !important;
311
+ width: 60px !important;
312
312
  }
313
313
  }
314
314
  }
@@ -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,45 @@ 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
+ // 先删除所有旧的可编辑对象
224
+ const allObjects = canvas.getObjects();
225
+ const toRemove: any[] = [];
226
+ allObjects.forEach((obj: any) => {
227
+ if (obj?.clipData?.originClip) {
228
+ toRemove.push(obj);
229
+ }
230
+ });
231
+ toRemove.forEach(obj => canvas.remove(obj));
232
+
233
+ const promises = editClips.map((clip: any) => {
234
+ if (clip.videoClip) {
235
+ return createImageFromClip(clip);
236
+ }
237
+ if (clip.textClip && !clip.audioClip) {
238
+ return createTextFromClip(clip);
239
+ }
240
+ });
241
+ const res = await Promise.allSettled(promises);
242
+ const objects: any = [];
243
+ res.forEach((item: any) => {
244
+ if (item.status === 'fulfilled' && item.value) {
245
+ objects.push(item.value);
246
+ }
247
+ });
248
+ if (editRenderTime.current === time) {
249
+ canvas.add(...objects).renderAll();
250
+ }
251
+ };
252
+
253
+ // 创建可编辑的videoClip
254
+ const createImageFromClip = (clip: any, fc2?: any) => {
255
+ return new Promise((resolve) => {
221
256
  const canvas = fc || fc2;
222
- if (canvas && canvasSize.width) {
257
+ if (!canvas || !canvasSize.width) {
258
+ resolve(null);
259
+ return;
260
+ }
223
261
  const url = /video/g.test(clip.videoClip.mimeType) ? "" : clip.videoClip.sourceUrl;
224
262
  fabric.Image.fromURL(url, (img: any) => {
225
263
  const { dimension, posParam } = clip.videoClip;
@@ -253,100 +291,131 @@ const EditorCanvas = forwardRef(
253
291
  },
254
292
  visible: frame >= inFrame && frame < inFrame + durationFrame
255
293
  });
256
- canvas.add(img);
257
- img.on('modified', () => {
258
- const fObj = convertToJSON(img);
259
- fObj.src = "";
260
- vmmlConverterRef.current.updateClip(fObj);
261
- });
294
+ img.on('modified', () => {
295
+ const fObj = convertToJSON(img);
296
+ fObj.src = "";
297
+ vmmlConverterRef.current.updateClip(fObj);
262
298
  });
263
- } else {
264
- waitFcTasks.current.push(createImageFromClip.bind(this, clip));
265
- }
266
- }
299
+ resolve(img);
300
+ });
301
+ });
302
+ }
267
303
 
268
- const handleRedo = () => {
269
- history.redo();
270
- };
271
- const handleUndo = () => {
272
- history.undo();
273
- };
304
+ const handleRedo = () => {
305
+ history.redo();
306
+ };
307
+ const handleUndo = () => {
308
+ history.undo();
309
+ };
310
+
311
+ const onBatchModify = (fObj: any, canvas: any) => {
312
+ if (!canvas) return;
313
+ const textObjects = canvas.getObjects().filter((item: any) => item?.clipData?.type === "文字");
314
+ const { left, top, scaleX, scaleY, angle } = fObj;
315
+
316
+ // 批量更新每个文字对象的位置
317
+ textObjects.forEach((textObj: any) => {
318
+ textObj.set({
319
+ left,
320
+ top,
321
+ scaleX,
322
+ scaleY,
323
+ angle
324
+ });
325
+ textObj.setCoords();
326
+ const updatedFObj = convertToJSON(textObj);
327
+ vmmlConverterRef.current.updateClip(updatedFObj);
328
+ });
329
+ canvas.renderAll();
330
+ const event = new CustomEvent('editor-vmml-batch-change', {
331
+ detail: {
332
+ vmml: vmmlConverterRef.current?.vmml ?? null
333
+ }
334
+ });
335
+ window.dispatchEvent(event);
336
+ }
274
337
 
275
- // 创建可编辑的textclip
276
- const createTextFromClip = async (clip: any, fc2: any) => {
338
+ // 创建可编辑的textclip
339
+ const createTextFromClip = async (clip: any, fc2?: any) => {
340
+ return new Promise(async (resolve) => {
277
341
  const canvas = fc || fc2;
278
- if (canvas) {
279
- const { width, height } = vmml.template.dimension;
280
- const fontSize = getFontSize(width, height);
281
- const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType } = clip.textClip;
282
- const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
283
- const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
284
- const left = canvasSize.width * posParam.centerX;
285
- const top = canvasSize.height * posParam.centerY;
286
- const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
287
- const isAiError = textContent === '请输入文案' && textColor === '#00000000';
288
- const textFill = argbToRgba(isAiError ? '#ffffffff' : (textColor || '#ffffffff'));
289
- const textBasicInfo = {
290
- isBack: backgroundColor ? true : false,
291
- colorValue: textFill,
292
- colorName: 'custom',
293
- textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
294
- }
295
- const textImgData = await createTextImg({ textContent, bgColor, textColor: textFill, fontAssetUrl, textBasicInfo });
296
- const fontJSON = localStorage.getItem("VMML_PLAYER_FONTSMAP");
297
- let fontMap: any = {};
298
- try {
299
- fontMap = fontJSON ? JSON.parse(fontJSON) : {};
300
- } catch {
301
- fontMap = {};
302
- }
303
- const fontFamily = fontMap[fontAssetUrl] || '';
304
- fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
305
- imgData.set({
306
- left,
307
- top,
308
- width: textImgData.width,
309
- height: textImgData.height,
310
- scaleX,
311
- scaleY,
312
- angle: posParam.rotationZ,
313
- originX: 'center',
314
- originY: 'center',
315
- clipData: {
316
- id: uuidv4(),
317
- inPoint: clip.inPoint,
318
- inFrame: getFrames(clip.inPoint, 30),
319
- type: "文字",
320
- textColor: textFill,
321
- text: textContent,
322
- bgColor,
323
- originClip: clip,
324
- fontAssetUrl,
325
- fontFamily,
326
- textBasicInfo,
327
- isAiError,
328
- duration: clip.duration
329
- },
330
- })
331
- imgData.on("selected", (options: any) => {
332
- options.target.isSelected = -1;
333
- });
334
- imgData.on("moving", (options: any) => {
335
- options.transform.target.isSelected = 0;
336
- });
337
- imgData.on('modified', () => {
338
- const fObj = convertToJSON(imgData);
339
- if (fObj.clipData.isAiError) {
340
- fObj.clipData.textColor = 'rgba(0, 0, 0, 0)';
341
- }
342
- vmmlConverterRef.current.updateClip(fObj);
343
- });
344
- canvas.add(imgData).renderAll();
345
- })
346
- } else {
347
- waitFcTasks.current.push(createTextFromClip.bind(this, clip));
342
+ if (!canvas) {
343
+ resolve(null);
344
+ return;
348
345
  }
349
- }
346
+ const { width, height } = vmml.template.dimension;
347
+ const fontSize = getFontSize(width, height);
348
+ const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType } = clip.textClip;
349
+ const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
350
+ const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
351
+ const left = canvasSize.width * posParam.centerX;
352
+ const top = canvasSize.height * posParam.centerY;
353
+ const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
354
+ const isAiError = textContent === '请输入文案' && textColor === '#00000000';
355
+ const textFill = argbToRgba(isAiError ? '#ffffffff' : (textColor || '#ffffffff'));
356
+ const textBasicInfo = {
357
+ isBack: backgroundColor ? true : false,
358
+ colorValue: textFill,
359
+ colorName: 'custom',
360
+ textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
361
+ }
362
+ const textImgData = await createTextImg({ textContent, bgColor, textColor: textFill, fontAssetUrl, textBasicInfo });
363
+ const fontJSON = localStorage.getItem("VMML_PLAYER_FONTSMAP");
364
+ let fontMap: any = {};
365
+ try {
366
+ fontMap = fontJSON ? JSON.parse(fontJSON) : {};
367
+ } catch {
368
+ fontMap = {};
369
+ }
370
+ const fontFamily = fontMap[fontAssetUrl] || '';
371
+ fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
372
+ imgData.set({
373
+ left,
374
+ top,
375
+ width: textImgData.width,
376
+ height: textImgData.height,
377
+ scaleX,
378
+ scaleY,
379
+ angle: posParam.rotationZ,
380
+ originX: 'center',
381
+ originY: 'center',
382
+ clipData: {
383
+ id: clip.id,
384
+ inPoint: clip.inPoint,
385
+ inFrame: getFrames(clip.inPoint, 30),
386
+ type: "文字",
387
+ textColor: textFill,
388
+ text: textContent,
389
+ bgColor,
390
+ originClip: clip,
391
+ fontAssetUrl,
392
+ fontFamily,
393
+ textBasicInfo,
394
+ isAiError,
395
+ duration: clip.duration
396
+ },
397
+ })
398
+ imgData.on("selected", (options: any) => {
399
+ options.target.isSelected = -1;
400
+ });
401
+ imgData.on("moving", (options: any) => {
402
+ options.transform.target.isSelected = 0;
403
+ });
404
+ imgData.on('modified', () => {
405
+ const fObj = convertToJSON(imgData);
406
+ if (fObj.clipData.isAiError) {
407
+ fObj.clipData.textColor = 'rgba(0, 0, 0, 0)';
408
+ }
409
+ if (isBatchModify) {
410
+ onBatchModify(fObj, canvas)
411
+ } else {
412
+ vmmlConverterRef.current.updateClip(fObj);
413
+ }
414
+ });
415
+ resolve(imgData);
416
+ });
417
+ });
418
+ }
350
419
 
351
420
  // 生成简短的字体名称
352
421
  const getFontFamilyName = (url: string) => {
@@ -538,11 +607,19 @@ const EditorCanvas = forwardRef(
538
607
  }
539
608
  }, [fc, dragState])
540
609
 
541
- useEffect(() => {
542
- if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current) {
543
- vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
544
- }
545
- }, [canvasSize, vmml]);
610
+ useEffect(() => {
611
+ if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current) {
612
+ vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
613
+ }
614
+ }, [canvasSize, vmml]);
615
+
616
+ // 监听 editClips 变化,自动重新渲染(参考 timeline)
617
+ useEffect(() => {
618
+ if (editClips.length && canvasReady && fc) {
619
+ editRenderTime.current = Date.now();
620
+ createEditObjes(fc, editRenderTime.current);
621
+ }
622
+ }, [editClips, canvasReady]);
546
623
 
547
624
  useEffect(() => {
548
625
  if (fc) {
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
- 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,23 @@ 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
+
430
+ const convertedVmml = convertVmmlTextScaleByForbidden(v);
431
+ setVmmlState(convertedVmml);
432
+ setDurationInFrames(getFrames(v?.template?.duration || 1, fps));
433
+ playerCurrent.setVmml(convertedVmml, pauseFrame);
434
+
435
+ setRefreshEdit(Date.now());
436
+
437
+ if (canvasCurrent) {
438
+ canvasCurrent.checkObjectInPoint(pauseFrame);
439
+ }
440
+ }
415
441
 
416
442
  useImperativeHandle(ref,
417
443
  () => ({
@@ -420,9 +446,10 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
420
446
  getVmml,
421
447
  getPlayer,
422
448
  texteditClose,
423
- textFinish
449
+ textFinish,
450
+ updateVmml
424
451
  }),
425
- [vmml, player]
452
+ [vmmlState, player]
426
453
  )
427
454
  const texteditClose = ()=>{
428
455
  if (textMenuRef) {
@@ -455,7 +482,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
455
482
  <div className="vessel" onClick={onClickMain}>
456
483
  <VmmlPlayer
457
484
  ref={vmmlPlayerRef}
458
- vmml={vmml}
485
+ vmml={vmmlState}
459
486
  existenceBorderRadio
460
487
  moveToBeginningWhenEnded
461
488
  muted={true}
@@ -467,22 +494,23 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
467
494
  filterIds={filterIds}
468
495
  />
469
496
  </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
- />
497
+ <EditorCanvas
498
+ ref={canvasRef}
499
+ previewState={previewState}
500
+ showCanvas={showCanvas}
501
+ canvasSize={canvasSize}
502
+ enterPreview={enterPreview}
503
+ intoTextEdit={intoTextEdit}
504
+ frame={frame}
505
+ vmml={vmmlState}
506
+ dragState={dragState}
507
+ initFcObjs={initFcObjs}
508
+ editClips={editClips}
509
+ onVideoChange={onVideoChange}
510
+ isBatchModify={isBatchModify}
511
+ hideConfig={hideConfig}
512
+ // textInfoReset={textInfoReset}
513
+ />
486
514
  <div className="controls-box">
487
515
  <Controls
488
516
  player={player}
@@ -27,7 +27,6 @@ class VmmlConverter {
27
27
 
28
28
  //更新位置
29
29
  private setPosParam(fObj: any): object {
30
- console.log("setPosParam fObj",fObj)
31
30
  const {
32
31
  clipData: { type },
33
32
  centerPoint,