@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/.turbo/turbo-build.log +9 -9
- package/dist/index.js +154 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +154 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/EditorCanvas.tsx +246 -87
- package/src/index.tsx +3 -2
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,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, '
|
|
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
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
}
|
|
738
|
+
bgColor: bgColor
|
|
739
|
+
},
|
|
617
740
|
})
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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 =
|
|
439
|
-
const isSame = JSON.stringify(convertedVmml)
|
|
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);
|