1 /****************************************************************************
  2  Copyright (c) 2010-2013 cocos2d-x.org
  3  Copyright (c) 2008-2010 Ricardo Quesada
  4  Copyright (c) 2011      Zynga Inc.
  5  Copyright (c) 2012 Pierre-David Bélanger
  6 
  7  http://www.cocos2d-x.org
  8 
  9  Permission is hereby granted, free of charge, to any person obtaining a copy
 10  of this software and associated documentation files (the "Software"), to deal
 11  in the Software without restriction, including without limitation the rights
 12  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 13  copies of the Software, and to permit persons to whom the Software is
 14  furnished to do so, subject to the following conditions:
 15 
 16  The above copyright notice and this permission notice shall be included in
 17  all copies or substantial portions of the Software.
 18 
 19  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 20  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 21  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 22  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 23  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 24  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 25  THE SOFTWARE.
 26  ****************************************************************************/
 27 
 28 /**
 29  * the value of stencil bits.
 30  * @type Number
 31  */
 32 cc.stencilBits = -1;
 33 
 34 cc.setProgram = function (node, program) {
 35     node.shaderProgram = program;
 36 
 37     var children = node.children;
 38     if (!children)
 39         return;
 40 
 41     for (var i = 0; i < children.length; i++)
 42         cc.setProgram(children[i], program);
 43 };
 44 
 45 /**
 46  * <p>
 47  *     cc.ClippingNode is a subclass of cc.Node.                                                            <br/>
 48  *     It draws its content (childs) clipped using a stencil.                                               <br/>
 49  *     The stencil is an other cc.Node that will not be drawn.                                               <br/>
 50  *     The clipping is done using the alpha part of the stencil (adjusted with an alphaThreshold).
 51  * </p>
 52  * @class
 53  * @extends cc.Node
 54  *
 55  * @property {Number}   alphaThreshold  - Threshold for alpha value.
 56  * @property {Boolean}  inverted        - Indicate whether in inverted mode.
 57  * @property {cc.Node}  stencil         - he cc.Node to use as a stencil to do the clipping.
 58  */
 59 cc.ClippingNode = cc.Node.extend(/** @lends cc.ClippingNode# */{
 60 	alphaThreshold: 0,
 61 	inverted: false,
 62 
 63     _stencil: null,
 64     _godhelpme: false,
 65 
 66     ctor: function () {
 67         cc.Node.prototype.ctor.call(this);
 68         this._stencil = null;
 69         this.alphaThreshold = 0;
 70         this.inverted = false;
 71     },
 72 
 73     /**
 74      * Initializes a clipping node with an other node as its stencil.                          <br/>
 75      * The stencil node will be retained, and its parent will be set to this clipping node.
 76      * @param {cc.Node} [stencil=null]
 77      */
 78     init: null,
 79     _className:"ClippingNode",
 80 
 81     _initForWebGL: function (stencil) {
 82         this._stencil = stencil;
 83 
 84         this.alphaThreshold = 1;
 85         this.inverted = false;
 86         // get (only once) the number of bits of the stencil buffer
 87         cc.ClippingNode._init_once = true;
 88         if (cc.ClippingNode._init_once) {
 89             cc.stencilBits = cc._renderContext.getParameter(cc._renderContext.STENCIL_BITS);
 90             if (cc.stencilBits <= 0)
 91                 cc.log("Stencil buffer is not enabled.");
 92             cc.ClippingNode._init_once = false;
 93         }
 94         return true;
 95     },
 96 
 97     _initForCanvas: function (stencil) {
 98         this._stencil = stencil;
 99         this.alphaThreshold = 1;
100         this.inverted = false;
101     },
102 
103     onEnter: function () {
104         cc.Node.prototype.onEnter.call(this);
105         this._stencil.onEnter();
106     },
107 
108     onEnterTransitionDidFinish: function () {
109         cc.Node.prototype.onEnterTransitionDidFinish.call(this);
110         this._stencil.onEnterTransitionDidFinish();
111     },
112 
113     onExitTransitionDidStart: function () {
114         this._stencil.onExitTransitionDidStart();
115         cc.Node.prototype.onExitTransitionDidStart.call(this);
116     },
117 
118     onExit: function () {
119         this._stencil.onExit();
120         cc.Node.prototype.onExit.call(this);
121     },
122 
123     visit: null,
124 
125     _visitForWebGL: function (ctx) {
126         var gl = ctx || cc._renderContext;
127 
128         // if stencil buffer disabled
129         if (cc.stencilBits < 1) {
130             // draw everything, as if there where no stencil
131             cc.Node.prototype.visit.call(this, ctx);
132             return;
133         }
134 
135         // return fast (draw nothing, or draw everything if in inverted mode) if:
136         // - nil stencil node
137         // - or stencil node invisible:
138         if (!this._stencil || !this._stencil.visible) {
139             if (this.inverted)
140                 cc.Node.prototype.visit.call(this, ctx);   // draw everything
141             return;
142         }
143 
144         // store the current stencil layer (position in the stencil buffer),
145         // this will allow nesting up to n CCClippingNode,
146         // where n is the number of bits of the stencil buffer.
147         cc.ClippingNode._layer = -1;
148 
149         // all the _stencilBits are in use?
150         if (cc.ClippingNode._layer + 1 == cc.stencilBits) {
151             // warn once
152             cc.ClippingNode._visit_once = true;
153             if (cc.ClippingNode._visit_once) {
154                 cc.log("Nesting more than " + cc.stencilBits + "stencils is not supported. Everything will be drawn without stencil for this node and its childs.");
155                 cc.ClippingNode._visit_once = false;
156             }
157             // draw everything, as if there where no stencil
158             cc.Node.prototype.visit.call(this, ctx);
159             return;
160         }
161 
162         ///////////////////////////////////
163         // INIT
164 
165         // increment the current layer
166         cc.ClippingNode._layer++;
167 
168         // mask of the current layer (ie: for layer 3: 00000100)
169         var mask_layer = 0x1 << cc.ClippingNode._layer;
170         // mask of all layers less than the current (ie: for layer 3: 00000011)
171         var mask_layer_l = mask_layer - 1;
172         // mask of all layers less than or equal to the current (ie: for layer 3: 00000111)
173         var mask_layer_le = mask_layer | mask_layer_l;
174 
175         // manually save the stencil state
176         var currentStencilEnabled = gl.isEnabled(gl.STENCIL_TEST);
177         var currentStencilWriteMask = gl.getParameter(gl.STENCIL_WRITEMASK);
178         var currentStencilFunc = gl.getParameter(gl.STENCIL_FUNC);
179         var currentStencilRef = gl.getParameter(gl.STENCIL_REF);
180         var currentStencilValueMask = gl.getParameter(gl.STENCIL_VALUE_MASK);
181         var currentStencilFail = gl.getParameter(gl.STENCIL_FAIL);
182         var currentStencilPassDepthFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL);
183         var currentStencilPassDepthPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS);
184 
185         // enable stencil use
186         gl.enable(gl.STENCIL_TEST);
187         // check for OpenGL error while enabling stencil test
188         //cc.CHECK_GL_ERROR_DEBUG();
189 
190         // all bits on the stencil buffer are readonly, except the current layer bit,
191         // this means that operation like glClear or glStencilOp will be masked with this value
192         gl.stencilMask(mask_layer);
193 
194         // manually save the depth test state
195         //GLboolean currentDepthTestEnabled = GL_TRUE;
196         //currentDepthTestEnabled = glIsEnabled(GL_DEPTH_TEST);
197         var currentDepthWriteMask = gl.getParameter(gl.DEPTH_WRITEMASK);
198 
199         // disable depth test while drawing the stencil
200         //glDisable(GL_DEPTH_TEST);
201         // disable update to the depth buffer while drawing the stencil,
202         // as the stencil is not meant to be rendered in the real scene,
203         // it should never prevent something else to be drawn,
204         // only disabling depth buffer update should do
205         gl.depthMask(false);
206 
207         ///////////////////////////////////
208         // CLEAR STENCIL BUFFER
209 
210         // manually clear the stencil buffer by drawing a fullscreen rectangle on it
211         // setup the stencil test func like this:
212         // for each pixel in the fullscreen rectangle
213         //     never draw it into the frame buffer
214         //     if not in inverted mode: set the current layer value to 0 in the stencil buffer
215         //     if in inverted mode: set the current layer value to 1 in the stencil buffer
216         gl.stencilFunc(gl.NEVER, mask_layer, mask_layer);
217         gl.stencilOp(!this.inverted ? gl.ZERO : gl.REPLACE, gl.KEEP, gl.KEEP);
218 
219         // draw a fullscreen solid rectangle to clear the stencil buffer
220         //ccDrawSolidRect(CCPointZero, ccpFromSize([[CCDirector sharedDirector] winSize]), ccc4f(1, 1, 1, 1));
221         cc._drawingUtil.drawSolidRect(cc.p(0,0), cc.pFromSize(cc.director.getWinSize()), cc.color(255, 255, 255, 255));
222 
223         ///////////////////////////////////
224         // DRAW CLIPPING STENCIL
225 
226         // setup the stencil test func like this:
227         // for each pixel in the stencil node
228         //     never draw it into the frame buffer
229         //     if not in inverted mode: set the current layer value to 1 in the stencil buffer
230         //     if in inverted mode: set the current layer value to 0 in the stencil buffer
231         gl.stencilFunc(gl.NEVER, mask_layer, mask_layer);
232         gl.stencilOp(!this.inverted ? gl.REPLACE : gl.ZERO, gl.KEEP, gl.KEEP);
233 
234         if (this.alphaThreshold < 1) {
235             // since glAlphaTest do not exists in OES, use a shader that writes
236             // pixel only if greater than an alpha threshold
237             var program = cc.shaderCache.programForKey(cc.SHADER_POSITION_TEXTURECOLORALPHATEST);
238             var alphaValueLocation = gl.getUniformLocation(program.getProgram(), cc.UNIFORM_ALPHA_TEST_VALUE_S);
239             // set our alphaThreshold
240             cc.glUseProgram(program.getProgram());
241             program.setUniformLocationWith1f(alphaValueLocation, this.alphaThreshold);
242             // we need to recursively apply this shader to all the nodes in the stencil node
243             // XXX: we should have a way to apply shader to all nodes without having to do this
244             cc.setProgram(this._stencil, program);
245         }
246 
247         // draw the stencil node as if it was one of our child
248         // (according to the stencil test func/op and alpha (or alpha shader) test)
249         cc.kmGLPushMatrix();
250         this.transform();
251         this._stencil.visit();
252         cc.kmGLPopMatrix();
253 
254         // restore alpha test state
255         //if (this.alphaThreshold < 1) {
256         // XXX: we need to find a way to restore the shaders of the stencil node and its childs
257         //}
258 
259         // restore the depth test state
260         gl.depthMask(currentDepthWriteMask);
261         //if (currentDepthTestEnabled) {
262         //    glEnable(GL_DEPTH_TEST);
263         //}
264 
265         ///////////////////////////////////
266         // DRAW CONTENT
267 
268         // setup the stencil test func like this:
269         // for each pixel of this node and its childs
270         //     if all layers less than or equals to the current are set to 1 in the stencil buffer
271         //         draw the pixel and keep the current layer in the stencil buffer
272         //     else
273         //         do not draw the pixel but keep the current layer in the stencil buffer
274         gl.stencilFunc(gl.EQUAL, mask_layer_le, mask_layer_le);
275         gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
276 
277         // draw (according to the stencil test func) this node and its childs
278         cc.Node.prototype.visit.call(this, ctx);
279 
280         ///////////////////////////////////
281         // CLEANUP
282 
283         // manually restore the stencil state
284         gl.stencilFunc(currentStencilFunc, currentStencilRef, currentStencilValueMask);
285         gl.stencilOp(currentStencilFail, currentStencilPassDepthFail, currentStencilPassDepthPass);
286         gl.stencilMask(currentStencilWriteMask);
287         if (!currentStencilEnabled)
288             gl.disable(gl.STENCIL_TEST);
289 
290         // we are done using this layer, decrement
291         cc.ClippingNode._layer--;
292     },
293 
294     _visitForCanvas: function (ctx) {
295         // return fast (draw nothing, or draw everything if in inverted mode) if:
296         // - nil stencil node
297         // - or stencil node invisible:
298         if (!this._stencil || !this._stencil.visible) {
299             if (this.inverted)
300                 cc.Node.prototype.visit.call(this, ctx);   // draw everything
301             return;
302         }
303 
304         var context = ctx || cc._renderContext;
305         // Composition mode, costy but support texture stencil
306         if (this._cangodhelpme() || this._stencil instanceof cc.Sprite) {
307             // Cache the current canvas, for later use (This is a little bit heavy, replace this solution with other walkthrough)
308             var canvas = context.canvas;
309             var locCache = cc.ClippingNode._getSharedCache();
310             locCache.width = canvas.width;
311             locCache.height = canvas.height;
312             var locCacheCtx = locCache.getContext("2d");
313             locCacheCtx.drawImage(canvas, 0, 0);
314 
315             context.save();
316             // Draw everything first using node visit function
317             this._super(context);
318 
319             context.globalCompositeOperation = this.inverted ? "destination-out" : "destination-in";
320 
321             this.transform(context);
322             this._stencil.visit();
323 
324             context.restore();
325 
326             // Redraw the cached canvas, so that the cliped area shows the background etc.
327             context.save();
328             context.setTransform(1, 0, 0, 1, 0, 0);
329             context.globalCompositeOperation = "destination-over";
330             context.drawImage(locCache, 0, 0);
331             context.restore();
332         }
333         // Clip mode, fast, but only support cc.DrawNode
334         else {
335             var i, children = this._children, locChild;
336 
337             context.save();
338             this.transform(context);
339             this._stencil.visit(context);
340             context.clip();
341 
342             // Clip mode doesn't support recusive stencil, so once we used a clip stencil,
343             // so if it has ClippingNode as a child, the child must uses composition stencil.
344             this._cangodhelpme(true);
345             var len = children.length;
346             if (len > 0) {
347                 this.sortAllChildren();
348                 // draw children zOrder < 0
349                 for (i = 0; i < len; i++) {
350                     locChild = children[i];
351                     if (locChild._localZOrder < 0)
352                         locChild.visit(context);
353                     else
354                         break;
355                 }
356                 this.draw(context);
357                 for (; i < len; i++) {
358                     children[i].visit(context);
359                 }
360             } else
361                 this.draw(context);
362             this._cangodhelpme(false);
363 
364             context.restore();
365         }
366     },
367 
368     /**
369      * The cc.Node to use as a stencil to do the clipping.                                   <br/>
370      * The stencil node will be retained. This default to nil.
371      * @return {cc.Node}
372      */
373     getStencil: function () {
374         return this._stencil;
375     },
376 
377     /**
378      * @function
379      * @param {cc.Node} stencil
380      */
381     setStencil: null,
382 
383     _setStencilForWebGL: function (stencil) {
384         this._stencil = stencil;
385     },
386 
387     _setStencilForCanvas: function (stencil) {
388         this._stencil = stencil;
389         var locEGL_ScaleX = cc.view.getScaleX(), locEGL_ScaleY = cc.view.getScaleY();
390         var locContext = cc._renderContext;
391         // For texture stencil, use the sprite itself
392         if (stencil instanceof cc.Sprite) {
393             return;
394         }
395         // For shape stencil, rewrite the draw of stencil ,only init the clip path and draw nothing.
396         else if (stencil instanceof cc.DrawNode) {
397             stencil.draw = function () {
398                 for (var i = 0; i < stencil._buffer.length; i++) {
399                     var element = stencil._buffer[i];
400                     var vertices = element.verts;
401                     var firstPoint = vertices[0];
402                     locContext.beginPath();
403                     locContext.moveTo(firstPoint.x * locEGL_ScaleX, -firstPoint.y * locEGL_ScaleY);
404                     for (var j = 1, len = vertices.length; j < len; j++)
405                         locContext.lineTo(vertices[j].x * locEGL_ScaleX, -vertices[j].y * locEGL_ScaleY);
406                 }
407             }
408         }
409     },
410 
411     /**
412      * <p>
413      * The alpha threshold.                                                                                   <br/>
414      * The content is drawn only where the stencil have pixel with alpha greater than the alphaThreshold.     <br/>
415      * Should be a float between 0 and 1.                                                                     <br/>
416      * This default to 1 (so alpha test is disabled).
417      * </P>
418      * @return {Number}
419      */
420     getAlphaThreshold: function () {
421         return this.alphaThreshold;
422     },
423 
424     /**
425      * set alpha threshold.
426      * @param {Number} alphaThreshold
427      */
428     setAlphaThreshold: function (alphaThreshold) {
429         this.alphaThreshold = alphaThreshold;
430     },
431 
432     /**
433      * <p>
434      *     Inverted. If this is set to YES,                                                                 <br/>
435      *     the stencil is inverted, so the content is drawn where the stencil is NOT drawn.                 <br/>
436      *     This default to NO.
437      * </p>
438      * @return {Boolean}
439      */
440     isInverted: function () {
441         return this.inverted;
442     },
443 
444 
445     /**
446      * set whether or not invert of stencil
447      * @param {Boolean} inverted
448      */
449     setInverted: function (inverted) {
450         this.inverted = inverted;
451     },
452 
453     _cangodhelpme: function (godhelpme) {
454         if (godhelpme === true || godhelpme === false)
455             cc.ClippingNode.prototype._godhelpme = godhelpme;
456         return cc.ClippingNode.prototype._godhelpme;
457     }
458 });
459 
460 window._p = cc.ClippingNode.prototype;
461 
462 if (cc._renderType === cc._RENDER_TYPE_WEBGL) {
463     //WebGL
464     _p.init = _p._initForWebGL;
465     _p.visit = _p._visitForWebGL;
466     _p.setStencil = _p._setStencilForWebGL;
467 } else {
468     _p.init = _p._initForCanvas;
469     _p.visit = _p._visitForCanvas;
470     _p.setStencil = _p._setStencilForCanvas;
471 }
472 
473 // Extended properties
474 cc.defineGetterSetter(_p, "stencil", _p.getStencil, _p.setStencil);
475 /** @expose */
476 _p.stencil;
477 
478 delete window._p;
479 
480 cc.ClippingNode._init_once = null;
481 cc.ClippingNode._visit_once = null;
482 cc.ClippingNode._layer = null;
483 cc.ClippingNode._sharedCache = null;
484 
485 cc.ClippingNode._getSharedCache = function () {
486     return (cc.ClippingNode._sharedCache) || (cc.ClippingNode._sharedCache = document.createElement("canvas"));
487 }
488 
489 /**
490  * Creates and initializes a clipping node with an other node as its stencil.                               <br/>
491  * The stencil node will be retained.
492  * @param {cc.Node} [stencil=null]
493  * @return {cc.ClippingNode}
494  */
495 cc.ClippingNode.create = function (stencil) {
496     var node = new cc.ClippingNode();
497     node.init(stencil);
498     return node;
499 };
500