You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Timeline.js 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import { globals } from '../utils/window.js'
  2. import { registerMethods } from '../utils/methods.js'
  3. import Animator from './Animator.js'
  4. import EventTarget from '../types/EventTarget.js'
  5. const makeSchedule = function (runnerInfo) {
  6. const start = runnerInfo.start
  7. const duration = runnerInfo.runner.duration()
  8. const end = start + duration
  9. return { start: start, duration: duration, end: end, runner: runnerInfo.runner }
  10. }
  11. const defaultSource = function () {
  12. const w = globals.window
  13. return (w.performance || w.Date).now()
  14. }
  15. export default class Timeline extends EventTarget {
  16. // Construct a new timeline on the given element
  17. constructor (timeSource = defaultSource) {
  18. super()
  19. this._timeSource = timeSource
  20. // Store the timing variables
  21. this._startTime = 0
  22. this._speed = 1.0
  23. // Determines how long a runner is hold in memory. Can be a dt or true/false
  24. this._persist = 0
  25. // Keep track of the running animations and their starting parameters
  26. this._nextFrame = null
  27. this._paused = true
  28. this._runners = []
  29. this._runnerIds = []
  30. this._lastRunnerId = -1
  31. this._time = 0
  32. this._lastSourceTime = 0
  33. this._lastStepTime = 0
  34. // Make sure that step is always called in class context
  35. this._step = this._stepFn.bind(this, false)
  36. this._stepImmediate = this._stepFn.bind(this, true)
  37. }
  38. active () {
  39. return !!this._nextFrame
  40. }
  41. finish () {
  42. // Go to end and pause
  43. this.time(this.getEndTimeOfTimeline() + 1)
  44. return this.pause()
  45. }
  46. // Calculates the end of the timeline
  47. getEndTime () {
  48. const lastRunnerInfo = this.getLastRunnerInfo()
  49. const lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0
  50. const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
  51. return lastStartTime + lastDuration
  52. }
  53. getEndTimeOfTimeline () {
  54. const endTimes = this._runners.map((i) => i.start + i.runner.duration())
  55. return Math.max(0, ...endTimes)
  56. }
  57. getLastRunnerInfo () {
  58. return this.getRunnerInfoById(this._lastRunnerId)
  59. }
  60. getRunnerInfoById (id) {
  61. return this._runners[this._runnerIds.indexOf(id)] || null
  62. }
  63. pause () {
  64. this._paused = true
  65. return this._continue()
  66. }
  67. persist (dtOrForever) {
  68. if (dtOrForever == null) return this._persist
  69. this._persist = dtOrForever
  70. return this
  71. }
  72. play () {
  73. // Now make sure we are not paused and continue the animation
  74. this._paused = false
  75. return this.updateTime()._continue()
  76. }
  77. reverse (yes) {
  78. const currentSpeed = this.speed()
  79. if (yes == null) return this.speed(-currentSpeed)
  80. const positive = Math.abs(currentSpeed)
  81. return this.speed(yes ? -positive : positive)
  82. }
  83. // schedules a runner on the timeline
  84. schedule (runner, delay, when) {
  85. if (runner == null) {
  86. return this._runners.map(makeSchedule)
  87. }
  88. // The start time for the next animation can either be given explicitly,
  89. // derived from the current timeline time or it can be relative to the
  90. // last start time to chain animations directly
  91. let absoluteStartTime = 0
  92. const endTime = this.getEndTime()
  93. delay = delay || 0
  94. // Work out when to start the animation
  95. if (when == null || when === 'last' || when === 'after') {
  96. // Take the last time and increment
  97. absoluteStartTime = endTime
  98. } else if (when === 'absolute' || when === 'start') {
  99. absoluteStartTime = delay
  100. delay = 0
  101. } else if (when === 'now') {
  102. absoluteStartTime = this._time
  103. } else if (when === 'relative') {
  104. const runnerInfo = this.getRunnerInfoById(runner.id)
  105. if (runnerInfo) {
  106. absoluteStartTime = runnerInfo.start + delay
  107. delay = 0
  108. }
  109. } else if (when === 'with-last') {
  110. const lastRunnerInfo = this.getLastRunnerInfo()
  111. const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
  112. absoluteStartTime = lastStartTime
  113. } else {
  114. throw new Error('Invalid value for the "when" parameter')
  115. }
  116. // Manage runner
  117. runner.unschedule()
  118. runner.timeline(this)
  119. const persist = runner.persist()
  120. const runnerInfo = {
  121. persist: persist === null ? this._persist : persist,
  122. start: absoluteStartTime + delay,
  123. runner
  124. }
  125. this._lastRunnerId = runner.id
  126. this._runners.push(runnerInfo)
  127. this._runners.sort((a, b) => a.start - b.start)
  128. this._runnerIds = this._runners.map(info => info.runner.id)
  129. this.updateTime()._continue()
  130. return this
  131. }
  132. seek (dt) {
  133. return this.time(this._time + dt)
  134. }
  135. source (fn) {
  136. if (fn == null) return this._timeSource
  137. this._timeSource = fn
  138. return this
  139. }
  140. speed (speed) {
  141. if (speed == null) return this._speed
  142. this._speed = speed
  143. return this
  144. }
  145. stop () {
  146. // Go to start and pause
  147. this.time(0)
  148. return this.pause()
  149. }
  150. time (time) {
  151. if (time == null) return this._time
  152. this._time = time
  153. return this._continue(true)
  154. }
  155. // Remove the runner from this timeline
  156. unschedule (runner) {
  157. const index = this._runnerIds.indexOf(runner.id)
  158. if (index < 0) return this
  159. this._runners.splice(index, 1)
  160. this._runnerIds.splice(index, 1)
  161. runner.timeline(null)
  162. return this
  163. }
  164. // Makes sure, that after pausing the time doesn't jump
  165. updateTime () {
  166. if (!this.active()) {
  167. this._lastSourceTime = this._timeSource()
  168. }
  169. return this
  170. }
  171. // Checks if we are running and continues the animation
  172. _continue (immediateStep = false) {
  173. Animator.cancelFrame(this._nextFrame)
  174. this._nextFrame = null
  175. if (immediateStep) return this._stepImmediate()
  176. if (this._paused) return this
  177. this._nextFrame = Animator.frame(this._step)
  178. return this
  179. }
  180. _stepFn (immediateStep = false) {
  181. // Get the time delta from the last time and update the time
  182. const time = this._timeSource()
  183. let dtSource = time - this._lastSourceTime
  184. if (immediateStep) dtSource = 0
  185. const dtTime = this._speed * dtSource + (this._time - this._lastStepTime)
  186. this._lastSourceTime = time
  187. // Only update the time if we use the timeSource.
  188. // Otherwise use the current time
  189. if (!immediateStep) {
  190. // Update the time
  191. this._time += dtTime
  192. this._time = this._time < 0 ? 0 : this._time
  193. }
  194. this._lastStepTime = this._time
  195. this.fire('time', this._time)
  196. // This is for the case that the timeline was seeked so that the time
  197. // is now before the startTime of the runner. That is why we need to set
  198. // the runner to position 0
  199. // FIXME:
  200. // However, resetting in insertion order leads to bugs. Considering the case,
  201. // where 2 runners change the same attribute but in different times,
  202. // resetting both of them will lead to the case where the later defined
  203. // runner always wins the reset even if the other runner started earlier
  204. // and therefore should win the attribute battle
  205. // this can be solved by resetting them backwards
  206. for (let k = this._runners.length; k--;) {
  207. // Get and run the current runner and ignore it if its inactive
  208. const runnerInfo = this._runners[k]
  209. const runner = runnerInfo.runner
  210. // Make sure that we give the actual difference
  211. // between runner start time and now
  212. const dtToStart = this._time - runnerInfo.start
  213. // Dont run runner if not started yet
  214. // and try to reset it
  215. if (dtToStart <= 0) {
  216. runner.reset()
  217. }
  218. }
  219. // Run all of the runners directly
  220. let runnersLeft = false
  221. for (let i = 0, len = this._runners.length; i < len; i++) {
  222. // Get and run the current runner and ignore it if its inactive
  223. const runnerInfo = this._runners[i]
  224. const runner = runnerInfo.runner
  225. let dt = dtTime
  226. // Make sure that we give the actual difference
  227. // between runner start time and now
  228. const dtToStart = this._time - runnerInfo.start
  229. // Dont run runner if not started yet
  230. if (dtToStart <= 0) {
  231. runnersLeft = true
  232. continue
  233. } else if (dtToStart < dt) {
  234. // Adjust dt to make sure that animation is on point
  235. dt = dtToStart
  236. }
  237. if (!runner.active()) continue
  238. // If this runner is still going, signal that we need another animation
  239. // frame, otherwise, remove the completed runner
  240. const finished = runner.step(dt).done
  241. if (!finished) {
  242. runnersLeft = true
  243. // continue
  244. } else if (runnerInfo.persist !== true) {
  245. // runner is finished. And runner might get removed
  246. const endTime = runner.duration() - runner.time() + this._time
  247. if (endTime + runnerInfo.persist < this._time) {
  248. // Delete runner and correct index
  249. runner.unschedule()
  250. --i
  251. --len
  252. }
  253. }
  254. }
  255. // Basically: we continue when there are runners right from us in time
  256. // when -->, and when runners are left from us when <--
  257. if ((runnersLeft && !(this._speed < 0 && this._time === 0)) || (this._runnerIds.length && this._speed < 0 && this._time > 0)) {
  258. this._continue()
  259. } else {
  260. this.pause()
  261. this.fire('finished')
  262. }
  263. return this
  264. }
  265. }
  266. registerMethods({
  267. Element: {
  268. timeline: function (timeline) {
  269. if (timeline == null) {
  270. this._timeline = (this._timeline || new Timeline())
  271. return this._timeline
  272. } else {
  273. this._timeline = timeline
  274. return this
  275. }
  276. }
  277. }
  278. })