@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/.turbo/turbo-build.log +9 -9
- package/dist/index.js +151 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +151 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/EditorCanvas.tsx +246 -86
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versa_ai/vmml-editor",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
}
|
|
738
|
+
bgColor: bgColor
|
|
739
|
+
},
|
|
616
740
|
})
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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 });
|