1 /**
  2  * Copyright (c) 2010-2012 cocos2d-x.org
  3  * Copyright (C) 2009 Matt Oswald
  4  * Copyright (c) 2009-2010 Ricardo Quesada
  5  * Copyright (c) 2011 Zynga Inc.
  6  * Copyright (c) 2011 Marco Tillemans
  7  *
  8  * http://www.cocos2d-x.org
  9  *
 10  * Permission is hereby granted, free of charge, to any person obtaining a copy
 11  * of this software and associated documentation files (the "Software"), to deal
 12  * in the Software without restriction, including without limitation the rights
 13  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 14  * copies of the Software, and to permit persons to whom the Software is
 15  * furnished to do so, subject to the following conditions:
 16  *
 17  * The above copyright notice and this permission notice shall be included in
 18  * all copies or substantial portions of the Software.
 19  *
 20  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 21  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 22  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 23  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 24  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 25  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 26  * THE SOFTWARE.
 27  *
 28  */
 29 
 30 /**
 31  * paticle default capacity
 32  * @constant
 33  * @type Number
 34  */
 35 cc.PARTICLE_DEFAULT_CAPACITY = 500;
 36 
 37 /**
 38  * <p>
 39  *    cc.ParticleBatchNode is like a batch node: if it contains children, it will draw them in 1 single OpenGL call  <br/>
 40  *    (often known as "batch draw").  </br>
 41  *
 42  *    A cc.ParticleBatchNode can reference one and only one texture (one image file, one texture atlas).<br/>
 43  *    Only the cc.ParticleSystems that are contained in that texture can be added to the cc.SpriteBatchNode.<br/>
 44  *    All cc.ParticleSystems added to a cc.SpriteBatchNode are drawn in one OpenGL ES draw call.<br/>
 45  *    If the cc.ParticleSystems are not added to a cc.ParticleBatchNode then an OpenGL ES draw call will be needed for each one, which is less efficient.</br>
 46  *
 47  *    Limitations:<br/>
 48  *    - At the moment only cc.ParticleSystem is supported<br/>
 49  *    - All systems need to be drawn with the same parameters, blend function, aliasing, texture<br/>
 50  *
 51  *    Most efficient usage<br/>
 52  *    - Initialize the ParticleBatchNode with the texture and enough capacity for all the particle systems<br/>
 53  *    - Initialize all particle systems and add them as child to the batch node<br/>
 54  * </p>
 55  * @class
 56  * @extends cc.ParticleSystem
 57  *
 58  * @property {cc.Texture2D|HTMLImageElement|HTMLCanvasElement}  texture         - The used texture
 59  * @property {cc.TextureAtlas}                                  textureAtlas    - The texture atlas used for drawing the quads
 60  */
 61 cc.ParticleBatchNode = cc.Node.extend(/** @lends cc.ParticleBatchNode# */{
 62 	textureAtlas:null,
 63 
 64     TextureProtocol:true,
 65     //the blend function used for drawing the quads
 66     _blendFunc:null,
 67     _className:"ParticleBatchNode",
 68 
 69     ctor:function () {
 70         cc.Node.prototype.ctor.call(this);
 71         this._blendFunc = {src:cc.BLEND_SRC, dst:cc.BLEND_DST};
 72     },
 73 
 74     /**
 75      * initializes the particle system with cc.Texture2D, a capacity of particles
 76      * @param {cc.Texture2D|HTMLImageElement|HTMLCanvasElement} texture
 77      * @param {Number} capacity
 78      * @return {Boolean}
 79      */
 80     initWithTexture:function (texture, capacity) {
 81         this.textureAtlas = new cc.TextureAtlas();
 82         this.textureAtlas.initWithTexture(texture, capacity);
 83 
 84         // no lazy alloc in this node
 85         this._children.length = 0;
 86 
 87         if (cc._renderType === cc._RENDER_TYPE_WEBGL)
 88             this.shaderProgram = cc.shaderCache.programForKey(cc.SHADER_POSITION_TEXTURECOLOR);
 89         return true;
 90     },
 91 
 92     /**
 93      * initializes the particle system with the name of a file on disk (for a list of supported formats look at the cc.Texture2D class), a capacity of particles
 94      * @param {String} fileImage
 95      * @param {Number} capacity
 96      * @return {Boolean}
 97      */
 98     initWithFile:function (fileImage, capacity) {
 99         var tex = cc.textureCache.addImage(fileImage);
100         return this.initWithTexture(tex, capacity);
101     },
102 
103     /**
104      * initializes the particle system with the name of a file on disk (for a list of supported formats look at the cc.Texture2D class), a capacity of particles
105      * @param {String} fileImage
106      * @param {Number} capacity
107      * @return {Boolean}
108      */
109     init:function (fileImage, capacity) {
110         var tex = cc.TextureCache.getInstance().addImage(fileImage);
111         return this.initWithTexture(tex, capacity);
112     },
113 
114     /**
115      * Add a child into the cc.ParticleBatchNode
116      * @param {cc.ParticleSystem} child
117      * @param {Number} zOrder
118      * @param {Number} tag
119      */
120     addChild:function (child, zOrder, tag) {
121         if(!child)
122             throw "cc.ParticleBatchNode.addChild() : child should be non-null";
123         if(!(child instanceof cc.ParticleSystem))
124             throw "cc.ParticleBatchNode.addChild() : only supports cc.ParticleSystem as children";
125         zOrder = (zOrder == null) ? child.zIndex : zOrder;
126         tag = (tag == null) ? child.tag : tag;
127 
128         if(child.getTexture() != this.textureAtlas.texture)
129             throw "cc.ParticleSystem.addChild() : the child is not using the same texture id";
130 
131         // If this is the 1st children, then copy blending function
132         var childBlendFunc = child.getBlendFunc();
133         if (this._children.length === 0)
134             this.setBlendFunc(childBlendFunc);
135         else{
136             if((childBlendFunc.src != this._blendFunc.src) || (childBlendFunc.dst != this._blendFunc.dst)){
137                 cc.log("cc.ParticleSystem.addChild() : Can't add a ParticleSystem that uses a different blending function");
138                 return;
139             }
140         }
141 
142         //no lazy sorting, so don't call super addChild, call helper instead
143         var pos = this._addChildHelper(child, zOrder, tag);
144 
145         //get new atlasIndex
146         var atlasIndex = 0;
147 
148         if (pos != 0) {
149             var p = this._children[pos - 1];
150             atlasIndex = p.getAtlasIndex() + p.getTotalParticles();
151         } else
152             atlasIndex = 0;
153 
154         this.insertChild(child, atlasIndex);
155 
156         // update quad info
157         child.setBatchNode(this);
158     },
159 
160     /**
161      * Inserts a child into the cc.ParticleBatchNode
162      * @param {cc.ParticleSystem} pSystem
163      * @param {Number} index
164      */
165     insertChild:function (pSystem, index) {
166         var totalParticles = pSystem.getTotalParticles();
167         var locTextureAtlas = this.textureAtlas;
168         var totalQuads = locTextureAtlas.totalQuads;
169         pSystem.setAtlasIndex(index);
170         if (totalQuads + totalParticles > locTextureAtlas.getCapacity()) {
171             this._increaseAtlasCapacityTo(totalQuads + totalParticles);
172             // after a realloc empty quads of textureAtlas can be filled with gibberish (realloc doesn't perform calloc), insert empty quads to prevent it
173             locTextureAtlas.fillWithEmptyQuadsFromIndex(locTextureAtlas.getCapacity() - totalParticles, totalParticles);
174         }
175 
176         // make room for quads, not necessary for last child
177         if (pSystem.getAtlasIndex() + totalParticles != totalQuads)
178             locTextureAtlas.moveQuadsFromIndex(index, index + totalParticles);
179 
180         // increase totalParticles here for new particles, update method of particlesystem will fill the quads
181         locTextureAtlas.increaseTotalQuadsWith(totalParticles);
182         this._updateAllAtlasIndexes();
183     },
184 
185     /**
186      * @param {cc.ParticleSystem} child
187      * @param {Boolean} cleanup
188      */
189     removeChild:function (child, cleanup) {
190         // explicit nil handling
191         if (child == null)
192             return;
193 
194         if(!(child instanceof cc.ParticleSystem))
195             throw "cc.ParticleBatchNode.removeChild(): only supports cc.ParticleSystem as children";
196         if(this._children.indexOf(child) == -1){
197             cc.log("cc.ParticleBatchNode.removeChild(): doesn't contain the sprite. Can't remove it");
198             return;
199         }
200 
201         cc.Node.prototype.removeChild.call(this, child, cleanup);
202 
203         var locTextureAtlas = this.textureAtlas;
204         // remove child helper
205         locTextureAtlas.removeQuadsAtIndex(child.getAtlasIndex(), child.getTotalParticles());
206 
207         // after memmove of data, empty the quads at the end of array
208         locTextureAtlas.fillWithEmptyQuadsFromIndex(locTextureAtlas.totalQuads, child.getTotalParticles());
209 
210         // paticle could be reused for self rendering
211         child.setBatchNode(null);
212 
213         this._updateAllAtlasIndexes();
214     },
215 
216     /**
217      * Reorder will be done in this function, no "lazy" reorder to particles
218      * @param {cc.ParticleSystem} child
219      * @param {Number} zOrder
220      */
221     reorderChild:function (child, zOrder) {
222         if(!child)
223             throw "cc.ParticleBatchNode.reorderChild(): child should be non-null";
224         if(!(child instanceof cc.ParticleSystem))
225             throw "cc.ParticleBatchNode.reorderChild(): only supports cc.QuadParticleSystems as children";
226         if(this._children.indexOf(child) === -1){
227             cc.log("cc.ParticleBatchNode.reorderChild(): Child doesn't belong to batch");
228             return;
229         }
230 
231         if (zOrder == child.zIndex)
232             return;
233 
234         // no reordering if only 1 child
235         if (this._children.length > 1) {
236             var getIndexes = this._getCurrentIndex(child, zOrder);
237 
238             if (getIndexes.oldIndex != getIndexes.newIndex) {
239                 // reorder m_pChildren.array
240                 this._children.splice(getIndexes.oldIndex, 1)
241                 this._children.splice(getIndexes.newIndex, 0, child);
242 
243                 // save old altasIndex
244                 var oldAtlasIndex = child.getAtlasIndex();
245 
246                 // update atlas index
247                 this._updateAllAtlasIndexes();
248 
249                 // Find new AtlasIndex
250                 var newAtlasIndex = 0;
251                 var locChildren = this._children;
252                 for (var i = 0; i < locChildren.length; i++) {
253                     var pNode = locChildren[i];
254                     if (pNode == child) {
255                         newAtlasIndex = child.getAtlasIndex();
256                         break;
257                     }
258                 }
259 
260                 // reorder textureAtlas quads
261                 this.textureAtlas.moveQuadsFromIndex(oldAtlasIndex, child.getTotalParticles(), newAtlasIndex);
262 
263                 child.updateWithNoTime();
264             }
265         }
266         child._setLocalZOrder(zOrder);
267     },
268 
269     /**
270      * @param {Number} index
271      * @param {Boolean} doCleanup
272      */
273     removeChildAtIndex:function (index, doCleanup) {
274         this.removeChild(this._children[i], doCleanup);
275     },
276 
277     /**
278      * @param {Boolean} doCleanup
279      */
280     removeAllChildren:function (doCleanup) {
281         var locChildren = this._children;
282         for (var i = 0; i < locChildren.length; i++) {
283             locChildren[i].setBatchNode(null);
284         }
285         cc.Node.prototype.removeAllChildren.call(this, doCleanup);
286         this.textureAtlas.removeAllQuads();
287     },
288 
289     /**
290      * disables a particle by inserting a 0'd quad into the texture atlas
291      * @param {Number} particleIndex
292      */
293     disableParticle:function (particleIndex) {
294         var quad = this.textureAtlas.quads[particleIndex];
295         quad.br.vertices.x = quad.br.vertices.y = quad.tr.vertices.x = quad.tr.vertices.y =
296             quad.tl.vertices.x = quad.tl.vertices.y = quad.bl.vertices.x = quad.bl.vertices.y = 0.0;
297         this.textureAtlas._setDirty(true);
298     },
299 
300     /**
301      * @override
302      * @param {CanvasContext} ctx
303      */
304     draw:function (ctx) {
305         //cc.PROFILER_STOP("CCParticleBatchNode - draw");
306         if (cc._renderType === cc._RENDER_TYPE_CANVAS)
307             return;
308 
309         if (this.textureAtlas.totalQuads == 0)
310             return;
311 
312         cc.NODE_DRAW_SETUP(this);
313         cc.glBlendFuncForParticle(this._blendFunc.src, this._blendFunc.dst);
314         this.textureAtlas.drawQuads();
315 
316         //cc.PROFILER_STOP("CCParticleBatchNode - draw");
317     },
318 
319     /**
320      * returns the used texture
321      * @return {cc.Texture2D|HTMLImageElement|HTMLCanvasElement}
322      */
323     getTexture:function () {
324         return this.textureAtlas.texture;
325     },
326 
327     /**
328      * sets a new texture. it will be retained
329      * @param {cc.Texture2D|HTMLImageElement|HTMLCanvasElement} texture
330      */
331     setTexture:function (texture) {
332         this.textureAtlas.texture = texture;
333 
334         // If the new texture has No premultiplied alpha, AND the blendFunc hasn't been changed, then update it
335         var locBlendFunc = this._blendFunc;
336         if (texture && !texture.hasPremultipliedAlpha() && ( locBlendFunc.src == cc.BLEND_SRC && locBlendFunc.dst == cc.BLEND_DST )) {
337             locBlendFunc.src = cc.SRC_ALPHA;
338             locBlendFunc.dst = cc.ONE_MINUS_SRC_ALPHA;
339         }
340     },
341 
342     /**
343      * set the blending function used for the texture
344      * @param {Number|cc.BlencFunc} src
345      * @param {Number} dst
346      */
347     setBlendFunc:function (src, dst) {
348         if (dst === undefined){
349             this._blendFunc.src = src.src;
350             this._blendFunc.dst = src.dst;
351         } else{
352             this._blendFunc.src = src;
353             this._blendFunc.src = dst;
354         }
355 
356     },
357 
358     /**
359      * returns the blending function used for the texture
360      * @return {cc.BlendFunc}
361      */
362     getBlendFunc:function () {
363         return {src:this._blendFunc.src, dst:this._blendFunc.dst};
364     },
365 
366     // override visit.
367     // Don't call visit on it's children
368     visit:function (ctx) {
369         if (cc._renderType === cc._RENDER_TYPE_CANVAS)
370             return;
371 
372         // CAREFUL:
373         // This visit is almost identical to cc.Node#visit
374         // with the exception that it doesn't call visit on it's children
375         //
376         // The alternative is to have a void cc.Sprite#visit, but
377         // although this is less mantainable, is faster
378         //
379         if (!this._visible)
380             return;
381 
382         cc.kmGLPushMatrix();
383         if (this.grid && this.grid.isActive()) {
384             this.grid.beforeDraw();
385             this.transformAncestors();
386         }
387 
388         this.transform(ctx);
389         this.draw(ctx);
390 
391         if (this.grid && this.grid.isActive())
392             this.grid.afterDraw(this);
393 
394         cc.kmGLPopMatrix();
395     },
396 
397     _updateAllAtlasIndexes:function () {
398         var index = 0;
399         var locChildren = this._children;
400         for (var i = 0; i < locChildren.length; i++) {
401             var child = locChildren[i];
402             child.setAtlasIndex(index);
403             index += child.getTotalParticles();
404         }
405     },
406 
407     _increaseAtlasCapacityTo:function (quantity) {
408         cc.log("cocos2d: cc.ParticleBatchNode: resizing TextureAtlas capacity from [" + this.textureAtlas.getCapacity()
409             + "] to [" + quantity + "].");
410 
411         if (!this.textureAtlas.resizeCapacity(quantity)) {
412             // serious problems
413             cc.log("cc.ParticleBatchNode._increaseAtlasCapacityTo() : WARNING: Not enough memory to resize the atlas");
414         }
415     },
416 
417     _searchNewPositionInChildrenForZ:function (z) {
418         var locChildren = this._children;
419         var count = locChildren.length;
420         for (var i = 0; i < count; i++) {
421             if (locChildren[i].zIndex > z)
422                 return i;
423         }
424         return count;
425     },
426 
427     _getCurrentIndex:function (child, z) {
428         var foundCurrentIdx = false;
429         var foundNewIdx = false;
430 
431         var newIndex = 0;
432         var oldIndex = 0;
433 
434         var minusOne = 0, locChildren = this._children;
435         var count = locChildren.length;
436         for (var i = 0; i < count; i++) {
437             var pNode = locChildren[i];
438             // new index
439             if (pNode.zIndex > z && !foundNewIdx) {
440                 newIndex = i;
441                 foundNewIdx = true;
442 
443                 if (foundCurrentIdx && foundNewIdx)
444                     break;
445             }
446             // current index
447             if (child == pNode) {
448                 oldIndex = i;
449                 foundCurrentIdx = true;
450                 if (!foundNewIdx)
451                     minusOne = -1;
452                 if (foundCurrentIdx && foundNewIdx)
453                     break;
454             }
455         }
456         if (!foundNewIdx)
457             newIndex = count;
458         newIndex += minusOne;
459         return {newIndex:newIndex, oldIndex:oldIndex};
460     },
461 
462     /**
463      * <p>
464      *     don't use lazy sorting, reordering the particle systems quads afterwards would be too complex                                    <br/>
465      *     XXX research whether lazy sorting + freeing current quads and calloc a new block with size of capacity would be faster           <br/>
466      *     XXX or possibly using vertexZ for reordering, that would be fastest                                                              <br/>
467      *     this helper is almost equivalent to CCNode's addChild, but doesn't make use of the lazy sorting                                  <br/>
468      * </p>
469      * @param {cc.ParticleSystem} child
470      * @param {Number} z
471      * @param {Number} aTag
472      * @return {Number}
473      * @private
474      */
475     _addChildHelper:function (child, z, aTag) {
476         if(!child)
477             throw "cc.ParticleBatchNode._addChildHelper(): child should be non-null";
478         if(child.parent){
479             cc.log("cc.ParticleBatchNode._addChildHelper(): child already added. It can't be added again");
480             return null;
481         }
482 
483 
484         if (!this._children)
485             this._children = [];
486 
487         //don't use a lazy insert
488         var pos = this._searchNewPositionInChildrenForZ(z);
489 
490         this._children.splice(pos, 0, child);
491         child.tag = aTag;
492         child._setLocalZOrder(z);
493         child.parent = this;
494         if (this._running) {
495             child.onEnter();
496             child.onEnterTransitionDidFinish();
497         }
498         return pos;
499     },
500 
501     _updateBlendFunc:function () {
502         if (!this.textureAtlas.texture.hasPremultipliedAlpha()) {
503             this._blendFunc.src = cc.SRC_ALPHA;
504             this._blendFunc.dst = cc.ONE_MINUS_SRC_ALPHA;
505         }
506     },
507 
508     /**
509      * return the texture atlas used for drawing the quads
510      * @return {cc.TextureAtlas}
511      */
512     getTextureAtlas:function () {
513         return this.textureAtlas;
514     },
515 
516     /**
517      * set the texture atlas used for drawing the quads
518      * @param {cc.TextureAtlas} textureAtlas
519      */
520     setTextureAtlas:function (textureAtlas) {
521         this.textureAtlas = textureAtlas;
522     }
523 });
524 
525 window._p = cc.ParticleBatchNode.prototype;
526 
527 // Extended properties
528 /** @expose */
529 _p.texture;
530 cc.defineGetterSetter(_p, "texture", _p.getTexture, _p.setTexture);
531 
532 delete window._p;
533 
534 /**
535  * initializes the particle system with the name of a file on disk (for a list of supported formats look at the cc.Texture2D class), a capacity of particles
536  * @param {String|cc.Texture2D} fileImage
537  * @param {Number} capacity
538  * @return {cc.ParticleBatchNode}
539  * @example
540  * 1.
541  * //Create a cc.ParticleBatchNode with image path  and capacity
542  * var particleBatchNode = cc.ParticleBatchNode.create("res/grossini_dance.png",30);
543  *
544  * 2.
545  * //Create a cc.ParticleBatchNode with a texture and capacity
546  * var texture = cc.TextureCache.getInstance().addImage("res/grossini_dance.png");
547  * var particleBatchNode = cc.ParticleBatchNode.create(texture, 30);
548  */
549 cc.ParticleBatchNode.create = function (fileImage, capacity) {
550     var ret = new cc.ParticleBatchNode();
551     if (typeof(fileImage) == "string") {
552         ret.init(fileImage, capacity);
553     } else if (fileImage instanceof cc.Texture2D) {
554         ret.initWithTexture(fileImage, capacity);
555     }
556     return ret;
557 };
558