@@ -9,6 +9,8 @@ export type TransitionKeyframeSet = {
99 maskXKeyframes : AnimationKeyframe [ ] ;
1010 /** Wipe brightness sweep (+1 hidden → −1 revealed), applied through WipeFilter. */
1111 brightnessKeyframes : AnimationKeyframe [ ] ;
12+ /** Zoom blur radius, in pixels of the clip before its scale applies. */
13+ blurKeyframes : AnimationKeyframe [ ] ;
1214 /** Wipe reveal direction for this phase: true = from the right edge inward, false = from the left. */
1315 wipeFromRight : boolean ;
1416} ;
@@ -20,6 +22,11 @@ export type RelativeTransitionKeyframeSet = {
2022
2123type ParsedTransition = { name : string ; speed : string | undefined } ;
2224
25+ // The render's presets use its default Bezier handles, cubic-bezier(0.5, 0, 0.5, 1).
26+ const SMOOTH = { interpolation : "bezier" , easing : "smooth" } as const ;
27+ const LINEAR = { interpolation : "linear" } as const ;
28+ type Curve = typeof SMOOTH | typeof LINEAR ;
29+
2330export class TransitionPresetBuilder {
2431 private readonly clipConfiguration : ResolvedClip ;
2532 private readonly inPreset : ParsedTransition ;
@@ -33,7 +40,7 @@ export class TransitionPresetBuilder {
3340 }
3441
3542 private parseTransition ( value : string | undefined ) : ParsedTransition {
36- const [ name , speed ] = ( value ?? "" ) . split ( / ( S l o w | F a s t | V e r y F a s t ) / ) ;
43+ const [ name , speed ] = ( value ?? "" ) . split ( / ( S l o w | F a s t ) / ) ;
3744 return { name, speed } ;
3845 }
3946
@@ -53,6 +60,7 @@ export class TransitionPresetBuilder {
5360 rotationKeyframes : [ ] ,
5461 maskXKeyframes : [ ] ,
5562 brightnessKeyframes : [ ] ,
63+ blurKeyframes : [ ] ,
5664 wipeFromRight : false
5765 } ;
5866 }
@@ -68,75 +76,54 @@ export class TransitionPresetBuilder {
6876 const transitionName = this . getPresetName ( direction ) ;
6977 const isIn = direction === "in" ;
7078
79+ // The render eases between a point at the clip's edge and one where the transition meets the clip, and each
80+ // segment takes its later point's curve: an in transition follows the inner point, an out transition the edge.
81+ const ramp = ( edge : number , rest : number , edgeCurve : Curve , innerCurve : Curve ) : AnimationKeyframe =>
82+ isIn ? { from : edge , to : rest , start, length, ...innerCurve } : { from : rest , to : edge , start, length, ...edgeCurve } ;
83+ const fade = ramp ( 0 , 1 , SMOOTH , SMOOTH ) ;
84+
7185 switch ( transitionName ) {
7286 case "fade" : {
73- const [ from , to ] = isIn ? [ 0 , 1 ] : [ 1 , 0 ] ;
74- keyframes . opacityKeyframes . push ( { from, to, start, length, interpolation : "bezier" , easing : "easeInOut" } ) ;
87+ keyframes . opacityKeyframes . push ( fade ) ;
7588 break ;
7689 }
7790 case "zoom" : {
78- const [ scaleFrom , scaleTo ] = isIn ? [ 10 , 1 ] : [ 1 , 10 ] ;
79- const [ opacityFrom , opacityTo ] = isIn ? [ 0 , 1 ] : [ 1 , 0 ] ;
80- const easing = isIn ? "easeIn" : "easeOut" ;
81- if ( isIn ) {
82- keyframes . scaleKeyframes . push ( { from : scaleFrom , to : scaleTo , start, length, interpolation : "linear" } ) ;
83- } else {
84- keyframes . scaleKeyframes . push ( { from : scaleFrom , to : scaleTo , start, length, interpolation : "bezier" , easing } ) ;
85- }
86- keyframes . opacityKeyframes . push ( { from : opacityFrom , to : opacityTo , start, length, interpolation : "bezier" , easing } ) ;
91+ keyframes . scaleKeyframes . push ( ramp ( 10 , 1 , SMOOTH , LINEAR ) ) ;
92+ keyframes . opacityKeyframes . push ( fade ) ;
93+ keyframes . blurKeyframes . push ( ramp ( 5 , 0 , SMOOTH , SMOOTH ) ) ;
8794 break ;
8895 }
8996 case "slideLeft" :
9097 case "slideRight" :
9198 case "slideUp" :
92- case "slideDown" : {
93- const isHorizontal = transitionName === "slideLeft" || transitionName === "slideRight" ;
94- const isNegative = transitionName === "slideLeft" || transitionName === "slideUp" ;
95- const offset = 0.025 ;
96-
97- const [ offsetFrom , offsetTo ] = isIn ? [ isNegative ? offset : - offset , 0 ] : [ 0 , isNegative ? - offset : offset ] ;
98- const interpolation = isIn ? "linear" : "bezier" ;
99- const [ opacityFrom , opacityTo ] = isIn ? [ 0 , 1 ] : [ 1 , 0 ] ;
100-
101- const targetKeyframes = isHorizontal ? keyframes . offsetXKeyframes : keyframes . offsetYKeyframes ;
102- targetKeyframes . push (
103- isIn
104- ? { from : offsetFrom , to : offsetTo , start, length, interpolation }
105- : { from : offsetFrom , to : offsetTo , start, length, interpolation, easing : "ease" }
106- ) ;
107- keyframes . opacityKeyframes . push ( { from : opacityFrom , to : opacityTo , start, length, interpolation : "bezier" , easing : "ease" } ) ;
108- break ;
109- }
99+ case "slideDown" :
110100 case "carouselLeft" :
111101 case "carouselRight" :
112102 case "carouselUp" :
113103 case "carouselDown" : {
114- const isHorizontal = transitionName === "carouselLeft" || transitionName === "carouselRight" ;
115- const isNegative = transitionName === "carouselLeft" || transitionName === "carouselUp" ;
116-
117- // Carousel uses different offsets for in vs out, and different interpolation
118- const baseOffset = ( isHorizontal && 1 ) || ( isIn ? 1.05 : 1.1 ) ;
119- const [ offsetFrom , offsetTo ] = isIn ? [ isNegative ? baseOffset : - baseOffset , 0 ] : [ 0 , isNegative ? - baseOffset : baseOffset ] ;
104+ const isSlide = transitionName . startsWith ( "slide" ) ;
105+ const isHorizontal = transitionName . endsWith ( "Left" ) || transitionName . endsWith ( "Right" ) ;
106+ // Fractions of the frame, matching the render; vertical carousels travel further to clear the edge.
107+ let distance = isSlide ? 0.025 : 1 ;
108+ if ( ! isSlide && ! isHorizontal ) distance = isIn ? 1.05 : 1.1 ;
109+ // Left and down enter from the positive side of their axis (offset y points up) and leave by the other.
110+ const entersFromPositive = transitionName . endsWith ( "Left" ) || transitionName . endsWith ( "Down" ) ;
111+ const edge = ( isIn === entersFromPositive ? 1 : - 1 ) * distance ;
120112
121113 const targetKeyframes = isHorizontal ? keyframes . offsetXKeyframes : keyframes . offsetYKeyframes ;
122- targetKeyframes . push (
123- isIn
124- ? { from : offsetFrom , to : offsetTo , start, length, interpolation : "linear" }
125- : { from : offsetFrom , to : offsetTo , start, length, interpolation : "bezier" , easing : "ease" }
126- ) ;
114+ targetKeyframes . push ( ramp ( edge , 0 , SMOOTH , LINEAR ) ) ;
115+ if ( isSlide ) keyframes . opacityKeyframes . push ( fade ) ;
127116 break ;
128117 }
129118 case "reveal" : {
130- const [ from , to ] = isIn ? [ 0 , 1 ] : [ 1 , 0 ] ;
131- keyframes . maskXKeyframes . push ( { from, to, start, length, interpolation : "bezier" , easing : "ease" } ) ;
119+ keyframes . maskXKeyframes . push ( ramp ( 0 , 1 , SMOOTH , SMOOTH ) ) ;
132120 break ;
133121 }
134122 case "wipeLeft" :
135123 case "wipeRight" : {
136124 // Brightness sweeps +1 (hidden) → −1 (revealed) in, and back out, applied via WipeFilter.
137125 // wipeLeft reveals from the right edge inward; wipeRight from the left.
138- const [ from , to ] = isIn ? [ 1 , - 1 ] : [ - 1 , 1 ] ;
139- keyframes . brightnessKeyframes . push ( { from, to, start, length, interpolation : "bezier" , easing : "easeInOut" } ) ;
126+ keyframes . brightnessKeyframes . push ( ramp ( 1 , - 1 , SMOOTH , SMOOTH ) ) ;
140127 keyframes . wipeFromRight = transitionName === "wipeLeft" ? isIn : ! isIn ;
141128 break ;
142129 }
@@ -153,20 +140,18 @@ export class TransitionPresetBuilder {
153140
154141 private getPresetLength ( direction : "in" | "out" ) : number {
155142 const { name, speed } = direction === "in" ? this . inPreset : this . outPreset ;
156- const isCarousel = name . startsWith ( "carousel" ) ;
157- const isSlide = name . startsWith ( "slide" ) ;
158143
159144 if ( name === "zoom" ) return 0.4 ;
160145
146+ // Matches the render: slides and carousels take half as long as other transitions at every speed.
147+ const base = name . startsWith ( "slide" ) || name . startsWith ( "carousel" ) ? 0.5 : 1 ;
161148 switch ( speed ) {
162149 case "Slow" :
163- return 2 ;
150+ return base * 2 ;
164151 case "Fast" :
165- return isCarousel || isSlide ? 0.25 : 0.5 ;
166- case "VeryFast" :
167- return 0.25 ;
152+ return base / 2 ;
168153 default :
169- return isCarousel || isSlide ? 0.5 : 1 ;
154+ return base ;
170155 }
171156 }
172157}
0 commit comments