@versa_ai/vmml-editor 1.0.37 → 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.37",
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,7 +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();
59
- console.log(objects, 'checkObjectInPoint>>>objects>>>>>>>>>>>', ns, f, frame)
60
+ console.log(objects, 'fc>>>>>>>>>>>>>>>>', frame)
60
61
  objects.forEach((item: any) => {
61
62
  if (item?.clipData?.type === "文字") {
62
63
  item.set("visible", ns >= item.clipData.inPoint && ns < item.clipData.inPoint + (item.clipData.duration || vmml.template.duration));
@@ -226,7 +227,7 @@ const EditorCanvas = forwardRef(
226
227
  return createImageFromClip(clip);
227
228
  }
228
229
  if (clip.textClip && !clip.audioClip) {
229
- return createTextFromClip(clip);
230
+ return createTextFromClipCanvas(clip);
230
231
  }
231
232
  });
232
233
  const res = await Promise.allSettled(promises);
@@ -236,6 +237,7 @@ const EditorCanvas = forwardRef(
236
237
  objects.push(item.value);
237
238
  }
238
239
  });
240
+ console.log(editRenderTime.current === time, 'editRenderTime.current === time')
239
241
  if (editRenderTime.current === time) {
240
242
  canvas.add(...objects).renderAll();
241
243
  checkObjectInPoint()
@@ -328,6 +330,152 @@ const EditorCanvas = forwardRef(
328
330
  }
329
331
 
330
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
+ };
331
479
  const createTextFromClip = async (clip: any, fc2?: any) => {
332
480
  return new Promise(async (resolve) => {
333
481
  const canvas = fc || fc2;
@@ -408,6 +556,7 @@ const EditorCanvas = forwardRef(
408
556
  vmmlConverterRef.current.updateClip(fObj);
409
557
  }
410
558
  });
559
+ console.log('fabricjs>>>end>>>>>>>>>>>>')
411
560
  resolve(imgData);
412
561
  });
413
562
  });
@@ -452,18 +601,30 @@ const EditorCanvas = forwardRef(
452
601
 
453
602
  const loadFont = async (url: any) => {
454
603
  if (!url) return null
455
- try {
456
- const base64 = await urlToBlob({ url });
457
- const fontFamilyName = getFontFamilyName(url);
458
- const format = detectFontFormat(url);
459
-
460
- const fontFace = new FontFace(fontFamilyName, `url(${base64})${format ? ` format('${format}')` : ''}`);
461
- await fontFace.load();
462
- (document.fonts as any).add(fontFace);
463
- return base64
464
- } catch (e) {
465
- return null
604
+
605
+ // 检查缓存
606
+ if (fontCacheRef.current.has(url)) {
607
+ return fontCacheRef.current.get(url)!;
466
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;
467
628
  }
468
629
 
469
630
  const setTextAlign = (p: any, stroke: any, direction: 'left' | 'center' | 'right') => {
@@ -488,11 +649,9 @@ const EditorCanvas = forwardRef(
488
649
 
489
650
  //文字转图片
490
651
  const createTextImg = async ({ textContent, bgColor, textColor, stColor, strokeW, fontAssetUrl = null, textBasicInfo, letterSpacing }: any) => {
491
- const fontBase64 = await loadFont(fontAssetUrl)
652
+ const fontBase64 = await loadFont(fontAssetUrl);
492
653
  const container = document.createElement('div');
493
654
  container.style.backgroundColor = bgColor
494
- // container.style.width = `fit-content`
495
- // container.style.height = `fit-content`
496
655
  container.style.boxSizing = 'content-box'
497
656
  container.style.display = 'inline-block'
498
657
  container.style.textAlign = textBasicInfo.textAlign || 'left';
@@ -554,72 +713,72 @@ const EditorCanvas = forwardRef(
554
713
  const base64Image = await embedFontInSVG(dataurl, fontAssetUrl, fontBase64);
555
714
  return { base64Image, height, width };
556
715
  }
557
- const createText = async ({ textContent, bgColor, textColor, position, textBasicInfo, id }: any, fc2: any) => {
558
- const canvas = fc || fc2;
559
- const { left, top, angle, scaleX, scaleY, zoomX, zoomY } = position;
560
- const textImgData = await createTextImg({ textContent, bgColor, textColor, textBasicInfo });
561
- return new Promise((resolve, reject) => {
562
- fabric.Image.fromURL(textImgData.base64Image, (imgData: any) => {
563
- imgData.set({
564
- left,
565
- top,
566
- angle,
567
- width: textImgData.width,
568
- height: textImgData.height,
569
- scaleX,
570
- scaleY,
571
- clipData: {
572
- id: uuidv4(),
573
- inPoint: Math.floor((frame / 30) * 1000000),
574
- inFrame: frame,
575
- type: "文字",
576
- textBasicInfo,
577
- textColor: textColor,
578
- text: textContent,
579
- bgColor: bgColor
580
- },
581
- })
582
- imgData.on("selected", (options: any) => {
583
- options.target.isSelected = -1;
584
- });
585
- imgData.on("moving", (options: any) => {
586
- options.transform.target.isSelected = 0;
587
- });
588
- imgData.on('modified', () => {
589
- const fObj = convertToJSON(imgData)
590
- vmmlConverterRef.current.updateClip(fObj);
591
- });
592
- canvas.centerObject(imgData);
593
- canvas.add(imgData)
594
- setTimeout(()=>{
595
- canvas.renderAll();
596
- })
597
- onVideoChange(imgData.clipData);
598
- vmmlConverterRef.current.addTextClip(convertToJSON(imgData));
599
- resolve(true);
600
- })
601
- })
602
- };
603
- const updateText = async ({ id, textContent, bgColor, textColor, textBasicInfo, fontAssetUrl }: any) => {
604
- const textImgData = await createTextImg({ textContent, bgColor, textColor, fontAssetUrl, textBasicInfo });
605
- const target = fc.getObjects().find((item: any) => item.clipData.id === id);
606
- target.setSrc(textImgData.base64Image, (img: any) => {
607
- img.set({
608
- 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,
609
730
  clipData: {
610
- ...img.clipData,
731
+ id: uuidv4(),
732
+ inPoint: Math.floor((frame / 30) * 1000000),
733
+ inFrame: frame,
734
+ type: "文字",
611
735
  textBasicInfo,
612
736
  textColor: textColor,
613
737
  text: textContent,
614
- bgColor: bgColor,
615
- isAiError: false
616
- }
738
+ bgColor: bgColor
739
+ },
617
740
  })
618
- img.setCoords();
619
- fc.renderAll();
620
- vmmlConverterRef.current.updateClip(convertToJSON(img));
621
- });
622
- }
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
+ }
623
782
  const changeObjectVisible = (id: string, visible: boolean = true) => {
624
783
  const target = fc.getObjects().find((item: any) => item.clipData.id === id);
625
784
  target.set({ visible });
package/src/index.tsx CHANGED
@@ -435,8 +435,9 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
435
435
  const currentFrame = checkFrame ?? (player?.getCurrentFrame?.() ?? frame ?? pauseFrame);
436
436
  setFrame(currentFrame) // 设置当前frame
437
437
 
438
- const convertedVmml = convertVmmlTextScaleByForbidden(v);
439
- const isSame = JSON.stringify(convertedVmml) === JSON.stringify(vmmlState);
438
+ const convertedVmml = v;
439
+ const isSame = JSON.stringify(convertedVmml) == JSON.stringify(vmmlState);
440
+ console.log(isSame, 'VMML是否相同', convertedVmml, vmmlState)
440
441
  if (!isSame) {
441
442
  canvasCurrent?.getCanvasCtx()?.clear?.()
442
443
  setVmmlState(convertedVmml);