11import { useEffect , useState } from "react" ;
2- import { type Styles } from "../utils/react-helpers.js" ;
2+
33import { roundToDecimal } from "../utils/math.js" ;
4+ import interpolate from "../interpolate.js" ;
45
56export interface VideoCaptionsProps {
67 videoRef : React . RefObject < HTMLVideoElement > ;
@@ -15,28 +16,35 @@ export interface ScrollyVideoCaptionsConfig {
1516 horizontalPadding ?: string | number ;
1617 /** Styles applied to all caption containers */
1718 style ?: React . CSSProperties ;
19+
20+ animation ?: ScrollyVideoCaptionAnimation ;
1821}
1922
2023export default function VideoCaptions (
2124 props : VideoCaptionsProps
2225) : JSX . Element | null {
2326 const { captions = [ ] , videoRef, config = { } } = props ;
24- const { horizontalPadding, verticalPadding, style } = config ;
27+ const { horizontalPadding, verticalPadding, style, animation } = config ;
2528 const currentTime = useVideoCurrentTime ( videoRef ) ;
2629
2730 return (
28- < >
29- { captions ?. map ( ( caption , i ) => (
31+ < ul
32+ style = { { listStyle : "none" , padding : "none" } }
33+ className = "scrolly-video-captions"
34+ data-current-time = { currentTime }
35+ >
36+ { captions ?. map ( ( caption ) => (
3037 < VideoCaption
3138 horizontalPadding = { horizontalPadding }
3239 verticalPadding = { verticalPadding }
40+ animation = { animation }
3341 { ...caption }
3442 commonStyle = { style }
3543 currentTime = { currentTime }
36- key = { i }
44+ key = { ` ${ caption . fromTimestamp } - ${ caption . toTimestamp } - ${ caption . position } ` }
3745 />
3846 ) ) }
39- </ >
47+ </ ul >
4048 ) ;
4149}
4250
@@ -76,6 +84,13 @@ export type ScrollyVideoCaptionPosition = `${
7684 | CaptionPosition . Center
7785 | CaptionPosition . Right } `;
7886
87+ export interface ScrollyVideoCaptionAnimation {
88+ durationInSeconds : number ;
89+ variant : ScrollyVideoCaptionAnimationVariant ;
90+ }
91+
92+ export type ScrollyVideoCaptionAnimationVariant = "fade" ;
93+
7994export interface ScrollyVideoCaptionProps {
8095 /** Content of the caption. Can be string or a component. */
8196 children : React . ReactNode ;
@@ -91,38 +106,95 @@ export interface ScrollyVideoCaptionProps {
91106 verticalPadding ?: string | number ;
92107 /** @default 5vw */
93108 horizontalPadding ?: string | number ;
109+
110+ animation ?: ScrollyVideoCaptionAnimation ;
94111}
95112
96113interface VideoCaptionProps extends ScrollyVideoCaptionProps {
97114 currentTime : number ;
98115 commonStyle ?: React . CSSProperties ;
99116}
100117
101- const styles = {
102- caption : {
103- boxSizing : "border-box" ,
104- position : "absolute" ,
105- zIndex : 0 ,
106- } ,
107- } satisfies Styles ;
108-
109- const DEFAULT_POSITION : ScrollyVideoCaptionPosition = `${ CaptionPosition . Center } -${ CaptionPosition . Center } ` ;
110- const DEFAULT_VERTICAL_PADDING = "5vh" ;
111- const DEFAULT_HORIZONTAL_PADDING = "5vw" ;
118+ const DEFAULT_ANIMATION_DURATION = 0.5 ;
119+ const DEFAULT_ANIMATION_VARIANT : ScrollyVideoCaptionAnimationVariant = "fade" ;
112120
113121function VideoCaption ( props : VideoCaptionProps ) : JSX . Element | null {
114122 const {
115123 children,
116124 currentTime = 0 ,
117125 fromTimestamp,
118126 toTimestamp,
127+ commonStyle,
128+ style,
129+ animation,
119130 ...options
120131 } = props ;
121- const isVisible = currentTime >= fromTimestamp && currentTime <= toTimestamp ;
132+ const animationDuration =
133+ animation ?. durationInSeconds ?? DEFAULT_ANIMATION_DURATION ;
134+
135+ // Overlap animation by half of the duration
136+ const isVisible =
137+ currentTime >= fromTimestamp - animationDuration / 2 &&
138+ currentTime <= toTimestamp + animationDuration / 2 ;
122139
123140 if ( ! isVisible ) return null ;
124141
125- return < div style = { calculateVideoCaptionStyle ( options ) } > { children } </ div > ;
142+ return (
143+ < li
144+ className = "scrolly-video-caption"
145+ data-timestamp-from = { fromTimestamp }
146+ data-timestamp-to = { toTimestamp }
147+ style = { {
148+ ...commonStyle ,
149+ ...style ,
150+ ...calculateVideoCaptionStyle ( options ) ,
151+ ...calculateVideoCaptionAnimation ( animation , {
152+ currentTime,
153+ fromTimestamp,
154+ toTimestamp,
155+ } ) ,
156+ } }
157+ >
158+ { children }
159+ </ li >
160+ ) ;
161+ }
162+
163+ function calculateVideoCaptionAnimation (
164+ animation : ScrollyVideoCaptionAnimation | undefined ,
165+ options : {
166+ currentTime : number ;
167+ fromTimestamp : number ;
168+ toTimestamp : number ;
169+ }
170+ ) : React . CSSProperties | undefined {
171+ const { currentTime, fromTimestamp, toTimestamp } = options ;
172+
173+ const animationVariant = animation ?. variant ?? DEFAULT_ANIMATION_VARIANT ;
174+ const animationDuration =
175+ animation ?. durationInSeconds ?? DEFAULT_ANIMATION_DURATION ;
176+
177+ const fromTimeWithAnimation = fromTimestamp - animationDuration / 2 ;
178+ const toTimeWithAnimation = toTimestamp + animationDuration / 2 ;
179+
180+ const entryRatio = interpolate ( currentTime , {
181+ sourceFrom : fromTimeWithAnimation ,
182+ sourceTo : fromTimeWithAnimation + animationDuration ,
183+ targetFrom : 0 ,
184+ targetTo : 1 ,
185+ precision : 2 ,
186+ } ) ;
187+ const exitRatio = interpolate ( currentTime , {
188+ sourceFrom : toTimeWithAnimation - animationDuration ,
189+ sourceTo : toTimeWithAnimation ,
190+ targetFrom : 1 ,
191+ targetTo : 0 ,
192+ precision : 2 ,
193+ } ) ;
194+
195+ return animationVariant === "fade"
196+ ? { opacity : entryRatio === 1 ? exitRatio : entryRatio }
197+ : undefined ;
126198}
127199
128200interface CalculateVideoCaptionStyleOptions {
@@ -133,45 +205,51 @@ interface CalculateVideoCaptionStyleOptions {
133205 horizontalPadding ?: string | number ;
134206}
135207
208+ const DEFAULT_POSITION : ScrollyVideoCaptionPosition = `${ CaptionPosition . Center } -${ CaptionPosition . Center } ` ;
209+ const DEFAULT_VERTICAL_PADDING = "5vh" ;
210+ const DEFAULT_HORIZONTAL_PADDING = "5vw" ;
211+
136212function calculateVideoCaptionStyle ( {
137- commonStyle,
138- style,
139213 position = DEFAULT_POSITION ,
140214 verticalPadding = DEFAULT_VERTICAL_PADDING ,
141215 horizontalPadding = DEFAULT_HORIZONTAL_PADDING ,
142216} : CalculateVideoCaptionStyleOptions ) : React . CSSProperties {
143217 const [ verticalPosition , horizontalPosition ] = position . split ( "-" ) ;
144218
145- const verticalStyle : React . CSSProperties =
219+ const alignItems : React . CSSProperties [ "alignItems" ] =
146220 verticalPosition === CaptionPosition . Top
147- ? { top : verticalPadding , verticalAlign : verticalPosition }
221+ ? "flex-start"
148222 : verticalPosition === CaptionPosition . Bottom
149- ? { bottom : verticalPadding , verticalAlign : verticalPosition }
150- : { top : "50%" , verticalAlign : "middle" } ;
223+ ? "flex-end"
224+ : "center" ;
225+
226+ const justifyContent : React . CSSProperties [ "justifyContent" ] =
227+ horizontalPosition === CaptionPosition . Left
228+ ? "flex-start"
229+ : horizontalPosition === CaptionPosition . Right
230+ ? "flex-end"
231+ : "center" ;
151232
152- const horizontalStyle : React . CSSProperties =
233+ const textAlign : React . CSSProperties [ "textAlign" ] =
153234 horizontalPosition === CaptionPosition . Left
154- ? { left : horizontalPadding , textAlign : horizontalPosition }
235+ ? " left"
155236 : horizontalPosition === CaptionPosition . Right
156- ? { right : horizontalPadding , textAlign : horizontalPosition }
157- : { left : "50%" , textAlign : "center" } ;
158-
159- const transform : string | undefined =
160- verticalPosition === CaptionPosition . Center &&
161- horizontalPosition === CaptionPosition . Center
162- ? "translate(-50%,-50%)"
163- : verticalPosition === CaptionPosition . Center
164- ? "translateY(-50%)"
165- : horizontalPosition === CaptionPosition . Center
166- ? "translateX(-50%)"
167- : undefined ;
237+ ? "right"
238+ : "center" ;
168239
169240 return {
170- ...commonStyle ,
171- ...style ,
172- ...styles . caption ,
173- ...verticalStyle ,
174- ...horizontalStyle ,
175- transform,
241+ boxSizing : "border-box" ,
242+ position : "absolute" ,
243+ display : "flex" ,
244+ flexDirection : "row" ,
245+ top : verticalPadding ,
246+ bottom : verticalPadding ,
247+ left : horizontalPadding ,
248+ right : horizontalPadding ,
249+ insetBlock : verticalPadding ,
250+ insetInline : horizontalPadding ,
251+ alignItems,
252+ justifyContent,
253+ textAlign,
176254 } ;
177255}
0 commit comments