diff --git a/bower.json b/bower.json index 57543f66..71f804f9 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "pixi-animate", - "version": "0.5.0", + "version": "0.5.1", "main": "dist/pixi-animate.min.js", "dependencies": { @@ -18,4 +18,4 @@ "package.json", ".gitignore" ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index ca15300a..5c5f98cc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pixi-animate", "description": "PIXI plugin for the PixiAnimate Extension", - "version": "0.5.0", + "version": "0.5.1", "main": "dist/pixi-animate.min.js", "author": "Matt Karl ", "dependencies": { diff --git a/src/animate/MovieClip.js b/src/animate/MovieClip.js index ad9d1f11..69719356 100644 --- a/src/animate/MovieClip.js +++ b/src/animate/MovieClip.js @@ -339,6 +339,51 @@ class MovieClip extends Container { return this._totalFrames; } + /** + * Extend the timeline to the last frame. + * @method _autoExtend + * @private + * @param {int} endFrame + */ + _autoExtend(endFrame) { + if (this._totalFrames < endFrame) { + this._totalFrames = endFrame; + } + } + + /** + * Convert values of properties + * @method _parseProperties + * @private + * @param {Object} properties + */ + _parseProperties(properties) { + // Convert any string colors to uints + if (typeof properties.t === 'string') { + properties.t = utils.hexToUint(properties.t); + } else if (typeof properties.v === 'number') { + properties.v = !!properties.v; + } + } + + /** + * Get a timeline for a child, synced timeline. + * @method _getChildTimeline + * @private + * @param {PIXI.animate.MovieClip} instance + * @return {PIXI.animate.Timeline} + */ + _getChildTimeline(instance) { + for (let i = this._timelines.length - 1; i >= 0; --i) { + if (this._timelines[i].target === instance) { + return this._timelines[i]; + } + } + let timeline = new Timeline(instance); + this._timelines.push(timeline); + return timeline; + } + /** * Add mask or masks * @method addTimedMask @@ -348,7 +393,7 @@ class MovieClip extends Container { */ addTimedMask(instance, keyframes) { for (let i in keyframes) { - this.addTween(instance, { + this.addKeyframe(instance, { m: keyframes[i] }, parseInt(i, 10)); } @@ -370,17 +415,28 @@ class MovieClip extends Container { } /** - * Alias for method `addTween` - * @method tw + * Add a tween to the clip + * @method addTween + * @param {PIXI.DisplayObject} instance The clip to tween + * @param {Object} properties The property or property to tween + * @param {int} startFrame The frame to start tweening + * @param {int} [duration=0] Number of frames to tween. If 0, then the properties are set + * with no tweening. + * @param {Function} [ease] An optional easing function that takes the tween time from 0-1. * @return {PIXI.animate.MovieClip} */ - tw(instance, properties, startFrame, duration, ease) { - return this.addTween(instance, properties, startFrame, duration, ease); + addTween(instance, properties, startFrame, duration, ease) { + + let timeline = this._getChildTimeline(instance); + this._parseProperties(properties); + timeline.addTween(properties, startFrame, duration, ease); + this._autoExtend(startFrame + duration); + return this; } /** * Add a tween to the clip - * @method addTween + * @method addKeyframe * @param {PIXI.DisplayObject} instance The clip to tween * @param {Object} properties The property or property to tween * @param {int} startFrame The frame to start tweening @@ -389,36 +445,12 @@ class MovieClip extends Container { * @param {Function} [ease] An optional easing function that takes the tween time from 0-1. * @return {PIXI.animate.MovieClip} */ - addTween(instance, properties, startFrame, duration, ease) { - duration = duration || 0; - - //1. determine if there is already a tween for this instance, and if so prepare to add it - // on/insert it - if there isn't, then make one and set up a wait until startFrame - let timeline, i; - for (i = this._timelines.length - 1; i >= 0; --i) { - if (this._timelines[i].target === instance) { - timeline = this._timelines[i]; - break; - } - } - if (!timeline) { - timeline = new Timeline(instance); - this._timelines.push(timeline); - } + addKeyframe(instance, properties, startFrame) { - // Convert any string colors to uints - if (typeof properties.t === 'string') { - properties.t = utils.hexToUint(properties.t); - } else if (typeof properties.v === 'number') { - properties.v = !!properties.v; - } - - //2. create the tween segment, recording the starting values of properties and using the - // supplied properties as the ending values - timeline.addTween(instance, properties, startFrame, duration, ease); - if (this._totalFrames < startFrame + duration) { - this._totalFrames = startFrame + duration; - } + let timeline = this._getChildTimeline(instance); + this._parseProperties(properties); + timeline.addKeyframe(properties, startFrame); + this._autoExtend(startFrame); return this; } @@ -493,8 +525,10 @@ class MovieClip extends Container { let lastFrame = {}; for (let i in keyframes) { lastFrame = Object.assign({}, lastFrame, keyframes[i]); - this.addTween(instance, lastFrame, parseInt(i, 10)); + this.addKeyframe(instance, lastFrame, parseInt(i, 10)); } + this._getChildTimeline(instance) + .extendLastFrame(startFrame + duration); } // Set the initial position/add @@ -504,8 +538,8 @@ class MovieClip extends Container { } /** - * Handle frame actions, callback is bound to the instance of the MovieClip - * @method addAction + * Short cut for `addAction` + * @method aa * @param {Function} callback The clip call on a certain frame * @param {int} startFrame The starting frame * @return {PIXI.animate.MovieClip} @@ -514,6 +548,13 @@ class MovieClip extends Container { return this.addAction(callback, startFrame); } + /** + * Handle frame actions, callback is bound to the instance of the MovieClip. + * @method addAction + * @param {Function} callback The clip call on a certain frame + * @param {int} startFrame The starting frame + * @return {PIXI.animate.MovieClip} + */ addAction(callback, startFrame) { let actions = this._actions; //ensure that the movieclip timeline is long enough to support the target frame diff --git a/src/animate/Timeline.js b/src/animate/Timeline.js index e3bdc597..9bbc0a31 100644 --- a/src/animate/Timeline.js +++ b/src/animate/Timeline.js @@ -35,7 +35,8 @@ const p = Timeline.prototype = Object.create(Array.prototype); * @param tween The tween(s) to add. Accepts multiple arguments. * @return Tween The first tween that was passed in. */ -p.addTween = function(instance, properties, startFrame, duration, ease) { +p.addTween = function(properties, startFrame, duration, ease) { + this.extendLastFrame(startFrame - 1); //ownership of startProps is passed to the new Tween - this object should not be reused let startProps = {}; let prop; @@ -47,7 +48,7 @@ p.addTween = function(instance, properties, startFrame, duration, ease) { } //otherwise, get the current value else { - let startValue = startProps[prop] = this.getPropFromShorthand(instance, prop); + let startValue = startProps[prop] = this.getPropFromShorthand(prop); //go through previous tweens to set the value so that when the timeline loops //around, the values are set properly - having each tween know what came before //allows us to set to a specific frame without running through the entire timeline @@ -58,17 +59,57 @@ p.addTween = function(instance, properties, startFrame, duration, ease) { } } //create the new Tween and add it to the list - let tween = new Tween(instance, startProps, properties, startFrame, duration, ease); + let tween = new Tween(this.target, startProps, properties, startFrame, duration, ease); this.push(tween); //update starting values for the next tween - if tweened values included 'p', then Tween //parsed that to add additional data that is required - properties = tween.endProps; - for (prop in properties) { - this._currentProps[prop] = properties[prop]; + Object.assign(this._currentProps, tween.endProps); +}; + +/** + * Add a single keyframe that doesn't tween. + * @method addKeyframe + * @param {Object} properties The properties to set. + * @param {int} startFrame The starting frame index. + */ +p.addKeyframe = function(properties, startFrame) { + this.extendLastFrame(startFrame - 1); + let startProps = Object.assign({}, this._currentProps, properties); + //create the new Tween and add it to the list + let tween = new Tween(this.target, startProps, null, startFrame, 0); + this.push(tween); + Object.assign(this._currentProps, tween.endProps); +}; + +/** + * Extend the last frame of the tween. + * @method extendLastFrame + * @param {int} endFrame The ending frame index. + */ +p.extendLastFrame = function(endFrame) { + if (this.length) { + let prevTween = this[this.length - 1]; + if (prevTween.endFrame < endFrame) { + if (prevTween.isTweenlessFrame) { + prevTween.endFrame = endFrame; + } else { + this.addKeyframe( + this._currentProps, + prevTween.endFrame + 1, + endFrame - prevTween.endFrame + 1 + ); + } + } } }; -p.getPropFromShorthand = function(target, prop) { +/** + * Get the value for a property + * @method getPropFromShorthand + * @param {string} prop + */ +p.getPropFromShorthand = function(prop) { + const target = this.target; switch (prop) { case 'x': return target.position.x; diff --git a/src/animate/Tween.js b/src/animate/Tween.js index bbcb9533..cc110cc0 100644 --- a/src/animate/Tween.js +++ b/src/animate/Tween.js @@ -13,20 +13,63 @@ class Tween { constructor(target, startProps, endProps, startFrame, duration, ease) { - //target display object + + /** + * target display object + * @property {Object} target + */ this.target = target; - //properties at the start of the tween + /** + * properties at the start of the tween + * @property {Object} startProps + */ this.startProps = startProps; - //properties at the end of the tween, as well as any properties that are set - //instead of tweened + /** + * properties at the end of the tween, as well as any properties that are set + * instead of tweened + * @property {Object} endProps + */ this.endProps = {}; - let prop; - //make a copy to safely include any unchanged values from the start of the tween - for (prop in endProps) { - this.endProps[prop] = endProps[prop]; + /** + * duration of tween in frames. For a keyframe with no tweening, the duration will be 0. + * @property {int} duration + */ + this.duration = duration; + + /** + * The frame that the tween starts on + * @property {int} startFrame + */ + this.startFrame = startFrame; + + /** + * the frame that the tween ends on + * @property {int} endFrame + */ + this.endFrame = startFrame + duration; + + /** + * easing function to use, if any + * @property {Function} ease + */ + this.ease = ease; + + /** + * If we don't tween. + * @property {Boolean} isTweenlessFrame + */ + this.isTweenlessFrame = !endProps; + + + let prop; + if (endProps) { + //make a copy to safely include any unchanged values from the start of the tween + for (prop in endProps) { + this.endProps[prop] = endProps[prop]; + } } //copy in any starting properties don't change @@ -35,20 +78,13 @@ class Tween { this.endProps[prop] = startProps[prop]; } } - //duration of tween in frames. For a keyframe with no tweening, the duration - //will be 0. - this.duration = duration; - - //the frame that the tween starts on - this.startFrame = startFrame; - - //the frame that the tween ends on - this.endFrame = startFrame + duration; - - //easing function to use, if any - this.ease = ease; } + /** + * Set the current frame. + * @method setPosition + * @param {int} currentFrame + */ setPosition(currentFrame) { //if this is a single frame with no tweening, or at the end of the tween, then //just speed up the process by setting values @@ -57,6 +93,11 @@ class Tween { return; } + if (this.isTweenlessFrame) { + this.setToEnd(); + return; + } + let time = (currentFrame - this.startFrame) / this.duration; if (this.ease) { time = this.ease(time); @@ -74,6 +115,10 @@ class Tween { } } + /** + * Set to the end position + * @method setToEnd + */ setToEnd() { let endProps = this.endProps; let target = this.target; diff --git a/src/animate/load.js b/src/animate/load.js index d0fab028..63d52c56 100644 --- a/src/animate/load.js +++ b/src/animate/load.js @@ -31,65 +31,78 @@ /** * Load the stage class and preload any assets * @method load - * @param {Function} StageRef Reference to the stage class - * @param {Array} [StageRef.assets] Assets used to preload - * @param {PIXI.Container} parent The Container to auto-add the stage to. - * @param {Function} [complete] Function to call when complete - * @param {String} [assetBaseDir] Base root directory + * @param {Object} options Options for loading. + * @param {Function} options.stage Reference to the stage class + * @param {Object} [options.stage.assets] Assets used to preload + * @param {PIXI.Container} options.parent The Container to auto-add the stage to. + * @param {String} [options.basePath] Base root directory */ /** * Load the stage class and preload any assets * @method load - * @param {Function} StageRef Reference to the stage class - * @param {Array} [StageRef.assets] Assets used to preload - * @param {PIXI.Container} parent The Container to auto-add the stage to. - * @param {String} [assetBaseDir] Base root directory + * @param {Function} StageRef Reference to the stage class. + * @param {Object} [StageRef.assets] Assets used to preload. + * @param {Function} complete The callback function when complete. */ /** * Load the stage class and preload any assets * @method load - * @param {Function} StageRef Reference to the stage class - * @param {Array} [StageRef.assets] Assets used to preload - * @param {Function} complete The callback function when complete. - * @param {String} [assetBaseDir] Base root directory + * @param {Function} StageRef Reference to the stage class. + * @param {Object} [StageRef.assets] Assets used to preload. + * @param {PIXI.Container} parent The Container to auto-add the stage to. */ -const load = function(StageRef, parent, complete, assetBaseDir) { - // Support arguments (ref, complete, assetBaseDir) +const load = function(options, parent, complete, basePath) { + + // Support arguments (ref, complete, basePath) if (typeof parent === "function") { - assetBaseDir = complete; + basePath = complete; complete = parent; parent = null; } else { if (typeof complete === "string") { - assetBaseDir = complete; + basePath = complete; complete = null; } } - // Root load directory - assetBaseDir = assetBaseDir || ""; + if (typeof options === "function") { + options = { + stage: options, + parent: parent, + basePath: basePath || "", + complete: complete + }; + } + + options = Object.assign({ + stage: null, + parent: null, + basePath: '', + complete: null + }, options || {}); - let assets = StageRef.assets || []; const loader = new PIXI.loaders.Loader(); function done() { - let stage = new StageRef(); - if (parent) { - parent.addChild(stage); + let instance = new options.stage(); + if (options.parent) { + options.parent.addChild(instance); } - if (complete) { - complete(stage); + if (options.complete) { + options.complete(instance); } } // Check for assets to preload + let assets = options.stage.assets || {}; if (assets && Object.keys(assets).length) { // assetBaseDir can accept either with trailing slash or not - if (assetBaseDir) { - assetBaseDir += "/"; + let basePath = options.basePath; + if (basePath) { + basePath += "/"; } for (let id in assets) { - loader.add(id, assetBaseDir + assets[id]); + loader.add(id, basePath + assets[id]); } loader.once('complete', done).load(); } else { diff --git a/src/animate/utils.js b/src/animate/utils.js index 835cb5ae..d2311580 100644 --- a/src/animate/utils.js +++ b/src/animate/utils.js @@ -1,3 +1,12 @@ +const SharedTicker = PIXI.ticker.shared; + +// Number of assets to upload per frame +let _uploadsPerFrame = 4; + +// Collectsion of graphics and textures +const _graphics = []; +const _textures = []; + /** * @namespace PIXI.animate * @class utils @@ -184,4 +193,112 @@ export default class AnimateUtils { } } } + + /** + * The number of graphics or textures to upload to the GPU, if using + * utils.upload and WebGLRenderer + * @property {int} UPLOADS_PER_FRAME + * @static + * @default 4 + */ + static set UPLOADS_PER_FRAME(value) { + _uploadsPerFrame = value; + } + static get UPLOADS_PER_FRAME() { + return _uploadsPerFrame; + } + + /** + * Upload all the textures and graphics to the GPU. + * @method upload + * @static + * @param {PIXI.WebGLRenderer} renderer Render to upload to + * @param {PIXI.DisplayObject} clip MovieClip to upload + * @param {function} done When complete + */ + static upload(renderer, displayObject, done) { + + // No need to upload if CanvasRenderer + if (!(renderer instanceof PIXI.WebGLRenderer)) { + return done(); + } + + // Get global properties + const textures = _textures; + const graphics = _graphics; + + // Get the items for upload from the display + this.getUploadable(displayObject, textures, graphics); + + let numLeft = this.UPLOADS_PER_FRAME; + + const update = () => { + + // Upload the graphics + while (graphics.length && numLeft) { + renderer.plugins.graphics.updateGraphics(graphics.pop()); + numLeft--; + } + + // Upload the textures + while (textures.length && numLeft) { + renderer.textureManager.updateTexture(textures.pop()); + numLeft--; + } + + // We're finished + if (textures.length || graphics.length) { + numLeft = this.UPLOADS_PER_FRAME; + } else { + SharedTicker.remove(update); + done(); + } + }; + + // Listen to frame updates + SharedTicker.add(update); + } + + /** + * Get the list of renderable items. + * @method getUploadable + * @static + * @private + * @param {PIXI.DisplayObject} displayObject + * @param {Array} textures Collection of textures + * @param {Array} graphics Collection of graphics + */ + static getUploadable(displayObject, textures, graphics) { + + // Objects with textures, like Sprites + if (displayObject._texture) { + let texture = displayObject._texture.baseTexture; + if (textures.indexOf(texture) == -1) { + textures.push(texture); + } + } else if (displayObject instanceof PIXI.Graphics) { + graphics.push(displayObject); + } + + // Get timed childen + if (displayObject instanceof PIXI.animate.MovieClip) { + let children = displayObject.children.slice(); + displayObject._timedChildTimelines.forEach((timeline) => { + this.getUploadable(timeline.target, textures, graphics); + const index = children.indexOf(timeline.target); + if (index > -1) { + children.splice(index, 1); + } + }); + children.forEach((child) => { + this.getUploadable(child, textures, graphics); + }); + } + // Recursively get textures + else if (displayObject instanceof PIXI.Container) { + displayObject.children.forEach((child) => { + this.getUploadable(child, textures, graphics); + }); + } + } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index c055c0f8..d43eece2 100644 --- a/src/index.js +++ b/src/index.js @@ -23,7 +23,7 @@ if (typeof module !== 'undefined' && module.exports) { // Export the module module.exports = require('./animate').default; } -// If we're in the browser make sure PIXI is available +// If we're in the browser make sure PIXI is available else if (typeof PIXI === 'undefined') { throw "Requires PIXI"; } @@ -32,4 +32,5 @@ else if (typeof PIXI === 'undefined') { require('./mixins'); // Assign to global namespace -PIXI.animate = require('./animate').default; \ No newline at end of file +PIXI.animate = require('./animate').default; +PIXI.animate.VERSION = '/* @echo VERSION */' || ''; \ No newline at end of file diff --git a/tasks/common/bundler.js b/tasks/common/bundler.js index 97aaac1c..645fb4b3 100644 --- a/tasks/common/bundler.js +++ b/tasks/common/bundler.js @@ -5,7 +5,7 @@ module.exports = function(gulp, options, plugins, debug) { function header() { return plugins.header( plugins.fs.readFileSync(__dirname + '/header.txt', 'utf8'), { - pkg: options.pkg + pkg: options.pkg } ); } @@ -46,7 +46,8 @@ module.exports = function(gulp, options, plugins, debug) { .pipe(plugins.preprocess({ context: { DEBUG: debug, - RELEASE: !debug + RELEASE: !debug, + VERSION: options.pkg.version } })); @@ -58,4 +59,4 @@ module.exports = function(gulp, options, plugins, debug) { .pipe(header()) .pipe(gulp.dest(options.dest)); } -}; \ No newline at end of file +};