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