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 cc.ParticleBatchNode = cc.Node.extend(/** @lends cc.ParticleBatchNode# */{
 59     TextureProtocol:true,
 60     //the blend function used for drawing the quads
 61     _blendFunc:null,
 62     _textureAtlas:null,
 63 
 64     ctor:function () {
 65         cc.Node.prototype.ctor.call(this);
 66         this._blendFunc = {src:cc.BLEND_SRC, dst:cc.BLEND_DST};
 67     },
 68 
 69     /**
 70      * initializes the particle system with cc.Texture2D, a capacity of particles
 71      * @param {cc.Texture2D|HTMLImageElement|HTMLCanvasElement} texture
 72      * @param {Number} capacity
 73      * @return {Boolean}
 74      */
 75     initWithTexture:function (texture, capacity) {
 76         this._textureAtlas = new cc.TextureAtlas();
 77         this._textureAtlas.initWithTexture(texture, capacity);
 78 
 79         // no lazy alloc in this node
 80         this._children.length = 0;
 81 
 82         if (cc.renderContextType === cc.WEBGL)
 83             this.setShaderProgram(cc.ShaderCache.getInstance().programForKey(cc.SHADER_POSITION_TEXTURECOLOR));
 84         return true;
 85     },
 86 
 87     /**
 88      * 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
 89      * @param {String} fileImage
 90      * @param {Number} capacity
 91      * @return {Boolean}
 92      */
 93     initWithFile:function (fileImage, capacity) {
 94         var tex = cc.TextureCache.getInstance().addImage(fileImage);
 95         return this.initWithTexture(tex, capacity);
 96     },
 97 
 98     /**
 99      * 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
100      * @param {String} fileImage
101      * @param {Number} capacity
102      * @return {Boolean}
103      */
104     init:function (fileImage, capacity) {
105         var tex = cc.TextureCache.getInstance().addImage(fileImage);
106         return this.initWithTexture(tex, capacity);
107     },
108 
109     /**
110      * Add a child into the cc.ParticleBatchNode
111      * @param {cc.ParticleSystem} child
112      * @param {Number} zOrder
113      * @param {Number} tag
114      */
115     addChild:function (child, zOrder, tag) {
116         if(!child)
117             throw "cc.ParticleBatchNode.addChild() : child should be non-null";
118         if(!(child instanceof cc.ParticleSystem))
119             throw "cc.ParticleBatchNode.addChild() : only supports cc.ParticleSystem as children";
120         zOrder = (zOrder == null) ? child.getZOrder() : zOrder;
121         tag = (tag == null) ? child.getTag() : tag;
122 
123         if(child.getTexture() != this._textureAtlas.getTexture())
124             throw "cc.ParticleSystem.addChild() : the child is not using the same texture id";
125 
126         // If this is the 1st children, then copy blending function
127         var childBlendFunc = child.getBlendFunc();
128         if (this._children.length === 0)
129             this.setBlendFunc(childBlendFunc);
130         else{
131             if((childBlendFunc.src != this._blendFunc.src) || (childBlendFunc.dst != this._blendFunc.dst)){
132                 cc.log("cc.ParticleSystem.addChild() : Can't add a ParticleSystem that uses a different blending function");
133                 return;
134             }
135         }
136 
137         //no lazy sorting, so don't call super addChild, call helper instead
138         var pos = this._addChildHelper(child, zOrder, tag);
139 
140         //get new atlasIndex
141         var atlasIndex = 0;
142 
143         if (pos != 0) {
144             var p = this._children[pos - 1];
145             atlasIndex = p.getAtlasIndex() + p.getTotalParticles();
146         } else
147             atlasIndex = 0;
148 
149         this.insertChild(child, atlasIndex);
150 
151         // update quad info
152         child.setBatchNode(this);
153     },
154 
155     /**
156      * Inserts a child into the cc.ParticleBatchNode
157      * @param {cc.ParticleSystem} pSystem
158      * @param {Number} index
159      */
160     insertChild:function (pSystem, index) {
161         var totalParticles = pSystem.getTotalParticles();
162         var locTextureAtlas = this._textureAtlas;
163         var totalQuads = locTextureAtlas.getTotalQuads();
164         pSystem.setAtlasIndex(index);
165         if (totalQuads + totalParticles > locTextureAtlas.getCapacity()) {
166             this._increaseAtlasCapacityTo(totalQuads + totalParticles);
167             // after a realloc empty quads of textureAtlas can be filled with gibberish (realloc doesn't perform calloc), insert empty quads to prevent it
168             locTextureAtlas.fillWithEmptyQuadsFromIndex(locTextureAtlas.getCapacity() - totalParticles, totalParticles);
169         }
170 
171         // make room for quads, not necessary for last child
172         if (pSystem.getAtlasIndex() + totalParticles != totalQuads)
173             locTextureAtlas.moveQuadsFromIndex(index, index + totalParticles);
174 
175         // increase totalParticles here for new particles, update method of particlesystem will fill the quads
176         locTextureAtlas.increaseTotalQuadsWith(totalParticles);
177         this._updateAllAtlasIndexes();
178     },
179 
180     /**
181      * @param {cc.ParticleSystem} child
182      * @param {Boolean} cleanup
183      */
184     removeChild:function (child, cleanup) {
185         // explicit nil handling
186         if (child == null)
187             return;
188 
189         if(!(child instanceof cc.ParticleSystem))
190             throw "cc.ParticleBatchNode.removeChild(): only supports cc.ParticleSystem as children";
191         if(this._children.indexOf(child) == -1){
192             cc.log("cc.ParticleBatchNode.removeChild(): doesn't contain the sprite. Can't remove it");
193             return;
194         }
195 
196         cc.Node.prototype.removeChild.call(this, child, cleanup);
197 
198         var locTextureAtlas = this._textureAtlas;
199         // remove child helper
200         locTextureAtlas.removeQuadsAtIndex(child.getAtlasIndex(), child.getTotalParticles());
201 
202         // after memmove of data, empty the quads at the end of array
203         locTextureAtlas.fillWithEmptyQuadsFromIndex(locTextureAtlas.getTotalQuads(), child.getTotalParticles());
204 
205         // paticle could be reused for self rendering
206         child.setBatchNode(null);
207 
208         this._updateAllAtlasIndexes();
209     },
210 
211     /**
212      * Reorder will be done in this function, no "lazy" reorder to particles
213      * @param {cc.ParticleSystem} child
214      * @param {Number} zOrder
215      */
216     reorderChild:function (child, zOrder) {
217         if(!child)
218             throw "cc.ParticleBatchNode.reorderChild(): child should be non-null";
219         if(!(child instanceof cc.ParticleSystem))
220             throw "cc.ParticleBatchNode.reorderChild(): only supports cc.QuadParticleSystems as children";
221         if(this._children.indexOf(child) === -1){
222             cc.log("cc.ParticleBatchNode.reorderChild(): Child doesn't belong to batch");
223             return;
224         }
225 
226         if (zOrder == child.getZOrder())
227             return;
228 
229         // no reordering if only 1 child
230         if (this._children.length > 1) {
231             var getIndexes = this._getCurrentIndex(child, zOrder);
232 
233             if (getIndexes.oldIndex != getIndexes.newIndex) {
234                 // reorder m_pChildren.array
235                 cc.ArrayRemoveObjectAtIndex(this._children, getIndexes.oldIndex);
236                 this._children = cc.ArrayAppendObjectToIndex(this._children, child, getIndexes.newIndex);
237 
238                 // save old altasIndex
239                 var oldAtlasIndex = child.getAtlasIndex();
240 
241                 // update atlas index
242                 this._updateAllAtlasIndexes();
243 
244                 // Find new AtlasIndex
245                 var newAtlasIndex = 0;
246                 var locChildren = this._children;
247                 for (var i = 0; i < locChildren.length; i++) {
248                     var pNode = locChildren[i];
249                     if (pNode == child) {
250                         newAtlasIndex = child.getAtlasIndex();
251                         break;
252                     }
253                 }
254 
255                 // reorder textureAtlas quads
256                 this._textureAtlas.moveQuadsFromIndex(oldAtlasIndex, child.getTotalParticles(), newAtlasIndex);
257 
258                 child.updateWithNoTime();
259             }
260         }
261         child._setZOrder(zOrder);
262     },
263 
264     /**
265      * @param {Number} index
266      * @param {Boolean} doCleanup
267      */
268     removeChildAtIndex:function (index, doCleanup) {
269         this.removeChild(this._children[i], doCleanup);
270     },
271 
272     /**
273      * @param {Boolean} doCleanup
274      */
275     removeAllChildren:function (doCleanup) {
276         var locChildren = this._children;
277         for (var i = 0; i < locChildren.length; i++) {
278             locChildren[i].setBatchNode(null);
279         }
280         cc.Node.prototype.removeAllChildren.call(this, doCleanup);
281         this._textureAtlas.removeAllQuads();
282     },
283 
284     /**
285      * disables a particle by inserting a 0'd quad into the texture atlas
286      * @param {Number} particleIndex
287      */
288     disableParticle:function (particleIndex) {
289         var quad = ((this._textureAtlas.getQuads())[particleIndex]);
290         quad.br.vertices.x = quad.br.vertices.y = quad.tr.vertices.x = quad.tr.vertices.y =
291             quad.tl.vertices.x = quad.tl.vertices.y = quad.bl.vertices.x = quad.bl.vertices.y = 0.0;
292         this._textureAtlas._setDirty(true);
293     },
294 
295     /**
296      * @override
297      * @param {CanvasContext} ctx
298      */
299     draw:function (ctx) {
300         //cc.PROFILER_STOP("CCParticleBatchNode - draw");
301         if (cc.renderContextType === cc.CANVAS)
302             return;
303 
304         if (this._textureAtlas.getTotalQuads() == 0)
305             return;
306 
307         cc.NODE_DRAW_SETUP(this);
308         cc.glBlendFuncForParticle(this._blendFunc.src, this._blendFunc.dst);
309         this._textureAtlas.drawQuads();
310 
311         //cc.PROFILER_STOP("CCParticleBatchNode - draw");
312     },
313 
314     /**
315      * returns the used texture
316      * @return {cc.Texture2D|HTMLImageElement|HTMLCanvasElement}
317      */
318     getTexture:function () {
319         return this._textureAtlas.getTexture();
320     },
321 
322     /**
323      * sets a new texture. it will be retained
324      * @param {cc.Texture2D|HTMLImageElement|HTMLCanvasElement} texture
325      */
326     setTexture:function (texture) {
327         this._textureAtlas.setTexture(texture);
328 
329         // If the new texture has No premultiplied alpha, AND the blendFunc hasn't been changed, then update it
330         var locBlendFunc = this._blendFunc;
331         if (texture && !texture.hasPremultipliedAlpha() && ( locBlendFunc.src == gl.BLEND_SRC && locBlendFunc.dst == gl.BLEND_DST )) {
332             locBlendFunc.src = gl.SRC_ALPHA;
333             locBlendFunc.dst = gl.ONE_MINUS_SRC_ALPHA;
334         }
335     },
336 
337     /**
338      * set the blending function used for the texture
339      * @param {Number} src
340      * @param {Number} dst
341      */
342     setBlendFunc:function (src, dst) {
343         if (dst === undefined){
344             this._blendFunc.src = src.src;
345             this._blendFunc.dst = src.dst;
346         } else{
347             this._blendFunc.src = src;
348             this._blendFunc.src = dst;
349         }
350 
351     },
352 
353     /**
354      * returns the blending function used for the texture
355      * @return {cc.BlendFunc}
356      */
357     getBlendFunc:function () {
358         return {src:this._blendFunc.src, dst:this._blendFunc.dst};
359     },
360 
361     // override visit.
362     // Don't call visit on it's children
363     visit:function (ctx) {
364         if (cc.renderContextType === cc.CANVAS)
365             return;
366 
367         // CAREFUL:
368         // This visit is almost identical to cc.Node#visit
369         // with the exception that it doesn't call visit on it's children
370         //
371         // The alternative is to have a void cc.Sprite#visit, but
372         // although this is less mantainable, is faster
373         //
374         if (!this._visible)
375             return;
376 
377         cc.kmGLPushMatrix();
378         if (this._grid && this._grid.isActive()) {
379             this._grid.beforeDraw();
380             this.transformAncestors();
381         }
382 
383         this.transform(ctx);
384         this.draw(ctx);
385 
386         if (this._grid && this._grid.isActive())
387             this._grid.afterDraw(this);
388 
389         cc.kmGLPopMatrix();
390     },
391 
392     _updateAllAtlasIndexes:function () {
393         var index = 0;
394         var locChildren = this._children;
395         for (var i = 0; i < locChildren.length; i++) {
396             var child = locChildren[i];
397             child.setAtlasIndex(index);
398             index += child.getTotalParticles();
399         }
400     },
401 
402     _increaseAtlasCapacityTo:function (quantity) {
403         cc.log("cocos2d: cc.ParticleBatchNode: resizing TextureAtlas capacity from [" + this._textureAtlas.getCapacity()
404             + "] to [" + quantity + "].");
405 
406         if (!this._textureAtlas.resizeCapacity(quantity)) {
407             // serious problems
408             cc.log("cc.ParticleBatchNode._increaseAtlasCapacityTo() : WARNING: Not enough memory to resize the atlas");
409         }
410     },
411 
412     _searchNewPositionInChildrenForZ:function (z) {
413         var locChildren = this._children;
414         var count = locChildren.length;
415         for (var i = 0; i < count; i++) {
416             if (locChildren[i].getZOrder() > z)
417                 return i;
418         }
419         return count;
420     },
421 
422     _getCurrentIndex:function (child, z) {
423         var foundCurrentIdx = false;
424         var foundNewIdx = false;
425 
426         var newIndex = 0;
427         var oldIndex = 0;
428 
429         var minusOne = 0, locChildren = this._children;
430         var count = locChildren.length;
431         for (var i = 0; i < count; i++) {
432             var pNode = locChildren[i];
433             // new index
434             if (pNode.getZOrder() > z && !foundNewIdx) {
435                 newIndex = i;
436                 foundNewIdx = true;
437 
438                 if (foundCurrentIdx && foundNewIdx)
439                     break;
440             }
441             // current index
442             if (child == pNode) {
443                 oldIndex = i;
444                 foundCurrentIdx = true;
445                 if (!foundNewIdx)
446                     minusOne = -1;
447                 if (foundCurrentIdx && foundNewIdx)
448                     break;
449             }
450         }
451         if (!foundNewIdx)
452             newIndex = count;
453         newIndex += minusOne;
454         return {newIndex:newIndex, oldIndex:oldIndex};
455     },
456 
457     /**
458      * <p>
459      *     don't use lazy sorting, reordering the particle systems quads afterwards would be too complex                                    <br/>
460      *     XXX research whether lazy sorting + freeing current quads and calloc a new block with size of capacity would be faster           <br/>
461      *     XXX or possibly using vertexZ for reordering, that would be fastest                                                              <br/>
462      *     this helper is almost equivalent to CCNode's addChild, but doesn't make use of the lazy sorting                                  <br/>
463      * </p>
464      * @param {cc.ParticleSystem} child
465      * @param {Number} z
466      * @param {Number} aTag
467      * @return {Number}
468      * @private
469      */
470     _addChildHelper:function (child, z, aTag) {
471         if(!child)
472             throw "cc.ParticleBatchNode._addChildHelper(): child should be non-null";
473         if(child.getParent()){
474             cc.log("cc.ParticleBatchNode._addChildHelper(): child already added. It can't be added again");
475             return null;
476         }
477 
478 
479         if (!this._children)
480             this._children = [];
481 
482         //don't use a lazy insert
483         var pos = this._searchNewPositionInChildrenForZ(z);
484 
485         this._children = cc.ArrayAppendObjectToIndex(this._children, child, pos);
486         child.setTag(aTag);
487         child._setZOrder(z);
488         child.setParent(this);
489         if (this._running) {
490             child.onEnter();
491             child.onEnterTransitionDidFinish();
492         }
493         return pos;
494     },
495 
496     _updateBlendFunc:function () {
497         if (!this._textureAtlas.getTexture().hasPremultipliedAlpha()) {
498             this._blendFunc.src = gl.SRC_ALPHA;
499             this._blendFunc.dst = gl.ONE_MINUS_SRC_ALPHA;
500         }
501     },
502 
503     /**
504      * return the texture atlas used for drawing the quads
505      * @return {cc.TextureAtlas}
506      */
507     getTextureAtlas:function () {
508         return this._textureAtlas;
509     },
510 
511     /**
512      * set the texture atlas used for drawing the quads
513      * @param {cc.TextureAtlas} textureAtlas
514      */
515     setTextureAtlas:function (textureAtlas) {
516         this._textureAtlas = textureAtlas;
517     }
518 });
519 
520 /**
521  * initializes the particle system with cc.Texture2D, a capacity of particles, which particle system to use
522  * @param {cc.Texture2D|HTMLImageElement|HTMLCanvasElement} texture
523  * @param {Number} capacity
524  * @return {cc.ParticleBatchNode}
525  */
526 cc.ParticleBatchNode.createWithTexture = function (texture, capacity) {
527     var ret = new cc.ParticleBatchNode();
528     if (ret && ret.initWithTexture(texture, capacity))
529         return ret;
530     return null;
531 };
532 
533 /**
534  * 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
535  * @param {String} fileImage
536  * @param capacity
537  * @return {cc.ParticleBatchNode}
538  */
539 cc.ParticleBatchNode.create = function (fileImage, capacity) {
540     var ret = new cc.ParticleBatchNode();
541     if (ret && ret.init(fileImage, capacity))
542         return ret;
543     return null;
544 };
545