@versa_ai/vmml-editor 1.0.38 → 1.0.40

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.38",
3
+ "version": "1.0.40",
4
4
  "module": "dist/index.mjs",
5
5
  "main": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -16,7 +16,7 @@
16
16
  "remotion": "4.0.166",
17
17
  "uuid": "^10.0.0",
18
18
  "zod": "^3.23.8",
19
- "@versa_ai/vmml-player": "1.1.20",
19
+ "@versa_ai/vmml-player": "1.1.21",
20
20
  "@versa_ai/vmml-utils": "1.0.15"
21
21
  },
22
22
  "devDependencies": {
@@ -9,16 +9,17 @@ import { toSvg } from 'dom-to-image'
9
9
 
10
10
  const EditorCanvas = forwardRef(
11
11
  ({ previewState, showCanvas, canvasSize, enterPreview, intoTextEdit, frame, vmml, dragState, initFcObjs, editClips = [], onVideoChange, isBatchModify, hideConfig, textWarapCenter }: any, ref: any) => {
12
- const [fc, setFc] = useState<any>(null);
13
- const [history, setHistory] = useState<any>(null);
14
- const [canvasReady, setCanvasReady] = useState(false);
15
- const editRenderTime = useRef(0);
16
- const waitFcTasks = useRef<any>([]);
17
- const vmmlConverterRef = useRef<any>(null);
18
- const heightScaleRef = useRef<number>(1);
19
- const widthScaleRef = useRef<number>(1);
20
-
21
- const initCanvas = () => {
12
+ const [fc, setFc] = useState<any>(null);
13
+ const [history, setHistory] = useState<any>(null);
14
+ const [canvasReady, setCanvasReady] = useState(false);
15
+ const editRenderTime = useRef(0);
16
+ const waitFcTasks = useRef<any>([]);
17
+ const vmmlConverterRef = useRef<any>(null);
18
+ const heightScaleRef = useRef<number>(1);
19
+ const widthScaleRef = useRef<number>(1);
20
+ const fontCacheRef = useRef<Map<string, Promise<string | null>>>(new Map());
21
+
22
+ const initCanvas = () => {
22
23
  const canvas = new fabric.Canvas("canvas", {
23
24
  width: canvasSize.width,
24
25
  height: canvasSize.height,
@@ -58,9 +59,9 @@ const EditorCanvas = forwardRef(
58
59
  const objects = fc.getObjects();
59
60
  objects.forEach((item: any) => {
60
61
  if (item?.clipData?.type === "文字") {
61
- item.set("visible", ns >= item.clipData.inPoint && ns < item.clipData.inPoint + (item.clipData.duration || vmml.template.duration));
62
+ item.set("visible", item.clipData.duration >0 && ns >= item.clipData.inPoint && ns < item.clipData.inPoint + (item.clipData.duration || vmml.template.duration));
62
63
  } else {
63
- item.set("visible", ns >= item.clipData.inPoint && ns < item.clipData.inPoint + item.clipData?.fileUrl?.duration);
64
+ item.set("visible", item.clipData.duration >0 && ns >= item.clipData.inPoint && ns < item.clipData.inPoint + item.clipData?.fileUrl?.duration);
64
65
  }
65
66
  });
66
67
  fc.discardActiveObject();
@@ -225,7 +226,7 @@ const EditorCanvas = forwardRef(
225
226
  return createImageFromClip(clip);
226
227
  }
227
228
  if (clip.textClip && !clip.audioClip) {
228
- return createTextFromClip(clip);
229
+ return createTextFromClipCanvas(clip);
229
230
  }
230
231
  });
231
232
  const res = await Promise.allSettled(promises);
@@ -235,6 +236,7 @@ const EditorCanvas = forwardRef(
235
236
  objects.push(item.value);
236
237
  }
237
238
  });
239
+ console.log(editRenderTime.current === time, 'editRenderTime.current === time')
238
240
  if (editRenderTime.current === time) {
239
241
  canvas.add(...objects).renderAll();
240
242
  checkObjectInPoint()
@@ -327,6 +329,151 @@ const EditorCanvas = forwardRef(
327
329
  }
328
330
 
329
331
  // 创建可编辑的textclip
332
+ const createTextFromClipCanvas = async (clip: any, fc2?: fabric.Canvas) => {
333
+ return new Promise<fabric.Group>(async (resolve) => {
334
+ const canvas = fc || fc2;
335
+ if (!canvas) return null;
336
+
337
+ const { width, height } = vmml.template.dimension;
338
+ const fontSize = getFontSize(width, height);
339
+
340
+ const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType, strokeColor, strokeWidth, letterSpacing } = clip.textClip;
341
+
342
+ const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
343
+ const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
344
+ const left = canvasSize.width * posParam.centerX;
345
+ const top = canvasSize.height * posParam.centerY
346
+ const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
347
+ const stColor = strokeColor ? argbToRgba(strokeColor) : 'transparent';
348
+ const textFill = argbToRgba(textColor || '#ffffffff');
349
+ const strokeW = strokeColor&& strokeWidth ? strokeWidth * 26 * 1.5 / fontSize : 0;
350
+ const letterSpace = letterSpacing * 22 / fontSize;
351
+
352
+ // 加载字体
353
+ let fontFamily = 'sansMedium';
354
+ if (fontAssetUrl) {
355
+ const base64 = await loadFont(fontAssetUrl);
356
+ if (base64) {
357
+ fontFamily = getFontFamilyName(fontAssetUrl);
358
+ await document.fonts.ready;
359
+ }
360
+ }
361
+ const lines = textContent.split('\n').filter((item: string) => item);
362
+ const lineHeight = 22 + strokeW; // 行高
363
+ const textHeight = lines.length * lineHeight;
364
+ const groupWidth = Math.max(...lines.map((l: any) => {
365
+ const temp = new fabric.Text(l || ' ', { fontSize: 22, fontFamily, charSpacing: (letterSpace || 0) * 50, strokeWidth: strokeW ?? 0});
366
+ return temp?.width ?? 0;
367
+ })) + 14;
368
+ const groupHeight = textHeight + 13; // padding
369
+
370
+ // 创建双层文字
371
+ const textObjs: fabric.Object[] = [];
372
+ lines.forEach((line: string, idx: number) => {
373
+ const y = (groupHeight - textHeight) / 2 + idx * lineHeight + lineHeight / 2 + 1;
374
+
375
+ // 描边文字
376
+ const strokeText = new fabric.Text(line || ' ', {
377
+ fontFamily,
378
+ fontSize: 22,
379
+ fill: 'transparent',
380
+ stroke: stColor,
381
+ strokeWidth: strokeW,
382
+ originX: 'center',
383
+ originY: 'center',
384
+ left: groupWidth / 2, // 水平居中
385
+ top: y,
386
+ charSpacing: (letterSpace || 0) * 50,
387
+ textAlign: alignType === 1 ? 'center' : alignType === 2 ? 'right' : 'left',
388
+ objectCaching: false,
389
+ });
390
+
391
+ // 填充文字
392
+ const fillText = new fabric.Text(line || ' ', {
393
+ fontFamily,
394
+ fontSize: 22,
395
+ fill: textFill,
396
+ stroke: 'transparent',
397
+ originX: 'center',
398
+ originY: 'center',
399
+ strokeWidth: 0,
400
+ left: groupWidth / 2, // 水平居中
401
+ top: y,
402
+ charSpacing: (letterSpace || 0) * 50,
403
+ textAlign: alignType === 1 ? 'center' : alignType === 2 ? 'right' : 'left',
404
+ objectCaching: false,
405
+ });
406
+
407
+ textObjs.push(strokeText, fillText);
408
+ });
409
+
410
+ // 背景矩形
411
+ const bgRect = new fabric.Rect({
412
+ width: groupWidth,
413
+ height: groupHeight,
414
+ fill: bgColor,
415
+ originX: 'left',
416
+ originY: 'top',
417
+ rx: 5,
418
+ ry: 5,
419
+ left: 0,
420
+ top: 0,
421
+ });
422
+
423
+ const textBasicInfo = {
424
+ isBack: backgroundColor ? true : false,
425
+ colorValue: textFill,
426
+ colorName: 'custom',
427
+ textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
428
+ }
429
+ // 组合 group
430
+ const group = new fabric.Group([bgRect, ...textObjs], {
431
+ left,
432
+ top,
433
+ scaleX,
434
+ scaleY,
435
+ angle: posParam.rotationZ,
436
+ originX: 'center',
437
+ originY: 'center',
438
+ clipData: {
439
+ id: clip.id,
440
+ inPoint: clip.inPoint,
441
+ inFrame: getFrames(clip.inPoint, 30),
442
+ type: "文字",
443
+ textColor: textFill,
444
+ text: textContent,
445
+ bgColor,
446
+ originClip: clip,
447
+ fontAssetUrl,
448
+ fontFamily,
449
+ textBasicInfo,
450
+ duration: clip.duration
451
+ },
452
+ objectCaching: false,
453
+ });
454
+
455
+ // 事件
456
+ group.on("selected", (options: any) => {
457
+ options.target.isSelected = -1;
458
+ });
459
+ group.on("moving", (options: any) => {
460
+ options.transform.target.isSelected = 0;
461
+ });
462
+ group.on('modified', () => {
463
+ const fObj = convertToJSON(group);
464
+ if (fObj.clipData.isAiError) {
465
+ fObj.clipData.textColor = 'rgba(0, 0, 0, 0)';
466
+ }
467
+ if (isBatchModify) {
468
+ onBatchModify(fObj, canvas)
469
+ } else {
470
+ vmmlConverterRef.current.updateClip(fObj);
471
+ }
472
+ });
473
+
474
+ resolve(group);
475
+ });
476
+ };
330
477
  const createTextFromClip = async (clip: any, fc2?: any) => {
331
478
  return new Promise(async (resolve) => {
332
479
  const canvas = fc || fc2;
@@ -407,6 +554,7 @@ const EditorCanvas = forwardRef(
407
554
  vmmlConverterRef.current.updateClip(fObj);
408
555
  }
409
556
  });
557
+ console.log('fabricjs>>>end>>>>>>>>>>>>')
410
558
  resolve(imgData);
411
559
  });
412
560
  });
@@ -451,18 +599,30 @@ const EditorCanvas = forwardRef(
451
599
 
452
600
  const loadFont = async (url: any) => {
453
601
  if (!url) return null
454
- try {
455
- const base64 = await urlToBlob({ url });
456
- const fontFamilyName = getFontFamilyName(url);
457
- const format = detectFontFormat(url);
458
-
459
- const fontFace = new FontFace(fontFamilyName, `url(${base64})${format ? ` format('${format}')` : ''}`);
460
- await fontFace.load();
461
- (document.fonts as any).add(fontFace);
462
- return base64
463
- } catch (e) {
464
- return null
602
+
603
+ // 检查缓存
604
+ if (fontCacheRef.current.has(url)) {
605
+ return fontCacheRef.current.get(url)!;
465
606
  }
607
+
608
+ const loadPromise = (async () => {
609
+ try {
610
+ const base64 = await urlToBlob({ url });
611
+ const fontFamilyName = getFontFamilyName(url);
612
+ const format = detectFontFormat(url);
613
+
614
+ const fontFace = new FontFace(fontFamilyName, `url(${base64})${format ? ` format('${format}')` : ''}`)
615
+ await fontFace.load();
616
+ (document.fonts as any).add(fontFace);
617
+ return base64
618
+ } catch (e) {
619
+ console.error('Font load failed:', url, e);
620
+ return null
621
+ }
622
+ })();
623
+
624
+ fontCacheRef.current.set(url, loadPromise as any);
625
+ return loadPromise;
466
626
  }
467
627
 
468
628
  const setTextAlign = (p: any, stroke: any, direction: 'left' | 'center' | 'right') => {
@@ -487,11 +647,9 @@ const EditorCanvas = forwardRef(
487
647
 
488
648
  //文字转图片
489
649
  const createTextImg = async ({ textContent, bgColor, textColor, stColor, strokeW, fontAssetUrl = null, textBasicInfo, letterSpacing }: any) => {
490
- const fontBase64 = await loadFont(fontAssetUrl)
650
+ const fontBase64 = await loadFont(fontAssetUrl);
491
651
  const container = document.createElement('div');
492
652
  container.style.backgroundColor = bgColor
493
- // container.style.width = `fit-content`
494
- // container.style.height = `fit-content`
495
653
  container.style.boxSizing = 'content-box'
496
654
  container.style.display = 'inline-block'
497
655
  container.style.textAlign = textBasicInfo.textAlign || 'left';
@@ -553,72 +711,72 @@ const EditorCanvas = forwardRef(
553
711
  const base64Image = await embedFontInSVG(dataurl, fontAssetUrl, fontBase64);
554
712
  return { base64Image, height, width };
555
713
  }
556
- const createText = async ({ textContent, bgColor, textColor, position, textBasicInfo, id }: any, fc2: any) => {
557
- const canvas = fc || fc2;
558
- const { left, top, angle, scaleX, scaleY, zoomX, zoomY } = position;
559
- const textImgData = await createTextImg({ textContent, bgColor, textColor, textBasicInfo });
560
- return new Promise((resolve, reject) => {
561
- fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
562
- imgData.set({
563
- left,
564
- top,
565
- angle,
566
- width: textImgData.width,
567
- height: textImgData.height,
568
- scaleX,
569
- scaleY,
570
- clipData: {
571
- id: uuidv4(),
572
- inPoint: Math.floor((frame / 30) * 1000000),
573
- inFrame: frame,
574
- type: "文字",
575
- textBasicInfo,
576
- textColor: textColor,
577
- text: textContent,
578
- bgColor: bgColor
579
- },
580
- })
581
- imgData.on("selected", (options: any) => {
582
- options.target.isSelected = -1;
583
- });
584
- imgData.on("moving", (options: any) => {
585
- options.transform.target.isSelected = 0;
586
- });
587
- imgData.on('modified', () => {
588
- const fObj = convertToJSON(imgData)
589
- vmmlConverterRef.current.updateClip(fObj);
590
- });
591
- canvas.centerObject(imgData);
592
- canvas.add(imgData)
593
- setTimeout(()=>{
594
- canvas.renderAll();
595
- })
596
- onVideoChange(imgData.clipData);
597
- vmmlConverterRef.current.addTextClip(convertToJSON(imgData));
598
- resolve(true);
599
- })
600
- })
601
- };
602
- const updateText = async ({ id, textContent, bgColor, textColor, textBasicInfo, fontAssetUrl }: any) => {
603
- const textImgData = await createTextImg({ textContent, bgColor, textColor, fontAssetUrl, textBasicInfo });
604
- const target = fc.getObjects().find((item: any) => item.clipData.id === id);
605
- target.setSrc(textImgData.base64Image, (img: any) => {
606
- img.set({
607
- visible: true,
714
+ const createText = async ({ textContent, bgColor, textColor, position, textBasicInfo, id }: any, fc2: any) => {
715
+ const canvas = fc || fc2;
716
+ const { left, top, angle, scaleX, scaleY, zoomX, zoomY } = position;
717
+ const textImgData = await createTextImg({ textContent, bgColor, textColor, textBasicInfo });
718
+ return new Promise((resolve, reject) => {
719
+ fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
720
+ imgData.set({
721
+ left,
722
+ top,
723
+ angle,
724
+ width: textImgData.width,
725
+ height: textImgData.height,
726
+ scaleX,
727
+ scaleY,
608
728
  clipData: {
609
- ...img.clipData,
729
+ id: uuidv4(),
730
+ inPoint: Math.floor((frame / 30) * 1000000),
731
+ inFrame: frame,
732
+ type: "文字",
610
733
  textBasicInfo,
611
734
  textColor: textColor,
612
735
  text: textContent,
613
- bgColor: bgColor,
614
- isAiError: false
615
- }
736
+ bgColor: bgColor
737
+ },
616
738
  })
617
- img.setCoords();
618
- fc.renderAll();
619
- vmmlConverterRef.current.updateClip(convertToJSON(img));
620
- });
621
- }
739
+ imgData.on("selected", (options: any) => {
740
+ options.target.isSelected = -1;
741
+ });
742
+ imgData.on("moving", (options: any) => {
743
+ options.transform.target.isSelected = 0;
744
+ });
745
+ imgData.on('modified', () => {
746
+ const fObj = convertToJSON(imgData)
747
+ vmmlConverterRef.current.updateClip(fObj);
748
+ });
749
+ canvas.centerObject(imgData);
750
+ canvas.add(imgData)
751
+ setTimeout(()=>{
752
+ canvas.renderAll();
753
+ })
754
+ onVideoChange(imgData.clipData);
755
+ vmmlConverterRef.current.addTextClip(convertToJSON(imgData));
756
+ resolve(true);
757
+ })
758
+ })
759
+ };
760
+ const updateText = async ({ id, textContent, bgColor, textColor, textBasicInfo, fontAssetUrl }: any) => {
761
+ const textImgData = await createTextImg({ textContent, bgColor, textColor, fontAssetUrl, textBasicInfo });
762
+ const target = fc.getObjects().find((item: any) => item.clipData.id === id);
763
+ target.setSrc(textImgData.base64Image, (img: any) => {
764
+ img.set({
765
+ visible: true,
766
+ clipData: {
767
+ ...img.clipData,
768
+ textBasicInfo,
769
+ textColor: textColor,
770
+ text: textContent,
771
+ bgColor: bgColor,
772
+ isAiError: false
773
+ }
774
+ })
775
+ img.setCoords();
776
+ fc.renderAll();
777
+ vmmlConverterRef.current.updateClip(convertToJSON(img));
778
+ });
779
+ }
622
780
  const changeObjectVisible = (id: string, visible: boolean = true) => {
623
781
  const target = fc.getObjects().find((item: any) => item.clipData.id === id);
624
782
  target.set({ visible });
@@ -673,8 +831,12 @@ const EditorCanvas = forwardRef(
673
831
  }, [fc, dragState])
674
832
 
675
833
  useEffect(() => {
676
- if (canvasSize.width && canvasSize.height && !vmmlConverterRef.current) {
677
- vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
834
+ if (canvasSize.width && canvasSize.height) {
835
+ if (vmmlConverterRef.current) {
836
+
837
+ } else {
838
+ vmmlConverterRef.current = new VmmlConverter({ vmml, canvasSize });
839
+ }
678
840
  }
679
841
  }, [canvasSize, vmml]);
680
842
 
@@ -176,26 +176,30 @@ class VmmlConverter {
176
176
  * @param fObj - 画布fObj
177
177
  */
178
178
  public updateClip(fObj: any) {
179
- console.log("updateClip fObj", fObj);
179
+ console.log("updateClip fObj", fObj, this.tracks);
180
180
  const posParam = this.setPosParam(fObj);
181
181
  const {
182
- // clipData: { id, type, lineSpacing },
183
182
  clipData: { id, type, lineSpacing, originClip },
184
183
  } = fObj;
185
- let existClip = null;
186
- // 模板内可编辑的clip
187
- if (originClip) {
188
- existClip = originClip;
189
- } else {
190
- const editorTrack = this.tracks.find((track: any) => track.editorType === type);
191
- existClip = (editorTrack?.clips || []).find((clip: any) => clip.id === id);
184
+ let existClip = null;
185
+ if (originClip) {
186
+ for (const track of this.tracks) {
187
+ const clip = (track.clips || []).find((c: any) => c.id === originClip.id);
188
+ if (clip) {
189
+ existClip = clip;
190
+ break;
191
+ }
192
192
  }
193
+ } else {
194
+ const editorTrack = this.tracks.find((track: any) => track.editorType === type);
195
+ existClip = (editorTrack?.clips || []).find((clip: any) => clip.id === id);
196
+ }
193
197
 
194
198
  if (existClip) {
195
199
  //修改现有clip的代码
196
200
  !originClip && (existClip.fObj = fObj);
197
201
  if (type === "表情包") {
198
- existClip.videoClip.posParam = posParam;
202
+ existClip.videoClip.posParam = posParam;
199
203
  } else if (type === "文字") {
200
204
  const { clipData: { text, textColor, bgColor}} = fObj;
201
205
  const scale = this.fontSize / 22;
@@ -213,7 +217,7 @@ class VmmlConverter {
213
217
  }
214
218
  }
215
219
 
216
- console.log("updateClip 最终vmml", this.vmml);
220
+ // console.log("updateClip 最终vmml", this.vmml);
217
221
  }
218
222
 
219
223
  /**
@@ -222,13 +226,18 @@ class VmmlConverter {
222
226
  * @param type - 实例 类型
223
227
  */
224
228
  public deleteClip({ id, type, originClip }: { id: string; type: string, originClip: any }): void {
225
- // 模板内的可编辑clip
226
229
  if (originClip) {
227
- originClip.duration = 0;
230
+ for (const track of this.tracks) {
231
+ const clip = (track.clips || []).find((c: any) => c.id === originClip.id);
232
+ if (clip) {
233
+ clip.duration = 0;
234
+ break;
235
+ }
236
+ }
228
237
  } else {
229
238
  const editorTrack = this.tracks.find((track: any) => track.editorType === type);
230
239
  const index = editorTrack.clips.findIndex((item: any) => item.id === id);
231
-
240
+
232
241
  if (index !== -1) {
233
242
  if (editorTrack.clips[index + 1] && editorTrack.clips[index + 1].audioClip) {
234
243
  editorTrack.clips.splice(index, 2); // 删除当前元素及下一个 audio 元素
@@ -244,8 +253,10 @@ class VmmlConverter {
244
253
  }
245
254
  }
246
255
  }
256
+ }
247
257
 
248
- console.log("deleteClip 最终Vmml", this.vmml);
258
+ changeVmml (newVmml: any) {
259
+ this.vmml = newVmml;
249
260
  }
250
261
 
251
262
  //切换静音 视频/音频