@@ -10,109 +10,42 @@ import { CurveInterpolator } from "./curve-interpolator";
1010 */
1111export const TIME_EPSILON = 1e-6 ;
1212
13- export class KeyframeBuilder {
14- private readonly property : NumericKeyframe [ ] ;
15- private readonly length : number ;
16-
17- private readonly cubicBuilder : CurveInterpolator ;
13+ /** A curve point as the render keeps them: a segment takes its right point's interpolation and the handles facing into it. */
14+ type Point = { time : number ; value : number ; interpolation : NumericKeyframe [ "interpolation" ] ; handleLeft : number [ ] ; handleRight : number [ ] } ;
1815
19- private cachedIndex = 0 ;
16+ const curves = new CurveInterpolator ( ) ;
17+ // The render gives every point its default curve's handles unless a tween sets them.
18+ const [ DEFAULT_RIGHT , DEFAULT_LEFT ] = curves . getHandles ( "smooth" ) ;
2019
21- constructor ( value : AnimationKeyframe [ ] | number , length : number , initialValue = 0 ) {
22- this . property = this . createKeyframes ( value , length , initialValue ) ;
23- this . length = length ;
20+ export class KeyframeBuilder {
21+ private readonly points : Point [ ] ;
2422
25- this . cubicBuilder = new CurveInterpolator ( ) ;
23+ constructor ( value : AnimationKeyframe [ ] | number ) {
24+ this . points = this . createPoints ( value ) ;
2625 }
2726
2827 public getValue ( time : number ) : number {
29- const keyframe = this . findKeyframe ( time ) ;
30- if ( ! keyframe ) {
31- if ( this . property . length > 0 ) {
32- if ( time >= this . length ) return this . property [ this . property . length - 1 ] . to ;
33- if ( time < 0 ) return this . property [ 0 ] . from ;
34- }
35- return 1 ;
36- }
37-
38- const progress = ( time - keyframe . start ) / keyframe . length ;
39- switch ( keyframe . interpolation ) {
28+ const index = this . points . findLastIndex ( point => point . time <= time ) ;
29+ const left = this . points [ index ] ;
30+ const right = this . points [ index + 1 ] ;
31+ // Before the first point and after the last the curve holds that point's value; with no usable tween it reads 1.
32+ if ( ! left || ! right ) return ( left ?? right ) ?. value ?? 1 ;
33+
34+ const progress = ( time - left . time ) / ( right . time - left . time ) ;
35+ switch ( right . interpolation ) {
4036 case "bezier" :
41- return this . cubicBuilder . getValue ( keyframe . from , keyframe . to , progress , keyframe . easing ) ;
37+ return curves . getValue ( left . value , right . value , progress , [ left . handleRight , right . handleLeft ] ) ;
4238 case "constant" :
43- return keyframe . from ;
39+ return left . value ;
4440 case "linear" :
4541 default :
46- return keyframe . from + ( keyframe . to - keyframe . from ) * progress ;
47- }
48- }
49-
50- private findKeyframe ( time : number ) : NumericKeyframe | undefined {
51- const props = this . property ;
52- if ( props . length === 0 ) return undefined ;
53-
54- const cached = props [ this . cachedIndex ] ;
55- if ( cached ) {
56- const cachedEnd = cached . start + cached . length ;
57- if ( Number . isFinite ( cachedEnd ) && time >= cached . start && time < cachedEnd ) {
58- return cached ;
59- }
60- }
61-
62- const nextIdx = this . cachedIndex + 1 ;
63- if ( nextIdx < props . length ) {
64- const next = props [ nextIdx ] ;
65- const nextEnd = next . start + next . length ;
66- if ( Number . isFinite ( nextEnd ) && time >= next . start && time < nextEnd ) {
67- this . cachedIndex = nextIdx ;
68- return next ;
69- }
70- }
71-
72- const prevIdx = this . cachedIndex - 1 ;
73- if ( prevIdx >= 0 ) {
74- const prev = props [ prevIdx ] ;
75- const prevEnd = prev . start + prev . length ;
76- if ( Number . isFinite ( prevEnd ) && time >= prev . start && time < prevEnd ) {
77- this . cachedIndex = prevIdx ;
78- return prev ;
79- }
80- }
81-
82- const idx = this . binarySearchKeyframe ( time ) ;
83- if ( idx !== - 1 ) {
84- this . cachedIndex = idx ;
85- return props [ idx ] ;
86- }
87-
88- return undefined ;
89- }
90-
91- private binarySearchKeyframe ( time : number ) : number {
92- const props = this . property ;
93- let low = 0 ;
94- let high = props . length - 1 ;
95-
96- while ( low <= high ) {
97- const mid = Math . floor ( ( low + high ) / 2 ) ;
98- const kf = props [ mid ] ;
99- const end = kf . start + kf . length ;
100-
101- if ( ! Number . isFinite ( end ) || time >= end ) {
102- low = mid + 1 ;
103- } else if ( time < kf . start ) {
104- high = mid - 1 ;
105- } else {
106- return mid ;
107- }
42+ return left . value + ( right . value - left . value ) * progress ;
10843 }
109-
110- return - 1 ;
11144 }
11245
113- private createKeyframes ( value : AnimationKeyframe [ ] | number , length : number , initialValue = 0 ) : NumericKeyframe [ ] {
46+ private createPoints ( value : AnimationKeyframe [ ] | number ) : Point [ ] {
11447 if ( typeof value === "number" ) {
115- return [ { start : 0 , length , from : value , to : value } ] ;
48+ return [ { time : 0 , value , interpolation : "constant" , handleLeft : DEFAULT_LEFT , handleRight : DEFAULT_RIGHT } ] ;
11649 }
11750
11851 if ( ! value . length ) {
@@ -124,12 +57,19 @@ export class KeyframeBuilder {
12457 this . normaliseAdjacentBoundaries ( normalizedKeyframes ) ;
12558 this . validateKeyframes ( normalizedKeyframes ) ;
12659
127- return this . insertFillerKeyframes ( normalizedKeyframes , length , initialValue ) ;
60+ // Adjacent tweens each keep a point at their shared time, so each keeps its own curve.
61+ return normalizedKeyframes . flatMap ( ( { start, length, from, to, interpolation, easing } ) => {
62+ const [ handleRight , handleLeft ] = interpolation === "bezier" ? curves . getHandles ( easing ) : [ DEFAULT_RIGHT , DEFAULT_LEFT ] ;
63+ return [
64+ { time : start , value : from , interpolation, handleLeft : DEFAULT_LEFT , handleRight } ,
65+ { time : start + length , value : to , interpolation, handleLeft, handleRight : DEFAULT_RIGHT }
66+ ] ;
67+ } ) ;
12868 }
12969
13070 private createNormalizedKeyframes ( keyframes : AnimationKeyframe [ ] ) : NumericKeyframe [ ] {
13171 return keyframes
132- . filter ( ( kf ) : kf is AnimationKeyframe & { start : number ; length : number } => typeof kf . start === "number" && typeof kf . length === "number" )
72+ . filter ( ( kf ) : kf is AnimationKeyframe & { start : number ; length : number } => Number . isFinite ( kf . start ) && Number . isFinite ( kf . length ) )
13373 . sort ( ( a , b ) => a . start - b . start )
13474 . map ( keyframe => ( {
13575 start : keyframe . start ,
@@ -166,43 +106,4 @@ export class KeyframeBuilder {
166106 }
167107 }
168108 }
169-
170- private insertFillerKeyframes ( keyframes : NumericKeyframe [ ] , length : number , initialValue = 0 ) : NumericKeyframe [ ] {
171- const updatedKeyframes : NumericKeyframe [ ] = [ ] ;
172-
173- for ( let i = 0 ; i < keyframes . length ; i += 1 ) {
174- const current = keyframes [ i ] ;
175- const next = keyframes [ i + 1 ] ;
176-
177- const shouldFillStart = i === 0 && current . start > 0 ;
178- if ( shouldFillStart ) {
179- const fillerKeyframe : NumericKeyframe = { start : 0 , length : current . start , from : initialValue , to : current . from } ;
180- updatedKeyframes . push ( fillerKeyframe ) ;
181- }
182-
183- updatedKeyframes . push ( current ) ;
184-
185- if ( ! next ) {
186- const shouldFillEnd = length - ( current . start + current . length ) > 0 ;
187- if ( shouldFillEnd ) {
188- const currentStart = current . start + current . length ;
189- const fillerKeyframe : NumericKeyframe = { start : currentStart , length : length - currentStart , from : current . to , to : current . to } ;
190-
191- updatedKeyframes . push ( fillerKeyframe ) ;
192- }
193-
194- break ;
195- }
196-
197- const shouldFillMiddle = next . start - ( current . start + current . length ) > 0 ;
198- if ( shouldFillMiddle ) {
199- const fillerStart = current . start + current . length ;
200- const fillerLength = next . start - fillerStart ;
201- const fillerKeyframe : NumericKeyframe = { start : fillerStart , length : fillerLength , from : current . to , to : next . from } ;
202- updatedKeyframes . push ( fillerKeyframe ) ;
203- }
204- }
205-
206- return updatedKeyframes ;
207- }
208109}
0 commit comments