diff --git a/lib/animation-scheduler.js b/lib/animation-scheduler.js new file mode 100644 index 0000000..d844c1e --- /dev/null +++ b/lib/animation-scheduler.js @@ -0,0 +1,94 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); + +var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); + +var _createClass2 = require('babel-runtime/helpers/createClass'); + +var _createClass3 = _interopRequireDefault(_createClass2); + +var _spriteTimeline = require('sprite-timeline'); + +var _spriteTimeline2 = _interopRequireDefault(_spriteTimeline); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var isBrowser = typeof document !== 'undefined' && document.documentElement && document.documentElement.contains; + +var AnimationScheduler = function () { + function AnimationScheduler() { + (0, _classCallCheck3.default)(this, AnimationScheduler); + this._animations = []; + } + + (0, _createClass3.default)(AnimationScheduler, [{ + key: 'add', + value: function add(animation) { + this._animations.push(animation); + this.scheduleAnimation(); + } + }, { + key: 'scheduleAnimation', + value: function scheduleAnimation() { + var _this = this; + + if (this.requestId) return; + this.requestId = requestAnimationFrame(function () { + var ntime = _spriteTimeline2.default.nowtime(); + _this.updateFrame(ntime); + }); + } + }, { + key: 'updateFrame', + value: function updateFrame(ntime) { + var nullAnimationCount = 0; + var scheduleNext = false; + for (var i = 0; i < this._animations.length; i++) { + var animation = this._animations[i]; + if (animation === null) { + nullAnimationCount++; + continue; + } + var sprite = animation.target; + + if (isBrowser && sprite.layer && sprite.layer.canvas && !document.documentElement.contains(sprite.layer.canvas)) { + // if dom element has been removed stop animation. + // it usually occurs in single page applications. + animation.cancel(); + this._animations[i] = null; + continue; + } + var playState = animation.getPlayState(ntime); + sprite.attr(animation.getFrame(ntime)); + if (playState === 'idle') { + this._animations[i] = null; + continue; + } + if (playState === 'running') { + scheduleNext = true; + } else if (playState === 'paused' || playState === 'pending' && animation.timeline.getCurrentTime(ntime) < 0) { + // playbackRate < 0 will cause playState reset to pending... + this.add(animation); + } + } + // if there are more than 10 animation is finished we do a cleaning, avoid GC. + if (nullAnimationCount > 10) { + this._animations = this._animations.filter(function (i) { + return i !== null; + }); + } + if (scheduleNext) { + this.requestId = null; + this.scheduleAnimation(); + } + } + }]); + return AnimationScheduler; +}(); + +exports.default = new AnimationScheduler(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6cd8d24..0bf54c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sprite-core", - "version": "2.14.2", + "version": "2.14.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c36f02c..e7cbee0 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "css-line-break": "^1.0.1", "fast-animation-frame": "^0.3.3", "sprite-animator": "^1.10.4", + "sprite-timeline": "^1.9.2", "sprite-math": "^1.0.4", "sprite-utils": "^1.11.4", "svg-path-to-canvas": "^1.9.5" diff --git a/src/animation-scheduler.js b/src/animation-scheduler.js new file mode 100644 index 0000000..b70ddce --- /dev/null +++ b/src/animation-scheduler.js @@ -0,0 +1,63 @@ +import Timeline from 'sprite-timeline'; + +const isBrowser = typeof document !== 'undefined' + && document.documentElement + && document.documentElement.contains; + +class AnimationScheduler { + _animations = new Set() + add(animation) { + animation.ready.then(() => { + this._animations.add(animation); + animation.target.attr(animation.frame); + this.scheduleAnimation(); + }); + } + + delete(animation) { + this._animations.delete(animation); + } + + scheduleAnimation() { + if(this.requestId) return; + this.requestId = requestAnimationFrame(this.updateFrame.bind(this)); + } + + updateFrame() { + const ntime = Timeline.nowtime(); + var scheduleNext = false; + this._animations.forEach(animation => { + + let sprite = animation.target; + + if(isBrowser + && sprite.layer + && sprite.layer.canvas + && !document.documentElement.contains(sprite.layer.canvas)) { + // if dom element has been removed stop animation. + // it usually occurs in single page applications. + animation.cancel(); + return this._animations.delete(animation); + } + const playState = animation.getPlayState(ntime); + sprite.attr(animation.getFrame(ntime)); + if(playState === 'idle') { + return this._animations.delete(animation); + } + if(playState === 'running') { + scheduleNext = true; + } else if(playState === 'paused' || playState === 'pending' && animation.timeline.getCurrentTime(ntime) < 0) { + // playbackRate < 0 will cause playState reset to pending... + this.add(animation) + } + }) + + this.requestId = null; + if(!scheduleNext) { + return; + } + this.scheduleAnimation(); + } +} + +export default new AnimationScheduler(); \ No newline at end of file diff --git a/src/animation.js b/src/animation.js index 5e40a40..9bea2ad 100644 --- a/src/animation.js +++ b/src/animation.js @@ -2,7 +2,9 @@ import {Animator, Effects} from 'sprite-animator'; import {requestAnimationFrame, cancelAnimationFrame} from 'fast-animation-frame'; import {Matrix} from 'sprite-math'; import {parseColor, parseStringTransform} from 'sprite-utils'; - +// to use Timeline.nowtime, fast-animation-frame also implement nowtime, should be extract to use the same code. +import Timeline from 'sprite-timeline'; +import animationScheduler from './animation-scheduler'; const defaultEffect = Effects.default; function arrayEffect(arr1, arr2, p, start, end) { @@ -114,6 +116,13 @@ export default class extends Animator { return super.playState; } + getPlayState(ntime) { + if(!this.target.parent) { + return 'idle'; + } + return super.getPlayState(ntime); + } + get finished() { // set last frame when finished // because while the web page is not focused @@ -128,52 +137,21 @@ export default class extends Animator { finish() { super.finish(); - cancelAnimationFrame(this.requestId); + animationScheduler.delete(this); const sprite = this.target; sprite.attr(this.frame); } play() { - if(!this.target.parent || this.playState === 'running') { - return; - } - - super.play(); + var ntime = Timeline.nowtime(); const sprite = this.target; - - sprite.attr(this.frame); - - const that = this; - this.ready.then(() => { - sprite.attr(that.frame); - that.requestId = requestAnimationFrame(function update() { - const target = that.target; - if(typeof document !== 'undefined' - && document.documentElement - && document.documentElement.contains - && target.layer - && target.layer.canvas - && !document.documentElement.contains(target.layer.canvas)) { - // if dom element has been removed stop animation. - // it usually occurs in single page applications. - that.cancel(); - return; - } - const playState = that.playState; - sprite.attr(that.frame); - if(playState === 'idle') return; - if(playState === 'running') { - that.requestId = requestAnimationFrame(update); - } else if(playState === 'paused' || playState === 'pending' && that.timeline.currentTime < 0) { - // playbackRate < 0 will cause playState reset to pending... - that.ready.then(() => { - sprite.attr(that.frame); - that.requestId = requestAnimationFrame(update); - }); - } - }); - }); + if(!sprite.parent || this.getPlayState(ntime) === 'running') { + return; + } + super.play(ntime); + sprite.attr(this.getFrame(ntime)); + animationScheduler.add(this); } cancel(preserveState = false) {