Source: core/ticker/Ticker.js

core/ticker/Ticker.js

  1. import settings from '../settings';
  2. import { UPDATE_PRIORITY } from '../const';
  3. import TickerListener from './TickerListener';
  4. //引入小程序补丁
  5. import { requestAnimationFrame, cancelAnimationFrame, performance } from '@ali/pixi-miniprogram-adapter';
  6. /**
  7. * A Ticker class that runs an update loop that other objects listen to.
  8. * This class is composed around listeners
  9. * meant for execution on the next requested animation frame.
  10. * Animation frames are requested only when necessary,
  11. * e.g. When the ticker is started and the emitter has listeners.
  12. *
  13. * @class
  14. * @memberof PIXI.ticker
  15. */
  16. export default class Ticker
  17. {
  18. /**
  19. *
  20. */
  21. constructor()
  22. {
  23. /**
  24. * The first listener. All new listeners added are chained on this.
  25. * @private
  26. * @type {TickerListener}
  27. */
  28. this._head = new TickerListener(null, null, Infinity);
  29. /**
  30. * Internal current frame request ID
  31. * @private
  32. */
  33. this._requestId = null;
  34. /**
  35. * Internal value managed by minFPS property setter and getter.
  36. * This is the maximum allowed milliseconds between updates.
  37. * @private
  38. */
  39. this._maxElapsedMS = 100;
  40. /**
  41. * Whether or not this ticker should invoke the method
  42. * {@link PIXI.ticker.Ticker#start} automatically
  43. * when a listener is added.
  44. *
  45. * @member {boolean}
  46. * @default false
  47. */
  48. this.autoStart = false;
  49. /**
  50. * Scalar time value from last frame to this frame.
  51. * This value is capped by setting {@link PIXI.ticker.Ticker#minFPS}
  52. * and is scaled with {@link PIXI.ticker.Ticker#speed}.
  53. * **Note:** The cap may be exceeded by scaling.
  54. *
  55. * @member {number}
  56. * @default 1
  57. */
  58. this.deltaTime = 1;
  59. /**
  60. * Time elapsed in milliseconds from last frame to this frame.
  61. * Opposed to what the scalar {@link PIXI.ticker.Ticker#deltaTime}
  62. * is based, this value is neither capped nor scaled.
  63. * If the platform supports DOMHighResTimeStamp,
  64. * this value will have a precision of 1 µs.
  65. * Defaults to target frame time
  66. *
  67. * @member {number}
  68. * @default 16.66
  69. */
  70. this.elapsedMS = 1 / settings.TARGET_FPMS;
  71. /**
  72. * The last time {@link PIXI.ticker.Ticker#update} was invoked.
  73. * This value is also reset internally outside of invoking
  74. * update, but only when a new animation frame is requested.
  75. * If the platform supports DOMHighResTimeStamp,
  76. * this value will have a precision of 1 µs.
  77. *
  78. * @member {number}
  79. * @default -1
  80. */
  81. this.lastTime = -1;
  82. /**
  83. * Factor of current {@link PIXI.ticker.Ticker#deltaTime}.
  84. * @example
  85. * // Scales ticker.deltaTime to what would be
  86. * // the equivalent of approximately 120 FPS
  87. * ticker.speed = 2;
  88. *
  89. * @member {number}
  90. * @default 1
  91. */
  92. this.speed = 1;
  93. /**
  94. * Whether or not this ticker has been started.
  95. * `true` if {@link PIXI.ticker.Ticker#start} has been called.
  96. * `false` if {@link PIXI.ticker.Ticker#stop} has been called.
  97. * While `false`, this value may change to `true` in the
  98. * event of {@link PIXI.ticker.Ticker#autoStart} being `true`
  99. * and a listener is added.
  100. *
  101. * @member {boolean}
  102. * @default false
  103. */
  104. this.started = false;
  105. /**
  106. * Internal tick method bound to ticker instance.
  107. * This is because in early 2015, Function.bind
  108. * is still 60% slower in high performance scenarios.
  109. * Also separating frame requests from update method
  110. * so listeners may be called at any time and with
  111. * any animation API, just invoke ticker.update(time).
  112. *
  113. * @private
  114. * @param {number} time - Time since last tick.
  115. */
  116. this._tick = (time) =>
  117. {
  118. this._requestId = null;
  119. if (this.started)
  120. {
  121. // Invoke listeners now
  122. this.update(time);
  123. // Listener side effects may have modified ticker state.
  124. if (this.started && this._requestId === null && this._head.next)
  125. {
  126. this._requestId = requestAnimationFrame(this._tick);
  127. }
  128. }
  129. };
  130. }
  131. /**
  132. * Conditionally requests a new animation frame.
  133. * If a frame has not already been requested, and if the internal
  134. * emitter has listeners, a new frame is requested.
  135. *
  136. * @private
  137. */
  138. _requestIfNeeded()
  139. {
  140. if (this._requestId === null && this._head.next)
  141. {
  142. // ensure callbacks get correct delta
  143. this.lastTime = performance.now();
  144. this._requestId = requestAnimationFrame(this._tick);
  145. }
  146. }
  147. /**
  148. * Conditionally cancels a pending animation frame.
  149. *
  150. * @private
  151. */
  152. _cancelIfNeeded()
  153. {
  154. if (this._requestId !== null)
  155. {
  156. cancelAnimationFrame(this._requestId);
  157. this._requestId = null;
  158. }
  159. }
  160. /**
  161. * Conditionally requests a new animation frame.
  162. * If the ticker has been started it checks if a frame has not already
  163. * been requested, and if the internal emitter has listeners. If these
  164. * conditions are met, a new frame is requested. If the ticker has not
  165. * been started, but autoStart is `true`, then the ticker starts now,
  166. * and continues with the previous conditions to request a new frame.
  167. *
  168. * @private
  169. */
  170. _startIfPossible()
  171. {
  172. if (this.started)
  173. {
  174. this._requestIfNeeded();
  175. }
  176. else if (this.autoStart)
  177. {
  178. this.start();
  179. }
  180. }
  181. /**
  182. * Register a handler for tick events. Calls continuously unless
  183. * it is removed or the ticker is stopped.
  184. *
  185. * @param {Function} fn - The listener function to be added for updates
  186. * @param {Function} [context] - The listener context
  187. * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting
  188. * @returns {PIXI.ticker.Ticker} This instance of a ticker
  189. */
  190. add(fn, context, priority = UPDATE_PRIORITY.NORMAL)
  191. {
  192. return this._addListener(new TickerListener(fn, context, priority));
  193. }
  194. /**
  195. * Add a handler for the tick event which is only execute once.
  196. *
  197. * @param {Function} fn - The listener function to be added for one update
  198. * @param {Function} [context] - The listener context
  199. * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting
  200. * @returns {PIXI.ticker.Ticker} This instance of a ticker
  201. */
  202. addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL)
  203. {
  204. return this._addListener(new TickerListener(fn, context, priority, true));
  205. }
  206. /**
  207. * Internally adds the event handler so that it can be sorted by priority.
  208. * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run
  209. * before the rendering.
  210. *
  211. * @private
  212. * @param {TickerListener} listener - Current listener being added.
  213. * @returns {PIXI.ticker.Ticker} This instance of a ticker
  214. */
  215. _addListener(listener)
  216. {
  217. // For attaching to head
  218. let current = this._head.next;
  219. let previous = this._head;
  220. // Add the first item
  221. if (!current)
  222. {
  223. listener.connect(previous);
  224. }
  225. else
  226. {
  227. // Go from highest to lowest priority
  228. while (current)
  229. {
  230. if (listener.priority > current.priority)
  231. {
  232. listener.connect(previous);
  233. break;
  234. }
  235. previous = current;
  236. current = current.next;
  237. }
  238. // Not yet connected
  239. if (!listener.previous)
  240. {
  241. listener.connect(previous);
  242. }
  243. }
  244. this._startIfPossible();
  245. return this;
  246. }
  247. /**
  248. * Removes any handlers matching the function and context parameters.
  249. * If no handlers are left after removing, then it cancels the animation frame.
  250. *
  251. * @param {Function} fn - The listener function to be removed
  252. * @param {Function} [context] - The listener context to be removed
  253. * @returns {PIXI.ticker.Ticker} This instance of a ticker
  254. */
  255. remove(fn, context)
  256. {
  257. let listener = this._head.next;
  258. while (listener)
  259. {
  260. // We found a match, lets remove it
  261. // no break to delete all possible matches
  262. // incase a listener was added 2+ times
  263. if (listener.match(fn, context))
  264. {
  265. listener = listener.destroy();
  266. }
  267. else
  268. {
  269. listener = listener.next;
  270. }
  271. }
  272. if (!this._head.next)
  273. {
  274. this._cancelIfNeeded();
  275. }
  276. return this;
  277. }
  278. /**
  279. * Starts the ticker. If the ticker has listeners
  280. * a new animation frame is requested at this point.
  281. */
  282. start()
  283. {
  284. if (!this.started)
  285. {
  286. this.started = true;
  287. this._requestIfNeeded();
  288. }
  289. }
  290. /**
  291. * Stops the ticker. If the ticker has requested
  292. * an animation frame it is canceled at this point.
  293. */
  294. stop()
  295. {
  296. if (this.started)
  297. {
  298. this.started = false;
  299. this._cancelIfNeeded();
  300. }
  301. }
  302. /**
  303. * Destroy the ticker and don't use after this. Calling
  304. * this method removes all references to internal events.
  305. */
  306. destroy()
  307. {
  308. this.stop();
  309. let listener = this._head.next;
  310. while (listener)
  311. {
  312. listener = listener.destroy(true);
  313. }
  314. this._head.destroy();
  315. this._head = null;
  316. }
  317. /**
  318. * Triggers an update. An update entails setting the
  319. * current {@link PIXI.ticker.Ticker#elapsedMS},
  320. * the current {@link PIXI.ticker.Ticker#deltaTime},
  321. * invoking all listeners with current deltaTime,
  322. * and then finally setting {@link PIXI.ticker.Ticker#lastTime}
  323. * with the value of currentTime that was provided.
  324. * This method will be called automatically by animation
  325. * frame callbacks if the ticker instance has been started
  326. * and listeners are added.
  327. *
  328. * @param {number} [currentTime=performance.now()] - the current time of execution
  329. */
  330. update(currentTime = performance.now())
  331. {
  332. let elapsedMS;
  333. // If the difference in time is zero or negative, we ignore most of the work done here.
  334. // If there is no valid difference, then should be no reason to let anyone know about it.
  335. // A zero delta, is exactly that, nothing should update.
  336. //
  337. // The difference in time can be negative, and no this does not mean time traveling.
  338. // This can be the result of a race condition between when an animation frame is requested
  339. // on the current JavaScript engine event loop, and when the ticker's start method is invoked
  340. // (which invokes the internal _requestIfNeeded method). If a frame is requested before
  341. // _requestIfNeeded is invoked, then the callback for the animation frame the ticker requests,
  342. // can receive a time argument that can be less than the lastTime value that was set within
  343. // _requestIfNeeded. This difference is in microseconds, but this is enough to cause problems.
  344. //
  345. // This check covers this browser engine timing issue, as well as if consumers pass an invalid
  346. // currentTime value. This may happen if consumers opt-out of the autoStart, and update themselves.
  347. if (currentTime > this.lastTime)
  348. {
  349. // Save uncapped elapsedMS for measurement
  350. elapsedMS = this.elapsedMS = currentTime - this.lastTime;
  351. // cap the milliseconds elapsed used for deltaTime
  352. if (elapsedMS > this._maxElapsedMS)
  353. {
  354. elapsedMS = this._maxElapsedMS;
  355. }
  356. this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed;
  357. // Cache a local reference, in-case ticker is destroyed
  358. // during the emit, we can still check for head.next
  359. const head = this._head;
  360. // Invoke listeners added to internal emitter
  361. let listener = head.next;
  362. while (listener)
  363. {
  364. listener = listener.emit(this.deltaTime);
  365. }
  366. if (!head.next)
  367. {
  368. this._cancelIfNeeded();
  369. }
  370. }
  371. else
  372. {
  373. this.deltaTime = this.elapsedMS = 0;
  374. }
  375. this.lastTime = currentTime;
  376. }
  377. /**
  378. * The frames per second at which this ticker is running.
  379. * The default is approximately 60 in most modern browsers.
  380. * **Note:** This does not factor in the value of
  381. * {@link PIXI.ticker.Ticker#speed}, which is specific
  382. * to scaling {@link PIXI.ticker.Ticker#deltaTime}.
  383. *
  384. * @member {number}
  385. * @readonly
  386. */
  387. get FPS()
  388. {
  389. return 1000 / this.elapsedMS;
  390. }
  391. /**
  392. * Manages the maximum amount of milliseconds allowed to
  393. * elapse between invoking {@link PIXI.ticker.Ticker#update}.
  394. * This value is used to cap {@link PIXI.ticker.Ticker#deltaTime},
  395. * but does not effect the measured value of {@link PIXI.ticker.Ticker#FPS}.
  396. * When setting this property it is clamped to a value between
  397. * `0` and `PIXI.settings.TARGET_FPMS * 1000`.
  398. *
  399. * @member {number}
  400. * @default 10
  401. */
  402. get minFPS()
  403. {
  404. return 1000 / this._maxElapsedMS;
  405. }
  406. set minFPS(fps) // eslint-disable-line require-jsdoc
  407. {
  408. // Clamp: 0 to TARGET_FPMS
  409. const minFPMS = Math.min(Math.max(0, fps) / 1000, settings.TARGET_FPMS);
  410. this._maxElapsedMS = 1 / minFPMS;
  411. }
  412. }