1- import { ComposedKeyframeBuilder } from "@animations/composed-keyframe-builder" ;
21import { EffectPresetBuilder } from "@animations/effect-preset-builder" ;
3- import { KeyframeBuilder } from "@animations/keyframe-builder" ;
2+ import { KeyframeBuilder , type PresetPoint } from "@animations/keyframe-builder" ;
43import { TransitionPresetBuilder } from "@animations/transition-preset-builder" ;
54import { WipeFilter } from "@animations/wipe-filter" ;
65import { type Edit } from "@core/edit-session" ;
76import { InternalEvent } from "@core/events/edit-events" ;
87import { calculateContainerScale , calculateFitScale , calculateSpriteTransform , type FitMode } from "@core/layout/fit-system" ;
98import { type PlaybackReadiness } from "@core/pending-captures" ;
10- import { hasKeyframedVisualProperty } from "@core/shared/clip-utils" ;
119import {
1210 type AliasReference ,
1311 type ResolvedTiming ,
@@ -20,7 +18,7 @@ import {
2018import { Pointer } from "@inputs/pointer" ;
2119import { type Size , type Vector } from "@layouts/geometry" ;
2220import { relativeToAbsolute , absoluteToRelative , rotationOriginOffset } from "@layouts/position-builder" ;
23- import { type Clip , type ResolvedClip } from "@schemas" ;
21+ import { type Clip , type Keyframe , type ResolvedClip } from "@schemas" ;
2422import * as pixi from "pixi.js" ;
2523
2624import { Entity } from "../../../core/shared/entity" ;
@@ -88,13 +86,13 @@ export abstract class Player extends Entity {
8886
8987 private resolvedTiming : ResolvedTiming ;
9088
91- private offsetXKeyframeBuilder ?: ComposedKeyframeBuilder ;
92- private offsetYKeyframeBuilder ?: ComposedKeyframeBuilder ;
93- private scaleKeyframeBuilder ?: ComposedKeyframeBuilder ;
94- private opacityKeyframeBuilder ?: ComposedKeyframeBuilder ;
95- private rotationKeyframeBuilder ?: ComposedKeyframeBuilder ;
96- private skewXKeyframeBuilder ?: ComposedKeyframeBuilder ;
97- private skewYKeyframeBuilder ?: ComposedKeyframeBuilder ;
89+ private offsetXKeyframeBuilder ?: KeyframeBuilder ;
90+ private offsetYKeyframeBuilder ?: KeyframeBuilder ;
91+ private scaleKeyframeBuilder ?: KeyframeBuilder ;
92+ private opacityKeyframeBuilder ?: KeyframeBuilder ;
93+ private rotationKeyframeBuilder ?: KeyframeBuilder ;
94+ private skewXKeyframeBuilder ?: KeyframeBuilder ;
95+ private skewYKeyframeBuilder ?: KeyframeBuilder ;
9896 private maskXKeyframeBuilder ?: KeyframeBuilder ;
9997 private wipeBrightnessBuilder ?: KeyframeBuilder ;
10098 private wipeInFromRight : boolean = false ;
@@ -146,50 +144,17 @@ export abstract class Player extends Entity {
146144 protected configureKeyframes ( ) {
147145 const length = this . getLength ( ) ;
148146 const config = this . clipConfiguration ;
149-
150- // Extract base values from clip configuration
151- const baseOffsetX = typeof config . offset ?. x === "number" ? config . offset . x : 0 ;
152- const baseOffsetY = typeof config . offset ?. y === "number" ? config . offset . y : 0 ;
153- const baseScale = typeof config . scale === "number" ? config . scale : 1 ;
154- const baseOpacity = typeof config . opacity === "number" ? config . opacity : 1 ;
155- const baseRotation = typeof config . transform ?. rotate ?. angle === "number" ? config . transform . rotate . angle : 0 ;
156- const baseSkewX = typeof config . transform ?. skew ?. x === "number" ? config . transform . skew . x : 0 ;
157- const baseSkewY = typeof config . transform ?. skew ?. y === "number" ? config . transform . skew . y : 0 ;
158-
159- // Create composed builders with base values
160- this . offsetXKeyframeBuilder = new ComposedKeyframeBuilder ( baseOffsetX , "additive" ) ;
161- this . offsetYKeyframeBuilder = new ComposedKeyframeBuilder ( baseOffsetY , "additive" ) ;
162- this . scaleKeyframeBuilder = new ComposedKeyframeBuilder ( baseScale , "multiplicative" ) ;
163- this . opacityKeyframeBuilder = new ComposedKeyframeBuilder ( baseOpacity , "multiplicative" , { min : 0 , max : 1 } ) ;
164- this . rotationKeyframeBuilder = new ComposedKeyframeBuilder ( baseRotation , "additive" ) ;
165- this . skewXKeyframeBuilder = new ComposedKeyframeBuilder ( baseSkewX , "additive" ) ;
166- this . skewYKeyframeBuilder = new ComposedKeyframeBuilder ( baseSkewY , "additive" ) ;
167-
168- // If user has custom keyframes, add them and skip effect/transition layers
169- if ( hasKeyframedVisualProperty ( this . clipConfiguration ) ) {
170- if ( Array . isArray ( config . scale ) ) {
171- this . scaleKeyframeBuilder . addLayer ( config . scale ) ;
172- }
173- if ( Array . isArray ( config . opacity ) ) {
174- this . opacityKeyframeBuilder . addLayer ( config . opacity ) ;
175- }
176- if ( Array . isArray ( config . offset ?. x ) ) {
177- this . offsetXKeyframeBuilder . addLayer ( config . offset . x ) ;
178- }
179- if ( Array . isArray ( config . offset ?. y ) ) {
180- this . offsetYKeyframeBuilder . addLayer ( config . offset . y ) ;
181- }
182- if ( Array . isArray ( config . transform ?. rotate ?. angle ) ) {
183- this . rotationKeyframeBuilder . addLayer ( config . transform . rotate . angle ) ;
184- }
185- if ( Array . isArray ( config . transform ?. skew ?. x ) ) {
186- this . skewXKeyframeBuilder . addLayer ( config . transform . skew . x ) ;
187- }
188- if ( Array . isArray ( config . transform ?. skew ?. y ) ) {
189- this . skewYKeyframeBuilder . addLayer ( config . transform . skew . y ) ;
190- }
191- return ;
192- }
147+ // A property that holds neither a number nor tweens, such as an unresolved merge field, plays its default.
148+ const curve = ( value : Keyframe [ ] | number | undefined , fallback : number ) =>
149+ new KeyframeBuilder ( typeof value === "number" || ( Array . isArray ( value ) && value . length > 0 ) ? value : fallback ) ;
150+
151+ this . offsetXKeyframeBuilder = curve ( config . offset ?. x , 0 ) ;
152+ this . offsetYKeyframeBuilder = curve ( config . offset ?. y , 0 ) ;
153+ this . scaleKeyframeBuilder = curve ( config . scale , 1 ) ;
154+ this . opacityKeyframeBuilder = curve ( config . opacity , 1 ) ;
155+ this . rotationKeyframeBuilder = curve ( config . transform ?. rotate ?. angle , 0 ) ;
156+ this . skewXKeyframeBuilder = curve ( config . transform ?. skew ?. x , 0 ) ;
157+ this . skewYKeyframeBuilder = curve ( config . transform ?. skew ?. y , 0 ) ;
193158
194159 // Build resolved clip config for preset builders
195160 const resolvedClipConfig : ResolvedClip = {
@@ -198,59 +163,40 @@ export abstract class Player extends Entity {
198163 length
199164 } ;
200165
201- // Build relative effect keyframes (factors/deltas)
202166 const effectSet = new EffectPresetBuilder ( resolvedClipConfig ) . buildRelative ( this . edit . size , this . getSize ( ) ) ;
203-
204- // Build relative transition keyframes (separate in/out sets)
205167 const transitionSet = new TransitionPresetBuilder ( resolvedClipConfig ) . buildRelative ( ) ;
206168
207- // Add effect layer
208- this . offsetXKeyframeBuilder . addLayer ( effectSet . offsetXKeyframes ) ;
209- this . offsetYKeyframeBuilder . addLayer ( effectSet . offsetYKeyframes ) ;
210- this . scaleKeyframeBuilder . addLayer ( effectSet . scaleKeyframes ) ;
211- this . opacityKeyframeBuilder . addLayer ( effectSet . opacityKeyframes ) ;
212- this . rotationKeyframeBuilder . addLayer ( effectSet . rotationKeyframes ) ;
213-
214- // Add transition-in layer
215- this . offsetXKeyframeBuilder . addLayer ( transitionSet . in . offsetXKeyframes ) ;
216- this . offsetYKeyframeBuilder . addLayer ( transitionSet . in . offsetYKeyframes ) ;
217- this . scaleKeyframeBuilder . addLayer ( transitionSet . in . scaleKeyframes ) ;
218- this . opacityKeyframeBuilder . addLayer ( transitionSet . in . opacityKeyframes ) ;
219- this . rotationKeyframeBuilder . addLayer ( transitionSet . in . rotationKeyframes ) ;
220-
221- // Add transition-out layer
222- this . offsetXKeyframeBuilder . addLayer ( transitionSet . out . offsetXKeyframes ) ;
223- this . offsetYKeyframeBuilder . addLayer ( transitionSet . out . offsetYKeyframes ) ;
224- this . scaleKeyframeBuilder . addLayer ( transitionSet . out . scaleKeyframes ) ;
225- this . opacityKeyframeBuilder . addLayer ( transitionSet . out . opacityKeyframes ) ;
226- this . rotationKeyframeBuilder . addLayer ( transitionSet . out . rotationKeyframes ) ;
227-
228- // Mask keyframes (reveal effect)
229- const maskXKeyframes = [ ...transitionSet . in . maskXKeyframes , ...transitionSet . out . maskXKeyframes ] ;
230- if ( maskXKeyframes . length ) {
231- this . maskXKeyframeBuilder = new KeyframeBuilder ( maskXKeyframes ) ;
169+ // The render applies the effect, then the in transition, then the out, each over the curves left by those before.
170+ for ( const preset of [ effectSet , transitionSet . in , transitionSet . out ] ) {
171+ this . offsetXKeyframeBuilder . addPresetPoints ( preset . offsetXKeyframes ) ;
172+ this . offsetYKeyframeBuilder . addPresetPoints ( preset . offsetYKeyframes ) ;
173+ this . scaleKeyframeBuilder . addPresetPoints ( preset . scaleKeyframes ) ;
174+ this . opacityKeyframeBuilder . addPresetPoints ( preset . opacityKeyframes ) ;
175+ this . rotationKeyframeBuilder . addPresetPoints ( preset . rotationKeyframes ) ;
232176 }
233177
178+ // Curves only transitions drive, resting at a value that leaves the clip untouched.
179+ const transitionCurve = ( rest : number , ...phases : PresetPoint [ ] [ ] ) => {
180+ if ( ! phases . some ( points => points . length ) ) return undefined ;
181+ const builder = new KeyframeBuilder ( rest ) ;
182+ phases . forEach ( points => builder . addPresetPoints ( points ) ) ;
183+ return builder ;
184+ } ;
185+ const { in : transitionIn , out : transitionOut } = transitionSet ;
186+
187+ this . maskXKeyframeBuilder = transitionCurve ( 1 , transitionIn . maskXKeyframes , transitionOut . maskXKeyframes ) ;
188+
234189 // Wipe brightness sweep (wipeLeft/wipeRight) — driven through WipeFilter, not a mask.
235- const brightnessKeyframes = [ ...transitionSet . in . brightnessKeyframes , ...transitionSet . out . brightnessKeyframes ] ;
236- if ( brightnessKeyframes . length ) {
237- this . wipeBrightnessBuilder = new KeyframeBuilder ( brightnessKeyframes ) ;
238- this . wipeInFromRight = transitionSet . in . wipeFromRight ;
239- this . wipeOutFromRight = transitionSet . out . wipeFromRight ;
240- this . wipeOutStart = transitionSet . out . brightnessKeyframes [ 0 ] ?. start ?? Number . POSITIVE_INFINITY ;
241- }
190+ this . wipeBrightnessBuilder = transitionCurve ( - 1 , transitionIn . brightnessKeyframes , transitionOut . brightnessKeyframes ) ;
191+ this . wipeInFromRight = transitionIn . wipeFromRight ;
192+ this . wipeOutFromRight = transitionOut . wipeFromRight ;
193+ this . wipeOutStart = transitionOut . brightnessKeyframes [ 0 ] ?. time ?? Number . POSITIVE_INFINITY ;
242194
243- const blurKeyframes = [ ...transitionSet . in . blurKeyframes , ...transitionSet . out . blurKeyframes ] ;
244- this . zoomBlurBuilder = blurKeyframes . length ? new KeyframeBuilder ( blurKeyframes ) : undefined ;
195+ this . zoomBlurBuilder = transitionCurve ( 0 , transitionIn . blurKeyframes , transitionOut . blurKeyframes ) ;
245196
246- // The render keeps one origin curve: the in shuffle's origin (or the centre) eases to the out shuffle's by the time it starts.
247- const { origin : inOrigin } = transitionSet . in ;
248- const { origin : outOrigin } = transitionSet . out ;
249- const outStart = transitionSet . out . rotationKeyframes [ 0 ] ?. start ?? length ;
250- const originCurve = ( from = 0.5 , to ?: number ) =>
251- new KeyframeBuilder ( to === undefined ? from : [ { from, to, start : 0 , length : outStart , interpolation : "bezier" , easing : "smooth" } ] ) ;
252- this . rotationOrigin =
253- inOrigin || outOrigin ? { x : originCurve ( inOrigin ?. x , outOrigin ?. x ) , y : originCurve ( inOrigin ?. y , outOrigin ?. y ) } : undefined ;
197+ const originX = transitionCurve ( 0.5 , transitionIn . originXKeyframes , transitionOut . originXKeyframes ) ;
198+ const originY = transitionCurve ( 0.5 , transitionIn . originYKeyframes , transitionOut . originYKeyframes ) ;
199+ this . rotationOrigin = originX && originY ? { x : originX , y : originY } : undefined ;
254200 }
255201
256202 private getRotationOriginOffset ( angle : number , scale : Vector ) : Vector {
@@ -562,7 +508,8 @@ export abstract class Player extends Entity {
562508 }
563509
564510 public getOpacity ( ) : number {
565- return this . opacityKeyframeBuilder ?. getValue ( this . getPlaybackTime ( ) ) ?? 1 ;
511+ const opacity = this . opacityKeyframeBuilder ?. getValue ( this . getPlaybackTime ( ) ) ?? 1 ;
512+ return Math . min ( 1 , Math . max ( 0 , opacity ) ) ;
566513 }
567514
568515 public getPosition ( ) : Vector {
0 commit comments