更新
This commit is contained in:
@@ -19,6 +19,8 @@ interface RenderOptions {
|
||||
titleDisplayMode?: 'short' | 'persistent';
|
||||
subtitleStyle?: Record<string, unknown>;
|
||||
titleStyle?: Record<string, unknown>;
|
||||
secondaryTitle?: string;
|
||||
secondaryTitleStyle?: Record<string, unknown>;
|
||||
outputPath: string;
|
||||
fps?: number;
|
||||
enableSubtitles?: boolean;
|
||||
@@ -75,6 +77,16 @@ async function parseArgs(): Promise<RenderOptions> {
|
||||
console.warn('Invalid titleStyle JSON');
|
||||
}
|
||||
break;
|
||||
case 'secondaryTitle':
|
||||
options.secondaryTitle = value;
|
||||
break;
|
||||
case 'secondaryTitleStyle':
|
||||
try {
|
||||
options.secondaryTitleStyle = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.warn('Invalid secondaryTitleStyle JSON');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +173,8 @@ async function main() {
|
||||
titleDisplayMode: options.titleDisplayMode || 'short',
|
||||
subtitleStyle: options.subtitleStyle,
|
||||
titleStyle: options.titleStyle,
|
||||
secondaryTitle: options.secondaryTitle,
|
||||
secondaryTitleStyle: options.secondaryTitleStyle,
|
||||
enableSubtitles: options.enableSubtitles !== false,
|
||||
width: videoWidth,
|
||||
height: videoHeight,
|
||||
|
||||
@@ -25,9 +25,11 @@ export const RemotionRoot: React.FC = () => {
|
||||
audioSrc: undefined,
|
||||
captions: undefined,
|
||||
title: undefined,
|
||||
secondaryTitle: undefined,
|
||||
titleDuration: 4,
|
||||
titleDisplayMode: 'short',
|
||||
enableSubtitles: true,
|
||||
secondaryTitleStyle: undefined,
|
||||
width: 1080,
|
||||
height: 1920,
|
||||
}}
|
||||
|
||||
@@ -10,11 +10,13 @@ export interface VideoProps {
|
||||
audioSrc?: string;
|
||||
captions?: CaptionsData;
|
||||
title?: string;
|
||||
secondaryTitle?: string;
|
||||
titleDuration?: number;
|
||||
titleDisplayMode?: 'short' | 'persistent';
|
||||
enableSubtitles?: boolean;
|
||||
subtitleStyle?: SubtitleStyle;
|
||||
titleStyle?: TitleStyle;
|
||||
secondaryTitleStyle?: TitleStyle;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
@@ -28,11 +30,13 @@ export const Video: React.FC<VideoProps> = ({
|
||||
audioSrc,
|
||||
captions,
|
||||
title,
|
||||
secondaryTitle,
|
||||
titleDuration = 4,
|
||||
titleDisplayMode = 'short',
|
||||
enableSubtitles = true,
|
||||
subtitleStyle,
|
||||
titleStyle,
|
||||
secondaryTitleStyle,
|
||||
}) => {
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: 'black' }}>
|
||||
@@ -45,8 +49,15 @@ export const Video: React.FC<VideoProps> = ({
|
||||
)}
|
||||
|
||||
{/* 顶层:标题 */}
|
||||
{title && (
|
||||
<Title title={title} duration={titleDuration} displayMode={titleDisplayMode} style={titleStyle} />
|
||||
{(title || secondaryTitle) && (
|
||||
<Title
|
||||
title={title || ''}
|
||||
secondaryTitle={secondaryTitle}
|
||||
duration={titleDuration}
|
||||
displayMode={titleDisplayMode}
|
||||
style={titleStyle}
|
||||
secondaryTitleStyle={secondaryTitleStyle}
|
||||
/>
|
||||
)}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
|
||||
@@ -25,10 +25,12 @@ export interface TitleStyle {
|
||||
|
||||
interface TitleProps {
|
||||
title: string;
|
||||
secondaryTitle?: string;
|
||||
duration?: number; // 标题显示时长(秒)
|
||||
displayMode?: 'short' | 'persistent'; // 短暂显示或常驻显示
|
||||
fadeOutStart?: number; // 开始淡出的时间(秒)
|
||||
style?: TitleStyle;
|
||||
secondaryTitleStyle?: TitleStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,17 +50,19 @@ const buildTextShadow = (color: string, size: number) => {
|
||||
`${size}px -${size}px 0 ${color}`,
|
||||
`-${size}px ${size}px 0 ${color}`,
|
||||
`${size}px ${size}px 0 ${color}`,
|
||||
`0 0 ${size * 4}px rgba(0,0,0,0.9)`,
|
||||
`0 4px 8px rgba(0,0,0,0.6)`
|
||||
`0 0 ${size * 2}px rgba(0,0,0,0.5)`,
|
||||
`0 2px 4px rgba(0,0,0,0.3)`
|
||||
].join(',');
|
||||
};
|
||||
|
||||
export const Title: React.FC<TitleProps> = ({
|
||||
title,
|
||||
secondaryTitle,
|
||||
duration = 4,
|
||||
displayMode = 'short',
|
||||
fadeOutStart,
|
||||
style,
|
||||
secondaryTitleStyle,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, width } = useVideoConfig();
|
||||
@@ -130,9 +134,32 @@ export const Title: React.FC<TitleProps> = ({
|
||||
? `'${fontFamilyName}'`
|
||||
: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif';
|
||||
|
||||
// 副标题样式
|
||||
const stStyle = secondaryTitleStyle || style;
|
||||
const stFontFile = secondaryTitleStyle?.font_file ?? style?.font_file;
|
||||
const stFontFamily = secondaryTitleStyle?.font_family ?? style?.font_family;
|
||||
const stBaseFontSize = stStyle?.font_size ?? 48;
|
||||
const stBaseStrokeSize = stStyle?.stroke_size ?? 3;
|
||||
const stBaseLetterSpacing = stStyle?.letter_spacing ?? 2;
|
||||
const stBaseTopMargin = secondaryTitleStyle?.top_margin;
|
||||
const stFontSize = Math.max(24, Math.round(stBaseFontSize * responsiveScale));
|
||||
const stColor = stStyle?.color ?? '#FFFFFF';
|
||||
const stStrokeColor = stStyle?.stroke_color ?? '#000000';
|
||||
const stStrokeSize = Math.max(1, Math.round(stBaseStrokeSize * responsiveScale));
|
||||
const stLetterSpacing = Math.max(0, stBaseLetterSpacing * responsiveScale);
|
||||
const stFontWeight = stStyle?.font_weight ?? 700;
|
||||
const stFontFamilyName = stFontFamily || 'SecondaryTitleFont';
|
||||
const stFontFamilyCss = stFontFile
|
||||
? `'${stFontFamilyName}'`
|
||||
: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif';
|
||||
const stMarginTop = typeof stBaseTopMargin === 'number'
|
||||
? Math.max(0, Math.round(stBaseTopMargin * responsiveScale))
|
||||
: Math.round(12 * responsiveScale);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
paddingTop: typeof topMargin === 'number' ? `${topMargin}px` : '6%',
|
||||
@@ -149,6 +176,16 @@ export const Title: React.FC<TitleProps> = ({
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
{secondaryTitle && stFontFile && stFontFile !== fontFile && (
|
||||
<style>{`
|
||||
@font-face {
|
||||
font-family: '${stFontFamilyName}';
|
||||
src: url('${staticFile(stFontFile)}') format('${getFontFormat(stFontFile)}');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
<h1
|
||||
style={{
|
||||
transform: `translateY(${translateY}px)`,
|
||||
@@ -171,6 +208,31 @@ export const Title: React.FC<TitleProps> = ({
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{secondaryTitle && (
|
||||
<h2
|
||||
style={{
|
||||
transform: `translateY(${translateY}px)`,
|
||||
textAlign: 'center',
|
||||
color: stColor,
|
||||
fontSize: `${stFontSize}px`,
|
||||
fontWeight: stFontWeight,
|
||||
fontFamily: stFontFile && stFontFile !== fontFile ? stFontFamilyCss : fontFamilyCss,
|
||||
textShadow: buildTextShadow(stStrokeColor, stStrokeSize),
|
||||
margin: 0,
|
||||
marginTop: `${stMarginTop}px`,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
padding: '0 5%',
|
||||
lineHeight: 1.3,
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
overflowWrap: 'anywhere',
|
||||
letterSpacing: `${stLetterSpacing}px`,
|
||||
}}
|
||||
>
|
||||
{secondaryTitle}
|
||||
</h2>
|
||||
)}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user