Rounded Captions
Based on the @remotion/rounded-text-box demo.

rounded-captions.tsximport type {Caption } from '@remotion/captions'; import {createTikTokStyleCaptions } from '@remotion/captions'; import {loadFont } from '@remotion/google-fonts/Figtree'; import {fitTextOnNLines ,measureText } from '@remotion/layout-utils'; import {createRoundedTextBox } from '@remotion/rounded-text-box'; importReact , {useEffect ,useMemo ,useState } from 'react'; import {cancelRender ,Interactive ,useCurrentFrame ,useVideoConfig , typeInteractiveTransformProps , typeInteractivitySchema , typeSequenceProps , } from 'remotion'; typeRoundedCaptionsProps =InteractiveTransformProps &Pick <SequenceProps , 'width'> & { readonlycaptions :Caption []; readonlycombineTokensWithinMilliseconds : number | null; }; constdefaultCombineTokensWithinMilliseconds = 2000; constdefaultWidth = 900; constfontWeight = '700'; constmaxFontSize = 64; constlineHeight = 1.5; consthorizontalPadding = 22; constborderRadius = 20; constroundedCaptionsSchema = { ...Interactive .captionsSchema ,width : {type : 'number',min :horizontalPadding * 2 + 1,step : 1,default :undefined ,description : 'Caption area width',hiddenFromList : false, },combineTokensWithinMilliseconds : {type : 'number',min : 0,step : 50,default :defaultCombineTokensWithinMilliseconds ,description : 'Time between caption pages',hiddenFromList : false, }, } asconst satisfiesInteractivitySchema ; const {fontFamily ,waitUntilDone } =loadFont ('normal', {weights : [fontWeight ],subsets : ['latin'], }); constRoundedCaptionsContent :React .FC <RoundedCaptionsProps > = ({captions ,combineTokensWithinMilliseconds :providedCombineTokensWithinMilliseconds ,style ,width =defaultWidth , }) => { const [fontLoaded ,setFontLoaded ] =useState (false);useEffect (() => {waitUntilDone () .then (() =>setFontLoaded (true)) .catch ((error ) =>cancelRender (error )); }, []); constcombineTokensWithinMilliseconds =providedCombineTokensWithinMilliseconds ??defaultCombineTokensWithinMilliseconds ; constframe =useCurrentFrame (); const {fps } =useVideoConfig (); constpages =useMemo ( () =>createTikTokStyleCaptions ({captions ,combineTokensWithinMilliseconds , }).pages , [captions ,combineTokensWithinMilliseconds ], ); constcurrentTimeMs = (frame /fps ) * 1000; constpage =pages .find ( (candidate ) =>currentTimeMs >=candidate .startMs &¤tTimeMs <candidate .startMs +candidate .durationMs , ); constlayout =useMemo (() => { if (!fontLoaded || !page ?.text .trim ()) { return null; } constparagraphs =page .text .trim () .split (/\r?\n/) .map ((line ) =>line .trim ()) .filter (Boolean ); constmaxLines =Math .max (2,paragraphs .length ); constfitted =paragraphs .map ((text ) =>fitTextOnNLines ({text ,maxLines :paragraphs .length === 1 ?maxLines : 1,maxBoxWidth :Math .max (1,width -horizontalPadding * 2),fontFamily ,fontWeight ,maxFontSize ,validateFontIsLoaded : true, }), ); if (fitted .some ((result ) =>result .lines .length === 0)) { return null; } constfontSize =Math .min (...fitted .map ((result ) =>result .fontSize )); constlines =fitted .flatMap ((result ) =>result .lines ); consttextMeasurements =lines .map ((text ) =>measureText ({text ,fontFamily ,fontSize ,fontWeight ,additionalStyles : {lineHeight },validateFontIsLoaded : true, }), ); return {fontSize ,lines , ...createRoundedTextBox ({textMeasurements ,textAlign : 'center',horizontalPadding ,borderRadius , }), }; }, [fontLoaded ,page ,width ]); return ( <div style ={{alignItems : 'center',display : 'flex',justifyContent : 'center',width , ...style , }} > {layout ? ( <div style ={{position : 'relative',width :layout .boundingBox .width ,height :layout .boundingBox .height ,flexShrink : 0, }} > <svg viewBox ={layout .boundingBox .viewBox }style ={{position : 'absolute',left : 0,top : 0,width :layout .boundingBox .width ,height :layout .boundingBox .height ,overflow : 'visible', }} > <path fill ="#ffffff"d ={layout .d } /> </svg > <div style ={{position : 'relative'}}> {layout .lines .map ((line ,index ) => ( <div // eslint-disable-next-line react/no-array-index-keykey ={index }style ={{color : '#000000',fontFamily ,fontSize :layout .fontSize ,fontWeight ,lineHeight ,paddingInline :horizontalPadding ,textAlign : 'center',whiteSpace : 'pre', }} > {line } </div > ))} </div > </div > ) : null} </div > ); }; export constRoundedCaptions =Interactive .withSchema ({Component :RoundedCaptionsContent ,componentName : '<RoundedCaptions>',schema :roundedCaptionsSchema ,wrapInSequence : true, });