-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclass-timeline.js
More file actions
263 lines (229 loc) · 7.83 KB
/
class-timeline.js
File metadata and controls
263 lines (229 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/**
* The timeline runs all the actions. This is the only object that should be
* calling the animation library / AnimeJS
*/
import {
animate, createTimeline, stagger, utils,
} from '~interact/editor/animejs'
import { Action } from './class-action'
// The timeline contains all dyanmic data, replaces the "helpers" object in the old system.
export class Timeline {
constructor( timelineData, interaction ) {
this.timelineData = timelineData
this.interaction = interaction
this.type = this.getRunner().getTimelineType( interaction )
if ( timelineData ) {
this.actions = this.timelineData.actions.map( actionData => {
return new Action( actionData, this )
} )
// If reset at start is set to true, then always reset the animation when it's starting.
this._resetAtStart = !! timelineData.reset
this._onceOnly = !! timelineData.onceOnly
this._actionStarts = {}
// Only track played triggers if onceOnly is enabled.
if ( this._onceOnly && ! this.timelineData._playedTriggers ) {
this.timelineData._playedTriggers = new Set()
}
}
this._targets = []
}
// If true, then the timeline will print out debug logs while running.
get debugMode() {
return this.timelineData.debug
}
get hasActions() {
return this.actions.length > 0
}
createInstance( options ) {
// If triggered only once, then we don't create anymore animations.
const currentTrigger = this.interaction.getCurrentTrigger()
if ( this.getRunner().isFrontend && currentTrigger && this._onceOnly ) {
if ( this.timelineData._playedTriggers.has( currentTrigger ) ) {
return null
}
this.timelineData._playedTriggers.add( currentTrigger )
}
// We have to empty the promises here because we are creating a new timeline.
// This is to prevent the promises from the previous timeline from affecting the new one.
this._funcPromises = {}
const propsToPass = {}
if ( this.type === 'percentage' ) {
propsToPass.duration = 100 // 100% so it's easier to compute the actions later.
}
const timelineArgs = {
loop: this.timelineData.loop ? ( this.timelineData.loopTimes || true ) : ( this.timelineData.alternate ? 1 : false ),
loopDelay: this.timelineData.loop ? ( this.timelineData.loopDelay * 1000 || 0 ) : undefined,
reversed: this.timelineData.reverse,
alternate: this.timelineData.alternate,
...options,
...propsToPass,
}
const frontendArgs = {
onBegin: () => {
// Force remove transitions - since it affects our animations.
const addClass = el => el.style?.setProperty( 'transition', 'none', 'important' )
this._targets.forEach( el => {
if ( Array.isArray( el ) ) {
el.forEach( addClass )
} else {
addClass( el )
}
} )
if ( this.debugMode ) {
// eslint-disable-next-line no-console
console.debug( `[Interactions Debug] Timeline started. Triggered by:`, this.interaction.getCurrentTrigger() )
}
},
onComplete: () => {
// Bring back transitions.
const removeClass = el => el.style?.removeProperty( 'transition' )
this._targets.forEach( el => {
if ( Array.isArray( el ) ) {
el.forEach( removeClass )
} else {
removeClass( el )
}
} )
if ( this.debugMode ) {
// eslint-disable-next-line no-console
console.debug( `[Interactions Debug] Timeline completed` )
}
},
onLoop: () => {
if ( this.debugMode ) {
this._debugLoopCount = ( this._debugLoopCount || 0 ) + 1
const ordinalCount = this._debugLoopCount === 1 ? '1st' : this._debugLoopCount === 2 ? '2nd' : this._debugLoopCount === 3 ? '3rd' : `${ this._debugLoopCount }th`
// eslint-disable-next-line no-console
console.debug( `[Interactions Debug] Looping: ${ ordinalCount } time` )
}
},
}
const animation = createTimeline( {
...timelineArgs,
autoplay: false,
defaults: {
ease: 'outCirc',
autoplay: false,
},
...( this.getRunner().isFrontend ? frontendArgs : {} ),
} )
// We need to add this for force the animation to play from 0-100. Or
// else if we have an action that doesn't end at 100, our animation will
// stretch the last action to be 100
if ( this.type === 'percentage' ) {
animation.add( { duration: 100 }, 0 )
}
// Store all our targets here so we can reference them and destroy them later.
this._targets = []
// For percentage based animations, we need to store the start time of each
this._actionStarts = {}
// Data that's acquired by actions and can be used by other actions.
this._dynamicData = {}
if ( this.debugMode ) {
// eslint-disable-next-line no-console
console.debug( '[Interactions Debug] Interaction starting: ', this.interaction.data )
// eslint-disable-next-line no-console
console.debug( '[Interactions Debug] Initializing timeline with arguments:', timelineArgs )
}
this._animation = animation
// Initialize actions.
this.actions.forEach( action => {
const actionConfig = this.getRunner().getActionConfig( action.type )
if ( actionConfig ) {
actionConfig.initAction( action )
}
} )
return this
}
getRunner() {
return this.interaction.getRunner()
}
getProvidedValue( name ) {
if ( this._dynamicData[ name ] ) {
return this._dynamicData[ name ]
}
return undefined
}
getAllProvidedData() {
return this._dynamicData
}
provideData( data ) {
this._dynamicData = { ...this._dynamicData, ...data }
}
// Provides the start time for the timeline, uses stagger if present.
getStartTime( start, timing ) {
if ( ! timing.stagger ) {
return start
}
return stagger( timing.stagger * 1000, { start } )
}
getProgress() {
if ( this._animation ) {
return this._animation.currentTime / this._animation.duration
}
return 0
}
play() {
if ( this._animation ) {
this._animation.reset()
this._animation.play()
}
}
seek( time ) {
if ( this._animation ) {
this._animation.seek( time )
}
}
/**
* Seeks the animation to a percentage.
*
* @param {float} percentage Percentage from 0 to 1
* @param {int} smoothness If 0, the animation will seek directly to the
* percentage. If not 0, the animation will seek to the percentage with a
* smoothness effect. Default smoothness in the frontend is 200.
*/
seekPercentage( percentage, smoothness = 0 ) {
if ( this._animation ) {
if ( smoothness ) {
const smoothnessInt = parseInt( smoothness, 10 )
// Make it so the farther the percentage is from the current progress, the longer the duration.
let smoothDuration = utils.lerp( 0, 1, Math.abs( percentage - this._animation.progress ) )
smoothDuration = smoothDuration * smoothDuration
// Get the duration taking into account the smoothness.
smoothDuration = utils.interpolate( smoothnessInt, smoothnessInt + 300, smoothDuration )
// The end duration would be shorter then smaller the percentage is from the current progress.
animate( this._animation, {
progress: percentage,
duration: smoothDuration,
ease: 'outQuad',
composition: this.getRunner().isFrontend ? 'blend' : 'replace',
} )
} else {
this._animation.seek( this._animation.duration * percentage )
}
}
}
pause() {
if ( this._animation ) {
this._animation.pause()
}
}
destroy( reset = true ) {
if ( this.timelineData.loop && this.timelineData.onceOnly ) {
return
}
if ( this._animation ) {
// We need to check onceOnly because we don't want to reset the
// animation if it will only play once.
const resetAtStart = this._resetAtStart && ! this._onceOnly
this._animation.pause()
if ( reset || resetAtStart ) {
this._animation.revert()
} else {
this._animation.cancel()
}
// DEV NOTE: Comment this out because it makes animations skip when transitioning from hover in to hover out, and stops "play once" from working.
// utils.cleanInlineStyles( this._animation ) // Removes all inline styles.
}
}
}