@versa_ai/vmml-editor 1.0.38 → 1.0.39

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.39",
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,
@@ -56,6 +57,7 @@ const EditorCanvas = forwardRef(
56
57
  if (fc) {
57
58
  const ns = Math.floor(((f ?? frame) / 30) * 1000000);
58
59
  const objects = fc.getObjects();
60
+ console.log(objects, 'fc>>>>>>>>>>>>>>>>', frame)
59
61
  objects.forEach((item: any) => {
60
62
  if (item?.clipData?.type === "文字") {
61
63
  item.set("visible", ns >= item.clipData.inPoint && ns < item.clipData.inPoint + (item.clipData.duration || vmml.template.duration));
@@ -225,7 +227,7 @@ const EditorCanvas = forwardRef(
225
227
  return createImageFromClip(clip);
226
228
  }
227
229
  if (clip.textClip && !clip.audioClip) {
228
- return createTextFromClip(clip);
230
+ return createTextFromClipCanvas(clip);
229
231
  }
230
232
  });
231
233
  const res = await Promise.allSettled(promises);
@@ -235,6 +237,7 @@ const EditorCanvas = forwardRef(
235
237
  objects.push(item.value);
236
238
  }
237
239
  });
240
+ console.log(editRenderTime.current === time, 'editRenderTime.current === time')
238
241
  if (editRenderTime.current === time) {
239
242
  canvas.add(...objects).renderAll();
240
243
  checkObjectInPoint()
@@ -327,6 +330,152 @@ const EditorCanvas = forwardRef(
327
330
  }
328
331
 
329
332
  // 创建可编辑的textclip
333
+ const createTextFromClipCanvas = async (clip: any, fc2?: fabric.Canvas) => {
334
+ return new Promise<fabric.Group>(async (resolve) => {
335
+ const canvas = fc || fc2;
336
+ if (!canvas) return null;
337
+
338
+ const { width, height } = vmml.template.dimension;
339
+ const fontSize = getFontSize(width, height);
340
+
341
+ const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType, strokeColor, strokeWidth, letterSpacing } = clip.textClip;
342
+
343
+ const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
344
+ const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
345
+ const left = canvasSize.width * posParam.centerX;
346
+ const top = canvasSize.height * posParam.centerY
347
+ const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
348
+ const stColor = strokeColor ? argbToRgba(strokeColor) : 'transparent';
349
+ const textFill = argbToRgba(textColor || '#ffffffff');
350
+ const strokeW = strokeColor&& strokeWidth ? strokeWidth * 26 * 1.5 / fontSize : 0;
351
+ const letterSpace = letterSpacing * 22 / fontSize;
352
+
353
+ // 加载字体
354
+ let fontFamily = 'sansMedium';
355
+ if (fontAssetUrl) {
356
+ const base64 = await loadFont(fontAssetUrl);
357
+ if (base64) {
358
+ fontFamily = getFontFamilyName(fontAssetUrl);
359
+ await document.fonts.ready;
360
+ }
361
+ }
362
+
363
+ const lines = textContent.split('\n');
364
+ const lineHeight = 22 + strokeW; // 行高
365
+ const textHeight = lines.length * lineHeight;
366
+ const groupWidth = Math.max(...lines.map((l: any) => {
367
+ const temp = new fabric.Text(l || ' ', { fontSize: 22, fontFamily, charSpacing: (letterSpace || 0) * 50, strokeWidth: strokeW ?? 0});
368
+ return temp?.width ?? 0;
369
+ })) + 14;
370
+ const groupHeight = textHeight + 13; // padding
371
+
372
+ // 创建双层文字
373
+ const textObjs: fabric.Object[] = [];
374
+ lines.forEach((line: string, idx: number) => {
375
+ const y = (groupHeight - textHeight) / 2 + idx * lineHeight + lineHeight / 2 + 1;
376
+
377
+ // 描边文字
378
+ const strokeText = new fabric.Text(line || ' ', {
379
+ fontFamily,
380
+ fontSize: 22,
381
+ fill: 'transparent',
382
+ stroke: stColor,
383
+ strokeWidth: strokeW,
384
+ originX: 'center',
385
+ originY: 'center',
386
+ left: groupWidth / 2, // 水平居中
387
+ top: y,
388
+ charSpacing: (letterSpace || 0) * 50,
389
+ textAlign: alignType === 1 ? 'center' : alignType === 2 ? 'right' : 'left',
390
+ objectCaching: false,
391
+ });
392
+
393
+ // 填充文字
394
+ const fillText = new fabric.Text(line || ' ', {
395
+ fontFamily,
396
+ fontSize: 22,
397
+ fill: textFill,
398
+ stroke: 'transparent',
399
+ originX: 'center',
400
+ originY: 'center',
401
+ strokeWidth: 0,
402
+ left: groupWidth / 2, // 水平居中
403
+ top: y,
404
+ charSpacing: (letterSpace || 0) * 50,
405
+ textAlign: alignType === 1 ? 'center' : alignType === 2 ? 'right' : 'left',
406
+ objectCaching: false,
407
+ });
408
+
409
+ textObjs.push(strokeText, fillText);
410
+ });
411
+
412
+ // 背景矩形
413
+ const bgRect = new fabric.Rect({
414
+ width: groupWidth,
415
+ height: groupHeight,
416
+ fill: bgColor,
417
+ originX: 'left',
418
+ originY: 'top',
419
+ rx: 5,
420
+ ry: 5,
421
+ left: 0,
422
+ top: 0,
423
+ });
424
+
425
+ const textBasicInfo = {
426
+ isBack: backgroundColor ? true : false,
427
+ colorValue: textFill,
428
+ colorName: 'custom',
429
+ textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
430
+ }
431
+ // 组合 group
432
+ const group = new fabric.Group([bgRect, ...textObjs], {
433
+ left,
434
+ top,
435
+ scaleX,
436
+ scaleY,
437
+ angle: posParam.rotationZ,
438
+ originX: 'center',
439
+ originY: 'center',
440
+ clipData: {
441
+ id: clip.id,
442
+ inPoint: clip.inPoint,
443
+ inFrame: getFrames(clip.inPoint, 30),
444
+ type: "文字",
445
+ textColor: textFill,
446
+ text: textContent,
447
+ bgColor,
448
+ originClip: clip,
449
+ fontAssetUrl,
450
+ fontFamily,
451
+ textBasicInfo,
452
+ duration: clip.duration
453
+ },
454
+ objectCaching: false,
455
+ });
456
+
457
+ // 事件
458
+ group.on("selected", (options: any) => {
459
+ options.target.isSelected = -1;
460
+ });
461
+ group.on("moving", (options: any) => {
462
+ options.transform.target.isSelected = 0;
463
+ });
464
+ group.on('modified', () => {
465
+ const fObj = convertToJSON(group);
466
+ if (fObj.clipData.isAiError) {
467
+ fObj.clipData.textColor = 'rgba(0, 0, 0, 0)';
468
+ }
469
+ if (isBatchModify) {
470
+ onBatchModify(fObj, canvas)
471
+ } else {
472
+ vmmlConverterRef.current.updateClip(fObj);
473
+ }
474
+ });
475
+
476
+ resolve(group);
477
+ });
478
+ };
330
479
  const createTextFromClip = async (clip: any, fc2?: any) => {
331
480
  return new Promise(async (resolve) => {
332
481
  const canvas = fc || fc2;
@@ -407,6 +556,7 @@ const EditorCanvas = forwardRef(
407
556
  vmmlConverterRef.current.updateClip(fObj);
408
557
  }
409
558
  });
559
+ console.log('fabricjs>>>end>>>>>>>>>>>>')
410
560
  resolve(imgData);
411
561
  });
412
562
  });
@@ -451,18 +601,30 @@ const EditorCanvas = forwardRef(
451
601
 
452
602
  const loadFont = async (url: any) => {
453
603
  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
604
+
605
+ // 检查缓存
606
+ if (fontCacheRef.current.has(url)) {
607
+ return fontCacheRef.current.get(url)!;
465
608
  }
609
+
610
+ const loadPromise = (async () => {
611
+ try {
612
+ const base64 = await urlToBlob({ url });
613
+ const fontFamilyName = getFontFamilyName(url);
614
+ const format = detectFontFormat(url);
615
+
616
+ const fontFace = new FontFace(fontFamilyName, `url(${base64})${format ? ` format('${format}')` : ''}`)
617
+ await fontFace.load();
618
+ (document.fonts as any).add(fontFace);
619
+ return base64
620
+ } catch (e) {
621
+ console.error('Font load failed:', url, e);
622
+ return null
623
+ }
624
+ })();
625
+
626
+ fontCacheRef.current.set(url, loadPromise as any);
627
+ return loadPromise;
466
628
  }
467
629
 
468
630
  const setTextAlign = (p: any, stroke: any, direction: 'left' | 'center' | 'right') => {
@@ -487,11 +649,9 @@ const EditorCanvas = forwardRef(
487
649
 
488
650
  //文字转图片
489
651
  const createTextImg = async ({ textContent, bgColor, textColor, stColor, strokeW, fontAssetUrl = null, textBasicInfo, letterSpacing }: any) => {
490
- const fontBase64 = await loadFont(fontAssetUrl)
652
+ const fontBase64 = await loadFont(fontAssetUrl);
491
653
  const container = document.createElement('div');
492
654
  container.style.backgroundColor = bgColor
493
- // container.style.width = `fit-content`
494
- // container.style.height = `fit-content`
495
655
  container.style.boxSizing = 'content-box'
496
656
  container.style.display = 'inline-block'
497
657
  container.style.textAlign = textBasicInfo.textAlign || 'left';
@@ -553,72 +713,72 @@ const EditorCanvas = forwardRef(
553
713
  const base64Image = await embedFontInSVG(dataurl, fontAssetUrl, fontBase64);
554
714
  return { base64Image, height, width };
555
715
  }
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,
716
+ const createText = async ({ textContent, bgColor, textColor, position, textBasicInfo, id }: any, fc2: any) => {
717
+ const canvas = fc || fc2;
718
+ const { left, top, angle, scaleX, scaleY, zoomX, zoomY } = position;
719
+ const textImgData = await createTextImg({ textContent, bgColor, textColor, textBasicInfo });
720
+ return new Promise((resolve, reject) => {
721
+ fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
722
+ imgData.set({
723
+ left,
724
+ top,
725
+ angle,
726
+ width: textImgData.width,
727
+ height: textImgData.height,
728
+ scaleX,
729
+ scaleY,
608
730
  clipData: {
609
- ...img.clipData,
731
+ id: uuidv4(),
732
+ inPoint: Math.floor((frame / 30) * 1000000),
733
+ inFrame: frame,
734
+ type: "文字",
610
735
  textBasicInfo,
611
736
  textColor: textColor,
612
737
  text: textContent,
613
- bgColor: bgColor,
614
- isAiError: false
615
- }
738
+ bgColor: bgColor
739
+ },
616
740
  })
617
- img.setCoords();
618
- fc.renderAll();
619
- vmmlConverterRef.current.updateClip(convertToJSON(img));
620
- });
621
- }
741
+ imgData.on("selected", (options: any) => {
742
+ options.target.isSelected = -1;
743
+ });
744
+ imgData.on("moving", (options: any) => {
745
+ options.transform.target.isSelected = 0;
746
+ });
747
+ imgData.on('modified', () => {
748
+ const fObj = convertToJSON(imgData)
749
+ vmmlConverterRef.current.updateClip(fObj);
750
+ });
751
+ canvas.centerObject(imgData);
752
+ canvas.add(imgData)
753
+ setTimeout(()=>{
754
+ canvas.renderAll();
755
+ })
756
+ onVideoChange(imgData.clipData);
757
+ vmmlConverterRef.current.addTextClip(convertToJSON(imgData));
758
+ resolve(true);
759
+ })
760
+ })
761
+ };
762
+ const updateText = async ({ id, textContent, bgColor, textColor, textBasicInfo, fontAssetUrl }: any) => {
763
+ const textImgData = await createTextImg({ textContent, bgColor, textColor, fontAssetUrl, textBasicInfo });
764
+ const target = fc.getObjects().find((item: any) => item.clipData.id === id);
765
+ target.setSrc(textImgData.base64Image, (img: any) => {
766
+ img.set({
767
+ visible: true,
768
+ clipData: {
769
+ ...img.clipData,
770
+ textBasicInfo,
771
+ textColor: textColor,
772
+ text: textContent,
773
+ bgColor: bgColor,
774
+ isAiError: false
775
+ }
776
+ })
777
+ img.setCoords();
778
+ fc.renderAll();
779
+ vmmlConverterRef.current.updateClip(convertToJSON(img));
780
+ });
781
+ }
622
782
  const changeObjectVisible = (id: string, visible: boolean = true) => {
623
783
  const target = fc.getObjects().find((item: any) => item.clipData.id === id);
624
784
  target.set({ visible });