From 45a9f9efb6a07a7e8769e432a0fcd6348a33ce32 Mon Sep 17 00:00:00 2001 From: SijieCai Date: Mon, 3 Sep 2018 18:41:51 +0800 Subject: [PATCH 1/4] to use a animation scheduler to update all animation at a time, so that to use one nowtime for all calculation, benchmark show for animation sprites larger than 1000 we have 20% more fps gain. this commit depends on two other commmit in sprite-timeline and sprite-animation. --- package-lock.json | 2 +- package.json | 1 + src/animation-scheduler.js | 68 ++++++++++++++++++++++++++++++++++++++ src/animation.js | 51 +++++++++------------------- 4 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 src/animation-scheduler.js 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..113507a --- /dev/null +++ b/src/animation-scheduler.js @@ -0,0 +1,68 @@ +import Timeline from 'sprite-timeline'; + +const isBrowser = typeof document !== 'undefined' + && document.documentElement + && document.documentElement.contains; + +class AnimationScheduler { + _animations = [] + add(animation) { + this._animations.push(animation); + this.scheduleAnimation(); + } + + + scheduleAnimation() { + if(this.requestId) return; + this.requestId = requestAnimationFrame(() => { + var ntime = Timeline.nowtime(); + this.updateFrame(ntime); + }); + } + + updateFrame(ntime) { + var nullAnimationCount = 0; + var scheduleNext = false; + for(var i = 0; i < this._animations.length; i++) { + let animation = this._animations[i]; + if(animation === null) { + nullAnimationCount++; + continue; + } + 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(); + this._animations[i] = null; + continue; + } + const 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(i => i !== null); + } + if(scheduleNext) { + this.requestId = null; + this.scheduleAnimation(); + } + } +} + +export default new AnimationScheduler(); \ No newline at end of file diff --git a/src/animation.js b/src/animation.js index 5e40a40..d243c82 100644 --- a/src/animation.js +++ b/src/animation.js @@ -2,6 +2,8 @@ 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'; const defaultEffect = Effects.default; @@ -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 @@ -134,45 +143,17 @@ export default class extends Animator { } play() { - if(!this.target.parent || this.playState === 'running') { - return; - } - - super.play(); + var ntime = Timeline.nowtime(); const sprite = this.target; + if(!sprite.parent || this.getPlayState(ntime) === 'running') { + return; + } - sprite.attr(this.frame); - - const that = this; + super.play(ntime); + sprite.attr(this.getFrame(ntime)); 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); - }); - } - }); + animationScheduler.add(this); }); } From 60cb2a6edd68e39399bc0ea50b2e9895c589a9ea Mon Sep 17 00:00:00 2001 From: SijieCai Date: Mon, 3 Sep 2018 19:42:20 +0800 Subject: [PATCH 2/4] add missing file --- lib/animation-scheduler.js | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 lib/animation-scheduler.js 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 From 1ae4de789118c2691cb3023bc9780568b31bcd70 Mon Sep 17 00:00:00 2001 From: SijieCai Date: Tue, 4 Sep 2018 11:02:26 +0800 Subject: [PATCH 3/4] fix bug --- src/animation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/animation.js b/src/animation.js index d243c82..cfd5f23 100644 --- a/src/animation.js +++ b/src/animation.js @@ -4,7 +4,7 @@ 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) { From cc937e66d5757b170547fe210fcb8ee7fdc9d5de Mon Sep 17 00:00:00 2001 From: SijieCai Date: Tue, 4 Sep 2018 14:02:06 +0800 Subject: [PATCH 4/4] fix a bug forget to clear the requestAnimationFrame id cause scheduler can't start --- src/animation-scheduler.js | 49 +++++++++++++++++--------------------- src/animation.js | 7 ++---- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/animation-scheduler.js b/src/animation-scheduler.js index 113507a..b70ddce 100644 --- a/src/animation-scheduler.js +++ b/src/animation-scheduler.js @@ -5,30 +5,29 @@ const isBrowser = typeof document !== 'undefined' && document.documentElement.contains; class AnimationScheduler { - _animations = [] + _animations = new Set() add(animation) { - this._animations.push(animation); - this.scheduleAnimation(); + 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(() => { - var ntime = Timeline.nowtime(); - this.updateFrame(ntime); - }); + this.requestId = requestAnimationFrame(this.updateFrame.bind(this)); } - updateFrame(ntime) { - var nullAnimationCount = 0; + updateFrame() { + const ntime = Timeline.nowtime(); var scheduleNext = false; - for(var i = 0; i < this._animations.length; i++) { - let animation = this._animations[i]; - if(animation === null) { - nullAnimationCount++; - continue; - } + this._animations.forEach(animation => { + let sprite = animation.target; if(isBrowser @@ -38,14 +37,12 @@ class AnimationScheduler { // if dom element has been removed stop animation. // it usually occurs in single page applications. animation.cancel(); - this._animations[i] = null; - continue; + return this._animations.delete(animation); } const playState = animation.getPlayState(ntime); sprite.attr(animation.getFrame(ntime)); if(playState === 'idle') { - this._animations[i] = null; - continue; + return this._animations.delete(animation); } if(playState === 'running') { scheduleNext = true; @@ -53,15 +50,13 @@ class AnimationScheduler { // playbackRate < 0 will cause playState reset to pending... this.add(animation) } + }) + + this.requestId = null; + if(!scheduleNext) { + return; } - // if there are more than 10 animation is finished we do a cleaning, avoid GC. - if(nullAnimationCount > 10) { - this._animations = this._animations.filter(i => i !== null); - } - if(scheduleNext) { - this.requestId = null; - this.scheduleAnimation(); - } + this.scheduleAnimation(); } } diff --git a/src/animation.js b/src/animation.js index cfd5f23..9bea2ad 100644 --- a/src/animation.js +++ b/src/animation.js @@ -137,7 +137,7 @@ export default class extends Animator { finish() { super.finish(); - cancelAnimationFrame(this.requestId); + animationScheduler.delete(this); const sprite = this.target; sprite.attr(this.frame); } @@ -149,12 +149,9 @@ export default class extends Animator { if(!sprite.parent || this.getPlayState(ntime) === 'running') { return; } - super.play(ntime); sprite.attr(this.getFrame(ntime)); - this.ready.then(() => { - animationScheduler.add(this); - }); + animationScheduler.add(this); } cancel(preserveState = false) {