@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versa_ai/vmml-editor",
3
- "version": "1.0.47",
3
+ "version": "1.0.49",
4
4
  "module": "dist/index.mjs",
5
5
  "main": "dist/index.mjs",
6
6
  "types": "dist/index.d.mts",
@@ -72,7 +72,7 @@
72
72
  position: relative;
73
73
  display: flex;
74
74
  justify-content: center;
75
- border-radius: 3.2vw;
75
+ // border-radius: 3.2vw;
76
76
  overflow: hidden;
77
77
  .vessel {
78
78
  width: 100%;
@@ -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 = groupWidth / 2; // group 中心
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 + 13, // padddingY: 6.5
409
+ height: groupHeight + paddingY * 2, // padddingY: 6.5
406
410
  fill: bgColor,
407
- originX: 'left',
411
+ originX: 'center',
408
412
  originY: 'center',
409
- rx: 5,
410
- ry: 5,
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
- // textContent = 'ฉันคือไมโครเวฟที่ครบทุกฟังก์\nชัน\nใครเห็นก็ต้องว้าว\n这是修改的啊啊啊啊'
429
- const scaleX = posParam.scaleX * fontSize / 22 / widthScaleRef.current;
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
- // 1. 找到目标对象
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 center = target.getCenterPoint();
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 { objects: textObjects } = await createTextObjs({ strokeW: strokeText.strokeWidth, stColor: '', fontAssetUrl, letterSpace: strokeText.charSpacing, bgColor, textContent, textBasicInfo, textColor })
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: true,
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
- strokeColor: ''
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