@versa_ai/vmml-editor 1.0.47 → 1.0.49
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/README.md +1 -1
- package/dist/index.js +119 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +119 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/assets/css/index.scss +1 -1
- package/src/components/Controls.tsx +2 -2
- package/src/components/EditorCanvas.tsx +72 -29
- package/src/index.tsx +62 -6
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { formatTime } from "@versa_ai/vmml-utils"
|
|
|
2
2
|
import { pauseIcon, playIcon } from "../utils/const"
|
|
3
3
|
import SeekBar from "./SeekBar"
|
|
4
4
|
|
|
5
|
-
const Controls = ({ player, vmmlRef, frame, fps, durationInFrames, intoEdit, isPlaying, setDragState, onControlsClick, signList }: any) => {
|
|
5
|
+
const Controls = ({ player, vmmlRef, frame, fps, durationInFrames, intoEdit, isPlaying, setDragState, onControlsClick, signList, backgroundColor }: any) => {
|
|
6
6
|
const onClickToggle = (e: any) => {
|
|
7
7
|
if (isPlaying) {
|
|
8
8
|
intoEdit();
|
|
@@ -12,7 +12,7 @@ const Controls = ({ player, vmmlRef, frame, fps, durationInFrames, intoEdit, isP
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<div className="player-controls">
|
|
15
|
+
<div className="player-controls" style={{ backgroundColor }}>
|
|
16
16
|
<div className="player-controls-toggle mr-16 opacity-06" onClick={onClickToggle}>
|
|
17
17
|
{ isPlaying ? <img src={pauseIcon} alt="暂停" /> : <img src={playIcon} alt="播放" /> }
|
|
18
18
|
</div>
|
|
@@ -220,6 +220,7 @@ const EditorCanvas = forwardRef(
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
const createEditObjes = async (canvas: any, time: number) => {
|
|
223
|
+
setCanvasScale(vmml)
|
|
223
224
|
const promises = editClips.map((clip: any) => {
|
|
224
225
|
if (clip.videoClip) {
|
|
225
226
|
return createImageFromClip(clip);
|
|
@@ -327,7 +328,8 @@ const EditorCanvas = forwardRef(
|
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
const createTextObjs = async ({ strokeW, stColor, fontAssetUrl = null, letterSpace, bgColor, textContent, textBasicInfo, textColor }: any) => {
|
|
330
|
-
|
|
331
|
+
const { width, height } = vmml.template.dimension;
|
|
332
|
+
const fontSize = getFontSize(width, height);
|
|
331
333
|
// 加载字体
|
|
332
334
|
let fontFamily = 'sansMedium';
|
|
333
335
|
if (fontAssetUrl) {
|
|
@@ -340,7 +342,12 @@ const EditorCanvas = forwardRef(
|
|
|
340
342
|
const lines = textContent.split('\n').filter((item: string) => item);
|
|
341
343
|
|
|
342
344
|
const lineHeight = 22 + strokeW; // 行高
|
|
343
|
-
const paddingX = 7; // 左右内边距
|
|
345
|
+
// const paddingX = 7; // 左右内边距
|
|
346
|
+
// const paddingY = 7; // 上下内边距
|
|
347
|
+
// const round = 4; // 圆角
|
|
348
|
+
const paddingX = 60 * 22 / fontSize // 基于player做的修改
|
|
349
|
+
const paddingY = 50 * 22 / fontSize // 基于player做的修改
|
|
350
|
+
const round = 40 * 22 / fontSize // 基于player做的修改
|
|
344
351
|
const groupWidth = Math.max(...lines.map((l: any) => {
|
|
345
352
|
let options: any = { fontSize: 22, fontFamily, strokeWidth: strokeW ?? 0 }
|
|
346
353
|
if (letterSpace) options.charSpacing = letterSpace
|
|
@@ -354,12 +361,11 @@ const EditorCanvas = forwardRef(
|
|
|
354
361
|
const y = -groupHeight / 2 + lineHeight / 2 + idx * lineHeight
|
|
355
362
|
// 设置文字对齐方式
|
|
356
363
|
let originX: 'left' | 'center' | 'right' = textBasicInfo.textAlign;
|
|
357
|
-
let left = paddingX;
|
|
358
|
-
|
|
364
|
+
let left = -groupWidth / 2 + paddingX;
|
|
359
365
|
if (originX === 'center') {
|
|
360
|
-
left =
|
|
366
|
+
left = 0; // group 中心
|
|
361
367
|
} else if (originX === 'right') {
|
|
362
|
-
left = groupWidth - paddingX;
|
|
368
|
+
left = groupWidth / 2 - paddingX;
|
|
363
369
|
}
|
|
364
370
|
|
|
365
371
|
// 描边文字
|
|
@@ -373,7 +379,6 @@ const EditorCanvas = forwardRef(
|
|
|
373
379
|
originY: 'center',
|
|
374
380
|
left,
|
|
375
381
|
top: y,
|
|
376
|
-
textAlign: 'left',
|
|
377
382
|
objectCaching: false,
|
|
378
383
|
});
|
|
379
384
|
|
|
@@ -388,7 +393,6 @@ const EditorCanvas = forwardRef(
|
|
|
388
393
|
strokeWidth: 0,
|
|
389
394
|
left,
|
|
390
395
|
top: y,
|
|
391
|
-
textAlign: 'left',
|
|
392
396
|
objectCaching: false,
|
|
393
397
|
});
|
|
394
398
|
|
|
@@ -402,18 +406,31 @@ const EditorCanvas = forwardRef(
|
|
|
402
406
|
// 背景矩形
|
|
403
407
|
const bgRect = new fabric.Rect({
|
|
404
408
|
width: groupWidth,
|
|
405
|
-
height: groupHeight +
|
|
409
|
+
height: groupHeight + paddingY * 2, // padddingY: 6.5
|
|
406
410
|
fill: bgColor,
|
|
407
|
-
originX: '
|
|
411
|
+
originX: 'center',
|
|
408
412
|
originY: 'center',
|
|
409
|
-
rx:
|
|
410
|
-
ry:
|
|
413
|
+
rx: round,
|
|
414
|
+
ry: round,
|
|
411
415
|
});
|
|
412
416
|
return {
|
|
413
417
|
objects: [bgRect, ...textObjs],
|
|
414
418
|
fontFamily,
|
|
415
419
|
}
|
|
416
420
|
}
|
|
421
|
+
|
|
422
|
+
const getOrigin = (posParam: any, fontSize: number): any => {
|
|
423
|
+
const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
|
|
424
|
+
const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
|
|
425
|
+
const left = canvasSize.width * posParam.centerX;
|
|
426
|
+
const top = canvasSize.height * posParam.centerY
|
|
427
|
+
return { scaleX, scaleY, top, left }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const setCanvasScale = (vmml: any) => {
|
|
431
|
+
heightScaleRef.current = vmml.template.dimension.height / canvasSize. height;
|
|
432
|
+
widthScaleRef.current = vmml.template.dimension.width / canvasSize.width;
|
|
433
|
+
}
|
|
417
434
|
|
|
418
435
|
// 创建可编辑的textclip
|
|
419
436
|
const createTextFromClipCanvas = async (clip: any, fc2?: fabric.Canvas) => {
|
|
@@ -423,13 +440,10 @@ const EditorCanvas = forwardRef(
|
|
|
423
440
|
|
|
424
441
|
const { width, height } = vmml.template.dimension;
|
|
425
442
|
const fontSize = getFontSize(width, height);
|
|
426
|
-
|
|
443
|
+
|
|
427
444
|
const { textContent, backgroundColor, textColor, posParam, fontAssetUrl, alignType, strokeColor, strokeWidth, letterSpacing } = clip.textClip;
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
const scaleY = posParam.scaleY * fontSize / 22 / heightScaleRef.current;
|
|
431
|
-
const left = canvasSize.width * posParam.centerX;
|
|
432
|
-
const top = canvasSize.height * posParam.centerY
|
|
445
|
+
|
|
446
|
+
const { left, top, scaleX, scaleY } = getOrigin(posParam, fontSize)
|
|
433
447
|
const bgColor = backgroundColor ? argbToRgba(backgroundColor) : 'transparent';
|
|
434
448
|
const strokeW = strokeColor && strokeWidth ? strokeWidth * 30 * 1.5 / fontSize : 0;
|
|
435
449
|
const stColor = strokeColor ? argbToRgba(strokeColor) : 'transparent';
|
|
@@ -587,42 +601,71 @@ const EditorCanvas = forwardRef(
|
|
|
587
601
|
|
|
588
602
|
}
|
|
589
603
|
// 修改文字
|
|
590
|
-
const updateText = async ({ id, textContent, bgColor, textColor, textBasicInfo, fontAssetUrl }: any) => {
|
|
591
|
-
|
|
604
|
+
const updateText = async ({ id, textContent, bgColor, textColor, strokeColor, letterSpacing, strokeWidth, textBasicInfo, fontAssetUrl, posParam = null, alignType }: any) => {
|
|
605
|
+
const { width, height } = vmml.template.dimension;
|
|
606
|
+
const fontSize = getFontSize(width, height);
|
|
607
|
+
|
|
592
608
|
const target = fc.getObjects().find((item: any) => item.clipData?.id === id);
|
|
593
|
-
const
|
|
594
|
-
const originClip = target.clipData?.originClip;
|
|
609
|
+
const originClip = target?.clipData?.originClip ?? null;
|
|
595
610
|
if (originClip && target?.type === 'group') {
|
|
611
|
+
const center = target.getCenterPoint();
|
|
596
612
|
const objects = target.getObjects();
|
|
597
|
-
const strokeText = objects[1]
|
|
613
|
+
const strokeText = objects[1]
|
|
598
614
|
// 清空
|
|
599
615
|
target._objects.length = 0;
|
|
600
616
|
|
|
601
|
-
const
|
|
617
|
+
const stColor = strokeColor ?? ''
|
|
618
|
+
const strokeW = strokeColor && strokeWidth ? strokeWidth * 30 * 1.5 / fontSize : strokeText.strokeWidth;
|
|
619
|
+
const letterSpace = letterSpacing ? (letterSpacing * 22 / fontSize) * 45 : strokeText.charSpacing;
|
|
620
|
+
|
|
621
|
+
const { objects: textObjects, fontFamily } = await createTextObjs({ strokeW: strokeW ?? 0, stColor, fontAssetUrl, letterSpace: letterSpace ?? 0, bgColor, textContent, textBasicInfo, textColor })
|
|
622
|
+
// 位置计算
|
|
623
|
+
let top = target.top
|
|
624
|
+
let left = target.left
|
|
625
|
+
let scaleX = target.scaleX
|
|
626
|
+
let scaleY = target.scaleY
|
|
627
|
+
if (posParam) {
|
|
628
|
+
const obj = getOrigin(posParam, fontSize);
|
|
629
|
+
({ left, top, scaleX, scaleY } = obj);
|
|
630
|
+
}
|
|
602
631
|
// 添加
|
|
603
632
|
textObjects.forEach((obj: any) => {
|
|
604
633
|
target.addWithUpdate(obj);
|
|
605
634
|
});
|
|
635
|
+
const inFrame = getFrames(originClip.inPoint, 30);
|
|
636
|
+
const durationFrame = getFrames(originClip.duration, 30);
|
|
606
637
|
target.set({
|
|
607
|
-
visible:
|
|
638
|
+
visible: frame >= inFrame && frame < inFrame + durationFrame,
|
|
608
639
|
clipData: {
|
|
609
640
|
...target.clipData,
|
|
610
641
|
textBasicInfo,
|
|
611
642
|
textColor,
|
|
612
643
|
text: textContent,
|
|
613
644
|
bgColor,
|
|
645
|
+
fontAssetUrl,
|
|
646
|
+
fontFamily,
|
|
614
647
|
isAiError: false
|
|
615
648
|
}
|
|
616
649
|
})
|
|
617
|
-
target._calcBounds();
|
|
618
|
-
target._updateObjectsCoords();
|
|
619
650
|
target.setPositionByOrigin(center, 'center', 'center');
|
|
651
|
+
target.set({
|
|
652
|
+
visible: frame >= inFrame && frame < inFrame + durationFrame,
|
|
653
|
+
top,
|
|
654
|
+
left,
|
|
655
|
+
scaleY,
|
|
656
|
+
scaleX
|
|
657
|
+
})
|
|
620
658
|
originClip.textClip = {
|
|
621
659
|
...originClip.textClip,
|
|
622
660
|
textContent,
|
|
661
|
+
letterSpacing: letterSpacing ?? 0,
|
|
662
|
+
strokeWidth: strokeWidth ?? 0,
|
|
663
|
+
alignType: alignType ?? originClip?.alignType ?? 0,
|
|
664
|
+
posParam: posParam ?? originClip.posParam,
|
|
623
665
|
backgroundColor: bgColor,
|
|
624
|
-
textColor,
|
|
625
|
-
|
|
666
|
+
textColor: textColor,
|
|
667
|
+
fontAssetUrl,
|
|
668
|
+
strokeColor: stColor
|
|
626
669
|
}
|
|
627
670
|
target.setCoords();
|
|
628
671
|
fc?.requestRenderAll();
|
package/src/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { VmmlPlayer } from "@versa_ai/vmml-player";
|
|
|
2
2
|
import cloneDeep from "lodash.clonedeep";
|
|
3
3
|
import { type MutableRefObject, forwardRef, useEffect, useRef, useState, useImperativeHandle, useMemo } from "react";
|
|
4
4
|
import type { AnyZodObject } from "zod";
|
|
5
|
-
import { getFrames, takeScreenshot, convertVmmlTextScaleByForbidden } from "@versa_ai/vmml-utils";
|
|
5
|
+
import { getFrames, takeScreenshot, convertVmmlTextScaleByForbidden, argbToRgba } from "@versa_ai/vmml-utils";
|
|
6
6
|
import "./assets/css/index.scss";
|
|
7
7
|
import Controls from "./components/Controls";
|
|
8
8
|
import EditorCanvas from "./components/EditorCanvas";
|
|
@@ -31,6 +31,8 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
31
31
|
pauseWhenBuffering = false,
|
|
32
32
|
isBatchModify = false,
|
|
33
33
|
textWarapCenter = false,
|
|
34
|
+
backgroundColor = '#000',
|
|
35
|
+
existenceBorderRadio = true,
|
|
34
36
|
hideConfig = { hideDelete: false, hideEdit: false, hideMute: false, hideVolume: false } // { hideDelete: false, hideEdit: false, hideMute: false, hideVolume: false }
|
|
35
37
|
}: any,
|
|
36
38
|
ref: any,
|
|
@@ -61,9 +63,9 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
61
63
|
const vmmlConverterRef = useRef<any>(null);
|
|
62
64
|
const [initFcObjs, setInitFcObjs] = useState([]);
|
|
63
65
|
const [editClips, setEditClips] = useState<any[]>([]); // 可编辑的 clips
|
|
64
|
-
const [refreshEdit, setRefreshEdit] = useState(0); // 触发画布刷新
|
|
65
66
|
const vmmlFlag = useRef(false);
|
|
66
67
|
const needPlay = useRef(true);
|
|
68
|
+
const needFreshEdit = useRef<boolean>(true)
|
|
67
69
|
|
|
68
70
|
const setVmml = () => {
|
|
69
71
|
const { current }: any = vmmlPlayerRef;
|
|
@@ -373,7 +375,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
373
375
|
}, [canvasSize, vmmlState]);
|
|
374
376
|
|
|
375
377
|
useEffect(() => {
|
|
376
|
-
if (editableArray.length && vmmlState?.template) {
|
|
378
|
+
if (editableArray.length && vmmlState?.template && needFreshEdit.current) {
|
|
377
379
|
initCanEditClips(vmmlState.template.tracks);
|
|
378
380
|
}
|
|
379
381
|
}, [editableArray, vmmlState]);
|
|
@@ -442,10 +444,10 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
442
444
|
|
|
443
445
|
const convertedVmml = v;
|
|
444
446
|
const isSame = JSON.stringify(convertedVmml) == JSON.stringify(vmmlState);
|
|
445
|
-
// console.log(isSame, 'isSamew>>>')
|
|
446
447
|
if (!isSame) {
|
|
447
448
|
const currentV = cloneDeep(convertedVmml)
|
|
448
449
|
canvasCurrent?.getCanvasCtx()?.clear?.()
|
|
450
|
+
needFreshEdit.current = true
|
|
449
451
|
setVmmlState(currentV);
|
|
450
452
|
setDurationInFrames(getFrames(currentV?.template?.duration || 1, fps));
|
|
451
453
|
}
|
|
@@ -454,6 +456,55 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
454
456
|
if (isSame) canvasCurrent?.checkObjectInPoint?.(currentFrame);
|
|
455
457
|
}
|
|
456
458
|
|
|
459
|
+
const updateEditorTextClips = (v: any, checkFrame?: number) => { // 相同id批量更新文字editor
|
|
460
|
+
const { current: playerCurrent }: any = vmmlPlayerRef;
|
|
461
|
+
const { current: canvasCurrent }: any = canvasRef;
|
|
462
|
+
if (!playerCurrent) return;
|
|
463
|
+
|
|
464
|
+
const playing = playerCurrent?.playerRef?.isPlaying?.() ?? false
|
|
465
|
+
if (!playing) needPlay.current = false // 暂停状态 不用自动播放
|
|
466
|
+
|
|
467
|
+
const currentFrame = checkFrame ?? (player?.getCurrentFrame?.() ?? frame ?? pauseFrame);
|
|
468
|
+
setFrame(currentFrame) // 设置当前frame
|
|
469
|
+
|
|
470
|
+
const convertedVmml = v;
|
|
471
|
+
const currentV = cloneDeep(convertedVmml)
|
|
472
|
+
needFreshEdit.current = false
|
|
473
|
+
setVmmlState(currentV);
|
|
474
|
+
const tracks = currentV?.template?.tracks?.filter((track: any) => track.type === 2)
|
|
475
|
+
tracks?.forEach((track: any) => {
|
|
476
|
+
track?.clips?.forEach((clip: any) => {
|
|
477
|
+
const { textClip = null } = clip
|
|
478
|
+
if (textClip) {
|
|
479
|
+
const { textContent, backgroundColor, textColor, fontAssetUrl, alignType, letterSpacing, strokeWidth, strokeColor, posParam } = textClip
|
|
480
|
+
const textFill = argbToRgba(textColor || '#ffffffff');
|
|
481
|
+
const textBasicInfo = {
|
|
482
|
+
isBack: backgroundColor ? true : false,
|
|
483
|
+
colorValue: textFill,
|
|
484
|
+
colorName: 'custom',
|
|
485
|
+
textAlign: alignType === 1 ? 'center' : (alignType === 2 ? 'right' : 'left')
|
|
486
|
+
}
|
|
487
|
+
canvasCurrent?.updateText({
|
|
488
|
+
id: clip.id,
|
|
489
|
+
textContent,
|
|
490
|
+
letterSpacing,
|
|
491
|
+
strokeWidth,
|
|
492
|
+
strokeColor: argbToRgba(strokeColor),
|
|
493
|
+
bgColor: argbToRgba(backgroundColor),
|
|
494
|
+
textColor: argbToRgba(textColor),
|
|
495
|
+
fontAssetUrl,
|
|
496
|
+
textBasicInfo,
|
|
497
|
+
posParam
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
playerCurrent.setVmml(convertedVmml, currentFrame);
|
|
504
|
+
canvasCurrent?.checkObjectInPoint?.(currentFrame);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
457
508
|
const getCurrentFrame = () => { // 获取当前帧
|
|
458
509
|
if (!player) return 0
|
|
459
510
|
return player.getCurrentFrame()
|
|
@@ -478,7 +529,8 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
478
529
|
texteditClose,
|
|
479
530
|
textFinish, // 完成按钮事件
|
|
480
531
|
editorSeekTo, // editor的seek事件 入参:frame
|
|
481
|
-
updateEditor // 更新editor 入参:vmml, frame
|
|
532
|
+
updateEditor, // 更新editor 入参:vmml, frame
|
|
533
|
+
updateEditorTextClips // 更新editor 入参:vmml, frame
|
|
482
534
|
}),
|
|
483
535
|
[vmmlState, player]
|
|
484
536
|
)
|
|
@@ -513,9 +565,10 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
513
565
|
<div className="vessel" onClick={onClickMain}>
|
|
514
566
|
{/* 编辑器 */}
|
|
515
567
|
<VmmlPlayer
|
|
568
|
+
backgroundColor={backgroundColor}
|
|
516
569
|
ref={vmmlPlayerRef}
|
|
517
570
|
vmml={vmmlState}
|
|
518
|
-
existenceBorderRadio
|
|
571
|
+
existenceBorderRadio={existenceBorderRadio}
|
|
519
572
|
moveToBeginningWhenEnded
|
|
520
573
|
muted={true}
|
|
521
574
|
fps={fps}
|
|
@@ -553,6 +606,7 @@ const EditorFn = <Schema extends AnyZodObject, Props>(
|
|
|
553
606
|
{/* 控制器 */}
|
|
554
607
|
<div className="controls-box">
|
|
555
608
|
<Controls
|
|
609
|
+
backgroundColor={backgroundColor}
|
|
556
610
|
player={player}
|
|
557
611
|
vmmlRef={vmmlPlayerRef}
|
|
558
612
|
fps={fps}
|
|
@@ -632,5 +686,7 @@ export const Editor = forward(EditorFn);
|
|
|
632
686
|
* @param isBatchModify 是否批量更新 默认:false.
|
|
633
687
|
* @param hideConfig 编辑框四个角的显示配置 默认:{ hideDelete: false, hideEdit: false, hideMute: false, hideVolume: false }
|
|
634
688
|
* @param textWarapCenter 文字是否居中 默认:false
|
|
689
|
+
* @param backgroundColor 背景色,默认#000.
|
|
690
|
+
* @param existenceBorderRadio player是否圆角,圆角0.16rem.
|
|
635
691
|
*/
|
|
636
692
|
|