3v324v23's picture
upload
bc18ad5
import { ICaption } from "@designcombo/types";
import { BaseSequence, SequenceItemOptions } from "../base-sequence";
import { BoxAnim, ContentAnim } from "@designcombo/animations";
import { calculateContainerStyles, calculateTextStyles } from "../styles";
import { getAnimations } from "../../utils/get-animations";
import { calculateFrames } from "../../utils/frames";
import { CaptionWord } from "./caption-word";
import {
TranslateAnimationCaption,
TranslateOnceAnimation,
ScaleAnimationCaption,
ScaleAnimationBetween,
ScalePulseAnimationCaption,
ScaleAnimationLoop,
OpacityAnimationCaption,
ANIMATION_CONFIGS,
captionRotationCache,
rotationOptions,
AnimationConfig
} from "./caption-animations";
export default function Caption({
item,
options
}: {
item: ICaption;
options: SequenceItemOptions;
}) {
const { fps, frame } = options;
const { details, display, animations } = item as ICaption;
const { animationIn, animationOut, animationTimed } = getAnimations(
animations!,
item,
frame,
fps
);
const { from, durationInFrames } = calculateFrames(item.display, fps);
const currentFrame = (frame || 0) - (item.display.from * fps) / 1000;
const [firstWord] = details.words;
const offsetFrom = display.from - firstWord.start;
// Calculate scale factor and update details
const updatedDetails = calculateUpdatedDetails(details);
const scaleFactor = updatedDetails.scaleFactor;
// Calculate animation transforms
const { transformStyles, globalOpacity, extraStyles } =
calculateAnimationTransforms(updatedDetails, frame || 0, from, fps, item);
const { height, width, ...filteredStyles } =
calculateContainerStyles(updatedDetails);
const children = (
<BoxAnim
style={{
...filteredStyles,
display: "flex",
alignItems: "center",
justifyContent: "center",
maxHeight: "max-content",
height: "100%",
width: "auto"
}}
animationIn={animationIn}
animationOut={animationOut}
frame={currentFrame}
durationInFrames={durationInFrames}
>
<ContentAnim
animationTimed={animationTimed}
durationInFrames={durationInFrames}
frame={currentFrame}
style={{
height: "100%",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<div
id={`caption-${item.id}`}
style={{
...calculateTextStyles(updatedDetails),
...transformStyles,
...extraStyles,
transition: "transform 0.2s ease",
borderRadius: "16px",
display: currentFrame > 0 ? "block" : "none",
maxWidth: "100%",
maxHeight: "max-content",
height: "100%",
padding: "8px"
}}
>
{renderWords(
item,
updatedDetails,
scaleFactor,
offsetFrom,
fps,
currentFrame,
globalOpacity
)}
</div>
</ContentAnim>
</BoxAnim>
);
return BaseSequence({ item, options, children });
}
// Helper functions
function calculateUpdatedDetails(details: any) {
const baseFontSize = 20;
const fontSize = details.fontSize || baseFontSize;
const scaleFactor = fontSize / baseFontSize;
let strokeWidth = details.WebkitTextStrokeWidth ?? "0px";
if (typeof strokeWidth === "string" && strokeWidth.endsWith("px")) {
strokeWidth = parseFloat(strokeWidth) * scaleFactor + "px";
}
let borderWidth = details.borderWidth ?? 0;
if (typeof borderWidth === "number") {
borderWidth = borderWidth * scaleFactor;
}
const boxShadow = details.boxShadow || {
x: 0,
y: 0,
blur: 0,
color: "#000000"
};
const scaledBoxShadow = {
...boxShadow,
x: boxShadow.x * scaleFactor,
y: boxShadow.y * scaleFactor,
blur: boxShadow.blur * scaleFactor
};
return {
...details,
WebkitTextStrokeWidth: strokeWidth,
boxShadow: scaledBoxShadow,
borderWidth,
scaleFactor
};
}
function calculateAnimationTransforms(
updatedDetails: any,
frame: number,
from: number,
fps: number,
item: ICaption
) {
const relativeFrame = Math.max(frame - from, 0);
let transformValues: {
translateX?: number;
translateY?: number;
scale?: number;
opacity?: number;
rotate?: number;
} = {};
let globalOpacity: number | undefined;
let extraStyles: React.CSSProperties = {};
if (updatedDetails?.animation) {
const config = ANIMATION_CONFIGS[updatedDetails.animation] || {};
const userConfig = parseUserAnimationConfig(updatedDetails.animation);
// Apply scale animation
if (config.scale || userConfig.scale) {
const scaleConfig = userConfig.scale ?? config.scale;
if (scaleConfig) {
transformValues.scale = ScaleAnimationCaption(
relativeFrame,
item.display.to,
fps,
scaleConfig.value,
scaleConfig.mode
);
}
}
// Apply translate animation
if (config.translate || userConfig.translate) {
const translateConfig = userConfig.translate ?? config.translate;
const translation = TranslateAnimationCaption(
relativeFrame,
translateConfig
);
transformValues = { ...transformValues, ...translation };
}
// Apply opacity animation
if (config.opacity || userConfig.opacity) {
const opacityConfig = userConfig.opacity ?? config.opacity;
transformValues.opacity = OpacityAnimationCaption(
relativeFrame,
item.display.to,
fps,
opacityConfig
);
}
// Apply scale pulse animation
if (config.scalePulse || userConfig.scalePulse) {
transformValues.scale = ScalePulseAnimationCaption(
relativeFrame,
item.display.to,
fps
);
}
// Apply scale loop animation
if (config.scaleAnimationLoop || userConfig.scaleAnimationLoop) {
transformValues.scale = ScaleAnimationLoop(relativeFrame);
}
// Apply scale between animation
if (config.scaleAnimationBetween || userConfig.scaleAnimationBetween) {
transformValues.scale = ScaleAnimationBetween(
relativeFrame,
item.display.to,
fps,
50,
0.8,
1
);
}
// Apply translate once animation
if (config.translateOnceAnimation || userConfig.translateOnceAnimation) {
const translateOnceConfig =
userConfig.translateOnceAnimation ?? config.translateOnceAnimation;
if (translateOnceConfig) {
const { duration = 30, orientation = "horizontal" } =
translateOnceConfig;
transformValues = {
...transformValues,
...TranslateOnceAnimation(relativeFrame, duration, orientation)
};
}
}
// Apply rotation animations
if (config.rotateRandom || userConfig.rotateRandom) {
const captionId = item.id;
if (!captionRotationCache.has(captionId)) {
const randomRotation =
rotationOptions[Math.floor(Math.random() * rotationOptions.length)];
captionRotationCache.set(captionId, randomRotation);
}
transformValues.rotate = captionRotationCache.get(captionId)!;
}
if (
config.rotateFixed !== undefined ||
userConfig.rotateFixed !== undefined
) {
transformValues.rotate = userConfig.rotateFixed ?? config.rotateFixed;
}
// Apply global opacity
if (
config.globalOpacity !== undefined ||
userConfig.globalOpacity !== undefined
) {
const globalOpacityConfig =
userConfig.globalOpacity ?? config.globalOpacity;
globalOpacity = OpacityAnimationCaption(
relativeFrame,
item.display.to,
fps,
globalOpacityConfig
);
}
// Apply extra styles
if (config.extraStyles || userConfig.extraStyles) {
extraStyles = {
...extraStyles,
...(userConfig.extraStyles ?? config.extraStyles)
};
}
}
// Build transform styles
const transformParts: string[] = [];
if (
transformValues.translateX !== undefined ||
transformValues.translateY !== undefined
) {
const x = transformValues.translateX ?? 0;
const y = transformValues.translateY ?? 0;
transformParts.push(`translate(${x}px, ${y}px)`);
}
if (transformValues.scale !== undefined) {
transformParts.push(`scale(${transformValues.scale})`);
}
if (transformValues.rotate !== undefined) {
transformParts.push(`rotate(${transformValues.rotate}deg)`);
}
const transformStyles = {
...(transformParts.length > 0 && { transform: transformParts.join(" ") }),
...(transformValues.opacity !== undefined && {
opacity: transformValues.opacity
})
};
return { transformStyles, globalOpacity, extraStyles };
}
function parseUserAnimationConfig(animationString: string): AnimationConfig {
const userConfig: AnimationConfig = {};
const animationsUser = animationString.split("/");
animationsUser.forEach((anim) => {
// Scale
if (
anim.includes("scale") &&
!anim.startsWith("scalePulse") &&
!anim.startsWith("scaleAnimationLoop") &&
!anim.startsWith("scale-up-0") &&
!anim.startsWith("scale-up-08") &&
!anim.startsWith("scale-up-12")
) {
userConfig.scale = { value: 50 };
}
// Translate
if (anim.startsWith("translate-")) {
const option = anim.split("-")[1];
if (
option === "horizontal" ||
option === "vertical" ||
option === "bilateral"
) {
userConfig.translate = option;
}
}
// Opacity
if (anim.includes("opacity")) {
userConfig.opacity = 2;
}
// Scale Pulse
if (anim.includes("scale-pulse")) {
userConfig.scalePulse = true;
}
// Scale Animation Loop
if (anim.includes("scale-animation-loop")) {
userConfig.scaleAnimationLoop = true;
}
// Rotate Random
if (anim.includes("rotate-random")) {
userConfig.rotateRandom = true;
}
// Rotate Fixed
if (anim.includes("rotate-fixed")) {
userConfig.rotateFixed = 10;
}
});
return userConfig;
}
function renderWords(
item: ICaption,
updatedDetails: any,
scaleFactor: number,
offsetFrom: number,
fps: number,
currentFrame: number,
globalOpacity?: number
) {
if (
updatedDetails?.showObject === "line" &&
updatedDetails?.linesPerCaption
) {
console.log("renderLineBasedWords");
return renderLineBasedWords(
item,
updatedDetails,
scaleFactor,
offsetFrom,
fps,
currentFrame,
globalOpacity
);
} else if (updatedDetails?.animation === "customAnimation1") {
console.log("renderCustomAnimation1Words");
return renderCustomAnimation1Words(
item,
updatedDetails,
scaleFactor,
offsetFrom,
globalOpacity
);
} else {
console.log("renderStandardWords");
return renderStandardWords(
item,
updatedDetails,
scaleFactor,
offsetFrom,
globalOpacity
);
}
}
function renderLineBasedWords(
item: ICaption,
updatedDetails: any,
scaleFactor: number,
offsetFrom: number,
fps: number,
currentFrame: number,
globalOpacity?: number
) {
const wordsPerLine = Math.ceil(
item.details.words.length / updatedDetails.linesPerCaption
);
const lines: any[][] = [];
for (let i = 0; i < updatedDetails.linesPerCaption; i++) {
const startIndex = i * wordsPerLine;
const endIndex = Math.min(
startIndex + wordsPerLine,
item.details.words.length
);
lines.push(item.details.words.slice(startIndex, endIndex));
}
const lineDuration =
(item.display.to - item.display.from) / updatedDetails.linesPerCaption;
const currentLine = Math.min(
Math.floor(currentFrame / ((lineDuration * fps) / 1000)),
updatedDetails.linesPerCaption - 1
);
return lines.map((lineWords, lineIndex) => (
<div
key={lineIndex}
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
marginBottom: lineIndex < lines.length - 1 ? "8px" : "0"
}}
>
{lineWords.map((word: any, wordIndex: number) => (
<CaptionWord
{...createCaptionWordProps(
word,
updatedDetails,
scaleFactor,
offsetFrom,
updatedDetails.animation || "",
globalOpacity,
undefined,
lineIndex,
currentLine
)}
key={`${lineIndex}-${wordIndex}`}
/>
))}
</div>
));
}
function renderCustomAnimation1Words(
item: ICaption,
updatedDetails: any,
scaleFactor: number,
offsetFrom: number,
globalOpacity?: number
) {
const nonKeywordWords = item.details.words.filter(
(word: any) => !word.is_keyword
);
const keywordWords = item.details.words.filter(
(word: any) => word.is_keyword
);
const groupedWords: any[] = [];
if (nonKeywordWords.length > 0) {
const firstNonKeyword = nonKeywordWords[0];
const lastNonKeyword = nonKeywordWords[nonKeywordWords.length - 1];
const firstKeywordWord = keywordWords[0];
const groupEndTime = firstKeywordWord
? firstKeywordWord.start
: lastNonKeyword.end;
const groupWord = {
word: nonKeywordWords.map((w) => w.word).join(" "),
start: firstNonKeyword.start,
end: groupEndTime,
is_keyword: false
};
groupedWords.push(groupWord);
}
keywordWords.forEach((word) => {
groupedWords.push(word);
});
return groupedWords.map((word: any, index: number) => (
<CaptionWord
{...createCaptionWordProps(
word,
updatedDetails,
scaleFactor,
offsetFrom,
updatedDetails.animation || "",
globalOpacity,
"word"
)}
key={index}
/>
));
}
function renderStandardWords(
item: ICaption,
updatedDetails: any,
scaleFactor: number,
offsetFrom: number,
globalOpacity?: number
) {
return item.details.words.map((word: any, index: number) => (
<CaptionWord
{...createCaptionWordProps(
word,
updatedDetails,
scaleFactor,
offsetFrom,
updatedDetails.animation || "",
globalOpacity
)}
key={index}
/>
));
}
// Helper function to create common CaptionWord props
const createCaptionWordProps = (
word: any,
updatedDetails: any,
scaleFactor: number,
offsetFrom: number,
animation: string,
globalOpacity?: number,
showObject?: string,
lineIndex?: number,
currentLine?: number
) => ({
word,
offsetFrom,
activeColor: updatedDetails.activeColor || updatedDetails.color,
activeFillColor: updatedDetails.activeFillColor || "transparent",
appearedColor: updatedDetails.appearedColor || updatedDetails.color,
color: updatedDetails.color,
animation,
globalOpacity,
isKeywordColor: updatedDetails?.isKeywordColor || "transparent",
preservedColorKeyWord: updatedDetails?.preservedColorKeyWord || false,
scaleFactor,
animationNoneCaption: false,
showObject: showObject || updatedDetails?.showObject || "page",
lineIndex,
currentLine
});